- Replace get_pipeline() closure with PipelinePool from domain/pipeline_factory
- Replace resolve_voice_target() closure with domain function from voice_utils
- Remove dead _load_pipeline() function and unused is_plugin_registered import
- Add 33 tests for resolve_voice_target and PipelinePool
- Add 10 regression tests verifying domain extraction preserves behavior
- 1131 tests pass (+61 new)
Replace 2 inline 'if * in voice: get_new_voice(...)' patterns with
resolve_voice() from domain/voice_loader.py. Removes unused import
of get_new_voice.
PyQt had 7 inline float32 conversion patterns:
hasattr(x, 'numpy') ? x.numpy().astype('float32') : x.astype('float32')
spread across TTS loop, subtitle processing, and streaming.
All replaced with domain.audio_helpers.to_float32() which handles:
- None → zeros
- PyTorch tensors → .detach().cpu().numpy()
- Plain numpy → asarray(dtype=float32)
- reshape(-1) for consistent 1D output
The old inline code missed .detach() and .cpu() on GPU tensors,
causing potential crashes. Now both UIs use the same robust conversion.
1053 tests pass.
- Move pronunciation imports from inside run() to top-level imports
- Extract _FakeToken to module level (was redefined every loop iteration)
- use_spacy_segmentation now mirrors PyQt logic: pass the flag,
let process_subtitle_tokens filter by language internally
Before: WebUI wrote one subtitle entry per TTS segment (no sentence
grouping, no comma splitting, no karaoke highlighting). The subtitle
modes 'Sentence', 'Sentence + Comma', and 'Sentence + Highlighting'
produced broken output.
After: emit_text() accumulates tokens_with_timestamps from each
segment's .tokens attribute, then flushes them through
domain.subtitle_generation.process_subtitle_tokens() at the end.
This gives the WebUI the same subtitle quality as the PyQt desktop GUI:
- Sentence mode: groups tokens into sentences
- Sentence + Comma: splits on commas within sentences
- Sentence + Highlighting: karaoke timing per word
- Word-count mode: groups by N words
Also removed the duplicate _to_float32 function from synthesize.py
(now imports from domain.audio_helpers).
1053 tests pass.
PyQt desktop GUI now calls the shared normalization pipeline before
TTS synthesis, matching the Web UI's behavior:
1. Heteronym sentence rules (context-dependent pronunciation)
2. Pronunciation rules (token-level replacements)
3. Pipeline normalization (apostrophe handling, LLM)
Before: PyQt passed raw text to the backend — no normalization at all,
resulting in inferior audio quality compared to the Web UI.
The normalization rules are compiled once at the start of run() from
pronunciation_overrides and heteronym_overrides (currently None since
the PyQt GUI doesn't expose these settings yet — basic apostrophe
normalization still applies).
1053 tests pass.
Before:
- PyQt: inline ETR using chars-based formula
- WebUI: Job.estimated_time_remaining using progress-based formula
(different formulas → different ETR estimates)
After:
- Both UIs call domain.progress.calc_etr_str(elapsed, done, total)
- Same formula, same ETR, single source of truth
- WebUI now stores etr_str on Job and displays it directly
- Job.estimated_time_remaining property kept for backward compat
domain/progress.py: ProgressTracker class + calc_etr_str function
1053 tests pass.
All three Web UI consumers now call domain.split_pattern.get_split_pattern()
which selects the correct split pattern based on language and subtitle mode.
Before: WebUI always split on \\n+ regardless of language (CJK missed
punctuation-based splitting that PyQt already had).
After: Both UIs share identical language-aware splitting logic.
1038 tests pass.
New function chains all three normalization stages:
1. Heteronym sentence rules (context-dependent pronunciation)
2. Pronunciation rules (token-level replacements)
3. Pipeline normalization (apostrophe, LLM)
This is the single entry point that both Web UI and PyQt should call
before TTS synthesis. Currently only Web UI uses it; PyQt has NO
normalization — this unlocks that capability.
Updated conversion_runner.emit_text to use the new function.
1038 tests pass.
Moved supplement_score, should_preselect_chapter, and
ensure_at_least_one_chapter_enabled to domain/chapter_classification.py.
Also moved coerce_bool from settings.py to common.py to break circular
import introduced in previous commit.
1025 tests pass.
Extracted extract_checkbox from settings.py, form.py (x2) into
webui/routes/utils/common.py. Moved coerce_bool from settings.py
to common.py to break circular import.
Fixed bug in second form.py copy (was missing __contains__ check).
1006 tests pass.
Extracted 8 metadata functions from service.py, exporters.py, and
audiobookshelf.py into domain/metadata_helpers.py:
- normalize_metadata_casefold, split_people_field, split_simple_list
- first_nonempty, extract_year, normalize_series_sequence
- build_audiobookshelf_metadata, load_audiobookshelf_chapters
service.py, exporters.py, and audiobookshelf.py now import from domain
instead of maintaining separate copies. Thin wrappers adapt to layer
interfaces (Job objects, etc.).
Net -231 lines. 1006 tests pass.
- Rename abogen/webui/routes/utils/preview.py → synthesize.py
The file contains the core TTS synthesis pipeline (generate_preview_audio,
synthesize_preview), not just preview logic. Name now matches responsibility.
- Remove dead code from voice.py: get_preview_pipeline(), synthesize_audio_from_normalized(),
_preview_pipeline_lock, _preview_pipelines, and unused imports (threading, numpy,
create_pipeline, get_new_voice, _select_device, _to_float32, SAMPLE_RATE, SPLIT_PATTERN).
These were never called — identical logic lives in synthesize.py.
- Update imports in api.py, voices.py, and test_preview_applies_manual_overrides.py
- 7 new tests in test_synthesize_module.py enforce file naming and import rules
- 7 tests in test_domain_imports.py updated for renamed module
Replace private imports (_select_device, _to_float32, SAMPLE_RATE, SPLIT_PATTERN)
from abogen.webui.conversion_runner with proper domain imports:
- select_device from abogen.domain.device
- to_float32, SAMPLE_RATE from abogen.domain.audio_helpers
- SPLIT_PATTERN defined locally (r'\n+')
Also verifies preview.py already uses domain imports correctly.
7 new tests in tests/test_domain_imports.py enforce the architecture rule.
- Replace manual metadata extraction with regex in pyqt/conversion.py
with calls to domain/metadata_extraction.py functions
- Remove duplicate _embed_m4b_metadata and _apply_m4b_chapters_with_mutagen
functions from webui/conversion_runner.py
- Use ExportService.embed_m4b_metadata for m4b metadata embedding
- Reduce code duplication between PyQt and WebUI interfaces
- Fix mix_audio to return target buffer (was not modifying in-place)
- Fix samples_for_duration to return 0 for negative durations
- Fix test assertions for numpy 2.x compatibility (share_memory -> shares_memory)
- Adjust subtitle_generation tests to match actual behavior
- Add abogen/domain/voice_loader.py with:
- VoiceCache class: unified cache for loaded voices
- resolve_voice(): load voice with optional caching
- load_voice_cached(): compatibility wrapper for PyQt
- Update abogen/pyqt/conversion.py:
- Replace load_voice_cached method body with call to domain function
- Maintain backward compatibility with existing interface
- Add tests/test_voice_loader.py with unit tests for VoiceCache and voice loading
- Add abogen/domain/subtitle_generation.py with:
- process_subtitle_tokens(): main function for converting TTS tokens to subtitles
- Support for all subtitle modes: Line, Sentence, Sentence + Comma, Sentence + Highlighting
- Support for word-count based grouping (e.g., '5' for 5 words per entry)
- spaCy integration for English sentence boundary detection
- Karaoke highlighting tags for Sentence + Highlighting mode
- Punctuation constants for sentence splitting
- Update abogen/pyqt/conversion.py:
- Replace _process_subtitle_tokens method body with call to domain function
- Remove ~260 lines of duplicate logic
- Add tests/test_subtitle_generation.py with comprehensive unit tests
- Add abogen/domain/audio_buffer.py with core audio operations:
- create_silence(): create silence audio buffer
- mix_audio(): mix source into target buffer with auto-resize
- normalize_audio(): normalize to prevent clipping
- ensure_buffer_size(): extend buffer to minimum size
- concatenate_audio(): join multiple audio buffers
- audio_duration(): calculate duration from samples
- samples_for_duration(): calculate samples from duration
- SAMPLE_RATE constant (24000)
- Update abogen/pyqt/conversion.py:
- Import and use create_silence for chapter silence
- Use mix_audio for subtitle file mixing
- Use normalize_audio for clipping prevention
- Use create_silence for padding in subtitle processing
- Update abogen/webui/conversion_runner.py:
- Import and use create_silence in append_silence
- Replace np.zeros with domain function
- Add tests/test_audio_buffer.py with comprehensive unit tests
- Update chapter_overrides.py to return tuple matching original signature
- Import apply_chapter_overrides and merge_metadata from domain modules
- Remove old function bodies from conversion_runner.py
- Add tests/test_chapter_merge_normalize.py (19 tests)
- conversion_runner.py: 1677 → 1574 lines
- All tests pass
- Extract infer_provider_from_spec, supertonic_voice_from_spec, split_speaker_reference, formula_from_kokoro_entry, coerce_truthy to domain/voice_utils.py
- Add tests/test_voice_utils.py with 24 tests
- All tests match old behavior
- Extract build_title_intro_text and build_outro_text into domain/title_builder.py
- Uses metadata_helpers for metadata processing
- Remove _build_title_intro_text and _build_outro_text from conversion_runner.py
- Add tests/test_title_builder.py with 12 tests
- All tests match old behavior
- Remove _srt_time() and _ass_time() methods from ConversionThread
- Use _format_timestamp() from infrastructure/subtitle_writer.py instead
- Supports both SRT (ass=False) and ASS (ass=True) formats
- All existing tests pass
- Extract unified split pattern logic to domain/split_pattern.py
- Add get_split_pattern() function with language and subtitle_mode support
- Remove duplicated logic from pyqt/conversion.py
- Update pyqt/conversion.py to use domain.split_pattern.get_split_pattern
- Add tests/test_split_pattern.py with 20 tests covering English, CJK, Spanish, French, and pattern structure
- Extract SubtitleWriter classes (SrtWriter, AssWriter, VttWriter) to infrastructure/subtitle_writer.py
- Add create_subtitle_writer() factory function
- Remove old SubtitleWriter class and _format_timestamp from conversion_runner.py
- Use create_subtitle_writer() factory from infrastructure layer
- Add tests/test_subtitle_writer.py with 28 tests covering SrtWriter, AssWriter, VttWriter
- All tests match old _format_timestamp behavior
- Monkey-patch transformers.AlbertModel in plugins/kokoro/__init__.py before kokoro imports it
- Works with transformers 4.x (no-op) and 5.x (adds moved symbol)
- No pinning, forks, or extra files needed
- Add .gitattributes with text=auto eol=lf for all text file types
- Ensures consistent line endings across platforms
- Prevents future CRLF/LF diffs in pull requests