extract conversion_engine: shared TTS segment iteration loop for WebUI and PyQt

- domain/conversion_engine.py: run_tts_segment_loop() with CancelChecker,
  SegmentStats, SegmentInfo protocols; on_segment callback for per-segment
  subtitle processing; process_and_write_subtitles() helper
- conversion_runner.py: emit_text() delegates TTS iteration to engine
- pyqt/conversion.py: inner tts_segments loop replaced with engine call,
  on_segment handles dual merged+chapter subtitle writers
- routes/utils/settings.py: re-exports load_settings, coerce_int/float,
  llm_ready, settings_defaults from domain for backward compat
This commit is contained in:
Artem Akymenko
2026-07-20 07:35:58 +00:00
parent 476063bc3d
commit 5d30903149
4 changed files with 283 additions and 89 deletions
+43 -44
View File
@@ -36,7 +36,7 @@ from abogen.domain.output_paths import (
)
from abogen.domain.audio_helpers import build_ffmpeg_command, to_float32
from abogen.domain.audio_sink import AudioSink, open_audio_sink
from abogen.domain.conversion_pipeline import tts_segments
from abogen.domain.conversion_engine import run_tts_segment_loop, SegmentStats, SegmentInfo
from abogen.domain.audio_buffer import (
create_silence,
mix_audio,
@@ -950,41 +950,32 @@ class ConversionThread(QThread):
(f"Warning: Text normalization failed: {exc}", "orange")
)
for seg in tts_segments(
text_segment,
backend=self.backend,
voice=loaded_voice,
speed=self.speed,
split_pattern=active_split_pattern,
current_time=current_time,
):
def _qt_check_cancel() -> bool:
if self.cancel_requested:
sink_stack.close()
self.conversion_finished.emit("Cancelled", None)
return
current_segment += 1
self.processed_char_count += len(seg.graphemes)
return True
return False
def _qt_on_progress(pct: int, etr: str) -> None:
self.processed_char_count = stats.processed_chars
self.progress_updated.emit(pct, etr)
def _qt_on_segment(info: SegmentInfo) -> None:
nonlocal chapter_current_time
self.log_updated.emit(
f"\n{self.processed_char_count:,}/{self.total_char_count:,}: {seg.graphemes}"
f"\n{stats.processed_chars:,}/{self.total_char_count:,}: {info.graphemes}"
)
# Write audio
if merge_chapters_at_end and merged_sink:
merged_sink.write(seg.audio)
if chapter_sink:
chapter_sink.write(seg.audio)
# Subtitle logic
if self.subtitle_mode != "Disabled" and seg.tokens:
tokens_with_timestamps = list(seg.tokens)
if self.subtitle_mode != "Disabled" and info.tokens:
tokens_with_timestamps = list(info.tokens)
chapter_tokens_with_timestamps = [
{
"start": chapter_current_time + (t["start"] - seg.chunk_start),
"end": chapter_current_time + (t["end"] - seg.chunk_start),
"start": chapter_current_time + (t["start"] - info.chunk_start),
"end": chapter_current_time + (t["end"] - info.chunk_start),
"text": t["text"],
"whitespace": t["whitespace"],
}
for t in seg.tokens
for t in info.tokens
]
if merge_chapters_at_end:
new_entries = []
@@ -992,7 +983,7 @@ class ConversionThread(QThread):
tokens_with_timestamps,
new_entries,
self.max_subtitle_words,
fallback_end_time=seg.chunk_start + seg.duration,
fallback_end_time=info.chunk_start + info.duration,
)
if merged_subtitle_writer:
for start, end, text in new_entries:
@@ -1003,29 +994,37 @@ class ConversionThread(QThread):
chapter_tokens_with_timestamps,
new_chapter_entries,
self.max_subtitle_words,
fallback_end_time=chapter_current_time + seg.duration,
fallback_end_time=chapter_current_time + info.duration,
)
if chapter_subtitle_writer:
for start, end, text in new_chapter_entries:
chapter_subtitle_writer.write_entry(start, end, text)
# Update timing
if merge_chapters_at_end:
current_time += seg.duration
if chapter_sink:
chapter_current_time += seg.duration
chapter_current_time += info.duration
# Progress
percent = min(
int(self.processed_char_count / self.total_char_count * 100),
99,
)
etr_str = calc_etr_str(
time.time() - self.etr_start_time,
self.processed_char_count,
self.total_char_count,
)
self.progress_updated.emit(percent, etr_str)
stats = SegmentStats(
processed_chars=self.processed_char_count,
current_time=current_time,
etr_start_time=self.etr_start_time,
total_characters=self.total_char_count,
)
run_tts_segment_loop(
text=text_segment,
backend=self.backend,
voice=loaded_voice,
speed=self.speed,
split_pattern=active_split_pattern,
stats=stats,
check_cancel=_qt_check_cancel,
on_progress=_qt_on_progress,
chapter_sink=chapter_sink,
audio_sink=merged_sink if merge_chapters_at_end else None,
on_segment=_qt_on_segment,
)
self.processed_char_count = stats.processed_chars
current_time = stats.current_time
# Add silence between chapters for merged output (except after the last chapter)
if merge_chapters_at_end and chapter_idx < total_chapters: