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

253 lines
8.9 KiB
Python

"""Verified Windows audio.cpp release-0.5.1 runtime installer."""
from __future__ import annotations
import hashlib
import os
import shutil
import stat
import sys
import tempfile
import uuid
import zipfile
from dataclasses import dataclass
from pathlib import Path, PurePosixPath
from typing import Callable, Mapping, Optional, Sequence, Union
from .catalog import AUDIO_CPP_RELEASE_COMMIT, AUDIO_CPP_RELEASE_TAG, AUDIO_CPP_RELEASE_VERSION
from .downloader import AudioCppDownloadError, ProgressCallback, download_url_to_path
class RuntimeInstallError(RuntimeError):
"""Raised when a managed audio.cpp runtime cannot be verified or installed."""
@dataclass(frozen=True)
class RuntimeAsset:
filename: str
url: str
size: int
sha256: str
@dataclass(frozen=True)
class RuntimeManifest:
backend: str
assets: tuple[RuntimeAsset, ...]
required_files: tuple[str, ...]
release_version: str = AUDIO_CPP_RELEASE_VERSION
release_tag: str = AUDIO_CPP_RELEASE_TAG
release_commit: str = AUDIO_CPP_RELEASE_COMMIT
profile: str = "balance"
platform: str = "windows"
_RELEASE_URL = "https://github.com/0xShug0/audio.cpp/releases/download/release-0.5.1"
WINDOWS_RUNTIME_MANIFESTS: Mapping[str, RuntimeManifest] = {
"cpu": RuntimeManifest(
backend="cpu",
assets=(
RuntimeAsset(
filename="audiocpp-windows-cpu-balance-238ab6a9.zip",
url=f"{_RELEASE_URL}/audiocpp-windows-cpu-balance-238ab6a9.zip",
size=11_435_334,
sha256="c9db54d75becfc9dfa6930d469b6f201f0f3919e36dfa7f488d0ee754ab5fe23",
),
),
required_files=("audiocpp_server.exe", "audiocpp_cli.exe"),
),
"cuda": RuntimeManifest(
backend="cuda",
assets=(
RuntimeAsset(
filename="audiocpp-windows-cuda-balance-238ab6a9.zip",
url=f"{_RELEASE_URL}/audiocpp-windows-cuda-balance-238ab6a9.zip",
size=248_519_503,
sha256="7e20f1fa984960b327700365a9a8434dc8effa0fcaa8d871c6cf5a35bf77b2a7",
),
RuntimeAsset(
filename="audiocpp-windows-cuda-runtime.zip",
url=f"{_RELEASE_URL}/audiocpp-windows-cuda-runtime.zip",
size=575_505_446,
sha256="46016655aff8f050806d81efd0fe256c15b86527935bfb3896208d4cac6b5ff8",
),
),
required_files=(
"audiocpp_server.exe",
"audiocpp_cli.exe",
"cublas64_13.dll",
"cublasLt64_13.dll",
"cufft64_12.dll",
),
),
}
@dataclass(frozen=True)
class RuntimeInstallResult:
manifest: RuntimeManifest
path: Path
executable: Path
already_present: bool = False
def get_runtime_manifest(backend: str) -> RuntimeManifest:
normalized = str(backend).strip().lower()
try:
return WINDOWS_RUNTIME_MANIFESTS[normalized]
except KeyError as exc:
raise RuntimeInstallError("audio.cpp runtime backend must be 'cpu' or 'cuda'") from exc
def runtime_install_path(runtime_root: Union[str, Path], backend: str) -> Path:
manifest = get_runtime_manifest(backend)
return (
Path(runtime_root).expanduser()
/ f"release-{manifest.release_version}"
/ f"windows-{manifest.backend}-{manifest.profile}"
)
def runtime_is_complete(path: Union[str, Path], manifest: RuntimeManifest) -> bool:
root = Path(path)
return root.is_dir() and all(
(root / relative).is_file() and (root / relative).stat().st_size > 0
for relative in manifest.required_files
)
def _sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as handle:
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def _safe_member_path(member_name: str) -> Path:
normalized = member_name.replace("\\", "/")
path = PurePosixPath(normalized)
if (
not normalized
or path.is_absolute()
or ".." in path.parts
or (path.parts and ":" in path.parts[0])
):
raise RuntimeInstallError(f"Unsafe path in runtime archive: {member_name!r}")
return Path(*path.parts)
def _extract_zip(archive: Path, destination: Path) -> None:
try:
with zipfile.ZipFile(archive) as bundle:
for info in bundle.infolist():
relative = _safe_member_path(info.filename)
unix_mode = info.external_attr >> 16
if stat.S_ISLNK(unix_mode):
raise RuntimeInstallError(f"Runtime archive contains a symlink: {info.filename}")
target = destination / relative
if info.is_dir():
target.mkdir(parents=True, exist_ok=True)
continue
target.parent.mkdir(parents=True, exist_ok=True)
with bundle.open(info, "r") as source, target.open("wb") as output:
shutil.copyfileobj(source, output, length=1024 * 1024)
except (OSError, zipfile.BadZipFile) as exc:
raise RuntimeInstallError(f"Cannot extract runtime archive {archive.name}: {exc}") from exc
def _remove_path(path: Path) -> None:
if path.is_symlink() or path.is_file():
path.unlink(missing_ok=True)
elif path.is_dir():
shutil.rmtree(path)
def _publish_directory(staging: Path, target: Path, overwrite: bool) -> None:
if not target.exists() and not target.is_symlink():
staging.rename(target)
return
if not overwrite:
raise RuntimeInstallError(f"Runtime target exists but is incomplete: {target}")
backup = target.with_name(f".{target.name}.{uuid.uuid4().hex}.backup")
target.rename(backup)
try:
staging.rename(target)
except BaseException:
if not target.exists() and backup.exists():
backup.rename(target)
raise
_remove_path(backup)
def install_windows_runtime(
runtime_root: Union[str, Path],
backend: str,
*,
overwrite: bool = False,
manifest: Optional[RuntimeManifest] = None,
platform_name: Optional[str] = None,
timeout: int = 600,
opener=None,
progress: Optional[ProgressCallback] = None,
) -> RuntimeInstallResult:
"""Download, hash, extract, validate, and atomically publish a runtime."""
platform_value = (platform_name or sys.platform).lower()
if platform_value not in {"win32", "windows"}:
raise RuntimeInstallError("Managed audio.cpp release binaries are currently Windows-only")
selected = manifest or get_runtime_manifest(backend)
if selected.backend != str(backend).strip().lower():
raise RuntimeInstallError("Runtime manifest backend does not match the requested backend")
target = runtime_install_path(runtime_root, selected.backend)
if runtime_is_complete(target, selected) and not overwrite:
return RuntimeInstallResult(
selected, target, target / "audiocpp_server.exe", already_present=True
)
if (target.exists() or target.is_symlink()) and not overwrite:
raise RuntimeInstallError(f"Runtime target exists but is incomplete: {target}")
target.parent.mkdir(parents=True, exist_ok=True)
work = Path(tempfile.mkdtemp(prefix=f".{target.name}.", suffix=".staging", dir=target.parent))
archives = work / "archives"
payload = work / "payload"
archives.mkdir()
payload.mkdir()
try:
for asset in selected.assets:
archive = archives / asset.filename
try:
downloaded = download_url_to_path(
asset.url,
archive,
timeout=timeout,
opener=opener,
progress=progress,
progress_label=asset.filename,
)
except AudioCppDownloadError as exc:
raise RuntimeInstallError(str(exc)) from exc
if downloaded != asset.size:
raise RuntimeInstallError(
f"Runtime asset {asset.filename} has size {downloaded}; expected {asset.size}"
)
actual_hash = _sha256(archive)
if actual_hash.lower() != asset.sha256.lower():
raise RuntimeInstallError(
f"SHA256 mismatch for {asset.filename}: expected {asset.sha256}, got {actual_hash}"
)
_extract_zip(archive, payload)
if not runtime_is_complete(payload, selected):
missing = [
item
for item in selected.required_files
if not (payload / item).is_file() or (payload / item).stat().st_size == 0
]
raise RuntimeInstallError(f"Runtime staging validation failed; missing: {missing}")
_publish_directory(payload, target, overwrite=overwrite)
return RuntimeInstallResult(selected, target, target / "audiocpp_server.exe")
finally:
_remove_path(work)