Artem Akymenko
7ed2addb11
refactor: add build_metadata_payload() to domain, unify metadata assembly in both UIs
2026-07-24 21:26:21 +03:00
Artem Akymenko
cfc7de7abf
refactor: unify PUNCTUATION constants in domain/split_pattern.py
2026-07-24 19:17:39 +03:00
Artem Akymenko
654c395943
refactor: move sanitize_name_for_os to domain/output_paths.py
2026-07-24 19:17:39 +03:00
Artem Akymenko
d1a84cfb8b
refactor: move voice marker functions to domain/voice_markers.py
2026-07-24 19:17:38 +03:00
Artem Akymenko
332934c0cf
test: tests for apply_overrides, LLM mode, usage_counter, chunk_groups
2026-07-24 19:17:38 +03:00
Artem Akymenko
c79838a5a1
fix: chunk_groups_by_chapter in planner, chunks assigned to correct chapter
2026-07-24 19:17:38 +03:00
Artem Akymenko
d51a9118e4
fix: apply_overrides, LLM mode check, usage_counter in service
2026-07-24 19:17:38 +03:00
Artem Akymenko
3311bef2f7
feat: EPUB3 finalizer in service, extraction in ConversionPlan
2026-07-24 19:17:37 +03:00
Artem Akymenko
4123cadd87
feat: m4b finalizer in service (embed_m4b_metadata)
2026-07-24 19:17:37 +03:00
Artem Akymenko
2f83d10a1e
feat: per-chapter subtitle writer in executor
2026-07-24 19:17:37 +03:00
Artem Akymenko
a1241ee9ca
refactor: word substitution in planner, add planner tests
2026-07-24 19:17:37 +03:00
Artem Akymenko
0ee5bb0496
refactor: config objects for feature toggles in ConversionRequest
2026-07-24 19:17:36 +03:00
Artem Akymenko
7d28b7eb52
test: mock subprocess.Popen in test_executor_m4b_forces_merge to eliminate ffmpeg dependency
2026-07-24 19:17:36 +03:00
Artem Akymenko
fcec4e9fe5
fix: test_stretch_reduces_duration — remove stale self param, fix mock data size, fix atempo assertion
2026-07-22 15:32:18 +03:00
Artem Akymenko
72d5e3d1db
fix: add keys() method to VoiceCache for resolve_intro compatibility
2026-07-22 15:31:43 +03:00
Artem Akymenko
d3682e7672
refactor: dynamic ConversionRequest validation, remove 'or default' from adapters
...
- __post_init__: _apply_none_defaults() iterates dataclasses.fields() dynamically
- _NUMERIC_CONSTRAINTS and _ENUM_CONSTRAINTS dicts replace per-field if chains
- Both adapters pass values as-is (no 'or default' fallbacks)
- 18 validation tests + updated adapter tests for Enum assertions
2026-07-22 11:33:44 +00:00
Artem Akymenko
0805e9fdae
refactor: Language Enum with ISO codes
...
- 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
2026-07-22 10:54:39 +00:00
Artem Akymenko
4aef73ff85
refactor: remove infrastructure enum duplicates
...
- SubtitleFormat/SubtitleMode now only in domain/enums.py
- Added VTT to SubtitleFormat
- Renamed SENTENCE_HIGHLIGHTING → SENTENCE_HIGHLIGHT for consistency
- Infrastructure subtitle_writer imports from domain
2026-07-22 09:16:40 +00:00
Artem Akymenko
f6a8008f51
refactor: typed Enums for format/mode fields
...
- 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
2026-07-22 09:01:50 +00:00
Artem Akymenko
dc5257252f
refactor: run_tts_segment_loop also accepts SynthParams
...
- Reduces from 14 params to 5 unique params + SynthParams
- synthesize_text now passes params through cleanly
- PyQt intro/outro direct calls updated
2026-07-22 08:28:19 +00:00
Artem Akymenko
c4cebb8822
refactor: SynthParams dataclass for synthesize_text
...
- 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
2026-07-22 08:21:45 +00:00
Artem Akymenko
93f5a46485
refactor: deduplicate synth params in executor
...
- Compute use_spacy and effective_subtitle_mode once instead of 6x each
- Reduces repeated ternary expressions across synthesize_text calls
2026-07-22 11:06:14 +03:00
Artem Akymenko
5f169a4921
refactor: replace executor _slugify with domain sanitize_filename_for_chapter
...
- Use existing domain function instead of duplicated local implementation
- Domain version includes OS-specific sanitization
2026-07-22 11:06:14 +03:00
Artem Akymenko
df5705779e
refactor: extract ConversionCancelled to conversion_ports
...
- Single definition in application layer
- Both adapters import from ports instead of defining locally
2026-07-22 11:06:14 +03:00
Artem Akymenko
d0fe221176
refactor: ConversionPlan.request forward ref
...
- request type: Any → ConversionRequest via TYPE_CHECKING
2026-07-22 07:31:46 +00:00
Artem Akymenko
17700426fd
clean: dead code removal + unused imports
...
- executor: remove dead subtitle_writer stub (lines 121-125)
- executor: replace getattr with direct field access on ResolvedVoice
- service: remove duplicate split_pattern import
- service: remove unused import time
- planner: remove unused import os
- adapters: remove unused import threading, time
2026-07-22 07:21:23 +00:00
Artem Akymenko
16b3f7d8a8
fix: clean direct_text in planner + remove redundant if/else
...
- _extract_source_text now applies clean_text() to direct_text (was skipped)
- _parse_chapters simplified: identical branches collapsed to single call
2026-07-22 07:05:50 +00:00
Artem Akymenko
1a3741ec50
fix: max_subtitle_words default 5 → 50
...
All UI layers use 50 (settings, Job, Thread, adapters). The value 5 was
incorrect and only masked by adapter fallbacks.
2026-07-21 14:58:58 +03:00
Artem Akymenko
7f317ca784
test: adapter field mapping + import layering tests
...
- 28 adapter tests (WebUI + PyQt): field mapping, events, provider, resolver
- 15 import/layering tests: no PyQt/WebUI in app layer, all models importable
2026-07-21 11:47:46 +00:00
Artem Akymenko
d0e42ee691
fix: executor subtitle_writer leak + adapter Path/None-default fixes
...
- executor: manage subtitle_writer via ExitStack (stack.callback)
- executor: remove manual subtitle_writer.close()
- WebUI adapter: wrap source_path, output_folder, cover_image_path in Path()
- WebUI adapter: remove unused threading/time imports
- PyQt adapter: fix getattr(attr, None) or default for 4 fields
2026-07-21 11:47:36 +00:00
Artem Akymenko
b1392084e1
chore: add .coverage to .gitignore
2026-07-21 09:35:36 +00:00
Artem Akymenko
71916aa39f
fix: conversion_service import bug + coverage tests
...
Fix import error in _prepare_tts_context:
- apply_normalization_overrides doesn't exist in domain.normalization
- merge_pronunciation_overrides expects job-like object, not two lists
- Use _MockJob adapter to bridge ConversionRequest to existing API
Add test_application_coverage.py (23 tests):
- ConversionService: simple, logs, cancellation, empty text, multi-chapter, intro/outro, error
- OutputLayoutService: custom folder, source path, project, merged path, chapter path, should_merge
- Executor gaps: no layout, m4b, separate chapters, no intro/outro, voice fallback, silence
Coverage: 80% -> 92%
2026-07-21 09:34:47 +00:00
Artem Akymenko
4c4434c309
fix: sanitize_output_stem signature + audio_sink import
...
Two pre-existing bugs found during test coverage analysis:
1. sanitize_output_stem() only accepted 1 arg but resolve_project_layout
passed 2 args (name, index) via sanitize_fn parameter.
Fix: added optional index parameter to sanitize_output_stem.
2. audio_sink.py imported get_internal_cache_path from
abogen.infrastructure.cache which doesn't exist.
Fix: import from abogen.utils where the function lives.
2026-07-21 09:18:51 +00:00
Artem Akymenko
7973de3868
feat: ConversionService
...
Main orchestrator for the conversion flow. Both UIs call run_conversion().
Functions:
- run_conversion(request, events, pipeline_provider, voice_resolver) -> ConversionResult
- _prepare_tts_context(request, events) -> TTSContext
The service ties together planner, executor, and finalizers.
2026-07-21 11:30:15 +03:00
Artem Akymenko
fd659d0f4f
feat: PyQt adapter
...
Converts PyQt ConversionThread to ConversionRequest for the application layer.
Functions:
- build_conversion_request_from_thread(thread) -> ConversionRequest
- PyQtEvents: wraps thread signals for logging, progress, cancellation
- PyQtPipelineProvider: wraps existing backend
- PyQtVoiceResolver: wraps load_voice_cached
Subtitle file/timestamp special paths remain in ConversionThread.run().
2026-07-21 11:29:47 +03:00
Artem Akymenko
e53251ef81
feat: WebUI adapter
...
Converts WebUI Job to ConversionRequest for the application layer.
Functions:
- build_conversion_request_from_job(job) -> ConversionRequest
- WebJobEvents: wraps Job for logging, progress, cancellation
- WebPipelineProvider: wraps PipelinePool for TTS backends
- WebVoiceResolver: wraps voice resolution function
The adapter is the bridge between WebUI layer and application/domain.
Application layer never accesses Job directly.
2026-07-21 11:29:38 +03:00
Artem Akymenko
cd3cc9bce7
refactor: extract OutputLayoutService
...
Extract output path resolution from conversion_planner.py into
application/output_layout_service.py as a standalone service.
Functions:
- resolve_output_layout(request) -> OutputLayout
- resolve_merged_path(layout, request) -> Path
- resolve_chapter_path(layout, request, title, index) -> Path
- should_merge_output(request) -> bool
Planner now imports from output_layout_service instead of inline logic.
2026-07-21 11:17:28 +03:00
Artem Akymenko
75a3ad517a
test: executor tests with fake backend/sink/ports
...
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.
2026-07-21 11:16:41 +03:00
Artem Akymenko
a6b7ce69aa
feat: unified conversion executor (execute_conversion)
...
Takes ConversionPlan + ports, executes TTS conversion, returns ConversionResult.
- Opens/closes audio sinks and subtitle writers
- Processes intro/outro
- Executes chapter loop with heading + body segments
- Collects chapter_markers and chunk_markers
- Uses domain functions only (no UI imports)
2026-07-21 11:16:41 +03:00
Artem Akymenko
680418fa1d
test: planner tests + domain regression tests
...
51 tests for the unified conversion planner:
- build_conversion_plan: direct text, voice markers, chunks, chapters, intro/outro, output layout
- Domain regression: chapter parsing, voice markers, TTSContext, voice resolution, intro/outro, output paths, subtitles
- All tests use domain functions only (no UI, no TTS, no audio I/O)
2026-07-21 11:16:41 +03:00
Artem Akymenko
53b850ef41
feat: unified conversion planner (build_conversion_plan)
...
Pure function that takes ConversionRequest -> ConversionPlan.
Handles chapter parsing, voice markers, chunks, intro/outro, output layout.
Replaces duplicated planning logic in both PyQt and WebUI runners.
2026-07-21 11:16:41 +03:00
Artem Akymenko
7ed4eca68c
feat: application layer models and ports for conversion unification
...
- application/conversion_models.py: SegmentPlan, ChapterPlan, ConversionPlan, OutputLayout, IntroOutroSpec
- application/conversion_request.py: ConversionRequest (normalized input)
- application/conversion_result.py: ConversionResult, ConversionError (normalized output)
- application/conversion_ports.py: protocols (ConversionEvents, PipelineProvider, VoiceResolver, SubtitleWriter, AudioSink)
These are pure data models and interfaces. No implementation yet.
2026-07-21 11:16:41 +03:00
Artem Akymenko
e1e49e8a0f
test: regression tests for conversion flow unification
...
Three new test files describing expected behavior before refactoring:
- test_conversion_planner.py: chapter parsing, voice markers, TTSContext, intro/outro, output paths, subtitles (30 tests)
- test_conversion_request.py: settings, context building, chapter selection, cancellation/logging protocols (15 tests)
- test_conversion_executor.py: synthesize_text, process_and_write_subtitles, full pipeline with fake backend/sink (12 tests)
All 1310 tests pass (1253 existing + 57 new).
2026-07-21 11:16:40 +03:00
Artem Akymenko
28998e1e5c
refactor: delete redundant _prepare_project_layout wrapper
...
resolve_project_layout() from domain already handles mkdir.
Callers now use the domain function directly.
Tests: 1253 passed
2026-07-20 10:10:50 +00:00
Artem Akymenko
8a220a936c
refactor: extract extract_metadata_for_file() domain function, delete PyQt wrapper
...
domain/metadata_extraction.py gains extract_metadata_for_file() combining
read_text_for_metadata + extract_metadata_from_text. PyQt _extract_metadata_dict
deleted, calls replaced with domain function.
Tests: 1253 passed
2026-07-20 09:49:33 +00:00
Artem Akymenko
ccc2cdb166
refactor: replace manual suffix loop with resolve_unique_path ( #4 )
...
PyQt output path resolution now uses resolve_unique_path() from domain
instead of a hand-rolled counter loop. Output path logic is now fully
shared via domain functions.
Tests: 1253 passed
2026-07-20 09:27:31 +00:00
Artem Akymenko
79ff7e4682
refactor: delete _process_subtitle_tokens wrapper, use domain function directly ( #6 )
...
PyQt now calls process_subtitle_tokens() from domain instead of a thin
wrapper that just forwarded self.subtitle_mode/lang_code/use_spacy.
Tests: 1253 passed
2026-07-20 09:22:31 +00:00
Artem Akymenko
2a54b8fdf1
refactor: extract synthesize_text() domain function ( #2 )
...
Combines TTSContext.normalize() + run_tts_segment_loop() into a single
domain function. Both UIs call synthesize_text() instead of inlining
normalize → TTS loop. UI-specific concerns (provider resolution,
progress display, cancellation) stay in the UI layer.
Tests: 1253 passed
2026-07-20 09:12:40 +00:00
Artem Akymenko
68e5adb091
refactor: consolidate voice resolution via resolve_voice_choice ( #3 )
...
Chapter and chunk loops now call resolve_voice_choice() instead of
inlining _resolve_voice_target + cache check + resolve_voice.
Reduces 3 duplicated voice resolution blocks to 1 closure.
Tests: 1253 passed
2026-07-20 09:05:26 +00:00
Artem Akymenko
c4870eece6
refactor: extract TTSContext dataclass for normalization parameters ( #5 )
...
Bundles pronunciation_rules, heteronym_rules, normalization_overrides,
usage_counter, and split_pattern into a single TTSContext dataclass.
Both UIs create it once and use tts_context.normalize() instead of
threading 5 separate parameters through prepare_text_for_tts calls.
Tests: 1253 passed
2026-07-20 09:00:55 +00:00