mirror of
https://github.com/denizsafak/abogen.git
synced 2026-07-18 13:40:27 +02:00
Merge pull request #183 from denizsafak/refactor/store-supported-voices-in-backend-metadata
feat: store supported voices in TTSBackendMetadata
This commit is contained in:
@@ -18,11 +18,13 @@ class TTSBackendMetadata:
|
|||||||
id: Unique backend identifier (e.g. ``"kokoro"``, ``"supertonic"``).
|
id: Unique backend identifier (e.g. ``"kokoro"``, ``"supertonic"``).
|
||||||
name: Human-readable display name.
|
name: Human-readable display name.
|
||||||
description: Short description of the backend.
|
description: Short description of the backend.
|
||||||
|
voices: Tuple of supported voice identifiers.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
id: str
|
id: str
|
||||||
name: str
|
name: str
|
||||||
description: str
|
description: str
|
||||||
|
voices: tuple[str, ...] = ()
|
||||||
|
|
||||||
|
|
||||||
class TTSBackend(Protocol):
|
class TTSBackend(Protocol):
|
||||||
|
|||||||
@@ -10,6 +10,16 @@ from typing import Any, Dict, Iterator, List, Optional
|
|||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
|
from abogen.constants import VOICES_INTERNAL
|
||||||
|
from abogen.tts_backend import TTSBackendMetadata
|
||||||
|
|
||||||
|
_KOKORO_METADATA = TTSBackendMetadata(
|
||||||
|
id="kokoro",
|
||||||
|
name="Kokoro",
|
||||||
|
description="Kokoro TTS engine",
|
||||||
|
voices=tuple(VOICES_INTERNAL),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _load_kpipeline():
|
def _load_kpipeline():
|
||||||
"""Lazy-load Kokoro dependencies."""
|
"""Lazy-load Kokoro dependencies."""
|
||||||
@@ -39,14 +49,8 @@ class KokoroBackend:
|
|||||||
self._lang_code = lang_code
|
self._lang_code = lang_code
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def metadata(self):
|
def metadata(self) -> TTSBackendMetadata:
|
||||||
from abogen.tts_backend import TTSBackendMetadata
|
return _KOKORO_METADATA
|
||||||
|
|
||||||
return TTSBackendMetadata(
|
|
||||||
id="kokoro",
|
|
||||||
name="Kokoro",
|
|
||||||
description="Kokoro TTS engine",
|
|
||||||
)
|
|
||||||
|
|
||||||
def __call__(
|
def __call__(
|
||||||
self,
|
self,
|
||||||
@@ -89,9 +93,7 @@ class KokoroBackend:
|
|||||||
|
|
||||||
def get_available_voices(self) -> List[str]:
|
def get_available_voices(self) -> List[str]:
|
||||||
"""Return known Kokoro voice identifiers."""
|
"""Return known Kokoro voice identifiers."""
|
||||||
from abogen.constants import VOICES_INTERNAL
|
return list(self.metadata.voices)
|
||||||
|
|
||||||
return list(VOICES_INTERNAL)
|
|
||||||
|
|
||||||
def get_supported_formats(self) -> List[str]:
|
def get_supported_formats(self) -> List[str]:
|
||||||
"""Kokoro outputs raw PCM float32 audio."""
|
"""Kokoro outputs raw PCM float32 audio."""
|
||||||
@@ -111,14 +113,9 @@ def create_kokoro_backend(**kwargs: Any) -> KokoroBackend:
|
|||||||
|
|
||||||
|
|
||||||
# --- Registration ---
|
# --- Registration ---
|
||||||
from abogen.tts_backend import TTSBackendMetadata # noqa: E402
|
|
||||||
from abogen.tts_backend_registry import register_backend # noqa: E402
|
from abogen.tts_backend_registry import register_backend # noqa: E402
|
||||||
|
|
||||||
register_backend(
|
register_backend(
|
||||||
metadata=TTSBackendMetadata(
|
metadata=_KOKORO_METADATA,
|
||||||
id="kokoro",
|
|
||||||
name="Kokoro",
|
|
||||||
description="Kokoro TTS engine",
|
|
||||||
),
|
|
||||||
factory=create_kokoro_backend,
|
factory=create_kokoro_backend,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -15,6 +15,15 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
DEFAULT_SUPERTONIC_VOICES = ("M1", "M2", "M3", "M4", "M5", "F1", "F2", "F3", "F4", "F5")
|
DEFAULT_SUPERTONIC_VOICES = ("M1", "M2", "M3", "M4", "M5", "F1", "F2", "F3", "F4", "F5")
|
||||||
|
|
||||||
|
from abogen.tts_backend import TTSBackendMetadata
|
||||||
|
|
||||||
|
_SUPERTONIC_METADATA = TTSBackendMetadata(
|
||||||
|
id="supertonic",
|
||||||
|
name="SuperTonic",
|
||||||
|
description="SuperTonic TTS engine",
|
||||||
|
voices=DEFAULT_SUPERTONIC_VOICES,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class SupertonicSegment:
|
class SupertonicSegment:
|
||||||
@@ -282,12 +291,8 @@ class SupertonicBackend:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def metadata(self) -> "TTSBackendMetadata":
|
def metadata(self) -> TTSBackendMetadata:
|
||||||
return TTSBackendMetadata(
|
return _SUPERTONIC_METADATA
|
||||||
id="supertonic",
|
|
||||||
name="SuperTonic",
|
|
||||||
description="SuperTonic TTS engine",
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(self, **kwargs: Any) -> None:
|
def __init__(self, **kwargs: Any) -> None:
|
||||||
self._pipeline = SupertonicPipeline(
|
self._pipeline = SupertonicPipeline(
|
||||||
@@ -333,7 +338,7 @@ class SupertonicBackend:
|
|||||||
|
|
||||||
def get_available_voices(self) -> List[str]:
|
def get_available_voices(self) -> List[str]:
|
||||||
"""Return the list of built-in SuperTonic voice identifiers."""
|
"""Return the list of built-in SuperTonic voice identifiers."""
|
||||||
return list(DEFAULT_SUPERTONIC_VOICES)
|
return list(self.metadata.voices)
|
||||||
|
|
||||||
def get_supported_formats(self) -> List[str]:
|
def get_supported_formats(self) -> List[str]:
|
||||||
return ["wav"]
|
return ["wav"]
|
||||||
@@ -379,14 +384,9 @@ def create_supertonic_backend(**kwargs: Any) -> SupertonicBackend:
|
|||||||
return SupertonicBackend(**kwargs)
|
return SupertonicBackend(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
from abogen.tts_backend import TTSBackendMetadata
|
from abogen.tts_backend_registry import register_backend # noqa: E402
|
||||||
from abogen.tts_backend_registry import register_backend
|
|
||||||
|
|
||||||
register_backend(
|
register_backend(
|
||||||
metadata=TTSBackendMetadata(
|
metadata=_SUPERTONIC_METADATA,
|
||||||
id="supertonic",
|
|
||||||
name="SuperTonic",
|
|
||||||
description="SuperTonic TTS engine",
|
|
||||||
),
|
|
||||||
factory=create_supertonic_backend,
|
factory=create_supertonic_backend,
|
||||||
)
|
)
|
||||||
|
|||||||
+204
-149
@@ -1,149 +1,204 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
from abogen.tts_backend import TTSBackendMetadata
|
from abogen.tts_backend import TTSBackendMetadata
|
||||||
from abogen.tts_backend_registry import TTSBackendRegistry
|
from abogen.tts_backend_registry import TTSBackendRegistry
|
||||||
|
|
||||||
|
|
||||||
class TestTTSBackendMetadata:
|
class TestTTSBackendMetadata:
|
||||||
def test_is_frozen_dataclass(self):
|
def test_is_frozen_dataclass(self):
|
||||||
assert dataclass(TTSBackendMetadata)
|
assert dataclass(TTSBackendMetadata)
|
||||||
|
|
||||||
def test_fields_are_present(self):
|
def test_fields_are_present(self):
|
||||||
meta = TTSBackendMetadata(
|
meta = TTSBackendMetadata(
|
||||||
id="test",
|
id="test",
|
||||||
name="Test Backend",
|
name="Test Backend",
|
||||||
description="A test backend",
|
description="A test backend",
|
||||||
)
|
)
|
||||||
assert meta.id == "test"
|
assert meta.id == "test"
|
||||||
assert meta.name == "Test Backend"
|
assert meta.name == "Test Backend"
|
||||||
assert meta.description == "A test backend"
|
assert meta.description == "A test backend"
|
||||||
|
|
||||||
def test_is_immutable(self):
|
def test_voices_field_default_empty(self):
|
||||||
import pytest
|
meta = TTSBackendMetadata(
|
||||||
|
id="test",
|
||||||
meta = TTSBackendMetadata(
|
name="Test",
|
||||||
id="kokoro",
|
description="Test backend",
|
||||||
name="Kokoro",
|
)
|
||||||
description="Test",
|
assert meta.voices == ()
|
||||||
)
|
|
||||||
with pytest.raises(Exception):
|
def test_voices_field_stored(self):
|
||||||
meta.id = "changed"
|
meta = TTSBackendMetadata(
|
||||||
|
id="test",
|
||||||
|
name="Test",
|
||||||
class TestTTSBackendRegistry:
|
description="Test backend",
|
||||||
def test_register_and_list(self):
|
voices=("v1", "v2"),
|
||||||
registry = TTSBackendRegistry()
|
)
|
||||||
meta = TTSBackendMetadata(id="a", name="A", description="Backend A")
|
assert meta.voices == ("v1", "v2")
|
||||||
registry.register(metadata=meta, factory=lambda: None)
|
|
||||||
|
def test_is_immutable(self):
|
||||||
backends = registry.list_backends()
|
import pytest
|
||||||
assert len(backends) == 1
|
|
||||||
assert backends[0].id == "a"
|
meta = TTSBackendMetadata(
|
||||||
|
id="kokoro",
|
||||||
def test_list_multiple(self):
|
name="Kokoro",
|
||||||
registry = TTSBackendRegistry()
|
description="Test",
|
||||||
meta_a = TTSBackendMetadata(id="a", name="A", description="A")
|
)
|
||||||
meta_b = TTSBackendMetadata(id="b", name="B", description="B")
|
with pytest.raises(Exception):
|
||||||
registry.register(metadata=meta_a, factory=lambda: None)
|
meta.id = "changed"
|
||||||
registry.register(metadata=meta_b, factory=lambda: None)
|
|
||||||
|
|
||||||
backends = registry.list_backends()
|
class TestTTSBackendRegistry:
|
||||||
ids = [b.id for b in backends]
|
def test_register_and_list(self):
|
||||||
assert "a" in ids
|
registry = TTSBackendRegistry()
|
||||||
assert "b" in ids
|
meta = TTSBackendMetadata(id="a", name="A", description="Backend A")
|
||||||
|
registry.register(metadata=meta, factory=lambda: None)
|
||||||
def test_get_metadata(self):
|
|
||||||
registry = TTSBackendRegistry()
|
backends = registry.list_backends()
|
||||||
meta = TTSBackendMetadata(id="x", name="X", description="X backend")
|
assert len(backends) == 1
|
||||||
registry.register(metadata=meta, factory=lambda: None)
|
assert backends[0].id == "a"
|
||||||
|
|
||||||
result = registry.get_metadata("x")
|
def test_list_multiple(self):
|
||||||
assert result.id == "x"
|
registry = TTSBackendRegistry()
|
||||||
assert result.name == "X"
|
meta_a = TTSBackendMetadata(id="a", name="A", description="A")
|
||||||
|
meta_b = TTSBackendMetadata(id="b", name="B", description="B")
|
||||||
def test_get_metadata_unknown_raises(self):
|
registry.register(metadata=meta_a, factory=lambda: None)
|
||||||
import pytest
|
registry.register(metadata=meta_b, factory=lambda: None)
|
||||||
|
|
||||||
registry = TTSBackendRegistry()
|
backends = registry.list_backends()
|
||||||
with pytest.raises(KeyError, match="Unknown backend: nope"):
|
ids = [b.id for b in backends]
|
||||||
registry.get_metadata("nope")
|
assert "a" in ids
|
||||||
|
assert "b" in ids
|
||||||
def test_create_backend(self):
|
|
||||||
registry = TTSBackendRegistry()
|
def test_get_metadata(self):
|
||||||
meta = TTSBackendMetadata(id="test", name="Test", description="Test backend")
|
registry = TTSBackendRegistry()
|
||||||
|
meta = TTSBackendMetadata(id="x", name="X", description="X backend")
|
||||||
def factory(**kwargs):
|
registry.register(metadata=meta, factory=lambda: None)
|
||||||
return {"created": True, "kwargs": kwargs}
|
|
||||||
|
result = registry.get_metadata("x")
|
||||||
registry.register(metadata=meta, factory=factory)
|
assert result.id == "x"
|
||||||
result = registry.create_backend("test", foo="bar")
|
assert result.name == "X"
|
||||||
|
|
||||||
assert result == {"created": True, "kwargs": {"foo": "bar"}}
|
def test_get_metadata_unknown_raises(self):
|
||||||
|
import pytest
|
||||||
def test_create_backend_unknown_raises(self):
|
|
||||||
import pytest
|
registry = TTSBackendRegistry()
|
||||||
|
with pytest.raises(KeyError, match="Unknown backend: nope"):
|
||||||
registry = TTSBackendRegistry()
|
registry.get_metadata("nope")
|
||||||
with pytest.raises(KeyError, match="Unknown backend: missing"):
|
|
||||||
registry.create_backend("missing")
|
def test_create_backend(self):
|
||||||
|
registry = TTSBackendRegistry()
|
||||||
def test_register_overwrites(self):
|
meta = TTSBackendMetadata(id="test", name="Test", description="Test backend")
|
||||||
registry = TTSBackendRegistry()
|
|
||||||
meta1 = TTSBackendMetadata(id="x", name="V1", description="First")
|
def factory(**kwargs):
|
||||||
meta2 = TTSBackendMetadata(id="x", name="V2", description="Second")
|
return {"created": True, "kwargs": kwargs}
|
||||||
registry.register(metadata=meta1, factory=lambda: "v1")
|
|
||||||
registry.register(metadata=meta2, factory=lambda: "v2")
|
registry.register(metadata=meta, factory=factory)
|
||||||
|
result = registry.create_backend("test", foo="bar")
|
||||||
result = registry.get_metadata("x")
|
|
||||||
assert result.name == "V2"
|
assert result == {"created": True, "kwargs": {"foo": "bar"}}
|
||||||
assert registry.create_backend("x") == "v2"
|
|
||||||
|
def test_create_backend_unknown_raises(self):
|
||||||
|
import pytest
|
||||||
class TestBackendRegistration:
|
|
||||||
"""Tests that existing backends are auto-registered."""
|
registry = TTSBackendRegistry()
|
||||||
|
with pytest.raises(KeyError, match="Unknown backend: missing"):
|
||||||
def test_import_triggers_registration(self):
|
registry.create_backend("missing")
|
||||||
import abogen.tts_backends # noqa: F401
|
|
||||||
|
def test_register_overwrites(self):
|
||||||
from abogen.tts_backend_registry import _registry
|
registry = TTSBackendRegistry()
|
||||||
|
meta1 = TTSBackendMetadata(id="x", name="V1", description="First")
|
||||||
backends = _registry.list_backends()
|
meta2 = TTSBackendMetadata(id="x", name="V2", description="Second")
|
||||||
ids = [b.id for b in backends]
|
registry.register(metadata=meta1, factory=lambda: "v1")
|
||||||
assert "kokoro" in ids
|
registry.register(metadata=meta2, factory=lambda: "v2")
|
||||||
assert "supertonic" in ids
|
|
||||||
|
result = registry.get_metadata("x")
|
||||||
def test_kokoro_metadata(self):
|
assert result.name == "V2"
|
||||||
import abogen.tts_backends # noqa: F401
|
assert registry.create_backend("x") == "v2"
|
||||||
|
|
||||||
from abogen.tts_backend_registry import _registry
|
|
||||||
|
class TestBackendRegistration:
|
||||||
meta = _registry.get_metadata("kokoro")
|
"""Tests that existing backends are auto-registered."""
|
||||||
assert meta.id == "kokoro"
|
|
||||||
assert meta.name == "Kokoro"
|
def test_import_triggers_registration(self):
|
||||||
assert "Kokoro" in meta.description
|
import abogen.tts_backends # noqa: F401
|
||||||
|
|
||||||
def test_supertonic_metadata(self):
|
from abogen.tts_backend_registry import _registry
|
||||||
import abogen.tts_backends # noqa: F401
|
|
||||||
|
backends = _registry.list_backends()
|
||||||
from abogen.tts_backend_registry import _registry
|
ids = [b.id for b in backends]
|
||||||
|
assert "kokoro" in ids
|
||||||
meta = _registry.get_metadata("supertonic")
|
assert "supertonic" in ids
|
||||||
assert meta.id == "supertonic"
|
|
||||||
assert meta.name == "SuperTonic"
|
def test_kokoro_metadata(self):
|
||||||
assert "SuperTonic" in meta.description
|
import abogen.tts_backends # noqa: F401
|
||||||
|
|
||||||
def test_kokoro_factory_callable(self):
|
from abogen.tts_backend_registry import _registry
|
||||||
import abogen.tts_backends # noqa: F401
|
|
||||||
|
meta = _registry.get_metadata("kokoro")
|
||||||
from abogen.tts_backend_registry import _registry
|
assert meta.id == "kokoro"
|
||||||
|
assert meta.name == "Kokoro"
|
||||||
factory = _registry._factories["kokoro"]
|
assert "Kokoro" in meta.description
|
||||||
assert callable(factory)
|
|
||||||
|
def test_supertonic_metadata(self):
|
||||||
def test_supertonic_factory_callable(self):
|
import abogen.tts_backends # noqa: F401
|
||||||
import abogen.tts_backends # noqa: F401
|
|
||||||
|
from abogen.tts_backend_registry import _registry
|
||||||
from abogen.tts_backend_registry import _registry
|
|
||||||
|
meta = _registry.get_metadata("supertonic")
|
||||||
factory = _registry._factories["supertonic"]
|
assert meta.id == "supertonic"
|
||||||
assert callable(factory)
|
assert meta.name == "SuperTonic"
|
||||||
|
assert "SuperTonic" in meta.description
|
||||||
|
|
||||||
|
def test_kokoro_metadata_has_voices(self):
|
||||||
|
import abogen.tts_backends # noqa: F401
|
||||||
|
|
||||||
|
from abogen.tts_backend_registry import _registry
|
||||||
|
|
||||||
|
meta = _registry.get_metadata("kokoro")
|
||||||
|
assert isinstance(meta.voices, tuple)
|
||||||
|
assert len(meta.voices) > 0
|
||||||
|
assert all(isinstance(v, str) for v in meta.voices)
|
||||||
|
|
||||||
|
def test_supertonic_metadata_has_voices(self):
|
||||||
|
import abogen.tts_backends # noqa: F401
|
||||||
|
|
||||||
|
from abogen.tts_backend_registry import _registry
|
||||||
|
|
||||||
|
meta = _registry.get_metadata("supertonic")
|
||||||
|
assert isinstance(meta.voices, tuple)
|
||||||
|
assert len(meta.voices) == 10
|
||||||
|
assert meta.voices == ("M1", "M2", "M3", "M4", "M5", "F1", "F2", "F3", "F4", "F5")
|
||||||
|
|
||||||
|
def test_kokoro_factory_callable(self):
|
||||||
|
import abogen.tts_backends # noqa: F401
|
||||||
|
|
||||||
|
from abogen.tts_backend_registry import _registry
|
||||||
|
|
||||||
|
factory = _registry._factories["kokoro"]
|
||||||
|
assert callable(factory)
|
||||||
|
|
||||||
|
def test_supertonic_factory_callable(self):
|
||||||
|
import abogen.tts_backends # noqa: F401
|
||||||
|
|
||||||
|
from abogen.tts_backend_registry import _registry
|
||||||
|
|
||||||
|
factory = _registry._factories["supertonic"]
|
||||||
|
assert callable(factory)
|
||||||
|
|
||||||
|
def test_kokoro_metadata_voices_match_registry(self):
|
||||||
|
"""Ensure the metadata property on the instance shares voices with registry."""
|
||||||
|
from abogen.tts_backends.kokoro import _KOKORO_METADATA
|
||||||
|
from abogen.tts_backend_registry import _registry
|
||||||
|
|
||||||
|
registry_meta = _registry.get_metadata("kokoro")
|
||||||
|
assert _KOKORO_METADATA is registry_meta
|
||||||
|
assert _KOKORO_METADATA.voices == registry_meta.voices
|
||||||
|
|
||||||
|
def test_supertonic_metadata_voices_match_registry(self):
|
||||||
|
"""Ensure the metadata property on the instance shares voices with registry."""
|
||||||
|
from abogen.tts_backends.supertonic import _SUPERTONIC_METADATA
|
||||||
|
from abogen.tts_backend_registry import _registry
|
||||||
|
|
||||||
|
registry_meta = _registry.get_metadata("supertonic")
|
||||||
|
assert _SUPERTONIC_METADATA is registry_meta
|
||||||
|
assert _SUPERTONIC_METADATA.voices == registry_meta.voices
|
||||||
|
|||||||
Reference in New Issue
Block a user