# IndexTTS-2 Bilibili's emotion-controllable zero-shot TTS ([paper](https://arxiv.org/abs/2506.21619), [code](https://github.com/index-tts/index-tts), [weights](https://huggingface.co/IndexTeam/IndexTTS-2)) served behind our own thin FastAPI wrapper. ## Why this stack exists alongside the other two TTS | | CosyVoice 3 | Qwen3-TTS-1.7B-Base | **IndexTTS-2** | |---|---|---|---| | Voice cloning | ✅ | ✅ (`-Base` variant only) | ✅ | | English quality | medium (Chinese-leaning) | high (English-first) | medium (better than CosyVoice) | | Emotion control | `instruct` mode is Chinese-only | inline tags | **explicit: audio / 8-vector / text** | | Duration control | implicit | implicit | **explicit token-count mode** | | License | Apache 2.0 | Apache 2.0 | custom (Bilibili — free at our scale) | | Wrapper | bare CosyVoice CLI | groxaxo upstream FastAPI | ours, in this dir | The differentiator is **disentangled emotion**. IndexTTS-2 lets you clone a voice's timbre from one reference and the emotion from a different reference — or skip emotion-audio entirely and supply an 8-vector or a text description. Neither of the other two does this cleanly in English. ## API OpenAI-compat-ish: ```bash # List available voices + emotions curl http://10.100.79.3:8192/v1/voices # Basic synthesis (uses speaker WAV's natural emotion) curl -X POST http://10.100.79.3:8192/v1/audio/speech \ -H 'Content-Type: application/json' \ -d '{ "input": "I have all the time in the world.", "voice": "glados" }' > glados.wav # Same speaker, emotion taken from a separate reference WAV curl -X POST http://10.100.79.3:8192/v1/audio/speech \ -H 'Content-Type: application/json' \ -d '{ "input": "I have all the time in the world.", "voice": "glados", "emotion_voice": "menacing", "emotion_alpha": 0.9 }' > glados-menacing.wav # Same speaker, emotion as 8-vector # Order: happy, angry, sad, afraid, disgusted, melancholic, surprised, calm curl -X POST http://10.100.79.3:8192/v1/audio/speech \ -H 'Content-Type: application/json' \ -d '{ "input": "I have all the time in the world.", "voice": "glados", "emotion_vector": [0, 0.7, 0, 0, 0.2, 0, 0, 0] }' > glados-angry.wav # Same speaker, emotion derived from text by bundled QwenEmotion model curl -X POST http://10.100.79.3:8192/v1/audio/speech \ -H 'Content-Type: application/json' \ -d '{ "input": "I have all the time in the world.", "voice": "glados", "emotion_text": "she said with quiet menace" }' > glados-menacing.wav # Health curl http://10.100.79.3:8192/healthz ``` Output is always WAV (PCM_16, 22050 Hz mono — IndexTTS-2's native rate). `response_format` other than `wav` is rejected. ### Streaming (since 0.2.0) Add `"stream": true` to any request to stream the WAV as it generates. IndexTTS-2 emits one chunk per text segment (~120 tokens) as soon as it finishes synthesizing it; long inputs start playing while the rest is still being generated. ```bash # Pipe straight into a player. Time-to-first-audio drops from # whole-file latency to ~one-segment latency. curl -fsS -X POST http://10.100.79.3:8192/v1/audio/speech \ -H 'Content-Type: application/json' \ -d '{"input":"long passage of text...","voice":"glados","stream":true}' \ | mpv --no-cache - # Or save while playing (tee). curl -fsS -X POST http://10.100.79.3:8192/v1/audio/speech \ -H 'Content-Type: application/json' \ -d '{"input":"...","voice":"glados","stream":true}' \ | tee out.wav | mpv - ``` The streaming WAV uses a placeholder data-length in the header (`0xFFFFFFFF`) so chunks can be written before the total is known. Players that read until EOF (mpv, ffplay, aplay, sox, browsers via `