mirror of
https://github.com/denizsafak/abogen.git
synced 2026-07-18 13:40:27 +02:00
feat: add plugin loader infrastructure
Implement plugin loading and validation: - loader.py: discover, import, validate plugins - validate PLUGIN_MANIFEST, MODEL_REQUIREMENTS, create_engine - validate api_version compatibility (major must match) - validate capabilities (reject unknown) - diagnostic messages for all error cases - no partial registration after error Test plugins: - fake_plugin: minimal valid plugin for testing - missing_manifest: no PLUGIN_MANIFEST - invalid_api_version: major version mismatch - invalid_capabilities: unknown capabilities - missing_create_engine: no create_engine function - import_error: raises ImportError during import - missing_model_requirements: no MODEL_REQUIREMENTS 39 new tests covering all loader functionality.
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
"""Fake plugin for testing the plugin loader.
|
||||
|
||||
This is a minimal valid plugin that satisfies the Plugin API contract.
|
||||
It does NOT perform any real TTS synthesis.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from abogen.tts_plugin.engine import Engine, EngineSession
|
||||
from abogen.tts_plugin.errors import EngineError
|
||||
from abogen.tts_plugin.host_context import HostContext
|
||||
from abogen.tts_plugin.manifest import (
|
||||
AudioFormatManifest,
|
||||
EngineManifest,
|
||||
PluginManifest,
|
||||
VoiceSourceManifest,
|
||||
)
|
||||
from abogen.tts_plugin.types import (
|
||||
AudioFormat,
|
||||
Duration,
|
||||
EngineConfig,
|
||||
ParameterValues,
|
||||
SynthesisRequest,
|
||||
SynthesizedAudio,
|
||||
)
|
||||
|
||||
|
||||
class FakeSession:
|
||||
"""Minimal EngineSession implementation for testing."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._disposed = False
|
||||
|
||||
def synthesize(self, request: SynthesisRequest) -> SynthesizedAudio:
|
||||
if self._disposed:
|
||||
raise EngineError("Session disposed")
|
||||
return SynthesizedAudio(
|
||||
data=b"\x00" * 100,
|
||||
format=AudioFormat(mime="audio/wav", extension="wav"),
|
||||
duration=Duration(seconds=1.0),
|
||||
)
|
||||
|
||||
def dispose(self) -> None:
|
||||
self._disposed = True
|
||||
|
||||
|
||||
class FakeEngine:
|
||||
"""Minimal Engine implementation for testing."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._disposed = False
|
||||
|
||||
def createSession(self) -> EngineSession:
|
||||
if self._disposed:
|
||||
raise EngineError("Engine disposed")
|
||||
return FakeSession()
|
||||
|
||||
def dispose(self) -> None:
|
||||
self._disposed = True
|
||||
|
||||
|
||||
PLUGIN_MANIFEST = PluginManifest(
|
||||
id="fake_plugin",
|
||||
name="Fake Plugin",
|
||||
version="1.0.0",
|
||||
api_version="1.0",
|
||||
description="A fake plugin for testing",
|
||||
author="Test Author",
|
||||
capabilities=(),
|
||||
engine=EngineManifest(
|
||||
voiceSources=(
|
||||
VoiceSourceManifest(id="builtin", name="Builtin", type="list"),
|
||||
),
|
||||
audioFormats=(
|
||||
AudioFormatManifest(mime="audio/wav", extension="wav"),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
MODEL_REQUIREMENTS: list[Any] = []
|
||||
|
||||
|
||||
def create_engine(
|
||||
context: HostContext,
|
||||
model_path: Path | None,
|
||||
config: EngineConfig,
|
||||
) -> Engine:
|
||||
"""Create a fake engine instance."""
|
||||
return FakeEngine()
|
||||
@@ -0,0 +1,18 @@
|
||||
"""Invalid plugin: raises ImportError during import."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
# This plugin intentionally raises an ImportError
|
||||
raise ImportError("Simulated import error for testing")
|
||||
|
||||
# The following code will never be reached, but is here for documentation
|
||||
from abogen.tts_plugin.manifest import PluginManifest
|
||||
|
||||
PLUGIN_MANIFEST = PluginManifest(
|
||||
id="import_error",
|
||||
name="Import Error Plugin",
|
||||
version="1.0.0",
|
||||
api_version="1.0",
|
||||
description="Plugin that fails to import",
|
||||
author="Test Author",
|
||||
)
|
||||
@@ -0,0 +1,29 @@
|
||||
"""Invalid plugin: incompatible api_version (major version mismatch)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from abogen.tts_plugin.manifest import PluginManifest
|
||||
from abogen.tts_plugin.types import EngineConfig
|
||||
|
||||
# api_version "2.0" has major version 2, but host expects major version 1
|
||||
PLUGIN_MANIFEST = PluginManifest(
|
||||
id="invalid_api_version",
|
||||
name="Invalid API Version Plugin",
|
||||
version="1.0.0",
|
||||
api_version="2.0", # Major version mismatch!
|
||||
description="Plugin with incompatible api_version",
|
||||
author="Test Author",
|
||||
)
|
||||
|
||||
MODEL_REQUIREMENTS: list[Any] = []
|
||||
|
||||
|
||||
def create_engine(
|
||||
context: Any,
|
||||
model_path: Path | None,
|
||||
config: EngineConfig,
|
||||
) -> Any:
|
||||
raise NotImplementedError("This plugin is invalid")
|
||||
@@ -0,0 +1,29 @@
|
||||
"""Invalid plugin: unknown capabilities."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from abogen.tts_plugin.manifest import PluginManifest
|
||||
from abogen.tts_plugin.types import EngineConfig
|
||||
|
||||
PLUGIN_MANIFEST = PluginManifest(
|
||||
id="invalid_capabilities",
|
||||
name="Invalid Capabilities Plugin",
|
||||
version="1.0.0",
|
||||
api_version="1.0",
|
||||
description="Plugin with unknown capabilities",
|
||||
author="Test Author",
|
||||
capabilities=("voice_list", "unknown_capability", "another_unknown"),
|
||||
)
|
||||
|
||||
MODEL_REQUIREMENTS: list[Any] = []
|
||||
|
||||
|
||||
def create_engine(
|
||||
context: Any,
|
||||
model_path: Path | None,
|
||||
config: EngineConfig,
|
||||
) -> Any:
|
||||
raise NotImplementedError("This plugin is invalid")
|
||||
@@ -0,0 +1,18 @@
|
||||
"""Invalid plugin: missing create_engine function."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from abogen.tts_plugin.manifest import PluginManifest
|
||||
|
||||
PLUGIN_MANIFEST = PluginManifest(
|
||||
id="missing_create_engine",
|
||||
name="Missing Create Engine Plugin",
|
||||
version="1.0.0",
|
||||
api_version="1.0",
|
||||
description="Plugin missing create_engine",
|
||||
author="Test Author",
|
||||
)
|
||||
|
||||
MODEL_REQUIREMENTS: list = []
|
||||
|
||||
# This plugin intentionally does NOT export create_engine
|
||||
@@ -0,0 +1,10 @@
|
||||
"""Invalid plugin: missing PLUGIN_MANIFEST."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
# This plugin intentionally does NOT export PLUGIN_MANIFEST
|
||||
MODEL_REQUIREMENTS: list = []
|
||||
|
||||
|
||||
def create_engine(context, model_path, config):
|
||||
raise NotImplementedError("This plugin is invalid")
|
||||
@@ -0,0 +1,28 @@
|
||||
"""Invalid plugin: missing MODEL_REQUIREMENTS."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from abogen.tts_plugin.manifest import PluginManifest
|
||||
from abogen.tts_plugin.types import EngineConfig
|
||||
|
||||
PLUGIN_MANIFEST = PluginManifest(
|
||||
id="missing_model_requirements",
|
||||
name="Missing Model Requirements Plugin",
|
||||
version="1.0.0",
|
||||
api_version="1.0",
|
||||
description="Plugin missing MODEL_REQUIREMENTS",
|
||||
author="Test Author",
|
||||
)
|
||||
|
||||
# This plugin intentionally does NOT export MODEL_REQUIREMENTS
|
||||
|
||||
|
||||
def create_engine(
|
||||
context: Any,
|
||||
model_path: Path | None,
|
||||
config: EngineConfig,
|
||||
) -> Any:
|
||||
raise NotImplementedError("This plugin is invalid")
|
||||
Reference in New Issue
Block a user