@drakulavich/kesha-voice-kit 1.18.5 → 1.19.0-beta.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/README.md CHANGED
@@ -15,7 +15,7 @@
15
15
 
16
16
  - **Transcribe locally** — 25 languages, up to ~19x faster than Whisper on Apple Silicon, ~2.5x on CPU
17
17
  - **Speak back** — Kokoro (EN), Vosk-TTS (RU), macOS system voices, and SSML preview
18
- - **Plug into agents** — ship voice workflows as CLI commands or an <a href="https://github.com/openclaw/openclaw">OpenClaw</a> skill
18
+ - **Plug into agents** — ship voice workflows as CLI commands or an <a href="https://github.com/openclaw/openclaw">OpenClaw</a> skill; setup docs: [OpenClaw](docs/openclaw.md), [Hermes](docs/hermes.md)
19
19
  - **Small Rust engine** — single ~20MB binary, no ffmpeg, no Python, no native Node addons
20
20
 
21
21
  See [Product positioning](docs/product-positioning.md) for supported workflows, non-goals, maturity labels, and the platform matrix.
@@ -51,139 +51,10 @@ kesha audio.ogg # transcript to stdout
51
51
 
52
52
  Air-gapped or behind a corporate mirror? See [docs/model-mirror.md](docs/model-mirror.md).
53
53
 
54
- ## Shell Completions and Manpage
55
-
56
- The npm package includes bash, zsh, and fish completions plus `kesha(1)`.
57
- The CLI can print the packaged files, so install paths do not depend on the
58
- Bun global package layout:
59
-
60
- ```bash
61
- # bash
62
- mkdir -p ~/.local/share/bash-completion/completions
63
- kesha completions bash > ~/.local/share/bash-completion/completions/kesha
64
-
65
- # zsh
66
- mkdir -p ~/.zsh/completions
67
- kesha completions zsh > ~/.zsh/completions/_kesha
68
- # add to ~/.zshrc once: fpath=(~/.zsh/completions $fpath); autoload -Uz compinit; compinit
69
-
70
- # fish
71
- mkdir -p ~/.config/fish/completions
72
- kesha completions fish > ~/.config/fish/completions/kesha.fish
73
-
74
- # manpage
75
- mkdir -p ~/.local/share/man/man1
76
- kesha manpage > ~/.local/share/man/man1/kesha.1
77
- mandb ~/.local/share/man 2>/dev/null || true
78
- ```
79
-
80
- ## Homebrew Install
81
-
82
- Homebrew installs the Bun-based CLI wrapper. Engine and model downloads remain
83
- explicit:
84
-
85
- ```bash
86
- brew tap oven-sh/bun
87
- brew install drakulavich/tap/kesha-voice-kit
88
- kesha install
89
- kesha audio.ogg
90
- ```
91
-
92
- See [Homebrew install](docs/homebrew.md) for package scope and maintainer
93
- validation.
94
-
95
- ## Support diagnostics
96
-
97
- Kesha can collect local diagnostics without downloading models or mutating cache state:
98
-
99
- ```bash
100
- kesha doctor --json --redact
101
- kesha support-bundle --output kesha-support.tar.gz
102
- ```
103
-
104
- `support-bundle` creates a redacted `.tar.gz` archive for GitHub issues. It includes runtime, engine, cache, optional-component, Stats status, and known Kesha environment settings. It does not include audio, transcripts, model files, or the Stats database.
105
-
106
- ## Local Stats privacy and lifecycle
107
-
108
- Kesha Stats is disabled by default. When you opt in with `kesha stats enable`,
109
- Kesha writes a local SQLite database only on your machine:
110
-
111
- ```bash
112
- kesha stats status
113
- kesha stats week
114
- kesha stats errors
115
- kesha stats export --format json # or csv
116
- kesha stats retention 30 # default: 90 days
117
- kesha stats retention off # keep until reset
118
- kesha stats reset # delete recorded stats rows
119
- kesha stats vacuum # compact the SQLite file
120
- ```
121
-
122
- The database stores content-free operational records only: command name
123
- (`transcribe` or `say`), timestamps, success/failure status, app version, item
124
- count, anonymous stage timings, input/output artifact kind, file extension,
125
- size, optional duration/sample-rate/channel counts, and sanitized error
126
- class/code/message.
127
-
128
- Stats never stores audio bytes, transcripts, input text, generated speech text,
129
- file names, full file paths, raw stdout/stderr, environment variables, model
130
- files, API tokens, or cloud identifiers. `support-bundle` reports Stats status
131
- only; it never includes the Stats SQLite database.
132
-
133
- By default, Stats prunes rows older than 90 days before writing or exporting
134
- data. Use `kesha stats retention <days>` to change the TTL or `kesha stats
135
- retention off` to disable TTL pruning. `kesha stats reset` deletes recorded
136
- runs, artifacts, timings, and errors while preserving settings such as enabled
137
- state and retention.
138
-
139
- ## Nix Install
140
-
141
- Alternative reproducible-build path on `aarch64-darwin` / `x86_64-linux`:
142
-
143
- ```bash
144
- nix run github:drakulavich/kesha-voice-kit -- install # downloads models (engine is bundled)
145
- nix run github:drakulavich/kesha-voice-kit -- audio.ogg # transcribe
146
- ```
147
-
148
- Full recipes (one-liner, profile install, engine-only, dev shell) live in [docs/nix-install.md](docs/nix-install.md).
149
-
150
- ## Docker
151
-
152
- Linux x64 CLI image, published to GHCR:
153
-
154
- ```bash
155
- docker run --rm \
156
- -v kesha-cache:/cache/kesha \
157
- -v "$PWD:/work" -w /work \
158
- ghcr.io/drakulavich/kesha-voice-kit:latest install
159
-
160
- docker run --rm \
161
- -v kesha-cache:/cache/kesha \
162
- -v "$PWD:/work" -w /work \
163
- ghcr.io/drakulavich/kesha-voice-kit:latest audio.ogg
164
- ```
165
-
166
- The image keeps model downloads and the engine cache under `/cache/kesha`.
167
- Mount that path as a named volume so `kesha install`, TTS models, VAD, and future
168
- runs reuse the same cache. `compose.yml` provides the same layout:
169
-
170
- ```bash
171
- docker compose run --rm kesha install
172
- docker compose run --rm kesha audio.ogg
173
- ```
174
-
175
- ## Linux Packages
176
-
177
- Stable engine releases also publish `.deb` and `.rpm` packages for Linux x64.
178
- They install the standalone CLI wrapper; engine and model downloads remain explicit:
179
-
180
- ```bash
181
- kesha install
182
- kesha audio.ogg
183
- ```
54
+ ## Requirements
184
55
 
185
- See [Linux packages](docs/linux-packages.md) for install commands and package
186
- scope.
56
+ - [Bun](https://bun.sh) >= 1.3
57
+ - macOS arm64, Linux x64, or Windows x64
187
58
 
188
59
  ## Speech-to-text
189
60
 
@@ -265,6 +136,96 @@ Honored when `<prosody rate>` wraps the whole utterance. Mid-utterance prosody w
265
136
 
266
137
  macOS system voices, SSML, voice listing, and the full voice catalogue: [docs/tts.md](docs/tts.md).
267
138
 
139
+ ## Homebrew Install
140
+
141
+ Homebrew installs the Bun-based CLI wrapper. Engine and model downloads remain
142
+ explicit:
143
+
144
+ ```bash
145
+ brew tap oven-sh/bun
146
+ brew install drakulavich/tap/kesha-voice-kit
147
+ kesha install
148
+ kesha audio.ogg
149
+ ```
150
+
151
+ See [Homebrew install](docs/homebrew.md) for package scope and maintainer
152
+ validation.
153
+
154
+ ## Linux Packages
155
+
156
+ Stable engine releases also publish `.deb` and `.rpm` packages for Linux x64.
157
+ They install the standalone CLI wrapper; engine and model downloads remain explicit:
158
+
159
+ ```bash
160
+ kesha install
161
+ kesha audio.ogg
162
+ ```
163
+
164
+ See [Linux packages](docs/linux-packages.md) for install commands and package
165
+ scope.
166
+
167
+ ## Docker
168
+
169
+ Linux x64 CLI image, published to GHCR:
170
+
171
+ ```bash
172
+ docker run --rm \
173
+ -v kesha-cache:/cache/kesha \
174
+ -v "$PWD:/work" -w /work \
175
+ ghcr.io/drakulavich/kesha-voice-kit:latest install
176
+
177
+ docker run --rm \
178
+ -v kesha-cache:/cache/kesha \
179
+ -v "$PWD:/work" -w /work \
180
+ ghcr.io/drakulavich/kesha-voice-kit:latest audio.ogg
181
+ ```
182
+
183
+ The image keeps model downloads and the engine cache under `/cache/kesha`.
184
+ Mount that path as a named volume so `kesha install`, TTS models, VAD, and future
185
+ runs reuse the same cache. `compose.yml` provides the same layout:
186
+
187
+ ```bash
188
+ docker compose run --rm kesha install
189
+ docker compose run --rm kesha audio.ogg
190
+ ```
191
+
192
+ ## Nix Install
193
+
194
+ Alternative reproducible-build path on `aarch64-darwin` / `x86_64-linux`:
195
+
196
+ ```bash
197
+ nix run github:drakulavich/kesha-voice-kit -- install # downloads models (engine is bundled)
198
+ nix run github:drakulavich/kesha-voice-kit -- audio.ogg # transcribe
199
+ ```
200
+
201
+ Full recipes (one-liner, profile install, engine-only, dev shell) live in [docs/nix-install.md](docs/nix-install.md).
202
+
203
+ ## Shell Completions and Manpage
204
+
205
+ The npm package includes bash, zsh, and fish completions plus `kesha(1)`.
206
+ The CLI can print the packaged files, so install paths do not depend on the
207
+ Bun global package layout:
208
+
209
+ ```bash
210
+ # bash
211
+ mkdir -p ~/.local/share/bash-completion/completions
212
+ kesha completions bash > ~/.local/share/bash-completion/completions/kesha
213
+
214
+ # zsh
215
+ mkdir -p ~/.zsh/completions
216
+ kesha completions zsh > ~/.zsh/completions/_kesha
217
+ # add to ~/.zshrc once: fpath=(~/.zsh/completions $fpath); autoload -Uz compinit; compinit
218
+
219
+ # fish
220
+ mkdir -p ~/.config/fish/completions
221
+ kesha completions fish > ~/.config/fish/completions/kesha.fish
222
+
223
+ # manpage
224
+ mkdir -p ~/.local/share/man/man1
225
+ kesha manpage > ~/.local/share/man/man1/kesha.1
226
+ mandb ~/.local/share/man 2>/dev/null || true
227
+ ```
228
+
268
229
  ## Performance
269
230
 
270
231
  > **Up to ~19x faster than Whisper** on Apple Silicon (M2), **~2.5x faster** on CPU
@@ -275,6 +236,50 @@ Compared against Whisper `large-v3-turbo` — all engines auto-detect language.
275
236
 
276
237
  See [BENCHMARK.md](BENCHMARK.md) for the full per-file breakdown (Russian + English).
277
238
 
239
+ ## Architecture
240
+
241
+ ```text
242
+ Users / agents
243
+ shell | scripts | OpenClaw | Hermes | Raycast | @drakulavich/kesha-voice-kit/core
244
+ |
245
+ v
246
+ +------------------------------- Kesha CLI -------------------------------+
247
+ | Bun + TypeScript wrapper |
248
+ | - parses commands and formats stdout/stderr |
249
+ | - installs pinned engine/model assets only when explicitly requested |
250
+ | - keeps cache, support bundles, and local Stats in the CLI |
251
+ +-----------------------------------+-------------------------------------+
252
+ |
253
+ | spawns one local process
254
+ v
255
+ +----------------------------- kesha-engine ------------------------------+
256
+ | Rust binary, no cloud calls, no Python, no ffmpeg |
257
+ | |
258
+ | Audio input Text input Diagnostics |
259
+ | WAV/MP3/OGG/FLAC/AAC/M4A plain text / SSML status/support |
260
+ | | | | |
261
+ | v v v |
262
+ | Symphonia decode TTS preprocessing runtime probes |
263
+ | | | |
264
+ | +--> optional VAD +--> voice routing |
265
+ | | + diarization | Kokoro / Vosk / macOS voices |
266
+ | | | |
267
+ | +--> audio lang ID +--> speech synthesis |
268
+ | | SpeechBrain ONNX |
269
+ | | |
270
+ | +--> ASR backend |
271
+ | CoreML on Apple Silicon |
272
+ | ONNX Runtime on Linux/Windows/fallback |
273
+ +-----------------------------------+-------------------------------------+
274
+ |
275
+ v
276
+ transcript | JSON/TOON | WAV | local diagnostics
277
+ ```
278
+
279
+ Cache boundary: `kesha install` and opt-in feature installs populate the local
280
+ cache; ordinary transcription and speech commands fail fast if required assets
281
+ are missing.
282
+
278
283
  ## What's Inside
279
284
 
280
285
  | Model | Task | Size | Source |
@@ -309,10 +314,49 @@ await downloadModel(); // install engine + models
309
314
  const text = await transcribe("audio.ogg"); // transcribe
310
315
  ```
311
316
 
312
- ## Requirements
317
+ ## Support diagnostics
313
318
 
314
- - [Bun](https://bun.sh) >= 1.3
315
- - macOS arm64, Linux x64, or Windows x64
319
+ Kesha can collect local diagnostics without downloading models or mutating cache state:
320
+
321
+ ```bash
322
+ kesha doctor --json --redact
323
+ kesha support-bundle --output kesha-support.tar.gz
324
+ ```
325
+
326
+ `support-bundle` creates a redacted `.tar.gz` archive for GitHub issues. It includes runtime, engine, cache, optional-component, Stats status, and known Kesha environment settings. It does not include audio, transcripts, model files, or the Stats database.
327
+
328
+ ## Local Stats privacy and lifecycle
329
+
330
+ Kesha Stats is disabled by default. When you opt in with `kesha stats enable`,
331
+ Kesha writes a local SQLite database only on your machine:
332
+
333
+ ```bash
334
+ kesha stats status
335
+ kesha stats week
336
+ kesha stats errors
337
+ kesha stats export --format json # or csv
338
+ kesha stats retention 30 # default: 90 days
339
+ kesha stats retention off # keep until reset
340
+ kesha stats reset # delete recorded stats rows
341
+ kesha stats vacuum # compact the SQLite file
342
+ ```
343
+
344
+ The database stores content-free operational records only: command name
345
+ (`transcribe` or `say`), timestamps, success/failure status, app version, item
346
+ count, anonymous stage timings, input/output artifact kind, file extension,
347
+ size, optional duration/sample-rate/channel counts, and sanitized error
348
+ class/code/message.
349
+
350
+ Stats never stores audio bytes, transcripts, input text, generated speech text,
351
+ file names, full file paths, raw stdout/stderr, environment variables, model
352
+ files, API tokens, or cloud identifiers. `support-bundle` reports Stats status
353
+ only; it never includes the Stats SQLite database.
354
+
355
+ By default, Stats prunes rows older than 90 days before writing or exporting
356
+ data. Use `kesha stats retention <days>` to change the TTL or `kesha stats
357
+ retention off` to disable TTL pruning. `kesha stats reset` deletes recorded
358
+ runs, artifacts, timings, and errors while preserving settings such as enabled
359
+ state and retention.
316
360
 
317
361
  ## Contributing
318
362
 
package/SKILL.md CHANGED
@@ -22,8 +22,8 @@ Local voice toolkit: transcribe voice messages to text, synthesize speech, detec
22
22
  ## When to use
23
23
 
24
24
  - **Voice memo arrived** (Telegram, WhatsApp, Slack, Signal .ogg/.opus/.m4a): transcribe with `kesha --json <path>` and branch on the detected language.
25
- - **Need to reply with audio (file playback)**: synthesize with `kesha say "<text>" > reply.wav`. Auto-routes by detected language (Kokoro-82M for English, Vosk-TTS for Russian). On darwin-arm64, English Kokoro uses FluidAudio CoreML instead of ONNX. For other languages and ~180 more voices use `--voice macos-*` on macOS (zero model download).
26
- - **Need to send a voice note (Telegram, WhatsApp, Signal, Discord)**: synthesize directly into the messenger-native format with `kesha say "<text>" --format ogg-opus --out reply.ogg`. Default is mono 24 kHz @ 32 kbps — what Telegram `sendVoice` expects. No `ffmpeg` round-trip needed.
25
+ - **Need to send a voice note (Telegram, WhatsApp, Signal, Discord)**: synthesize directly into messenger-native OGG/Opus with `kesha say --format ogg-opus --out reply.ogg "<text>"`. Default is mono 24 kHz @ 32 kbps - what Telegram `sendVoice` expects. No WAV redirect and no `ffmpeg` round-trip.
26
+ - **Need local file playback/debug output**: WAV is still available with `kesha say --out reply.wav "<text>"`, but do not use WAV for Telegram voice replies. Auto-routes by detected language (Kokoro-82M for English, Vosk-TTS for Russian). On darwin-arm64, English Kokoro uses FluidAudio CoreML instead of ONNX. For other languages and ~180 more voices use `--voice macos-*` on macOS (zero model download).
27
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
28
 
29
29
  ## OpenClaw plugin setup
@@ -34,12 +34,30 @@ Install the plugin, then explicitly route OpenClaw audio understanding through t
34
34
  bun add -g @drakulavich/kesha-voice-kit
35
35
  kesha install
36
36
  openclaw plugins install @drakulavich/kesha-voice-kit
37
- openclaw config set tools.media.audio.enabled true
38
- openclaw config set tools.media.audio.models \
39
- '[{"type":"cli","command":"kesha","args":["--format","transcript","{{MediaPath}}"],"timeoutSeconds":15}]'
37
+ openclaw config patch --stdin <<'JSON5'
38
+ {
39
+ tools: {
40
+ media: {
41
+ audio: {
42
+ enabled: true,
43
+ models: [
44
+ {
45
+ type: "cli",
46
+ command: "kesha",
47
+ args: ["{{MediaPath}}"],
48
+ timeoutSeconds: 15,
49
+ },
50
+ ],
51
+ echoTranscript: true,
52
+ echoFormat: '🦜 "{transcript}"',
53
+ },
54
+ },
55
+ },
56
+ }
57
+ JSON5
40
58
  ```
41
59
 
42
- Use `--format transcript` for OpenClaw's normal voice-message path: stdout is compact text plus language metadata, while progress and errors stay off the transcript payload.
60
+ Use Kesha's default output for OpenClaw's normal voice-message path: stdout is the bare transcript text, while progress and errors stay off the transcript payload. The default setup echoes each transcript back to chat as `🦜 "{transcript}"` before the agent responds.
43
61
 
44
62
  For agents that need timestamped segments, switch the model entry to JSON output and allow a longer timeout:
45
63
 
@@ -55,10 +73,39 @@ which kesha
55
73
  kesha status
56
74
  openclaw plugins list
57
75
  openclaw config get tools.media.audio.models
76
+ openclaw config get tools.media.audio.echoTranscript
77
+ openclaw config get tools.media.audio.echoFormat
58
78
  ```
59
79
 
60
80
  Do not rely on `openclaw.plugin.json` to patch `tools.media.audio.models`; OpenClaw ignores non-schema fields such as `configPatch`. Keep the CLI route in user config.
61
81
 
82
+ For OpenClaw TTS replies, route the local TTS provider to Kesha OGG/Opus output. This is the Telegram-safe path:
83
+
84
+ ```bash
85
+ openclaw config patch --stdin <<'JSON5'
86
+ {
87
+ messages: {
88
+ tts: {
89
+ auto: "always",
90
+ provider: "tts-local-cli",
91
+ providers: {
92
+ "tts-local-cli": {
93
+ command: "kesha",
94
+ args: ["say", "--format", "ogg-opus", "--out", "{{OutputPath}}", "{{Text}}"],
95
+ outputFormat: "opus",
96
+ timeoutMs: 120000,
97
+ },
98
+ },
99
+ },
100
+ },
101
+ }
102
+ JSON5
103
+ ```
104
+
105
+ When invoking Kesha manually from an OpenClaw flow, write OGG/Opus into an OpenClaw-owned temp path, for example `kesha say --format ogg-opus --out /tmp/openclaw/reply.ogg "<text>"`, after ensuring the directory exists. The configured `tts-local-cli` provider should use OpenClaw's `{{OutputPath}}` placeholder instead of a hardcoded path.
106
+
107
+ Do not configure OpenClaw Telegram TTS as `kesha say "<text>" > reply.wav`; that creates a WAV file and will not render as a native Telegram voice note.
108
+
62
109
  ## STT: transcribe audio
63
110
 
64
111
  ```bash
@@ -115,14 +162,14 @@ kesha say --voice macos-de-DE "Guten Tag" > de.wav # any macOS system voice —
115
162
  kesha say --list-voices # Kokoro + Vosk-TTS + ~180 macos-* voices
116
163
  ```
117
164
 
118
- Output: WAV mono float32 by default. `--out <path>` writes to a file instead of stdout.
165
+ Output: WAV mono float32 by default. `--out <path>` writes to a file instead of stdout. For Telegram/OpenClaw replies, prefer `--format ogg-opus --out reply.ogg` or the OpenClaw-provided `{{OutputPath}}`.
119
166
 
120
167
  **Voice notes (Telegram / WhatsApp / Signal / Discord):** add `--format ogg-opus` to emit OGG/Opus directly — the format messenger APIs render as a native voice message:
121
168
 
122
169
  ```bash
123
- kesha say "Hello there" --format ogg-opus --out reply.ogg # 24 kHz @ 32 kbps mono — Telegram-grade
124
- kesha say "Привет" --voice ru-vosk-m02 --format ogg-opus --out reply.ogg # Russian voice note
125
- kesha say "Hi" --format ogg-opus --bitrate 16000 --out tiny.ogg # tinier file, intelligible but lossy
170
+ kesha say --format ogg-opus --out reply.ogg "Hello there" # 24 kHz @ 32 kbps mono - Telegram-grade
171
+ kesha say --voice ru-vosk-m02 --format ogg-opus --out reply.ogg "Привет" # Russian voice note
172
+ kesha say --format ogg-opus --bitrate 16000 --out tiny.ogg "Hi" # tinier file, intelligible but lossy
126
173
  ```
127
174
 
128
175
  Format is also inferred from `--out` extension (`.ogg` / `.opus` / `.oga` → OGG/Opus). `--bitrate` (6 000–510 000 bps) and `--sample-rate` (8 000 / 12 000 / 16 000 / 24 000 / 48 000 Hz) tune the encoder.
@@ -6,7 +6,7 @@ _kesha_completion() {
6
6
  COMPREPLY=()
7
7
  cur="${COMP_WORDS[COMP_CWORD]}"
8
8
  command="${COMP_WORDS[1]}"
9
- commands="completions doctor install manpage record say stats status support-bundle"
9
+ commands="completions doctor init install manpage record say stats status support-bundle"
10
10
 
11
11
  if [[ "$COMP_CWORD" -eq 1 ]]; then
12
12
  if [[ "$cur" == -* ]]; then
@@ -20,6 +20,7 @@ _kesha_completion() {
20
20
  case "$command" in
21
21
  completions) opts="--help -h" ;;
22
22
  doctor) opts="--help -h --json --redact" ;;
23
+ init) opts="--help -h --coreml --onnx --no-cache --plan --yes --tts --vad --diarize" ;;
23
24
  install) opts="--help -h --coreml --onnx --no-cache --plan --tts --vad --diarize" ;;
24
25
  manpage) opts="--help -h" ;;
25
26
  record) opts="--help -h --out --max-seconds --debug" ;;
@@ -2,6 +2,7 @@
2
2
  complete -c kesha -f
3
3
  complete -c kesha -n '__fish_use_subcommand' -a 'completions' -d 'Print shell completion script for bash, zsh, or fish'
4
4
  complete -c kesha -n '__fish_use_subcommand' -a 'doctor' -d 'Collect support diagnostics without changing local state'
5
+ complete -c kesha -n '__fish_use_subcommand' -a 'init' -d 'Interactive setup guide for Kesha features'
5
6
  complete -c kesha -n '__fish_use_subcommand' -a 'install' -d 'Download inference engine and models'
6
7
  complete -c kesha -n '__fish_use_subcommand' -a 'manpage' -d 'Print the kesha(1) manpage'
7
8
  complete -c kesha -n '__fish_use_subcommand' -a 'record' -d 'Record microphone audio to a WAV file'
@@ -30,6 +31,16 @@ complete -c kesha -n '__fish_seen_subcommand_from doctor' -l help -d 'Show help'
30
31
  complete -c kesha -n '__fish_seen_subcommand_from doctor' -s h -d 'Show help'
31
32
  complete -c kesha -n '__fish_seen_subcommand_from doctor' -l json -d 'Output diagnostics as JSON'
32
33
  complete -c kesha -n '__fish_seen_subcommand_from doctor' -l redact -d 'Redact secrets and user-home paths from diagnostic output'
34
+ complete -c kesha -n '__fish_seen_subcommand_from init' -l help -d 'Show help'
35
+ complete -c kesha -n '__fish_seen_subcommand_from init' -s h -d 'Show help'
36
+ complete -c kesha -n '__fish_seen_subcommand_from init' -l coreml -d 'Preselect CoreML backend (macOS arm64)'
37
+ complete -c kesha -n '__fish_seen_subcommand_from init' -l onnx -d 'Preselect ONNX backend'
38
+ complete -c kesha -n '__fish_seen_subcommand_from init' -l no-cache -d 'Re-download even if cached'
39
+ complete -c kesha -n '__fish_seen_subcommand_from init' -l plan -d 'Show the selected install plan without downloading'
40
+ complete -c kesha -n '__fish_seen_subcommand_from init' -l yes -d 'Accept defaults and run without prompts'
41
+ complete -c kesha -n '__fish_seen_subcommand_from init' -l tts -d 'Preselect TTS models (Kokoro EN + Vosk-TTS RU, ~990MB)'
42
+ complete -c kesha -n '__fish_seen_subcommand_from init' -l vad -d 'Preselect Silero VAD (~2.3MB) for long-audio preprocessing'
43
+ complete -c kesha -n '__fish_seen_subcommand_from init' -l diarize -d 'Preselect Sortformer speaker diarization (~245MB, darwin-arm64 only)'
33
44
  complete -c kesha -n '__fish_seen_subcommand_from install' -l help -d 'Show help'
34
45
  complete -c kesha -n '__fish_seen_subcommand_from install' -s h -d 'Show help'
35
46
  complete -c kesha -n '__fish_seen_subcommand_from install' -l coreml -d 'Force CoreML backend (macOS arm64)'
@@ -6,6 +6,7 @@ _kesha() {
6
6
  commands=(
7
7
  'completions:Print shell completion script for bash, zsh, or fish'
8
8
  'doctor:Collect support diagnostics without changing local state'
9
+ 'init:Interactive setup guide for Kesha features'
9
10
  'install:Download inference engine and models'
10
11
  'manpage:Print the kesha(1) manpage'
11
12
  'record:Record microphone audio to a WAV file'
@@ -49,6 +50,18 @@ _kesha() {
49
50
  '--json[Output diagnostics as JSON]' \
50
51
  '--redact[Redact secrets and user-home paths from diagnostic output]'
51
52
  ;;
53
+ init)
54
+ _arguments '--help[Show help]' \
55
+ '-h[Show help]' \
56
+ '--coreml[Preselect CoreML backend (macOS arm64)]' \
57
+ '--onnx[Preselect ONNX backend]' \
58
+ '--no-cache[Re-download even if cached]' \
59
+ '--plan[Show the selected install plan without downloading]' \
60
+ '--yes[Accept defaults and run without prompts]' \
61
+ '--tts[Preselect TTS models (Kokoro EN + Vosk-TTS RU, ~990MB)]' \
62
+ '--vad[Preselect Silero VAD (~2.3MB) for long-audio preprocessing]' \
63
+ '--diarize[Preselect Sortformer speaker diarization (~245MB, darwin-arm64 only)]'
64
+ ;;
52
65
  install)
53
66
  _arguments '--help[Show help]' \
54
67
  '-h[Show help]' \
package/man/kesha.1 CHANGED
@@ -1,4 +1,4 @@
1
- .TH KESHA 1 "May 2026" "kesha 1.18.5" "User Commands"
1
+ .TH KESHA 1 "May 2026" "kesha 1.19.0-beta.1" "User Commands"
2
2
  .SH NAME
3
3
  kesha \- open-source local voice toolkit
4
4
  .SH SYNOPSIS
@@ -17,6 +17,9 @@ Print shell completion script for bash, zsh, or fish
17
17
  .B doctor
18
18
  Collect support diagnostics without changing local state
19
19
  .TP
20
+ .B init
21
+ Interactive setup guide for Kesha features
22
+ .TP
20
23
  .B install
21
24
  Download inference engine and models
22
25
  .TP
@@ -79,6 +82,31 @@ Output diagnostics as JSON
79
82
  .TP
80
83
  .B \-\-redact
81
84
  Redact secrets and user\-home paths from diagnostic output
85
+ .SS init
86
+ .TP
87
+ .B \-\-coreml
88
+ Preselect CoreML backend (macOS arm64)
89
+ .TP
90
+ .B \-\-onnx
91
+ Preselect ONNX backend
92
+ .TP
93
+ .B \-\-no\-cache
94
+ Re\-download even if cached
95
+ .TP
96
+ .B \-\-plan
97
+ Show the selected install plan without downloading
98
+ .TP
99
+ .B \-\-yes
100
+ Accept defaults and run without prompts
101
+ .TP
102
+ .B \-\-tts
103
+ Preselect TTS models (Kokoro EN + Vosk\-TTS RU, ~990MB)
104
+ .TP
105
+ .B \-\-vad
106
+ Preselect Silero VAD (~2.3MB) for long\-audio preprocessing
107
+ .TP
108
+ .B \-\-diarize
109
+ Preselect Sortformer speaker diarization (~245MB, darwin\-arm64 only)
82
110
  .SS install
83
111
  .TP
84
112
  .B \-\-coreml
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drakulavich/kesha-voice-kit",
3
- "version": "1.18.5",
3
+ "version": "1.19.0-beta.1",
4
4
  "description": "Give your local tools a voice: fast STT in 25 languages (~19x faster than Whisper on Apple Silicon, ONNX CPU fallback), TTS via Kokoro/Vosk/macOS voices, VAD + 107-language detection. Rust engine, Bun CLI, OpenClaw skill.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -32,7 +32,7 @@
32
32
  ]
33
33
  },
34
34
  "keshaEngine": {
35
- "version": "1.18.5"
35
+ "version": "1.19.0-beta.1"
36
36
  },
37
37
  "scripts": {
38
38
  "test": "bun test",
@@ -4,6 +4,7 @@ import { log } from "../log";
4
4
  import { suggestCommand } from "../suggest-command";
5
5
  import { completionsCommand } from "./completions";
6
6
  import { doctorCommand } from "./doctor";
7
+ import { initCommand } from "./init";
7
8
  import { installCommand } from "./install";
8
9
  import { manpageCommand } from "./manpage";
9
10
  import { recordCommand } from "./record";
@@ -15,6 +16,7 @@ import { mainCommand } from "./main";
15
16
 
16
17
  const SUBCOMMANDS = [
17
18
  "doctor",
19
+ "init",
18
20
  "install",
19
21
  "status",
20
22
  "record",
@@ -37,6 +39,11 @@ export async function runCli(rawArgs = process.argv.slice(2)): Promise<void> {
37
39
  return;
38
40
  }
39
41
 
42
+ if (firstArg === "init") {
43
+ await runMain(initCommand, { rawArgs: restArgs });
44
+ return;
45
+ }
46
+
40
47
  if (firstArg === "completions") {
41
48
  await runMain(completionsCommand, { rawArgs: restArgs });
42
49
  return;
@@ -0,0 +1,273 @@
1
+ import { defineCommand } from "citty";
2
+ import { createInterface } from "node:readline/promises";
3
+ import { stdin as input, stdout as output } from "node:process";
4
+ import { renderInstallPlan } from "../install-plan";
5
+ import { log } from "../log";
6
+ import { performInstall, resolveBackendFlag, resolveNoCacheFlag } from "./install";
7
+
8
+ export interface InitCommandArgs {
9
+ coreml: boolean;
10
+ onnx: boolean;
11
+ "no-cache": boolean;
12
+ noCache?: boolean;
13
+ no_cache?: boolean;
14
+ tts: boolean;
15
+ vad: boolean;
16
+ diarize: boolean;
17
+ plan: boolean;
18
+ yes: boolean;
19
+ }
20
+
21
+ export interface InitSelection {
22
+ noCache: boolean;
23
+ backend?: string;
24
+ tts: boolean;
25
+ vad: boolean;
26
+ diarize: boolean;
27
+ }
28
+
29
+ interface PromptApi {
30
+ question(prompt: string): Promise<string>;
31
+ }
32
+
33
+ export function canInstallDiarizeOnPlatform(
34
+ platform = process.platform,
35
+ arch = process.arch,
36
+ ): boolean {
37
+ return platform === "darwin" && arch === "arm64";
38
+ }
39
+
40
+ export function resolveInitSelection(
41
+ args: InitCommandArgs,
42
+ backend = resolveBackendFlag(args.coreml, args.onnx),
43
+ noCache = resolveNoCacheFlag(args),
44
+ ): InitSelection {
45
+ return {
46
+ noCache,
47
+ backend,
48
+ tts: args.tts,
49
+ vad: args.vad,
50
+ diarize: args.diarize,
51
+ };
52
+ }
53
+
54
+ export function initInstallArgs(selection: InitSelection): string[] {
55
+ return [
56
+ "kesha",
57
+ "install",
58
+ selection.noCache ? "--no-cache" : "",
59
+ selection.backend === "coreml" ? "--coreml" : "",
60
+ selection.backend === "onnx" ? "--onnx" : "",
61
+ selection.tts ? "--tts" : "",
62
+ selection.vad ? "--vad" : "",
63
+ selection.diarize ? "--diarize" : "",
64
+ ].filter(Boolean);
65
+ }
66
+
67
+ export function initSuggestionCommands(
68
+ selection: InitSelection,
69
+ canDiarize = canInstallDiarizeOnPlatform(),
70
+ ): string[][] {
71
+ const variants: InitSelection[] = [
72
+ selection,
73
+ { ...selection, tts: false, vad: true, diarize: false },
74
+ { ...selection, tts: true, vad: true, diarize: false },
75
+ ];
76
+
77
+ if (canDiarize) {
78
+ variants.push({ ...selection, tts: false, vad: true, diarize: true });
79
+ }
80
+
81
+ const seen = new Set<string>();
82
+ return variants.map(initInstallArgs).filter((command) => {
83
+ const key = command.join("\0");
84
+ if (seen.has(key)) return false;
85
+ seen.add(key);
86
+ return true;
87
+ });
88
+ }
89
+
90
+ export function omitUnsupportedDiarize(
91
+ selection: InitSelection,
92
+ canDiarize = canInstallDiarizeOnPlatform(),
93
+ ): InitSelection {
94
+ return selection.diarize && !canDiarize ? { ...selection, diarize: false } : selection;
95
+ }
96
+
97
+ export function renderInitOverview(canDiarize = canInstallDiarizeOnPlatform()): string {
98
+ const lines = [
99
+ "Kesha init",
100
+ "",
101
+ "Kesha is a local voice toolkit. The base install downloads the engine, speech-to-text models, and language detection models.",
102
+ "",
103
+ "Optional features:",
104
+ " - Text-to-speech: enables `kesha say` with Kokoro English and Vosk-TTS Russian voices (~990MB).",
105
+ " - VAD: skips silence in long audio and improves meeting, lecture, and podcast transcripts (~2.3MB).",
106
+ canDiarize
107
+ ? " - Speaker diarization: labels speakers in JSON/TOON transcript segments (~245MB, darwin-arm64)."
108
+ : " - Speaker diarization: labels speakers, but the install is currently darwin-arm64 only.",
109
+ "",
110
+ "Nothing downloads until you confirm the final install plan.",
111
+ ];
112
+ return `${lines.join("\n")}\n`;
113
+ }
114
+
115
+ export async function promptInitSelection(
116
+ args: InitCommandArgs,
117
+ prompt: PromptApi,
118
+ backend = resolveBackendFlag(args.coreml, args.onnx),
119
+ canDiarize = canInstallDiarizeOnPlatform(),
120
+ noCache = resolveNoCacheFlag(args),
121
+ ): Promise<InitSelection> {
122
+ const tts = await askYesNo(prompt, "Install text-to-speech models for `kesha say`?", args.tts);
123
+ const vad = await askYesNo(prompt, "Install VAD for long or silence-heavy audio?", args.vad);
124
+ let diarize = false;
125
+ if (canDiarize) {
126
+ diarize = await askYesNo(prompt, "Install speaker diarization for `--speakers`?", args.diarize);
127
+ } else if (args.diarize) {
128
+ log.warn("--diarize is currently darwin-arm64 only; omitting it from the interactive install.");
129
+ }
130
+
131
+ return {
132
+ noCache,
133
+ backend,
134
+ tts,
135
+ vad,
136
+ diarize,
137
+ };
138
+ }
139
+
140
+ async function askYesNo(prompt: PromptApi, message: string, defaultValue: boolean): Promise<boolean> {
141
+ const suffix = defaultValue ? "Y/n" : "y/N";
142
+ for (;;) {
143
+ const answer = (await prompt.question(`${message} [${suffix}] `)).trim().toLowerCase();
144
+ if (answer === "") return defaultValue;
145
+ if (answer === "y" || answer === "yes") return true;
146
+ if (answer === "n" || answer === "no") return false;
147
+ log.warn("Please answer yes or no.");
148
+ }
149
+ }
150
+
151
+ async function printPlan(selection: InitSelection): Promise<void> {
152
+ log.info(
153
+ await renderInstallPlan({
154
+ noCache: selection.noCache,
155
+ backend: selection.backend,
156
+ tts: selection.tts,
157
+ vad: selection.vad,
158
+ diarize: selection.diarize,
159
+ }),
160
+ );
161
+ }
162
+
163
+ async function runNonInteractive(selection: InitSelection): Promise<void> {
164
+ const canDiarize = canInstallDiarizeOnPlatform();
165
+ const printableSelection = omitUnsupportedDiarize(selection, canDiarize);
166
+ if (selection.diarize && !canDiarize) {
167
+ log.warn("--diarize is currently darwin-arm64 only; omitting it from non-interactive examples.");
168
+ }
169
+ log.info(renderInitOverview(canDiarize));
170
+ await printPlan(printableSelection);
171
+ log.info("Run one of these commands from an interactive terminal:");
172
+ for (const command of initSuggestionCommands(printableSelection, canDiarize)) {
173
+ log.info(` ${command.join(" ")}`);
174
+ }
175
+ }
176
+
177
+ export const initCommand = defineCommand({
178
+ meta: {
179
+ name: "init",
180
+ description: "Interactive setup guide for Kesha features",
181
+ },
182
+ args: {
183
+ coreml: {
184
+ type: "boolean",
185
+ description: "Preselect CoreML backend (macOS arm64)",
186
+ default: false,
187
+ },
188
+ onnx: {
189
+ type: "boolean",
190
+ description: "Preselect ONNX backend",
191
+ default: false,
192
+ },
193
+ "no-cache": {
194
+ type: "boolean",
195
+ description: "Re-download even if cached",
196
+ default: false,
197
+ },
198
+ plan: {
199
+ type: "boolean",
200
+ description: "Show the selected install plan without downloading",
201
+ default: false,
202
+ },
203
+ yes: {
204
+ type: "boolean",
205
+ description: "Accept defaults and run without prompts",
206
+ default: false,
207
+ },
208
+ tts: {
209
+ type: "boolean",
210
+ description: "Preselect TTS models (Kokoro EN + Vosk-TTS RU, ~990MB)",
211
+ default: false,
212
+ },
213
+ vad: {
214
+ type: "boolean",
215
+ description: "Preselect Silero VAD (~2.3MB) for long-audio preprocessing",
216
+ default: false,
217
+ },
218
+ diarize: {
219
+ type: "boolean",
220
+ description: "Preselect Sortformer speaker diarization (~245MB, darwin-arm64 only)",
221
+ default: false,
222
+ },
223
+ },
224
+ async run({ args, rawArgs }: { args: InitCommandArgs; rawArgs: string[] }) {
225
+ const backend = resolveBackendFlag(args.coreml, args.onnx);
226
+ const noCache = resolveNoCacheFlag(args, rawArgs);
227
+ const selection = resolveInitSelection(args, backend, noCache);
228
+
229
+ if (args.plan) {
230
+ log.info(renderInitOverview());
231
+ await printPlan(selection);
232
+ return;
233
+ }
234
+
235
+ if (args.yes) {
236
+ const installSelection = omitUnsupportedDiarize(selection);
237
+ if (selection.diarize && !installSelection.diarize) {
238
+ log.warn("--diarize is currently darwin-arm64 only; omitting it from the --yes install.");
239
+ }
240
+ await performInstall(
241
+ installSelection.noCache,
242
+ installSelection.backend,
243
+ installSelection.tts,
244
+ installSelection.vad,
245
+ installSelection.diarize,
246
+ );
247
+ return;
248
+ }
249
+
250
+ const stdinIsTty = process.stdin.isTTY === true;
251
+ const stdoutIsTty = process.stdout.isTTY === true;
252
+ if (!stdinIsTty || !stdoutIsTty) {
253
+ await runNonInteractive(selection);
254
+ return;
255
+ }
256
+
257
+ log.info(renderInitOverview());
258
+ const rl = createInterface({ input, output });
259
+ try {
260
+ const prompted = await promptInitSelection(args, rl, backend, canInstallDiarizeOnPlatform(), noCache);
261
+ log.info("");
262
+ await printPlan(prompted);
263
+ const confirmed = await askYesNo(rl, `Run \`${initInstallArgs(prompted).join(" ")}\` now?`, true);
264
+ if (!confirmed) {
265
+ log.info(`Skipped install. Run later: ${initInstallArgs(prompted).join(" ")}`);
266
+ return;
267
+ }
268
+ await performInstall(prompted.noCache, prompted.backend, prompted.tts, prompted.vad, prompted.diarize);
269
+ } finally {
270
+ rl.close();
271
+ }
272
+ },
273
+ });
@@ -6,17 +6,31 @@ import { maybeAskForStar } from "../star";
6
6
  import { log } from "../log";
7
7
  import { packageVersion } from "../package-info";
8
8
 
9
- interface InstallCommandArgs {
9
+ export interface InstallCommandArgs {
10
10
  coreml: boolean;
11
11
  onnx: boolean;
12
12
  "no-cache": boolean;
13
+ noCache?: boolean;
14
+ no_cache?: boolean;
13
15
  tts: boolean;
14
16
  vad: boolean;
15
17
  diarize: boolean;
16
18
  plan: boolean;
17
19
  }
18
20
 
19
- function resolveBackendFlag(coreml: boolean, onnx: boolean): string | undefined {
21
+ export function resolveNoCacheFlag(
22
+ args: Pick<InstallCommandArgs, "no-cache" | "noCache" | "no_cache">,
23
+ rawArgs: string[] = [],
24
+ ): boolean {
25
+ return (
26
+ rawArgs.includes("--no-cache") ||
27
+ args["no-cache"] === true ||
28
+ args.noCache === true ||
29
+ args.no_cache === true
30
+ );
31
+ }
32
+
33
+ export function resolveBackendFlag(coreml: boolean, onnx: boolean): string | undefined {
20
34
  if (coreml && onnx) {
21
35
  log.error('Choose only one backend: "--coreml" or "--onnx".');
22
36
  process.exit(1);
@@ -32,7 +46,7 @@ function defaultBackendForPlatform(): string | undefined {
32
46
  return undefined;
33
47
  }
34
48
 
35
- async function performInstall(
49
+ export async function performInstall(
36
50
  noCache: boolean,
37
51
  backend?: string,
38
52
  tts = false,
@@ -111,8 +125,8 @@ export const installCommand = defineCommand({
111
125
  default: false,
112
126
  },
113
127
  },
114
- async run({ args }: { args: InstallCommandArgs }) {
128
+ async run({ args, rawArgs }: { args: InstallCommandArgs; rawArgs: string[] }) {
115
129
  const backend = resolveBackendFlag(args.coreml, args.onnx);
116
- await performInstall(args["no-cache"], backend, args.tts, args.vad, args.diarize, args.plan);
130
+ await performInstall(resolveNoCacheFlag(args, rawArgs), backend, args.tts, args.vad, args.diarize, args.plan);
117
131
  },
118
132
  });
package/src/cli/main.ts CHANGED
@@ -17,6 +17,7 @@ import { formatToonOutput } from "../toon";
17
17
  import { artifactFromFile, createStatsRecorder } from "../stats";
18
18
  import { createPercentProgress } from "../progress";
19
19
  import { getPendingSignalExitCode, waitForPendingSignalCleanup } from "../process-tree";
20
+ import type { TranscriptionSegment } from "../types";
20
21
 
21
22
  interface MainCommandArgs {
22
23
  _: string[];
@@ -117,6 +118,25 @@ export function shouldReportTranscribeProgress(input: {
117
118
  return input.stderrIsTty || !input.stdoutIsTty;
118
119
  }
119
120
 
121
+ const AUDIO_LANG_ID_LONG_AUDIO_THRESHOLD_SECONDS = 10 * 60;
122
+
123
+ export function estimateTranscriptDurationSeconds(segments: TranscriptionSegment[]): number | null {
124
+ const duration = segments.reduce((max, segment) => {
125
+ const end = Number(segment.end);
126
+ return Number.isFinite(end) && end > max ? end : max;
127
+ }, 0);
128
+ return duration > 0 ? duration : null;
129
+ }
130
+
131
+ export function shouldRunAudioLanguageDetection(input: {
132
+ wantsLangId: boolean;
133
+ transcriptDurationSeconds: number | null;
134
+ }): boolean {
135
+ if (!input.wantsLangId) return false;
136
+ if (input.transcriptDurationSeconds === null) return true;
137
+ return input.transcriptDurationSeconds <= AUDIO_LANG_ID_LONG_AUDIO_THRESHOLD_SECONDS;
138
+ }
139
+
120
140
  export const mainCommand = defineCommand({
121
141
  meta: {
122
142
  name: "kesha",
@@ -127,6 +147,7 @@ export const mainCommand = defineCommand({
127
147
  "Commands:\n" +
128
148
  " completions Print shell completion script.\n" +
129
149
  " doctor Collect support diagnostics.\n" +
150
+ " init Interactive setup guide for Kesha features.\n" +
130
151
  " install Download engine and models.\n" +
131
152
  " manpage Print the kesha(1) manpage.\n" +
132
153
  " record Record microphone audio to a WAV file.\n" +
@@ -285,32 +306,25 @@ export const mainCommand = defineCommand({
285
306
  estimatedTotalMs: args.speakers ? 60 * 60 * 1000 : 30 * 60 * 1000,
286
307
  })
287
308
  : null;
288
- // Run audio lang-id and transcription concurrently, but do not leave
289
- // the sibling engine process alive if one side fails first.
290
- const engineAbort = new AbortController();
291
- const audioPromise = wantsLangId
292
- ? stats.timeStage("lang_id_audio", () =>
293
- detectAudioLanguageEngine(file, { signal: engineAbort.signal })
294
- )
295
- : Promise.resolve(null);
296
- const transcriptPromise = stats.timeStage("transcribe", () =>
309
+ const transcript = await stats.timeStage("transcribe", () =>
297
310
  transcribeWithSegments(file, {
298
311
  vad: vadMode,
299
- signal: engineAbort.signal,
300
312
  timestamps: args.timestamps,
301
313
  speakers: args.speakers,
302
314
  })
303
315
  );
304
- let audioResult: LangDetectResult | null;
305
- let transcript: Awaited<ReturnType<typeof transcribeWithSegments>>;
306
- try {
307
- [audioResult, transcript] = await Promise.all([audioPromise, transcriptPromise]);
308
- } catch (err) {
309
- engineAbort.abort();
310
- await Promise.allSettled([audioPromise, transcriptPromise]);
311
- throw err;
312
- }
313
316
  const { text, segments } = transcript;
317
+ const transcriptDurationSeconds = estimateTranscriptDurationSeconds(segments);
318
+
319
+ let audioResult: LangDetectResult | null = null;
320
+ if (shouldRunAudioLanguageDetection({ wantsLangId, transcriptDurationSeconds })) {
321
+ audioResult = await stats.timeStage("lang_id_audio", () => detectAudioLanguageEngine(file));
322
+ } else if (wantsLangId) {
323
+ log.debug(
324
+ `skip lang_id_audio for ${file}: transcript duration ${transcriptDurationSeconds?.toFixed(1)}s exceeds ` +
325
+ `${AUDIO_LANG_ID_LONG_AUDIO_THRESHOLD_SECONDS}s`,
326
+ );
327
+ }
314
328
 
315
329
  let audioLanguage: LangDetectResult | undefined;
316
330
  if (audioResult && audioResult.code) {
package/src/cli.ts CHANGED
@@ -5,6 +5,17 @@
5
5
  // public surface is unchanged.
6
6
  export { doctorCommand } from "./cli/doctor";
7
7
  export { completionsCommand } from "./cli/completions";
8
+ export {
9
+ canInstallDiarizeOnPlatform,
10
+ initCommand,
11
+ initInstallArgs,
12
+ initSuggestionCommands,
13
+ omitUnsupportedDiarize,
14
+ promptInitSelection,
15
+ renderInitOverview,
16
+ resolveInitSelection,
17
+ } from "./cli/init";
18
+ export type { InitCommandArgs, InitSelection } from "./cli/init";
8
19
  export { installCommand } from "./cli/install";
9
20
  export { manpageCommand } from "./cli/manpage";
10
21
  export { recordCommand, resolveRecordArgs } from "./cli/record";
@@ -18,7 +29,9 @@ export {
18
29
  mainCommand,
19
30
  detectLanguage,
20
31
  checkLanguageMismatch,
32
+ estimateTranscriptDurationSeconds,
21
33
  resolveOutputFormat,
34
+ shouldRunAudioLanguageDetection,
22
35
  shouldReportTranscribeProgress,
23
36
  } from "./cli/main";
24
37
  export type { ResolvedOutputFormat } from "./cli/main";
package/src/doctor.ts CHANGED
@@ -257,20 +257,19 @@ function collectOptionalComponents(redact: boolean): OptionalComponent[] {
257
257
  ...pathSummary(join(cache, "models/vosk-ru")),
258
258
  },
259
259
  {
260
- name: "Diarization sidecar",
261
- note: "darwin-arm64 only",
262
- ...pathSummary(join(sidecarDir, "kesha-diarize-darwin-arm64")),
260
+ // Diarization (#199) and Kokoro (#207) run in-engine now (native
261
+ // fluidaudio-rs) — no Swift sidecar binaries. The Sortformer model is
262
+ // the only installable diarization artifact; Kokoro is covered by the
263
+ // "TTS (Kokoro)" entry above.
264
+ name: "Diarization (Sortformer)",
265
+ note: "enabled with `kesha install --diarize` (darwin-arm64); runs in-engine",
266
+ ...pathSummary(join(cache, "models/diarize")),
263
267
  },
264
268
  {
265
269
  name: "AVSpeech sidecar",
266
270
  note: "macOS voices",
267
271
  ...pathSummary(join(sidecarDir, "say-avspeech")),
268
272
  },
269
- {
270
- name: "FluidAudio Kokoro sidecar",
271
- note: "darwin-arm64 Kokoro",
272
- ...pathSummary(join(sidecarDir, "kesha-kokoro")),
273
- },
274
273
  {
275
274
  name: "Text language sidecar",
276
275
  note: "darwin text language fast path",
@@ -69,20 +69,9 @@ const SIDECARS: SidecarSpec[] = [
69
69
  availableHint: "macOS voices available",
70
70
  unavailableHint: "macos-* voices unavailable",
71
71
  },
72
- {
73
- fileBasename: "kesha-diarize-darwin-arm64",
74
- assetName: "kesha-diarize-darwin-arm64",
75
- displayName: "Diarization sidecar",
76
- availableHint: "--speakers available",
77
- unavailableHint: "--speakers unavailable",
78
- },
79
- {
80
- fileBasename: "kesha-kokoro",
81
- assetName: "kesha-kokoro-darwin-arm64",
82
- displayName: "FluidAudio Kokoro sidecar",
83
- availableHint: "Kokoro uses FluidAudio CoreML",
84
- unavailableHint: "en-* Kokoro voices unavailable on Darwin",
85
- },
72
+ // Kokoro TTS (#207) and speaker diarization (#199) no longer ship as Swift
73
+ // sidecars — both run in-engine via the native `fluidaudio-rs` binding. Only
74
+ // the AVSpeech and text-lang sidecars remain.
86
75
  {
87
76
  // Runtime resolver looks for plain `kesha-textlang` next to the engine
88
77
  // (see `rust/src/text_lang.rs::helper_path`), not the platform-suffixed
@@ -99,6 +88,40 @@ const SIDECARS: SidecarSpec[] = [
99
88
  },
100
89
  ];
101
90
 
91
+ const RETIRED_SIDECAR_FILENAMES = [
92
+ // Historical installed filenames.
93
+ "kesha-kokoro",
94
+ "kesha-diarize",
95
+ // Historical release-asset filenames. Keep these explicit: AVSpeech and
96
+ // text-lang helpers are still active and must not be swept up by a glob.
97
+ "kesha-kokoro-darwin-arm64",
98
+ "kesha-diarize-darwin-arm64",
99
+ ];
100
+
101
+ export function cleanupRetiredSidecars(engineDir: string): string[] {
102
+ const removed: string[] = [];
103
+
104
+ for (const filename of RETIRED_SIDECAR_FILENAMES) {
105
+ const path = join(engineDir, filename);
106
+ if (!existsSync(path)) continue;
107
+
108
+ try {
109
+ rmSync(path);
110
+ removed.push(filename);
111
+ } catch (e) {
112
+ log.warn(
113
+ `Could not remove retired sidecar ${filename} (${e instanceof Error ? e.message : e}); continuing.`,
114
+ );
115
+ }
116
+ }
117
+
118
+ if (removed.length > 0) {
119
+ log.success(`Removed retired sidecars: ${removed.join(", ")}.`);
120
+ }
121
+
122
+ return removed;
123
+ }
124
+
102
125
  /**
103
126
  * Make a freshly-downloaded Mach-O runnable on macOS 15+ Sequoia.
104
127
  *
@@ -232,8 +255,10 @@ async function downloadSidecar(
232
255
 
233
256
  async function warmDarwinKokoro(binPath: string): Promise<void> {
234
257
  if (process.platform !== "darwin" || process.arch !== "arm64") return;
235
- const sidecarPath = join(dirname(binPath), "kesha-kokoro");
236
- if (!existsSync(sidecarPath)) return;
258
+ // Kokoro now runs in-engine (FluidAudio CoreML, system_kokoro) — warm it by
259
+ // exercising the engine's own `say`, not a sidecar. The first synthesis
260
+ // compiles/fetches FluidAudio's CoreML Kokoro cache.
261
+ if (!existsSync(binPath)) return;
237
262
 
238
263
  const outPath = join(tmpdir(), `kesha-kokoro-warmup-${process.pid}.wav`);
239
264
  log.progress("Warming FluidAudio Kokoro CoreML cache...");
@@ -499,6 +524,10 @@ export async function downloadEngine(
499
524
  await warmDarwinKokoro(binPath);
500
525
  }
501
526
 
527
+ if (canWriteEngineDir) {
528
+ cleanupRetiredSidecars(engineDir);
529
+ }
530
+
502
531
  log.success("Backend installed successfully.");
503
532
  return binPath;
504
533
  }
@@ -81,10 +81,10 @@ const DIARIZE_FILES: PlanFile[] = [
81
81
  },
82
82
  ];
83
83
 
84
+ // Kokoro (#207) and diarization (#199) are in-engine now (native fluidaudio-rs),
85
+ // so only AVSpeech and text-lang ship as separate Swift-sidecar assets.
84
86
  const DARWIN_SIDECARS: ReleaseAssetSpec[] = [
85
87
  { assetName: "say-avspeech-darwin-arm64", sizeBytes: 63_056 },
86
- { assetName: "kesha-diarize-darwin-arm64", sizeBytes: 7_228_000 },
87
- { assetName: "kesha-kokoro-darwin-arm64", sizeBytes: 7_189_648 },
88
88
  { assetName: "kesha-textlang-darwin-arm64", sizeBytes: 57_648 },
89
89
  ];
90
90
 
@@ -123,8 +123,6 @@ function engineAssetForPlatform(): ReleaseAssetSpec | null {
123
123
 
124
124
  function sidecarFilename(assetName: string): string {
125
125
  if (assetName === "say-avspeech-darwin-arm64") return "say-avspeech";
126
- if (assetName === "kesha-diarize-darwin-arm64") return "kesha-diarize-darwin-arm64";
127
- if (assetName === "kesha-kokoro-darwin-arm64") return "kesha-kokoro";
128
126
  if (assetName === "kesha-textlang-darwin-arm64") return "kesha-textlang";
129
127
  return assetName;
130
128
  }
@@ -234,11 +232,11 @@ export async function renderInstallPlan(options: InstallPlanOptions = {}): Promi
234
232
  );
235
233
  components.push({
236
234
  name: "TTS Kokoro EN",
237
- source: "FluidAudio CoreML sidecar",
235
+ source: "FluidAudio CoreML (in-engine)",
238
236
  sizeBytes: 0,
239
- cached: existsSync(join(engineDir, "kesha-kokoro")),
237
+ cached: existsSync(binPath),
240
238
  refresh: false,
241
- note: "no Kokoro ONNX model is downloaded by Kesha on darwin-arm64; warm-up may compile/update FluidAudio's CoreML cache",
239
+ note: "Kokoro runs in-engine via FluidAudio CoreML; no Kokoro ONNX model is downloaded by Kesha on darwin-arm64; warm-up may compile/update FluidAudio's CoreML cache",
242
240
  });
243
241
  } else {
244
242
  components.push(
@@ -1,6 +1,7 @@
1
1
  import type { ArgDef, ArgsDef, CommandDef, CommandMeta, Resolvable } from "citty";
2
2
  import { completionsCommand } from "./cli/completions";
3
3
  import { doctorCommand } from "./cli/doctor";
4
+ import { initCommand } from "./cli/init";
4
5
  import { installCommand } from "./cli/install";
5
6
  import { mainCommand } from "./cli/main";
6
7
  import { manpageCommand } from "./cli/manpage";
@@ -44,6 +45,7 @@ export interface ShellArtifact {
44
45
  const CLI_COMMANDS: CliCommand[] = [
45
46
  { name: "completions", command: completionsCommand },
46
47
  { name: "doctor", command: doctorCommand },
48
+ { name: "init", command: initCommand },
47
49
  { name: "install", command: installCommand },
48
50
  { name: "manpage", command: manpageCommand },
49
51
  { name: "record", command: recordCommand },