420 lines
16 KiB
Makefile
420 lines
16 KiB
Makefile
# Voicebox development commands
|
|
# Install: brew install just (or cargo install just)
|
|
# Usage: just --list
|
|
|
|
# Directories
|
|
backend_dir := "backend"
|
|
tauri_dir := "tauri"
|
|
app_dir := "app"
|
|
web_dir := "web"
|
|
venv := backend_dir / "venv"
|
|
|
|
# Platform-aware paths
|
|
venv_bin := if os() == "windows" { venv / "Scripts" } else { venv / "bin" }
|
|
python := if os() == "windows" { venv_bin / "python.exe" } else { venv_bin / "python" }
|
|
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` }
|
|
|
|
# ─── Setup ────────────────────────────────────────────────────────────
|
|
|
|
# Full project setup (python venv + JS deps + dev sidecar)
|
|
setup: setup-python setup-js
|
|
@echo ""
|
|
@echo "Setup complete! Run: just dev"
|
|
|
|
# Create venv and install Python dependencies
|
|
[unix]
|
|
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 python@3.12"
|
|
fi
|
|
{{ system_python }} -m venv {{ venv }}
|
|
fi
|
|
echo "Installing Python dependencies..."
|
|
{{ pip }} install --upgrade pip -q
|
|
{{ pip }} install -r {{ backend_dir }}/requirements.txt
|
|
# Chatterbox pins numpy<1.26 / torch==2.6 which break on Python 3.12+
|
|
{{ pip }} install --no-deps chatterbox-tts
|
|
# HumeAI TADA pins torch>=2.7,<2.8 which conflicts with our torch>=2.1
|
|
{{ pip }} install --no-deps hume-tada
|
|
# Apple Silicon: install MLX backend
|
|
if [ "$(uname -m)" = "arm64" ] && [ "$(uname)" = "Darwin" ]; then
|
|
echo "Detected Apple Silicon — installing MLX dependencies..."
|
|
{{ pip }} install -r {{ backend_dir }}/requirements-mlx.txt
|
|
fi
|
|
{{ pip }} install git+https://github.com/QwenLM/Qwen3-TTS.git
|
|
{{ pip }} install pyinstaller ruff pytest pytest-asyncio -q
|
|
echo "Python environment ready."
|
|
|
|
[windows]
|
|
setup-python:
|
|
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 "Installing Python dependencies..."
|
|
& "{{ python }}" -m pip install --upgrade pip -q
|
|
$gpus = Get-CimInstance Win32_VideoController | Select-Object -ExpandProperty Name
|
|
Write-Host "Detected GPUs: $($gpus -join ', ')"
|
|
$hasNvidia = ($gpus | Where-Object { $_ -match 'NVIDIA' }).Count -gt 0
|
|
$hasIntelArc = ($gpus | Where-Object { $_ -match 'Arc' }).Count -gt 0
|
|
if ($hasNvidia) { \
|
|
Write-Host "NVIDIA GPU detected — installing PyTorch with CUDA support..."; \
|
|
& "{{ pip }}" install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu128; \
|
|
} elseif ($hasIntelArc) { \
|
|
Write-Host "Intel Arc GPU detected — installing PyTorch with XPU support..."; \
|
|
& "{{ pip }}" install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/xpu; \
|
|
& "{{ pip }}" install intel-extension-for-pytorch --index-url https://download.pytorch.org/whl/xpu; \
|
|
} else { \
|
|
Write-Host "No NVIDIA or Intel Arc GPU detected — using CPU-only PyTorch."; \
|
|
Write-Host "If you have an Intel Arc GPU, install XPU support manually:"; \
|
|
Write-Host " pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/xpu"; \
|
|
Write-Host " pip install intel-extension-for-pytorch --index-url https://download.pytorch.org/whl/xpu"; \
|
|
}
|
|
& "{{ pip }}" install -r {{ backend_dir }}/requirements.txt
|
|
& "{{ pip }}" install --no-deps chatterbox-tts
|
|
& "{{ pip }}" install --no-deps hume-tada
|
|
& "{{ pip }}" install git+https://github.com/QwenLM/Qwen3-TTS.git
|
|
& "{{ pip }}" install pyinstaller ruff pytest pytest-asyncio -q
|
|
Write-Host "Python environment ready."
|
|
|
|
# Install JavaScript dependencies
|
|
setup-js:
|
|
bun install
|
|
|
|
# ─── Development ──────────────────────────────────────────────────────
|
|
|
|
# Start backend (if not already running) + frontend for development
|
|
[unix]
|
|
dev: _ensure-venv _ensure-sidecar
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
backend_pid=""
|
|
if curl -sf http://127.0.0.1:17493/health > /dev/null 2>&1; then
|
|
echo "Backend already running on http://localhost:17493"
|
|
else
|
|
echo "Starting backend on http://localhost:17493 ..."
|
|
{{ venv_bin }}/uvicorn backend.main:app --reload --port 17493 &
|
|
backend_pid=$!
|
|
sleep 2
|
|
fi
|
|
|
|
trap '[ -n "$backend_pid" ] && kill "$backend_pid" 2>/dev/null; wait' EXIT
|
|
|
|
echo "Starting Tauri desktop app..."
|
|
cd {{ tauri_dir }} && bun run tauri dev
|
|
|
|
[windows]
|
|
dev: _ensure-venv _ensure-sidecar
|
|
$backendJob = $null; \
|
|
try { $null = Invoke-WebRequest -Uri "http://127.0.0.1:17493/health" -UseBasicParsing -TimeoutSec 2 -ErrorAction Stop; Write-Host "Backend already running on http://localhost:17493" } catch { \
|
|
Write-Host "Starting backend on http://localhost:17493 ..."; \
|
|
$backendJob = Start-Process -PassThru -NoNewWindow -FilePath "{{ python }}" -ArgumentList "-m","uvicorn","backend.main:app","--reload","--port","17493"; \
|
|
Start-Sleep -Seconds 2; \
|
|
}; \
|
|
Write-Host "Starting Tauri desktop app..."; \
|
|
try { Set-Location "{{ tauri_dir }}"; bun run tauri dev } finally { if ($backendJob) { taskkill /PID $backendJob.Id /T /F 2>$null | Out-Null } }
|
|
|
|
# Start backend only
|
|
[unix]
|
|
dev-backend: _ensure-venv
|
|
{{ venv_bin }}/uvicorn backend.main:app --reload --port 17493
|
|
|
|
[windows]
|
|
dev-backend: _ensure-venv
|
|
& "{{ python }}" -m uvicorn backend.main:app --reload --port 17493
|
|
|
|
# Start Tauri desktop app only (backend must be running separately)
|
|
[unix]
|
|
dev-frontend: _ensure-sidecar
|
|
cd {{ tauri_dir }} && bun run tauri dev
|
|
|
|
[windows]
|
|
dev-frontend: _ensure-sidecar
|
|
Set-Location "{{ tauri_dir }}"; bun run tauri dev
|
|
|
|
# Start backend (if not already running) + web app (no Tauri)
|
|
[unix]
|
|
dev-web: _ensure-venv
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
backend_pid=""
|
|
if curl -sf http://127.0.0.1:17493/health > /dev/null 2>&1; then
|
|
echo "Backend already running on http://localhost:17493"
|
|
else
|
|
echo "Starting backend on http://localhost:17493 ..."
|
|
{{ venv_bin }}/uvicorn backend.main:app --reload --port 17493 &
|
|
backend_pid=$!
|
|
sleep 2
|
|
fi
|
|
|
|
trap '[ -n "$backend_pid" ] && kill "$backend_pid" 2>/dev/null; wait' EXIT
|
|
|
|
cd {{ web_dir }} && bun run dev
|
|
|
|
[windows]
|
|
dev-web: _ensure-venv
|
|
$backendJob = $null; \
|
|
try { $null = Invoke-WebRequest -Uri "http://127.0.0.1:17493/health" -UseBasicParsing -TimeoutSec 2 -ErrorAction Stop; Write-Host "Backend already running on http://localhost:17493" } catch { \
|
|
Write-Host "Starting backend on http://localhost:17493 ..."; \
|
|
$backendJob = Start-Process -PassThru -NoNewWindow -FilePath "{{ python }}" -ArgumentList "-m","uvicorn","backend.main:app","--reload","--port","17493"; \
|
|
Start-Sleep -Seconds 2; \
|
|
}; \
|
|
Write-Host "Starting web app..."; \
|
|
try { Set-Location "{{ web_dir }}"; bun run dev } finally { if ($backendJob) { taskkill /PID $backendJob.Id /T /F 2>$null | Out-Null } }
|
|
|
|
# Kill all dev processes
|
|
[unix]
|
|
kill:
|
|
-pkill -f "uvicorn backend.main:app" 2>/dev/null || true
|
|
-pkill -f "vite" 2>/dev/null || true
|
|
@echo "Dev processes killed."
|
|
|
|
[windows]
|
|
kill:
|
|
Get-Process -ErrorAction SilentlyContinue | Where-Object { $_.CommandLine -like '*uvicorn*backend.main*' -or $_.CommandLine -like '*vite*' } | Stop-Process -Force -ErrorAction SilentlyContinue
|
|
Write-Host "Dev processes killed."
|
|
|
|
# ─── Build ────────────────────────────────────────────────────────────
|
|
|
|
# Build everything (server binary + desktop app)
|
|
build: build-server build-tauri
|
|
|
|
# Build Python server binary (CPU)
|
|
[unix]
|
|
build-server: _ensure-venv
|
|
PATH="{{ venv_bin }}:$PATH" ./scripts/build-server.sh
|
|
|
|
[windows]
|
|
build-server: _ensure-venv
|
|
$ErrorActionPreference = "Stop"; \
|
|
$env:PATH = "{{ venv_bin }};$env:PATH"; \
|
|
& "{{ python }}" backend/build_binary.py; \
|
|
if ($LASTEXITCODE -ne 0) { throw "build_binary.py failed with exit code $LASTEXITCODE" }; \
|
|
$triple = (rustc --print host-tuple); \
|
|
New-Item -ItemType Directory -Path "{{ tauri_dir }}/src-tauri/binaries" -Force | Out-Null; \
|
|
Copy-Item "backend/dist/voicebox-server.exe" "{{ tauri_dir }}/src-tauri/binaries/voicebox-server-$triple.exe" -Force; \
|
|
Write-Host "Copied sidecar: voicebox-server-$triple.exe"
|
|
|
|
# Build CUDA server binary and place in app data dir for local testing
|
|
[windows]
|
|
build-server-cuda: _ensure-venv
|
|
$ErrorActionPreference = "Stop"; \
|
|
$env:PATH = "{{ venv_bin }};$env:PATH"; \
|
|
& "{{ python }}" backend/build_binary.py --cuda; \
|
|
if ($LASTEXITCODE -ne 0) { throw "build_binary.py --cuda failed with exit code $LASTEXITCODE" }; \
|
|
$dest = "$env:APPDATA/sh.voicebox.app/backends/cuda"; \
|
|
if (Test-Path $dest) { Remove-Item -Recurse -Force $dest }; \
|
|
New-Item -ItemType Directory -Path $dest -Force | Out-Null; \
|
|
Copy-Item "backend/dist/voicebox-server-cuda/*" $dest -Recurse -Force; \
|
|
Write-Host "Copied CUDA backend to $dest"
|
|
|
|
# Build everything locally: CPU server + CUDA server + installable Tauri app
|
|
[windows]
|
|
build-local: build-server build-server-cuda build-tauri
|
|
|
|
# Build Tauri desktop app
|
|
[unix]
|
|
build-tauri:
|
|
cd {{ tauri_dir }} && bun run tauri build
|
|
|
|
[windows]
|
|
build-tauri:
|
|
Set-Location "{{ tauri_dir }}"; bun run tauri build
|
|
|
|
# Build web app
|
|
[unix]
|
|
build-web:
|
|
cd {{ web_dir }} && bun run build
|
|
|
|
[windows]
|
|
build-web:
|
|
Set-Location "{{ web_dir }}"; bun run build
|
|
|
|
# ─── Code Quality ────────────────────────────────────────────────────
|
|
|
|
# Run all checks (JS + Python lint + format)
|
|
check: check-js check-python
|
|
|
|
# JS/TS: lint + format + typecheck (Biome)
|
|
check-js:
|
|
bun run check
|
|
|
|
# Python: lint + format check (ruff)
|
|
check-python: _ensure-venv
|
|
{{ venv_bin }}/ruff check {{ backend_dir }}
|
|
{{ venv_bin }}/ruff format --check {{ backend_dir }}
|
|
|
|
# Lint with Biome (JS) + ruff (Python)
|
|
lint: _ensure-venv
|
|
bun run lint
|
|
{{ venv_bin }}/ruff check {{ backend_dir }}
|
|
|
|
# Format with Biome (JS) + ruff (Python)
|
|
format: _ensure-venv
|
|
bun run format
|
|
{{ venv_bin }}/ruff format {{ backend_dir }}
|
|
|
|
# Fix lint + format issues (JS + Python)
|
|
fix: _ensure-venv
|
|
bun run check:fix
|
|
{{ venv_bin }}/ruff check {{ backend_dir }} --fix
|
|
{{ venv_bin }}/ruff format {{ backend_dir }}
|
|
|
|
# Python lint only
|
|
lint-python: _ensure-venv
|
|
{{ venv_bin }}/ruff check {{ backend_dir }}
|
|
|
|
# Python format only
|
|
format-python: _ensure-venv
|
|
{{ venv_bin }}/ruff format {{ backend_dir }}
|
|
|
|
# Python auto-fix lint issues
|
|
fix-python: _ensure-venv
|
|
{{ venv_bin }}/ruff check {{ backend_dir }} --fix
|
|
{{ venv_bin }}/ruff format {{ backend_dir }}
|
|
|
|
# Run Python tests
|
|
test: _ensure-venv
|
|
{{ venv_bin }}/python -m pytest {{ backend_dir }}/tests -v
|
|
|
|
# E2E: generate with every TTS model against the frozen binary (pass extra flags like --only kokoro)
|
|
[unix]
|
|
test-models *ARGS: _ensure-venv
|
|
{{ venv_bin }}/python {{ backend_dir }}/tests/test_all_models_e2e.py {{ ARGS }}
|
|
|
|
[windows]
|
|
test-models *ARGS: _ensure-venv
|
|
& "{{ python }}" {{ backend_dir }}/tests/test_all_models_e2e.py {{ ARGS }}
|
|
|
|
# ─── Database ─────────────────────────────────────────────────────────
|
|
|
|
# Initialize SQLite database
|
|
[unix]
|
|
db-init: _ensure-venv
|
|
{{ python }} -c "from backend.database import init_db; init_db()"
|
|
|
|
[windows]
|
|
db-init: _ensure-venv
|
|
& "{{ python }}" -c "from backend.database import init_db; init_db()"
|
|
|
|
# Reset database (delete + reinit)
|
|
[unix]
|
|
db-reset:
|
|
rm -f {{ backend_dir }}/data/voicebox.db
|
|
just db-init
|
|
|
|
[windows]
|
|
db-reset:
|
|
if (Test-Path "{{ backend_dir }}/data/voicebox.db") { Remove-Item -Force "{{ backend_dir }}/data/voicebox.db" }
|
|
just db-init
|
|
|
|
# ─── Utilities ────────────────────────────────────────────────────────
|
|
|
|
# Generate TypeScript API client (backend must be running)
|
|
[unix]
|
|
generate-api:
|
|
./scripts/generate-api.sh
|
|
|
|
[windows]
|
|
generate-api:
|
|
bash scripts/generate-api.sh
|
|
|
|
# Open API docs in browser
|
|
[unix]
|
|
docs:
|
|
open http://localhost:17493/docs 2>/dev/null || xdg-open http://localhost:17493/docs
|
|
|
|
[windows]
|
|
docs:
|
|
Start-Process "http://localhost:17493/docs"
|
|
|
|
# Tail backend logs
|
|
[unix]
|
|
logs:
|
|
tail -f {{ backend_dir }}/logs/*.log 2>/dev/null || echo "No log files found"
|
|
|
|
[windows]
|
|
logs:
|
|
Get-ChildItem {{ backend_dir }}/logs/*.log -ErrorAction SilentlyContinue | ForEach-Object { Get-Content $_.FullName -Tail 50 -Wait } ; if (-not $?) { Write-Host "No log files found" }
|
|
|
|
# ─── Clean ────────────────────────────────────────────────────────────
|
|
|
|
# Clean build artifacts
|
|
[unix]
|
|
clean:
|
|
rm -rf {{ tauri_dir }}/src-tauri/target/release
|
|
rm -rf {{ web_dir }}/dist
|
|
rm -rf {{ app_dir }}/dist
|
|
|
|
[windows]
|
|
clean:
|
|
if (Test-Path "{{ tauri_dir }}/src-tauri/target/release") { Remove-Item -Recurse -Force "{{ tauri_dir }}/src-tauri/target/release" }
|
|
if (Test-Path "{{ web_dir }}/dist") { Remove-Item -Recurse -Force "{{ web_dir }}/dist" }
|
|
if (Test-Path "{{ app_dir }}/dist") { Remove-Item -Recurse -Force "{{ app_dir }}/dist" }
|
|
|
|
# Clean Python venv and cache
|
|
[unix]
|
|
clean-python:
|
|
rm -rf {{ venv }}
|
|
find {{ backend_dir }} -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true
|
|
|
|
[windows]
|
|
clean-python:
|
|
if (Test-Path "{{ venv }}") { Remove-Item -Recurse -Force "{{ venv }}" }
|
|
Get-ChildItem -Path "{{ backend_dir }}" -Directory -Recurse -Filter "__pycache__" -ErrorAction SilentlyContinue | Remove-Item -Recurse -Force
|
|
|
|
# Nuclear clean (everything including node_modules)
|
|
[unix]
|
|
clean-all: clean clean-python
|
|
rm -rf node_modules
|
|
rm -rf {{ app_dir }}/node_modules
|
|
rm -rf {{ tauri_dir }}/node_modules
|
|
rm -rf {{ web_dir }}/node_modules
|
|
cd {{ tauri_dir }}/src-tauri && cargo clean
|
|
|
|
[windows]
|
|
clean-all: clean clean-python
|
|
if (Test-Path "node_modules") { Remove-Item -Recurse -Force "node_modules" }
|
|
if (Test-Path "{{ app_dir }}/node_modules") { Remove-Item -Recurse -Force "{{ app_dir }}/node_modules" }
|
|
if (Test-Path "{{ tauri_dir }}/node_modules") { Remove-Item -Recurse -Force "{{ tauri_dir }}/node_modules" }
|
|
if (Test-Path "{{ web_dir }}/node_modules") { Remove-Item -Recurse -Force "{{ web_dir }}/node_modules" }
|
|
Push-Location "{{ tauri_dir }}/src-tauri"; cargo clean; Pop-Location
|
|
|
|
# ─── Internal ─────────────────────────────────────────────────────────
|
|
|
|
# Ensure venv exists (prompt to run setup if not)
|
|
[private, unix]
|
|
_ensure-venv:
|
|
#!/usr/bin/env bash
|
|
if [ ! -d "{{ venv }}" ]; then
|
|
echo "Python venv not found. Run: just setup"
|
|
exit 1
|
|
fi
|
|
|
|
[private, windows]
|
|
_ensure-venv:
|
|
if (-not (Test-Path "{{ venv }}")) { Write-Host "Python venv not found. Run: just setup"; exit 1 }
|
|
|
|
# Ensure Tauri dev sidecar placeholder exists
|
|
[private]
|
|
_ensure-sidecar:
|
|
bun run setup:dev
|