@drakulavich/kesha-voice-kit 1.14.0 → 1.15.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 +18 -34
- package/package.json +2 -2
- package/src/cli/main.ts +85 -11
- package/src/cli.ts +7 -1
- package/src/engine-install.ts +102 -1
- package/src/log.ts +16 -1
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://
|
|
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>
|
|
@@ -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
|
-
|
|
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
|
|
48
|
+
## Nix Install
|
|
36
49
|
|
|
37
|
-
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "1.15.1",
|
|
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.
|
|
33
|
+
"version": "1.15.0"
|
|
34
34
|
},
|
|
35
35
|
"scripts": {
|
|
36
36
|
"test": "bun test",
|
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 (
|
|
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
|
-
|
|
106
|
-
|
|
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 && !(
|
|
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 && !(
|
|
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 ||
|
|
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 (
|
|
262
|
+
if (wantsJson) {
|
|
189
263
|
process.stdout.write(formatJsonOutput(results));
|
|
190
|
-
} else if (
|
|
264
|
+
} else if (wantsToon) {
|
|
191
265
|
process.stdout.write(formatToonOutput(results));
|
|
192
|
-
} else if (
|
|
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 {
|
|
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";
|
package/src/engine-install.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
};
|