@drakulavich/kesha-voice-kit 1.15.0 → 1.16.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/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  <h1 align="center">Kesha Voice Kit</h1>
6
6
 
7
7
  <p align="center">
8
- <a href="https://github.com/drakulavich/kesha-voice-kit/actions/workflows/ci.yml"><img src="https://github.com/drakulavich/kesha-voice-kit/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
8
+ <a href="https://flakiness.io/Laputa/kesha-voice-kit"><img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fflakiness.io%2Fapi%2Fbadge%3Finput%3D%257B%2522badgeToken%2522%253A%2522badge-2IKMRRqUxh9P3w8Ym3Szf0%2522%257D" alt="Tests"></a>
9
9
  <a href="https://www.npmjs.com/package/@drakulavich/kesha-voice-kit"><img src="https://img.shields.io/npm/v/@drakulavich/kesha-voice-kit" alt="npm version"></a>
10
10
  <a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License: MIT"></a>
11
11
  <a href="https://bun.sh"><img src="https://img.shields.io/badge/runtime-Bun-f9f1e1?logo=bun" alt="Bun"></a>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drakulavich/kesha-voice-kit",
3
- "version": "1.15.0",
3
+ "version": "1.16.0",
4
4
  "description": "Open-source voice toolkit. Speech-to-text (25 langs, ~19x faster than Whisper on Apple Silicon, ONNX fallback on CPU), text-to-speech (Kokoro + Vosk-TTS + ~180 macOS system voices, SSML), voice-activity detection, language detection (107 langs). Rust engine, OpenClaw skill.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -30,7 +30,7 @@
30
30
  ]
31
31
  },
32
32
  "keshaEngine": {
33
- "version": "1.15.0"
33
+ "version": "1.16.0"
34
34
  },
35
35
  "scripts": {
36
36
  "test": "bun test",
@@ -38,6 +38,7 @@
38
38
  "test:integration": "bun test tests/integration/",
39
39
  "lint": "bunx tsc --noEmit",
40
40
  "lint:tsgo": "bunx tsgo --noEmit",
41
+ "check:workflows": "bun .github/scripts/check-workflows.ts",
41
42
  "postinstall": "node scripts/postinstall.cjs"
42
43
  },
43
44
  "keywords": [
@@ -69,7 +70,8 @@
69
70
  "@types/bun": "latest",
70
71
  "@typescript/native-preview": "^7.0.0-dev.20260423.1",
71
72
  "fast-xml-parser": "^5.5.10",
72
- "typescript": "^6.0.2"
73
+ "typescript": "^6.0.2",
74
+ "yaml": "^2.9.0"
73
75
  },
74
76
  "dependencies": {
75
77
  "@toon-format/toon": "^2.1.0",
package/src/cli/main.ts CHANGED
@@ -34,6 +34,69 @@ export function detectLanguage(text: string): string {
34
34
  return detect(text);
35
35
  }
36
36
 
37
+ /**
38
+ * Pure validation + normalization of the output-format selection. Pulled
39
+ * out of the citty `run` handler so the contract is unit-testable without
40
+ * spawning the CLI binary; the handler just owns the side effects
41
+ * (log.error + process.exit) when this returns `{ ok: false }`.
42
+ *
43
+ * Inputs accept the three knobs the user can flip:
44
+ * - `--json` (boolean) — long-form alias for `--format json`
45
+ * - `--toon` (boolean) — long-form alias for `--format toon`
46
+ * - `--format <value>` — must be one of `transcript`, `json`, `toon`
47
+ *
48
+ * Mutex: `--json` and `--toon` cannot both be requested. Either via the
49
+ * booleans or `--format` cross-pollination (`--json --format toon` →
50
+ * error). The mutex check happens AFTER format validation, so unknown
51
+ * `--format` still surfaces with its own clearer error first.
52
+ */
53
+ export type ResolvedOutputFormat =
54
+ | {
55
+ ok: true;
56
+ wantsJson: boolean;
57
+ wantsToon: boolean;
58
+ wantsTranscript: boolean;
59
+ }
60
+ | { ok: false; error: string };
61
+
62
+ const SUPPORTED_FORMATS = ["transcript", "json", "toon"] as const;
63
+
64
+ export function resolveOutputFormat(input: {
65
+ json?: boolean;
66
+ toon?: boolean;
67
+ format?: string;
68
+ }): ResolvedOutputFormat {
69
+ if (input.format !== undefined && !SUPPORTED_FORMATS.includes(input.format as never)) {
70
+ return {
71
+ ok: false,
72
+ error: `unknown --format '${input.format}'. supported: ${SUPPORTED_FORMATS.join(", ")}`,
73
+ };
74
+ }
75
+ const wantsJson = !!input.json || input.format === "json";
76
+ const wantsToon = !!input.toon || input.format === "toon";
77
+ const wantsTranscript = input.format === "transcript";
78
+ if (wantsJson && wantsToon) {
79
+ return {
80
+ ok: false,
81
+ error: "--json and --toon are mutually exclusive (pick one output format).",
82
+ };
83
+ }
84
+ // `--format transcript` + boolean `--json` / `--toon` was previously
85
+ // accepted and silently produced the boolean's format (because the
86
+ // dispatch checked wantsJson/wantsToon first). Greptile P2 on #300
87
+ // flagged the silent override. Fail loudly with the same shape as
88
+ // the json/toon mutex — symmetric across all three formats.
89
+ if (wantsTranscript && (wantsJson || wantsToon)) {
90
+ return {
91
+ ok: false,
92
+ error:
93
+ "--format transcript is mutually exclusive with --json / --toon " +
94
+ "(pick one output format).",
95
+ };
96
+ }
97
+ return { ok: true, wantsJson, wantsToon, wantsTranscript };
98
+ }
99
+
37
100
  export function checkLanguageMismatch(expected: string | undefined, detected: string): string | null {
38
101
  if (!expected || !detected || expected === detected) return null;
39
102
  return `warning: expected language "${expected}" but detected "${detected}"`;
@@ -76,7 +139,7 @@ export const mainCommand = defineCommand({
76
139
  },
77
140
  format: {
78
141
  type: "string",
79
- description: "Output format: transcript (enriched text with lang/confidence)",
142
+ description: "Output format: transcript | json | toon (long-form alias for --json / --toon)",
80
143
  },
81
144
  lang: {
82
145
  type: "string",
@@ -102,21 +165,32 @@ export const mainCommand = defineCommand({
102
165
  if (args.debug) log.debugEnabled = true;
103
166
  const files = args._;
104
167
 
105
- if ((args.json || args.format === "json") && args.toon) {
106
- log.error("--json and --toon are mutually exclusive (pick one output format).");
168
+ // Validate `--format <value>` and normalize into the boolean flags
169
+ // that the rest of this handler consults. Routing happens in
170
+ // `resolveOutputFormat` so the contract is unit-testable without
171
+ // spawning the CLI; the handler just owns the side-effect arms
172
+ // (log.error + process.exit).
173
+ const fmt = resolveOutputFormat({
174
+ json: args.json,
175
+ toon: args.toon,
176
+ format: args.format,
177
+ });
178
+ if (!fmt.ok) {
179
+ log.error(fmt.error);
107
180
  process.exit(2);
108
181
  }
182
+ const { wantsJson, wantsToon, wantsTranscript } = fmt;
109
183
 
110
184
  if (args.vad && args["no-vad"]) {
111
185
  log.error("--vad and --no-vad are mutually exclusive.");
112
186
  process.exit(2);
113
187
  }
114
- if (args.timestamps && !(args.json || args.toon || args.format === "json")) {
115
- log.error("--timestamps requires --json, --toon, or --format json.");
188
+ if (args.timestamps && !(wantsJson || wantsToon)) {
189
+ log.error("--timestamps requires --json, --toon, or --format {json,toon}.");
116
190
  process.exit(2);
117
191
  }
118
- if (args.speakers && !(args.json || args.toon || args.format === "json")) {
119
- log.error("--speakers requires --json, --toon, or --format json.");
192
+ if (args.speakers && !(wantsJson || wantsToon)) {
193
+ log.error("--speakers requires --json, --toon, or --format {json,toon}.");
120
194
  process.exit(2);
121
195
  }
122
196
  const vadMode = args.vad ? "on" : args["no-vad"] ? "off" : "auto";
@@ -129,7 +203,7 @@ export const mainCommand = defineCommand({
129
203
  let hasError = false;
130
204
  const results: TranscribeResult[] = [];
131
205
 
132
- const wantsLangId = !!(args.lang || args.verbose || args.json || args.toon || args.format === "transcript" || args.format === "json");
206
+ const wantsLangId = !!(args.lang || args.verbose || wantsJson || wantsToon || wantsTranscript);
133
207
 
134
208
  for (const file of files) {
135
209
  const startedAt = performance.now();
@@ -185,11 +259,11 @@ export const mainCommand = defineCommand({
185
259
  }
186
260
  }
187
261
 
188
- if (args.json || args.format === "json") {
262
+ if (wantsJson) {
189
263
  process.stdout.write(formatJsonOutput(results));
190
- } else if (args.toon) {
264
+ } else if (wantsToon) {
191
265
  process.stdout.write(formatToonOutput(results));
192
- } else if (args.format === "transcript") {
266
+ } else if (wantsTranscript) {
193
267
  process.stdout.write(formatTranscriptOutput(results));
194
268
  } else if (args.verbose) {
195
269
  process.stdout.write(formatVerboseOutput(results));
package/src/cli.ts CHANGED
@@ -7,7 +7,13 @@ export { installCommand } from "./cli/install";
7
7
  export { sayCommand } from "./cli/say";
8
8
  export { pickVoiceForLang } from "./voice-routing";
9
9
  export { statusCommand } from "./cli/status";
10
- export { mainCommand, detectLanguage, checkLanguageMismatch } from "./cli/main";
10
+ export {
11
+ mainCommand,
12
+ detectLanguage,
13
+ checkLanguageMismatch,
14
+ resolveOutputFormat,
15
+ } from "./cli/main";
16
+ export type { ResolvedOutputFormat } from "./cli/main";
11
17
  export { runCli } from "./cli/dispatch";
12
18
 
13
19
  export type { TranscribeResult } from "./types";
@@ -74,6 +74,20 @@ const SIDECARS: SidecarSpec[] = [
74
74
  availableHint: "--speakers available",
75
75
  unavailableHint: "--speakers unavailable",
76
76
  },
77
+ {
78
+ // Runtime resolver looks for plain `kesha-textlang` next to the engine
79
+ // (see `rust/src/text_lang.rs::helper_path`), not the platform-suffixed
80
+ // release-asset name. Mismatch is intentional: the asset name needs the
81
+ // suffix for GitHub-release uniqueness; the sidecar lookup wants the
82
+ // unsuffixed binary so the same Rust code path works on the build-time
83
+ // OUT_DIR baked fallback.
84
+ fileBasename: "kesha-textlang",
85
+ assetName: "kesha-textlang-darwin-arm64",
86
+ displayName: "Text-lang sidecar",
87
+ availableHint: "detect-text-lang fast path",
88
+ unavailableHint:
89
+ "detect-text-lang will fail until next `kesha install` (no swift -e fallback)",
90
+ },
77
91
  ];
78
92
 
79
93
  /**