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).
This commit is contained in:
@@ -111,7 +111,7 @@ no longer deployed sidecars here. See Recent decisions.)
|
||||
|
||||
_As of 2026-08-10 — long session; the dots.tts TTS-engine + LFM2.5 arcs LANDED (full detail per arc in Recent decisions). Genuinely in-flight is thin:_
|
||||
|
||||
- **dots.tts is the new fleet RP-TTS engine — LIVE + consumer-confirmed.** v2 on irv-ml1:8198 (curly-sanitize + sentence-chunking), ratatoskr migrated + verified clean end-to-end; chatterbox-fast RECLAIMED (:8197 stopped, revive `docker start chatterbox-fast`). Canonical voice corpus in `voices/`. ⚠ WATCH: deployed container GPU creeps ~6→14GB/8h (cache accumulation; a redeploy resets it); the **pinned Dockerfile is the only rebuild path** until upstream fixes the phantom `gradio==6.17.0` constraint. Full arc → Recent decisions.
|
||||
- **dots.tts is the new fleet RP-TTS engine — LIVE + consumer-confirmed.** v3 on irv-ml1:8198 (curly-sanitize + sentence-chunking + clause-break→period pause mapping), ratatoskr migrated + verified clean end-to-end; chatterbox-fast RECLAIMED (:8197 stopped, revive `docker start chatterbox-fast`). Canonical voice corpus in `voices/`. ⚠ WATCH: deployed container GPU creeps ~6→14GB/8h (cache accumulation; a redeploy resets it); the **pinned Dockerfile is the only rebuild path** until upstream fixes the phantom `gradio==6.17.0` constraint. Full arc → Recent decisions.
|
||||
|
||||
- **LFM2.5-2.6b bake-off alias for brokkr — DELIVERED, awaiting his bench.** `vllm-lfm25` ana-ml2:8021 + LiteLLM `lfm2.5-2.6b` (deepseek_r1 reasoning-parser, vendor sampling); PRODUCTION-CLEARED (operator <USD 10M ruling) but kept OUT of default/fallback routing per brokkr's measurement gate. Peer-side now.
|
||||
|
||||
@@ -132,6 +132,8 @@ _As of 2026-08-10 — long session; the dots.tts TTS-engine + LFM2.5 arcs LANDED
|
||||
|
||||
## Recent decisions
|
||||
|
||||
- `[2026-08-10]` **dots-tts v3 — clause-break → period pause mapping.** Operator: v2 "sounds good" but donut won't pause at semicolons/dashes. ROOT CAUSE (measured via a pause-probe A/B — synth duration over N runs, non-determinism averaged out): dots' prosody honors a real pause **only for ellipsis (~+0.43s) and period (~+0.3s, capitalization-independent)**; comma/semicolon/colon/dash all run **flat (~+0.03s vs no-punct)**. Two distinct sub-causes: **dashes regressed in v2** (the `—`→`-` fold made em-dashes read as word-joiners), while **semicolons were NEVER a v2 change** — dots ignores them natively, only newly noticeable because v2 made everything else clean. Operator call: ellipsis "too much" → **map `;`, clause `:`, and em-dash `—` → period** in `_sanitize` (believable ~0.3s clause break). GUARDS (pinned by 11 unit tests, `stacks/dots-tts/test_sanitize.py`): digit-guarded colon `(?<!\d)\s*:\s*(?!\d)` so times `3:45` / ratios `2:1` survive; en-dash `–`→hyphen KEPT (numeric-range `10–20` safety — em-dash breaks, en-dash ranges, different jobs); genuine ellipsis left at full strength (author meant a long pause). Gated deploy (redeploy2 pattern → v3): build → throwaway :8199 test container + **pause-gate** (semicolon sentence must run ≥0.12s longer than baseline; measured **+0.427s**) → only then cut live over. LIVE + healthy `local/dots-tts:v3` on :8198. **rollback = `sed -i 's/^DOTS_TAG=.*/DOTS_TAG=v2/' .env + docker compose up -d dots-tts`** (v2 image retained). Booth `dots-pauses` (A=old-flat / C=ellipsis-too-much / D=live-v3). [[reference_chatterbox_fast_repo]]
|
||||
|
||||
- `[2026-08-10]` **dots-tts v2 — contraction fix (curly-sanitize) + sentence-chunking + dependency-pin recovery.** Operator: donut read contractions wrong ("you're"→"you ree", "donut's"→"donut ess"). ROOT CAUSE (isolated via A/B booth): **curly/typographic apostrophes** (`’` U+2019 from ratatoskr's LLM) — dots' tokenizer mispronounces them; STRAIGHT apostrophes read clean under `normalize_text=True`. FIX (`app.py`): fold curly→ASCII (`str.maketrans`) before synth, **KEEP `normalize_text=True`** (operator call — retains number/date expansion). Also added **server-side sentence-chunking** (pack ≤280 chars): dots caps one `generate()` at ~500 patches/~40s, so long RP turns (the Zev monologue = 160s audio) truncated; chunking stitches them (verified full 160.3s, not 40s-cut). **⚠ BUILD FOOT-GUNS (both bit this redeploy):** (1) upstream dots.tts `constraints/recommended.txt` now pins **`gradio==6.17.0` — phantom, not on PyPI** → fresh `pip install dots.tts` unsatisfiable; FIX = pin `dots.tts==0.2.1` + **DROP** the `-c recommended.txt` constraints (0.2.1 pulls working gradio 6.17.3). (2) pinning only `torch==2.8.0` let **torchaudio float to 2.11.0 → dots.tts refuses to load** (minor-version match check); FIX = pin `torchaudio==2.8.0`. **⚠ DEPLOY LESSON:** `docker compose up -d` to a new tag swaps the LIVE container BEFORE any health check — a broken image crash-loops production (**ratatoskr TTS down ~1-2min this session**). NEW PATTERN = build → test in a THROWAWAY container on an alt port (:8199) → health+verify → only THEN cut live over (redeploy2.sh). v2 LIVE + healthy on irv-ml1:8198, **CONSUMER-CONFIRMED clean** (ratatoskr verified end-to-end on their :8765 — apostrophe string reads clean, /api/tts 200 @ 48kHz, no client change; the ~1-2min blip didn't hit them, their concurrent auto-audio issue was client-side localStorage). **rollback = `sed DOTS_TAG=v1 + docker compose up -d dots-tts`** (v1 image retained). Also: deployed container GPU crept ~6→13.9GB over 8h serving (cache accumulation; a redeploy resets it — watch item). [[reference_chatterbox_fast_repo]]
|
||||
|
||||
- `[2026-08-09→10]` **dots.tts (rednote-hilab) TTS burn-in on irv-ml1 + canonical voice corpus built (`voices/`).** Operator-directed eval to potentially replace chatterbox-fast. **dots.tts VERIFIED real** (canonical HF ns `dots-studio/`, `rednote-hilab/dots.tts-*` redirects there; Apache-2.0; PyPI `dots.tts` 0.2.1; 2B continuous-AR = semantic enc + Qwen2.5-1.5B LLM + flow-matching acoustic head over 48kHz AudioVAE; zero-shot clone from wav+transcript). **Runs on Ampere 3090** (sm_86, bf16, no fp8 dep); **optimized RTF 0.22** at num_steps=10 (`from_pretrained(..., optimize=True)` CUDA graphs — raw unoptimized was 1.21), **~6GB VRAM**, 48kHz, streams (`generate_stream`). Venv+cache at `irv-ml1:/home/lkraven/dots-tts` (~10GB). **Operator design calls:** SGLang Omni serving (OpenAI `/v1/audio/speech`), transcribe-refs-first, `soar` variant. ⚠ Omni serves soar but its continuous-batching + streaming opts are **mf-only** (soar = single-request) — non-issue for ratatoskr's single-consumer RP surface. **KEY FINDING — dots is highly sensitive to an accurate AND sentence-bounded reference transcript:** mismatched transcript → 0.16s collapse; over-long/messy transcript → reference-audio BLEEDS as an output prefix; mid-clause trim → dangling-word leak (glados "we'll", emmie "And,"). RECIPE (baked into `voices/derive.py`): trim ref to a clean ~6–10s clip ending on a sentence boundary + accurate transcript of exactly that clip. **CANONICAL VOICE CORPUS** stood up in eshpfi `voices/` (operator idea): engine-agnostic `canonical/<v>.wav` + `transcripts/<v>.txt` → per-engine ref sets DERIVED by `derive.py` reading `engines.yaml` profiles (dots/chatterbox/zonos); canonical wavs git-tracked (small/curated), `derived/` gitignored. **4 voices optimized + verified CLEAN for dots: donut, glados, emmie, miranda** (glados canonical is low-SR 16kHz — flagged upgrade candidate). ⚠ GPU GOTCHA: irv-ml1 native CUDA orders **A6000=device0** (ComfyUI-full) — pin the 3090 with `CUDA_DEVICE_ORDER=PCI_BUS_ID CUDA_VISIBLE_DEVICES=0`; and `PYTORCH_CUDA_ALLOC_CONF=expandable_segments` CONFLICTS with `optimize=True` CUDA graphs (curr_block error). Booths: `dots-vs-chatterbox`, `dots-voices-optimized`. **SHIPPED 2026-08-10:** operator A/B verdict "dots is very good" → containerized as a **thin FastAPI wrapper over DotsTtsRuntime** (chosen over SGLang Omni — Omni's batching is mf-only, unneeded for ratatoskr's single consumer; wrapper is SERIALIZED one-gen-at-a-time via a threading.Lock, Omni+mf = parked API-compatible escalation if multi-consumer ever lands). **LIVE on irv-ml1:8198** (`local/dots-tts:v1`, OpenAI `/v1/audio/speech` + `/health` + `/v1/voices`, container healthy, both stream + non-stream verified CLEAN, 4 voices donut/glados/emmie/miranda) alongside chatterbox :8197 (nothing repointed). Stack = `stacks/dots-tts/` (Dockerfile/app.py/compose/.env.example/README). ⚠ CONTAINER GOTCHA: `optimize=True` (torch.compile/inductor/triton) needs a **C compiler at RUNTIME** — slim image must `apt install build-essential` or model-load dies "Failed to find C compiler" (host venv had gcc ambient, masking it); persist `TORCHINDUCTOR_CACHE_DIR` to a mounted dir or every restart re-JITs ~5min. Corpus home = eshpfi `voices/` (operator ruled keep-here). **REMAINING: ratatoskr client cutover** to :8198 `/v1/audio/speech` (Phase-2 tail, peer-coupled — draft the ask). [[reference_chatterbox_fast_repo]] [[reference_zonos_tts_stack]] [[reference_verify_hf_repo_ids_before_pull]]
|
||||
|
||||
+12
-2
@@ -37,7 +37,7 @@ SAMPLE_RATE = 48000 # dots.tts fixed native output
|
||||
# expansion); the sanitize just removes the curly trigger the model chokes on.
|
||||
CURLY_MAP = str.maketrans({
|
||||
"’": "'", "‘": "'", "“": '"', "”": '"',
|
||||
"—": "-", "–": "-", "…": "...", " ": " ",
|
||||
"–": "-", "…": "...", " ": " ",
|
||||
})
|
||||
# dots caps a single generate() at ~500 audio patches (~40s). Long turns (RP
|
||||
# monologues) truncate without chunking, so split into <=CHUNK_MAX_CHARS pieces
|
||||
@@ -100,7 +100,17 @@ class SpeechRequest(BaseModel):
|
||||
|
||||
|
||||
def _sanitize(text: str) -> str:
|
||||
return text.translate(CURLY_MAP)
|
||||
"""Fold curly punctuation to ASCII, then map the clause breaks dots runs flat
|
||||
on (semicolon, clause colon, em-dash — each measured ~+0.03s vs no pause) to a
|
||||
period, which dots honors as a believable ~0.3s pause (ellipsis, at ~+0.43s,
|
||||
read as too much). Number contexts are guarded: times (3:45) and ratios (2:1)
|
||||
keep their colon, and en-dash ranges (folded to hyphen in CURLY_MAP) never
|
||||
become "10.20"."""
|
||||
text = text.translate(CURLY_MAP)
|
||||
text = re.sub(r"\s*;\s*", ". ", text) # semicolon -> period
|
||||
text = re.sub(r"(?<!\d)\s*:\s*(?!\d)", ". ", text) # clause colon (not 3:45)
|
||||
text = re.sub(r"\s*—\s*", ". ", text) # em-dash clause break
|
||||
return text
|
||||
|
||||
|
||||
def _chunk(text: str, max_chars: int = CHUNK_MAX_CHARS) -> list:
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
"""Unit tests for _sanitize — the text pre-fold dots.tts sees before synth.
|
||||
|
||||
app.py imports dots_tts.runtime at module load (heavy, GPU-only), so we stub it
|
||||
before import; _sanitize itself is pure and needs no model.
|
||||
|
||||
Behavior under test (v3 clause-pause mapping):
|
||||
* dots runs flat on ; : and em-dash (measured ~+0.03s vs none); a period gives
|
||||
a believable ~0.3s clause pause. So map those clause breaks -> period.
|
||||
* Guard number contexts: times (3:45) and ratios (2:1) keep their colon;
|
||||
en-dash ranges (10-20) must NOT become "10.20". En-dash -> hyphen (as v2).
|
||||
* Curly-apostrophe fix (the v2 reason this map exists) stays intact.
|
||||
* A genuine ellipsis keeps its strong pause (-> "...").
|
||||
"""
|
||||
import sys
|
||||
import types
|
||||
|
||||
# Stub the GPU-only runtime import so app.py loads on a CPU test box.
|
||||
_stub = types.ModuleType("dots_tts.runtime")
|
||||
_stub.DotsTtsRuntime = object # type: ignore[attr-defined]
|
||||
sys.modules.setdefault("dots_tts", types.ModuleType("dots_tts"))
|
||||
sys.modules["dots_tts.runtime"] = _stub
|
||||
|
||||
import app # noqa: E402
|
||||
|
||||
s = app._sanitize
|
||||
|
||||
|
||||
def test_semicolon_becomes_period():
|
||||
assert s("I waited; you left") == "I waited. you left"
|
||||
|
||||
|
||||
def test_clause_colon_becomes_period():
|
||||
assert s("the truth: nobody knew") == "the truth. nobody knew"
|
||||
|
||||
|
||||
def test_time_colon_preserved():
|
||||
# 3:45 must not become 3.45 ("three point four five")
|
||||
assert "3:45" in s("meet me at 3:45 sharp")
|
||||
|
||||
|
||||
def test_ratio_colon_preserved():
|
||||
assert "2:1" in s("the odds were 2:1 against")
|
||||
|
||||
|
||||
def test_em_dash_becomes_period_spaced():
|
||||
assert s("you came — how touching") == "you came. how touching"
|
||||
|
||||
|
||||
def test_em_dash_becomes_period_unspaced():
|
||||
assert s("you came—how touching") == "you came. how touching"
|
||||
|
||||
|
||||
def test_en_dash_range_preserved_not_period():
|
||||
out = s("wait 10–20 minutes")
|
||||
assert "10.20" not in out # the corruption we're guarding against
|
||||
assert "10-20" in out # en-dash folds to hyphen (v2 behavior)
|
||||
|
||||
|
||||
def test_curly_apostrophe_folds():
|
||||
# the original v2 bug: curly ' made "Donut's" -> "donut ess"
|
||||
assert s("Donut’s treat") == "Donut's treat"
|
||||
|
||||
|
||||
def test_curly_quotes_fold():
|
||||
assert s("“hi” there") == '"hi" there'
|
||||
|
||||
|
||||
def test_ellipsis_keeps_strong_pause():
|
||||
assert s("wait… now") == "wait... now"
|
||||
|
||||
|
||||
def test_no_doubled_spaces_introduced():
|
||||
assert " " not in s("a ; b : c — d")
|
||||
Reference in New Issue
Block a user