#!/usr/bin/env python3
"""SYNC_CONSENSUS_PLAN.md etapa 4 — desempate por CONTEÚDO (palavra-âncora) pros clips ORANGE
(sync fraco/interpolado). Transcreve o clip + a janela da espinha na posição PREVISTA (via ct/
bracket) e compara PALAVRAS (não espectro — imune a música repetida, a causa do chunk-off).

Score alto = evidência de que a posição prevista está correta (corrobora o placements.py); score
baixo = sinal de que a posição pode estar errada (aparece só como aviso).

ADVISORY, não decide sozinho: só ANOTA evidência em weddings/<nome>/word_anchor_report.json.
Nunca muda offset/orange — verde errado é o pior erro (não criar caminho de auto-confirmação sem
supervisão). Um judge/o usuário decide o que fazer com a evidência.

PORT VideoTask: a versão original chamava `import_media`/`inspect_media` (MCP do app PalmierPro,
GPLv3 — transcript on-device embutido no app). Substituído por transcrição NATIVA do arquivo real
(`TranscribeNative`/Speech.SpeechTranscriber, API pública Apple, zero GPL — mesma substituição já
feita em classify_recorders.py) com fallback faster-whisper. Não depende mais de nenhum app externo
rodando — roda sempre que houver clip orange pra checar.

Uso: sync_wordanchor.py decupagem_<x>.json [--limit N]"""
import os, sys, json, re, argparse, subprocess, tempfile
import classify_recorders as cr

# PORT VideoTask (mesma substituição já feita em classify_recorders.py): a versão original
# chamava `import_media`/`inspect_media` (MCP do app PalmierPro, GPLv3 — transcript on-device
# embutido no app antigo). Substituído por transcrição NATIVA do arquivo real via
# `classify_recorders._transcribe_window_native` (Speech.SpeechTranscriber, API pública Apple,
# zero GPL) — nem precisa mais de "import_media" (esse passo só existia pra registrar o arquivo
# no app antigo e ganhar um `mediaRef`; aqui já temos o PATH real do arquivo direto).
FFMPEG = "/opt/homebrew/bin/ffmpeg"


def inspect_segments(path, s0, s1):
    """Transcreve o TRECHO [s0,s1) de `path` (arquivo real, read-only — só lê, nunca escreve nele)
    via TranscribeNative (fallback faster-whisper já embutido em `cr.transcribe_windows` se o
    binário nativo faltar). Extrai o trecho num WAV temporário (ffmpeg -ss/-t) e transcreve só
    ele — mesma técnica de `classify_recorders.transcribe_windows`. Devolve no formato
    [(texto, s0, s1)] pra `clip_words`/chamador tratarem igual ao antigo `segments`."""
    if s1 <= s0:
        return []
    with tempfile.TemporaryDirectory() as tmp:
        window_wav = os.path.join(tmp, "w.wav")
        try:
            subprocess.run(
                [FFMPEG, "-v", "error", "-ss", str(s0), "-i", path, "-t", str(s1 - s0),
                 "-ar", "16000", "-ac", "1", "-y", window_wav],
                capture_output=True, timeout=60
            )
        except Exception:
            return []
        if not os.path.exists(window_wav):
            return []
        text = cr._transcribe_window_native(window_wav)
        if text is None:
            # fallback faster-whisper (mesmo padrão de classify_recorders, zero contaminação)
            try:
                model = cr._whisper()
                segments, _info = model.transcribe(window_wav, language="pt", vad_filter=True)
                text = " ".join(seg.text for seg in segments)
            except Exception:
                text = ""
        return [(text or "", s0, s1)]


def clip_words(path, dur):
    s = min(30, max(0, dur/2 - 15))
    segs = inspect_segments(path, s, s + min(60, dur))
    return " ".join(x[0] for x in segs)


def word_set(txt):
    return set(re.findall(r"[a-zà-ú]{4,}", txt.lower()))


def overlap_score(a, b):
    wa, wb = word_set(a), word_set(b)
    if not wa or not wb:
        return 0.0
    return len(wa & wb) / min(len(wa), len(wb))


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("decupagem")
    ap.add_argument("--limit", type=int, default=15, help="máx clips orange a checar (custo)")
    a = ap.parse_args()
    decup = json.load(open(a.decupagem)); base = decup["base"]
    import wedslug
    name = wedslug.from_path(a.decupagem) or wedslug.slug(decup.get("casamento", "wedding"))
    ws = os.path.join("weddings", name)
    pf = os.path.join(ws, "placements.json")
    if not os.path.exists(pf):
        print("ABORT: sem placements.json (rode placements.py primeiro)"); sys.exit(1)
    P = json.load(open(pf))
    clips = P["clips"]
    orange = [c for c in clips if c.get("orange")]
    if not orange:
        print("nenhum clip orange — nada a desempatar por conteúdo"); return
    orange = orange[: a.limit]
    print(f"palavra-âncora: {len(orange)} clip(s) orange (de {sum(1 for c in clips if c.get('orange'))} total, limit={a.limit})")

    import sync_ingest as si
    _vidcache = {}
    def src_of(cam, name_):
        stem = re.sub(r'^' + re.escape(cam) + r'_', '', name_)
        ci = decup["cameras"].get(cam)
        if not ci:
            return None
        if cam not in _vidcache:
            _vidcache[cam] = {os.path.splitext(os.path.basename(v))[0]: v for v in si.find_videos_multi(base, ci["pasta"])}
        return _vidcache[cam].get(stem)

    # espinha: mesmo arquivo usado pelo sync (audio_cerimonia), transcrita 1x com timestamps
    sp = decup.get("audio_cerimonia", {}).get("arquivos", [])
    sp_paths = [os.path.join(base, p) for p in sp if os.path.exists(os.path.join(base, p))]
    if not sp_paths:
        print("ABORT: sem espinha (audio_cerimonia.arquivos vazio)"); sys.exit(1)

    report = {}
    for c in orange:
        src = src_of(c["cam"], c["name"])
        if not src or not os.path.exists(src):
            print(f"  ! fonte sumiu: {c['name']}"); continue
        from classify_audio_content import ffdur
        dur = ffdur(src)
        # PORT: sem app antigo, transcreve o arquivo REAL direto (src já é o path — nunca
        # precisou de "import_media" pra isso, esse passo só existia no app antigo).
        ctxt = clip_words(src, dur)
        if not ctxt.strip():
            print(f"  {c['name']:35} sem transcript (silêncio)"); continue
        # janela da espinha na posição PREVISTA (startFrame já é a posição estimada pelo placements)
        pos_s = c["startFrame"] / 30.0
        # concatena espinha em 1 wav já foi feito por sync_ingest (cache scratch); aqui usamos o
        # arquivo bruto mais próximo por simplicidade (posição relativa dentro do chunk certo).
        # Se pos_s cai fora do 1º chunk, aproxima pelo offset acumulado dos chunks anteriores.
        acc = 0.0
        sp_pick, local_s = sp_paths[0], pos_s
        for p in sp_paths:
            d = ffdur(p)
            if acc + d > pos_s or p is sp_paths[-1]:
                sp_pick, local_s = p, max(0.0, pos_s - acc)
                break
            acc += d
        segs = inspect_segments(sp_pick, max(0, local_s - 20), local_s + 40)
        stxt = " ".join(x[0] for x in segs)
        score = overlap_score(ctxt, stxt)
        verdict = "CORROBORA" if score >= 0.15 else ("fraco" if score > 0 else "sem_sinal")
        report[c["name"]] = {"score": round(score, 3), "verdict": verdict,
                              "pos_s": round(pos_s, 1), "clip_sample": ctxt[:100], "spine_sample": stxt[:100]}
        print(f"  {c['name']:35} score={score:.2f} {verdict}")

    out = os.path.join(ws, "word_anchor_report.json")
    json.dump(report, open(out, "w"), indent=1, ensure_ascii=False)
    n_corr = sum(1 for v in report.values() if v["verdict"] == "CORROBORA")
    print(f"\n{len(report)} clips checados | {n_corr} corroboram a posição prevista -> {out}")
    print("(advisory: NÃO altera offset/orange — evidência p/ revisão)")


if __name__ == "__main__":
    main()
