# 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`: ```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 ` - 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`: ```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"\`` a `AgentConfig`. - [ ] **2.5** Factory function en `shell/stt/`: ```go 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`: ```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 ```yaml # 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 |