@drakulavich/kesha-voice-kit 2.0.0 → 2.1.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/package.json +8 -8
- package/src/cli/init.ts +10 -11
- package/src/cli/install.ts +1 -1
- package/src/cli/support-bundle.ts +2 -1
- package/src/engine-install.ts +135 -63
- package/src/path-errnos.ts +11 -0
- package/src/progress.ts +30 -13
- package/src/support-bundle.ts +72 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@drakulavich/kesha-voice-kit",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Give your tools a voice — speech to text and back, 25 languages, up to ~19× faster than Whisper. On your machine.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -109,18 +109,18 @@
|
|
|
109
109
|
},
|
|
110
110
|
"kesha": {
|
|
111
111
|
"engine": {
|
|
112
|
-
"version": "2.
|
|
112
|
+
"version": "2.1.0",
|
|
113
113
|
"sha256": {
|
|
114
|
-
"kesha-engine-darwin-arm64": "
|
|
115
|
-
"kesha-engine-linux-x64": "
|
|
116
|
-
"kesha-engine-windows-x64.exe": "
|
|
114
|
+
"kesha-engine-darwin-arm64": "51446285f272a3d5060aa2dd21d2d16a5c31682a8554f29c944fe1af27677008",
|
|
115
|
+
"kesha-engine-linux-x64": "8728b467ca31640739dcbe18e53eadd27e795033075f6442fc14b729e0014a62",
|
|
116
|
+
"kesha-engine-windows-x64.exe": "82515821a6b424ff9c75462721f7723891705a2f1edcba0a1eb5c1704cfadbec",
|
|
117
117
|
"say-avspeech-darwin-arm64": "a3aa5b75ddff68a48f1158e838310b37cac72ef86841d9cfcdf1ee233cb7451a",
|
|
118
118
|
"kesha-textlang-darwin-arm64": "e45e2822248b14dd72aa79e2def8fa6b9c07c97423f11c841508769225d72da4"
|
|
119
119
|
},
|
|
120
120
|
"size": {
|
|
121
|
-
"kesha-engine-darwin-arm64":
|
|
122
|
-
"kesha-engine-linux-x64":
|
|
123
|
-
"kesha-engine-windows-x64.exe":
|
|
121
|
+
"kesha-engine-darwin-arm64": 73639664,
|
|
122
|
+
"kesha-engine-linux-x64": 67394344,
|
|
123
|
+
"kesha-engine-windows-x64.exe": 66586112,
|
|
124
124
|
"say-avspeech-darwin-arm64": 64208,
|
|
125
125
|
"kesha-textlang-darwin-arm64": 57648
|
|
126
126
|
}
|
package/src/cli/init.ts
CHANGED
|
@@ -176,14 +176,7 @@ export async function promptInitSelection(
|
|
|
176
176
|
};
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
-
|
|
180
|
-
async function printPlan(selection: InitSelection): Promise<boolean> {
|
|
181
|
-
const backendError = unavailableBackendRefusal(selection.backend);
|
|
182
|
-
if (backendError) {
|
|
183
|
-
log.error(errorMessage(backendError));
|
|
184
|
-
process.exitCode = exitCodeFor(backendError);
|
|
185
|
-
return false;
|
|
186
|
-
}
|
|
179
|
+
async function printPlan(selection: InitSelection): Promise<void> {
|
|
187
180
|
log.info(
|
|
188
181
|
await renderInstallPlan({
|
|
189
182
|
noCache: selection.noCache,
|
|
@@ -193,7 +186,6 @@ async function printPlan(selection: InitSelection): Promise<boolean> {
|
|
|
193
186
|
diarize: selection.diarize,
|
|
194
187
|
}),
|
|
195
188
|
);
|
|
196
|
-
return true;
|
|
197
189
|
}
|
|
198
190
|
|
|
199
191
|
async function runNonInteractive(selection: InitSelection): Promise<void> {
|
|
@@ -203,7 +195,7 @@ async function runNonInteractive(selection: InitSelection): Promise<void> {
|
|
|
203
195
|
log.warn("--diarize is currently darwin-arm64 only; omitting it from non-interactive examples.");
|
|
204
196
|
}
|
|
205
197
|
log.info(renderInitOverview(canDiarize));
|
|
206
|
-
|
|
198
|
+
await printPlan(printableSelection);
|
|
207
199
|
log.info("Run one of these commands from an interactive terminal:");
|
|
208
200
|
for (const command of initSuggestionCommands(printableSelection, canDiarize)) {
|
|
209
201
|
log.info(` ${command.join(" ")}`);
|
|
@@ -262,6 +254,13 @@ export const initCommand = defineCommand({
|
|
|
262
254
|
const noCache = resolveNoCacheFlag(args, rawArgs);
|
|
263
255
|
const selection = resolveInitSelection(args, backend, noCache);
|
|
264
256
|
|
|
257
|
+
const backendError = args.yes && !args.plan ? null : unavailableBackendRefusal(backend);
|
|
258
|
+
if (backendError) {
|
|
259
|
+
log.error(errorMessage(backendError));
|
|
260
|
+
process.exitCode = exitCodeFor(backendError);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
265
264
|
if (args.plan) {
|
|
266
265
|
log.info(renderInitOverview());
|
|
267
266
|
await printPlan(selection);
|
|
@@ -293,7 +292,7 @@ export const initCommand = defineCommand({
|
|
|
293
292
|
noCache,
|
|
294
293
|
);
|
|
295
294
|
log.info("");
|
|
296
|
-
|
|
295
|
+
await printPlan(prompted);
|
|
297
296
|
const confirmed = await promptConfirm(
|
|
298
297
|
`Run \`${initInstallArgs(prompted).join(" ")}\` now?`,
|
|
299
298
|
true,
|
package/src/cli/install.ts
CHANGED
|
@@ -246,7 +246,7 @@ export async function performInstall(options: PerformInstallOptions) {
|
|
|
246
246
|
throw backendError;
|
|
247
247
|
}
|
|
248
248
|
await installEngine({ noCache, backend, ttsLangs, vad, diarize, version: engineVersion });
|
|
249
|
-
await maybeAskForStar(getEngineBinPath(), packageVersion, log);
|
|
249
|
+
if (!log.quietEnabled) await maybeAskForStar(getEngineBinPath(), packageVersion, { info: log.progress, warn: log.warn });
|
|
250
250
|
finishInstallDiagnostic(diagnosticLog, startedAt, "success");
|
|
251
251
|
} catch (err: unknown) {
|
|
252
252
|
const signalExitCode = getPendingSignalExitCode();
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { defineCommand } from "citty";
|
|
2
2
|
import { errorMessage } from "../error-utils";
|
|
3
|
+
import { exitCodeFor, KeshaError } from "../engine/events";
|
|
3
4
|
import { createSupportBundle } from "../support-bundle";
|
|
4
5
|
import { log } from "../log";
|
|
5
6
|
|
|
@@ -35,7 +36,7 @@ export const supportBundleCommand = defineCommand({
|
|
|
35
36
|
log.status(`Size: ${bundle.sizeBytes} bytes`);
|
|
36
37
|
} catch (err) {
|
|
37
38
|
log.error(errorMessage(err));
|
|
38
|
-
process.exit(1);
|
|
39
|
+
process.exit(err instanceof KeshaError ? exitCodeFor(err) : 1);
|
|
39
40
|
}
|
|
40
41
|
},
|
|
41
42
|
});
|
package/src/engine-install.ts
CHANGED
|
@@ -19,6 +19,7 @@ import { runEngineProcess } from "./engine/spawn";
|
|
|
19
19
|
import { acquireInstallLock } from "./install-lock";
|
|
20
20
|
import { log } from "./log";
|
|
21
21
|
import { engineVersion, injectedEnginePins } from "./package-info";
|
|
22
|
+
import { PATH_ERRNOS } from "./path-errnos";
|
|
22
23
|
import { keshaCacheDir } from "./paths";
|
|
23
24
|
import { createLiveStatus, streamResponseToFile } from "./progress";
|
|
24
25
|
import { interruptedRun } from "./process-tree";
|
|
@@ -37,6 +38,8 @@ export {
|
|
|
37
38
|
} from "./engine-version-marker";
|
|
38
39
|
|
|
39
40
|
const GITHUB_REPO = "drakulavich/kesha-voice-kit";
|
|
41
|
+
const NETWORK_HINT = "check your network connection and try again.";
|
|
42
|
+
const MISMATCH_HINT = `re-run \`kesha install\`; if the mismatch persists, report it at https://github.com/${GITHUB_REPO}/issues.`;
|
|
40
43
|
|
|
41
44
|
interface ExpectedSha256 {
|
|
42
45
|
sha256: string;
|
|
@@ -51,30 +54,46 @@ export interface AssetPins {
|
|
|
51
54
|
}
|
|
52
55
|
|
|
53
56
|
/** Without `--engine-version` nothing but the maintainer can make an unverifiable pinned release verifiable. */
|
|
54
|
-
function unverifiableFix(version: string): string {
|
|
57
|
+
function unverifiableFix(version: string, pins: AssetPins | undefined): string {
|
|
58
|
+
if (!pins) {
|
|
59
|
+
return version === engineVersion
|
|
60
|
+
? `this source checkout installs its own version v${engineVersion}, which may not be released yet: run ` +
|
|
61
|
+
`\`kesha install --engine-version <version>\` with a published release that ships SHA256SUMS (https://github.com/${GITHUB_REPO}/releases).`
|
|
62
|
+
: `pass --engine-version a published release that ships SHA256SUMS (https://github.com/${GITHUB_REPO}/releases).`;
|
|
63
|
+
}
|
|
55
64
|
return version === engineVersion
|
|
56
65
|
? `report it at https://github.com/${GITHUB_REPO}/issues: the pinned engine release v${version} must publish its checksums.`
|
|
57
66
|
: `pick a release that ships SHA256SUMS (https://github.com/${GITHUB_REPO}/releases), or run ` +
|
|
58
67
|
`\`kesha install\` without --engine-version to install the pinned v${engineVersion}.`;
|
|
59
68
|
}
|
|
60
69
|
|
|
61
|
-
async function fetchSha256Sums(version: string): Promise<Map<string, string>> {
|
|
70
|
+
async function fetchSha256Sums(version: string, pins: AssetPins | undefined): Promise<Map<string, string>> {
|
|
62
71
|
const url = `https://github.com/${GITHUB_REPO}/releases/download/v${version}/SHA256SUMS`;
|
|
63
72
|
let res: Response;
|
|
64
73
|
try {
|
|
65
74
|
res = await fetch(url, { redirect: "follow" });
|
|
66
75
|
} catch (e) {
|
|
67
|
-
throw new
|
|
68
|
-
|
|
69
|
-
);
|
|
76
|
+
throw new KeshaError("E_MODEL_DOWNLOAD", `Failed to fetch SHA256SUMS for release v${version}: ${errorMessage(e)}`, {
|
|
77
|
+
hint: NETWORK_HINT,
|
|
78
|
+
});
|
|
70
79
|
}
|
|
71
80
|
if (!res.ok) {
|
|
72
|
-
throw new
|
|
81
|
+
throw new KeshaError(
|
|
82
|
+
"E_MODEL_DOWNLOAD",
|
|
73
83
|
`Cannot verify engine v${version}: release v${version} publishes no SHA256SUMS (HTTP ${res.status}), ` +
|
|
74
|
-
|
|
84
|
+
"so its binaries cannot be checked before they are installed.",
|
|
85
|
+
{ hint: unverifiableFix(version, pins) },
|
|
75
86
|
);
|
|
76
87
|
}
|
|
77
|
-
|
|
88
|
+
let body: string;
|
|
89
|
+
try {
|
|
90
|
+
body = await res.text();
|
|
91
|
+
} catch (e) {
|
|
92
|
+
throw new KeshaError("E_MODEL_DOWNLOAD", `Failed to read SHA256SUMS for release v${version}: ${errorMessage(e)}`, {
|
|
93
|
+
hint: NETWORK_HINT,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
return parseSha256Sums(body);
|
|
78
97
|
}
|
|
79
98
|
|
|
80
99
|
/**
|
|
@@ -92,12 +111,14 @@ export function releaseChecksums(
|
|
|
92
111
|
if (!pinned) throw new Error(`No pinned SHA-256 for ${assetName} of engine v${version}; this CLI build is incomplete.\n Fix: report it at https://github.com/${GITHUB_REPO}/issues.`);
|
|
93
112
|
return { sha256: pinned, source: "its pinned SHA-256" };
|
|
94
113
|
}
|
|
95
|
-
sums ??= fetchSha256Sums(version);
|
|
114
|
+
sums ??= fetchSha256Sums(version, pins);
|
|
96
115
|
const sha256 = (await sums).get(assetName);
|
|
97
116
|
if (!sha256) {
|
|
98
|
-
throw new
|
|
117
|
+
throw new KeshaError(
|
|
118
|
+
"E_MODEL_DOWNLOAD",
|
|
99
119
|
`Cannot verify ${assetName}: the SHA256SUMS of release v${version} does not list it, ` +
|
|
100
|
-
|
|
120
|
+
"so it cannot be checked before it is installed.",
|
|
121
|
+
{ hint: unverifiableFix(version, pins) },
|
|
101
122
|
);
|
|
102
123
|
}
|
|
103
124
|
return { sha256, source: `the SHA-256 in the SHA256SUMS of release v${version}` };
|
|
@@ -129,25 +150,44 @@ async function rejectMismatchedDownload(
|
|
|
129
150
|
return (
|
|
130
151
|
`${what} from release v${version} does not match ${expected.source}: ` +
|
|
131
152
|
`expected sha256 ${expected.sha256}, got ${actual}. The download was corrupted or altered, ` +
|
|
132
|
-
"so it was deleted instead of installed
|
|
133
|
-
` Fix: re-run \`kesha install\`; if the mismatch persists, report it at https://github.com/${GITHUB_REPO}/issues.`
|
|
153
|
+
"so it was deleted instead of installed."
|
|
134
154
|
);
|
|
135
155
|
}
|
|
136
156
|
|
|
137
157
|
/**
|
|
138
158
|
* Whether a cached asset of the pinned release still hashes to its pin; one that does not is
|
|
139
|
-
* deleted so the verified download replaces it
|
|
159
|
+
* deleted so the verified download replaces it, or refused when its directory is read-only.
|
|
160
|
+
* `--engine-version` caches are not re-hashed.
|
|
140
161
|
*/
|
|
141
|
-
async function cachedAssetMatchesPin(
|
|
162
|
+
async function cachedAssetMatchesPin(
|
|
163
|
+
path: string,
|
|
164
|
+
assetName: string,
|
|
165
|
+
what: string,
|
|
166
|
+
version: string,
|
|
167
|
+
canRepair: boolean,
|
|
168
|
+
): Promise<boolean> {
|
|
142
169
|
// KESHA_ENGINE_BIN names the user's own build, which no release pin describes.
|
|
143
170
|
if (version !== engineChecksums.pins?.version || process.env.KESHA_ENGINE_BIN || !existsSync(path)) return true;
|
|
144
171
|
const expected = await engineChecksums.forRelease(version)(assetName);
|
|
145
|
-
|
|
172
|
+
let actual: string;
|
|
173
|
+
try {
|
|
174
|
+
actual = await sha256OfFile(path);
|
|
175
|
+
} catch (e) {
|
|
176
|
+
throw canRepair ? e : unreadableInReadOnlyDir(path, e);
|
|
177
|
+
}
|
|
146
178
|
if (actual === expected.sha256) return true;
|
|
147
|
-
|
|
148
|
-
`Installed ${what} ${assetName} does not match ${expected.source}
|
|
149
|
-
|
|
150
|
-
)
|
|
179
|
+
const mismatch =
|
|
180
|
+
`Installed ${what} ${assetName} at ${path} does not match ${expected.source} ` +
|
|
181
|
+
`(expected sha256 ${expected.sha256}, got ${actual})`;
|
|
182
|
+
if (!canRepair) {
|
|
183
|
+
const dir = dirname(path);
|
|
184
|
+
throw new KeshaError("E_CACHE_CORRUPT", `${mismatch}; ${dir} is read-only, so it cannot be replaced.`, {
|
|
185
|
+
hint:
|
|
186
|
+
`make ${dir} writable and re-run \`kesha install\` to replace it with a verified copy, ` +
|
|
187
|
+
"or point KESHA_CACHE_DIR at a writable directory and re-run `kesha install`.",
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
log.warn(`${mismatch}; deleting it and downloading a verified copy.`);
|
|
151
191
|
rmSync(path, { force: true });
|
|
152
192
|
return false;
|
|
153
193
|
}
|
|
@@ -209,7 +249,7 @@ export function cleanupRetiredSidecars(engineDir: string): string[] {
|
|
|
209
249
|
}
|
|
210
250
|
|
|
211
251
|
if (removed.length > 0) {
|
|
212
|
-
log.
|
|
252
|
+
log.progress(`Removed retired sidecars: ${removed.join(", ")}.`);
|
|
213
253
|
}
|
|
214
254
|
|
|
215
255
|
return removed;
|
|
@@ -320,12 +360,12 @@ async function downloadSidecar(
|
|
|
320
360
|
await streamResponseToFile(res, sidecarPath, spec.displayName);
|
|
321
361
|
const refusal = await rejectMismatchedDownload(sidecarPath, `${spec.displayName} ${spec.assetName}`, version, expected);
|
|
322
362
|
if (refusal) {
|
|
323
|
-
log.warn(`${refusal}\n ${spec.displayName} not installed; ${spec.unavailableHint}.`);
|
|
363
|
+
log.warn(`${refusal}\n Fix: ${MISMATCH_HINT}\n ${spec.displayName} not installed; ${spec.unavailableHint}.`);
|
|
324
364
|
return;
|
|
325
365
|
}
|
|
326
366
|
chmodSync(sidecarPath, 0o755);
|
|
327
367
|
darwinTrustBinary(sidecarPath, spec.displayName);
|
|
328
|
-
log.
|
|
368
|
+
log.progress(`${spec.displayName} installed (${spec.availableHint}).`);
|
|
329
369
|
} catch (e) {
|
|
330
370
|
log.warn(
|
|
331
371
|
`${spec.displayName} install failed (${errorMessage(e)}); ${spec.unavailableHint}.`,
|
|
@@ -376,7 +416,7 @@ export async function warmDarwinKokoro(binPath: string, timeoutMs = 180_000): Pr
|
|
|
376
416
|
return;
|
|
377
417
|
}
|
|
378
418
|
|
|
379
|
-
log.
|
|
419
|
+
log.progress(
|
|
380
420
|
`FluidAudio Kokoro warmed (${Math.round(performance.now() - startedAt)}ms).`,
|
|
381
421
|
);
|
|
382
422
|
} catch (e) {
|
|
@@ -446,19 +486,18 @@ async function refreshCachedEngine(
|
|
|
446
486
|
): Promise<void> {
|
|
447
487
|
const engineDir = dirname(binPath);
|
|
448
488
|
if (noCache && !canWriteEngineDir) {
|
|
449
|
-
log.
|
|
489
|
+
log.warn(
|
|
450
490
|
`Engine binary at v${version} is on a read-only filesystem; --no-cache skipped for engine (still forwarded to model installs).`,
|
|
451
491
|
);
|
|
452
492
|
} else {
|
|
453
|
-
log.
|
|
493
|
+
log.progress(`Engine binary already installed (v${version}).`);
|
|
454
494
|
}
|
|
455
495
|
// Re-trust on cache hit: a user who upgraded to Sequoia after install would still have
|
|
456
496
|
// com.apple.provenance attached; idempotent (~10ms no-op if already correct).
|
|
457
497
|
if (canWriteEngineDir && existsSync(binPath)) {
|
|
458
498
|
darwinTrustBinary(binPath, "kesha-engine binary");
|
|
459
499
|
}
|
|
460
|
-
// Top up missing or broken sidecars (pre-#141/#199 cached binaries never had them)
|
|
461
|
-
// skip on read-only fs (Nix-store) to avoid confusing "install failed" warnings.
|
|
500
|
+
// Top up missing or broken sidecars (pre-#141/#199 cached binaries never had them).
|
|
462
501
|
if (canWriteEngineDir) {
|
|
463
502
|
const checksums = engineChecksums.forRelease(version);
|
|
464
503
|
await Promise.all(
|
|
@@ -468,7 +507,7 @@ async function refreshCachedEngine(
|
|
|
468
507
|
// sidecar is SIGKILLed on spawn, and re-downloading it would not lift the block.
|
|
469
508
|
let pinned = false;
|
|
470
509
|
try {
|
|
471
|
-
pinned = existsSync(path) && (await cachedAssetMatchesPin(path, spec.assetName, spec.displayName, version));
|
|
510
|
+
pinned = existsSync(path) && (await cachedAssetMatchesPin(path, spec.assetName, spec.displayName, version, true));
|
|
472
511
|
} catch (e) {
|
|
473
512
|
log.warn(`Could not read ${spec.displayName} at ${path} (${errorMessage(e)}); removing it and downloading it again.`);
|
|
474
513
|
rmSync(path, { force: true });
|
|
@@ -479,6 +518,10 @@ async function refreshCachedEngine(
|
|
|
479
518
|
}
|
|
480
519
|
}),
|
|
481
520
|
);
|
|
521
|
+
} else {
|
|
522
|
+
for (const spec of SIDECARS) {
|
|
523
|
+
await cachedAssetMatchesPin(join(engineDir, spec.fileBasename), spec.assetName, spec.displayName, version, false);
|
|
524
|
+
}
|
|
482
525
|
}
|
|
483
526
|
}
|
|
484
527
|
|
|
@@ -602,32 +645,35 @@ async function fetchEngineBinary(
|
|
|
602
645
|
res = await fetch(url, { redirect: "follow" });
|
|
603
646
|
} catch (e) {
|
|
604
647
|
muteSidecarRejections();
|
|
605
|
-
throw new
|
|
606
|
-
`Failed to fetch engine binary: ${errorMessage(e)}\n Fix: Check your network connection and try again`,
|
|
607
|
-
);
|
|
648
|
+
throw new KeshaError("E_MODEL_DOWNLOAD", `Failed to fetch engine binary: ${errorMessage(e)}`, { hint: NETWORK_HINT });
|
|
608
649
|
}
|
|
609
650
|
|
|
610
651
|
if (!res.ok) {
|
|
611
652
|
muteSidecarRejections();
|
|
612
653
|
// Names the tag: nothing falls back to the pin, so the caller must see which release 404ed.
|
|
613
|
-
throw new
|
|
614
|
-
|
|
615
|
-
);
|
|
654
|
+
throw new KeshaError("E_MODEL_DOWNLOAD", `Failed to download engine binary from release v${version} (HTTP ${res.status})`, {
|
|
655
|
+
hint: `check https://github.com/${GITHUB_REPO}/releases for available versions.`,
|
|
656
|
+
});
|
|
616
657
|
}
|
|
617
658
|
|
|
618
|
-
|
|
659
|
+
try {
|
|
660
|
+
await streamResponseToFile(res, binPath, "kesha-engine binary");
|
|
661
|
+
} catch (e) {
|
|
662
|
+
muteSidecarRejections();
|
|
663
|
+
throw e instanceof KeshaError ? e : (engineWriteFailure(binPath, e) ?? e);
|
|
664
|
+
}
|
|
619
665
|
const refusal = await rejectMismatchedDownload(binPath, `kesha-engine binary ${binaryName}`, version, expected);
|
|
620
666
|
if (refusal) {
|
|
621
667
|
muteSidecarRejections();
|
|
622
668
|
rmSync(getVersionMarkerPath(binPath), { force: true });
|
|
623
|
-
throw new
|
|
669
|
+
throw new KeshaError("E_CACHE_CORRUPT", refusal, { hint: MISMATCH_HINT });
|
|
624
670
|
}
|
|
625
671
|
chmodSync(binPath, 0o755);
|
|
626
672
|
darwinTrustBinary(binPath, "kesha-engine binary");
|
|
627
673
|
// Marker last: writing it first sends the retry down the cacheValid branch, which never waits.
|
|
628
674
|
await waitUntilSpawnable(binPath);
|
|
629
675
|
writeInstalledEngineVersion(binPath, version);
|
|
630
|
-
log.
|
|
676
|
+
log.progress(`Engine binary downloaded (v${version}).`);
|
|
631
677
|
await Promise.all(sidecarPromises);
|
|
632
678
|
}
|
|
633
679
|
|
|
@@ -807,17 +853,28 @@ async function assertRequestedVersionLanded(binPath: string, version: string): P
|
|
|
807
853
|
);
|
|
808
854
|
}
|
|
809
855
|
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
856
|
+
function engineDirSetting(): { name: string; value: string } | null {
|
|
857
|
+
if (process.env.KESHA_ENGINE_BIN) return { name: "KESHA_ENGINE_BIN", value: process.env.KESHA_ENGINE_BIN };
|
|
858
|
+
if (process.env.KESHA_CACHE_DIR) return { name: "KESHA_CACHE_DIR", value: process.env.KESHA_CACHE_DIR };
|
|
859
|
+
if (process.env.KESHA_HOME) return { name: "KESHA_HOME", value: process.env.KESHA_HOME };
|
|
860
|
+
return null;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
function unreadableInReadOnlyDir(path: string, e: unknown): unknown {
|
|
864
|
+
const errno = (e as NodeJS.ErrnoException).code ?? "";
|
|
865
|
+
const why = PATH_ERRNOS[errno];
|
|
866
|
+
if (!why) return e;
|
|
867
|
+
const dir = dirname(path);
|
|
868
|
+
return new KeshaError(
|
|
869
|
+
"E_INVALID_ARG",
|
|
870
|
+
`Cannot verify ${path} against its pinned SHA-256: ${why} (${errno}), and ${dir} is read-only, so it cannot be replaced.`,
|
|
871
|
+
{
|
|
872
|
+
hint:
|
|
873
|
+
`make ${path} readable and ${dir} writable, then re-run \`kesha install\`. Or ` +
|
|
874
|
+
engineDirFix(engineDirSetting()?.name, false, dir),
|
|
875
|
+
},
|
|
876
|
+
);
|
|
877
|
+
}
|
|
821
878
|
|
|
822
879
|
/** `KESHA_ENGINE_BIN` names the binary file, so "point it at a writable directory" breaks the next install. */
|
|
823
880
|
function engineDirFix(setting: string | undefined, notADir: boolean, engineDir: string): string {
|
|
@@ -847,15 +904,9 @@ function ensureEngineDirCreatable(binPath: string): void {
|
|
|
847
904
|
try {
|
|
848
905
|
mkdirSync(engineDir, { recursive: true });
|
|
849
906
|
} catch (e) {
|
|
850
|
-
const setting =
|
|
851
|
-
? { name: "KESHA_ENGINE_BIN", value: process.env.KESHA_ENGINE_BIN }
|
|
852
|
-
: process.env.KESHA_CACHE_DIR
|
|
853
|
-
? { name: "KESHA_CACHE_DIR", value: process.env.KESHA_CACHE_DIR }
|
|
854
|
-
: process.env.KESHA_HOME
|
|
855
|
-
? { name: "KESHA_HOME", value: process.env.KESHA_HOME }
|
|
856
|
-
: null;
|
|
907
|
+
const setting = engineDirSetting();
|
|
857
908
|
const errno = (e as NodeJS.ErrnoException).code ?? "";
|
|
858
|
-
const why =
|
|
909
|
+
const why = PATH_ERRNOS[errno];
|
|
859
910
|
const what = setting
|
|
860
911
|
? `${setting.name}="${setting.value}" cannot hold the engine directory ${engineDir}: ${why ?? errorMessage(e)}`
|
|
861
912
|
: `cannot create the engine directory ${engineDir}: ${why ?? errorMessage(e)}`;
|
|
@@ -871,6 +922,28 @@ function ensureEngineDirCreatable(binPath: string): void {
|
|
|
871
922
|
}
|
|
872
923
|
}
|
|
873
924
|
|
|
925
|
+
function engineWriteFailure(binPath: string, e: unknown): KeshaError | null {
|
|
926
|
+
const engineDir = dirname(binPath);
|
|
927
|
+
const errno = (e as NodeJS.ErrnoException).code ?? "";
|
|
928
|
+
const setting = engineDirSetting();
|
|
929
|
+
const into = setting ? `${engineDir} (from ${setting.name}="${setting.value}")` : engineDir;
|
|
930
|
+
const why = PATH_ERRNOS[errno];
|
|
931
|
+
if (why) {
|
|
932
|
+
return new KeshaError("E_INVALID_ARG", `Cannot write the engine binary into ${into}: ${why}`, {
|
|
933
|
+
hint: engineDirFix(setting?.name, errno === "ENOTDIR" || errno === "EEXIST", engineDir),
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
if (!errno) return null;
|
|
937
|
+
if (errno === "ENOSPC") {
|
|
938
|
+
return new KeshaError("E_INTERNAL", `Cannot write the engine binary into ${into}: no space left on its disk (ENOSPC)`, {
|
|
939
|
+
hint: `the disk is full: free space on the volume that holds ${engineDir}, then re-run \`kesha install\`.`,
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
return new KeshaError("E_INTERNAL", `Cannot write the engine binary into ${into}: ${errorMessage(e)}`, {
|
|
943
|
+
hint: `resolve that filesystem error on ${engineDir} and re-run \`kesha install\`; if it persists, file a bug with \`kesha support-bundle\`.`,
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
|
|
874
947
|
export interface EngineInstallRequest extends InstallOptions {
|
|
875
948
|
noCache?: boolean;
|
|
876
949
|
backend?: string;
|
|
@@ -913,7 +986,7 @@ async function installLockedEngine(
|
|
|
913
986
|
const engineDir = dirname(binPath);
|
|
914
987
|
|
|
915
988
|
if (version !== engineVersion) {
|
|
916
|
-
log.
|
|
989
|
+
log.warn(
|
|
917
990
|
`Installing engine v${version} instead of the pinned v${engineVersion}; ` +
|
|
918
991
|
"a later `kesha install` without --engine-version restores the pin.",
|
|
919
992
|
);
|
|
@@ -923,15 +996,14 @@ async function installLockedEngine(
|
|
|
923
996
|
const canWriteEngineDir = checkEngineWritable(engineDir);
|
|
924
997
|
|
|
925
998
|
const markerMatches = existsSync(binPath) && installedVersion === version;
|
|
926
|
-
// The marker vouches for a file, not for a working binary.
|
|
927
|
-
// dir: nothing there can be repaired, so a failed probe would only turn a usable Nix
|
|
999
|
+
// The marker vouches for a file, not for a working binary. The probe is skipped on a read-only
|
|
1000
|
+
// engine dir: nothing there can be repaired, so a failed probe would only turn a usable Nix
|
|
928
1001
|
// install into a hard error.
|
|
929
1002
|
// Hashed before the health probe, so an altered binary is never run.
|
|
930
1003
|
const versionMatches =
|
|
931
1004
|
markerMatches &&
|
|
932
|
-
(
|
|
933
|
-
|
|
934
|
-
(await engineWorks(binPath))));
|
|
1005
|
+
(await cachedAssetMatchesPin(binPath, getEngineBinaryName(), "kesha-engine binary", version, canWriteEngineDir)) &&
|
|
1006
|
+
(!canWriteEngineDir || (await engineWorks(binPath)));
|
|
935
1007
|
// On read-only fs, --no-cache can't re-download; treat as cache-valid and forward flag to model install.
|
|
936
1008
|
const cacheValid = versionMatches && (!noCache || !canWriteEngineDir);
|
|
937
1009
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Errnos that mean the path itself is wrong for writing, so the fix is a different path; anything else is not a bad argument. */
|
|
2
|
+
export const PATH_ERRNOS: Record<string, string> = {
|
|
3
|
+
EACCES: "permission denied",
|
|
4
|
+
EPERM: "permission denied",
|
|
5
|
+
ENOTDIR: "a component of it is a file, not a directory",
|
|
6
|
+
// mkdir(recursive) raises this, not ENOTDIR, when the directory itself is an existing file.
|
|
7
|
+
EEXIST: "it already exists as a file, not a directory",
|
|
8
|
+
EROFS: "the filesystem is read-only",
|
|
9
|
+
ELOOP: "the path loops through symlinks",
|
|
10
|
+
ENAMETOOLONG: "the path is too long",
|
|
11
|
+
};
|
package/src/progress.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { copyFileSync, readdirSync, renameSync, rmSync, statSync } from "fs";
|
|
2
2
|
import { basename, dirname, join } from "path";
|
|
3
3
|
import { errorMessage } from "./error-utils";
|
|
4
|
+
import { KeshaError } from "./engine/events";
|
|
4
5
|
import { log } from "./log";
|
|
5
6
|
|
|
6
7
|
const BAR_WIDTH = 20;
|
|
@@ -172,11 +173,15 @@ export async function applyBackpressure(
|
|
|
172
173
|
*/
|
|
173
174
|
const STALE_STAGING_MS = 24 * 60 * 60 * 1000;
|
|
174
175
|
|
|
176
|
+
export function stagingPrefix(destPath: string): string {
|
|
177
|
+
return `${destPath}.part.`;
|
|
178
|
+
}
|
|
179
|
+
|
|
175
180
|
/** A Ctrl-C kills the process before any cleanup runs, so last run's staging file is swept by the next one (#770). */
|
|
176
181
|
function sweepStagingFiles(destPath: string): void {
|
|
177
182
|
if (process.platform === "win32") return;
|
|
178
183
|
const dir = dirname(destPath);
|
|
179
|
-
const prefix =
|
|
184
|
+
const prefix = basename(stagingPrefix(destPath));
|
|
180
185
|
const cutoffMs = Date.now() - STALE_STAGING_MS;
|
|
181
186
|
|
|
182
187
|
let names: string[];
|
|
@@ -225,32 +230,44 @@ export async function streamResponseToFile(
|
|
|
225
230
|
destPath: string,
|
|
226
231
|
label: string,
|
|
227
232
|
): Promise<number> {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
);
|
|
232
|
-
|
|
233
|
+
const emptyResponse = () =>
|
|
234
|
+
new KeshaError("E_MODEL_DOWNLOAD", `Download failed: empty response for ${label}`, {
|
|
235
|
+
hint: "try again; the server may be temporarily unavailable.",
|
|
236
|
+
});
|
|
237
|
+
if (!res.body) throw emptyResponse();
|
|
233
238
|
|
|
234
239
|
const totalBytes = Number(res.headers.get("content-length") || 0);
|
|
235
240
|
const progress = createProgressBar(label, totalBytes);
|
|
236
241
|
|
|
237
242
|
sweepStagingFiles(destPath);
|
|
238
|
-
const stagingPath = `${destPath}
|
|
239
|
-
const
|
|
243
|
+
const stagingPath = `${stagingPrefix(destPath)}${process.pid}.${stagingSeq++}`;
|
|
244
|
+
const reader = res.body.getReader();
|
|
240
245
|
let bytes = 0;
|
|
241
246
|
try {
|
|
247
|
+
const writer = Bun.file(stagingPath).writer();
|
|
242
248
|
try {
|
|
243
|
-
for
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
249
|
+
for (;;) {
|
|
250
|
+
let next: Awaited<ReturnType<typeof reader.read>>;
|
|
251
|
+
try {
|
|
252
|
+
next = await reader.read();
|
|
253
|
+
} catch (err) {
|
|
254
|
+
throw new KeshaError("E_MODEL_DOWNLOAD", `Download of ${label} broke off after ${bytes} bytes: ${errorMessage(err)}`, {
|
|
255
|
+
hint: "check your network connection and try again.",
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
if (next.done) break;
|
|
259
|
+
await applyBackpressure(writer, writer.write(next.value));
|
|
260
|
+
bytes += next.value.length;
|
|
261
|
+
progress.update(next.value.length);
|
|
247
262
|
}
|
|
248
263
|
} finally {
|
|
249
264
|
// Must await: an open write handle makes the freshly-downloaded engine unspawnable — EBUSY on Windows, ETXTBSY on Linux.
|
|
250
265
|
await writer.end();
|
|
251
266
|
}
|
|
267
|
+
if (bytes === 0) throw emptyResponse();
|
|
252
268
|
publishStaging(stagingPath, destPath);
|
|
253
269
|
} catch (err) {
|
|
270
|
+
reader.cancel().catch(() => {});
|
|
254
271
|
rmSync(stagingPath, { force: true });
|
|
255
272
|
throw err;
|
|
256
273
|
}
|
|
@@ -272,7 +289,7 @@ export function createProgressBar(label: string, totalBytes: number): {
|
|
|
272
289
|
return {
|
|
273
290
|
update() {},
|
|
274
291
|
finish() {
|
|
275
|
-
log.
|
|
292
|
+
log.progress(`Downloaded ${label} ✓`);
|
|
276
293
|
},
|
|
277
294
|
};
|
|
278
295
|
}
|
package/src/support-bundle.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { basename, dirname, resolve } from "path";
|
|
1
|
+
import { lstatSync, mkdirSync, statSync, writeFileSync } from "fs";
|
|
2
|
+
import { basename, dirname, join, resolve } from "path";
|
|
3
3
|
import { gzipSync } from "node:zlib";
|
|
4
4
|
import { collectDoctorReport, formatDoctorReport } from "./doctor";
|
|
5
5
|
import { readDiagnosticLogTail } from "./diagnostic-log";
|
|
6
|
+
import { KeshaError } from "./engine/events";
|
|
7
|
+
import { errorMessage } from "./error-utils";
|
|
8
|
+
import { PATH_ERRNOS } from "./path-errnos";
|
|
6
9
|
|
|
7
10
|
interface SupportBundleOptions {
|
|
8
11
|
output?: string;
|
|
@@ -99,6 +102,66 @@ function createTarArchive(entries: TarEntry[]): Uint8Array {
|
|
|
99
102
|
return archive;
|
|
100
103
|
}
|
|
101
104
|
|
|
105
|
+
function existsAsLink(path: string): boolean {
|
|
106
|
+
try {
|
|
107
|
+
return lstatSync(path).isSymbolicLink();
|
|
108
|
+
} catch {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function fileInTheWay(dir: string): string | null {
|
|
114
|
+
for (let d = dir; ; d = dirname(d)) {
|
|
115
|
+
try {
|
|
116
|
+
return statSync(d).isDirectory() ? null : d;
|
|
117
|
+
} catch (e) {
|
|
118
|
+
if ((e as NodeJS.ErrnoException).code === "ENOENT" && existsAsLink(d)) return d;
|
|
119
|
+
if (dirname(d) === d) return null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function mkdirReason(dir: string, errno: string): string {
|
|
125
|
+
if (errno !== "EEXIST") return errno;
|
|
126
|
+
try {
|
|
127
|
+
statSync(dir);
|
|
128
|
+
return errno;
|
|
129
|
+
} catch (e) {
|
|
130
|
+
return (e as NodeJS.ErrnoException).code ?? errno;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function bundleWriteFailure(outputPath: string, e: unknown): unknown {
|
|
135
|
+
const dir = dirname(outputPath);
|
|
136
|
+
const errno = mkdirReason(dir, (e as NodeJS.ErrnoException).code ?? "");
|
|
137
|
+
const blocker = fileInTheWay(dir);
|
|
138
|
+
if (blocker) {
|
|
139
|
+
return new KeshaError("E_INVALID_ARG", `Cannot write the support bundle to ${outputPath}: ${blocker} exists and is not a directory`, {
|
|
140
|
+
hint: `remove or rename the file blocking ${blocker}, or pass --output a path under a directory.`,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
if (errno === "EISDIR") {
|
|
144
|
+
return new KeshaError("E_INVALID_ARG", `Cannot write the support bundle to ${outputPath}: it is a directory (EISDIR)`, {
|
|
145
|
+
hint: `pass --output a .tar.gz file path, for example ${join(outputPath, "kesha-support-bundle.tar.gz")}.`,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
const why = PATH_ERRNOS[errno];
|
|
149
|
+
if (why) {
|
|
150
|
+
return new KeshaError("E_INVALID_ARG", `Cannot write the support bundle to ${outputPath}: ${why} (${errno})`, {
|
|
151
|
+
hint: `pass --output a path in a directory you can write to.`,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
if (!errno) return e;
|
|
155
|
+
if (errno === "ENOSPC") {
|
|
156
|
+
return new KeshaError("E_INTERNAL", `Cannot write the support bundle to ${outputPath}: no space left on its disk (ENOSPC)`, {
|
|
157
|
+
hint: `the disk is full: free space on the volume that holds ${dir}, or pass --output a path on another disk.`,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
return new KeshaError("E_INTERNAL", `Cannot write the support bundle to ${outputPath}: ${errorMessage(e)}`, {
|
|
161
|
+
hint: `resolve that filesystem error on ${dir}, or pass --output a path elsewhere.`,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
102
165
|
export async function createSupportBundle(
|
|
103
166
|
options: SupportBundleOptions = {},
|
|
104
167
|
): Promise<SupportBundleResult> {
|
|
@@ -180,16 +243,17 @@ export async function createSupportBundle(
|
|
|
180
243
|
);
|
|
181
244
|
}
|
|
182
245
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
246
|
+
const archive = gzipSync(createTarArchive(files));
|
|
247
|
+
try {
|
|
248
|
+
mkdirSync(dirname(outputPath), { recursive: true });
|
|
249
|
+
writeFileSync(outputPath, archive);
|
|
250
|
+
} catch (e) {
|
|
251
|
+
throw bundleWriteFailure(outputPath, e);
|
|
188
252
|
}
|
|
189
253
|
|
|
190
254
|
return {
|
|
191
255
|
path: outputPath,
|
|
192
|
-
sizeBytes:
|
|
256
|
+
sizeBytes: archive.byteLength,
|
|
193
257
|
entries: files.map((file) => file.name),
|
|
194
258
|
};
|
|
195
259
|
}
|