diff --git a/server/operators/tts_local/manifest.json b/server/operators/tts_local/manifest.json new file mode 100644 index 0000000..25631a0 --- /dev/null +++ b/server/operators/tts_local/manifest.json @@ -0,0 +1,19 @@ +{ + "id": "tts_local", + "name": "Speak (local, Kokoro)", + "category": "audio", + "description": "Text → speech WAV entirely on this Mac via Kokoro-82M on MLX (Apache 2.0, ungated, ~350MB first download). ~50 built-in voices spanning male/female US/UK = instant character voice templates. Faster than realtime, no API cost, no cloud.", + "accepts": [], + "produces": ["audio"], + "resources": "gpu", + "entry": "run.py", + "python": "venvs/kokoro/bin/python", + "params_schema": { + "type": "object", + "properties": { + "text": {"type": "string", "default": "", "description": "The line to speak"}, + "voice": {"type": "string", "default": "af_heart", "description": "Kokoro voice id (af_/am_ = US female/male, bf_/bm_ = UK): af_heart, af_bella, af_nicole, am_adam, am_michael, am_onyx, bf_emma, bf_isabella, bm_george, bm_lewis, ..."}, + "speed": {"type": "number", "default": 1.0, "minimum": 0.5, "maximum": 2.0, "description": "Speaking rate"} + } + } +} diff --git a/server/operators/tts_local/run.py b/server/operators/tts_local/run.py new file mode 100644 index 0000000..3ddc2b2 --- /dev/null +++ b/server/operators/tts_local/run.py @@ -0,0 +1,43 @@ +import argparse +import json +import subprocess +import sys +import time +from pathlib import Path + +ap = argparse.ArgumentParser() +ap.add_argument("--input", action="append", default=[]) +ap.add_argument("--outdir", required=True) +ap.add_argument("--params", default="{}") +a = ap.parse_args() +p = json.loads(a.params) + +text = (p.get("text") or "").strip() +if not text: + print("ERROR: text is required") + sys.exit(1) + +voice = p.get("voice", "af_heart") +speed = float(p.get("speed", 1.0)) +outdir = Path(a.outdir) + +cmd = [ + sys.executable, "-m", "mlx_audio.tts.generate", + "--model", "prince-canuma/Kokoro-82M", + "--text", text, + "--voice", voice, + "--speed", str(speed), + "--file_prefix", str(outdir / f"tts_{voice}"), +] +t0 = time.time() +r = subprocess.run(cmd, capture_output=True, text=True) +if r.stdout: + print(r.stdout[-2000:]) +if r.stderr: + print(r.stderr[-2000:], file=sys.stderr) + +wavs = sorted(outdir.glob("*.wav")) +if r.returncode != 0 or not wavs: + print("ERROR: kokoro generation failed") + sys.exit(1) +print(f"done in {time.time() - t0:.1f}s: {', '.join(w.name for w in wavs)}")