@1agh/maude 0.43.0 → 0.44.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/apps/studio/acp/bridge.ts +44 -10
- package/apps/studio/acp/env.ts +23 -0
- package/apps/studio/acp/login-state.ts +263 -0
- package/apps/studio/acp/plugin-bootstrap.ts +31 -25
- package/apps/studio/acp/probe.ts +152 -1
- package/apps/studio/annotations-layer.tsx +396 -46
- package/apps/studio/api.ts +395 -2
- package/apps/studio/bin/_audio-search.mjs +140 -0
- package/apps/studio/bin/_import-asset-pdf-worker.mjs +34 -0
- package/apps/studio/bin/_import-asset.mjs +815 -0
- package/apps/studio/bin/_import-brand.mjs +635 -0
- package/apps/studio/bin/_import-tokens-alias-resolver.mjs +95 -0
- package/apps/studio/bin/_import-tokens.mjs +1201 -0
- package/apps/studio/bin/_transcribe.mjs +419 -0
- package/apps/studio/bin/_transcribe.test.mjs +80 -0
- package/apps/studio/bin/audio-search.sh +28 -0
- package/apps/studio/bin/generate.sh +154 -0
- package/apps/studio/bin/import-asset.sh +34 -0
- package/apps/studio/bin/import-brand.sh +29 -0
- package/apps/studio/bin/import-tokens.sh +33 -0
- package/apps/studio/bin/photo-bg-remove.sh +119 -21
- package/apps/studio/bin/transcribe.sh +39 -0
- package/apps/studio/canvas-edit.ts +197 -2
- package/apps/studio/canvas-lib.tsx +115 -6
- package/apps/studio/client/app.jsx +1954 -685
- package/apps/studio/client/generate-dialog.jsx +352 -0
- package/apps/studio/client/github.js +14 -0
- package/apps/studio/client/inspector-controls.jsx +781 -0
- package/apps/studio/client/panels/BrandUploadPanel.jsx +233 -0
- package/apps/studio/client/panels/ChatPanel.jsx +15 -33
- package/apps/studio/client/panels/IntroVideoDialog.jsx +44 -0
- package/apps/studio/client/panels/OnboardingWizard.jsx +12 -0
- package/apps/studio/client/panels/ReadinessList.jsx +227 -7
- package/apps/studio/client/panels/SettingsPanel.jsx +828 -0
- package/apps/studio/client/panels/SetupChecklist.jsx +223 -0
- package/apps/studio/client/photo-knobs.jsx +167 -172
- package/apps/studio/client/styles/3-shell-maude.css +334 -28
- package/apps/studio/client/styles/4-components.css +223 -0
- package/apps/studio/client/styles/6-acp-chat.css +86 -0
- package/apps/studio/client/tour/quick-setup-tour.js +39 -0
- package/apps/studio/commands/annotation-strokes-command.ts +13 -3
- package/apps/studio/config.schema.json +70 -0
- package/apps/studio/context-menu.tsx +11 -2
- package/apps/studio/context.ts +16 -0
- package/apps/studio/design-setup-readiness.ts +115 -0
- package/apps/studio/dist/client.bundle.js +2097 -2088
- package/apps/studio/dist/styles.css +1 -1
- package/apps/studio/draw/palette.ts +34 -0
- package/apps/studio/footage/schema.test.ts +92 -0
- package/apps/studio/footage/schema.ts +179 -1
- package/apps/studio/generation/adapters/elevenlabs.ts +370 -0
- package/apps/studio/generation/adapters/gemini.ts +542 -0
- package/apps/studio/generation/adapters/groq.test.ts +80 -0
- package/apps/studio/generation/adapters/groq.ts +247 -0
- package/apps/studio/generation/audio-library.test.ts +95 -0
- package/apps/studio/generation/audio-library.ts +156 -0
- package/apps/studio/generation/captions.test.ts +78 -0
- package/apps/studio/generation/captions.ts +132 -0
- package/apps/studio/generation/download.ts +76 -0
- package/apps/studio/generation/elevenlabs.test.ts +223 -0
- package/apps/studio/generation/gemini.test.ts +350 -0
- package/apps/studio/generation/jobs.test.ts +128 -0
- package/apps/studio/generation/jobs.ts +243 -0
- package/apps/studio/generation/keys.test.ts +75 -0
- package/apps/studio/generation/keys.ts +162 -0
- package/apps/studio/generation/prefs.test.ts +95 -0
- package/apps/studio/generation/prefs.ts +82 -0
- package/apps/studio/generation/registry.ts +88 -0
- package/apps/studio/generation/types.test.ts +123 -0
- package/apps/studio/generation/types.ts +266 -0
- package/apps/studio/generation/whisper-models.test.ts +152 -0
- package/apps/studio/generation/whisper-models.ts +257 -0
- package/apps/studio/generation/whisper-spike-results.md +61 -0
- package/apps/studio/git/service.ts +3 -1
- package/apps/studio/http.ts +867 -9
- package/apps/studio/media/intro.mp4 +0 -0
- package/apps/studio/media-commit-chain.ts +119 -0
- package/apps/studio/paths.ts +10 -0
- package/apps/studio/readiness.ts +108 -20
- package/apps/studio/scaffold-design.ts +1145 -0
- package/apps/studio/server.ts +13 -1
- package/apps/studio/test/_helpers.ts +15 -2
- package/apps/studio/test/acp-bridge.test.ts +34 -1
- package/apps/studio/test/acp-env.test.ts +30 -0
- package/apps/studio/test/acp-plugin-bootstrap.test.ts +23 -57
- package/apps/studio/test/acp-session-plugins.test.ts +72 -1
- package/apps/studio/test/annotation-strokes-command.test.ts +17 -0
- package/apps/studio/test/annotations-layer.test.ts +174 -0
- package/apps/studio/test/canvas-origin-gate.test.ts +37 -0
- package/apps/studio/test/csp-canvas-shell.test.ts +11 -0
- package/apps/studio/test/csrf-write-guard.test.ts +23 -1
- package/apps/studio/test/design-setup-readiness.test.ts +155 -0
- package/apps/studio/test/element-structural-edit.test.ts +108 -0
- package/apps/studio/test/fixtures/fake-claude-auth.mjs +13 -0
- package/apps/studio/test/generate-route.test.ts +106 -0
- package/apps/studio/test/import-asset-browser.test.ts +64 -0
- package/apps/studio/test/import-asset-route.test.ts +71 -0
- package/apps/studio/test/import-asset.test.ts +399 -0
- package/apps/studio/test/import-brand-browser.test.ts +88 -0
- package/apps/studio/test/import-brand-route.test.ts +120 -0
- package/apps/studio/test/import-brand.test.ts +206 -0
- package/apps/studio/test/import-tokens.test.ts +722 -0
- package/apps/studio/test/media-commit-chain.test.ts +210 -0
- package/apps/studio/test/photo-bg-remove-validation.test.ts +71 -0
- package/apps/studio/test/readiness.test.ts +13 -11
- package/apps/studio/test/scaffold-design.test.ts +122 -0
- package/apps/studio/test/tour-overlay.test.tsx +16 -0
- package/apps/studio/tool-palette.tsx +11 -6
- package/apps/studio/ui-prefs.ts +130 -0
- package/apps/studio/use-canvas-media-drop.tsx +4 -2
- package/apps/studio/use-chrome-visibility.tsx +8 -2
- package/apps/studio/whats-new.json +96 -0
- package/apps/studio/ws.ts +5 -0
- package/cli/bin/maude.mjs +6 -6
- package/cli/commands/design.mjs +33 -0
- package/cli/lib/gitignore-block.mjs +1 -0
- package/cli/lib/pkg-root.mjs +107 -0
- package/cli/lib/pkg-root.test.mjs +79 -0
- package/cli/lib/reconstruct-toolset.test.mjs +194 -0
- package/cli/lib/update-check.mjs +8 -0
- package/package.json +12 -10
- package/plugins/design/dependencies.json +18 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
// generation/whisper-models.ts — managed local whisper.cpp GGML models (Task 2.7,
|
|
2
|
+
// approach A, DDR-164). The "one-click local subtitles" story WITHOUT a heavy
|
|
3
|
+
// WASM dependency: keep the fast native whisper.cpp engine, but remove the
|
|
4
|
+
// hand-fetch-a-ggml-from-Hugging-Face friction the owner hit in testing — a
|
|
5
|
+
// Settings button downloads a model into a Maude-managed, gitignored cache and
|
|
6
|
+
// `maude design transcribe --provider whisper` auto-resolves it (no --model).
|
|
7
|
+
//
|
|
8
|
+
// The whisper.cpp BINARY is still a soft dep (brew / build); this closes the
|
|
9
|
+
// MODEL half of the friction. Models live OUTSIDE any served project tree (a
|
|
10
|
+
// per-machine cache, re-downloadable), never in `.design/`, never committed.
|
|
11
|
+
//
|
|
12
|
+
// This module owns the model REGISTRY + dir + list/resolve (fs, server-side).
|
|
13
|
+
// The actual download (streamed, SSRF-hardened, progress-tracked) lives in the
|
|
14
|
+
// http route so the egress discipline sits next to the other provider egress.
|
|
15
|
+
|
|
16
|
+
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
17
|
+
import { mkdir, rename, rm } from 'node:fs/promises';
|
|
18
|
+
import { homedir } from 'node:os';
|
|
19
|
+
import { join } from 'node:path';
|
|
20
|
+
|
|
21
|
+
export interface WhisperModelDescriptor {
|
|
22
|
+
/** Stable id used by the config + route (`base`, `base.en`, …). */
|
|
23
|
+
id: string;
|
|
24
|
+
/** The on-disk ggml filename. */
|
|
25
|
+
file: string;
|
|
26
|
+
label: string;
|
|
27
|
+
/** Approximate download size, for the consent copy + the download size cap. */
|
|
28
|
+
sizeMB: number;
|
|
29
|
+
/** Multilingual vs English-only (the owner's Czech-footage gotcha: `.en`
|
|
30
|
+
* garbles non-English — surface this in the picker). */
|
|
31
|
+
multilingual: boolean;
|
|
32
|
+
note: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// The subset of ggerganov/whisper.cpp ggml models Maude offers. Kept small +
|
|
36
|
+
// curated (a fixed allowlist — the download URL is derived from `file`, never
|
|
37
|
+
// user input, so this is also the SSRF allowlist for the model host).
|
|
38
|
+
export const WHISPER_MODELS: readonly WhisperModelDescriptor[] = [
|
|
39
|
+
{
|
|
40
|
+
id: 'tiny',
|
|
41
|
+
file: 'ggml-tiny.bin',
|
|
42
|
+
label: 'Tiny (multilingual)',
|
|
43
|
+
sizeMB: 75,
|
|
44
|
+
multilingual: true,
|
|
45
|
+
note: 'Fastest, lowest accuracy. Good for a quick draft in any language.',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: 'base',
|
|
49
|
+
file: 'ggml-base.bin',
|
|
50
|
+
label: 'Base (multilingual)',
|
|
51
|
+
sizeMB: 142,
|
|
52
|
+
multilingual: true,
|
|
53
|
+
note: 'The recommended default — works in any language (incl. Czech). Modest accuracy.',
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: 'base.en',
|
|
57
|
+
file: 'ggml-base.en.bin',
|
|
58
|
+
label: 'Base (English-only)',
|
|
59
|
+
sizeMB: 142,
|
|
60
|
+
multilingual: false,
|
|
61
|
+
note: 'English audio only — do NOT use for other languages (it garbles them).',
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: 'small',
|
|
65
|
+
file: 'ggml-small.bin',
|
|
66
|
+
label: 'Small (multilingual)',
|
|
67
|
+
sizeMB: 466,
|
|
68
|
+
multilingual: true,
|
|
69
|
+
note: 'Noticeably more accurate than base, ~3× larger.',
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: 'large-v3-turbo',
|
|
73
|
+
file: 'ggml-large-v3-turbo.bin',
|
|
74
|
+
label: 'Large v3 Turbo (multilingual)',
|
|
75
|
+
sizeMB: 1560,
|
|
76
|
+
multilingual: true,
|
|
77
|
+
note: 'Best accuracy, ~1.5 GB. Slower and disk-heavy, but close to cloud quality.',
|
|
78
|
+
},
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
const MODEL_HOST = 'https://huggingface.co';
|
|
82
|
+
const MODEL_REPO_PATH = '/ggerganov/whisper.cpp/resolve/main';
|
|
83
|
+
|
|
84
|
+
/** The fixed, non-interpolated download URL for a model (SSRF-safe — the file
|
|
85
|
+
* comes from the frozen registry above, never from a request). */
|
|
86
|
+
export function whisperModelUrl(m: WhisperModelDescriptor): string {
|
|
87
|
+
return `${MODEL_HOST}${MODEL_REPO_PATH}/${m.file}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function getWhisperModel(id: unknown): WhisperModelDescriptor | null {
|
|
91
|
+
return WHISPER_MODELS.find((m) => m.id === id) ?? null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Maude-managed model cache dir — per-machine, gitignored, never in a project.
|
|
95
|
+
* XDG_CACHE_HOME-aware (falls back to ~/.cache), mirroring keys.ts's XDG logic
|
|
96
|
+
* but a CACHE location (large, re-downloadable) not a config one. */
|
|
97
|
+
export function whisperModelsDir(): string {
|
|
98
|
+
const xdg = process.env.XDG_CACHE_HOME;
|
|
99
|
+
const base = xdg && xdg.length > 0 ? xdg : join(homedir(), '.cache');
|
|
100
|
+
return join(base, 'maude', 'whisper-models');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface WhisperModelStatus extends WhisperModelDescriptor {
|
|
104
|
+
downloaded: boolean;
|
|
105
|
+
/** Absolute path when downloaded. */
|
|
106
|
+
path?: string;
|
|
107
|
+
/** Actual on-disk bytes when downloaded (for the "Remove" affordance). */
|
|
108
|
+
bytes?: number;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** The absolute on-disk path a model WOULD live at (whether present or not). */
|
|
112
|
+
export function whisperModelPath(m: WhisperModelDescriptor): string {
|
|
113
|
+
return join(whisperModelsDir(), m.file);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Every registered model + whether it's already downloaded. */
|
|
117
|
+
export function listWhisperModels(): WhisperModelStatus[] {
|
|
118
|
+
return WHISPER_MODELS.map((m) => {
|
|
119
|
+
const p = whisperModelPath(m);
|
|
120
|
+
let downloaded = false;
|
|
121
|
+
let bytes: number | undefined;
|
|
122
|
+
try {
|
|
123
|
+
const st = statSync(p);
|
|
124
|
+
if (st.isFile() && st.size > 0) {
|
|
125
|
+
downloaded = true;
|
|
126
|
+
bytes = st.size;
|
|
127
|
+
}
|
|
128
|
+
} catch {
|
|
129
|
+
/* not downloaded */
|
|
130
|
+
}
|
|
131
|
+
return { ...m, downloaded, ...(downloaded ? { path: p, bytes } : {}) };
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Resolve a downloaded model's path for `maude design transcribe` (no --model
|
|
137
|
+
* needed once one is downloaded). Preference order: the caller-preferred id
|
|
138
|
+
* (from `generation.transcription.whisperModel`) → any downloaded MULTILINGUAL
|
|
139
|
+
* model (safe for non-English) → any downloaded model → null.
|
|
140
|
+
*/
|
|
141
|
+
export function resolveWhisperModel(preferId?: string): string | null {
|
|
142
|
+
const dir = whisperModelsDir();
|
|
143
|
+
if (!existsSync(dir)) return null;
|
|
144
|
+
let present: Set<string>;
|
|
145
|
+
try {
|
|
146
|
+
present = new Set(readdirSync(dir));
|
|
147
|
+
} catch {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
const has = (m: WhisperModelDescriptor) => present.has(m.file);
|
|
151
|
+
if (preferId) {
|
|
152
|
+
const pref = getWhisperModel(preferId);
|
|
153
|
+
if (pref && has(pref)) return join(dir, pref.file);
|
|
154
|
+
}
|
|
155
|
+
const multi = WHISPER_MODELS.find((m) => m.multilingual && has(m));
|
|
156
|
+
if (multi) return join(dir, multi.file);
|
|
157
|
+
const any = WHISPER_MODELS.find(has);
|
|
158
|
+
return any ? join(dir, any.file) : null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Download one model into the managed cache, streamed with a progress callback
|
|
163
|
+
* (Task 2.7). Egress discipline: the URL is derived from the FROZEN registry
|
|
164
|
+
* (initial host is always huggingface.co, asserted https + fixed host); the
|
|
165
|
+
* redirect the HF blob store issues is followed but the FINAL hop is re-asserted
|
|
166
|
+
* https + a `*.huggingface.co` host (ethical-hacker Finding 2); the body is
|
|
167
|
+
* size-capped per-chunk to the model's expected size (+20% margin) before it can
|
|
168
|
+
* fill the disk; written to a `.part` temp and atomically renamed so a
|
|
169
|
+
* partial/aborted download never looks complete. Pass an AbortSignal (a timeout)
|
|
170
|
+
* so a stalled connection can't wedge the single download slot. Not a full
|
|
171
|
+
* `_fetch-asset.mjs` (no resolved-IP private-range guard) — proportionate because
|
|
172
|
+
* the host is pinned to HF and the route is loopback + same-origin only. Throws
|
|
173
|
+
* on any failure (the temp file is cleaned up); the caller owns progress state.
|
|
174
|
+
*/
|
|
175
|
+
export async function downloadWhisperModel(
|
|
176
|
+
id: string,
|
|
177
|
+
onProgress: (received: number, total: number) => void,
|
|
178
|
+
signal?: AbortSignal
|
|
179
|
+
): Promise<string> {
|
|
180
|
+
const m = getWhisperModel(id);
|
|
181
|
+
if (!m) throw new Error(`unknown whisper model: ${id}`);
|
|
182
|
+
const url = whisperModelUrl(m);
|
|
183
|
+
const u = new URL(url);
|
|
184
|
+
if (u.protocol !== 'https:') throw new Error('model URL must be https');
|
|
185
|
+
if (u.hostname !== 'huggingface.co') throw new Error('model host not allowlisted');
|
|
186
|
+
|
|
187
|
+
const dir = whisperModelsDir();
|
|
188
|
+
await mkdir(dir, { recursive: true });
|
|
189
|
+
const finalPath = join(dir, m.file);
|
|
190
|
+
const tmpPath = `${finalPath}.part`;
|
|
191
|
+
|
|
192
|
+
const cap = Math.ceil(m.sizeMB * 1.2) * 1024 * 1024; // expected size + 20% margin
|
|
193
|
+
// huggingface.co 302-redirects model blobs to its own LFS CDN, so we must
|
|
194
|
+
// follow — but re-assert the FINAL hop is still https + a huggingface.co host
|
|
195
|
+
// (ethical-hacker Finding 2): an open-redirect/on-path must not land the fetch
|
|
196
|
+
// on an arbitrary host. A wall-clock timeout is layered by the caller's signal.
|
|
197
|
+
const res = await fetch(url, { signal, redirect: 'follow' });
|
|
198
|
+
try {
|
|
199
|
+
const finalUrl = new URL(res.url || url);
|
|
200
|
+
if (finalUrl.protocol !== 'https:' || !/(^|\.)huggingface\.co$/.test(finalUrl.hostname))
|
|
201
|
+
throw new Error(`model download redirected off huggingface.co (${finalUrl.hostname})`);
|
|
202
|
+
} catch (err) {
|
|
203
|
+
if (err instanceof Error && err.message.startsWith('model download redirected')) throw err;
|
|
204
|
+
throw new Error('model download resolved to an invalid URL');
|
|
205
|
+
}
|
|
206
|
+
if (!res.ok) throw new Error(`model download failed: HTTP ${res.status}`);
|
|
207
|
+
const declared = Number(res.headers.get('content-length'));
|
|
208
|
+
if (Number.isFinite(declared) && declared > cap)
|
|
209
|
+
throw new Error(`model larger than expected (${declared} > ${cap} bytes)`);
|
|
210
|
+
const total = Number.isFinite(declared) && declared > 0 ? declared : m.sizeMB * 1024 * 1024;
|
|
211
|
+
|
|
212
|
+
const body = res.body;
|
|
213
|
+
if (!body) throw new Error('empty model response');
|
|
214
|
+
const reader = body.getReader();
|
|
215
|
+
const writer = Bun.file(tmpPath).writer();
|
|
216
|
+
let received = 0;
|
|
217
|
+
try {
|
|
218
|
+
for (;;) {
|
|
219
|
+
const { done, value } = await reader.read();
|
|
220
|
+
if (done) break;
|
|
221
|
+
if (value) {
|
|
222
|
+
received += value.byteLength;
|
|
223
|
+
if (received > cap) throw new Error(`model exceeded ${cap} bytes`);
|
|
224
|
+
writer.write(value);
|
|
225
|
+
onProgress(received, total);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
await writer.end();
|
|
229
|
+
if (received === 0) throw new Error('model download was empty');
|
|
230
|
+
await rename(tmpPath, finalPath);
|
|
231
|
+
return finalPath;
|
|
232
|
+
} catch (err) {
|
|
233
|
+
try {
|
|
234
|
+
await writer.end();
|
|
235
|
+
} catch {
|
|
236
|
+
/* ignore */
|
|
237
|
+
}
|
|
238
|
+
await rm(tmpPath, { force: true }).catch(() => {});
|
|
239
|
+
throw err;
|
|
240
|
+
} finally {
|
|
241
|
+
try {
|
|
242
|
+
reader.releaseLock();
|
|
243
|
+
} catch {
|
|
244
|
+
/* already released */
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Remove a downloaded model (reclaim disk). Returns true if a file was removed. */
|
|
250
|
+
export async function removeWhisperModel(id: string): Promise<boolean> {
|
|
251
|
+
const m = getWhisperModel(id);
|
|
252
|
+
if (!m) return false;
|
|
253
|
+
const p = whisperModelPath(m);
|
|
254
|
+
if (!existsSync(p)) return false;
|
|
255
|
+
await rm(p, { force: true });
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Task 2.7 feasibility spike — transformers.js whisper (word timestamps in the sidecar)
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-07-11 · **Spike verdict: B is functionally feasible.** · **Build outcome: approach A shipped** (B blocked on a native-dep / DDR-070 conflict — see the resolution note at the bottom).
|
|
4
|
+
|
|
5
|
+
The plan gated the one-click local-subtitle build on a spike (DDR-164 Open-Question 7):
|
|
6
|
+
in-process transformers.js whisper must produce word-level timestamps, at tolerable
|
|
7
|
+
speed + memory, running MAIN-origin / in the sidecar (not the untrusted canvas realm).
|
|
8
|
+
|
|
9
|
+
## Result (Node sidecar, `@huggingface/transformers` v3, `onnx-community/whisper-base_timestamped`, q8)
|
|
10
|
+
|
|
11
|
+
| Criterion | Measured | Gate |
|
|
12
|
+
| --- | --- | --- |
|
|
13
|
+
| Word-level timestamps | `hasWordTimestamps: true` — `"And" [0.22–0.52] "so" [0.52–0.84] "my" [0.84–1.2]…` | ✅ |
|
|
14
|
+
| Transcript accuracy | "And so my fellow Americans ask not what your country can do for you…" (JFK 11s clip, verbatim) | ✅ |
|
|
15
|
+
| Speed | 2.66× realtime (4.14 s for 11.0 s audio) | ✅ tolerable; show an ETA for long clips |
|
|
16
|
+
| Memory | ~676 MB peak RSS | ✅ fits |
|
|
17
|
+
| In-process, main-origin | Node/Bun via onnxruntime-node — no browser CSP surface | ✅ |
|
|
18
|
+
|
|
19
|
+
## Build notes (feed the DDR + the implementation)
|
|
20
|
+
|
|
21
|
+
1. **Must use a `_timestamped` model variant.** Plain `onnx-community/whisper-base`
|
|
22
|
+
throws `Model outputs must contain cross attentions to extract timestamps` —
|
|
23
|
+
word timestamps need the cross-attention export. Use `onnx-community/whisper-*_timestamped`.
|
|
24
|
+
2. **Model size default.** `base` (q8) is the sweet spot for the WASM/sidecar default
|
|
25
|
+
(accuracy vs download size vs the 2.66× speed). Offer `tiny`/`small` in the size picker.
|
|
26
|
+
3. **onnxruntime-node teardown quirk.** A cosmetic `mutex lock failed` SIGABRT fires on
|
|
27
|
+
process exit AFTER all output is produced — the shim must `process.exit(0)` cleanly
|
|
28
|
+
once the SRT is written (the inference itself is unaffected).
|
|
29
|
+
4. **Packaging fork (the genuinely consequential decision — DDR-worthy).** transformers.js
|
|
30
|
+
in Node defaults to **onnxruntime-node** (native `.node` binaries) — fast, but a heavy
|
|
31
|
+
per-platform native dep that fights the `bun --compile` standalone-binary distribution
|
|
32
|
+
(DDR-045) and bloats the npm tarball. Alternatives that avoid that: (a) transformers.js
|
|
33
|
+
with **onnxruntime-web (WASM)** in the browser proof-canvas harness — mirrors the shipped
|
|
34
|
+
`@imgly/background-removal` precedent (DDR-161) exactly, no native dep, but slower and
|
|
35
|
+
in a browser context; (b) native **whisper.cpp + a managed model-download button** (the
|
|
36
|
+
plan's fallback A) — fastest, but needs the binary. This fork is surfaced to the owner
|
|
37
|
+
before the heavy dep ships to every user.
|
|
38
|
+
|
|
39
|
+
## Repro
|
|
40
|
+
|
|
41
|
+
`scratchpad/whisper-spike/spike3.mjs` (stereo→mono + 44.1k→16k resample; the earlier
|
|
42
|
+
garbled "[BIRDS CHIRPING]" run was a WAV-parse bug in the harness, not the model).
|
|
43
|
+
|
|
44
|
+
## Resolution (2026-07-11) — shipped approach A, not B
|
|
45
|
+
|
|
46
|
+
Building B revealed the packaging fork wasn't a preference but a **blocker**:
|
|
47
|
+
`@huggingface/transformers@3.8.1` hard-depends on **`sharp` + `onnxruntime-node`**,
|
|
48
|
+
the exact native "bun-compile-hostile" class **DDR-070 excludes**. A clean `npm add`
|
|
49
|
+
is impossible; B would require vendoring the prebuilt browser bundle + allowlisting
|
|
50
|
+
`cdn.jsdelivr.net` (WASM) and `huggingface.co` (models) in the canvas CSP — a
|
|
51
|
+
DDR-worthy vendoring + CDN commitment. `device:'wasm'` is also unsupported in the
|
|
52
|
+
transformers.js **Node** build (only `cpu` = onnxruntime-node), so sidecar-WASM is
|
|
53
|
+
out — B would have to run in a browser proof-canvas harness (the @imgly pattern).
|
|
54
|
+
|
|
55
|
+
**Owner chose approach A:** keep the fast native whisper.cpp engine (already validated
|
|
56
|
+
on real footage) and remove the *friction* instead — a one-click managed model
|
|
57
|
+
download (`generation/whisper-models.ts` + `/_api/generate/whisper-model` + a Settings
|
|
58
|
+
card), `--provider whisper` model auto-resolution, and auto-ffmpeg container decode.
|
|
59
|
+
No new runtime dependency; native binary still required (brew / build). True
|
|
60
|
+
binary-free zero-install (B) stays a documented future option behind the vendoring
|
|
61
|
+
decision.
|
|
@@ -209,7 +209,9 @@ function underPrefix(filepath: string, prefix: string): boolean {
|
|
|
209
209
|
* double-transports through git. */
|
|
210
210
|
function isMaudeRuntimeState(p: string): boolean {
|
|
211
211
|
return (
|
|
212
|
-
/(^|\/)_(?:server|active|sync|preflight|locator|export-history)\.json$/.test(
|
|
212
|
+
/(^|\/)_(?:server|active|sync|preflight|locator|export-history|generate-history)\.json$/.test(
|
|
213
|
+
p
|
|
214
|
+
) ||
|
|
213
215
|
/(^|\/)_server\.(?:lock|log)$/.test(p) ||
|
|
214
216
|
/(^|\/)_(?:history|trash|draw|photo|smoke|canvas-state|state|chat|comments|untrusted|export-jobs)(?:\/|$)/.test(
|
|
215
217
|
p
|