#!/usr/bin/env python3 """La voce del narratore dei video «Come si gioca» (video/SCALETTA-CARTE.md), PRIMA di girare. python3 video/narra.py video/tutorial-briscola.mjs Legge le battute del copione (cues, «it»), le fa dire a XTTS con `mbot parla` (voce del narratore, diversa da quelle dei personaggi), le controlla con `mbot ascolta` e scrive video/out/-narrazione.json con file e durata di ognuna. Il copione aspetta che la frase sia finita prima di passare alla successiva; monta.py la mette nel video all'inizio della sua battuta. Le frasi già fatte si saltano (per rifarne una: cancellare il file). """ import json import subprocess import sys from pathlib import Path VIDEO = Path(__file__).resolve().parent GIOCO = VIDEO.parent OUT = VIDEO / "out" NARRATORE = "Claribel Dervla" PRONUNCIA = GIOCO / "tools" / "pronuncia.json" def durata(f: Path) -> float: return float(subprocess.run(["ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "csv=p=0", str(f)], capture_output=True, text=True, check=True).stdout) def main() -> None: copione = Path(sys.argv[1]).resolve() clip = copione.stem cues = json.loads(subprocess.run(["node", "--input-type=module", "-e", f"const m = await import({json.dumps(copione.as_uri())}); console.log(JSON.stringify(m.cues))"], capture_output=True, text=True, check=True, cwd=GIOCO).stdout) cartella = OUT / f"{clip}-narrazione" cartella.mkdir(parents=True, exist_ok=True) # XTTS dice «punto» se la frase finisce col punto: si toglie, come fa narrate.py del tour. lavori = [{"text": c["it"].rstrip(". "), "out": str(cartella / f"{c['id']}.wav"), "lang": "it", "speaker": NARRATORE} for c in cues] (cartella / "lavori.json").write_text(json.dumps(lavori, ensure_ascii=False, indent=1)) # Le pronunce del gioco più quelle del narratore, in un file solo per `mbot parla`. regole = {k: v for f in (PRONUNCIA, VIDEO / "pronuncia-narratore.json") if f.exists() for k, v in json.loads(f.read_text()).items() if not k.startswith("_")} (cartella / "pronuncia.json").write_text(json.dumps(regole, ensure_ascii=False, indent=1)) pron = ["--pronounce", str(cartella / "pronuncia.json")] subprocess.run(["mbot", "parla", "--jobs", str(cartella / "lavori.json"), *pron], check=True) ascolto = [{"file": l["out"], "expect": l["text"]} for l in lavori] (cartella / "ascolto.json").write_text(json.dumps(ascolto, ensure_ascii=False, indent=1)) subprocess.run(["mbot", "ascolta", "--jobs", str(cartella / "ascolto.json")]) voci = [{"id": c["id"], "src": str(Path(l["out"]).relative_to(GIOCO)), "sec": round(durata(Path(l["out"])), 2)} for c, l in zip(cues, lavori)] (OUT / f"{clip}-narrazione.json").write_text(json.dumps({"voci": voci}, ensure_ascii=False, indent=1)) print(f"-> {OUT / f'{clip}-narrazione.json'}: {len(voci)} frasi, {sum(v['sec'] for v in voci):.0f} s di voce") if __name__ == "__main__": main()