#!/usr/bin/env python3 """El emparejador: que siga acertando y, sobre todo, que no se invente ordenes. ./venv/bin/python pruebas/prueba_manos.py Porta las comprobaciones de `pruebas/08_acciones.sh` de la epoca de Newelle y añade las que hacen falta ahora que el emparejador compara tambien por sonido. La regla que ordena este fichero: **equivocarse cuesta mas que no reaccionar.** Esto lanza comandos en la maquina; que no entienda una orden es molesto, que ejecute la que no es puede costar trabajo perdido. Por eso las comprobaciones de falsos positivos son las que mandan, y por eso el suelo de acierto va aparte y mas bajo de lo medido: para que una mejora legitima no obligue a tocar el test, pero una regresion de verdad lo tire. """ import json import os import shlex import sys AQUI = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, AQUI) from manos.acciones import Catalogo # noqa: E402 from manos.fonetica import fonetica # noqa: E402 from oido.whisper import es_ruido # noqa: E402 CATALOGO = os.path.join(AQUI, "datos", "acciones.json") BANCO = os.path.join(AQUI, "datos", "banco.json") # Medido el 15 de agosto de 2026: 84,7 %. El suelo se pone tres puntos por # debajo para dejar sitio al ruido de un banco de 150, donde una frase es 0,67. SUELO = 81.0 # Lo que NO puede disparar una accion jamas. Las cinco primeras vienen del test # 08; el resto son conversacion que empieza igual que una orden, que es # exactamente el riesgo de comparar por sonido. CHARLA = [ "necesito un abrazo", "quiero contarte una cosa", "necesito pensar", "puedes explicarme como funciona el kernel", "me gustaria que fueramos amigos", "que tal estas", "como te encuentras hoy", "cuentame un chiste", "que opinas de la musica electronica", "me apetece un cafe", "hoy hace un dia estupendo", "estoy pensando en cambiar de trabajo", "quien fue alan turing", "explicame la teoria de la relatividad", "me gusta mucho como suena tu voz", "cuanto tiempo llevas funcionando", "no se que hacer este fin de semana", "que peliculas me recomiendas", "he tenido un dia muy largo", "sabes cantar", "no quiero que abras nada", "no busques nada por ahora", "antes buscabas mejor las cosas", "el disco duro de mi hermano se rompio", "la memoria me falla ultimamente", "que temperatura hace en la calle", "me han abierto una cuenta en el banco", "cierro los ojos y pienso", "mi ventana da al patio", "la pantalla del movil se ha roto", "ese proceso judicial fue largo", "tengo un fichero de esos antiguos", "el kernel de la cuestion es otro", "hablame de la red de metro", "pon atencion a lo que te digo", "sube el animo que no pasa nada", "baja la voz que estan durmiendo", "abre tu corazon", "escanea el documento con la impresora", "mata el tiempo como puedas", "el volumen de trabajo es enorme", "que hora era cuando llegaste", "las descargas de agua fueron fuertes", "mi carpeta de fotos favorita", "el brillo de sus ojos", "conecta conmigo emocionalmente", "quiero que sepas que me importa", "necesito que me escuches", "puedes decirme si estoy equivocado", "me gustaria saber tu opinion", ] # Como habla la gente de verdad. Sin quitar la cortesia no encajaba ninguna: # era el fallo de casi todas las ordenes reales que se perdian. CON_CORTESIA = [ ("Necesito que pongas la película de Iron Man.", "pelicula_abrir"), ("¿Puedes poner la película de Iron Man?", "pelicula_abrir"), ("Necesito que me abras una terminal.", "terminal_abrir"), ("¿Podrías abrir una terminal?", "terminal_abrir"), ("Me puedes decir cuánto espacio queda.", "disco_libre"), ] fallos = [] def afirma(condicion, queja): if condicion: print(f" OK {queja}") else: print(f" MAL {queja}") fallos.append(queja) def main() -> int: cat = Catalogo([CATALOGO]) print("\n el catalogo") afirma(len(cat.acciones) >= 150, f"{len(cat.acciones)} acciones cargadas") afirma(not cat.errores, f"sin errores de carga ({len(cat.errores)})") ids = [a.id for a in cat.acciones] afirma(len(ids) == len(set(ids)), "no hay ids repetidos") afirma(all(a.acuse for a in cat.acciones), "todas tienen acuse") afirma(all("{argumento" in a.comando for a in cat.acciones if a.captura), "las que capturan argumento lo usan en el comando") afirma(all(a.confirmar for a in cat.acciones if "rm -rf" in a.comando), "nada que borre se ejecuta sin confirmar") print("\n entiende como habla la gente") for frase, esperado in CON_CORTESIA: a, _ = cat.busca(frase) afirma(a is not None and a.id == esperado, f"'{frase[:38]}' -> {esperado}" + ("" if a is None or a.id == esperado else f" (fue a {a.id})")) print("\n y NO se inventa ordenes") coladas = [(c, cat.busca(c)[0]) for c in CHARLA] coladas = [(c, a.id) for c, a in coladas if a is not None] afirma(not coladas, f"ninguna de las {len(CHARLA)} frases de charla dispara nada" + ("" if not coladas else f" — se colo {coladas[0][0]!r} -> {coladas[0][1]}")) print("\n el argumento viene de un microfono: no puede ser una orden") for veneno in ["gatos; rm -rf /", 'algo " ; touch /tmp/jarvis_pwned ; "']: a, arg = cat.busca(f"busca en internet {veneno}") if a is None: fallos.append(f"no encaja la busqueda con {veneno!r}") print(f" MAL no encaja la busqueda con {veneno!r}") continue comando = a.comando.replace("{argumento_url}", shlex.quote(arg)) comando = comando.replace("{argumento}", shlex.quote(arg)) afirma("; rm" not in comando and "; touch" not in comando, f"entrecomillado: {veneno[:24]!r}") print("\n el filtro de alucinaciones de Whisper") for t in ["[Música]", "(aplausos)", "[motor]", "¡Suscríbete!", ""]: afirma(es_ruido(t), f"descarta {t!r}") for t in ["cuanto espacio queda", "abre la terminal"]: afirma(not es_ruido(t), f"deja pasar {t!r}") print("\n la comparacion por sonido") # Lo que se afirma es que ACERCA, no que iguale: `parecido()` se queda con # el mayor de los dos parecidos, asi que basta con que el del sonido suba. # Exigir igualdad seria pedirle a esto que fuera un Metaphone, y no lo es # a proposito: comprimir mas junta ordenes que no se parecen. import difflib def sube(a, b): letra = difflib.SequenceMatcher(None, a, b).ratio() sonido = difflib.SequenceMatcher(None, fonetica(a), fonetica(b)).ratio() return sonido > letra afirma(sube("abre van kamp", "abre bandcamp"), "'van kamp' se acerca a 'bandcamp'") afirma(fonetica("haz") == fonetica("has"), "'haz' suena igual que 'has'") afirma(fonetica("llave") == fonetica("yave"), "'yave' suena igual que 'llave'") afirma(fonetica("bino") == fonetica("vino"), "la b y la v son el mismo sonido") # Y lo que importa de verdad: que no acerque lo que no se parece. afirma(not sube("abre la terminal", "apaga la pantalla"), "y NO acerca ordenes distintas") print("\n el banco fijo") if not os.path.exists(BANCO): print(" ·· no hay banco; crealo con pruebas/banco.py --crear") else: casos = json.load(open(BANCO, encoding="utf-8"))["casos"] bien = otra = 0 for c in casos: a, _ = cat.busca(c["oido"]) if a is None: continue if a.id == c["id"]: bien += 1 else: otra += 1 pct = 100 * bien / len(casos) afirma(pct >= SUELO, f"acierto {pct:.1f} % sobre {len(casos)} (suelo {SUELO})") afirma(otra == 0, f"ninguna orden acaba en la accion equivocada ({otra})") 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())