@drakulavich/kesha-voice-kit 1.4.1 → 1.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +25 -135
  2. package/package.json +5 -2
  3. package/src/status.ts +86 -1
package/README.md CHANGED
@@ -14,7 +14,7 @@
14
14
  <p align="center"><b>Open-source voice toolkit.</b> Optimized for Apple Silicon (CoreML), works on any platform (ONNX fallback).<br>A collection of small, fast, open-source audio models — packaged as CLI tools and an <a href="https://github.com/openclaw/openclaw">OpenClaw</a> skill for LLM agents.</p>
15
15
 
16
16
  - **Speech-to-text** — 25 languages, ~15x faster than Whisper on Apple Silicon, ~2.5x on CPU
17
- - **Language detection** — 107 languages from audio, text language via NLLanguageRecognizer
17
+ - **Text-to-speech** — Kokoro (EN) + Piper (RU) + macOS system voices, SSML preview
18
18
  - **Rust engine** — single 20MB binary, no ffmpeg, no Python, no native Node addons
19
19
  - **OpenClaw-ready** — plug into your LLM agent as a voice processing skill
20
20
 
@@ -30,52 +30,17 @@ kesha install # downloads engine + models
30
30
  kesha audio.ogg # transcript to stdout
31
31
  ```
32
32
 
33
- ### Air-gapped / corporate mirrors
33
+ Air-gapped or behind a corporate mirror? See [docs/model-mirror.md](docs/model-mirror.md).
34
34
 
35
- Set `KESHA_MODEL_MIRROR` to redirect all HuggingFace model downloads to an internal mirror ([#121](https://github.com/drakulavich/kesha-voice-kit/issues/121)). The HF path hierarchy is preserved, so any HTTP-readable mirror populated with `wget --mirror` or `rsync` works:
35
+ ## Speech-to-text
36
36
 
37
37
  ```bash
38
- export KESHA_MODEL_MIRROR=https://models.corp.internal/kesha
39
- kesha install # ASR + lang-id + TTS models fetch from your mirror
40
- kesha status # confirms the active Mirror URL
41
- ```
42
-
43
- Unset / empty falls back to `huggingface.co` with no regression. The engine binary itself still comes from GitHub Releases — this env var only redirects model downloads.
44
-
45
- ## OpenClaw Integration
46
-
47
- Kesha Voice Kit ships as a plugin for [OpenClaw](https://github.com/openclaw/openclaw) — give your LLM agent ears. No API keys, everything runs locally on your machine.
48
-
49
- ```bash
50
- bun add -g @drakulavich/kesha-voice-kit && kesha install
51
- openclaw plugins install @drakulavich/kesha-voice-kit
52
- openclaw config set tools.media.audio.models \
53
- '[{"type":"cli","command":"kesha","args":["--format","transcript","{{MediaPath}}"],"timeoutSeconds":15}]'
54
- ```
55
-
56
- > If audio transcription is not already enabled: `openclaw config set tools.media.audio.enabled true`
57
-
58
- Your agent receives a voice message in Telegram/WhatsApp/Slack, Kesha transcribes it locally, and the agent sees enriched context:
59
-
60
- ```
61
- Таити, Таити! Не были мы ни в какой Таити! Нас и тут неплохо кормят.
62
- [lang: ru, confidence: 1.00]
63
- ```
64
-
65
- Manage the plugin with `openclaw plugins list`, `openclaw plugins disable kesha-voice-kit`, or `openclaw plugins uninstall kesha-voice-kit`.
66
-
67
- ## CLI Tools
68
-
69
- ```bash
70
- kesha install # download engine and models
71
38
  kesha audio.ogg # transcribe (plain text)
72
39
  kesha --format transcript audio.ogg # text + language/confidence
73
40
  kesha --format json audio.ogg # full JSON with lang fields
74
- kesha --json audio.ogg # alias for --format json
75
- kesha --toon audio.ogg # compact LLM-friendly TOON (same data as --json)
41
+ kesha --toon audio.ogg # compact LLM-friendly TOON
76
42
  kesha --verbose audio.ogg # show language detection details
77
43
  kesha --lang en audio.ogg # warn if detected language differs
78
- kesha --vad lecture.m4a # segment with Silero VAD first (long/silence-heavy audio)
79
44
  kesha status # show installed backend info
80
45
  ```
81
46
 
@@ -92,136 +57,61 @@ $ kesha freedom.ogg tahiti.ogg
92
57
 
93
58
  Stdout: transcript. Stderr: errors. Pipe-friendly. Also available as `parakeet` command (backward-compatible alias).
94
59
 
95
- ### Long / silence-heavy audio: `--vad`
96
-
97
- For meetings, lectures, and podcasts, enable Silero VAD so Parakeet only sees the speech bits. Segment boundaries land at natural speech starts/ends instead of arbitrary cuts, and long silences are skipped entirely.
98
-
99
- ```bash
100
- kesha install --vad # one-time, ~2.3MB
101
- kesha lecture.m4a # auto-on when audio ≥ 120s and VAD installed
102
- kesha --vad short-clip.ogg # force VAD on any input
103
- kesha --no-vad meeting.m4a # force VAD off even on long audio
104
- ```
60
+ For long / silence-heavy audio, use `--vad` (auto-on past 120 s). Details: [docs/vad.md](docs/vad.md).
105
61
 
106
- Auto-triggers at 120 s so voice messages (< 30 s of near-pure speech) stay on the fast path. If you have long audio without VAD installed, Kesha prints a one-time stderr hint. Defaults: threshold 0.5, min-speech 250 ms, min-silence 100 ms, 30 ms edge padding. See issues #128 (base) and #187 (auto-trigger).
62
+ ## Text-to-speech
107
63
 
108
- ## Text-to-Speech
109
-
110
- Kesha speaks back via Kokoro-82M (English) and Piper (Russian). Voice is auto-picked from the input text's language — `en` routes to Kokoro, `ru` to Piper. Pass `--voice` to override.
64
+ Kesha speaks back via Kokoro-82M (English) and Piper (Russian) — voice auto-picks from the text's language:
111
65
 
112
66
  ```bash
113
- kesha install --tts # ~490MB (Kokoro + Piper RU + ONNX G2P, opt-in)
67
+ kesha install --tts # ~490MB (Kokoro + Piper RU + ONNX G2P, opt-in)
114
68
  kesha say "Hello, world" > hello.wav
115
- kesha say "Привет, мир" > privet.wav # auto-routes to ru-denis
116
- echo "long text" | kesha say > reply.wav
117
- kesha say --out reply.wav "text"
118
- kesha say --voice en-af_heart "text" # explicit voice overrides auto-routing
119
- kesha say --list-voices
69
+ kesha say "Привет, мир" > privet.wav # auto-routes to ru-denis
120
70
  ```
121
71
 
122
- Output format: WAV mono float32 (24 kHz for Kokoro, 22.05 kHz for Piper). OGG/Opus and MP3 are tracked in follow-up issues. Grapheme-to-phoneme runs entirely through ONNX (CharsiuG2P ByT5-tiny, [#123](https://github.com/drakulavich/kesha-voice-kit/issues/123)) — no `espeak-ng` system dep.
123
-
124
- **Supported voices:**
125
- - English: `en-af_heart` (default), plus any Kokoro voice you download into `~/.cache/kesha/models/kokoro-82m/voices/`
126
- - Russian: `ru-denis` (default). More speakers (dmitri, irina, ruslan) are ready to drop in once needed.
127
- - macOS system voices: `macos-<identifier-or-language>` routes to `AVSpeechSynthesizer`. Zero install, any of the 180+ voices already on your Mac.
128
-
129
- ### macOS system voices
72
+ macOS system voices, SSML, voice listing, and the full voice catalogue: [docs/tts.md](docs/tts.md).
130
73
 
131
- `kesha say --voice macos-*` routes through `AVSpeechSynthesizer` on macOS, so you get voice synthesis for free — no 490 MB TTS bundle. The sidecar binary ships alongside `kesha-engine` on darwin-arm64 releases (#141); `kesha install` places both in `~/.cache/kesha/bin/`.
132
-
133
- ```bash
134
- kesha say --list-voices | grep ^macos- # discover installed voices
135
- kesha say --voice macos-com.apple.voice.compact.en-US.Samantha "Hello" > out.wav
136
- kesha say --voice macos-ru-RU "Привет, мир" > hello-ru.wav # language-code fallback
137
- ```
138
-
139
- Voice id format: `macos-<id>` where `<id>` is either a full Apple identifier (`com.apple.voice.compact.en-US.Samantha`) or a language code (`en-US`, `ru-RU`) — the Swift helper tries the identifier first and falls back to the language. Output is mono float32 @ 22050 Hz, structurally identical to Piper.
140
-
141
- Quality tradeoff is honest: macOS system voices are notification-grade. Use them when you want zero-install TTS on macOS; keep Kokoro/Piper for anything that needs to sound good.
142
-
143
- ### SSML (preview)
74
+ ## Performance
144
75
 
145
- `kesha say --ssml` accepts [SSML](https://www.w3.org/TR/speech-synthesis11/) for pauses and text-structuring. v1 is deliberately small:
76
+ > **~15x faster than Whisper** on Apple Silicon (M3 Pro), **~2.5x faster** on CPU
146
77
 
147
- ```bash
148
- kesha say --ssml '<speak>Hello <break time="500ms"/> world.</speak>'
149
- kesha say --ssml --voice ru-denis '<speak>Привет <break time="1s"/> мир.</speak>'
150
- ```
78
+ Compared against Whisper `large-v3-turbo` — all engines auto-detect language.
151
79
 
152
- | Tag | Status |
153
- |---|---|
154
- | `<speak>` | ✅ required root |
155
- | `<break time="Nms"\|"Ns"\|default>` | ✅ inserts silence of the given duration |
156
- | plain text inside `<speak>` | ✅ synthesized via the selected engine |
157
- | `<emphasis>`, `<prosody>`, `<phoneme>`, `<say-as>` | ⚠️ stripped with a stderr warning (contained text still synthesized); tracked in [#122](https://github.com/drakulavich/kesha-voice-kit/issues/122) |
158
- | `<!DOCTYPE>` | ❌ rejected (hardening against XXE) |
80
+ ![Benchmark: openai-whisper vs faster-whisper vs Kesha Voice Kit](assets/benchmark.svg)
159
81
 
160
- SSML is opt-in via the explicit `--ssml` flag — inputs that happen to contain `<angle brackets>` aren't misinterpreted as SSML.
82
+ See [BENCHMARK.md](BENCHMARK.md) for the full per-file breakdown (Russian + English).
161
83
 
162
84
  ## What's Inside
163
85
 
164
- Kesha Voice Kit bundles open-source models optimized for on-device inference:
165
-
166
86
  | Model | Task | Size | Source |
167
87
  |---|---|---|---|
168
88
  | NVIDIA Parakeet TDT 0.6B v3 | Speech-to-text | ~2.5GB | [HuggingFace](https://huggingface.co/nvidia/parakeet-tdt-0.6b-v3) |
169
89
  | SpeechBrain ECAPA-TDNN | Audio language detection | ~86MB | [HuggingFace](https://huggingface.co/speechbrain/lang-id-voxlingua107-ecapa) |
170
90
  | Apple NLLanguageRecognizer | Text language detection | built-in | macOS system framework |
171
91
  | Silero VAD v5 (opt-in) | Voice activity detection | ~2.3MB | [snakers4/silero-vad](https://github.com/snakers4/silero-vad) |
92
+ | Kokoro-82M / Piper (opt-in) | Text-to-speech | ~490MB | [Kokoro](https://huggingface.co/hexgrad/Kokoro-82M) · [Piper](https://github.com/rhasspy/piper) |
172
93
 
173
94
  All models run through `kesha-engine` — a Rust binary using [FluidAudio](https://github.com/FluidInference/FluidAudio) (CoreML) on Apple Silicon and [ort](https://github.com/pykeio/ort) (ONNX Runtime) on other platforms.
174
95
 
175
- ## Performance
176
-
177
- > **~15x faster than Whisper** on Apple Silicon (M3 Pro), **~2.5x faster** on CPU
178
-
179
- Compared against Whisper `large-v3-turbo` — all engines auto-detect language.
180
-
181
- ![Benchmark: openai-whisper vs faster-whisper vs Kesha Voice Kit](assets/benchmark.svg)
182
-
183
- <details>
184
- <summary>Full results with per-file breakdown</summary>
185
-
186
- See [BENCHMARK.md](BENCHMARK.md) — includes Russian (real voice messages) and English transcription results with all four engines.
187
-
188
- </details>
96
+ Audio decoding via [symphonia](https://github.com/pdeljanov/Symphonia) — WAV, MP3, OGG/Opus, FLAC, AAC, M4A. No ffmpeg.
189
97
 
190
- ## Supported Audio Formats
98
+ ## Languages
191
99
 
192
- Built-in audio decoding via [symphonia](https://github.com/pdeljanov/Symphonia) — no ffmpeg required:
100
+ - **Speech-to-text (25):** Bulgarian, Croatian, Czech, Danish, Dutch, English, Estonian, Finnish, French, German, Greek, Hungarian, Italian, Latvian, Lithuanian, Maltese, Polish, Portuguese, Romanian, Russian, Slovak, Slovenian, Spanish, Swedish, Ukrainian.
101
+ - **Audio language detection (107):** [full list](https://huggingface.co/speechbrain/lang-id-voxlingua107-ecapa).
193
102
 
194
- | Format | Extension |
195
- |---|---|
196
- | WAV | `.wav` |
197
- | MP3 | `.mp3` |
198
- | OGG Vorbis/Opus | `.ogg`, `.opus` |
199
- | FLAC | `.flac` |
200
- | AAC / M4A | `.aac`, `.m4a` |
103
+ ## Integrations
201
104
 
202
- ## Supported Languages
203
-
204
- **Speech-to-text (25):** :bulgaria: Bulgarian, :croatia: Croatian, :czech_republic: Czech, :denmark: Danish, :netherlands: Dutch, :gb: English, :estonia: Estonian, :finland: Finnish, :fr: French, :de: German, :greece: Greek, :hungary: Hungarian, :it: Italian, :latvia: Latvian, :lithuania: Lithuanian, :malta: Maltese, :poland: Polish, :portugal: Portuguese, :romania: Romanian, :ru: Russian, :slovakia: Slovak, :slovenia: Slovenian, :es: Spanish, :sweden: Swedish, :ukraine: Ukrainian
205
-
206
- **Audio language detection (107):** Full list at [speechbrain/lang-id-voxlingua107-ecapa](https://huggingface.co/speechbrain/lang-id-voxlingua107-ecapa)
207
-
208
- ## Architecture
209
-
210
- ```
211
- kesha audio.ogg
212
- → kesha-engine (Rust binary)
213
- ├── Apple Silicon? → FluidAudio (CoreML / Neural Engine)
214
- └── Other? → ort (ONNX Runtime / CPU)
215
- → transcript to stdout
216
- ```
105
+ - **OpenClaw** — give your LLM agent ears. Install & config: [docs/openclaw.md](docs/openclaw.md).
106
+ - **Raycast** (macOS) — transcribe selected audio & speak clipboard from the launcher. Source + install: [`raycast/`](raycast/).
217
107
 
218
108
  ## Programmatic API
219
109
 
220
110
  ```typescript
221
111
  import { transcribe, downloadModel } from "@drakulavich/kesha-voice-kit/core";
222
112
 
223
- await downloadModel(); // install engine + models
224
- const text = await transcribe("audio.ogg"); // transcribe
113
+ await downloadModel(); // install engine + models
114
+ const text = await transcribe("audio.ogg"); // transcribe
225
115
  ```
226
116
 
227
117
  ## Requirements
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@drakulavich/kesha-voice-kit",
3
- "version": "1.4.1",
4
- "description": "Open-source voice toolkit for Apple Silicon. Speech-to-text, language detection. 25 languages.",
3
+ "version": "1.4.3",
4
+ "description": "Open-source voice toolkit. Speech-to-text (25 langs, ~19x faster than Whisper on Apple Silicon, ONNX fallback on CPU), text-to-speech (Kokoro + Piper + ~180 macOS system voices, SSML), voice-activity detection, language detection (107 langs). Rust engine, OpenClaw skill.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "kesha": "bin/kesha.js",
@@ -36,6 +36,8 @@
36
36
  "test": "bun test",
37
37
  "test:unit": "bun test tests/unit/",
38
38
  "test:integration": "bun test tests/integration/",
39
+ "lint": "bunx tsc --noEmit",
40
+ "lint:tsgo": "bunx tsgo --noEmit",
39
41
  "postinstall": "node scripts/postinstall.cjs"
40
42
  },
41
43
  "keywords": [
@@ -65,6 +67,7 @@
65
67
  },
66
68
  "devDependencies": {
67
69
  "@types/bun": "latest",
70
+ "@typescript/native-preview": "^7.0.0-dev.20260423.1",
68
71
  "fast-xml-parser": "^5.5.10",
69
72
  "typescript": "^6.0.2"
70
73
  },
package/src/status.ts CHANGED
@@ -1,10 +1,37 @@
1
- import { readdirSync } from "fs";
1
+ import { readdirSync, statSync } from "fs";
2
2
  import { homedir } from "os";
3
3
  import { join } from "path";
4
4
  import { isEngineInstalled, getEngineBinPath, getEngineCapabilities } from "./engine";
5
5
  import { log } from "./log";
6
6
  import pc from "picocolors";
7
7
 
8
+ function humanBytes(bytes: number): string {
9
+ if (bytes < 1024) return `${bytes} B`;
10
+ const units = ["KB", "MB", "GB"];
11
+ let n = bytes / 1024;
12
+ let i = 0;
13
+ while (n >= 1024 && i < units.length - 1) {
14
+ n /= 1024;
15
+ i++;
16
+ }
17
+ return `${n.toFixed(n >= 100 ? 0 : 1)} ${units[i]}`;
18
+ }
19
+
20
+ function dirSizeBytes(path: string): number {
21
+ let total = 0;
22
+ try {
23
+ const st = statSync(path);
24
+ if (st.isFile()) return st.size;
25
+ for (const entry of readdirSync(path, { withFileTypes: true })) {
26
+ const p = join(path, entry.name);
27
+ total += entry.isDirectory() ? dirSizeBytes(p) : statSync(p).size;
28
+ }
29
+ } catch {
30
+ /* missing path — component not installed */
31
+ }
32
+ return total;
33
+ }
34
+
8
35
  export function formatStatusLine(
9
36
  label: string,
10
37
  path: string | null,
@@ -53,6 +80,8 @@ export async function showStatus(): Promise<void> {
53
80
  }
54
81
  log.info("");
55
82
  }
83
+
84
+ showDiskUsage(binPath);
56
85
  }
57
86
 
58
87
  if (!installed) {
@@ -60,6 +89,62 @@ export async function showStatus(): Promise<void> {
60
89
  }
61
90
  }
62
91
 
92
+ function showDiskUsage(binPath: string): void {
93
+ const cache = kesheCacheDir();
94
+ // Engine binary lives under `<cache>/engine/bin/` (managed by the TS CLI's
95
+ // engine-install) while all models live under `<cache>/models/` (managed by
96
+ // the Rust engine). Point at `engine/` (two levels up from the binary) so
97
+ // any future sibling files under that root (metadata, hooks, etc.) are
98
+ // counted too.
99
+ const engineDir = join(binPath, "..", "..");
100
+
101
+ const components: Array<{ label: string; path: string }> = [
102
+ { label: "Engine", path: engineDir },
103
+ { label: "ASR (Parakeet)", path: join(cache, "models/parakeet-tdt-v3") },
104
+ { label: "Language ID", path: join(cache, "models/lang-id-ecapa") },
105
+ { label: "VAD (Silero)", path: join(cache, "models/silero-vad") },
106
+ { label: "TTS (Kokoro)", path: join(cache, "models/kokoro-82m") },
107
+ { label: "TTS (Piper)", path: join(cache, "models/piper-ru") },
108
+ { label: "TTS (G2P)", path: join(cache, "models/g2p") },
109
+ ];
110
+
111
+ const rows: Array<{ label: string; size: number }> = [];
112
+ for (const c of components) {
113
+ const size = dirSizeBytes(c.path);
114
+ if (size > 0) rows.push({ label: c.label, size });
115
+ }
116
+
117
+ if (rows.length === 0) return;
118
+
119
+ // Total counts everything under both the model cache root AND the engine
120
+ // dir (which may live outside the cache when `KESHA_ENGINE_BIN` overrides
121
+ // the default layout). That way the number matches what the `rm -rf` hint
122
+ // below would actually free, including any temp downloads or future
123
+ // components not in the per-row list.
124
+ const componentTotal = rows.reduce((n, r) => n + r.size, 0);
125
+ const cacheTotal = dirSizeBytes(cache);
126
+ const engineOutsideCache = engineDir.startsWith(cache)
127
+ ? 0
128
+ : dirSizeBytes(engineDir);
129
+ const total = cacheTotal + engineOutsideCache;
130
+
131
+ log.info(`Disk usage (${cache}):`);
132
+ const labelWidth = Math.max(...rows.map((r) => r.label.length), "Total".length);
133
+ for (const r of rows) {
134
+ const pad = " ".repeat(labelWidth - r.label.length + 2);
135
+ log.info(` ${r.label}:${pad}${humanBytes(r.size)}`);
136
+ }
137
+ const totalPad = " ".repeat(labelWidth - "Total".length + 2);
138
+ log.info(` ${pc.bold("Total")}:${totalPad}${pc.bold(humanBytes(total))}`);
139
+ if (total > componentTotal) {
140
+ const other = total - componentTotal;
141
+ log.info(pc.dim(` (includes ${humanBytes(other)} of other cache files)`));
142
+ }
143
+ log.info("");
144
+ log.info(pc.dim(` To reset cache: rm -rf ${cache} — next \`kesha install\` re-downloads.`));
145
+ log.info("");
146
+ }
147
+
63
148
  function kesheCacheDir(): string {
64
149
  return process.env.KESHA_CACHE_DIR ?? join(homedir(), ".cache", "kesha");
65
150
  }