Files
abogen/abogen/webui/conversion_adapter.py
T
Artem Akymenko d3682e7672 refactor: dynamic ConversionRequest validation, remove 'or default' from adapters
- __post_init__: _apply_none_defaults() iterates dataclasses.fields() dynamically
- _NUMERIC_CONSTRAINTS and _ENUM_CONSTRAINTS dicts replace per-field if chains
- Both adapters pass values as-is (no 'or default' fallbacks)
- 18 validation tests + updated adapter tests for Enum assertions
2026-07-22 11:33:44 +00:00

163 lines
5.6 KiB
Python

"""WebUI adapter: Job -> ConversionRequest.
Converts a WebUI Job into a ConversionRequest that the application layer can process.
This adapter is the bridge between the WebUI layer and the application/domain layer.
The adapter is responsible for:
- Mapping Job fields to ConversionRequest fields
- Handling UI-specific state (logs, progress, cancellation)
- Providing PipelineProvider and VoiceResolver implementations
All conversions happen through this adapter — the application layer
never accesses Job directly.
"""
from __future__ import annotations
from pathlib import Path
from typing import Any, Callable, Dict, List, Optional
from abogen.application.conversion_request import ConversionRequest
from abogen.application.conversion_ports import ConversionCancelled, ResolvedVoice
def build_conversion_request_from_job(job: Any) -> ConversionRequest:
"""Convert a WebUI Job into a ConversionRequest.
This is the primary function that maps Job fields to ConversionRequest.
All fields are copied — the request is independent of the Job.
Args:
job: WebUI Job instance
Returns:
ConversionRequest with all Job data mapped
"""
return ConversionRequest(
# Source
source_path=Path(job.stored_path) if job.stored_path else None,
original_filename=job.original_filename,
# TTS Settings
language=job.language,
tts_provider=job.tts_provider,
voice=job.voice,
voice_profile=job.voice_profile,
speed=job.speed,
use_gpu=job.use_gpu,
supertonic_total_steps=job.supertonic_total_steps,
# Output Format
output_format=job.output_format,
subtitle_mode=job.subtitle_mode,
subtitle_format=job.subtitle_format,
max_subtitle_words=job.max_subtitle_words,
# Save Options
save_mode=job.save_mode,
output_folder=Path(job.output_folder) if job.output_folder else None,
save_chapters_separately=job.save_chapters_separately,
merge_chapters_at_end=job.merge_chapters_at_end,
separate_chapters_format=job.separate_chapters_format,
save_as_project=job.save_as_project,
# Timing
silence_between_chapters=job.silence_between_chapters,
chapter_intro_delay=job.chapter_intro_delay,
# Content Processing
replace_single_newlines=job.replace_single_newlines,
read_title_intro=job.read_title_intro,
read_closing_outro=job.read_closing_outro,
auto_prefix_chapter_titles=job.auto_prefix_chapter_titles,
normalize_chapter_opening_caps=job.normalize_chapter_opening_caps,
# Pronunciation / Normalization
pronunciation_overrides=job.pronunciation_overrides or [],
manual_overrides=job.manual_overrides or [],
heteronym_overrides=job.heteronym_overrides or [],
normalization_overrides=job.normalization_overrides or {},
# Chapter/Chunk Configuration
chapter_overrides=job.chapters or [],
chunks=job.chunks or [],
chunk_level=job.chunk_level,
speaker_mode=job.speaker_mode,
speakers=job.speakers or {},
# Metadata
metadata_tags=job.metadata_tags or {},
# Artifacts
cover_image_path=Path(job.cover_image_path) if job.cover_image_path else None,
cover_image_mime=job.cover_image_mime,
generate_epub3=job.generate_epub3,
)
class WebJobEvents:
"""WebUI implementation of ConversionEvents protocol.
Wraps a Job to provide logging, progress, and cancellation.
"""
def __init__(self, job: Any):
self._job = job
def log(self, message: str, level: str = "info") -> None:
"""Log a message to the Job."""
self._job.add_log(message, level=level)
def progress(self, pct: int, etr: str) -> None:
"""Update progress on the Job."""
self._job.progress = pct / 100.0
self._job.etr_str = etr
def check_cancelled(self) -> None:
"""Check if the Job was cancelled.
Raises:
ConversionCancelled: If cancellation was requested
"""
if self._job.cancel_requested:
raise ConversionCancelled("Job cancelled by user")
class WebPipelineProvider:
"""WebUI implementation of PipelineProvider protocol.
Wraps PipelinePool to provide TTS backends.
"""
def __init__(self, pipeline_pool: Any):
self._pool = pipeline_pool
def get(self, provider: str, language: str, use_gpu: bool) -> Any:
"""Get a TTS backend instance."""
return self._pool.get(provider, language, use_gpu)
def dispose_all(self) -> None:
"""Dispose all backend resources."""
self._pool.dispose_all()
class WebVoiceResolver:
"""WebUI implementation of VoiceResolver protocol.
Wraps the voice resolution logic from conversion_runner.py.
"""
def __init__(
self,
resolve_fn: Callable[[str], tuple[str, str, Any, Optional[float], Optional[int]]],
):
"""Initialize with a voice resolution function.
Args:
resolve_fn: Function that takes a voice_spec and returns
(provider, resolved_spec, voice_choice, speed, steps)
"""
self._resolve_fn = resolve_fn
def resolve(self, voice_spec: str) -> ResolvedVoice:
"""Resolve a voice spec into a loaded voice."""
provider, resolved_spec, voice, speed, steps = self._resolve_fn(voice_spec)
return ResolvedVoice(
provider=provider,
resolved_spec=resolved_spec,
voice=voice,
speed=speed or 1.0,
supertonic_steps=steps or 5,
)