dots' prosody honors a pause only for ellipsis (~+0.43s) and period (~+0.3s); comma/semicolon/colon/dash all run flat (~+0.03s vs no-punct), measured via a duration-over-N-runs pause probe against the live service. Two sub-causes for the flat clause reads: em-dashes regressed in v2 (the —→- fold made them read as word-joiners), and semicolons were never honored by dots at all. Operator ruled ellipsis "too much" → map semicolon, clause colon, and em-dash to a period in _sanitize (believable ~0.3s clause pause). Guards, pinned by tests: digit-guarded colon so times (3:45) and ratios (2:1) keep their colon; en-dash kept folding to hyphen so numeric ranges (10–20) don't become "10.20"; a genuine ellipsis retains its strong pause. Deployed to irv-ml1:8198 as local/dots-tts:v3 via the redeploy2 build → :8199-test → pause-gate → cutover pattern (gate measured +0.427s, live healthy).
dots-tts
OpenAI-compatible zero-shot voice-clone TTS over dots.tts (rednote-hilab) —
2B continuous-AR, native 48kHz, optimize=True CUDA graphs → RTF ~0.22 on
the irv-ml1 3090. Thin FastAPI wrapper around DotsTtsRuntime (chosen over SGLang
Omni: Omni's batching is MeanFlow-only and unneeded for a single consumer; the raw
runtime already streams at the same RTF and is ~100 lines we control).
- Host: irv-ml1, port 8198 (chatterbox-fast is :8197 — they co-reside on the 3090)
- Model:
dots-studio/dots.tts-soar, bf16, num_steps=10 - Voices: every
<name>.wav(+<name>.txttranscript) in the mounted voices dir, sourced from thevoices/canonical corpus viaderive.py dots.
API
GET /health -> {status, model, sample_rate, voices[]}
GET /v1/voices -> {voices[]}
POST /v1/audio/speech -> audio
body: {input, voice, response_format?("wav"|"pcm"), stream?}
stream:true returns a WAV stream (placeholder-header + PCM frames, 48kHz mono
s16le) — the same shape the Zonos/chatterbox consumers already handle. Non-stream
returns a complete WAV (or raw PCM with response_format:"pcm").
curl -X POST http://10.100.79.3:8198/v1/audio/speech \
-H 'Content-Type: application/json' \
-d '{"input":"Well, look who finally showed up.","voice":"glados"}' \
--output out.wav
Deploy
Reference sets come from the canonical corpus, not this stack — derive then point the mount at them:
# 1. produce dots refs from the corpus (on a box with the whisper venv):
python voices/derive.py dots # -> voices/derived/dots/*.{wav,txt}
# 2. build + run on irv-ml1 (cp .env.example .env first; adjust mounts):
scripts/deploy-stack.sh irv-ml1 dots-tts # or, on the host:
docker compose build && docker compose up -d
The model (~5GB) is not baked — it's read from the mounted HF_HOME
(DOTS_HFCACHE_DIR). First boot downloads it there if absent.
Voice cloning gotcha
dots.tts clones from (reference wav + its transcript) and leaks reference
audio into the output if the transcript is inaccurate or ends mid-clause. The
voices/ corpus + derive.py handle this (sentence-bounded trim + accurate
transcript); don't hand this server a raw reference wav without a matching .txt.
Notes
- GPU:
NVIDIA_VISIBLE_DEVICES=0= the 3090 in Docker (PCI order).optimize=Trueis incompatible withPYTORCH_CUDA_ALLOC_CONF=expandable_segments(CUDA-graph capture error) — don't set it. - Variants:
dots.tts-mf(MeanFlow, faster) is a drop-in viaDOTS_MODEL; soar is the quality pick and single-consumer doesn't need mf's batching.