Este es el proyecto técnico más ambicioso que tengo corriendo en producción ahora mismo — no por una sola pieza de código, sino por todo lo que conecta: hardware físico en casa de mi abuela, un sistema de memoria biográfica que crece solo y pide aprobación humana antes de dar nada por sabido, una web familiar que se actualiza sin backend, un panel de control completo por Telegram, y hoy, tras el pivote, una sesión real de Claude Code a la que puedo hablarle en voz alta o por Telegram desde cualquier parte.
El origen: un asistente de voz para Dolores
La primera versión, analog-assistant, no nació pensando en mí. Construí una Raspberry Pi con una matriz de LEDs, un sensor de movimiento y un interruptor físico de encendido, y le di una personalidad completa: la de mi abuela Dolores. Memoria de familia, anécdotas, medicación, fechas importantes — un fichero de biografía que alimentaba cada respuesta de Claude, leída en voz alta con mi propia voz profesional, clonada en ElevenLabs, y un bot de Telegram como canal secundario.
Funcionaba. El problema apareció al usarlo con ella de verdad: Dolores tiene 93 años y bastante sordera. Seguir una conversación ya le cuesta con una persona delante, gesticulando, repitiendo si hace falta. Con un altavoz sintético sin ese apoyo visual, el asistente perdía casi todo su sentido. Con menos pérdida auditiva, o unos años antes, habría funcionado de maravilla.
Lo que tenía por dentro: un sistema de memoria vivo
Esta es la parte de la que estoy más orgulloso, y la que casi nadie ve por fuera. No era un chatbot con una ficha de personaje pegada al prompt — era un sistema que escuchaba, proponía y esperaba aprobación antes de dar nada por sabido, turno a turno.
Cada conversación se guardaba en SQLite. Cada 6 horas, un consolidador programado por cron le pasaba a Claude todo lo hablado ese día y le pedía dos cosas a la vez: un informe (estado emocional, si tomó la medicación de mañana/mediodía/noche según la hora, temas tratados, alertas si mencionaba dolor o tristeza inusual) y, por separado, una detección de memoria biográfica nueva — cosas que Dolores contó por primera vez, clasificadas por capítulos de su vida, cada una con su nivel de confianza y la cita literal de lo que dijo.
Nada entra directo
Cada memoria nueva o conflicto detectado (una fecha que Dolores dice distinta a la registrada, por ejemplo) queda pendiente en un buffer. Yo lo apruebo o descarto por Telegram, viendo la cita exacta y el nivel de confianza antes de decidir.
Hilos abiertos
Si mencionó algo sin desarrollarlo, queda anotado como hilo abierto con una pregunta sugerida para retomarlo con naturalidad en la próxima charla — no una lista de tareas, una forma de que la conversación tenga memoria de verdad.
Temas delicados, marcados aparte
Algunos recuerdos —la muerte de su padre en la Guerra Civil, entre otros— están marcados para que el asistente nunca los inicie por su cuenta. Solo acompaña si es ella quien los saca.
Fechas con sensibilidad
Detecta efemérides solo: felicita cumpleaños de familiares vivos, y en los aniversarios de quienes ya no están —su marido, su hijo Fili— no felicita, pero sí presta más atención al estado de ánimo de Dolores ese día.
Hay incluso un recurso que solo se desbloquea en circunstancias muy concretas: una carta de sus hijos, reservada para el aniversario o el cumpleaños de su marido fallecido, el aniversario de Fili, o un bajón emocional fuerte — nunca en una conversación cualquiera. Y todo esto se actualiza solo: el propio system prompt de Claude se recarga en caliente cada 30 segundos si algo cambia en la memoria, sin reiniciar ningún servicio.
La web que se alimenta sola
Los datos "públicos" de la biografía —familia, anécdotas, fechas, medicación, memoria biográfica, nunca las claves ni nada sensible— se sincronizan aparte, cada 6 horas, a un segundo repositorio: biografia-dolores. Un sitio estático, sin backend propio, desplegado gratis en Netlify.
- Dolores habla con el asistente por voz, en su salón.
- El consolidador analiza el día y actualiza los JSONs en la Raspberry Pi.
- sync_to_github.py sube esos JSONs al repo
biografia-doloresvía la API de GitHub. - La web hace
fetchdirecto a raw.githubusercontent.com con un cache-buster por timestamp — sin build, sin deploy, se refleja sola en la siguiente visita. - La familia entra y lo ve actualizado.
- Quien quiera puede dejar un recuerdo o una corrección en un formulario — llega a mi email.
- Yo decido qué de eso entra en los JSONs y vuelve a alimentar al asistente.
Es un círculo completo: lo que Dolores cuenta en voz alta en su salón termina, sin que nadie lo copie a mano, en una web que ve el resto de la familia — y lo que la familia aporta puede volver a entrar en lo que el asistente sabe de ella. Nada de esto necesitó un backend propio: GitHub como base de datos versionada, Netlify como hosting estático gratuito, Formspree como buzón de entrada.
Comandos de Telegram: control remoto completo
El bot de Telegram no era un canal secundario — era el panel de control de todo el proyecto, pensado para gestionarlo entero desde el móvil sin tocar la Raspberry Pi ni el código.
/walkie activo, cualquier texto que le escriba a este bot se convierte en mi propia voz sonando en el altavoz de casa de mi abuela en tiempo real — un walkie-talkie de verdad, disfrazado de bot de Telegram.El dato que mejor resume lo que era este proyecto/walkieActiva el modo walkie-talkie: los LEDs de casa de Dolores se ponen en morado. Cualquier texto que le escriba al bot se sintetiza al momento con mi voz clonada y suena en su altavoz; si mando una nota de voz o un audio, se reproduce tal cual, sin pasar por síntesis.
/finDesactiva el walkie e interrumpe de inmediato el ciclo de escucha del asistente, para que Dolores vuelva a hablar con él normalmente.
/estadoÚltima interacción (qué dijo, hace cuánto), conversaciones de hoy, si el walkie está activo.
/sistemaSalud del hardware: temperatura de la CPU, RAM, disco, tamaño de los audios generados, uptime.
/redSeñal WiFi en dBm, conectividad a internet, y si llega a las APIs de Claude y ElevenLabs.
/memoriaVer el resumen de memoria activo y el pendiente del consolidador.
/confirmarAplica el resumen pendiente para la siguiente conversación.
/biografiaLista las memorias biográficas y conflictos pendientes con su nivel de confianza y cita literal. /biografia 1,3,5 guarda las elegidas, /biografia all las guarda todas, /biografia descarta 2 descarta sin guardar. Los conflictos confirmados corrigen el JSON correspondiente solos.
/recordar [texto]Anoto algo suelto en lenguaje natural — Claude decide qué campos de qué JSONs tocaría y me enseña la propuesta exacta antes de aplicarla.
/recordar_okAplica los cambios que propuso el /recordar anterior.
/vozVer o ajustar en caliente los parámetros de la voz clonada de ElevenLabs — velocidad, estabilidad, parecido, estilo — sin tocar una línea de código.
/apisCréditos de ElevenLabs restantes y tokens de Claude consumidos hoy, con enlace directo a la facturación.
/informeÚltimo informe diario generado por el consolidador.
/logsÚltimas líneas del log del asistente, en vivo.
/reiniciarReinicia la Raspberry Pi en remoto, sin necesidad de estar delante.
El pipeline de voz en tiempo real
La primera versión esperaba a que Claude terminara de generar toda la respuesta antes de mandarla a ElevenLabs. La versión final no: el texto llega token a token en streaming, se trocea en frases completas conforme se detecta un punto de corte natural, y cada frase entra en una cola que un hilo aparte va sintetizando y reproduciendo — así el altavoz empieza a sonar con la primera frase mientras Claude todavía está generando la segunda. La latencia percibida cae muchísimo.
El prompt biográfico completo —familia, anécdotas, protocolo de escucha, memoria acumulada— es largo, así que usa prompt caching (cache_control: ephemeral) para no pagar ni esperar por reenviarlo entero en cada turno. Y Claude tiene herramientas reales disponibles: búsqueda web nativa, y dos propias, play_music y stop_music, que solo invoca cuando Dolores pide explícitamente una canción — zarzuela, pasodoble, copla — resolviendo el audio con yt-dlp contra YouTube y reproduciéndolo con ffmpeg, con una cola que evita procesos huérfanos si pide cambiar de canción a media reproducción.
El prototipo, probado de verdad
El vídeo más importante de todo el proceso: soy yo probándolo en directo.
El pivote: de dos caminos, el del agente
La decisión no fue abandonar el proyecto — fue no tirar el hardware que ya funcionaba. Sobre la mesa había dos caminos: un puente sencillo que reenviara mensajes entre Telegram y Claude, o un agente de verdad con acceso a herramientas y al servidor. La elección fue explícita y sin rodeos:
Nació analog-vps, sucesor directo del proyecto de Dolores: mismo hardware físico, "cerebro" completamente distinto. Deliberadamente se mantuvo la lógica de arranque y apagado por toggle físico, la animación de LEDs y el saludo por presencia — ya funcionaba, cambiar eso no aportaba nada.
Arquitectura: dos entradas, un bridge, un Claude Code real
Hay tres piezas de software independientes, cada una en su propio servicio systemd:
pi_client/
Raspberry Pi · en casaEscucha por micrófono, transcribe con Whisper local y sintetiza la respuesta con Piper. Gestiona LEDs, sensor PIR y el toggle físico.
bridge/
FastAPI · en el VPS, solo TailscaleRecibe texto, invoca claude -p en modo headless con la sesión correspondiente y devuelve la respuesta. No es un agente recortado: es Claude Code completo.
telegram_client/
Bot de Telegram · en el VPSMismo bridge, canal gemelo. Conversación continua con /reset para limpiar el hilo cuando hace falta.
La pieza que cambia todo es cómo habla el bridge con Claude. No llama a la API de Anthropic con una clave de pago por token: invoca el propio binario claude en modo no interactivo (claude -p --output-format json --permission-mode auto), usando el mismo login de la suscripción Pro que ya está activo en este VPS — el mismo con el que trabajo yo en las sesiones interactivas. Eso tiene tres consecuencias directas:
Lo que implica usar claude -p en vez de la API
Bash, Read, Write, Edit, búsqueda web, memoria y skills — el mismo Claude Code de esta conversación, no un agente con una única tool a medida.
Cero marginal por encima de la suscripción — no hay clave de API de pago por token de por medio.
Comparte límite de uso con mis propias sesiones interactivas — no es un cubo aparte. Si le doy mucha caña por Telegram, se nota en mi cuota normal.
Ni la Pi ni el bot hablan directamente con Claude ni con el VPS por internet abierto: la Raspberry Pi (en casa) y el VPS están en la misma red privada de Tailscale, y el bridge escucha solo en su IP de esa red — nunca hay subdominio público, certificado ni puerto expuesto para esta pieza. Cada petición añade además una cabecera X-API-Key propia.
La seguridad no es un añadido — es la misma que me protege a mí
La primera versión del bridge tenía una única herramienta a medida (run_command) con una lista negra de comandos peligrosos escrita a mano. Funcionaba, pero era frágil: cualquier comando fuera de esa lista pasaba sin más criterio. Al reescribir el bridge para invocar claude -p directamente, esa capa entera se volvió innecesaria — --permission-mode auto reutiliza el mismo clasificador de modo automático y el mismo settings.json que gobiernan esta sesión interactiva de Claude Code en el VPS. Las mismas reglas de "pide confirmación antes de algo destructivo" se aplican ahí.
El sistema sí distingue el canal de origen: por voz, el system prompt le recuerda a Claude que el texto viene transcrito por Whisper y puede tener errores, y que las respuestas se leen en voz alta (evitar listas largas, bloques de código o markdown ilegible hablado). Por Telegram, sabe que el texto es exacto y que puedo estar fuera de casa.
Sesiones que no crecen para siempre
Cada canal mantiene su propio session_id de Claude Code, independiente entre sí. La Pi arranca una sesión nueva cada vez que subo el toggle — no tiene sentido arrastrar un hilo de conversación de voz entre encendidos. Telegram, en cambio, sigue una conversación continua, con /reset disponible para cuando quiero limpiarla a mano.
Esto no pierde memoria real: lo que importa de verdad —proyectos, decisiones, contexto persistente— vive en el sistema de memoria a largo plazo de Claude Code, el mismo que usa esta página para saber qué es este proyecto. Sobrevive a cualquier reinicio de sesión, no depende de mantener vivo un hilo de chat.
El hardware, pieza a pieza
Todo el lado físico —LEDs, sensor, toggle, micrófono, altavoz— es el mismo que ya tenía analog-assistant. Es pura electrónica y no dependía de Dolores ni de Claude, así que no había motivo para tocarlo. Así está cableado hoy alrededor de la Raspberry Pi:
Matriz de LEDs 8×8 (SPI)
Bus SPI0.0 a 6.4 MHz con una tabla de bits precalculada para no perder frames. Antes de que exista sesión: rayos de arranque, en blanco si todo va bien o en rojo/naranja/ámbar según falle el WiFi, internet o el bridge. Ya en marcha: pulso naranja escuchando, azul transcribiendo, blanco hablando.
Sensor de movimiento (PIR)
GPIO17 vía gpiozero.MotionSensor. Cuando expira la ventana de escucha de 300 s y detecta presencia, dispara un saludo generado por Claude — no una frase enlatada— y reabre la conversación.
Toggle físico ON/OFF
GPIO26 con pull-up — nivel bajo es encendido. Lo vigila un servicio systemd siempre activo, independiente del SSH, que arranca o para el proceso principal con SIGINT al cambiar de estado.
Tailscale hacia el bridge
La Pi y el VPS viven en el mismo tailnet privado. El bridge no tiene IP pública, subdominio ni certificado — cada petición añade además una clave propia por cabecera.
Altavoz
Los efectos de arranque y apagado se reproducen con ffmpeg | aplay sobre la tarjeta de sonido USB; las respuestas de Piper se guardan primero a .wav y se lanzan con aplay normal.
Piper — voz local y gratis
Voz es_ES-sharvard-medium, hablante femenino, español peninsular. Sustituyó a ElevenLabs, donde la voz era la mía propia (profesional, clonada) — antes había coste por carácter generado además del de la clonación, ahora es un binario local sin llamadas externas.
faster-whisper — oídos locales
Modelo "small" en CPU con cuantización int8 — el modelo "tiny" resultó demasiado impreciso. Transcribe entero en la propia Pi, sin mandar audio a ningún sitio, y le pasa el texto al bridge.
Micrófono USB
Captura cruda a 16 kHz con arecord. El fin de turno se detecta por energía RMS de la señal, no con un modelo de detección de voz: 1.5 s de silencio corta la grabación, con 30 s de máximo por turno.
El making of
Antes de que la Raspberry Pi tuviera carcasa, vivió unos días dentro del cajón de un mueble, conectada a un breadboard. Aquí van los bocetos que descarté, el blueprint de la primera versión y los vídeos reales de la matriz de LEDs cobrando vida cable a cable — mudos a propósito, para no publicar ninguna conversación de por medio, salvo el primero: soy yo probándolo de verdad, con sonido.
Lo que se rompió por el camino
Tres bugs concretos durante el despliegue en la Pi, ninguno relacionado con Claude:
gpiozero necesita el paquete lgpio en Raspberry Pi 5 para no fallar con BadPinFactory — no estaba en el venv anterior. Piper fallaba bajo systemd porque el PATH del venv no está disponible ahí; se resolvió resolviendo el binario a partir de sys.executable en vez de confiar en el PATH. Y la primera versión del bridge, con su herramienta a medida, se caía con bloques de texto vacíos que Claude a veces emite junto a llamadas a herramientas — un problema que desapareció entero al reescribir el bridge para invocar claude -p directamente, porque Claude Code gestiona su propio historial internamente.
Qué se dejó fuera, a propósito
La reproducción de música por YouTube y el modo walkie-talkie por Telegram que tenía analog-assistant no entraron en esta reescritura — no encajaban con el nuevo diseño centrado en un agente real. Quedan como candidatas a volver, si hace falta, como herramientas del propio bridge.