mirror of
https://github.com/denizsafak/abogen.git
synced 2026-07-18 13:40:27 +02:00
refactor(cleanup): remove Legacy TTS Architecture
Delete legacy backend infrastructure: - abogen/tts_backend.py (TTSBackend protocol, TTSBackendMetadata) - abogen/tts_backend_registry.py (TTSBackendRegistry, global singleton, register_backend) - abogen/tts_backends/ (kokoro.py, supertonic.py, __init__.py) Delete legacy tests: - tests/test_tts_backend.py - tests/test_kokoro_backend.py - tests/test_voice_formula_resolution.py - tests/test_tts_supertonic_unsupported_chars.py Production code now uses only Plugin Architecture via create_pipeline(). All contract, behavioral, and integration tests pass. 2 pre-existing failures in test_supertonic_plugin.py (mock engine mismatch).
This commit is contained in:
@@ -1,216 +0,0 @@
|
||||
"""Tests for KokoroBackend class."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Iterator, List
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from abogen.tts_backend import TTSBackendMetadata
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@dataclass
|
||||
class _FakeSegment:
|
||||
graphemes: str
|
||||
audio: Any # np.ndarray or torch-like tensor
|
||||
|
||||
|
||||
class _FakePipeline:
|
||||
"""Minimal mock for kokoro.KPipeline."""
|
||||
|
||||
def __init__(self, *, lang_code: str, repo_id: str, device: str):
|
||||
self.lang_code = lang_code
|
||||
self.repo_id = repo_id
|
||||
self.device = device
|
||||
self._voices: dict[str, np.ndarray] = {}
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
text: str,
|
||||
*,
|
||||
voice: Any = "",
|
||||
speed: float = 1.0,
|
||||
split_pattern: str | None = None,
|
||||
) -> Iterator[_FakeSegment]:
|
||||
yield _FakeSegment(graphemes=text, audio=np.zeros(100, dtype="float32"))
|
||||
|
||||
def load_single_voice(self, name: str) -> np.ndarray:
|
||||
if name not in self._voices:
|
||||
self._voices[name] = np.ones((1, 256), dtype="float32") * 0.5
|
||||
return self._voices[name]
|
||||
|
||||
|
||||
def _make_backend(**kwargs: Any):
|
||||
"""Create KokoroBackend with mocked KPipeline."""
|
||||
with patch("abogen.tts_backends.kokoro._load_kpipeline") as load:
|
||||
load.return_value = _FakePipeline
|
||||
from abogen.tts_backends.kokoro import KokoroBackend
|
||||
|
||||
return KokoroBackend(**kwargs)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestKokoroBackendMetadata:
|
||||
def test_metadata_returns_tts_backend_metadata(self):
|
||||
backend = _make_backend(lang_code="a")
|
||||
meta = backend.metadata
|
||||
assert isinstance(meta, TTSBackendMetadata)
|
||||
|
||||
def test_metadata_fields(self):
|
||||
backend = _make_backend(lang_code="a")
|
||||
meta = backend.metadata
|
||||
assert meta.id == "kokoro"
|
||||
assert meta.name == "Kokoro"
|
||||
assert "Kokoro" in meta.description
|
||||
|
||||
|
||||
class TestKokoroBackendInit:
|
||||
def test_stores_lang_code(self):
|
||||
backend = _make_backend(lang_code="b")
|
||||
assert backend._lang_code == "b"
|
||||
|
||||
def test_default_repo_id(self):
|
||||
with patch("abogen.tts_backends.kokoro._load_kpipeline") as load:
|
||||
load.return_value = _FakePipeline
|
||||
from abogen.tts_backends.kokoro import KokoroBackend
|
||||
|
||||
b = KokoroBackend(lang_code="a")
|
||||
assert b._pipeline.repo_id == "hexgrad/Kokoro-82M"
|
||||
|
||||
def test_custom_repo_id(self):
|
||||
backend = _make_backend(lang_code="a", repo_id="custom/repo")
|
||||
assert backend._pipeline.repo_id == "custom/repo"
|
||||
|
||||
def test_default_device(self):
|
||||
backend = _make_backend(lang_code="a")
|
||||
assert backend._pipeline.device == "cpu"
|
||||
|
||||
def test_custom_device(self):
|
||||
backend = _make_backend(lang_code="a", device="cuda")
|
||||
assert backend._pipeline.device == "cuda"
|
||||
|
||||
|
||||
class TestKokoroBackendCall:
|
||||
def test_call_delegates_to_pipeline(self):
|
||||
backend = _make_backend(lang_code="a")
|
||||
results = list(backend("hello", voice="af_heart", speed=1.2, split_pattern=r"\n"))
|
||||
assert len(results) == 1
|
||||
assert results[0].graphemes == "hello"
|
||||
|
||||
def test_call_returns_iterator(self):
|
||||
backend = _make_backend(lang_code="a")
|
||||
result = backend("test", voice="af_heart")
|
||||
assert hasattr(result, "__iter__")
|
||||
|
||||
def test_call_with_voice_tensor(self):
|
||||
backend = _make_backend(lang_code="a")
|
||||
voice_tensor = np.ones((1, 256), dtype="float32")
|
||||
results = list(backend("test", voice=voice_tensor))
|
||||
assert len(results) == 1
|
||||
|
||||
def test_call_default_speed(self):
|
||||
backend = _make_backend(lang_code="a")
|
||||
# Should not raise with default speed
|
||||
list(backend("text", voice="af_heart"))
|
||||
|
||||
def test_call_default_split_pattern_is_none(self):
|
||||
backend = _make_backend(lang_code="a")
|
||||
# split_pattern defaults to None
|
||||
list(backend("text", voice="af_heart"))
|
||||
|
||||
|
||||
class TestLoadSingleVoice:
|
||||
def test_load_single_voice_delegates(self):
|
||||
backend = _make_backend(lang_code="a")
|
||||
tensor = backend.load_single_voice("af_heart")
|
||||
assert isinstance(tensor, np.ndarray)
|
||||
assert tensor.shape == (1, 256)
|
||||
|
||||
def test_load_single_voice_caches(self):
|
||||
backend = _make_backend(lang_code="a")
|
||||
t1 = backend.load_single_voice("af_heart")
|
||||
t2 = backend.load_single_voice("af_heart")
|
||||
assert t1 is t2 # same object
|
||||
|
||||
|
||||
class TestSynthesize:
|
||||
def test_synthesize_returns_bytes(self):
|
||||
backend = _make_backend(lang_code="a")
|
||||
result = backend.synthesize("hello", voice="af_heart")
|
||||
assert isinstance(result, bytes)
|
||||
|
||||
def test_synthesize_nonempty(self):
|
||||
backend = _make_backend(lang_code="a")
|
||||
result = backend.synthesize("hello", voice="af_heart")
|
||||
assert len(result) > 0
|
||||
|
||||
def test_synthesize_with_speed(self):
|
||||
backend = _make_backend(lang_code="a")
|
||||
result = backend.synthesize("hello", voice="af_heart", speed=1.5)
|
||||
assert isinstance(result, bytes)
|
||||
|
||||
def test_synthesize_empty_text(self):
|
||||
backend = _make_backend(lang_code="a")
|
||||
# Empty text produces no segments
|
||||
result = backend.synthesize("", voice="af_heart")
|
||||
assert isinstance(result, bytes)
|
||||
|
||||
|
||||
class TestProtocolMethods:
|
||||
def test_get_available_voices(self):
|
||||
backend = _make_backend(lang_code="a")
|
||||
voices = backend.get_available_voices()
|
||||
assert isinstance(voices, list)
|
||||
assert len(voices) > 0
|
||||
assert all(isinstance(v, str) for v in voices)
|
||||
|
||||
def test_get_supported_formats(self):
|
||||
backend = _make_backend(lang_code="a")
|
||||
formats = backend.get_supported_formats()
|
||||
assert "pcm_float32" in formats
|
||||
|
||||
def test_get_info(self):
|
||||
backend = _make_backend(lang_code="a")
|
||||
info = backend.get_info()
|
||||
assert info["id"] == "kokoro"
|
||||
assert info["name"] == "Kokoro"
|
||||
assert info["lang_code"] == "a"
|
||||
|
||||
|
||||
class TestRegistration:
|
||||
def test_factory_creates_kokoro_backend(self):
|
||||
from abogen.tts_backends.kokoro import create_kokoro_backend, KokoroBackend
|
||||
|
||||
with patch("abogen.tts_backends.kokoro._load_kpipeline") as load:
|
||||
load.return_value = _FakePipeline
|
||||
backend = create_kokoro_backend(lang_code="a")
|
||||
assert isinstance(backend, KokoroBackend)
|
||||
|
||||
def test_registry_has_kokoro(self):
|
||||
import abogen.tts_backends # noqa: F401
|
||||
from abogen.tts_backend_registry import _registry
|
||||
|
||||
meta = _registry.get_metadata("kokoro")
|
||||
assert meta.id == "kokoro"
|
||||
assert meta.name == "Kokoro"
|
||||
|
||||
def test_registry_factory_returns_kokoro_backend(self):
|
||||
import abogen.tts_backends # noqa: F401
|
||||
from abogen.tts_backend_registry import _registry
|
||||
from abogen.tts_backends.kokoro import KokoroBackend
|
||||
|
||||
factory = _registry._factories["kokoro"]
|
||||
with patch("abogen.tts_backends.kokoro._load_kpipeline") as load:
|
||||
load.return_value = _FakePipeline
|
||||
backend = factory(lang_code="a")
|
||||
assert isinstance(backend, KokoroBackend)
|
||||
@@ -1,314 +0,0 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
from abogen.tts_backend import TTSBackendMetadata
|
||||
from abogen.tts_backend_registry import TTSBackendRegistry
|
||||
|
||||
|
||||
class TestTTSBackendMetadata:
|
||||
def test_is_frozen_dataclass(self):
|
||||
assert dataclass(TTSBackendMetadata)
|
||||
|
||||
def test_fields_are_present(self):
|
||||
meta = TTSBackendMetadata(
|
||||
id="test",
|
||||
name="Test Backend",
|
||||
description="A test backend",
|
||||
)
|
||||
assert meta.id == "test"
|
||||
assert meta.name == "Test Backend"
|
||||
assert meta.description == "A test backend"
|
||||
|
||||
def test_voices_field_default_empty(self):
|
||||
meta = TTSBackendMetadata(
|
||||
id="test",
|
||||
name="Test",
|
||||
description="Test backend",
|
||||
)
|
||||
assert meta.voices == ()
|
||||
|
||||
def test_voices_field_stored(self):
|
||||
meta = TTSBackendMetadata(
|
||||
id="test",
|
||||
name="Test",
|
||||
description="Test backend",
|
||||
voices=("v1", "v2"),
|
||||
)
|
||||
assert meta.voices == ("v1", "v2")
|
||||
|
||||
def test_is_immutable(self):
|
||||
import pytest
|
||||
|
||||
meta = TTSBackendMetadata(
|
||||
id="kokoro",
|
||||
name="Kokoro",
|
||||
description="Test",
|
||||
)
|
||||
with pytest.raises(Exception):
|
||||
meta.id = "changed"
|
||||
|
||||
|
||||
class TestTTSBackendRegistry:
|
||||
def test_register_and_list(self):
|
||||
registry = TTSBackendRegistry()
|
||||
meta = TTSBackendMetadata(id="a", name="A", description="Backend A")
|
||||
registry.register(metadata=meta, factory=lambda: None)
|
||||
|
||||
backends = registry.list_backends()
|
||||
assert len(backends) == 1
|
||||
assert backends[0].id == "a"
|
||||
|
||||
def test_list_multiple(self):
|
||||
registry = TTSBackendRegistry()
|
||||
meta_a = TTSBackendMetadata(id="a", name="A", description="A")
|
||||
meta_b = TTSBackendMetadata(id="b", name="B", description="B")
|
||||
registry.register(metadata=meta_a, factory=lambda: None)
|
||||
registry.register(metadata=meta_b, factory=lambda: None)
|
||||
|
||||
backends = registry.list_backends()
|
||||
ids = [b.id for b in backends]
|
||||
assert "a" in ids
|
||||
assert "b" in ids
|
||||
|
||||
def test_get_metadata(self):
|
||||
registry = TTSBackendRegistry()
|
||||
meta = TTSBackendMetadata(id="x", name="X", description="X backend")
|
||||
registry.register(metadata=meta, factory=lambda: None)
|
||||
|
||||
result = registry.get_metadata("x")
|
||||
assert result.id == "x"
|
||||
assert result.name == "X"
|
||||
|
||||
def test_get_metadata_unknown_raises(self):
|
||||
import pytest
|
||||
|
||||
registry = TTSBackendRegistry()
|
||||
with pytest.raises(KeyError, match="Unknown backend: nope"):
|
||||
registry.get_metadata("nope")
|
||||
|
||||
def test_create_backend(self):
|
||||
registry = TTSBackendRegistry()
|
||||
meta = TTSBackendMetadata(id="test", name="Test", description="Test backend")
|
||||
|
||||
def factory(**kwargs):
|
||||
return {"created": True, "kwargs": kwargs}
|
||||
|
||||
registry.register(metadata=meta, factory=factory)
|
||||
result = registry.create_backend("test", foo="bar")
|
||||
|
||||
assert result == {"created": True, "kwargs": {"foo": "bar"}}
|
||||
|
||||
def test_create_backend_unknown_raises(self):
|
||||
import pytest
|
||||
|
||||
registry = TTSBackendRegistry()
|
||||
with pytest.raises(KeyError, match="Unknown backend: missing"):
|
||||
registry.create_backend("missing")
|
||||
|
||||
def test_register_overwrites(self):
|
||||
registry = TTSBackendRegistry()
|
||||
meta1 = TTSBackendMetadata(id="x", name="V1", description="First")
|
||||
meta2 = TTSBackendMetadata(id="x", name="V2", description="Second")
|
||||
registry.register(metadata=meta1, factory=lambda: "v1")
|
||||
registry.register(metadata=meta2, factory=lambda: "v2")
|
||||
|
||||
result = registry.get_metadata("x")
|
||||
assert result.name == "V2"
|
||||
assert registry.create_backend("x") == "v2"
|
||||
|
||||
|
||||
class TestBackendRegistration:
|
||||
"""Tests that existing backends are auto-registered."""
|
||||
|
||||
def test_import_triggers_registration(self):
|
||||
import abogen.tts_backends # noqa: F401
|
||||
|
||||
from abogen.tts_backend_registry import _registry
|
||||
|
||||
backends = _registry.list_backends()
|
||||
ids = [b.id for b in backends]
|
||||
assert "kokoro" in ids
|
||||
assert "supertonic" in ids
|
||||
|
||||
def test_kokoro_metadata(self):
|
||||
import abogen.tts_backends # noqa: F401
|
||||
|
||||
from abogen.tts_backend_registry import _registry
|
||||
|
||||
meta = _registry.get_metadata("kokoro")
|
||||
assert meta.id == "kokoro"
|
||||
assert meta.name == "Kokoro"
|
||||
assert "Kokoro" in meta.description
|
||||
|
||||
def test_supertonic_metadata(self):
|
||||
import abogen.tts_backends # noqa: F401
|
||||
|
||||
from abogen.tts_backend_registry import _registry
|
||||
|
||||
meta = _registry.get_metadata("supertonic")
|
||||
assert meta.id == "supertonic"
|
||||
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
|
||||
|
||||
|
||||
class TestResolveBackendForVoice:
|
||||
"""Tests for the resolve_backend_for_voice method."""
|
||||
|
||||
def test_empty_spec_returns_fallback(self):
|
||||
registry = TTSBackendRegistry()
|
||||
assert registry.resolve_backend_for_voice("", fallback="kokoro") == "kokoro"
|
||||
assert registry.resolve_backend_for_voice("", fallback="supertonic") == "supertonic"
|
||||
|
||||
def test_none_spec_returns_fallback(self):
|
||||
registry = TTSBackendRegistry()
|
||||
assert registry.resolve_backend_for_voice(None, fallback="kokoro") == "kokoro"
|
||||
|
||||
def test_kokoro_formula_with_star_returns_kokoro(self):
|
||||
registry = TTSBackendRegistry()
|
||||
assert registry.resolve_backend_for_voice("af_nova*0.7") == "kokoro"
|
||||
|
||||
def test_kokoro_formula_with_plus_returns_kokoro(self):
|
||||
registry = TTSBackendRegistry()
|
||||
assert registry.resolve_backend_for_voice("af_nova*0.7+am_liam*0.3") == "kokoro"
|
||||
|
||||
def test_kokoro_voice_id_resolves_to_kokoro(self):
|
||||
registry = TTSBackendRegistry()
|
||||
meta = TTSBackendMetadata(
|
||||
id="kokoro",
|
||||
name="Kokoro",
|
||||
description="Kokoro TTS",
|
||||
voices=("af_nova", "am_liam"),
|
||||
)
|
||||
registry.register(metadata=meta, factory=lambda: None)
|
||||
|
||||
assert registry.resolve_backend_for_voice("af_nova") == "kokoro"
|
||||
assert registry.resolve_backend_for_voice("am_liam") == "kokoro"
|
||||
|
||||
def test_supertonic_voice_id_resolves_to_supertonic(self):
|
||||
registry = TTSBackendRegistry()
|
||||
meta = TTSBackendMetadata(
|
||||
id="supertonic",
|
||||
name="SuperTonic",
|
||||
description="SuperTonic TTS",
|
||||
voices=("M1", "M2", "F1", "F2"),
|
||||
)
|
||||
registry.register(metadata=meta, factory=lambda: None)
|
||||
|
||||
assert registry.resolve_backend_for_voice("M1") == "supertonic"
|
||||
assert registry.resolve_backend_for_voice("F2") == "supertonic"
|
||||
|
||||
def test_unknown_voice_returns_fallback(self):
|
||||
registry = TTSBackendRegistry()
|
||||
meta = TTSBackendMetadata(
|
||||
id="kokoro",
|
||||
name="Kokoro",
|
||||
description="Kokoro TTS",
|
||||
voices=("af_nova",),
|
||||
)
|
||||
registry.register(metadata=meta, factory=lambda: None)
|
||||
|
||||
assert registry.resolve_backend_for_voice("unknown_voice") == "kokoro"
|
||||
assert registry.resolve_backend_for_voice("unknown_voice", fallback="supertonic") == "supertonic"
|
||||
|
||||
def test_case_insensitive_matching(self):
|
||||
registry = TTSBackendRegistry()
|
||||
meta = TTSBackendMetadata(
|
||||
id="supertonic",
|
||||
name="SuperTonic",
|
||||
description="SuperTonic TTS",
|
||||
voices=("M1", "F1"),
|
||||
)
|
||||
registry.register(metadata=meta, factory=lambda: None)
|
||||
|
||||
assert registry.resolve_backend_for_voice("m1") == "supertonic"
|
||||
assert registry.resolve_backend_for_voice("f1") == "supertonic"
|
||||
|
||||
def test_default_fallback_is_kokoro(self):
|
||||
registry = TTSBackendRegistry()
|
||||
assert registry.resolve_backend_for_voice("unknown") == "kokoro"
|
||||
|
||||
def test_multiple_backends_resolution(self):
|
||||
registry = TTSBackendRegistry()
|
||||
kokoro_meta = TTSBackendMetadata(
|
||||
id="kokoro",
|
||||
name="Kokoro",
|
||||
description="Kokoro TTS",
|
||||
voices=("af_nova",),
|
||||
)
|
||||
supertonic_meta = TTSBackendMetadata(
|
||||
id="supertonic",
|
||||
name="SuperTonic",
|
||||
description="SuperTonic TTS",
|
||||
voices=("M1",),
|
||||
)
|
||||
registry.register(metadata=kokoro_meta, factory=lambda: None)
|
||||
registry.register(metadata=supertonic_meta, factory=lambda: None)
|
||||
|
||||
assert registry.resolve_backend_for_voice("af_nova") == "kokoro"
|
||||
assert registry.resolve_backend_for_voice("M1") == "supertonic"
|
||||
|
||||
def test_global_wrapper_resolve_backend_for_voice(self):
|
||||
from abogen.tts_backend_registry import resolve_backend_for_voice
|
||||
|
||||
# Test with empty spec
|
||||
assert resolve_backend_for_voice("") == "kokoro"
|
||||
|
||||
# Test with formula
|
||||
assert resolve_backend_for_voice("af_nova*0.7") == "kokoro"
|
||||
|
||||
# Test with a registered voice
|
||||
assert resolve_backend_for_voice("af_nova") == "kokoro"
|
||||
assert resolve_backend_for_voice("M1") == "supertonic"
|
||||
@@ -1,108 +0,0 @@
|
||||
import numpy as np
|
||||
|
||||
from abogen.tts_backends.supertonic import SupertonicBackend, SupertonicPipeline
|
||||
|
||||
|
||||
class _DummyTTS:
|
||||
def get_voice_style(self, voice_name: str):
|
||||
return {"voice": voice_name}
|
||||
|
||||
def synthesize(
|
||||
self,
|
||||
*,
|
||||
text: str,
|
||||
voice_style,
|
||||
total_steps: int,
|
||||
speed: float,
|
||||
max_chunk_length: int,
|
||||
silence_duration: float,
|
||||
verbose: bool,
|
||||
):
|
||||
if "•" in text:
|
||||
raise ValueError("Found 1 unsupported character(s): ['•']")
|
||||
# Return 50ms of audio at 24kHz.
|
||||
sr = 24000
|
||||
audio = np.zeros(int(0.05 * sr), dtype="float32")
|
||||
return audio, 0.05
|
||||
|
||||
|
||||
def _make_pipeline() -> SupertonicPipeline:
|
||||
pipeline = SupertonicPipeline.__new__(SupertonicPipeline)
|
||||
pipeline.sample_rate = 24000
|
||||
pipeline.total_steps = 5
|
||||
pipeline.max_chunk_length = 1000
|
||||
pipeline._tts = _DummyTTS()
|
||||
return pipeline
|
||||
|
||||
|
||||
def _make_backend() -> SupertonicBackend:
|
||||
backend = SupertonicBackend.__new__(SupertonicBackend)
|
||||
backend._pipeline = _make_pipeline()
|
||||
return backend
|
||||
|
||||
|
||||
def test_supertonic_pipeline_strips_unsupported_characters_and_retries():
|
||||
pipeline = _make_pipeline()
|
||||
|
||||
segs = list(pipeline("Hello • world", voice="M1", speed=1.0))
|
||||
assert len(segs) == 1
|
||||
assert segs[0].graphemes == "Hello world" or segs[0].graphemes == "Hello world"
|
||||
assert isinstance(segs[0].audio, np.ndarray)
|
||||
assert segs[0].audio.dtype == np.float32
|
||||
assert segs[0].audio.size > 0
|
||||
|
||||
|
||||
def test_supertonic_pipeline_drops_chunk_if_only_unsupported_characters():
|
||||
pipeline = _make_pipeline()
|
||||
|
||||
segs = list(pipeline("•", voice="M1", speed=1.0))
|
||||
assert segs == []
|
||||
|
||||
|
||||
# --- SupertonicBackend tests ---
|
||||
|
||||
|
||||
def test_backend_metadata():
|
||||
backend = _make_backend()
|
||||
meta = backend.metadata
|
||||
assert meta.id == "supertonic"
|
||||
assert meta.name == "SuperTonic"
|
||||
assert "SuperTonic" in meta.description
|
||||
|
||||
|
||||
def test_backend_get_available_voices():
|
||||
backend = _make_backend()
|
||||
voices = backend.get_available_voices()
|
||||
assert isinstance(voices, list)
|
||||
assert "M1" in voices
|
||||
assert "F1" in voices
|
||||
|
||||
|
||||
def test_backend_get_supported_formats():
|
||||
backend = _make_backend()
|
||||
formats = backend.get_supported_formats()
|
||||
assert "wav" in formats
|
||||
|
||||
|
||||
def test_backend_get_info():
|
||||
backend = _make_backend()
|
||||
info = backend.get_info()
|
||||
assert info["sample_rate"] == 24000
|
||||
assert info["total_steps"] == 5
|
||||
assert isinstance(info["voices"], list)
|
||||
|
||||
|
||||
def test_backend_call_delegates_to_pipeline():
|
||||
backend = _make_backend()
|
||||
segs = list(backend("Hello • world", voice="M1", speed=1.0))
|
||||
assert len(segs) == 1
|
||||
assert segs[0].audio.size > 0
|
||||
|
||||
|
||||
def test_backend_synthesize_returns_wav_bytes():
|
||||
backend = _make_backend()
|
||||
wav_bytes = backend.synthesize("Hello world", voice="M1", speed=1.0)
|
||||
assert isinstance(wav_bytes, bytes)
|
||||
assert len(wav_bytes) > 0
|
||||
# WAV magic number
|
||||
assert wav_bytes[:4] == b"RIFF"
|
||||
@@ -1,18 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from abogen.webui.conversion_runner import _resolve_voice, _supertonic_voice_from_spec
|
||||
from abogen.tts_backends.supertonic import DEFAULT_SUPERTONIC_VOICES
|
||||
|
||||
|
||||
def test_resolve_voice_formula_without_pipeline_does_not_crash() -> None:
|
||||
# This can happen when a previously-saved Kokoro mix formula is present
|
||||
# but the active provider is SuperTonic (no Kokoro pipeline object).
|
||||
formula = "af_heart*0.5+af_sky*0.5"
|
||||
resolved = _resolve_voice(None, formula, use_gpu=False)
|
||||
assert resolved == formula
|
||||
|
||||
|
||||
def test_supertonic_voice_from_formula_falls_back_to_valid_voice() -> None:
|
||||
# When a stale Kokoro mix formula is present, SuperTonic should not receive it.
|
||||
chosen = _supertonic_voice_from_spec("af_heart*0.5+af_sky*0.5", "af_heart*1.0")
|
||||
assert chosen in DEFAULT_SUPERTONIC_VOICES
|
||||
Reference in New Issue
Block a user