/**
* Пайплайн AI-аудио (работа A1) — манифест-driven, аналог `npm run aiart`.
*
* Команды:
* npm run aiaudio list — таблица позиций манифеста (gate/сиды/promote)
* npm run aiaudio gen [id[:seed,seed]...] — генерация AudioLDM2 + пост-обработка
* (без аргументов — все сиды манифеста; sfx — без лупа, остальное — луп + --check)
* npm run aiaudio post <file.wav> [...] — только пост-обработка (pass-through post.py)
* npm run aiaudio promote <id> <seed> — апрув: review/<id>_<seed>_loop.wav →
* assets/audio/<promote>.wav + gate=approved в манифесте (sfx не promote-ится —
* SFX_KEYS пуст, спек-синтез; нужен файловый SFX-путь в движке)
*
* Модель/окружение — см. шапку manifest.json; python — venv ~/.cache/rpg-ai/venv-aiaudio.
*/
import { spawnSync } from 'node:child_process';
import { copyFileSync, existsSync, readFileSync, writeFileSync } from 'node:fs';
import { createRequire } from 'node:module';
import path from 'node:path';
const require = createRequire(import.meta.url);
const HERE = path.dirname(require.resolve('./manifest.json'));
const ASSETS = path.resolve(HERE, '../../assets'); // apps/game/assets
const PY = path.join(process.env.HOME ?? '', '.cache/rpg-ai/venv-aiaudio/bin/python');
const USAGE = 'npm run aiaudio <list|gen [id[:seed,seed]...]|post <file>...|promote <id> <seed>>';
function fail(msg) {
console.error(msg);
process.exit(1);
}
function readManifest() {
return JSON.parse(readFileSync(path.join(HERE, 'manifest.json'), 'utf8'));
}
/** Цели генерации: без аргументов — все сиды манифеста, иначе id[:seed,seed]. */
function parseTargets(args, manifest) {
if (args.length === 0) {
return manifest.items.flatMap((it) => (it.seeds ?? [1]).map((seed) => ({ it, seed })));
}
const out = [];
for (const arg of args) {
const [id, seeds] = arg.split(':');
const it = manifest.items.find((x) => x.id === id);
if (!it) fail(`неизвестный id ${id}; доступно: ${manifest.items.map((x) => x.id).join(', ')}`);
for (const seed of (seeds ? seeds.split(',') : it.seeds ?? [1]).map(Number)) {
out.push({ it, seed });
}
}
return out;
}
function run(cmd, pyArgs) {
if (!existsSync(PY)) fail(`нет venv-питона: ${PY} (см. шапку manifest.json)`);
const r = spawnSync(PY, [cmd, ...pyArgs], { cwd: HERE, stdio: 'inherit' });
if (r.status !== 0) fail(`${cmd} завершился с ошибкой`);
}
function cmdGen(args) {
const manifest = readManifest();
const targets = parseTargets(args, manifest);
console.log(`целей: ${targets.length} (${targets.map((t) => `${t.it.id}_${t.seed}`).join(', ')})`);
run('gen_audioldm2.py', targets.map((t) => `${t.it.id}:${t.seed}`));
// Пост-обработка сразу: sfx — без лупа (_fx), остальное — луп + проверка шва.
for (const t of targets) {
const flags = t.it.kind === 'sfx' ? ['--noloop'] : ['--check'];
if (t.it.peak) flags.push('--peak', String(t.it.peak));
run('post.py', [`review/${t.it.id}_${t.seed}.wav`, ...flags]);
}
}
function cmdPromote(args) {
const [id, seedStr] = args;
if (!id || !seedStr) fail(USAGE);
const manifest = readManifest();
const it = manifest.items.find((x) => x.id === id);
if (!it) fail(`неизвестный id ${id}`);
if (!it.promote) fail(`${id}: позиция без promote (SFX_KEYS пуст — файловых sfx пока нет)`);
const src = path.join(HERE, 'review', `${id}_${seedStr}_loop.wav`);
if (!existsSync(src)) fail(`нет файла ${src} (сначала npm run aiaudio gen ${id}:${seedStr})`);
// Промотируем только ключ, известный реестрам игры (audio.ts).
const audioTs = readFileSync(path.resolve(HERE, '../../src/data/audio.ts'), 'utf8');
if (!audioTs.includes(`'${it.promote}'`)) fail(`ключ '${it.promote}' не объявлен в data/audio.ts — добавь сначала`);
const dst = path.join(ASSETS, 'audio', `${it.promote}.wav`);
copyFileSync(src, dst);
it.gate = 'approved';
it.approved_seed = Number(seedStr);
it.date = new Date().toISOString().slice(0, 10);
writeFileSync(path.join(HERE, 'manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`);
console.log(`промотировано: ${it.promote}.wav (seed ${seedStr}, gate=approved)`);
}
function cmdList() {
const manifest = readManifest();
for (const it of manifest.items) {
const gate = it.gate === 'approved' ? `approved(seed ${it.approved_seed})` : it.gate ?? '—';
console.log(`${it.id.padEnd(20)} ${it.kind.padEnd(8)} сиды ${(it.seeds ?? [1]).join(',').padEnd(8)} ${gate.padEnd(20)} ${it.promote ?? ''}`);
}
}
const [cmd, ...args] = process.argv.slice(2);
if (cmd === 'list') cmdList();
else if (cmd === 'gen') cmdGen(args);
else if (cmd === 'post') run('post.py', args);
else if (cmd === 'promote') cmdPromote(args);
else fail(`использование: ${USAGE}`);