fix(subtitles): enhance ellipsis and paragraph break handling in subtitle processing
feat(gui): implement background update check for new versions
test(tests): add tests for ellipsis handling and paragraph breaks preservation
- kokoro_text_normalization:
- refactor _cleanup_spacing to contextually distinguish opening vs closing
straight quotes (" and ') so leading quotes do not have spaces added after
them and spaces before opening quotes are preserved
- recognize non-English opening delimiters (¡, ¿, «, 「, etc.) and closing
delimiters (», 」, etc.) in spacing normalization
- in normalize_apostrophes, reconstruct text using original token offsets
to preserve inter-token whitespace instead of blindly joining with spaces
- subtitle_generation:
- add _is_sentence_boundary to recognize sentence punctuation followed by
closing quotes/brackets (.", !", ?") in regex and karaoke modes
- support proportional splitting for single FakeToken multi-sentence segments
in spaCy mode (Supertonic / non-English Kokoro)
- support newline splitting for Line mode in FakeToken fallbacks
- safely convert language and subtitle_mode inputs
- tests:
- add tests/test_subtitle_scenarios.py covering quotes, dialogues,
paragraphs, contractions, all subtitle modes, and all TTS token styles
Three related fixes:
- make_subtitle_writer: accept word-count modes ("N words") in
create_subtitle_writer. SubtitleMode("5 words") raised ValueError,
which make_subtitle_writer swallowed and returned None, so PyQt never
created or wrote the subtitle file when an "N words" mode was selected.
Unknown modes now fall back to SubtitleMode.SENTENCE (writers only
branch on SENTENCE_HIGHLIGHT). Regression test added.
- gui.py: refresh subtitle combo item availability after initial
voice/profile selection. update_subtitle_options_availability() ran
during initUI with selected_lang=None for a profile, taking the
non-English branch and disabling Highlighting/N-words items; the fix-up
never re-ran because setCurrentIndex on an already-current index emits
no signal. Now called once more after the profile/voice language is
resolved.
- AssWriter: stop discarding per-word karaoke timing. _add_karaoke_tags
unconditionally replaced entry text with uniform {\\k100} tags,
destroying the real per-word {\\kf} timings from
_process_karaoke_highlighting. Only synthesize simplified tags when the
text has no karaoke tags. Regression test added.
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)
Clicking the window close button froze the app: closeEvent ran the full
process cleanup (engine disposal, CUDA flush, subprocess termination) and
unbounded thread joins synchronously on the GUI thread before the window
could start closing, and a 3.5s delay came from flush_cuda importing
torch even when it was never loaded.
- closeEvent no longer runs cleanup synchronously; the aboutToQuit hook,
which was never connected (registered before QApplication existed), now
runs it after the window is gone
- bound thread waits in cleanup_conversion_thread/cleanup_preview_threads
with a terminate() fallback so closing never hangs
- flush_cuda skips torch work when torch was never imported
- restore the default Qt message handler before sys.exit; the custom
Python handler was invoked during interpreter teardown and caused a
SIGSEGV after shutdown cleanups finished
- log and time the close sequence (closeEvent steps + each shutdown hook)
Complete the Language enum unification (commit 0dc491e) for the PyQt
GUI, which was left resolving languages from kokoro letter codes
(voice[0]) while domain and WebUI already used Language enums.
- selected_lang is now always a Language enum: voices resolve via
language_for_voice_id(), profiles via resolve_profile_language()
- kokoro letter->Language mapping stays in the engine (new public
language_for_code()); the Language.from_code() domain shim is removed
- legacy profile files with letter codes ("a", "e", ...) are tolerated
only at the profile read boundary; new profiles save ISO codes
- conversion.py letter comparisons replaced with enum comparisons, and
SynthParams lang_code= -> language= (would TypeError at runtime)
- subtitle dropdown enabled for all languages; word-count and
highlighting modes restricted to English with auto-switch to Sentence
- tests: engine mapping + profile language resolution added; 1566 pass
PluginManager.discover() used a relative path 'plugins', which resolved
against the CWD. When launched from a desktop shortcut the CWD is ~, so
the plugins directory was never found and no voices appeared in the list.
Fall back to the project-relative plugins path when the default relative
path doesn't resolve.
- subtitle_generation: split multi-sentence FakeToken into separate entries
- conversion.py: add missing run_tts_segment_loop import
No changes to spacy_utils or Language enum.