aboutsummaryrefslogtreecommitdiff
path: root/backends/envs.py
diff options
context:
space:
mode:
Diffstat (limited to 'backends/envs.py')
-rw-r--r--backends/envs.py185
1 files changed, 0 insertions, 185 deletions
diff --git a/backends/envs.py b/backends/envs.py
deleted file mode 100644
index 7596db6..0000000
--- a/backends/envs.py
+++ /dev/null
@@ -1,185 +0,0 @@
-"""The managed Python environment for the audiobook generator and its backends.
-
-audiobook.py is meant to be launched from any Python (a bare system interpreter
-is fine): on startup it bootstraps a single tool-managed venv at
-``envs/tts`` and re-execs itself inside it. That venv holds both the
-audiobook app's own ``requirements.txt`` dependencies and the backend TTS
-packages (``qwen-tts``, ``faster-qwen3-tts[demo]``) the setup wizards pip
-install, so nothing is ever installed into the launching interpreter's
-environment.
-
-A parent process never needs to "activate" an environment — activation is
-just a shell convenience that puts an env's ``bin`` on PATH. Instead every
-helper here resolves the env's binaries by absolute path
-(``envs/tts/bin/python``, ``envs/tts/bin/qwen-tts-demo``), so the hub can
-spawn servers in this env from any parent environment.
-
-This module is imported before audiobook.py's third-party dependencies, so
-it must stay stdlib-only (it may import ``backends.common``, which is also
-stdlib-only, but never ``converter`` or the backend modules).
-"""
-
-import hashlib
-import os
-import sys
-from pathlib import Path
-from typing import List
-
-from backends import common
-
-# The tts-audiobook-generator checkout root (where audiobook.py lives).
-TTS_ROOT = Path(__file__).resolve().parent.parent
-
-# One shared venv for the app requirements and every pip-installed backend.
-ENV_DIR = TTS_ROOT / "envs" / "tts"
-REQUIREMENTS_PATH = TTS_ROOT / "requirements.txt"
-
-# Marker file recording the requirements.txt hash last installed into the env,
-# so ensure_app_env() re-installs when requirements.txt changes.
-MARKER_PATH = ENV_DIR / ".audiobook_env_ready"
-
-
-def _is_windows() -> bool:
- return sys.platform == "win32"
-
-
-def env_python() -> Path:
- """Absolute path to the venv's python interpreter."""
- return ENV_DIR / ("Scripts/python.exe" if _is_windows() else "bin/python")
-
-
-def env_script(name: str) -> Path:
- """Absolute path to a console script installed in the venv (e.g. qwen-tts-demo)."""
- subdir = "Scripts" if _is_windows() else "bin"
- suffix = ".exe" if _is_windows() else ""
- return ENV_DIR / subdir / f"{name}{suffix}"
-
-
-def env_exists() -> bool:
- """True when the venv's python interpreter is present on disk."""
- return env_python().is_file()
-
-
-def is_managed_env() -> bool:
- """True when the current process is already running inside the managed venv."""
- try:
- return Path(sys.executable).resolve() == env_python().resolve()
- except OSError:
- return False
-
-
-def create_env() -> int:
- """Create the venv with the launching interpreter (inherits its version).
-
- pip is bootstrapped inside the venv by ensurepip. Returns the ``python -m
- venv`` exit code; a non-zero result is reported with platform remediation.
- """
- print(f"[INFO] creating managed environment at {ENV_DIR}...")
- rc = common.run_console_subprocess(
- [sys.executable, "-m", "venv", str(ENV_DIR)])
- if rc != 0:
- print(f"[ERROR] python -m venv failed (exit {rc}).")
- if _is_windows():
- print(" On Windows make sure the launcher has the venv module.")
- else:
- print(" On Debian/Ubuntu install the venv package, e.g.:")
- print(" sudo apt install python3-venv")
- return rc
-
-
-def install_requirements() -> int:
- """pip install -r requirements.txt into the venv. Returns pip's exit code."""
- print(f"[INFO] pip install -r {REQUIREMENTS_PATH} into {ENV_DIR}...")
- return common.run_console_subprocess(
- [str(env_python()), "-m", "pip", "install", "-r", str(REQUIREMENTS_PATH)])
-
-
-def pip_install(packages: List[str]) -> int:
- """pip install PACKAGES into the venv, creating it first if needed.
-
- Used by the qwen/faster setup wizards to install backend TTS packages
- alongside the app requirements. Returns pip's exit code.
- """
- if not env_exists() and create_env() != 0:
- return 1
- print(f"[INFO] pip install {' '.join(packages)} into {ENV_DIR}...")
- return common.run_console_subprocess(
- [str(env_python()), "-m", "pip", "install", *packages])
-
-
-def module_available(module: str) -> bool:
- """True when MODULE imports inside the venv (e.g. qwen_tts, faster_qwen3_tts).
-
- A short subprocess probe against the venv's interpreter — the equivalent of
- importlib.util.find_spec, but for the managed env rather than the current
- one. Used by each backend's ``_is_installed``.
- """
- if not env_exists():
- return False
- import subprocess
- try:
- result = subprocess.run(
- [str(env_python()), "-c", f"import {module}"],
- stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
- timeout=30, check=False)
- except (OSError, subprocess.TimeoutExpired):
- return False
- return result.returncode == 0
-
-
-def _requirements_sha() -> str:
- try:
- data = REQUIREMENTS_PATH.read_bytes()
- except OSError:
- return ""
- return hashlib.sha256(data).hexdigest()
-
-
-def _marker_valid() -> bool:
- try:
- return MARKER_PATH.read_text(encoding="utf-8").strip() == _requirements_sha()
- except OSError:
- return False
-
-
-def _write_marker() -> None:
- try:
- MARKER_PATH.write_text(_requirements_sha() + "\n", encoding="utf-8")
- except OSError:
- pass
-
-
-def ensure_app_env() -> None:
- """Make sure the venv exists and has the current requirements.txt installed.
-
- Creates the venv when missing, and (re)installs requirements.txt when it is
- missing or has changed since the last install (tracked by a hash marker).
- Raises RuntimeError on any failure so the caller can abort before re-exec.
- """
- if not env_exists() and create_env() != 0:
- raise RuntimeError("could not create the managed environment")
- if not _marker_valid():
- if install_requirements() != 0:
- raise RuntimeError("pip install -r requirements.txt failed")
- _write_marker()
-
-
-def bootstrap(script_path: str) -> None:
- """Run audiobook.py inside the managed venv, creating it first if needed.
-
- A no-op when the current process is already the venv's interpreter. Otherwise
- ensures the env (and requirements) are ready, then replaces the process with
- the venv's python running the same script and CLI args. Called at the top of
- audiobook.py before any third-party import.
- """
- if is_managed_env():
- return
- try:
- ensure_app_env()
- except RuntimeError as exc:
- print(f"[FATAL] {exc}", file=sys.stderr)
- sys.exit(1)
- py = str(env_python())
- target = str(Path(script_path).resolve())
- print(f"[INFO] re-launching inside managed environment: {py}")
- os.execv(py, [py, target, *sys.argv[1:]])