mirror of
https://github.com/denizsafak/abogen.git
synced 2026-09-20 11:40:57 +02:00
refactor: config objects for feature toggles in ConversionRequest
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
"""Feature config objects for ConversionRequest.
|
||||
|
||||
Each config object groups parameters for a specific feature.
|
||||
If the object is None, the feature is disabled.
|
||||
|
||||
This keeps ConversionRequest clean: no boolean flags for feature toggles,
|
||||
no scattered parameters across unrelated fields.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class WordSubstitutionConfig:
|
||||
"""Word substitution settings.
|
||||
|
||||
When present on ConversionRequest, word substitution is applied
|
||||
to the source text before chapter parsing.
|
||||
"""
|
||||
substitutions_list: str = ""
|
||||
case_sensitive: bool = False
|
||||
replace_caps: bool = False
|
||||
replace_numerals: bool = False
|
||||
fix_punctuation: bool = False
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SubtitleInputConfig:
|
||||
"""Subtitle file input settings.
|
||||
|
||||
When present on ConversionRequest, the source is treated as a
|
||||
subtitle file (.srt/.ass/.vtt) or timestamp text, and the
|
||||
subtitle-to-audio pipeline is used instead of normal text conversion.
|
||||
"""
|
||||
is_timestamp_text: bool = False
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Epub3ExportConfig:
|
||||
"""EPUB3 export settings.
|
||||
|
||||
When present on ConversionRequest, an EPUB3 package with
|
||||
synchronized audio narration is generated after conversion.
|
||||
"""
|
||||
book_id: str = ""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PronunciationConfig:
|
||||
"""Pronunciation and normalization override settings.
|
||||
|
||||
Groups all pronunciation/heteronym/normalization overrides
|
||||
that are compiled into a TTSContext before conversion.
|
||||
"""
|
||||
pronunciation_overrides: List[Dict[str, Any]] = field(default_factory=list)
|
||||
manual_overrides: List[Dict[str, Any]] = field(default_factory=list)
|
||||
heteronym_overrides: List[Dict[str, Any]] = field(default_factory=list)
|
||||
normalization_overrides: Optional[Dict[str, Any]] = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ChapterChunkConfig:
|
||||
"""Chapter and chunk configuration.
|
||||
|
||||
Groups chapter overrides, chunk data, and speaker settings
|
||||
used by the planner to build segments.
|
||||
"""
|
||||
chapter_overrides: List[Dict[str, Any]] = field(default_factory=list)
|
||||
chunks: List[Dict[str, Any]] = field(default_factory=list)
|
||||
chunk_level: str = "paragraph"
|
||||
speaker_mode: str = "single"
|
||||
speakers: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
_VALID_CHUNK_LEVELS = ("paragraph", "sentence")
|
||||
_VALID_SPEAKER_MODES = ("single", "multi")
|
||||
if self.chunk_level not in _VALID_CHUNK_LEVELS:
|
||||
raise ValueError(
|
||||
f"chunk_level must be one of {_VALID_CHUNK_LEVELS}, got {self.chunk_level!r}"
|
||||
)
|
||||
if self.speaker_mode not in _VALID_SPEAKER_MODES:
|
||||
raise ValueError(
|
||||
f"speaker_mode must be one of {_VALID_SPEAKER_MODES}, got {self.speaker_mode!r}"
|
||||
)
|
||||
@@ -302,7 +302,7 @@ def execute_conversion(
|
||||
"end": stats.current_time,
|
||||
"speaker_id": segment.speaker_id,
|
||||
"voice": segment.voice_spec,
|
||||
"level": segment.level or request.chunk_level,
|
||||
"level": segment.level or (request.chapter_chunk.chunk_level if request.chapter_chunk else "paragraph"),
|
||||
"characters": len(segment.text),
|
||||
})
|
||||
|
||||
|
||||
@@ -144,8 +144,9 @@ def _apply_selection(
|
||||
]
|
||||
|
||||
# If user specified chapters, apply overrides
|
||||
if request.chapter_overrides:
|
||||
selected, _, diagnostics = apply_chapter_overrides(extracted, request.chapter_overrides)
|
||||
chapter_chunk = request.chapter_chunk
|
||||
if chapter_chunk and chapter_chunk.chapter_overrides:
|
||||
selected, _, diagnostics = apply_chapter_overrides(extracted, chapter_chunk.chapter_overrides)
|
||||
if selected:
|
||||
# Map back to (title, text, voice) tuples
|
||||
result = []
|
||||
@@ -216,9 +217,10 @@ def _build_segments(
|
||||
segments = []
|
||||
|
||||
# Check for chunks (WebUI style)
|
||||
if request.chunks:
|
||||
chapter_chunk = request.chapter_chunk
|
||||
if chapter_chunk and chapter_chunk.chunks:
|
||||
# Group chunks by chapter (simplified — assume chunks are for current chapter)
|
||||
for chunk_idx, chunk in enumerate(request.chunks):
|
||||
for chunk_idx, chunk in enumerate(chapter_chunk.chunks):
|
||||
chunk_text = chunk.get("normalized_text") or chunk.get("text", "")
|
||||
if not chunk_text or not chunk_text.strip():
|
||||
continue
|
||||
@@ -234,7 +236,7 @@ def _build_segments(
|
||||
speaker_id=speaker_id,
|
||||
chunk_id=chunk.get("id"),
|
||||
chunk_index=chunk.get("chunk_index", chunk_idx),
|
||||
level=chunk.get("level", request.chunk_level),
|
||||
level=chunk.get("level", chapter_chunk.chunk_level),
|
||||
source="chunk",
|
||||
)
|
||||
)
|
||||
@@ -284,8 +286,9 @@ def _resolve_chunk_voice(
|
||||
"""Resolve voice for a chunk."""
|
||||
# Check for speaker-based voice
|
||||
speaker_id = chunk.get("speaker_id", "narrator")
|
||||
if speaker_id and speaker_id != "narrator" and request.speakers:
|
||||
speaker_config = request.speakers.get(speaker_id, {})
|
||||
speakers = request.chapter_chunk.speakers if request.chapter_chunk else {}
|
||||
if speaker_id and speaker_id != "narrator" and speakers:
|
||||
speaker_config = speakers.get(speaker_id, {})
|
||||
if isinstance(speaker_config, dict):
|
||||
voice = speaker_config.get("voice")
|
||||
if voice:
|
||||
|
||||
@@ -14,6 +14,13 @@ from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from abogen.application.conversion_config import (
|
||||
ChapterChunkConfig,
|
||||
Epub3ExportConfig,
|
||||
PronunciationConfig,
|
||||
SubtitleInputConfig,
|
||||
WordSubstitutionConfig,
|
||||
)
|
||||
from abogen.domain.enums import Language, OutputFormat, SaveMode, SubtitleFormat, SubtitleMode
|
||||
|
||||
|
||||
@@ -30,12 +37,6 @@ _NUMERIC_CONSTRAINTS: dict[str, tuple[float, float | None]] = {
|
||||
"chapter_intro_delay": (0.0, None),
|
||||
}
|
||||
|
||||
# Enum-like fields that must be in allowed set
|
||||
_ENUM_CONSTRAINTS: dict[str, tuple[str, ...]] = {
|
||||
"chunk_level": ("paragraph", "sentence"),
|
||||
"speaker_mode": ("single", "multi"),
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConversionRequest:
|
||||
@@ -44,10 +45,12 @@ class ConversionRequest:
|
||||
Only contains fields that describe the conversion task itself.
|
||||
UI-only fields (display, logging, user prompts) stay in adapters.
|
||||
|
||||
Feature toggles use config objects: if the object is present,
|
||||
the feature is enabled. No boolean flags needed.
|
||||
|
||||
Validation runs on creation via __post_init__:
|
||||
- None values → replaced with field default (from declaration)
|
||||
- Numeric fields → clamped to valid range
|
||||
- String enums → validated against allowed set
|
||||
"""
|
||||
|
||||
# --- Source ---
|
||||
@@ -89,26 +92,19 @@ class ConversionRequest:
|
||||
auto_prefix_chapter_titles: bool = True
|
||||
normalize_chapter_opening_caps: bool = False
|
||||
|
||||
# --- Pronunciation / Normalization ---
|
||||
pronunciation_overrides: List[Dict[str, Any]] = field(default_factory=list)
|
||||
manual_overrides: List[Dict[str, Any]] = field(default_factory=list)
|
||||
heteronym_overrides: List[Dict[str, Any]] = field(default_factory=list)
|
||||
normalization_overrides: Optional[Dict[str, Any]] = None
|
||||
|
||||
# --- Chapter/Chunk Configuration ---
|
||||
chapter_overrides: List[Dict[str, Any]] = field(default_factory=list)
|
||||
chunks: List[Dict[str, Any]] = field(default_factory=list)
|
||||
chunk_level: str = "paragraph"
|
||||
speaker_mode: str = "single"
|
||||
speakers: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
# --- Metadata ---
|
||||
metadata_tags: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
# --- Artifacts ---
|
||||
cover_image_path: Optional[Path] = None
|
||||
cover_image_mime: Optional[str] = None
|
||||
generate_epub3: bool = False
|
||||
|
||||
# --- Feature configs (None = disabled) ---
|
||||
word_substitution: Optional[WordSubstitutionConfig] = None
|
||||
subtitle_input: Optional[SubtitleInputConfig] = None
|
||||
epub3_export: Optional[Epub3ExportConfig] = None
|
||||
pronunciation: Optional[PronunciationConfig] = None
|
||||
chapter_chunk: Optional[ChapterChunkConfig] = None
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
"""Resolve None → default, then validate and clamp."""
|
||||
@@ -116,7 +112,6 @@ class ConversionRequest:
|
||||
if not self.tts_provider:
|
||||
self.tts_provider = "kokoro"
|
||||
_clamp_numerics(self)
|
||||
_validate_enums(self)
|
||||
|
||||
|
||||
def _apply_none_defaults(obj: ConversionRequest) -> None:
|
||||
@@ -144,13 +139,3 @@ def _clamp_numerics(obj: ConversionRequest) -> None:
|
||||
if max_v is not None:
|
||||
clamped = min(max_v, clamped)
|
||||
setattr(obj, attr, clamped)
|
||||
|
||||
|
||||
def _validate_enums(obj: ConversionRequest) -> None:
|
||||
"""Validate string enum fields against allowed values."""
|
||||
for attr, allowed in _ENUM_CONSTRAINTS.items():
|
||||
val = getattr(obj, attr)
|
||||
if val not in allowed:
|
||||
raise ConversionRequestError(
|
||||
f"{attr} must be one of {allowed}, got {val!r}"
|
||||
)
|
||||
|
||||
@@ -141,17 +141,20 @@ def _prepare_tts_context(
|
||||
|
||||
# Merge pronunciation overrides (manual + pronunciation)
|
||||
# Create a mock job-like object for merge_pronunciation_overrides
|
||||
class _MockJob:
|
||||
def __init__(self, req):
|
||||
self.pronunciation_overrides = req.pronunciation_overrides
|
||||
self.manual_overrides = req.manual_overrides
|
||||
self.heteronym_overrides = req.heteronym_overrides
|
||||
pronunciation = request.pronunciation
|
||||
|
||||
merged_overrides = merge_pronunciation_overrides(_MockJob(request))
|
||||
class _MockJob:
|
||||
def __init__(self, pron):
|
||||
self.pronunciation_overrides = pron.pronunciation_overrides if pron else []
|
||||
self.manual_overrides = pron.manual_overrides if pron else []
|
||||
self.heteronym_overrides = pron.heteronym_overrides if pron else []
|
||||
|
||||
merged_overrides = merge_pronunciation_overrides(_MockJob(pronunciation))
|
||||
|
||||
# Compile rules
|
||||
pronunciation_rules = compile_pronunciation_rules(merged_overrides)
|
||||
heteronym_rules = compile_heteronym_sentence_rules(request.heteronym_overrides)
|
||||
heteronym_overrides = pronunciation.heteronym_overrides if pronunciation else []
|
||||
heteronym_rules = compile_heteronym_sentence_rules(heteronym_overrides)
|
||||
|
||||
if heteronym_rules:
|
||||
events.log(
|
||||
@@ -168,5 +171,5 @@ def _prepare_tts_context(
|
||||
split_pattern=split_pattern,
|
||||
pronunciation_rules=pronunciation_rules,
|
||||
heteronym_rules=heteronym_rules,
|
||||
normalization_overrides=request.normalization_overrides,
|
||||
normalization_overrides=pronunciation.normalization_overrides if pronunciation else None,
|
||||
)
|
||||
|
||||
@@ -17,6 +17,12 @@ import os
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, Dict, List, Optional
|
||||
|
||||
from abogen.application.conversion_config import (
|
||||
ChapterChunkConfig,
|
||||
Epub3ExportConfig,
|
||||
PronunciationConfig,
|
||||
WordSubstitutionConfig,
|
||||
)
|
||||
from abogen.application.conversion_request import ConversionRequest
|
||||
from abogen.application.conversion_ports import ConversionCancelled, ResolvedVoice
|
||||
|
||||
@@ -54,6 +60,25 @@ def build_conversion_request_from_thread(thread: Any) -> ConversionRequest:
|
||||
if thread.output_folder:
|
||||
output_folder = Path(thread.output_folder)
|
||||
|
||||
# Build pronunciation config
|
||||
pronunciation = None
|
||||
pron_overrides = getattr(thread, "pronunciation_overrides", []) or []
|
||||
manual_overrides = getattr(thread, "manual_overrides", []) or []
|
||||
heteronym_overrides = getattr(thread, "heteronym_overrides", []) or []
|
||||
norm_overrides = getattr(thread, "normalization_overrides", None)
|
||||
if pron_overrides or manual_overrides or heteronym_overrides or norm_overrides:
|
||||
pronunciation = PronunciationConfig(
|
||||
pronunciation_overrides=pron_overrides,
|
||||
manual_overrides=manual_overrides,
|
||||
heteronym_overrides=heteronym_overrides,
|
||||
normalization_overrides=norm_overrides,
|
||||
)
|
||||
|
||||
# Build epub3 config
|
||||
epub3_export = None
|
||||
if getattr(thread, "generate_epub3", False):
|
||||
epub3_export = Epub3ExportConfig()
|
||||
|
||||
return ConversionRequest(
|
||||
# Source
|
||||
source_path=source_path,
|
||||
@@ -88,23 +113,15 @@ def build_conversion_request_from_thread(thread: Any) -> ConversionRequest:
|
||||
read_closing_outro=getattr(thread, "read_closing_outro", True),
|
||||
auto_prefix_chapter_titles=getattr(thread, "auto_prefix_chapter_titles", True),
|
||||
normalize_chapter_opening_caps=getattr(thread, "normalize_chapter_opening_caps", False),
|
||||
# Pronunciation / Normalization
|
||||
pronunciation_overrides=getattr(thread, "pronunciation_overrides", []) or [],
|
||||
manual_overrides=getattr(thread, "manual_overrides", []) or [],
|
||||
heteronym_overrides=getattr(thread, "heteronym_overrides", []) or [],
|
||||
normalization_overrides=getattr(thread, "normalization_overrides", None),
|
||||
# Chapter/Chunk Configuration
|
||||
chapter_overrides=[], # PyQt doesn't use chapter overrides from GUI
|
||||
chunks=[], # PyQt doesn't use chunks from GUI
|
||||
chunk_level="paragraph",
|
||||
speaker_mode="single",
|
||||
speakers={},
|
||||
# Metadata
|
||||
metadata_tags=getattr(thread, "metadata_tags", {}) or {},
|
||||
# Artifacts
|
||||
cover_image_path=getattr(thread, "cover_image_path", None),
|
||||
cover_image_mime=getattr(thread, "cover_image_mime", None),
|
||||
generate_epub3=getattr(thread, "generate_epub3", False),
|
||||
# Feature configs
|
||||
pronunciation=pronunciation,
|
||||
epub3_export=epub3_export,
|
||||
chapter_chunk=ChapterChunkConfig(), # PyQt doesn't use chapter overrides from GUI
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -17,6 +17,12 @@ from __future__ import annotations
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, Dict, List, Optional
|
||||
|
||||
from abogen.application.conversion_config import (
|
||||
ChapterChunkConfig,
|
||||
Epub3ExportConfig,
|
||||
PronunciationConfig,
|
||||
WordSubstitutionConfig,
|
||||
)
|
||||
from abogen.application.conversion_request import ConversionRequest
|
||||
from abogen.application.conversion_ports import ConversionCancelled, ResolvedVoice
|
||||
|
||||
@@ -33,6 +39,47 @@ def build_conversion_request_from_job(job: Any) -> ConversionRequest:
|
||||
Returns:
|
||||
ConversionRequest with all Job data mapped
|
||||
"""
|
||||
# Build word substitution config
|
||||
word_substitution = None
|
||||
if getattr(job, "word_substitutions_enabled", False):
|
||||
word_substitution = WordSubstitutionConfig(
|
||||
substitutions_list=getattr(job, "word_substitutions_list", ""),
|
||||
case_sensitive=getattr(job, "case_sensitive_substitutions", False),
|
||||
replace_caps=getattr(job, "replace_all_caps", False),
|
||||
replace_numerals=getattr(job, "replace_numerals", False),
|
||||
fix_punctuation=getattr(job, "fix_nonstandard_punctuation", False),
|
||||
)
|
||||
|
||||
# Build pronunciation config
|
||||
pronunciation = None
|
||||
pron_overrides = job.pronunciation_overrides or []
|
||||
manual_overrides = job.manual_overrides or []
|
||||
heteronym_overrides = job.heteronym_overrides or []
|
||||
norm_overrides = job.normalization_overrides or None
|
||||
if pron_overrides or manual_overrides or heteronym_overrides or norm_overrides:
|
||||
pronunciation = PronunciationConfig(
|
||||
pronunciation_overrides=pron_overrides,
|
||||
manual_overrides=manual_overrides,
|
||||
heteronym_overrides=heteronym_overrides,
|
||||
normalization_overrides=norm_overrides,
|
||||
)
|
||||
|
||||
# Build chapter/chunk config
|
||||
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 {},
|
||||
)
|
||||
|
||||
# Build epub3 config
|
||||
epub3_export = None
|
||||
if getattr(job, "generate_epub3", False):
|
||||
epub3_export = Epub3ExportConfig(
|
||||
book_id=getattr(job, "id", ""),
|
||||
)
|
||||
|
||||
return ConversionRequest(
|
||||
# Source
|
||||
source_path=Path(job.stored_path) if job.stored_path else None,
|
||||
@@ -66,23 +113,16 @@ def build_conversion_request_from_job(job: Any) -> ConversionRequest:
|
||||
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,
|
||||
# Feature configs
|
||||
word_substitution=word_substitution,
|
||||
pronunciation=pronunciation,
|
||||
chapter_chunk=chapter_chunk,
|
||||
epub3_export=epub3_export,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ from unittest.mock import MagicMock, patch
|
||||
import pytest
|
||||
|
||||
from abogen.application.conversion_request import ConversionRequest
|
||||
from abogen.application.conversion_config import ChapterChunkConfig, PronunciationConfig
|
||||
from abogen.application.conversion_ports import ResolvedVoice
|
||||
|
||||
|
||||
@@ -112,25 +113,22 @@ class TestWebUIAdapter:
|
||||
assert req.output_folder is None
|
||||
assert req.cover_image_path is None
|
||||
|
||||
def test_chapter_overrides_mapped(self):
|
||||
def test_chapter_chunk_mapped(self):
|
||||
from abogen.webui.conversion_adapter import build_conversion_request_from_job
|
||||
|
||||
chapters = [{"title": "Ch1", "voice": "F1"}]
|
||||
job = self._make_job(chapters=chapters)
|
||||
req = build_conversion_request_from_job(job)
|
||||
|
||||
assert req.chapter_overrides == chapters
|
||||
|
||||
def test_chunks_mapped(self):
|
||||
from abogen.webui.conversion_adapter import build_conversion_request_from_job
|
||||
|
||||
chunks = [{"text": "Hello", "speaker": "A"}]
|
||||
job = self._make_job(chunks=chunks)
|
||||
job = self._make_job(chapters=chapters, chunks=chunks)
|
||||
req = build_conversion_request_from_job(job)
|
||||
|
||||
assert req.chunks == chunks
|
||||
assert isinstance(req.chapter_chunk, ChapterChunkConfig)
|
||||
assert req.chapter_chunk.chapter_overrides == chapters
|
||||
assert req.chapter_chunk.chunks == chunks
|
||||
assert req.chapter_chunk.chunk_level == "paragraph"
|
||||
assert req.chapter_chunk.speaker_mode == "single"
|
||||
assert req.chapter_chunk.speakers == {}
|
||||
|
||||
def test_pronunciation_overrides_mapped(self):
|
||||
def test_pronunciation_mapped(self):
|
||||
from abogen.webui.conversion_adapter import build_conversion_request_from_job
|
||||
|
||||
job = self._make_job(
|
||||
@@ -140,9 +138,10 @@ class TestWebUIAdapter:
|
||||
)
|
||||
req = build_conversion_request_from_job(job)
|
||||
|
||||
assert req.pronunciation_overrides == ["word=pron"]
|
||||
assert req.manual_overrides == ["manual=override"]
|
||||
assert req.heteronym_overrides == ["read=reed"]
|
||||
assert isinstance(req.pronunciation, PronunciationConfig)
|
||||
assert req.pronunciation.pronunciation_overrides == ["word=pron"]
|
||||
assert req.pronunciation.manual_overrides == ["manual=override"]
|
||||
assert req.pronunciation.heteronym_overrides == ["read=reed"]
|
||||
|
||||
def test_none_defaults_handled(self):
|
||||
from abogen.webui.conversion_adapter import build_conversion_request_from_job
|
||||
@@ -399,17 +398,18 @@ class TestPyQtAdapter:
|
||||
assert req.supertonic_total_steps == 5
|
||||
assert req.max_subtitle_words == 50
|
||||
|
||||
def test_chapter_chunks_not_mapped(self):
|
||||
def test_chapter_chunk_default(self):
|
||||
from abogen.pyqt.conversion_adapter import build_conversion_request_from_thread
|
||||
|
||||
thread = self._make_thread()
|
||||
req = build_conversion_request_from_thread(thread)
|
||||
|
||||
assert req.chapter_overrides == []
|
||||
assert req.chunks == []
|
||||
assert req.chunk_level == "paragraph"
|
||||
assert req.speaker_mode == "single"
|
||||
assert req.speakers == {}
|
||||
assert isinstance(req.chapter_chunk, ChapterChunkConfig)
|
||||
assert req.chapter_chunk.chapter_overrides == []
|
||||
assert req.chapter_chunk.chunks == []
|
||||
assert req.chapter_chunk.chunk_level == "paragraph"
|
||||
assert req.chapter_chunk.speaker_mode == "single"
|
||||
assert req.chapter_chunk.speakers == {}
|
||||
|
||||
|
||||
class TestPyQtEvents:
|
||||
|
||||
@@ -25,6 +25,7 @@ from abogen.application.conversion_models import (
|
||||
OutputLayout,
|
||||
SegmentPlan,
|
||||
)
|
||||
from abogen.application.conversion_config import ChapterChunkConfig
|
||||
from abogen.application.conversion_planner import build_conversion_plan
|
||||
from abogen.application.conversion_request import ConversionRequest
|
||||
|
||||
@@ -74,10 +75,12 @@ class TestBuildConversionPlan:
|
||||
req = ConversionRequest(
|
||||
direct_text="Some text",
|
||||
voice="M1",
|
||||
chapter_chunk=ChapterChunkConfig(
|
||||
chunks=[
|
||||
{"text": "Chunk 1", "speaker_id": "narrator"},
|
||||
{"text": "Chunk 2", "speaker_id": "narrator"},
|
||||
],
|
||||
),
|
||||
)
|
||||
plan = build_conversion_plan(req)
|
||||
|
||||
@@ -92,11 +95,13 @@ class TestBuildConversionPlan:
|
||||
req = ConversionRequest(
|
||||
direct_text="Text",
|
||||
voice="M1",
|
||||
chapter_chunk=ChapterChunkConfig(
|
||||
chunks=[
|
||||
{"text": "Narrator speaks", "speaker_id": "narrator"},
|
||||
{"text": "Character speaks", "speaker_id": "alice", "voice": "F1"},
|
||||
],
|
||||
speakers={"alice": {"voice": "F1"}},
|
||||
),
|
||||
)
|
||||
plan = build_conversion_plan(req)
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ from pathlib import Path
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from abogen.application.conversion_request import ConversionRequest, ConversionRequestError
|
||||
from abogen.application.conversion_config import ChapterChunkConfig, WordSubstitutionConfig
|
||||
from abogen.domain.enums import Language, OutputFormat, SaveMode, SubtitleFormat, SubtitleMode
|
||||
from abogen.domain.normalization import TTSContext
|
||||
from abogen.domain.settings_core import settings_defaults
|
||||
@@ -248,12 +249,12 @@ class TestConversionRequestValidation:
|
||||
assert req.chapter_intro_delay == 0.0
|
||||
|
||||
def test_invalid_chunk_level_raises(self):
|
||||
with pytest.raises(ConversionRequestError, match="chunk_level"):
|
||||
ConversionRequest(chunk_level="invalid")
|
||||
with pytest.raises(ValueError, match="chunk_level"):
|
||||
ConversionRequest(chapter_chunk=ChapterChunkConfig(chunk_level="invalid"))
|
||||
|
||||
def test_invalid_speaker_mode_raises(self):
|
||||
with pytest.raises(ConversionRequestError, match="speaker_mode"):
|
||||
ConversionRequest(speaker_mode="invalid")
|
||||
with pytest.raises(ValueError, match="speaker_mode"):
|
||||
ConversionRequest(chapter_chunk=ChapterChunkConfig(speaker_mode="invalid"))
|
||||
|
||||
def test_invalid_max_subtitle_words_type_raises(self):
|
||||
with pytest.raises(ConversionRequestError, match="max_subtitle_words"):
|
||||
|
||||
Reference in New Issue
Block a user