@drakulavich/kesha-voice-kit 1.3.0 → 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 +67 -0
- package/README.md +31 -6
- package/SKILL.md +70 -24
- package/package.json +4 -2
- package/src/cli/dispatch.ts +40 -0
- package/src/cli/install.ts +108 -0
- package/src/cli/main.ts +177 -0
- package/src/cli/say.ts +119 -0
- package/src/cli/status.ts +12 -0
- package/src/cli.ts +19 -463
- package/src/engine-install.ts +37 -5
- package/src/engine.ts +19 -2
- package/src/format.ts +57 -0
- package/src/lib.ts +14 -0
- package/src/say.ts +1 -1
- package/src/status.ts +16 -0
- package/src/toon.ts +15 -0
- package/src/transcribe.ts +7 -3
- package/src/types.ts +18 -0
- package/src/models.ts +0 -6
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,15 +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
|
-
|
|
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)
|
|
113
|
+
kesha install --tts # ~490MB (Kokoro + Piper RU + ONNX G2P, opt-in)
|
|
90
114
|
kesha say "Hello, world" > hello.wav
|
|
91
115
|
kesha say "Привет, мир" > privet.wav # auto-routes to ru-denis
|
|
92
116
|
echo "long text" | kesha say > reply.wav
|
|
@@ -95,7 +119,7 @@ kesha say --voice en-af_heart "text" # explicit voice overrides auto-routing
|
|
|
95
119
|
kesha say --list-voices
|
|
96
120
|
```
|
|
97
121
|
|
|
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.
|
|
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.
|
|
99
123
|
|
|
100
124
|
**Supported voices:**
|
|
101
125
|
- English: `en-af_heart` (default), plus any Kokoro voice you download into `~/.cache/kesha/models/kokoro-82m/voices/`
|
|
@@ -104,7 +128,7 @@ Output format: WAV mono float32 (24 kHz for Kokoro, 22.05 kHz for Piper). OGG/Op
|
|
|
104
128
|
|
|
105
129
|
### macOS system voices
|
|
106
130
|
|
|
107
|
-
`kesha say --voice macos-*` routes through `AVSpeechSynthesizer` on macOS, so you get voice synthesis for free — no
|
|
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/`.
|
|
108
132
|
|
|
109
133
|
```bash
|
|
110
134
|
kesha say --list-voices | grep ^macos- # discover installed voices
|
|
@@ -144,6 +168,7 @@ Kesha Voice Kit bundles open-source models optimized for on-device inference:
|
|
|
144
168
|
| NVIDIA Parakeet TDT 0.6B v3 | Speech-to-text | ~2.5GB | [HuggingFace](https://huggingface.co/nvidia/parakeet-tdt-0.6b-v3) |
|
|
145
169
|
| SpeechBrain ECAPA-TDNN | Audio language detection | ~86MB | [HuggingFace](https://huggingface.co/speechbrain/lang-id-voxlingua107-ecapa) |
|
|
146
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) |
|
|
147
172
|
|
|
148
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.
|
|
149
174
|
|
package/SKILL.md
CHANGED
|
@@ -1,54 +1,100 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
6
|
+
requires:
|
|
7
|
+
bins: [kesha]
|
|
4
8
|
|
|
5
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
56
|
+
## TTS: synthesize speech
|
|
23
57
|
|
|
24
58
|
```bash
|
|
25
|
-
kesha
|
|
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
|
-
|
|
65
|
+
Output: WAV mono float32. `--out <path>` writes to a file instead of stdout.
|
|
29
66
|
|
|
30
|
-
|
|
67
|
+
## Language detection standalone
|
|
31
68
|
|
|
32
|
-
|
|
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
|
-
|
|
71
|
+
## Install
|
|
41
72
|
|
|
42
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
~
|
|
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.
|
|
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.
|
|
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
|
+
});
|
package/src/cli/main.ts
ADDED
|
@@ -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
|
+
});
|