#!/usr/bin/env python3
"""QA de pronunciacion de la voz clonada, en silencio y sin intervencion.

Idea: si sintetizo una palabra con XTTS y luego se la paso a Whisper y NO la
reconoce, es que la voz la pronuncia mal. Sirve para cazar siglas y anglicismos
(GPU, SSH, hacking, firewall...) y construir un diccionario de correcciones.

Dos fases en un mismo proceso para no cargar dos modelos en los 4 GB a la vez:
  1) XTTS (GPU) sintetiza todas las palabras a WAV.
  2) se libera XTTS y Whisper (GPU/CPU) las transcribe y compara.

    python qa_voz.py [cuda|cpu]

Salida: voz/qa_resultados.json  (palabras sospechosas ordenadas por rareza) y
un arranque de voz/pronunciacion.json con las siglas deletreadas.
No reproduce nada.
"""
import os, sys, re, json, time, unicodedata, difflib, tempfile
os.environ.setdefault("COQUI_TOS_AGREED", "1")

DISP = sys.argv[1] if len(sys.argv) > 1 else "cuda"
BASE = os.path.dirname(os.path.abspath(__file__))
REFS = [os.path.join(BASE, "ref", n) for n in
        ("jarvis_lento_b.wav", "jarvis_lento_a.wav", "jarvis_ref.wav")]
REFS = [r for r in REFS if os.path.exists(r)]
PARAMS = dict(speed=1.0, temperature=0.65, repetition_penalty=2.0)

# Vocabulario donde se concentran los fallos: siglas, anglicismos y tecnicos.
# (los terminos normales en espanol XTTS los dice bien; no hace falta el
#  diccionario entero, que serian 55 h de GPU)
VOCAB = """
GPU CPU RAM SSD USB SSH SSL TLS HTTP HTTPS FTP DNS IP VPN API URL SQL RSA AES
PGP GPG VLAN LAN WAN NAT DDoS XSS CSRF SQLi IPCC ONU UE OTAN CO2
hacking hacker cracker exploit payload malware ransomware phishing spoofing
firewall software hardware kernel shell root sudo bash script daemon backup
proxy router switch bytes gigabyte terabyte firmware bootloader overflow buffer
nmap metasploit wireshark linux debian ubuntu android
criptografia cifrado hash blockchain bitcoin ethereum wallet token
calentamiento invernadero climatico renovable emisiones
reconquista carlista franquismo transicion
señor máquina información galería atención
""".split()

# siglas que casi siempre se deletrean en espanol
DELETREO = {
    "a": "a", "b": "be", "c": "ce", "d": "de", "e": "e", "f": "efe",
    "g": "ge", "h": "hache", "i": "i", "j": "jota", "k": "ka", "l": "ele",
    "m": "eme", "n": "ene", "o": "o", "p": "pe", "q": "cu", "r": "erre",
    "s": "ese", "t": "te", "u": "u", "v": "uve", "w": "uve doble",
    "x": "equis", "y": "i griega", "z": "zeta", "0": "cero", "1": "uno",
    "2": "dos", "3": "tres",
}


def normaliza(s):
    s = unicodedata.normalize("NFD", s.lower())
    s = "".join(c for c in s if unicodedata.category(c) != "Mn")
    return re.sub(r"[^a-z0-9 ]", "", s).strip()


def es_sigla(w):
    return w.isupper() and 2 <= len(w) <= 6 and w.isalpha()


def deletrea(w):
    return " ".join(DELETREO.get(c, c) for c in w.lower())


def main():
    palabras = []
    vistos = set()
    for w in VOCAB:
        if w and w.lower() not in vistos:
            vistos.add(w.lower()); palabras.append(w)
    print(f"{len(palabras)} palabras a revisar en {DISP}", flush=True)

    tmp = tempfile.mkdtemp(prefix="qa_voz_")
    import torch
    from TTS.api import TTS
    tts = TTS("tts_models/multilingual/multi-dataset/xtts_v2").to(DISP)

    # Fase 1: sintetizar todas (GPU)
    wavs = {}
    t0 = time.time()
    for i, w in enumerate(palabras, 1):
        dst = os.path.join(tmp, f"{i:03d}.wav")
        try:
            tts.tts_to_file(text=w + ".", speaker_wav=REFS, language="es",
                            file_path=dst, **PARAMS)
            wavs[w] = dst
        except Exception as e:
            print(f"  fallo sintesis '{w}': {e}", flush=True)
    print(f"fase 1 (sintesis): {len(wavs)} en {time.time()-t0:.0f}s", flush=True)

    # liberar XTTS antes de cargar Whisper
    del tts
    if DISP == "cuda":
        torch.cuda.empty_cache()

    # Fase 2: transcribir y comparar
    from transformers import pipeline
    asr = pipeline("automatic-speech-recognition",
                   model="openai/whisper-small",
                   device=0 if DISP == "cuda" else -1)

    resultados = []
    t0 = time.time()
    for i, (w, path) in enumerate(wavs.items(), 1):
        try:
            oido = asr(path, generate_kwargs={"language": "spanish"})["text"]
        except Exception as e:
            oido = f"<error: {e}>"
        ratio = difflib.SequenceMatcher(
            None, normaliza(w), normaliza(oido)).ratio()
        resultados.append({"palabra": w, "oido": oido.strip(), "ratio": round(ratio, 2)})
        if i % 20 == 0:
            print(f"  ...{i}/{len(wavs)}", flush=True)
    print(f"fase 2 (asr): {time.time()-t0:.0f}s", flush=True)

    resultados.sort(key=lambda r: r["ratio"])
    with open(os.path.join(BASE, "qa_resultados.json"), "w", encoding="utf-8") as f:
        json.dump(resultados, f, ensure_ascii=False, indent=2)

    # arranque del diccionario de pronunciacion: siglas deletreadas cuyo
    # reconocimiento fue pobre
    dic = {}
    for r in resultados:
        w = r["palabra"]
        if es_sigla(w) and r["ratio"] < 0.6:
            dic[w] = deletrea(w)
    dic_path = os.path.join(BASE, "pronunciacion.json")
    if not os.path.exists(dic_path):
        with open(dic_path, "w", encoding="utf-8") as f:
            json.dump(dic, f, ensure_ascii=False, indent=2)

    malas = [r for r in resultados if r["ratio"] < 0.6]
    print(f"\nlisto: {len(malas)} sospechosas (ratio<0.6). "
          f"detalle en qa_resultados.json", flush=True)
    for r in malas[:15]:
        print(f"  {r['ratio']:.2f}  {r['palabra']:14s} -> «{r['oido']}»", flush=True)


if __name__ == "__main__":
    main()
