#!/usr/bin/env python3 """La prueba que Newelle no pasa: que JARVIS no se oiga a si mismo. En Newelle, mas del 21 % de lo que oye y no entiende es su propia voz. Esta prueba lo comprueba de verdad, con el microfono y los altavoces, en dos partes: A. Suena una frase POR FUERA de JARVIS (un reproductor a pelo, como si hablara una persona). Tiene que captarla. Si no, el microfono no llega a los altavoces y la parte B no demuestra nada. B. La misma frase, pero dicha POR JARVIS. NO tiene que captarla. Las dos partes juntas son la prueba: A sola no dice nada, B sola podria pasar simplemente porque el microfono no oye nada. ./venv/bin/python pruebas/prueba_eco.py Necesita altavoces encendidos y a un volumen normal. Si estan en silencio, la parte A falla y avisa. """ import os import subprocess import sys import threading import time AQUI = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, AQUI) from boca.voz import Boca # noqa: E402 from oido.captura import Captura # noqa: E402 from oido.whisper import Oido, es_ruido # noqa: E402 FRASE = "El sistema esta operativo y a su disposicion, señor." ESPERA = 14 # cuanto se escucha en cada parte def _se_parece(oido: str, dicho: str) -> bool: """Si lo transcrito es la frase que se dijo, aunque venga maltratada. Se compara por palabras y no por parecido de cadena: un eco recortado ("operativo y a su disposicion") no se parece a la frase entera como cadena, pero comparte casi todas sus palabras, y eso es inequivoco. """ import unicodedata def palabras(t): t = unicodedata.normalize("NFD", t.lower()) t = "".join(c for c in t if unicodedata.category(c) != "Mn") return {p.strip(".,¡!¿?") for p in t.split() if len(p) > 3} a, b = palabras(oido), palabras(dicho) return bool(b) and len(a & b) >= 2 def escucha_un_rato(captura, oreja, segundos): """Devuelve lo que se haya oido en ese rato.""" oido = [] fin = time.monotonic() + segundos hilo_paro = threading.Event() def bucle(): for wav in captura.frases(para_cuando=lambda: hilo_paro.is_set()): try: t = oreja.transcribe(wav) finally: try: os.unlink(wav) except OSError: pass if t and not es_ruido(t): oido.append(t) h = threading.Thread(target=bucle, daemon=True) h.start() while time.monotonic() < fin: time.sleep(0.2) hilo_paro.set() captura.para() h.join(timeout=5) return oido def main() -> int: boca = Boca("davefx") oreja = Oido(modelo="small") if not oreja.disponible(): print(" no esta whisper o el modelo; no se puede probar") return 2 print(" arrancando whisper...") if not oreja.arranca(): print(" whisper no arranco") return 2 # Se genera el WAV una vez para poder reproducirlo por los dos caminos con # exactamente el mismo audio. wav = boca._genera(FRASE) if not wav: print(" no se pudo generar la voz") return 2 fallos = 0 # --- A: suena por fuera, tiene que oirse ----------------------------- print("\n A · suena por fuera (como si hablara alguien)") captura = Captura(boca=boca) def suelta_fuera(): time.sleep(2.5) subprocess.run(["paplay", wav], capture_output=True) threading.Thread(target=suelta_fuera, daemon=True).start() oido_a = escucha_un_rato(captura, oreja, ESPERA) for t in oido_a: print(f" oido: {t!r}") if oido_a: print(" OK el microfono llega a los altavoces") else: print(" MAL no se oyo nada: ¿altavoces en silencio? sin esto la") print(" parte B no demuestra nada") fallos += 1 time.sleep(1.5) # --- B: lo dice JARVIS, NO tiene que oirse --------------------------- print("\n B · lo dice JARVIS (la guarda tiene que taparlo)") captura = Captura(boca=boca) def suelta_jarvis(): time.sleep(2.5) boca.di(FRASE) threading.Thread(target=suelta_jarvis, daemon=True).start() oido_b = escucha_un_rato(captura, oreja, ESPERA) # Lo que se juzga es si se colo LA FRASE, no si se transcribio algo. # # Esto costo una conclusion equivocada: probando con colas de 350, 700 y # 1100 ms el resultado no era monotono —1100 fallaba y 700 no—, lo cual es # imposible si el unico factor fuera el eco. Mirando lo que se colaba, eran # "¡Adios!" y un parrafo inventado: Whisper ALUCINANDO sobre el silencio, # que es otro problema distinto y da igual lo larga que sea la guarda. eco = [t for t in oido_b if _se_parece(t, FRASE)] alucinado = [t for t in oido_b if t not in eco] for t in eco: print(f" ECO: {t!r}") for t in alucinado: print(f" (whisper alucinando sobre el silencio: {t[:52]!r})") if not eco: print(" OK JARVIS no se oye a si mismo") else: print(" MAL se ha oido hablar: la guarda no tapa") fallos += 1 oreja.para() print(f"\n {'todo en orden' if not fallos else str(fallos) + ' fallan'}") return 1 if fallos else 0 if __name__ == "__main__": sys.exit(main())