| 1 | from __future__ import annotations |
| 2 | |
| 3 | import asyncio |
| 4 | from pathlib import Path |
| 5 | |
| 6 | from plugins._kokoro_tts.helpers import runtime |
| 7 | |
| 8 | |
| 9 | PROJECT_ROOT = Path(__file__).resolve().parents[1] |
| 10 | |
| 11 | |
| 12 | def test_config_keeps_legacy_voice_and_normalizes_weighted_blends() -> None: |
| 13 | legacy = runtime.normalize_config( |
| 14 | {"voice": "custom/voice.pt", "speed": 2.2} |
| 15 | ) |
| 16 | assert legacy == { |
| 17 | "voice": "custom/voice.pt", |
| 18 | "voice_weights": {}, |
| 19 | "speed": 2.2, |
| 20 | } |
| 21 | |
| 22 | weighted = runtime.normalize_config( |
| 23 | { |
| 24 | "voice": "ignored_when_weights_are_present", |
| 25 | "voice_weights": { |
| 26 | "af_heart": "3", |
| 27 | "am_puck": 1, |
| 28 | "../bad.pt": 5, |
| 29 | "am_onyx": float("nan"), |
| 30 | "am_echo": 0, |
| 31 | }, |
| 32 | "speed": float("inf"), |
| 33 | } |
| 34 | ) |
| 35 | assert weighted == { |
| 36 | "voice": "af_heart,am_puck", |
| 37 | "voice_weights": {"af_heart": 3.0, "am_puck": 1.0}, |
| 38 | "speed": 1.1, |
| 39 | } |
| 40 | |
| 41 | |
| 42 | def test_weighted_blend_reuses_the_existing_pipeline() -> None: |
| 43 | class FakePipeline: |
| 44 | packs = {"af_heart": 2.0, "am_puck": 10.0} |
| 45 | |
| 46 | def __init__(self) -> None: |
| 47 | self.loaded: list[str] = [] |
| 48 | |
| 49 | def load_single_voice(self, voice: str) -> float: |
| 50 | self.loaded.append(voice) |
| 51 | return self.packs[voice] |
| 52 | |
| 53 | pipeline = FakePipeline() |
| 54 | blend = runtime._resolve_voice( |
| 55 | pipeline, |
| 56 | "legacy", |
| 57 | {"af_heart": 3.0, "am_puck": 1.0}, |
| 58 | ) |
| 59 | |
| 60 | assert blend == 4.0 |
| 61 | assert pipeline.loaded == ["af_heart", "am_puck"] |
| 62 | assert runtime._resolve_voice(pipeline, "am_puck,am_onyx", {}) == ( |
| 63 | "am_puck,am_onyx" |
| 64 | ) |
| 65 | |
| 66 | |
| 67 | def test_synthesis_forwards_normalized_weights(monkeypatch) -> None: |
| 68 | captured: dict = {} |
| 69 | |
| 70 | async def fake_synthesize(sentences, **kwargs): |
| 71 | captured.update({"sentences": sentences, **kwargs}) |
| 72 | return "audio" |
| 73 | |
| 74 | monkeypatch.setattr(runtime, "_synthesize_sentences", fake_synthesize) |
| 75 | |
| 76 | result = asyncio.run( |
| 77 | runtime.synthesize_sentences( |
| 78 | ["Hello"], |
| 79 | { |
| 80 | "voice": "legacy", |
| 81 | "voice_weights": {"af_heart": 2, "am_puck": 1}, |
| 82 | "speed": 1.4, |
| 83 | }, |
| 84 | ) |
| 85 | ) |
| 86 | |
| 87 | assert result == "audio" |
| 88 | assert captured == { |
| 89 | "sentences": ["Hello"], |
| 90 | "voice": "af_heart,am_puck", |
| 91 | "voice_weights": {"af_heart": 2.0, "am_puck": 1.0}, |
| 92 | "speed": 1.4, |
| 93 | } |
| 94 | |
| 95 | |
| 96 | def test_settings_expose_catalog_weights_and_speed_without_disk_blends() -> None: |
| 97 | config_ui = ( |
| 98 | PROJECT_ROOT / "plugins/_kokoro_tts/webui/config.html" |
| 99 | ).read_text(encoding="utf-8") |
| 100 | store = ( |
| 101 | PROJECT_ROOT / "plugins/_kokoro_tts/webui/kokoro-tts-store.js" |
| 102 | ).read_text(encoding="utf-8") |
| 103 | |
| 104 | assert "af_heart" in config_ui |
| 105 | assert "am_puck" in config_ui |
| 106 | assert "am_onyx" in config_ui |
| 107 | assert "config.voice_weights" in config_ui |
| 108 | assert "Use equal weights" in config_ui |
| 109 | assert 'type="range"' in config_ui |
| 110 | assert 'min="0.5"' in config_ui |
| 111 | assert 'max="3"' in config_ui |
| 112 | assert "create_blend" not in config_ui |
| 113 | assert "lang_code" not in config_ui |
| 114 | assert "voiceSummary" in store |