mirror of
https://github.com/denizsafak/abogen.git
synced 2026-07-18 05:40:26 +02:00
refactor: remove compatibility layer, use Plugin Architecture directly
- Delete abogen/tts_plugin/compat.py (CompatBackend, create_backend, get_metadata, etc.) - Add abogen/tts_plugin/utils.py with direct Plugin Manager functions: get_voices, get_default_voice, is_plugin_registered, resolve_voice_to_plugin, create_pipeline - Update all 16 consumer files to import from utils instead of compat - Update __init__.py to re-export utils instead of compat - Update 5 test files and add TestNoCompatLayer regression tests - All 493 tests pass
This commit is contained in:
@@ -17,7 +17,7 @@ import numpy as np
|
||||
from abogen.tts_plugin.engine import Engine, EngineSession
|
||||
from abogen.tts_plugin.errors import EngineError
|
||||
from abogen.tts_plugin.plugin_manager import PluginManager, get_plugin_manager, reset_plugin_manager
|
||||
from abogen.tts_plugin.compat import CompatBackend, create_backend
|
||||
from abogen.tts_plugin.utils import Pipeline, create_pipeline
|
||||
from abogen.tts_plugin.types import (
|
||||
AudioFormat,
|
||||
Duration,
|
||||
@@ -148,8 +148,8 @@ class TestConsumerFlow:
|
||||
assert result.format.mime == "audio/wav"
|
||||
assert result.duration.seconds > 0
|
||||
|
||||
def test_consumer_flow_via_compat_adapter(self):
|
||||
"""Verify flow through compatibility adapter matches direct flow."""
|
||||
def test_consumer_flow_via_pipeline(self):
|
||||
"""Verify flow through Pipeline utility matches direct flow."""
|
||||
manager = PluginManager()
|
||||
|
||||
# Register mock plugin
|
||||
@@ -157,9 +157,9 @@ class TestConsumerFlow:
|
||||
manager._plugins["mock_tts"] = mock_plugin
|
||||
manager._loaded = True
|
||||
|
||||
# Use compat adapter
|
||||
with patch("abogen.tts_plugin.compat.get_plugin_manager", return_value=manager):
|
||||
backend = create_backend("mock_tts")
|
||||
# Use Pipeline utility
|
||||
with patch("abogen.tts_plugin.utils.get_plugin_manager", return_value=manager):
|
||||
backend = create_pipeline("mock_tts")
|
||||
|
||||
# Call like old TTSBackend
|
||||
segments = list(backend("Hello world", voice="default", speed=1.0))
|
||||
@@ -296,8 +296,8 @@ class TestRegression:
|
||||
manager._loaded = True
|
||||
|
||||
# New path: Plugin Manager → Engine → Session → Synthesis
|
||||
with patch("abogen.tts_plugin.compat.get_plugin_manager", return_value=manager):
|
||||
new_backend = create_backend("mock_tts")
|
||||
with patch("abogen.tts_plugin.utils.get_plugin_manager", return_value=manager):
|
||||
new_backend = create_pipeline("mock_tts")
|
||||
new_segments = list(new_backend("Hello world", voice="default", speed=1.0))
|
||||
|
||||
# Old path: Direct MockEngine (simulating old registry)
|
||||
@@ -320,15 +320,15 @@ class TestRegression:
|
||||
# Both should have same format
|
||||
assert new_segments[0].audio.dtype == np.float32
|
||||
|
||||
def test_compat_adapter_matches_old_interface(self):
|
||||
"""Compat adapter should match old TTSBackend interface."""
|
||||
def test_pipeline_matches_old_interface(self):
|
||||
"""Pipeline utility should match old TTSBackend interface."""
|
||||
manager = PluginManager()
|
||||
mock_plugin = create_mock_plugin()
|
||||
manager._plugins["mock_tts"] = mock_plugin
|
||||
manager._loaded = True
|
||||
|
||||
with patch("abogen.tts_plugin.compat.get_plugin_manager", return_value=manager):
|
||||
backend = create_backend("mock_tts", lang_code="a", device="cpu")
|
||||
with patch("abogen.tts_plugin.utils.get_plugin_manager", return_value=manager):
|
||||
backend = create_pipeline("mock_tts", lang_code="a", device="cpu")
|
||||
|
||||
# Old interface: pipeline(text, voice=..., speed=..., split_pattern=...)
|
||||
segments = list(backend(
|
||||
@@ -398,3 +398,23 @@ class TestPluginManagerIntegration:
|
||||
|
||||
# Cache should be empty
|
||||
assert len(manager._engines) == 0
|
||||
|
||||
|
||||
class TestNoCompatLayer:
|
||||
"""Regression: confirm the compatibility layer has been removed."""
|
||||
|
||||
def test_compat_module_does_not_exist(self):
|
||||
"""abogen.tts_plugin.compat must not be importable."""
|
||||
import importlib
|
||||
with pytest.raises((ImportError, ModuleNotFoundError)):
|
||||
importlib.import_module("abogen.tts_plugin.compat")
|
||||
|
||||
def test_consumers_use_plugin_architecture_directly(self):
|
||||
"""Key consumers import from abogen.tts_plugin.utils, not compat."""
|
||||
import inspect, abogen.voice_profiles, abogen.voice_formulas, abogen.voice_cache
|
||||
|
||||
for mod in (abogen.voice_profiles, abogen.voice_formulas, abogen.voice_cache):
|
||||
source = inspect.getsource(mod)
|
||||
assert "tts_plugin.compat" not in source, (
|
||||
f"{mod.__name__} still references tts_plugin.compat"
|
||||
)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"""Integration tests for Plugin Manager and compatibility adapter."""
|
||||
"""Integration tests for Plugin Manager and direct utility functions."""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from abogen.tts_plugin.plugin_manager import PluginManager, get_plugin_manager, reset_plugin_manager
|
||||
from abogen.tts_plugin.compat import CompatBackend, create_backend
|
||||
from abogen.tts_plugin.utils import Pipeline, create_pipeline
|
||||
from abogen.tts_plugin.engine import Engine, EngineSession
|
||||
from abogen.tts_plugin.types import SynthesisRequest, SynthesizedAudio, AudioFormat
|
||||
|
||||
@@ -110,27 +110,27 @@ class TestPluginManager:
|
||||
reset_plugin_manager()
|
||||
|
||||
|
||||
class TestCompatBackend:
|
||||
"""Test CompatBackend functionality."""
|
||||
class TestPipeline:
|
||||
"""Test Pipeline functionality."""
|
||||
|
||||
def test_compat_backend_creation(self):
|
||||
"""CompatBackend can be created."""
|
||||
def test_pipeline_creation(self):
|
||||
"""Pipeline can be created."""
|
||||
engine = FakeEngine()
|
||||
backend = CompatBackend(engine)
|
||||
backend = Pipeline(engine)
|
||||
assert backend is not None
|
||||
|
||||
def test_compat_backend_callable(self):
|
||||
"""CompatBackend is callable like old TTSBackend."""
|
||||
def test_pipeline_callable(self):
|
||||
"""Pipeline is callable like old TTSBackend."""
|
||||
engine = FakeEngine()
|
||||
backend = CompatBackend(engine)
|
||||
backend = Pipeline(engine)
|
||||
|
||||
# Should be callable
|
||||
assert callable(backend)
|
||||
|
||||
def test_compat_backend_synthesize(self):
|
||||
"""CompatBackend can synthesize text."""
|
||||
def test_pipeline_synthesize(self):
|
||||
"""Pipeline can synthesize text."""
|
||||
engine = FakeEngine()
|
||||
backend = CompatBackend(engine)
|
||||
backend = Pipeline(engine)
|
||||
|
||||
# Call the backend
|
||||
segments = list(backend("Hello world", voice="default", speed=1.0))
|
||||
@@ -144,10 +144,10 @@ class TestCompatBackend:
|
||||
assert hasattr(segment, "audio")
|
||||
assert segment.graphemes == "Hello world"
|
||||
|
||||
def test_compat_backend_dispose(self):
|
||||
"""CompatBackend can be disposed."""
|
||||
def test_pipeline_dispose(self):
|
||||
"""Pipeline can be disposed."""
|
||||
engine = FakeEngine()
|
||||
backend = CompatBackend(engine)
|
||||
backend = Pipeline(engine)
|
||||
|
||||
# Create a session by calling
|
||||
list(backend("test"))
|
||||
@@ -159,33 +159,33 @@ class TestCompatBackend:
|
||||
backend.dispose()
|
||||
|
||||
|
||||
class TestCreateBackendCompat:
|
||||
"""Test create_backend compatibility function."""
|
||||
class TestCreatePipelineCompat:
|
||||
"""Test create_pipeline utility function."""
|
||||
|
||||
def test_create_backend_returns_callable(self):
|
||||
"""create_backend returns a callable backend."""
|
||||
def test_create_pipeline_returns_callable(self):
|
||||
"""create_pipeline returns a callable backend."""
|
||||
# Mock the plugin manager
|
||||
with patch("abogen.tts_plugin.compat.get_plugin_manager") as mock_get_manager:
|
||||
with patch("abogen.tts_plugin.utils.get_plugin_manager") as mock_get_manager:
|
||||
mock_manager = MagicMock()
|
||||
mock_get_manager.return_value = mock_manager
|
||||
|
||||
mock_engine = FakeEngine()
|
||||
mock_manager.create_engine.return_value = mock_engine
|
||||
|
||||
backend = create_backend("kokoro", lang_code="a", device="cpu")
|
||||
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")
|
||||
|
||||
def test_create_backend_raises_for_unknown_plugin(self):
|
||||
"""create_backend raises KeyError for unknown plugins."""
|
||||
with patch("abogen.tts_plugin.compat.get_plugin_manager") as mock_get_manager:
|
||||
def test_create_pipeline_raises_for_unknown_plugin(self):
|
||||
"""create_pipeline raises KeyError for unknown plugins."""
|
||||
with patch("abogen.tts_plugin.utils.get_plugin_manager") as mock_get_manager:
|
||||
mock_manager = MagicMock()
|
||||
mock_get_manager.return_value = mock_manager
|
||||
mock_manager.create_engine.side_effect = KeyError("Plugin not found")
|
||||
|
||||
with pytest.raises(KeyError):
|
||||
create_backend("nonexistent")
|
||||
create_pipeline("nonexistent")
|
||||
|
||||
|
||||
class TestPluginManagerWithFakePlugins:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from types import SimpleNamespace
|
||||
from typing import cast
|
||||
|
||||
from abogen.tts_plugin.compat import get_metadata
|
||||
from abogen.tts_plugin.utils import get_voices
|
||||
from abogen.webui.conversion_runner import (
|
||||
_chapter_voice_spec,
|
||||
_chunk_voice_spec,
|
||||
@@ -49,4 +49,4 @@ def test_voice_collection_includes_formula_components():
|
||||
voices = _collect_required_voice_ids(job)
|
||||
|
||||
assert {"af_nova", "am_liam"}.issubset(voices)
|
||||
assert voices.issuperset(get_metadata("kokoro").voices)
|
||||
assert voices.issuperset(get_voices("kokoro"))
|
||||
|
||||
@@ -30,16 +30,16 @@ def test_preview_applies_manual_override_before_normalization(monkeypatch):
|
||||
captured["text"] = text
|
||||
return iter(())
|
||||
|
||||
from abogen.tts_plugin import compat
|
||||
from abogen.tts_plugin import utils
|
||||
|
||||
original_create_backend = compat.create_backend
|
||||
original_create_pipeline = utils.create_pipeline
|
||||
|
||||
def _mock_create_backend(backend_id, **kwargs):
|
||||
def _mock_create_pipeline(backend_id, **kwargs):
|
||||
if backend_id == "supertonic":
|
||||
return DummyPipeline(**kwargs)
|
||||
return original_create_backend(backend_id, **kwargs)
|
||||
return original_create_pipeline(backend_id, **kwargs)
|
||||
|
||||
monkeypatch.setattr(compat, "create_backend", _mock_create_backend)
|
||||
monkeypatch.setattr(utils, "create_pipeline", _mock_create_pipeline)
|
||||
|
||||
try:
|
||||
preview.generate_preview_audio(
|
||||
|
||||
@@ -3,7 +3,7 @@ from typing import cast
|
||||
|
||||
import pytest
|
||||
|
||||
from abogen.tts_plugin.compat import get_metadata
|
||||
from abogen.tts_plugin.utils import get_voices
|
||||
from abogen.voice_cache import (
|
||||
LocalEntryNotFoundError,
|
||||
_CACHED_VOICES,
|
||||
@@ -66,4 +66,4 @@ def test_collect_required_voice_ids_includes_all():
|
||||
voices = _collect_required_voice_ids(cast(Job, job))
|
||||
|
||||
assert {"af_nova", "am_liam", "am_michael"}.issubset(voices)
|
||||
assert voices.issuperset(get_metadata("kokoro").voices)
|
||||
assert voices.issuperset(get_voices("kokoro"))
|
||||
|
||||
Reference in New Issue
Block a user