cpu-tts / app /voice_map.py
grimshaw's picture
Upload current hf-space contents
8986c3d verified
Raw
History Blame Contribute Delete
4.59 kB
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