enes
Todas las notas

IA

Le puse voz a Claude Code para dejar de vigilar la terminal

Tenía agentes trabajando en otro monitor y pasaba el día mirando de reojo si habían terminado o si estaban esperando un permiso. Ahora me avisan en voz alta, con un resumen que escribe el propio Claude y una voz que corre en mi máquina, sin APIs.

8 min de lectura

Una ventana de terminal de la que sale una onda de sonido naranja

Trabajo con Claude Code casi todo el día, y muchas veces con más de una sesión abierta: una refactorizando en un proyecto, otra escribiendo tests en otro. El problema es que un agente no avisa. Termina, o se frena a pedirte permiso para correr un comando, y se queda ahí, quieto, hasta que te das cuenta.

Yo me daba cuenta tarde. Volvía a la terminal después de diez minutos y el agente llevaba nueve esperando que le dijera "sí". O lo contrario: me quedaba mirando la pantalla por las dudas, que es justamente lo que quería dejar de hacer al delegarle el trabajo.

La solución obvia es una notificación de escritorio, y no me sirvió. Una notificación compite con todas las demás, aparece en el monitor que no estás mirando y desaparece sola. Lo que quería era lo que hace una persona sentada al lado: que me diga, en voz alta, "terminé con esto" o "necesito que mires algo".

Así nació HeyDev. Arrancó como un par de hooks sueltos en mi configuración y terminó siendo un plugin que se instala con dos comandos.

Cómo suena

Cuando Claude termina una tarea escucho algo así:

"Gastón, terminé en tienda online. Listo el endpoint de pagos, falta probar el webhook."

Y cuando se frena a pedir un permiso o me hace una pregunta:

"Gastón, en app del clima necesito tu atención."

El nombre del proyecto sale de la carpeta (tienda-online se dice "tienda online"), así que con varias sesiones abiertas sé cuál me está hablando sin mirar. La segunda frase es la interesante: no es un texto fijo, es un resumen de lo que hizo.

El resumen lo escribe el propio Claude

La primera idea para tener un resumen era la evidente: agarrar la respuesta de Claude y mandársela a otro modelo para que la resuma en una línea. Funciona, pero es una llamada más por cada aviso, con su costo, su latencia y su API key.

Pero Claude ya sabe exactamente qué hizo. Es el que mejor puede resumirlo, y ya está generando texto. Así que en vez de resumir después, le pido que lo haga mientras responde.

Claude Code tiene hooks: comandos que corren en momentos puntuales de la sesión. El plugin usa tres:

Evento Qué hace
SessionStart Le agrega al contexto una instrucción: terminar cada respuesta con <!-- voice: resumen -->.
Stop Cuando Claude termina, busca ese comentario y lo dice en voz alta.
Notification Cuando Claude pide un permiso o hace una pregunta, avisa que necesita atención.

El truco está en el formato: un comentario HTML. Claude Code muestra las respuestas como markdown, y en markdown un comentario HTML no se ve. La respuesta queda exactamente igual que siempre; el resumen viaja escondido en la última línea y solo lo lee el hook:

MARCA = re.compile(r"<!--\s*voice:\s*(.+?)\s*-->", re.S)
 
def resumen_de(texto):
    hallados = MARCA.findall(texto or "")
    return hallados[-1].strip() if hallados else ""

La instrucción le pide un máximo de doce palabras, en el idioma del aviso, en primera persona y sin rutas ni código, porque una ruta de archivo dicha en voz alta no le sirve a nadie. Si una respuesta no trae resumen (una pregunta rápida, por ejemplo), el aviso dice solo "terminé en tal proyecto" y listo.

Cero llamadas extra. El resumen sale gratis porque ya venía en la respuesta.

La latencia: el hook no puede esperar

Un hook de Claude Code corre en el medio de tu sesión. Si tarda, la sesión tarda. Y generar voz no es instantáneo: sintetizar una frase lleva un rato, y reproducirla, lo que dure la frase.

Así que el hook no hace nada de eso. Lee lo que le manda Claude Code, extrae el resumen, lanza un proceso aparte y sale. Todo lo lento corre en segundo plano:

def en_segundo_plano(evento, datos):
    cmd = [sys.executable, __file__, "_trabajo", evento, json.dumps(datos)]
    subprocess.Popen(cmd, start_new_session=True,
                     stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
                     stderr=subprocess.DEVNULL)

Medido de punta a punta, el hook responde en unos 50 milisegundos. No se nota.

Después hay otras dos decisiones para que el aviso llegue rápido y suene fluido:

  • Las frases fijas quedan en caché. "Gastón, terminé en tienda online" se dice igual todas las veces, así que el audio se genera una vez y se guarda con un hash del texto como nombre. La segunda vez no se sintetiza nada: se reproduce directo.
  • El resumen se genera mientras suena lo anterior. Las partes del aviso se sintetizan en paralelo, y cuando termina de sonar la primera la segunda ya está lista. No queda un silencio en el medio.

Y un detalle que aparece apenas usás más de una sesión: dos agentes que terminan al mismo tiempo hablan uno encima del otro. Lo resolví con un lock sobre un archivo. El que llega segundo espera su turno y habla después, como una fila.

Voz local, sin APIs

Para la voz había dos caminos. Uno era un servicio de texto a voz en la nube: suenan muy bien, pero cada aviso es una llamada a internet, con su latencia, su costo y su API key. Y lo que mandás son resúmenes de lo que estás haciendo en tu código, que no tienen por qué salir de tu máquina.

El otro era un modelo local. Elegí Piper, un motor de voz neural que corre sobre ONNX. La voz por defecto es es_AR-daniela-high, una voz rioplatense que pesa unos 114 MB. Lo que más me importaba:

  • Corre en CPU. No hace falta GPU, y una frase corta se genera en mucho menos de lo que tarda en decirse.
  • No hay cuentas ni claves. Después de la instalación no vuelve a conectarse a nada.
  • No depende de que un servicio ajeno esté funcionando, ni de que cambien los precios o el plan gratuito.

La instalación pesa unos 300 MB entre el entorno de Python, Piper y la voz, y pasa una sola vez, en segundo plano, la primera vez que abrís una sesión. Mientras tanto el plugin usa la voz que trae el sistema, así que los avisos llegan desde el minuto uno. Cuando termina te dice "ya tengo voz".

El script en sí solo usa la biblioteca estándar de Python. Todo lo pesado vive en un entorno aparte que el plugin arma y mantiene solo.

Que te llame por tu nombre

Un aviso que empieza con tu nombre se registra distinto que un "tarea finalizada". Por eso el plugin pregunta, al instalarlo, cómo querés que te llame. Si lo dejás vacío, usa el primer nombre de tu usuario del sistema, y con un guion no dice ningún nombre. Se puede cambiar después desde /plugin.

También te pregunta el idioma: español, inglés, portugués, francés, alemán o italiano. Las frases, el resumen de Claude y la voz salen en ese idioma, y si lo dejás vacío usa el del sistema. La voz se puede cambiar por cualquiera del catálogo de Piper.

Funciona en Linux, macOS y Windows

Empezó como algo solo para Linux, porque es lo que uso yo. Pasarlo a los tres sistemas implicó resolver los detalles de cada uno: cómo se bloquea un archivo, con qué se reproduce un audio, cuál es la voz de respaldo, dónde se guardan los datos.

Lo más traicionero fue Windows, por dos cosas que no avisan. La primera: el comando python3 muchas veces no es Python sino un acceso directo que abre la Microsoft Store, así que el plugin prueba varios hasta encontrar uno que funcione. La segunda: los textos con tildes se rompían porque Windows no usa UTF-8 por defecto entre procesos. "Terminé" llegaba a la voz como cualquier cosa.

Probalo

Dentro de Claude Code:

/plugin marketplace add gastonnrodriguez/heydev
/plugin install heydev@heydev

Solo necesitás Python 3.9 o superior. El código está en github.com/gastonnrodriguez/heydev, con licencia Apache 2.0.

Es open source: sumate

HeyDev es un proyecto abierto y la idea es que crezca con quien lo use. Si encontrás un bug, si algo no suena en tu sistema o si se te ocurre cómo mejorarlo, abrí un issue en GitHub. Y si querés meter mano en el código, los pull requests son más que bienvenidos.

Hay algunas cosas en las que me vendría muy bien una mano:

  • Probarlo en macOS y Windows. Lo uso todos los días en Linux; en los otros dos sistemas, cada reporte de cómo te anda (y sobre todo de cómo no) vale oro.
  • Voces en otros idiomas. Si encontrás una voz de Piper que suene bien en tu idioma, contala en un issue para recomendarla en el README.
  • Ideas para nuevos avisos. Hoy avisa cuando termina y cuando necesita tu atención. Seguro hay otros momentos en los que conviene que te hable.

No hace falta que sea algo grande: un issue de dos líneas diciendo "en mi máquina no suena" ya ayuda.

Lo que me llevo

Lo que más me sorprendió fue lo que cambió en la forma de trabajar. Ya no miro la terminal para ver si terminó; me entero cuando termina. Puedo tener dos o tres agentes trabajando y estar en otra cosa.

Y la decisión técnica que más rindió fue la más chica: no agregar un modelo para resumir, sino pedirle al que ya estaba trabajando que dejara el resumen escondido en su propia respuesta.