From 458850483a45d39d9f358fada5dc66b40de980ad Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 21 Jan 2026 00:13:03 +0000 Subject: [PATCH] feat: add sherpa-onnx-tts skill --- skills/sherpa-onnx-tts/SKILL.md | 49 ++++++ skills/sherpa-onnx-tts/bin/sherpa-onnx-tts | 178 +++++++++++++++++++++ 2 files changed, 227 insertions(+) create mode 100644 skills/sherpa-onnx-tts/SKILL.md create mode 100755 skills/sherpa-onnx-tts/bin/sherpa-onnx-tts diff --git a/skills/sherpa-onnx-tts/SKILL.md b/skills/sherpa-onnx-tts/SKILL.md new file mode 100644 index 0000000000..9f5a87352c --- /dev/null +++ b/skills/sherpa-onnx-tts/SKILL.md @@ -0,0 +1,49 @@ +--- +name: sherpa-onnx-tts +description: Local text-to-speech via sherpa-onnx (offline, no cloud) +metadata: {"clawdbot":{"emoji":"🗣️","os":["darwin","linux","win32"],"requires":{"env":["SHERPA_ONNX_RUNTIME_DIR","SHERPA_ONNX_MODEL_DIR"]},"install":[{"id":"download-runtime-macos","kind":"download","os":["darwin"],"url":"https://github.com/k2-fsa/sherpa-onnx/releases/download/v1.12.23/sherpa-onnx-v1.12.23-osx-universal2-shared.tar.bz2","archive":"tar.bz2","extract":true,"stripComponents":1,"targetDir":"~/.clawdbot/tools/sherpa-onnx-tts/runtime","label":"Download sherpa-onnx runtime (macOS)"},{"id":"download-runtime-linux-x64","kind":"download","os":["linux"],"url":"https://github.com/k2-fsa/sherpa-onnx/releases/download/v1.12.23/sherpa-onnx-v1.12.23-linux-x64-shared.tar.bz2","archive":"tar.bz2","extract":true,"stripComponents":1,"targetDir":"~/.clawdbot/tools/sherpa-onnx-tts/runtime","label":"Download sherpa-onnx runtime (Linux x64)"},{"id":"download-runtime-win-x64","kind":"download","os":["win32"],"url":"https://github.com/k2-fsa/sherpa-onnx/releases/download/v1.12.23/sherpa-onnx-v1.12.23-win-x64-shared.tar.bz2","archive":"tar.bz2","extract":true,"stripComponents":1,"targetDir":"~/.clawdbot/tools/sherpa-onnx-tts/runtime","label":"Download sherpa-onnx runtime (Windows x64)"},{"id":"download-model-lessac","kind":"download","url":"https://github.com/k2-fsa/sherpa-onnx/releases/download/tts-models/vits-piper-en_US-lessac-high.tar.bz2","archive":"tar.bz2","extract":true,"targetDir":"~/.clawdbot/tools/sherpa-onnx-tts/models","label":"Download Piper en_US lessac (high)"}]}} +--- + +# sherpa-onnx-tts + +Local TTS using the sherpa-onnx offline CLI. + +## Install + +1) Download the runtime for your OS (extracts into `~/.clawdbot/tools/sherpa-onnx-tts/runtime`) +2) Download a voice model (extracts into `~/.clawdbot/tools/sherpa-onnx-tts/models`) + +Update `~/.clawdbot/clawdbot.json`: + +```json5 +{ + skills: { + entries: { + "sherpa-onnx-tts": { + env: { + SHERPA_ONNX_RUNTIME_DIR: "~/.clawdbot/tools/sherpa-onnx-tts/runtime", + SHERPA_ONNX_MODEL_DIR: "~/.clawdbot/tools/sherpa-onnx-tts/models/vits-piper-en_US-lessac-high" + } + } + } + } +} +``` + +The wrapper lives in this skill folder. Run it directly, or add the wrapper to PATH: + +```bash +export PATH="{baseDir}/bin:$PATH" +``` + +## Usage + +```bash +{baseDir}/bin/sherpa-onnx-tts -o ./tts.wav "Hello from local TTS." +``` + +Notes: +- Pick a different model from the sherpa-onnx `tts-models` release if you want another voice. +- If the model dir has multiple `.onnx` files, set `SHERPA_ONNX_MODEL_FILE` or pass `--model-file`. +- You can also pass `--tokens-file` or `--data-dir` to override the defaults. +- Windows: run `node {baseDir}\\bin\\sherpa-onnx-tts -o tts.wav "Hello from local TTS."` diff --git a/skills/sherpa-onnx-tts/bin/sherpa-onnx-tts b/skills/sherpa-onnx-tts/bin/sherpa-onnx-tts new file mode 100755 index 0000000000..82a7cceaf1 --- /dev/null +++ b/skills/sherpa-onnx-tts/bin/sherpa-onnx-tts @@ -0,0 +1,178 @@ +#!/usr/bin/env node + +const fs = require("node:fs"); +const path = require("node:path"); +const { spawnSync } = require("node:child_process"); + +function usage(message) { + if (message) { + console.error(message); + } + console.error( + "\nUsage: sherpa-onnx-tts [--runtime-dir ] [--model-dir ] [--model-file ] [--tokens-file ] [--data-dir ] [--output ] \"text\"", + ); + console.error("\nRequired env (or flags):\n SHERPA_ONNX_RUNTIME_DIR\n SHERPA_ONNX_MODEL_DIR"); + process.exit(1); +} + +function resolveRuntimeDir(explicit) { + const value = explicit || process.env.SHERPA_ONNX_RUNTIME_DIR || ""; + return value.trim(); +} + +function resolveModelDir(explicit) { + const value = explicit || process.env.SHERPA_ONNX_MODEL_DIR || ""; + return value.trim(); +} + +function resolveModelFile(modelDir, explicitFlag) { + const explicit = (explicitFlag || process.env.SHERPA_ONNX_MODEL_FILE || "").trim(); + if (explicit) return explicit; + try { + const candidates = fs + .readdirSync(modelDir) + .filter((entry) => entry.endsWith(".onnx")) + .map((entry) => path.join(modelDir, entry)); + if (candidates.length === 1) return candidates[0]; + } catch { + return ""; + } + return ""; +} + +function resolveTokensFile(modelDir, explicitFlag) { + const explicit = (explicitFlag || process.env.SHERPA_ONNX_TOKENS_FILE || "").trim(); + if (explicit) return explicit; + const candidate = path.join(modelDir, "tokens.txt"); + return fs.existsSync(candidate) ? candidate : ""; +} + +function resolveDataDir(modelDir, explicitFlag) { + const explicit = (explicitFlag || process.env.SHERPA_ONNX_DATA_DIR || "").trim(); + if (explicit) return explicit; + const candidate = path.join(modelDir, "espeak-ng-data"); + return fs.existsSync(candidate) ? candidate : ""; +} + +function resolveBinary(runtimeDir) { + const binName = process.platform === "win32" ? "sherpa-onnx-offline-tts.exe" : "sherpa-onnx-offline-tts"; + return path.join(runtimeDir, "bin", binName); +} + +function prependEnvPath(current, next) { + if (!next) return current; + if (!current) return next; + return `${next}${path.delimiter}${current}`; +} + +const args = process.argv.slice(2); +let runtimeDir = ""; +let modelDir = ""; +let modelFile = ""; +let tokensFile = ""; +let dataDir = ""; +let output = "tts.wav"; +const textParts = []; + +for (let i = 0; i < args.length; i += 1) { + const arg = args[i]; + if (arg === "--runtime-dir") { + runtimeDir = args[i + 1] || ""; + i += 1; + continue; + } + if (arg === "--model-dir") { + modelDir = args[i + 1] || ""; + i += 1; + continue; + } + if (arg === "--model-file") { + modelFile = args[i + 1] || ""; + i += 1; + continue; + } + if (arg === "--tokens-file") { + tokensFile = args[i + 1] || ""; + i += 1; + continue; + } + if (arg === "--data-dir") { + dataDir = args[i + 1] || ""; + i += 1; + continue; + } + if (arg === "-o" || arg === "--output") { + output = args[i + 1] || output; + i += 1; + continue; + } + if (arg === "--text") { + textParts.push(args[i + 1] || ""); + i += 1; + continue; + } + textParts.push(arg); +} + +runtimeDir = resolveRuntimeDir(runtimeDir); +modelDir = resolveModelDir(modelDir); + +if (!runtimeDir || !modelDir) { + usage("Missing runtime/model directory."); +} + +modelFile = resolveModelFile(modelDir, modelFile); +tokensFile = resolveTokensFile(modelDir, tokensFile); +dataDir = resolveDataDir(modelDir, dataDir); + +if (!modelFile || !tokensFile || !dataDir) { + usage( + "Model directory is missing required files. Set SHERPA_ONNX_MODEL_FILE, SHERPA_ONNX_TOKENS_FILE, SHERPA_ONNX_DATA_DIR or pass --model-file/--tokens-file/--data-dir.", + ); +} + +const text = textParts.join(" ").trim(); +if (!text) { + usage("Missing text."); +} + +const bin = resolveBinary(runtimeDir); +if (!fs.existsSync(bin)) { + usage(`TTS binary not found: ${bin}`); +} + +const env = { ...process.env }; +const libDir = path.join(runtimeDir, "lib"); +if (process.platform === "darwin") { + env.DYLD_LIBRARY_PATH = prependEnvPath(env.DYLD_LIBRARY_PATH || "", libDir); +} else if (process.platform === "win32") { + env.PATH = prependEnvPath(env.PATH || "", [path.join(runtimeDir, "bin"), libDir].join(path.delimiter)); +} else { + env.LD_LIBRARY_PATH = prependEnvPath(env.LD_LIBRARY_PATH || "", libDir); +} + +const outputPath = path.isAbsolute(output) ? output : path.join(process.cwd(), output); +fs.mkdirSync(path.dirname(outputPath), { recursive: true }); + +const child = spawnSync( + bin, + [ + `--vits-model=${modelFile}`, + `--vits-tokens=${tokensFile}`, + `--vits-data-dir=${dataDir}`, + `--output-filename=${outputPath}`, + text, + ], + { + stdio: "inherit", + env, + }, +); + +if (typeof child.status === "number") { + process.exit(child.status); +} +if (child.error) { + console.error(child.error.message || String(child.error)); +} +process.exit(1);