Files
Deniz Şafak 5432de7ac5 fix(segmentation): process TTS segments per sentence, fix all subtitle modes
Sentence modes processed all text as a whole: Pipeline.__call__ merged every
engine segment back into one (whole text, no per-token timings), producing a
single giant subtitle and whole-text progress logs.

- tts_plugin/types: add TokenTiming, AudioSegment, SynthesizedAudio.segments
- tts_plugin/utils: Pipeline yields one Segment per engine segment (with
  tokens); merged fallback only when engine provides none
- kokoro engine: expose per-segment graphemes/audio + per-word token timings
- supertonic engine: expose per-segment graphemes/audio (no tokens)
- split_pattern: English Sentence/Sentence+Comma engine split is newline-only
  (boundaries applied at subtitle time via spaCy); non-English Sentence+Comma
  with spaCy ON uses spaCy pre-segmentation + newline engine split (no
  commas); spaCy-off fallback keeps comma pattern
- tts_segments: restore inter-segment whitespace on real per-word token
  boundaries only (never FakeToken fallbacks)
- _to_language_enum: accept Language enum input (str(enum) is "Language.ES",
  silently resolved to EN_US and disabled spaCy pre-TTS for every language
  in WebUI)
- pyqt/conversion, utils: replace print with logging
- add AGENTS.md documenting the segmentation/subtitle contract for future
  sessions
- tests: update English split-pattern expectations (1566 passing)
2026-08-20 23:00:15 +03:00

4.4 KiB

AGENTS.md — Segmentation & Subtitle System Contract

This document is the source of truth for how text is split for voice processing (TTS engine segmentation) and subtitle processing, across languages, TTS engines, and subtitle modes. It was written after a bug where sentence modes "processed all text as a whole" (one merged engine segment → one giant subtitle). Do not change this behavior without updating this table.

Voice processing — split pattern passed to the TTS engine

get_split_pattern(language, mode) in abogen/domain/split_pattern.py is the default; the spaCy pre-TTS path overrides it. Both UIs must stay in sync: spacy_pre_tts_segmentation (abogen/domain/conversion_pipeline.py, WebUI) and the inline branch in abogen/pyqt/conversion.py (~line 860, PyQt).

Subtitle mode English (en-US/en-GB) Non-English, spaCy ON Non-English, spaCy OFF CJK (ja/zh)
Disabled \n spaCy pre-split, engine \n \n+ (?<=[.!?؟。!?।])\s*|\n+
Line \n spaCy pre-split, engine \n \n (?<=[.!?؟。!?।])\s*|\n+
Sentence \n spaCy pre-split, engine \n (?<=[.!?؟。!?।])\s+|\n+ (?<=[.!?؟。!?।])\s*|\n+
Sentence + Comma \n spaCy pre-split, engine \n (?<=[.!?,؟。!?،،、।])\s+|\n+ (commas kept) (?<=[.!?,؟。!?،،、।])\s*|\n+
Sentence + Highlighting \n+ \n+ \n+ \n+
N words ("5 words") \n (→ Disabled) \n+ \n+ Disabled CJK pattern

Rules baked into this table:

  • English voice splitting is ALWAYS newline-only for Disabled, Line, Sentence, and Sentence + Comma. English sentence/comma boundaries are produced ONLY at subtitle time (spaCy post-TTS / regex fallback). Never add punctuation to the English engine pattern.
  • Non-English + spaCy ON: spaCy pre-segments the text (pre-TTS); the engine pattern is \n for Sentence AND Sentence + Comma — never commas. spaCy is skipped when the toggle is off, mode is Disabled/Line, or input is a subtitle file.
  • Non-English + spaCy OFF (toggle off, spaCy failure, subtitle input): the default pattern is used — Sentence + Comma KEEPS its commas here. This is the intentional fallback, not a bug.
  • CJK: punctuation-based patterns for Disabled/Line (historical); spacing is \s* (no spaces needed between CJK chars).
  • Engine-level extra chunking (applies after the pattern): kokoro English re-chunks at ~510 phonemes; kokoro non-English at ~400 chars; supertonic caps each part at 300 chars.

Subtitle processing — post-TTS, from tokens

Mode Behavior
Disabled no subtitles
Line one entry per TTS segment (line)
Sentence sentence boundaries: English → spaCy; others → regex on [.!?…]
Sentence + Comma sentence + comma boundaries at subtitle time (both languages) — commas never affect voice
Sentence + Highlighting karaoke {\kf…} per word, grouped by sentence
N words groups of N words by whitespace counting

Token granularity (timing quality): kokoro English emits per-word tokens with timestamps; kokoro non-English and supertonic emit no tokens → each engine segment becomes one FakeToken, split by regex with proportional timing when it contains multiple sentences.

Hard invariants (breaking these reintroduces the original bug)

  1. Pipeline.__call__ (abogen/tts_plugin/utils.py) must yield ONE Segment per engine segment (with tokens) — never merge segments back into the whole text. SynthesizedAudio.segments carries the per-segment data; engines expose it in plugins/kokoro/engine.py and plugins/supertonic/engine.py.
  2. tts_segments (abogen/domain/conversion_pipeline.py) restores trailing whitespace on segment-boundary tokens ONLY for real per-word tokens, never for FakeToken fallbacks.
  3. _to_language_enum must return lang_code as-is when it is already a Language enum (str(Language.ES) is "Language.ES", which silently resolved to EN_US and disabled spaCy pre-TTS for every language in WebUI).
  4. English must never use spaCy for PRE-TTS segmentation — only for subtitles.

Guarded by tests

  • tests/test_split_pattern.py — English newline-only; non-English sentence patterns; CJK behavior.
  • tests/test_domain_conversion_pipeline.pytts_segments / spaCy segmentation helpers.
  • Full suite: python -m pytest tests/ -q (expect 1566+ passing).