| 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 |
|
|