@drakulavich/kesha-voice-kit 1.3.0 → 1.4.2

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.
package/NOTICES.md ADDED
@@ -0,0 +1,67 @@
1
+ # Third-Party Notices
2
+
3
+ Kesha Voice Kit itself is MIT-licensed (see `LICENSE`). It bundles or downloads
4
+ third-party models and libraries at runtime. Their licenses and attributions
5
+ are listed below.
6
+
7
+ ## Models downloaded by `kesha install`
8
+
9
+ ### NVIDIA Parakeet TDT 0.6B v3
10
+
11
+ Automatic speech recognition. CC-BY 4.0. Trained and published by NVIDIA
12
+ Corporation; ONNX export by [istupakov/parakeet-tdt-0.6b-v3-onnx](https://huggingface.co/istupakov/parakeet-tdt-0.6b-v3-onnx).
13
+
14
+ ### SpeechBrain ECAPA-TDNN VoxLingua107
15
+
16
+ Audio language identification. Apache 2.0. Trained by the SpeechBrain team;
17
+ see [speechbrain/lang-id-voxlingua107-ecapa](https://huggingface.co/speechbrain/lang-id-voxlingua107-ecapa).
18
+
19
+ ### Silero VAD v5 (opt-in: `kesha install --vad`)
20
+
21
+ Voice activity detection. MIT. Authored by [Silero Team](https://github.com/snakers4/silero-vad).
22
+ Pinned to the `v6.2.1` tag.
23
+
24
+ ## Models downloaded by `kesha install --tts`
25
+
26
+ ### Kokoro-82M
27
+
28
+ Text-to-speech (English). Apache 2.0. ONNX export by [onnx-community/Kokoro-82M-v1.0-ONNX](https://huggingface.co/onnx-community/Kokoro-82M-v1.0-ONNX).
29
+
30
+ ### Piper voices (Russian: `ru-denis`)
31
+
32
+ Text-to-speech (multilingual, Piper VITS). MIT. Voice packs from [rhasspy/piper-voices](https://huggingface.co/rhasspy/piper-voices).
33
+
34
+ ### CharsiuG2P ByT5-tiny (ONNX export)
35
+
36
+ Grapheme-to-phoneme conversion for Kokoro and Piper pipelines (#123).
37
+
38
+ **License: CC-BY 4.0** (declared on the ONNX-export repo). Attribution required.
39
+
40
+ - ONNX export: [klebster/g2p_multilingual_byT5_tiny_onnx](https://huggingface.co/klebster/g2p_multilingual_byT5_tiny_onnx) by Kleber Noel — CC-BY 4.0.
41
+ - Base checkpoint: [charsiu/g2p_multilingual_byT5_tiny_16_layers_100](https://huggingface.co/charsiu/g2p_multilingual_byT5_tiny_16_layers_100) — **no explicit license declared on HuggingFace**; usage here follows the CC-BY 4.0 declared by the downstream export and the paper's terms.
42
+ - Paper: Zhu, J., Zhang, C., & Jurgens, D. (2022). "ByT5 model for massively multilingual grapheme-to-phoneme conversion." [arXiv:2204.03067](https://arxiv.org/abs/2204.03067) · Interspeech 2022.
43
+
44
+ ## System libraries linked by the engine binary
45
+
46
+ ### ONNX Runtime (`ort` / `ort-sys` crates)
47
+
48
+ MIT. Microsoft. The engine dynamically links a vendored `onnxruntime` shared
49
+ library downloaded to `~/Library/Caches/ort.pyke.io/` on first build.
50
+
51
+ ### FluidAudio (`fluidaudio-rs` crate, CoreML backend only)
52
+
53
+ Apache 2.0. [FluidInference/FluidAudio](https://github.com/FluidInference/FluidAudio).
54
+ Linked into the engine when built with `--features coreml`.
55
+
56
+ ### symphonia
57
+
58
+ MPL-2.0. [pdeljanov/Symphonia](https://github.com/pdeljanov/Symphonia). Audio
59
+ container decoder.
60
+
61
+ ### symphonia-adapter-libopus
62
+
63
+ MIT OR Apache-2.0. Opus-decoder adapter, by [aschey](https://github.com/aschey/symphonia-adapters).
64
+
65
+ ### libopus
66
+
67
+ BSD-3-Clause. Upstream Opus codec, by Xiph.Org / the IETF Opus working group. Statically linked via `opusic-sys`.
package/README.md CHANGED
@@ -30,6 +30,18 @@ kesha install # downloads engine + models
30
30
  kesha audio.ogg # transcript to stdout
31
31
  ```
32
32
 
33
+ ### Air-gapped / corporate mirrors
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:
36
+
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
+
33
45
  ## OpenClaw Integration
34
46
 
35
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.
@@ -52,6 +64,15 @@ Your agent receives a voice message in Telegram/WhatsApp/Slack, Kesha transcribe
52
64
 
53
65
  Manage the plugin with `openclaw plugins list`, `openclaw plugins disable kesha-voice-kit`, or `openclaw plugins uninstall kesha-voice-kit`.
54
66
 
67
+ ## Raycast Extension (macOS)
68
+
69
+ Transcribe audio and speak clipboard text from Raycast's launcher without opening a terminal. Two commands:
70
+
71
+ - **Transcribe Selected Audio** — pick an audio file in Finder, hit the command, transcript lands on your clipboard.
72
+ - **Speak Clipboard** — synthesize whatever's on your clipboard and play it through the default output.
73
+
74
+ Source + install instructions: [`raycast/`](raycast/). Tracked in [#145](https://github.com/drakulavich/kesha-voice-kit/issues/145); not yet on the Raycast Store — install via `ray develop` from the subdirectory.
75
+
55
76
  ## CLI Tools
56
77
 
57
78
  ```bash
@@ -60,8 +81,10 @@ kesha audio.ogg # transcribe (plain text)
60
81
  kesha --format transcript audio.ogg # text + language/confidence
61
82
  kesha --format json audio.ogg # full JSON with lang fields
62
83
  kesha --json audio.ogg # alias for --format json
84
+ kesha --toon audio.ogg # compact LLM-friendly TOON (same data as --json)
63
85
  kesha --verbose audio.ogg # show language detection details
64
86
  kesha --lang en audio.ogg # warn if detected language differs
87
+ kesha --vad lecture.m4a # segment with Silero VAD first (long/silence-heavy audio)
65
88
  kesha status # show installed backend info
66
89
  ```
67
90
 
@@ -78,15 +101,25 @@ $ kesha freedom.ogg tahiti.ogg
78
101
 
79
102
  Stdout: transcript. Stderr: errors. Pipe-friendly. Also available as `parakeet` command (backward-compatible alias).
80
103
 
104
+ ### Long / silence-heavy audio: `--vad`
105
+
106
+ 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.
107
+
108
+ ```bash
109
+ kesha install --vad # one-time, ~2.3MB
110
+ kesha lecture.m4a # auto-on when audio ≥ 120s and VAD installed
111
+ kesha --vad short-clip.ogg # force VAD on any input
112
+ kesha --no-vad meeting.m4a # force VAD off even on long audio
113
+ ```
114
+
115
+ 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).
116
+
81
117
  ## Text-to-Speech
82
118
 
83
119
  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.
84
120
 
85
121
  ```bash
86
- brew install espeak-ng # one-time system dep — macOS
87
- # Linux: sudo apt install espeak-ng
88
- # Windows: choco install espeak-ng (puts libespeak-ng.dll on PATH)
89
- kesha install --tts # ~390MB (Kokoro + Piper RU, opt-in)
122
+ kesha install --tts # ~490MB (Kokoro + Piper RU + ONNX G2P, opt-in)
90
123
  kesha say "Hello, world" > hello.wav
91
124
  kesha say "Привет, мир" > privet.wav # auto-routes to ru-denis
92
125
  echo "long text" | kesha say > reply.wav
@@ -95,7 +128,7 @@ kesha say --voice en-af_heart "text" # explicit voice overrides auto-routing
95
128
  kesha say --list-voices
96
129
  ```
97
130
 
98
- Output format: WAV mono float32 (24 kHz for Kokoro, 22.05 kHz for Piper). OGG/Opus and MP3 are tracked in follow-up issues. Static-linking of `espeak-ng` to remove the system dep is [#124](https://github.com/drakulavich/kesha-voice-kit/issues/124).
131
+ 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.
99
132
 
100
133
  **Supported voices:**
101
134
  - English: `en-af_heart` (default), plus any Kokoro voice you download into `~/.cache/kesha/models/kokoro-82m/voices/`
@@ -104,7 +137,7 @@ Output format: WAV mono float32 (24 kHz for Kokoro, 22.05 kHz for Piper). OGG/Op
104
137
 
105
138
  ### macOS system voices
106
139
 
107
- `kesha say --voice macos-*` routes through `AVSpeechSynthesizer` on macOS, so you get voice synthesis for free — no 390 MB TTS bundle, no `espeak-ng` dep. The sidecar binary ships alongside `kesha-engine` on darwin-arm64 releases (#141); `kesha install` places both in `~/.cache/kesha/bin/`.
140
+ `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/`.
108
141
 
109
142
  ```bash
110
143
  kesha say --list-voices | grep ^macos- # discover installed voices
@@ -144,6 +177,7 @@ Kesha Voice Kit bundles open-source models optimized for on-device inference:
144
177
  | NVIDIA Parakeet TDT 0.6B v3 | Speech-to-text | ~2.5GB | [HuggingFace](https://huggingface.co/nvidia/parakeet-tdt-0.6b-v3) |
145
178
  | SpeechBrain ECAPA-TDNN | Audio language detection | ~86MB | [HuggingFace](https://huggingface.co/speechbrain/lang-id-voxlingua107-ecapa) |
146
179
  | Apple NLLanguageRecognizer | Text language detection | built-in | macOS system framework |
180
+ | Silero VAD v5 (opt-in) | Voice activity detection | ~2.3MB | [snakers4/silero-vad](https://github.com/snakers4/silero-vad) |
147
181
 
148
182
  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.
149
183
 
package/SKILL.md CHANGED
@@ -1,54 +1,100 @@
1
- # Kesha Voice Kit
1
+ ---
2
+ name: kesha-voice-kit
3
+ description: Local multilingual voice toolkit — speech-to-text (STT), text-to-speech (TTS), and language detection. Runs entirely offline on Apple Silicon, Linux, and Windows. No API keys, no cloud. NVIDIA Parakeet TDT for STT across 25 European languages, Kokoro-82M + Piper VITS for TTS, plus macOS AVSpeechSynthesizer for ~180 system voices with zero install.
4
+ emoji: 🎙️
2
5
 
3
- Local speech-to-text for voice messages. Runs entirely on your machine — no API keys, no cloud.
6
+ requires:
7
+ bins: [kesha]
4
8
 
5
- ## What it does
9
+ install:
10
+ - kind: npm
11
+ packages: [@drakulavich/kesha-voice-kit]
12
+ flags: [--global]
13
+ - kind: bash
14
+ cmd: kesha install
15
+ ---
6
16
 
7
- When a voice message arrives (Telegram, WhatsApp, Slack), Kesha transcribes it and returns JSON with the transcript and detected language:
17
+ # kesha-voice-kit
18
+
19
+ Local voice toolkit: transcribe voice messages to text, synthesize speech, detect language of audio or text. Fully offline after `kesha install`. No API keys, no per-minute billing.
20
+
21
+ **Trigger keywords for when to use this skill:** voice message, voice memo, .ogg, .wav, .mp3, audio file, transcribe, transcription, speech-to-text, STT, text-to-speech, TTS, synthesize speech, say, multilingual voice, multilingual ASR, language detection, offline voice, privacy, Apple Silicon, CoreML.
22
+
23
+ ## When to use
24
+
25
+ - **Voice memo arrived** (Telegram, WhatsApp, Slack, Signal .ogg/.opus/.m4a): transcribe with `kesha --json <path>` and branch on the detected language.
26
+ - **Need to reply with audio**: synthesize with `kesha say "<text>" > reply.wav`. Auto-routes by detected language (Kokoro-82M for English, Piper for Russian). For other languages and ~180 more voices use `--voice macos-*` on macOS (zero model download).
27
+ - **Need to detect what language a file is in** before choosing a pipeline: `kesha --json audio.ogg` returns both audio-based and text-based language detection with confidence scores.
28
+
29
+ ## STT: transcribe audio
30
+
31
+ ```bash
32
+ # JSON output with language detection (recommended for automation)
33
+ kesha --json voice.ogg
34
+ ```
8
35
 
9
36
  ```json
10
37
  [{
11
38
  "file": "voice.ogg",
12
39
  "text": "Привет, как дела?",
13
40
  "lang": "ru",
41
+ "audioLanguage": { "code": "ru", "confidence": 0.98 },
14
42
  "textLanguage": { "code": "ru", "confidence": 0.99 }
15
43
  }]
16
44
  ```
17
45
 
18
- Use the `lang` field to detect which language the user spoke and respond accordingly.
46
+ Use `lang` (or the more detailed `audioLanguage`/`textLanguage`) to decide how to respond.
47
+
48
+ **Formats:** .ogg, .opus, .mp3, .m4a, .wav, .flac, .webm — decoded via symphonia, no ffmpeg required.
19
49
 
20
- ## Setup
50
+ **Other output modes:**
51
+ - `kesha audio.ogg` — plain transcript on stdout
52
+ - `kesha --format transcript audio.ogg` — transcript + `[lang: ru, confidence: 0.99]` footer
53
+ - `kesha --verbose audio.ogg` — human-readable with language info
54
+ - `kesha --lang en audio.ogg` — warn if detected language differs (useful sanity check)
21
55
 
22
- After installing the plugin, run once to download the engine and models:
56
+ ## TTS: synthesize speech
23
57
 
24
58
  ```bash
25
- kesha install
59
+ kesha say "Hello, world" > hello.wav # auto-routes en → Kokoro-82M
60
+ kesha say "Привет, мир" > privet.wav # auto-routes ru → Piper
61
+ kesha say --voice macos-de-DE "Guten Tag" > de.wav # any macOS system voice — German, French, Italian, ...
62
+ kesha say --list-voices # Kokoro + Piper + ~180 macos-* voices
26
63
  ```
27
64
 
28
- ## Plain text mode
65
+ Output: WAV mono float32. `--out <path>` writes to a file instead of stdout.
29
66
 
30
- If you don't need language detection, switch to plain text output in your config:
67
+ ## Language detection standalone
31
68
 
32
- ```json
33
- {
34
- "type": "cli",
35
- "command": "kesha",
36
- "args": ["{{MediaPath}}"]
37
- }
38
- ```
69
+ `kesha --json audio.ogg` includes both audio-based (`audioLanguage`) and text-based (`textLanguage`) detection. Use audio detection to identify the language before running language-specific logic.
39
70
 
40
- This returns just the transcript text without JSON wrapping.
71
+ ## Install
41
72
 
42
- ## Available flags
73
+ ```bash
74
+ bun add --global @drakulavich/kesha-voice-kit # or: npm i -g @drakulavich/kesha-voice-kit
75
+ kesha install # downloads engine (~350 MB)
76
+ kesha install --tts # adds Kokoro + Piper RU + ONNX G2P (~490 MB more, for TTS)
77
+ ```
43
78
 
44
- - `--json` — JSON output with language detection (default in this plugin)
45
- - `--verbose` — show language detection details (audio + text language with confidence)
46
- - `--lang <code>` — warn if detected language differs from expected (e.g. `--lang en`)
79
+ No system deps — G2P runs as ONNX alongside Kokoro/Piper. `macos-*` voices need no install either — they use voices already on the Mac.
47
80
 
48
81
  ## Supported languages
49
82
 
50
- Speech-to-text: 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.
83
+ **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.
84
+
85
+ **Text-to-speech:** English (Kokoro-82M, ~70 voices), Russian (Piper `ru-denis`), plus any macOS system voice via `--voice macos-*`.
51
86
 
52
87
  ## Performance
53
88
 
54
- ~19x faster than Whisper on Apple Silicon (CoreML), ~2.5x faster on CPU (ONNX).
89
+ - ASR: ~19× faster than OpenAI Whisper on Apple Silicon (CoreML via FluidAudio), ~2.5× on CPU (ONNX via `ort`).
90
+ - TTS: sub-second latency for short utterances on Apple Silicon.
91
+
92
+ ## Why local
93
+
94
+ No API keys to manage. No per-minute billing. Voice data never leaves the machine — important for regulated industries, personal messaging, and anything that shouldn't be in a third-party log.
95
+
96
+ ## Links
97
+
98
+ - Source: https://github.com/drakulavich/kesha-voice-kit
99
+ - npm: https://www.npmjs.com/package/@drakulavich/kesha-voice-kit
100
+ - Releases: https://github.com/drakulavich/kesha-voice-kit/releases
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@drakulavich/kesha-voice-kit",
3
- "version": "1.3.0",
4
- "description": "Open-source voice toolkit for Apple Silicon. Speech-to-text, language detection. 25 languages.",
3
+ "version": "1.4.2",
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",
@@ -21,6 +21,7 @@
21
21
  "SKILL.md",
22
22
  "scripts/postinstall.cjs",
23
23
  "LICENSE",
24
+ "NOTICES.md",
24
25
  "README.md"
25
26
  ],
26
27
  "openclaw": {
@@ -29,7 +30,7 @@
29
30
  ]
30
31
  },
31
32
  "keshaEngine": {
32
- "version": "1.3.0"
33
+ "version": "1.4.1"
33
34
  },
34
35
  "scripts": {
35
36
  "test": "bun test",
@@ -68,6 +69,7 @@
68
69
  "typescript": "^6.0.2"
69
70
  },
70
71
  "dependencies": {
72
+ "@toon-format/toon": "^2.1.0",
71
73
  "citty": "^0.2.2",
72
74
  "fastest-levenshtein": "^1.0.16",
73
75
  "picocolors": "^1.1.1",
@@ -0,0 +1,40 @@
1
+ import { runMain } from "citty";
2
+ import { log } from "../log";
3
+ import { suggestCommand } from "../suggest-command";
4
+ import { installCommand } from "./install";
5
+ import { sayCommand } from "./say";
6
+ import { statusCommand } from "./status";
7
+ import { mainCommand } from "./main";
8
+
9
+ const SUBCOMMANDS = ["install", "status", "say"];
10
+
11
+ export async function runCli(rawArgs = process.argv.slice(2)): Promise<void> {
12
+ const [firstArg, ...restArgs] = rawArgs;
13
+
14
+ if (firstArg === "install") {
15
+ await runMain(installCommand, { rawArgs: restArgs });
16
+ return;
17
+ }
18
+
19
+ if (firstArg === "status") {
20
+ await runMain(statusCommand, { rawArgs: restArgs });
21
+ return;
22
+ }
23
+
24
+ if (firstArg === "say") {
25
+ await runMain(sayCommand, { rawArgs: restArgs });
26
+ return;
27
+ }
28
+
29
+ // Check for unknown subcommands (non-flag, non-file-path args)
30
+ if (firstArg && !firstArg.startsWith("-") && !firstArg.includes(".") && !firstArg.includes("/")) {
31
+ const suggestion = suggestCommand(firstArg, SUBCOMMANDS);
32
+ if (suggestion && suggestion !== firstArg) {
33
+ log.error(`unknown command '${firstArg}'`);
34
+ log.warn(`(Did you mean ${suggestion}?)`);
35
+ process.exit(1);
36
+ }
37
+ }
38
+
39
+ await runMain(mainCommand, { rawArgs });
40
+ }
@@ -0,0 +1,108 @@
1
+ import { defineCommand } from "citty";
2
+ import { downloadEngine } from "../engine-install";
3
+ import { getEngineBinPath } from "../engine";
4
+ import { readStarSeen, shouldShowStarPrompt, writeStarSeen } from "../star";
5
+ import { log } from "../log";
6
+
7
+ interface InstallCommandArgs {
8
+ coreml: boolean;
9
+ onnx: boolean;
10
+ "no-cache": boolean;
11
+ tts: boolean;
12
+ vad: boolean;
13
+ }
14
+
15
+ const pkg = await Bun.file(new URL("../../package.json", import.meta.url)).json();
16
+
17
+ function resolveBackendFlag(coreml: boolean, onnx: boolean): string | undefined {
18
+ if (coreml && onnx) {
19
+ log.error('Choose only one backend: "--coreml" or "--onnx".');
20
+ process.exit(1);
21
+ }
22
+ if (coreml) return "coreml";
23
+ if (onnx) return "onnx";
24
+ return undefined;
25
+ }
26
+
27
+ async function askForStar() {
28
+ // Gate on major-or-minor bump only — patch releases and re-installs of the
29
+ // same version shouldn't nag. First-ever install (no marker) still prompts.
30
+ const currentVersion = typeof pkg.version === "string" ? pkg.version : null;
31
+ if (!currentVersion) return;
32
+ const binPath = getEngineBinPath();
33
+ const seen = readStarSeen(binPath);
34
+ if (!shouldShowStarPrompt(currentVersion, seen)) {
35
+ return;
36
+ }
37
+ // Record the version up front so a single run never prompts twice, even
38
+ // if the gh subprocess below throws.
39
+ try {
40
+ writeStarSeen(binPath, currentVersion);
41
+ } catch {
42
+ // Non-fatal — falling through to the prompt is still OK, just means we
43
+ // may nag again on the next install if the write failed for IO reasons.
44
+ }
45
+
46
+ const gh = Bun.which("gh");
47
+ if (!gh) {
48
+ log.info("\nIf you enjoy Kesha Voice Kit, consider starring the repo:");
49
+ log.info(" https://github.com/drakulavich/kesha-voice-kit");
50
+ return;
51
+ }
52
+ const authCheck = Bun.spawnSync([gh, "auth", "status"], { stdout: "ignore", stderr: "ignore" });
53
+ if (authCheck.exitCode !== 0) return;
54
+ const starred = Bun.spawnSync([gh, "api", "user/starred/drakulavich/kesha-voice-kit"], { stdout: "ignore", stderr: "ignore" });
55
+ if (starred.exitCode === 0) return; // already starred
56
+ log.info("\n⭐ If you enjoy Kesha Voice Kit, star it on GitHub:");
57
+ log.info(" https://github.com/drakulavich/kesha-voice-kit");
58
+ log.info(' Or run: gh api -X PUT /user/starred/drakulavich/kesha-voice-kit');
59
+ }
60
+
61
+ async function performInstall(noCache: boolean, backend?: string, tts = false, vad = false) {
62
+ try {
63
+ await downloadEngine(noCache, backend, { tts, vad });
64
+ await askForStar();
65
+ } catch (err: unknown) {
66
+ const message = err instanceof Error ? err.message : String(err);
67
+ log.error(message);
68
+ process.exit(1);
69
+ }
70
+ }
71
+
72
+ export const installCommand = defineCommand({
73
+ meta: {
74
+ name: "install",
75
+ description: "Download inference engine and models",
76
+ },
77
+ args: {
78
+ coreml: {
79
+ type: "boolean",
80
+ description: "Force CoreML backend (macOS arm64)",
81
+ default: false,
82
+ },
83
+ onnx: {
84
+ type: "boolean",
85
+ description: "Force ONNX backend",
86
+ default: false,
87
+ },
88
+ "no-cache": {
89
+ type: "boolean",
90
+ description: "Re-download even if cached",
91
+ default: false,
92
+ },
93
+ tts: {
94
+ type: "boolean",
95
+ description: "Also install TTS models (Kokoro EN + Piper RU + ONNX G2P, ~490MB)",
96
+ default: false,
97
+ },
98
+ vad: {
99
+ type: "boolean",
100
+ description: "Also install Silero VAD (~2.3MB) for long-audio preprocessing",
101
+ default: false,
102
+ },
103
+ },
104
+ async run({ args }: { args: InstallCommandArgs }) {
105
+ const backend = resolveBackendFlag(args.coreml, args.onnx);
106
+ await performInstall(args["no-cache"], backend, args.tts, args.vad);
107
+ },
108
+ });
@@ -0,0 +1,177 @@
1
+ import { defineCommand } from "citty";
2
+ import { detect } from "tinyld";
3
+ import { transcribe } from "../lib";
4
+ import { detectAudioLanguageEngine, detectTextLanguageEngine } from "../engine";
5
+ import type { LangDetectResult } from "../engine";
6
+ import { log } from "../log";
7
+ import type { TranscribeResult } from "../types";
8
+ import {
9
+ formatJsonOutput,
10
+ formatTextOutput,
11
+ formatTranscriptOutput,
12
+ formatVerboseOutput,
13
+ } from "../format";
14
+ import { formatToonOutput } from "../toon";
15
+
16
+ const pkg = await Bun.file(new URL("../../package.json", import.meta.url)).json();
17
+
18
+ interface MainCommandArgs {
19
+ _: string[];
20
+ json: boolean;
21
+ toon: boolean;
22
+ verbose: boolean;
23
+ debug: boolean;
24
+ vad: boolean;
25
+ "no-vad": boolean;
26
+ format?: string;
27
+ lang?: string;
28
+ }
29
+
30
+ export function detectLanguage(text: string): string {
31
+ if (!text) return "";
32
+ return detect(text);
33
+ }
34
+
35
+ export function checkLanguageMismatch(expected: string | undefined, detected: string): string | null {
36
+ if (!expected || !detected || expected === detected) return null;
37
+ return `warning: expected language "${expected}" but detected "${detected}"`;
38
+ }
39
+
40
+ export const mainCommand = defineCommand({
41
+ meta: {
42
+ name: "kesha",
43
+ version: pkg.version,
44
+ description:
45
+ "Kesha Voice Kit — open-source voice toolkit for Apple Silicon.\n" +
46
+ " Run 'kesha install [--no-cache]' to download engine and models.\n" +
47
+ " Run 'kesha status' to inspect installed backend.",
48
+ },
49
+ args: {
50
+ json: {
51
+ type: "boolean",
52
+ description: "Output results as JSON",
53
+ default: false,
54
+ },
55
+ toon: {
56
+ type: "boolean",
57
+ description: "Output results as TOON (compact, LLM-friendly encoding of the same data as --json)",
58
+ default: false,
59
+ },
60
+ verbose: {
61
+ type: "boolean",
62
+ description: "Show language detection details",
63
+ default: false,
64
+ },
65
+ format: {
66
+ type: "string",
67
+ description: "Output format: transcript (enriched text with lang/confidence)",
68
+ },
69
+ lang: {
70
+ type: "string",
71
+ description: "Expected language code (ISO 639-1), warn if mismatch",
72
+ },
73
+ debug: {
74
+ type: "boolean",
75
+ description: "Trace engine subprocess calls on stderr (or KESHA_DEBUG=1)",
76
+ default: false,
77
+ },
78
+ vad: {
79
+ type: "boolean",
80
+ description: "Force Silero VAD preprocessing (kesha install --vad first). Without this, VAD auto-engages on audio ≥ 120s.",
81
+ default: false,
82
+ },
83
+ "no-vad": {
84
+ type: "boolean",
85
+ description: "Disable VAD preprocessing regardless of duration or install state",
86
+ default: false,
87
+ },
88
+ },
89
+ async run({ args }: { args: MainCommandArgs }) {
90
+ if (args.debug) log.debugEnabled = true;
91
+ const files = args._;
92
+
93
+ if ((args.json || args.format === "json") && args.toon) {
94
+ log.error("--json and --toon are mutually exclusive (pick one output format).");
95
+ process.exit(2);
96
+ }
97
+
98
+ if (args.vad && args["no-vad"]) {
99
+ log.error("--vad and --no-vad are mutually exclusive.");
100
+ process.exit(2);
101
+ }
102
+ const vadMode = args.vad ? "on" : args["no-vad"] ? "off" : "auto";
103
+
104
+ if (files.length === 0) {
105
+ log.info("Usage: kesha <audio_file> [audio_file ...]\n kesha install [--no-cache]\n kesha status");
106
+ process.exit(1);
107
+ }
108
+
109
+ let hasError = false;
110
+ const results: TranscribeResult[] = [];
111
+
112
+ const wantsLangId = !!(args.lang || args.verbose || args.json || args.toon || args.format === "transcript" || args.format === "json");
113
+
114
+ for (const file of files) {
115
+ const startedAt = performance.now();
116
+ try {
117
+ // Run audio lang-id and transcription concurrently
118
+ const [audioResult, text] = await Promise.all([
119
+ wantsLangId ? detectAudioLanguageEngine(file) : Promise.resolve(null),
120
+ transcribe(file, { vad: vadMode }),
121
+ ]);
122
+
123
+ let audioLanguage: LangDetectResult | undefined;
124
+ if (audioResult && audioResult.code) {
125
+ audioLanguage = audioResult;
126
+ }
127
+
128
+ if (audioLanguage && args.lang && audioLanguage.confidence > 0.8) {
129
+ const mismatch = checkLanguageMismatch(args.lang, audioLanguage.code);
130
+ if (mismatch) log.warn(`${file}: ${mismatch} (from audio)`);
131
+ }
132
+
133
+ const tinyldLang = wantsLangId ? detectLanguage(text) : "";
134
+ let textLanguage: LangDetectResult | undefined;
135
+
136
+ if (wantsLangId) {
137
+ const engineTextResult = await detectTextLanguageEngine(text);
138
+ if (engineTextResult && engineTextResult.code) {
139
+ textLanguage = engineTextResult;
140
+ }
141
+ }
142
+
143
+ const lang = textLanguage?.code || tinyldLang;
144
+
145
+ const mismatchWarning = checkLanguageMismatch(args.lang, lang);
146
+ if (mismatchWarning) log.warn(`${file}: ${mismatchWarning}`);
147
+
148
+ results.push({
149
+ file,
150
+ text,
151
+ lang,
152
+ audioLanguage,
153
+ textLanguage: textLanguage ?? (tinyldLang ? { code: tinyldLang, confidence: 0 } : undefined),
154
+ sttTimeMs: Math.round(performance.now() - startedAt),
155
+ });
156
+ } catch (err: unknown) {
157
+ hasError = true;
158
+ const message = err instanceof Error ? err.message : String(err);
159
+ log.error(`${file}: ${message}`);
160
+ }
161
+ }
162
+
163
+ if (args.json || args.format === "json") {
164
+ process.stdout.write(formatJsonOutput(results));
165
+ } else if (args.toon) {
166
+ process.stdout.write(formatToonOutput(results));
167
+ } else if (args.format === "transcript") {
168
+ process.stdout.write(formatTranscriptOutput(results));
169
+ } else if (args.verbose) {
170
+ process.stdout.write(formatVerboseOutput(results));
171
+ } else {
172
+ process.stdout.write(formatTextOutput(results));
173
+ }
174
+
175
+ if (hasError) process.exit(1);
176
+ },
177
+ });