Files
diodiogod-TTS-Audio-Suite/utils/audio_cpp/capabilities.py

185 lines
7.1 KiB
Python

"""Resolve Suite integration capabilities for pinned audio.cpp families."""
from __future__ import annotations
from copy import deepcopy
from functools import lru_cache
from pathlib import Path
from typing import Any, Dict, Mapping
import yaml
from .catalog import AUDIO_CPP_RELEASE_TAG, PackageRecord, load_catalog
class CapabilityError(ValueError):
pass
def get_capability_path() -> Path:
return Path(__file__).resolve().with_name("integration_capabilities.yaml")
@lru_cache(maxsize=1)
def _load_overlay() -> Dict[str, Any]:
path = get_capability_path()
try:
raw = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
except (OSError, yaml.YAMLError) as exc:
raise CapabilityError(f"Cannot read audio.cpp capability overlay {path}: {exc}") from exc
if raw.get("schema_version") != 1 or raw.get("release") != AUDIO_CPP_RELEASE_TAG:
raise CapabilityError("audio.cpp capability overlay release/schema does not match the catalog")
return dict(raw)
def _merge(base: Mapping[str, Any], override: Mapping[str, Any]) -> Dict[str, Any]:
value = deepcopy(dict(base))
for key, item in override.items():
if isinstance(item, Mapping) and isinstance(value.get(key), Mapping):
value[key] = _merge(value[key], item)
else:
value[key] = deepcopy(item)
return value
@lru_cache(maxsize=1)
def load_capabilities() -> Dict[str, Dict[str, Any]]:
raw = _load_overlay()
catalog = load_catalog()
families = raw.get("families") or {}
if set(families) != set(catalog.families):
missing = sorted(set(catalog.families) - set(families))
extra = sorted(set(families) - set(catalog.families))
raise CapabilityError(f"audio.cpp capability families mismatch; missing={missing}, extra={extra}")
resolved: Dict[str, Dict[str, Any]] = {}
defaults = raw.get("defaults") or {}
for family_id, override in families.items():
family = catalog.family(family_id)
item = _merge(defaults, override or {})
item.update(
id=family.id,
display_name=family.display_name,
description=family.description,
languages=list(family.languages),
upstream_tasks=list(family.runtime_tasks),
packages=[package.id for package in family.packages],
recommended_package_id=family.recommended_package_id,
)
asr_features = item.get("asr_features") or {}
if not isinstance(asr_features, Mapping):
raise CapabilityError(
f"audio.cpp {family_id} asr_features must be an object"
)
diarization = str(asr_features.get("diarization", "none"))
timing = str(asr_features.get("timing", "none"))
if diarization not in {"none", "native"}:
raise CapabilityError(
f"audio.cpp {family_id} has invalid ASR diarization capability: {diarization}"
)
if timing not in {
"none",
"native_word",
"native_segment",
"optional_forced_aligner",
}:
raise CapabilityError(
f"audio.cpp {family_id} has invalid ASR timing capability: {timing}"
)
resolved[family_id] = item
return resolved
def get_capability(family: str) -> Dict[str, Any]:
try:
return deepcopy(load_capabilities()[str(family)])
except KeyError as exc:
raise CapabilityError(f"Unknown audio.cpp capability family: {family!r}") from exc
def public_capabilities() -> Dict[str, Any]:
catalog = load_catalog()
raw_sizes = _load_overlay().get("package_sizes") or {}
if set(raw_sizes) != set(catalog.packages):
missing = sorted(set(catalog.packages) - set(raw_sizes))
extra = sorted(set(raw_sizes) - set(catalog.packages))
raise CapabilityError(f"audio.cpp package sizes mismatch; missing={missing}, extra={extra}")
packages = {}
for package_id, package in catalog.packages.items():
size = raw_sizes[package_id]
if not isinstance(size, int) or size <= 0:
raise CapabilityError(f"Invalid estimated size for audio.cpp package {package_id!r}")
dependencies = get_package_dependencies(package_id)
dependency_bytes = sum(
int(item["estimated_download_bytes"]) for item in dependencies
)
packages[package_id] = {
"id": package.id,
"family": package.family,
"display_name": package.display_name,
"format": package.format,
"precision": package.precision,
"estimated_download_bytes": size + dependency_bytes,
"primary_download_bytes": size,
"dependencies": [item["package"].id for item in dependencies],
}
return {
"schema_version": 1,
"release": AUDIO_CPP_RELEASE_TAG,
"sizes_checked_at": "2026-08-13",
"families": load_capabilities(),
"packages": packages,
}
def get_package_dependencies(package_id: str) -> list[Dict[str, Any]]:
entries = (_load_overlay().get("package_dependencies") or {}).get(package_id, [])
dependencies = []
for entry in entries:
package = PackageRecord(
family=str(entry["family"]),
id=str(entry["id"]),
display_name=str(entry["display_name"]),
target_directory=str(entry["target_directory"]),
format=str(entry["format"]),
precision=str(entry["precision"]),
files=tuple(str(value) for value in entry["files"]),
strip_prefix=str(entry.get("strip_prefix", "")),
download={
"kind": "huggingface_snapshot",
"repo": str(entry["repo"]),
"revision": str(entry.get("revision", "main")),
"gated": False,
},
)
# Validate local mappings before the downloader touches disk.
package.local_files
dependencies.append(
{
"package": package,
"session_option": str(entry["session_option"]),
"estimated_download_bytes": int(entry["estimated_download_bytes"]),
}
)
return dependencies
def validate_voice_reference(family: str, voice_ref: Any, character: str = "narrator") -> None:
"""Enforce requirements that the frontend panel merely explains."""
from utils.voice.reference import effective_voice_audio
capability = get_capability(family)
audio_requirement = capability["reference_audio"]
has_audio = isinstance(voice_ref, Mapping) and effective_voice_audio(voice_ref) is not None
if audio_requirement in {"required", "required_per_speaker"} and not has_audio:
raise ValueError(f"audio.cpp {family} requires reference audio for '{character}'")
transcript = ""
if isinstance(voice_ref, Mapping):
transcript = str(
voice_ref.get("reference_text") or voice_ref.get("prompt_text") or voice_ref.get("text") or ""
).strip()
if capability["reference_transcript"] == "required" and not transcript:
raise ValueError(
f"audio.cpp {family} requires the transcript matching '{character}' reference audio"
)