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
This commit is contained in:
Artem Akymenko
2026-07-22 08:28:19 +00:00
parent c4cebb8822
commit dc5257252f
2 changed files with 35 additions and 51 deletions
+19 -41
View File
@@ -55,44 +55,29 @@ class SegmentInfo:
def run_tts_segment_loop( def run_tts_segment_loop(
*, *,
text: str, text: str,
params: SynthParams,
backend: Any, backend: Any,
voice: Any, voice: Any,
speed: float, speed: float,
split_pattern: str, split_pattern: str,
stats: SegmentStats,
check_cancel: CancelChecker,
on_progress: Callable[[int, str], None],
chapter_sink: Optional[AudioSink] = None, chapter_sink: Optional[AudioSink] = None,
audio_sink: Optional[AudioSink] = None,
preview_callback: Optional[Callable[[str], None]] = None, preview_callback: Optional[Callable[[str], None]] = None,
on_segment: Optional[Callable[[SegmentInfo], None]] = None, on_segment: Optional[Callable[[SegmentInfo], None]] = None,
subtitle_mode: str = "Disabled",
max_subtitle_words: int = 50,
lang_code: str = "a",
use_spacy_segmentation: bool = False,
) -> tuple[int, list]: ) -> tuple[int, list]:
"""Run the core TTS segment iteration loop. """Run the core TTS segment iteration loop.
Args: Args:
text: Normalized text to synthesize. text: Normalized text to synthesize.
params: Common synthesis parameters (stats, callbacks, sinks, etc.).
backend: TTS pipeline instance (Kokoro or Supertonic). backend: TTS pipeline instance (Kokoro or Supertonic).
voice: Voice name/id for the backend. voice: Voice name/id for the backend.
speed: Speech speed multiplier. speed: Speech speed multiplier.
split_pattern: Regex pattern used by the TTS engine for sentence splitting. split_pattern: Regex pattern used by the TTS engine for sentence splitting.
stats: Running character/timing stats (mutated in place).
check_cancel: Called each segment; if it returns True, iteration stops.
on_progress: Called with (percent, etr_str) after each segment.
chapter_sink: Optional audio sink for the current chapter.
audio_sink: Optional audio sink for the merged output.
preview_callback: Called with a short preview string per segment. preview_callback: Called with a short preview string per segment.
on_segment: Called with a SegmentInfo for each segment *before* on_segment: Called with a SegmentInfo for each segment *before*
audio is written. Useful for callers that need per-segment audio is written. Useful for callers that need per-segment
subtitle processing (e.g. PyQt dual-writer pattern). subtitle processing (e.g. PyQt dual-writer pattern).
When provided, the default subtitle accumulation is skipped. When provided, the default subtitle accumulation is skipped.
subtitle_mode: Subtitle mode string (e.g. "Disabled", "Sentence").
max_subtitle_words: Max words per subtitle entry.
lang_code: Language code for subtitle processing.
use_spacy_segmentation: Whether spaCy sentence boundaries are active.
Returns: Returns:
Tuple of (segment_count, accumulated_subtitle_tokens). Tuple of (segment_count, accumulated_subtitle_tokens).
@@ -108,26 +93,26 @@ def run_tts_segment_loop(
voice=voice, voice=voice,
speed=speed, speed=speed,
split_pattern=split_pattern, split_pattern=split_pattern,
current_time=stats.current_time, current_time=params.stats.current_time,
): ):
if check_cancel(): if params.check_cancel():
break break
local_segments += 1 local_segments += 1
stats.processed_chars += len(seg.graphemes) params.stats.processed_chars += len(seg.graphemes)
# Progress # Progress
if stats.total_characters: if params.stats.total_characters:
percent = min(int(stats.processed_chars / stats.total_characters * 100), 99) percent = min(int(params.stats.processed_chars / params.stats.total_characters * 100), 99)
else: else:
percent = 0 if stats.processed_chars == 0 else 99 percent = 0 if params.stats.processed_chars == 0 else 99
etr_str = calc_etr_str( etr_str = calc_etr_str(
time.time() - stats.etr_start_time, time.time() - params.stats.etr_start_time,
stats.processed_chars, params.stats.processed_chars,
stats.total_characters, params.stats.total_characters,
) )
on_progress(percent, etr_str) params.on_progress(percent, etr_str)
# Preview / log # Preview / log
if preview_callback: if preview_callback:
@@ -140,23 +125,23 @@ def run_tts_segment_loop(
audio=seg.audio, audio=seg.audio,
tokens=list(seg.tokens) if seg.tokens else [], tokens=list(seg.tokens) if seg.tokens else [],
duration=seg.duration, duration=seg.duration,
chunk_start=getattr(seg, "chunk_start", stats.current_time), chunk_start=getattr(seg, "chunk_start", params.stats.current_time),
) )
on_segment(info) on_segment(info)
# Write audio # Write audio
if chapter_sink: if chapter_sink:
chapter_sink.write(seg.audio) chapter_sink.write(seg.audio)
if audio_sink: if params.audio_sink:
audio_sink.write(seg.audio) params.audio_sink.write(seg.audio)
# Accumulate subtitle tokens (default path; skipped if on_segment handles it) # Accumulate subtitle tokens (default path; skipped if on_segment handles it)
if not on_segment and subtitle_mode != "Disabled" and seg.tokens: if not on_segment and params.subtitle_mode != "Disabled" and seg.tokens:
accumulated_tokens.extend(seg.tokens) accumulated_tokens.extend(seg.tokens)
# Update timing # Update timing
if audio_sink: if params.audio_sink:
stats.current_time += seg.duration params.stats.current_time += seg.duration
return local_segments, accumulated_tokens return local_segments, accumulated_tokens
@@ -229,19 +214,12 @@ def synthesize_text(
normalized = params.tts_context.normalize(text) normalized = params.tts_context.normalize(text)
return run_tts_segment_loop( return run_tts_segment_loop(
text=normalized, text=normalized,
params=params,
backend=backend, backend=backend,
voice=voice, voice=voice,
speed=speed, speed=speed,
split_pattern=split_pattern_override or params.tts_context.split_pattern, split_pattern=split_pattern_override or params.tts_context.split_pattern,
stats=params.stats,
check_cancel=params.check_cancel,
on_progress=params.on_progress,
chapter_sink=chapter_sink, chapter_sink=chapter_sink,
audio_sink=params.audio_sink,
preview_callback=preview_callback, preview_callback=preview_callback,
on_segment=on_segment, on_segment=on_segment,
subtitle_mode=params.subtitle_mode,
max_subtitle_words=params.max_subtitle_words,
lang_code=params.lang_code,
use_spacy_segmentation=params.use_spacy_segmentation,
) )
+16 -10
View File
@@ -773,17 +773,20 @@ class ConversionThread(QThread):
etr_start_time=self.etr_start_time, etr_start_time=self.etr_start_time,
total_characters=self.total_char_count, total_characters=self.total_char_count,
) )
intro_synth = SynthParams(
tts_context=self._tts_context,
stats=intro_stats,
check_cancel=lambda: self.cancel_requested,
on_progress=lambda pct, etr: self.progress_updated.emit(pct, etr),
audio_sink=merged_sink,
)
run_tts_segment_loop( run_tts_segment_loop(
text=intro_spec.text, text=intro_spec.text,
params=intro_synth,
backend=self.backend, backend=self.backend,
voice=loaded_intro_voice, voice=loaded_intro_voice,
speed=self.speed, speed=self.speed,
split_pattern=self.split_pattern, split_pattern=self.split_pattern,
stats=intro_stats,
check_cancel=lambda: self.cancel_requested,
on_progress=lambda pct, etr: self.progress_updated.emit(pct, etr),
chapter_sink=None,
audio_sink=merged_sink,
) )
self.processed_char_count = intro_stats.processed_chars self.processed_char_count = intro_stats.processed_chars
current_time = intro_stats.current_time current_time = intro_stats.current_time
@@ -1106,17 +1109,20 @@ class ConversionThread(QThread):
etr_start_time=self.etr_start_time, etr_start_time=self.etr_start_time,
total_characters=self.total_char_count, total_characters=self.total_char_count,
) )
outro_synth = SynthParams(
tts_context=self._tts_context,
stats=outro_stats,
check_cancel=lambda: self.cancel_requested,
on_progress=lambda pct, etr: self.progress_updated.emit(pct, etr),
audio_sink=merged_sink,
)
run_tts_segment_loop( run_tts_segment_loop(
text=outro_spec.text, text=outro_spec.text,
params=outro_synth,
backend=self.backend, backend=self.backend,
voice=loaded_outro_voice, voice=loaded_outro_voice,
speed=self.speed, speed=self.speed,
split_pattern=self.split_pattern, split_pattern=self.split_pattern,
stats=outro_stats,
check_cancel=lambda: self.cancel_requested,
on_progress=lambda pct, etr: self.progress_updated.emit(pct, etr),
chapter_sink=None,
audio_sink=merged_sink,
) )
self.processed_char_count = outro_stats.processed_chars self.processed_char_count = outro_stats.processed_chars
current_time = outro_stats.current_time current_time = outro_stats.current_time