"""transcrever.py — a fala vira texto AQUI, na máquina.

    "Pode ser local, daí pra transcrever ele instala Whisper." (23/09/2026)

── POR QUE LOCAL ───────────────────────────────────────────────────────────
A transcrição ia para o servidor do PrimoCode (`/api/transcrever`). Com o
servidor fora do ar, o modo voz ouvia, pensava — e não entendia nada. Local,
ela não depende de rede, não custa chave e o áudio não sai do computador.

── POR QUE faster-whisper, E POR QUE O `small` ─────────────────────────────
É o Whisper da OpenAI reimplementado em CTranslate2: roda no processador de
qualquer máquina (Mac Intel ou Apple Silicon, Linux, Windows), instala por pip
sem compilar nada e lê o wav direto, sem ffmpeg. O modelo `small` (~480 MB,
uma vez) é o menor que entende português falado sem tropeçar em nome próprio;
o `base` erra demais, o `medium` dobra o tempo de resposta. Quem quiser outro
troca por PRIMOCODE_WHISPER_MODELO.

O modelo carrega UMA vez por sessão, em segundo plano, logo que o modo voz
abre — senão a primeira frase esperaria o carregamento inteiro.
"""
from __future__ import annotations

import json
import os
import sys
import threading
from pathlib import Path
from typing import Optional

MODELO = os.environ.get("PRIMOCODE_WHISPER_MODELO", "small").strip() or "small"
PASTA = Path.home() / ".primocode" / "modelos" / "whisper"

_modelo = None
_trava = threading.Lock()
_erro: Optional[str] = None


def disponivel() -> bool:
    try:
        import faster_whisper  # noqa: F401
        return True
    except Exception:
        return False


def carregar():
    """O modelo, carregado uma vez (e baixado, se for a primeira)."""
    global _modelo, _erro
    with _trava:
        if _modelo is not None:
            return _modelo
        from faster_whisper import WhisperModel
        PASTA.mkdir(parents=True, exist_ok=True)
        try:
            # int8 no processador: é o que cabe em qualquer máquina e ainda
            # responde uma frase de dez segundos em um ou dois.
            _modelo = WhisperModel(MODELO, device="cpu", compute_type="int8",
                                   download_root=str(PASTA))
            _erro = None
        except Exception as e:
            _erro = str(e)
            raise
        return _modelo


def aquecer() -> None:
    """Carrega em segundo plano. Erro aqui não derruba nada: a transcrição
    tenta de novo na hora, e cai para o servidor se não der."""
    def _ir():
        try:
            carregar()
        except Exception:
            pass
    threading.Thread(target=_ir, daemon=True).start()


# ── O QUE O WHISPER INVENTA ─────────────────────────────────────────────
# Treinado em legenda de vídeo, ele "ouve" no ruído o fim de um vídeo do
# YouTube. Essas frases chegavam ao agente como pedido, e ele respondia a
# elas: "eu mandei oi e ele respondeu nada a ver" (23/09/2026).
ALUCINACOES = (
    "obrigado por assistir", "obrigada por assistir", "obrigado pela atenção",
    "legendas pela comunidade", "amara.org", "inscreva-se", "se inscreva",
    "deixe seu like", "ative o sininho", "até o próximo vídeo", "até a próxima",
    "legendado por", "legenda por", "tradução e legendas", "transcrição por",
    "www.", ".com.br",
)
# Acima disto o próprio Whisper acha que não havia fala no trecho.
SEM_FALA = 0.6
# Abaixo disto a confiança média é baixa demais para virar pedido.
CONFIANCA_MINIMA = -1.0


def limpar(texto: str) -> str:
    """Tira a frase inventada. Devolve "" quando não sobra pedido."""
    t = " ".join(str(texto or "").split())
    baixo = t.lower()
    if any(frase in baixo for frase in ALUCINACOES):
        return ""
    # Só pontuação ou uma letra solta não é pedido.
    if len("".join(ch for ch in t if ch.isalnum())) < 2:
        return ""
    return t


def aproveitavel(segmento) -> bool:
    """O segmento tem fala de verdade, pelo que o próprio Whisper mediu?"""
    sem_fala = getattr(segmento, "no_speech_prob", 0.0) or 0.0
    confianca = getattr(segmento, "avg_logprob", 0.0) or 0.0
    return not (sem_fala > SEM_FALA and confianca < CONFIANCA_MINIMA)


def transcrever(wav: Path, idioma: str = "pt") -> str:
    modelo = carregar()
    # vad_filter corta o silêncio das pontas: sem ele, o Whisper às vezes
    # "ouve" uma frase inteira inventada no silêncio ("Obrigado por assistir").
    segmentos, _ = modelo.transcribe(str(wav), language=idioma, beam_size=5,
                                     vad_filter=True, condition_on_previous_text=False)
    return limpar(" ".join(s.text.strip() for s in segmentos if aproveitavel(s)))


def palavras(arquivo: str, idioma: str = "pt") -> list:
    """Cada palavra com o tempo dela, no formato do /api/transcrever
    ({de, ate, texto}). É o que o editor de vídeo usa para legenda e para
    apagar palavra (lib/video/transcript.js)."""
    modelo = carregar()
    segmentos, _ = modelo.transcribe(str(arquivo), language=idioma, beam_size=5, word_timestamps=True,
                                     vad_filter=True, condition_on_previous_text=False)
    out = []
    for s in segmentos:
        if not aproveitavel(s):
            continue
        for w in (s.words or []):
            texto = (w.word or "").strip()
            if texto:
                out.append({"de": round(float(w.start), 3), "ate": round(float(w.end), 3), "texto": texto})
    return out


def main() -> int:
    """`python -m primo_voz.transcrever baixar` — usado pelo /voice para deixar
    o modelo pronto antes da primeira frase. Responde uma linha de JSON.
    `... palavras <arquivo> [idioma]` devolve as palavras com tempo."""
    acao = sys.argv[1] if len(sys.argv) > 1 else "baixar"
    if acao == "palavras":
        if not disponivel():
            print(json.dumps({"ok": False, "error": "o faster-whisper não está instalado"}))
            return 1
        try:
            ws = palavras(sys.argv[2], sys.argv[3] if len(sys.argv) > 3 else "pt")
        except Exception as e:
            print(json.dumps({"ok": False, "error": str(e)}))
            return 1
        print(json.dumps({"ok": True, "palavras": ws}, ensure_ascii=False))
        return 0
    if acao != "baixar":
        print(json.dumps({"ok": False, "error": f"ação desconhecida: {acao}"}))
        return 2
    if not disponivel():
        print(json.dumps({"ok": False, "error": "o faster-whisper não está instalado"}))
        return 1
    try:
        carregar()
    except Exception as e:
        print(json.dumps({"ok": False, "error": f"não consegui baixar o modelo {MODELO}: {e}"}))
        return 1
    print(json.dumps({"ok": True, "modelo": MODELO, "pasta": str(PASTA)}))
    return 0


if __name__ == "__main__":
    sys.exit(main())
