Files
esh-pfi-infrastructure/stacks/dots-tts
vh a80f6e958f fix(dots-tts): v3 — clause-break (; : em-dash) → period pause mapping
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).
2026-08-10 21:54:35 -07:00
..

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>.txt transcript) in the mounted voices dir, sourced from the voices/ canonical corpus via derive.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=True is incompatible with PYTORCH_CUDA_ALLOC_CONF=expandable_segments (CUDA-graph capture error) — don't set it.
  • Variants: dots.tts-mf (MeanFlow, faster) is a drop-in via DOTS_MODEL; soar is the quality pick and single-consumer doesn't need mf's batching.