#!/usr/bin/env python3 """Por donde va la migracion, en la terminal. ./estado.py las barras ./estado.py -v y que falta exactamente en cada hito ./estado.py --rapido sin ejecutar nada, solo mirar ficheros El porcentaje NO se escribe a mano: sale de comprobaciones de verdad —que un fichero exista, que una funcion este, que una prueba pase—. Un contador que se actualiza a mano miente a la semana, y ademas es justo lo que uno quiere que no mienta: si dice 60 % es porque tres de cinco comprobaciones pasan ahora mismo. Las que cuestan tiempo (arrancar whisper, hablar con ollama) van marcadas como lentas y se saltan con --rapido. """ import argparse import os import subprocess import sys AQUI = os.path.dirname(os.path.abspath(__file__)) RAIZ = os.path.dirname(AQUI) VENV = os.path.join(AQUI, "venv", "bin", "python") # ------------------------------------------------------------- comprobantes def hay(*trozos) -> bool: return os.path.exists(os.path.join(AQUI, *trozos)) def dentro(fichero, texto) -> bool: try: with open(os.path.join(AQUI, fichero), encoding="utf-8") as f: return texto in f.read() except OSError: return False def corre(orden, espera=90) -> bool: try: r = subprocess.run(orden, cwd=AQUI, capture_output=True, timeout=espera) return r.returncode == 0 except (OSError, subprocess.TimeoutExpired): return False def modulo(nombre) -> bool: return corre([VENV if os.path.exists(VENV) else sys.executable, "-c", f"import {nombre}"], espera=30) def sin_comentarios(fichero, texto) -> bool: """Si el texto aparece en una linea que NO es un comentario.""" try: with open(os.path.join(AQUI, fichero), encoding="utf-8") as f: for l in f: limpia = l.strip() if limpia and not limpia.startswith(("#", "//", "*")) and texto in limpia: return True except OSError: pass return False def catalogo_tiene(n) -> bool: try: import json with open(os.path.join(AQUI, "datos", "acciones.json"), encoding="utf-8") as f: return len(json.load(f)["acciones"]) >= n except Exception: return False # ---------------------------------------------------------------- los hitos # # Cada comprobacion es (que se comprueba, como, si es lenta). El orden dentro # de cada hito va de lo mas basico a lo mas fino, que es como se construyo. HITOS = [ ("la boca", "hablar por los altavoces", [ ("el modulo esta", lambda: hay("boca", "voz.py"), False), ("hay tres voces o mas", lambda: dentro("boca/voz.py", '"claude"'), False), ("cache de frases por sha1", lambda: dentro("boca/voz.py", "_clave"), False), ("sabe si esta sonando", lambda: dentro("boca/voz.py", "def hablando"), False), ("habla de verdad", lambda: corre( [VENV, "jarvis.py", "--di", "prueba"], 60), True), ]), ("el oido", "escuchar y transcribir", [ ("captura con pw-record", lambda: dentro("oido/captura.py", "pw-record"), False), ("detector de voz Silero", lambda: modulo("pysilero_vad"), False), ("cliente de whisper-server", lambda: hay("oido", "whisper.py"), False), ("filtro de alucinaciones", lambda: dentro("oido/whisper.py", "def es_ruido"), False), ("guarda contra el eco", lambda: dentro("oido/captura.py", "boca.hablando"), False), ("whisper arranca y transcribe", lambda: corre( [VENV, "-c", "import sys;sys.path.insert(0,'.');from oido.whisper import Oido;" "o=Oido();import sys as s;s.exit(0 if o.arranca() else 1)"], 90), True), ]), ("las manos", "ejecutar las 150 acciones", [ ("el emparejador copiado", lambda: hay("manos", "acciones.py"), False), ("las tareas en paralelo", lambda: hay("manos", "tareas.py"), False), ("el catalogo entero", lambda: catalogo_tiene(150), False), ("una sola puerta para ejecutar", lambda: dentro("manos/__init__.py", "class Manos"), False), ("compara tambien por sonido", lambda: hay("manos", "fonetica.py"), False), ("banco fijo para medir", lambda: hay("datos", "banco.json"), False), ("las pruebas en verde", lambda: corre( [VENV, "pruebas/prueba_manos.py"], 120), True), ]), ("el cerebro", "pensar lo que el catalogo no cubre", [ ("cliente de ollama", lambda: hay("cerebro", "ollama.py"), False), ("dos herramientas, no una", lambda: dentro("cerebro/ollama.py", "comando_de_shell"), False), ("no puede inventarse datos", lambda: dentro("cerebro/ollama.py", "REGLA QUE NO SE ROMPE"), False), ("tope de vueltas", lambda: dentro("cerebro/ollama.py", "VUELTAS"), False), ("cadena catalogo-primero", lambda: dentro("jarvis.py", "def atiende"), False), ("ollama contesta", lambda: corre( ["curl", "-sf", "-m", "10", "http://127.0.0.1:11434/api/tags"], 20), True), ]), ("la palabra", "que le llamen JARVIS", [ ("el centinela existe", lambda: hay("oido", "despierta.py"), False), ("se llama JARVIS", lambda: dentro("oido/despierta.py", 'NOMBRE = "jarvis"'), False), ("aguanta como lo oye Whisper", lambda: dentro("oido/despierta.py", "PARECIDAS"), False), ("la orden va en la misma frase", lambda: dentro("oido/despierta.py", "def parte_el_nombre"), False), ("conectado al bucle de voz", lambda: dentro("jarvis.py", "centinela.filtra"), False), # El que de verdad importaba: sin el prompt del nombre, Whisper nunca # escribe JARVIS y el centinela no despierta jamas. ("prompt del nombre para Whisper", lambda: dentro("oido/whisper.py", "PROMPT_NOMBRE"), False), ("boton para dormirlo y despertarlo", lambda: dentro("cara/panel.py", 'dato == "sueno"'), False), ("prueba de voz de punta a punta", lambda: hay("pruebas", "prueba_voz.py"), False), ("y la palabra va APAGADA por defecto", lambda: dentro("jarvis.py", "activo=a.palabra"), False), ]), ("la cara", "el panel", [ ("el HUD copiado", lambda: hay("cara", "hud", "index.html"), False), ("servidor http y websocket", lambda: hay("cara", "servidor.py"), False), # El WebSocket vive en puente.js, no en hud.js: hud.js no se toca a # proposito y el puente suplanta window.webkit por debajo. ("puente por WebSocket", lambda: dentro("cara/hud/puente.js", "WebSocket"), False), ("y hud.js sigue sin tocarse", lambda: dentro("cara/hud/hud.js", "messageHandlers"), False), ("el panel se conecta al nucleo", lambda: dentro("cara/servidor.py", "async"), False), # Ojo con las comprobaciones flojas: esta buscaba la palabra "tarea" en # servidor.py y pasaba porque salia en un COMENTARIO, con las tarjetas # sin conectar. Una barra que miente es peor que no tener barra. ("tarjetas de tareas en marcha", lambda: dentro("cara/panel.py", "GestorTareas"), False), ("la consola lleva la conversacion", lambda: dentro("cara/hud/hud.js", "aConsolaDicho"), False), ]), ("el dialogo", "que pregunte en vez de recitar", [ ("las preguntas salen del catalogo", lambda: hay("manos", "preguntar.py"), False), ("pulsar pregunta lo que falta", lambda: dentro("cara/panel.py", "_pregunta("), False), ("la respuesta hablada la recoge", lambda: dentro("jarvis.py", "panel.responde"), False), ("lo destructivo confirma", lambda: dentro("cara/panel.py", "accion.confirmar"), False), ("y el panel enseña que espera", lambda: dentro("cara/hud/hud.js", "preguntando"), False), ("la prueba pasa", lambda: corre([VENV, "pruebas/prueba_dialogo.py"], 120), True), ]), ("el candado", "que no obedezca a cualquiera", [ ("el candado en el HUD", lambda: dentro("cara/hud/index.html", "candado"), False), ("lo atiende el nucleo", lambda: dentro("cara/panel.py", "_intenta_desbloquear"), False), ("valida con la contrasena del sistema", lambda: dentro("cara/panel.py", "GestorRoot"), False), ("con el candado puesto no ejecuta", lambda: dentro("cara/panel.py", "no hay ordenes"), False), ("y la VOZ tampoco obedece", lambda: dentro("jarvis.py", "(bloqueado)"), False), ("la prueba pasa", lambda: corre([VENV, "pruebas/prueba_candado.py"], 120), True), ]), ("el saber", "conocer COFRE entero", [ ("indexador de COFRE", lambda: hay("saber", "indexa.py"), False), ("busqueda por significado", lambda: hay("saber", "busca.py"), False), ("indice construido", lambda: hay("datos", "saber.jsonl"), False), ("el cerebro lo consulta antes de responder", lambda: dentro( "cerebro/ollama.py", "def _saber"), False), ("filtra el codigo de terceros", lambda: dentro("saber/indexa.py", "IGNORA"), False), ("indexa la documentacion de Linux", lambda: hay("saber", "sistema.py"), False), ("y los servidores SSH", lambda: dentro("saber/sistema.py", "def servidores"), False), ("el cerebro puede ejecutar como root", lambda: dentro("manos/__init__.py", "necesita_root"), False), ("root ligado al candado", lambda: dentro("cara/panel.py", "root.recuerda"), False), ]), ("el pipeline", "aprender del uso", [ ("diario de lo que oye y hace", lambda: hay("manos", "diario.py"), False), ("apunta la VIA de cada frase", lambda: dentro("jarvis.py", 'anota(texto, "ruido")'), False), ("y tambien los aciertos", lambda: dentro("jarvis.py", '"catalogo", accion.id'), False), ("herramienta para revisarlo", lambda: hay("mejora.py"), False), ("propone, NO commitea solo", lambda: dentro("mejora.py", "Esto NO cambia nada"), False), ("mide contra el banco fijo", lambda: hay("datos", "banco.json"), False), ]), ("paridad", "usarlo a diario", [ ("icono naranja", lambda: hay("jarvis-naranja.svg"), False), ("icono instalado", lambda: os.path.exists(os.path.expanduser( "~/.local/share/icons/jarvis-nucleo.png")), False), ("lanzador propio", lambda: hay("arranca.sh"), False), ("ventana propia, no pestaña", lambda: hay("ventana.sh"), False), # Se miran las lineas de CODIGO, no el fichero entero: el comentario que # explica por que ya no se usa xdg-open contiene la palabra, y una # comprobacion ingenua se cree que sigue ahi. Es la tercera vez que la # barra casi miente por buscar texto suelto en vez de codigo. ("una sola forma de abrirla", lambda: not sin_comentarios( "arranca.sh", "xdg-open"), False), ("la barra de tareas la reconoce", lambda: "StartupWMClass" in open( os.path.expanduser("~/.local/share/applications/jarvis-nucleo.desktop")).read(), False), ("entrada en el escritorio", lambda: os.path.exists(os.path.expanduser( "~/.local/share/applications/jarvis-nucleo.desktop")), False), ("Newelle congelado y aparte", lambda: corre( ["git", "-C", RAIZ, "rev-parse", "jarvis-newelle-final"], 15), False), ]), ] # ------------------------------------------------------------------ pintarlo ANCHO = 34 def color(t, c): return f"\033[{c}m{t}\033[0m" if sys.stdout.isatty() else t def barra(pct, hecho, total): lleno = round(ANCHO * pct / 100) # verde entero, amarillo a medias, gris sin empezar c = "32" if pct == 100 else ("33" if pct > 0 else "90") return color("█" * lleno, c) + color("·" * (ANCHO - lleno), "90") def main() -> int: p = argparse.ArgumentParser() p.add_argument("-v", "--detalle", action="store_true") p.add_argument("--rapido", action="store_true", help="sin ejecutar nada, solo mirar ficheros") a = p.parse_args() if not os.path.exists(VENV): print(color(" aviso: no hay venv; algunas comprobaciones no se pueden hacer\n", "33")) print() print(color(" JARVIS · migracion a nucleo propio", "1")) print() hechas = totales = 0 for i, (nombre, para_que, comprobaciones) in enumerate(HITOS, 1): activas = [c for c in comprobaciones if not (a.rapido and c[2])] pasan = [] for texto, prueba, lenta in activas: try: ok = bool(prueba()) except Exception: ok = False pasan.append((texto, ok, lenta)) n = sum(1 for _, ok, _ in pasan if ok) pct = 100 * n / len(pasan) if pasan else 0 hechas += n totales += len(pasan) marca = color("✓", "32") if pct == 100 else (" " if pct == 0 else color("·", "33")) print(f" {marca} {i}. {nombre:12s} {barra(pct, n, len(pasan))} " f"{pct:3.0f}% {n}/{len(pasan)} {color(para_que, '90')}") if a.detalle: for texto, ok, lenta in pasan: s = color(" ok ", "32") if ok else color(" --", "31") print(f" {s} {texto}" + (color(" (lenta)", "90") if lenta else "")) print() print() total_pct = 100 * hechas / totales if totales else 0 print(f" {'TOTAL':14s} {barra(total_pct, hechas, totales)} " f"{total_pct:3.0f}% {hechas}/{totales}") print() if a.rapido: print(color(" (rapido: sin las comprobaciones que ejecutan cosas)\n", "90")) return 0 if __name__ == "__main__": sys.exit(main())