from __future__ import annotations import json import os from typing import Any from sqlalchemy import select from services.shared.core import utc_now_iso from services.shared.models import ( VoiceTTSConfig, VoiceTTSConfigOut, VoiceTTSLanguageConfig, VoiceTTSProviderConfig, VoiceTTSVoiceOption, ) from services.shared.sql_models import VoiceTTSSettingsRow VOICE_TTS_SETTINGS_KEY = "global" def _normalize_provider_name(value: str | None) -> str: normalized = str(value or "yandex").strip().lower() if normalized in {"speechkit", "yandex_speechkit"}: return "yandex" if normalized in {"11labs"}: return "elevenlabs" if normalized not in {"yandex", "elevenlabs", "openai"}: return "yandex" return normalized def _openai_default_model() -> str: return str(os.getenv("AI_VOICE_TTS_MODEL", "gpt-4o-mini-tts") or "gpt-4o-mini-tts").strip() or "gpt-4o-mini-tts" def _openai_default_voice() -> str: return str(os.getenv("AI_VOICE_TTS_VOICE", "alloy") or "alloy").strip() or "alloy" def _yandex_default_voice(language: str) -> str: normalized = str(language or "ru").strip().lower() if normalized == "kz": return str(os.getenv("AI_VOICE_TTS_YANDEX_KK_VOICE", "amira") or "amira").strip() or "amira" return str(os.getenv("AI_VOICE_TTS_YANDEX_VOICE", "jane") or "jane").strip() or "jane" def _elevenlabs_default_model() -> str: return str(os.getenv("AI_VOICE_TTS_ELEVENLABS_MODEL_ID", "eleven_v3") or "eleven_v3").strip() or "eleven_v3" def _elevenlabs_default_voice(language: str) -> str: normalized = str(language or "ru").strip().lower() if normalized == "kz": return ( str(os.getenv("AI_VOICE_TTS_ELEVENLABS_KK_VOICE_ID", "nPczCjzI2devNBz1zQrb") or "nPczCjzI2devNBz1zQrb").strip() or "nPczCjzI2devNBz1zQrb" ) return ( str(os.getenv("AI_VOICE_TTS_ELEVENLABS_RU_VOICE_ID", "nPczCjzI2devNBz1zQrb") or "nPczCjzI2devNBz1zQrb").strip() or "nPczCjzI2devNBz1zQrb" ) def voice_tts_default_config() -> VoiceTTSConfig: return VoiceTTSConfig( provider=_normalize_provider_name(os.getenv("AI_VOICE_TTS_PROVIDER", "yandex")), yandex=VoiceTTSProviderConfig( ru=VoiceTTSLanguageConfig(voice=_yandex_default_voice("ru")), kz=VoiceTTSLanguageConfig(voice=_yandex_default_voice("kz")), ), elevenlabs=VoiceTTSProviderConfig( ru=VoiceTTSLanguageConfig( voice=_elevenlabs_default_voice("ru"), model_id=_elevenlabs_default_model(), language_code="ru", ), kz=VoiceTTSLanguageConfig( voice=_elevenlabs_default_voice("kz"), model_id=_elevenlabs_default_model(), language_code="kk", ), ), openai=VoiceTTSProviderConfig( ru=VoiceTTSLanguageConfig( voice=_openai_default_voice(), model_id=_openai_default_model(), language_code="ru", ), kz=VoiceTTSLanguageConfig( voice=_openai_default_voice(), model_id=_openai_default_model(), language_code="kz", ), ), ) def voice_tts_provider_options() -> list[str]: return ["yandex", "elevenlabs", "openai"] def voice_tts_voice_options() -> dict[str, dict[str, list[VoiceTTSVoiceOption]]]: return { "yandex": { "ru": [ VoiceTTSVoiceOption(value="jane", label="Jane"), VoiceTTSVoiceOption(value="oksana", label="Oksana"), VoiceTTSVoiceOption(value="ermil", label="Ermil"), ], "kz": [ VoiceTTSVoiceOption(value="amira", label="Amira"), VoiceTTSVoiceOption(value="madi", label="Madi"), VoiceTTSVoiceOption(value="saule", label="Saule"), VoiceTTSVoiceOption(value="zhanar", label="Zhanar"), ], }, "elevenlabs": { "ru": [ VoiceTTSVoiceOption(value="nPczCjzI2devNBz1zQrb", label="Brian"), ], "kz": [ VoiceTTSVoiceOption(value="nPczCjzI2devNBz1zQrb", label="Brian"), ], }, "openai": { "ru": [ VoiceTTSVoiceOption(value="alloy", label="Alloy"), VoiceTTSVoiceOption(value="echo", label="Echo"), VoiceTTSVoiceOption(value="fable", label="Fable"), VoiceTTSVoiceOption(value="onyx", label="Onyx"), VoiceTTSVoiceOption(value="nova", label="Nova"), VoiceTTSVoiceOption(value="shimmer", label="Shimmer"), ], "kz": [ VoiceTTSVoiceOption(value="alloy", label="Alloy"), VoiceTTSVoiceOption(value="echo", label="Echo"), VoiceTTSVoiceOption(value="fable", label="Fable"), VoiceTTSVoiceOption(value="onyx", label="Onyx"), VoiceTTSVoiceOption(value="nova", label="Nova"), VoiceTTSVoiceOption(value="shimmer", label="Shimmer"), ], }, } def _settings_row(session) -> VoiceTTSSettingsRow | None: return session.execute( select(VoiceTTSSettingsRow).where(VoiceTTSSettingsRow.settings_key == VOICE_TTS_SETTINGS_KEY) ).scalar_one_or_none() def _normalize_config_payload(raw: Any) -> VoiceTTSConfig: if isinstance(raw, VoiceTTSConfig): return raw if not isinstance(raw, dict): raise ValueError("Voice TTS config payload must be an object") return VoiceTTSConfig.model_validate(raw) def load_voice_tts_config(session) -> VoiceTTSConfigOut: default_config = voice_tts_default_config() row = _settings_row(session) if row is None: return VoiceTTSConfigOut( config=default_config, updated_at=None, source="defaults", provider_options=voice_tts_provider_options(), voice_options=voice_tts_voice_options(), ) try: parsed = json.loads(row.config_json or "{}") config = _normalize_config_payload(parsed) return VoiceTTSConfigOut( config=config, updated_at=row.updated_at, source="database", provider_options=voice_tts_provider_options(), voice_options=voice_tts_voice_options(), ) except Exception: return VoiceTTSConfigOut( config=default_config, updated_at=row.updated_at, source="defaults", provider_options=voice_tts_provider_options(), voice_options=voice_tts_voice_options(), ) def load_effective_voice_tts_config(session) -> VoiceTTSConfig: config = load_voice_tts_config(session).config config.provider = _normalize_provider_name(config.provider) return config def save_voice_tts_config(session, config_payload: VoiceTTSConfig | dict[str, Any]) -> VoiceTTSConfigOut: config = _normalize_config_payload(config_payload) config.provider = _normalize_provider_name(config.provider) row = _settings_row(session) now = utc_now_iso() if row is None: row = VoiceTTSSettingsRow( settings_key=VOICE_TTS_SETTINGS_KEY, config_json="{}", updated_at=now, ) session.add(row) row.config_json = json.dumps(config.model_dump(), ensure_ascii=False) row.updated_at = now session.commit() session.refresh(row) return VoiceTTSConfigOut( config=config, updated_at=row.updated_at, source="database", provider_options=voice_tts_provider_options(), voice_options=voice_tts_voice_options(), ) def voice_tts_language_config(config: VoiceTTSConfig, provider: str, language: str) -> VoiceTTSLanguageConfig: normalized_provider = str(provider or config.provider or "yandex").strip().lower() provider_config = getattr(config, normalized_provider, None) if not isinstance(provider_config, VoiceTTSProviderConfig): provider_config = getattr(voice_tts_default_config(), normalized_provider) normalized_language = "kz" if str(language or "").strip().lower() == "kz" else "ru" return provider_config.kz if normalized_language == "kz" else provider_config.ru