feat: add CW-026 Local Voice plugin
This commit is contained in:
Executable
+181
@@ -0,0 +1,181 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user