#!/usr/bin/env python3 """Banco fijo para medir el emparejador sin volver a grabar. banco.py --crear [n] graba y transcribe una vez (lento) banco.py --mide puntua el catalogo actual (instantaneo) banco.py --fallos enseña que se sigue fallando ## Por que hace falta La mejora nocturna medía generando audio nuevo cada ronda: nueve horas, 39 rondas, +1,45 puntos de mejora contra ±1,9 puntos de ruido de medida. El ruido era mayor que la mejora, asi que "¿esto mejora o empeora?" se decidia a cara o cruz. Hoy ha vuelto a pasar: los flags de Whisper parecian dar +5 puntos con 40 muestras y con 150 no dan nada. Aqui se graba UNA vez con semilla fija y se guardan las TRANSCRIPCIONES. A partir de ahi, probar un cambio en el emparejador es instantaneo y determinista: la misma entrada siempre, asi que dos medidas son comparables de verdad. ## La trampa que ya costo una conclusion equivocada El 24 % del catalogo son acciones que piden argumento. Diciendolas sueltas —"busca el texto"— el emparejador NO las dispara, y hace bien: no son una orden. Un banco que las dice sueltas mide 56 % donde la realidad es 79 %, y hace parecer que el emparejador esta roto cuando lo que esta roto es el banco. Por eso `--crear` les añade argumento. """ import argparse import collections import difflib import json import os import random import subprocess import sys import tempfile AQUI = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, AQUI) from manos.acciones import Catalogo, normaliza # noqa: E402 from oido.whisper import Oido, es_ruido # noqa: E402 RAIZ = os.path.dirname(AQUI) PIPER = os.path.join(RAIZ, "config", "jarvis-piper.sh") CATALOGO = os.path.join(AQUI, "datos", "acciones.json") BANCO = os.path.join(AQUI, "datos", "banco.json") # Fijas a proposito: son lo que hace el banco reproducible. Cambiarlas obliga a # rehacerlo, y esa es justamente la señal de que ya no es comparable. VOCES = ["es_ES-davefx-medium", "es_MX-claude-high", "es_ES-sharvard-medium"] SEMILLA = 20260815 ARGUMENTOS = ["informes", "descargas", "musica", "el proyecto", "documentos"] def crear(n: int) -> int: cat = Catalogo([CATALOGO]) rnd = random.Random(SEMILLA) conf = [a for a in cat.acciones if a.frases] casos = [] for a in rnd.sample(conf, min(n, len(conf))): frase = rnd.choice(a.frases) if a.captura: frase += " " + rnd.choice(ARGUMENTOS) casos.append({"id": a.id, "frase": frase, "voz": rnd.choice(VOCES), "captura": a.captura}) print(f" grabando {len(casos)} frases...", flush=True) tmp = tempfile.mkdtemp(prefix="banco") for i, c in enumerate(casos): wav = os.path.join(tmp, f"{i:03d}.wav") subprocess.run([PIPER, wav, c["frase"]], env={**os.environ, "JARVIS_PIPER_VOZ": c["voz"], "JARVIS_PIPER_TONO": "1.0"}, capture_output=True, timeout=90) c["wav"] = wav if os.path.exists(wav) and os.path.getsize(wav) else None oreja = Oido(modelo="small", beam=5) if not oreja.arranca(): print(" whisper no arranco") return 2 print(" transcribiendo...", flush=True) for i, c in enumerate(casos): c["oido"] = oreja.transcribe(c["wav"]) if c["wav"] else "" c.pop("wav", None) if (i + 1) % 40 == 0: print(f" {i+1}/{len(casos)}", flush=True) oreja.para() subprocess.run(["rm", "-rf", tmp]) casos = [c for c in casos if c.get("oido")] os.makedirs(os.path.dirname(BANCO), exist_ok=True) with open(BANCO, "w", encoding="utf-8") as f: json.dump({"semilla": SEMILLA, "modelo": "small", "casos": casos}, f, ensure_ascii=False, indent=1) print(f" guardado: {len(casos)} transcripciones en {BANCO}") return 0 def carga(): try: with open(BANCO, encoding="utf-8") as f: return json.load(f)["casos"] except (OSError, json.JSONDecodeError, KeyError): return [] def mide(detalle=False) -> int: casos = carga() if not casos: print(" no hay banco: crealo con --crear") return 1 cat = Catalogo([CATALOGO]) bien = 0 oye_mal, no_relaciona, otra, ruido = [], [], [], [] for c in casos: t = c["oido"] if es_ruido(t): ruido.append(c) continue a, _ = cat.busca(t) if a is not None and a.id == c["id"]: bien += 1 continue p = difflib.SequenceMatcher(None, normaliza(t), normaliza(c["frase"])).ratio() if p < 0.75: oye_mal.append((c, t, p)) elif a is None: no_relaciona.append((c, t, p)) else: otra.append((c, t, a.id)) n = len(casos) print(f" {n} frases · acierto {100*bien/n:.1f} %\n") print(f" {'que pasa':28s} {'n':>4s} {'%':>7s}") print(" " + "-" * 44) for nombre, k in [("bien", bien), ("oye bien, NO RELACIONA", len(no_relaciona)), ("OYE MAL la frase", len(oye_mal)), ("oye bien, OTRA accion", len(otra)), ("lo tira por ruido", len(ruido))]: print(f" {nombre:28s} {k:4d} {100*k/n:6.1f} %") if detalle: print("\n --- oye bien pero no relaciona ---") for c, t, p in no_relaciona: print(f" {c['id']:24s} dijo: {c['frase']}") print(f" {'':24s} oyo: {t} ({p:.2f})") print("\n --- se la lleva otra accion (lo peor: ejecuta lo que no es) ---") for c, t, o in otra: print(f" {c['id']:24s} -> {o} oyo: {t}") return 0 def main() -> int: p = argparse.ArgumentParser() p.add_argument("--crear", action="store_true") p.add_argument("--mide", action="store_true") p.add_argument("--fallos", action="store_true") p.add_argument("n", nargs="?", type=int, default=150) a = p.parse_args() if a.crear: return crear(a.n) return mide(detalle=a.fallos) if __name__ == "__main__": sys.exit(main())