Newer
Older
rpg / apps / game / tools / aiaudio / gen_audioldm2.py
"""PoC (работа A, этап A0): text-to-audio через AudioLDM2 на CPU.

Главный кандидат плана — Stable Audio Open Small — gated (нужен HF-токен),
поэтому PoC делаем на фолбэке AudioLDM2 (cvssp/audioldm2, CC-BY-NC-SA —
в ОСНОВНОЙ пайплайн с некоммерческими весами не пойдёт, см. docs/plan.md).

Запуск:
  ~/.cache/rpg-ai/venv-pxgpt/bin/python tools/aiaudio/gen_audioldm2.py <id> [seed]

Выход: tools/aiaudio/review/<id>_<seed>.wav (16000 Гц — ресэмпл делает post).
Детерминизм: seed + num_inferences фиксированы, промпт в манифесте.
"""
import sys
from pathlib import Path

import numpy as np
import torch

HERE = Path(__file__).resolve().parent
REVIEW = HERE / "review"

# Промпты PoC: амбиент прудов (туман, вода) против текущего процедурного
# gen.mjs (band-noise ветер + капли, 8 с) — критерий «лучше на слух».
PROMPTS = {
    "amb_ponds": {
        "positive": (
            "field recording, calm misty pond at dusk, gentle water lapping, "
            "soft wind through reeds, occasional distant croaking, eerie "
            "quiet melancholic atmosphere, seamless ambience"
        ),
        "negative": (
            "music, melody, rhythm, drums, speech, voices, sudden loud "
            "noises, harsh noise, distortion"
        ),
    },
    "amb_zvenets": {
        "positive": (
            "field recording, quiet rural settlement at dusk, low distant "
            "metallic bell toll, soft wind, faint rustling, somber "
            "melancholic atmosphere, seamless ambience"
        ),
        "negative": (
            "music, melody, song, rhythm, drums, speech, voices, sirens, "
            "harsh noise, distortion"
        ),
    },
}

DURATION = 10.0  # с — заметно длиннее процедурного лупа (8 с), до crossfade
NUM_INFERENCE_STEPS = 100  # 200 — дефолт качества; на CPU берём 100 для PoC
GUIDANCE = 3.0


def main() -> None:
    if len(sys.argv) < 2 or sys.argv[1] not in PROMPTS:
        print(f"использование: gen_audioldm2.py <{'|'.join(PROMPTS)}> [seed]")
        sys.exit(1)
    name = sys.argv[1]
    seed = int(sys.argv[2]) if len(sys.argv) > 2 else 1
    spec = PROMPTS[name]

    import diffusers  # noqa: F401  (после разбора аргументов — чтобы --help был быстрым)

    torch.manual_seed(seed)
    np.random.seed(seed)

    from diffusers import AudioLDM2Pipeline

    # GPU (RTX 3090) при наличии: fp16, иначе CPU fp32. Seed-детерминизм —
    # по-устройству: fp16-GPU и fp32-CPU дают разные реализации одного seed.
    use_gpu = torch.cuda.is_available()
    dtype = torch.float16 if use_gpu else torch.float32
    pipe = AudioLDM2Pipeline.from_pretrained("cvssp/audioldm2", torch_dtype=dtype)
    pipe.to("cuda" if use_gpu else "cpu")
    print(f"устройство: {'cuda fp16' if use_gpu else 'cpu fp32 (12 потоков)'}")
    if not use_gpu:
        torch.set_num_threads(12)

    REVIEW.mkdir(parents=True, exist_ok=True)
    out = REVIEW / f"{name}_{seed}.wav"
    print(f"генерация {name} (seed={seed}, {DURATION} с, шаги {NUM_INFERENCE_STEPS})...")

    audio = pipe(
        spec["positive"],
        negative_prompt=spec["negative"],
        num_inference_steps=NUM_INFERENCE_STEPS,
        guidance_scale=GUIDANCE,
        audio_length_in_s=DURATION,
    ).audios[0]

    # AudioLDM2 отдаёт float [-1..1] 16000 Гц моно; пишем PCM16 без клипа.
    audio = np.clip(audio, -1.0, 1.0)
    pcm = (audio * 32767.0).astype(np.int16)
    import wave

    with wave.open(str(out), "wb") as w:
        w.setnchannels(1)
        w.setsampwidth(2)
        w.setframerate(16000)
        w.writeframes(pcm.tobytes())
    print(f"готово: {out.relative_to(HERE)} ({pcm.shape[0] / 16000:.1f} с, 16 кГц)")


if __name__ == "__main__":
    main()