#!/usr/bin/env python3 """Un banco de pruebas FIJO para medir si el reconocimiento mejora. ## El problema que resuelve La mejora nocturna medía así: decía las 150 frases con voces, velocidades y ruido ALEATORIOS, las transcribía, y comparaba el porcentaje con el de la ronda anterior. Nueve horas y 39 rondas después: ruido de medida (desviacion): ±1,9 puntos mejora acumulada en la noche: +1,45 puntos El ruido era MAYOR que la mejora. Con eso, la pregunta que el bucle necesita contestar —"¿este alias mejora o empeora?"— se decidía a cara o cruz, y por eso en nueve horas aplicó cuatro alias y no dejó ni un commit. ## Cómo lo arregla Se graba el audio UNA vez, con semillas fijas, y se guardan las TRANSCRIPCIONES. A partir de ahí, medir el catálogo es volver a pasar el emparejador sobre esas mismas transcripciones guardadas: - determinista: la misma entrada siempre, así que dos medidas son comparables - instantáneo: sin TTS ni Whisper, milisegundos en vez de cinco minutos - sensible: se nota una frase de diferencia, no hacen falta cuatro puntos Lo que se pierde: deja de explorar ruidos nuevos. Por eso el banco se REHACE de vez en cuando (--crear), y entre medias se usa fijo para decidir. banco.py --crear [--muestras 2] graba y transcribe (lento, una vez) banco.py --medir puntúa el catálogo actual (instantáneo) banco.py --fallos enseña qué se sigue fallando """ import argparse import collections import importlib.util import json import os import random import subprocess import sys import tempfile RAIZ = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) CATALOGO = os.path.join(RAIZ, "newelle", "data", "acciones", "acciones.json") MOTOR = os.path.join(RAIZ, "newelle", "src", "utility", "acciones.py") BANCO = os.path.join(RAIZ, "muestras", "banco.json") PIPER = os.path.join(RAIZ, "config", "jarvis-piper.sh") MODELO_WHISPER = os.path.expanduser( "~/.var/app/io.github.qwersyk.Newelle/config/models/whisper/whisper.cpp/models/ggml-small.bin") BIN_WHISPER = os.path.join(RAIZ, "voz", "whisper-cuda", "whisper.cpp", "build", "bin", "whisper-server") # Fijas a proposito: son las que hacen 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"] VELOCIDADES = [0.9, 1.0, 1.15] SEMILLA = 20260815 def motor(): spec = importlib.util.spec_from_file_location("acciones", MOTOR) m = importlib.util.module_from_spec(spec) spec.loader.exec_module(m) return m def crear(muestras): m = motor() cat = m.Catalogo([CATALOGO]) rnd = random.Random(SEMILLA) casos = [] for a in cat.acciones: if not a.frases: continue for _ in range(muestras): casos.append({ "id": a.id, "frase": rnd.choice(a.frases), "voz": rnd.choice(VOCES), "velocidad": rnd.choice(VELOCIDADES), }) print(f" {len(casos)} clips a grabar y transcribir...", flush=True) puerto = 8993 servidor = subprocess.Popen( [BIN_WHISPER, "-m", MODELO_WHISPER, "-t", "8", "--host", "127.0.0.1", "--port", str(puerto), "-l", "es", "--no-gpu"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, env={**os.environ, "LD_LIBRARY_PATH": "/usr/local/cuda/lib64"}) import time time.sleep(10) import urllib.request hechos = 0 with tempfile.TemporaryDirectory() as tmp: for c in casos: wav = os.path.join(tmp, "c.wav") entorno = {**os.environ, "JARVIS_PIPER_VOZ": c["voz"], "JARVIS_PIPER_TONO": "1.0"} r = subprocess.run([PIPER, wav, c["frase"]], env=entorno, capture_output=True, timeout=60) if not os.path.exists(wav) or os.path.getsize(wav) == 0: continue # la velocidad se aplica al vuelo, sin tocar el tono wav16 = os.path.join(tmp, "c16.wav") subprocess.run(["ffmpeg", "-y", "-loglevel", "error", "-i", wav, "-filter:a", f"atempo={c['velocidad']}", "-ar", "16000", "-ac", "1", wav16], capture_output=True, timeout=60) try: with open(wav16, "rb") as f: datos = f.read() lim = "----b" cuerpo = (f"--{lim}\r\nContent-Disposition: form-data; " f'name="file"; filename="c.wav"\r\n' f"Content-Type: audio/wav\r\n\r\n").encode() + datos + \ f"\r\n--{lim}--\r\n".encode() req = urllib.request.Request( f"http://127.0.0.1:{puerto}/inference", data=cuerpo, headers={"Content-Type": f"multipart/form-data; boundary={lim}"}) with urllib.request.urlopen(req, timeout=120) as resp: c["oido"] = json.loads(resp.read().decode()).get("text", "").strip() hechos += 1 if hechos % 50 == 0: print(f" {hechos}/{len(casos)}", flush=True) except Exception as e: c["oido"] = "" servidor.terminate() casos = [c for c in casos if c.get("oido")] os.makedirs(os.path.dirname(BANCO), exist_ok=True) json.dump({"semilla": SEMILLA, "casos": casos}, open(BANCO, "w"), ensure_ascii=False, indent=1) print(f" banco guardado: {len(casos)} transcripciones en {BANCO}") return 0 def cargar(): try: return json.load(open(BANCO))["casos"] except Exception: return [] def medir(detalle=False): casos = cargar() if not casos: print("no hay banco; crealo con --crear") return 1 m = motor() cat = m.Catalogo([CATALOGO]) aciertos, fallos = 0, [] for c in casos: a, _ = cat.busca(c["oido"]) if a is not None and a.id == c["id"]: aciertos += 1 else: fallos.append((c["id"], c["oido"], a.id if a else None)) pct = 100.0 * aciertos / len(casos) print(f" acierto {pct:.2f} % ({aciertos}/{len(casos)})") if detalle: cuenta = collections.Counter(f[0] for f in fallos) print(f"\n las acciones que mas se fallan:") for ident, n in cuenta.most_common(12): ejemplo = next(f[1] for f in fallos if f[0] == ident) print(f" {n:3d} {ident:22s} p.ej. se oyo: \"{ejemplo[:44]}\"") return 0 def main(): p = argparse.ArgumentParser() p.add_argument("--crear", action="store_true") p.add_argument("--medir", action="store_true") p.add_argument("--fallos", action="store_true") p.add_argument("--muestras", type=int, default=2) a = p.parse_args() if a.crear: return crear(a.muestras) return medir(detalle=a.fallos) if __name__ == "__main__": sys.exit(main())