#!/usr/bin/env bash # install-ksay — setup Kokoro 82M + ksay on any mac (mini or air) set -e ROOT="$(cd "$(dirname "$0")" && pwd)" PY="$ROOT/venv/bin/python" echo "[ksay] installing deps..." $PY -m pip install kokoro==0.9.4 misaki[en] spacy soundfile --quiet 2>&1 | tail -5 $PY -m spacy download en_core_web_sm --quiet 2>&1 | tail -2 || true echo "[ksay] pre-downloading model (hexgrad/Kokoro-82M ~310MB)..." $PY -c "from kokoro import KPipeline; p=KPipeline(lang_code='a'); print('model ready')" 2>&1 | tail -5 echo "[ksay] creating engine..." cat > "$ROOT/engine_kokoro_tts.py" <<'PYEOF' """engine_kokoro_tts.py — Kokoro 82M local TTS, drop-in say replacement, 4x RTF, offline""" from __future__ import annotations import argparse from kokoro import KPipeline import soundfile as sf VOICES=["af_bella","af_heart","af_sarah","af_nova","af_sky","bf_emma","bf_isabella","am_adam","am_michael"] _cache={} def get_pipe(lang='a'): if lang not in _cache: _cache[lang]=KPipeline(lang_code=lang) return _cache[lang] def tts(text, voice="af_bella", outfile="/tmp/kokoro_out.wav"): pipe=get_pipe('a') for _,_,audio in pipe(text, voice=voice): sf.write(outfile, audio, 24000) return outfile if __name__=="__main__": ap=argparse.ArgumentParser() ap.add_argument("text", nargs="?", default="Hello world") ap.add_argument("-v","--voice", default="af_bella") ap.add_argument("-o","--out", default="/tmp/kokoro_out.wav") ap.add_argument("--list-voices", action="store_true") ap.add_argument("--play", action="store_true") a=ap.parse_args() if a.list_voices: print("\n".join(VOICES)); raise SystemExit out=tts(a.text, voice=a.voice, outfile=a.out) print(f"wrote {out}") if a.play: import subprocess; subprocess.run(["afplay", out]) PYEOF echo "[ksay] creating ksay shim..." cat > "$ROOT/ksay" <<'SHEOF' #!/bin/bash # ksay — Kokoro 82M TTS, better than say -v Eddy, fully offline after install, 4x RTF set -e ROOT="$(cd "$(dirname "$0")" && pwd)" VOICE="af_bella" OUT="" TEXT="" PLAY=1 while [[ $# -gt 0 ]]; do case "$1" in -v|--voice) VOICE="$2"; shift 2;; -o|--output-file) OUT="$2"; shift 2;; -l|--list-voices) PYTHONPATH="" "$ROOT/venv/bin/python" "$ROOT/engine_kokoro_tts.py" --list-voices; exit 0;; --no-play) PLAY=0; shift;; --) shift; TEXT="$*"; break;; -*) echo "unknown $1" >&2; exit 2;; *) TEXT="${TEXT:+$TEXT }$1"; shift;; esac done if [[ -z "$TEXT" && ! -t 0 ]]; then TEXT=$(cat); fi if [[ -z "$TEXT" ]]; then echo "ksay [-v voice] [-o file.wav] \"text\" | echo text | ksay" >&2; exit 1; fi TMP="${OUT:-/tmp/ksay-$$.wav}" PYTHONPATH="" "$ROOT/venv/bin/python" "$ROOT/engine_kokoro_tts.py" "$TEXT" -v "$VOICE" -o "$TMP" --play 2>&1 | grep -vE "UserWarning|FutureWarning|Defaulting|You are sending|torch/nn|super\(\)" || true # engine always writes, check if we played if [[ -n "$OUT" ]]; then afplay "$TMP" 2>/dev/null || true echo "saved $OUT" else if [[ "$PLAY" -eq 1 ]]; then afplay "$TMP" rm -f "$TMP" fi fi SHEOF chmod +x "$ROOT/ksay" # also keep kokoro-say compat cp "$ROOT/ksay" "$ROOT/kokoro-say" chmod +x "$ROOT/kokoro-say" echo "[ksay] testing..." PYTHONPATH="" "$ROOT/venv/bin/python" "$ROOT/engine_kokoro_tts.py" "Hello world, ksay is ready on this Mac." -v af_bella -o /tmp/ksay_test.wav 2>&1 | tail -2 ls -lh /tmp/ksay_test.wav echo "" echo "[ksay] install done. Commands:" echo " $ROOT/ksay \"Hello world\"" echo " $ROOT/ksay -v am_adam --no-play -o /tmp/out.wav \"Hello\"" echo " echo \"hi\" | $ROOT/ksay" echo "" echo "To make global: add to ~/.zshrc:" echo " alias ksay=\"$ROOT/ksay\"" echo " alias kokoro-say=\"$ROOT/kokoro-say\""