aboutsummaryrefslogtreecommitdiff
path: root/lib/project/src/voiceforge/setup.py
diff options
context:
space:
mode:
authorhistoria <historiavg@proton.me>2026-09-11 17:41:33 -0400
committerhistoria <historiavg@proton.me>2026-09-11 17:41:33 -0400
commit077c82d9614328ff267632298da6c88009313337 (patch)
tree412f9e2d60c320e0c014eda45e76e36205b526ed /lib/project/src/voiceforge/setup.py
parent84dd2d068317998f6fb59400c534ef5be6b51b53 (diff)
downloadproducer-077c82d9614328ff267632298da6c88009313337.tar.gz
fix: consistently rename to producer
Diffstat (limited to 'lib/project/src/voiceforge/setup.py')
-rw-r--r--lib/project/src/voiceforge/setup.py203
1 files changed, 0 insertions, 203 deletions
diff --git a/lib/project/src/voiceforge/setup.py b/lib/project/src/voiceforge/setup.py
deleted file mode 100644
index 9afeb44..0000000
--- a/lib/project/src/voiceforge/setup.py
+++ /dev/null
@@ -1,203 +0,0 @@
-"""User-local tools. No sudo and no changes to the application's interpreter."""
-from __future__ import annotations
-
-from collections import deque
-from collections.abc import Callable
-import json
-import os
-from pathlib import Path
-import selectors
-import shutil
-import signal
-import subprocess
-import sys
-import time
-
-Progress = Callable[[str, float | None, float | None], None]
-
-
-def data_dir() -> Path:
- """The VoiceForge data/prefix directory.
-
- VOICEFORGE_HOME wins (the single-directory launcher exports it), then
- XDG_DATA_HOME/voiceforge, then ~/.local/share/voiceforge. A relative
- VOICEFORGE_HOME resolves against the current directory.
- """
- home = os.environ.get("VOICEFORGE_HOME")
- if home:
- return Path(home).absolute()
- root = Path(os.environ.get("XDG_DATA_HOME", str(Path.home() / ".local/share")))
- if not root.is_absolute():
- root = Path.home() / ".local/share"
- return root / "voiceforge"
-
-
-def tool_env() -> dict[str, str]:
- """Sanitized environment for bootstrap, setup, and worker subprocesses.
-
- Host Python settings are removed so a virtualenv or sitecustomize on the
- machine cannot leak into managed environments. Installation-target,
- index, and constraint overrides are dropped and package-manager
- configuration files disabled, so packages can only come from the
- explicitly passed trusted indexes and land inside the data directory;
- every cache (pip, uv, managed Python, XDG, temporary files) stays there
- too.
- """
- env = os.environ.copy()
- for name in ("PYTHONPATH", "PYTHONHOME", "VIRTUAL_ENV",
- "PIP_TARGET", "PIP_PREFIX", "PIP_USER", "PIP_FIND_LINKS",
- "PIP_INDEX_URL", "PIP_EXTRA_INDEX_URL", "PIP_CONSTRAINT",
- "UV_INDEX_URL", "UV_DEFAULT_INDEX", "UV_EXTRA_INDEX_URL",
- "UV_INDEX", "UV_FIND_LINKS", "UV_CONSTRAINT"):
- env.pop(name, None)
- env["PYTHONNOUSERSITE"] = "1"
- env["PIP_CONFIG_FILE"] = os.devnull
- env["UV_NO_CONFIG"] = "1"
- cache = data_dir() / "cache"
- env["XDG_CACHE_HOME"] = str(cache)
- env["PIP_CACHE_DIR"] = str(cache / "pip")
- env["UV_CACHE_DIR"] = str(cache / "uv")
- env["UV_PYTHON_INSTALL_DIR"] = str(data_dir() / "uv/python")
- tmp = data_dir() / "tmp"
- try:
- tmp.mkdir(parents=True, exist_ok=True)
- env["TMPDIR"] = str(tmp)
- except OSError:
- pass # Best effort: never break subprocesses over temporary-file containment.
- return env
-
-
-def terminate_group(process: subprocess.Popen) -> None:
- """SIGTERM the child's whole process group, escalate to SIGKILL, and reap.
-
- The group is signalled even when the leader has already exited, because
- descendants can outlive it while still holding the output pipe open.
- A race between exit and signalling is tolerated.
- """
- for sig in (signal.SIGTERM, signal.SIGKILL):
- try:
- os.killpg(process.pid, sig)
- except ProcessLookupError:
- pass
- try:
- process.wait(timeout=5)
- except subprocess.TimeoutExpired:
- continue
- try:
- os.killpg(process.pid, 0)
- except ProcessLookupError:
- return
- process.wait()
-
-
-def run_process(args: list[str], stage: str, progress: Progress | None = None,
- *, env: dict[str, str] | None = None) -> None:
- """Drain output without blocking; send heartbeat callbacks every 0.25s.
-
- Worker JSON records carry frame/byte counts. Other processes report elapsed seconds
- as completed with total=None. Only the last 64 KiB of diagnostics are retained.
- Callback exceptions (including cancellation) terminate the process group.
- """
- started = time.monotonic()
- current_stage, completed, total = stage, None, None
- diagnostics: deque[bytes] = deque(maxlen=16)
- pending = b""
- if progress:
- progress(stage, None, None)
- with subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
- env=env, start_new_session=True) as process:
- assert process.stdout is not None
- os.set_blocking(process.stdout.fileno(), False)
- try:
- with selectors.DefaultSelector() as selector:
- selector.register(process.stdout, selectors.EVENT_READ)
- while selector.get_map() or process.poll() is None:
- for key, _ in selector.select(timeout=0.25):
- chunk = os.read(key.fd, 4096)
- if not chunk:
- selector.unregister(key.fileobj)
- continue
- diagnostics.append(chunk)
- pending += chunk
- while b"\n" in pending:
- line, pending = pending.split(b"\n", 1)
- if line.startswith(b"VOICEFORGE_PROGRESS "):
- event = json.loads(line[len(b"VOICEFORGE_PROGRESS "):])
- current_stage, completed, total = event
- if len(pending) > 65536:
- pending = pending[-4096:]
- if progress:
- progress(current_stage, completed if total is not None else
- time.monotonic() - started, total)
- code = process.wait()
- if code:
- detail = b"".join(diagnostics).decode("utf-8", errors="replace")
- raise RuntimeError(f"{stage} failed (exit {code}):\n{detail}")
- except BaseException:
- terminate_group(process)
- raise
-
-
-def _uv_supports_relocatable(uv: str) -> bool:
- """Whether this uv understands the venv flags AI setup relies on.
-
- A host uv can be older than the pinned one; capability is checked
- directly instead of trusting the version string.
- """
- try:
- result = subprocess.run([uv, "venv", "--help"], capture_output=True,
- text=True, check=True)
- except (OSError, subprocess.SubprocessError):
- return False
- return "--relocatable" in result.stdout + result.stderr
-
-
-def ensure_uv(progress: Progress | None = None) -> str:
- """Return a uv that supports the flags AI setup relies on.
-
- The launcher's contained bootstrap uv is preferred; a host uv is trusted
- only if it actually supports --relocatable. Otherwise uv==0.8.17 is
- installed from PyPI (trust PyPI's distribution, not a downloaded shell
- script).
- """
- contained = data_dir() / "bootstrap/bin/uv"
- if contained.is_file() and os.access(contained, os.X_OK):
- return str(contained)
- found = shutil.which("uv")
- if found and _uv_supports_relocatable(found):
- return found
- root = contained.parent.parent
- root.parent.mkdir(parents=True, exist_ok=True)
- python = sys.executable or shutil.which("python3")
- env = tool_env()
- run_process([python, "-m", "venv", str(root)], "Creating uv bootstrap",
- progress, env=env)
- run_process([str(root / "bin/python"), "-m", "pip", "install",
- "--index-url", "https://pypi.org/simple", "uv==0.8.17"],
- "Installing uv from PyPI", progress, env=env)
- if not (contained.is_file() and os.access(contained, os.X_OK)):
- raise RuntimeError("Installing uv from PyPI did not produce a usable "
- "uv; check for host pip settings (such as "
- "PIP_TARGET) that redirect installations outside "
- "the data directory.")
- return str(contained)
-
-
-def ensure_ffmpeg() -> str:
- """Return the bundled FFmpeg, or a system one where the bundle is
- absent, not ffprobe.
-
- The launcher installs imageio-ffmpeg via the bundled-ffmpeg extra; direct
- package installs should declare it as a dependency. This helper never
- runs pip in the current interpreter.
- """
- try:
- import imageio_ffmpeg
- return imageio_ffmpeg.get_ffmpeg_exe()
- except (ImportError, RuntimeError) as error:
- found = shutil.which("ffmpeg")
- if found:
- return found
- raise RuntimeError("FFmpeg is unavailable. Rebuild the contained "
- "runtime with './producer.sh --rebuild', or "
- "install a system FFmpeg.") from error