mirror of
https://github.com/denizsafak/abogen.git
synced 2026-07-22 07:10:28 +02:00
refactor(webui): replace inline get_pipeline/resolve_voice_target closures with domain modules
- 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)
This commit is contained in:
@@ -13,7 +13,6 @@ from typing import Any, Callable, Dict, List, Mapping, Optional
|
||||
|
||||
import numpy as np
|
||||
|
||||
from abogen.tts_plugin.utils import is_plugin_registered
|
||||
from abogen.infrastructure.exporters import ExportService
|
||||
from abogen.epub3.exporter import build_epub3_package
|
||||
from abogen.kokoro_text_normalization import ApostropheConfig, normalize_for_pipeline, HAS_NUM2WORDS
|
||||
@@ -31,10 +30,7 @@ from abogen.utils import (
|
||||
get_internal_cache_path,
|
||||
get_user_cache_path,
|
||||
get_user_output_path,
|
||||
load_config,
|
||||
)
|
||||
from abogen.tts_plugin.utils import create_pipeline
|
||||
from abogen.voice_formulas import get_new_voice
|
||||
from abogen.voice_profiles import load_profiles, normalize_profile_entry
|
||||
from abogen.llm_client import LLMClientError
|
||||
from abogen.infrastructure.subtitle_writer import create_subtitle_writer
|
||||
@@ -122,6 +118,8 @@ from abogen.domain.audio_buffer import (
|
||||
)
|
||||
from abogen.domain.audio_sink import AudioSink, open_audio_sink
|
||||
from abogen.domain.tokens import FakeToken
|
||||
from abogen.domain.pipeline_factory import PipelinePool
|
||||
from abogen.domain.voice_utils import resolve_voice_target as _resolve_voice_target
|
||||
|
||||
|
||||
from .service import Job, JobStatus
|
||||
@@ -184,8 +182,7 @@ def run_conversion_job(job: Job) -> None:
|
||||
audio_output_path: Optional[Path] = None
|
||||
extraction: Optional[Any] = None
|
||||
pipeline: Any = None
|
||||
pipelines: Dict[str, Any] = {}
|
||||
kokoro_cache_ready = False
|
||||
pipeline_pool = PipelinePool()
|
||||
normalized_profiles: Dict[str, Dict[str, Any]] = {}
|
||||
chunk_groups: Dict[int, List[Dict[str, Any]]] = {}
|
||||
active_chapter_configs: List[Dict[str, Any]] = []
|
||||
@@ -202,75 +199,23 @@ def run_conversion_job(job: Job) -> None:
|
||||
if normalized:
|
||||
normalized_profiles[str(name)] = normalized
|
||||
|
||||
def get_pipeline(provider: str) -> Any:
|
||||
nonlocal kokoro_cache_ready
|
||||
provider_norm = str(provider or "kokoro").strip().lower() or "kokoro"
|
||||
if not is_plugin_registered(provider_norm):
|
||||
provider_norm = "kokoro"
|
||||
|
||||
existing = pipelines.get(provider_norm)
|
||||
if existing is not None:
|
||||
return existing
|
||||
|
||||
if provider_norm == "supertonic":
|
||||
pipelines[provider_norm] = create_pipeline(
|
||||
"supertonic",
|
||||
)
|
||||
return pipelines[provider_norm]
|
||||
|
||||
# Kokoro
|
||||
cfg = load_config()
|
||||
disable_gpu = not job.use_gpu or not cfg.get("use_gpu", True)
|
||||
device = "cpu"
|
||||
if not disable_gpu:
|
||||
device = _select_device()
|
||||
# Create KPipeline instance directly (uses new Plugin Architecture)
|
||||
pipelines[provider_norm] = create_pipeline(
|
||||
"kokoro",
|
||||
lang_code=job.language,
|
||||
device=device
|
||||
)
|
||||
if not kokoro_cache_ready:
|
||||
_initialize_voice_cache(job)
|
||||
kokoro_cache_ready = True
|
||||
return pipelines[provider_norm]
|
||||
|
||||
def resolve_voice_target(raw_spec: str) -> tuple[str, str, Optional[float], Optional[int]]:
|
||||
"""Return (provider, voice_spec, speed_override, steps_override)."""
|
||||
spec = str(raw_spec or "").strip()
|
||||
speaker_name, _ = _split_speaker_reference(spec)
|
||||
if speaker_name and speaker_name in normalized_profiles:
|
||||
entry = normalized_profiles[speaker_name]
|
||||
provider = str(entry.get("provider") or "kokoro").strip().lower() or "kokoro"
|
||||
if provider == "supertonic":
|
||||
voice = str(entry.get("voice") or getattr(job, "voice", "M1") or "M1").strip() or "M1"
|
||||
steps = int(entry.get("total_steps") or getattr(job, "supertonic_total_steps", 5) or 5)
|
||||
speed = float(entry.get("speed") or getattr(job, "speed", 1.0) or 1.0)
|
||||
return "supertonic", _supertonic_voice_from_spec(voice, getattr(job, "voice", "M1")), speed, steps
|
||||
formula = _formula_from_kokoro_entry(entry)
|
||||
return "kokoro", formula or spec, None, None
|
||||
|
||||
fallback_provider = str(getattr(job, "tts_provider", "kokoro") or "kokoro").strip().lower() or "kokoro"
|
||||
inferred = _infer_provider_from_spec(spec, fallback=fallback_provider)
|
||||
if inferred == "supertonic":
|
||||
return "supertonic", _supertonic_voice_from_spec(spec, getattr(job, "voice", "M1")), None, None
|
||||
return "kokoro", spec, None, None
|
||||
|
||||
def resolve_voice_choice(raw_spec: str) -> tuple[str, str, Any, Optional[float], Optional[int]]:
|
||||
"""Resolve a raw voice spec into (provider, resolved_spec, choice, speed, steps).
|
||||
|
||||
For Kokoro formulas, `choice` will be a resolved voice tensor (via `voice_formulas`).
|
||||
For SuperTonic, `choice` will be a valid SuperTonic voice id.
|
||||
"""
|
||||
|
||||
provider, resolved, speed, steps = resolve_voice_target(raw_spec)
|
||||
"""Resolve a raw voice spec into (provider, resolved_spec, choice, speed, steps)."""
|
||||
provider, resolved, speed, steps = _resolve_voice_target(
|
||||
raw_spec,
|
||||
normalized_profiles,
|
||||
job_voice=getattr(job, "voice", "M1"),
|
||||
job_tts_provider=getattr(job, "tts_provider", "kokoro"),
|
||||
job_supertonic_total_steps=getattr(job, "supertonic_total_steps", 5),
|
||||
job_speed=getattr(job, "speed", 1.0),
|
||||
)
|
||||
cache_key = f"{provider}:{resolved}" if resolved else provider
|
||||
cached = voice_cache.get(cache_key)
|
||||
if cached is not None:
|
||||
return provider, resolved, cached, speed, steps
|
||||
|
||||
if provider == "kokoro":
|
||||
kokoro_backend = get_pipeline("kokoro")
|
||||
kokoro_backend = pipeline_pool.get("kokoro", job.language, job.use_gpu, job=job)
|
||||
choice = _resolve_voice(kokoro_backend, resolved, job.use_gpu)
|
||||
else:
|
||||
choice = resolved
|
||||
@@ -405,9 +350,13 @@ def run_conversion_job(job: Job) -> None:
|
||||
|
||||
base_voice_spec = _job_voice_fallback(job)
|
||||
voice_cache: Dict[str, Any] = {}
|
||||
base_provider, base_voice_resolved, _, _ = resolve_voice_target(base_voice_spec)
|
||||
base_provider, base_voice_resolved, _, _ = _resolve_voice_target(
|
||||
base_voice_spec, normalized_profiles,
|
||||
job_voice=getattr(job, "voice", "M1"),
|
||||
job_tts_provider=getattr(job, "tts_provider", "kokoro"),
|
||||
)
|
||||
if base_provider == "kokoro" and base_voice_resolved and "*" not in base_voice_resolved:
|
||||
kokoro_backend = get_pipeline("kokoro")
|
||||
kokoro_backend = pipeline_pool.get("kokoro", job.language, job.use_gpu, job=job)
|
||||
voice_cache[f"kokoro:{base_voice_resolved}"] = _resolve_voice(kokoro_backend, base_voice_resolved, job.use_gpu)
|
||||
processed_chars = 0
|
||||
current_time = 0.0
|
||||
@@ -473,7 +422,7 @@ def run_conversion_job(job: Job) -> None:
|
||||
|
||||
provider = str(tts_provider or getattr(job, "tts_provider", "kokoro") or "kokoro").strip().lower() or "kokoro"
|
||||
if provider == "supertonic":
|
||||
supertonic_pipeline = get_pipeline("supertonic")
|
||||
supertonic_pipeline = pipeline_pool.get("supertonic", job.language, job.use_gpu, job=job)
|
||||
voice_name = _supertonic_voice_from_spec(voice_choice, getattr(job, "voice", "M1"))
|
||||
segment_iter = supertonic_pipeline(
|
||||
normalized,
|
||||
@@ -483,7 +432,7 @@ def run_conversion_job(job: Job) -> None:
|
||||
total_steps=int(supertonic_steps_override if supertonic_steps_override is not None else getattr(job, "supertonic_total_steps", 5)),
|
||||
)
|
||||
else:
|
||||
kokoro_backend = get_pipeline("kokoro")
|
||||
kokoro_backend = pipeline_pool.get("kokoro", job.language, job.use_gpu, job=job)
|
||||
segment_iter = kokoro_backend(
|
||||
normalized,
|
||||
voice=voice_choice,
|
||||
@@ -605,12 +554,18 @@ def run_conversion_job(job: Job) -> None:
|
||||
if not chapter_voice_spec:
|
||||
chapter_voice_spec = base_voice_spec
|
||||
|
||||
chapter_provider, chapter_voice_resolved, chapter_speed, chapter_steps = resolve_voice_target(chapter_voice_spec)
|
||||
chapter_provider, chapter_voice_resolved, chapter_speed, chapter_steps = _resolve_voice_target(
|
||||
chapter_voice_spec, normalized_profiles,
|
||||
job_voice=getattr(job, "voice", "M1"),
|
||||
job_tts_provider=getattr(job, "tts_provider", "kokoro"),
|
||||
job_supertonic_total_steps=getattr(job, "supertonic_total_steps", 5),
|
||||
job_speed=getattr(job, "speed", 1.0),
|
||||
)
|
||||
chapter_cache_key = f"{chapter_provider}:{chapter_voice_resolved}" if chapter_voice_resolved else chapter_provider
|
||||
if chapter_provider == "kokoro":
|
||||
voice_choice = voice_cache.get(chapter_cache_key)
|
||||
if voice_choice is None:
|
||||
kokoro_backend = get_pipeline("kokoro")
|
||||
kokoro_backend = pipeline_pool.get("kokoro", job.language, job.use_gpu, job=job)
|
||||
voice_choice = _resolve_voice(kokoro_backend, chapter_voice_resolved, job.use_gpu)
|
||||
voice_cache[chapter_cache_key] = voice_choice
|
||||
else:
|
||||
@@ -743,12 +698,18 @@ def run_conversion_job(job: Job) -> None:
|
||||
chunk_steps_use = chapter_steps
|
||||
chunk_voice_choice = voice_choice
|
||||
else:
|
||||
chunk_provider, chunk_voice_resolved, chunk_speed_use, chunk_steps_use = resolve_voice_target(chunk_voice_spec)
|
||||
chunk_provider, chunk_voice_resolved, chunk_speed_use, chunk_steps_use = _resolve_voice_target(
|
||||
chunk_voice_spec, normalized_profiles,
|
||||
job_voice=getattr(job, "voice", "M1"),
|
||||
job_tts_provider=getattr(job, "tts_provider", "kokoro"),
|
||||
job_supertonic_total_steps=getattr(job, "supertonic_total_steps", 5),
|
||||
job_speed=getattr(job, "speed", 1.0),
|
||||
)
|
||||
chunk_cache_key = f"{chunk_provider}:{chunk_voice_resolved}" if chunk_voice_resolved else chunk_provider
|
||||
if chunk_provider == "kokoro":
|
||||
chunk_voice_choice = voice_cache.get(chunk_cache_key)
|
||||
if chunk_voice_choice is None:
|
||||
kokoro_backend = get_pipeline("kokoro")
|
||||
kokoro_backend = pipeline_pool.get("kokoro", job.language, job.use_gpu, job=job)
|
||||
chunk_voice_choice = _resolve_voice(
|
||||
kokoro_backend,
|
||||
chunk_voice_resolved,
|
||||
@@ -1054,12 +1015,7 @@ def run_conversion_job(job: Job) -> None:
|
||||
|
||||
# Explicitly release the pipeline and force garbage collection to prevent
|
||||
# memory accumulation in the worker process, which can lead to host lockups.
|
||||
for p in pipelines.values():
|
||||
try:
|
||||
p.dispose()
|
||||
except Exception:
|
||||
pass
|
||||
pipelines.clear()
|
||||
pipeline_pool.dispose_all()
|
||||
pipeline = None
|
||||
gc.collect()
|
||||
try:
|
||||
@@ -1100,21 +1056,6 @@ def run_conversion_job(job: Job) -> None:
|
||||
) from exc
|
||||
|
||||
|
||||
def _load_pipeline(job: Job):
|
||||
cfg = load_config()
|
||||
disable_gpu = not job.use_gpu or not cfg.get("use_gpu", True)
|
||||
provider = str(getattr(job, "tts_provider", "kokoro") or "kokoro").strip().lower()
|
||||
if provider == "supertonic":
|
||||
return create_pipeline(
|
||||
"supertonic",
|
||||
)
|
||||
|
||||
device = "cpu"
|
||||
if not disable_gpu:
|
||||
device = _select_device()
|
||||
return create_pipeline("kokoro", lang_code=job.language, device=device)
|
||||
|
||||
|
||||
def _prepare_output_dir(job: Job) -> Path:
|
||||
from platformdirs import user_desktop_dir # type: ignore[import-not-found]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user