Newer
Older
rpg / apps / game / tools / aiaudio / analyze.py
"""Численный отбор AI-клипов (работа A1): метрики вместо слушания.

Для review/<id>_<seed>_loop.wav считает: RMS, диапазон огибающей (дыхание
лупа — std скользящего RMS), долю высокочастотной энергии (резкость/жёсткость),
центроид спектра, число клиппингов. Кандидаты против эталона сравниваются
по этим цифрам; финальный отбор — таблицей в консоль.

Запуск: ~/.cache/rpg-ai/venv-aiaudio/bin/python tools/aiaudio/analyze.py <glob...>
"""
import sys
import wave
from pathlib import Path

import numpy as np

HERE = Path(__file__).resolve().parent


def read_wav(path: Path) -> np.ndarray:
    with wave.open(str(path), "rb") as w:
        data = np.frombuffer(w.readframes(w.getnframes()), dtype=np.int16)
    return data.astype(np.float64) / 32768.0


def metrics(s: np.ndarray, rate: int) -> dict:
    win = rate // 10  # окно огибающей 100 мс
    n = len(s) // win
    env = np.sqrt((s[: n * win].reshape(n, win) ** 2).mean(axis=1))
    spec = np.abs(np.fft.rfft(s * np.hanning(len(s))))
    freqs = np.fft.rfftfreq(len(s), 1 / rate)
    total = spec.sum() + 1e-12
    hi = spec[freqs > 4000].sum() / total  # резкость: доля >4 кГц
    centroid = (spec * freqs).sum() / total
    clips = int((np.abs(s) > 0.99).sum())
    return {
        "rms": float(np.sqrt((s**2).mean())),
        "dyn": float(env.std() / (env.mean() + 1e-12)),  # дыхание: неровность огибающей
        "hi": float(hi),
        "cent": float(centroid),
        "clip": clips,
    }


def main() -> None:
    import glob

    files: list[Path] = []
    for pat in sys.argv[1:]:
        # Шаблон относительный: сначала от cwd, при пустом результате — от tools/aiaudio.
        for base in (Path.cwd(), HERE):
            found = sorted(glob.glob(str(base / pat)))
            if found:
                files.extend(Path(p) for p in found)
                break
    if not files:
        print("нет файлов под шаблонами")
        sys.exit(1)
    print(f"{'файл':44} {'rms':>6} {'dyn':>6} {'hi':>6} {'cent':>6} {'clip':>5}")
    for f in files:
        m = metrics(*((lambda r, rate: (r, rate))(read_wav(f), 22050)))
        print(
            f"{f.name:44} {m['rms']:6.3f} {m['dyn']:6.3f} {m['hi']:6.3f} "
            f"{m['cent']:6.0f} {m['clip']:5d}"
        )


if __name__ == "__main__":
    main()