@drakulavich/kesha-voice-kit 1.14.0 → 1.15.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
@@ -22,9 +22,22 @@
22
22
 
23
23
  Runtime: **[Bun](https://bun.sh)** >= 1.3.0.
24
24
 
25
+ Install Bun (skip if already installed) — pick one:
26
+
25
27
  ```bash
26
- curl -fsSL https://bun.sh/install | bash # skip if Bun is already installed
28
+ # Linux & macOS
29
+ curl -fsSL https://bun.sh/install | bash # upstream installer
30
+ brew install oven-sh/bun/bun # Homebrew
31
+ ```
32
+
33
+ ```powershell
34
+ # Windows
35
+ powershell -c "irm bun.sh/install.ps1 | iex"
36
+ ```
37
+
38
+ Then install Kesha:
27
39
 
40
+ ```bash
28
41
  bun add -g @drakulavich/kesha-voice-kit
29
42
  kesha install # downloads engine + models
30
43
  kesha audio.ogg # transcript to stdout
@@ -32,45 +45,16 @@ kesha audio.ogg # transcript to stdout
32
45
 
33
46
  Air-gapped or behind a corporate mirror? See [docs/model-mirror.md](docs/model-mirror.md).
34
47
 
35
- ## Nix Install (Recommended)
48
+ ## Nix Install
36
49
 
37
- **Prerequisites:** [Nix](https://nixos.org/download/) with flakes enabled. Supported systems: `aarch64-darwin`, `x86_64-linux`.
50
+ Alternative reproducible-build path on `aarch64-darwin` / `x86_64-linux`:
38
51
 
39
- ### One-liner run (no install)
40
52
  ```bash
41
53
  nix run github:drakulavich/kesha-voice-kit -- install # downloads models (engine is bundled)
42
54
  nix run github:drakulavich/kesha-voice-kit -- audio.ogg # transcribe
43
55
  ```
44
56
 
45
- `nix run` resolves to `apps.default` (the `kesha` Bun CLI), which has the engine binary baked in via `KESHA_ENGINE_BIN`, so there's no separate engine download.
46
-
47
- ### Install to profile (persistent)
48
- ```bash
49
- nix profile install github:drakulavich/kesha-voice-kit
50
- kesha install # downloads models
51
- kesha audio.ogg # transcript to stdout
52
- ```
53
-
54
- `packages.default` ships the Bun CLI (`kesha`, `parakeet`) wired to the Nix-built engine. After `nix profile install`, both shims are on `PATH` and run transcription, language detection, and TTS (including `macos-*` AVSpeech voices on darwin-arm64) identically to the npm install. Speaker diarization (`kesha install --diarize` / `--speakers`) is not yet wired into the Nix build — the `kesha-diarize` Swift sidecar needs network-fetched FluidAudio at build time, which the Nix sandbox forbids; use the Bun install path (`bun add -g @drakulavich/kesha-voice-kit`) on darwin-arm64 if you need that feature (tracked alongside [#199](https://github.com/drakulavich/kesha-voice-kit/issues/199)).
55
-
56
- ### Engine only (no Bun, no Node)
57
- For users who just want the Rust binary:
58
- ```bash
59
- nix build github:drakulavich/kesha-voice-kit#kesha-engine
60
- ./result/bin/kesha-engine --help
61
- ./result/bin/kesha-engine --capabilities-json # see which backends compiled in
62
- ```
63
-
64
- ### Development shell
65
- ```bash
66
- nix develop github:drakulavich/kesha-voice-kit
67
- # Now you have: pinned rustc/cargo (via rust-overlay), bun, protoc, cmake, pkg-config, libclang
68
- ```
69
-
70
- **Why Nix?**
71
- - ✅ Reproducible builds across Linux/macOS
72
- - ✅ All native deps (onnxruntime, protobuf, abseil) handled automatically
73
- - ✅ No "works on my machine" — same `flake.nix` = identical results everywhere
57
+ Full recipes (one-liner, profile install, engine-only, dev shell) live in [docs/nix-install.md](docs/nix-install.md).
74
58
 
75
59
  ## Speech-to-text
76
60
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drakulavich/kesha-voice-kit",
3
- "version": "1.14.0",
3
+ "version": "1.15.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.14.0"
33
+ "version": "1.15.0"
34
34
  },
35
35
  "scripts": {
36
36
  "test": "bun test",
@@ -76,6 +76,85 @@ const SIDECARS: SidecarSpec[] = [
76
76
  },
77
77
  ];
78
78
 
79
+ /**
80
+ * Make a freshly-downloaded Mach-O runnable on macOS 15+ Sequoia.
81
+ *
82
+ * The release binaries ship `Signature=adhoc` (built via `cargo build`,
83
+ * no Apple Developer ID). When fetched via HTTPS into `~/.cache/...`,
84
+ * macOS attaches a `com.apple.provenance` xattr identifying the download
85
+ * source. Combined with stricter Gatekeeper policy on macOS 15+ Sequoia,
86
+ * the resulting "untrusted downloaded ad-hoc binary" is killed with
87
+ * SIGKILL on first invocation (exit 137), with no log line — Gatekeeper
88
+ * denies before Rust's main runs.
89
+ *
90
+ * Two independent fixes run in sequence; both are best-effort:
91
+ *
92
+ * 1. `codesign --force --sign - <path>` — re-applies the ad-hoc
93
+ * signature with the user's own host identity, defeating the trust
94
+ * mismatch.
95
+ * 2. `xattr -d com.apple.provenance <path>` — strips the provenance
96
+ * marker entirely. Cheaper than codesign and works even when
97
+ * `codesign` is absent from PATH (corporate-locked machine, minimal
98
+ * CI image).
99
+ *
100
+ * Either step alone has unblocked the SIGKILL in field reports, so we
101
+ * run both. If both fail, surface the manual recovery commands in
102
+ * stderr. Linux/Windows paths never reach this function.
103
+ */
104
+ function darwinTrustBinary(path: string, displayName: string): void {
105
+ if (process.platform !== "darwin") return;
106
+ let codesignOk = false;
107
+ let xattrOk = false;
108
+ try {
109
+ const proc = Bun.spawnSync(["codesign", "--force", "--sign", "-", path], {
110
+ stdout: "pipe",
111
+ stderr: "pipe",
112
+ });
113
+ codesignOk = proc.exitCode === 0;
114
+ if (!codesignOk) {
115
+ const stderr = new TextDecoder().decode(proc.stderr).trim();
116
+ log.debug(`codesign on ${displayName} exited ${proc.exitCode}: ${stderr}`);
117
+ }
118
+ } catch (e) {
119
+ log.debug(
120
+ `codesign on ${displayName} threw: ${e instanceof Error ? e.message : e}`,
121
+ );
122
+ }
123
+ try {
124
+ // `xattr -d` returns exit 1 with "No such xattr" if the attribute is
125
+ // already absent (e.g. the file was placed via `bun link` instead of
126
+ // downloaded). That's a success outcome for our purposes — the
127
+ // attribute we wanted gone is already gone — so treat exit 1 as ok.
128
+ const proc = Bun.spawnSync(
129
+ ["xattr", "-d", "com.apple.provenance", path],
130
+ { stdout: "pipe", stderr: "pipe" },
131
+ );
132
+ const stderr = new TextDecoder().decode(proc.stderr).trim();
133
+ xattrOk =
134
+ proc.exitCode === 0 ||
135
+ (proc.exitCode === 1 && /No such xattr/i.test(stderr));
136
+ if (!xattrOk) {
137
+ log.debug(`xattr -d on ${displayName} exited ${proc.exitCode}: ${stderr}`);
138
+ }
139
+ } catch (e) {
140
+ log.debug(
141
+ `xattr -d on ${displayName} threw: ${e instanceof Error ? e.message : e}`,
142
+ );
143
+ }
144
+ if (!codesignOk && !xattrOk) {
145
+ // Single-quote the path in the manual-fix hint so spaces / shell
146
+ // metachars in `~/.cache/.../bin/...` don't break paste-into-shell.
147
+ // POSIX single-quote escape: close the quote, insert escaped `'`,
148
+ // re-open. Cheap and correct on bash/zsh.
149
+ const q = (p: string) => `'${p.replace(/'/g, `'\\''`)}'`;
150
+ log.warn(
151
+ `Could not unblock ${displayName} for macOS Gatekeeper (both codesign ` +
152
+ `and xattr failed); if the binary refuses to run, manually run: ` +
153
+ `codesign --force --sign - ${q(path)} && xattr -d com.apple.provenance ${q(path)}`,
154
+ );
155
+ }
156
+ }
157
+
79
158
  /**
80
159
  * Fetch a single Swift sidecar and place it next to the engine binary on
81
160
  * darwin-arm64. Best-effort: 404s (older engine versions predate this
@@ -119,6 +198,7 @@ async function downloadSidecar(
119
198
  try {
120
199
  await streamResponseToFile(res, sidecarPath, spec.displayName);
121
200
  chmodSync(sidecarPath, 0o755);
201
+ darwinTrustBinary(sidecarPath, spec.displayName);
122
202
  log.success(`${spec.displayName} installed (${spec.availableHint}).`);
123
203
  } catch (e) {
124
204
  log.warn(
@@ -187,6 +267,18 @@ export async function downloadEngine(
187
267
  } else {
188
268
  log.success(`Engine binary already installed (v${engineVersion}).`);
189
269
  }
270
+ // Re-run the macOS trust step against the cached binary too. Reason:
271
+ // a user who upgraded to macOS 15+ Sequoia AFTER installing kesha
272
+ // would have a v<X> binary on disk with `com.apple.provenance` still
273
+ // attached, and `kesha install` would normally bail at "already
274
+ // installed" without healing the SIGKILL. Idempotent — re-signing
275
+ // an already-correctly-signed binary is a ~10 ms no-op; `xattr -d`
276
+ // on an absent attr is handled (exit 1 + "No such xattr" treated
277
+ // as success in `darwinTrustBinary`). Skipped on read-only
278
+ // filesystems (Nix-store installs) — no write access anyway.
279
+ if (canWriteEngineDir && existsSync(binPath)) {
280
+ darwinTrustBinary(binPath, "kesha-engine binary");
281
+ }
190
282
  // Top up any sidecars missing from this cached install. Pre-#141 / pre-#199
191
283
  // engines never shipped them, so a cache-valid binary may still need
192
284
  // fetching. Run independent fetches concurrently — same shape as the
@@ -204,6 +296,14 @@ export async function downloadEngine(
204
296
  (s) => !existsSync(join(engineDir, s.fileBasename)),
205
297
  );
206
298
  await Promise.all(missing.map((s) => downloadSidecar(s, binPath, engineVersion)));
299
+ // The cache-valid branch also re-trusts any sidecars that already
300
+ // exist alongside the engine — same upgrade-to-Sequoia scenario as
301
+ // above. The `missing.map` above only handles NEW sidecars; this
302
+ // loop handles ALREADY-PRESENT ones.
303
+ for (const s of SIDECARS) {
304
+ const p = join(engineDir, s.fileBasename);
305
+ if (existsSync(p)) darwinTrustBinary(p, s.displayName);
306
+ }
207
307
  }
208
308
  } else {
209
309
  // Log why we're downloading — helps diagnose surprising re-downloads.
@@ -252,6 +352,7 @@ export async function downloadEngine(
252
352
 
253
353
  await streamResponseToFile(res, binPath, "kesha-engine binary");
254
354
  chmodSync(binPath, 0o755);
355
+ darwinTrustBinary(binPath, "kesha-engine binary");
255
356
  writeInstalledEngineVersion(binPath, engineVersion);
256
357
  log.success(`Engine binary downloaded (v${engineVersion}).`);
257
358
  await Promise.all(sidecarPromises);
@@ -282,7 +383,7 @@ export async function downloadEngine(
282
383
  throw new Error(
283
384
  "--diarize is not supported by the installed engine: it was built " +
284
385
  "without the 'system_diarize' feature (the Nix build is one such " +
285
- "case — see README's Nix Install section).\n" +
386
+ "case — see docs/nix-install.md).\n" +
286
387
  " Fix: install via the npm release with `bun add -g @drakulavich/kesha-voice-kit`, " +
287
388
  "which ships the diarize-enabled engine on darwin-arm64.",
288
389
  );
package/src/log.ts CHANGED
@@ -19,6 +19,13 @@ function envDebug(): boolean {
19
19
  return !KESHA_DEBUG_OFF_VALUES.has(v.trim().toLowerCase());
20
20
  }
21
21
 
22
+ /**
23
+ * Module-load timestamp for relative-since-start prefixes on debug lines.
24
+ * The CLI runs nothing of substance before this file is imported, so this
25
+ * is effectively process-start. Recorded once.
26
+ */
27
+ const PROCESS_T0_MS = performance.now();
28
+
22
29
  export const log = {
23
30
  info: (msg: string) => console.log(msg),
24
31
  success: (msg: string) => console.log(pc.green(msg)),
@@ -29,7 +36,15 @@ export const log = {
29
36
  debugEnabled: false,
30
37
  debug(msg: string): void {
31
38
  if (this.debugEnabled || envDebug()) {
32
- console.error(pc.dim(`[debug] ${msg}`));
39
+ // `[debug +Nms]` prefix sits on the CLI process's own timeline so
40
+ // the reader can see when each line fired. The Rust engine emits
41
+ // the same `+Nms` shape from `rust/src/debug.rs::trace_fmt`, but
42
+ // anchored to its own process start — the two axes are
43
+ // independent. For "duration between two events on the same
44
+ // process", read the prefix difference; for cross-process spans,
45
+ // the spawn→exit `dt=Nms` inside the message remains authoritative.
46
+ const t = Math.round(performance.now() - PROCESS_T0_MS);
47
+ console.error(pc.dim(`[debug +${t}ms] ${msg}`));
33
48
  }
34
49
  },
35
50
  };