SurfSense/surfsense_backend/app/podcasts/voices/catalog.py
2026-06-10 18:44:03 +02:00

55 lines
2.2 KiB
Python

"""The voice catalog: look up and filter selectable voices.
A :class:`VoiceCatalog` is the single source of truth for which voices exist,
replacing the hardcoded speaker-id maps. Resolution uses it to pick defaults
for a brief, the API exposes it as picker options, and the renderer uses it to
turn a stored ``voice_id`` back into the provider-native reference.
"""
from __future__ import annotations
from collections.abc import Iterable
from functools import lru_cache
from .data import AZURE_VOICES, KOKORO_VOICES, OPENAI_VOICES, VERTEX_VOICES
from .provider import TtsProvider
from .voice import CatalogVoice
class VoiceCatalog:
"""An indexed, read-only collection of :class:`CatalogVoice`."""
def __init__(self, voices: Iterable[CatalogVoice]) -> None:
self._by_id: dict[str, CatalogVoice] = {}
self._by_provider: dict[TtsProvider, list[CatalogVoice]] = {}
for voice in voices:
if voice.voice_id in self._by_id:
raise ValueError(f"duplicate voice_id: {voice.voice_id}")
self._by_id[voice.voice_id] = voice
self._by_provider.setdefault(voice.provider, []).append(voice)
def get(self, voice_id: str) -> CatalogVoice:
"""Return the voice with ``voice_id`` or raise ``KeyError``."""
return self._by_id[voice_id]
def for_provider(self, provider: TtsProvider) -> list[CatalogVoice]:
"""All voices offered by ``provider``, in catalog order."""
return list(self._by_provider.get(provider, ()))
def for_language(
self, provider: TtsProvider, language: str
) -> list[CatalogVoice]:
"""``provider`` voices that can render ``language``, in catalog order."""
return [v for v in self.for_provider(provider) if v.speaks(language)]
def supports_language(self, provider: TtsProvider, language: str) -> bool:
"""Whether ``provider`` has at least one voice for ``language``."""
return any(v.speaks(language) for v in self.for_provider(provider))
@lru_cache(maxsize=1)
def get_voice_catalog() -> VoiceCatalog:
"""The process-wide catalog assembled from every provider's roster."""
return VoiceCatalog(
(*KOKORO_VOICES, *OPENAI_VOICES, *AZURE_VOICES, *VERTEX_VOICES)
)