mirror of
https://github.com/denizsafak/abogen.git
synced 2026-09-20 11:40:57 +02:00
refactor: voice resolution on ConversionRequest, PipelinePool without job param
- Added speakers field to ConversionRequest - Rewrote collect_required_voice_ids(), initialize_voice_cache(), job_voice_fallback(), chapter_voice_spec(), chunk_voice_spec() to accept ConversionRequest instead of job - PipelinePool.get() now takes request= instead of job= - Updated all tests to use ConversionRequest interface - 1493 tests passing
This commit is contained in:
@@ -95,6 +95,9 @@ class ConversionRequest:
|
||||
# --- Metadata ---
|
||||
metadata_tags: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
# --- Voice profiles (loaded by UI, used by app for voice resolution) ---
|
||||
speakers: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
# --- Artifacts ---
|
||||
cover_image_path: Optional[Path] = None
|
||||
cover_image_mime: Optional[str] = None
|
||||
|
||||
@@ -95,7 +95,8 @@ class PipelinePool:
|
||||
language: str,
|
||||
use_gpu: bool,
|
||||
*,
|
||||
job: Any = None,
|
||||
request: Any = None,
|
||||
events: Any = None,
|
||||
) -> Any:
|
||||
"""Get or create a cached pipeline for the given provider.
|
||||
|
||||
@@ -103,7 +104,8 @@ class PipelinePool:
|
||||
provider: TTS provider name ("kokoro" or "supertonic").
|
||||
language: Language code (for kokoro).
|
||||
use_gpu: Whether GPU acceleration is requested.
|
||||
job: Optional job object for voice cache initialization.
|
||||
request: ConversionRequest for voice cache initialization.
|
||||
events: ConversionEvents for logging during cache init.
|
||||
"""
|
||||
provider = str(provider or "kokoro").strip().lower() or "kokoro"
|
||||
if not is_plugin_registered(provider):
|
||||
@@ -116,8 +118,8 @@ class PipelinePool:
|
||||
pipeline = create_pipeline_for_job(provider, language, use_gpu)
|
||||
self._pipelines[provider] = pipeline
|
||||
|
||||
if provider == "kokoro" and not self._voice_cache_initialized and job is not None:
|
||||
initialize_voice_cache(job)
|
||||
if provider == "kokoro" and not self._voice_cache_initialized and request is not None:
|
||||
initialize_voice_cache(request, events=events)
|
||||
self._voice_cache_initialized = True
|
||||
|
||||
return pipeline
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
Functions for resolving voice specifications, collecting required voice IDs,
|
||||
and determining the voice to use for chapters and chunks.
|
||||
|
||||
All functions accept ConversionRequest (the app-layer contract) instead of
|
||||
UI-specific objects. This keeps the domain layer UI-agnostic.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -29,12 +32,28 @@ def spec_to_voice_ids(spec: Any) -> Set[str]:
|
||||
return set()
|
||||
|
||||
|
||||
def job_voice_fallback(job: Any) -> str:
|
||||
base = str(getattr(job, "voice", "") or "").strip()
|
||||
def _get_chapter_overrides(request: Any) -> list:
|
||||
"""Extract chapter overrides from ConversionRequest."""
|
||||
cc = getattr(request, "chapter_chunk", None)
|
||||
if cc is not None:
|
||||
return getattr(cc, "chapter_overrides", []) or []
|
||||
return []
|
||||
|
||||
|
||||
def _get_chunks(request: Any) -> list:
|
||||
"""Extract chunks from ConversionRequest."""
|
||||
cc = getattr(request, "chapter_chunk", None)
|
||||
if cc is not None:
|
||||
return getattr(cc, "chunks", []) or []
|
||||
return []
|
||||
|
||||
|
||||
def job_voice_fallback(request: Any) -> str:
|
||||
base = str(getattr(request, "voice", "") or "").strip()
|
||||
if base and base != "__custom_mix":
|
||||
return base
|
||||
|
||||
speakers = getattr(job, "speakers", None)
|
||||
speakers = getattr(request, "speakers", None)
|
||||
if isinstance(speakers, dict):
|
||||
narrator = speakers.get("narrator")
|
||||
if isinstance(narrator, dict):
|
||||
@@ -52,7 +71,7 @@ def job_voice_fallback(job: Any) -> str:
|
||||
if candidate and candidate != "__custom_mix":
|
||||
return candidate
|
||||
|
||||
for chapter in getattr(job, "chapters", []) or []:
|
||||
for chapter in _get_chapter_overrides(request):
|
||||
if not isinstance(chapter, dict):
|
||||
continue
|
||||
for key in ("resolved_voice", "voice_formula", "voice"):
|
||||
@@ -63,24 +82,24 @@ def job_voice_fallback(job: Any) -> str:
|
||||
return ""
|
||||
|
||||
|
||||
def collect_required_voice_ids(job: Any) -> Set[str]:
|
||||
def collect_required_voice_ids(request: Any) -> Set[str]:
|
||||
voices: Set[str] = set()
|
||||
voices.update(spec_to_voice_ids(job.voice))
|
||||
voices.update(spec_to_voice_ids(job_voice_fallback(job)))
|
||||
voices.update(spec_to_voice_ids(request.voice))
|
||||
voices.update(spec_to_voice_ids(job_voice_fallback(request)))
|
||||
|
||||
for chapter in getattr(job, "chapters", []) or []:
|
||||
for chapter in _get_chapter_overrides(request):
|
||||
if not isinstance(chapter, dict):
|
||||
continue
|
||||
for key in ("resolved_voice", "voice_formula", "voice"):
|
||||
voices.update(spec_to_voice_ids(chapter.get(key)))
|
||||
|
||||
for chunk in getattr(job, "chunks", []) or []:
|
||||
for chunk in _get_chunks(request):
|
||||
if not isinstance(chunk, dict):
|
||||
continue
|
||||
for key in ("resolved_voice", "voice_formula", "voice"):
|
||||
voices.update(spec_to_voice_ids(chunk.get(key)))
|
||||
|
||||
speakers = getattr(job, "speakers", {})
|
||||
speakers = getattr(request, "speakers", {})
|
||||
if isinstance(speakers, dict):
|
||||
for payload in speakers.values() or []:
|
||||
if not isinstance(payload, dict):
|
||||
@@ -92,30 +111,38 @@ def collect_required_voice_ids(job: Any) -> Set[str]:
|
||||
return voices
|
||||
|
||||
|
||||
def initialize_voice_cache(job: Any) -> None:
|
||||
def initialize_voice_cache(request: Any, events: Any = None) -> None:
|
||||
"""Initialize voice cache by downloading required voice assets.
|
||||
|
||||
Args:
|
||||
request: ConversionRequest with voice/chapter/chunk/speaker info.
|
||||
events: ConversionEvents for logging (optional, for backward compat).
|
||||
"""
|
||||
log = (lambda msg, level="info": events.log(msg, level=level)) if events else (lambda msg, level="info": None)
|
||||
|
||||
try:
|
||||
targets = collect_required_voice_ids(job)
|
||||
targets = collect_required_voice_ids(request)
|
||||
downloaded, errors = ensure_voice_assets(
|
||||
targets,
|
||||
on_progress=lambda message: job.add_log(message, level="debug"),
|
||||
on_progress=lambda message: log(message, level="debug"),
|
||||
)
|
||||
except RuntimeError as exc:
|
||||
job.add_log(f"Voice cache unavailable: {exc}", level="warning")
|
||||
log(f"Voice cache unavailable: {exc}", level="warning")
|
||||
return
|
||||
|
||||
if downloaded:
|
||||
job.add_log(
|
||||
log(
|
||||
f"Cached {len(downloaded)} voice asset{'s' if len(downloaded) != 1 else ''} locally.",
|
||||
level="info",
|
||||
)
|
||||
|
||||
for voice_id, error in errors.items():
|
||||
job.add_log(f"Failed to cache voice '{voice_id}': {error}", level="warning")
|
||||
log(f"Failed to cache voice '{voice_id}': {error}", level="warning")
|
||||
|
||||
|
||||
def chapter_voice_spec(job: Any, override: Optional[Dict[str, Any]]) -> str:
|
||||
def chapter_voice_spec(request: Any, override: Optional[Dict[str, Any]]) -> str:
|
||||
if not override:
|
||||
return job_voice_fallback(job)
|
||||
return job_voice_fallback(request)
|
||||
|
||||
resolved = str(override.get("resolved_voice", "")).strip()
|
||||
if resolved:
|
||||
@@ -129,17 +156,17 @@ def chapter_voice_spec(job: Any, override: Optional[Dict[str, Any]]) -> str:
|
||||
if voice:
|
||||
return voice
|
||||
|
||||
return job_voice_fallback(job)
|
||||
return job_voice_fallback(request)
|
||||
|
||||
|
||||
def chunk_voice_spec(job: Any, chunk: Dict[str, Any], fallback: str) -> str:
|
||||
def chunk_voice_spec(request: Any, chunk: Dict[str, Any], fallback: str) -> str:
|
||||
for key in ("resolved_voice", "voice_formula", "voice"):
|
||||
value = chunk.get(key)
|
||||
if value:
|
||||
return str(value)
|
||||
|
||||
speaker_id = chunk.get("speaker_id")
|
||||
speakers = getattr(job, "speakers", None)
|
||||
speakers = getattr(request, "speakers", None)
|
||||
if isinstance(speakers, dict) and speaker_id in speakers:
|
||||
speaker_entry = speakers.get(speaker_id) or {}
|
||||
if isinstance(speaker_entry, dict):
|
||||
@@ -163,7 +190,7 @@ def chunk_voice_spec(job: Any, chunk: Dict[str, Any], fallback: str) -> str:
|
||||
|
||||
if fallback:
|
||||
return fallback
|
||||
return job_voice_fallback(job)
|
||||
return job_voice_fallback(request)
|
||||
|
||||
|
||||
def resolve_fallback_voice_spec(
|
||||
|
||||
Reference in New Issue
Block a user