from __future__ import annotations from dataclasses import dataclass from pathlib import Path OPENAI_TO_SUPERTONIC = { "alloy": "M1", "echo": "M2", "nova": "F1", "shimmer": "F2", } FRIENDLY_BUILTIN_NAMES = { "M1": "atlas", "M2": "ember", "M3": "flint", "M4": "orion", "M5": "vale", "F1": "aria", "F2": "selene", "F3": "ivy", "F4": "luma", "F5": "sora", } CUSTOM_VOICE_NAME_OVERRIDES = { "preset_voice_m": "shepherd", "preset_voice_f": "grace", } @dataclass(frozen=True) class VoiceSpec: alias: str canonical_name: str provider_voice_id: str source: str style_path: str | None = None def _canonical_builtin_voice_specs(model_dir: str | Path) -> dict[str, VoiceSpec]: voice_dir = Path(model_dir) / "voice_styles" specs: dict[str, VoiceSpec] = {} voice_ids = [style_file.stem for style_file in sorted(voice_dir.glob("*.json"))] if not voice_ids: voice_ids = [f"M{i}" for i in range(1, 6)] + [f"F{i}" for i in range(1, 6)] for provider_voice_id in voice_ids: canonical_name = FRIENDLY_BUILTIN_NAMES.get(provider_voice_id, provider_voice_id.lower()) specs[provider_voice_id.lower()] = VoiceSpec( alias=provider_voice_id.lower(), canonical_name=canonical_name, provider_voice_id=provider_voice_id, source="builtin", ) return specs def _custom_voice_specs(custom_voice_dir: str | Path) -> dict[str, VoiceSpec]: voice_dir = Path(custom_voice_dir) if not voice_dir.exists(): return {} specs: dict[str, VoiceSpec] = {} for style_file in sorted(voice_dir.glob("*.json")): stem = style_file.stem.lower() canonical_name = CUSTOM_VOICE_NAME_OVERRIDES.get(stem, stem) specs[canonical_name] = VoiceSpec( alias=canonical_name, canonical_name=canonical_name, provider_voice_id=style_file.stem, source="custom", style_path=str(style_file), ) if stem != canonical_name: specs[stem] = VoiceSpec( alias=stem, canonical_name=canonical_name, provider_voice_id=style_file.stem, source="custom", style_path=str(style_file), ) return specs def get_voice_alias_map(model_dir: str | Path, custom_voice_dir: str | Path) -> dict[str, VoiceSpec]: builtins = _canonical_builtin_voice_specs(model_dir) custom = _custom_voice_specs(custom_voice_dir) alias_map: dict[str, VoiceSpec] = {} for key, spec in builtins.items(): alias_map[key] = spec alias_map[spec.canonical_name] = VoiceSpec( alias=spec.canonical_name, canonical_name=spec.canonical_name, provider_voice_id=spec.provider_voice_id, source=spec.source, style_path=spec.style_path, ) for alias, provider_voice_id in OPENAI_TO_SUPERTONIC.items(): builtin = builtins.get(provider_voice_id.lower()) if builtin: alias_map[alias] = VoiceSpec( alias=alias, canonical_name=builtin.canonical_name, provider_voice_id=builtin.provider_voice_id, source=builtin.source, style_path=builtin.style_path, ) alias_map.update(custom) return alias_map def resolve_voice_spec(voice: str, model_dir: str | Path, custom_voice_dir: str | Path) -> VoiceSpec: alias_map = get_voice_alias_map(model_dir, custom_voice_dir) normalized = voice.strip().lower() return alias_map[normalized] def list_supported_voices(model_dir: str | Path, custom_voice_dir: str | Path) -> list[str]: return sorted(get_voice_alias_map(model_dir, custom_voice_dir)) def list_voice_bindings(model_dir: str | Path, custom_voice_dir: str | Path) -> list[dict[str, str]]: alias_map = get_voice_alias_map(model_dir, custom_voice_dir) bindings: list[dict[str, str]] = [] seen: set[tuple[str, str, str]] = set() for alias, spec in sorted(alias_map.items()): row = (alias, spec.canonical_name, spec.provider_voice_id) if row in seen: continue seen.add(row) bindings.append( { "alias": alias, "canonical_name": spec.canonical_name, "provider_voice_id": spec.provider_voice_id, "source": spec.source, "style_path": spec.style_path or "", } ) return bindings