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

223 lines
11 KiB
Markdown

# 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 <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`:
```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 |