#!/usr/bin/env python3 """Create and optionally render fresh Local Voice acceptance plans.""" from __future__ import annotations import argparse import json import subprocess import sys from pathlib import Path ROOT = Path(__file__).resolve().parent.parent RENDERER = ROOT / "scripts" / "local_voice.py" CASES = ( "donna", "chris-engineer", "grandpa-bomber", "val-holiday", "ryan-pitch-meeting", ) def run(command: list[str]) -> None: subprocess.run(command, check=True) def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("--runtime-root", required=True, type=Path) parser.add_argument("--output-dir", required=True, type=Path) parser.add_argument("--device", choices=("metal", "cpu", "cuda"), default="metal") parser.add_argument("--render", action="store_true") parser.add_argument( "--voices", help="Comma-separated subset of profile IDs; defaults to every case.", ) args = parser.parse_args() args.output_dir.mkdir(parents=True, exist_ok=True) selected_cases = ( tuple(item.strip() for item in args.voices.split(",") if item.strip()) if args.voices else CASES ) unknown = sorted(set(selected_cases) - set(CASES)) if unknown: parser.error("Unknown acceptance voice(s): " + ", ".join(unknown)) results = [] for voice in selected_cases: script = ROOT / "tests" / "acceptance" / f"{voice}.md" plan = args.output_dir / f"{voice}.json" audio = args.output_dir / f"{voice}.mp3" job_id = f"acceptance-v9-{voice}" run( [ sys.executable, str(RENDERER), "plan", "--voice", voice, "--script", str(script), "--output-plan", str(plan), "--output-audio", str(audio), "--job-id", job_id, ] ) if voice == "ryan-pitch-meeting": data = json.loads(plan.read_text(encoding="utf-8")) data["segments"] = [ { "id": "01-producer", "role": "producer", "text": "So, you have a Monday briefing for me?", "context_after": "Yes sir, I do!", "pause_after_ms": 95, }, { "id": "02-yes-sir", "role": "writer", "text": "Yes sir, I do!", "fixed_asset": ( "voices/ryan-pitch-meeting/signature-exchanges/" "yes-sir-i-do/lotr_two_towers.wav" ), "pause_after_ms": 55, }, { "id": "03-writer", "role": "writer", "text": ( "The Phoenix hardware shipped, but the final quantity " "approval is still missing!" ), "context_before": "Yes sir, I do!", "context_after": ( "Oh, paperwork blocking shipped hardware is tight!" ), "pause_after_ms": 95, }, { "id": "04-tight", "role": "producer", "text": "Oh, paperwork blocking shipped hardware is tight!", "tts_text": "Oh, paperwork blocking shipped hardware", "context_before": ( "The final quantity approval is still missing!" ), "context_after": "It sure is!", "suffix_asset": ( "voices/ryan-pitch-meeting/signature-exchanges/" "tight-endings/is/disclosure.wav" ), "suffix_gap_ms": 130, "pause_after_ms": 95, }, { "id": "05-producer", "role": "producer", "text": ( "Is it going to be hard to clear that before noon?" ), "context_before": ( "Oh, paperwork blocking shipped hardware is tight!" ), "context_after": ( "Actually, it's gonna be super easy, barely an " "inconvenience! Oh, really?" ), "pause_after_ms": 95, }, { "id": "06-super-easy", "role": "writer", "text": ( "Actually, it's gonna be super easy, barely an " "inconvenience! Oh, really?" ), "fixed_asset": ( "voices/ryan-pitch-meeting/signature-exchanges/" "super_easy_oh_really.wav" ), "pause_after_ms": 95, }, ] plan.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8") run([sys.executable, str(RENDERER), "validate", str(plan)]) if args.render: run( [ sys.executable, str(RENDERER), "render", str(plan), "--runtime-root", str(args.runtime_root), "--device", args.device, "--resume", ] ) results.append({"voice": voice, "plan": str(plan), "audio": str(audio)}) summary = args.output_dir / "acceptance-summary.json" existing = [] if summary.exists(): existing = json.loads(summary.read_text(encoding="utf-8")) merged = {item["voice"]: item for item in existing} merged.update({item["voice"]: item for item in results}) summary.write_text( json.dumps([merged[key] for key in sorted(merged)], indent=2) + "\n", encoding="utf-8", ) print(f"Acceptance summary: {summary}") return 0 if __name__ == "__main__": raise SystemExit(main())