Files
Qwen3-tts/STREAMING_API.md

148 lines
6.7 KiB
Markdown
Raw Normal View History

2026-06-17 08:33:47 +02:00
# Qwen3-TTS — Streaming-API (Token-Stream → Audio)
WebSocket-Endpoint, um Text **inkrementell** (z. B. Token für Token aus einem LLM)
zu senden und Audio **satzweise und chunked** zurückzubekommen. Der Server puffert
den einströmenden Text, segmentiert ihn an Satz-/Teilsatzgrenzen und synthetisiert
jeden Abschnitt, sobald er vollständig ist.
## Endpunkte & Auth
| | Lokal | Extern |
|---|---|---|
| WebSocket | `ws://localhost:8091/v1/audio/speech/stream` | `wss://qwen3-tts.aquantico.de/v1/audio/speech/stream` |
| REST (Vollausgabe) | `http://localhost:8091/v1/audio/speech` | `https://qwen3-tts.aquantico.de/v1/audio/speech` |
| Health / Modelle | `…/health`, `…/v1/models` | dito |
- **Auth (nur extern):** Header `Authorization: Bearer <TOKEN>`. Lokal ist kein Token nötig.
- **TLS (extern):** Das Zertifikat ist self-signed → Client-Zertifikatsprüfung ggf. deaktivieren
(`curl -k`, bzw. `ssl.CERT_NONE` in Python). Intern kein TLS.
- **Audioformat:** 24 000 Hz, mono, 16-bit PCM (little-endian). Bei `response_format: "wav"`
kommt pro Satz eine komplette WAV-Datei; bei `"pcm"` rohe PCM-Chunks (ohne Header).
## Protokoll
Eine WebSocket-Verbindung = eine Session. Ablauf:
### Client → Server (JSON-Textframes)
1. **`session.config`** — genau einmal, als allererste Nachricht (Timeout 10 s):
```json
{
"type": "session.config",
"model": "Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice",
"voice": "Ryan",
"language": "German",
"response_format": "pcm",
"stream_audio": true,
"split_granularity": "sentence"
}
```
2. **`input.text`** — beliebig viele; je Nachricht ein Token/Fragment/Satz (max. 128 KB je Frame):
```json
{"type": "input.text", "text": "Hallo"}
{"type": "input.text", "text": ", wie geht es "}
{"type": "input.text", "text": "Ihnen?"}
```
Der Server hängt alles an einen Puffer an und löst Synthese aus, **sobald eine
Satz-/Teilsatzgrenze erkannt wird**. Unvollständige Reste bleiben gepuffert.
Max. 30 s Pause zwischen zwei Nachrichten (idle timeout).
3. **`input.done`** — Eingabe beendet; verbleibender Pufferinhalt wird noch synthetisiert,
dann folgt `session.done`.
### Server → Client
| Nachricht | Inhalt |
|---|---|
| `audio.start` (JSON) | `{"type":"audio.start","sentence_index":0,"sentence_text":"…","format":"pcm","sample_rate":24000}` — Beginn eines Satzes. `sample_rate` nur bei PCM-Streaming. |
| *(Binärframe)* | Audio-Bytes. Bei `stream_audio:true` + `pcm` mehrere Chunks, die schon **während** der Generierung kommen. Bei `wav` ein vollständiger WAV-Block. |
| `audio.done` (JSON) | `{"type":"audio.done","sentence_index":0}` — Satz fertig. |
| `session.done` (JSON) | `{"type":"session.done","total_sentences":N}` — Session fertig. |
| `error` (JSON) | `{"type":"error","message":"…"}` |
## `session.config` — Felder
| Feld | Default | Beschreibung |
|---|---|---|
| `model` | — | Modell-ID (wird validiert). |
| `voice` | (modellabh.) | Eingebauter Sprecher, z. B. `Ryan`, `Aiden`, `Vivian` (CustomVoice). |
| `language` | `Auto` | z. B. `German`, `English`. |
| `task_type` | `CustomVoice` | `CustomVoice` \| `VoiceDesign` \| `Base`. |
| `instructions` | — | Natürlichsprachliche Stil-/Emotionssteuerung (z. B. „sachlich, ruhig"). |
| `response_format` | `wav` | `pcm` (für echtes Low-Latency-Streaming empfohlen) oder `wav`. |
| `stream_audio` | `false` | `true` = Audio-Chunks während der Generierung; `false` = ganzer Satz auf einmal. |
| `split_granularity` | `sentence` | `sentence` oder `clause` (an Kommas etc. → noch kleinere Stücke, niedrigere Latenz). |
| `speed` | `1.0` | Sprechtempo. |
| `max_new_tokens` | (modellabh.) | Obergrenze Token. |
| `initial_codec_chunk_frames` | (deploy-cfg) | Größe des ersten Audio-Chunks (Latenz-Tuning). |
| `ref_audio` / `ref_text` | — | Referenz für Voice-Clone (`Base`-Task; nur mit klon-fähigem Modell). |
| `speaker_embedding` | — | Fester 2048-dim-Stimmvektor (1.7B) für konstantes Timbre. |
| `x_vector_only_mode` | — | Klon nur über x-Vector (ohne ICL-Transkript). |
## Segmentierung & Stimm-Konsistenz
- Synthese erfolgt **pro Segment** (Satz bzw. Teilsatz). `split_granularity: "clause"`
senkt die Latenz, erzeugt aber mehr Segmentgrenzen.
- ⚠️ **Drift:** Jedes Segment wird unabhängig generiert; Stimmlage/Stimmung können zwischen
Segmenten leicht schwanken (gemessen ~11 % F0-Streuung). Temperatur senken hilft **nicht**.
Gegenmittel: (a) mehr Text pro Segment bündeln, (b) Timbre über einen festen
`speaker_embedding` verankern (Base-Modell nötig).
## Gemessene Latenz
| Pfad | Time-to-first-audio | Hinweis |
|---|---|---|
| Lokal, 1 Stream | ~0,16 s | ab Satzende |
| Lokal, 8 parallele Streams | ~0,23 s | TTFT lastunabhängig stabil |
| Extern (WSS, Token-Stream à 0,12 s) | ~1,6 s gesamt | inkl. Token-Eintrudeln + Netz; ~0,3 s ab Satzende |
## Python-Beispiel (Token-Stream → PCM)
```python
import asyncio, json, ssl, inspect, websockets
WSS = "wss://qwen3-tts.aquantico.de/v1/audio/speech/stream"
TOKEN = "<BEARER_TOKEN>"
def connect():
ctx = ssl.create_default_context()
ctx.check_hostname = False # self-signed cert
ctx.verify_mode = ssl.CERT_NONE
hdr = {"Authorization": f"Bearer {TOKEN}"}
params = inspect.signature(websockets.connect).parameters
key = "additional_headers" if "additional_headers" in params else "extra_headers"
return websockets.connect(WSS, max_size=None, ssl=ctx, **{key: hdr})
async def speak(token_iter):
async with connect() as ws:
await ws.send(json.dumps({
"type": "session.config",
"model": "Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice",
"voice": "Ryan", "language": "German",
"response_format": "pcm", "stream_audio": True,
}))
async def reader():
async for m in ws:
if isinstance(m, (bytes, bytearray)):
handle_pcm(m) # an Player/Buffer geben (24kHz mono s16le)
else:
msg = json.loads(m)
if msg["type"] == "session.done": return
if msg["type"] == "error": raise RuntimeError(msg["message"])
rt = asyncio.create_task(reader())
for tok in token_iter: # Tokens direkt aus dem LLM
await ws.send(json.dumps({"type": "input.text", "text": tok}))
await ws.send(json.dumps({"type": "input.done"}))
await rt
def handle_pcm(chunk: bytes):
... # z.B. sounddevice / Datei / Web-Client
```
## Wichtige Limits
- `session.config` muss innerhalb **10 s** kommen, sonst Abbruch.
- Max. **30 s** Pause zwischen Nachrichten (idle timeout).
- `session.config` ≤ 4 MB (für große `ref_audio`-Payloads), `input.text` ≤ 128 KB pro Frame.