feat: ksay — Kokoro 82M local TTS replaces say (4x RTF, offline, 9 voices)

- engine_kokoro_tts.py: tts(text, voice, outfile) 0.5s gen 4-5x RTF M2
- ksay: drop-in say replacement, usage: ksay [-v voice] [-o file] "text"
- install-ksay.sh: one-shot setup for macmini (pip + model 310MB + warm)
- voices: af_bella (default), af_heart, am_adam, bf_emma etc
- model ready after: ./install-ksay.sh (pre-downloads hexgrad/Kokoro-82M)
- macmini: git pull gitea main && bash install-ksay.sh && ./ksay "hello"
This commit is contained in:
Adolfo Reyna
2026-07-14 13:37:58 -04:00
parent 2e6d6d24ce
commit 354cacd417
5 changed files with 169 additions and 0 deletions
+102
View File
@@ -0,0 +1,102 @@
#!/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\""