mirror of
https://github.com/denizsafak/abogen.git
synced 2026-09-20 11:40:57 +02:00
refactor: move pool/cache/resolver into run_conversion()
- New: application/voice_resolver.py — AppVoiceResolver (app-layer, takes ConversionRequest) - conversion_service.py: run_conversion(request, events) creates PipelinePool, VoiceCache, AppVoiceResolver internally - conversion_runner.py: 268→165 lines, removed WebUIVoiceResolver, pool/cache creation - Tests: mock fixture for pool/resolver, removed unused variables - Added pool.dispose_all() in finally block for cleanup
This commit is contained in:
@@ -16,16 +16,12 @@ The service NEVER imports from PyQt or WebUI.
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import defaultdict
|
||||
from typing import Dict
|
||||
from typing import Any, Dict
|
||||
|
||||
from abogen.application.conversion_executor import execute_conversion
|
||||
from abogen.application.conversion_models import ConversionPlan
|
||||
from abogen.application.conversion_planner import build_conversion_plan
|
||||
from abogen.application.conversion_ports import (
|
||||
ConversionEvents,
|
||||
PipelineProvider,
|
||||
VoiceResolver,
|
||||
)
|
||||
from abogen.application.conversion_ports import ConversionEvents
|
||||
from abogen.application.conversion_request import ConversionRequest
|
||||
from abogen.application.conversion_result import ConversionResult
|
||||
from abogen.domain.normalization import build_tts_context
|
||||
@@ -34,22 +30,19 @@ from abogen.domain.normalization import build_tts_context
|
||||
def run_conversion(
|
||||
request: ConversionRequest,
|
||||
events: ConversionEvents,
|
||||
pipeline_provider: PipelineProvider,
|
||||
voice_resolver: VoiceResolver,
|
||||
) -> ConversionResult:
|
||||
"""Execute a conversion request and return the result.
|
||||
|
||||
This is the single entry point for both UIs. It orchestrates:
|
||||
1. TTS context preparation
|
||||
2. Conversion planning
|
||||
3. Conversion execution
|
||||
4. Resource cleanup
|
||||
1. Voice infrastructure setup (pool, cache, resolver)
|
||||
2. TTS context preparation
|
||||
3. Conversion planning
|
||||
4. Conversion execution
|
||||
5. Resource cleanup
|
||||
|
||||
Args:
|
||||
request: Normalized conversion request
|
||||
events: UI-specific callbacks (log, progress, check_cancelled)
|
||||
pipeline_provider: Provides TTS backends
|
||||
voice_resolver: Resolves voice specs into loaded voices
|
||||
|
||||
Returns:
|
||||
ConversionResult with paths and markers
|
||||
@@ -59,9 +52,18 @@ def run_conversion(
|
||||
ValueError: If request is invalid
|
||||
Exception: On TTS or I/O errors
|
||||
"""
|
||||
from abogen.domain.pipeline_factory import PipelinePool
|
||||
from abogen.domain.voice_loader import VoiceCache
|
||||
|
||||
pool = PipelinePool()
|
||||
voice_cache = VoiceCache()
|
||||
|
||||
try:
|
||||
# Stage 1: Prepare TTS context
|
||||
# Stage 0: Create voice resolver
|
||||
events.log("Preparing conversion pipeline")
|
||||
resolver = _create_voice_resolver(request, pool, voice_cache)
|
||||
|
||||
# Stage 1: Prepare TTS context
|
||||
usage_counter: Dict[str, int] = defaultdict(int)
|
||||
tts_context = build_tts_context(
|
||||
language=request.language,
|
||||
@@ -83,8 +85,8 @@ def run_conversion(
|
||||
result = execute_conversion(
|
||||
plan=plan,
|
||||
events=events,
|
||||
pipeline_provider=pipeline_provider,
|
||||
voice_resolver=voice_resolver,
|
||||
pipeline_provider=pool,
|
||||
voice_resolver=resolver,
|
||||
tts_context=tts_context,
|
||||
)
|
||||
|
||||
@@ -100,6 +102,35 @@ def run_conversion(
|
||||
except Exception as e:
|
||||
events.log(f"Conversion failed: {e}", level="error")
|
||||
raise
|
||||
finally:
|
||||
pool.dispose_all()
|
||||
|
||||
|
||||
def _create_voice_resolver(
|
||||
request: ConversionRequest,
|
||||
pool: Any,
|
||||
cache: Any,
|
||||
) -> Any:
|
||||
"""Create AppVoiceResolver with loaded profiles.
|
||||
|
||||
Loads voice profiles from disk, normalizes them, and creates
|
||||
an AppVoiceResolver that can resolve voice specs into loaded voices.
|
||||
"""
|
||||
from abogen.application.voice_resolver import AppVoiceResolver
|
||||
from abogen.voice_profiles import load_profiles, normalize_profile_entry
|
||||
|
||||
try:
|
||||
profiles = load_profiles()
|
||||
except Exception:
|
||||
profiles = {}
|
||||
|
||||
normalized_profiles: Dict[str, Dict[str, Any]] = {}
|
||||
for name, entry in (profiles or {}).items():
|
||||
normalized = normalize_profile_entry(entry)
|
||||
if normalized:
|
||||
normalized_profiles[str(name)] = normalized
|
||||
|
||||
return AppVoiceResolver(request, normalized_profiles, pool, cache)
|
||||
|
||||
|
||||
def _finalize(
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
"""AppVoiceResolver — voice resolution inside the application layer.
|
||||
|
||||
Resolves voice specs into loaded voices using profiles, pipeline pool,
|
||||
and voice cache. Replaces UI-specific resolvers (WebUIVoiceResolver,
|
||||
PyQtVoiceResolver) with a single app-layer implementation.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from abogen.application.conversion_ports import ResolvedVoice, VoiceResolver
|
||||
from abogen.application.conversion_request import ConversionRequest
|
||||
from abogen.domain.pipeline_factory import PipelinePool
|
||||
from abogen.domain.voice_loader import VoiceCache, resolve_voice
|
||||
from abogen.domain.voice_utils import resolve_voice_target
|
||||
|
||||
|
||||
class AppVoiceResolver:
|
||||
"""App-layer implementation of VoiceResolver protocol.
|
||||
|
||||
Uses ConversionRequest instead of Job. Loads profiles, creates
|
||||
resolver internally — UIs don't need to manage this.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
request: ConversionRequest,
|
||||
normalized_profiles: Dict[str, Dict[str, Any]],
|
||||
pool: PipelinePool,
|
||||
cache: VoiceCache,
|
||||
):
|
||||
self._request = request
|
||||
self._profiles = normalized_profiles
|
||||
self._cache = cache
|
||||
self._pool = pool
|
||||
|
||||
def resolve(self, voice_spec: str) -> ResolvedVoice:
|
||||
"""Resolve a voice spec into a loaded voice."""
|
||||
provider, resolved, speed, steps = resolve_voice_target(
|
||||
voice_spec,
|
||||
self._profiles,
|
||||
job_voice=self._request.voice,
|
||||
job_tts_provider=self._request.tts_provider,
|
||||
job_supertonic_total_steps=self._request.supertonic_total_steps,
|
||||
job_speed=self._request.speed,
|
||||
)
|
||||
|
||||
cache_key = f"{provider}:{resolved}" if resolved else provider
|
||||
cached = self._cache.get(cache_key)
|
||||
if cached is not None:
|
||||
return ResolvedVoice(
|
||||
provider=provider,
|
||||
resolved_spec=resolved,
|
||||
voice=cached,
|
||||
speed=speed,
|
||||
supertonic_steps=steps or 0,
|
||||
)
|
||||
|
||||
if provider == "kokoro":
|
||||
kokoro_backend = self._pool.get(
|
||||
"kokoro", self._request.language, self._request.use_gpu,
|
||||
)
|
||||
loaded = resolve_voice(
|
||||
resolved, kokoro_backend, self._request.use_gpu, cache=self._cache,
|
||||
)
|
||||
else:
|
||||
loaded = resolved
|
||||
|
||||
self._cache.set(cache_key, loaded)
|
||||
return ResolvedVoice(
|
||||
provider=provider,
|
||||
resolved_spec=resolved,
|
||||
voice=loaded,
|
||||
speed=speed,
|
||||
supertonic_steps=steps or 0,
|
||||
)
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
This module is the WebUI's adapter for the conversion system. It:
|
||||
1. Builds a ConversionRequest from a Job
|
||||
2. Creates adapter objects (Events, VoiceResolver)
|
||||
2. Creates an Events adapter
|
||||
3. Calls run_conversion() from the shared application layer
|
||||
4. Maps the ConversionResult back to Job state
|
||||
|
||||
@@ -17,25 +17,21 @@ from __future__ import annotations
|
||||
|
||||
import gc
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
from typing import Any
|
||||
|
||||
from abogen.application.conversion_config import (
|
||||
ChapterChunkConfig,
|
||||
Epub3ExportConfig,
|
||||
)
|
||||
from abogen.application.conversion_ports import ConversionCancelled, ResolvedVoice
|
||||
from abogen.application.conversion_ports import ConversionCancelled
|
||||
from abogen.application.conversion_request import ConversionRequest
|
||||
from abogen.application.conversion_service import run_conversion
|
||||
from abogen.domain.enums import (
|
||||
Language,
|
||||
OutputFormat,
|
||||
SaveMode,
|
||||
SubtitleFormat,
|
||||
SubtitleMode,
|
||||
)
|
||||
from abogen.domain.pipeline_factory import PipelinePool
|
||||
from abogen.domain.voice_loader import VoiceCache, resolve_voice
|
||||
from abogen.domain.voice_utils import resolve_voice_target as _resolve_voice_target
|
||||
|
||||
from .service import Job, JobStatus
|
||||
|
||||
@@ -47,7 +43,6 @@ from .service import Job, JobStatus
|
||||
|
||||
def _build_request(job: Job) -> ConversionRequest:
|
||||
"""Build a ConversionRequest from a WebUI Job."""
|
||||
# Determine source path
|
||||
source_path = Path(job.stored_path) if job.stored_path else None
|
||||
|
||||
return ConversionRequest(
|
||||
@@ -148,7 +143,7 @@ def _apply_result(job: Job, result: Any) -> None:
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Adapters: Events, VoiceResolver
|
||||
# Events adapter
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -170,58 +165,6 @@ class WebUIEventsAdapter:
|
||||
raise ConversionCancelled("Job cancelled")
|
||||
|
||||
|
||||
class WebUIVoiceResolver:
|
||||
"""Adapts WebUI voice resolution to VoiceResolver protocol."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
job: Job,
|
||||
normalized_profiles: Dict[str, Dict[str, Any]],
|
||||
voice_cache: VoiceCache,
|
||||
pool: PipelinePool,
|
||||
):
|
||||
self._job = job
|
||||
self._profiles = normalized_profiles
|
||||
self._cache = voice_cache
|
||||
self._pool = pool
|
||||
|
||||
def resolve(self, voice_spec: str) -> ResolvedVoice:
|
||||
provider, resolved, speed, steps = _resolve_voice_target(
|
||||
voice_spec,
|
||||
self._profiles,
|
||||
job_voice=self._job.voice,
|
||||
job_tts_provider=self._job.tts_provider,
|
||||
job_supertonic_total_steps=self._job.supertonic_total_steps,
|
||||
job_speed=self._job.speed,
|
||||
)
|
||||
|
||||
cache_key = f"{provider}:{resolved}" if resolved else provider
|
||||
cached = self._cache.get(cache_key)
|
||||
if cached is not None:
|
||||
return ResolvedVoice(
|
||||
provider=provider,
|
||||
resolved_spec=resolved,
|
||||
voice=cached,
|
||||
speed=speed,
|
||||
supertonic_steps=steps or 0,
|
||||
)
|
||||
|
||||
if provider == "kokoro":
|
||||
kokoro_backend = self._pool.get("kokoro", self._job.language, self._job.use_gpu)
|
||||
loaded = resolve_voice(resolved, kokoro_backend, self._job.use_gpu, cache=self._cache)
|
||||
else:
|
||||
loaded = resolved
|
||||
|
||||
self._cache.set(cache_key, loaded)
|
||||
return ResolvedVoice(
|
||||
provider=provider,
|
||||
resolved_spec=resolved,
|
||||
voice=loaded,
|
||||
speed=speed,
|
||||
supertonic_steps=steps or 0,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Main entry point
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -231,19 +174,11 @@ def run_conversion_job(job: Job) -> None:
|
||||
"""Run a conversion job using the shared application layer."""
|
||||
job.add_log("Preparing conversion pipeline")
|
||||
|
||||
# Build request from job data
|
||||
request = _build_request(job)
|
||||
|
||||
# Create adapters
|
||||
events = WebUIEventsAdapter(job)
|
||||
pool = PipelinePool()
|
||||
voice_cache = VoiceCache()
|
||||
voice_resolver = WebUIVoiceResolver(
|
||||
job, request.speakers, voice_cache, pool,
|
||||
)
|
||||
|
||||
try:
|
||||
result = run_conversion(request, events, pool, voice_resolver)
|
||||
result = run_conversion(request, events)
|
||||
_apply_result(job, result)
|
||||
|
||||
if job.status != JobStatus.CANCELLED:
|
||||
@@ -257,8 +192,6 @@ def run_conversion_job(job: Job) -> None:
|
||||
job.status = JobStatus.FAILED
|
||||
job.add_log(f"Job failed: {exc}", level="error")
|
||||
finally:
|
||||
pool.dispose_all()
|
||||
voice_cache.clear()
|
||||
gc.collect()
|
||||
try:
|
||||
import torch
|
||||
|
||||
Reference in New Issue
Block a user