from __future__ import annotations import json import os from typing import Any, Dict, List, Optional from abogen.constants import LANGUAGE_DESCRIPTIONS from abogen.utils import get_user_config_path _CONFIG_WRAPPER_KEY = "abogen_speaker_configs" def _config_path() -> str: config_path = get_user_config_path() config_dir = os.path.dirname(config_path) os.makedirs(config_dir, exist_ok=True) return os.path.join(config_dir, "speaker_configs.json") def load_configs() -> Dict[str, Dict[str, Any]]: path = _config_path() if not os.path.exists(path): return {} try: with open(path, "r", encoding="utf-8") as handle: payload = json.load(handle) except Exception: return {} if isinstance(payload, dict) and _CONFIG_WRAPPER_KEY in payload: payload = payload[_CONFIG_WRAPPER_KEY] if not isinstance(payload, dict): return {} sanitized: Dict[str, Dict[str, Any]] = {} for name, entry in payload.items(): if not isinstance(name, str) or not isinstance(entry, dict): continue sanitized[name] = _sanitize_config(entry) return sanitized def save_configs(configs: Dict[str, Dict[str, Any]]) -> None: path = _config_path() sanitized: Dict[str, Dict[str, Any]] = {} for name, entry in configs.items(): if not isinstance(name, str) or not name.strip(): continue sanitized[name] = _sanitize_config(entry) with open(path, "w", encoding="utf-8") as handle: json.dump({_CONFIG_WRAPPER_KEY: sanitized}, handle, indent=2, sort_keys=True) def get_config(name: str) -> Optional[Dict[str, Any]]: name = (name or "").strip() if not name: return None configs = load_configs() data = configs.get(name) return dict(data) if isinstance(data, dict) else None def upsert_config(name: str, payload: Dict[str, Any]) -> Dict[str, Any]: name = (name or "").strip() if not name: raise ValueError("Configuration name is required") configs = load_configs() configs[name] = _sanitize_config(payload or {}) save_configs(configs) return configs[name] def delete_config(name: str) -> None: name = (name or "").strip() if not name: return configs = load_configs() if name in configs: del configs[name] save_configs(configs) def _sanitize_config(entry: Dict[str, Any]) -> Dict[str, Any]: language = str(entry.get("language") or "a").strip() or "a" speakers_raw = entry.get("speakers") if not isinstance(speakers_raw, dict): speakers_raw = {} speakers: Dict[str, Any] = {} for speaker_id, payload in speakers_raw.items(): if not isinstance(speaker_id, str) or not isinstance(payload, dict): continue record = _sanitize_speaker({"id": speaker_id, **payload}) speakers[record["id"]] = record allowed_languages = entry.get("languages") or entry.get("allowed_languages") or [] if not isinstance(allowed_languages, list): allowed_languages = [] normalized_langs = [] for code in allowed_languages: if isinstance(code, str) and code: normalized_langs.append(code.lower()) default_voice = entry.get("default_voice") if not isinstance(default_voice, str): default_voice = "" return { "language": language.lower(), "languages": normalized_langs, "default_voice": default_voice, "speakers": speakers, "version": int(entry.get("version", 1)), "notes": entry.get("notes") if isinstance(entry.get("notes"), str) else "", } def slugify_label(label: str) -> str: normalized = (label or "").strip().lower() if not normalized: return "speaker" slug = "".join(ch if ch.isalnum() else "_" for ch in normalized) slug = "_".join(filter(None, slug.split("_"))) return slug or "speaker" def _sanitize_speaker(entry: Dict[str, Any]) -> Dict[str, Any]: label = (entry.get("label") or entry.get("name") or "").strip() gender = (entry.get("gender") or "unknown").strip().lower() if gender not in {"male", "female", "unknown"}: gender = "unknown" voice = entry.get("voice") voice_profile = entry.get("voice_profile") voice_formula = entry.get("voice_formula") voice_languages = entry.get("languages") or [] if not isinstance(voice_languages, list): voice_languages = [] normalized_langs = [] for code in voice_languages: if isinstance(code, str) and code: normalized_langs.append(code.lower()) resolved_voice = entry.get("resolved_voice") or voice_formula or voice resolved_label = label or entry.get("id") or "" slug = ( entry.get("id") if isinstance(entry.get("id"), str) else slugify_label(resolved_label) ) return { "id": slug, "label": resolved_label, "gender": gender, "voice": voice if isinstance(voice, str) else "", "voice_profile": voice_profile if isinstance(voice_profile, str) else "", "voice_formula": voice_formula if isinstance(voice_formula, str) else "", "resolved_voice": resolved_voice if isinstance(resolved_voice, str) else "", "languages": normalized_langs, } def list_configs() -> List[Dict[str, Any]]: configs = load_configs() ordered = [] for name in sorted(configs): entry = configs[name] ordered.append({"name": name, **entry}) return ordered def describe_language(code: str) -> str: code = (code or "a").lower() return LANGUAGE_DESCRIPTIONS.get(code, code.upper())