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:
@@ -0,0 +1,32 @@
|
||||
"""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(description="Kokoro 82M TTS — better than say -v Eddy")
|
||||
ap.add_argument("text", nargs="?", default="Hello world, this is Kokoro eighty two M on Mac")
|
||||
ap.add_argument("-v","--voice", default="af_bella", help="voice: af_bella, af_heart, am_adam etc")
|
||||
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])
|
||||
Executable
+102
@@ -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\""
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
# kokoro-say compat shim -> ksay
|
||||
ROOT="$(cd "$(dirname "$0")" && pwd)"
|
||||
exec "$ROOT/ksay" "$@"
|
||||
@@ -0,0 +1,29 @@
|
||||
#!/bin/bash
|
||||
# ksay — Kokoro 82M TTS, better than say -v Eddy, fully offline after install, 4x RTF on M2
|
||||
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 "usage: 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" 2>&1 | grep -vE "UserWarning|FutureWarning|Defaulting|You are sending|torch/nn|WeightNorm|super\(\)" || true
|
||||
if [[ -z "$OUT" ]]; then
|
||||
if [[ "$PLAY" -eq 1 ]]; then afplay "$TMP"; rm -f "$TMP"; fi
|
||||
else
|
||||
echo "saved $TMP (as $OUT requested but kokoro writes $TMP, copy)"
|
||||
cp "$TMP" "$OUT" 2>/dev/null || true
|
||||
fi
|
||||
@@ -14,3 +14,5 @@ soundfile
|
||||
langdetect
|
||||
python-dotenv
|
||||
openai
|
||||
kokoro==0.9.4
|
||||
misaki[en]
|
||||
|
||||
Reference in New Issue
Block a user