#!/usr/bin/env python3 """La cadena de voz entera, con altavoces y microfono de verdad. ./venv/bin/python pruebas/prueba_voz.py Las otras pruebas miran piezas: que el emparejador acierte, que el candado cierre, que el puente mande los campos que el HUD lee. Ninguna contesta a la pregunta que de verdad importa —**si le hablo, ¿me contesta?**— y por eso el fallo de que oia bien pero descartaba en silencio llego hasta el usuario. Esta habla por los altavoces y escucha por el microfono, como una persona. Es lenta y necesita el volumen puesto, pero es la unica que prueba lo que se usa. ## Como funciona Se genera la frase con Piper, se reproduce POR FUERA de la boca de JARVIS —con un reproductor a pelo, para que la guarda del eco no la tape— y se mira que la cadena entera reaccione. Que el microfono llega a los altavoces ya lo demuestra prueba_eco.py; aqui se da por hecho y se avisa si falla. ## Lo que se comprueba, en orden 1. sin llamarle se oye pero NO se atiende, y se dice por que 2. llamandole se atiende y contesta 3. encadenando la siguiente sin repetir el nombre 4. como lo oye Whisper "Yarvis" y "Charles" tambien valen """ 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 manos import Manos # noqa: E402 from oido.captura import Captura # noqa: E402 from oido.despierta import Centinela, NOMBRE # noqa: E402 from oido.whisper import Oido, es_ruido # noqa: E402 from jarvis import atiende # noqa: E402 ESPERA = 16 # cuanto se escucha por cada frase dicha fallos = [] ajenas = [] # lo que se oyo y no era de la prueba: ruido de la sala def _se_parece(oido: str, dicho: str) -> bool: """Si lo transcrito es la frase que se reprodujo, aunque venga maltratada. Hace falta porque la prueba usa el microfono de la habitacion: si hay una tele, una conversacion o un video sonando, el bucle oye ESO y una prueba ingenua lo da por bueno. Ha pasado —una pasada dio por valida "¡No te olvides de todo ese chico!"— y una prueba que se cree cualquier ruido no prueba nada. """ import unicodedata def palabras(t): t = unicodedata.normalize("NFD", (t or "").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) if not b: return False return len(a & b) >= max(1, min(2, len(b) - 1)) def afirma(condicion, queja): print(f" {'OK ' if condicion else 'MAL'} {queja}") if not condicion: fallos.append(queja) class Escucha: """El bucle de voz de jarvis.py, reducido a lo que hace falta probar.""" def __init__(self, boca, oreja, centinela): self.boca, self.oreja, self.centinela = boca, oreja, centinela self.manos = Manos() self.oido = [] # (texto, atendido, motivo) self.dicho = [] # lo que ha contestado self._para = threading.Event() self._hilo = None def arranca(self): self._hilo = threading.Thread(target=self._bucle, daemon=True) self._hilo.start() def _bucle(self): cap = Captura(boca=self.boca) for wav in cap.frases(para_cuando=lambda: self._para.is_set()): try: texto = self.oreja.transcribe(wav) finally: try: os.unlink(wav) except OSError: pass if es_ruido(texto): continue atender, orden, _ = self.centinela.filtra(texto) if not atender: self.oido.append((texto, False, "no le han llamado")) continue self.oido.append((texto, True, "")) respuesta = atiende(orden, self.manos, None, traza=lambda t: None) if respuesta: self.dicho.append(respuesta) cap.para() def para(self): self._para.set() if self._hilo: self._hilo.join(timeout=6) def suelta(self, frase, espera=ESPERA): """Dice la frase POR FUERA de JARVIS y espera a que reaccione. Solo cuenta lo que se PARECE a lo reproducido: si hay ruido en la habitacion, el bucle lo oye tambien y darlo por bueno haria pasar la prueba sin haber probado nada. """ antes_oido, antes_dicho = len(self.oido), len(self.dicho) wav = self.boca._genera(frase) if not wav: return None, None time.sleep(0.6) subprocess.run(["paplay", wav], capture_output=True) fin = time.monotonic() + espera mio = None while time.monotonic() < fin: for entrada in self.oido[antes_oido:]: if _se_parece(entrada[0], frase): mio = entrada break if mio and (not mio[1] or len(self.dicho) > antes_dicho): break time.sleep(0.2) nuevo_dicho = self.dicho[antes_dicho:] ajeno = [o[0] for o in self.oido[antes_oido:] if o is not mio] if ajeno: ajenas.extend(ajeno) print(f" (ruido de la habitacion, ignorado: {ajeno[0][:44]!r})") return mio, (nuevo_dicho[-1] if nuevo_dicho else None) 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 centinela = Centinela(activo=True) esc = Escucha(boca, oreja, centinela) esc.arranca() time.sleep(1.5) print("\n 0 · con la palabra APAGADA (el defecto): atiende directo") centinela.activo = False oido, dicho = esc.suelta("Cuanta memoria queda.") if oido: print(f" oyo: {oido[0]!r}") if dicho: print(f" dijo: {dicho}") afirma(oido is not None and oido[1], "sin exigir el nombre, atiende lo que oiga") afirma(dicho is not None, "y contesta") centinela.activo = True centinela.duerme() print("\n 1 · con la palabra puesta y sin llamarle: oye pero no atiende") oido, dicho = esc.suelta("Cuanto espacio queda en el disco.") if oido is None: print(" ·· no se oyo nada: ¿altavoces en silencio? sin esto el resto") print(" de la prueba no demuestra nada") fallos.append("el microfono no capta los altavoces") else: print(f" oyo: {oido[0]!r}") afirma(not oido[1], "lo oye pero no lo atiende, porque no le han llamado") afirma(dicho is None, "y no contesta") centinela.duerme() print(f"\n 2 · llamandole por su nombre") oido, dicho = esc.suelta(f"{NOMBRE.capitalize()}, cuanto espacio queda.") if oido: print(f" oyo: {oido[0]!r}") if dicho: print(f" dijo: {dicho}") afirma(oido is not None and oido[1], "lo atiende") afirma(dicho is not None, "y contesta algo") afirma(dicho is not None and any(c.isdigit() for c in dicho), "con una cifra de verdad, no una excusa") print("\n 3 · encadenando, sin repetir el nombre") oido, dicho = esc.suelta("Cuanta memoria queda.") if oido: print(f" oyo: {oido[0]!r}") afirma(oido is not None and oido[1], "sigue despierto y atiende sin que le vuelvan a llamar") print("\n 4 · como lo oye Whisper de verdad") centinela.duerme() oido, dicho = esc.suelta("Yarvis, que hora es.") if oido: print(f" oyo: {oido[0]!r}") afirma(oido is not None and oido[1], "'Yarvis' tambien le despierta (Whisper escribe el nombre de mil formas)") esc.para() oreja.para() # Una prueba que usa el microfono de la sala no puede fallar en silencio por # culpa de la sala. Si ha entrado mucho ruido, el resultado no dice nada del # codigo y hay que decirlo, no soltar una pared de MAL. if fallos and len(ajenas) >= 2: print(f"\n ┌─ SALA RUIDOSA ─────────────────────────────────────────") print(f" │ Han entrado {len(ajenas)} frases que no eran de la prueba.") print(f" │ Con audio sonando cerca del microfono, este resultado NO") print(f" │ dice nada del codigo. Ejemplos de lo que se colo:") for a in ajenas[:3]: print(f" │ {a[:52]!r}") print(f" │ Repitelo en silencio.") print(f" └────────────────────────────────────────────────────────") print(f"\n {len(fallos)} fallan, pero no son concluyentes\n") return 2 print(f"\n {'todo en orden' if not fallos else str(len(fallos)) + ' fallan'}\n") return 1 if fallos else 0 if __name__ == "__main__": sys.exit(main())