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