#!/usr/bin/env python3
"""Localizador NATIVO de candidatos de VARIEDADE/REAÇÃO (b-roll) pro judge_variety decidir.
Ancora nos HITS SigLIP de variedade (plateia/músicos/celebrante/corredor/emoção) numa cam coberta+ESTÁVEL
onde a cam ATIVA naquele instante NÃO mostra aquilo. Determinístico, thresholds gerais (não tunado a este
vídeo). FORA de clímax. Saída: variety_candidates.json -> judge (subagente) decide -> variety_inserts.json."""
import json, bisect, collections
import vlib as V

FPS = 30
SPAN = int(3 * FPS)
CLIMAX = {"vows", "rings", "kiss", "vow_yes", "resposta_sim", "beijo", "aliancas", "declaration", "declaracao"}
VARIETY = {"reacao_publico", "publico_assistindo", "aplausos", "orquestra", "musicos", "sax", "flauta",
           "violino", "celebrante_close", "corredor_movimento", "corredor_nave", "choro_emocao", "vo_lagrima"}
# Escalas diferentes no content_map_dense.json:
#   - top-50 (gen_content_map / moments): cosine raw ~0.02–0.08 neste evento
#   - denso (gen_scene_map_dense emit): score = z-score ~1.0–4.0
# MINSCORE=0.125 antigo matava o top-50 (0 hits no v0.2) e ainda era baixo p/ z.
MINSCORE_Z = 1.0       # denso: z >= 1.0
MINSCORE_ABS = 0.02    # top-50 raw: generoso; judge-VISÃO filtra


def _hit_strong_enough(h):
    """Aceita hit de variedade em qualquer escala (z denso ou raw top-50)."""
    if h.get("z") is not None:
        try:
            return float(h["z"]) >= MINSCORE_Z
        except (TypeError, ValueError):
            pass
    try:
        sc = float(h.get("score") or 0)
    except (TypeError, ValueError):
        return False
    if sc >= MINSCORE_Z:  # score campo carrega z no emit denso
        return True
    return sc >= MINSCORE_ABS

def tcf(tc):
    p = [int(x) for x in tc.split(":")]; return p[0]*108000 + p[1]*1800 + p[2]*30 + (p[3] if len(p) > 3 else 0)
SB = sorted([b for b in json.load(open("storyboard.json")) if b.get("tc_start")], key=lambda b: tcf(b["tc_start"]))
sbf = [tcf(b["tc_start"]) for b in SB]
def beat_at(f):
    i = bisect.bisect_right(sbf, f) - 1
    return SB[max(0, i)].get("beat", "ceremony") if SB else "ceremony"

COV = V.coverage(); JERK = V.load_jerk()
# v0.3: mesma regra do unified (bloco laranja ≥30s). min(orange) matava tudo se orange@f=0.
SYNC_END = V.derive_sync_end()
print(f"SYNC_END={SYNC_END} (inserts só com f < SYNC_END)", flush=True)
cps = sorted(json.load(open("versions/UNI.json"))["cutpoints"], key=lambda c: c["frame"])
cf = [c["frame"] for c in cps]
def active_at(f): return cps[max(0, bisect.bisect_right(cf, f)-1)]["angle"]

# 1. hits de variedade fortes, agrupados por (cam, key) em clusters temporais (gap<=4s)
HITS = [h for h in json.load(open("content_map_dense.json"))
        if h.get("key") in VARIETY and _hit_strong_enough(h) and h["f"] < SYNC_END]
HITS.sort(key=lambda h: h["f"])
clusters = collections.defaultdict(list)
seen = {}
for h in HITS:
    k = (h["cam"], h["key"])
    if k in seen and h["f"] - seen[k] <= 4*FPS: clusters[(k, seen[k])].append(h)
    else: clusters[(k, h["f"])].append(h); seen[k] = h["f"]
    seen[k] = h["f"]

cands = []
for (k, _), hs in clusters.items():
    cam, key = k
    f = int(sum(x["f"] for x in hs) / len(hs))
    an = V.CAM_ANGLE[cam]
    if active_at(f) == an: continue                       # já estamos mostrando essa cam
    beat = beat_at(f)
    if beat in CLIMAX: continue
    w0, w1 = f - SPAN//2, f + SPAN//2
    if not V.covers(COV, an, w0, w1): continue
    # v0.3.1: PRÉ-filtra tremor. Review 2026-07-13: 17/33 inserts aceitos com p80≥1
    # (ex. vo_lagrima@21:25 p80=4.8) — judge VLM sozinho NÃO segura. B-roll tem que ser
    # estável; se a cam do hit treme, não vira candidato (alt estável fica pro motor nativo).
    jk = V.mean_jerk(JERK, cam, w0, w1)
    if jk is None: continue
    try:
        import numpy as np
        _ts, _js = V.jerk_series(JERK, cam, w0, w1)
        p80 = float(np.percentile(_js, 80)) if _js is not None and len(_js) else 9.0
    except Exception:
        p80 = float(jk)
    if p80 >= 0.85 or float(jk) >= 0.75:
        continue
    cands.append({"f0": w0, "f1": w1, "tc": V.tc(w0/30)[:8], "active": active_at(f),
                  "cam": an, "key": key, "beat": beat, "jerk": round(float(jk), 2),
                  "jerk_p80": round(p80, 2),
                  "score": round(max(x["score"] for x in hs), 3)})

# dedup denso: no máx 1 candidato por janela de 20s (o de maior score) — variedade é ESPARSA/tasteful,
# não pipoca. O judge ainda filtra os melhores desses.
cands.sort(key=lambda c: (-c["score"]))
merged = []
def near(c): return any(abs(c["f0"]-m["f0"]) < 12*FPS for m in merged)   # 1 por ~12s (mais generoso: João quer + variedade)
for c in cands:
    if not near(c): merged.append(c)
merged.sort(key=lambda c: c["f0"])
json.dump(merged, open("variety_candidates.json", "w"), indent=1)
print(f"{len(merged)} candidatos de variedade (hit SigLIP estável fora-clímax, ativa não mostra)")
for c in merged: print(f"  {c['tc']} beat={c['beat']:16} active=cam{c['active']} -> cam{c['cam']} ({c['key']}, sc{c['score']})")
