#!/bin/bash # One-time, per-Mac installer for the free local voices # (Kokoro and Qwen3-TTS, both via mlx-audio). # Bought once for $0: Apache-2.0 weights, runs entirely on this Mac. # The venv CANNOT be copied between machines — run this on each Mac. set -euo pipefail DIR="$HOME/Library/Application Support/Church Translate Local Voices" VENV="$DIR/venv" PORT="${1:-43111}" echo "== Church Translate — local voices installer ==" # macOS ships Python 3.9, which mlx-audio cannot use — so FIND a 3.10+ python # rather than assuming the one on PATH. Checked in order: uv-managed (this Mac # has them), Homebrew, versioned names on PATH, then plain python3. PY="" pick_py() { for c in "$@"; do [ -x "$c" ] || continue "$c" -c 'import sys; sys.exit(0 if sys.version_info >= (3,10) else 1)' 2>/dev/null && { PY="$c"; return 0; } done return 1 } pick_py $(ls -d "$HOME/.local/share/uv/python/"*/bin/python3 2>/dev/null | sort -r) \ /opt/homebrew/bin/python3.13 /opt/homebrew/bin/python3.12 /opt/homebrew/bin/python3.11 \ /opt/homebrew/bin/python3 /usr/local/bin/python3 \ "$(command -v python3.13 || true)" "$(command -v python3.12 || true)" \ "$(command -v python3.11 || true)" "$(command -v python3 || true)" || { echo "No Python 3.10+ found. Install one, then re-run:" echo " brew install python@3.12 # or: xcode-select --install" exit 1 } echo "using python: $PY ($("$PY" --version 2>&1))" mkdir -p "$DIR" [ -d "$VENV" ] || "$PY" -m venv "$VENV" "$VENV/bin/pip" install --upgrade pip >/dev/null # The exact dependency set, learned from consecutive boot failures: # mlx_audio.server needs uvicorn/fastapi/python-multipart; webrtcvad still # imports pkg_resources, which setuptools>=81 removed. "$VENV/bin/pip" install 'setuptools<81' mlx-audio uvicorn fastapi python-multipart webrtcvad # Kokoro's text processing. mlx-audio ships the model code but not these, and # without them synthesis fails at the last step with an ImportError for # 'misaki' — after the 330MB model has already downloaded. espeakng-loader # carries its own libespeak-ng, so no Homebrew and nothing system-wide. "$VENV/bin/pip" install misaki phonemizer-fork espeakng-loader echo "== prefetching the model (one-time download, ~2GB) ==" "$VENV/bin/python3" - <<'PY' from huggingface_hub import snapshot_download snapshot_download('mlx-community/Qwen3-TTS-12Hz-1.7B-CustomVoice-8bit') print('model cached') PY echo "== self-test: boot the server and synthesize one sentence ==" "$VENV/bin/python3" -m mlx_audio.server --host 127.0.0.1 --port "$PORT" & SRV=$! trap 'kill $SRV 2>/dev/null' EXIT for i in $(seq 1 30); do curl -s -o /dev/null "http://127.0.0.1:$PORT/v1/models" && break; sleep 1; done OUT="$DIR/selftest.wav" curl -s -X POST "http://127.0.0.1:$PORT/v1/audio/speech" \ -H 'Content-Type: application/json' \ -d '{"model":"mlx-community/Qwen3-TTS-12Hz-1.7B-CustomVoice-8bit","voice":"aiden","input":"The Lord bless you and keep you.","lang_code":"english","response_format":"wav","max_tokens":300,"temperature":0.2}' \ -o "$OUT" head -c 4 "$OUT" | grep -q RIFF && echo "SELF-TEST OK — $OUT ($(stat -f%z "$OUT") bytes)" || { echo "SELF-TEST FAILED"; exit 1; } kill $SRV 2>/dev/null; trap - EXIT echo echo "Done. In Church Translate: Operator pane → Premium voices → 'Local (this Mac — free)'."