fix(setup): pin dev venv to Python 3.12

This commit is contained in:
dhananjaypai08
2026-10-03 09:24:23 +00:00
committed by jamiepine
parent 51f49dea19
commit a14f0ce3d9
8 changed files with 147 additions and 32 deletions
+19
View File
@@ -7,6 +7,25 @@
## [Unreleased]
### Developer Experience
- **`just setup` now pins the Python venv to 3.12.** Setup preferred `python3.12` when
it found one but silently fell back to 3.13 or bare `python3`, so a machine on
3.13/3.14 built a venv that then failed to install dependencies capped below 3.13
(kokoro, misaki, numpy, numba) with `Could not find a version that satisfies the
requirement`. Setup now requires 3.12, and recreates a venv that is on the wrong
version or whose pip doesn't run, so a stale or half-built one heals on the next
`just setup` rather than needing a manual `rm -rf backend/venv`. Reporting 3.12
isn't enough for an interpreter to be chosen — it has to actually build a working
venv, so setup falls through to the next candidate when one can't (Debian packages
`ensurepip` separately as `python3.12-venv`; current Homebrew `[email protected]` bottles
carry a `pyexpat` that won't load on macOS 26). If none work it exits with the
install command for your platform instead of failing deep inside pip. Windows
resolves it through the `py -3.12` launcher, since Windows installers don't create
a `python3.12` command.
- **The 3.12 requirement is now machine-readable.** `backend/pyproject.toml`
declares `requires-python = ">=3.12,<3.13"` rather than an open-ended `>=3.12`,
so the constraint the docs describe is the one tooling resolves against.
### Linux
- **ROCm setup works on Linux AMD systems.** Docker ROCm builds now keep PyTorch
+7 -5
View File
@@ -18,9 +18,11 @@ Thank you for your interest in contributing to Voicebox! This document provides
curl -fsSL https://bun.sh/install | bash
```
- **[Python 3.11+](https://python.org)** - For backend development
- **[Python 3.12](https://python.org)** - For backend development. Voicebox pins its
virtual environment to 3.12 to match CI, because some dependencies have no 3.13
release yet. On Debian/Ubuntu also install `python3.12-venv`.
```bash
python --version # Should be 3.11 or higher
python3.12 --version # Should be 3.12.x
```
- **[Rust](https://rustup.rs)** - For Tauri desktop app (installed automatically by Tauri CLI)
@@ -44,7 +46,7 @@ just dev # starts backend + desktop app
```
`just setup` handles everything automatically, including:
- Creating a Python virtual environment
- Creating a Python virtual environment pinned to 3.12 (recreating it if an existing venv is on the wrong version)
- Installing Python dependencies (with CUDA PyTorch on Windows if an NVIDIA GPU is detected)
- Installing MLX dependencies on Apple Silicon
- Installing JavaScript dependencies
@@ -67,7 +69,7 @@ just --list # see all available commands
#### Windows Notes
The justfile works natively on Windows via PowerShell. No WSL or Git Bash required. On Windows with an NVIDIA GPU, `just setup` automatically installs CUDA-enabled PyTorch for GPU acceleration.
The justfile works natively on Windows via PowerShell. No WSL or Git Bash required. On Windows with an NVIDIA GPU, `just setup` automatically installs CUDA-enabled PyTorch for GPU acceleration. Python 3.12 is located via the `py -3.12` launcher; install it with `winget install -e --id Python.Python.3.12` if it's missing.
### Model Downloads
@@ -363,7 +365,7 @@ See [docs/content/docs/overview/troubleshooting.mdx](docs/content/docs/overview/
**Quick fixes:**
- **Backend won't start:** Check Python version (3.11+), ensure venv is activated, install dependencies
- **Backend won't start:** Check Python version (must be 3.12), ensure venv is activated, install dependencies
- **Tauri build fails:** Ensure Rust is installed, clean build with `cd tauri/src-tauri && cargo clean`
- **OpenAPI client generation fails:** Ensure backend is running, check `curl http://localhost:17493/openapi.json`
+1 -1
View File
@@ -417,7 +417,7 @@ just dev # starts backend + desktop app
Install [just](https://github.com/casey/just): `brew install just` or `cargo install just`. Run `just --list` to see all commands.
**Prerequisites:** [Bun](https://bun.sh), [Rust](https://rustup.rs), [Python 3.11+](https://python.org), [Tauri Prerequisites](https://v2.tauri.app/start/prerequisites/), and [Xcode](https://developer.apple.com/xcode/) on macOS.
**Prerequisites:** [Bun](https://bun.sh), [Rust](https://rustup.rs), [Python 3.12](https://python.org), [Tauri Prerequisites](https://v2.tauri.app/start/prerequisites/), and [Xcode](https://developer.apple.com/xcode/) on macOS.
The repo ships a pre-wired `.mcp.json` at the root — running Claude Code inside this checkout picks up the Voicebox MCP tools automatically once the dev app is running.
+6 -1
View File
@@ -1,7 +1,12 @@
[project]
name = "voicebox-backend"
version = "0.2.3"
requires-python = ">=3.12"
# This is the one place the Python pin is explained; the justfile and docs point here
# rather than restating it. The upper bound is real, not defensive: requirements.txt
# currently pulls in kokoro and misaki (which declare requires-python <3.13) plus
# numpy<2 and numba<0.61 (no 3.13 wheels), so `pip install -r requirements.txt` fails
# outright on a later interpreter. Recheck those four before raising it.
requires-python = ">=3.12,<3.13"
# ---------------------------------------------------------------------------
# Ruff – linter + formatter
+1 -1
View File
@@ -48,7 +48,7 @@ These two layers communicate via HTTP on `localhost:17493`, with the frontend ma
### Tech Stack
- **Framework**: FastAPI (Python 3.11+)
- **Framework**: FastAPI (Python 3.12)
- **TTS Engines**: Qwen3-TTS, Qwen CustomVoice, LuxTTS, Chatterbox, Chatterbox Turbo, TADA, Kokoro
- **Transcription**: Whisper (PyTorch or MLX-Whisper)
- **Inference Backends**: MLX (Apple Silicon), PyTorch (CUDA / ROCm / XPU / DirectML / CPU)
+7 -6
View File
@@ -32,11 +32,12 @@ Ensure you have these installed:
curl -fsSL https://bun.sh/install | bash
```
</Card>
<Card title="Python 3.11+" icon={<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>}>
<Card title="Python 3.12" icon={<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>}>
[Download Python](https://python.org)
```bash
python --version
python3.12 --version # Should be 3.12.x
```
Voicebox pins its venv to 3.12 to match CI, because some dependencies have no 3.13 release yet. On Debian/Ubuntu also install `python3.12-venv`.
</Card>
<Card title="Rust" icon={<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m6 9 6 6 6-6"/></svg>}>
[Install Rust](https://rustup.rs)
@@ -65,7 +66,7 @@ Run `just --list` to see all available commands. Highlights:
| Command | Description |
|---------|-------------|
| `just setup` | Full setup (Python venv + JS deps + dev sidecar). Detects Apple Silicon for MLX and NVIDIA/Intel Arc on Windows for accelerated PyTorch. |
| `just setup` | Full setup (Python venv + JS deps + dev sidecar). Pins the venv to Python 3.12 (recreating a wrong-version one) and detects Apple Silicon for MLX and NVIDIA/Intel Arc on Windows for accelerated PyTorch. |
| `just setup-python` | Python venv + dependencies only |
| `just setup-js` | `bun install` only |
@@ -214,8 +215,8 @@ This installs dependencies for:
```bash
cd backend
# Create virtual environment
python -m venv venv
# Create virtual environment (use Python 3.12 — see Prerequisites)
python3.12 -m venv venv
# Activate virtual environment
source venv/bin/activate # macOS/Linux
@@ -277,7 +278,7 @@ bun run tauri dev
<AccordionGroup>
<Accordion title="Backend won't start">
- Check Python version (must be 3.11+)
- Check Python version (must be 3.12)
- Ensure virtual environment is activated: `source backend/venv/bin/activate`
- Verify all dependencies are installed: `pip install -r requirements.txt`
- Check if port 17493 is available
@@ -292,13 +292,19 @@ This is expected behavior. The first generation downloads the selected TTS engin
<AccordionGroup>
<Accordion title="Python Version">
Ensure Python 3.11 or higher:
Ensure Python 3.12:
```bash
python --version
python3.12 --version
```
If not, install Python 3.11+ and recreate the virtual environment.
Voicebox pins its venv to Python 3.12 because some dependencies have no 3.13
release yet. `just setup` recreates a venv that was built on the wrong version,
so re-running it is usually the fix.
If `just setup` can't find a usable 3.12, install one: `uv python install 3.12`
works anywhere, or use `brew install [email protected]` on macOS and `sudo apt install
python3.12 python3.12-venv` on Debian/Ubuntu.
</Accordion>
<Accordion title="Virtual Environment">
+97 -15
View File
@@ -17,8 +17,11 @@ pip := if os() == "windows" { venv_bin / "pip.exe" } else { venv_bin / "pip" }
# Shell selection: use powershell on Windows, bash elsewhere
set windows-shell := ["powershell", "-NoProfile", "-Command"]
# Detect best python for venv creation (platform-aware)
system_python := if os() == "windows" { "python" } else { `command -v python3.12 2>/dev/null || command -v python3.13 2>/dev/null || echo python3` }
# Pin the venv to one version instead of following whatever `python3` happens to be;
# a newer interpreter fails `pip install` with "no matching distribution". See
# requires-python in backend/pyproject.toml for which dependencies set the ceiling.
# Keep in sync with that and with actions/setup-python in .github/workflows.
python_version := "3.12"
# ─── Setup ────────────────────────────────────────────────────────────
@@ -32,14 +35,66 @@ setup: setup-python setup-js
setup-python:
#!/usr/bin/env bash
set -euo pipefail
if [ ! -d "{{ venv }}" ]; then
echo "Creating Python virtual environment..."
PY_MINOR=$({{ system_python }} -c "import sys; print(sys.version_info[1])")
if [ "$PY_MINOR" -gt 13 ]; then
echo "Warning: Python 3.$PY_MINOR detected. ML packages may not be compatible."
echo "Recommended: brew install [email protected]"
is_pinned() {
"$1" -c 'import sys; sys.exit(sys.version_info[:2] != tuple(map(int, "{{ python_version }}".split("."))))' 2>/dev/null
}
# Testing for an executable file is not enough: console scripts hard-code the
# venv's absolute path in their shebang, so a venv that was built elsewhere and
# moved into place has a pip that exists, is executable, and still dies with
# "cannot execute: required file not found". Running it is the only real check.
pip_works() {
"{{ pip }}" --version >/dev/null 2>&1
}
# Every interpreter on PATH that could be the pinned version, plus uv's, which
# keeps its Pythons off PATH. More than one can match, and the first is not
# necessarily the good one.
candidates() {
type -aP "python{{ python_version }}" python3 python 2>/dev/null || true
if command -v uv >/dev/null 2>&1; then
uv python find "{{ python_version }}" 2>/dev/null || true
fi
{{ system_python }} -m venv {{ venv }}
}
# Reuse the venv only if it is on the pinned version and has a working pip: a
# wrong-version venv resolves dependencies against the wrong Python, and a
# broken pip means an earlier `-m venv` was interrupted or the venv was moved.
if [ -d "{{ venv }}" ] && ! { is_pinned "{{ python }}" && pip_works; }; then
echo "Existing venv is not a working Python {{ python_version }} environment — recreating..."
rm -rf "{{ venv }}"
fi
if [ ! -d "{{ venv }}" ]; then
# Building the venv is itself the test, so there is no separate probe to keep
# in sync. An interpreter can report the right version and still fail inside
# `-m venv` — Debian packages ensurepip separately, and Homebrew has shipped a
# pyexpat that won't dlopen — so fall through to the next candidate rather
# than giving up on the first. Their output is held back until every candidate
# has failed, so a run that recovers doesn't look like a broken one.
log="$(mktemp)"
while IFS= read -r py; do
is_pinned "$py" || continue
echo "Creating Python virtual environment with $py ..."
"$py" -m venv "{{ venv }}" >>"$log" 2>&1 && pip_works && break
echo " ...it reports {{ python_version }} but cannot build a working venv; trying the next."
rm -rf "{{ venv }}"
done < <(candidates | sed 's|//*|/|g' | awk '!seen[$0]++')
pip_works || cat "$log"
rm -f "$log"
fi
if ! pip_works; then
echo ""
echo "ERROR: Voicebox needs a Python {{ python_version }} that can create virtual"
echo " environments (see requires-python in backend/pyproject.toml)."
echo " Any: uv python install {{ python_version }}"
echo " macOS: brew install python@{{ python_version }}"
echo " Debian: sudo apt install python{{ python_version }} python{{ python_version }}-venv"
echo ""
echo "Then re-run: just setup"
exit 1
fi
echo "Installing Python dependencies..."
{{ pip }} install --upgrade pip -q
@@ -85,13 +140,40 @@ setup-python:
[windows]
setup-python:
$target = "{{ python_version }}"; \
function Test-Py($exe) { \
if (-not $exe -or -not (Test-Path $exe)) { return $false }; \
$v = & $exe -c "import sys; print('%s.%s' % sys.version_info[:2])" 2>$null; \
return ($LASTEXITCODE -eq 0 -and $v -eq $target); \
}; \
function Test-Pip { \
if (-not (Test-Path "{{ pip }}")) { return $false }; \
& "{{ pip }}" --version *> $null; \
return ($LASTEXITCODE -eq 0); \
}; \
$py = $null; \
if (Get-Command py -ErrorAction SilentlyContinue) { \
$p = & py "-$target" -c "import sys; print(sys.executable)" 2>$null; \
if ($LASTEXITCODE -eq 0) { $py = $p }; \
}; \
if (-not $py) { foreach ($n in @("python$target", "python")) { \
$c = Get-Command $n -ErrorAction SilentlyContinue; \
if ($c -and (Test-Py $c.Source)) { $py = $c.Source; break }; \
} }; \
if (-not $py) { \
Write-Host "ERROR: Voicebox requires Python $target (see requires-python in backend/pyproject.toml)."; \
Write-Host "Install it from https://python.org or: winget install -e --id Python.Python.$target"; \
Write-Host "Then re-run: just setup"; \
exit 1; \
}; \
if ((Test-Path "{{ venv }}") -and (-not (Test-Py "{{ python }}") -or -not (Test-Pip))) { \
Write-Host "Existing venv is not a working Python $target env - recreating..."; \
Remove-Item -Recurse -Force "{{ venv }}"; \
}; \
if (-not (Test-Path "{{ venv }}")) { \
Write-Host "Creating Python virtual environment..."; \
$pyMinor = & {{ system_python }} -c "import sys; print(sys.version_info[1])"; \
if ([int]$pyMinor -gt 13) { \
Write-Host "Warning: Python 3.$pyMinor detected. ML packages may not be compatible."; \
}; \
& {{ system_python }} -m venv {{ venv }}; \
Write-Host "Creating Python virtual environment with $py ..."; \
& $py -m venv "{{ venv }}"; \
if ($LASTEXITCODE -ne 0 -or -not (Test-Pip)) { Write-Host "ERROR: could not create a working venv with $py"; exit 1 }; \
}
Write-Host "Installing Python dependencies..."
& "{{ python }}" -m pip install --upgrade pip -q