- conversion_pipeline.py: new spacy_pre_tts_segmentation() function
- Handles: condition checks, language exclusion, split_pattern override
- Executor: integrated spaCy pre-TTS before each synthesize_text() call
- 6 new tests for condition checks and fallback behavior
- English excluded from pre-TTS (spaCy only for post-TTS subtitles)
- Fallback to regex when spaCy unavailable
- MarkerCollector: SRP extraction from executor (observation, not execution)
- Outro marker: executor now records outro as chapter marker
- Voice format: chapter voices [{provider, voice}], chunk voice {provider, voice}
- _finalize(): build_metadata_payload() + metadata.json + record_override_usage()
- ffmetadata: voices list → comma-separated string
- EPUB3: ChunkOverlay.voice = dict, _render_chunk_inline handles dict format
- 8 new tests: multi-speaker, ffmetadata format, EPUB3 format
- Updated existing tests for new voice format
- Remove 77 unused imports across domain/application/runner files via ruff
- Add # noqa: F401 to re-exports used by tests and debug_tts_runner
- Fix build_tts_context: usage_counter uses 'is not None' instead of truthiness
- Fix test assertions: compiled rules use 'replacement' key not 'pronunciation'
- Add re-exports: _compile_pronunciation_rules, _merge_pronunciation_overrides
- Language enum: en-US, en-GB, es, fr, hi, it, ja, pt-BR, zh
- Engine-specific mappings (kokoro → single-letter) live in pipeline_factory and synthesize
- spacy_utils uses Language enum keys for model mapping
- split_pattern uses Language enum properties (is_cjk)
- Updated all tests to use ISO codes
- SubtitleMode, OutputFormat, SaveMode, SubtitleFormat, InputFormat
- Properties: dot_ext, is_lossless, is_book, is_subtitle
- from_str/from_path class methods with normalization
- Updated domain and application layers to use Enums
- 17 new tests for enum validation and properties
- Frozen dataclass in domain/conversion_engine.py with common params
- synthesize_text now takes params=SynthParams + unique kwargs
- Executor, PyQt legacy, WebUI legacy, and tests updated
- Adding new common params now only requires changing the dataclass
7 tests for the unified conversion executor:
- simple text conversion
- multi-chapter with separate chapter output
- voice markers
- intro/outro
- cancellation behavior
- progress reporting
- metadata preservation
Uses FakeBackend, FakeAudioSink, FakeSubtitleWriter, FakeEvents,
FakePipelineProvider, FakeVoiceResolver to test without real TTS.
- voice_formulas.py: add pairs_to_formula() as canonical implementation
- webui/routes/utils/voice.py: formula_from_profile() and pairs_to_formula()
now delegate to voice_formulas.pairs_to_formula()
- pyqt/gui.py: get_voice_formula() now uses voice_formulas.pairs_to_formula()
instead of inline string formatting
- Eliminates 3 duplicate implementations of voice*weight formula building
- +9 tests
- 1178 tests pass
- 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)
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.
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 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