From 10d379db5b27591fef1f0e5cbdb00b5cc15e1a06 Mon Sep 17 00:00:00 2001 From: Vuong Hoang Date: Mon, 10 Aug 2026 09:11:56 -0700 Subject: [PATCH] =?UTF-8?q?fix(dots-tts):=20v2=20=E2=80=94=20curly-punctua?= =?UTF-8?q?tion=20sanitize=20+=20sentence-chunking?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Curly apostrophes (ratatoskr's LLM emits typographic punctuation) made dots mispronounce contractions ("Donut's"->"donut ess"); fold curly->ASCII before synth, keep normalize_text on. Add server-side sentence-chunking so long turns stop truncating at dots' ~40s single-generate cap (verified full 160s Zev). Dockerfile: pin dots.tts==0.2.1 + torch/torchaudio==2.8.0 (upstream constraints now pin a phantom gradio==6.17.0; float torchaudio->2.11.0 crashes the load). --- persistent-memory.md | 2 ++ stacks/dots-tts/.env.example | 6 +++- stacks/dots-tts/Dockerfile | 13 ++++--- stacks/dots-tts/app.py | 66 +++++++++++++++++++++++++++++++++--- 4 files changed, 77 insertions(+), 10 deletions(-) diff --git a/persistent-memory.md b/persistent-memory.md index 7876314..f248c21 100644 --- a/persistent-memory.md +++ b/persistent-memory.md @@ -130,6 +130,8 @@ _As of 2026-08-08 — long session; all major arcs LANDED (full detail per arc i ## Recent decisions +- `[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; **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/.wav` + `transcripts/.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]] - `[2026-08-08]` **worldtree-dev #400 CLOSED → fiction-decomp snapshot cleared from nh3-dev.** worldtree-dev signaled #400 done (shipped v1.0.0b185; exact-lexical efficacy 79%→12% on ratatoskr's gate, brokkr no-harm bracket green both ends; the snapshot served 4 probe rounds — rank decomposition, promoted-vs-gold annotation, tie-set falsification, A0/A1/A2 mechanism probe). Cleared `~/snapshots/worldtree-400-fiction-decomp` (208M: chroma + manifest/provenance/stamp) — a read-only rsync copy of PERSONAL Worldtree's Chroma (source on corviduo-dev, so safe to remove). **LEFT INTACT:** `rex393-fiction-index`/`rex393-fiction-snapshot` (separate operator KEEP word, unchanged) + `r42-gate-*`. No config deltas rode this train. Only remaining non-blocking await = ratatoskr-dev's chatterbox-fast knob revert. Replied confirming (`01KZJ9GMCC…`). diff --git a/stacks/dots-tts/.env.example b/stacks/dots-tts/.env.example index 8216c5a..69a5331 100644 --- a/stacks/dots-tts/.env.example +++ b/stacks/dots-tts/.env.example @@ -1,7 +1,10 @@ # dots-tts stack tunables. Copy to `.env` on irv-ml1 before deploying. # ── image ──────────────────────────────────────────────────────────── -DOTS_TAG=v1 +# v2 (2026-08-10): curly->ASCII sanitize (fixes "Donut's"->"donut ess" on +# typographic apostrophes) + server-side sentence-chunking (long turns no longer +# truncate at dots' ~40s single-generate cap). +DOTS_TAG=v2 # ── network ────────────────────────────────────────────────────────── DOTS_BIND=0.0.0.0 @@ -17,6 +20,7 @@ DOTS_MODEL=dots-studio/dots.tts-soar DOTS_DEFAULT_VOICE=donut DOTS_NUM_STEPS=10 # 10 = full quality @ RTF ~0.22; lower = faster/rougher DOTS_GUIDANCE_SCALE=1.2 +DOTS_CHUNK_MAX_CHARS=280 # max chars per generate() chunk (dots caps ~40s/~500 patches) # ── host mounts ────────────────────────────────────────────────────── # HF cache holding the downloaded soar snapshot (~5GB). Reuse the burn-in cache. diff --git a/stacks/dots-tts/Dockerfile b/stacks/dots-tts/Dockerfile index adfbb43..a74c1ff 100644 --- a/stacks/dots-tts/Dockerfile +++ b/stacks/dots-tts/Dockerfile @@ -12,11 +12,14 @@ RUN pip install --no-cache-dir uv WORKDIR /app -# Pin torch + deps to dots.tts upstream recommended constraints (the same set the -# irv-ml1 venv installed against). ADD caches on the URL contents. -ADD https://raw.githubusercontent.com/rednote-hilab/dots.tts/main/constraints/recommended.txt /tmp/rec.txt -RUN uv pip install --system -c /tmp/rec.txt \ - dots.tts soundfile fastapi "uvicorn[standard]" +# Pin the proven-working version set (captured from the running v1 image). +# NOT using upstream constraints/recommended.txt: as of 2026-08-10 it pins +# gradio==6.17.0, which does not exist on PyPI and makes a fresh resolve +# unsatisfiable (upstream regression). dots.tts 0.2.1 pulls a working gradio +# (6.17.3) on its own; torch/numpy/soundfile pinned to the v1-image versions. +RUN uv pip install --system \ + dots.tts==0.2.1 torch==2.8.0 torchaudio==2.8.0 numpy==2.2.6 soundfile==0.13.1 \ + fastapi "uvicorn[standard]" # C compiler for the RUNTIME (not build): optimize=True drives torch.compile / # inductor / triton, which JIT-compile kernels via gcc on model load. Without it diff --git a/stacks/dots-tts/app.py b/stacks/dots-tts/app.py index 392d0d2..45e7383 100644 --- a/stacks/dots-tts/app.py +++ b/stacks/dots-tts/app.py @@ -12,6 +12,7 @@ is that transcript; without it the model leaks reference audio into the output. import io import os import glob +import re import struct import threading import wave @@ -30,6 +31,20 @@ NUM_STEPS = int(os.environ.get("DOTS_NUM_STEPS", "10")) GUIDANCE = float(os.environ.get("DOTS_GUIDANCE_SCALE", "1.2")) SAMPLE_RATE = 48000 # dots.tts fixed native output +# ratatoskr's LLM emits typographic (curly) punctuation, and dots' tokenizer +# mispronounces curly apostrophes ("Donut's" -> "donut ess"). Fold curly -> ASCII +# before synthesis. normalize_text stays ON (operator call — keeps number/date +# 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 +# on sentence (then clause) boundaries and stitch. A short input is one chunk = +# unchanged behavior. +CHUNK_MAX_CHARS = int(os.environ.get("DOTS_CHUNK_MAX_CHARS", "280")) + app = FastAPI(title="dots.tts") _rt = None _voices: dict = {} @@ -84,6 +99,39 @@ class SpeechRequest(BaseModel): stream: bool = False +def _sanitize(text: str) -> str: + return text.translate(CURLY_MAP) + + +def _chunk(text: str, max_chars: int = CHUNK_MAX_CHARS) -> list: + """Pack sentences into <=max_chars chunks (sub-splitting an over-long sentence + on commas) so each generate() stays under dots' ~40s cap. One chunk for short + input.""" + text = text.strip() + if len(text) <= max_chars: + return [text] + sentences = re.split(r"(?<=[.!?])\s+", text) + chunks, cur = [], "" + for s in sentences: + s = s.strip() + if not s: + continue + pieces = [s] + if len(s) > max_chars: # rare: a single sentence over the cap + pieces = [p.strip() for p in re.split(r"(?<=,)\s+", s) if p.strip()] + for p in pieces: + if not cur: + cur = p + elif len(cur) + 1 + len(p) <= max_chars: + cur = cur + " " + p + else: + chunks.append(cur) + cur = p + if cur: + chunks.append(cur) + return chunks or [text] + + def _to_pcm16(audio: np.ndarray) -> bytes: return np.round(np.clip(audio, -1.0, 1.0) * 32767.0).astype("