Hace dos días publicamos María, nuestra agente VoIP con IA, tras un sprint de un día. Desde entonces hemos escuchado todas las llamadas, leído todas las transcripciones y observado dónde flaquea el pipeline. En 48 horas María ha ganado un bot operativo en Telegram, grabaciones con las dos voces adjuntas al lead, y una serie de fixes de resiliencia que convierten una demo en algo que ya nos fiamos en producción.
Este post arranca justo donde acabó el anterior. Si no lo has leído, empieza aquí: Cómo construimos María.
Lo que aprendimos en 48 horas
Tres cosas se hicieron evidentes en cuanto empezó a entrar tráfico real. Primero, hacía falta un canal paralelo para hablar con María fuera de la línea telefónica, para ver el estado, lanzar consultas puntuales y algún reinicio ocasional cuando un worker se atascaba. Segundo, una transcripción no basta: cuando hay una duda o un matiz, queremos oír las dos voces. Y tercero, el pipeline post-llamada era un único punto de fallo. Si Egress fallaba, el lead no llegaba al CRM.
Las siguientes secciones van en ese mismo orden.
Bot de Telegram @maria_ltc_bot
Construimos una segunda María que no descuelga el teléfono. Escucha en Telegram. El bot corre como servicio systemd independiente (maria-telegram.service) con long polling, y solo se acepta el chat id del owner. Los demás reciben un rechazo educado.
Diez comandos cubren la operativa diaria:
/backups Estado de los backups diarios (ict_fer + elPanocho + oci-test)
/query <q> Consulta en lenguaje natural sobre Odoo (XML-RPC por debajo)
/oca <q> Pregunta al skill OCA expert
/pilot <q> Pregunta al skill Odoo pilot (admin automatizado)
/sysadmin <q> Pregunta al skill system-admin (SSH a pve1/ipve1)
/calls Llamadas recientes de María con lead id y resumen
/status Salud del agente: workers, RAM, última llamada, cola
/restart_maria Reinicio ordenado del agente de voz
/start Intro + lista de comandos
/help Igual que /start, versión breve

Captura real de @maria_ltc_bot respondiendo /backups, /query y /oca — cada resultado de tool aparece de vuelta como mensaje en Telegram.
Lo interesante es cómo responde el bot. Los tools no viven dentro del proceso del bot. El bot es un dispatcher delgado. Cuando llega un comando, escribe una petición JSON en un socket Unix, y un daemon local de Claude la recoge y ejecuta el skill Claude Code correspondiente con acceso completo a herramientas.
¿Por qué tanta indirección? Porque la CLI de Claude se niega a correr con --dangerously-skip-permissions como root, y con razón. Así que añadimos un usuario dedicado no-root, claude-runner, con su propio venv, sus propias claves SSH a pve1 e ipve1, y su propia configuración Claude con el sandbox desactivado. El daemon corre como ese usuario, expone un socket Unix, y el bot (que corre como root porque necesita leer algunos logs del sistema) le habla por el socket. Un lado privilegiado orquesta, el otro lado ejecuta los tools. Frontera limpia, y reutilizamos todos los skills Claude Code que ya teníamos: system-admin, odoo-pilot, oca-expert, todos.
El canal de Telegram se ha convertido, en dos días, en la consola operativa principal. Teléfono para los clientes, chat para nosotros.
Grabación de llamadas con las dos voces
La primera versión de María capturaba audio de forma pasiva con VoIPmonitor, un sidecar que esnifa RTP y escribe WAV. Funcionaba, pero el camino de pcap a WAV a adjunto de Odoo era frágil, y los archivos eran volcados de paquetes sin separación limpia entre llamante y agente. Queríamos, dentro del lead del CRM, un solo archivo de audio con las dos voces, reproducible en el navegador, sin herramientas extra.
Primero probamos LiveKit Egress en modo room-composite, que renderiza la sala WebRTC con un Chrome headless y produce una mezcla limpia. En nuestro CT, Chrome arrancaba pero el pipeline de composición nunca emitía la señal de inicio, y Egress acababa abortando. En lugar de pelear con Chromium dentro de un LXC unprivileged, migramos a otro modo de Egress: dos peticiones track_egress en paralelo, una por participante, cada una escribe su propio OGG/Opus. Al acabar la llamada, ffmpeg hace lo que Chrome no quiso hacer:
ffmpeg -i caller.ogg -i agent.ogg \
-filter_complex "[0:a][1:a]amix=inputs=2:duration=longest[a]" \
-map "[a]" -c:a libopus -b:a 32k -ac 1 mix.ogg
El resultado es un único OGG/Opus mono a 32 kbps, pequeño para vivir como ir.attachment, limpio para ser útil. Un método nuevo en nuestro módulo custom, voip.call.attach_recording_to_lead, publica el archivo en el chatter del lead existente con un mensaje corto. El mensaje de la transcripción ya ha llegado segundos antes desde register_call. Dos mensajes, mismo lead, en orden: primero texto, luego audio.
Mejoras de UX
Tres cambios pequeños con un impacto desproporcionado.
- 100 keyterms en Deepgram. Le pasamos al STT una lista curada de 100 términos: OCA, Odoo, ERP, ICT y compañía, más los apellidos españoles más frecuentes del INE. Ahora cuando llama un cliente apellidado Conesa, María oye "Conesa" y no "Conhexa". El lead sale con el nombre correcto, lo que importa más de lo que parece cuando toca devolver la llamada.
- Odoo se pronuncia "Odú". ElevenLabs, por defecto, lo deletrea como "o-do-o". Añadimos una línea al system prompt forzando la pronunciación a "Odú", y ahora María lo dice como un humano.
- "Al colgar se crea la oportunidad automáticamente." Un fallo sutil pero molesto: clientes decían "¿puedes crearme una oportunidad?" y María, entrenada para ser prudente, respondía "por seguridad no puedo crear registros". Técnicamente correcto, socialmente equivocado, porque la oportunidad se crea, solo que al colgar. Una línea en el prompt reescribió esa interacción: María confirma ahora que el lead quedará registrado al finalizar la llamada, y sigue adelante.
Fixes de resiliencia
Los llamantes reales encuentran bugs que ningún diseñador anticipa. Un puñado de parches defensivos se ganaron su sitio esta semana.
- Watchdog de silencio. Si ninguna voz suena durante 120 segundos, María cuelga educadamente. Sin esto, una llamada que diera con un 429 de Anthropic podía quedarse eternamente con aire muerto.
- Shutdown timeout subido a 60 s. El valor por defecto de livekit-agents para
shutdown_process_timeout es 10 segundos. Nos estaba matando el pipeline post-llamada en plena subida del audio. Sesenta segundos es holgado pero acotado, y ahora la grabación llega a Odoo.
- Pipeline reordenado: primero lead, después grabación. Crear el lead es rápido y debe tener éxito. Adjuntar la grabación es lento y best-effort. Los separamos. Aunque Egress falle o ffmpeg se quede colgado, la oportunidad está en el CRM con su transcripción y su resumen antes de que nos preocupemos por el audio.
- Parking defensivo. Justo antes del XML-RPC a Odoo, volcamos el payload a
/var/log/voip/pending_leads/<call_id>.json. Si Odoo está caído, el payload no se pierde: un cron de recuperación lo recoge en la siguiente pasada. Si Odoo lo acepta, el archivo parkeado se borra. Cambio minúsculo, tranquilidad enorme.
Tope de capacidad vía load_fnc
La restricción más dura no es CPU ni RAM, es el techo de peticiones concurrentes de Anthropic en nuestro tier actual. Dos conversaciones en vivo más el bot de Telegram ya puede disparar un 429. Así que instalamos un tope blando en el agente de voz: una load_fnc a medida que inspecciona el número de llamadas en curso y devuelve load=1.0 cuando estamos en dos concurrentes. El SFU de LiveKit rechaza la tercera llamada limpiamente, y el llamante oye tono de ocupado en vez de una sesión rota. Ni fallos silenciosos, ni leads a medio procesar.
Es un tope bruto, pero convierte un límite blando en uno duro, que es exactamente lo que quieres cuando el cuello de botella real es un rate limit downstream. Cuando subamos el tier Anthropic, subimos el tope cambiando un entero.
Qué viene después
Lista corta, en orden de alivio probable: subir el tier Anthropic para levantar el tope de llamadas concurrentes, ampliar el rango de puertos RTP para absorber más de cinco llamadas en paralelo, y añadir una segunda voz del agente para llamantes en inglés. Los huesos están firmes. De aquí en adelante es pulido y volumen.
Si quieres oír a María en directo, llámanos al +34 868 35 37 57 o escríbenos. Te dirá que es una IA, registrará la llamada, y las dos voces te estarán esperando en el lead para cuando cuelgues.
Fer & Claude, 22 de abril de 2026.
Informe técnico completo
La sección de abajo es el informe interno completo que escribimos para nosotros al cerrar el sprint. Cubre todo el stack, la arquitectura, cada fase, los límites de capacidad, los costes y el trabajo pendiente. No hemos recortado nada salvo las IPs públicas y un número de teléfono privado.
Descargar informe en Markdown
Informe final — voip-expert / agente María
Fecha: 2026-04-22
Sesiones de trabajo: 2026-04-20 · 2026-04-21 · 2026-04-22
Estado: operativo en producción
1. Resumen ejecutivo
Agente VoIP autónomo que:
- Atiende el DID +34 868 35 37 57 (Zadarma) en español con voz natural (ElevenLabs Flash v2.5, voz España
uQw4jpKzMLrZuo0RLPS9).
- Mantiene conversación con Claude Haiku 4.5, STT con Deepgram Nova-3.
- Al colgar genera automáticamente: opportunity en CRM elPanocho + transcripción en chatter + resumen con Claude Sonnet 4.6 + grabación de audio en OGG/Opus mezclado (ambas voces).
- Tiene modo debug activado desde
XXX XXX XXX (Fernando) con tools internos: check_backups, query_odoo, ask_oca_expert, ask_odoo_pilot, ask_system_admin, send_telegram.
- Bot Telegram independiente (
@maria_ltc_bot) para consultas textuales con los mismos tools.
- Watchdog de silencio que cuelga automáticamente si no hay actividad 120 s.
- Self-hosted: todo corre en un CT LXC en pve1 (Proxmox local).
- Código versionado en repos privados GitLab, sin dependencias cloud para la lógica.
2. Stack tecnológico
| Capa |
Tecnología |
Versión |
Rol |
| Telefonía PSTN |
Zadarma |
— |
DID + SIP trunk |
| Router / NAT |
Huawei EG8145V5 |
— |
Port-forward UDP 5060 + 10000-10999 |
| SIP gateway |
livekit-sip |
1.2.0 |
Terminación SIP, puente a WebRTC |
| SFU / Realtime |
livekit-server |
1.11.0 |
Rooms WebRTC, media routing |
| Agent framework |
livekit-agents (Python) |
1.5.4 |
Ciclo de vida de la llamada |
| LLM (diálogo) |
Claude Haiku 4.5 |
— |
Streaming en tiempo real |
| LLM (resumen) |
Claude Sonnet 4.6 |
— |
Summary post-llamada |
| STT |
Deepgram Nova-3 (multi) |
— |
Transcripción ES/EN + keyterms |
| TTS |
ElevenLabs Flash v2.5 |
— |
Voz synth, speed 0.85 |
| VAD |
Silero |
— |
Detección de turno |
| Grabación |
LiveKit Egress + ffmpeg |
— |
2 track_egress + amix |
| CRM / Backoffice |
Odoo 16 (elPanocho DB) |
16.0 |
Opportunity, chatter, attachment |
| Módulo custom |
custom_fer |
16.0.1.8 |
voip.call.register_call + attach_recording_to_lead |
| Bot Telegram |
python-telegram-bot |
21.11.1 |
Canal de consulta por chat |
| Daemon Claude CLI |
Unix socket |
— |
Subprocess claude con perms bypass |
3. Arquitectura
PSTN
│
▼
┌──────────┐ SIP/RTP ┌──────────────────┐ WebRTC ┌────────────────┐
│ Zadarma │─────────────▶│ Router Huawei │───────▶│ livekit-sip │
│ DID │ UDP 5060 │ NAT 1:1 + fwd │ │ (CT-140) │
└──────────┘ 10000-10999 └──────────────────┘ └────────┬───────┘
│
┌────────────────────────┼─────────────────┐
▼ ▼ ▼
┌────────────────┐ ┌─────────────────┐ ┌───────────────┐
│ livekit-server │◀────▶│ maria-agent │ │ livekit-egress│
│ (CT-140) │ │ (entrypoint) │ │ (Docker) │
└────────────────┘ └──┬──────────────┘ └──────┬────────┘
│ │ track x2
┌───────────────────────────────┤ │
│ │ │ │
▼ ▼ ▼ ▼
┌──────────────┐ ┌─────────────┐ ┌──────────────┐ /var/lib/livekit-egress/
│ Deepgram STT │ │ Anthropic │ │ ElevenLabs │ recordings/*.ogg
│ Nova-3 │ │ Claude API │ │ Flash v2.5 │ │
└──────────────┘ └─────────────┘ └──────────────┘ ▼
ffmpeg amix
│
┌───────────────────────────────────────────────────────────┘
▼
┌─────────────────────┐ XML-RPC ┌─────────────────────────┐
│ run_post_call_ │─────────────▶│ Odoo 16 (CT-116 ipve1) │
│ pipeline │ │ custom_fer.voip.call │
│ (Sonnet + parking) │ │ register_call + │
└─────────────────────┘ │ attach_recording_to_lead│
└─────────────────────────┘
┌───────────────────────────────────────────────────────────────┐
│ Canales operativos (debug) │
│ │
│ Telegram @maria_ltc_bot ◀─▶ maria-telegram.service │
│ (long polling) │ │
│ ▼ │
│ claude-daemon ◀─▶ Unix socket │
│ (claude-runner user, --bypass) │
└───────────────────────────────────────────────────────────────┘
4. Infraestructura
CT-140 ct-voip en pve1 (Proxmox local)
- LXC unprivileged con
nesting=1, keyctl=1
- Debian 12, Python 3.11, Node 20
- IP LAN estática 192.168.1.7/24 (gw 192.168.1.1)
- 4 cores / 4 GB RAM / 20 GB ZFS (
zfs-storage:subvol-140-disk-0)
- Paquetes relevantes:
bubblewrap, socat, ffmpeg 5.1.8, docker.io
Servicios systemd activos en CT-140
| Unit |
Descripción |
Path script/unit |
livekit-server.service |
SFU WebRTC |
/opt/livekit/... |
livekit-sip.service |
SIP gateway |
build from source v1.2.0 |
livekit-egress.service |
Docker livekit/egress:latest |
config /etc/livekit/egress.yaml |
maria-agent.service |
Agent Python (entrypoint) |
/opt/voip-agent/maria_phase2.py |
maria-telegram.service |
Bot Telegram |
/opt/voip-agent/maria_telegram.py |
claude-daemon.service |
Unix socket → claude CLI |
/opt/voip-agent/tools/claude_daemon.py |
redis-server.service |
Backend livekit + egress |
default |
Usuarios del sistema
root — administra todos los servicios
claude-runner (uid 1001, home /home/claude-runner) — ejecuta claude CLI con --dangerously-skip-permissions (no permitido como root)
- venv propio con
paramiko, requests, pyyaml, tabulate, lxml, bs4, python-dotenv
- SSH keys a
pve1 e ipve1 (claves /home/claude-runner/.ssh/id_ed25519*)
- Sandbox Claude deshabilitado:
~/.claude/settings.json con {sandbox:{enabled:false}}
Container livekit-egress (Docker)
- Imagen oficial
livekit/egress:latest
--network host, --cap-add=SYS_ADMIN
- Bind mount
/var/lib/livekit-egress/recordings:/out (chmod 777)
- User interno
egress (uid 1001)
- Chrome 125 incluido (no usado actualmente; usamos
track_egress directo)
Infraestructura externa
- Router Huawei EG8145V5 (192.168.1.1): admin via Selenium (credenciales documentadas en
reference_home_router.md)
- WAN IP
public.ip.address.number (dinámica; hay que monitorizar)
- ipve1 (OVH, public.ip.address.number): CT-116 con Odoo 16 elPanocho — destino CRM
- CT-200 (pve1): server de backups, corre
check_backups.sh
5. Flujo de llamada entrante
T+0.0s caller marca +34 868 35 37 57
T+0.5s Zadarma INVITE → WAN public.ip.address.number:5060
T+0.6s router NAT → CT-140:5060
T+0.7s livekit-sip valida trunk (IP 185.45.152.0/22), crea room maria-_<caller>_<random>
T+1.0s dispatch rule lanza agent worker → entrypoint(JobContext)
T+1.5s agent joins room, ejecuta greeting GDPR:
"Hola, le atiende María, asistente virtual de lemontreecloud.
Esta llamada será grabada para atención al cliente. ¿En qué puedo ayudarle?"
T+3s saludo TTS reproducido
T+3s _start_recording: 2 track_egress paralelos
- caller audio → /out/<room>_<ts>_caller.ogg
- agent audio → /out/<room>_<ts>_agent.ogg
T+3s silence watchdog armed (120 s)
── Ciclo conversacional ──
STT final usuario → Haiku (streaming)
Haiku decide: respuesta vocal + quizá tool call (solo debug)
Si tool: ctx.session.say("Espere un segundo...") luego tool execution
Respuesta TTS se reproduce
Cada turno resetea watchdog
── Fin de llamada ──
T+N usuario dice "adiós" (o equivalente)
T+N+Δ Haiku invoca end_call
T+N+Δ+1s TTS drain + grace period → room.disconnect()
T+N+5s shutdown hook fires (process exiting)
┌─ Pipeline post-call (shutdown_process_timeout=60s) ─┐
│ 1. Build transcript snapshot from recorder │
│ 2. Sonnet 4.6 → {summary, intent, language} │
│ 3. _park_pending(payload) → /var/log/voip/pending_leads/ (safety) │
│ 4. XML-RPC voip.call.register_call → lead_id │
│ 5. Unpark (delete safety file) si éxito │
│ 6. Stop egress (EGRESS_COMPLETE → ok) │
│ 7. Poll 2 .ogg files (estables, ≥500 bytes) │
│ 8. ffmpeg amix → mix.ogg (libopus 32k mono) │
│ 9. XML-RPC voip.call.attach_recording_to_lead │
│ 10. Cleanup files locales │
└──────────────────────────────────────────────────────┘
Resultado en Odoo CRM:
- Lead (type=opportunity) con:
· name = primera frase del summary
· phone = número caller
· description = summary
· source_id = VoIP Inbound
· tag_ids = [voip-inbound, <intent>]
· user_id = Fernando
· chatter:
· mensaje 1: transcript completo renderizado HTML
· mensaje 2: "Grabación adjuntada" + attachment caller_agent_mix.ogg
6. Integraciones externas
| Servicio |
Cuenta |
Credenciales |
Notas |
| Zadarma |
DID +34 868 35 37 57 |
Panel zadarma.com |
"Servidor externo" apunta a WAN + modo SIP IP-based auth |
| Anthropic API |
Fernando (tier actual) |
ANTHROPIC_API_KEY en .env |
⚠️ rate-limit concurrent ya bloqueó conversaciones |
| Deepgram |
Fernando |
DEEPGRAM_API_KEY en .env |
Nova-3 multi con 100 keyterms (acrónimos + nombres INE) |
| ElevenLabs |
Fernando (paid) |
ELEVENLABS_API_KEY en .env |
Voz ES España uQw4jpKzMLrZuo0RLPS9, speed 0.85, model Flash v2.5 |
| Telegram Bot |
@maria_ltc_bot |
Token en .env |
Whitelist chat_id Fernando, rechaza otros |
| GitLab |
fernandohc |
PAT rotatorio |
Repos privados |
| Odoo elPanocho |
fernando@elpanocho.com |
API key en .env |
CRM destino de leads |
7. Módulo custom_fer v16.0.1.8 (Odoo 16 elPanocho)
Path: /opt/odoo/custom/apps/custom_fer/ (CT-116 ipve1). Repo gitlab.com/fernandohc/apps branch antigravity.
Modelo voip.call
Métodos públicos XML-RPC:
register_call(payload) — crea opportunity a partir de payload de llamada finalizada.
- payload:
caller_phone, started_at, ended_at, duration_s, language, transcript, summary, intent, debug_mode, silence_hangup, extra_tags
- Retorno:
{lead_id, lead_url, attachment_id (False)} (None convertido a False para XML-RPC compat)
-
Acciones: crm.lead.create(type=opportunity), posts transcripción renderizada HTML, aplica tags y source.
-
attach_recording_to_lead(payload) — adjunta audio a un lead existente.
- payload:
lead_id, recording_b64, recording_filename
- Mime detect
.ogg → audio/ogg, .mp4 → audio/mp4
ir.attachment.create + mail.message con "Grabación adjuntada".
- Pipeline de 2 pasos: primero crea lead (rápido, debe tener éxito), luego attach (lento, best-effort).
Flujo email inbound (nuevo 2026-04-22)
fetchmail.server id=3 tira email de fernando@lemontreecloud.com cada 5 min.
mail.alias id=49 con defaults:
python
{
'type': 'opportunity',
'user_id': 2, # Fernando
'team_id': 1, # Sales
'source_id': 12, # Email Inbound
'tag_ids': [(6, 0, [19])], # email-inbound
}
- Cada email a
fernando@lemontreecloud.com genera un opportunity taggeado.
8. Fases del proyecto (cronológico)
| Fase |
Fecha |
Descripción |
Resultado |
| 1 — Hello World |
2026-04-20 |
Llamada hace ring + TTS "hola" |
Zadarma + livekit-sip + NAT 1:1 + primer agent Python |
| 2 — Diálogo producción |
2026-04-20 |
María mantiene conversación bilingüe con Haiku + Deepgram + ElevenLabs, GDPR, end_call tool |
Primeras llamadas con Raúl |
| 3 — CRM integration |
2026-04-20 |
Hook on_shutdown crea opportunity en elPanocho con transcript + resumen Sonnet |
custom_fer.voip.call.register_call + fix type=opportunity para visibilidad |
| 4 — Grabación pasiva (deprecado) |
2026-04-20 |
VoIPmonitor sidecar con MariaDB + GUI |
reemplazado en Fase 6 |
| 5 — Tools operativos |
2026-04-21 |
6 function_tool debug-only en el agent + OCA primer en system prompt |
Gateway para ask_oca_expert, ask_odoo_pilot, ask_system_admin, check_backups, query_odoo, send_telegram |
| 5.1 — Bot Telegram |
2026-04-21 |
@maria_ltc_bot con 10 comandos, long polling, whitelist Fernando |
Servicio independiente maria-telegram.service |
| 5.2 — Daemon Claude CLI |
2026-04-21 |
Unix socket daemon corriendo como claude-runner no-root, bypass sandbox |
Tools ask_* ya funcionales; sandbox Claude deshabilitado en settings.json |
| 6 — LiveKit Egress |
2026-04-21 |
Reemplaza VoIPmonitor por grabación activa self-contained |
Archivo OGG adjunto al chatter del lead |
| 6.1 — Ambas voces |
2026-04-22 |
Room composite audio_only con Chrome (falló: start signal not received) → migrado a dual track_egress + ffmpeg amix |
Un OGG/Opus con caller + agent mezclados |
| 6.2 — Resiliencia pipeline |
2026-04-22 |
Reordering (lead primero, recording después) + parking defensivo + attach_recording_to_lead async |
Lead se crea aunque egress falle |
| 6.3 — Fix perms + egress ending |
2026-04-22 |
chmod 777 /out, EGRESS_ENDING/COMPLETE diferenciados de ABORTED/FAILED |
Grabación se adjunta fiable |
| 6.3.1 — keyterms + prompt |
2026-04-22 |
100 keyterms (acrónimos + INE top apellidos) + prompt "al colgar se crea automáticamente" + fix Odú vs o-do-o |
UX conversacional |
| 6.3.3 — Silence watchdog |
2026-04-22 |
Auto-hangup a 120 s de silencio |
Resiliencia ante rate limit 429 |
| 6.3.4 — Shutdown timeout |
2026-04-22 |
shutdown_process_timeout=60s (default 10s mataba el pipeline) |
Attach completa antes del SIGKILL |
| 7 — Email inbound CRM |
2026-04-22 |
Alias fernando@ añade source+tag al opportunity |
Emails también entran al CRM taggeados |
9. Capacidad y límites
| Componente |
Límite efectivo |
Observaciones |
| Zadarma trunk |
10 canales simultáneos |
Capa externa |
| livekit-sip RTP ports |
10 ports (10050-10059) → ~5 llamadas paralelas |
Se puede ampliar; router ya permite 10000-10999 |
| livekit-server |
cientos de rooms |
Sin cuello de botella |
| maria-agent workers |
num_idle_processes=4 + auto-spawn |
RAM/CPU real marcan el techo |
| CT-140 recursos |
4 cores / 4 GB RAM |
Cada llamada activa ~300 MB RAM |
| Anthropic concurrent |
tier actual → 429 con 1 llamada + bot activo |
cuello de botella principal; upgrade de tier o retry+backoff |
| Deepgram / ElevenLabs |
tier actual suele soportar docenas |
No hemos visto bottleneck |
Veredicto: soporta razonablemente 2-3 llamadas concurrentes. Para absorber 10 simultáneas de Zadarma hay que (a) ampliar RTP range, (b) subir tier Anthropic, (c) probablemente subir RAM a 8 GB.
10. Repositorios Git
| Repo |
URL |
Contenido |
voip-ai-agent |
https://gitlab.com/fernandohc/voip-ai-agent |
Agent Python, bot Telegram, claude-daemon, scripts, deploy/ |
apps (branch antigravity) |
https://gitlab.com/fernandohc/apps |
Módulo custom_fer con voip.call, otros módulos Odoo |
claude-memory |
https://gitlab.com/fernandohc/claude-memory |
Memoria persistente de Claude Code Fernando (170+ archivos) |
ai-tools/voip-expert |
local en /opt/odoo/custom/ai-tools/ |
Skill Claude Code con SKILL.md + references + templates |
Todas privadas. Auto-sync de claude-memory vía hook SessionEnd + PreCompact.
11. Memorias / referencias clave
feedback_livekit_sip_zadarma.md — 3 gotchas NAT (flood/Numbers/nat_1_to_1)
feedback_voip_debug_tools.md — patrón debug-only tools
feedback_livekit_egress_record.md — track_egress + ffmpeg mix
feedback_crm_lead_vs_opportunity.md — crm.lead type=lead es invisible en UI
reference_ct140_voip.md — specs del contenedor
reference_home_router.md — admin router Huawei
reference_elevenlabs_account.md — SSO Google, API key narrow
reference_voipmonitor_ct140.md — deprecado (Fase 6 reemplazó)
feedback_claude_daemon.md — patrón Unix socket para bypass sandbox/root
user_fernando_contact.md — whitelist debug +34XXXXXXXXX
12. Pendientes y roadmap
Pendientes operativos
- Rotar 3 credenciales de
oci_test_deployment.md (test env de hace 37 días, repo privado pero documentadas)
- Borrar
/opt/odoo/obsidian-vault/ (ya migrado el contenido útil a claude-memory)
- Activar skill
voip-expert con /opt/odoo/custom/ai-tools/sync-skills.sh
Roadmap técnico
- Subir tier Anthropic o implementar retry+backoff en 429 dentro del plugin anthropic de livekit-agents
- Ampliar RTP range (10050-10149) si se necesitan >5 llamadas paralelas
- WAN IP dinámica: DDNS o script de reload si cambia (por ahora static)
- Sync CT-116 environment: clonar repos y skills en CT-116 (ipve1) para que el CRM tenga herramientas claude locales también
- Blog post sobre la experiencia: ya live en
https://lemontreecloud.com/blog/ltc-labs-3/como-construimos-maria-nuestra-agente-voip-con-ia-en-un-dia-12
Ideas a evaluar
- PersonaPlex 7B v1 (NVIDIA) cuando salga versión ES: full-duplex speech-to-speech con latencia <300 ms, pero requiere GPU 16 GB y v1 es English-only
- Voxtral TTS (Mistral): alternativa open-source a ElevenLabs si queremos bajar coste variable
- Recordings storage: actualmente OGG en
ir.attachment. Si volumen crece, considerar S3/MinIO externo
- Multi-lang: María responde en el idioma del caller, pero OCA primer está en español. Ampliar a EN/PT si hay leads internacionales
- Voice biometrics: detectar caller recurrente por voiceprint para personalizar saludo
13. Datos sensibles (referencias)
Credenciales operativas viven en /opt/voip-agent/.env de CT-140 (permisos 640 root:claude-runner, no versionado). Contiene:
ANTHROPIC_API_KEY
DEEPGRAM_API_KEY
ELEVENLABS_API_KEY (+ alias ELEVEN_API_KEY)
LIVEKIT_API_KEY + LIVEKIT_API_SECRET + LIVEKIT_URL
ZADARMA_* (si usadas)
TELEGRAM_BOT_TOKEN + TELEGRAM_CHAT_ID
ODOO_URL + ODOO_DB + ODOO_USER + ODOO_API_KEY
SILENCE_TIMEOUT_S=120
PVE1_HOST=root@192.168.1.2
Tokens GitLab son efímeros (PAT rotatorio Fernando, scope api, vida 90 días). Nunca se commitean; se usan vía URL transitoria oauth2:<token>@ y luego se limpia el remote.
14. Coste por llamada (estimación)
Llamada típica de 3 minutos:
| Ítem |
Coste aprox |
| Zadarma DID + minutos |
~0,03 € |
| Deepgram Nova-3 |
~0,12 € |
| ElevenLabs Flash v2.5 (~600-800 chars agente) |
~0,05 € |
| Claude Haiku 4.5 streaming + Sonnet 4.6 summary |
~0,01 € |
| Total por llamada 3 min |
< 0,25 € |
Infra self-hosted (CT-140 + livekit-*): coste marginal ya absorbido por el Proxmox de casa.
Generado por Claude Code en colaboración con Fernando Hernández, sesiones 2026-04-20 → 2026-04-22.