Files
diodiogod-TTS-Audio-Suite/utils/runtimes/bootstrap.py
diodiogod 4937ff9a5b Repair engine runtime installation for issue 322
- Restore Fish S2 implicit namespace packages correctly
- Use the Dots TTS normalizer fallback for incomplete tn installations
- Remove conflicting WebRTC dependencies from VibeVoice runtimes
- Rebuild isolated runtimes when profile definitions change
- Add focused installer regression coverage
2026-07-15 06:53:52 -03:00

277 lines
8.8 KiB
Python

from __future__ import annotations
"""
Bootstrap isolated Python runtimes on demand.
"""
import json
import os
import shutil
import subprocess
import sys
from dataclasses import asdict
from datetime import datetime, timezone
from pathlib import Path
from typing import Optional
from .profiles import RuntimeProfile
PROJECT_ROOT = Path(__file__).resolve().parents[2]
RUNTIME_ROOT = PROJECT_ROOT / "runtimes"
BOOTSTRAP_STRATEGY = "profile_packages_v6"
def _venv_python_path(runtime_dir: Path) -> Path:
if os.name == "nt":
return runtime_dir / "Scripts" / "python.exe"
return runtime_dir / "bin" / "python"
def resolve_runtime_dir(profile: RuntimeProfile) -> Path:
hint = Path(profile.python_path_hint or "")
if hint.parts and hint.parts[0] == "runtimes" and len(hint.parents) >= 2:
return PROJECT_ROOT / hint.parent.parent
return RUNTIME_ROOT / profile.name
def resolve_runtime_python(profile: RuntimeProfile) -> Path:
return _venv_python_path(resolve_runtime_dir(profile))
def _venv_site_packages_path(runtime_dir: Path) -> Path:
if os.name == "nt":
return runtime_dir / "Lib" / "site-packages"
major = sys.version_info.major
minor = sys.version_info.minor
return runtime_dir / "lib" / f"python{major}.{minor}" / "site-packages"
def _detect_base_runtime_context(source_python: str) -> Optional[dict]:
probe = (
"import json\n"
"import os\n"
"import site\n"
"try:\n"
" import torch\n"
"except Exception:\n"
" torch = None\n"
"try:\n"
" import torchaudio\n"
"except Exception:\n"
" torchaudio = None\n"
"print(json.dumps({"
"'torch': getattr(torch, '__version__', None), "
"'torchaudio': getattr(torchaudio, '__version__', None), "
"'site_packages': site.getsitepackages()"
"}))\n"
)
try:
result = subprocess.run(
[source_python, "-c", probe],
cwd=str(PROJECT_ROOT),
check=True,
capture_output=True,
text=True,
)
payload = json.loads(result.stdout.strip())
return payload
except Exception:
return None
def _inherit_base_site_packages(runtime_dir: Path, source_python: str) -> Optional[str]:
context = _detect_base_runtime_context(source_python)
if not context:
return None
site_packages = context.get("site_packages") or []
if not site_packages:
return None
base_site_packages = None
for candidate in site_packages:
if candidate and os.path.exists(os.path.join(candidate, "torch")):
base_site_packages = candidate
break
if not base_site_packages:
base_site_packages = next((candidate for candidate in site_packages if candidate), None)
if not base_site_packages:
return None
venv_site_packages = _venv_site_packages_path(runtime_dir)
venv_site_packages.mkdir(parents=True, exist_ok=True)
pth_path = venv_site_packages / "_tts_audio_suite_base_runtime.pth"
pth_path.write_text(base_site_packages + "\n", encoding="utf-8")
print(f"🔧 Reusing base runtime site-packages: {base_site_packages}")
return base_site_packages
def _run_bootstrap_command(command: list[str]) -> subprocess.CompletedProcess:
return subprocess.run(
command,
cwd=str(PROJECT_ROOT),
check=False,
capture_output=True,
text=True,
)
def _ensure_virtualenv_available(source_python: str) -> None:
probe = subprocess.run(
[source_python, "-m", "virtualenv", "--version"],
cwd=str(PROJECT_ROOT),
check=False,
capture_output=True,
text=True,
)
if probe.returncode == 0:
return
print("🔧 Installing virtualenv in base runtime for isolated-runtime bootstrap fallback")
install = subprocess.run(
[source_python, "-m", "pip", "install", "virtualenv"],
cwd=str(PROJECT_ROOT),
check=False,
capture_output=True,
text=True,
)
if install.returncode != 0:
stderr = (install.stderr or "").strip()
stdout = (install.stdout or "").strip()
details = stderr or stdout or "unknown error"
raise RuntimeError(
"Failed to install virtualenv for isolated-runtime bootstrap fallback. "
f"Command: {source_python} -m pip install virtualenv\n{details}"
)
def _create_runtime_environment(runtime_dir: Path, source_python: str) -> None:
venv_command = [source_python, "-m", "venv", str(runtime_dir)]
venv_result = _run_bootstrap_command(venv_command)
if venv_result.returncode == 0:
return
print("⚠️ Standard venv bootstrap failed; trying virtualenv fallback")
shutil.rmtree(runtime_dir, ignore_errors=True)
_ensure_virtualenv_available(source_python)
virtualenv_command = [source_python, "-m", "virtualenv", str(runtime_dir)]
virtualenv_result = _run_bootstrap_command(virtualenv_command)
if virtualenv_result.returncode == 0:
return
stderr = (virtualenv_result.stderr or "").strip()
stdout = (virtualenv_result.stdout or "").strip()
fallback_details = stderr or stdout or "unknown error"
original_stderr = (venv_result.stderr or "").strip()
original_stdout = (venv_result.stdout or "").strip()
original_details = original_stderr or original_stdout or "unknown error"
raise RuntimeError(
"Failed to create isolated runtime environment.\n"
f"Primary command: {' '.join(venv_command)}\n"
f"Primary error: {original_details}\n"
f"Fallback command: {' '.join(virtualenv_command)}\n"
f"Fallback error: {fallback_details}"
)
def ensure_runtime(
profile: RuntimeProfile,
*,
base_python: Optional[str] = None,
) -> Path:
runtime_dir = resolve_runtime_dir(profile)
python_path = _venv_python_path(runtime_dir)
metadata_path = runtime_dir / "runtime_metadata.json"
if python_path.exists():
if metadata_path.exists():
try:
metadata = json.loads(metadata_path.read_text(encoding="utf-8"))
if (
metadata.get("bootstrap_strategy") == BOOTSTRAP_STRATEGY
and metadata.get("install_complete") is True
and metadata.get("profile") == asdict(profile)
):
return python_path
except Exception:
pass
print(
f"🔧 Rebuilding isolated runtime '{profile.name}' because it was created "
f"with an older bootstrap strategy or profile"
)
shutil.rmtree(runtime_dir, ignore_errors=True)
runtime_dir.parent.mkdir(parents=True, exist_ok=True)
source_python = base_python or sys.executable
print(
f"🔧 Creating isolated runtime '{profile.name}' at {runtime_dir} "
f"(reuses heavy base packages like PyTorch from the main runtime when configured)"
)
_create_runtime_environment(runtime_dir, source_python)
subprocess.run(
[str(python_path), "-m", "pip", "install", "--upgrade", "pip", "setuptools", "wheel"],
cwd=str(PROJECT_ROOT),
check=True,
)
inherited_site_packages = None
if profile.inherit_base_site_packages:
inherited_site_packages = _inherit_base_site_packages(runtime_dir, source_python)
if not inherited_site_packages:
print("⚠️ Failed to inherit base site-packages; isolated runtime will rely only on local installs")
if not profile.pip_packages:
raise RuntimeError(
f"Runtime profile '{profile.name}' has no package list. "
f"Do not route engines into isolated mode until that profile is defined."
)
print(f"🔧 Installing profile-specific dependencies for '{profile.name}'")
subprocess.run(
[str(python_path), "-m", "pip", "install", *profile.pip_packages],
cwd=str(PROJECT_ROOT),
check=True,
)
if profile.pip_packages_no_deps:
print(
f"🔧 Installing source packages without dependency resolution for "
f"'{profile.name}'"
)
subprocess.run(
[
str(python_path),
"-m",
"pip",
"install",
"--no-deps",
*profile.pip_packages_no_deps,
],
cwd=str(PROJECT_ROOT),
check=True,
)
metadata = {
"profile": asdict(profile),
"bootstrap_strategy": BOOTSTRAP_STRATEGY,
"install_complete": True,
"inherited_base_site_packages": inherited_site_packages,
"created_at": datetime.now(timezone.utc).isoformat(),
"source_python": source_python,
}
(runtime_dir / "runtime_metadata.json").write_text(
json.dumps(metadata, indent=2, ensure_ascii=True),
encoding="utf-8",
)
return python_path