mirror of
https://github.com/denizsafak/abogen.git
synced 2026-07-22 07:10:28 +02:00
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
This commit is contained in:
@@ -11,8 +11,8 @@ The core pattern is identical across both UIs:
|
|||||||
After the loop, the caller processes accumulated subtitle tokens.
|
After the loop, the caller processes accumulated subtitle tokens.
|
||||||
|
|
||||||
This module provides ``run_tts_segment_loop`` which encapsulates that
|
This module provides ``run_tts_segment_loop`` which encapsulates that
|
||||||
iteration, while leaving UI-specific concerns (progress display, subtitle
|
iteration, and ``synthesize_text`` which adds normalization on top —
|
||||||
processing strategy) to the caller via callbacks.
|
the single entry point both UIs should call for text-to-speech.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -23,6 +23,7 @@ from typing import Any, Callable, List, Optional, Protocol
|
|||||||
|
|
||||||
from abogen.domain.audio_sink import AudioSink
|
from abogen.domain.audio_sink import AudioSink
|
||||||
from abogen.domain.conversion_pipeline import tts_segments
|
from abogen.domain.conversion_pipeline import tts_segments
|
||||||
|
from abogen.domain.normalization import TTSContext
|
||||||
from abogen.domain.progress import calc_etr_str
|
from abogen.domain.progress import calc_etr_str
|
||||||
from abogen.domain.subtitle_generation import process_subtitle_tokens
|
from abogen.domain.subtitle_generation import process_subtitle_tokens
|
||||||
|
|
||||||
@@ -188,3 +189,49 @@ def process_and_write_subtitles(
|
|||||||
)
|
)
|
||||||
for start, end, text in new_entries:
|
for start, end, text in new_entries:
|
||||||
subtitle_writer.write_entry(start=start, end=end, text=text)
|
subtitle_writer.write_entry(start=start, end=end, text=text)
|
||||||
|
|
||||||
|
|
||||||
|
def synthesize_text(
|
||||||
|
*,
|
||||||
|
text: str,
|
||||||
|
tts_context: TTSContext,
|
||||||
|
backend: Any,
|
||||||
|
voice: Any,
|
||||||
|
speed: float,
|
||||||
|
stats: SegmentStats,
|
||||||
|
check_cancel: CancelChecker,
|
||||||
|
on_progress: Callable[[int, str], None],
|
||||||
|
chapter_sink: Optional[AudioSink] = None,
|
||||||
|
audio_sink: Optional[AudioSink] = None,
|
||||||
|
preview_callback: Optional[Callable[[str], None]] = None,
|
||||||
|
on_segment: Optional[Callable[[SegmentInfo], None]] = None,
|
||||||
|
subtitle_mode: str = "Disabled",
|
||||||
|
max_subtitle_words: int = 5,
|
||||||
|
lang_code: str = "a",
|
||||||
|
use_spacy_segmentation: bool = False,
|
||||||
|
split_pattern_override: Optional[str] = None,
|
||||||
|
) -> tuple[int, list]:
|
||||||
|
"""Normalize text and run TTS — the single entry point for both UIs.
|
||||||
|
|
||||||
|
Combines TTSContext.normalize() + run_tts_segment_loop() into one call.
|
||||||
|
UI-specific concerns (provider resolution, progress display) stay in the UI.
|
||||||
|
"""
|
||||||
|
normalized = tts_context.normalize(text)
|
||||||
|
return run_tts_segment_loop(
|
||||||
|
text=normalized,
|
||||||
|
backend=backend,
|
||||||
|
voice=voice,
|
||||||
|
speed=speed,
|
||||||
|
split_pattern=split_pattern_override or tts_context.split_pattern,
|
||||||
|
stats=stats,
|
||||||
|
check_cancel=check_cancel,
|
||||||
|
on_progress=on_progress,
|
||||||
|
chapter_sink=chapter_sink,
|
||||||
|
audio_sink=audio_sink,
|
||||||
|
preview_callback=preview_callback,
|
||||||
|
on_segment=on_segment,
|
||||||
|
subtitle_mode=subtitle_mode,
|
||||||
|
max_subtitle_words=max_subtitle_words,
|
||||||
|
lang_code=lang_code,
|
||||||
|
use_spacy_segmentation=use_spacy_segmentation,
|
||||||
|
)
|
||||||
|
|||||||
+20
-22
@@ -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_helpers import build_ffmpeg_command, to_float32
|
||||||
from abogen.domain.audio_sink import AudioSink, open_audio_sink
|
from abogen.domain.audio_sink import AudioSink, open_audio_sink
|
||||||
from abogen.domain.conversion_engine import run_tts_segment_loop, SegmentStats, SegmentInfo
|
from abogen.domain.conversion_engine import synthesize_text, SegmentStats, SegmentInfo
|
||||||
from abogen.domain.intro_outro import resolve_intro, resolve_outro
|
from abogen.domain.intro_outro import resolve_intro, resolve_outro
|
||||||
from abogen.domain.audio_buffer import (
|
from abogen.domain.audio_buffer import (
|
||||||
create_silence,
|
create_silence,
|
||||||
@@ -982,14 +982,6 @@ class ConversionThread(QThread):
|
|||||||
print("Using split pattern: (unprintable)")
|
print("Using split pattern: (unprintable)")
|
||||||
|
|
||||||
for text_segment in text_segments:
|
for text_segment in text_segments:
|
||||||
# Normalize text before TTS
|
|
||||||
try:
|
|
||||||
text_segment = self._tts_context.normalize(text_segment)
|
|
||||||
except Exception as exc:
|
|
||||||
self.log_updated.emit(
|
|
||||||
(f"Warning: Text normalization failed: {exc}", "orange")
|
|
||||||
)
|
|
||||||
|
|
||||||
def _qt_check_cancel() -> bool:
|
def _qt_check_cancel() -> bool:
|
||||||
if self.cancel_requested:
|
if self.cancel_requested:
|
||||||
sink_stack.close()
|
sink_stack.close()
|
||||||
@@ -1049,19 +1041,25 @@ class ConversionThread(QThread):
|
|||||||
total_characters=self.total_char_count,
|
total_characters=self.total_char_count,
|
||||||
)
|
)
|
||||||
|
|
||||||
run_tts_segment_loop(
|
try:
|
||||||
text=text_segment,
|
synthesize_text(
|
||||||
backend=self.backend,
|
text=text_segment,
|
||||||
voice=loaded_voice,
|
tts_context=self._tts_context,
|
||||||
speed=self.speed,
|
backend=self.backend,
|
||||||
split_pattern=active_split_pattern,
|
voice=loaded_voice,
|
||||||
stats=stats,
|
speed=self.speed,
|
||||||
check_cancel=_qt_check_cancel,
|
stats=stats,
|
||||||
on_progress=_qt_on_progress,
|
check_cancel=_qt_check_cancel,
|
||||||
chapter_sink=chapter_sink,
|
on_progress=_qt_on_progress,
|
||||||
audio_sink=merged_sink if merge_chapters_at_end else None,
|
chapter_sink=chapter_sink,
|
||||||
on_segment=_qt_on_segment,
|
audio_sink=merged_sink if merge_chapters_at_end else None,
|
||||||
)
|
on_segment=_qt_on_segment,
|
||||||
|
split_pattern_override=active_split_pattern,
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
self.log_updated.emit(
|
||||||
|
(f"Warning: TTS failed: {exc}", "orange")
|
||||||
|
)
|
||||||
|
|
||||||
self.processed_char_count = stats.processed_chars
|
self.processed_char_count = stats.processed_chars
|
||||||
current_time = stats.current_time
|
current_time = stats.current_time
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ from abogen.utils import (
|
|||||||
get_user_output_path,
|
get_user_output_path,
|
||||||
)
|
)
|
||||||
from abogen.voice_profiles import load_profiles, normalize_profile_entry
|
from abogen.voice_profiles import load_profiles, normalize_profile_entry
|
||||||
from abogen.llm_client import LLMClientError
|
|
||||||
from abogen.infrastructure.subtitle_writer import make_subtitle_writer
|
from abogen.infrastructure.subtitle_writer import make_subtitle_writer
|
||||||
from abogen.domain.chapter_titles import (
|
from abogen.domain.chapter_titles import (
|
||||||
simplify_heading_text as _simplify_heading_text,
|
simplify_heading_text as _simplify_heading_text,
|
||||||
@@ -118,7 +117,7 @@ from abogen.domain.audio_buffer import (
|
|||||||
)
|
)
|
||||||
from abogen.domain.audio_sink import AudioSink, open_audio_sink
|
from abogen.domain.audio_sink import AudioSink, open_audio_sink
|
||||||
from abogen.domain.pipeline_factory import PipelinePool
|
from abogen.domain.pipeline_factory import PipelinePool
|
||||||
from abogen.domain.conversion_engine import run_tts_segment_loop, process_and_write_subtitles, SegmentStats
|
from abogen.domain.conversion_engine import synthesize_text, process_and_write_subtitles, SegmentStats
|
||||||
from abogen.domain.voice_loader import VoiceCache, resolve_voice
|
from abogen.domain.voice_loader import VoiceCache, resolve_voice
|
||||||
from abogen.domain.voice_utils import resolve_voice_target as _resolve_voice_target
|
from abogen.domain.voice_utils import resolve_voice_target as _resolve_voice_target
|
||||||
|
|
||||||
@@ -422,15 +421,7 @@ def run_conversion_job(job: Job) -> None:
|
|||||||
supertonic_steps_override: Optional[int] = None,
|
supertonic_steps_override: Optional[int] = None,
|
||||||
) -> int:
|
) -> int:
|
||||||
nonlocal processed_chars, current_time
|
nonlocal processed_chars, current_time
|
||||||
if split_pattern is None:
|
|
||||||
split_pattern = tts_context.split_pattern
|
|
||||||
source_text = str(text or "")
|
source_text = str(text or "")
|
||||||
try:
|
|
||||||
normalized = tts_context.normalize(source_text)
|
|
||||||
except LLMClientError as exc:
|
|
||||||
job.add_log(f"LLM normalization failed: {exc}", level="error")
|
|
||||||
raise
|
|
||||||
local_segments = 0
|
|
||||||
|
|
||||||
provider = str(tts_provider or getattr(job, "tts_provider", "kokoro") or "kokoro").strip().lower() or "kokoro"
|
provider = str(tts_provider or getattr(job, "tts_provider", "kokoro") or "kokoro").strip().lower() or "kokoro"
|
||||||
if provider == "supertonic":
|
if provider == "supertonic":
|
||||||
@@ -467,12 +458,12 @@ def run_conversion_job(job: Job) -> None:
|
|||||||
def _preview(text: str) -> None:
|
def _preview(text: str) -> None:
|
||||||
job.add_log(f"{prefix}{stats.processed_chars:,}/{job.total_characters or '—'}: {text[:80]}")
|
job.add_log(f"{prefix}{stats.processed_chars:,}/{job.total_characters or '—'}: {text[:80]}")
|
||||||
|
|
||||||
local_segments, accumulated_tokens = run_tts_segment_loop(
|
local_segments, accumulated_tokens = synthesize_text(
|
||||||
text=normalized,
|
text=source_text,
|
||||||
|
tts_context=tts_context,
|
||||||
backend=backend,
|
backend=backend,
|
||||||
voice=resolved_voice,
|
voice=resolved_voice,
|
||||||
speed=effective_speed,
|
speed=effective_speed,
|
||||||
split_pattern=split_pattern,
|
|
||||||
stats=stats,
|
stats=stats,
|
||||||
check_cancel=canceller,
|
check_cancel=canceller,
|
||||||
on_progress=_on_progress,
|
on_progress=_on_progress,
|
||||||
|
|||||||
Reference in New Issue
Block a user