52d5632d89
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>
223 lines
11 KiB
Markdown
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 |
|