@openparachute/vault 0.6.5-rc.2 → 0.6.5
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/.parachute/module.json +1 -0
- package/core/src/core.test.ts +7 -7
- package/core/src/do-param-cap.test.ts +161 -0
- package/core/src/links.ts +8 -13
- package/core/src/mcp.ts +1 -1
- package/core/src/notes.ts +33 -15
- package/core/src/onboarding.ts +14 -296
- package/core/src/schema.ts +5 -5
- package/core/src/seed-packs.test.ts +357 -0
- package/core/src/seed-packs.ts +823 -0
- package/core/src/sql-in.test.ts +58 -0
- package/core/src/sql-in.ts +76 -0
- package/core/src/tag-hierarchy.ts +1 -1
- package/core/src/tag-schemas.ts +2 -2
- package/core/src/transcription/provider.ts +141 -0
- package/core/src/types.ts +1 -1
- package/core/src/vault-projection.ts +1 -1
- package/core/src/wikilinks.ts +10 -4
- package/package.json +1 -1
- package/src/add-pack.test.ts +142 -0
- package/src/admin-spa.ts +2 -1
- package/src/auth.ts +1 -1
- package/src/cli.ts +806 -7
- package/src/export-watch.ts +1 -1
- package/src/live-frame-parity.test.ts +201 -0
- package/src/module-manifest.ts +8 -0
- package/src/onboarding-seed.test.ts +188 -40
- package/src/onboarding-seed.ts +41 -46
- package/src/routes.ts +20 -3
- package/src/routing.test.ts +2 -2
- package/src/routing.ts +18 -6
- package/src/self-register.test.ts +19 -0
- package/src/self-register.ts +6 -1
- package/src/server.ts +133 -31
- package/src/services-manifest.ts +8 -0
- package/src/subscriptions.ts +100 -42
- package/src/tag-scope.ts +3 -3
- package/src/test-support/live-frame-corpus.ts +72 -0
- package/src/token-store.ts +2 -2
- package/src/transcription/build.test.ts +305 -0
- package/src/transcription/build.ts +332 -0
- package/src/transcription/capability.test.ts +118 -0
- package/src/transcription/capability.ts +96 -0
- package/src/transcription/download.test.ts +101 -0
- package/src/transcription/download.ts +71 -0
- package/src/transcription/install-python.test.ts +366 -0
- package/src/transcription/install-python.ts +471 -0
- package/src/transcription/install.test.ts +167 -0
- package/src/transcription/install.ts +296 -0
- package/src/transcription/providers/onnx-asr.test.ts +229 -0
- package/src/transcription/providers/onnx-asr.ts +239 -0
- package/src/transcription/providers/parakeet-mlx.test.ts +290 -0
- package/src/transcription/providers/parakeet-mlx.ts +242 -0
- package/src/transcription/providers/scribe-http.test.ts +195 -0
- package/src/transcription/providers/scribe-http.ts +144 -0
- package/src/transcription/providers/transcribe-cpp.test.ts +314 -0
- package/src/transcription/providers/transcribe-cpp.ts +293 -0
- package/src/transcription/select.test.ts +299 -0
- package/src/transcription/select.ts +397 -0
- package/src/transcription/tiers.test.ts +197 -0
- package/src/transcription/tiers.ts +184 -0
- package/src/transcription-worker.test.ts +44 -0
- package/src/transcription-worker.ts +57 -122
- package/src/vault-create.test.ts +43 -10
- package/src/vault.test.ts +49 -1
- package/src/ws-server.ts +408 -0
- package/src/ws-subscribe.test.ts +474 -0
- package/src/ws-subscribe.ts +242 -0
- package/src/subscribe.test.ts +0 -609
- package/src/subscribe.ts +0 -248
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { describe, test, expect, afterEach } from "bun:test";
|
|
2
|
+
import { mkdirSync, writeFileSync, rmSync, chmodSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { tmpdir } from "os";
|
|
5
|
+
import {
|
|
6
|
+
resolveTranscriptionProviderName,
|
|
7
|
+
resolveTranscribeCppPaths,
|
|
8
|
+
transcribeCppInstalled,
|
|
9
|
+
probeTranscribeCliRunnable,
|
|
10
|
+
readManifest,
|
|
11
|
+
transcriptionHomeDir,
|
|
12
|
+
pythonVenvDir,
|
|
13
|
+
pythonManifestPath,
|
|
14
|
+
readPythonManifest,
|
|
15
|
+
resolveParakeetMlxBin,
|
|
16
|
+
resolveOnnxAsrBin,
|
|
17
|
+
resolveParakeetMlxModel,
|
|
18
|
+
resolveOnnxAsrModel,
|
|
19
|
+
parakeetMlxInstalled,
|
|
20
|
+
onnxAsrInstalled,
|
|
21
|
+
DEFAULT_PARAKEET_MLX_MODEL,
|
|
22
|
+
DEFAULT_ONNX_ASR_MODEL,
|
|
23
|
+
} from "./select.ts";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Provider-selection + path-resolution tests (scribe-fold Phase 2a). All env is
|
|
27
|
+
* passed explicitly so the shared bun-test process isn't polluted.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
const silent = { warn: () => {} };
|
|
31
|
+
|
|
32
|
+
describe("resolveTranscriptionProviderName", () => {
|
|
33
|
+
test("unset → scribe-http (behavior-preserving default)", () => {
|
|
34
|
+
expect(resolveTranscriptionProviderName({}, silent)).toBe("scribe-http");
|
|
35
|
+
});
|
|
36
|
+
test("blank → scribe-http", () => {
|
|
37
|
+
expect(resolveTranscriptionProviderName({ TRANSCRIPTION_PROVIDER: " " }, silent)).toBe("scribe-http");
|
|
38
|
+
});
|
|
39
|
+
test("explicit transcribe-cpp", () => {
|
|
40
|
+
expect(resolveTranscriptionProviderName({ TRANSCRIPTION_PROVIDER: "transcribe-cpp" }, silent)).toBe(
|
|
41
|
+
"transcribe-cpp",
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
test("explicit parakeet-mlx / onnx-asr (scribe-fold Phase 2b)", () => {
|
|
45
|
+
expect(resolveTranscriptionProviderName({ TRANSCRIPTION_PROVIDER: "parakeet-mlx" }, silent)).toBe(
|
|
46
|
+
"parakeet-mlx",
|
|
47
|
+
);
|
|
48
|
+
expect(resolveTranscriptionProviderName({ TRANSCRIPTION_PROVIDER: "onnx-asr" }, silent)).toBe(
|
|
49
|
+
"onnx-asr",
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
test("explicit scribe-http", () => {
|
|
53
|
+
expect(resolveTranscriptionProviderName({ TRANSCRIPTION_PROVIDER: "scribe-http" }, silent)).toBe(
|
|
54
|
+
"scribe-http",
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
test("unknown value → warns + falls back to scribe-http", () => {
|
|
58
|
+
let warned = false;
|
|
59
|
+
const name = resolveTranscriptionProviderName(
|
|
60
|
+
{ TRANSCRIPTION_PROVIDER: "whisper-magic" },
|
|
61
|
+
{ warn: () => (warned = true) },
|
|
62
|
+
);
|
|
63
|
+
expect(name).toBe("scribe-http");
|
|
64
|
+
expect(warned).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe("transcriptionHomeDir + resolveTranscribeCppPaths — PARACHUTE_HOME + overrides", () => {
|
|
69
|
+
test("honors PARACHUTE_HOME", () => {
|
|
70
|
+
const dir = transcriptionHomeDir({ PARACHUTE_HOME: "/custom/home" });
|
|
71
|
+
expect(dir).toBe(join("/custom/home", "transcription"));
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("default binPath/modelsDir under the transcription dir", () => {
|
|
75
|
+
const paths = resolveTranscribeCppPaths({ PARACHUTE_HOME: "/h" });
|
|
76
|
+
expect(paths.binPath).toBe(join("/h", "transcription", "bin", "transcribe-cli"));
|
|
77
|
+
expect(paths.modelsDir).toBe(join("/h", "transcription", "models"));
|
|
78
|
+
expect(paths.manifestPath).toBe(join("/h", "transcription", "install.json"));
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("TRANSCRIBE_CPP_BIN + TRANSCRIBE_CPP_MODEL env overrides win", () => {
|
|
82
|
+
const paths = resolveTranscribeCppPaths({
|
|
83
|
+
PARACHUTE_HOME: "/h",
|
|
84
|
+
TRANSCRIBE_CPP_BIN: "/opt/tc",
|
|
85
|
+
TRANSCRIBE_CPP_MODEL: "/models/custom.gguf",
|
|
86
|
+
});
|
|
87
|
+
expect(paths.binPath).toBe("/opt/tc");
|
|
88
|
+
expect(paths.modelPath).toBe("/models/custom.gguf");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("model path resolves from the install manifest when no env override", () => {
|
|
92
|
+
const home = mkTmpHome();
|
|
93
|
+
writeManifest(home, { modelFile: "whisper-small.en-Q5_K_M.gguf", model: "whisper-small.en" });
|
|
94
|
+
const paths = resolveTranscribeCppPaths({ PARACHUTE_HOME: home });
|
|
95
|
+
expect(paths.modelPath).toBe(join(home, "transcription", "models", "whisper-small.en-Q5_K_M.gguf"));
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("no manifest, no env → modelPath undefined", () => {
|
|
99
|
+
const home = mkTmpHome();
|
|
100
|
+
expect(resolveTranscribeCppPaths({ PARACHUTE_HOME: home }).modelPath).toBeUndefined();
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe("transcribeCppInstalled", () => {
|
|
105
|
+
test("true only when both binary AND model exist on disk", () => {
|
|
106
|
+
const home = mkTmpHome();
|
|
107
|
+
const tcDir = join(home, "transcription");
|
|
108
|
+
mkdirSync(join(tcDir, "bin"), { recursive: true });
|
|
109
|
+
mkdirSync(join(tcDir, "models"), { recursive: true });
|
|
110
|
+
// Neither present yet.
|
|
111
|
+
expect(transcribeCppInstalled(resolveTranscribeCppPaths({ PARACHUTE_HOME: home }))).toBe(false);
|
|
112
|
+
|
|
113
|
+
// Binary only → still false.
|
|
114
|
+
writeFileSync(join(tcDir, "bin", "transcribe-cli"), "#!/bin/sh\n");
|
|
115
|
+
writeManifest(home, { modelFile: "m.gguf", model: "whisper-tiny.en" });
|
|
116
|
+
expect(transcribeCppInstalled(resolveTranscribeCppPaths({ PARACHUTE_HOME: home }))).toBe(false);
|
|
117
|
+
|
|
118
|
+
// Model too → true.
|
|
119
|
+
writeFileSync(join(tcDir, "models", "m.gguf"), "gguf");
|
|
120
|
+
expect(transcribeCppInstalled(resolveTranscribeCppPaths({ PARACHUTE_HOME: home }))).toBe(true);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe("probeTranscribeCliRunnable — EXECUTED, not stat'd (vault#534)", () => {
|
|
125
|
+
// Tiny real shell scripts stand in for transcribe-cli: the probe's whole
|
|
126
|
+
// point is that it actually runs the binary, so mocks would test nothing.
|
|
127
|
+
function script(home: string, body: string): string {
|
|
128
|
+
const p = join(home, "fake-cli");
|
|
129
|
+
writeFileSync(p, `#!/bin/sh\n${body}\n`);
|
|
130
|
+
chmodSync(p, 0o755);
|
|
131
|
+
return p;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
test("exit 0 ⇒ ok", async () => {
|
|
135
|
+
const bin = script(mkTmpHome(), "exit 0");
|
|
136
|
+
expect(await probeTranscribeCliRunnable(bin)).toEqual({ ok: true });
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("nonzero exit ⇒ not ok, reason carries the exit code + first stderr line", async () => {
|
|
140
|
+
// Mirrors the vault#534 Linux failure shape: binary present + launches,
|
|
141
|
+
// but errors out (there: empty ggml backend registry, exit 1).
|
|
142
|
+
const bin = script(mkTmpHome(), 'echo "whisper: failed to initialize CPU backend" >&2\nexit 1');
|
|
143
|
+
const r = await probeTranscribeCliRunnable(bin);
|
|
144
|
+
expect(r.ok).toBe(false);
|
|
145
|
+
expect(r.reason).toContain("exited 1");
|
|
146
|
+
expect(r.reason).toContain("failed to initialize CPU backend");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("unlaunchable binary (missing) ⇒ not ok with a reason", async () => {
|
|
150
|
+
const r = await probeTranscribeCliRunnable(join(mkTmpHome(), "no-such-cli"));
|
|
151
|
+
expect(r.ok).toBe(false);
|
|
152
|
+
expect(r.reason).toBeTruthy();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("hang ⇒ not ok after the timeout", async () => {
|
|
156
|
+
const bin = script(mkTmpHome(), "sleep 30");
|
|
157
|
+
const r = await probeTranscribeCliRunnable(bin, { timeoutMs: 250 });
|
|
158
|
+
expect(r.ok).toBe(false);
|
|
159
|
+
expect(r.reason).toContain("250ms");
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe("readManifest", () => {
|
|
164
|
+
test("returns null for a missing/unreadable manifest", () => {
|
|
165
|
+
expect(readManifest(join(tmpdir(), "does-not-exist-xyz.json"))).toBeNull();
|
|
166
|
+
});
|
|
167
|
+
test("parses a written manifest", () => {
|
|
168
|
+
const home = mkTmpHome();
|
|
169
|
+
writeManifest(home, { model: "whisper-tiny.en", modelFile: "t.gguf" });
|
|
170
|
+
const m = readManifest(join(home, "transcription", "install.json"));
|
|
171
|
+
expect(m?.model).toBe("whisper-tiny.en");
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// --- python providers (parakeet-mlx / onnx-asr) — scribe-fold Phase 2b ------
|
|
176
|
+
|
|
177
|
+
describe("python provider bin resolution (env → venv → PATH)", () => {
|
|
178
|
+
const noHit = { existsImpl: () => false, whichImpl: () => null };
|
|
179
|
+
|
|
180
|
+
test("pythonVenvDir lives under the transcription home", () => {
|
|
181
|
+
expect(pythonVenvDir({ PARACHUTE_HOME: "/h" })).toBe(join("/h", "transcription", "venv"));
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("env override wins and is returned verbatim (existence is the caller's check)", () => {
|
|
185
|
+
expect(resolveParakeetMlxBin({ PARACHUTE_HOME: "/h", PARAKEET_MLX_BIN: "/opt/pk" }, noHit)).toBe("/opt/pk");
|
|
186
|
+
expect(resolveOnnxAsrBin({ PARACHUTE_HOME: "/h", ONNX_ASR_BIN: "/opt/ox" }, noHit)).toBe("/opt/ox");
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("managed venv binary beats PATH", () => {
|
|
190
|
+
const venvPk = join("/h", "transcription", "venv", "bin", "parakeet-mlx");
|
|
191
|
+
const bin = resolveParakeetMlxBin(
|
|
192
|
+
{ PARACHUTE_HOME: "/h" },
|
|
193
|
+
{ existsImpl: (p) => p === venvPk, whichImpl: () => "/usr/local/bin/parakeet-mlx" },
|
|
194
|
+
);
|
|
195
|
+
expect(bin).toBe(venvPk);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("falls back to PATH when no venv binary exists", () => {
|
|
199
|
+
const bin = resolveOnnxAsrBin(
|
|
200
|
+
{ PARACHUTE_HOME: "/h" },
|
|
201
|
+
{ existsImpl: () => false, whichImpl: (b) => (b === "onnx-asr" ? "/usr/local/bin/onnx-asr" : null) },
|
|
202
|
+
);
|
|
203
|
+
expect(bin).toBe("/usr/local/bin/onnx-asr");
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("nothing anywhere → undefined (provider reports unavailable)", () => {
|
|
207
|
+
expect(resolveParakeetMlxBin({ PARACHUTE_HOME: "/h" }, noHit)).toBeUndefined();
|
|
208
|
+
expect(resolveOnnxAsrBin({ PARACHUTE_HOME: "/h" }, noHit)).toBeUndefined();
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
describe("parakeetMlxInstalled / onnxAsrInstalled", () => {
|
|
213
|
+
test("true only when the resolved binary exists on disk", () => {
|
|
214
|
+
const home = mkTmpHome();
|
|
215
|
+
const env = { PARACHUTE_HOME: home };
|
|
216
|
+
expect(parakeetMlxInstalled(env, { whichImpl: () => null })).toBe(false);
|
|
217
|
+
expect(onnxAsrInstalled(env, { whichImpl: () => null })).toBe(false);
|
|
218
|
+
|
|
219
|
+
const binDir = join(home, "transcription", "venv", "bin");
|
|
220
|
+
mkdirSync(binDir, { recursive: true });
|
|
221
|
+
writeFileSync(join(binDir, "onnx-asr"), "#!/bin/sh\n");
|
|
222
|
+
expect(onnxAsrInstalled(env, { whichImpl: () => null })).toBe(true);
|
|
223
|
+
expect(parakeetMlxInstalled(env, { whichImpl: () => null })).toBe(false);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("an env override pointing at a missing file is NOT installed (honesty)", () => {
|
|
227
|
+
const home = mkTmpHome();
|
|
228
|
+
expect(
|
|
229
|
+
onnxAsrInstalled({ PARACHUTE_HOME: home, ONNX_ASR_BIN: "/nope/onnx-asr" }, { whichImpl: () => null }),
|
|
230
|
+
).toBe(false);
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
describe("python model resolution (env → manifest → ratified default)", () => {
|
|
235
|
+
test("defaults are the ratified v3 models", () => {
|
|
236
|
+
const home = mkTmpHome();
|
|
237
|
+
expect(resolveParakeetMlxModel({ PARACHUTE_HOME: home })).toBe(DEFAULT_PARAKEET_MLX_MODEL);
|
|
238
|
+
expect(resolveOnnxAsrModel({ PARACHUTE_HOME: home })).toBe(DEFAULT_ONNX_ASR_MODEL);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test("env overrides win", () => {
|
|
242
|
+
const home = mkTmpHome();
|
|
243
|
+
expect(resolveParakeetMlxModel({ PARACHUTE_HOME: home, PARAKEET_MLX_MODEL: "mlx-community/custom" })).toBe(
|
|
244
|
+
"mlx-community/custom",
|
|
245
|
+
);
|
|
246
|
+
expect(resolveOnnxAsrModel({ PARACHUTE_HOME: home, ONNX_ASR_MODEL: "whisper-base" })).toBe("whisper-base");
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test("the install manifest's model applies to ITS provider only", () => {
|
|
250
|
+
const home = mkTmpHome();
|
|
251
|
+
const tcDir = join(home, "transcription");
|
|
252
|
+
mkdirSync(tcDir, { recursive: true });
|
|
253
|
+
writeFileSync(
|
|
254
|
+
join(tcDir, "install-python.json"),
|
|
255
|
+
JSON.stringify({ provider: "onnx-asr", model: "manifest-model" }),
|
|
256
|
+
);
|
|
257
|
+
expect(resolveOnnxAsrModel({ PARACHUTE_HOME: home })).toBe("manifest-model");
|
|
258
|
+
// A parakeet lookup ignores an onnx manifest.
|
|
259
|
+
expect(resolveParakeetMlxModel({ PARACHUTE_HOME: home })).toBe(DEFAULT_PARAKEET_MLX_MODEL);
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
describe("readPythonManifest", () => {
|
|
264
|
+
test("returns null for a missing manifest", () => {
|
|
265
|
+
const home = mkTmpHome();
|
|
266
|
+
expect(readPythonManifest(pythonManifestPath({ PARACHUTE_HOME: home }))).toBeNull();
|
|
267
|
+
});
|
|
268
|
+
test("parses a written manifest", () => {
|
|
269
|
+
const home = mkTmpHome();
|
|
270
|
+
const tcDir = join(home, "transcription");
|
|
271
|
+
mkdirSync(tcDir, { recursive: true });
|
|
272
|
+
writeFileSync(
|
|
273
|
+
join(tcDir, "install-python.json"),
|
|
274
|
+
JSON.stringify({ provider: "parakeet-mlx", model: "m", pipTarget: "parakeet-mlx" }),
|
|
275
|
+
);
|
|
276
|
+
const m = readPythonManifest(pythonManifestPath({ PARACHUTE_HOME: home }));
|
|
277
|
+
expect(m?.provider).toBe("parakeet-mlx");
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// --- helpers ---------------------------------------------------------------
|
|
282
|
+
|
|
283
|
+
const tmpHomes: string[] = [];
|
|
284
|
+
afterEach(() => {
|
|
285
|
+
for (const h of tmpHomes.splice(0)) rmSync(h, { recursive: true, force: true });
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
function mkTmpHome(): string {
|
|
289
|
+
const home = join(tmpdir(), `tc-select-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
290
|
+
mkdirSync(home, { recursive: true });
|
|
291
|
+
tmpHomes.push(home);
|
|
292
|
+
return home;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function writeManifest(home: string, fields: Record<string, unknown>): void {
|
|
296
|
+
const tcDir = join(home, "transcription");
|
|
297
|
+
mkdirSync(tcDir, { recursive: true });
|
|
298
|
+
writeFileSync(join(tcDir, "install.json"), JSON.stringify(fields));
|
|
299
|
+
}
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transcription provider selection + `transcribe-cpp` path resolution
|
|
3
|
+
* (scribe-fold Phase 2a).
|
|
4
|
+
*
|
|
5
|
+
* Vault now ships TWO transcription providers behind the Phase 1
|
|
6
|
+
* `TranscriptionProvider` seam:
|
|
7
|
+
*
|
|
8
|
+
* - `scribe-http` — the remote/compat provider (the DEFAULT; existing scribe
|
|
9
|
+
* installs are unchanged);
|
|
10
|
+
* - `transcribe-cpp` — a local, no-Python provider that subprocesses the
|
|
11
|
+
* prebuilt `transcribe-cli` (opt-in via `transcription install` + config).
|
|
12
|
+
*
|
|
13
|
+
* The active provider is chosen by the `TRANSCRIPTION_PROVIDER` env var
|
|
14
|
+
* (persisted in `~/.parachute/vault/.env`), resolved here so the worker boot
|
|
15
|
+
* (`server.ts`) and the capability flag (`capability.ts`) agree on one source
|
|
16
|
+
* of truth. Unset ⇒ `scribe-http`, so no config change means no behavior
|
|
17
|
+
* change.
|
|
18
|
+
*
|
|
19
|
+
* The `transcribe-cli` binary, GGUF model, and runtime shared libraries live
|
|
20
|
+
* under `$PARACHUTE_HOME/transcription/` (parallel to the vault's other
|
|
21
|
+
* ecosystem state), written there by `transcription install`. NOTE:
|
|
22
|
+
* transcribe.cpp v0.1.1 ships a *library*, not a prebuilt CLI, so after an
|
|
23
|
+
* install the `libs/` + `models/` are present but `bin/transcribe-cli` may be
|
|
24
|
+
* absent until a CLI is built or `TRANSCRIBE_CPP_BIN` is set — the readiness
|
|
25
|
+
* checks below gate on the binary existing. Paths are resolved per-call so
|
|
26
|
+
* `PARACHUTE_HOME` overrides (tests, Docker) apply.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { join } from "path";
|
|
30
|
+
import { homedir } from "os";
|
|
31
|
+
import { existsSync, readFileSync } from "fs";
|
|
32
|
+
|
|
33
|
+
export const TRANSCRIPTION_PROVIDERS = [
|
|
34
|
+
"scribe-http",
|
|
35
|
+
"transcribe-cpp",
|
|
36
|
+
"parakeet-mlx",
|
|
37
|
+
"onnx-asr",
|
|
38
|
+
] as const;
|
|
39
|
+
export type TranscriptionProviderName = (typeof TRANSCRIPTION_PROVIDERS)[number];
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Default models for the Python-based local providers (scribe-fold Phase 2b —
|
|
43
|
+
* the ratified RAM-tier table, 2026-07-03).
|
|
44
|
+
*
|
|
45
|
+
* - parakeet-mlx drives the `parakeet-mlx` CLI (MLX, Apple Silicon); the
|
|
46
|
+
* model is an mlx-community HuggingFace repo fetched into the HF cache on
|
|
47
|
+
* first use (~2.5GB).
|
|
48
|
+
* - onnx-asr drives the `onnx-asr` CLI (ONNX Runtime, CPU); the model id is
|
|
49
|
+
* onnx-asr's own registry name for NVIDIA Parakeet TDT 0.6b v3 (int8 ONNX,
|
|
50
|
+
* ~670MB), fetched on first use / warm-pulled at install.
|
|
51
|
+
*/
|
|
52
|
+
export const DEFAULT_PARAKEET_MLX_MODEL = "mlx-community/parakeet-tdt-0.6b-v3";
|
|
53
|
+
export const DEFAULT_ONNX_ASR_MODEL = "nemo-parakeet-tdt-0.6b-v3";
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Resolve the configured provider name. `TRANSCRIPTION_PROVIDER` selects it;
|
|
57
|
+
* unset (or blank) ⇒ `scribe-http` (the behavior-preserving default). An
|
|
58
|
+
* unrecognized value warns once and falls back to `scribe-http` rather than
|
|
59
|
+
* failing boot — a typo shouldn't take transcription offline hard.
|
|
60
|
+
*/
|
|
61
|
+
export function resolveTranscriptionProviderName(
|
|
62
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
63
|
+
logger: { warn?: (...args: unknown[]) => void } = console,
|
|
64
|
+
): TranscriptionProviderName {
|
|
65
|
+
const raw = env.TRANSCRIPTION_PROVIDER?.trim();
|
|
66
|
+
if (!raw) return "scribe-http";
|
|
67
|
+
if ((TRANSCRIPTION_PROVIDERS as readonly string[]).includes(raw)) {
|
|
68
|
+
return raw as TranscriptionProviderName;
|
|
69
|
+
}
|
|
70
|
+
logger.warn?.(
|
|
71
|
+
`[transcribe] unknown TRANSCRIPTION_PROVIDER="${raw}" — falling back to scribe-http. ` +
|
|
72
|
+
`Valid values: ${TRANSCRIPTION_PROVIDERS.join(", ")}.`,
|
|
73
|
+
);
|
|
74
|
+
return "scribe-http";
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The ecosystem root (shared with `config.ts`'s `configDirPath`), per-call so
|
|
78
|
+
* `PARACHUTE_HOME` overrides apply in tests / Docker. */
|
|
79
|
+
function ecosystemRoot(env: NodeJS.ProcessEnv): string {
|
|
80
|
+
return env.PARACHUTE_HOME ?? join(homedir(), ".parachute");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The cache dir for transcribe-cpp's binary + models: `<root>/transcription/`. */
|
|
84
|
+
export function transcriptionHomeDir(env: NodeJS.ProcessEnv = process.env): string {
|
|
85
|
+
return join(ecosystemRoot(env), "transcription");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Resolved filesystem locations for the transcribe-cpp install. */
|
|
89
|
+
export interface TranscribeCppPaths {
|
|
90
|
+
/** `<root>/transcription/`. */
|
|
91
|
+
dir: string;
|
|
92
|
+
/** `<dir>/bin/`. */
|
|
93
|
+
binDir: string;
|
|
94
|
+
/** The `transcribe-cli` binary path (env `TRANSCRIBE_CPP_BIN` overrides).
|
|
95
|
+
* May not exist on disk — v0.1.1 ships a library, not a CLI. */
|
|
96
|
+
binPath: string;
|
|
97
|
+
/** `<dir>/libs/` — the extracted runtime shared libraries (libtranscribe +
|
|
98
|
+
* libggml) the CLI links against. */
|
|
99
|
+
libsDir: string;
|
|
100
|
+
/** `<dir>/models/`. */
|
|
101
|
+
modelsDir: string;
|
|
102
|
+
/** `<dir>/install.json` — the install manifest. */
|
|
103
|
+
manifestPath: string;
|
|
104
|
+
/**
|
|
105
|
+
* The active GGUF model path, or `undefined` when nothing is installed.
|
|
106
|
+
* Env `TRANSCRIBE_CPP_MODEL` overrides; otherwise resolved from the
|
|
107
|
+
* manifest's `modelFile` under `modelsDir`.
|
|
108
|
+
*/
|
|
109
|
+
modelPath: string | undefined;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Persisted `install.json` shape (written by `transcription install`). */
|
|
113
|
+
export interface TranscribeCppManifest {
|
|
114
|
+
version: string;
|
|
115
|
+
asset: string;
|
|
116
|
+
/** Binary filename under `bin/` (usually "transcribe-cli"). */
|
|
117
|
+
binFile: string;
|
|
118
|
+
/** Model id (e.g. "whisper-small.en"). */
|
|
119
|
+
model: string;
|
|
120
|
+
/** GGUF filename under `models/`. */
|
|
121
|
+
modelFile: string;
|
|
122
|
+
/** Runtime shared-library filenames extracted under `libs/` (libtranscribe +
|
|
123
|
+
* libggml). v0.1.1 ships these instead of a CLI. */
|
|
124
|
+
libFiles?: string[];
|
|
125
|
+
/** When the `transcribe-cli` was built from source at install, the pinned
|
|
126
|
+
* upstream source ref it was compiled from. Absent when no CLI was built
|
|
127
|
+
* (build skipped/failed, or an operator-supplied `TRANSCRIBE_CPP_BIN`). */
|
|
128
|
+
binBuiltFrom?: string;
|
|
129
|
+
os: string;
|
|
130
|
+
arch: string;
|
|
131
|
+
ram_gb: number;
|
|
132
|
+
installedAt: string;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Resolve the transcribe-cpp binary + model paths. Env overrides
|
|
137
|
+
* (`TRANSCRIBE_CPP_BIN`, `TRANSCRIBE_CPP_MODEL`) win; otherwise the binary is
|
|
138
|
+
* `<dir>/bin/transcribe-cli` and the model comes from the install manifest.
|
|
139
|
+
*/
|
|
140
|
+
export function resolveTranscribeCppPaths(
|
|
141
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
142
|
+
): TranscribeCppPaths {
|
|
143
|
+
const dir = transcriptionHomeDir(env);
|
|
144
|
+
const binDir = join(dir, "bin");
|
|
145
|
+
const libsDir = join(dir, "libs");
|
|
146
|
+
const modelsDir = join(dir, "models");
|
|
147
|
+
const manifestPath = join(dir, "install.json");
|
|
148
|
+
|
|
149
|
+
const binPath = env.TRANSCRIBE_CPP_BIN?.trim() || join(binDir, "transcribe-cli");
|
|
150
|
+
|
|
151
|
+
let modelPath = env.TRANSCRIBE_CPP_MODEL?.trim() || undefined;
|
|
152
|
+
if (!modelPath) {
|
|
153
|
+
const manifest = readManifest(manifestPath);
|
|
154
|
+
if (manifest?.modelFile) modelPath = join(modelsDir, manifest.modelFile);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return { dir, binDir, binPath, libsDir, modelsDir, manifestPath, modelPath };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Read + parse the install manifest, or `null` when absent/unreadable. */
|
|
161
|
+
export function readManifest(manifestPath: string): TranscribeCppManifest | null {
|
|
162
|
+
try {
|
|
163
|
+
if (!existsSync(manifestPath)) return null;
|
|
164
|
+
const parsed = JSON.parse(readFileSync(manifestPath, "utf8")) as TranscribeCppManifest;
|
|
165
|
+
return parsed && typeof parsed === "object" ? parsed : null;
|
|
166
|
+
} catch {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Cheap, spawn-free readiness check: a runnable `transcribe-cli` binary AND a
|
|
173
|
+
* model both exist on disk. Used by the worker-boot gate (`server.ts`) and
|
|
174
|
+
* mirrors the provider's `available()`. Because v0.1.1 ships a library (no CLI),
|
|
175
|
+
* this stays `false` after an install until a CLI is built or
|
|
176
|
+
* `TRANSCRIBE_CPP_BIN` points at one — so the worker never starts only to
|
|
177
|
+
* terminal-fail every item.
|
|
178
|
+
*/
|
|
179
|
+
export function transcribeCppInstalled(
|
|
180
|
+
paths: TranscribeCppPaths = resolveTranscribeCppPaths(),
|
|
181
|
+
): boolean {
|
|
182
|
+
return existsSync(paths.binPath) && !!paths.modelPath && existsSync(paths.modelPath);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Outcome of the EXECUTED runnable probe (`probeTranscribeCliRunnable`). */
|
|
186
|
+
export interface RunnableProbeResult {
|
|
187
|
+
ok: boolean;
|
|
188
|
+
/** Why the probe failed, for the `runnable: no (<reason>)` print. */
|
|
189
|
+
reason?: string;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* EXECUTED runnable probe: actually launch `transcribe-cli --help` and check
|
|
194
|
+
* the exit code. Cheap (no model load, no inference — upstream's `--help`
|
|
195
|
+
* prints usage and exits 0) but honest where a stat can't be: it catches a
|
|
196
|
+
* binary whose shared libraries don't resolve (broken/missing `libs/`, a bad
|
|
197
|
+
* rpath), a wrong-arch or corrupt binary, and a missing exec bit — all cases
|
|
198
|
+
* where `existsSync` says "yes" while every real run fails (the vault#534
|
|
199
|
+
* honesty bug: Linux installs reported `runnable: yes` while the CLI exited 1
|
|
200
|
+
* on every transcription).
|
|
201
|
+
*
|
|
202
|
+
* Deliberately NOT used by the per-request capability gate or the provider's
|
|
203
|
+
* `available()` (both are documented spawn-free); this is for the human-paced
|
|
204
|
+
* paths — `transcription status` and the install verb's activation check.
|
|
205
|
+
*/
|
|
206
|
+
export async function probeTranscribeCliRunnable(
|
|
207
|
+
binPath: string,
|
|
208
|
+
opts: { timeoutMs?: number } = {},
|
|
209
|
+
): Promise<RunnableProbeResult> {
|
|
210
|
+
const timeoutMs = opts.timeoutMs ?? 10_000;
|
|
211
|
+
let proc: ReturnType<typeof Bun.spawn>;
|
|
212
|
+
try {
|
|
213
|
+
proc = Bun.spawn([binPath, "--help"], { stdin: "ignore", stdout: "ignore", stderr: "pipe" });
|
|
214
|
+
} catch (err) {
|
|
215
|
+
return {
|
|
216
|
+
ok: false,
|
|
217
|
+
reason: `could not launch ${binPath}: ${err instanceof Error ? err.message : String(err)}`,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
let timedOut = false;
|
|
221
|
+
const timer = setTimeout(() => {
|
|
222
|
+
timedOut = true;
|
|
223
|
+
proc.kill("SIGKILL"); // diagnostic probe — no graceful shutdown to preserve
|
|
224
|
+
}, timeoutMs);
|
|
225
|
+
try {
|
|
226
|
+
const exitCode = await proc.exited;
|
|
227
|
+
if (timedOut) return { ok: false, reason: `--help did not exit within ${timeoutMs}ms` };
|
|
228
|
+
// stderr normally EOFs at exit; the race guards against a wrapper-script
|
|
229
|
+
// binary whose orphaned grandchild still holds the pipe open.
|
|
230
|
+
const stderr = await Promise.race([
|
|
231
|
+
new Response(proc.stderr as ReadableStream).text(),
|
|
232
|
+
new Promise<string>((resolve) => setTimeout(resolve, 2_000, "")),
|
|
233
|
+
]);
|
|
234
|
+
const detail = stderr.trim().split("\n")[0]?.slice(0, 200);
|
|
235
|
+
if (proc.signalCode) {
|
|
236
|
+
// e.g. macOS dyld aborts with SIGABRT when a linked dylib is missing.
|
|
237
|
+
return { ok: false, reason: `--help died on ${proc.signalCode}${detail ? `: ${detail}` : ""}` };
|
|
238
|
+
}
|
|
239
|
+
if (exitCode !== 0) {
|
|
240
|
+
return { ok: false, reason: `--help exited ${exitCode}${detail ? `: ${detail}` : ""}` };
|
|
241
|
+
}
|
|
242
|
+
return { ok: true };
|
|
243
|
+
} finally {
|
|
244
|
+
clearTimeout(timer);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ---------------------------------------------------------------------------
|
|
249
|
+
// Python-based local providers (parakeet-mlx / onnx-asr) — scribe-fold 2b
|
|
250
|
+
// ---------------------------------------------------------------------------
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* The dedicated venv `transcription install` creates for the Python-based
|
|
254
|
+
* providers: `<root>/transcription/venv/`. A deliberate deviation from
|
|
255
|
+
* scribe's installer (which preferred `uv tool install` + a `~/.venvs`
|
|
256
|
+
* fallback and then had to caveat "add the venv to your PATH"): vault resolves
|
|
257
|
+
* the venv binary by ABSOLUTE path, so the daemon needs no PATH surgery and
|
|
258
|
+
* the whole install is sandboxed under `PARACHUTE_HOME` (tests, Docker).
|
|
259
|
+
*/
|
|
260
|
+
export function pythonVenvDir(env: NodeJS.ProcessEnv = process.env): string {
|
|
261
|
+
return join(transcriptionHomeDir(env), "venv");
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** `<root>/transcription/install-python.json` — the python-provider install manifest. */
|
|
265
|
+
export function pythonManifestPath(env: NodeJS.ProcessEnv = process.env): string {
|
|
266
|
+
return join(transcriptionHomeDir(env), "install-python.json");
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** Persisted manifest for a python-provider install (written by `transcription install`). */
|
|
270
|
+
export interface PythonInstallManifest {
|
|
271
|
+
/** Which provider this install set up ("parakeet-mlx" | "onnx-asr"). */
|
|
272
|
+
provider: string;
|
|
273
|
+
/** The pip install target (e.g. "onnx-asr[cpu,hub]"). */
|
|
274
|
+
pipTarget: string;
|
|
275
|
+
/** Resolved binary path at install time. */
|
|
276
|
+
bin: string;
|
|
277
|
+
/** The venv the package landed in ("" when the binary came from PATH). */
|
|
278
|
+
venv: string;
|
|
279
|
+
/** Model id the provider was configured with. */
|
|
280
|
+
model: string;
|
|
281
|
+
os: string;
|
|
282
|
+
arch: string;
|
|
283
|
+
ram_gb: number;
|
|
284
|
+
installedAt: string;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** Read + parse the python install manifest, or `null` when absent/unreadable. */
|
|
288
|
+
export function readPythonManifest(
|
|
289
|
+
manifestPath: string = pythonManifestPath(),
|
|
290
|
+
): PythonInstallManifest | null {
|
|
291
|
+
try {
|
|
292
|
+
if (!existsSync(manifestPath)) return null;
|
|
293
|
+
const parsed = JSON.parse(readFileSync(manifestPath, "utf8")) as PythonInstallManifest;
|
|
294
|
+
return parsed && typeof parsed === "object" ? parsed : null;
|
|
295
|
+
} catch {
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** Injection seams for binary resolution (tests stub the fs + PATH probes). */
|
|
301
|
+
export interface BinResolveDeps {
|
|
302
|
+
existsImpl?: (p: string) => boolean;
|
|
303
|
+
whichImpl?: (bin: string) => string | null;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Resolve a python-provider binary. Ladder (first hit wins):
|
|
308
|
+
*
|
|
309
|
+
* 1. an explicit env override (`PARAKEET_MLX_BIN` / `ONNX_ASR_BIN`) —
|
|
310
|
+
* returned verbatim, existence-checked by the caller (mirrors
|
|
311
|
+
* `TRANSCRIBE_CPP_BIN`'s contract);
|
|
312
|
+
* 2. the managed venv binary `<root>/transcription/venv/bin/<bin>` when it
|
|
313
|
+
* exists (vault's own install — deterministic under the daemon);
|
|
314
|
+
* 3. a PATH-installed binary via `Bun.which` (an operator's own
|
|
315
|
+
* `uv tool install` / pipx install keeps working).
|
|
316
|
+
*
|
|
317
|
+
* Returns `undefined` when nothing is found — the provider reports itself
|
|
318
|
+
* unavailable and the worker-boot gate stays closed.
|
|
319
|
+
*/
|
|
320
|
+
function resolvePythonBin(
|
|
321
|
+
envVar: string,
|
|
322
|
+
bin: string,
|
|
323
|
+
env: NodeJS.ProcessEnv,
|
|
324
|
+
deps: BinResolveDeps,
|
|
325
|
+
): string | undefined {
|
|
326
|
+
const override = env[envVar]?.trim();
|
|
327
|
+
if (override) return override;
|
|
328
|
+
const exists = deps.existsImpl ?? existsSync;
|
|
329
|
+
const which = deps.whichImpl ?? ((b: string) => Bun.which(b));
|
|
330
|
+
const venvBin = join(pythonVenvDir(env), "bin", bin);
|
|
331
|
+
if (exists(venvBin)) return venvBin;
|
|
332
|
+
return which(bin) ?? undefined;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** Resolve the `parakeet-mlx` binary (env `PARAKEET_MLX_BIN` → venv → PATH). */
|
|
336
|
+
export function resolveParakeetMlxBin(
|
|
337
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
338
|
+
deps: BinResolveDeps = {},
|
|
339
|
+
): string | undefined {
|
|
340
|
+
return resolvePythonBin("PARAKEET_MLX_BIN", "parakeet-mlx", env, deps);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** Resolve the `onnx-asr` binary (env `ONNX_ASR_BIN` → venv → PATH). */
|
|
344
|
+
export function resolveOnnxAsrBin(
|
|
345
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
346
|
+
deps: BinResolveDeps = {},
|
|
347
|
+
): string | undefined {
|
|
348
|
+
return resolvePythonBin("ONNX_ASR_BIN", "onnx-asr", env, deps);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** Model resolution: env override → install manifest → ratified default. */
|
|
352
|
+
function resolvePythonModel(
|
|
353
|
+
envVar: string,
|
|
354
|
+
provider: string,
|
|
355
|
+
fallback: string,
|
|
356
|
+
env: NodeJS.ProcessEnv,
|
|
357
|
+
): string {
|
|
358
|
+
const override = env[envVar]?.trim();
|
|
359
|
+
if (override) return override;
|
|
360
|
+
const manifest = readPythonManifest(pythonManifestPath(env));
|
|
361
|
+
if (manifest?.provider === provider && manifest.model) return manifest.model;
|
|
362
|
+
return fallback;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** The parakeet-mlx model id (`PARAKEET_MLX_MODEL` → manifest → default v3). */
|
|
366
|
+
export function resolveParakeetMlxModel(env: NodeJS.ProcessEnv = process.env): string {
|
|
367
|
+
return resolvePythonModel("PARAKEET_MLX_MODEL", "parakeet-mlx", DEFAULT_PARAKEET_MLX_MODEL, env);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** The onnx-asr model id (`ONNX_ASR_MODEL` → manifest → default parakeet v3 int8). */
|
|
371
|
+
export function resolveOnnxAsrModel(env: NodeJS.ProcessEnv = process.env): string {
|
|
372
|
+
return resolvePythonModel("ONNX_ASR_MODEL", "onnx-asr", DEFAULT_ONNX_ASR_MODEL, env);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Cheap, spawn-free readiness check for a python provider: a resolved binary
|
|
377
|
+
* that exists on disk. (The MODEL is fetched into the HF cache on first use —
|
|
378
|
+
* there is no cheap on-disk check for it, so readiness is binary-only,
|
|
379
|
+
* matching the providers' `available()`.) Used by the worker-boot gate and
|
|
380
|
+
* `transcription status`.
|
|
381
|
+
*/
|
|
382
|
+
export function parakeetMlxInstalled(
|
|
383
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
384
|
+
deps: BinResolveDeps = {},
|
|
385
|
+
): boolean {
|
|
386
|
+
const bin = resolveParakeetMlxBin(env, deps);
|
|
387
|
+
return !!bin && (deps.existsImpl ?? existsSync)(bin);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** See `parakeetMlxInstalled`. */
|
|
391
|
+
export function onnxAsrInstalled(
|
|
392
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
393
|
+
deps: BinResolveDeps = {},
|
|
394
|
+
): boolean {
|
|
395
|
+
const bin = resolveOnnxAsrBin(env, deps);
|
|
396
|
+
return !!bin && (deps.existsImpl ?? existsSync)(bin);
|
|
397
|
+
}
|