"""Standalone runner for the approved flagged-audio fixes (2026-07-03).

Regenerates the 7 approved clips (5 pronunciation/audio fixes + 2 text-edit
fixes) via ElevenLabs, writing straight over the files in data/audio_preview/.

Deliberately does NOT import scripts.compiler.* — that package's __init__
pulls torch/librosa/numpy. We only need tts.py (stdlib + lazy elevenlabs),
loaded directly by file path, plus a trivial inline scenario loader.

Requirements:  Python >=3.10,  pip install elevenlabs,  ffmpeg on PATH.
Run:
    cd animasync-face-v3
    ELEVENLABS_API_KEY=sk_... python3 scripts/_fix_flagged_run.py
Add --dry-run to print the plan without calling the API.
"""
import argparse
import asyncio
import importlib.util
import json
import sys
from pathlib import Path

HERE = Path(__file__).resolve().parent
ROOT = HERE.parent  # animasync-face-v3/
SPEC = HERE / "_fix_flagged_turns.txt"
SOURCES = [
    ROOT / "data/emotion/seed_train_final.jsonl",
    ROOT / "data/emotion/seed_val.jsonl",
    ROOT / "data/emotion/seed_test.jsonl",
]


def _load_tts():
    """Import tts.py by path so we skip the compiler package __init__."""
    path = HERE / "compiler" / "tts.py"
    spec = importlib.util.spec_from_file_location("_flagged_tts", path)
    mod = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(mod)
    return mod


def load_scenarios():
    scen = {}
    for p in SOURCES:
        if not p.exists():
            continue
        for line in p.open(encoding="utf-8"):
            d = json.loads(line)
            scen[d["scenario_id"]] = d
    return scen


def parse_spec():
    out = []
    for line in SPEC.read_text(encoding="utf-8").splitlines():
        line = line.rstrip()
        if not line.strip() or line.lstrip().startswith("#"):
            continue
        # format: sid:ti \t dest [\t text_override]
        parts = line.split("\t")
        spec = parts[0].strip()
        dest = parts[1].strip() if len(parts) > 1 and parts[1].strip() else None
        override = parts[2] if len(parts) > 2 and parts[2].strip() else None
        sid, ti = spec.split(":")
        out.append((sid, int(ti), dest, override))
    return out


async def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--dry-run", action="store_true")
    ap.add_argument("--concurrency", type=int, default=4)
    ap.add_argument("--variant", type=int, default=0,
                    help="Voice variant. 0 = default pipeline voice. 1,2,3 pick a "
                         "DIFFERENT voice from the same emotion pool (to fix "
                         "mispronunciation) and write to <name>__vN.mp3 so takes "
                         "don't clobber each other.")
    ap.add_argument("--female", action="store_true",
                    help="Force every clip to its emotion's FEMALE voice "
                         "(FEMALE_BY_BASE). Overrides the filename-hash pick, "
                         "which can land on male/unlabeled voices.")
    args = ap.parse_args()

    tts = _load_tts()
    scen = load_scenarios()
    wanted = parse_spec()

    import hashlib

    def _variant_voice(emotion, stem, variant):
        """Deterministically shift to a DIFFERENT voice in the same emotion pool."""
        base = tts.EMOTION_TO_BASE.get(emotion or "neutral", "neutral")
        pool = tts.VOICE_POOLS_BY_BASE[base]
        base_idx = int(hashlib.md5(stem.encode("utf-8")).hexdigest(), 16) % len(pool)
        return pool[(base_idx + variant) % len(pool)]

    texts, emotions, vads, out_paths, voice_seeds, voice_ids = [], [], [], [], [], []
    for sid, ti, dest, override in wanted:
        s = scen.get(sid)
        if not s:
            print(f"[miss] scenario not found: {sid}"); continue
        if ti >= len(s["turns"]):
            print(f"[miss] {sid}: turn {ti} out of range"); continue
        turn = s["turns"][ti]
        # per-clip TTS pronunciation override — respell for the model only;
        # the real script in the dataset is left untouched.
        turn_text = override if override else turn["text"]
        if not turn_text.strip():
            print(f"[skip] {sid}:{ti} empty text"); continue
        # all 7 targets are daily_* dialogue (not long_/solo_ monologue),
        # so voice is auto-picked by emotion+seed — no dominant-voice lock.
        out_path = Path(dest)
        vid = None
        if args.female:
            # force the emotion's FEMALE voice, keep the plain filename
            base = tts.EMOTION_TO_BASE.get(turn["emotion"] or "neutral", "neutral")
            vid = tts.FEMALE_BY_BASE[base]
        elif args.variant:
            # guaranteed-different voice from the same pool + non-clobbering name
            vid = _variant_voice(turn["emotion"], Path(dest).stem, args.variant)
            out_path = out_path.with_name(f"{out_path.stem}__v{args.variant}.mp3")
        out_path.parent.mkdir(parents=True, exist_ok=True)
        if out_path.exists() and not args.dry_run:
            out_path.unlink()  # force overwrite (synth_all skips existing >1KB)
        texts.append(turn_text)
        emotions.append(turn["emotion"])
        vads.append(turn.get("vad"))
        out_paths.append(out_path)
        voice_seeds.append(None)
        voice_ids.append(vid)

    tag = f" (variant {args.variant})" if args.variant else ""
    print(f"[plan] {len(texts)} turns{tag} -> {Path(wanted[0][2]).parent if wanted else ''}")
    for t, e, o, v in zip(texts, emotions, out_paths, voice_ids):
        vtag = f"  voice={v[:8]}" if v else ""
        print(f"   {e:9s} {o.name}{vtag}\n        {t!r}")
    if args.dry_run:
        print("[dry-run] no API calls made"); return
    if not texts:
        return

    ok = await tts.synth_all(
        texts, out_paths,
        backend="elevenlabs",
        concurrency=args.concurrency,
        emotions=emotions,
        vads=vads,
        voice_seeds=voice_seeds,
        voice_ids=voice_ids,
    )
    n = sum(1 for x in ok if x)
    print(f"\n[done] {n}/{len(texts)} regenerated")
    if n != len(texts):
        print("[warn] some clips failed — see warnings above"); sys.exit(1)


if __name__ == "__main__":
    asyncio.run(main())
