@drakulavich/kesha-voice-kit 1.18.6 → 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
@@ -236,6 +236,50 @@ Compared against Whisper `large-v3-turbo` — all engines auto-detect language.
236
236
 
237
237
  See [BENCHMARK.md](BENCHMARK.md) for the full per-file breakdown (Russian + English).
238
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
+
239
283
  ## What's Inside
240
284
 
241
285
  | Model | Task | Size | Source |
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
@@ -79,6 +79,33 @@ openclaw config get tools.media.audio.echoFormat
79
79
 
80
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.
81
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
+
82
109
  ## STT: transcribe audio
83
110
 
84
111
  ```bash
@@ -135,14 +162,14 @@ kesha say --voice macos-de-DE "Guten Tag" > de.wav # any macOS system voice —
135
162
  kesha say --list-voices # Kokoro + Vosk-TTS + ~180 macos-* voices
136
163
  ```
137
164
 
138
- 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}}`.
139
166
 
140
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:
141
168
 
142
169
  ```bash
143
- kesha say "Hello there" --format ogg-opus --out reply.ogg # 24 kHz @ 32 kbps mono — Telegram-grade
144
- kesha say "Привет" --voice ru-vosk-m02 --format ogg-opus --out reply.ogg # Russian voice note
145
- 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
146
173
  ```
147
174
 
148
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.6" "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.6",
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.6"
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
@@ -147,6 +147,7 @@ export const mainCommand = defineCommand({
147
147
  "Commands:\n" +
148
148
  " completions Print shell completion script.\n" +
149
149
  " doctor Collect support diagnostics.\n" +
150
+ " init Interactive setup guide for Kesha features.\n" +
150
151
  " install Download engine and models.\n" +
151
152
  " manpage Print the kesha(1) manpage.\n" +
152
153
  " record Record microphone audio to a WAV file.\n" +
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";
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 },