# Trebe Voice EU — API de locución en euskera en streaming

API de texto → voz **en euskera** con clonación de voz **zero-shot**. El audio se transmite
según se genera: el cliente puede empezar a reproducir en cuanto llega el primer fragmento
(≈0,4-0,7 s), sin esperar a la locución completa.

- **WebSocket** `ws://HOST:5055/v1/stream` — protocolo `trebe-voice-stream/1`: varios streams
  multiplexados por conexión, texto completo o incremental (p. ej. tokens de un LLM),
  pausa, reanudación, cancelación y barge-in.
- **HTTP** `POST|GET /v1/tts/stream` — una respuesta chunked con PCM o WAV.
- **REST** `/v1/streams/...` — listar, inspeccionar y controlar cualquier stream (WS o HTTP).

Motor: [Pocket TTS Basque](https://huggingface.co/itzune/pocket-tts-basque) (Itzune, sobre Kyutai
Pocket TTS), entrenado solo con euskera. Es autoregresivo: emite audio cada 80 ms según lo genera.

## Cómo se consigue la baja latencia

1. **Segmenta** el texto en frases (`segmentation: "sentence"`, por defecto).
2. Cada frase se pide al motor, que **devuelve el audio en trozos según lo genera**.
3. Las frases se encadenan con **prefetch**: la siguiente se pide en cuanto el motor queda libre.
4. Una **cola con prioridad** reparte el motor entre streams simultáneos (`priority` y orden de
   llegada), de modo que varios streams se intercalan frase a frase.

## Formato de audio

PCM lineal **int16 little-endian, 24 000 Hz, mono** (48 000 bytes por segundo). Se anuncia en el
mensaje `ready` del WebSocket, en `GET /v1/config` y en las cabeceras `X-Audio-*` de HTTP.

---

## Parámetros

Se usan igual en el mensaje `start` del WebSocket, en el cuerpo JSON de `POST /v1/tts/stream`
y en la query string de `GET /v1/tts/stream`. Todos son opcionales salvo el texto.

### Texto y voz

| Parámetro | Tipo | Descripción |
|---|---|---|
| `text` (alias `input`) | string | Texto en euskera. Obligatorio en modo `full`. Las cifras, porcentajes y € se pasan a palabras (ver `normalize`); las siglas deben escribirse como se pronuncian. |
| `voice` | string | Voz del modelo (`Maider`, `Antton`, `Ane`, `Aitor`, `Nahia`), perfil clonado (`clone:<id>`, o el id suelto) o `auto` (voz por defecto de la configuración, `Maider`). Ver `GET /v1/voices`. |
| `ref_audio_b64` | string | Clonación one-shot: audio de referencia en base64 (WAV/MP3/FLAC/OGG, 5-10 s de habla limpia en euskera). No necesita transcripción. |
| `ref_audio_name`, `ref_audio_type` | string | Nombre y MIME del audio de referencia. |
| `language` | string | Opcional; si se envía debe ser `eu`. |

### Generación

| Parámetro | Rango | Defecto | Descripción |
|---|---|---|---|
| `temperature` | 0-1.5 | 0.3 | Variabilidad de la entonación. Más alta: más expresiva y algo menos estable. |
| `seed` | ≥0 | — | Semilla: con la misma semilla, el mismo resultado. |
| `normalize` | bool | true | Cifras a palabras: `2024an` → «bi mila eta hogeita lauan», `3,5` → «hiru koma bost», `%30` → «ehuneko hogeita hamar», `15€` → «hamabost euro», `1.250.000` → «milioi bat berrehun eta berrogeita hamar mila». |

### Opciones del stream

| Parámetro | Valores | Defecto | Descripción |
|---|---|---|---|
| `id` | `[\w.:-]{1,64}` | autogenerado | Identificador del stream (útil para controlarlo por REST desde el inicio). |
| `label` | string | — | Etiqueta libre que se muestra en listados. |
| `mode` | `full` \| `incremental` | `full` si hay texto | `incremental`: el texto se envía después, a trozos. |
| `segmentation` | `sentence` \| `none` | `sentence` | `none`: una sola petición al motor (se trocea solo si supera 10 000 caracteres). |
| `min_segment_chars` | 1-2000 | 30 | Longitud mínima de un segmento (las frases cortas se agrupan). |
| `max_segment_chars` | 50-10000 | 300 | Longitud máxima (corta en coma/espacio). |
| `prefetch` | 0-4 | 1 | Segmentos que se piden por adelantado. |
| `priority` | -10…10 | 0 | Prioridad en la cola del motor. |
| `pacing` | `burst` \| `realtime` | `burst` | `realtime`: el servidor no se adelanta más de `prebuffer_ms` al tiempo real (útil para clientes sin buffer, telefonía, o para que cancelar tenga efecto inmediato en el servidor). |
| `prebuffer_ms` | 0-30000 | 500 | Margen de adelanto en modo `realtime`. |
| `idle_timeout_s` | 0-3600 | 60 | Modo incremental: cierra la entrada tras ese tiempo sin texto (0 = nunca). |
| `interrupt` | bool | false | (WS) Al iniciar, cancela los demás streams de la sesión (barge-in). |
| `audio_transport` | `binary` \| `base64` | `binary` | (WS) Formato del audio en el WebSocket. |
| `format` | `pcm` \| `wav` | `pcm` | (HTTP) Formato de la respuesta. |

---

## WebSocket — protocolo `trebe-voice-stream/1`

Conexión: `ws://HOST:5055/v1/stream` (`?token=...` si el servidor tiene `API_TOKEN`).
Los mensajes de control son JSON de texto. El audio llega en **frames binarios**:

```
 0       4       8                       N
 ├───────┼───────┼───────────────────────┤
 │ sid   │ seq   │ PCM int16 LE          │
 │u32 LE │u32 LE │                       │
```

`sid` es el identificador numérico del stream (se comunica en `started`) y `seq` el número de
chunk dentro del stream (1, 2, 3…). Con `audio_transport: "base64"` el audio llega como JSON
`{type:"audio", id, sid, seq, segment, data}`.

Todo mensaje del cliente puede llevar `req_id`; las respuestas directas (`started`, `ack`,
`streams`, `stats`, `pong`, `error`) lo devuelven.

### Cliente → servidor

| `type` | Campos | Efecto |
|---|---|---|
| `start` | parámetros + opciones | Abre un stream. Respuesta: `started`. |
| `text` | `id`, `text`, `flush?`, `end?` | Añade texto a un stream incremental. |
| `flush` | `id` | Sintetiza ya el texto pendiente aunque no acabe en frase. |
| `end` | `id` | Cierra la entrada: se sintetiza lo pendiente y el stream termina al acabar. |
| `pause` / `resume` | `id` | Detiene / reanuda el **envío** de audio (la generación continúa en segundo plano, así que reanudar es inmediato). |
| `cancel` | `id` o `all: true`, `reason?` | Corta el stream: aborta las peticiones al motor y deja de enviar audio. |
| `list` | `history?` | Streams de esta sesión. |
| `stats` | | Estadísticas del servidor. |
| `watch` | `enabled` | Suscribe a `monitor`: estado de **todos** los streams del servidor (cada ~1 s y en cada cambio). |
| `ping` | `t?` | Respuesta `pong` con `server_time`. |

### Servidor → cliente

| `type` | Campos | Cuándo |
|---|---|---|
| `ready` | `session_id`, `protocol`, `language`, `default_voice`, `audio`, `binary_header`, `limits` | Al conectar. |
| `started` | `id`, `sid`, `mode`, `audio`, `options`, `params` (con la `voice` resuelta) | Stream aceptado. |
| `queued` | `position` | Esperando hueco en el motor. |
| `state` | `state` | `queued` → `running` ⇄ `paused`. |
| `segment_start` | `index`, `text`, `chars` | Empieza a emitirse un segmento. |
| `first_audio` | `latency_ms` | Primer audio del stream (desde `start`). |
| `segment_end` | `index`, `bytes`, `duration_s`, `queued_ms`, `first_byte_ms`, `gen_ms` | Segmento completo. |
| `input_closed` | `segments` | La entrada de texto se ha cerrado. |
| `done` | `stats` | Terminado con éxito. |
| `cancelled` | `reason`, `stats` | Cortado (`client`, `interrupted`, `disconnected`, `api`…). |
| `error` | `code`, `message`, `status?`, `id?` | Error. Si lleva `sid` es el stream el que ha fallado; si no, es un error de la petición. |
| `ack` | `req_id` | Confirmación de `text`/`flush`/`end`/`pause`/`resume`/`cancel` (solo si se envió `req_id`). |
| `monitor` | `streams`, `stats` | Con `watch` activo. |

`stats`: `first_audio_ms`, `elapsed_ms`, `bytes`, `chunks`, `audio_s`, `segments`, `segments_done`, `input_chars`.

### Ejemplo

```jsonc
// → cliente
{"type":"start","id":"s1","text":"Kaixo. Zer moduz zaude?","voice":"Maider"}
// ← servidor
{"type":"started","id":"s1","sid":1,"mode":"full","params":{"voice":"Maider","preset":"Maider"}, ...}
{"type":"segment_start","id":"s1","sid":1,"index":0,"text":"Kaixo. Zer moduz zaude?"}
{"type":"state","id":"s1","sid":1,"state":"running"}
{"type":"first_audio","id":"s1","sid":1,"latency_ms":420}
<binario sid=1 seq=1 PCM…> <binario sid=1 seq=2 PCM…> …
{"type":"segment_end","id":"s1","sid":1,"index":0,"bytes":96000,"duration_s":2.0, ...}
{"type":"done","id":"s1","sid":1,"stats":{...}}
```

Texto incremental (p. ej. salida de un LLM):

```jsonc
{"type":"start","id":"llm","mode":"incremental","voice":"clone:Mikel"}
{"type":"text","id":"llm","text":"Kaixo, zer moduz? Gaur "}
{"type":"text","id":"llm","text":"eguraldi ona dago."}
{"type":"end","id":"llm"}
```

Una frase se sintetiza en cuanto se detecta su final (`.`, `!`, `?`, `…`, `;`, `:` seguidos de
espacio, o salto de línea) y supera `min_segment_chars`.

Cliente de navegador listo para usar: [`/static/trebe-voice-client.js`](../public/trebe-voice-client.js)
y reproductor con pausa/corte instantáneos: [`/static/pcm-player.js`](../public/pcm-player.js).
Ejemplo en Node: [`examples/node-client.js`](../examples/node-client.js).

---

## HTTP

### `POST /v1/tts/stream`

Cuerpo JSON con los mismos parámetros. Respuesta `200` chunked (`audio/pcm` o `audio/wav`
con `format: "wav"`) con cabeceras `X-Stream-Id`, `X-Voice`, `X-Audio-Sample-Rate`, `X-Audio-Channels`,
`X-Audio-Bit-Depth`, y el trailer `X-Stream-State` (`done` | `cancelled`) al terminar.

Las cabeceras se envían con el primer audio, de modo que si el motor falla antes se recibe el
código real (`400`, `404` voz inexistente, `422`, `502`…) con `{"error":{"code","message"}}`.
Si se cancela antes de emitir audio: `409`. Cerrar la conexión cancela el stream.

```bash
curl -N -X POST http://localhost:5055/v1/tts/stream \
  -H 'Content-Type: application/json' \
  -d '{"text":"Kaixo mundua. Hau streaming bidezko audioa da.","voice":"Maider","format":"wav"}' \
  -o hola.wav
```

Con `mode: "incremental"` (y un `id` propio) la respuesta queda abierta y el texto se añade con
`POST /v1/streams/:id/text`.

### `GET /v1/tts/stream?text=...&voice=...&format=wav`

Igual, con los parámetros en la query. Sirve directamente como `src` de un `<audio>`:

```html
<audio controls src="/v1/tts/stream?text=Kaixo%20mundua&voice=Maider&format=wav"></audio>
```

## REST

| Método y ruta | Descripción |
|---|---|
| `GET /v1/streams?active=1&session=` | Lista de streams (activos + historial reciente, o solo activos). |
| `GET /v1/streams/:id` | Detalle: estado, voz, segmento actual, estadísticas, parámetros. |
| `DELETE /v1/streams/:id` · `POST /v1/streams/:id/cancel` | Cancela. |
| `DELETE /v1/streams` | Cancela todos. |
| `POST /v1/streams/:id/pause` · `/resume` | Pausa / reanuda el envío. |
| `POST /v1/streams/:id/text` | `{"text", "flush?", "end?"}` — añade texto (modo incremental). |
| `POST /v1/streams/:id/flush` · `/end` | Fuerza la síntesis de lo pendiente / cierra la entrada. |
| `GET /v1/voices` | Catálogo: voces del modelo (`Maider`…) y perfiles clonados (`clone:<id>`), y la voz por defecto. |
| `POST /v1/voices/profiles` | `{"profile_id","ref_audio_b64","ref_audio_type?","gender?","description?","overwrite?"}` — crea un perfil de clonación (se guarda en `voices/<id>/`). |
| `DELETE /v1/voices/profiles/:id` | Elimina un perfil. |
| `GET /v1/health` | Estado del motor y del servidor (`503` si el motor no está listo). |
| `GET /v1/stats` | Streams activos, segmentos en curso / en cola, totales. |
| `GET /v1/config` | Idioma, modelo, formato de audio, voz por defecto, límites y si requiere token. |

Otras rutas: `/` consola de pruebas · `/docs` esta documentación · `/docs/curl` ejemplos con curl · `/licenses` avisos de terceros.

## Perfiles de voz (clonación zero-shot)

Un perfil es una carpeta `voices/<id>/` con `ref.wav` (5-10 s de habla limpia **en euskera**) y
`meta.json` (`gender`, `description`). Se crean con `POST /v1/voices/profiles`, desde la consola
(pestaña «Clonar») o copiando la carpeta. El motor cachea el estado de cada voz: solo la primera
locución con un perfil paga su extracción (~0,5 s). La referencia define también el estilo: un
audio con ruido, música o eco se reproduce en la locución; si una voz sale mal, prueba con otro
fragmento o uno más corto.

## Configuración del servidor

Fichero `config/trebe-voice-eu.json`. Las variables de entorno tienen prioridad.

| Clave | Variable | Defecto | |
|---|---|---|---|
| `host` | `HOST` | `0.0.0.0` | Interfaz de escucha. `127.0.0.1` para aceptar solo conexiones locales. |
| `port` | `PORT` | 5055 | Puerto HTTP/WS. |
| `apiToken` | `API_TOKEN` | — | Si se define, exige `Authorization: Bearer <token>` o `?token=` en `/v1` y WebSocket. |
| `defaultVoice` | `DEFAULT_VOICE` | `Maider` | Voz para `voice: "auto"`: del modelo o un perfil (`Mikel`). |
| `maxStreams` | `MAX_STREAMS` | 64 | Streams activos en todo el servidor. |
| `maxStreamsPerSession` | `MAX_STREAMS_PER_SESSION` | 16 | Streams activos por conexión WS. |
| `engine.variant` | — | `6l` | `24l`: modelo de 24 capas, más natural y más lento, solo clonación. |
| `engine.device` | `ENGINE_DEVICE` | `cpu` | `cpu` o `cuda` (requiere instalar PyTorch con CUDA en `engine/env`). |
| `engine.threads` | — | 0 | Hilos de CPU del motor (0 = la mitad de los núcleos, máx. 8). |
| `engine.port` | — | 5056 | Puerto interno del motor (solo 127.0.0.1). Evita 5060/5061: `fetch` los bloquea. |
| `engine.offline` | — | `false` | No descargar nada al arrancar (el modelo ya debe estar en `engine/cache`). |
| `engine.url` | `ENGINE_URL` | — | Usar un motor externo (otra máquina con `engine/launcher.py`) en vez del local. |

Logs: carpeta `logs/` (`trebe-voice-eu.log` y `engine.log`).

## Licencias de terceros

Pesos del modelo: CC BY 4.0 (Itzune; voces Maider y Antton del corpus HiTZ-Aholab). Código de
Pocket TTS: MIT. Detalle y avisos en la carpeta [`LICENSES`](../LICENSES/README.md).
