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>
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.goactualmente solo maneja mensajes de texto: extraebodydeevent.EventMessagey lo pasa comoContentalMessageContext. - 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-openaicomo dependencia, que incluye el endpointCreateTranscriptionpara Whisper API. - Element envia notas de voz como eventos
m.room.messageconmsgtype: m.audioy contenido en formato OGG/Opus via URImxc://. - 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.goen el handlerOnEventType(event.EventMessage)para inspeccionar el campomsgtypedel evento. Si esm.audio, extraer:url(URImxc://),info.mimetype,info.duration(milisegundos). -
1.2 Implementar
DownloadMedia(ctx context.Context, mxcURL string) ([]byte, string, error)enshell/matrix/client.go. Usamautrix.Client.Download()para obtener el contenido binario desde la URImxc://. 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_durationdel config (default 120 segundos). Responder al usuario con mensaje explicativo. -
1.5 Tests: mock de evento
m.audiocon 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-openaiCreateTranscription - 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
- Usar
-
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
- Ejecutar:
-
2.4 Añadir
STTCfgainternal/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.MessageContextenpkg/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 eventosm.audio, ejecutar el flujo completo:- Descargar audio via
DownloadMedia - Validar tamaño y duracion
- Transcribir via
Transcriber - Crear
MessageContextconContent = texto transcrito,IsVoice = true,AudioDuration = duracion - Pasar al handler normal (reglas → LLM)
- Descargar audio via
-
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
Transcriberendevagents/runtime.gocuandocfg.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
MessageContextcorrecto conIsVoice=truey 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
-
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). -
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
localfalla al inicializar con error claro. -
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.
-
IsVoiceflag 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. -
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.
-
Interfaz
Transcriberenpkg/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 enshell/stt/.
Prerequisitos
- Ninguna dependencia nueva de Go —
go-openaiya esta engo.mody tieneCreateTranscription. mautrixya soporta descarga de contenido media viamxc://URIs.- Para el provider
local:whisper.cppdebe 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 |