mirror of
https://github.com/denizsafak/abogen.git
synced 2026-09-20 11:40:57 +02:00
Domain config types (domain/config_types.py): - PronunciationConfig: pronunciation/heteronym/normalization overrides - SubtitleConfig: mode, format, max_words - CoverConfig: path, mime Domain functions now accept config objects: - build_tts_context(subtitle=, pronunciation=) instead of 9 individual params - make_subtitle_writer(subtitle=) instead of 3 params - process_and_write_subtitles(subtitle=) instead of 2 params - embed_m4b_metadata(cover=) instead of 2 params - build_epub3_package(cover=) instead of 2 params ConversionRequest: 18 flat fields + 8 config objects Application/config.py re-exports domain types All tests updated to new API
203 lines
6.7 KiB
Python
203 lines
6.7 KiB
Python
"""WebUI conversion runner — thin adapter between Job and shared layer.
|
|
|
|
This module is the WebUI's adapter for the conversion system. It:
|
|
1. Builds a ConversionRequest from a Job
|
|
2. Creates an Events adapter
|
|
3. Calls run_conversion() from the shared application layer
|
|
4. Maps the ConversionResult back to Job state
|
|
|
|
All conversion logic (chapter loop, voice resolution, TTS, metadata,
|
|
subtitle writing, m4b/epub3 finalization) lives in the application layer.
|
|
|
|
Language: Job.language is Language enum. Frontend must send ISO codes.
|
|
Engine converts Language → its own format internally.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from abogen.application.conversion_config import (
|
|
ChapterChunkConfig,
|
|
CoverConfig,
|
|
Epub3ExportConfig,
|
|
PronunciationConfig,
|
|
SaveConfig,
|
|
SubtitleConfig,
|
|
)
|
|
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 (
|
|
OutputFormat,
|
|
SaveMode,
|
|
SubtitleFormat,
|
|
SubtitleMode,
|
|
)
|
|
|
|
from .service import Job, JobStatus
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Build ConversionRequest from Job
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _build_request(job: Job) -> ConversionRequest:
|
|
"""Build a ConversionRequest from a WebUI Job."""
|
|
source_path = Path(job.stored_path) if job.stored_path else None
|
|
|
|
return ConversionRequest(
|
|
# Source
|
|
source_path=source_path,
|
|
original_filename=job.original_filename,
|
|
# TTS Settings
|
|
language=job.language,
|
|
tts_provider=job.tts_provider,
|
|
voice=job.voice,
|
|
speed=job.speed,
|
|
use_gpu=job.use_gpu,
|
|
supertonic_total_steps=job.supertonic_total_steps,
|
|
# Output Format
|
|
output_format=_resolve_output_format(job.output_format),
|
|
# 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,
|
|
# Metadata
|
|
metadata_tags=job.metadata_tags or {},
|
|
# Grouped configs
|
|
subtitle=SubtitleConfig(
|
|
mode=_resolve_subtitle_mode(job.subtitle_mode),
|
|
format=_resolve_subtitle_format(job.subtitle_format),
|
|
max_words=job.max_subtitle_words,
|
|
),
|
|
save=SaveConfig(
|
|
mode=_resolve_save_mode(job.save_mode),
|
|
output_folder=job.output_folder,
|
|
save_chapters_separately=job.save_chapters_separately,
|
|
merge_chapters_at_end=job.merge_chapters_at_end,
|
|
separate_chapters_format=_resolve_output_format(job.separate_chapters_format),
|
|
save_as_project=job.save_as_project,
|
|
),
|
|
cover=CoverConfig(
|
|
path=job.cover_image_path,
|
|
mime=job.cover_image_mime,
|
|
),
|
|
pronunciation=PronunciationConfig(
|
|
pronunciation_overrides=job.pronunciation_overrides or [],
|
|
manual_overrides=job.manual_overrides or [],
|
|
heteronym_overrides=job.heteronym_overrides or [],
|
|
normalization_overrides=job.normalization_overrides or None,
|
|
),
|
|
# Feature configs
|
|
epub3_export=Epub3ExportConfig(book_id=job.id) if job.generate_epub3 else None,
|
|
chapter_chunk=ChapterChunkConfig(
|
|
chapter_overrides=job.chapters or [],
|
|
chunks=job.chunks or [],
|
|
chunk_level=job.chunk_level,
|
|
speaker_mode=job.speaker_mode,
|
|
speakers=job.speakers or {},
|
|
),
|
|
)
|
|
|
|
|
|
def _resolve_output_format(fmt: str) -> OutputFormat:
|
|
try:
|
|
return OutputFormat.from_str(fmt)
|
|
except ValueError:
|
|
return OutputFormat.WAV
|
|
|
|
|
|
def _resolve_subtitle_mode(mode: str) -> SubtitleMode:
|
|
try:
|
|
return SubtitleMode.from_str(mode)
|
|
except ValueError:
|
|
return SubtitleMode.DISABLED
|
|
|
|
|
|
def _resolve_subtitle_format(fmt: str) -> SubtitleFormat:
|
|
try:
|
|
return SubtitleFormat.from_str(fmt)
|
|
except ValueError:
|
|
return SubtitleFormat.SRT
|
|
|
|
|
|
def _resolve_save_mode(mode: str) -> SaveMode:
|
|
normalized = mode.strip().lower()
|
|
for m in SaveMode:
|
|
if m.value == normalized:
|
|
return m
|
|
return SaveMode.SAVE_NEXT_TO_INPUT
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Apply ConversionResult back to Job
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _apply_result(job: Job, result: Any) -> None:
|
|
"""Map ConversionResult fields back to Job result and state."""
|
|
job.result.audio_path = result.audio_path
|
|
job.result.subtitle_paths = list(result.subtitle_paths)
|
|
job.result.artifacts = dict(result.artifacts)
|
|
job.result.epub_path = result.epub_path
|
|
job.progress = 1.0
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Events adapter
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class WebUIEventsAdapter:
|
|
"""Adapts Job to ConversionEvents protocol."""
|
|
|
|
def __init__(self, job: Job):
|
|
self._job = job
|
|
|
|
def log(self, message: str, level: str = "info") -> None:
|
|
self._job.add_log(message, level=level)
|
|
|
|
def progress(self, pct: int, etr: str) -> None:
|
|
self._job.progress = min(pct / 100.0, 0.999)
|
|
self._job.etr_str = etr
|
|
|
|
def check_cancelled(self) -> None:
|
|
if self._job.cancel_requested:
|
|
raise ConversionCancelled("Job cancelled")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Main entry point
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def run_conversion_job(job: Job) -> None:
|
|
"""Run a conversion job using the shared application layer."""
|
|
job.add_log("Preparing conversion pipeline")
|
|
|
|
request = _build_request(job)
|
|
events = WebUIEventsAdapter(job)
|
|
|
|
try:
|
|
result = run_conversion(request, events)
|
|
_apply_result(job, result)
|
|
|
|
if job.status != JobStatus.CANCELLED:
|
|
job.progress = 1.0
|
|
|
|
except ConversionCancelled:
|
|
job.status = JobStatus.CANCELLED
|
|
job.add_log("Job cancelled", level="warning")
|
|
except Exception as exc:
|
|
job.error = str(exc)
|
|
job.status = JobStatus.FAILED
|
|
job.add_log(f"Job failed: {exc}", level="error")
|