setup.sh
Rama main · réplica de lectura del repositorio original.
#!/usr/bin/env bash
#
# First-time setup for the PolyTTS server.
# Creates a venv, installs dependencies, and downloads models.
#
# PyTorch (MPS on Mac, CUDA on Linux/Windows): uses the same HuggingFace
# models — setup downloads them below. MLX (Apple Silicon only): uses a
# different 8-bit model, downloaded automatically when you run with MLX.
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
cd "$SCRIPT_DIR"
# ── Colours ──────────────────────────────────────────────────────────────────
bold=$(tput bold 2>/dev/null || true)
reset=$(tput sgr0 2>/dev/null || true)
green=$(tput setaf 2 2>/dev/null || true)
red=$(tput setaf 1 2>/dev/null || true)
yellow=$(tput setaf 3 2>/dev/null || true)
info() { echo "${bold}${green}==> ${reset}${bold}$*${reset}"; }
warn() { echo "${bold}${yellow}==> ${reset}${bold}$*${reset}"; }
fail() { echo "${bold}${red}==> ${reset}${bold}$*${reset}"; exit 1; }
# ── Pre-flight checks ───────────────────────────────────────────────────────
info "Checking prerequisites…"
arch=$(uname -m)
echo " Architecture: $arch"
# Python 3.10–3.12 (the VoxCPM engine caps at <3.13; MLX/Qwen run on 3.12 too).
if ! command -v python3 &>/dev/null; then
fail "python3 not found. Install Python 3.10–3.12 first."
fi
py_version=$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')
py_major=$(echo "$py_version" | cut -d. -f1)
py_minor=$(echo "$py_version" | cut -d. -f2)
if (( py_major != 3 || py_minor < 10 || py_minor > 12 )); then
fail "Python 3.10–3.12 required (VoxCPM caps <3.13) — detected $py_version."
fi
echo " Python: $py_version"
# ── Virtual environment ──────────────────────────────────────────────────────
if [[ -d venv ]] && [[ ! -x venv/bin/python3 ]] && [[ ! -x venv/bin/python ]]; then
warn "Existing virtual environment looks stale — recreating it."
rm -rf venv
fi
if [[ -d venv ]]; then
info "Virtual environment already exists — skipping creation."
else
info "Creating virtual environment…"
python3 -m venv venv
fi
if [[ -x venv/bin/python3 ]]; then
VENV_PYTHON="venv/bin/python3"
elif [[ -x venv/bin/python ]]; then
VENV_PYTHON="venv/bin/python"
else
fail "Virtual environment was created, but no Python interpreter was found in venv/bin/."
fi
VENV_PIP="venv/bin/pip"
# ── Install dependencies ─────────────────────────────────────────────────────
info "Installing Python dependencies (PyTorch backend; same models for MPS and CUDA)…"
"$VENV_PIP" install --upgrade pip -q
"$VENV_PIP" install -r requirements.txt -q
# MLX is Apple Silicon only; optional for PyTorch+MPS/CUDA
if [[ "$arch" == "arm64" ]] && [[ "$(uname -s)" == "Darwin" ]]; then
info "Installing MLX backend (Apple Silicon)…"
"$VENV_PIP" install -r requirements-mlx.txt -q
fi
echo " Done."
# ── Livestack residency manager (polycore + shared_py) ───────────────────────
# On the PyTorch/CUDA runtime the server REQUIRES the polycore residency seam and
# its compiled shared_py planner (server.py: `_MANAGER_PATH = RUNTIME != "mlx"`,
# which imports `polycore` at module load). Both live in the livestack monorepo.
# Install them EDITABLE so the venv resolves them by package name at their current
# location — never a hardcoded path that goes stale when the repo moves. (The old
# breakage was a hand-written polycore.pth pointing at a since-moved directory.)
# Skipped under MLX (Apple Silicon), which runs the standalone residency path.
install_livestack_residency() {
if [[ "$arch" == "arm64" ]] && [[ "$(uname -s)" == "Darwin" ]]; then
echo " MLX runtime — standalone residency; polycore not required."
return
fi
local LIVESTACK_DIR="${LIVESTACK_DIR:-$HOME/livestack}"
if [[ ! -d "$LIVESTACK_DIR/polycore" ]]; then
warn "polycore not found under LIVESTACK_DIR=$LIVESTACK_DIR — point LIVESTACK_DIR at your livestack checkout and re-run. The CUDA server cannot start without it."
return
fi
# Prefer uv (this venv may be uv-managed and lack pip); fall back to venv pip.
local INSTALL=("$VENV_PIP" install)
command -v uv &>/dev/null && INSTALL=(uv pip install --python "$VENV_PYTHON")
info "Installing livestack residency (editable) from $LIVESTACK_DIR…"
"${INSTALL[@]}" -e "$LIVESTACK_DIR/polycore" --no-deps
[[ -d "$LIVESTACK_DIR/node-py" ]] && "${INSTALL[@]}" -e "$LIVESTACK_DIR/node-py" --no-deps
# shared_py is a Rust/pyo3 extension — build it into THIS venv with maturin.
"${INSTALL[@]}" maturin
( cd "$LIVESTACK_DIR/shared-py" \
&& env -u CONDA_PREFIX VIRTUAL_ENV="$SCRIPT_DIR/venv" PYO3_PYTHON="$VENV_PYTHON" \
"$VENV_PYTHON" -m maturin develop --release )
"$VENV_PYTHON" -c "import polycore, shared_py; print(' polycore + shared_py OK')"
}
install_livestack_residency
# ── Download models ──────────────────────────────────────────────────────────
info "Downloading PyTorch models (used for both MPS and CUDA)…"
mkdir -p models
download_model() {
local repo="$1"
local dest="models/$(basename "$repo")"
if [[ -d "$dest" ]]; then
echo " $dest already exists — skipping."
return
fi
echo " Downloading $repo …"
"$VENV_PYTHON" -c "
from huggingface_hub import snapshot_download
snapshot_download('$repo', local_dir='$dest')
"
echo " Saved to $dest"
}
# 1.7B model (default)
download_model "Qwen/Qwen3-TTS-12Hz-1.7B-Base"
# 0.6B model (optional)
echo ""
read -rp "${bold}Download the smaller 0.6B model too? [y/N] ${reset}" choice
if [[ "$choice" == "y" || "$choice" == "Y" ]]; then
download_model "Qwen/Qwen3-TTS-12Hz-0.6B-Base"
fi
# ── Verify imports ───────────────────────────────────────────────────────────
info "Verifying key imports…"
"$VENV_PYTHON" -c "
import torch; print(f' torch {torch.__version__}')
from qwen_tts import Qwen3TTSModel; print(' qwen_tts OK')
import soundfile; print(' soundfile OK')
import fastapi; print(' fastapi OK')
"
# The dots engine is import-fragile in one specific way: dots.tts refuses to load
# when torch and torchaudio disagree on their MINOR version, and nothing else in
# this venv cares, so the drift only shows up as "the dots voices 503". Assert the
# pair HERE, at install time, rather than letting it surface at first synthesis.
# Not fatal — a box that only serves qwen/voxcpm is still a working polytts.
if [[ "$arch" != "arm64" || "$(uname -s)" != "Darwin" ]]; then
"$VENV_PYTHON" -c "
import torch, torchaudio
tm = torch.__version__.split('+')[0].rsplit('.', 1)[0]
am = torchaudio.__version__.split('+')[0].rsplit('.', 1)[0]
assert tm == am, f'torch {torch.__version__} / torchaudio {torchaudio.__version__} minors differ — dots.tts will not import'
import dots_tts.runtime # noqa: F401
print(' dots_tts OK (torch/torchaudio pair matched)')
" || warn "dots.tts is not importable in this venv — the 'dots' engine will fail; qwen/voxcpm/cosyvoice are unaffected. Check that torch and torchaudio share a minor version."
fi
if [[ "$arch" == "arm64" ]] && [[ "$(uname -s)" == "Darwin" ]]; then
"$VENV_PYTHON" -c "
import mlx.core; print(f' mlx {mlx.core.__version__}')
from mlx_audio.tts.utils import load_model; print(' mlx_audio OK')
"
fi
# ── Done ─────────────────────────────────────────────────────────────────────
echo ""
info "Setup complete!"
echo ""
if [[ "$arch" == "arm64" ]] && [[ "$(uname -s)" == "Darwin" ]]; then
echo " Start the server (MLX backend — default on Apple Silicon):"
echo " ${bold}./run.sh${reset}"
echo ""
echo " Or use the PyTorch backend (MPS):"
echo " ${bold}POLYTTS_RUNTIME=pytorch ./run.sh${reset}"
else
echo " Start the server (PyTorch backend; uses CUDA if available):"
echo " ${bold}POLYTTS_RUNTIME=pytorch ./run.sh${reset}"
fi
echo ""
echo " Test it:"
echo " ${bold}curl -X POST http://localhost:8100/tts \\${reset}"
echo " ${bold}-H 'Content-Type: application/json' \\${reset}"
echo " ${bold}-d '{\"text\": \"Hello world\", \"pack_id\": \"sc2-kerrigan-infested\"}' \\${reset}"
echo " ${bold}--output test.wav${reset}"
echo ""