#!/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())
