@drakulavich/kesha-voice-kit 1.2.2 → 1.4.1

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.
@@ -60,8 +72,10 @@ kesha audio.ogg # transcribe (plain text)
60
72
  kesha --format transcript audio.ogg # text + language/confidence
61
73
  kesha --format json audio.ogg # full JSON with lang fields
62
74
  kesha --json audio.ogg # alias for --format json
75
+ kesha --toon audio.ogg # compact LLM-friendly TOON (same data as --json)
63
76
  kesha --verbose audio.ogg # show language detection details
64
77
  kesha --lang en audio.ogg # warn if detected language differs
78
+ kesha --vad lecture.m4a # segment with Silero VAD first (long/silence-heavy audio)
65
79
  kesha status # show installed backend info
66
80
  ```
67
81
 
@@ -78,13 +92,25 @@ $ kesha freedom.ogg tahiti.ogg
78
92
 
79
93
  Stdout: transcript. Stderr: errors. Pipe-friendly. Also available as `parakeet` command (backward-compatible alias).
80
94
 
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
+ ```
105
+
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).
107
+
81
108
  ## Text-to-Speech
82
109
 
83
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.
84
111
 
85
112
  ```bash
86
- brew install espeak-ng # one-time system dep (macOS — apt/choco elsewhere)
87
- kesha install --tts # ~390MB (Kokoro + Piper RU, opt-in)
113
+ kesha install --tts # ~490MB (Kokoro + Piper RU + ONNX G2P, opt-in)
88
114
  kesha say "Hello, world" > hello.wav
89
115
  kesha say "Привет, мир" > privet.wav # auto-routes to ru-denis
90
116
  echo "long text" | kesha say > reply.wav
@@ -93,21 +119,21 @@ kesha say --voice en-af_heart "text" # explicit voice overrides auto-routing
93
119
  kesha say --list-voices
94
120
  ```
95
121
 
96
- 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).
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.
97
123
 
98
124
  **Supported voices:**
99
125
  - English: `en-af_heart` (default), plus any Kokoro voice you download into `~/.cache/kesha/models/kokoro-82m/voices/`
100
126
  - Russian: `ru-denis` (default). More speakers (dmitri, irina, ruslan) are ready to drop in once needed.
101
- - macOS system voices (preview — dev builds only, see below): `macos-<identifier-or-language>` routes to `AVSpeechSynthesizer`. Zero install, any of the 180+ voices already on your Mac.
127
+ - macOS system voices: `macos-<identifier-or-language>` routes to `AVSpeechSynthesizer`. Zero install, any of the 180+ voices already on your Mac.
102
128
 
103
- ### macOS system voices (preview)
129
+ ### macOS system voices
104
130
 
105
- `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. Currently gated behind the `system_tts` cargo feature; release-binary integration is tracked as part of [#141](https://github.com/drakulavich/kesha-voice-kit/issues/141).
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/`.
106
132
 
107
133
  ```bash
108
- # From a dev build: `cargo build --release --features system_tts` in rust/.
109
- kesha say --voice macos-com.apple.voice.compact.en-US.Samantha "Hello from system TTS" > out.wav
110
- kesha say --voice macos-ru-RU "Привет, мир" > hello-ru.wav # language-code fallback
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
111
137
  ```
112
138
 
113
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.
@@ -142,6 +168,7 @@ Kesha Voice Kit bundles open-source models optimized for on-device inference:
142
168
  | NVIDIA Parakeet TDT 0.6B v3 | Speech-to-text | ~2.5GB | [HuggingFace](https://huggingface.co/nvidia/parakeet-tdt-0.6b-v3) |
143
169
  | SpeechBrain ECAPA-TDNN | Audio language detection | ~86MB | [HuggingFace](https://huggingface.co/speechbrain/lang-id-voxlingua107-ecapa) |
144
170
  | Apple NLLanguageRecognizer | Text language detection | built-in | macOS system framework |
171
+ | Silero VAD v5 (opt-in) | Voice activity detection | ~2.3MB | [snakers4/silero-vad](https://github.com/snakers4/silero-vad) |
145
172
 
146
173
  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.
147
174
 
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@drakulavich/kesha-voice-kit",
3
- "version": "1.2.2",
3
+ "version": "1.4.1",
4
4
  "description": "Open-source voice toolkit for Apple Silicon. Speech-to-text, language detection. 25 languages.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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.2.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
+ });