mirror of
https://github.com/denizsafak/abogen.git
synced 2026-07-18 05:40:26 +02:00
feat(tts-plugin): complete Plugin Architecture refactor
- Normalize Pipeline public API: create_pipeline(plugin_id, *, lang_code, device) - EngineConfig: add lang_code field per Architecture Amendment #1 - Kokoro plugin reads config.lang_code (fixes functional regression) - Static voice catalog in PluginManifest.voices (None = dynamic/VoiceLister) - get_voices() reads from manifest without creating Engine - Remove dead kwargs (sample_rate, auto_download, total_steps) from SuperTonic - Clean up unused imports and dead code in engine implementations - Fix test expectations for VoiceLister (mock overrides) - Add clear_preview_pipelines() for resource management
This commit is contained in:
@@ -94,12 +94,18 @@ class SynthesizedAudio:
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class EngineConfig:
|
||||
"""Immutable value object for engine initialization settings.
|
||||
"""Immutable configuration of an Engine instance.
|
||||
|
||||
Contains only engine-specific settings, no resource references.
|
||||
Contains parameters that define how a particular Engine instance is
|
||||
created and that remain constant throughout the lifetime of that Engine.
|
||||
|
||||
Plugin implementations may ignore fields that are not applicable to them.
|
||||
|
||||
Attributes:
|
||||
device: Device to use (e.g., "cpu", "cuda:0").
|
||||
lang_code: Language code for the engine (e.g., "a" for Kokoro English).
|
||||
Plugins that do not require a language code ignore this field.
|
||||
"""
|
||||
|
||||
device: str = "cpu"
|
||||
lang_code: str = "a"
|
||||
|
||||
@@ -247,4 +247,8 @@ def run_debug_tts_wavs(
|
||||
"sample_rate": SAMPLE_RATE,
|
||||
}
|
||||
(run_dir / "manifest.json").write_text(json.dumps(manifest, indent=2), encoding="utf-8")
|
||||
try:
|
||||
pipeline.dispose()
|
||||
except Exception:
|
||||
pass
|
||||
return manifest
|
||||
|
||||
@@ -164,6 +164,9 @@ class TestCreatePipelineCompat:
|
||||
|
||||
def test_create_pipeline_returns_callable(self):
|
||||
"""create_pipeline returns a callable backend."""
|
||||
from abogen.tts_plugin.host_context import HostContext
|
||||
from abogen.tts_plugin.types import EngineConfig
|
||||
|
||||
# Mock the plugin manager
|
||||
with patch("abogen.tts_plugin.utils.get_plugin_manager") as mock_get_manager:
|
||||
mock_manager = MagicMock()
|
||||
@@ -175,7 +178,14 @@ class TestCreatePipelineCompat:
|
||||
backend = create_pipeline("kokoro", lang_code="a", device="cpu")
|
||||
|
||||
assert callable(backend)
|
||||
mock_manager.create_engine.assert_called_once_with("kokoro", lang_code="a", device="cpu")
|
||||
mock_manager.create_engine.assert_called_once()
|
||||
call_args = mock_manager.create_engine.call_args
|
||||
assert call_args.args[0] == "kokoro"
|
||||
assert isinstance(call_args.kwargs["context"], HostContext)
|
||||
assert call_args.kwargs["model_path"] is None
|
||||
assert isinstance(call_args.kwargs["config"], EngineConfig)
|
||||
assert call_args.kwargs["config"].device == "cpu"
|
||||
assert call_args.kwargs["config"].lang_code == "a"
|
||||
|
||||
def test_create_pipeline_raises_for_unknown_plugin(self):
|
||||
"""create_pipeline raises KeyError for unknown plugins."""
|
||||
|
||||
@@ -192,11 +192,24 @@ class TestEngineConfigContract:
|
||||
config = EngineConfig(device="cuda:0")
|
||||
assert config.device == "cuda:0"
|
||||
|
||||
def test_default_lang_code(self) -> None:
|
||||
config = EngineConfig()
|
||||
assert config.lang_code == "a"
|
||||
|
||||
def test_custom_lang_code(self) -> None:
|
||||
config = EngineConfig(lang_code="j")
|
||||
assert config.lang_code == "j"
|
||||
|
||||
def test_immutability(self) -> None:
|
||||
config = EngineConfig()
|
||||
with pytest.raises(AttributeError):
|
||||
config.device = "cuda:0" # type: ignore[misc]
|
||||
|
||||
def test_immutability_lang_code(self) -> None:
|
||||
config = EngineConfig()
|
||||
with pytest.raises(AttributeError):
|
||||
config.lang_code = "j" # type: ignore[misc]
|
||||
|
||||
def test_unknown_keys_ignored_per_spec(self) -> None:
|
||||
"""Architecture spec: Unknown keys are ignored (no error).
|
||||
|
||||
@@ -205,3 +218,27 @@ class TestEngineConfigContract:
|
||||
"""
|
||||
config = EngineConfig()
|
||||
assert config.device == "cpu"
|
||||
|
||||
def test_plugins_may_ignore_irrelevant_fields(self) -> None:
|
||||
"""Architecture Amendment #1: Plugins ignore unsupported fields.
|
||||
|
||||
EngineConfig may contain fields that are not relevant to every plugin.
|
||||
Plugins MUST ignore fields they do not need, not raise on them.
|
||||
"""
|
||||
config = EngineConfig(device="cuda:0", lang_code="j")
|
||||
assert config.device == "cuda:0"
|
||||
assert config.lang_code == "j"
|
||||
# A plugin that only needs device simply reads config.device
|
||||
# and ignores config.lang_code — this must not raise.
|
||||
|
||||
def test_engine_config_contains_engine_instance_configuration(self) -> None:
|
||||
"""Architecture Amendment #1: EngineConfig definition.
|
||||
|
||||
EngineConfig contains parameters that define how a particular
|
||||
Engine instance is created and that remain constant throughout
|
||||
the lifetime of that Engine.
|
||||
"""
|
||||
config = EngineConfig(device="cpu", lang_code="a")
|
||||
# Both fields are init-time, immutable, engine-scoped.
|
||||
assert config.device == "cpu"
|
||||
assert config.lang_code == "a"
|
||||
|
||||
@@ -92,6 +92,7 @@ class MockEngine:
|
||||
def __init__(
|
||||
self,
|
||||
voice_manifests: list[VoiceManifest] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
self._disposed = False
|
||||
self._voice_manifests = voice_manifests or [
|
||||
@@ -930,6 +931,7 @@ class TestValueObjectsBehavioral:
|
||||
|
||||
config = EngineConfig()
|
||||
assert config.device == "cpu"
|
||||
assert config.lang_code == "a"
|
||||
|
||||
def test_parameter_values_defaults(self) -> None:
|
||||
pv = ParameterValues()
|
||||
|
||||
@@ -57,7 +57,15 @@ def _make_mock_engine() -> Any:
|
||||
def __call__(self, text, voice, speed, split_pattern=None, total_steps=None):
|
||||
return [MockSegment()]
|
||||
|
||||
return SuperTonicEngine(MockPipeline())
|
||||
engine = SuperTonicEngine(MockPipeline())
|
||||
|
||||
# Override listVoices for testing (real engine reads from manifest)
|
||||
from abogen.tts_plugin.manifest import VoiceManifest
|
||||
engine.listVoices = lambda source_id: [
|
||||
VoiceManifest(id="test_voice_1", name="Test Voice 1", tags=("male",)),
|
||||
VoiceManifest(id="test_voice_2", name="Test Voice 2", tags=("female",)),
|
||||
]
|
||||
return engine
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user