mirror of
https://github.com/denizsafak/abogen.git
synced 2026-09-20 11:40:57 +02:00
refactor: voice resolution on ConversionRequest, PipelinePool without job param
- Added speakers field to ConversionRequest - Rewrote collect_required_voice_ids(), initialize_voice_cache(), job_voice_fallback(), chapter_voice_spec(), chunk_voice_spec() to accept ConversionRequest instead of job - PipelinePool.get() now takes request= instead of job= - Updated all tests to use ConversionRequest interface - 1493 tests passing
This commit is contained in:
@@ -1,6 +1,11 @@
|
||||
"""Tests for voice resolution helpers.
|
||||
|
||||
Tests import from domain.voice_resolution (new location).
|
||||
All test objects match the ConversionRequest interface:
|
||||
- voice: str
|
||||
- speakers: dict
|
||||
- chapter_chunk.chapter_overrides: list
|
||||
- chapter_chunk.chunks: list
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -11,6 +16,25 @@ from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from abogen.application.conversion_config import ChapterChunkConfig
|
||||
|
||||
|
||||
def _make_request(
|
||||
voice: str = "",
|
||||
speakers: dict | None = None,
|
||||
chapter_overrides: list | None = None,
|
||||
chunks: list | None = None,
|
||||
) -> SimpleNamespace:
|
||||
"""Create a ConversionRequest-like object for testing."""
|
||||
return SimpleNamespace(
|
||||
voice=voice,
|
||||
speakers=speakers,
|
||||
chapter_chunk=ChapterChunkConfig(
|
||||
chapter_overrides=chapter_overrides or [],
|
||||
chunks=chunks or [],
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# spec_to_voice_ids
|
||||
@@ -72,65 +96,62 @@ class TestSpecToVoiceIds:
|
||||
|
||||
|
||||
class TestJobVoiceFallback:
|
||||
"""job_voice_fallback resolves a fallback voice from job attributes."""
|
||||
"""job_voice_fallback resolves a fallback voice from request attributes."""
|
||||
|
||||
def test_direct_voice(self):
|
||||
from abogen.domain.voice_resolution import job_voice_fallback
|
||||
|
||||
job = SimpleNamespace(voice="af_heart", speakers=None, chapters=[])
|
||||
assert job_voice_fallback(job) == "af_heart"
|
||||
request = _make_request(voice="af_heart")
|
||||
assert job_voice_fallback(request) == "af_heart"
|
||||
|
||||
def test_custom_mix_ignored(self):
|
||||
from abogen.domain.voice_resolution import job_voice_fallback
|
||||
|
||||
job = SimpleNamespace(voice="__custom_mix", speakers=None, chapters=[])
|
||||
assert job_voice_fallback(job) == ""
|
||||
request = _make_request(voice="__custom_mix")
|
||||
assert job_voice_fallback(request) == ""
|
||||
|
||||
def test_narrator_speaker(self):
|
||||
from abogen.domain.voice_resolution import job_voice_fallback
|
||||
|
||||
job = SimpleNamespace(
|
||||
request = _make_request(
|
||||
voice="__custom_mix",
|
||||
speakers={"narrator": {"resolved_voice": "af_heart"}},
|
||||
chapters=[],
|
||||
)
|
||||
assert job_voice_fallback(job) == "af_heart"
|
||||
assert job_voice_fallback(request) == "af_heart"
|
||||
|
||||
def test_speaker_voice_formula(self):
|
||||
from abogen.domain.voice_resolution import job_voice_fallback
|
||||
|
||||
job = SimpleNamespace(
|
||||
request = _make_request(
|
||||
voice="",
|
||||
speakers={"speaker1": {"voice_formula": "v1*v2"}},
|
||||
chapters=[],
|
||||
)
|
||||
assert job_voice_fallback(job) == "v1*v2"
|
||||
assert job_voice_fallback(request) == "v1*v2"
|
||||
|
||||
def test_chapter_voice(self):
|
||||
from abogen.domain.voice_resolution import job_voice_fallback
|
||||
|
||||
job = SimpleNamespace(
|
||||
request = _make_request(
|
||||
voice="",
|
||||
speakers=None,
|
||||
chapters=[{"resolved_voice": "af_bella"}],
|
||||
chapter_overrides=[{"resolved_voice": "af_bella"}],
|
||||
)
|
||||
assert job_voice_fallback(job) == "af_bella"
|
||||
assert job_voice_fallback(request) == "af_bella"
|
||||
|
||||
def test_empty_job(self):
|
||||
def test_empty_request(self):
|
||||
from abogen.domain.voice_resolution import job_voice_fallback
|
||||
|
||||
job = SimpleNamespace(voice="", speakers=None, chapters=[])
|
||||
assert job_voice_fallback(job) == ""
|
||||
request = _make_request(voice="")
|
||||
assert job_voice_fallback(request) == ""
|
||||
|
||||
def test_narrator_custom_mix_falls_through(self):
|
||||
from abogen.domain.voice_resolution import job_voice_fallback
|
||||
|
||||
job = SimpleNamespace(
|
||||
request = _make_request(
|
||||
voice="",
|
||||
speakers={"narrator": {"voice": "__custom_mix"}},
|
||||
chapters=[{"voice": "af_heart"}],
|
||||
chapter_overrides=[{"voice": "af_heart"}],
|
||||
)
|
||||
assert job_voice_fallback(job) == "af_heart"
|
||||
assert job_voice_fallback(request) == "af_heart"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -144,35 +165,35 @@ class TestChapterVoiceSpec:
|
||||
def test_no_override_uses_fallback(self):
|
||||
from abogen.domain.voice_resolution import chapter_voice_spec
|
||||
|
||||
job = SimpleNamespace(voice="af_heart", speakers=None, chapters=[])
|
||||
assert chapter_voice_spec(job, None) == "af_heart"
|
||||
request = _make_request(voice="af_heart")
|
||||
assert chapter_voice_spec(request, None) == "af_heart"
|
||||
|
||||
def test_resolved_voice_wins(self):
|
||||
from abogen.domain.voice_resolution import chapter_voice_spec
|
||||
|
||||
job = SimpleNamespace(voice="af_heart", speakers=None, chapters=[])
|
||||
request = _make_request(voice="af_heart")
|
||||
override = {"resolved_voice": "af_bella", "voice_formula": "x", "voice": "y"}
|
||||
assert chapter_voice_spec(job, override) == "af_bella"
|
||||
assert chapter_voice_spec(request, override) == "af_bella"
|
||||
|
||||
def test_formula_second(self):
|
||||
from abogen.domain.voice_resolution import chapter_voice_spec
|
||||
|
||||
job = SimpleNamespace(voice="", speakers=None, chapters=[])
|
||||
request = _make_request(voice="")
|
||||
override = {"voice_formula": "v1*v2", "voice": "y"}
|
||||
assert chapter_voice_spec(job, override) == "v1*v2"
|
||||
assert chapter_voice_spec(request, override) == "v1*v2"
|
||||
|
||||
def test_voice_third(self):
|
||||
from abogen.domain.voice_resolution import chapter_voice_spec
|
||||
|
||||
job = SimpleNamespace(voice="", speakers=None, chapters=[])
|
||||
request = _make_request(voice="")
|
||||
override = {"voice": "af_nicole"}
|
||||
assert chapter_voice_spec(job, override) == "af_nicole"
|
||||
assert chapter_voice_spec(request, override) == "af_nicole"
|
||||
|
||||
def test_empty_override_falls_to_fallback(self):
|
||||
from abogen.domain.voice_resolution import chapter_voice_spec
|
||||
|
||||
job = SimpleNamespace(voice="af_heart", speakers=None, chapters=[])
|
||||
assert chapter_voice_spec(job, {}) == "af_heart"
|
||||
request = _make_request(voice="af_heart")
|
||||
assert chapter_voice_spec(request, {}) == "af_heart"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -186,37 +207,37 @@ class TestChunkVoiceSpec:
|
||||
def test_chunk_direct_voice(self):
|
||||
from abogen.domain.voice_resolution import chunk_voice_spec
|
||||
|
||||
job = SimpleNamespace(speakers=None)
|
||||
request = _make_request()
|
||||
chunk = {"resolved_voice": "af_heart"}
|
||||
assert chunk_voice_spec(job, chunk, "fallback") == "af_heart"
|
||||
assert chunk_voice_spec(request, chunk, "fallback") == "af_heart"
|
||||
|
||||
def test_chunk_speaker_lookup(self):
|
||||
from abogen.domain.voice_resolution import chunk_voice_spec
|
||||
|
||||
job = SimpleNamespace(speakers={"narrator": {"resolved_voice": "af_bella"}})
|
||||
request = _make_request(speakers={"narrator": {"resolved_voice": "af_bella"}})
|
||||
chunk = {"speaker_id": "narrator"}
|
||||
assert chunk_voice_spec(job, chunk, "") == "af_bella"
|
||||
assert chunk_voice_spec(request, chunk, "") == "af_bella"
|
||||
|
||||
def test_chunk_voice_profile_lookup(self):
|
||||
from abogen.domain.voice_resolution import chunk_voice_spec
|
||||
|
||||
job = SimpleNamespace(speakers={"角色A": {"voice": "af_nicole"}})
|
||||
request = _make_request(speakers={"角色A": {"voice": "af_nicole"}})
|
||||
chunk = {"voice_profile": "角色A"}
|
||||
assert chunk_voice_spec(job, chunk, "") == "af_nicole"
|
||||
assert chunk_voice_spec(request, chunk, "") == "af_nicole"
|
||||
|
||||
def test_uses_fallback_string(self):
|
||||
from abogen.domain.voice_resolution import chunk_voice_spec
|
||||
|
||||
job = SimpleNamespace(speakers=None)
|
||||
request = _make_request()
|
||||
chunk = {}
|
||||
assert chunk_voice_spec(job, chunk, "my_fallback") == "my_fallback"
|
||||
assert chunk_voice_spec(request, chunk, "my_fallback") == "my_fallback"
|
||||
|
||||
def test_fallback_to_job(self):
|
||||
def test_fallback_to_request(self):
|
||||
from abogen.domain.voice_resolution import chunk_voice_spec
|
||||
|
||||
job = SimpleNamespace(voice="af_heart", speakers=None, chapters=[])
|
||||
request = _make_request(voice="af_heart")
|
||||
chunk = {}
|
||||
assert chunk_voice_spec(job, chunk, "") == "af_heart"
|
||||
assert chunk_voice_spec(request, chunk, "") == "af_heart"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -225,50 +246,46 @@ class TestChunkVoiceSpec:
|
||||
|
||||
|
||||
class TestCollectRequiredVoiceIds:
|
||||
"""collect_required_voice_ids gathers all voice IDs from a job."""
|
||||
"""collect_required_voice_ids gathers all voice IDs from a request."""
|
||||
|
||||
def test_includes_job_voice(self):
|
||||
def test_includes_request_voice(self):
|
||||
from abogen.domain.voice_resolution import collect_required_voice_ids
|
||||
|
||||
job = SimpleNamespace(voice="af_heart", chapters=[], chunks=[], speakers={})
|
||||
request = _make_request(voice="af_heart")
|
||||
with patch("abogen.domain.voice_resolution.get_voices", return_value={"af_heart"}), \
|
||||
patch("abogen.domain.voice_resolution.job_voice_fallback", return_value=""):
|
||||
result = collect_required_voice_ids(job)
|
||||
result = collect_required_voice_ids(request)
|
||||
assert "af_heart" in result
|
||||
|
||||
def test_includes_chapter_voices(self):
|
||||
from abogen.domain.voice_resolution import collect_required_voice_ids
|
||||
|
||||
job = SimpleNamespace(
|
||||
request = _make_request(
|
||||
voice="",
|
||||
chapters=[{"resolved_voice": "af_bella"}],
|
||||
chunks=[],
|
||||
speakers={},
|
||||
chapter_overrides=[{"resolved_voice": "af_bella"}],
|
||||
)
|
||||
with patch("abogen.domain.voice_resolution.get_voices", return_value={"af_bella"}), \
|
||||
patch("abogen.domain.voice_resolution.job_voice_fallback", return_value=""):
|
||||
result = collect_required_voice_ids(job)
|
||||
result = collect_required_voice_ids(request)
|
||||
assert "af_bella" in result
|
||||
|
||||
def test_includes_chunk_voices(self):
|
||||
from abogen.domain.voice_resolution import collect_required_voice_ids
|
||||
|
||||
job = SimpleNamespace(
|
||||
request = _make_request(
|
||||
voice="",
|
||||
chapters=[],
|
||||
chunks=[{"voice": "af_nicole"}],
|
||||
speakers={},
|
||||
)
|
||||
with patch("abogen.domain.voice_resolution.get_voices", return_value={"af_nicole"}), \
|
||||
patch("abogen.domain.voice_resolution.job_voice_fallback", return_value=""):
|
||||
result = collect_required_voice_ids(job)
|
||||
result = collect_required_voice_ids(request)
|
||||
assert "af_nicole" in result
|
||||
|
||||
def test_always_includes_kokoro_voices(self):
|
||||
from abogen.domain.voice_resolution import collect_required_voice_ids
|
||||
|
||||
job = SimpleNamespace(voice="", chapters=[], chunks=[], speakers={})
|
||||
request = _make_request(voice="")
|
||||
with patch("abogen.domain.voice_resolution.get_voices", return_value={"af_heart", "af_bella"}), \
|
||||
patch("abogen.domain.voice_resolution.job_voice_fallback", return_value=""):
|
||||
result = collect_required_voice_ids(job)
|
||||
result = collect_required_voice_ids(request)
|
||||
assert {"af_heart", "af_bella"}.issubset(result)
|
||||
|
||||
Reference in New Issue
Block a user