Deja de pagar por el silencio: silence trim en el STT cloud y descarga por inactividad de Whisper local en Hermes Agent

Los mensajes de voz son la entrada más natural en las plataformas de mensajería de Hermes Agent — mantén pulsado el botón, habla y el transcript llega al agente. Pero si usas un proveedor de speech-to-text (STT) en la nube, cada nota de voz te cuesta en silencio dos veces: una por el silencio.
Una nota de voz de 13 segundos puede contener solo 6 segundos de habla real — los otros 7 segundos eres tú pensando. Whisper cloud factura por minuto de audio, y el silencio se cobra a la misma tarifa que el habla (OpenAI cobra $0.006/min por ello). Peor aún, Whisper alucina palabras en los tramos de silencio — por eso los transcripts cloud contienen a veces frases que nunca se pronunciaron.
Dos PRs de Hermes Agent fusionados recientemente arreglan exactamente estos dos problemas, y ambos son prácticamente plug-and-play (uno activado por defecto, el otro una sola línea de configuración):
- #77581 — Silence trim antes de la subida para STT cloud: colapsa las pausas largas con ffmpeg antes de la subida. Resultado medido: una nota de voz de 13.2s recortada a 6.2s (-53%) con un transcript totalmente equivalente.
- #81027 — Descarga por inactividad para el modelo Whisper local: descarga automáticamente el modelo local tras 5 minutos de inactividad, liberando ~370MB de RAM/VRAM, y lo recarga de forma transparente con el siguiente mensaje de voz.
Contexto: dos rutas de STT, local y cloud
Antes de las nuevas funciones, esta era la arquitectura. El STT vive en la sección stt de config.yaml; provider elige la ruta:
stt:
enabled: true
provider: local # local | groq | openai | mistral | xai | elevenlabs | deepinfra
language: "en" # global language hint, avoids wrong-language detection on short clips
Ruta local (local): faster-whisper se ejecuta en tu propia máquina — gratis y privado, pero el modelo permanece residente en memoria. Un endurecimiento anterior ya añadió Silero VAD (voice activity detection), así que el silencio nunca llega al modelo.
Ruta cloud (groq/openai/mistral/xai/elevenlabs/deepinfra): el audio se sube en bruto a una API de terceros y se factura por minuto de audio. El problema: la ruta cloud nunca tuvo una protección equivalente al VAD — el audio en bruto, pausas incluidas, subía intacto.
Los dos PRs cierran exactamente esas lagunas: silence trim para la ruta cloud y descarga por inactividad para la ruta local.
Parte 1 — Silence trim cloud: colapsa las pausas antes de la subida
Cómo activarlo
Está activado por defecto — tres ajustes:
stt:
cloud_trim_silence: true # false = always upload the original audio
cloud_trim_threshold_db: -40 # audio quieter than this counts as silence
cloud_trim_keep_ms: 300 # keep 300ms of each pause, preserving word boundaries and pacing
El flujo: antes de subir, cualquier clip de más de 12 segundos tiene sus pausas largas colapsadas en huecos de 300ms mediante el filtro silenceremove de ffmpeg (el audio por debajo de -40dB cuenta como silencio), y después se sube la versión recortada. ffmpeg ya era una dependencia de este mismo código (transcodificación CAF), así que no hay dependencias nuevas.
Números medidos (ejecución real del autor del PR)
original: 13.15 s (speech 3s + pause 7s + speech 3s)
INFO Trimmed silence from voicenote.wav before cloud STT upload (13.2s -> 6.2s, -53%)
trimmed: 6.24 s
El audio recortado se verificó con faster-whisper: ambas locuciones se transcriben de forma idéntica al original — pausas fuera, habla intacta.
El gate de 12 segundos: por qué los clips cortos se saltan el trim
Todo lo anterior dice «clips de más de 12s». Ese gate es un control de costes deliberado: los clips cortos reciben un ffprobe de ~50ms y se saltan la codificación. El razonamiento es concreto — en un clip de menos de 12s el ahorro máximo posible es de ~10%, alrededor de 1 segundo de audio, y varios proveedores facturan un mínimo por petición de todos modos (Groq factura un mínimo de 10s). La codificación nunca se amortiza en clips cortos; solo los clips lo bastante largos como para beneficiarse de verdad asumen el coste de codificación.
Estrictamente best-effort: el trim nunca puede romper la transcripción
Este es el núcleo del diseño: el trim es best-effort — todos los modos de fallo suben el original intacto, y la transcripción nunca falla por culpa del trim:
| Condición | Comportamiento |
|---|---|
cloud_trim_silence: false |
Se sube el original |
| ffmpeg/ffprobe ausente | Se sube el original |
| Clip de menos de 12s | Se sube el original (una inspección, sin codificación) |
| El comando de trim falla / expira | Se sube el original |
| Resultado del trim casi vacío (clip casi silencioso) | Se sube el original — el proveedor, no una heurística de dB del lado cliente, decide si contiene habla |
| El trim ahorra <10% | Se sube el original |
Las dos últimas filas merecen una segunda lectura: si una grabación casi silenciosa «contiene habla» lo decide el proveedor (que tiene su propia detección de voz), y re-codificar para ahorrar <10% es puro desperdicio — así que el trim simplemente se rinde.
Cuándo desactivarlo
El umbral de -40dB trata los entornos silenciosos como silencio. Si tus mensajes de voz suelen ser música, sonido ambiente o ruido blanco en lugar de habla (por ejemplo, pidiendo al agente que identifique una canción o una grabación de campo), pon cloud_trim_silence: false para restaurar las subidas en bruto — la misma filosofía que vad: false en la ruta local.
Parte 2 — Descarga por inactividad de Whisper local: la fuga de 370MB que no notaste
El problema: cargar una vez, retener para siempre
El modelo local faster-whisper es un singleton: se carga con el primer mensaje de voz y nunca se libera — durante toda la vida del proceso. El modelo base retiene aproximadamente 370MB, aunque no llegue ningún mensaje de voz en horas o días.
Eso es especialmente derrochador en procesos de gateway de Hermes de larga duración — sobre todo en máquinas donde Whisper compite con un LLM local por la misma GPU: la VRAM que Whisper retiene es VRAM que tu modelo local no puede usar.
Cómo activarlo
stt:
local:
model: "base" # tiny | base | small | medium | large-v3
unload_after_idle_seconds: 300 # 0 = never unload (default); 300 = unload after 5 idle minutes
El valor por defecto 0 significa no descargar nunca — cero cambios de comportamiento para los usuarios existentes. Ponlo a 300 (recomendado para configuraciones de gateway) y:
- Un hilo de vigilancia comprueba cada 30 segundos; cuando el tiempo de inactividad supera el umbral, la referencia al modelo se suelta para el GC
- El siguiente mensaje de voz lo recarga de forma transparente mediante la ruta lazy-load existente — ninguna diferencia visible para el usuario salvo una demora de carga del modelo
- La configuración se relee en cada ciclo: edita el valor en
config.yamly surte efecto en un intervalo de comprobación — sin reiniciar el proceso; volver a ponerlo a 0 a mitad de la inactividad incluso cancela la descarga pendiente
Los números de memoria honestos: GPU vs CPU
El autor del PR midió ambos casos con honestidad y vale la pena citarlo:
- CUDA/GPU: una vez que el objeto del modelo pasa por el GC, la VRAM se devuelve al dispositivo — la gran victoria para las configuraciones que comparten GPU.
- CPU (medido en macOS): las referencias de Python se sueltan y la memoria pasa a ser reutilizable, pero el asignador de C++ de ctranslate2 no devuelve páginas al SO, así que el RSS apenas se mueve (medido: 388MB antes y después). En Linux, el comportamiento de
malloc_trimde glibc puede devolver algo.
En términos llanos: en GPU es una liberación real; en CPU, sobre todo, la memoria queda reutilizable — el modelo ya no retiene cientos de MB de objetos vivos, y un modelo de otro tamaño configurado más tarde se carga en el espacio recuperado en lugar de hacer crecer más el proceso. En cualquier caso, ya no es «cargar una vez, retener para siempre».
Parte 3 — Configuraciones recomendadas según el caso de uso
| Escenario | Recomendación |
|---|---|
| Gateway de larga duración, LLM local en la misma GPU | local.unload_after_idle_seconds: 300 (muy recomendado — la VRAM se libera de verdad) |
| Escritorio/CLI, voz ocasional, poca RAM | local.unload_after_idle_seconds: 600 — descarga tras 10 minutos de inactividad |
| Mensajes de voz frecuentes, sensible a la latencia | Mantén 0 — evita la espera de carga del modelo en el primer mensaje |
| STT cloud (groq/openai, etc.) | Mantén cloud_trim_silence: true (por defecto) — los costes bajan de inmediato |
| Los mensajes de voz suelen ser música/ambiente | cloud_trim_silence: false |
Tras editar, abre el archivo con hermes config edit, o verifica los valores actuales con hermes config get stt.local.unload_after_idle_seconds.
Cuatro consejos prácticos
- STT cloud + comandos de voz cortos es la mejor combinación: el gate de 12s hace que los comandos normales («mira el tiempo de mañana») nunca activen el trim ni paguen el coste de codificación — el recorte solo entra en juego con notas de voz largas.
- No te saltes la pista de idioma: un
stt.language: "en"global también se aplica a los proveedores cloud (gana la configuración por proveedor), y los comandos de voz cortos fallan a menudo porque la auto-detección de Whisper adivina el idioma equivocado. - Comprueba antes de cambiar:
hermes config get stt.cloud_trim_silencemuestra el valor efectivo directamente; ejecutahermes config checktras editar para validar la sintaxis. - Local y cloud se pueden intercambiar según la necesidad: cambia el campo
providercuando quieras. ¿Quieres coste cero?local(gratis pero residente en memoria — combínalo con la descarga por inactividad). ¿Quieres alta precisión multilingüe? Ruta cloud — y el silence trim mantiene la factura a raya.
Resumen
En conjunto, los dos PRs ordenan ambas rutas de STT: la ruta cloud ya no paga por las pausas ni recibe transcripts contaminados por alucinaciones de silencio; la ruta local ya no retiene 370MB de RAM/VRAM girando en vacío. Configuraciones pequeñas, cero dependencias nuevas, mejoras puramente incrementales — los usuarios intensivos de voz (sobre todo configuraciones de gateway + STT cloud) deberían activarlas hoy mismo.
Para profundizar en las capacidades de voz de Hermes Agent y su configuración relacionada, consulta las notas de la versión v0.19.1 Voice Patch, la guía de instalación y las notas de la versión v0.20.0 Herald.