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

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
@@ -96,10 +96,10 @@ Each segment gets a `speaker` integer (cluster ID, stable within one file). Linu
96
96
 
97
97
  ## Text-to-speech
98
98
 
99
- Kesha speaks back via Kokoro-82M (English) and Vosk-TTS (Russian) — voice auto-picks from the text's language. On darwin-arm64, Kokoro uses FluidAudio CoreML instead of ONNX:
99
+ Kesha speaks back via Kokoro-82M (English) and Vosk-TTS (Russian) — voice auto-picks from the text's language. On darwin-arm64, Kokoro uses FluidAudio CoreML instead of ONNX; FluidAudio stores that CoreML cache at `~/.cache/fluidaudio/Models/kokoro`, outside Kesha's pinned model cache:
100
100
 
101
101
  ```bash
102
- kesha install --tts # ~990MB (Kokoro + Vosk-TTS RU, opt-in)
102
+ kesha install --tts # TTS, opt-in; Darwin Kokoro uses FluidAudio cache
103
103
  kesha say "Hello, world" > hello.wav
104
104
  kesha say "Привет, мир" > privet.wav # auto-routes (Milena on darwin, ru-vosk-m02 elsewhere)
105
105
  ```
@@ -288,7 +288,7 @@ are missing.
288
288
  | SpeechBrain ECAPA-TDNN | Audio language detection | ~86MB | [HuggingFace](https://huggingface.co/speechbrain/lang-id-voxlingua107-ecapa) |
289
289
  | Apple NLLanguageRecognizer | Text language detection | built-in | macOS system framework |
290
290
  | Silero VAD v5 (opt-in) | Voice activity detection | ~2.3MB | [snakers4/silero-vad](https://github.com/snakers4/silero-vad) |
291
- | Kokoro-82M / Vosk-TTS (opt-in) | Text-to-speech | ~990MB | [FluidAudio Kokoro](https://github.com/FluidInference/FluidAudio) on darwin-arm64; ONNX Kokoro elsewhere · [Vosk-TTS](https://github.com/alphacep/vosk-tts) |
291
+ | Kokoro-82M / Vosk-TTS (opt-in) | Text-to-speech | ~990MB | [FluidAudio Kokoro](https://github.com/FluidInference/FluidAudio) on darwin-arm64 (FluidAudio cache, not Kesha-verified); ONNX Kokoro elsewhere · [Vosk-TTS](https://github.com/alphacep/vosk-tts) |
292
292
 
293
293
  All models run through `kesha-engine` — a Rust binary using [FluidAudio](https://github.com/FluidInference/FluidAudio) (CoreML) on Apple Silicon and [ort](https://github.com/pykeio/ort) (ONNX Runtime) on other platforms.
294
294
 
@@ -321,9 +321,23 @@ Kesha can collect local diagnostics without downloading models or mutating cache
321
321
  ```bash
322
322
  kesha doctor --json --redact
323
323
  kesha support-bundle --output kesha-support.tar.gz
324
+ kesha support-bundle --include-logs --output kesha-support-with-logs.tar.gz
325
+ kesha logs status
326
+ kesha logs status --json
324
327
  ```
325
328
 
326
329
  `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.
330
+ Diagnostic log contents are excluded by default; pass `--include-logs` to add a
331
+ bounded tail of Kesha's privacy-safe NDJSON diagnostic log.
332
+
333
+ `kesha logs` manages local, rotated diagnostic logs for troubleshooting. Logs
334
+ default to `retain-on-failure` and also support explicit `off` and `on` modes.
335
+ Use `kesha logs status --json` for a stable machine-readable status contract
336
+ covering the active path, mode, size, rotation settings, and rotated-file list.
337
+ They use content-free NDJSON events: command/stage names, versions, durations,
338
+ exit codes, and coarse audio metadata only. They must not store audio,
339
+ transcripts, input text, generated speech text, file names, full paths, raw
340
+ stdout/stderr, environment variables, tokens, or URLs. See [Diagnostic logs](docs/diagnostic-logs.md).
327
341
 
328
342
  ## Local Stats privacy and lifecycle
329
343
 
@@ -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 init install manpage record say stats status support-bundle"
9
+ commands="completions doctor init install logs manpage record say stats status support-bundle"
10
10
 
11
11
  if [[ "$COMP_CWORD" -eq 1 ]]; then
12
12
  if [[ "$cur" == -* ]]; then
@@ -22,12 +22,13 @@ _kesha_completion() {
22
22
  doctor) opts="--help -h --json --redact" ;;
23
23
  init) opts="--help -h --coreml --onnx --no-cache --plan --yes --tts --vad --diarize" ;;
24
24
  install) opts="--help -h --coreml --onnx --no-cache --plan --tts --vad --diarize" ;;
25
+ logs) opts="--help -h --json" ;;
25
26
  manpage) opts="--help -h" ;;
26
27
  record) opts="--help -h --out --max-seconds --debug" ;;
27
28
  say) opts="--help -h --voice --lang --out --rate --list-voices --ssml --format --bitrate --sample-rate --no-expand-abbrev --verbose --debug" ;;
28
29
  stats) opts="--help -h --format" ;;
29
30
  status) opts="--help -h --disk" ;;
30
- support-bundle) opts="--help -h --output" ;;
31
+ support-bundle) opts="--help -h --output --include-logs" ;;
31
32
  *) opts="--help -h --version -v --json --toon --timestamps --speakers --include-errors --verbose --format --lang --debug --vad --no-vad" ;;
32
33
  esac
33
34
 
@@ -4,6 +4,7 @@ complete -c kesha -n '__fish_use_subcommand' -a 'completions' -d 'Print shell co
4
4
  complete -c kesha -n '__fish_use_subcommand' -a 'doctor' -d 'Collect support diagnostics without changing local state'
5
5
  complete -c kesha -n '__fish_use_subcommand' -a 'init' -d 'Interactive setup guide for Kesha features'
6
6
  complete -c kesha -n '__fish_use_subcommand' -a 'install' -d 'Download inference engine and models'
7
+ complete -c kesha -n '__fish_use_subcommand' -a 'logs' -d 'Manage local privacy-safe diagnostic logs'
7
8
  complete -c kesha -n '__fish_use_subcommand' -a 'manpage' -d 'Print the kesha(1) manpage'
8
9
  complete -c kesha -n '__fish_use_subcommand' -a 'record' -d 'Record microphone audio to a WAV file'
9
10
  complete -c kesha -n '__fish_use_subcommand' -a 'say' -d 'Synthesize speech from text (TTS). Writes audio to stdout (or --out file). Defaults to WAV; use --format ogg-opus for messenger-ready voice notes.'
@@ -50,6 +51,9 @@ complete -c kesha -n '__fish_seen_subcommand_from install' -l plan -d 'Show down
50
51
  complete -c kesha -n '__fish_seen_subcommand_from install' -l tts -d 'Also install TTS models (Kokoro EN + Vosk-TTS RU, ~990MB)'
51
52
  complete -c kesha -n '__fish_seen_subcommand_from install' -l vad -d 'Also install Silero VAD (~2.3MB) for long-audio preprocessing'
52
53
  complete -c kesha -n '__fish_seen_subcommand_from install' -l diarize -d 'Also install the Sortformer streaming-diarization model (~245MB, darwin-arm64 only — #199)'
54
+ complete -c kesha -n '__fish_seen_subcommand_from logs' -l help -d 'Show help'
55
+ complete -c kesha -n '__fish_seen_subcommand_from logs' -s h -d 'Show help'
56
+ complete -c kesha -n '__fish_seen_subcommand_from logs' -l json -d 'Output status as JSON'
53
57
  complete -c kesha -n '__fish_seen_subcommand_from manpage' -l help -d 'Show help'
54
58
  complete -c kesha -n '__fish_seen_subcommand_from manpage' -s h -d 'Show help'
55
59
  complete -c kesha -n '__fish_seen_subcommand_from record' -l help -d 'Show help'
@@ -80,3 +84,4 @@ complete -c kesha -n '__fish_seen_subcommand_from status' -l disk -d 'Include re
80
84
  complete -c kesha -n '__fish_seen_subcommand_from support-bundle' -l help -d 'Show help'
81
85
  complete -c kesha -n '__fish_seen_subcommand_from support-bundle' -s h -d 'Show help'
82
86
  complete -c kesha -n '__fish_seen_subcommand_from support-bundle' -l output -r -d 'Write archive to this .tar.gz path'
87
+ complete -c kesha -n '__fish_seen_subcommand_from support-bundle' -l include-logs -d 'Include a bounded tail of privacy-safe diagnostic logs'
@@ -8,6 +8,7 @@ _kesha() {
8
8
  'doctor:Collect support diagnostics without changing local state'
9
9
  'init:Interactive setup guide for Kesha features'
10
10
  'install:Download inference engine and models'
11
+ 'logs:Manage local privacy-safe diagnostic logs'
11
12
  'manpage:Print the kesha(1) manpage'
12
13
  'record:Record microphone audio to a WAV file'
13
14
  'say:Synthesize speech from text (TTS). Writes audio to stdout (or --out file). Defaults to WAV; use --format ogg-opus for messenger-ready voice notes.'
@@ -73,6 +74,11 @@ _kesha() {
73
74
  '--vad[Also install Silero VAD (~2.3MB) for long-audio preprocessing]' \
74
75
  '--diarize[Also install the Sortformer streaming-diarization model (~245MB, darwin-arm64 only — #199)]'
75
76
  ;;
77
+ logs)
78
+ _arguments '--help[Show help]' \
79
+ '-h[Show help]' \
80
+ '--json[Output status as JSON]'
81
+ ;;
76
82
  manpage)
77
83
  _arguments '--help[Show help]' \
78
84
  '-h[Show help]'
@@ -113,7 +119,8 @@ _kesha() {
113
119
  support-bundle)
114
120
  _arguments '--help[Show help]' \
115
121
  '-h[Show help]' \
116
- '--output=[Write archive to this .tar.gz path]:output:'
122
+ '--output=[Write archive to this .tar.gz path]:output:' \
123
+ '--include-logs[Include a bounded tail of privacy-safe diagnostic logs]'
117
124
  ;;
118
125
  *)
119
126
  _arguments '--help[Show help]' \
@@ -0,0 +1,93 @@
1
+ # Diagnostic Logs
2
+
3
+ Kesha diagnostic logs are local, rotated NDJSON event logs for install, engine,
4
+ and runtime troubleshooting. They are separate from Kesha Stats: Stats stores
5
+ aggregate timings in SQLite after explicit opt-in, while diagnostic logs keep a
6
+ small local ring buffer of structured events.
7
+
8
+ Diagnostic logs default to `retain-on-failure`: passing commands leave no log
9
+ file, while failed commands can preserve a small local trace for debugging.
10
+
11
+ ```bash
12
+ kesha logs status
13
+ kesha logs status --json
14
+ kesha logs enable
15
+ kesha logs disable
16
+ kesha logs mode retain-on-failure
17
+ kesha logs path
18
+ kesha logs reset
19
+ ```
20
+
21
+ ## Modes
22
+
23
+ Diagnostic logs follow a Playwright-style retention model:
24
+
25
+ - `off`: do not record diagnostic events.
26
+ - `on`: append events to the rotated local log immediately.
27
+ - `retain-on-failure`: buffer events for the current command and write them only
28
+ if the command fails. This is the default.
29
+
30
+ `kesha logs enable` is shorthand for `kesha logs mode on`; `kesha logs disable`
31
+ is shorthand for `kesha logs mode off`. `retain-on-failure` keeps passing runs
32
+ artifact-free while failed runs keep enough context to debug.
33
+
34
+ `kesha logs status --json` prints the same local status as a stable JSON object:
35
+ `dir`, `activePath`, `statePath`, `exists`, `activeSizeBytes`, `rotatedFiles`,
36
+ `totalSizeBytes`, `mode`, `maxBytes`, and `retain`.
37
+
38
+ ## Privacy Contract
39
+
40
+ Diagnostic logs are content-free. Event payloads are built from an allowlist of
41
+ typed fields such as command name, app version, platform, backend, feature flags,
42
+ stage names, exit codes, duration numbers, and stable error codes.
43
+
44
+ When enabled, `kesha install`, `kesha <audio>`, and `kesha say` record command
45
+ lifecycle events such as `command.start`, `input.audio`, `input.missing`,
46
+ `engine.exit`, and `command.finish`. These events use only counts, booleans,
47
+ format extensions, duration milliseconds, and bucket labels. In the default
48
+ `retain-on-failure` mode, successful runs still leave no log file behind.
49
+
50
+ Diagnostic logs must not store:
51
+
52
+ - audio bytes
53
+ - transcripts
54
+ - input text or generated speech text
55
+ - file names, basenames, or full file paths
56
+ - raw stdout or stderr
57
+ - environment variables
58
+ - URLs, tokens, secrets, API keys, or cloud identifiers
59
+ - model files or model contents
60
+
61
+ For audio shape, logs should use coarse metadata such as extension, duration
62
+ bucket, size bucket, sample-rate bucket, or channel count. Do not log a path or
63
+ basename even when it looks harmless; names like `therapy-session.m4a` can be
64
+ private.
65
+
66
+ ## Storage
67
+
68
+ Default paths:
69
+
70
+ - macOS: `~/Library/Logs/kesha/kesha.ndjson`
71
+ - Linux: `${XDG_STATE_HOME:-~/.local/state}/kesha/logs/kesha.ndjson`
72
+ - Windows: `%LOCALAPPDATA%\kesha\logs\kesha.ndjson`
73
+
74
+ Set `KESHA_LOG_DIR` to override the directory. The active file is
75
+ `kesha.ndjson`; rotated files are named `kesha.1.ndjson`, `kesha.2.ndjson`, and
76
+ so on.
77
+
78
+ The first implementation rotates at 10 MB and keeps 5 rotated files. `kesha logs
79
+ reset` deletes Kesha log files but preserves the selected mode.
80
+
81
+ ## Support Bundles
82
+
83
+ `kesha support-bundle` does not include diagnostic log contents by default. Use
84
+ `kesha support-bundle --include-logs` to add a bounded tail of the active
85
+ already-sanitized NDJSON log when a support issue needs recent command events.
86
+ Recommended flow for a bug report:
87
+
88
+ ```bash
89
+ kesha logs status
90
+ kesha logs mode retain-on-failure
91
+ # reproduce the failure
92
+ kesha support-bundle --include-logs --output kesha-support-with-logs.tar.gz
93
+ ```
package/man/kesha.1 CHANGED
@@ -1,4 +1,4 @@
1
- .TH KESHA 1 "May 2026" "kesha 1.19.0-beta.1" "User Commands"
1
+ .TH KESHA 1 "May 2026" "kesha 1.19.0-beta.2" "User Commands"
2
2
  .SH NAME
3
3
  kesha \- open-source local voice toolkit
4
4
  .SH SYNOPSIS
@@ -23,6 +23,9 @@ Interactive setup guide for Kesha features
23
23
  .B install
24
24
  Download inference engine and models
25
25
  .TP
26
+ .B logs
27
+ Manage local privacy\-safe diagnostic logs
28
+ .TP
26
29
  .B manpage
27
30
  Print the kesha(1) manpage
28
31
  .TP
@@ -129,6 +132,10 @@ Also install Silero VAD (~2.3MB) for long\-audio preprocessing
129
132
  .TP
130
133
  .B \-\-diarize
131
134
  Also install the Sortformer streaming\-diarization model (~245MB, darwin\-arm64 only — #199)
135
+ .SS logs
136
+ .TP
137
+ .B \-\-json
138
+ Output status as JSON
132
139
  .SS record
133
140
  .TP
134
141
  .B \-\-out
@@ -188,6 +195,9 @@ Include recursive cache disk usage
188
195
  .TP
189
196
  .B \-\-output
190
197
  Write archive to this .tar.gz path
198
+ .TP
199
+ .B \-\-include\-logs
200
+ Include a bounded tail of privacy\-safe diagnostic logs
191
201
  .SH FILES
192
202
  .TP
193
203
  .B completions/kesha.bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drakulavich/kesha-voice-kit",
3
- "version": "1.19.0-beta.1",
3
+ "version": "1.19.0-beta.2",
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": {
@@ -19,9 +19,9 @@
19
19
  "tsconfig.json",
20
20
  "openclaw.plugin.json",
21
21
  "openclaw-plugin.cjs",
22
+ "docs/diagnostic-logs.md",
22
23
  "docs/product-positioning.md",
23
24
  "SKILL.md",
24
- "scripts/postinstall.cjs",
25
25
  "LICENSE",
26
26
  "NOTICES.md",
27
27
  "README.md"
@@ -32,7 +32,7 @@
32
32
  ]
33
33
  },
34
34
  "keshaEngine": {
35
- "version": "1.19.0-beta.1"
35
+ "version": "1.19.0-beta.2"
36
36
  },
37
37
  "scripts": {
38
38
  "test": "bun test",
@@ -49,8 +49,7 @@
49
49
  "check": "bun run lint && bun run check:versions && bun run test:cli-fast",
50
50
  "check:workflows": "bun .github/scripts/check-workflows.ts",
51
51
  "check:release-manifest": "bun .github/scripts/release-manifest.mjs --check",
52
- "check:versions": "bun .github/scripts/check-versions.ts",
53
- "postinstall": "node scripts/postinstall.cjs"
52
+ "check:versions": "bun .github/scripts/check-versions.ts"
54
53
  },
55
54
  "keywords": [
56
55
  "asr",
@@ -6,6 +6,7 @@ import { completionsCommand } from "./completions";
6
6
  import { doctorCommand } from "./doctor";
7
7
  import { initCommand } from "./init";
8
8
  import { installCommand } from "./install";
9
+ import { logsCommand } from "./logs";
9
10
  import { manpageCommand } from "./manpage";
10
11
  import { recordCommand } from "./record";
11
12
  import { sayCommand } from "./say";
@@ -18,6 +19,7 @@ const SUBCOMMANDS = [
18
19
  "doctor",
19
20
  "init",
20
21
  "install",
22
+ "logs",
21
23
  "status",
22
24
  "record",
23
25
  "say",
@@ -54,6 +56,11 @@ export async function runCli(rawArgs = process.argv.slice(2)): Promise<void> {
54
56
  return;
55
57
  }
56
58
 
59
+ if (firstArg === "logs") {
60
+ await runMain(logsCommand, { rawArgs: restArgs });
61
+ return;
62
+ }
63
+
57
64
  if (firstArg === "manpage") {
58
65
  await runMain(manpageCommand, { rawArgs: restArgs });
59
66
  return;
@@ -5,6 +5,7 @@ import { renderInstallPlan } from "../install-plan";
5
5
  import { maybeAskForStar } from "../star";
6
6
  import { log } from "../log";
7
7
  import { packageVersion } from "../package-info";
8
+ import { createDiagnosticLogSession, type DiagnosticLogSession } from "../diagnostic-log";
8
9
 
9
10
  export interface InstallCommandArgs {
10
11
  coreml: boolean;
@@ -46,6 +47,28 @@ function defaultBackendForPlatform(): string | undefined {
46
47
  return undefined;
47
48
  }
48
49
 
50
+ type InstallDiagnosticErrorKind = "validation_failed" | "install_failed";
51
+
52
+ function finishInstallDiagnostic(
53
+ diagnosticLog: DiagnosticLogSession | null,
54
+ startedAt: number,
55
+ status: "success" | "failed",
56
+ errorKind?: InstallDiagnosticErrorKind,
57
+ ): void {
58
+ if (!diagnosticLog) return;
59
+ try {
60
+ diagnosticLog.event("command.finish", {
61
+ command: "install",
62
+ status,
63
+ durationMs: Math.round(performance.now() - startedAt),
64
+ ...(errorKind ? { errorKind } : {}),
65
+ });
66
+ diagnosticLog.finish(status);
67
+ } catch (err) {
68
+ log.debug(`install diagnostic log finish dropped: ${err instanceof Error ? err.message : String(err)}`);
69
+ }
70
+ }
71
+
49
72
  export async function performInstall(
50
73
  noCache: boolean,
51
74
  backend?: string,
@@ -58,26 +81,42 @@ export async function performInstall(
58
81
  log.info(await renderInstallPlan({ noCache, backend, tts, vad, diarize }));
59
82
  return;
60
83
  }
61
- if (diarize && !(process.platform === "darwin" && process.arch === "arm64")) {
62
- log.error(
63
- "--diarize is currently darwin-arm64 only " +
64
- "(see https://github.com/drakulavich/kesha-voice-kit/issues/199).",
65
- );
66
- process.exit(1);
67
- }
68
- const platformBackend = defaultBackendForPlatform();
69
- if (backend && !process.env.KESHA_ENGINE_BIN && platformBackend && backend !== platformBackend) {
70
- log.error(
71
- `Requested backend "${backend}" is not available on this platform; ` +
72
- `the release engine uses "${platformBackend}".`,
73
- );
74
- process.exit(1);
75
- }
84
+
85
+ let diagnosticLog: DiagnosticLogSession | null = null;
86
+ let errorKind: InstallDiagnosticErrorKind = "install_failed";
87
+ const startedAt = performance.now();
76
88
  try {
89
+ diagnosticLog = createDiagnosticLogSession();
90
+ diagnosticLog.event("command.start", {
91
+ command: "install",
92
+ backend: backend ?? "auto",
93
+ noCache,
94
+ tts,
95
+ vad,
96
+ diarize,
97
+ });
98
+
99
+ if (diarize && !(process.platform === "darwin" && process.arch === "arm64")) {
100
+ errorKind = "validation_failed";
101
+ throw new Error(
102
+ "--diarize is currently darwin-arm64 only " +
103
+ "(see https://github.com/drakulavich/kesha-voice-kit/issues/199).",
104
+ );
105
+ }
106
+ const platformBackend = defaultBackendForPlatform();
107
+ if (backend && !process.env.KESHA_ENGINE_BIN && platformBackend && backend !== platformBackend) {
108
+ errorKind = "validation_failed";
109
+ throw new Error(
110
+ `Requested backend "${backend}" is not available on this platform; ` +
111
+ `the release engine uses "${platformBackend}".`,
112
+ );
113
+ }
77
114
  await downloadEngine(noCache, backend, { tts, vad, diarize });
78
115
  await maybeAskForStar(getEngineBinPath(), packageVersion, log);
116
+ finishInstallDiagnostic(diagnosticLog, startedAt, "success");
79
117
  } catch (err: unknown) {
80
118
  const message = err instanceof Error ? err.message : String(err);
119
+ finishInstallDiagnostic(diagnosticLog, startedAt, "failed", errorKind);
81
120
  log.error(message);
82
121
  process.exit(1);
83
122
  }
@@ -0,0 +1,107 @@
1
+ import { defineCommand } from "citty";
2
+ import {
3
+ getDiagnosticLogStatus,
4
+ humanBytes,
5
+ parseDiagnosticLogMode,
6
+ resetDiagnosticLogs,
7
+ resolveDiagnosticLogPath,
8
+ setDiagnosticLogMode,
9
+ } from "../diagnostic-log";
10
+ import { log } from "../log";
11
+
12
+ interface LogsCommandArgs {
13
+ action?: string;
14
+ value?: string;
15
+ json?: boolean;
16
+ }
17
+
18
+ export const logsCommand = defineCommand({
19
+ meta: {
20
+ name: "logs",
21
+ description: "Manage local privacy-safe diagnostic logs",
22
+ },
23
+ args: {
24
+ action: {
25
+ type: "positional",
26
+ required: false,
27
+ description: "Action: status | enable | disable | mode | path | reset",
28
+ },
29
+ value: {
30
+ type: "positional",
31
+ required: false,
32
+ description: "Action value: off | on | retain-on-failure",
33
+ },
34
+ json: {
35
+ type: "boolean",
36
+ description: "Output status as JSON",
37
+ default: false,
38
+ },
39
+ },
40
+ run({ args }: { args: LogsCommandArgs }) {
41
+ const action = args.action ?? "status";
42
+ if (args.json && action !== "status") {
43
+ log.error("usage: kesha logs status --json");
44
+ process.exit(2);
45
+ }
46
+ switch (action) {
47
+ case "enable": {
48
+ const status = setDiagnosticLogMode("on");
49
+ log.success("Kesha diagnostic logs enabled");
50
+ log.info(`Mode: ${status.mode}`);
51
+ log.info(`Path: ${status.activePath}`);
52
+ log.info(`Rotation: ${humanBytes(status.maxBytes)}, keep ${status.retain}`);
53
+ return;
54
+ }
55
+ case "disable": {
56
+ const status = setDiagnosticLogMode("off");
57
+ log.info("Kesha diagnostic logs disabled");
58
+ log.info(`Mode: ${status.mode}`);
59
+ log.info(`Path: ${status.activePath}`);
60
+ return;
61
+ }
62
+ case "status": {
63
+ const status = getDiagnosticLogStatus();
64
+ if (args.json) {
65
+ console.log(JSON.stringify(status, null, 2));
66
+ return;
67
+ }
68
+ log.info(`Kesha diagnostic logs: ${status.mode === "off" ? "disabled" : "enabled"}`);
69
+ log.info(`Mode: ${status.mode}`);
70
+ log.info(`Path: ${status.activePath}`);
71
+ log.info(`Size: ${humanBytes(status.totalSizeBytes)}`);
72
+ log.info(`Rotated files: ${status.rotatedFiles.length}`);
73
+ log.info(`Rotation: ${humanBytes(status.maxBytes)}, keep ${status.retain}`);
74
+ return;
75
+ }
76
+ case "mode": {
77
+ if (!args.value) {
78
+ const status = getDiagnosticLogStatus();
79
+ log.info(`Kesha diagnostic log mode: ${status.mode}`);
80
+ return;
81
+ }
82
+ const mode = parseDiagnosticLogMode(args.value);
83
+ if (!mode) {
84
+ log.error("usage: kesha logs mode <off|on|retain-on-failure>");
85
+ process.exit(2);
86
+ }
87
+ const status = setDiagnosticLogMode(mode);
88
+ log.info(`Kesha diagnostic log mode set to ${status.mode}`);
89
+ log.info(`Path: ${status.activePath}`);
90
+ return;
91
+ }
92
+ case "path": {
93
+ log.info(resolveDiagnosticLogPath());
94
+ return;
95
+ }
96
+ case "reset": {
97
+ const result = resetDiagnosticLogs();
98
+ log.info(`Kesha diagnostic logs reset: ${result.deleted} file(s), ${humanBytes(result.bytes)} deleted`);
99
+ return;
100
+ }
101
+ default:
102
+ log.error(`unknown logs action '${action}'`);
103
+ log.warn("supported: enable, disable, mode, status, path, reset");
104
+ process.exit(2);
105
+ }
106
+ },
107
+ });
package/src/cli/main.ts CHANGED
@@ -18,6 +18,8 @@ import { artifactFromFile, createStatsRecorder } from "../stats";
18
18
  import { createPercentProgress } from "../progress";
19
19
  import { getPendingSignalExitCode, waitForPendingSignalCleanup } from "../process-tree";
20
20
  import type { TranscriptionSegment } from "../types";
21
+ import { createDiagnosticLogSession } from "../diagnostic-log";
22
+ import { diagnosticSizeBucket } from "../diagnostic-events";
21
23
 
22
24
  interface MainCommandArgs {
23
25
  _: string[];
@@ -149,6 +151,7 @@ export const mainCommand = defineCommand({
149
151
  " doctor Collect support diagnostics.\n" +
150
152
  " init Interactive setup guide for Kesha features.\n" +
151
153
  " install Download engine and models.\n" +
154
+ " logs Manage local privacy-safe diagnostic logs.\n" +
152
155
  " manpage Print the kesha(1) manpage.\n" +
153
156
  " record Record microphone audio to a WAV file.\n" +
154
157
  " status Inspect installed backend.\n" +
@@ -253,6 +256,7 @@ export const mainCommand = defineCommand({
253
256
  process.exit(2);
254
257
  }
255
258
  const vadMode = vad ? "on" : noVad ? "off" : "auto";
259
+ const outputFormat = wantsJson ? "json" : wantsToon ? "toon" : wantsTranscript ? "transcript" : "text";
256
260
 
257
261
  if (files.length === 0) {
258
262
  log.info(
@@ -260,6 +264,7 @@ export const mainCommand = defineCommand({
260
264
  " kesha completions <bash|zsh|fish>\n" +
261
265
  " kesha doctor [--json] [--redact]\n" +
262
266
  " kesha install [--no-cache]\n" +
267
+ " kesha logs [enable|disable|mode|status|path|reset]\n" +
263
268
  " kesha manpage\n" +
264
269
  " kesha record --out path.wav [--max-seconds 120]\n" +
265
270
  " kesha status\n" +
@@ -274,6 +279,17 @@ export const mainCommand = defineCommand({
274
279
  const results: TranscribeResult[] = [];
275
280
  const errors: TranscribeErrorRecord[] = [];
276
281
  const stats = createStatsRecorder("transcribe");
282
+ const diagnosticLog = createDiagnosticLogSession();
283
+ diagnosticLog.event("command.start", {
284
+ command: "transcribe",
285
+ itemCount: files.length,
286
+ outputFormat,
287
+ vadMode,
288
+ timestamps: args.timestamps,
289
+ speakers: args.speakers,
290
+ includeErrors: args["include-errors"],
291
+ hasExpectedLang: Boolean(args.lang),
292
+ });
277
293
 
278
294
  const wantsLangId = !!(args.lang || args.verbose || wantsJson || wantsToon || wantsTranscript);
279
295
  const reportProgress = shouldReportTranscribeProgress({
@@ -286,12 +302,20 @@ export const mainCommand = defineCommand({
286
302
  if (!existsSync(file)) {
287
303
  hasError = true;
288
304
  stats.recordError("input", new Error("File not found"), "file_not_found");
305
+ diagnosticLog.event("input.missing", { command: "transcribe" });
289
306
  errors.push({ file, code: "file_not_found", message: "File not found" });
290
307
  log.error(`${file}: File not found`);
291
308
  continue;
292
309
  }
293
310
  const inputArtifact = artifactFromFile(file, "input_audio");
294
- if (inputArtifact) stats.recordArtifact(inputArtifact);
311
+ if (inputArtifact) {
312
+ stats.recordArtifact(inputArtifact);
313
+ diagnosticLog.event("input.audio", {
314
+ command: "transcribe",
315
+ format: inputArtifact.format || null,
316
+ sizeBucket: diagnosticSizeBucket(inputArtifact.sizeBytes),
317
+ });
318
+ }
295
319
 
296
320
  const startedAt = performance.now();
297
321
  let progress: ReturnType<typeof createPercentProgress> | null = null;
@@ -368,11 +392,24 @@ export const mainCommand = defineCommand({
368
392
  result.segments = segments;
369
393
  }
370
394
  results.push(result);
395
+ diagnosticLog.event("engine.exit", {
396
+ command: "transcribe",
397
+ status: "success",
398
+ durationMs: sttTimeMs,
399
+ segmentCount: segments.length,
400
+ ranAudioLangId: audioResult !== null,
401
+ ranTxtLangId: textLanguage !== undefined,
402
+ });
371
403
  progress?.finish(`Transcribed ${file}`);
372
404
  } catch (err: unknown) {
373
405
  progress?.stop();
374
406
  hasError = true;
375
407
  stats.recordError("transcribe", err);
408
+ diagnosticLog.event("engine.exit", {
409
+ command: "transcribe",
410
+ status: "failed",
411
+ errorKind: "transcribe_failed",
412
+ });
376
413
  const message = err instanceof Error ? err.message : String(err);
377
414
  errors.push({ file, code: "transcribe_failed", message });
378
415
  log.error(`${file}: ${message}`);
@@ -392,6 +429,14 @@ export const mainCommand = defineCommand({
392
429
  }
393
430
 
394
431
  stats.finish(hasError ? "failed" : "success", files.length);
432
+ diagnosticLog.event("command.finish", {
433
+ command: "transcribe",
434
+ status: hasError ? "failed" : "success",
435
+ itemCount: files.length,
436
+ resultCount: results.length,
437
+ errorCount: errors.length,
438
+ });
439
+ diagnosticLog.finish(hasError ? "failed" : "success");
395
440
 
396
441
  if (hasError) {
397
442
  const signalExitCode = getPendingSignalExitCode();