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 __future__ import annotations
|
||||||
|
|
||||||
from collections import defaultdict
|
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_executor import execute_conversion
|
||||||
from abogen.application.conversion_models import ConversionPlan
|
from abogen.application.conversion_models import ConversionPlan
|
||||||
from abogen.application.conversion_planner import build_conversion_plan
|
from abogen.application.conversion_planner import build_conversion_plan
|
||||||
from abogen.application.conversion_ports import (
|
from abogen.application.conversion_ports import ConversionEvents
|
||||||
ConversionEvents,
|
|
||||||
PipelineProvider,
|
|
||||||
VoiceResolver,
|
|
||||||
)
|
|
||||||
from abogen.application.conversion_request import ConversionRequest
|
from abogen.application.conversion_request import ConversionRequest
|
||||||
from abogen.application.conversion_result import ConversionResult
|
from abogen.application.conversion_result import ConversionResult
|
||||||
from abogen.domain.normalization import build_tts_context
|
from abogen.domain.normalization import build_tts_context
|
||||||
@@ -34,22 +30,19 @@ from abogen.domain.normalization import build_tts_context
|
|||||||
def run_conversion(
|
def run_conversion(
|
||||||
request: ConversionRequest,
|
request: ConversionRequest,
|
||||||
events: ConversionEvents,
|
events: ConversionEvents,
|
||||||
pipeline_provider: PipelineProvider,
|
|
||||||
voice_resolver: VoiceResolver,
|
|
||||||
) -> ConversionResult:
|
) -> ConversionResult:
|
||||||
"""Execute a conversion request and return the result.
|
"""Execute a conversion request and return the result.
|
||||||
|
|
||||||
This is the single entry point for both UIs. It orchestrates:
|
This is the single entry point for both UIs. It orchestrates:
|
||||||
1. TTS context preparation
|
1. Voice infrastructure setup (pool, cache, resolver)
|
||||||
2. Conversion planning
|
2. TTS context preparation
|
||||||
3. Conversion execution
|
3. Conversion planning
|
||||||
4. Resource cleanup
|
4. Conversion execution
|
||||||
|
5. Resource cleanup
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
request: Normalized conversion request
|
request: Normalized conversion request
|
||||||
events: UI-specific callbacks (log, progress, check_cancelled)
|
events: UI-specific callbacks (log, progress, check_cancelled)
|
||||||
pipeline_provider: Provides TTS backends
|
|
||||||
voice_resolver: Resolves voice specs into loaded voices
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
ConversionResult with paths and markers
|
ConversionResult with paths and markers
|
||||||
@@ -59,9 +52,18 @@ def run_conversion(
|
|||||||
ValueError: If request is invalid
|
ValueError: If request is invalid
|
||||||
Exception: On TTS or I/O errors
|
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:
|
try:
|
||||||
# Stage 1: Prepare TTS context
|
# Stage 0: Create voice resolver
|
||||||
events.log("Preparing conversion pipeline")
|
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)
|
usage_counter: Dict[str, int] = defaultdict(int)
|
||||||
tts_context = build_tts_context(
|
tts_context = build_tts_context(
|
||||||
language=request.language,
|
language=request.language,
|
||||||
@@ -83,8 +85,8 @@ def run_conversion(
|
|||||||
result = execute_conversion(
|
result = execute_conversion(
|
||||||
plan=plan,
|
plan=plan,
|
||||||
events=events,
|
events=events,
|
||||||
pipeline_provider=pipeline_provider,
|
pipeline_provider=pool,
|
||||||
voice_resolver=voice_resolver,
|
voice_resolver=resolver,
|
||||||
tts_context=tts_context,
|
tts_context=tts_context,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -100,6 +102,35 @@ def run_conversion(
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
events.log(f"Conversion failed: {e}", level="error")
|
events.log(f"Conversion failed: {e}", level="error")
|
||||||
raise
|
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(
|
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:
|
This module is the WebUI's adapter for the conversion system. It:
|
||||||
1. Builds a ConversionRequest from a Job
|
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
|
3. Calls run_conversion() from the shared application layer
|
||||||
4. Maps the ConversionResult back to Job state
|
4. Maps the ConversionResult back to Job state
|
||||||
|
|
||||||
@@ -17,25 +17,21 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import gc
|
import gc
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict
|
from typing import Any
|
||||||
|
|
||||||
from abogen.application.conversion_config import (
|
from abogen.application.conversion_config import (
|
||||||
ChapterChunkConfig,
|
ChapterChunkConfig,
|
||||||
Epub3ExportConfig,
|
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_request import ConversionRequest
|
||||||
from abogen.application.conversion_service import run_conversion
|
from abogen.application.conversion_service import run_conversion
|
||||||
from abogen.domain.enums import (
|
from abogen.domain.enums import (
|
||||||
Language,
|
|
||||||
OutputFormat,
|
OutputFormat,
|
||||||
SaveMode,
|
SaveMode,
|
||||||
SubtitleFormat,
|
SubtitleFormat,
|
||||||
SubtitleMode,
|
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
|
from .service import Job, JobStatus
|
||||||
|
|
||||||
@@ -47,7 +43,6 @@ from .service import Job, JobStatus
|
|||||||
|
|
||||||
def _build_request(job: Job) -> ConversionRequest:
|
def _build_request(job: Job) -> ConversionRequest:
|
||||||
"""Build a ConversionRequest from a WebUI Job."""
|
"""Build a ConversionRequest from a WebUI Job."""
|
||||||
# Determine source path
|
|
||||||
source_path = Path(job.stored_path) if job.stored_path else None
|
source_path = Path(job.stored_path) if job.stored_path else None
|
||||||
|
|
||||||
return ConversionRequest(
|
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")
|
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
|
# Main entry point
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -231,19 +174,11 @@ def run_conversion_job(job: Job) -> None:
|
|||||||
"""Run a conversion job using the shared application layer."""
|
"""Run a conversion job using the shared application layer."""
|
||||||
job.add_log("Preparing conversion pipeline")
|
job.add_log("Preparing conversion pipeline")
|
||||||
|
|
||||||
# Build request from job data
|
|
||||||
request = _build_request(job)
|
request = _build_request(job)
|
||||||
|
|
||||||
# Create adapters
|
|
||||||
events = WebUIEventsAdapter(job)
|
events = WebUIEventsAdapter(job)
|
||||||
pool = PipelinePool()
|
|
||||||
voice_cache = VoiceCache()
|
|
||||||
voice_resolver = WebUIVoiceResolver(
|
|
||||||
job, request.speakers, voice_cache, pool,
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = run_conversion(request, events, pool, voice_resolver)
|
result = run_conversion(request, events)
|
||||||
_apply_result(job, result)
|
_apply_result(job, result)
|
||||||
|
|
||||||
if job.status != JobStatus.CANCELLED:
|
if job.status != JobStatus.CANCELLED:
|
||||||
@@ -257,8 +192,6 @@ def run_conversion_job(job: Job) -> None:
|
|||||||
job.status = JobStatus.FAILED
|
job.status = JobStatus.FAILED
|
||||||
job.add_log(f"Job failed: {exc}", level="error")
|
job.add_log(f"Job failed: {exc}", level="error")
|
||||||
finally:
|
finally:
|
||||||
pool.dispose_all()
|
|
||||||
voice_cache.clear()
|
|
||||||
gc.collect()
|
gc.collect()
|
||||||
try:
|
try:
|
||||||
import torch
|
import torch
|
||||||
|
|||||||
@@ -106,6 +106,23 @@ class FakeVoiceResolver:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def _mock_pool_and_resolver():
|
||||||
|
"""Mock PipelinePool and _create_voice_resolver for all service tests."""
|
||||||
|
fake_pool = FakePipelineProvider()
|
||||||
|
fake_resolver = FakeVoiceResolver()
|
||||||
|
with patch(
|
||||||
|
"abogen.domain.pipeline_factory.PipelinePool",
|
||||||
|
return_value=fake_pool,
|
||||||
|
), patch(
|
||||||
|
"abogen.domain.voice_loader.VoiceCache",
|
||||||
|
), patch(
|
||||||
|
"abogen.application.conversion_service._create_voice_resolver",
|
||||||
|
return_value=fake_resolver,
|
||||||
|
):
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
# ─── Tests for conversion_service.py ───────────────────────────────
|
# ─── Tests for conversion_service.py ───────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
@@ -124,10 +141,8 @@ class TestConversionService:
|
|||||||
output_folder=Path(tmpdir),
|
output_folder=Path(tmpdir),
|
||||||
)
|
)
|
||||||
events = FakeEvents()
|
events = FakeEvents()
|
||||||
pipeline = FakePipelineProvider()
|
|
||||||
resolver = FakeVoiceResolver()
|
|
||||||
|
|
||||||
result = run_conversion(req, events, pipeline, resolver)
|
result = run_conversion(req, events)
|
||||||
|
|
||||||
assert result is not None
|
assert result is not None
|
||||||
assert result.audio_path is not None
|
assert result.audio_path is not None
|
||||||
@@ -145,10 +160,8 @@ class TestConversionService:
|
|||||||
output_folder=Path(tmpdir),
|
output_folder=Path(tmpdir),
|
||||||
)
|
)
|
||||||
events = FakeEvents()
|
events = FakeEvents()
|
||||||
pipeline = FakePipelineProvider()
|
|
||||||
resolver = FakeVoiceResolver()
|
|
||||||
|
|
||||||
result = run_conversion(req, events, pipeline, resolver)
|
result = run_conversion(req, events)
|
||||||
|
|
||||||
log_messages = [msg for msg, _ in events.logs]
|
log_messages = [msg for msg, _ in events.logs]
|
||||||
assert any("Preparing conversion pipeline" in msg for msg in log_messages)
|
assert any("Preparing conversion pipeline" in msg for msg in log_messages)
|
||||||
@@ -169,11 +182,9 @@ class TestConversionService:
|
|||||||
)
|
)
|
||||||
events = FakeEvents()
|
events = FakeEvents()
|
||||||
events.cancelled = True
|
events.cancelled = True
|
||||||
pipeline = FakePipelineProvider()
|
|
||||||
resolver = FakeVoiceResolver()
|
|
||||||
|
|
||||||
with pytest.raises(RuntimeError, match="Conversion cancelled"):
|
with pytest.raises(RuntimeError, match="Conversion cancelled"):
|
||||||
run_conversion(req, events, pipeline, resolver)
|
run_conversion(req, events)
|
||||||
|
|
||||||
def test_service_handles_empty_text(self):
|
def test_service_handles_empty_text(self):
|
||||||
"""Service raises ValueError for empty text."""
|
"""Service raises ValueError for empty text."""
|
||||||
@@ -181,11 +192,9 @@ class TestConversionService:
|
|||||||
|
|
||||||
req = ConversionRequest(direct_text="", voice="M1")
|
req = ConversionRequest(direct_text="", voice="M1")
|
||||||
events = FakeEvents()
|
events = FakeEvents()
|
||||||
pipeline = FakePipelineProvider()
|
|
||||||
resolver = FakeVoiceResolver()
|
|
||||||
|
|
||||||
with pytest.raises(ValueError, match="No text content"):
|
with pytest.raises(ValueError, match="No text content"):
|
||||||
run_conversion(req, events, pipeline, resolver)
|
run_conversion(req, events)
|
||||||
|
|
||||||
def test_service_multi_chapter(self):
|
def test_service_multi_chapter(self):
|
||||||
"""Service handles multi-chapter conversion."""
|
"""Service handles multi-chapter conversion."""
|
||||||
@@ -199,10 +208,8 @@ class TestConversionService:
|
|||||||
output_folder=Path(tmpdir),
|
output_folder=Path(tmpdir),
|
||||||
)
|
)
|
||||||
events = FakeEvents()
|
events = FakeEvents()
|
||||||
pipeline = FakePipelineProvider()
|
|
||||||
resolver = FakeVoiceResolver()
|
|
||||||
|
|
||||||
result = run_conversion(req, events, pipeline, resolver)
|
result = run_conversion(req, events)
|
||||||
|
|
||||||
assert result.total_chapters == 2
|
assert result.total_chapters == 2
|
||||||
|
|
||||||
@@ -221,10 +228,8 @@ class TestConversionService:
|
|||||||
metadata_tags={"title": "Test Book", "author": "Author"},
|
metadata_tags={"title": "Test Book", "author": "Author"},
|
||||||
)
|
)
|
||||||
events = FakeEvents()
|
events = FakeEvents()
|
||||||
pipeline = FakePipelineProvider()
|
|
||||||
resolver = FakeVoiceResolver()
|
|
||||||
|
|
||||||
result = run_conversion(req, events, pipeline, resolver)
|
result = run_conversion(req, events)
|
||||||
|
|
||||||
assert result is not None
|
assert result is not None
|
||||||
|
|
||||||
@@ -234,13 +239,11 @@ class TestConversionService:
|
|||||||
|
|
||||||
req = ConversionRequest(direct_text="Hello", voice="M1")
|
req = ConversionRequest(direct_text="Hello", voice="M1")
|
||||||
events = FakeEvents()
|
events = FakeEvents()
|
||||||
pipeline = FakePipelineProvider()
|
|
||||||
resolver = FakeVoiceResolver()
|
|
||||||
|
|
||||||
# Mock build_conversion_plan to raise an error
|
# Mock build_conversion_plan to raise an error
|
||||||
with patch("abogen.application.conversion_service.build_conversion_plan", side_effect=RuntimeError("Test error")):
|
with patch("abogen.application.conversion_service.build_conversion_plan", side_effect=RuntimeError("Test error")):
|
||||||
with pytest.raises(RuntimeError, match="Test error"):
|
with pytest.raises(RuntimeError, match="Test error"):
|
||||||
run_conversion(req, events, pipeline, resolver)
|
run_conversion(req, events)
|
||||||
|
|
||||||
log_messages = [msg for msg, _ in events.logs]
|
log_messages = [msg for msg, _ in events.logs]
|
||||||
assert any("Conversion failed" in msg for msg in log_messages)
|
assert any("Conversion failed" in msg for msg in log_messages)
|
||||||
@@ -258,10 +261,8 @@ class TestConversionService:
|
|||||||
normalization_overrides={"normalization_numbers": False},
|
normalization_overrides={"normalization_numbers": False},
|
||||||
)
|
)
|
||||||
events = FakeEvents()
|
events = FakeEvents()
|
||||||
pipeline = FakePipelineProvider()
|
|
||||||
resolver = FakeVoiceResolver()
|
|
||||||
|
|
||||||
result = run_conversion(req, events, pipeline, resolver)
|
result = run_conversion(req, events)
|
||||||
assert result is not None
|
assert result is not None
|
||||||
|
|
||||||
def test_tts_context_rejects_unconfigured_llm_mode(self):
|
def test_tts_context_rejects_unconfigured_llm_mode(self):
|
||||||
@@ -277,11 +278,9 @@ class TestConversionService:
|
|||||||
normalization_overrides={"normalization_apostrophe_mode": "llm"},
|
normalization_overrides={"normalization_apostrophe_mode": "llm"},
|
||||||
)
|
)
|
||||||
events = FakeEvents()
|
events = FakeEvents()
|
||||||
pipeline = FakePipelineProvider()
|
|
||||||
resolver = FakeVoiceResolver()
|
|
||||||
|
|
||||||
with pytest.raises(RuntimeError, match="LLM.*apostrophe"):
|
with pytest.raises(RuntimeError, match="LLM.*apostrophe"):
|
||||||
run_conversion(req, events, pipeline, resolver)
|
run_conversion(req, events)
|
||||||
|
|
||||||
def test_usage_counter_populated_in_result(self):
|
def test_usage_counter_populated_in_result(self):
|
||||||
"""usage_counter is created and accessible in result."""
|
"""usage_counter is created and accessible in result."""
|
||||||
@@ -295,10 +294,8 @@ class TestConversionService:
|
|||||||
output_folder=Path(tmpdir),
|
output_folder=Path(tmpdir),
|
||||||
)
|
)
|
||||||
events = FakeEvents()
|
events = FakeEvents()
|
||||||
pipeline = FakePipelineProvider()
|
|
||||||
resolver = FakeVoiceResolver()
|
|
||||||
|
|
||||||
result = run_conversion(req, events, pipeline, resolver)
|
result = run_conversion(req, events)
|
||||||
assert hasattr(result, "usage_counter")
|
assert hasattr(result, "usage_counter")
|
||||||
assert isinstance(result.usage_counter, dict)
|
assert isinstance(result.usage_counter, dict)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user