diff --git a/abogen/tts_backend_registry.py b/abogen/tts_backend_registry.py index 6278624..fbd2548 100644 --- a/abogen/tts_backend_registry.py +++ b/abogen/tts_backend_registry.py @@ -79,6 +79,12 @@ def get_metadata(backend_id: str) -> TTSBackendMetadata: return _registry.get_metadata(backend_id) +def get_default_voice(backend_id: str, fallback: str = "") -> str: + """Return the first voice of a backend, or *fallback* if none.""" + voices = get_metadata(backend_id).voices + return voices[0] if voices else fallback + + def create_backend(backend_id: str, **kwargs: Any) -> TTSBackend: """Create a TTS backend instance by provider id.""" return _registry.create_backend(backend_id, **kwargs) diff --git a/abogen/webui/conversion_runner.py b/abogen/webui/conversion_runner.py index 24ad066..33763d1 100644 --- a/abogen/webui/conversion_runner.py +++ b/abogen/webui/conversion_runner.py @@ -20,7 +20,7 @@ import numpy as np import soundfile as sf import static_ffmpeg -from abogen.constants import VOICES_INTERNAL +from abogen.tts_backend_registry import get_metadata from abogen.epub3.exporter import build_epub3_package from abogen.kokoro_text_normalization import ApostropheConfig, normalize_for_pipeline, HAS_NUM2WORDS from abogen.normalization_settings import ( @@ -47,7 +47,7 @@ from abogen.voice_formulas import extract_voice_ids, get_new_voice from abogen.voice_profiles import load_profiles, normalize_profile_entry from abogen.pronunciation_store import increment_usage from abogen.llm_client import LLMClientError -from abogen.tts_backends.supertonic import DEFAULT_SUPERTONIC_VOICES + from .service import Job, JobStatus @@ -68,11 +68,11 @@ def _supertonic_voice_from_spec(spec: Any, fallback: str) -> str: raw = "M1" upper = raw.upper() - if upper in DEFAULT_SUPERTONIC_VOICES: + if upper in get_metadata("supertonic").voices: return upper fallback_upper = fallback_raw.upper() if fallback_raw else "" - if fallback_upper in DEFAULT_SUPERTONIC_VOICES: + if fallback_upper in get_metadata("supertonic").voices: return fallback_upper return "M1" @@ -123,7 +123,7 @@ def _infer_provider_from_spec(value: Any, fallback: str = "kokoro") -> str: if not raw: return fallback upper = raw.upper() - if upper in DEFAULT_SUPERTONIC_VOICES: + if upper in get_metadata("supertonic").voices: return "supertonic" if "*" in raw or "+" in raw: return "kokoro" @@ -576,7 +576,7 @@ def _spec_to_voice_ids(spec: Any) -> Set[str]: return set(extract_voice_ids(text)) except ValueError: return set() - if text in VOICES_INTERNAL: + if text in get_metadata("kokoro").voices: return {text} return set() @@ -640,7 +640,7 @@ def _collect_required_voice_ids(job: Job) -> Set[str]: for key in ("resolved_voice", "voice_formula", "voice"): voices.update(_spec_to_voice_ids(payload.get(key))) - voices.update(VOICES_INTERNAL) + voices.update(get_metadata("kokoro").voices) return voices @@ -1803,8 +1803,8 @@ def run_conversion_job(job: Job) -> None: fallback_key = next(iter(voice_cache.keys()), "") if fallback_key and fallback_key != "__custom_mix": intro_voice_spec = fallback_key.split(":", 1)[-1] - if not intro_voice_spec and VOICES_INTERNAL: - intro_voice_spec = VOICES_INTERNAL[0] + if not intro_voice_spec: + intro_voice_spec = get_default_voice("kokoro") if intro_voice_spec: intro_provider, _, intro_voice_choice, intro_speed, intro_steps = resolve_voice_choice( @@ -2237,8 +2237,8 @@ def run_conversion_job(job: Job) -> None: if fallback_key and fallback_key != "__custom_mix": # `voice_cache` keys are internal and include provider prefixes. outro_voice_spec = fallback_key.split(":", 1)[-1] - if not outro_voice_spec and VOICES_INTERNAL: - outro_voice_spec = VOICES_INTERNAL[0] + if not outro_voice_spec: + outro_voice_spec = get_default_voice("kokoro") if outro_text and outro_voice_spec: outro_start_time = current_time diff --git a/abogen/webui/routes/utils/form.py b/abogen/webui/routes/utils/form.py index 41d605e..eb601b5 100644 --- a/abogen/webui/routes/utils/form.py +++ b/abogen/webui/routes/utils/form.py @@ -32,7 +32,7 @@ from abogen.webui.routes.utils.common import split_profile_spec from abogen.utils import calculate_text_length from abogen.voice_profiles import serialize_profiles, normalize_profile_entry from abogen.chunking import ChunkLevel, build_chunks_for_chapters -from abogen.constants import VOICES_INTERNAL +from abogen.tts_backend_registry import get_default_voice from abogen.speaker_configs import get_config from abogen.kokoro_text_normalization import normalize_roman_numeral_titles from dataclasses import dataclass @@ -616,8 +616,8 @@ def apply_book_step_form( custom_formula = "" base_voice_spec = resolved_default_voice or narrator_voice_raw - if not base_voice_spec and VOICES_INTERNAL: - base_voice_spec = VOICES_INTERNAL[0] + if not base_voice_spec: + base_voice_spec = get_default_voice("kokoro") voice_choice, resolved_language, selected_profile = resolve_voice_choice( pending.language, @@ -796,8 +796,8 @@ def build_pending_job_from_extraction( profile_selection = inferred_profile base_voice = base_voice_input or resolved_default_voice or str(default_voice_setting).strip() - if not base_voice and VOICES_INTERNAL: - base_voice = VOICES_INTERNAL[0] + if not base_voice: + base_voice = get_default_voice("kokoro") selected_speaker_config = (form.get("speaker_config") or "").strip() speaker_config_payload = get_config(selected_speaker_config) if selected_speaker_config else None diff --git a/abogen/webui/routes/utils/settings.py b/abogen/webui/routes/utils/settings.py index c96a66c..3ffb072 100644 --- a/abogen/webui/routes/utils/settings.py +++ b/abogen/webui/routes/utils/settings.py @@ -6,8 +6,8 @@ from abogen.constants import ( LANGUAGE_DESCRIPTIONS, SUBTITLE_FORMATS, SUPPORTED_SOUND_FORMATS, - VOICES_INTERNAL, ) +from abogen.tts_backend_registry import get_default_voice from abogen.normalization_settings import ( DEFAULT_LLM_PROMPT, environment_llm_defaults, @@ -174,7 +174,7 @@ def settings_defaults() -> Dict[str, Any]: "subtitle_format": "srt", "save_mode": "default_output" if has_output_override() else "save_next_to_input", "default_speaker": "", - "default_voice": VOICES_INTERNAL[0] if VOICES_INTERNAL else "", + "default_voice": get_default_voice("kokoro"), "supertonic_total_steps": 5, "supertonic_speed": 1.0, "replace_single_newlines": False, diff --git a/abogen/webui/routes/utils/voice.py b/abogen/webui/routes/utils/voice.py index 8401e1e..42a2c13 100644 --- a/abogen/webui/routes/utils/voice.py +++ b/abogen/webui/routes/utils/voice.py @@ -17,8 +17,8 @@ from abogen.constants import ( SUPPORTED_SOUND_FORMATS, SUPPORTED_LANGUAGES_FOR_SUBTITLE_GENERATION, SAMPLE_VOICE_TEXTS, - VOICES_INTERNAL, ) +from abogen.tts_backend_registry import get_metadata from abogen.speaker_configs import list_configs from abogen.tts_backend_registry import create_backend from abogen.webui.conversion_runner import _select_device, _to_float32, SAMPLE_RATE, SPLIT_PATTERN @@ -285,7 +285,7 @@ def filter_voice_catalog( def build_voice_catalog() -> List[Dict[str, str]]: catalog: List[Dict[str, str]] = [] gender_map = {"f": "Female", "m": "Male"} - for voice_id in VOICES_INTERNAL: + for voice_id in get_metadata("kokoro").voices: prefix, _, rest = voice_id.partition("_") language_code = prefix[0] if prefix else "a" gender_code = prefix[1] if len(prefix) > 1 else "" @@ -590,7 +590,7 @@ def template_options() -> Dict[str, Any]: voice_catalog = build_voice_catalog() return { "languages": LANGUAGE_DESCRIPTIONS, - "voices": VOICES_INTERNAL, + "voices": get_metadata("kokoro").voices, "subtitle_formats": SUBTITLE_FORMATS, "supported_langs_for_subs": SUPPORTED_LANGUAGES_FOR_SUBTITLE_GENERATION, "output_formats": SUPPORTED_SOUND_FORMATS, diff --git a/abogen/webui/routes/voices.py b/abogen/webui/routes/voices.py index a7e9ce7..ac39760 100644 --- a/abogen/webui/routes/voices.py +++ b/abogen/webui/routes/voices.py @@ -17,7 +17,7 @@ from abogen.speaker_configs import ( save_configs, delete_config, ) -from abogen.constants import VOICES_INTERNAL + voices_bp = Blueprint("voices", __name__)