@drakulavich/kesha-voice-kit 1.19.0-beta.2 → 1.19.0

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/SKILL.md CHANGED
@@ -25,6 +25,7 @@ Local voice toolkit: transcribe voice messages to text, synthesize speech, detec
25
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
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
+ - **Need to capture your own voice** for transcription or as a voice-note source: `kesha record --out clip.wav` records up to 120s (override with `--max-seconds`) of mono 16 kHz WAV from the default microphone. Pipe straight into `kesha --json clip.wav` to close the loop.
28
29
 
29
30
  ## OpenClaw plugin setup
30
31
 
@@ -150,9 +151,12 @@ Each `segment.speaker` is a number (cluster id, stable within one file). On Linu
150
151
  - `kesha audio.ogg` — plain transcript on stdout
151
152
  - `kesha --format transcript audio.ogg` — transcript + `[lang: ru, confidence: 0.99]` footer
152
153
  - `kesha --json --timestamps audio.ogg` — JSON with timestamped `segments`
154
+ - `kesha --toon audio.ogg` — TOON (compact, LLM-friendly JSON encoding); preferred when piping multi-file results to an LLM/agent
153
155
  - `kesha --verbose audio.ogg` — human-readable with language info
154
156
  - `kesha --lang en audio.ogg` — warn if detected language differs (useful sanity check)
155
157
 
158
+ **Long audio:** files ≥ 120 s auto-engage Silero VAD chunking; force on with `--vad` or off with `--no-vad`. Short files use full-file ASR by default.
159
+
156
160
  ## TTS: synthesize speech
157
161
 
158
162
  ```bash
@@ -188,12 +192,25 @@ Format is also inferred from `--out` extension (`.ogg` / `.opus` / `.oga` → OG
188
192
 
189
193
  ## Install
190
194
 
195
+ **Humans use `kesha init` (guided). Agents and scripts use `kesha install` (deterministic).**
196
+
191
197
  ```bash
192
- bun add -g @drakulavich/kesha-voice-kit # global CLI install
193
- kesha install # downloads engine (~350 MB)
194
- kesha install --tts # adds Kokoro + Vosk-TTS RU (~990 MB more, for TTS)
198
+ bun add -g @drakulavich/kesha-voice-kit # global CLI install (always first)
199
+
200
+ # For humans: interactive setup that prompts for backend / TTS / VAD / diarize
201
+ kesha init
202
+
203
+ # For agents and CI: explicit, scriptable install commands
204
+ kesha install # engine only (~350 MB)
205
+ kesha install --tts # + Kokoro + Vosk-TTS RU (~990 MB more)
206
+ kesha install --tts --vad # + Silero VAD (long-audio chunking)
207
+ kesha install --tts --vad --diarize # + speaker diarization (darwin-arm64 only)
195
208
  ```
196
209
 
210
+ Kesha's runtime error/warning messages adapt to the same split: when `kesha` is invoked from a TTY, hints suggest `kesha init`; when stderr is piped (CI logs, OpenClaw, agent subprocess), hints suggest the equivalent `kesha install [...flags]`. Both run the same install code under the hood — pick the one your caller is.
211
+
212
+ For pre-release builds: `bun add -g @drakulavich/kesha-voice-kit@beta` (current `beta` channel; `@latest` stays on the last stable release).
213
+
197
214
  No system deps — English G2P is embedded (`misaki-rs`); Russian G2P is bundled inside Vosk-TTS. `macos-*` voices need no install either — they use voices already on the Mac.
198
215
 
199
216
  ## Supported languages
@@ -207,6 +224,13 @@ No system deps — English G2P is embedded (`misaki-rs`); Russian G2P is bundled
207
224
  - ASR: ~19× faster than OpenAI Whisper on Apple Silicon (CoreML via FluidAudio), ~2.5× on CPU (ONNX via `ort`).
208
225
  - TTS: sub-second latency for short utterances on Apple Silicon.
209
226
 
227
+ ## Troubleshooting
228
+
229
+ - `kesha doctor` — collect support diagnostics without changing local state. Add `--json` for machine-readable output, `--redact` to scrub secrets and home paths before sharing.
230
+ - `kesha logs` — manage privacy-safe local diagnostic logs. Default mode is `retain-on-failure` (events buffered in memory, flushed to disk only when a command fails). `kesha logs mode on` captures every run, `kesha logs path` prints the NDJSON file, `kesha logs disable` turns it off entirely.
231
+ - `kesha support-bundle --output bundle.tar.gz` — produce a redacted `.tar.gz` for filing an issue. Add `--include-logs` to bundle a bounded tail of diagnostic logs.
232
+ - `kesha stats` — manage local anonymous performance stats (per-command latency percentiles). Actions: `enable | disable | status | week | errors | export | reset | vacuum | retention`. Stays on the machine.
233
+
210
234
  ## Why local
211
235
 
212
236
  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.
package/man/kesha.1 CHANGED
@@ -1,4 +1,4 @@
1
- .TH KESHA 1 "May 2026" "kesha 1.19.0-beta.2" "User Commands"
1
+ .TH KESHA 1 "May 2026" "kesha 1.19.0" "User Commands"
2
2
  .SH NAME
3
3
  kesha \- open-source local voice toolkit
4
4
  .SH SYNOPSIS
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drakulavich/kesha-voice-kit",
3
- "version": "1.19.0-beta.2",
3
+ "version": "1.19.0",
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.19.0-beta.2"
35
+ "version": "1.19.0"
36
36
  },
37
37
  "scripts": {
38
38
  "test": "bun test",
@@ -1,4 +1,4 @@
1
- import { runMain } from "citty";
1
+ import { runMain, type CommandDef } from "citty";
2
2
  import { existsSync } from "fs";
3
3
  import { log } from "../log";
4
4
  import { suggestCommand } from "../suggest-command";
@@ -15,19 +15,23 @@ import { statusCommand } from "./status";
15
15
  import { supportBundleCommand } from "./support-bundle";
16
16
  import { mainCommand } from "./main";
17
17
 
18
- const SUBCOMMANDS = [
19
- "doctor",
20
- "init",
21
- "install",
22
- "logs",
23
- "status",
24
- "record",
25
- "say",
26
- "stats",
27
- "support-bundle",
28
- "completions",
29
- "manpage",
30
- ];
18
+ // Single source of truth: keyed lookup also feeds the `did you mean` suggester.
19
+ // `CommandDef<any>` is intentional — citty's generic is invariant in the args
20
+ // shape, and each subcommand has its own arg schema; the value here is only
21
+ // passed back to `runMain`, which re-reads the schema from the def itself.
22
+ const SUBCOMMANDS: Record<string, CommandDef<any>> = {
23
+ doctor: doctorCommand,
24
+ init: initCommand,
25
+ install: installCommand,
26
+ logs: logsCommand,
27
+ status: statusCommand,
28
+ record: recordCommand,
29
+ say: sayCommand,
30
+ stats: statsCommand,
31
+ "support-bundle": supportBundleCommand,
32
+ completions: completionsCommand,
33
+ manpage: manpageCommand,
34
+ };
31
35
 
32
36
  function isPathLike(arg: string): boolean {
33
37
  return arg.includes(".") || arg.includes("/") || existsSync(arg);
@@ -36,58 +40,8 @@ function isPathLike(arg: string): boolean {
36
40
  export async function runCli(rawArgs = process.argv.slice(2)): Promise<void> {
37
41
  const [firstArg, ...restArgs] = rawArgs;
38
42
 
39
- if (firstArg === "doctor") {
40
- await runMain(doctorCommand, { rawArgs: restArgs });
41
- return;
42
- }
43
-
44
- if (firstArg === "init") {
45
- await runMain(initCommand, { rawArgs: restArgs });
46
- return;
47
- }
48
-
49
- if (firstArg === "completions") {
50
- await runMain(completionsCommand, { rawArgs: restArgs });
51
- return;
52
- }
53
-
54
- if (firstArg === "install") {
55
- await runMain(installCommand, { rawArgs: restArgs });
56
- return;
57
- }
58
-
59
- if (firstArg === "logs") {
60
- await runMain(logsCommand, { rawArgs: restArgs });
61
- return;
62
- }
63
-
64
- if (firstArg === "manpage") {
65
- await runMain(manpageCommand, { rawArgs: restArgs });
66
- return;
67
- }
68
-
69
- if (firstArg === "status") {
70
- await runMain(statusCommand, { rawArgs: restArgs });
71
- return;
72
- }
73
-
74
- if (firstArg === "record") {
75
- await runMain(recordCommand, { rawArgs: restArgs });
76
- return;
77
- }
78
-
79
- if (firstArg === "say") {
80
- await runMain(sayCommand, { rawArgs: restArgs });
81
- return;
82
- }
83
-
84
- if (firstArg === "stats") {
85
- await runMain(statsCommand, { rawArgs: restArgs });
86
- return;
87
- }
88
-
89
- if (firstArg === "support-bundle") {
90
- await runMain(supportBundleCommand, { rawArgs: restArgs });
43
+ if (firstArg && Object.hasOwn(SUBCOMMANDS, firstArg)) {
44
+ await runMain(SUBCOMMANDS[firstArg], { rawArgs: restArgs });
91
45
  return;
92
46
  }
93
47
 
@@ -95,7 +49,7 @@ export async function runCli(rawArgs = process.argv.slice(2)): Promise<void> {
95
49
  // Extensionless existing files remain valid transcription inputs; missing
96
50
  // bare tokens are more likely command typos and should not start the engine.
97
51
  if (firstArg && !firstArg.startsWith("-") && !isPathLike(firstArg)) {
98
- const suggestion = suggestCommand(firstArg, SUBCOMMANDS);
52
+ const suggestion = suggestCommand(firstArg, Object.keys(SUBCOMMANDS));
99
53
  log.error(`unknown command '${firstArg}'`);
100
54
  if (suggestion && suggestion !== firstArg) {
101
55
  log.warn(`(Did you mean ${suggestion}?)`);
@@ -100,9 +100,8 @@ function readConfig(): DiagnosticLogConfig {
100
100
  if (!existsSync(statePath)) return defaultConfig();
101
101
  try {
102
102
  const parsed = JSON.parse(readFileSync(statePath, "utf8")) as Partial<DiagnosticLogConfig>;
103
- const parsedMode = parseMode(parsed.mode);
104
103
  return {
105
- mode: parsedMode ?? defaultConfig().mode,
104
+ mode: parseDiagnosticLogMode(parsed.mode) ?? defaultConfig().mode,
106
105
  maxBytes: positiveInt(parsed.maxBytes, DEFAULT_MAX_BYTES),
107
106
  retain: positiveInt(parsed.retain, DEFAULT_RETAIN),
108
107
  };
@@ -111,11 +110,7 @@ function readConfig(): DiagnosticLogConfig {
111
110
  }
112
111
  }
113
112
 
114
- export function parseDiagnosticLogMode(value: string): DiagnosticLogMode | null {
115
- return parseMode(value);
116
- }
117
-
118
- function parseMode(value: unknown): DiagnosticLogMode | null {
113
+ export function parseDiagnosticLogMode(value: unknown): DiagnosticLogMode | null {
119
114
  return value === "off" || value === "on" || value === "retain-on-failure" ? value : null;
120
115
  }
121
116
 
@@ -219,9 +214,11 @@ export function resetDiagnosticLogs(): { deleted: number; bytes: number; dir: st
219
214
 
220
215
  export function createDiagnosticLogSession(): DiagnosticLogSession {
221
216
  const status = getDiagnosticLogStatus();
217
+ if (status.mode === "off") {
218
+ return { event: () => false, finish: () => false };
219
+ }
222
220
  const buffered: Uint8Array[] = [];
223
221
  let finished = false;
224
- if (status.mode === "off") return noopSession();
225
222
 
226
223
  return {
227
224
  event(event: string, fields: DiagnosticLogFields = {}): boolean {
@@ -261,13 +258,6 @@ export function createDiagnosticLogSession(): DiagnosticLogSession {
261
258
  };
262
259
  }
263
260
 
264
- function noopSession(): DiagnosticLogSession {
265
- return {
266
- event: () => false,
267
- finish: () => false,
268
- };
269
- }
270
-
271
261
  function appendDiagnosticLogLine(line: Uint8Array, status: DiagnosticLogStatus): void {
272
262
  mkdirSync(status.dir, { recursive: true });
273
263
  rotateIfNeeded(status.activePath, line.byteLength, status.maxBytes, status.retain);
package/src/engine.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { existsSync, statSync } from "fs";
2
2
  import { join } from "path";
3
+ import { installHint } from "./install-hint";
3
4
  import { log } from "./log";
4
5
  import { defaultEngineBinPath, keshaCacheDir } from "./paths";
5
6
  import { engineAbortError, registerProcessTree } from "./process-tree";
@@ -219,17 +220,21 @@ export async function preflightTranscribeEngineWithSegments(
219
220
  if (hasDiarizeModelLayout(modelPath)) return;
220
221
  throw new Error(
221
222
  `speaker diarization requires a model path\n\nCaused by:\n diarization model not found at ${modelPath}. ` +
222
- "Run `kesha install --diarize` (or set KESHA_DIARIZE_MODEL_PATH).",
223
+ `Run \`${installHint("--diarize")}\` (or set KESHA_DIARIZE_MODEL_PATH).`,
223
224
  );
224
225
  }
225
226
 
227
+ function vadArg(vad: VadMode | undefined): string[] {
228
+ if (vad === "on") return ["--vad"];
229
+ if (vad === "off") return ["--no-vad"];
230
+ return [];
231
+ }
232
+
226
233
  export async function transcribeEngine(
227
234
  audioPath: string,
228
235
  opts: TranscribeEngineOptions = {},
229
236
  ): Promise<string> {
230
- const args = ["transcribe", audioPath];
231
- if (opts.vad === "on") args.push("--vad");
232
- else if (opts.vad === "off") args.push("--no-vad");
237
+ const args = ["transcribe", audioPath, ...vadArg(opts.vad)];
233
238
  const { stdout, stderr, exitCode } = await runEngine(args, { signal: opts.signal });
234
239
  if (exitCode !== 0) {
235
240
  throw new Error(stderr || `kesha-engine exited with code ${exitCode}`);
@@ -266,12 +271,8 @@ export async function transcribeEngineWithSegments(
266
271
  ): Promise<TranscriptionOutput> {
267
272
  await preflightTranscribeEngineWithSegments(opts);
268
273
 
269
- const args = ["transcribe", audioPath, "--json"];
270
- if (opts.vad === "on") args.push("--vad");
271
- else if (opts.vad === "off") args.push("--no-vad");
272
- if (opts.speakers) {
273
- args.push("--speakers");
274
- }
274
+ const args = ["transcribe", audioPath, "--json", ...vadArg(opts.vad)];
275
+ if (opts.speakers) args.push("--speakers");
275
276
  const { stdout, stderr, exitCode } = await runEngine(args, { signal: opts.signal });
276
277
  if (exitCode !== 0) {
277
278
  throw new Error(stderr || `kesha-engine exited with code ${exitCode}`);
@@ -0,0 +1,12 @@
1
+ // Pick the right setup-command hint for error/warning messages. Humans on an
2
+ // interactive TTY get `kesha init` (guided + idempotent), agents and scripts
3
+ // piping stderr get `kesha install [...flags]` (deterministic + scriptable).
4
+ //
5
+ // Flags forward to BOTH paths. `kesha init` accepts the same module-selection
6
+ // flags as `kesha install` (`--tts`, `--vad`, `--diarize`, `--coreml`, `--onnx`,
7
+ // `--no-cache`) — they preselect modules in the interactive wizard rather than
8
+ // being install-only flags. Source of truth: `src/cli/init.ts::initCommand.args`.
9
+ export function installHint(...flags: string[]): string {
10
+ const verb = process.stderr.isTTY ? "kesha init" : "kesha install";
11
+ return [verb, ...flags].join(" ");
12
+ }
package/src/status.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { readdirSync, statSync } from "fs";
2
2
  import { join } from "path";
3
3
  import { isEngineInstalled, getEngineBinPath, getEngineCapabilities } from "./engine";
4
+ import { installHint } from "./install-hint";
4
5
  import { log } from "./log";
5
6
  import { keshaCacheDir } from "./paths";
6
7
  import { fluidKokoroCacheInfo } from "./fluid-kokoro-cache";
@@ -82,7 +83,7 @@ export async function showStatus(options: ShowStatusOptions = {}): Promise<void>
82
83
  }
83
84
 
84
85
  if (!installed) {
85
- log.warn('Run "kesha install" to download the engine and models.');
86
+ log.warn(`Run \`${installHint()}\` to download the engine and models.`);
86
87
  return;
87
88
  }
88
89
  }
package/src/synth.ts CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  spawnStdioWithDebugFd,
6
6
  type EngineCapabilities,
7
7
  } from "./engine";
8
+ import { installHint } from "./install-hint";
8
9
  import { log } from "./log";
9
10
  import { registerProcessTree } from "./process-tree";
10
11
 
@@ -116,7 +117,7 @@ export async function say(opts: SayOptions): Promise<Uint8Array> {
116
117
 
117
118
  if (!isEngineInstalled()) {
118
119
  throw new SayError(
119
- "kesha-engine not installed. run: kesha install",
120
+ `kesha-engine not installed. run: ${installHint("--tts")}`,
120
121
  1,
121
122
  "",
122
123
  );
package/src/transcribe.ts CHANGED
@@ -6,6 +6,7 @@ import {
6
6
  type TranscriptionOutput,
7
7
  type VadMode,
8
8
  } from "./engine";
9
+ import { installHint } from "./install-hint";
9
10
 
10
11
  export type { VadMode };
11
12
  export type { TranscriptionOutput };
@@ -31,13 +32,10 @@ export async function transcribe(audioPath: string, opts: TranscribeOptions = {}
31
32
  export async function preflightTranscribeWithSegments(opts: TranscribeOptions = {}): Promise<void> {
32
33
  if (!isEngineInstalled()) {
33
34
  throw new Error(
34
- "Error: No transcription backend is installed\n\n" +
35
- "╔══════════════════════════════════════════════════════════╗\n" +
36
- "║ Please run the following commands to get started: ║\n" +
37
- "║ ║\n" +
38
- "║ bun add -g @drakulavich/kesha-voice-kit ║\n" +
39
- "║ kesha install ║\n" +
40
- "╚══════════════════════════════════════════════════════════╝",
35
+ "Error: No transcription backend is installed.\n\n" +
36
+ "Run the following to get started:\n\n" +
37
+ " bun add -g @drakulavich/kesha-voice-kit\n" +
38
+ ` ${installHint()}`,
41
39
  );
42
40
  }
43
41