AOP = Agent Orchestrator Panel: un panel para orquestar agentes de IA (Claude Code y Codex) desde tu propia PC, con transcriptor de WhatsApp y Telegram y asistente de voz.
Dos cosas en una, corriendo en tu PC:
- Transcriptor: le mandás audios o videos (por Telegram, WhatsApp o arrastrándolos) y te devuelve el texto y un resumen. La transcripción se hace en tu placa de video: el audio no se sube a ningún servicio.
- Asistente de voz: decís "Venus" y le pedís cosas a la PC (abrir programas, poner música, dictar donde esté el cursor), o decís "Laura" y charlás con Claude o ChatGPT por voz.
Todo se maneja desde un panel web en http://localhost:8750.
Son unos 30-40 minutos, casi todo esperando descargas. Andá en orden: cada paso termina con "Anduvo si…", y si eso no pasa, no sigas al siguiente (mirá el paso 16).
Todos los comandos se pegan en Anaconda PowerShell Prompt (se instala en el paso 2), uno por vez.
- Windows 10 u 11. No anda en Mac ni en Linux: usa partes de Windows para el audio y las ventanas.
- Placa de video NVIDIA (de 6 GB o más de memoria, idealmente). Sin NVIDIA no transcribe.
- Unos 10 GB libres en disco: 6 para los modelos y el resto para los programas.
- Micrófono, si vas a usar el asistente de voz.
- Opcional, para hablar con Laura: cuenta de Claude (plan Pro o Max) o de ChatGPT Plus.
Abrí PowerShell (tecla Windows → escribí "PowerShell" → Enter) y pegá:
winget install -e --id Git.Git
winget install -e --id Anaconda.Miniconda3
winget install -e --id Gyan.FFmpeg
winget install -e --id OpenJS.NodeJS.LTS
winget install -e --id Google.ChromeSi alguno dice que ya está instalado, está bien. Cerrá esa ventana cuando termine.
Desde ahora usá Anaconda PowerShell Prompt: tecla Windows → escribí "Anaconda PowerShell" → Enter.
Anduvo si en Anaconda PowerShell Prompt estos tres comandos muestran un número de versión:
git --version
ffmpeg -version
node --versionY con nvidia-smi tiene que aparecer el nombre de tu placa.
cd $HOME
git clone https://github.com/JonesMartin1/panel-de-agentes-aop.git wpp-transcriptor
cd wpp-transcriptorAnduvo si existe la carpeta wpp-transcriptor en tu carpeta de usuario, con panel.py adentro.
conda create -n wpp python=3.11 -y
conda activate wpp
pip install -r requirements.txt
playwright install chromiumTarda varios minutos. Si pip falla con un error de certificado (pasa en redes que
revisan el tráfico, como algunas oficinas o antivirus), repetilo así:
pip install -r requirements.txt --trusted-host pypi.org --trusted-host files.pythonhosted.orgAnduvo si termina con Successfully installed … y sin líneas rojas de ERROR.
⚠ Cada vez que abras una ventana nueva, antes de nada: conda activate wpp y
cd $HOME\wpp-transcriptor. Si te olvidás, vas a ver errores de "No module named…".
copy .env.ejemplo .env
notepad .envSe abre el Bloc de notas. Para arrancar alcanza con dos líneas:
WPP_MODELOS=→ una carpeta con lugar para los modelos, por ejemploC:\IA\modelos. No hace falta crearla.GEMINI_API_KEY=→ la clave de Gemini, que es gratis: entrá a https://aistudio.google.com/apikey, tocá "Create API key" y pegala.
Guardá (Ctrl+S) y cerrá. Todo lo demás del archivo es opcional: lo que quede vacío simplemente apaga esa parte, no rompe nada. Cada línea tiene su explicación.
⚠ El .env es solo tuyo: no lo mandes a nadie ni lo subas a GitHub (ya está excluido).
El proyecto no baja los modelos solo: si no están, no arranca. Pegá esta línea tal cual.
Baja los cuatro modelos de Whisper, unos 5 GB, a la carpeta que pusiste en WPP_MODELOS:
python -c "import truststore; truststore.inject_into_ssl(); from faster_whisper import download_model; from app.rutas import DIR_MODELOS; [print(download_model(m, cache_dir=str(DIR_MODELOS))) for m in ('large-v3', 'large-v3-turbo', 'small', 'tiny')]"Después, la voz que habla (Piper). Cambiá C:\IA\modelos en la primera línea si en el
.env pusiste otra carpeta:
$m = "C:\IA\modelos"
New-Item -ItemType Directory -Force "$m\piper" | Out-Null
$u = "https://huggingface.co/rhasspy/piper-voices/resolve/main/es/es_AR/daniela/high/es_AR-daniela-high.onnx"
Invoke-WebRequest $u -OutFile "$m\piper\es_AR-daniela-high.onnx"
Invoke-WebRequest "$u.json" -OutFile "$m\piper\es_AR-daniela-high.onnx.json"Anduvo si el primer comando imprime cuatro rutas (una por modelo) y en la carpeta
piper quedan los dos archivos es_AR-daniela-high.
Grabá cualquier cosa con la Grabadora de sonidos de Windows (o usá un audio de WhatsApp que tengas guardado) y pasale la ruta:
python -m app.nucleo.transcribir "C:\ruta\a\tu\audio.m4a" --analizarTip: podés arrastrar el archivo a la ventana y se escribe la ruta sola.
Anduvo si aparece el texto de lo que dijiste y después un análisis. Si llegaste acá, el corazón del proyecto funciona. Lo que sigue es opcional.
python panel.pyDejá esa ventana abierta (si la cerrás, se apaga todo) y abrí en Chrome http://localhost:8750.
- Tocá Probar micrófono y hablá: te tiene que dar un veredicto.
- Prendé Voz. Tarda unos 20 segundos; cuando está lista dice "Listo, te escucho".
- Decí "Venus, qué hora es".
Anduvo si te contesta la hora en voz alta.
Para Claude:
npm install -g @anthropic-ai/claude-code
claudeLa primera vez te abre el navegador para iniciar sesión con tu cuenta de Claude. Cuando
entres, escribí /exit.
Para ChatGPT, lo mismo con npm install -g @openai/codex y codex login.
En el .env, CLAUDE_VOZ_MODO=lectura hace que Laura solo lea y busque. Con
completo también puede escribir archivos y ejecutar comandos en tu PC: cambialo solo cuando
te sientas cómodo.
Anduvo si después de reiniciar la Voz desde el panel, "Laura, contame un chiste" te contesta.
- En Telegram, buscá @BotFather, escribile
/newboty seguí los pasos. Te da un token: pegalo enTELEGRAM_TOKEN=del.env. - En el panel prendé Servidor, buscá tu bot en Telegram y escribile
/start: te contesta con tu id (un número). - Poné ese número en
TELEGRAM_DEST_ID=y enTELEGRAM_ALLOWED_IDS=, y reiniciá el Servidor desde el panel.
⚠ TELEGRAM_ALLOWED_IDS es quien puede hablarle a Laura, y Laura puede ejecutar
comandos en tu PC. Poné solo tu id.
Anduvo si le mandás un audio al bot y te devuelve la transcripción.
WhatsApp pide bastante más: un servicio que conecte tu número (WaSenderAPI, pago, o Evolution API en un servidor propio) y un túnel de Cloudflare para que tu PC reciba los mensajes. Si no lo necesitás, salteálo: con Telegram alcanza.
La idea es un cuaderno compartido entre vos y las IAs: una carpeta de archivos de
texto (.md) que vos leés y ordenás con Obsidian, y donde Laura y Claude anotan qué se
hizo en cada proyecto, qué se decidió, qué quedó pendiente y qué está roto. Así, cuando
volvés a un proyecto un mes después, el estado está escrito y no hay que acordarse.
1. Instalar Obsidian (es gratis):
winget install -e --id Obsidian.Obsidian2. Crear el cuaderno: abrí Obsidian → Create new vault → nombre notas → elegí dónde
guardarlo, por ejemplo C:\IA\notas. Adentro creá dos notas:
-
Proyectos.md: el índice, con un renglón por proyecto enlazado así:[[wpp-transcriptor]]. -
Convenciones.md: las reglas de formato. Para empezar alcanza con esto: cada nota de proyecto empieza con estos tres datos, prosa corta, fechas completas (nunca "ayer") y un## Bugsal final:--- estado: activo actualizado: 2026-09-24 cliente: propio ---
estadopuede seractivo,pausado,produccionoarchivado.
3. Que Claude lo use: abrí notepad $HOME\.claude\CLAUDE.md (si no existe, se crea) y
pegá esto al final, cambiando la ruta por la de tu cuaderno:
# Cuaderno de notas (Obsidian)
`C:\IA\notas` es mi cuaderno compartido: notas .md enlazadas con [[dobles corchetes]].
La nota central es Proyectos.md y el formato está en Convenciones.md.
Al terminar algo importante (una función, un arreglo grande, una decisión, algo que se
rompió), actualizá la nota del proyecto: prosa corta, con la fecha, y refrescá `actualizado`.
Si el proyecto no tiene nota, creala y enlazala desde Proyectos.md.
Los bugs van en `## Bugs` al final: una viñeta por bug con fecha, estado y qué se ve.
Nada de claves, tokens ni datos de clientes en el cuaderno.Ese archivo es tu configuración personal de Claude: vale para todos tus proyectos, no solo para este.
4. Que Laura lo alcance: Laura solo puede tocar las carpetas que figuran en CARPETAS,
en app/voz/claude_voz.py. Agregá ahí la de tu cuaderno, por ejemplo r"C:\IA\notas", y
reiniciá la Voz desde el panel.
Anduvo si después de terminar algo le decís a Claude "anotalo en el cuaderno" y en Obsidian aparece la nota del proyecto con lo que se hizo.
graphify arma un mapa del proyecto: qué archivo llama a cuál, cómo se conectan las piezas y por qué se decidió cada cosa. Sirve para que Claude conteste preguntas como "¿qué pasa cuando llega un audio?" mirando el mapa en vez de leer archivos enteros: responde más rápido y gasta mucho menos. Necesita Claude instalado (paso 9).
1. Instalarlo (una sola vez en la PC):
winget install -e --id astral-sh.uvCerrá y volvé a abrir la ventana, y después:
uv tool install graphifyy
graphify installgraphify install le agrega a Claude el comando /graphify.
2. Armar el mapa de este proyecto, parado en la carpeta del proyecto:
claudeY adentro de Claude escribí /graphify .. La primera vez tarda unos minutos. Queda en la
carpeta graphify-out/, que no se sube a GitHub: cada uno arma el suyo.
3. Que se actualice solo con cada commit:
graphify hook installDesde ahí, cada commit actualiza el mapa del código solo, sin gastar nada. Los documentos
largos (README.md, INTERFAZ.md) no se actualizan solos: cuando cambien mucho, corré
/graphify --update desde Claude.
El archivo .graphifyignore ya deja afuera del mapa los audios, videos e imágenes:
graficarlos obligaría a transcribirlos y le pelearía la placa de video al asistente.
Anduvo si existe graphify-out\graph.json y, en Claude, /graphify query "qué pasa cuando llega un audio de Telegram" te contesta nombrando archivos del proyecto.
Las skills le enseñan a Claude a hacer tareas concretas siempre de la misma manera. Se usan
escribiendo /nombre en Claude, o solas cuando reconoce el pedido. Vienen en la carpeta
skills/ del proyecto:
/notas: deja al día la nota del proyecto en tu cuaderno de Obsidian (paso 11)./avisar: te manda un mensaje al celular cuando algo termina o se rompe./buzon: lo que le decís a Laura por voz le llega a la sesión de Claude./pizarra: pega papelitos en el pizarrón del panel./paralelo: prepara un proyecto para trabajar con 2-3 agentes a la vez./grill-me: te interroga sobre un plan antes de ejecutarlo./leeme: le pone voz a un informe./3d: convierte una imagen en un modelo 3D (necesita Hunyuan3D, pesado).
Para instalarlas todas, parado en la carpeta del proyecto:
New-Item -ItemType Directory -Force $HOME\.claude\skills | Out-Null
Copy-Item -Recurse -Force .\skills\* $HOME\.claude\skills\
Remove-Item $HOME\.claude\skills\README.mdavisar y buzon necesitan un paso más de configuración (el bot de Telegram y un hook):
está explicado en skills/README.md, junto con prompt-master, que se baja
de su autor original.
Anduvo si en una sesión nueva de Claude escribís / y aparecen notas, avisar,
grill-me y las demás.
Si las skills del repo cambian, las tuyas no cambian solas: después de un git pull, volvé a
correr el Copy-Item de arriba.
conda activate wpp
cd $HOME\wpp-transcriptor
git pull
pip install -r requirements.txtCerrá el panel y volvé a abrirlo (python panel.py) para que tome los cambios.
El proyecto nació en una máquina concreta y algunas partes necesitan que las configures:
| Qué | Qué hacer |
|---|---|
| El servicio "Túnel" del panel | Necesita tu propio túnel de Cloudflare: copiá wpp-config.ejemplo.yml como wpp-config.yml y completalo. Solo hace falta para WhatsApp y para hablarle a Laura desde el celular |
El acceso directo "Servidor IA" y launcher.vbs |
Apuntan a la carpeta original. Usá python panel.py, o editá launcher.vbs con tu ruta |
Los .bat de lanzadores/ |
Tienen rutas de ejemplo (D:\IA\...). Cambialas por las tuyas antes de usarlos |
| El generador 3D | Necesita Hunyuan3D-2 instalado aparte (ver la skill 3d) |
| Las carpetas que Laura puede tocar | De fábrica, la carpeta que contiene al proyecto. Sumá las tuyas en CARPETAS, en app/voz/claude_voz.py |
| La sensibilidad del micrófono | Está calibrada para unos auriculares en particular. Si se dispara solo o no te escucha, abrí un issue |
- Fijate que la ventana tenga
(wpp)al principio de la línea. Si no,conda activate wpp. - Mirá la carpeta
logs/:voz.log,telegram.logywebhook.logdicen qué pasó. - Casi todo lo que "no anda" es una clave vacía en el
.envo un modelo que no se bajó (paso 6). - Si tenés Claude instalado (paso 9), abrí
claudeen la carpeta del proyecto y decile: "Leé el README y ayudame con este error", y pegale el error.
Lo que sigue es el manual de trabajo del proyecto: la arquitectura, las decisiones y las
trampas que ya costaron caro. Para usar el proyecto no hace falta leerlo; para modificarlo, sí.
Está escrito en primera persona del día a día en que se armó, en una laptop con Windows 11 y
una RTX 4070 de 8 GB: las rutas D:\IA\... que aparecen son las de esa máquina.
Dos grandes piezas que conviven:
- Transcriptor de WhatsApp/Telegram: recibe audios y videos, los transcribe con Whisper local (GPU) y los analiza con Gemini.
- Asistente de voz ("Venus" y "Laura"): manos libres estilo Alexa para controlar la PC, dictar texto y conversar con Claude.
Entorno Python: el conda
wppde la instalación. GPU CUDA para Whisperturbo. Si tu red intercepta HTTPS (antivirus, proxy): conda/pip/Python necesitan el cert store de Windows (truststore/--trusted-host).
| Cómo | Qué hace |
|---|---|
Acceso directo "Servidor IA" (escritorio) → launcher.vbs |
Lanza panel.py --auto: prende TODO (servidor + voz). Al estar lista, la voz dice "Listo, te escucho" |
| Panel web | http://localhost:8750 — tarjetas Servidor / Voz / Conversación en vivo, con botones Prender/Apagar |
Nada de esto arranca solo con Windows. Verificado: no hay entradas del proyecto en las carpetas de Inicio, ni en las claves
Rundel registro, ni en tareas programadas. Si prendés la máquina y no tocás el acceso directo, el asistente no existe. (Sí arrancan solos, por su cuenta, Ollama y el túnel de n8n.)
En la tarjeta de Voz hay un botón "Pausar escucha". Deja de escuchar "Venus"/"Laura" pero no descarga los modelos, así que pausar y volver son instantáneos — contra los ~20 s que tarda prender la voz de cero. F9 y los atajos siguen funcionando.
Cómo está hecho: el panel crea o borra pausa_escucha.flag (definido en app/rutas.py) y
voz.py lo lee cada medio segundo en un hilo aparte. A propósito no se consulta el disco
dentro del callback de audio, que corre decenas de veces por segundo.
El micrófono se abre en WASAPI modo compartido, así que Discord, WhatsApp y los juegos pueden usarlo al mismo tiempo: el asistente no te lo bloquea. Lo que sí conviene es pausarlo, para que no se autodispare con la conversación ni le pelee la GPU al juego.
| Servicio | Archivo | Qué es |
|---|---|---|
| Bot de Telegram | bot_telegram.py |
Recibe audios/videos por Telegram, responde transcripción + análisis. Límite API: 20 MB (avisa). Los mensajes de texto van a Laura (misma sesión que por voz): el bot deja la pregunta en un buzón de archivos (app/voz/buzon.py) y la atiende voz.py, que es quien tiene el proceso claude vivo — por eso hablar con Laura por Telegram necesita el servicio de voz prendido. Dos candados independientes en el .env: TELEGRAM_EQUIPO_IDS (quién puede transcribir — vacío = abierto, pensado para compartirlo con el equipo de devs) y TELEGRAM_ALLOWED_IDS (quién puede hablarle a Laura — SIEMPRE solo Martín: ella ejecuta comandos en la máquina, así que sumar gente a un candado nunca abre el otro) |
| Webhook WhatsApp | webhook_wasender.py |
Recibe webhooks de WasenderAPI (puerto 8080), desencripta media, transcribe. También sirve /subir: página web para subir audios largos (>20 MB) directo; resultado va a Telegram |
| Túnel Cloudflare | cloudflared |
Expone el webhook en tu hostname (el de wpp-config.yml) (subidas por túnel: tope ~100 MB; por LAN http://IP:8080/subir sin tope). El mismo túnel también expone /laura-voz (ver "Laura por llamada" abajo), como una ruta aparte del mismo hostname — sin DNS nueva |
| Voz | voz.py |
El asistente completo (ver abajo). También levanta, en un hilo propio (app/voz/llamada.py), el servidor de "Laura por llamada" |
| Panel | panel.py |
Servidor FastAPI del panel (puerto 8750). Endpoints: /status, /start[/voz], /stop[/voz], /micros, /micro, /medidor, /chat |
- Wake words: "Venus" = asistente local (comandos y respuestas rápidas con Gemini) · "Laura" = Claude Code por voz (modo completo: puede leer/escribir archivos y ejecutar comandos, con confirmación hablada antes de tocar nada).
- Teclas:
F9dicta donde esté el cursor ·Shift+F9asistente ·Ctrl+F9Claude. - Venus: un comando por vez (así lo quiere Martín). "Venus, escribí/anotá..." = dictado manos libres. Números solos = cambiar de escritorio. Al captar lo que dijiste responde "Ok" (no tocar, así lo quiere).
- Laura no dice nada al captar. El "Entiendo." se sacó el 2026-08-10: la música de espera ya avisa que está trabajando, y encima
winsoundreproduce de a uno, así que la música se lo cortaba a mitad de palabra. Para volver a activarlo, buscarENTIENDO_TEXTOenapp/voz/voz.py. - Laura: conversacional (tras responder te da 4 s para empezar a hablar —
SEGUIMIENTO—, hasta 25 idas y vueltas sin volver a nombrarla). Una vez que arrancaste, tu turno lo cierranSIL_CORTE(2,5 s de silencio) yMAX_CLAUDE(45 s de tope): bajarSEGUIMIENTOno te corta al medio de una frase. La cola que se siente son ~5 s: 0,35 de margen + los 4 s + el aviso "Cierro" (~0,7 s). Si molesta, lo que queda por sacar es el aviso, no el número. Interrupción real por voz: "pará / basta / cortala / stop". Música de espera suave mientras piensa (musica_espera.wav). Si terminás haciendo otra cosa mientras ella trabaja, espera un silencio y anuncia "Laura terminó. ..." (no se descarta el trabajo largo). Tope por tarea: 10 min. - Sesión de Laura: proceso
claudepersistente (stream-json, ~3 s por turno) + sesión guardada enclaude_sesion.json— sobrevive reinicios. "Olvidate" archiva la actual y arranca otra.
Cada pregunta a Laura reenvía toda la conversación anterior: la número 50 le manda las
49 previas completas. Por eso una charla que dura días se encarece sola. Pero tirar el
historial todos los días no cuesta nada de verdad, porque lo que importa está en el
README.md y el CLAUDE.md, no en la charla — su identidad y las reglas se le pasan en
cada arranque con --append-system-prompt. La sesión es el hilo; el conocimiento está en el repo.
El día NO arranca a la medianoche, arranca a la 1 de la tarde (CORTE_HORA = 13).
Con el corte a las 12 la sesión se partía en dos en plena madrugada, que es cuando se
trabaja acá. Todo lo anterior a las 13:00 cuenta como el día anterior.
La primera vez que le hablás en un día nuevo, antes de gastar un solo token, contesta:
"Hola, hoy es nuestra primera sesión del día. ¿Querés continuar con la sesión anterior o empezamos un proyecto nuevo?"
Ese saludo lo dice Piper, no Claude — cuesta cero. Y lo que hayas preguntado queda
guardado en _pendiente: se contesta en cuanto elegís, no se pierde.
| Le decís | Qué hace |
|---|---|
| "continuá" · "seguí con la anterior" · "la de ayer" · "dale" | retoma la misma sesión, ahora contada como de hoy |
| "empecemos un proyecto nuevo" · "de cero" · "no" | archiva la de ayer y abre una limpia |
| "¿de qué hablamos ayer?" · "no me acuerdo" · "¿dónde quedamos?" | te resume la sesión anterior y arranca el día |
| cualquier otra cosa ("poné música") | resuelve el día con sesión nueva y atiende tu pedido igual |
El resumen no reabre la sesión vieja: lee su .jsonl de CLAUDE_SESIONES y le pasa la
charla como texto a la sesión de hoy. Sale más barato y no hay que malabarear dos procesos.
Las sesiones de Laura son chicas (contesta en dos frases: ~4.000 tokens un día entero).
⚠ Cuatro trampas que costaron encontrar, las cuatro cubiertas por pruebas/test_dia_laura.py:
- "seguí con la anterior" apretaba play. El patrón de
media_playpausetienesegu[ií]. Por eso la respuesta al saludo se intercepta en_manejar_textoantes queacciones.ejecutar, y es lo primero de la función. - "no, continuemos" elegía sesión nueva, porque ganaba el "no". Ahora el verbo explícito
(
_SIGUE_VERBO) se mira antes que el sí/no suelto. - "continuá donde quedamos" pedía un resumen, porque ganaba el "quedamos". Mismo arreglo. "¿Dónde quedamos?" pelado sí es un resumen: preguntás para poder decidir.
- El resumen se detectaba a sí mismo. El prompt lleva la charla de ayer adentro; si ayer
dijiste "de qué hablamos", el detector se disparaba con su propio pedido y se llamaba a sí
mismo para siempre. De ahí el
crudo=Trueenresumen_de().
Y GRACIA_CORTE (30 min): si venías hablando hace un rato, no te interrumpe con la
pregunta del día aunque el reloj haya cruzado las 13:00. Espera un hueco de verdad.
- Modelos: Whisper
turboen CUDA para transcribir; wake word continyen CPU int8 (inmune a la GPU cargada, ~0,5 s constante).
Trae la ventana de WhatsApp al escritorio virtual que estás mirando y la pone al
frente (con pyvda, el mismo que cambia de escritorio). Si no está abierto, lo abre
(protocolo whatsapp:). El detalle importante: si WhatsApp quedó en otro escritorio,
la ventana viene a vos — no te saltea a vos al escritorio de ella, que es lo que
hace Windows si la activás por la barra de tareas. También entra por "abrí/traeme/poné
el whatsapp". "Mandale un whatsapp a Juan" NO dispara esto: eso sigue siendo charla
para Laura. Casos en pruebas/test_ruteo_whatsapp.py — correr eso si se toca el orden
de COMANDOS en app/voz/acciones.py.
Busca y reproduce con la API oficial (app/voz/spotify.py), no automatizando la
ventana: pone la canción exacta en ~1 s sin depender del foco. Necesita Premium —
los endpoints de reproducción de la API son solo Premium.
Setup, una sola vez (ver el encabezado de app/voz/spotify.py):
developer.spotify.com/dashboard→ Create app. Redirect URI exacta:http://127.0.0.1:8888/callback— con127.0.0.1, nolocalhost: Spotify rechazalocalhost.SPOTIFY_CLIENT_IDySPOTIFY_CLIENT_SECRETen el.env.python -m app.voz.spotify --autorizar→ se abre el navegador, aceptás, y el token queda enspotify_token.json(fuera de git). El access token se renueva solo, pero ⚠ el dashboard marca "Refresh Token Lifetime: 180 days": cada ~180 días hay que volver a correr--autorizar. Si Venus dice "no pude renovar el token de Spotify", es eso — no está roto, se venció la autorización.- Probar sin la voz:
python -m app.voz.spotify "californication".
⚠ El paso 3 falló cuatro veces seguidas y siempre por lo mismo: el servidor local
sale de aire antes de que llegue el clic. --autorizar abre el navegador y se queda
esperando, pero si nadie aprieta Agree dentro de la ventana, el server se cierra y el
navegador se come un ERR_CONNECTION_REFUSED con el código ya en la mano. Ojo que el
código igual sirve: quedó en la barra de direcciones. Dos salidas, en orden de comodidad:
- Volver a levantar el server y apretar Reload en esa misma pestaña. El navegador
reenvía el mismo
?code=...y se canjea al toque. No hay que aceptar de nuevo ni copiar nada. - Copiar la URL completa y
python -m app.voz.spotify --codigo "<esa URL>".
En los dos casos hay ~10 minutos desde el Agree: pasado eso, invalid_grant y a
aceptar otra vez. Y ERR_CONNECTION_REFUSED en 127.0.0.1:8888 no es el antivirus —
es que el server ya no está escuchando. Antes de sospechar del firewall, mirá si el
proceso sigue vivo.
Qué entiende, y a qué lo traduce:
| Le decís | Busca |
|---|---|
| "poné Californication" | el tema |
| "poneme algo de Soda Stereo" · "la radio de Nirvana" · "lo mejor de Queen" | el artista (sus temas top) |
| "tocame mi lista de correr" · "la playlist de entrenamiento" | la playlist |
| "poné Spotify" (a secas) | abre la app, no busca un tema llamado "Spotify" |
Si Spotify no está abierto, lo abre y espera hasta 25 s a que se registre como dispositivo antes de darle play.
⚠ Tres cosas de COMANDOS en acciones.py que no hay que romper:
- El comando de Spotify va antes de
media_playpause, que tienereproduc[ií]en su patrón: si no, "reproducí tal canción" solo apretaría play sin poner nada. _norm()saca los acentos, así que los patrones van sin tildes (llegapone, noponé). Poner[eé]ahí no sirve para nada.- El ancla
^sola no alcanza: hay que dejar entrar la cortesía.\W*se come los signos, no las palabras. "¿Puedes poner la radio de los Red Hot Chili Peppers?" no matcheaba nada y se lo comía el LLM, que contestaba "No pude hacer eso" — falló tres veces al hilo el 2026-08-11 y parecía que Spotify estaba roto, cuando el token andaba perfecto. Cualquier comando nuevo tiene que tolerarpuedes / podés / podrías / por favor / quiero que / meadelante, y el verbo con sus pegotes (ponerme,ponémelo), porque así se habla. Un^pelado te hace creer que el bug está tres capas más abajo.
app/voz/cupo.py suma el usage real de todas tus sesiones de Claude Code (los
.jsonl lo guardan por mensaje), no solo las de Laura. Corré python -m app.voz.cupo
para ver los tres cupos.
Son tres, y el que se llena no es el que uno espera. Medido el 2026-08-11 (plan Max):
| Cupo | Estaba | Límite calibrado |
|---|---|---|
| sesión de 5 horas | 14 % | ~257 M |
| semanal | 58 % | ~2.350 M |
| semanal de Fable | 83 % | ~475 M |
Vigilar solo el de 5 horas es vigilar justo el que nunca se llena.
⭐ Pero el de Fable no habla: se mide y se ve en el medidor, y nada más
(CUPOS_QUE_AVISAN). Es el que menos importa saber en el momento, porque no es algo
sobre lo que se pueda hacer nada mientras trabajás. El aviso hablado sale solo por el
de 5 horas y el semanal.
Y Laura no es el problema, que era la sospecha original. Por modelo, 7 días:
sonnet-5 (Laura) 0,2 % · opus-4-8 40,7 % · opus-5 (VS Code) 30,1 % ·
fable-5 28,8 %. Hablarle más al asistente no te deja sin cupo; lo que consume son las
sesiones de programación.
Dos avisos distintos:
- Al 70 % (
AVISO_PORCENTAJE), por iniciativa propia, sin que le hables a nadie: "Ojo, estás llegando al límite: ya usaste el 82 por ciento del cupo semanal de Fable." Lo dice_avisar_sistema()envoz.py, que espera un hueco: que no estés hablando, que no esté hablando ella, y que la escucha no esté en pausa (si pausaste estás en una llamada). Espera hasta una hora; no lo tira, lo guarda. Se rearma al bajar del 65 % (REARMAR_PORCENTAJE), para que un cupo en el filo no te avise diez veces. - Cuando se agota, en vez de "No pude hablar con Claude": dice que fue el cupo, no reintenta (reintentar un límite es regalar 10 s para fallar igual) y te recuerda que Venus sigue andando.
⚠ Cómo calibrarlo (hace falta una vez): /usage en Claude Code y comparar con
python -m app.voz.cupo. El límite del plan no está en el disco — .credentials.json
trae qué plan tenés, nada del consumo. Los LIMITE_* están arriba de cupo.py.
⚠ Tres detalles que costaron:
- La ventana de 5 h de Claude es FIJA ("resets in 3h"), la de acá es móvil: mide igual o más, así que el aviso puede adelantarse. Para avisar, errar por adelantado va bien.
- El patrón de "sin cupo" va en dos versiones. El ancho se usa sobre el mensaje de
error; el angosto sobre la respuesta de Laura, porque con
429oquotaa secas un "el archivo tiene 429 líneas" te hacía escuchar "me quedé sin cupo" estando todo bien. SIN_SENAL_SEG(120 s) no reemplaza aTIMEOUT(600 s): una tarea larga sigue mandando eventos por el stream, así que si no llega ninguno está colgada. Bajar el total rompería los refactors largos; esto corta los cuelgues sin tocarlos.
En el panel hay un selector proyecto → sesión. Elegís una, apretás "Leer esta", y cada respuesta que esa sesión termine se lee en voz alta — sin pedirle "leeme" cada vez. Una sola sesión a la vez (el parlante tiene un solo dueño): elegir otra suelta la anterior. Se apaga con el botón o por voz: "Venus, dejá de leer".
Cómo funciona: todo Claude Code escribe su conversación EN VIVO en un .jsonl (carpetas
en CLAUDE_PROYECTOS, registro de sesiones vivas con nombre en CLAUDE_REGISTRO).
app/voz/seguir.py mira el archivo elegido como una cinta que avanza; el panel escribe
la elección en seguir_lectura.json (la perilla on/off, mismo patrón que
pausa_escucha.flag) y un hilo en voz.py la lee cada segundo. Para hablar usa la
espera-de-hueco de los avisos del sistema: no te pisa, no habla en pausa.
⚠ Detalles con historia:
- Fin de respuesta: el jsonl no trae marcador utilizable (
stopReasonviene vacío). Regla doble: llega tu próximo mensaje → lo anterior terminó; o el archivo queda quietoQUIETUD_SEG(5 s). Costado conocido: una herramienta lenta a mitad de turno hace que el texto previo se lea durante la espera — en la práctica te va contando lo que hace. - Se lee SOLO el texto del asistente: afuera herramientas, pensamiento interno,
sub-agentes (
isSidechain) y mensajes sintéticos. Y los resultados de herramienta llegan comotype=user— sin filtrarlos, cerrarían turnos que no terminaron. - Arranca desde el final del archivo: seguir una sesión es escuchar lo que venga,
no que te recite el historial. Tope
LECTURA_MAX(4.000 caracteres ≈ 2 min de voz): el resto "queda en pantalla". Cortás con "pará", como siempre. - Hay 6 casos en
pruebas/test_seguir.py— correr eso si se toca el extractor.
La ida del círculo (la lectura automática es la vuelta): dictás y el texto aterriza en
la sesión de Claude Code que elijas. El destino es UNA perilla con dos manos: el
selector del panel o la voz — cualquiera de los dos lo cambia (escribir_en.json), y
el panel lo refleja solo.
Formas de usarlo (⭐ regla 2026-08-11: "Venus, escribí" va SIEMPRE a la sesión elegida — para dictar donde esté el cursor está F9, que no cambió):
- "Venus, seleccioná la sesión del disco" → solo gira la perilla (también sirven "elegí / cambiá a / pasá a la sesión..."). Sin sesión elegida, "Venus escribí" no adivina: te dice cómo elegir una.
- "Venus, escribí: hola qué tal" o "Venus, escribí" (y dictás después) → trae al frente la ventana de la sesión elegida y pega ahí.
- "Venus, escribí en la sesión del tango: hola" → el combo: elige Y escribe en una sola frase.
Y el remate del flujo: "Venus, enter" aprieta Enter donde esté el cursor en ese momento — sin tocar ninguna ventana. Es para cuando ya dictaste, revisaste el texto en pantalla, y solo falta la tecla. A propósito NO va a la sesión elegida: te movería el foco justo cuando estás trabajando. También valen "dale enter" y "apretá enter"; la frase tiene que SER eso — "el enter del teclado" no manda nada.
⚠ Reglas de seguridad, en orden de importancia:
- Pega pero NO manda... salvo que VOS digas "enter" al final: "escribí: dale para adelante, enter" pega Y manda. La palabra dicha es tu firma — no es Venus mandando sola, sos vos autorizando en la misma frase. Solo la palabra "enter" al FINAL (no "entre", que es castellano común; "la tecla enter del teclado" en el medio no manda), y "enter" solo, sin texto adelante, no hace nada. Vale también para F9.
- Verifica la ventana DESPUÉS de activarla: si la que quedó al frente no es la buscada, no pega nada y avisa. Pegar en la ventana equivocada es peor que no pegar.
- Ante el empate no adivina: "del disco" con dos sesiones de disco vivas → te pide que aclares. El puntaje es por palabras del título + proyecto, con prefijos de 4 letras o más ("tienda" encuentra "Tienda2", que lleva el número pegado).
- El riesgo residual documentado: el foco dentro de VS Code queda donde estaba. Si el cursor estaba en el editor y no en el chat, el texto se pega en el archivo abierto — por eso avisa dónde escribió, y un Ctrl+Z lo deshace.
- Casos en
pruebas/test_escribir.py(patrón y buscador, sin arrancar el asistente).
Ni Telegram ni WhatsApp soportan llamadas de bot (es una limitación de esas plataformas,
no de este proyecto). La alternativa: una página web en https://<tu-hostname>/laura-voz
— la abrís en el navegador del celular, mantenés apretado el botón para hablar, soltás, y
te contesta con audio real. Es la MISMA Laura, mismo proceso, misma memoria que por
micrófono y por Telegram — la respuesta también respeta el saludo del "día de Laura".
No es una llamada de telefonía real (no hay interrupción a mitad de frase, es
"mantené y hablá, soltá y escuchá"), pero corre sobre el mismo Whisper turbo + Piper que
ya está cargado en la GPU: no se duplica ningún modelo. app/voz/llamada.py recibe esos
objetos ya cargados por parámetro (nunca los importa), justo para poder probarse sin
levantar el asistente entero.
Seguridad, en capas (Laura ejecuta comandos en esta PC — no es solo transcribir):
- El servidor solo escucha en
127.0.0.1:8760: nadie en la misma wifi lo encuentra por IP, únicamente por el hostname del túnel. - Requiere el header
X-Laura-Tokencon el secreto deLAURA_VOZ_TOKEN(.env). Sin ese secreto configurado, la llamada queda apagada, nunca "abierta por las dudas". La página lo pide una vez y lo guarda ensessionStorage(se olvida al cerrar la pestaña). - Los intentos fallidos se cuentan por IP real (
CF-Connecting-IP, la que manda Cloudflare) y se bloquean 10 minutos pasados 5 intentos. - Recomendado además (no lo hace el código, se configura una vez en el dashboard):
Cloudflare Access
sobre el path
/laura-voz*de tu hostname — gratis hasta 50 usuarios, exige un login (código a tu email) ANTES de que el pedido llegue siquiera a esta máquina. Hay que crear la Access Application con el path acotado a/laura-voz*: el resto del hostname (/webhook,/subir,/procesar-media) lo llaman n8n y WasenderAPI sin login interactivo, y gatear el hostname entero rompería esos flujos.
La ruta /laura-voz vive en el MISMO túnel y hostname que el webhook (wpp-config.yml,
regla de path antes de la genérica): no hace falta una DNS nueva.
- Micrófono actual: ROG CETRA OPEN WIRELESS (dongle USB). Elegible desde el panel (persiste en
config_dictado.json). - Niveles de referencia: ROG puesto hablando ≈ 0.05 rms · laptop a distancia ≈ 0.01–0.03 · umbrales
SIL_UMBRAL = WAKE_UMBRAL = 0.008. - Panel → "Probar micrófono (15 s)": barra en vivo con veredicto (silencio digital / bajo umbral / justo / bien).
- ⚠ PortAudio congela la lista de dispositivos al arrancar el proceso:
/microsdel panel hacesd._terminate()+sd._initialize()para refrescar. - ⚠ Si tocás la ganancia de grabación en Gear Link (Settings → Volume → Record), los cuatro umbrales de arriba quedan desfasados. Hay que volver a medir con "Probar micrófono" y actualizar código y README.
No se puede arreglar configurándolo. Recorrí Gear Link entero (gearlink.asus.com, versión web
de PC — la app móvil no ve el dongle, solo Bluetooth). Esto es todo lo que existe:
| Ajuste | Estado | Sirve |
|---|---|---|
| Noise Gate | ya venía en OFF | no era el culpable |
| Perfect Voice | ya venía en OFF | no era el culpable |
| AI Noise Cancellation | no tiene interruptor | la ficha la publicita, Gear Link no la deja apagar |
| Wear detection | no tiene interruptor | pausa el audio al sacarte un auricular |
| Idle mode | era 5 min → puesto en "Never" | ✅ sacó el apagado por inactividad |
Con el Noise Gate apagado y aun así dando ~0.00002, el causante es la cancelación por IA, que es permanente. Por eso el ROG entrega el mismo casi-cero (~0.00002 rms, unos −94 dBFS, por debajo del piso de 16 bits) en dos situaciones distintas: sin ponerse, y puesto pero callado. O sea que el rms no puede distinguir esos dos estados y ningún umbral lo va a arreglar — la información no está en el dato.
Y no es un defecto de ASUS: toda la categoría hace esto, porque mantener el micrófono siempre habilitado destruye la batería. Los auriculares que sí hacen wake word usan chips dedicados de ultra-bajo consumo. Ni RealtimeSTT (10,1k ★) ni wyoming-satellite implementan watchdog de micrófono: corren sobre micrófonos tontos, cableados y siempre encendidos, donde el problema no existe.
Ojo con "Never": ya no se duermen, así que se descargan si los dejás fuera del estuche. Guardalos en el estuche cuando no los uses.
Lo que conviene a futuro: un micrófono de escritorio siempre encendido como principal (tipo
ReSpeaker USB Mic Array). Los que traen AEC en hardware además cancelan el eco de tus parlantes
en el micrófono, lo que permitiría jubilar el parche de vad_filter documentado más abajo.
El mismo micrófono aparece varias veces en la lista de Windows, una por cada camino de audio. Ejemplo real con el ROG:
| Índice | Camino | Nativa | Sirve |
|---|---|---|---|
| 1 | MME | 44100 | nombre truncado a 31 letras, no coincide |
| 9 | DirectSound | 44100 | anda, pero es emulación legacy |
| 21 | WASAPI | 48000 | el bueno — necesita auto_convert |
| 31 | WDM-KS | 44100 | no sirve: sounddevice no soporta su API bloqueante |
Antes se tomaba la primera que coincidiera, o sea DirectSound. El problema: cuando el dispositivo se va y vuelve (los inalámbricos se duermen, el dongle se re-enumera), un buffer de captura de DirectSound se queda entregando ceros sin dar error. Los callbacks siguen llegando —así que el watchdog lo ve "vivo"— pero no capta nada. Eso explica el "se muere el micrófono a cada rato" mucho mejor que culpar a los auriculares.
Ahora _candidatos_micro() prueba WASAPI → DirectSound → predeterminado, en ese orden.
Dos cosas que costaron descubrirse:
- WASAPI en modo compartido exige la frecuencia nativa (48000) y nosotros abrimos a 16000
para Whisper →
Invalid sample rate [PaErrorCode -9997]. Se arregla consd.WasapiSettings(auto_convert=True), que hace que PortAudio resamplee solo. - El predeterminado de Windows queda último, no segundo: puede ser otro micrófono.
Qué pasó: la máquina estuvo suspendida y se reenchufó el dongle. Al suspender, Windows
desarma las conexiones de audio y al despertar arma otras con el mismo nombre: la que
voz.py tenía agarrada desde hacía 4 horas dejó de existir.
El watchdog lo detectó bien y hasta se recuperó pasando a MME — estaba escuchando. El
problema vino después: quiso volver al micrófono configurado, y _cambiar_stream()
cerraba el stream que andaba antes de intentar abrir el nuevo. El nuevo falló con
AUDCLNT_E_DEVICE_INVALIDATED y quedó sin ninguno: proceso vivo, panel en verde,
sordo, y sin ningún error visible. Es colgar el teléfono que funciona antes de comprobar
que el otro tiene tono.
Cómo quedó:
_cambiar_stream()abre primero y cierra después. Si el nuevo no abre, se queda con el que andaba. Si un driver no acepta dos streams sobre el mismo dispositivo, recién ahí suelta antes — y si eso falla, cae en la red de seguridad._asegurar_algun_stream()es esa red: prueba los caminos del configurado, después el predeterminado, y después cualquier entrada que abra. La regla es que nunca puede quedar sin micrófono.- El watchdog ahora detecta la vuelta de una suspensión por el salto del reloj (más de
60 s entre vueltas de un bucle que duerme 5 s), rearma el micrófono de una y refresca la
lista de dispositivos con
sd._terminate()+sd._initialize()(PortAudio la congela al arrancar y sigue viendo los de antes). Antes se enteraba recién cuando dejaba de llegar audio, y para entonces ya se había hecho lío. - Si por cualquier motivo
_streamqueda enNone, el watchdog lo ve en la vuelta siguiente y recupera.
⚠ Pendiente conocido: el medidor del panel (/medidor) usa su propio código para abrir
el micrófono y no tiene la preferencia de WASAPI de voz.py, así que después de una
suspensión puede fallar con un error de DirectSound aunque la voz esté escuchando bien.
Distingue dos medidas que no son lo mismo:
- FLUJO (
_ultimo_callback): llegó un callback de audio. Es la única prueba confiable de que el dispositivo existe — la cancelación de ruido vacía el contenido pero no puede parar el flujo de samples. Sin un solo callback enFLUJO_MUERTO_SEG(8 s), el dispositivo se fue de verdad. - CONTENIDO (
_ultimo_audio_real): hay audio real (pico > 0.002). Sirve para saber si te escucha, no para saber si está vivo.
Cambia de micrófono con solo dos disparadores, los dos confiables:
- Flujo cortado — no llegan callbacks.
- Verdad de campo (
_captura_vacia) — una grabación que tenía que traer audio (apretaste una tecla o dijiste el nombre) volvió en silencio digital (<0.0005). No puede dar falso positivo.
⚠ La vieja regla de "120 s pasivos sin pico" se eliminó el 2026-08-10. Causaba cambios de micrófono en plena sesión: te quedabas callado dos minutos, el gate de la IA daba casi-cero, y el watchdog saltaba al array de la laptop — que sí capta los parlantes, o sea justo la condición que hace alucinar a Whisper. Un falso positivo acá sale más caro que un falso negativo.
Además hay REFRACTARIO_SEG (60 s, "refractory period", como el --wake-refractory-seconds de
wyoming-satellite): después de cambiar de micrófono no se vuelve a cambiar por ese rato, en ninguna
de las dos direcciones. Sin eso rebotaba entre dos micrófonos. Cuando el configurado revive, vuelve
solo y avisa (¡el "default de Windows" puede ser el mismo micrófono muerto, por eso se sondea de
verdad en vez de confiar!).
voz.py loguea sus tiempos en líneas tiempos: de logs/voz.log. Para leerlas:
python -m pruebas.analizar_tiempos # todo
python -m pruebas.analizar_tiempos 50 # últimas 50 muestras
⚠ Esas líneas van con prefijo propio porque /chat del panel parsea con startswith:
poner un timestamp adelante de las otras líneas rompe la conversación en vivo.
Lo medido el 2026-08-10, que descartó dos culpables y encontró el real:
| Etapa | Medición | Veredicto |
|---|---|---|
Transcribir (Whisper large-v3-turbo int8 en GPU) |
12,5× tiempo real, sin espera de lock | no es el cuello |
| Síntesis del TTS (Piper) | ~30 ms por letra → 344 letras = 10,3 s de silencio | era el cuello |
| Respuesta completa | mediana 14 s, máx 31 s |
El arreglo fue sintetizar por trozos y empezar a reproducir el primero mientras se
generan los demás: la primera palabra pasó de 10,3 s a ~1,7 s en una respuesta larga,
sin tocar la calidad de la voz. Los trozos crecen 10 % cada uno — ver el aviso en CLAUDE.md
sobre por qué ese número no se toca sin volver a medir.
Sigue pendiente medir el LLM por separado (Gemini para Venus, Claude para Laura) para saber cuánto de la respuesta total es el modelo pensando.
- ⚠ El system prompt de Laura no puede tener
\nal pasar por el.CMDde Windows (rompía--resumesilenciosamente). Se aplana con regex enclaude_voz.py. - ⚠ Usar
dontAsk, nobypassPermissions(rompe--resume). Sin herramientaTask(pisaba el session_id). - ⚠ Para interrumpir a Laura:
control_requesttipointerruptpor stdin (requiere CLI ≥ 2.1.226). Nunca matar el proceso — la consulta quedaba pendiente y contestaba después. - ⚠ Whisper con parlantes:
vad_filter=True+ descartarno_speech_prob > 0.45, si no alucina el propio initial_prompt ("Laura/Venus") y se autodispara. Fragmentos de wake con >2 s de antigüedad se descartan (cola de audio con YouTube de fondo).
La raíz tiene solo lo que corre: los módulos que se importan entre sí y los archivos
que panel.py y los lanzadores buscan por ruta. El resto está agrupado.
wpp-transcriptor/
├─ launcher.vbs ← entrada principal (el acceso directo "Servidor IA" apunta acá: NO mover)
├─ panel.py ← panel web 8750: prende y apaga todos los servicios
├─ config_dictado.json · claude_sesion.json · wpp-config.yml · musica_espera.wav · .env
│
├─ app/ ← TODO el código
│ ├─ rutas.py ← ⭐ las rutas del proyecto, en un solo lugar
│ ├─ nucleo/ ← core.py · transcribir.py · procesar.py · analizar_gemini.py
│ ├─ voz/ ← voz.py · asistente.py · dictado.py · escucha.py
│ │ claude_voz.py · cerebro.py · acciones.py
│ ├─ web/ ← navegador.py · agente_web.py
│ ├─ ingesta/ ← bot_telegram.py · webhook_wasender.py
│ └─ generar_musica_espera.py
│
├─ lanzadores/ ← los .bat (Chrome IA, Iniciar WhatsApp, bot Telegram, Transcribir)
├─ pruebas/ ← las pruebas (probar_*, ver_*, test_*)
├─ skills/ ← atajos para Claude Code (ver skills/README.md)
├─ herramientas/ ← utilidades sueltas (actualizar Claude Code, sincronizar skills)
├─ logs/ ← voz.log (fuente del chat en vivo), telegram.log, webhook.log, tunel.log
├─ resultados/ ← salida de cada transcripción (audio, frames, texto). NO versionado
└─ graphify-out/ ← grafo de conocimiento generado con /graphify. NO versionado
Los módulos son un paquete de Python, así que se lanzan con -m desde la raíz del
proyecto (nunca por ruta al .py, porque entonces Python no encuentra el paquete app):
python -m app.voz.voz ← el asistente de voz
python -m app.ingesta.bot_telegram ← el bot de Telegram
python -m uvicorn app.ingesta.webhook_wasender:app --host 0.0.0.0 --port 8080
python -m app.nucleo.transcribir "archivo.ogg" [--analizar]
panel.py ya los lanza así (con cwd en la raíz), y los .bat de lanzadores/ también.
Si agregás un servicio al panel, acordate de que "match" en el dict SERVICIOS es el
texto con el que identifica al proceso: tiene que ser el nombre del módulo.
Antes cada módulo buscaba sus archivos con Path(__file__).with_name("config_dictado.json"),
o sea al lado de sí mismo. Funcionaba con todo suelto en la raíz, pero se rompía en
silencio en cuanto el módulo cambiaba de carpeta. Ahora hay una sola fuente de verdad:
from app.rutas import RAIZ, ENV, CONFIG_DICTADO, MUSICA_ESPERA, LOGS, RESULTADOSrutas.py vive siempre en app/, así que sabe dónde está la raíz. Si necesitás una ruta
nueva, agregala ahí y no la escribas a mano en otro módulo. Así los archivos se pueden
mover de carpeta sin romper nada.
Control de versiones: el proyecto tiene git desde 2026-08-10. .env, resultados/,
logs/ y claude_sesion.json están fuera por .gitignore (claves y datos de terceros).
Ya no hace falta guardar .bak a mano.
| Archivo | Rol |
|---|---|
app/rutas.py |
Las rutas del proyecto, en un solo lugar |
app/voz/voz.py |
Asistente completo: wake, dictado, comandos, watchdog del micro |
app/voz/claude_voz.py |
Puente con Claude Code (proceso persistente, sesión, interrupción) |
app/voz/cerebro.py / app/voz/acciones.py |
Intención → acción local (abrir apps, escritorios, ventanas) |
app/web/navegador.py / app/web/agente_web.py |
Control del navegador por voz (browser-use) |
panel.py |
Panel web 8750 + medidor de micro + chat en vivo |
app/ingesta/webhook_wasender.py |
WhatsApp + página /subir para audios largos |
app/ingesta/bot_telegram.py |
Bot de Telegram |
app/nucleo/ |
Pipeline: core.py, transcribir.py, procesar.py, analizar_gemini.py |
config_dictado.json |
Micrófono elegido |
claude_sesion.json |
Sesión persistente de Laura |
.env |
Claves (Telegram, Wasender, Gemini). No commitear |
logs/ |
voz.log (fuente del chat en vivo del panel), telegram.log, webhook.log, tunel.log |
- Voz más natural para Piper (TTS) — pendiente viejo.
- Timers/recordatorios por voz (Etapa 3 del roadmap).
- Probar la página
/subircon un audio real de 1 hora. - Revisar si
escucha.py(wake words con Porcupine/Picovoice) sigue en uso:voz.pyhace el wake con Whispertinyen CPU. Parecen dos caminos paralelos para lo mismo y uno puede estar muerto. - Que funcione sin placa NVIDIA: hoy Whisper está fijo en
cuda(app/nucleo/core.py). - Que el nombre con el que Laura te saluda y las carpetas que puede tocar se configuren desde el
.env.
Etapa 2: sistema/ventanas/archivos ✓ (parcial) · Etapa 3: productividad (timers/recordatorios) · Etapa 4: navegador avanzado · Etapa 5: manos libres (wake word) ✓ · Etapa 6: integraciones (Kommo/n8n/Calendar) · Etapa 7: modo autónomo con frenos.
GPL-3.0: podés usar, estudiar, modificar y compartir este proyecto. Si distribuís una versión
modificada, tiene que ser abierta también y con la misma licencia. El texto completo está en
LICENSE.