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);