# Propuesta B — Microservicio de audio en Go

> Evaluación de reescribir `herandro-audio-service` en Go. Documento hermano de
> [`MIGRACION_AUDIO_MICROSERVICIO.md`](./MIGRACION_AUDIO_MICROSERVICIO.md), que define el
> contrato y las fases.
>
> Objetivos declarados: eficiencia, control de carga, control de errores, menor consumo de
> recursos, más velocidad de respuesta.

---

## 1. La misma advertencia que en las otras dos propuestas

**La inferencia no se acelera cambiando de lenguaje.** ONNX Runtime es C++ y se ejecuta
igual desde Go que desde Python. Lo que cambia es el proceso que la rodea: memoria base,
concurrencia, control de carga y predictibilidad.

Diferencia específica de Go frente a Rust: en Go **toda la interacción con ONNX Runtime pasa
por cgo**, y eso condiciona el resto del análisis.

---

## 2. Alcance

Igual que en la propuesta Rust: solo la **fase 1**, el servicio de síntesis sin estado. El
orquestador de podcast depende de LangChain/LangGraph y no tiene equivalente Go.

---

## 3. Stack equivalente

| Componente actual | Equivalente Go | Madurez |
|---|---|---|
| FastAPI + uvicorn | `net/http` (stdlib) o `chi` / `echo` | Alta — el HTTP de la stdlib de Go es de primera |
| `StreamingResponse` con heartbeat | `http.ResponseWriter` + `http.Flusher` | Alta, y más simple que en Python |
| pydantic | `encoding/json` + `go-playground/validator` | Media — sin validación declarativa por tipos como pydantic |
| pydantic-settings | `kelseyhightower/envconfig` o `viper` | Alta |
| onnxruntime (python) | [`yalue/onnxruntime_go`](https://github.com/yalue/onnxruntime_go) — **binding cgo** | Media |
| tokenizers (HF) | `daulet/tokenizers` — binding cgo **a la librería Rust** | Media |
| numpy | `gonum` o slices planos | Media — sin broadcasting; el código de tensores se escribe a mano |
| soundfile / WAV | `go-audio/wav` | Media-alta |
| librosa (resample) | Escaso. `faiface/beep` resamplea, pero no con la calidad de librosa | **Baja** |
| `ProcessPoolExecutor` | Goroutines + `os/exec` si se quiere aislamiento anticrash | Alta |
| `_synthesize_lock` | `golang.org/x/sync/semaphore` (pesado, por tokens) | Alta |
| Prometheus | `prometheus/client_golang` | Alta — es la implementación de referencia |
| logging | `log/slog` (stdlib desde 1.21) | Alta |
| espeak-ng | Shell-out al binario, o cgo | Media |
| requests / hf_hub_download | `net/http` + descarga a mano (no hay cliente HF oficial) | Media |

Para la fase 2: `pgx` (excelente), `redis/go-redis` (excelente), `aws-sdk-go-v2` (excelente).
Celery no tiene cliente Go mantenido —`gocelery` está estancado— pero el dynamic job runner
ya expone los jobs por HTTP, así que da igual.

---

## 4. Portabilidad provider por provider

| Provider | Portabilidad a Go | Coste |
|---|---|---|
| **piper** | **Trivial.** `os/exec`, mismo binario | Horas |
| **cosyvoice** | **Trivial.** Sigue siendo shell-out | Horas |
| **kokoro** | **Media.** No hay port Go conocido. Hay que escribirlo sobre `onnxruntime_go`: fonemización → IDs → tensor `tokens` + `style` + `speed`. La fonemización es el trozo feo (espeak-ng por FFI o shell-out) | 2-3 semanas |
| **chatterbox** | **Media-baja.** Portar 527 líneas de manipulación de tensores con KV-cache de 30 capas **sin numpy ni ndarray** significa escribir a mano indexado, reshape y concatenación sobre slices. Es donde Go pierde claramente contra Rust | 5-7 semanas |
| **supertonic** | **Baja.** No hay port; hay que reimplementar el paquete Python | Alta, o no se porta |
| **f5_tts** | **Inviable.** Es PyTorch. Go no tiene runtime de PyTorch | No se porta |

Mismo pivote que en Rust, y más acusado: **el catálogo activo decide.** Si hay algo más que
`kokoro` + `piper`, Go deja de ser sustituto y pasa a ser un segundo servicio conviviendo con
el Python.

---

## 5. Control de carga

Aquí Go es genuinamente bueno, y la ergonomía es mejor que la de Python sin llegar a la
composición declarativa de `tower`.

```go
sem := semaphore.NewWeighted(maxConcurrent)

// Rechazo inmediato en vez de cola infinita
if !sem.TryAcquire(1) {
    http.Error(w, `{"error":"servicio saturado"}`, http.StatusServiceUnavailable)
    return
}
defer sem.Release(1)

ctx, cancel := context.WithTimeout(r.Context(), 900*time.Second)
defer cancel()
```

Lo que Go aporta de serie y hoy no existe:

- **`context.Context` propagado de extremo a extremo.** Si el cliente corta la conexión, el
  `ctx` se cancela y la síntesis puede abortarse. En el código Python actual, un cliente que
  se va **no cancela nada**: el subproceso sigue quemando CPU hasta terminar. Esto es
  desperdicio puro de recursos y Go lo resuelve de forma idiomática.
- **`semaphore.Weighted`** permite pesos distintos por provider: Chatterbox pide 4 tokens,
  Kokoro 1. Es una forma limpia de expresar "cabe una síntesis pesada o cuatro ligeras".
- **Goroutines baratas**: la cola de espera no cuesta un hilo del SO por petición.

Caveat de cgo, y es importante: **una llamada cgo bloquea el hilo del SO que la ejecuta**
durante toda la inferencia. El runtime de Go lo compensa creando más hilos, pero se pierde
parte de la ventaja del scheduler y hay que fijar `GOMAXPROCS` con cuidado para no competir
con los threads internos de ORT. Con `CHATTERBOX_ONNX_NUM_THREADS = cpu_count()-1`, dejar a
Go crear hilos libremente es contraproducente.

---

## 6. Control de errores

| Hoy | En Go |
|---|---|
| `SIGSEGV` de ORT mata el subproceso aislado | **Un SIGSEGV dentro de cgo mata el proceso Go entero.** Go no puede contener un fallo nativo. Si se quiere el aislamiento actual, hay que mantener subprocesos igual que hoy |
| Sin timeout | `context.WithTimeout`, idiomático |
| `except Exception` que degrada en silencio | `if err != nil` explícito en cada llamada. Verboso, pero no se ignora sin escribirlo |
| Cliente desconectado no cancela nada | `ctx.Done()` propaga la cancelación |

El punto de los SIGSEGV es un empate técnico con Python, no una mejora: en ambos casos se
necesita aislamiento por proceso. Rust sí mejora aquí, Go no.

---

## 7. Consumo de recursos

| Concepto | Python hoy | Go |
|---|---|---|
| RSS del servidor en vacío | ~250-400 MB | ~20-40 MB |
| RSS por modelo | Igual (lo reserva ORT) | Igual |
| Overhead de GC | n/a (Python usa refcount + gc; hay un middleware que fuerza `gc.collect()` cada 10 requests, `main.py` stack nº 4) | GC concurrente, pausas sub-milisegundo. **Pero la memoria de ORT está fuera del heap de Go**, así que el GC no la ve ni la gestiona |
| Imagen Docker | ~6-8 GB | ~200-400 MB (binario + `libonnxruntime.so`) |
| Compilación | n/a | Segundos. Es la mejor experiencia de desarrollo de las tres |

El punto sutil: como la memoria de los modelos vive fuera del heap gestionado, el GC de Go
no ayuda a controlarla. `GOMEMLIMIT` tampoco la cubre. La gestión de residencia de modelos
hay que escribirla igual que hoy (TTL + desalojo explícito).

---

## 8. Velocidad de respuesta

Idéntico análisis que en Rust: la síntesis tarda lo mismo; la ganancia está en eliminar el
lock global y en no encolar sin límite. Go añade una ventaja propia — **la cancelación por
contexto** — que en un servicio donde una petición abandonada quema 50 s de CPU es
dinero real.

Añade también una desventaja propia: **la frontera cgo tiene coste por llamada**. Es
despreciable para un `session.run()` de decenas de segundos, pero no lo es si el código porta
el bucle de Chatterbox haciendo muchas llamadas pequeñas a ORT por token generado. Con 384
tokens máximos por chunk y 30 capas, eso son miles de cruces de frontera por segmento. Hay
que medirlo antes de comprometerse.

---

## 9. Límites y riesgos

1. **Ecosistema numérico pobre.** Es el límite de fondo. Portar el bucle de Chatterbox sin
   `ndarray`/numpy es escribir aritmética de tensores a mano. Más código, más superficie de
   bug, más difícil de comparar contra la referencia Python.
2. **cgo en todas partes.** ORT y tokenizers son cgo. Se pierde la compilación estática
   limpia, el `CGO_ENABLED=0` y parte de la portabilidad. La imagen necesita las `.so`.
3. **Un SIGSEGV nativo se lleva el proceso.** Obliga a conservar el aislamiento por
   subproceso, es decir, se conserva la complejidad que hoy justifica
   `process_isolation.py`.
4. **Resampling y DSP.** `librosa` no tiene equivalente. La mezcla de música de fondo
   (`bg_music_mixer.py`) y cualquier ajuste de sample rate hay que escribirlos o delegarlos a
   `ffmpeg`, que ya está en la imagen.
5. **Sin ecosistema HF.** No hay `hf_hub_download`; la descarga y el cacheo de pesos, con su
   verificación de ficheros corruptos y el reintento forzado
   (`chatterbox_worker.py:65,148-156`), se reescriben.
6. **Mismo riesgo de equipo** que Rust: un lenguaje más en el stack.

---

## 10. Esfuerzo

| Trabajo | Estimación |
|---|---|
| Andamiaje HTTP, config, métricas, contrato interno | 4-5 días |
| `piper` + `cosyvoice` | 2 días |
| `kokoro` (desde cero, con fonemización) | 2-3 semanas |
| `chatterbox` (port sin numpy) | **5-7 semanas** |
| Descarga/cacheo de pesos, mezcla de fondo, normalizador | 1,5 semanas |
| Control de carga, cancelación, errores | 1 semana |
| Validación de audio y carga | 1-2 semanas |
| **Total con Chatterbox** | **~3-4 meses** |
| **Total solo kokoro + piper** | **~5-6 semanas** |

---

## 11. Veredicto

**Go es la peor opción de las tres para la capa de inferencia, y la mejor para la capa de
orquestación.** Esa división es el resumen honesto.

- Para **inferencia**: el ecosistema numérico y de ML es el más débil de los tres, todo pasa
  por cgo, y un fallo nativo tumba el proceso. Todo lo que Go hace bien —concurrencia,
  simplicidad, despliegue— se aplica al servidor, no al modelo. No lo recomiendo.
- Para **orquestación** (la fase 2: máquina de estados del podcast, coordinación de fases,
  llamadas HTTP a la API, gestión de progreso en Redis): Go sería excelente. Goroutines,
  `context`, `errgroup` y el cliente Redis encajan perfectamente con ese problema. Pero esa
  fase depende de LangChain/LangGraph para las fases de texto, y eso ata a Python.

Si la motivación real es **control de carga y cancelación**, Go da el 80 % de esa ganancia
con menos ceremonia que Rust — pero también se consigue en Python con un semáforo, un
timeout y `request.is_disconnected()`, sin cambiar de lenguaje. Esa es la comparación que
hay que hacer antes de escribir la primera línea de Go.

---

## Fuentes

- [`onnxruntime_go`](https://github.com/yalue/onnxruntime_go)
- [`daulet/tokenizers`](https://github.com/daulet/tokenizers)
