#!/usr/bin/env python3
"""Classifica cada CHUNK de gravador por CONTEÚDO (transcript LOCAL via Speech.SpeechTranscriber
nativo do macOS — API pública Apple, on-device, uso comercial livre, ZERO GPL — com fallback pra
faster-whisper se o binário nativo não estiver disponível) — cerimônia / festa-entrada / música.
Espectro fala-vs-música falha quando há música sob a voz do celebrante; as PALAVRAS não.
Uso: classify_recorders.py <base> <pasta1> [pasta2 ...]
Saída: recorder_content.json  {arquivo: {label, score, sample}}

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 `TranscribeNative` (binário Swift
próprio, Sources/TranscribeNative/, usa `Speech.SpeechTranscriber` — framework padrão do SDK
Apple, mesmo direito de uso de qualquer app comercial). Benchmark real (WAV 90s, cerimônia Paula/
Ronaldo): nativo ~0.8-2.6s vs faster-whisper ~18.7s (1ª transcrição, load do modelo Python) — a
decupagem inteira caía de ~30min pra bem menos. faster-whisper fica como FALLBACK (binário nativo
ausente/falha) — zero contaminação em ambos os caminhos. Ver NATIVO_PALMIER_PROIBIDO.md."""
import sys, os, re, glob, json, subprocess, threading

# palavras que marcam cada tipo (pt-BR, genérico p/ casamento)
CER = ["declaro", "matrimôni", "matrimoni", "aliança", "alianca", "aceita", "esposo", "esposa",
       "amar", "respeitar", "fidelidade", "deus vos", "vos declaro", "consagr", "abençoe",
       "aliança", "senhor", "senhora", "unir", "casamento", "juras", "voto", "eterno", "amém", "amem"]
FESTA = ["obrigad", "brigad", "palmas", "vamos", "festa", "galera", "boa noite a tod", "salva de",
         "dj", "pista", "brinde", "vem pra", "todo mundo", "bora"]

def score_text(txt):
    t = txt.lower()
    cer = sum(t.count(w) for w in CER)
    festa = sum(t.count(w) for w in FESTA)
    words = len(re.findall(r"[a-zà-ú]{3,}", t))
    return cer, festa, words

FFMPEG = "/opt/homebrew/bin/ffmpeg"
_WHISPER_MODEL = [None]
_WHISPER_LOCK = threading.Lock()

# TranscribeNative (Sources/TranscribeNative/, Speech.SpeechTranscriber nativo) — candidatos de
# path (debug local, release, ou override explícito via env pra quem empacotar diferente).
_PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
_NATIVE_BIN_CANDIDATES = [
    os.environ.get("TRANSCRIBE_NATIVE_BIN", ""),
    os.path.join(_PROJECT_ROOT, ".build", "arm64-apple-macosx", "release", "TranscribeNative"),
    os.path.join(_PROJECT_ROOT, ".build", "arm64-apple-macosx", "debug", "TranscribeNative"),
    os.path.join(_PROJECT_ROOT, ".build", "release", "TranscribeNative"),
    os.path.join(_PROJECT_ROOT, ".build", "debug", "TranscribeNative"),
]
_NATIVE_BIN = next((p for p in _NATIVE_BIN_CANDIDATES if p and os.path.isfile(p)), None)


def _whisper():
    """Carrega o modelo Whisper (lazy, 1x por processo) — CPU, int8 (rápido o bastante pra
    classificar, não precisa word-perfect, só achar palavras-chave). Tamanho via env
    WHISPER_MODEL_SIZE (default 'small' — melhor equilíbrio precisão/velocidade em pt-BR pra esta
    tarefa; 'base' se quiser mais rápido e menos memória)."""
    with _WHISPER_LOCK:
        if _WHISPER_MODEL[0] is None:
            from faster_whisper import WhisperModel
            size = os.environ.get("WHISPER_MODEL_SIZE", "small")
            _WHISPER_MODEL[0] = WhisperModel(size, device="cpu", compute_type="int8")
        return _WHISPER_MODEL[0]


def _transcribe_window_native(window_wav):
    """Chama o binário TranscribeNative (Speech.SpeechTranscriber, API pública Apple). Retorna o
    texto ou None se o binário faltar/falhar (caller cai pro fallback faster-whisper)."""
    if not _NATIVE_BIN:
        return None
    try:
        r = subprocess.run(
            [_NATIVE_BIN, window_wav, "--locale", "pt-BR"],
            capture_output=True, text=True, timeout=60
        )
        if r.returncode != 0:
            return None
        return json.loads(r.stdout.strip()).get("text", "")
    except Exception:
        return None


def transcribe_windows(path, dur, n=4):
    """N janelas de até 90s ao longo do arquivo, transcritas LOCALMENTE — `TranscribeNative`
    (Speech.SpeechTranscriber nativo do macOS, ~10x mais rápido) com fallback pra faster-whisper
    se o binário não existir/falhar (dev sem build release, plataforma sem Speech, etc). Substitui
    o antigo `inspect_media` (transcript on-device do app GPL). `path` é o WAV/áudio de verdade
    (lê o arquivo direto, read-only)."""
    out = []
    model = None  # lazy: só carrega whisper se o nativo faltar em alguma janela
    for i in range(n):
        s = int(dur * (i + 0.5) / n)
        e = min(s + 90, dur)
        if e <= s:
            out.append("")
            continue
        window_wav = os.path.join(
            os.path.dirname(_CACHE_PATH), f".w_{os.getpid()}_{threading.get_ident()}_{i}.wav"
        )
        try:
            subprocess.run(
                [FFMPEG, "-v", "error", "-ss", str(s), "-i", path, "-t", str(e - s),
                 "-ar", "16000", "-ac", "1", "-y", window_wav],
                capture_output=True
            )
            if not os.path.exists(window_wav):
                out.append("")
                continue
            text = _transcribe_window_native(window_wav)
            if text is None:
                if model is None:
                    model = _whisper()
                segments, _info = model.transcribe(window_wav, language="pt", vad_filter=True)
                text = " ".join(seg.text for seg in segments)
            out.append(text)
        except Exception:
            out.append("")
        finally:
            if os.path.exists(window_wav):
                try:
                    os.remove(window_wav)
                except OSError:
                    pass
    return out


# CACHE persistente por (path, mtime, size): transcript on-device NÃO é determinístico entre runs
# (Whisper reamostra) e o threshold binário (cer>=2) é frágil na fronteira — a mesma tomada podia
# classificar diferente entre runs, mudando a espinha e o naming (João pediu robustez pros 2 cenários).
# Cache mata a variabilidade (mesmo arquivo = mesma decisão) e evita re-transcrever (mais rápido).
_CACHE_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "recorder_content_cache.json")


def _cache_key(path):
    st = os.stat(path)
    return f"{os.path.abspath(path)}|{st.st_mtime_ns}|{st.st_size}"


def _load_cache():
    if os.path.exists(_CACHE_PATH):
        try:
            return json.load(open(_CACHE_PATH))
        except Exception:
            return {}
    return {}


def _save_cache(c):
    json.dump(c, open(_CACHE_PATH, "w"), indent=1, ensure_ascii=False)


def classify_chunk(path, n=4):
    """Transcreve n janelas do WAV (LOCAL, faster-whisper) e rotula cerimonia/festa/musica por CONTEÚDO.
    Robusto a música de fundo (usa PALAVRAS). Retorna dict ou None se o arquivo for curto demais.
    Cacheado por arquivo (determinístico entre runs + evita re-transcrever)."""
    cache = _load_cache()
    key = _cache_key(path)
    if key in cache:
        return cache[key]
    from classify_audio_content import ffdur
    dur = ffdur(path)
    if dur < 60:
        return None
    wins = transcribe_windows(path, dur, n)
    cer = festa = words = 0
    for w in wins:
        c, f, wd = score_text(w); cer += c; festa += f; words += wd
    if words < 8: label = "musica"
    elif cer >= 2 and cer >= festa: label = "cerimonia"
    elif festa > cer: label = "festa"
    else: label = "fala-indef"
    res = {"label": label, "cer": cer, "festa": festa, "words": words,
           "min": round(dur/60), "sample": (" ".join(wins))[:200]}
    cache[key] = res
    _save_cache(cache)
    return res

def main():
    base = sys.argv[1]; pastas = sys.argv[2:]
    from classify_audio_content import ffdur
    res = {}
    for pasta in pastas:
        files = sorted(glob.glob(os.path.join(base, pasta, "**", "*.WAV"), recursive=True) +
                       glob.glob(os.path.join(base, pasta, "**", "*.wav"), recursive=True))
        for p in files:
            c = classify_chunk(p)
            if not c: continue
            c["pasta"] = pasta
            res[os.path.basename(p)] = c
            print(f"  {pasta}/{os.path.basename(p)} ({c['min']}min): {c['label'].upper()} (cer={c['cer']} festa={c['festa']} words={c['words']})")
            print(f"      \"{c['sample'][:120]}\"")
    json.dump(res, open("recorder_content.json", "w"), indent=1, ensure_ascii=False)
    print(f"\nrecorder_content.json: {len(res)} chunks classificados")

if __name__ == "__main__":
    main()
