Files
agents_and_robots/dev/issues/0040-voice-messages-stt.md
T
egutierrez 52d5632d89 docs: crear issues 0036-0041 — nuevas features del sistema
Issues planificados:
- 0036: Claude Code streaming de progreso en Matrix
- 0037: Agente que crea otros agentes/bots via Matrix
- 0038: Webapps y dashboards embebidos en Element via widgets
- 0039: Recordatorios dinámicos y crons que invocan agentes
- 0040: Soporte para mensajes de voz (audio → STT)
- 0041: Videollamadas con agentes via LiveKit

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 21:19:09 +00:00

11 KiB

0040 — Soporte para mensajes de voz (audio → STT → procesamiento)

Estado: pendiente

Objetivo

Permitir que los agentes reciban y procesen mensajes de voz (m.audio) desde Matrix. El audio se descarga, se transcribe via Speech-to-Text (Whisper API), y el texto resultante entra al pipeline normal de procesamiento. Los usuarios pueden hablar con sus agentes enviando notas de voz desde Element.

Contexto

  • El listener en shell/matrix/listener.go actualmente solo maneja mensajes de texto: extrae body de event.EventMessage y lo pasa como Content al MessageContext.
  • Los agentes procesan texto puro a traves de reglas → LLM. No hay soporte para ningun tipo de media.
  • El proyecto ya tiene github.com/sashabaranov/go-openai como dependencia, que incluye el endpoint CreateTranscription para Whisper API.
  • Element envia notas de voz como eventos m.room.message con msgtype: m.audio y contenido en formato OGG/Opus via URI mxc://.
  • La Whisper API de OpenAI acepta OGG/Opus directamente — no se necesita conversion de formato.
  • Limite de la Whisper API: 25 MB por archivo de audio.

Arquitectura

Patron pure core / impure shell

pkg/stt/types.go          PURO — interfaz Transcriber + tipos de resultado (solo datos)
shell/stt/whisper.go       IMPURO — implementacion OpenAI Whisper API
shell/stt/local.go         IMPURO — implementacion opcional whisper.cpp (subproceso)
shell/matrix/listener.go   IMPURO — deteccion de m.audio, descarga, orquestacion
shell/matrix/client.go     IMPURO — metodo DownloadMedia para URIs mxc://
pkg/decision/types.go      PURO — campos IsVoice, AudioDuration en MessageContext
internal/config/schema.go  PURO — seccion STTCfg en el schema de configuracion
devagents/runtime.go          COMPOSICION — inicializar transcriber, conectar con listener

Flujo de datos

Matrix event (m.audio)
  → listener detecta msgtype "m.audio"
  → extrae mxc:// URI, mimetype, duration
  → client.DownloadMedia(mxcURL) → []byte
  → transcriber.Transcribe(ctx, audioData, "ogg") → texto
  → MessageContext{Content: texto, IsVoice: true, AudioDuration: 15.0}
  → reglas normales → LLM → respuesta de texto

Archivos afectados

Archivo Accion Descripcion
pkg/stt/types.go NEW Interfaz Transcriber y tipo TranscriptionResult
shell/stt/whisper.go NEW Implementacion OpenAI Whisper API
shell/stt/local.go NEW Implementacion opcional whisper.cpp via subproceso
shell/matrix/listener.go MOD Detectar m.audio, descargar audio, orquestar transcripcion
shell/matrix/client.go MOD Añadir DownloadMedia(ctx, mxcURL) ([]byte, string, error)
pkg/decision/types.go MOD Añadir IsVoice bool, AudioDuration float64 a MessageContext
internal/config/schema.go MOD Añadir STTCfg al schema de configuracion
devagents/runtime.go MOD Inicializar Transcriber cuando STT esta habilitado, pasar al listener

Tareas

Fase 1 — Deteccion y descarga de audio

  • 1.1 Modificar listener.go en el handler OnEventType(event.EventMessage) para inspeccionar el campo msgtype del evento. Si es m.audio, extraer: url (URI mxc://), info.mimetype, info.duration (milisegundos).

  • 1.2 Implementar DownloadMedia(ctx context.Context, mxcURL string) ([]byte, string, error) en shell/matrix/client.go. Usa mautrix.Client.Download() para obtener el contenido binario desde la URI mxc://. Retorna los bytes, el mimetype detectado y error.

  • 1.3 Validar tamaño del audio antes de transcribir: rechazar archivos > 25 MB (limite de la Whisper API). Responder al usuario con mensaje explicativo si el audio es demasiado grande.

  • 1.4 Validar duracion del audio: rechazar si excede stt.max_duration del config (default 120 segundos). Responder al usuario con mensaje explicativo.

  • 1.5 Tests: mock de evento m.audio con campos esperados, verificar extraccion correcta de URI y metadata. Test de rechazo por tamaño y duracion.

Fase 2 — Speech-to-Text

  • 2.1 Definir tipos puros en pkg/stt/types.go:

    // Transcriber converts audio data to text. Pure interface, no I/O.
    type Transcriber interface {
        Transcribe(ctx context.Context, audio []byte, format string) (TranscriptionResult, error)
    }
    
    // TranscriptionResult holds the output of a transcription.
    type TranscriptionResult struct {
        Text       string
        Language   string
        Duration   float64
        Confidence float64
    }
    
  • 2.2 Implementar shell/stt/whisper.go — OpenAI Whisper API:

    • Usar github.com/sashabaranov/go-openai CreateTranscription
    • Modelo: whisper-1
    • Language hint desde config del agente (mejora la precision)
    • Escribir audio a archivo temporal (el SDK requiere filepath), limpiar despues
    • Manejar errores de la API con contexto descriptivo
  • 2.3 Implementar shell/stt/local.go — whisper.cpp via subproceso (opcional):

    • Ejecutar: whisper --model base --language es --output-format txt <tmpfile>
    • Parsear stdout como texto transcrito
    • Verificar que el binario existe al inicializar; si no, retornar error descriptivo
    • Util para desarrollo local y ahorro de costos
  • 2.4 Añadir STTCfg a internal/config/schema.go:

    type STTCfg struct {
        Enabled     bool   `yaml:"enabled"`
        Provider    string `yaml:"provider"`      // "openai" | "local"
        Model       string `yaml:"model"`         // e.g. "whisper-1"
        Language    string `yaml:"language"`       // ISO 639-1, e.g. "es"
        MaxDuration int    `yaml:"max_duration"`   // seconds, default 120
        APIKeyEnv   string `yaml:"api_key_env"`   // e.g. "OPENAI_API_KEY"
    }
    

    Añadir campo STT STTCfg \yaml:"stt"`aAgentConfig`.

  • 2.5 Factory function en shell/stt/:

    func NewTranscriber(cfg config.STTCfg) (stt.Transcriber, error)
    

    Selecciona implementacion segun cfg.Provider. Resuelve API key desde env var.

  • 2.6 Tests: transcriber con mock de API responses. Test del factory con providers validos e invalidos. Test de manejo de errores (API timeout, audio corrupto).

Fase 3 — Integracion en el pipeline

  • 3.1 Añadir a decision.MessageContext en pkg/decision/types.go:

    IsVoice       bool    // true if the message originated from a voice note
    AudioDuration float64 // duration in seconds of the original audio
    
  • 3.2 En listener.go: para eventos m.audio, ejecutar el flujo completo:

    1. Descargar audio via DownloadMedia
    2. Validar tamaño y duracion
    3. Transcribir via Transcriber
    4. Crear MessageContext con Content = texto transcrito, IsVoice = true, AudioDuration = duracion
    5. Pasar al handler normal (reglas → LLM)
  • 3.3 Opcional: enviar typing indicator "Transcribiendo..." mientras se procesa el audio. Mejora la UX para audios largos donde la transcripcion tarda 2-5 segundos.

  • 3.4 Inicializar Transcriber en devagents/runtime.go cuando cfg.STT.Enabled == true. Pasar la instancia al listener para que pueda usarla al recibir eventos de audio.

  • 3.5 Si STT no esta habilitado y llega un m.audio, responder con mensaje informativo: "No tengo habilitada la transcripcion de audio. Enviame un mensaje de texto."

Fase 4 — Tests y cleanup

  • 4.1 Tests unitarios de pkg/stt/types.go: verificar que el tipo cumple la interfaz (compile-time check).

  • 4.2 Test de integracion: mock transcriber → listener recibe evento m.audio → produce MessageContext correcto con IsVoice=true y texto transcrito.

  • 4.3 Test de regresion: verificar que mensajes de texto (m.text) siguen funcionando identicamente tras los cambios en el listener.

  • 4.4 Documentar la configuracion STT en un ejemplo dentro del config template (agents/_template/config.yaml) con la seccion comentada.

Ejemplo de uso

Config del agente

# agents/asistente-2/config.yaml
stt:
  enabled: true
  provider: openai
  model: whisper-1
  language: es
  max_duration: 120
  api_key_env: OPENAI_API_KEY

Flujo en Matrix

Usuario: [envia nota de voz de 15 segundos desde Element]
         "¿Cuál es el estado de los servidores?"

→ Agente descarga OGG desde mxc://matrix-af2f3d.organic-machine.com/audio123
→ Whisper API transcribe: "¿Cuál es el estado de los servidores?"
→ Pipeline normal: reglas match → LLM responde con estado de servidores

Bot: Los servidores están todos operativos. El último check de salud
     fue hace 5 minutos y todos los servicios reportan status OK.

Sin STT habilitado

Usuario: [envia nota de voz]

Bot: No tengo habilitada la transcripción de audio.
     Enviame un mensaje de texto por favor.

Decisiones de diseño

  1. OpenAI Whisper como provider primario: ya tenemos el SDK (go-openai) como dependencia. Whisper-1 tiene excelente calidad para español y acepta OGG/Opus directamente sin conversion. Costo accesible (~$0.006/minuto).

  2. whisper.cpp como alternativa local: para desarrollo, testing y escenarios donde se prefiere no enviar audio a APIs externas. Es opcional — si el binario no esta instalado, el provider local falla al inicializar con error claro.

  3. Texto transcrito entra al pipeline existente: no se crea un flujo paralelo para audio. La transcripcion produce texto que pasa por las mismas reglas y LLM que un mensaje escrito. Esto maximiza la reutilizacion y minimiza la complejidad.

  4. IsVoice flag en MessageContext: permite que las reglas o el LLM ajusten su comportamiento para mensajes de voz (por ejemplo, respuestas mas concisas, o confirmar lo que se escucho). No es obligatorio usarlo — el agente puede ignorarlo.

  5. Audio no se persiste: los bytes del audio se mantienen en memoria solo durante la transcripcion y se descartan inmediatamente despues. No se guardan en disco ni en la base de datos. Esto simplifica el manejo y evita problemas de almacenamiento.

  6. Interfaz Transcriber en pkg/stt/ (puro): la interfaz y los tipos de resultado son datos puros sin I/O, coherente con el patron del proyecto. Las implementaciones impuras viven en shell/stt/.

Prerequisitos

  • Ninguna dependencia nueva de Go — go-openai ya esta en go.mod y tiene CreateTranscription.
  • mautrix ya soporta descarga de contenido media via mxc:// URIs.
  • Para el provider local: whisper.cpp debe estar compilado e instalado en el PATH del servidor (solo si se usa ese provider).

Riesgos

Riesgo Mitigacion
OGG/Opus no soportado por Whisper API Whisper API acepta OGG nativamente. Si cambiara, añadir conversion con ffmpeg como paso intermedio
Latencia de transcripcion (2-5s para 30s de audio) Typing indicator mientras se procesa. El usuario ya espera latencia del LLM, la transcripcion añade poco overhead relativo
Precision de Whisper varia con ruido de fondo El agente recibe el mejor texto posible y responde normalmente. Language hint en config mejora resultados para español
Audio muy largo satura memoria Limite de 25 MB (hard, API) + max_duration configurable (soft, UX). Audio tipico de Element: <1 MB para 30 segundos
Costo de API ($0.006/minuto) Configurable — el admin puede desactivar STT o usar provider local gratuito. Limite de duracion previene abusos
Archivo temporal para el SDK Se escribe a os.TempDir(), se elimina con defer os.Remove(). Sin riesgo de leak si se maneja correctamente