feat: Improve SuperTonic voice resolution and add tests for voice formula handling

This commit is contained in:
JB
2025-12-20 08:35:33 -08:00
parent 19e98c3ad6
commit 32f616e90b
2 changed files with 39 additions and 7 deletions
+21 -7
View File
@@ -56,16 +56,24 @@ SAMPLE_RATE = 24000
def _supertonic_voice_from_spec(spec: Any, fallback: str) -> str: def _supertonic_voice_from_spec(spec: Any, fallback: str) -> str:
raw = str(spec or "").strip() raw = str(spec or "").strip()
if not raw: fallback_raw = str(fallback or "").strip()
raw = str(fallback or "").strip()
if not raw: # SuperTonic voices are discrete IDs (M1/F3/...). If we see a Kokoro mix
return "M1" # formula (contains '*' or '+'), ignore it and fall back to a safe voice.
if "*" in raw or "+" in raw: if not raw or "*" in raw or "+" in raw:
raw = str(fallback or "").strip() or "M1" raw = fallback_raw
if not raw or "*" in raw or "+" in raw:
raw = "M1"
upper = raw.upper() upper = raw.upper()
if upper in DEFAULT_SUPERTONIC_VOICES: if upper in DEFAULT_SUPERTONIC_VOICES:
return upper return upper
return str(fallback or "").strip() or "M1"
fallback_upper = fallback_raw.upper() if fallback_raw else ""
if fallback_upper in DEFAULT_SUPERTONIC_VOICES:
return fallback_upper
return "M1"
def _split_speaker_reference(value: Any) -> tuple[Optional[str], str]: def _split_speaker_reference(value: Any) -> tuple[Optional[str], str]:
@@ -2439,6 +2447,12 @@ def _build_ffmpeg_command(path: Path, fmt: str, metadata: Optional[Dict[str, str
def _resolve_voice(pipeline, voice_spec: str, use_gpu: bool): def _resolve_voice(pipeline, voice_spec: str, use_gpu: bool):
if "*" in voice_spec: if "*" in voice_spec:
# Voice formulas are a Kokoro-only feature (they require a pipeline that can
# load individual Kokoro voices). When running with SuperTonic (or when the
# pipeline is otherwise unavailable), treat the spec as a plain string and
# allow downstream provider-specific resolution to choose a safe fallback.
if pipeline is None or not hasattr(pipeline, "load_single_voice"):
return voice_spec
return get_new_voice(pipeline, voice_spec, use_gpu) return get_new_voice(pipeline, voice_spec, use_gpu)
return voice_spec return voice_spec
+18
View File
@@ -0,0 +1,18 @@
from __future__ import annotations
from abogen.web.conversion_runner import _resolve_voice, _supertonic_voice_from_spec
from abogen.tts_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