mirror of
https://github.com/denizsafak/abogen.git
synced 2026-07-18 13:40:27 +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:
@@ -1,290 +1,290 @@
|
||||
"""Contract tests for plugin manifest types.
|
||||
|
||||
These tests verify that manifest types satisfy the architectural requirements:
|
||||
- All required fields are present
|
||||
- api_version follows semver format
|
||||
- capabilities are properly defined
|
||||
- engine manifest describes the engine correctly
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
from abogen.tts_plugin.manifest import (
|
||||
AudioFormatManifest,
|
||||
EngineManifest,
|
||||
EnumOption,
|
||||
GpuRequirement,
|
||||
ModelManifest,
|
||||
ParameterManifest,
|
||||
PluginManifest,
|
||||
RequirementManifest,
|
||||
VoiceManifest,
|
||||
VoiceSourceManifest,
|
||||
)
|
||||
|
||||
|
||||
class TestPluginManifestContract:
|
||||
"""Contract tests for PluginManifest."""
|
||||
|
||||
def test_required_fields(self) -> None:
|
||||
manifest = PluginManifest(
|
||||
id="test-plugin",
|
||||
name="Test Plugin",
|
||||
version="1.0.0",
|
||||
api_version="1.0",
|
||||
description="A test plugin",
|
||||
author="Test Author",
|
||||
)
|
||||
assert manifest.id == "test-plugin"
|
||||
assert manifest.name == "Test Plugin"
|
||||
assert manifest.version == "1.0.0"
|
||||
assert manifest.api_version == "1.0"
|
||||
assert manifest.description == "A test plugin"
|
||||
assert manifest.author == "Test Author"
|
||||
|
||||
def test_api_version_semver_format(self) -> None:
|
||||
"""Architecture spec: api_version format is semver (MAJOR.MINOR)."""
|
||||
valid_versions = ["1.0", "2.1", "10.5"]
|
||||
for version in valid_versions:
|
||||
manifest = PluginManifest(
|
||||
id="test",
|
||||
name="Test",
|
||||
version="1.0.0",
|
||||
api_version=version,
|
||||
description="Test",
|
||||
author="Test",
|
||||
)
|
||||
assert re.match(r"^\d+\.\d+$", manifest.api_version)
|
||||
|
||||
def test_capabilities_default_empty(self) -> None:
|
||||
manifest = PluginManifest(
|
||||
id="test",
|
||||
name="Test",
|
||||
version="1.0.0",
|
||||
api_version="1.0",
|
||||
description="Test",
|
||||
author="Test",
|
||||
)
|
||||
assert manifest.capabilities == ()
|
||||
|
||||
def test_capabilities_tuple(self) -> None:
|
||||
manifest = PluginManifest(
|
||||
id="test",
|
||||
name="Test",
|
||||
version="1.0.0",
|
||||
api_version="1.0",
|
||||
description="Test",
|
||||
author="Test",
|
||||
capabilities=("voice_list", "preview"),
|
||||
)
|
||||
assert "voice_list" in manifest.capabilities
|
||||
assert "preview" in manifest.capabilities
|
||||
|
||||
def test_requires_default(self) -> None:
|
||||
manifest = PluginManifest(
|
||||
id="test",
|
||||
name="Test",
|
||||
version="1.0.0",
|
||||
api_version="1.0",
|
||||
description="Test",
|
||||
author="Test",
|
||||
)
|
||||
assert isinstance(manifest.requires, RequirementManifest)
|
||||
|
||||
def test_engine_default(self) -> None:
|
||||
manifest = PluginManifest(
|
||||
id="test",
|
||||
name="Test",
|
||||
version="1.0.0",
|
||||
api_version="1.0",
|
||||
description="Test",
|
||||
author="Test",
|
||||
)
|
||||
assert isinstance(manifest.engine, EngineManifest)
|
||||
|
||||
|
||||
class TestEngineManifestContract:
|
||||
"""Contract tests for EngineManifest."""
|
||||
|
||||
def test_required_fields(self) -> None:
|
||||
manifest = EngineManifest(
|
||||
voiceSources=(
|
||||
VoiceSourceManifest(id="builtin", name="Builtin", type="list"),
|
||||
),
|
||||
parameters=(
|
||||
ParameterManifest(
|
||||
id="speed", name="Speed", description="Speed", type="float", default=1.0
|
||||
),
|
||||
),
|
||||
audioFormats=(AudioFormatManifest(mime="audio/wav", extension="wav"),),
|
||||
)
|
||||
assert len(manifest.voiceSources) == 1
|
||||
assert len(manifest.parameters) == 1
|
||||
assert len(manifest.audioFormats) == 1
|
||||
|
||||
def test_defaults_empty(self) -> None:
|
||||
manifest = EngineManifest()
|
||||
assert manifest.voiceSources == ()
|
||||
assert manifest.parameters == ()
|
||||
assert manifest.audioFormats == ()
|
||||
|
||||
|
||||
class TestVoiceSourceManifestContract:
|
||||
"""Contract tests for VoiceSourceManifest."""
|
||||
|
||||
def test_required_fields(self) -> None:
|
||||
vs = VoiceSourceManifest(id="builtin", name="Builtin", type="list")
|
||||
assert vs.id == "builtin"
|
||||
assert vs.name == "Builtin"
|
||||
assert vs.type == "list"
|
||||
|
||||
def test_valid_types(self) -> None:
|
||||
valid_types = ["list", "speaker_id", "clone", "blend", "generate", "none"]
|
||||
for vtype in valid_types:
|
||||
vs = VoiceSourceManifest(id="test", name="Test", type=vtype)
|
||||
assert vs.type == vtype
|
||||
|
||||
def test_config_optional(self) -> None:
|
||||
vs = VoiceSourceManifest(id="test", name="Test", type="list")
|
||||
assert vs.config is None
|
||||
|
||||
def test_config_any(self) -> None:
|
||||
config = {"voices": ["af_nova", "af_sky"]}
|
||||
vs = VoiceSourceManifest(id="test", name="Test", type="list", config=config)
|
||||
assert vs.config == config
|
||||
|
||||
|
||||
class TestVoiceManifestContract:
|
||||
"""Contract tests for VoiceManifest."""
|
||||
|
||||
def test_required_fields(self) -> None:
|
||||
v = VoiceManifest(id="af_nova", name="Nova")
|
||||
assert v.id == "af_nova"
|
||||
assert v.name == "Nova"
|
||||
|
||||
def test_tags_default_empty(self) -> None:
|
||||
v = VoiceManifest(id="af_nova", name="Nova")
|
||||
assert v.tags == ()
|
||||
|
||||
def test_tags_tuple(self) -> None:
|
||||
v = VoiceManifest(id="af_nova", name="Nova", tags=("en", "female"))
|
||||
assert "en" in v.tags
|
||||
assert "female" in v.tags
|
||||
|
||||
|
||||
class TestParameterManifestContract:
|
||||
"""Contract tests for ParameterManifest."""
|
||||
|
||||
def test_required_fields(self) -> None:
|
||||
p = ParameterManifest(
|
||||
id="speed", name="Speed", description="Speech speed", type="float", default=1.0
|
||||
)
|
||||
assert p.id == "speed"
|
||||
assert p.name == "Speed"
|
||||
assert p.description == "Speech speed"
|
||||
assert p.type == "float"
|
||||
assert p.default == 1.0
|
||||
|
||||
def test_valid_types(self) -> None:
|
||||
valid_types = ["float", "int", "string", "boolean", "enum"]
|
||||
for ptype in valid_types:
|
||||
p = ParameterManifest(
|
||||
id="test", name="Test", description="Test", type=ptype, default=None
|
||||
)
|
||||
assert p.type == ptype
|
||||
|
||||
def test_optional_numeric_bounds(self) -> None:
|
||||
p = ParameterManifest(
|
||||
id="speed",
|
||||
name="Speed",
|
||||
description="Speed",
|
||||
type="float",
|
||||
default=1.0,
|
||||
min=0.5,
|
||||
max=2.0,
|
||||
step=0.1,
|
||||
)
|
||||
assert p.min == 0.5
|
||||
assert p.max == 2.0
|
||||
assert p.step == 0.1
|
||||
|
||||
def test_enum_options(self) -> None:
|
||||
options = (
|
||||
EnumOption(value="low", label="Low"),
|
||||
EnumOption(value="high", label="High"),
|
||||
)
|
||||
p = ParameterManifest(
|
||||
id="quality",
|
||||
name="Quality",
|
||||
description="Quality",
|
||||
type="enum",
|
||||
default="low",
|
||||
options=options,
|
||||
)
|
||||
assert len(p.options) == 2
|
||||
assert p.options[0].value == "low"
|
||||
|
||||
|
||||
class TestAudioFormatManifestContract:
|
||||
"""Contract tests for AudioFormatManifest."""
|
||||
|
||||
def test_required_fields(self) -> None:
|
||||
af = AudioFormatManifest(mime="audio/wav", extension="wav")
|
||||
assert af.mime == "audio/wav"
|
||||
assert af.extension == "wav"
|
||||
|
||||
|
||||
class TestEnumOptionContract:
|
||||
"""Contract tests for EnumOption."""
|
||||
|
||||
def test_required_fields(self) -> None:
|
||||
opt = EnumOption(value="low", label="Low Quality")
|
||||
assert opt.value == "low"
|
||||
assert opt.label == "Low Quality"
|
||||
|
||||
|
||||
class TestRequirementManifestContract:
|
||||
"""Contract tests for RequirementManifest."""
|
||||
|
||||
def test_defaults(self) -> None:
|
||||
req = RequirementManifest()
|
||||
assert req.gpu is None
|
||||
assert req.memory is None
|
||||
assert req.internet is None
|
||||
|
||||
def test_with_gpu(self) -> None:
|
||||
gpu = GpuRequirement(required=True, type="cuda", memory=8.0)
|
||||
req = RequirementManifest(gpu=gpu)
|
||||
assert req.gpu.required is True
|
||||
assert req.gpu.type == "cuda"
|
||||
assert req.gpu.memory == 8.0
|
||||
|
||||
def test_with_internet(self) -> None:
|
||||
req = RequirementManifest(internet=True)
|
||||
assert req.internet is True
|
||||
|
||||
|
||||
class TestGpuRequirementContract:
|
||||
"""Contract tests for GpuRequirement."""
|
||||
|
||||
def test_defaults(self) -> None:
|
||||
gpu = GpuRequirement()
|
||||
assert gpu.required is False
|
||||
assert gpu.type is None
|
||||
assert gpu.memory is None
|
||||
|
||||
def test_required_gpu(self) -> None:
|
||||
gpu = GpuRequirement(required=True, type="cuda", memory=8.0)
|
||||
assert gpu.required is True
|
||||
|
||||
|
||||
class TestModelManifestContract:
|
||||
"""Contract tests for ModelManifest."""
|
||||
|
||||
def test_required_fields(self) -> None:
|
||||
m = ModelManifest(id="xtts_v2", name="XTTS v2", size="2GB")
|
||||
assert m.id == "xtts_v2"
|
||||
assert m.name == "XTTS v2"
|
||||
assert m.size == "2GB"
|
||||
"""Contract tests for plugin manifest types.
|
||||
|
||||
These tests verify that manifest types satisfy the architectural requirements:
|
||||
- All required fields are present
|
||||
- api_version follows semver format
|
||||
- capabilities are properly defined
|
||||
- engine manifest describes the engine correctly
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
from abogen.tts_plugin.manifest import (
|
||||
AudioFormatManifest,
|
||||
EngineManifest,
|
||||
EnumOption,
|
||||
GpuRequirement,
|
||||
ModelManifest,
|
||||
ParameterManifest,
|
||||
PluginManifest,
|
||||
RequirementManifest,
|
||||
VoiceManifest,
|
||||
VoiceSourceManifest,
|
||||
)
|
||||
|
||||
|
||||
class TestPluginManifestContract:
|
||||
"""Contract tests for PluginManifest."""
|
||||
|
||||
def test_required_fields(self) -> None:
|
||||
manifest = PluginManifest(
|
||||
id="test-plugin",
|
||||
name="Test Plugin",
|
||||
version="1.0.0",
|
||||
api_version="1.0",
|
||||
description="A test plugin",
|
||||
author="Test Author",
|
||||
)
|
||||
assert manifest.id == "test-plugin"
|
||||
assert manifest.name == "Test Plugin"
|
||||
assert manifest.version == "1.0.0"
|
||||
assert manifest.api_version == "1.0"
|
||||
assert manifest.description == "A test plugin"
|
||||
assert manifest.author == "Test Author"
|
||||
|
||||
def test_api_version_semver_format(self) -> None:
|
||||
"""Architecture spec: api_version format is semver (MAJOR.MINOR)."""
|
||||
valid_versions = ["1.0", "2.1", "10.5"]
|
||||
for version in valid_versions:
|
||||
manifest = PluginManifest(
|
||||
id="test",
|
||||
name="Test",
|
||||
version="1.0.0",
|
||||
api_version=version,
|
||||
description="Test",
|
||||
author="Test",
|
||||
)
|
||||
assert re.match(r"^\d+\.\d+$", manifest.api_version)
|
||||
|
||||
def test_capabilities_default_empty(self) -> None:
|
||||
manifest = PluginManifest(
|
||||
id="test",
|
||||
name="Test",
|
||||
version="1.0.0",
|
||||
api_version="1.0",
|
||||
description="Test",
|
||||
author="Test",
|
||||
)
|
||||
assert manifest.capabilities == ()
|
||||
|
||||
def test_capabilities_tuple(self) -> None:
|
||||
manifest = PluginManifest(
|
||||
id="test",
|
||||
name="Test",
|
||||
version="1.0.0",
|
||||
api_version="1.0",
|
||||
description="Test",
|
||||
author="Test",
|
||||
capabilities=("voice_list", "preview"),
|
||||
)
|
||||
assert "voice_list" in manifest.capabilities
|
||||
assert "preview" in manifest.capabilities
|
||||
|
||||
def test_requires_default(self) -> None:
|
||||
manifest = PluginManifest(
|
||||
id="test",
|
||||
name="Test",
|
||||
version="1.0.0",
|
||||
api_version="1.0",
|
||||
description="Test",
|
||||
author="Test",
|
||||
)
|
||||
assert isinstance(manifest.requires, RequirementManifest)
|
||||
|
||||
def test_engine_default(self) -> None:
|
||||
manifest = PluginManifest(
|
||||
id="test",
|
||||
name="Test",
|
||||
version="1.0.0",
|
||||
api_version="1.0",
|
||||
description="Test",
|
||||
author="Test",
|
||||
)
|
||||
assert isinstance(manifest.engine, EngineManifest)
|
||||
|
||||
|
||||
class TestEngineManifestContract:
|
||||
"""Contract tests for EngineManifest."""
|
||||
|
||||
def test_required_fields(self) -> None:
|
||||
manifest = EngineManifest(
|
||||
voiceSources=(
|
||||
VoiceSourceManifest(id="builtin", name="Builtin", type="list"),
|
||||
),
|
||||
parameters=(
|
||||
ParameterManifest(
|
||||
id="speed", name="Speed", description="Speed", type="float", default=1.0
|
||||
),
|
||||
),
|
||||
audioFormats=(AudioFormatManifest(mime="audio/wav", extension="wav"),),
|
||||
)
|
||||
assert len(manifest.voiceSources) == 1
|
||||
assert len(manifest.parameters) == 1
|
||||
assert len(manifest.audioFormats) == 1
|
||||
|
||||
def test_defaults_empty(self) -> None:
|
||||
manifest = EngineManifest()
|
||||
assert manifest.voiceSources == ()
|
||||
assert manifest.parameters == ()
|
||||
assert manifest.audioFormats == ()
|
||||
|
||||
|
||||
class TestVoiceSourceManifestContract:
|
||||
"""Contract tests for VoiceSourceManifest."""
|
||||
|
||||
def test_required_fields(self) -> None:
|
||||
vs = VoiceSourceManifest(id="builtin", name="Builtin", type="list")
|
||||
assert vs.id == "builtin"
|
||||
assert vs.name == "Builtin"
|
||||
assert vs.type == "list"
|
||||
|
||||
def test_valid_types(self) -> None:
|
||||
valid_types = ["list", "speaker_id", "clone", "blend", "generate", "none"]
|
||||
for vtype in valid_types:
|
||||
vs = VoiceSourceManifest(id="test", name="Test", type=vtype)
|
||||
assert vs.type == vtype
|
||||
|
||||
def test_config_optional(self) -> None:
|
||||
vs = VoiceSourceManifest(id="test", name="Test", type="list")
|
||||
assert vs.config is None
|
||||
|
||||
def test_config_any(self) -> None:
|
||||
config = {"voices": ["af_nova", "af_sky"]}
|
||||
vs = VoiceSourceManifest(id="test", name="Test", type="list", config=config)
|
||||
assert vs.config == config
|
||||
|
||||
|
||||
class TestVoiceManifestContract:
|
||||
"""Contract tests for VoiceManifest."""
|
||||
|
||||
def test_required_fields(self) -> None:
|
||||
v = VoiceManifest(id="af_nova", name="Nova")
|
||||
assert v.id == "af_nova"
|
||||
assert v.name == "Nova"
|
||||
|
||||
def test_tags_default_empty(self) -> None:
|
||||
v = VoiceManifest(id="af_nova", name="Nova")
|
||||
assert v.tags == ()
|
||||
|
||||
def test_tags_tuple(self) -> None:
|
||||
v = VoiceManifest(id="af_nova", name="Nova", tags=("en", "female"))
|
||||
assert "en" in v.tags
|
||||
assert "female" in v.tags
|
||||
|
||||
|
||||
class TestParameterManifestContract:
|
||||
"""Contract tests for ParameterManifest."""
|
||||
|
||||
def test_required_fields(self) -> None:
|
||||
p = ParameterManifest(
|
||||
id="speed", name="Speed", description="Speech speed", type="float", default=1.0
|
||||
)
|
||||
assert p.id == "speed"
|
||||
assert p.name == "Speed"
|
||||
assert p.description == "Speech speed"
|
||||
assert p.type == "float"
|
||||
assert p.default == 1.0
|
||||
|
||||
def test_valid_types(self) -> None:
|
||||
valid_types = ["float", "int", "string", "boolean", "enum"]
|
||||
for ptype in valid_types:
|
||||
p = ParameterManifest(
|
||||
id="test", name="Test", description="Test", type=ptype, default=None
|
||||
)
|
||||
assert p.type == ptype
|
||||
|
||||
def test_optional_numeric_bounds(self) -> None:
|
||||
p = ParameterManifest(
|
||||
id="speed",
|
||||
name="Speed",
|
||||
description="Speed",
|
||||
type="float",
|
||||
default=1.0,
|
||||
min=0.5,
|
||||
max=2.0,
|
||||
step=0.1,
|
||||
)
|
||||
assert p.min == 0.5
|
||||
assert p.max == 2.0
|
||||
assert p.step == 0.1
|
||||
|
||||
def test_enum_options(self) -> None:
|
||||
options = (
|
||||
EnumOption(value="low", label="Low"),
|
||||
EnumOption(value="high", label="High"),
|
||||
)
|
||||
p = ParameterManifest(
|
||||
id="quality",
|
||||
name="Quality",
|
||||
description="Quality",
|
||||
type="enum",
|
||||
default="low",
|
||||
options=options,
|
||||
)
|
||||
assert len(p.options) == 2
|
||||
assert p.options[0].value == "low"
|
||||
|
||||
|
||||
class TestAudioFormatManifestContract:
|
||||
"""Contract tests for AudioFormatManifest."""
|
||||
|
||||
def test_required_fields(self) -> None:
|
||||
af = AudioFormatManifest(mime="audio/wav", extension="wav")
|
||||
assert af.mime == "audio/wav"
|
||||
assert af.extension == "wav"
|
||||
|
||||
|
||||
class TestEnumOptionContract:
|
||||
"""Contract tests for EnumOption."""
|
||||
|
||||
def test_required_fields(self) -> None:
|
||||
opt = EnumOption(value="low", label="Low Quality")
|
||||
assert opt.value == "low"
|
||||
assert opt.label == "Low Quality"
|
||||
|
||||
|
||||
class TestRequirementManifestContract:
|
||||
"""Contract tests for RequirementManifest."""
|
||||
|
||||
def test_defaults(self) -> None:
|
||||
req = RequirementManifest()
|
||||
assert req.gpu is None
|
||||
assert req.memory is None
|
||||
assert req.internet is None
|
||||
|
||||
def test_with_gpu(self) -> None:
|
||||
gpu = GpuRequirement(required=True, type="cuda", memory=8.0)
|
||||
req = RequirementManifest(gpu=gpu)
|
||||
assert req.gpu.required is True
|
||||
assert req.gpu.type == "cuda"
|
||||
assert req.gpu.memory == 8.0
|
||||
|
||||
def test_with_internet(self) -> None:
|
||||
req = RequirementManifest(internet=True)
|
||||
assert req.internet is True
|
||||
|
||||
|
||||
class TestGpuRequirementContract:
|
||||
"""Contract tests for GpuRequirement."""
|
||||
|
||||
def test_defaults(self) -> None:
|
||||
gpu = GpuRequirement()
|
||||
assert gpu.required is False
|
||||
assert gpu.type is None
|
||||
assert gpu.memory is None
|
||||
|
||||
def test_required_gpu(self) -> None:
|
||||
gpu = GpuRequirement(required=True, type="cuda", memory=8.0)
|
||||
assert gpu.required is True
|
||||
|
||||
|
||||
class TestModelManifestContract:
|
||||
"""Contract tests for ModelManifest."""
|
||||
|
||||
def test_required_fields(self) -> None:
|
||||
m = ModelManifest(id="xtts_v2", name="XTTS v2", size="2GB")
|
||||
assert m.id == "xtts_v2"
|
||||
assert m.name == "XTTS v2"
|
||||
assert m.size == "2GB"
|
||||
|
||||
Reference in New Issue
Block a user