#!/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"" 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()