runntime 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -4
- package/dist/{classifier-ByH6OI6h.mjs → classifier-C6ZR3cyJ.mjs} +38576 -22654
- package/dist/{config-UyxIidZc.d.mts → config-DZyZ2TdH.d.mts} +21 -8
- package/dist/zoo/index.d.mts +206 -63
- package/dist/zoo/index.mjs +2217 -473
- package/dist/zoo/transformers/index.d.mts +10 -10
- package/dist/zoo/transformers/index.mjs +14 -16
- package/package.json +13 -5
package/dist/zoo/index.mjs
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { $ as
|
|
1
|
+
import { $ as derivedTensor, A as EMBEDDING_DIM$1, At as defaultRoot, B as Conv1d, C as createDetector, Ct as clamp$3, Dt as createMapToTexture, E as MinilmModel, Et as silu, F as messageOf, G as RMSNorm, H as LayerNorm, I as rethrowRunError, J as ModuleList, K as CpuParameter, L as models, M as asLoadError, Mt as RUNNTIME_ERROR_CODES, N as checkpointMismatch, Nt as RunntimeError, O as createMinilmEmbedCore, Ot as createImageToChw, P as isAbort, Pt as isRunntimeError, Q as bufferSource, R as createOpfsCache, St as asinh, Tt as mul, U as Linear, V as Embedding, W as MultiHeadAttention, Y as Parameter, Z as warmUp, _ as buildRopeTables, _t as reshape, a as MOBILENETV4_CROP_FRACTION, at as toArray, bt as writeRows, c as createDetector$1, ct as softmax, d as move, dt as sdpa, et as fromSafetensors, f as moveTiedEmbedding, ft as argmaxDot, g as MoonshineDecoder, gt as gatherRowsFrom, h as transformDecoder, ht as chunk, it as initRunntime, j as MINILM_L6, jt as supportsF16, kt as tensor, l as createTranscriber$1, lt as localAttention, m as take, mt as cat, n as IMAGENET_MEAN, nt as evalValues, o as DEPTHART_CONFIGS, ot as gluConv1d, p as qualify, pt as matmul, q as Module, r as IMAGENET_STD, rt as gpuExecutor, s as createEstimator, st as rmsNorm, t as createClassifier, u as fuseQkv, ut as rope, v as splitIntoChunks, vt as slice, wt as gelu, xt as add, yt as transpose, z as createReplayCache } from "../classifier-C6ZR3cyJ.mjs";
|
|
2
|
+
import { d, isBuffer, isTexture } from "typegpu";
|
|
3
|
+
import { Tokenizer } from "@huggingface/tokenizers";
|
|
2
4
|
//#region src/core/lifetime.ts
|
|
3
5
|
function createResourceScope() {
|
|
4
6
|
const tracked = [];
|
|
@@ -21,12 +23,13 @@ function createResourceScope() {
|
|
|
21
23
|
//#region src/zoo/load.ts
|
|
22
24
|
/** Loading helpers shared by every create<Task>() factory: one options
|
|
23
25
|
* shape, one way to open a weights file, one way to fetch a JSON asset. */
|
|
24
|
-
/** Throws when the signal is already aborted. */
|
|
26
|
+
/** Throws LOAD_ABORTED when the signal is already aborted. */
|
|
25
27
|
function throwIfAborted(signal) {
|
|
26
|
-
if (signal?.aborted) throw new
|
|
28
|
+
if (signal?.aborted) throw new RunntimeError("LOAD_ABORTED", "model load aborted", { cause: signal.reason });
|
|
27
29
|
}
|
|
28
30
|
/** Opens a safetensors file and reads its header. Tensor bytes download
|
|
29
|
-
* later,
|
|
31
|
+
* later, in loadStateDict. Fails with LOAD_FAILED, naming the file, or
|
|
32
|
+
* LOAD_ABORTED. */
|
|
30
33
|
async function openWeights(modelPath, opts = {}) {
|
|
31
34
|
throwIfAborted(opts.signal);
|
|
32
35
|
try {
|
|
@@ -36,13 +39,14 @@ async function openWeights(modelPath, opts = {}) {
|
|
|
36
39
|
cacheId: opts.cacheId
|
|
37
40
|
});
|
|
38
41
|
} catch (err) {
|
|
39
|
-
|
|
42
|
+
if (isRunntimeError(err)) throw err;
|
|
43
|
+
throw new RunntimeError("LOAD_FAILED", `${typeof modelPath === "string" ? modelPath : "weights"}: ${messageOf(err)}`, { cause: err });
|
|
40
44
|
}
|
|
41
45
|
}
|
|
42
46
|
/** Downloads and parses a JSON file (a tokenizer, a config). With a cache,
|
|
43
|
-
* the bytes are saved under `cacheId` (the file name by default) and
|
|
44
|
-
* next load
|
|
45
|
-
*
|
|
47
|
+
* the bytes are saved under `cacheId` (the file name by default) and reused
|
|
48
|
+
* on the next load. Fails with LOAD_FAILED, naming the URL, or
|
|
49
|
+
* LOAD_ABORTED. */
|
|
46
50
|
async function fetchJson(url, opts = {}) {
|
|
47
51
|
throwIfAborted(opts.signal);
|
|
48
52
|
const download = async () => {
|
|
@@ -50,138 +54,1946 @@ async function fetchJson(url, opts = {}) {
|
|
|
50
54
|
try {
|
|
51
55
|
res = await fetch(url, { signal: opts.signal });
|
|
52
56
|
} catch (err) {
|
|
53
|
-
|
|
57
|
+
if (opts.signal?.aborted || isAbort(err)) throw asLoadError(err, opts.signal);
|
|
58
|
+
throw new RunntimeError("LOAD_FAILED", `${url}: ${messageOf(err)}`, { cause: err });
|
|
54
59
|
}
|
|
55
|
-
if (!res.ok) throw new
|
|
60
|
+
if (!res.ok) throw new RunntimeError("LOAD_FAILED", `${url}: HTTP ${res.status}`);
|
|
56
61
|
return new Uint8Array(await res.arrayBuffer());
|
|
57
62
|
};
|
|
58
63
|
const bytes = opts.cache ? await opts.cache.getOrCompute(`json/${opts.cacheId ?? url.split("/").pop()}`, download) : await download();
|
|
59
64
|
try {
|
|
60
65
|
return JSON.parse(new TextDecoder().decode(bytes));
|
|
61
66
|
} catch (err) {
|
|
62
|
-
throw new
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
67
|
+
throw new RunntimeError("LOAD_FAILED", `${url}: not valid JSON`, { cause: err });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
//#endregion
|
|
71
|
+
//#region src/zoo/tasks/audio/audioInput.ts
|
|
72
|
+
/** Audio input helpers: an encoded clip or raw microphone samples in, the
|
|
73
|
+
* 16 kHz mono Float32Array the speech models take out. Both use the
|
|
74
|
+
* browser's own decoder and resampler through an OfflineAudioContext, so
|
|
75
|
+
* they run in the browser only. Importing the file is safe anywhere, the
|
|
76
|
+
* browser objects appear only inside the functions. */
|
|
77
|
+
/** Sample rate the speech models take. */
|
|
78
|
+
const SPEECH_SAMPLE_RATE = 16e3;
|
|
79
|
+
/** Decodes any clip the browser can play (wav, mp3, ogg, ...) to 16 kHz
|
|
80
|
+
* mono samples. Stereo is mixed down to one channel. */
|
|
81
|
+
async function decodeAudio(data) {
|
|
82
|
+
const decoded = await new OfflineAudioContext(1, 1, SPEECH_SAMPLE_RATE).decodeAudioData(data);
|
|
83
|
+
if (decoded.numberOfChannels === 1) return decoded.getChannelData(0);
|
|
84
|
+
const mix = new OfflineAudioContext(1, decoded.length, SPEECH_SAMPLE_RATE);
|
|
85
|
+
const src = mix.createBufferSource();
|
|
86
|
+
src.buffer = decoded;
|
|
87
|
+
src.connect(mix.destination);
|
|
88
|
+
src.start();
|
|
89
|
+
return (await mix.startRendering()).getChannelData(0);
|
|
90
|
+
}
|
|
91
|
+
/** Resamples raw mono samples recorded at `sourceRate` to 16 kHz. Returns
|
|
92
|
+
* the input as is when it is already at 16 kHz. Use it on microphone
|
|
93
|
+
* samples, a lossy re-encode is not needed. */
|
|
94
|
+
async function resampleAudio(samples, sourceRate) {
|
|
95
|
+
if (sourceRate === 16e3) return samples;
|
|
96
|
+
const frames = Math.ceil(samples.length * SPEECH_SAMPLE_RATE / sourceRate);
|
|
97
|
+
const off = new OfflineAudioContext(1, frames, SPEECH_SAMPLE_RATE);
|
|
98
|
+
const buf = off.createBuffer(1, samples.length, sourceRate);
|
|
99
|
+
buf.copyToChannel(samples, 0);
|
|
100
|
+
const src = off.createBufferSource();
|
|
101
|
+
src.buffer = buf;
|
|
102
|
+
src.connect(off.destination);
|
|
103
|
+
src.start();
|
|
104
|
+
return (await off.startRendering()).getChannelData(0);
|
|
105
|
+
}
|
|
106
|
+
//#endregion
|
|
107
|
+
//#region src/zoo/embeddinggemma2/audioEncoder.ts
|
|
108
|
+
/** EmbeddingGemma 2 audio encoding: 16 kHz samples in, their token rows for
|
|
109
|
+
* the text model out, one eager forward of the audio tower per clip. */
|
|
110
|
+
/** 20 ms frames every 10 ms, each zero-padded to the FFT size. */
|
|
111
|
+
const FRAME = 320;
|
|
112
|
+
const HOP = 160;
|
|
113
|
+
const FFT = 512;
|
|
114
|
+
const MEL_FLOOR = .001;
|
|
115
|
+
const MAX_FREQUENCY = 8e3;
|
|
116
|
+
/** Only the first 30 seconds of a clip count. */
|
|
117
|
+
const MAX_SAMPLES = 30 * SPEECH_SAMPLE_RATE;
|
|
118
|
+
/** Fewest samples that make one frame, about 10 ms. */
|
|
119
|
+
const MIN_SAMPLES = 161;
|
|
120
|
+
/** Mel frames of a clip of at least MIN_SAMPLES samples. The first frame is
|
|
121
|
+
* centered on sample 0. */
|
|
122
|
+
const frameCount = (samples) => Math.floor((Math.min(samples, MAX_SAMPLES) - MIN_SAMPLES) / HOP) + 1;
|
|
123
|
+
/** Tokens a clip of `samples` samples takes: each conv halves the frames,
|
|
124
|
+
* rounding up. Throws INVALID_ARGUMENT for a clip too short for one frame. */
|
|
125
|
+
function audioTokens(samples) {
|
|
126
|
+
if (samples < MIN_SAMPLES) throw new RunntimeError("INVALID_ARGUMENT", `audio: a clip needs at least ${MIN_SAMPLES} samples, got ${samples}`);
|
|
127
|
+
return Math.ceil(Math.ceil(frameCount(samples) / 2) / 2);
|
|
128
|
+
}
|
|
129
|
+
let bank;
|
|
130
|
+
/** Periodic Hann window, HTK mel triangles from 0 Hz to 8 kHz with no area
|
|
131
|
+
* normalization, and the FFT's tables. */
|
|
132
|
+
function melBank(mels) {
|
|
133
|
+
if (bank?.filters.length === mels) return bank;
|
|
134
|
+
const window = new Float32Array(FRAME);
|
|
135
|
+
for (let n = 0; n < FRAME; n++) window[n] = .5 - .5 * Math.cos(2 * Math.PI * n / FRAME);
|
|
136
|
+
const bins = 257;
|
|
137
|
+
const mel = (f) => 2595 * Math.log10(1 + f / 700);
|
|
138
|
+
const hz = (m) => 700 * (10 ** (m / 2595) - 1);
|
|
139
|
+
const top = mel(MAX_FREQUENCY);
|
|
140
|
+
const edge = (i) => hz(top * i / (mels + 1));
|
|
141
|
+
const filters = Array.from({ length: mels }, (_, m) => {
|
|
142
|
+
const [lo, mid, hi] = [
|
|
143
|
+
edge(m),
|
|
144
|
+
edge(m + 1),
|
|
145
|
+
edge(m + 2)
|
|
146
|
+
];
|
|
147
|
+
const w = new Float64Array(bins);
|
|
148
|
+
let start = bins;
|
|
149
|
+
let end = 0;
|
|
150
|
+
for (let k = 0; k < bins; k++) {
|
|
151
|
+
const f = k * SPEECH_SAMPLE_RATE / 2 / 256;
|
|
152
|
+
const v = Math.max(0, Math.min((f - lo) / (mid - lo), (hi - f) / (hi - mid)));
|
|
153
|
+
if (v > 0) {
|
|
154
|
+
w[k] = v;
|
|
155
|
+
start = Math.min(start, k);
|
|
156
|
+
end = k + 1;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
start,
|
|
161
|
+
weights: w.slice(start, Math.max(start, end))
|
|
162
|
+
};
|
|
163
|
+
});
|
|
164
|
+
const order = new Uint16Array(FFT);
|
|
165
|
+
for (let i = 1, j = 0; i < FFT; i++) {
|
|
166
|
+
let bit = 256;
|
|
167
|
+
for (; j & bit; bit >>= 1) j ^= bit;
|
|
168
|
+
j ^= bit;
|
|
169
|
+
order[i] = j;
|
|
170
|
+
}
|
|
171
|
+
const cos = new Float64Array(FFT / 2);
|
|
172
|
+
const sin = new Float64Array(FFT / 2);
|
|
173
|
+
for (let k = 0; k < FFT / 2; k++) {
|
|
174
|
+
cos[k] = Math.cos(2 * Math.PI * k / FFT);
|
|
175
|
+
sin[k] = -Math.sin(2 * Math.PI * k / FFT);
|
|
176
|
+
}
|
|
177
|
+
bank = {
|
|
178
|
+
window,
|
|
179
|
+
filters,
|
|
180
|
+
order,
|
|
181
|
+
cos,
|
|
182
|
+
sin
|
|
183
|
+
};
|
|
184
|
+
return bank;
|
|
185
|
+
}
|
|
186
|
+
/** In-place radix-2 FFT of FFT points already in bit-reversed order. */
|
|
187
|
+
function fft(re, im, cos, sin) {
|
|
188
|
+
for (let size = 2; size <= FFT; size <<= 1) {
|
|
189
|
+
const half = size >> 1;
|
|
190
|
+
const step = FFT / size;
|
|
191
|
+
for (let s = 0; s < FFT; s += size) for (let k = 0; k < half; k++) {
|
|
192
|
+
const c = cos[k * step];
|
|
193
|
+
const n = sin[k * step];
|
|
194
|
+
const a = s + k;
|
|
195
|
+
const b = a + half;
|
|
196
|
+
const tr = re[b] * c - im[b] * n;
|
|
197
|
+
const ti = re[b] * n + im[b] * c;
|
|
198
|
+
re[b] = re[a] - tr;
|
|
199
|
+
im[b] = im[a] - ti;
|
|
200
|
+
re[a] = re[a] + tr;
|
|
201
|
+
im[a] = im[a] + ti;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
/** The first `frames` log-mel frames of `samples`, [frames, mels]. */
|
|
206
|
+
function logMel(samples, frames, mels) {
|
|
207
|
+
const { window, filters, order, cos, sin } = melBank(mels);
|
|
208
|
+
const out = new Float32Array(frames * mels);
|
|
209
|
+
const re = new Float64Array(FFT);
|
|
210
|
+
const im = new Float64Array(FFT);
|
|
211
|
+
const mag = /* @__PURE__ */ new Float64Array(257);
|
|
212
|
+
for (let f = 0; f < frames; f++) {
|
|
213
|
+
re.fill(0);
|
|
214
|
+
im.fill(0);
|
|
215
|
+
const first = f * HOP - FRAME / 2;
|
|
216
|
+
for (let i = Math.max(0, -first); i < FRAME; i++) re[order[i]] = Math.fround(samples[first + i] * window[i]);
|
|
217
|
+
fft(re, im, cos, sin);
|
|
218
|
+
for (let k = 0; k < mag.length; k++) mag[k] = Math.sqrt(re[k] * re[k] + im[k] * im[k]);
|
|
219
|
+
filters.forEach(({ start, weights }, m) => {
|
|
220
|
+
let s = 0;
|
|
221
|
+
for (let k = 0; k < weights.length; k++) s += mag[start + k] * weights[k];
|
|
222
|
+
out[f * mels + m] = Math.log(s + MEL_FLOOR);
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
return out;
|
|
226
|
+
}
|
|
227
|
+
/** The first conv's taps [P + 1, 9]: a 3×3 window at stride 2, padded by 1,
|
|
228
|
+
* over the [frames, mels] image, then a row of zeros. */
|
|
229
|
+
function convPatches(mel, frames, mels) {
|
|
230
|
+
const rows = Math.ceil(frames / 2);
|
|
231
|
+
const cols = mels / 2;
|
|
232
|
+
const out = new Float32Array((rows * cols + 1) * 9);
|
|
233
|
+
for (let r = 0; r < rows; r++) for (let c = 0; c < cols; c++) {
|
|
234
|
+
const o = (r * cols + c) * 9;
|
|
235
|
+
for (let kh = 0; kh < 3; kh++) {
|
|
236
|
+
const y = 2 * r - 1 + kh;
|
|
237
|
+
if (y < 0 || y >= frames) continue;
|
|
238
|
+
for (let kw = 0; kw < 3; kw++) {
|
|
239
|
+
const x = 2 * c - 1 + kw;
|
|
240
|
+
if (x >= 0 && x < mels) out[o + kh * 3 + kw] = mel[y * mels + x];
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return out;
|
|
245
|
+
}
|
|
246
|
+
/** The second conv's taps: for each of its output pixels, the 3×3 first-conv
|
|
247
|
+
* pixels it reads at stride 2, the zero row where it reads padding. */
|
|
248
|
+
function convTaps(rows, cols) {
|
|
249
|
+
const outRows = Math.ceil(rows / 2);
|
|
250
|
+
const outCols = cols / 2;
|
|
251
|
+
const zero = rows * cols;
|
|
252
|
+
const out = new Float32Array(outRows * outCols * 9);
|
|
253
|
+
let o = 0;
|
|
254
|
+
for (let r = 0; r < outRows; r++) for (let c = 0; c < outCols; c++) for (let kh = 0; kh < 3; kh++) for (let kw = 0; kw < 3; kw++) {
|
|
255
|
+
const y = 2 * r - 1 + kh;
|
|
256
|
+
const x = 2 * c - 1 + kw;
|
|
257
|
+
out[o++] = y >= 0 && y < rows && x >= 0 && x < cols ? y * cols + x : zero;
|
|
258
|
+
}
|
|
259
|
+
return out;
|
|
260
|
+
}
|
|
261
|
+
/** A clip, mono samples at 16 kHz in [-1, 1], to its token rows
|
|
262
|
+
* [tokens · text hidden]. Only the first 30 s count. Runs on `ex` and trims
|
|
263
|
+
* its pool after. Throws INVALID_ARGUMENT for a clip shorter than one
|
|
264
|
+
* frame. */
|
|
265
|
+
async function encodeAudio(model, samples, ex) {
|
|
266
|
+
const { mels } = model.cfg;
|
|
267
|
+
const tokens = audioTokens(samples.length);
|
|
268
|
+
const frames = frameCount(samples.length);
|
|
269
|
+
const act = model.input_proj.weight.shape.dtype === "f16" ? "f16" : "f32";
|
|
270
|
+
const patches = convPatches(logMel(samples, frames, mels), frames, mels);
|
|
271
|
+
const taps = convTaps(Math.ceil(frames / 2), mels / 2);
|
|
272
|
+
const inputs = {
|
|
273
|
+
patches: tensor(patches, {
|
|
274
|
+
elems: patches.length,
|
|
275
|
+
dtype: act,
|
|
276
|
+
dims: [patches.length / 9, 9]
|
|
277
|
+
}),
|
|
278
|
+
taps: tensor(taps, {
|
|
279
|
+
elems: taps.length,
|
|
280
|
+
dtype: "f32",
|
|
281
|
+
dims: [taps.length]
|
|
282
|
+
}),
|
|
283
|
+
tokens
|
|
284
|
+
};
|
|
285
|
+
try {
|
|
286
|
+
return await toArray(model.forward(inputs), ex);
|
|
287
|
+
} finally {
|
|
288
|
+
inputs.patches.buffer.destroy();
|
|
289
|
+
inputs.taps.buffer.destroy();
|
|
290
|
+
ex.trim();
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
//#endregion
|
|
294
|
+
//#region src/zoo/embeddinggemma2/config.ts
|
|
295
|
+
const EMBEDDINGGEMMA2_TEXT = {
|
|
296
|
+
vocab: 262144,
|
|
297
|
+
hidden: 512,
|
|
298
|
+
ffn: 2048,
|
|
299
|
+
layers: 24,
|
|
300
|
+
heads: 4,
|
|
301
|
+
pleDim: 512,
|
|
302
|
+
embeddingDim: 768,
|
|
303
|
+
eps: 1e-6,
|
|
304
|
+
slidingWindow: 512,
|
|
305
|
+
layerTypes: Array.from({ length: 24 }, (_, i) => i % 6 === 5 ? "full" : "sliding"),
|
|
306
|
+
attention: {
|
|
307
|
+
sliding: {
|
|
308
|
+
headDim: 256,
|
|
309
|
+
kvHeads: 2,
|
|
310
|
+
theta: 1e4
|
|
311
|
+
},
|
|
312
|
+
full: {
|
|
313
|
+
headDim: 512,
|
|
314
|
+
kvHeads: 1,
|
|
315
|
+
theta: 1e6
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
const EMBEDDING_DIM = EMBEDDINGGEMMA2_TEXT.embeddingDim;
|
|
320
|
+
/** Output sizes the model was trained to be cut to (Matryoshka). */
|
|
321
|
+
const EMBEDDING_DIMS = [
|
|
322
|
+
768,
|
|
323
|
+
512,
|
|
324
|
+
256,
|
|
325
|
+
128
|
|
326
|
+
];
|
|
327
|
+
/** Longest input in tokens, `<bos>` and `<eos>` included, image and video
|
|
328
|
+
* tokens counted. The model takes 8192, this runner 2048. */
|
|
329
|
+
const EMBEDDINGGEMMA2_MAX_TOKENS = 2048;
|
|
330
|
+
const EMBEDDINGGEMMA2_VISION = {
|
|
331
|
+
hidden: 768,
|
|
332
|
+
ffn: 3072,
|
|
333
|
+
layers: 16,
|
|
334
|
+
heads: 12,
|
|
335
|
+
headDim: 64,
|
|
336
|
+
eps: 1e-6,
|
|
337
|
+
patch: 16,
|
|
338
|
+
pool: 3,
|
|
339
|
+
positions: 10240,
|
|
340
|
+
ropeTheta: 100
|
|
341
|
+
};
|
|
342
|
+
/** Token budgets an image may take, most tokens for the most detail. */
|
|
343
|
+
const IMAGE_TOKEN_BUDGETS = [
|
|
344
|
+
70,
|
|
345
|
+
140,
|
|
346
|
+
280,
|
|
347
|
+
560,
|
|
348
|
+
1120
|
|
349
|
+
];
|
|
350
|
+
const EMBEDDINGGEMMA2_AUDIO = {
|
|
351
|
+
hidden: 1024,
|
|
352
|
+
ffn: 4096,
|
|
353
|
+
layers: 12,
|
|
354
|
+
heads: 8,
|
|
355
|
+
headDim: 128,
|
|
356
|
+
eps: 1e-6,
|
|
357
|
+
mels: 128,
|
|
358
|
+
convChannels: [128, 32],
|
|
359
|
+
window: 12,
|
|
360
|
+
logitCap: 50,
|
|
361
|
+
convKernel: 5,
|
|
362
|
+
residualWeight: .5,
|
|
363
|
+
outputDim: 1536
|
|
364
|
+
};
|
|
365
|
+
/** The instruction each task puts before the text (the checkpoint's
|
|
366
|
+
* sentence-transformers prompts). `query` and `document` are the two sides
|
|
367
|
+
* of search: queries get one, the texts searched get the other. */
|
|
368
|
+
const EMBEDDINGGEMMA2_PROMPTS = {
|
|
369
|
+
query: "task: search result | query: ",
|
|
370
|
+
document: "title: none | text: ",
|
|
371
|
+
questionAnswering: "task: question answering | query: ",
|
|
372
|
+
factChecking: "task: fact checking | query: ",
|
|
373
|
+
classification: "task: classification | query: ",
|
|
374
|
+
clustering: "task: clustering | query: ",
|
|
375
|
+
similarity: "task: sentence similarity | query: ",
|
|
376
|
+
codeRetrieval: "task: code retrieval | query: "
|
|
377
|
+
};
|
|
378
|
+
//#endregion
|
|
379
|
+
//#region src/zoo/tasks/cv/image.ts
|
|
380
|
+
/** Raw CPU image types shared by every vision task. */
|
|
381
|
+
/** Bytes per pixel for each format. */
|
|
382
|
+
const FORMAT_CHANNELS = {
|
|
383
|
+
rgb: 3,
|
|
384
|
+
rgba: 4,
|
|
385
|
+
bgr: 3,
|
|
386
|
+
bgra: 4,
|
|
387
|
+
gray: 1
|
|
388
|
+
};
|
|
389
|
+
/** Throws unless `width` and `height` are positive integers. */
|
|
390
|
+
function checkImageSize(width, height, what = "image") {
|
|
391
|
+
if (!Number.isInteger(width) || !Number.isInteger(height) || width <= 0 || height <= 0) throw new RunntimeError("INVALID_ARGUMENT", `${what}: ${width}x${height} is not a positive size`);
|
|
392
|
+
}
|
|
393
|
+
/** Throws unless `image` has a positive size and exactly the bytes its
|
|
394
|
+
* size and format need. */
|
|
395
|
+
function checkImageBuffer(image) {
|
|
396
|
+
const { data, width, height, format } = image;
|
|
397
|
+
checkImageSize(width, height);
|
|
398
|
+
const expected = width * height * FORMAT_CHANNELS[format];
|
|
399
|
+
if (data.length !== expected) throw new RunntimeError("INVALID_ARGUMENT", `image: ${data.length} bytes for ${width}x${height} ${format}, expected ${expected}`);
|
|
400
|
+
}
|
|
401
|
+
/** Wraps raw bytes as an ImageBuffer. Checks the size and the byte count. */
|
|
402
|
+
function imageBuffer(data, width, height, format = "rgba") {
|
|
403
|
+
const image = {
|
|
404
|
+
data,
|
|
405
|
+
width,
|
|
406
|
+
height,
|
|
407
|
+
format,
|
|
408
|
+
layout: "hwc"
|
|
409
|
+
};
|
|
410
|
+
checkImageBuffer(image);
|
|
411
|
+
return image;
|
|
412
|
+
}
|
|
413
|
+
/** Views canvas ImageData as an ImageBuffer. No copy. */
|
|
414
|
+
function imageBufferFromImageData(img) {
|
|
415
|
+
return {
|
|
416
|
+
data: img.data,
|
|
417
|
+
width: img.width,
|
|
418
|
+
height: img.height,
|
|
419
|
+
format: "rgba",
|
|
420
|
+
layout: "hwc"
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
//#endregion
|
|
424
|
+
//#region src/zoo/embeddinggemma2/stateDictHooks.ts
|
|
425
|
+
/** Maps the HF EmbeddingGemma2Model checkpoint onto the text model and the
|
|
426
|
+
* vision and audio towers as lazy derived tensors; nothing is fetched here.
|
|
427
|
+
* Each keeps only its own tensors, so one checkpoint with every tower loads
|
|
428
|
+
* into any of them. */
|
|
429
|
+
const SOURCE = "language_model.";
|
|
430
|
+
const VISION_SOURCES = ["vision_tower.", "embed_vision."];
|
|
431
|
+
const AUDIO_SOURCES = ["audio_tower.", "embed_audio."];
|
|
432
|
+
/** Row r of each d-row head moves so (p, p + d/2) become (2p, 2p + 1). */
|
|
433
|
+
function interleaveHeads(src, rows, cols, d) {
|
|
434
|
+
const out = new Float32Array(src.length);
|
|
435
|
+
for (let r = 0; r < rows; r++) {
|
|
436
|
+
const head = r - r % d;
|
|
437
|
+
const i = r % d;
|
|
438
|
+
const to = head + (i < d / 2 ? 2 * i : 2 * (i - d / 2) + 1);
|
|
439
|
+
out.set(src.subarray(r * cols, (r + 1) * cols), to * cols);
|
|
440
|
+
}
|
|
441
|
+
return out;
|
|
442
|
+
}
|
|
443
|
+
/** Each layer's RMSNorms, renamed `weight` → `scale`. */
|
|
444
|
+
const LAYER_NORMS = [
|
|
445
|
+
"input_layernorm",
|
|
446
|
+
"post_attention_layernorm",
|
|
447
|
+
"pre_feedforward_layernorm",
|
|
448
|
+
"post_feedforward_layernorm"
|
|
449
|
+
];
|
|
450
|
+
/** Moves the tensors under `sources` out of `sd`. take() throws for a
|
|
451
|
+
* missing one, done() for any left unused. */
|
|
452
|
+
function remap(sd, prefix, sources, what) {
|
|
453
|
+
const src = /* @__PURE__ */ new Map();
|
|
454
|
+
for (const [key, t] of sd.tensors) if (sources.some((s) => key.startsWith(s))) src.set(key, t);
|
|
455
|
+
sd.tensors.clear();
|
|
456
|
+
return {
|
|
457
|
+
take(name) {
|
|
458
|
+
const t = src.get(name);
|
|
459
|
+
if (!t) throw checkpointMismatch(what, `missing tensor '${name}'`);
|
|
460
|
+
src.delete(name);
|
|
461
|
+
return t;
|
|
462
|
+
},
|
|
463
|
+
put(name, t) {
|
|
464
|
+
sd.tensors.set(prefix ? `${prefix}.${name}` : name, t);
|
|
465
|
+
},
|
|
466
|
+
done() {
|
|
467
|
+
if (src.size) throw checkpointMismatch(what, `unused tensors: ${[...src.keys()].slice(0, 5)}`);
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
/** A weightless norm's ones. */
|
|
472
|
+
const ones = (n) => derivedTensor([n], () => Promise.resolve(new Float32Array(n).fill(1)), 0);
|
|
473
|
+
/** q, k and v in one weight. q and k rows go from the checkpoint's
|
|
474
|
+
* half-split rope layout to the kernel's interleaved pairs, per `block`
|
|
475
|
+
* rows. */
|
|
476
|
+
function fusedQkv(wq, wk, wv, qRows, kvRows, hidden, block) {
|
|
477
|
+
const width = qRows + 2 * kvRows;
|
|
478
|
+
return derivedTensor([width, hidden], async () => {
|
|
479
|
+
const out = new Float32Array(width * hidden);
|
|
480
|
+
out.set(interleaveHeads(await wq.f32(), qRows, hidden, block), 0);
|
|
481
|
+
out.set(interleaveHeads(await wk.f32(), kvRows, hidden, block), qRows * hidden);
|
|
482
|
+
out.set(await wv.f32(), (qRows + kvRows) * hidden);
|
|
483
|
+
return out;
|
|
484
|
+
}, wq.byteLength + wk.byteLength + wv.byteLength);
|
|
485
|
+
}
|
|
486
|
+
/** q_norm and k_norm tiled over their heads and interleaved like
|
|
487
|
+
* fusedQkv, ones for v. */
|
|
488
|
+
function fusedQkvNorm(nq, nk, heads, kvHeads, d, block) {
|
|
489
|
+
const width = (heads + 2 * kvHeads) * d;
|
|
490
|
+
return derivedTensor([width], async () => {
|
|
491
|
+
const out = new Float32Array(width).fill(1);
|
|
492
|
+
const qn = interleaveHeads(await nq.f32(), d, 1, block);
|
|
493
|
+
const kn = interleaveHeads(await nk.f32(), d, 1, block);
|
|
494
|
+
for (let h = 0; h < heads; h++) out.set(qn, h * d);
|
|
495
|
+
for (let h = 0; h < kvHeads; h++) out.set(kn, (heads + h) * d);
|
|
496
|
+
return out;
|
|
497
|
+
}, nq.byteLength + nk.byteLength);
|
|
498
|
+
}
|
|
499
|
+
/** The text model's mapping. PLE's hidden^-½ folds into its projection. */
|
|
500
|
+
function transformEmbeddingGemma2StateDict(sd, prefix, cfg) {
|
|
501
|
+
const io = remap(sd, prefix, [SOURCE], "embeddinggemma2 state dict");
|
|
502
|
+
const take = (name) => io.take(SOURCE + name);
|
|
503
|
+
const { put } = io;
|
|
504
|
+
for (const [i, type] of cfg.layerTypes.entries()) {
|
|
505
|
+
const L = `layers.${i}`;
|
|
506
|
+
const { headDim: d, kvHeads } = cfg.attention[type];
|
|
507
|
+
const attn = (n) => take(`${L}.self_attn.${n}.weight`);
|
|
508
|
+
put(`${L}.qkv.weight`, fusedQkv(attn("q_proj"), attn("k_proj"), attn("v_proj"), cfg.heads * d, kvHeads * d, cfg.hidden, d));
|
|
509
|
+
put(`${L}.qkv_norm`, fusedQkvNorm(attn("q_norm"), attn("k_norm"), cfg.heads, kvHeads, d, d));
|
|
510
|
+
put(`${L}.o_proj.weight`, attn("o_proj"));
|
|
511
|
+
for (const n of [
|
|
512
|
+
"gate_proj",
|
|
513
|
+
"up_proj",
|
|
514
|
+
"down_proj"
|
|
515
|
+
]) put(`${L}.${n}.weight`, take(`${L}.mlp.${n}.weight`));
|
|
516
|
+
for (const n of ["per_layer_input_gate", "per_layer_projection"]) put(`${L}.${n}.weight`, take(`${L}.ple_block.${n}.weight`));
|
|
517
|
+
put(`${L}.post_per_layer_input_norm.scale`, take(`${L}.ple_block.post_per_layer_input_norm.weight`));
|
|
518
|
+
for (const n of LAYER_NORMS) put(`${L}.${n}.scale`, take(`${L}.${n}.weight`));
|
|
519
|
+
const s = take(`${L}.layer_scalar`);
|
|
520
|
+
put(`${L}.layer_scalar`, derivedTensor([1], () => s.f32(), s.byteLength));
|
|
521
|
+
}
|
|
522
|
+
const p = take("ple.per_layer_model_projection.weight");
|
|
523
|
+
const scale = cfg.hidden ** -.5;
|
|
524
|
+
put("ple.per_layer_model_projection.weight", derivedTensor(p.shape, async () => (await p.f32()).map((v) => v * scale), p.byteLength));
|
|
525
|
+
put("ple.per_layer_projection_norm.scale", take("ple.per_layer_projection_norm.weight"));
|
|
526
|
+
put("embed_tokens.weight", take("embed_tokens.weight"));
|
|
527
|
+
put("norm.scale", take("norm.weight"));
|
|
528
|
+
put("embedding_projection.weight", take("embedding_projection.weight"));
|
|
529
|
+
io.done();
|
|
530
|
+
}
|
|
531
|
+
/** The vision tower's mapping. Its 2D rope turns each half head on its
|
|
532
|
+
* own, so q and k interleave per half head. The position table splits
|
|
533
|
+
* into x and y, and the weightless norm gets ones. */
|
|
534
|
+
function transformEmbeddingGemma2VisionStateDict(sd, prefix, cfg) {
|
|
535
|
+
const { take, put, done } = remap(sd, prefix, VISION_SOURCES, "embeddinggemma2 vision state dict");
|
|
536
|
+
const { hidden, heads, headDim: d, positions } = cfg;
|
|
537
|
+
put("input_proj.weight", take("vision_tower.patch_embedder.input_proj.weight"));
|
|
538
|
+
const table = take("vision_tower.patch_embedder.position_embedding_table");
|
|
539
|
+
let both;
|
|
540
|
+
const axis = (i) => derivedTensor([positions, hidden], async () => (await (both ??= table.f32())).slice(i * positions * hidden, (i + 1) * positions * hidden), table.byteLength / 2);
|
|
541
|
+
put("pos_x.weight", axis(0));
|
|
542
|
+
put("pos_y.weight", axis(1));
|
|
543
|
+
for (let i = 0; i < cfg.layers; i++) {
|
|
544
|
+
const S = `vision_tower.encoder.layers.${i}`;
|
|
545
|
+
const L = `layers.${i}`;
|
|
546
|
+
const attn = (n) => take(`${S}.self_attn.${n}.weight`);
|
|
547
|
+
const proj = (n) => take(`${S}.self_attn.${n}.linear.weight`);
|
|
548
|
+
const rows = heads * d;
|
|
549
|
+
put(`${L}.qkv.weight`, fusedQkv(proj("q_proj"), proj("k_proj"), proj("v_proj"), rows, rows, hidden, d / 2));
|
|
550
|
+
put(`${L}.qkv_norm`, fusedQkvNorm(attn("q_norm"), attn("k_norm"), heads, heads, d, d / 2));
|
|
551
|
+
put(`${L}.o_proj.weight`, proj("o_proj"));
|
|
552
|
+
for (const n of [
|
|
553
|
+
"gate_proj",
|
|
554
|
+
"up_proj",
|
|
555
|
+
"down_proj"
|
|
556
|
+
]) put(`${L}.${n}.weight`, take(`${S}.mlp.${n}.linear.weight`));
|
|
557
|
+
for (const n of LAYER_NORMS) put(`${L}.${n}.scale`, take(`${S}.${n}.weight`));
|
|
558
|
+
}
|
|
559
|
+
put("embedding_pre_projection_norm.scale", ones(hidden));
|
|
560
|
+
put("embedding_projection.weight", take("embed_vision.embedding_projection.weight"));
|
|
561
|
+
done();
|
|
562
|
+
}
|
|
563
|
+
/** The audio tower's mapping. Each clipped linear's four bounds become one
|
|
564
|
+
* CPU tensor [inMin, inMax, outMin, outMax]. q, k and v fuse into one
|
|
565
|
+
* weight; they share their bounds. Each layer's distance bias rows and q
|
|
566
|
+
* column scales are computed here, and the feed-forward residual weight
|
|
567
|
+
* folds into its post norm. */
|
|
568
|
+
function transformEmbeddingGemma2AudioStateDict(sd, prefix, cfg) {
|
|
569
|
+
const what = "embeddinggemma2 audio state dict";
|
|
570
|
+
const { take, put, done } = remap(sd, prefix, AUDIO_SOURCES, what);
|
|
571
|
+
const { hidden, headDim, convKernel: K, window } = cfg;
|
|
572
|
+
const [c0, c1] = cfg.convChannels;
|
|
573
|
+
const S = "audio_tower.";
|
|
574
|
+
const scalar = async (t) => (await t.f32())[0];
|
|
575
|
+
const bounds = (name) => [
|
|
576
|
+
"input_min",
|
|
577
|
+
"input_max",
|
|
578
|
+
"output_min",
|
|
579
|
+
"output_max"
|
|
580
|
+
].map((b) => take(`${name}.${b}`));
|
|
581
|
+
const clip = (b) => derivedTensor([4], async () => Float32Array.from(await Promise.all(b.map(scalar))), 0);
|
|
582
|
+
const clipped = (from, to) => {
|
|
583
|
+
put(`${to}.weight`, take(`${from}.linear.weight`));
|
|
584
|
+
put(`${to}.clip`, clip(bounds(from)));
|
|
585
|
+
};
|
|
586
|
+
const reshaped = (t, shape, map) => derivedTensor(shape, async () => map ? map(await t.f32()) : t.f32(), t.byteLength);
|
|
587
|
+
const sub = `${S}subsample_conv_projection.`;
|
|
588
|
+
put("conv0.weight", reshaped(take(`${sub}layer0.conv.weight`), [c0, 9]));
|
|
589
|
+
put("norm0.weight", take(`${sub}layer0.norm.weight`));
|
|
590
|
+
put("conv1.weight", reshaped(take(`${sub}layer1.conv.weight`), [c1, 9 * c0], (w) => {
|
|
591
|
+
const out = new Float32Array(w.length);
|
|
592
|
+
for (let o = 0; o < c1; o++) for (let c = 0; c < c0; c++) for (let k = 0; k < 9; k++) out[(o * 9 + k) * c0 + c] = w[(o * c0 + c) * 9 + k];
|
|
593
|
+
return out;
|
|
594
|
+
}));
|
|
595
|
+
put("norm1.weight", take(`${sub}layer1.norm.weight`));
|
|
596
|
+
put("input_proj.weight", take(`${sub}input_proj_linear.weight`));
|
|
597
|
+
const half = hidden / 2;
|
|
598
|
+
const pe = new Float32Array(window * hidden);
|
|
599
|
+
for (let j = 0; j < window; j++) for (let i = 0; i < half; i++) {
|
|
600
|
+
const a = j * Math.exp(-i * Math.log(1e4) / (half - 1));
|
|
601
|
+
pe[j * hidden + i] = Math.sin(a);
|
|
602
|
+
pe[j * hidden + half + i] = Math.cos(a);
|
|
603
|
+
}
|
|
604
|
+
const qScale = headDim ** -.5 / Math.LN2;
|
|
605
|
+
for (let i = 0; i < cfg.layers; i++) {
|
|
606
|
+
const L = `layers.${i}`;
|
|
607
|
+
const T = `${S}layers.${i}`;
|
|
608
|
+
for (const ff of ["feed_forward1", "feed_forward2"]) {
|
|
609
|
+
put(`${L}.${ff}.pre_layer_norm.scale`, take(`${T}.${ff}.pre_layer_norm.weight`));
|
|
610
|
+
clipped(`${T}.${ff}.ffw_layer_1`, `${L}.${ff}.ffw_layer_1`);
|
|
611
|
+
clipped(`${T}.${ff}.ffw_layer_2`, `${L}.${ff}.ffw_layer_2`);
|
|
612
|
+
put(`${L}.${ff}.post_layer_norm.scale`, reshaped(take(`${T}.${ff}.post_layer_norm.weight`), [hidden], (w) => w.map((v) => v * cfg.residualWeight)));
|
|
613
|
+
}
|
|
614
|
+
const A = `${T}.self_attn`;
|
|
615
|
+
const qkv = [
|
|
616
|
+
"q_proj",
|
|
617
|
+
"k_proj",
|
|
618
|
+
"v_proj"
|
|
619
|
+
].map((n) => ({
|
|
620
|
+
w: take(`${A}.${n}.linear.weight`),
|
|
621
|
+
b: bounds(`${A}.${n}`)
|
|
622
|
+
}));
|
|
623
|
+
put(`${L}.qkv.weight`, derivedTensor([3 * hidden, hidden], async () => {
|
|
624
|
+
const out = new Float32Array(3 * hidden * hidden);
|
|
625
|
+
for (const [n, t] of qkv.entries()) out.set(await t.w.f32(), n * hidden * hidden);
|
|
626
|
+
return out;
|
|
627
|
+
}, qkv.reduce((s, t) => s + t.w.byteLength, 0)));
|
|
628
|
+
put(`${L}.qkv.clip`, derivedTensor([4], async () => {
|
|
629
|
+
const all = await Promise.all(qkv.map((t) => Promise.all(t.b.map(scalar))));
|
|
630
|
+
if (new Set(all.map(String)).size > 1) throw checkpointMismatch(what, `${A}: q, k and v have different bounds`);
|
|
631
|
+
return Float32Array.from(all.flat().slice(0, 4));
|
|
632
|
+
}));
|
|
633
|
+
clipped(`${A}.post`, `${L}.post`);
|
|
634
|
+
const rk = take(`${A}.relative_k_proj.weight`);
|
|
635
|
+
put(`${L}.rel`, derivedTensor([window, hidden], async () => {
|
|
636
|
+
const w = await rk.f32();
|
|
637
|
+
const out = new Float32Array(window * hidden);
|
|
638
|
+
for (let j = 0; j < window; j++) for (let o = 0; o < hidden; o++) {
|
|
639
|
+
let s = 0;
|
|
640
|
+
for (let n = 0; n < hidden; n++) s += pe[j * hidden + n] * w[o * hidden + n];
|
|
641
|
+
out[j * hidden + o] = s;
|
|
642
|
+
}
|
|
643
|
+
return out;
|
|
644
|
+
}, rk.byteLength));
|
|
645
|
+
put(`${L}.q_scale`, reshaped(take(`${A}.per_dim_scale`), [headDim], (s) => s.map((x) => qScale * Math.log1p(Math.exp(x)))));
|
|
646
|
+
const C = `${T}.lconv1d`;
|
|
647
|
+
put(`${L}.lconv.pre_layer_norm.scale`, take(`${C}.pre_layer_norm.weight`));
|
|
648
|
+
clipped(`${C}.linear_start`, `${L}.lconv.linear_start`);
|
|
649
|
+
put(`${L}.lconv.conv`, reshaped(take(`${C}.depthwise_conv1d.weight`), [K, hidden], (w) => {
|
|
650
|
+
const out = new Float32Array(w.length);
|
|
651
|
+
for (let c = 0; c < hidden; c++) for (let t = 0; t < K; t++) out[t * hidden + c] = w[c * K + t];
|
|
652
|
+
return out;
|
|
653
|
+
}));
|
|
654
|
+
put(`${L}.lconv.conv_norm.scale`, take(`${C}.conv_norm.weight`));
|
|
655
|
+
clipped(`${C}.linear_end`, `${L}.lconv.linear_end`);
|
|
656
|
+
for (const n of [
|
|
657
|
+
"norm_pre_attn",
|
|
658
|
+
"norm_post_attn",
|
|
659
|
+
"norm_out"
|
|
660
|
+
]) put(`${L}.${n}.scale`, take(`${T}.${n}.weight`));
|
|
661
|
+
}
|
|
662
|
+
put("output_proj.weight", take(`${S}output_proj.weight`));
|
|
663
|
+
put("output_proj.bias", take(`${S}output_proj.bias`));
|
|
664
|
+
put("embedding_pre_projection_norm.scale", ones(cfg.outputDim));
|
|
665
|
+
put("embedding_projection.weight", take("embed_audio.embedding_projection.weight"));
|
|
666
|
+
done();
|
|
667
|
+
}
|
|
668
|
+
//#endregion
|
|
669
|
+
//#region src/zoo/embeddinggemma2/audioModel.ts
|
|
670
|
+
/** EmbeddingGemma 2 audio tower: a conformer over a clip's log-mel frames,
|
|
671
|
+
* one token per 40 ms, projected to the text model's width.
|
|
672
|
+
*
|
|
673
|
+
* Two stride-2 convs shrink time and frequency 4×. Each layer is a
|
|
674
|
+
* half-weighted feed-forward, local attention over this token and the 11
|
|
675
|
+
* before it, a gated causal depthwise conv, a second feed-forward and a
|
|
676
|
+
* norm. Most linears clamp their input and output to bounds from the
|
|
677
|
+
* checkpoint. */
|
|
678
|
+
/** k's scale in every score, fixed by the architecture. */
|
|
679
|
+
const K_SCALE = Math.log1p(Math.E) / Math.LN2;
|
|
680
|
+
/** A linear that clamps its input and output to the checkpoint's bounds. */
|
|
681
|
+
var ClippedLinear = class extends Linear {
|
|
682
|
+
/** [inMin, inMax, outMin, outMax], read on the CPU. */
|
|
683
|
+
clip = new CpuParameter({
|
|
684
|
+
elems: 4,
|
|
685
|
+
dtype: "f32",
|
|
686
|
+
dims: [4]
|
|
687
|
+
});
|
|
688
|
+
constructor(inFeatures, outFeatures) {
|
|
689
|
+
super(inFeatures, outFeatures, { bias: false });
|
|
690
|
+
}
|
|
691
|
+
/** Bounds of the output. */
|
|
692
|
+
get outBounds() {
|
|
693
|
+
const [, , lo = -Infinity, hi = Infinity] = this.clip.data;
|
|
694
|
+
return {
|
|
695
|
+
lo,
|
|
696
|
+
hi
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
/** The matmul on the clamped input, its output left unclamped. */
|
|
700
|
+
unclamped(x) {
|
|
701
|
+
const [lo = -Infinity, hi = Infinity] = this.clip.data;
|
|
702
|
+
return super.forward(clamp$3(x, lo, hi));
|
|
703
|
+
}
|
|
704
|
+
forward(x) {
|
|
705
|
+
const { lo, hi } = this.outBounds;
|
|
706
|
+
return clamp$3(this.unclamped(x), lo, hi);
|
|
707
|
+
}
|
|
708
|
+
};
|
|
709
|
+
var FeedForward = class extends Module {
|
|
710
|
+
pre_layer_norm;
|
|
711
|
+
ffw_layer_1;
|
|
712
|
+
ffw_layer_2;
|
|
713
|
+
/** Its scale carries the residual weight. */
|
|
714
|
+
post_layer_norm;
|
|
715
|
+
constructor({ hidden, ffn, eps }) {
|
|
716
|
+
super();
|
|
717
|
+
this.pre_layer_norm = new RMSNorm(hidden, eps);
|
|
718
|
+
this.ffw_layer_1 = new ClippedLinear(hidden, ffn);
|
|
719
|
+
this.ffw_layer_2 = new ClippedLinear(ffn, hidden);
|
|
720
|
+
this.post_layer_norm = new RMSNorm(hidden, eps);
|
|
721
|
+
}
|
|
722
|
+
forward(x) {
|
|
723
|
+
const h = silu(this.ffw_layer_1.forward(this.pre_layer_norm.forward(x)));
|
|
724
|
+
const post = this.post_layer_norm;
|
|
725
|
+
return rmsNorm(this.ffw_layer_2.forward(h), post.scale.value, post.eps, { residual: x });
|
|
726
|
+
}
|
|
727
|
+
};
|
|
728
|
+
var LightConv = class extends Module {
|
|
729
|
+
pre_layer_norm;
|
|
730
|
+
/** The two GLU halves, value then gate. */
|
|
731
|
+
linear_start;
|
|
732
|
+
/** Depthwise taps [convKernel, hidden], oldest first. */
|
|
733
|
+
conv;
|
|
734
|
+
conv_norm;
|
|
735
|
+
linear_end;
|
|
736
|
+
constructor({ hidden, eps, convKernel }) {
|
|
737
|
+
super();
|
|
738
|
+
this.pre_layer_norm = new RMSNorm(hidden, eps);
|
|
739
|
+
this.linear_start = new ClippedLinear(hidden, 2 * hidden);
|
|
740
|
+
this.conv = new Parameter({
|
|
741
|
+
elems: convKernel * hidden,
|
|
742
|
+
dtype: "f32",
|
|
743
|
+
dims: [convKernel, hidden]
|
|
744
|
+
});
|
|
745
|
+
this.conv_norm = new RMSNorm(hidden, eps);
|
|
746
|
+
this.linear_end = new ClippedLinear(hidden, hidden);
|
|
747
|
+
}
|
|
748
|
+
forward(x) {
|
|
749
|
+
const z = this.linear_start.unclamped(this.pre_layer_norm.forward(x));
|
|
750
|
+
const g = gluConv1d(z, this.conv.value, this.linear_start.outBounds);
|
|
751
|
+
return add(this.linear_end.forward(silu(this.conv_norm.forward(g))), x);
|
|
752
|
+
}
|
|
753
|
+
};
|
|
754
|
+
var AudioLayer = class extends Module {
|
|
755
|
+
cfg;
|
|
756
|
+
feed_forward1;
|
|
757
|
+
norm_pre_attn;
|
|
758
|
+
/** q | k | v rows. */
|
|
759
|
+
qkv;
|
|
760
|
+
/** One bias row per key distance [window, hidden], f32. */
|
|
761
|
+
rel;
|
|
762
|
+
/** Scale of each column of q [headDim], f32. */
|
|
763
|
+
q_scale;
|
|
764
|
+
post;
|
|
765
|
+
norm_post_attn;
|
|
766
|
+
lconv;
|
|
767
|
+
feed_forward2;
|
|
768
|
+
norm_out;
|
|
769
|
+
constructor(cfg) {
|
|
770
|
+
super();
|
|
771
|
+
this.cfg = cfg;
|
|
772
|
+
const { hidden, headDim, window, eps } = cfg;
|
|
773
|
+
const f32 = (...dims) => new Parameter({
|
|
774
|
+
elems: dims.reduce((a, b) => a * b),
|
|
775
|
+
dtype: "f32",
|
|
776
|
+
dims
|
|
777
|
+
}, void 0, true);
|
|
778
|
+
this.feed_forward1 = new FeedForward(cfg);
|
|
779
|
+
this.norm_pre_attn = new RMSNorm(hidden, eps);
|
|
780
|
+
this.qkv = new ClippedLinear(hidden, 3 * hidden);
|
|
781
|
+
this.rel = f32(window, hidden);
|
|
782
|
+
this.q_scale = f32(headDim);
|
|
783
|
+
this.post = new ClippedLinear(hidden, hidden);
|
|
784
|
+
this.norm_post_attn = new RMSNorm(hidden, eps);
|
|
785
|
+
this.lconv = new LightConv(cfg);
|
|
786
|
+
this.feed_forward2 = new FeedForward(cfg);
|
|
787
|
+
this.norm_out = new RMSNorm(hidden, eps);
|
|
788
|
+
}
|
|
789
|
+
forward(x) {
|
|
790
|
+
const { heads, headDim, window, logitCap } = this.cfg;
|
|
791
|
+
x = this.feed_forward1.forward(x);
|
|
792
|
+
const qkv = this.qkv.forward(this.norm_pre_attn.forward(x));
|
|
793
|
+
const a = localAttention(qkv, this.rel.value, this.q_scale.value, {
|
|
794
|
+
heads,
|
|
795
|
+
headDim,
|
|
796
|
+
window,
|
|
797
|
+
kScale: K_SCALE,
|
|
798
|
+
softcap: logitCap
|
|
799
|
+
});
|
|
800
|
+
const norm = this.norm_post_attn;
|
|
801
|
+
x = rmsNorm(this.post.forward(a), norm.scale.value, norm.eps, { residual: x });
|
|
802
|
+
x = this.lconv.forward(x);
|
|
803
|
+
return this.norm_out.forward(this.feed_forward2.forward(x));
|
|
804
|
+
}
|
|
805
|
+
};
|
|
806
|
+
var EmbeddingGemma2AudioModel = class extends Module {
|
|
807
|
+
cfg;
|
|
808
|
+
conv0;
|
|
809
|
+
norm0;
|
|
810
|
+
conv1;
|
|
811
|
+
norm1;
|
|
812
|
+
input_proj;
|
|
813
|
+
layers;
|
|
814
|
+
output_proj;
|
|
815
|
+
embedding_pre_projection_norm;
|
|
816
|
+
embedding_projection;
|
|
817
|
+
constructor(cfg = EMBEDDINGGEMMA2_AUDIO) {
|
|
818
|
+
super();
|
|
819
|
+
this.cfg = cfg;
|
|
820
|
+
const { hidden, mels, eps, outputDim } = cfg;
|
|
821
|
+
const [c0, c1] = cfg.convChannels;
|
|
822
|
+
this.conv0 = new Linear(9, c0, { bias: false });
|
|
823
|
+
this.norm0 = new LayerNorm(c0, eps);
|
|
824
|
+
this.conv1 = new Linear(9 * c0, c1, { bias: false });
|
|
825
|
+
this.norm1 = new LayerNorm(c1, eps);
|
|
826
|
+
this.input_proj = new Linear(mels / 4 * c1, hidden, { bias: false });
|
|
827
|
+
this.layers = new ModuleList(Array.from({ length: cfg.layers }, () => new AudioLayer(cfg)));
|
|
828
|
+
this.output_proj = new Linear(hidden, outputDim);
|
|
829
|
+
this.embedding_pre_projection_norm = new RMSNorm(outputDim, eps);
|
|
830
|
+
this.embedding_projection = new Linear(outputDim, EMBEDDINGGEMMA2_TEXT.hidden, { bias: false });
|
|
831
|
+
}
|
|
832
|
+
/** One clip to its tokens [tokens, text hidden], ready to stand in for
|
|
833
|
+
* the placeholder rows of the text model's input. */
|
|
834
|
+
forward(inp) {
|
|
835
|
+
const [c0, c1] = this.cfg.convChannels;
|
|
836
|
+
const relu = (v) => clamp$3(v, 0, Infinity);
|
|
837
|
+
const y0 = relu(this.norm0.forward(this.conv0.forward(inp.patches)));
|
|
838
|
+
const pixels = inp.taps.shape.elems / 9;
|
|
839
|
+
const y1 = reshape(gatherRowsFrom(y0, inp.taps), [pixels, 9 * c0]);
|
|
840
|
+
const y = relu(this.norm1.forward(this.conv1.forward(y1)));
|
|
841
|
+
let x = this.input_proj.forward(reshape(y, [inp.tokens, pixels / inp.tokens * c1]));
|
|
842
|
+
for (const layer of this.layers.items) x = layer.forward(x);
|
|
843
|
+
const out = this.embedding_pre_projection_norm.forward(this.output_proj.forward(x));
|
|
844
|
+
return this.embedding_projection.forward(out);
|
|
845
|
+
}
|
|
846
|
+
transformStateDict(sd, prefix) {
|
|
847
|
+
transformEmbeddingGemma2AudioStateDict(sd, prefix, this.cfg);
|
|
848
|
+
}
|
|
849
|
+
};
|
|
850
|
+
//#endregion
|
|
851
|
+
//#region src/zoo/embeddinggemma2/rope.ts
|
|
852
|
+
/** EmbeddingGemma 2 rope tables, in the kernel's interleaved-pair layout:
|
|
853
|
+
* entries 2p and 2p+1 hold pair p's angle. The q and k weights are permuted
|
|
854
|
+
* at load so these pairs are HF's half split (stateDictHooks). */
|
|
855
|
+
/** cos and sin [positions, headDim] for theta. Row t depends only on t. */
|
|
856
|
+
function ropeTables(positions, headDim, theta) {
|
|
857
|
+
const cos = new Float32Array(positions * headDim);
|
|
858
|
+
const sin = new Float32Array(positions * headDim);
|
|
859
|
+
for (let t = 0; t < positions; t++) for (let p = 0; p < headDim / 2; p++) {
|
|
860
|
+
const angle = t / theta ** (2 * p / headDim);
|
|
861
|
+
cos[t * headDim + 2 * p] = cos[t * headDim + 2 * p + 1] = Math.cos(angle);
|
|
862
|
+
sin[t * headDim + 2 * p] = sin[t * headDim + 2 * p + 1] = Math.sin(angle);
|
|
863
|
+
}
|
|
864
|
+
return {
|
|
865
|
+
cos,
|
|
866
|
+
sin
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
/** The vision tower's 2D rope: cos and sin [cols · rows, headDim] for a grid
|
|
870
|
+
* of patches in row-major order. The first half of each head turns with the
|
|
871
|
+
* patch's column, the second half with its row, each half a rope of
|
|
872
|
+
* headDim / 2. */
|
|
873
|
+
function visionRopeTables(cols, rows, headDim, theta) {
|
|
874
|
+
const pairs = headDim / 2;
|
|
875
|
+
const halfPairs = pairs / 2;
|
|
876
|
+
const cos = new Float32Array(cols * rows * headDim);
|
|
877
|
+
const sin = new Float32Array(cols * rows * headDim);
|
|
878
|
+
for (let k = 0; k < cols * rows; k++) for (let p = 0; p < pairs; p++) {
|
|
879
|
+
const angle = (p < halfPairs ? k % cols : Math.floor(k / cols)) / theta ** (p % halfPairs / halfPairs);
|
|
880
|
+
cos[k * headDim + 2 * p] = cos[k * headDim + 2 * p + 1] = Math.cos(angle);
|
|
881
|
+
sin[k * headDim + 2 * p] = sin[k * headDim + 2 * p + 1] = Math.sin(angle);
|
|
882
|
+
}
|
|
883
|
+
return {
|
|
884
|
+
cos,
|
|
885
|
+
sin
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
//#endregion
|
|
889
|
+
//#region src/zoo/embeddinggemma2/embedCore.ts
|
|
890
|
+
/** EmbeddingGemma 2 embed core: token ids in, pooled embeddings out.
|
|
891
|
+
*
|
|
892
|
+
* The embedding table lives on the CPU, so a forward's input is the embedded
|
|
893
|
+
* rows, written with positions, segments and pool weights. Image, video and
|
|
894
|
+
* audio tokens arrive as rows from their tower and replace the rows of
|
|
895
|
+
* their placeholder ids. One input up to the largest bucket runs as a recorded
|
|
896
|
+
* replay per padded length; a longer input and a batch run eagerly. A
|
|
897
|
+
* batch packs its inputs into one sequence: segments keep them from
|
|
898
|
+
* attending to each other, positions restart per input and pick rope rows
|
|
899
|
+
* from one shared table, and each pool row averages one input. */
|
|
900
|
+
/** Token counts a single input pads up to. Longer inputs run eagerly. */
|
|
901
|
+
const EMBEDDINGGEMMA2_BUCKETS = [
|
|
902
|
+
16,
|
|
903
|
+
32,
|
|
904
|
+
64,
|
|
905
|
+
128,
|
|
906
|
+
192,
|
|
907
|
+
256,
|
|
908
|
+
384,
|
|
909
|
+
512
|
|
910
|
+
];
|
|
911
|
+
/** Packs inputs back to back into T rows: input b covers its own segment and
|
|
912
|
+
* pool row b. Rows past the inputs are one more segment, pooled by no one. */
|
|
913
|
+
function fillInputs(model, T, inputs, out) {
|
|
914
|
+
const { hidden } = model.cfg;
|
|
915
|
+
out.x0.fill(0);
|
|
916
|
+
out.selector.fill(0);
|
|
917
|
+
let start = 0;
|
|
918
|
+
inputs.forEach(({ ids, media }, b) => {
|
|
919
|
+
const len = ids.length;
|
|
920
|
+
model.embedRows(ids, out.x0, start);
|
|
921
|
+
for (const { at, rows } of media) out.x0.set(rows, (start + at) * hidden);
|
|
922
|
+
for (let i = 0; i < len; i++) {
|
|
923
|
+
const t = start + i;
|
|
924
|
+
out.positions[t] = i;
|
|
925
|
+
out.segments[2 * t] = start;
|
|
926
|
+
out.segments[2 * t + 1] = start + len;
|
|
927
|
+
out.selector[b * T + t] = 1 / len;
|
|
928
|
+
}
|
|
929
|
+
start += len;
|
|
930
|
+
});
|
|
931
|
+
for (let t = start; t < T; t++) {
|
|
932
|
+
out.positions[t] = t - start;
|
|
933
|
+
out.segments[2 * t] = start;
|
|
934
|
+
out.segments[2 * t + 1] = T;
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
function createEmbeddingGemma2EmbedCore(model) {
|
|
938
|
+
const { hidden, embeddingDim, attention } = model.cfg;
|
|
939
|
+
const act = model.norm.scale.shape.dtype === "f16" ? "f16" : "f32";
|
|
940
|
+
const maxT = EMBEDDINGGEMMA2_MAX_TOKENS;
|
|
941
|
+
const table = (type) => {
|
|
942
|
+
const { headDim, theta } = attention[type];
|
|
943
|
+
const { cos, sin } = ropeTables(maxT, headDim, theta);
|
|
944
|
+
return {
|
|
945
|
+
cos: tensor(cos, [maxT, headDim]),
|
|
946
|
+
sin: tensor(sin, [maxT, headDim])
|
|
947
|
+
};
|
|
948
|
+
};
|
|
949
|
+
const tables = {
|
|
950
|
+
sliding: table("sliding"),
|
|
951
|
+
full: table("full")
|
|
952
|
+
};
|
|
953
|
+
const eager = gpuExecutor(defaultRoot());
|
|
954
|
+
const inputsOf = (planes, maxSegment) => {
|
|
955
|
+
const pick = (type) => ({
|
|
956
|
+
cos: gatherRowsFrom(tables[type].cos, planes.positions),
|
|
957
|
+
sin: gatherRowsFrom(tables[type].sin, planes.positions)
|
|
958
|
+
});
|
|
959
|
+
return {
|
|
960
|
+
x0: planes.x0,
|
|
961
|
+
rope: {
|
|
962
|
+
sliding: pick("sliding"),
|
|
963
|
+
full: pick("full")
|
|
964
|
+
},
|
|
965
|
+
selector: planes.selector,
|
|
966
|
+
segments: planes.segments,
|
|
967
|
+
maxSegment
|
|
968
|
+
};
|
|
969
|
+
};
|
|
970
|
+
const replays = createReplayCache({
|
|
971
|
+
buckets: EMBEDDINGGEMMA2_BUCKETS,
|
|
972
|
+
inputs: (T) => ({
|
|
973
|
+
x0: [T, hidden],
|
|
974
|
+
positions: [T, 1],
|
|
975
|
+
segments: [T, 2],
|
|
976
|
+
selector: [1, T]
|
|
977
|
+
}),
|
|
978
|
+
planeDtype: {
|
|
979
|
+
x0: act,
|
|
980
|
+
selector: act
|
|
981
|
+
},
|
|
982
|
+
captureFill: (T, scratch) => fillInputs(model, T, [{
|
|
983
|
+
ids: new Array(T).fill(0),
|
|
984
|
+
media: []
|
|
985
|
+
}], scratch),
|
|
986
|
+
build: (T, inp) => model.forward(inputsOf(inp, T))
|
|
987
|
+
});
|
|
988
|
+
/** One eager forward over inputs packed back to back. T rounds up to a
|
|
989
|
+
* multiple of 4, so the attention score matmuls take the vec4 kernels. */
|
|
990
|
+
const runEager = async (inputs) => {
|
|
991
|
+
const T = Math.ceil(inputs.reduce((s, { ids }) => s + ids.length, 0) / 4) * 4;
|
|
992
|
+
const B = inputs.length;
|
|
993
|
+
const planes = {
|
|
994
|
+
x0: new Float32Array(T * hidden),
|
|
995
|
+
positions: new Float32Array(T),
|
|
996
|
+
segments: new Float32Array(2 * T),
|
|
997
|
+
selector: new Float32Array(B * T)
|
|
74
998
|
};
|
|
999
|
+
fillInputs(model, T, inputs, planes);
|
|
1000
|
+
const upload = (data, rows, dtype) => tensor(data, {
|
|
1001
|
+
elems: data.length,
|
|
1002
|
+
dtype,
|
|
1003
|
+
dims: [rows, data.length / rows]
|
|
1004
|
+
});
|
|
1005
|
+
const values = {
|
|
1006
|
+
x0: upload(planes.x0, T, act),
|
|
1007
|
+
positions: upload(planes.positions, T, "f32"),
|
|
1008
|
+
segments: upload(planes.segments, T, "f32"),
|
|
1009
|
+
selector: upload(planes.selector, B, act)
|
|
1010
|
+
};
|
|
1011
|
+
try {
|
|
1012
|
+
const longest = Math.max(...inputs.map(({ ids }) => ids.length));
|
|
1013
|
+
return await toArray(model.forward(inputsOf(values, longest)), eager);
|
|
1014
|
+
} finally {
|
|
1015
|
+
for (const v of Object.values(values)) v.buffer.destroy();
|
|
1016
|
+
eager.trim();
|
|
1017
|
+
}
|
|
1018
|
+
};
|
|
1019
|
+
const embedPooled = (input) => {
|
|
1020
|
+
const T = replays.pick(input.ids.length);
|
|
1021
|
+
return T === void 0 ? runEager([input]) : replays.run(T, (_, scratch) => fillInputs(model, T, [input], scratch));
|
|
1022
|
+
};
|
|
1023
|
+
const embedPooledBatch = async (inputs) => {
|
|
1024
|
+
const packs = [];
|
|
1025
|
+
let pack = [];
|
|
1026
|
+
let tokens = maxT;
|
|
1027
|
+
for (const input of inputs) {
|
|
1028
|
+
if (tokens + input.ids.length > maxT) {
|
|
1029
|
+
pack = [];
|
|
1030
|
+
packs.push(pack);
|
|
1031
|
+
tokens = 0;
|
|
1032
|
+
}
|
|
1033
|
+
pack.push(input);
|
|
1034
|
+
tokens += input.ids.length;
|
|
1035
|
+
}
|
|
1036
|
+
const out = new Float32Array(inputs.length * embeddingDim);
|
|
1037
|
+
let row = 0;
|
|
1038
|
+
for (const pack of packs) {
|
|
1039
|
+
out.set(await runEager(pack), row * embeddingDim);
|
|
1040
|
+
row += pack.length;
|
|
1041
|
+
}
|
|
1042
|
+
return out;
|
|
1043
|
+
};
|
|
1044
|
+
const warmBuckets = async (probe) => {
|
|
1045
|
+
for (const T of EMBEDDINGGEMMA2_BUCKETS) await replays.run(T, (_, scratch) => fillInputs(model, T, [probe], scratch));
|
|
1046
|
+
};
|
|
1047
|
+
return {
|
|
1048
|
+
embedPooled,
|
|
1049
|
+
embedPooledBatch,
|
|
1050
|
+
warmBuckets,
|
|
1051
|
+
dispose() {
|
|
1052
|
+
replays.dispose();
|
|
1053
|
+
eager.dispose();
|
|
1054
|
+
for (const { cos, sin } of Object.values(tables)) {
|
|
1055
|
+
cos.buffer.destroy();
|
|
1056
|
+
sin.buffer.destroy();
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
};
|
|
1060
|
+
}
|
|
1061
|
+
//#endregion
|
|
1062
|
+
//#region src/zoo/embeddinggemma2/model.ts
|
|
1063
|
+
/** EmbeddingGemma 2 text model: a bidirectional Gemma encoder with
|
|
1064
|
+
* per-layer embeddings (PLE), mean pooled and projected to 768.
|
|
1065
|
+
*
|
|
1066
|
+
* Every layer is attention, a GeGLU MLP and a PLE block, each between a pre
|
|
1067
|
+
* and a post RMSNorm. Five sliding layers (head 256, two kv heads) alternate
|
|
1068
|
+
* with one full layer (head 512, one kv head). q, k and v are RMS-normed per
|
|
1069
|
+
* head, so scores carry no 1/√d scale.
|
|
1070
|
+
*
|
|
1071
|
+
* Built for few dispatches: q/k/v are one matmul, their per-head norms one
|
|
1072
|
+
* grouped RMSNorm, and every post-norm also adds the residual. GELU rides
|
|
1073
|
+
* the gate matmul's epilogue. Long inputs run attention per head as two
|
|
1074
|
+
* matmuls around a masked softmax, short ones as one sdpa. */
|
|
1075
|
+
/** Longest-input length from which attention runs on matmuls. */
|
|
1076
|
+
const MATMUL_ATTENTION_MIN_SEGMENT = 256;
|
|
1077
|
+
/** Past this many rows a sliding layer keeps sdpa, which skips the keys
|
|
1078
|
+
* outside its window. */
|
|
1079
|
+
const MATMUL_ATTENTION_MAX_SLIDING_ROWS = 1024;
|
|
1080
|
+
var Layer = class extends Module {
|
|
1081
|
+
cfg;
|
|
1082
|
+
type;
|
|
1083
|
+
input_layernorm;
|
|
1084
|
+
/** q | k | v rows, with q and k permuted for the interleaved rope. */
|
|
1085
|
+
qkv;
|
|
1086
|
+
/** q_norm tiled per q head, k_norm per kv head, ones for v. */
|
|
1087
|
+
qkv_norm;
|
|
1088
|
+
o_proj;
|
|
1089
|
+
post_attention_layernorm;
|
|
1090
|
+
pre_feedforward_layernorm;
|
|
1091
|
+
gate_proj;
|
|
1092
|
+
up_proj;
|
|
1093
|
+
down_proj;
|
|
1094
|
+
post_feedforward_layernorm;
|
|
1095
|
+
per_layer_input_gate;
|
|
1096
|
+
per_layer_projection;
|
|
1097
|
+
post_per_layer_input_norm;
|
|
1098
|
+
/** One number that scales the layer's output; read on the CPU. */
|
|
1099
|
+
layer_scalar;
|
|
1100
|
+
constructor(cfg, type) {
|
|
1101
|
+
super();
|
|
1102
|
+
this.cfg = cfg;
|
|
1103
|
+
this.type = type;
|
|
1104
|
+
const { hidden, heads, ffn, pleDim, eps } = cfg;
|
|
1105
|
+
const { headDim, kvHeads } = cfg.attention[type];
|
|
1106
|
+
const width = (heads + 2 * kvHeads) * headDim;
|
|
1107
|
+
const norm = () => new RMSNorm(hidden, eps);
|
|
1108
|
+
this.input_layernorm = norm();
|
|
1109
|
+
this.qkv = new Linear(hidden, width, { bias: false });
|
|
1110
|
+
this.qkv_norm = new Parameter({
|
|
1111
|
+
elems: width,
|
|
1112
|
+
dtype: "f32",
|
|
1113
|
+
dims: [width]
|
|
1114
|
+
});
|
|
1115
|
+
this.o_proj = new Linear(heads * headDim, hidden, { bias: false });
|
|
1116
|
+
this.post_attention_layernorm = norm();
|
|
1117
|
+
this.pre_feedforward_layernorm = norm();
|
|
1118
|
+
this.gate_proj = new Linear(hidden, ffn, {
|
|
1119
|
+
bias: false,
|
|
1120
|
+
activation: "geluTanh"
|
|
1121
|
+
});
|
|
1122
|
+
this.up_proj = new Linear(hidden, ffn, { bias: false });
|
|
1123
|
+
this.down_proj = new Linear(ffn, hidden, { bias: false });
|
|
1124
|
+
this.post_feedforward_layernorm = norm();
|
|
1125
|
+
this.per_layer_input_gate = new Linear(hidden, pleDim, {
|
|
1126
|
+
bias: false,
|
|
1127
|
+
activation: "geluTanh"
|
|
1128
|
+
});
|
|
1129
|
+
this.per_layer_projection = new Linear(pleDim, hidden, { bias: false });
|
|
1130
|
+
this.post_per_layer_input_norm = norm();
|
|
1131
|
+
this.layer_scalar = new CpuParameter({
|
|
1132
|
+
elems: 1,
|
|
1133
|
+
dtype: "f32",
|
|
1134
|
+
dims: [1]
|
|
1135
|
+
});
|
|
75
1136
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
1137
|
+
forward(x, perLayer, inp) {
|
|
1138
|
+
const { heads, eps, slidingWindow } = this.cfg;
|
|
1139
|
+
const { headDim: d, kvHeads } = this.cfg.attention[this.type];
|
|
1140
|
+
const { cos, sin } = inp.rope[this.type];
|
|
1141
|
+
const window = this.type === "sliding" ? slidingWindow : Infinity;
|
|
1142
|
+
const postNorm = (norm, y, residual, scale) => rmsNorm(y, norm.scale.value, norm.eps, {
|
|
1143
|
+
residual,
|
|
1144
|
+
scale
|
|
1145
|
+
});
|
|
1146
|
+
const qkv = this.qkv.forward(this.input_layernorm.forward(x));
|
|
1147
|
+
const n = rmsNorm(qkv, this.qkv_norm.value, eps, { group: d });
|
|
1148
|
+
const roped = (start, width) => rope(n, cos, sin, {
|
|
1149
|
+
headDim: d,
|
|
1150
|
+
srcStart: start,
|
|
1151
|
+
width
|
|
1152
|
+
});
|
|
1153
|
+
const vStart = (heads + kvHeads) * d;
|
|
1154
|
+
const rows = x.shape.dims[0];
|
|
1155
|
+
const perHead = inp.maxSegment >= MATMUL_ATTENTION_MIN_SEGMENT && (this.type === "full" || rows <= MATMUL_ATTENTION_MAX_SLIDING_ROWS);
|
|
1156
|
+
let a;
|
|
1157
|
+
if (perHead) {
|
|
1158
|
+
const kT = Array.from({ length: kvHeads }, (_, h) => transpose(roped((heads + h) * d, d)));
|
|
1159
|
+
const v = Array.from({ length: kvHeads }, (_, h) => slice(n, 1, vStart + h * d, vStart + (h + 1) * d));
|
|
1160
|
+
const group = heads / kvHeads;
|
|
1161
|
+
const mask = {
|
|
1162
|
+
segments: inp.segments,
|
|
1163
|
+
windowLeft: window,
|
|
1164
|
+
windowRight: window
|
|
1165
|
+
};
|
|
1166
|
+
const outs = Array.from({ length: heads }, (_, h) => {
|
|
1167
|
+
const g = Math.floor(h / group);
|
|
1168
|
+
return matmul(softmax(matmul(roped(h * d, d), kT[g]), mask), v[g]);
|
|
1169
|
+
});
|
|
1170
|
+
a = cat(outs, 1);
|
|
1171
|
+
} else a = sdpa(roped(0, heads * d), roped(heads * d, kvHeads * d), slice(n, 1, vStart, vStart + kvHeads * d), {
|
|
1172
|
+
qHeads: heads,
|
|
1173
|
+
kvHeads,
|
|
1174
|
+
headDim: d,
|
|
1175
|
+
windowLeft: window,
|
|
1176
|
+
windowRight: window,
|
|
1177
|
+
segments: inp.segments,
|
|
1178
|
+
maxSegment: inp.maxSegment
|
|
1179
|
+
});
|
|
1180
|
+
x = postNorm(this.post_attention_layernorm, this.o_proj.forward(a), x);
|
|
1181
|
+
const f = this.pre_feedforward_layernorm.forward(x);
|
|
1182
|
+
const m = this.down_proj.forward(mul(this.gate_proj.forward(f), this.up_proj.forward(f)));
|
|
1183
|
+
x = postNorm(this.post_feedforward_layernorm, m, x);
|
|
1184
|
+
const g = mul(this.per_layer_input_gate.forward(x), perLayer);
|
|
1185
|
+
const p = this.per_layer_projection.forward(g);
|
|
1186
|
+
return postNorm(this.post_per_layer_input_norm, p, x, this.layer_scalar.data[0]);
|
|
1187
|
+
}
|
|
1188
|
+
};
|
|
1189
|
+
var Ple = class extends Module {
|
|
1190
|
+
cfg;
|
|
1191
|
+
per_layer_model_projection;
|
|
1192
|
+
per_layer_projection_norm;
|
|
1193
|
+
constructor(cfg) {
|
|
1194
|
+
super();
|
|
1195
|
+
this.cfg = cfg;
|
|
1196
|
+
const { hidden, layers, pleDim, eps } = cfg;
|
|
1197
|
+
this.per_layer_model_projection = new Linear(hidden, layers * pleDim, { bias: false });
|
|
1198
|
+
this.per_layer_projection_norm = new RMSNorm(pleDim, eps);
|
|
1199
|
+
}
|
|
1200
|
+
/** [T, hidden] to [T, layers·pleDim], each layer's slice RMS-normed. The
|
|
1201
|
+
* hidden^-½ scale is folded into the projection at load. */
|
|
1202
|
+
forward(x) {
|
|
1203
|
+
const norm = this.per_layer_projection_norm;
|
|
1204
|
+
return rmsNorm(this.per_layer_model_projection.forward(x), norm.scale.value, norm.eps, { group: this.cfg.pleDim });
|
|
1205
|
+
}
|
|
1206
|
+
};
|
|
1207
|
+
var EmbeddingGemma2Model = class extends Module {
|
|
1208
|
+
cfg;
|
|
1209
|
+
embed_tokens;
|
|
1210
|
+
ple;
|
|
1211
|
+
layers;
|
|
1212
|
+
norm;
|
|
1213
|
+
embedding_projection;
|
|
1214
|
+
constructor(cfg = EMBEDDINGGEMMA2_TEXT) {
|
|
1215
|
+
super();
|
|
1216
|
+
this.cfg = cfg;
|
|
1217
|
+
this.embed_tokens = new Embedding(cfg.vocab, cfg.hidden, { device: "cpu" });
|
|
1218
|
+
this.ple = new Ple(cfg);
|
|
1219
|
+
this.layers = new ModuleList(cfg.layerTypes.map((type) => new Layer(cfg, type)));
|
|
1220
|
+
this.norm = new RMSNorm(cfg.hidden, cfg.eps);
|
|
1221
|
+
this.embedding_projection = new Linear(cfg.hidden, cfg.embeddingDim, { bias: false });
|
|
1222
|
+
}
|
|
1223
|
+
/** Token ids to input rows [ids.length · hidden], scaled by √hidden,
|
|
1224
|
+
* written into `out` at row `offset`. */
|
|
1225
|
+
embedRows(ids, out, offset) {
|
|
1226
|
+
const rows = this.embed_tokens.gather(ids);
|
|
1227
|
+
const s = Math.sqrt(this.cfg.hidden);
|
|
1228
|
+
const base = offset * this.cfg.hidden;
|
|
1229
|
+
for (let i = 0; i < rows.length; i++) out[base + i] = rows[i] * s;
|
|
1230
|
+
}
|
|
1231
|
+
/** Packed inputs to their pooled, projected embeddings [B, embeddingDim],
|
|
1232
|
+
* not yet unit length. */
|
|
1233
|
+
forward(inp) {
|
|
1234
|
+
const perLayer = this.ple.forward(inp.x0);
|
|
1235
|
+
const pd = this.cfg.pleDim;
|
|
1236
|
+
let x = inp.x0;
|
|
1237
|
+
this.layers.items.forEach((layer, i) => {
|
|
1238
|
+
x = layer.forward(x, slice(perLayer, 1, i * pd, (i + 1) * pd), inp);
|
|
1239
|
+
});
|
|
1240
|
+
return this.embedding_projection.forward(matmul(inp.selector, this.norm.forward(x)));
|
|
1241
|
+
}
|
|
1242
|
+
transformStateDict(sd, prefix) {
|
|
1243
|
+
transformEmbeddingGemma2StateDict(sd, prefix, this.cfg);
|
|
1244
|
+
}
|
|
1245
|
+
};
|
|
1246
|
+
//#endregion
|
|
1247
|
+
//#region src/zoo/embeddinggemma2/tokenizer.ts
|
|
1248
|
+
/** Gemma tokenizer, encode-only: text → piece ids, without <bos> and <eos>,
|
|
1249
|
+
* which the caller places. tokenizers.js runs the Hub's tokenizer.json. */
|
|
1250
|
+
const isBpeTokenizerJson$1 = (json) => typeof json === "object" && json !== null && "model" in json && typeof json.model === "object" && json.model !== null && "type" in json.model && json.model.type === "BPE";
|
|
1251
|
+
/** Builds the tokenizer from a Hugging Face tokenizer.json of the Gemma
|
|
1252
|
+
* BPE kind. Throws CHECKPOINT_MISMATCH for any other file. */
|
|
1253
|
+
function createEmbeddingGemma2Tokenizer(json) {
|
|
1254
|
+
const bad = (why) => checkpointMismatch("embeddinggemma2 tokenizer", why);
|
|
1255
|
+
if (!isBpeTokenizerJson$1(json)) throw bad("expected a BPE tokenizer.json");
|
|
1256
|
+
const tokenizer = new Tokenizer(json, {});
|
|
81
1257
|
const idOf = (token) => {
|
|
82
|
-
const id =
|
|
1258
|
+
const id = tokenizer.token_to_id(token);
|
|
83
1259
|
if (id === void 0) throw bad(`vocab has no ${token}`);
|
|
84
1260
|
return id;
|
|
85
1261
|
};
|
|
86
1262
|
return {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
1263
|
+
bosId: idOf("<bos>"),
|
|
1264
|
+
eosId: idOf("<eos>"),
|
|
1265
|
+
padId: idOf("<pad>"),
|
|
1266
|
+
boiId: idOf("<|image>"),
|
|
1267
|
+
eoiId: idOf("<image|>"),
|
|
1268
|
+
boaId: idOf("<|audio>"),
|
|
1269
|
+
eoaId: idOf("<audio|>"),
|
|
1270
|
+
encode: (text) => tokenizer.encode(text, { add_special_tokens: false }).ids
|
|
1271
|
+
};
|
|
1272
|
+
}
|
|
1273
|
+
//#endregion
|
|
1274
|
+
//#region src/zoo/tasks/cv/ops/point.ts
|
|
1275
|
+
/** The resize as numbers: how many input pixels one image pixel takes, and
|
|
1276
|
+
* where the image's top-left corner lands inside the input. */
|
|
1277
|
+
function resizeTransform(options) {
|
|
1278
|
+
const { from, to, resizeMode } = options;
|
|
1279
|
+
switch (resizeMode) {
|
|
1280
|
+
case "stretch": return {
|
|
1281
|
+
scaleX: from.width / to.width,
|
|
1282
|
+
scaleY: from.height / to.height,
|
|
1283
|
+
offsetX: 0,
|
|
1284
|
+
offsetY: 0
|
|
1285
|
+
};
|
|
1286
|
+
case "letterbox": {
|
|
1287
|
+
const scale = Math.min(from.width / to.width, from.height / to.height);
|
|
1288
|
+
return {
|
|
1289
|
+
scaleX: scale,
|
|
1290
|
+
scaleY: scale,
|
|
1291
|
+
offsetX: (from.width - to.width * scale) / 2,
|
|
1292
|
+
offsetY: (from.height - to.height * scale) / 2
|
|
1293
|
+
};
|
|
1294
|
+
}
|
|
1295
|
+
case "crop": {
|
|
1296
|
+
const side = Math.min(to.width, to.height) * (options.cropFraction ?? 1);
|
|
1297
|
+
const scaleX = from.width / side;
|
|
1298
|
+
const scaleY = from.height / side;
|
|
1299
|
+
return {
|
|
1300
|
+
scaleX,
|
|
1301
|
+
scaleY,
|
|
1302
|
+
offsetX: -(to.width - side) / 2 * scaleX,
|
|
1303
|
+
offsetY: -(to.height - side) / 2 * scaleY
|
|
1304
|
+
};
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
/** Maps a point from model-input coordinates to original-image coordinates. */
|
|
1309
|
+
function scalePoint(point, options) {
|
|
1310
|
+
const { scaleX, scaleY, offsetX, offsetY } = resizeTransform(options);
|
|
1311
|
+
return {
|
|
1312
|
+
x: (point.x - offsetX) / scaleX,
|
|
1313
|
+
y: (point.y - offsetY) / scaleY
|
|
1314
|
+
};
|
|
1315
|
+
}
|
|
1316
|
+
//#endregion
|
|
1317
|
+
//#region src/zoo/tasks/cv/utils/imagePreprocessor.ts
|
|
1318
|
+
/** CPU image preprocessor: any ImageBuffer in, the model's RGB CHW f32 input
|
|
1319
|
+
* out. Resizes (stretch / letterbox / crop), converts the pixel format,
|
|
1320
|
+
* moves channels first and normalizes, all in one pass over the output. */
|
|
1321
|
+
/** Index of the R, G and B byte inside one pixel of each format. */
|
|
1322
|
+
const RGB_INDEX = {
|
|
1323
|
+
rgb: [
|
|
1324
|
+
0,
|
|
1325
|
+
1,
|
|
1326
|
+
2
|
|
1327
|
+
],
|
|
1328
|
+
rgba: [
|
|
1329
|
+
0,
|
|
1330
|
+
1,
|
|
1331
|
+
2
|
|
1332
|
+
],
|
|
1333
|
+
bgr: [
|
|
1334
|
+
2,
|
|
1335
|
+
1,
|
|
1336
|
+
0
|
|
1337
|
+
],
|
|
1338
|
+
bgra: [
|
|
1339
|
+
2,
|
|
1340
|
+
1,
|
|
1341
|
+
0
|
|
1342
|
+
],
|
|
1343
|
+
gray: [
|
|
1344
|
+
0,
|
|
1345
|
+
0,
|
|
1346
|
+
0
|
|
1347
|
+
]
|
|
1348
|
+
};
|
|
1349
|
+
function perChannel(v, name) {
|
|
1350
|
+
if (typeof v === "number") return [
|
|
1351
|
+
v,
|
|
1352
|
+
v,
|
|
1353
|
+
v
|
|
1354
|
+
];
|
|
1355
|
+
if (v.length !== 3) throw new RunntimeError("INVALID_ARGUMENT", `normalize.${name}: expected 3 values, got ${v.length}`);
|
|
1356
|
+
return [
|
|
1357
|
+
v[0],
|
|
1358
|
+
v[1],
|
|
1359
|
+
v[2]
|
|
1360
|
+
];
|
|
1361
|
+
}
|
|
1362
|
+
function buildAxis$1(outLen, srcLen, scale, offset, interpolation) {
|
|
1363
|
+
const i0 = new Int32Array(outLen);
|
|
1364
|
+
const i1 = new Int32Array(outLen);
|
|
1365
|
+
const w = new Float32Array(outLen);
|
|
1366
|
+
const inside = new Uint8Array(outLen);
|
|
1367
|
+
for (let d = 0; d < outLen; d++) {
|
|
1368
|
+
const s = (d + .5 - offset) / scale;
|
|
1369
|
+
if (s < 0 || s >= srcLen) continue;
|
|
1370
|
+
inside[d] = 1;
|
|
1371
|
+
if (interpolation === "nearest") {
|
|
1372
|
+
i0[d] = i1[d] = Math.min(srcLen - 1, Math.floor(s));
|
|
1373
|
+
continue;
|
|
1374
|
+
}
|
|
1375
|
+
const f = s - .5;
|
|
1376
|
+
const a = Math.floor(f);
|
|
1377
|
+
i0[d] = Math.max(0, a);
|
|
1378
|
+
i1[d] = Math.min(srcLen - 1, a + 1);
|
|
1379
|
+
w[d] = a < 0 ? 0 : f - a;
|
|
1380
|
+
}
|
|
1381
|
+
return {
|
|
1382
|
+
i0,
|
|
1383
|
+
i1,
|
|
1384
|
+
w,
|
|
1385
|
+
inside
|
|
1386
|
+
};
|
|
1387
|
+
}
|
|
1388
|
+
function createImagePreprocessor(options, size) {
|
|
1389
|
+
const { width, height, resizeMode } = {
|
|
1390
|
+
...size,
|
|
1391
|
+
resizeMode: options.resizeMode
|
|
1392
|
+
};
|
|
1393
|
+
checkImageSize(width, height, "input size");
|
|
1394
|
+
const interpolation = options.interpolation ?? "linear";
|
|
1395
|
+
const cropFraction = options.cropFraction ?? 1;
|
|
1396
|
+
if (!(cropFraction > 0 && cropFraction <= 1)) throw new RunntimeError("INVALID_ARGUMENT", `cropFraction: expected a number in (0, 1], got ${cropFraction}`);
|
|
1397
|
+
const padValue = options.padValue ?? 114;
|
|
1398
|
+
const alpha = perChannel(options.normalizeOpts?.alpha ?? 1 / 255, "alpha");
|
|
1399
|
+
const beta = perChannel(options.normalizeOpts?.beta ?? 0, "beta");
|
|
1400
|
+
const hw = width * height;
|
|
1401
|
+
const pad = [
|
|
1402
|
+
padValue * alpha[0] + beta[0],
|
|
1403
|
+
padValue * alpha[1] + beta[1],
|
|
1404
|
+
padValue * alpha[2] + beta[2]
|
|
1405
|
+
];
|
|
1406
|
+
const scaleOptions = (input) => ({
|
|
1407
|
+
from: {
|
|
1408
|
+
width,
|
|
1409
|
+
height
|
|
1410
|
+
},
|
|
1411
|
+
to: {
|
|
1412
|
+
width: input.width,
|
|
1413
|
+
height: input.height
|
|
1414
|
+
},
|
|
1415
|
+
resizeMode,
|
|
1416
|
+
cropFraction
|
|
1417
|
+
});
|
|
1418
|
+
const process = (input, out) => {
|
|
1419
|
+
out ??= new Float32Array(3 * hw);
|
|
1420
|
+
if (out.length !== 3 * hw) throw new RunntimeError("INVALID_ARGUMENT", `out: ${out.length} floats, expected ${3 * hw}`);
|
|
1421
|
+
checkImageBuffer(input);
|
|
1422
|
+
const ch = FORMAT_CHANNELS[input.format];
|
|
1423
|
+
const { data } = input;
|
|
1424
|
+
const [ri, gi, bi] = RGB_INDEX[input.format];
|
|
1425
|
+
const [ar, ag, ab] = alpha;
|
|
1426
|
+
const [br, bg, bb] = beta;
|
|
1427
|
+
const t = resizeTransform(scaleOptions(input));
|
|
1428
|
+
if (t.scaleX === 1 && t.scaleY === 1 && t.offsetX === 0 && t.offsetY === 0) {
|
|
1429
|
+
for (let i = 0; i < hw; i++) {
|
|
1430
|
+
const p = i * ch;
|
|
1431
|
+
out[i] = data[p + ri] * ar + br;
|
|
1432
|
+
out[hw + i] = data[p + gi] * ag + bg;
|
|
1433
|
+
out[2 * hw + i] = data[p + bi] * ab + bb;
|
|
1434
|
+
}
|
|
1435
|
+
return out;
|
|
1436
|
+
}
|
|
1437
|
+
const xs = buildAxis$1(width, input.width, t.scaleX, t.offsetX, interpolation);
|
|
1438
|
+
const ys = buildAxis$1(height, input.height, t.scaleY, t.offsetY, interpolation);
|
|
1439
|
+
const [padR, padG, padB] = pad;
|
|
1440
|
+
const rowStride = input.width * ch;
|
|
1441
|
+
for (let y = 0; y < height; y++) {
|
|
1442
|
+
const rowIn = ys.inside[y];
|
|
1443
|
+
const r0 = ys.i0[y] * rowStride;
|
|
1444
|
+
const r1 = ys.i1[y] * rowStride;
|
|
1445
|
+
const wy = ys.w[y];
|
|
1446
|
+
for (let x = 0; x < width; x++) {
|
|
1447
|
+
const o = y * width + x;
|
|
1448
|
+
if (!rowIn || !xs.inside[x]) {
|
|
1449
|
+
out[o] = padR;
|
|
1450
|
+
out[hw + o] = padG;
|
|
1451
|
+
out[2 * hw + o] = padB;
|
|
1452
|
+
continue;
|
|
1453
|
+
}
|
|
1454
|
+
const c0 = xs.i0[x] * ch;
|
|
1455
|
+
const c1 = xs.i1[x] * ch;
|
|
1456
|
+
const wx = xs.w[x];
|
|
1457
|
+
const w00 = (1 - wy) * (1 - wx);
|
|
1458
|
+
const w01 = (1 - wy) * wx;
|
|
1459
|
+
const w10 = wy * (1 - wx);
|
|
1460
|
+
const w11 = wy * wx;
|
|
1461
|
+
const r = data[r0 + c0 + ri] * w00 + data[r0 + c1 + ri] * w01 + data[r1 + c0 + ri] * w10 + data[r1 + c1 + ri] * w11;
|
|
1462
|
+
const g = data[r0 + c0 + gi] * w00 + data[r0 + c1 + gi] * w01 + data[r1 + c0 + gi] * w10 + data[r1 + c1 + gi] * w11;
|
|
1463
|
+
const b = data[r0 + c0 + bi] * w00 + data[r0 + c1 + bi] * w01 + data[r1 + c0 + bi] * w10 + data[r1 + c1 + bi] * w11;
|
|
1464
|
+
out[o] = r * ar + br;
|
|
1465
|
+
out[hw + o] = g * ag + bg;
|
|
1466
|
+
out[2 * hw + o] = b * ab + bb;
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
return out;
|
|
1470
|
+
};
|
|
1471
|
+
return {
|
|
1472
|
+
width,
|
|
1473
|
+
height,
|
|
1474
|
+
process,
|
|
1475
|
+
scaleOptions,
|
|
1476
|
+
gpuArgs: {
|
|
1477
|
+
mode: resizeMode,
|
|
1478
|
+
cropFraction,
|
|
1479
|
+
outW: width,
|
|
1480
|
+
outH: height,
|
|
1481
|
+
alpha,
|
|
1482
|
+
beta,
|
|
1483
|
+
pad
|
|
1484
|
+
}
|
|
1485
|
+
};
|
|
1486
|
+
}
|
|
1487
|
+
//#endregion
|
|
1488
|
+
//#region src/zoo/tasks/cv/utils/resizeBicubic.ts
|
|
1489
|
+
/** Antialiased bicubic resize on the CPU. Downscaling widens the kernel by
|
|
1490
|
+
* the scale, so every source pixel counts and fine detail does not
|
|
1491
|
+
* alias. */
|
|
1492
|
+
/** The a of Keys' cubic convolution kernel. */
|
|
1493
|
+
const CUBIC_A = -.5;
|
|
1494
|
+
function cubic(x) {
|
|
1495
|
+
const t = Math.abs(x);
|
|
1496
|
+
if (t < 1) return (1.5 * t - 2.5) * t * t + 1;
|
|
1497
|
+
if (t < 2) return (((t - 5) * t + 8) * t - 4) * CUBIC_A;
|
|
1498
|
+
return 0;
|
|
1499
|
+
}
|
|
1500
|
+
function buildAxis(outLen, srcLen) {
|
|
1501
|
+
const scale = srcLen / outLen;
|
|
1502
|
+
const support = 2 * Math.max(scale, 1);
|
|
1503
|
+
const inv = 1 / Math.max(scale, 1);
|
|
1504
|
+
const taps = Math.ceil(support) * 2 + 1;
|
|
1505
|
+
const start = new Int32Array(outLen);
|
|
1506
|
+
const count = new Int32Array(outLen);
|
|
1507
|
+
const weights = new Float32Array(outLen * taps);
|
|
1508
|
+
for (let i = 0; i < outLen; i++) {
|
|
1509
|
+
const center = scale * (i + .5);
|
|
1510
|
+
const lo = Math.max(Math.trunc(center - support + .5), 0);
|
|
1511
|
+
const n = Math.min(Math.trunc(center + support + .5), srcLen) - lo;
|
|
1512
|
+
let total = 0;
|
|
1513
|
+
for (let j = 0; j < n; j++) {
|
|
1514
|
+
const w = cubic((j + lo - center + .5) * inv);
|
|
1515
|
+
weights[i * taps + j] = w;
|
|
1516
|
+
total += w;
|
|
1517
|
+
}
|
|
1518
|
+
if (total !== 0) for (let j = 0; j < n; j++) weights[i * taps + j] /= total;
|
|
1519
|
+
start[i] = lo;
|
|
1520
|
+
count[i] = n;
|
|
1521
|
+
}
|
|
1522
|
+
return {
|
|
1523
|
+
start,
|
|
1524
|
+
count,
|
|
1525
|
+
weights,
|
|
1526
|
+
taps
|
|
1527
|
+
};
|
|
1528
|
+
}
|
|
1529
|
+
/** `image` resized to width × height: RGB [3, height, width] of whole
|
|
1530
|
+
* values in 0..255. Width resizes first, then height, rounded to bytes in
|
|
1531
|
+
* between. Throws INVALID_ARGUMENT for a bad size or byte count. */
|
|
1532
|
+
function resizeBicubic(image, width, height) {
|
|
1533
|
+
checkImageSize(width, height, "resize");
|
|
1534
|
+
checkImageBuffer(image);
|
|
1535
|
+
const ch = FORMAT_CHANNELS[image.format];
|
|
1536
|
+
const { data } = image;
|
|
1537
|
+
const rgb = RGB_INDEX[image.format];
|
|
1538
|
+
const srcW = image.width;
|
|
1539
|
+
const srcH = image.height;
|
|
1540
|
+
const xs = buildAxis(width, srcW);
|
|
1541
|
+
const ys = buildAxis(height, srcH);
|
|
1542
|
+
const plane = srcH * width;
|
|
1543
|
+
const tmp = new Uint8ClampedArray(3 * plane);
|
|
1544
|
+
const [ri, gi, bi] = rgb;
|
|
1545
|
+
for (let y = 0; y < srcH; y++) for (let x = 0; x < width; x++) {
|
|
1546
|
+
const n = xs.count[x];
|
|
1547
|
+
const w0 = x * xs.taps;
|
|
1548
|
+
let p = (y * srcW + xs.start[x]) * ch;
|
|
1549
|
+
let r = 0;
|
|
1550
|
+
let g = 0;
|
|
1551
|
+
let b = 0;
|
|
1552
|
+
for (let j = 0; j < n; j++, p += ch) {
|
|
1553
|
+
const w = xs.weights[w0 + j];
|
|
1554
|
+
r += data[p + ri] * w;
|
|
1555
|
+
g += data[p + gi] * w;
|
|
1556
|
+
b += data[p + bi] * w;
|
|
1557
|
+
}
|
|
1558
|
+
const o = y * width + x;
|
|
1559
|
+
tmp[o] = r;
|
|
1560
|
+
tmp[plane + o] = g;
|
|
1561
|
+
tmp[2 * plane + o] = b;
|
|
1562
|
+
}
|
|
1563
|
+
const out = new Float32Array(3 * height * width);
|
|
1564
|
+
const acc = new Float64Array(width);
|
|
1565
|
+
for (let c = 0; c < 3; c++) for (let y = 0; y < height; y++) {
|
|
1566
|
+
acc.fill(0);
|
|
1567
|
+
const w0 = y * ys.taps;
|
|
1568
|
+
for (let j = 0; j < ys.count[y]; j++) {
|
|
1569
|
+
const w = ys.weights[w0 + j];
|
|
1570
|
+
const src = (c * srcH + ys.start[y] + j) * width;
|
|
1571
|
+
for (let x = 0; x < width; x++) acc[x] += tmp[src + x] * w;
|
|
1572
|
+
}
|
|
1573
|
+
const dst = (c * height + y) * width;
|
|
1574
|
+
for (let x = 0; x < width; x++) out[dst + x] = Math.min(255, Math.max(0, Math.round(acc[x])));
|
|
1575
|
+
}
|
|
1576
|
+
return out;
|
|
1577
|
+
}
|
|
1578
|
+
//#endregion
|
|
1579
|
+
//#region src/zoo/embeddinggemma2/visionEncoder.ts
|
|
1580
|
+
/** EmbeddingGemma 2 image encoding: an image in, its token rows for the
|
|
1581
|
+
* text model out, one eager forward of the vision tower per image. */
|
|
1582
|
+
/** Patch grid [cols, rows] of the largest resize, aspect ratio kept, that
|
|
1583
|
+
* fits `maxTokens` image tokens with each side a multiple of pool ·
|
|
1584
|
+
* patch pixels. Throws INVALID_ARGUMENT unless the size is positive. */
|
|
1585
|
+
function patchGrid(width, height, maxTokens, patch, pool) {
|
|
1586
|
+
checkImageSize(width, height);
|
|
1587
|
+
const maxPatches = maxTokens * pool * pool;
|
|
1588
|
+
const factor = Math.sqrt(maxPatches * patch * patch / (width * height));
|
|
1589
|
+
const side = pool * patch;
|
|
1590
|
+
let w = Math.floor(factor * width / side) * side;
|
|
1591
|
+
let h = Math.floor(factor * height / side) * side;
|
|
1592
|
+
const maxSide = maxTokens * side;
|
|
1593
|
+
if (h === 0) {
|
|
1594
|
+
h = side;
|
|
1595
|
+
w = Math.min(Math.floor(width / height) * side, maxSide);
|
|
1596
|
+
} else if (w === 0) {
|
|
1597
|
+
w = side;
|
|
1598
|
+
h = Math.min(Math.floor(height / width) * side, maxSide);
|
|
1599
|
+
}
|
|
1600
|
+
return [w / patch, h / patch];
|
|
1601
|
+
}
|
|
1602
|
+
/** Pixels [cols · rows, 3·patch²] in [-1, 1], patches in row-major order,
|
|
1603
|
+
* each patch's pixels in (row, column, channel) order. */
|
|
1604
|
+
function patchPixels(image, cols, rows, patch) {
|
|
1605
|
+
const width = cols * patch;
|
|
1606
|
+
const height = rows * patch;
|
|
1607
|
+
const chw = resizeBicubic(image, width, height);
|
|
1608
|
+
const scaled = (v) => v * 2 / 255 - 1;
|
|
1609
|
+
const plane = width * height;
|
|
1610
|
+
const out = new Float32Array(3 * plane);
|
|
1611
|
+
let o = 0;
|
|
1612
|
+
for (let py = 0; py < rows; py++) for (let px = 0; px < cols; px++) for (let y = py * patch; y < (py + 1) * patch; y++) for (let x = px * patch; x < (px + 1) * patch; x++) {
|
|
1613
|
+
const i = y * width + x;
|
|
1614
|
+
out[o++] = scaled(chw[i]);
|
|
1615
|
+
out[o++] = scaled(chw[plane + i]);
|
|
1616
|
+
out[o++] = scaled(chw[2 * plane + i]);
|
|
1617
|
+
}
|
|
1618
|
+
return out;
|
|
1619
|
+
}
|
|
1620
|
+
/** Pooling weights [N, P]: each image token averages its pool×pool square
|
|
1621
|
+
* of patches. Tokens are in row-major order over the squares. */
|
|
1622
|
+
function poolWeights(cols, rows, pool) {
|
|
1623
|
+
const tokenCols = cols / pool;
|
|
1624
|
+
const n = tokenCols * (rows / pool);
|
|
1625
|
+
const out = new Float32Array(n * cols * rows);
|
|
1626
|
+
for (let k = 0; k < cols * rows; k++) {
|
|
1627
|
+
const token = Math.floor(k % cols / pool) + tokenCols * Math.floor(Math.floor(k / cols) / pool);
|
|
1628
|
+
out[token * cols * rows + k] = 1 / (pool * pool);
|
|
1629
|
+
}
|
|
1630
|
+
return out;
|
|
1631
|
+
}
|
|
1632
|
+
/** An image to its token rows [N · text hidden], N at most `maxTokens`.
|
|
1633
|
+
* Runs on `ex` and trims its pool after. Throws INVALID_ARGUMENT for a
|
|
1634
|
+
* malformed image. */
|
|
1635
|
+
async function encodeImage(model, image, maxTokens, ex) {
|
|
1636
|
+
const { patch, pool, headDim, ropeTheta } = model.cfg;
|
|
1637
|
+
const [cols, rows] = patchGrid(image.width, image.height, maxTokens, patch, pool);
|
|
1638
|
+
const p = cols * rows;
|
|
1639
|
+
const act = model.input_proj.weight.shape.dtype === "f16" ? "f16" : "f32";
|
|
1640
|
+
const upload = (data, r, dtype) => tensor(data, {
|
|
1641
|
+
elems: data.length,
|
|
1642
|
+
dtype,
|
|
1643
|
+
dims: [r, data.length / r]
|
|
1644
|
+
});
|
|
1645
|
+
const xs = new Float32Array(p);
|
|
1646
|
+
const ys = new Float32Array(p);
|
|
1647
|
+
for (let k = 0; k < p; k++) {
|
|
1648
|
+
xs[k] = k % cols;
|
|
1649
|
+
ys[k] = Math.floor(k / cols);
|
|
1650
|
+
}
|
|
1651
|
+
const { cos, sin } = visionRopeTables(cols, rows, headDim, ropeTheta);
|
|
1652
|
+
const inputs = {
|
|
1653
|
+
patches: upload(patchPixels(image, cols, rows, patch), p, act),
|
|
1654
|
+
xs: upload(xs, p, "f32"),
|
|
1655
|
+
ys: upload(ys, p, "f32"),
|
|
1656
|
+
cos: upload(cos, p, "f32"),
|
|
1657
|
+
sin: upload(sin, p, "f32"),
|
|
1658
|
+
pool: upload(poolWeights(cols, rows, pool), p / (pool * pool), act)
|
|
95
1659
|
};
|
|
1660
|
+
try {
|
|
1661
|
+
return await toArray(model.forward(inputs), ex);
|
|
1662
|
+
} finally {
|
|
1663
|
+
for (const v of Object.values(inputs)) v.buffer.destroy();
|
|
1664
|
+
ex.trim();
|
|
1665
|
+
}
|
|
96
1666
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
1667
|
+
//#endregion
|
|
1668
|
+
//#region src/zoo/embeddinggemma2/visionModel.ts
|
|
1669
|
+
/** EmbeddingGemma 2 vision tower: a bidirectional ViT over an image's 16×16
|
|
1670
|
+
* patches, each 3×3 square of patches averaged into one image token and
|
|
1671
|
+
* projected to the text model's width.
|
|
1672
|
+
*
|
|
1673
|
+
* Layers are the text model's without PLE. Positions enter twice: x and y
|
|
1674
|
+
* embeddings on the patches and a 2D rope on q and k. q, k and v are
|
|
1675
|
+
* RMS-normed per head, so scores carry no 1/√d scale. */
|
|
1676
|
+
/** Patch count from which attention runs on matmuls. */
|
|
1677
|
+
const MATMUL_ATTENTION_MIN_PATCHES = 2560;
|
|
1678
|
+
/** Most scores one matmul writes: 128 MiB of f32, WebGPU's default
|
|
1679
|
+
* storage binding limit. Larger images split their queries into equal
|
|
1680
|
+
* chunks. */
|
|
1681
|
+
const MAX_SCORES = 2 ** 25;
|
|
1682
|
+
var VisionLayer = class extends Module {
|
|
1683
|
+
cfg;
|
|
1684
|
+
input_layernorm;
|
|
1685
|
+
/** q | k | v rows, with q and k permuted for the interleaved rope. */
|
|
1686
|
+
qkv;
|
|
1687
|
+
/** q_norm and k_norm tiled per head, ones for v. */
|
|
1688
|
+
qkv_norm;
|
|
1689
|
+
o_proj;
|
|
1690
|
+
post_attention_layernorm;
|
|
1691
|
+
pre_feedforward_layernorm;
|
|
1692
|
+
gate_proj;
|
|
1693
|
+
up_proj;
|
|
1694
|
+
down_proj;
|
|
1695
|
+
post_feedforward_layernorm;
|
|
1696
|
+
constructor(cfg) {
|
|
1697
|
+
super();
|
|
1698
|
+
this.cfg = cfg;
|
|
1699
|
+
const { hidden, heads, headDim, ffn, eps } = cfg;
|
|
1700
|
+
const width = 3 * heads * headDim;
|
|
1701
|
+
const norm = () => new RMSNorm(hidden, eps);
|
|
1702
|
+
this.input_layernorm = norm();
|
|
1703
|
+
this.qkv = new Linear(hidden, width, { bias: false });
|
|
1704
|
+
this.qkv_norm = new Parameter({
|
|
1705
|
+
elems: width,
|
|
1706
|
+
dtype: "f32",
|
|
1707
|
+
dims: [width]
|
|
1708
|
+
});
|
|
1709
|
+
this.o_proj = new Linear(heads * headDim, hidden, { bias: false });
|
|
1710
|
+
this.post_attention_layernorm = norm();
|
|
1711
|
+
this.pre_feedforward_layernorm = norm();
|
|
1712
|
+
this.gate_proj = new Linear(hidden, ffn, {
|
|
1713
|
+
bias: false,
|
|
1714
|
+
activation: "geluTanh"
|
|
1715
|
+
});
|
|
1716
|
+
this.up_proj = new Linear(hidden, ffn, { bias: false });
|
|
1717
|
+
this.down_proj = new Linear(ffn, hidden, { bias: false });
|
|
1718
|
+
this.post_feedforward_layernorm = norm();
|
|
1719
|
+
}
|
|
1720
|
+
forward(x, inp) {
|
|
1721
|
+
const { hidden, heads, headDim: d, eps } = this.cfg;
|
|
1722
|
+
const w = heads * d;
|
|
1723
|
+
const postNorm = (norm, y, residual) => rmsNorm(y, norm.scale.value, norm.eps, { residual });
|
|
1724
|
+
const qkv = this.qkv.forward(this.input_layernorm.forward(x));
|
|
1725
|
+
const n = rmsNorm(qkv, this.qkv_norm.value, eps, { group: d });
|
|
1726
|
+
const qk = rope(n, inp.cos, inp.sin, {
|
|
1727
|
+
headDim: d,
|
|
1728
|
+
width: 2 * w
|
|
1729
|
+
});
|
|
1730
|
+
const head = (v, start) => slice(v, 1, start, start + d);
|
|
1731
|
+
const p = x.shape.elems / hidden;
|
|
1732
|
+
let a;
|
|
1733
|
+
if (p >= MATMUL_ATTENTION_MIN_PATCHES) {
|
|
1734
|
+
const kv = Array.from({ length: heads }, (_, h) => ({
|
|
1735
|
+
kT: transpose(head(qk, w + h * d)),
|
|
1736
|
+
v: head(n, 2 * w + h * d)
|
|
1737
|
+
}));
|
|
1738
|
+
const parts = Math.ceil(p * p / MAX_SCORES);
|
|
1739
|
+
const step = Math.ceil(p / parts);
|
|
1740
|
+
const chunks = [];
|
|
1741
|
+
for (let r = 0; r < p; r += step) {
|
|
1742
|
+
const q = slice(qk, 0, r, Math.min(r + step, p));
|
|
1743
|
+
const outs = kv.map(({ kT, v }, h) => matmul(softmax(matmul(head(q, h * d), kT)), v));
|
|
1744
|
+
chunks.push(cat(outs, 1));
|
|
1745
|
+
}
|
|
1746
|
+
a = cat(chunks, 0);
|
|
1747
|
+
} else a = sdpa(slice(qk, 1, 0, w), slice(qk, 1, w, 2 * w), slice(n, 1, 2 * w, 3 * w), {
|
|
1748
|
+
qHeads: heads,
|
|
1749
|
+
kvHeads: heads,
|
|
1750
|
+
headDim: d,
|
|
1751
|
+
windowLeft: Infinity,
|
|
1752
|
+
windowRight: Infinity
|
|
1753
|
+
});
|
|
1754
|
+
x = postNorm(this.post_attention_layernorm, this.o_proj.forward(a), x);
|
|
1755
|
+
const f = this.pre_feedforward_layernorm.forward(x);
|
|
1756
|
+
const m = this.down_proj.forward(mul(this.gate_proj.forward(f), this.up_proj.forward(f)));
|
|
1757
|
+
return postNorm(this.post_feedforward_layernorm, m, x);
|
|
1758
|
+
}
|
|
1759
|
+
};
|
|
1760
|
+
var EmbeddingGemma2VisionModel = class extends Module {
|
|
1761
|
+
cfg;
|
|
1762
|
+
input_proj;
|
|
1763
|
+
pos_x;
|
|
1764
|
+
pos_y;
|
|
1765
|
+
layers;
|
|
1766
|
+
/** The pooled tokens enter this weightless norm scaled by √hidden. The
|
|
1767
|
+
* norm cancels the scale, so eps divided by hidden stands in for it. */
|
|
1768
|
+
embedding_pre_projection_norm;
|
|
1769
|
+
embedding_projection;
|
|
1770
|
+
constructor(cfg = EMBEDDINGGEMMA2_VISION) {
|
|
1771
|
+
super();
|
|
1772
|
+
this.cfg = cfg;
|
|
1773
|
+
const { hidden, patch, positions, eps } = cfg;
|
|
1774
|
+
this.input_proj = new Linear(3 * patch * patch, hidden, { bias: false });
|
|
1775
|
+
this.pos_x = new Embedding(positions, hidden);
|
|
1776
|
+
this.pos_y = new Embedding(positions, hidden);
|
|
1777
|
+
this.layers = new ModuleList(Array.from({ length: cfg.layers }, () => new VisionLayer(cfg)));
|
|
1778
|
+
this.embedding_pre_projection_norm = new RMSNorm(hidden, eps / hidden);
|
|
1779
|
+
this.embedding_projection = new Linear(hidden, EMBEDDINGGEMMA2_TEXT.hidden, { bias: false });
|
|
1780
|
+
}
|
|
1781
|
+
/** One image to its tokens [N, text hidden], ready to stand in for N
|
|
1782
|
+
* placeholder rows of the text model's input. */
|
|
1783
|
+
forward(inp) {
|
|
1784
|
+
const addend = add(this.pos_x.forward(inp.xs), this.pos_y.forward(inp.ys));
|
|
1785
|
+
let x = this.input_proj.forward(inp.patches, { addend });
|
|
1786
|
+
for (const layer of this.layers.items) x = layer.forward(x, inp);
|
|
1787
|
+
const pooled = matmul(inp.pool, x);
|
|
1788
|
+
return this.embedding_projection.forward(this.embedding_pre_projection_norm.forward(pooled));
|
|
1789
|
+
}
|
|
1790
|
+
transformStateDict(sd, prefix) {
|
|
1791
|
+
transformEmbeddingGemma2VisionStateDict(sd, prefix, this.cfg);
|
|
1792
|
+
}
|
|
104
1793
|
};
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
127
|
-
return words;
|
|
128
|
-
}
|
|
129
|
-
function createMinilmTokenizer(asset) {
|
|
130
|
-
if (asset.version !== 1) throw new Error(`minilm tokenizer: unsupported asset version ${asset.version}`);
|
|
131
|
-
const tokenToId = /* @__PURE__ */ new Map();
|
|
132
|
-
asset.vocab.forEach((token, id) => tokenToId.set(token, id));
|
|
133
|
-
const wordpiece = (word, out) => {
|
|
134
|
-
const chars = [...word];
|
|
135
|
-
if (chars.length > MAX_WORD_CHARS) {
|
|
136
|
-
out.push(asset.unkId);
|
|
137
|
-
return;
|
|
1794
|
+
//#endregion
|
|
1795
|
+
//#region src/zoo/embeddinggemma2/embeddingModel.ts
|
|
1796
|
+
/** EmbeddingGemma 2 for createEmbedder: text, images, video and audio,
|
|
1797
|
+
* task instructions, 768 values or fewer with `dims`. */
|
|
1798
|
+
async function createEmbeddingModel$1(load) {
|
|
1799
|
+
const { maxTokens = EMBEDDINGGEMMA2_MAX_TOKENS, imageTokens = 280 } = load;
|
|
1800
|
+
if (!Number.isInteger(maxTokens) || maxTokens < 2 || maxTokens > 2048) throw new RunntimeError("INVALID_ARGUMENT", `createEmbedder: maxTokens must be an integer from 2 to ${EMBEDDINGGEMMA2_MAX_TOKENS}, got ${maxTokens}`);
|
|
1801
|
+
const budgets = IMAGE_TOKEN_BUDGETS;
|
|
1802
|
+
if (!budgets.includes(imageTokens)) throw new RunntimeError("INVALID_ARGUMENT", `createEmbedder: imageTokens must be one of ${budgets.join(", ")}, got ${imageTokens}`);
|
|
1803
|
+
const tokenizer = createEmbeddingGemma2Tokenizer(load.tokenizer);
|
|
1804
|
+
const scope = createResourceScope();
|
|
1805
|
+
try {
|
|
1806
|
+
const model = scope.track(new EmbeddingGemma2Model());
|
|
1807
|
+
const vision = load.vision && scope.track(new EmbeddingGemma2VisionModel());
|
|
1808
|
+
const audio = load.audio && scope.track(new EmbeddingGemma2AudioModel());
|
|
1809
|
+
const visionEx = scope.track(gpuExecutor(defaultRoot()));
|
|
1810
|
+
const audioEx = scope.track(gpuExecutor(defaultRoot()));
|
|
1811
|
+
if (load.dtype === "f16") {
|
|
1812
|
+
model.half();
|
|
1813
|
+
vision?.half();
|
|
1814
|
+
audio?.half();
|
|
138
1815
|
}
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
let
|
|
143
|
-
let
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
1816
|
+
const progress = /* @__PURE__ */ new Map();
|
|
1817
|
+
const track = (file) => (name, done, total) => {
|
|
1818
|
+
progress.set(file, [done, total]);
|
|
1819
|
+
let d = 0;
|
|
1820
|
+
let t = 0;
|
|
1821
|
+
for (const [fd, ft] of progress.values()) {
|
|
1822
|
+
d += fd;
|
|
1823
|
+
t += ft;
|
|
1824
|
+
}
|
|
1825
|
+
load.onProgress?.(name, d, t);
|
|
1826
|
+
};
|
|
1827
|
+
const loads = [model.loadStateDict(load.text, { onProgress: track("text") })];
|
|
1828
|
+
if (vision && load.vision) loads.push(vision.loadStateDict(load.vision, { onProgress: track("vision") }));
|
|
1829
|
+
if (audio && load.audio) loads.push(audio.loadStateDict(load.audio, { onProgress: track("audio") }));
|
|
1830
|
+
await Promise.all(loads);
|
|
1831
|
+
const core = scope.track(createEmbeddingGemma2EmbedCore(model));
|
|
1832
|
+
const { bosId, eosId, padId, boiId, eoiId, boaId, eoaId } = tokenizer;
|
|
1833
|
+
const { patch, pool } = EMBEDDINGGEMMA2_VISION;
|
|
1834
|
+
/** An input as token ids and media rows: <bos>, the instruction and the
|
|
1835
|
+
* parts in order, <eos>. Text next to other text joins it, each image
|
|
1836
|
+
* or video frame is <|image> rows <image|>, and each clip <|audio> rows
|
|
1837
|
+
* <audio|>. The length is checked before any tower runs. */
|
|
1838
|
+
const toInput = async (where, input, task) => {
|
|
1839
|
+
const parts = Array.isArray(input) ? input : [input];
|
|
1840
|
+
const ids = [bosId];
|
|
1841
|
+
const pending = [];
|
|
1842
|
+
let text = task === void 0 ? "" : EMBEDDINGGEMMA2_PROMPTS[task];
|
|
1843
|
+
const flush = () => {
|
|
1844
|
+
ids.push(...tokenizer.encode(text));
|
|
1845
|
+
text = "";
|
|
1846
|
+
};
|
|
1847
|
+
const addMedia = (begin, tokens, end, encode) => {
|
|
1848
|
+
flush();
|
|
1849
|
+
ids.push(begin);
|
|
1850
|
+
pending.push({
|
|
1851
|
+
at: ids.length,
|
|
1852
|
+
encode
|
|
1853
|
+
});
|
|
1854
|
+
for (let i = 0; i < tokens; i++) ids.push(padId);
|
|
1855
|
+
ids.push(end);
|
|
1856
|
+
};
|
|
1857
|
+
const addImage = (image, budget) => {
|
|
1858
|
+
const [cols, rows] = patchGrid(image.width, image.height, budget, patch, pool);
|
|
1859
|
+
addMedia(boiId, cols * rows / (pool * pool), eoiId, () => {
|
|
1860
|
+
if (!vision) throw new RunntimeError("INVALID_ARGUMENT", `${where}: images and video need the vision tower, which this embedder did not load`);
|
|
1861
|
+
return encodeImage(vision, image, budget, visionEx);
|
|
1862
|
+
});
|
|
1863
|
+
};
|
|
1864
|
+
for (const part of parts) if (typeof part === "string") text += part;
|
|
1865
|
+
else if ("image" in part) addImage(part.image, imageTokens);
|
|
1866
|
+
else if ("video" in part) for (const frame of part.video) addImage(frame, 140);
|
|
1867
|
+
else {
|
|
1868
|
+
const samples = part.audio;
|
|
1869
|
+
addMedia(boaId, audioTokens(samples.length), eoaId, () => {
|
|
1870
|
+
if (!audio) throw new RunntimeError("INVALID_ARGUMENT", `${where}: audio needs the audio tower, which this embedder did not load`);
|
|
1871
|
+
return encodeAudio(audio, samples, audioEx);
|
|
1872
|
+
});
|
|
149
1873
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
1874
|
+
flush();
|
|
1875
|
+
ids.push(eosId);
|
|
1876
|
+
if (ids.length > maxTokens) {
|
|
1877
|
+
if (pending.length === 0) return {
|
|
1878
|
+
ids: [...ids.slice(0, maxTokens - 1), eosId],
|
|
1879
|
+
media: []
|
|
1880
|
+
};
|
|
1881
|
+
throw new RunntimeError("INVALID_ARGUMENT", `${where}: the input takes ${ids.length} tokens, more than maxTokens ${maxTokens}`);
|
|
153
1882
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
1883
|
+
const media = [];
|
|
1884
|
+
for (const { at, encode } of pending) media.push({
|
|
1885
|
+
at,
|
|
1886
|
+
rows: await encode()
|
|
1887
|
+
});
|
|
1888
|
+
return {
|
|
1889
|
+
ids,
|
|
1890
|
+
media
|
|
1891
|
+
};
|
|
1892
|
+
};
|
|
1893
|
+
return {
|
|
1894
|
+
dim: EMBEDDING_DIM,
|
|
1895
|
+
dims: EMBEDDING_DIMS,
|
|
1896
|
+
media: true,
|
|
1897
|
+
async warm() {
|
|
1898
|
+
const probe = await toInput("createEmbedder", "", void 0);
|
|
1899
|
+
await core.embedPooledBatch([probe, probe]);
|
|
1900
|
+
await core.warmBuckets(probe);
|
|
1901
|
+
if (vision) {
|
|
1902
|
+
const dot = imageBuffer(/* @__PURE__ */ new Uint8Array(4), 1, 1);
|
|
1903
|
+
await encodeImage(vision, dot, imageTokens, visionEx);
|
|
1904
|
+
if (imageTokens !== 140) await encodeImage(vision, dot, 140, visionEx);
|
|
1905
|
+
}
|
|
1906
|
+
if (audio) await encodeAudio(audio, /* @__PURE__ */ new Float32Array(1600), audioEx);
|
|
1907
|
+
return core.embedPooled(probe);
|
|
1908
|
+
},
|
|
1909
|
+
async pool(where, inputs, task) {
|
|
1910
|
+
const prepared = [];
|
|
1911
|
+
for (const input of inputs) prepared.push(await toInput(where, input, task));
|
|
1912
|
+
return prepared.length === 1 ? core.embedPooled(prepared[0]) : core.embedPooledBatch(prepared);
|
|
1913
|
+
},
|
|
1914
|
+
dispose: () => scope.dispose()
|
|
1915
|
+
};
|
|
1916
|
+
} catch (err) {
|
|
1917
|
+
scope.dispose();
|
|
1918
|
+
throw err;
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
const isWordPieceTokenizerJson = (json) => typeof json === "object" && json !== null && "model" in json && typeof json.model === "object" && json.model !== null && "type" in json.model && json.model.type === "WordPiece";
|
|
1922
|
+
/** Builds the tokenizer from a Hugging Face tokenizer.json of the WordPiece
|
|
1923
|
+
* kind. Throws CHECKPOINT_MISMATCH for any other file. */
|
|
1924
|
+
function createMinilmTokenizer(json, maxTokens = 256) {
|
|
1925
|
+
const bad = (why) => checkpointMismatch("minilm tokenizer", why);
|
|
1926
|
+
if (!isWordPieceTokenizerJson(json)) throw bad("expected a WordPiece tokenizer.json");
|
|
1927
|
+
const tokenizer = new Tokenizer(json, {});
|
|
1928
|
+
const idOf = (token) => {
|
|
1929
|
+
const id = tokenizer.token_to_id(token);
|
|
1930
|
+
if (id === void 0) throw bad(`vocab has no ${token}`);
|
|
1931
|
+
return id;
|
|
158
1932
|
};
|
|
1933
|
+
const clsId = idOf("[CLS]");
|
|
1934
|
+
const sepId = idOf("[SEP]");
|
|
159
1935
|
return {
|
|
160
|
-
clsId
|
|
161
|
-
sepId
|
|
1936
|
+
clsId,
|
|
1937
|
+
sepId,
|
|
162
1938
|
encode(text) {
|
|
163
|
-
const pieces =
|
|
164
|
-
for (const word of basicTokenize(text, asset.doLowerCase)) wordpiece(word, pieces);
|
|
165
|
-
const body = pieces.slice(0, asset.maxWordpieces - 2);
|
|
1939
|
+
const pieces = tokenizer.encode(text, { add_special_tokens: false }).ids;
|
|
166
1940
|
return [
|
|
167
|
-
|
|
168
|
-
...
|
|
169
|
-
|
|
1941
|
+
clsId,
|
|
1942
|
+
...pieces.slice(0, maxTokens - 2),
|
|
1943
|
+
sepId
|
|
170
1944
|
];
|
|
171
1945
|
}
|
|
172
1946
|
};
|
|
173
1947
|
}
|
|
174
1948
|
//#endregion
|
|
175
|
-
//#region src/zoo/
|
|
176
|
-
/**
|
|
177
|
-
*
|
|
178
|
-
|
|
179
|
-
|
|
1949
|
+
//#region src/zoo/minilm/embeddingModel.ts
|
|
1950
|
+
/** all-MiniLM-L6-v2 for createEmbedder: English text only, no instructions,
|
|
1951
|
+
* 384 values. */
|
|
1952
|
+
async function createEmbeddingModel(load) {
|
|
1953
|
+
const { maxTokens } = load;
|
|
1954
|
+
const most = MINILM_L6.maxPositions;
|
|
1955
|
+
if (maxTokens !== void 0 && (!Number.isInteger(maxTokens) || maxTokens < 2 || maxTokens > most)) throw new RunntimeError("INVALID_ARGUMENT", `createEmbedder: maxTokens must be an integer from 2 to ${most}, got ${maxTokens}`);
|
|
1956
|
+
const tokenizer = createMinilmTokenizer(load.tokenizer, maxTokens);
|
|
1957
|
+
const scope = createResourceScope();
|
|
1958
|
+
try {
|
|
1959
|
+
const model = scope.track(new MinilmModel(MINILM_L6));
|
|
1960
|
+
if (load.dtype === "f16") model.half();
|
|
1961
|
+
await model.loadStateDict(load.text, { onProgress: load.onProgress });
|
|
1962
|
+
const core = scope.track(createMinilmEmbedCore(model));
|
|
1963
|
+
const pair = [tokenizer.clsId, tokenizer.sepId];
|
|
1964
|
+
return {
|
|
1965
|
+
dim: EMBEDDING_DIM$1,
|
|
1966
|
+
dims: [EMBEDDING_DIM$1],
|
|
1967
|
+
media: false,
|
|
1968
|
+
async warm() {
|
|
1969
|
+
await core.embedPooledBatch([pair, pair]);
|
|
1970
|
+
await core.warmBuckets(pair);
|
|
1971
|
+
return core.embedPooled(pair);
|
|
1972
|
+
},
|
|
1973
|
+
pool(_where, inputs) {
|
|
1974
|
+
const ids = inputs.map((input) => tokenizer.encode(typeof input === "string" ? input : input.join("")));
|
|
1975
|
+
return ids.length === 1 ? core.embedPooled(ids[0]) : core.embedPooledBatch(ids);
|
|
1976
|
+
},
|
|
1977
|
+
dispose: () => scope.dispose()
|
|
1978
|
+
};
|
|
1979
|
+
} catch (err) {
|
|
1980
|
+
scope.dispose();
|
|
1981
|
+
throw err;
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
//#endregion
|
|
1985
|
+
//#region src/zoo/tasks/multimodal/embedding.ts
|
|
1986
|
+
/** Embedding task: text, images, video and audio in, one unit-length vector
|
|
1987
|
+
* out, all in one space. Runs EmbeddingGemma 2 or all-MiniLM-L6-v2 on the
|
|
1988
|
+
* initRunntime() device. */
|
|
1989
|
+
/** Cosine similarity of two unit-length vectors: 1 for the same meaning,
|
|
1990
|
+
* near 0 for unrelated inputs. */
|
|
180
1991
|
function similarity(a, b) {
|
|
181
1992
|
let dot = 0;
|
|
182
1993
|
for (let i = 0; i < a.length; i++) dot += a[i] * b[i];
|
|
183
1994
|
return dot;
|
|
184
1995
|
}
|
|
1996
|
+
/** Scales `v` to unit length in place and returns it. */
|
|
185
1997
|
const normalize = (v) => {
|
|
186
1998
|
let sumSq = 0;
|
|
187
1999
|
for (const x of v) sumSq += x * x;
|
|
@@ -189,55 +2001,111 @@ const normalize = (v) => {
|
|
|
189
2001
|
for (let i = 0; i < v.length; i++) v[i] *= inv;
|
|
190
2002
|
return v;
|
|
191
2003
|
};
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
2004
|
+
const isImagePart = (part) => typeof part === "object" && part !== null && "image" in part;
|
|
2005
|
+
const isVideoPart = (part) => typeof part === "object" && part !== null && "video" in part && Array.isArray(part.video);
|
|
2006
|
+
const isAudioPart = (part) => typeof part === "object" && part !== null && "audio" in part && part.audio instanceof Float32Array;
|
|
2007
|
+
/** One cache key per file, using the whole path so same-named files of two
|
|
2008
|
+
* precisions stay apart. Bump the version to invalidate. */
|
|
2009
|
+
const cacheId = (path) => typeof path === "string" ? `embedding-v1/${path.replace(/^https?:\/\//, "")}` : void 0;
|
|
2010
|
+
/** Loads the model in `modelPath` with its tokenizer and towers, warms the
|
|
2011
|
+
* GPU, and returns the runner. With no config, loads EmbeddingGemma 2 from
|
|
2012
|
+
* the Hugging Face Hub. */
|
|
2013
|
+
async function createEmbedder(config = {}, opts = {}) {
|
|
196
2014
|
const scope = createResourceScope();
|
|
197
2015
|
try {
|
|
198
2016
|
const device = defaultRoot().device;
|
|
199
|
-
const
|
|
200
|
-
const
|
|
201
|
-
const sd = await openWeights(modelPath, {
|
|
2017
|
+
const modelPath = config.modelPath ?? models.embedding.EMBEDDINGGEMMA_2.DEFAULT.modelPath;
|
|
2018
|
+
const text = await openWeights(modelPath, {
|
|
202
2019
|
...opts,
|
|
203
2020
|
cacheId: opts.cacheId ?? cacheId(modelPath)
|
|
204
2021
|
});
|
|
205
|
-
const
|
|
2022
|
+
const minilm = text.tensors.has("embeddings.word_embeddings.weight");
|
|
2023
|
+
const defaults = minilm ? {
|
|
2024
|
+
...models.embedding.ALL_MINILM_L6_V2.DEFAULT,
|
|
2025
|
+
visionPath: null,
|
|
2026
|
+
audioPath: null
|
|
2027
|
+
} : models.embedding.EMBEDDINGGEMMA_2.DEFAULT;
|
|
2028
|
+
const { visionPath = defaults.visionPath, audioPath = defaults.audioPath, tokenizerPath = defaults.tokenizerPath, dtype = supportsF16() ? "f16" : "f32", maxTokens, imageTokens } = config;
|
|
2029
|
+
if (minilm && (visionPath !== null || audioPath !== null)) throw new RunntimeError("INVALID_ARGUMENT", "createEmbedder: all-MiniLM-L6-v2 has no vision or audio tower, set no visionPath or audioPath");
|
|
2030
|
+
const open = (path) => path === null ? void 0 : openWeights(path, {
|
|
2031
|
+
...opts,
|
|
2032
|
+
cacheId: cacheId(path)
|
|
2033
|
+
});
|
|
2034
|
+
const vision = await open(visionPath);
|
|
2035
|
+
const audio = await open(audioPath);
|
|
2036
|
+
const tokenizer = await fetchJson(tokenizerPath, {
|
|
206
2037
|
...opts,
|
|
207
2038
|
cacheId: cacheId(tokenizerPath)
|
|
208
|
-
})
|
|
2039
|
+
});
|
|
209
2040
|
throwIfAborted(opts.signal);
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
2041
|
+
const make = minilm ? createEmbeddingModel : createEmbeddingModel$1;
|
|
2042
|
+
const model = scope.track(await make({
|
|
2043
|
+
text,
|
|
2044
|
+
vision,
|
|
2045
|
+
audio,
|
|
2046
|
+
tokenizer,
|
|
2047
|
+
dtype,
|
|
2048
|
+
maxTokens,
|
|
2049
|
+
imageTokens,
|
|
2050
|
+
onProgress: opts.onProgress
|
|
2051
|
+
}));
|
|
2052
|
+
throwIfAborted(opts.signal);
|
|
2053
|
+
await warmUp(device, () => model.warm());
|
|
214
2054
|
let disposed = false;
|
|
215
2055
|
const guard = () => {
|
|
216
|
-
if (disposed) throw new
|
|
2056
|
+
if (disposed) throw new RunntimeError("RESOURCE_DISPOSED", "embedder is disposed");
|
|
217
2057
|
};
|
|
218
|
-
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
if (
|
|
222
|
-
const
|
|
223
|
-
|
|
2058
|
+
/** Throws unless `input` is an EmbedInput this runner can embed. */
|
|
2059
|
+
const check = (where, input) => {
|
|
2060
|
+
const parts = Array.isArray(input) ? input : [input];
|
|
2061
|
+
if (parts.length === 0) throw new RunntimeError("INVALID_ARGUMENT", `${where}: an input needs at least one part`);
|
|
2062
|
+
for (const part of parts) {
|
|
2063
|
+
if (typeof part === "string") continue;
|
|
2064
|
+
if (!model.media) throw new RunntimeError("INVALID_ARGUMENT", `${where}: this model reads only text, a part must be a string`);
|
|
2065
|
+
if (isAudioPart(part)) {
|
|
2066
|
+
audioTokens(part.audio.length);
|
|
2067
|
+
continue;
|
|
2068
|
+
}
|
|
2069
|
+
if (!isImagePart(part) && !(isVideoPart(part) && part.video.length > 0)) throw new RunntimeError("INVALID_ARGUMENT", `${where}: a part must be a string, { image }, { video } with at least one frame or { audio } as a Float32Array`);
|
|
2070
|
+
if (isImagePart(part)) checkImageBuffer(part.image);
|
|
2071
|
+
else for (const frame of part.video) checkImageBuffer(frame);
|
|
2072
|
+
}
|
|
2073
|
+
};
|
|
2074
|
+
const checkTask = (where, task) => {
|
|
2075
|
+
if (task !== void 0 && !Object.hasOwn(EMBEDDINGGEMMA2_PROMPTS, String(task))) throw new RunntimeError("INVALID_ARGUMENT", `${where}: task must be one of ${Object.keys(EMBEDDINGGEMMA2_PROMPTS).join(", ")}, got ${task}`);
|
|
2076
|
+
};
|
|
2077
|
+
const checkDims = (where, dims) => {
|
|
2078
|
+
if (dims !== void 0 && (typeof dims !== "number" || !model.dims.includes(dims))) throw new RunntimeError("INVALID_ARGUMENT", `${where}: dims must be one of ${model.dims.join(", ")}, got ${dims}`);
|
|
2079
|
+
};
|
|
2080
|
+
/** Each input's vector: the first `dims` values of its pooled row, unit
|
|
2081
|
+
* length. */
|
|
2082
|
+
const embedInputs = async (where, inputs, { task, dims = model.dim }) => {
|
|
2083
|
+
if (inputs.length === 0) return [];
|
|
2084
|
+
const pooled = await model.pool(where, inputs, task);
|
|
2085
|
+
return inputs.map((_, b) => normalize(pooled.slice(b * model.dim, b * model.dim + dims)));
|
|
2086
|
+
};
|
|
2087
|
+
let queue = Promise.resolve();
|
|
2088
|
+
const serial = (work) => {
|
|
2089
|
+
const run = queue.then(work);
|
|
2090
|
+
queue = run.catch(() => void 0);
|
|
2091
|
+
return run.catch(rethrowRunError);
|
|
224
2092
|
};
|
|
225
|
-
throwIfAborted(opts.signal);
|
|
226
|
-
await warmUp(device, async () => {
|
|
227
|
-
const pair = [tokenizer.clsId, tokenizer.sepId];
|
|
228
|
-
await embedTokensBatch([pair, pair]);
|
|
229
|
-
await core.warmBuckets(pair);
|
|
230
|
-
return embedTokens(pair);
|
|
231
|
-
});
|
|
232
2093
|
return {
|
|
233
|
-
dim:
|
|
234
|
-
async embed(input) {
|
|
2094
|
+
dim: model.dim,
|
|
2095
|
+
async embed(input, embedOpts = {}) {
|
|
235
2096
|
guard();
|
|
236
|
-
|
|
2097
|
+
check("embed", input);
|
|
2098
|
+
checkTask("embed", embedOpts.task);
|
|
2099
|
+
checkDims("embed", embedOpts.dims);
|
|
2100
|
+
return serial(async () => (await embedInputs("embed", [input], embedOpts))[0]);
|
|
237
2101
|
},
|
|
238
|
-
async embedBatch(inputs) {
|
|
2102
|
+
async embedBatch(inputs, embedOpts = {}) {
|
|
239
2103
|
guard();
|
|
240
|
-
|
|
2104
|
+
if (!Array.isArray(inputs)) throw new RunntimeError("INVALID_ARGUMENT", "embedBatch: inputs must be an array");
|
|
2105
|
+
for (const input of inputs) check("embedBatch", input);
|
|
2106
|
+
checkTask("embedBatch", embedOpts.task);
|
|
2107
|
+
checkDims("embedBatch", embedOpts.dims);
|
|
2108
|
+
return serial(() => embedInputs("embedBatch", inputs, embedOpts));
|
|
241
2109
|
},
|
|
242
2110
|
dispose() {
|
|
243
2111
|
disposed = true;
|
|
@@ -246,7 +2114,7 @@ async function createTextEmbedder(config = {}, opts = {}) {
|
|
|
246
2114
|
};
|
|
247
2115
|
} catch (err) {
|
|
248
2116
|
scope.dispose();
|
|
249
|
-
throw err;
|
|
2117
|
+
throw asLoadError(err, opts.signal);
|
|
250
2118
|
}
|
|
251
2119
|
}
|
|
252
2120
|
//#endregion
|
|
@@ -269,11 +2137,11 @@ async function createPrivacyFilter(config = {}, opts = {}) {
|
|
|
269
2137
|
let queue = Promise.resolve();
|
|
270
2138
|
return {
|
|
271
2139
|
async detect(text) {
|
|
272
|
-
if (disposed) throw new
|
|
273
|
-
if (typeof text !== "string") throw new
|
|
2140
|
+
if (disposed) throw new RunntimeError("RESOURCE_DISPOSED", "privacy filter is disposed");
|
|
2141
|
+
if (typeof text !== "string") throw new RunntimeError("INVALID_ARGUMENT", "detect: text must be a string");
|
|
274
2142
|
const run = queue.then(() => detector.detect(text));
|
|
275
2143
|
queue = run.catch(() => void 0);
|
|
276
|
-
return (await run).spans;
|
|
2144
|
+
return (await run.catch(rethrowRunError)).spans;
|
|
277
2145
|
},
|
|
278
2146
|
dispose() {
|
|
279
2147
|
disposed = true;
|
|
@@ -282,7 +2150,7 @@ async function createPrivacyFilter(config = {}, opts = {}) {
|
|
|
282
2150
|
};
|
|
283
2151
|
} catch (err) {
|
|
284
2152
|
scope.dispose();
|
|
285
|
-
throw err;
|
|
2153
|
+
throw asLoadError(err, opts.signal);
|
|
286
2154
|
}
|
|
287
2155
|
}
|
|
288
2156
|
//#endregion
|
|
@@ -319,6 +2187,28 @@ function analyzeSegment(audio, opts) {
|
|
|
319
2187
|
};
|
|
320
2188
|
}
|
|
321
2189
|
//#endregion
|
|
2190
|
+
//#region src/zoo/moonshine/tokenizer.ts
|
|
2191
|
+
/** Moonshine tokenizer, decode-only: the decoder starts from BOS and emits
|
|
2192
|
+
* ids, so id → text is the whole job. tokenizers.js runs the Hub's Llama-style
|
|
2193
|
+
* BPE tokenizer.json with its special ids skipped and the text left as is. */
|
|
2194
|
+
const isBpeTokenizerJson = (json) => typeof json === "object" && json !== null && "model" in json && typeof json.model === "object" && json.model !== null && "type" in json.model && json.model.type === "BPE";
|
|
2195
|
+
/** Builds the tokenizer from a Hugging Face tokenizer.json of the BPE kind.
|
|
2196
|
+
* Throws CHECKPOINT_MISMATCH for any other file. */
|
|
2197
|
+
function createMoonshineTokenizer(json) {
|
|
2198
|
+
const bad = (why) => checkpointMismatch("moonshine tokenizer", why);
|
|
2199
|
+
if (!isBpeTokenizerJson(json)) throw bad("expected a BPE tokenizer.json");
|
|
2200
|
+
const tokenizer = new Tokenizer(json, {});
|
|
2201
|
+
const eosId = tokenizer.token_to_id("</s>");
|
|
2202
|
+
if (eosId === void 0) throw bad("vocab has no </s>");
|
|
2203
|
+
return {
|
|
2204
|
+
eosId,
|
|
2205
|
+
decode: (ids) => tokenizer.decode(ids, {
|
|
2206
|
+
skip_special_tokens: true,
|
|
2207
|
+
clean_up_tokenization_spaces: false
|
|
2208
|
+
})
|
|
2209
|
+
};
|
|
2210
|
+
}
|
|
2211
|
+
//#endregion
|
|
322
2212
|
//#region src/zoo/moonshine-streaming/config.ts
|
|
323
2213
|
const windows = (layers) => Object.freeze(Array.from({ length: layers }, (_, i) => i < 2 || i >= layers - 2 ? [16, 4] : [16, 0]));
|
|
324
2214
|
const MOONSHINE_STREAMING_TINY = Object.freeze({
|
|
@@ -402,7 +2292,7 @@ function presetFromStateDict(sd) {
|
|
|
402
2292
|
MOONSHINE_STREAMING_MEDIUM
|
|
403
2293
|
];
|
|
404
2294
|
for (const cfg of presets) if (cfg.dec.hidden === width) return cfg;
|
|
405
|
-
throw
|
|
2295
|
+
throw checkpointMismatch("moonshine-streaming weights", embedding === void 0 ? "no token embedding, not a Moonshine checkpoint" : `embedding width ${width} matches no known size`);
|
|
406
2296
|
}
|
|
407
2297
|
//#endregion
|
|
408
2298
|
//#region src/zoo/moonshine-streaming/frontend.ts
|
|
@@ -702,7 +2592,7 @@ const MAX_CHUNK_SECONDS = 30;
|
|
|
702
2592
|
const SPLIT_SEARCH_SECONDS = 5;
|
|
703
2593
|
async function createTranscriber(sd, tokenizer, opts = {}) {
|
|
704
2594
|
const root = defaultRoot();
|
|
705
|
-
if (sd.metadata["model"] && sd.metadata["model"] !== "moonshine-streaming") throw
|
|
2595
|
+
if (sd.metadata["model"] && sd.metadata["model"] !== "moonshine-streaming") throw checkpointMismatch("moonshine-streaming weights", `file is for model '${sd.metadata["model"]}', expected 'moonshine-streaming'`);
|
|
706
2596
|
const cfg = opts.cfg ?? presetFromStateDict(sd);
|
|
707
2597
|
const dec = decoderView(cfg);
|
|
708
2598
|
const perf = opts.perf;
|
|
@@ -736,14 +2626,14 @@ async function createTranscriber(sd, tokenizer, opts = {}) {
|
|
|
736
2626
|
}));
|
|
737
2627
|
const idsBuf = tensor(root, new Float32Array(DECODE_POSITIONS), [DECODE_POSITIONS, 1]);
|
|
738
2628
|
const padToFrames = (audio) => {
|
|
739
|
-
if (audio.length === 0) throw new
|
|
2629
|
+
if (audio.length === 0) throw new RunntimeError("INVALID_ARGUMENT", "transcribe: empty audio");
|
|
740
2630
|
const frames = Math.ceil(audio.length / cfg.frameLen);
|
|
741
2631
|
if (frames * cfg.frameLen === audio.length) return audio;
|
|
742
2632
|
const padded = new Float32Array(frames * cfg.frameLen);
|
|
743
2633
|
padded.set(audio);
|
|
744
2634
|
return padded;
|
|
745
2635
|
};
|
|
746
|
-
const transcribeUtterance = async (rawAudio) => {
|
|
2636
|
+
const transcribeUtterance = async (rawAudio, tokensPerSecond) => {
|
|
747
2637
|
perf?.onPhase?.("encode");
|
|
748
2638
|
const t0 = performance.now();
|
|
749
2639
|
const audio = padToFrames(rawAudio);
|
|
@@ -754,7 +2644,7 @@ async function createTranscriber(sd, tokenizer, opts = {}) {
|
|
|
754
2644
|
evalValues([adapterOut, ...cross.flatMap((c) => [c.k, c.v])], ex);
|
|
755
2645
|
if (perf?.syncAfterEncode) await root.device.queue.onSubmittedWorkDone();
|
|
756
2646
|
const encodeMs = performance.now() - t0;
|
|
757
|
-
const maxTokens = Math.min(DECODE_POSITIONS, Math.ceil(rawAudio.length / SAMPLE_RATE *
|
|
2647
|
+
const maxTokens = Math.min(DECODE_POSITIONS, Math.ceil(rawAudio.length / SAMPLE_RATE * tokensPerSecond));
|
|
758
2648
|
const release = (kvs) => {
|
|
759
2649
|
for (const { k, v } of kvs) {
|
|
760
2650
|
ex.releaseOutput(k.buffer, k.shape);
|
|
@@ -804,7 +2694,7 @@ async function createTranscriber(sd, tokenizer, opts = {}) {
|
|
|
804
2694
|
release(cross);
|
|
805
2695
|
return {
|
|
806
2696
|
ids,
|
|
807
|
-
text:
|
|
2697
|
+
text: tokenizer.decode(ids),
|
|
808
2698
|
timings: {
|
|
809
2699
|
encodeMs,
|
|
810
2700
|
decodeMs: performance.now() - t1,
|
|
@@ -817,11 +2707,11 @@ async function createTranscriber(sd, tokenizer, opts = {}) {
|
|
|
817
2707
|
searchSeconds: opts.chunking?.searchSeconds ?? SPLIT_SEARCH_SECONDS,
|
|
818
2708
|
sampleRate: SAMPLE_RATE
|
|
819
2709
|
};
|
|
820
|
-
const transcribeAll = async (audio) => {
|
|
2710
|
+
const transcribeAll = async (audio, tokensPerSecond) => {
|
|
821
2711
|
const chunks = splitIntoChunks(audio, chunkOpts);
|
|
822
|
-
if (chunks.length === 1) return transcribeUtterance(audio);
|
|
2712
|
+
if (chunks.length === 1) return transcribeUtterance(audio, tokensPerSecond);
|
|
823
2713
|
const results = [];
|
|
824
|
-
for (const chunk of chunks) results.push(await transcribeUtterance(chunk));
|
|
2714
|
+
for (const chunk of chunks) results.push(await transcribeUtterance(chunk, tokensPerSecond));
|
|
825
2715
|
return {
|
|
826
2716
|
ids: results.flatMap((r) => r.ids),
|
|
827
2717
|
text: results.map((r) => r.text.trim()).filter((t) => t.length > 0).join(" "),
|
|
@@ -848,56 +2738,20 @@ async function createTranscriber(sd, tokenizer, opts = {}) {
|
|
|
848
2738
|
};
|
|
849
2739
|
let inFlight = Promise.resolve();
|
|
850
2740
|
let disposed = false;
|
|
851
|
-
const transcribe = (audio) => {
|
|
852
|
-
if (disposed) return Promise.reject(
|
|
853
|
-
const job = inFlight.then(() => transcribeAll(audio));
|
|
2741
|
+
const transcribe = (audio, tokensPerSecond = TOKENS_PER_SECOND) => {
|
|
2742
|
+
if (disposed) return Promise.reject(new RunntimeError("RESOURCE_DISPOSED", "transcriber is disposed"));
|
|
2743
|
+
const job = inFlight.then(() => transcribeAll(audio, tokensPerSecond));
|
|
854
2744
|
inFlight = job.catch(() => void 0);
|
|
855
2745
|
return job;
|
|
856
2746
|
};
|
|
857
2747
|
return {
|
|
858
2748
|
minSamples: cfg.frameLen,
|
|
859
|
-
transcribe,
|
|
860
|
-
dispose: () => {
|
|
861
|
-
disposed = true;
|
|
862
|
-
dispose();
|
|
863
|
-
}
|
|
864
|
-
};
|
|
865
|
-
}
|
|
866
|
-
//#endregion
|
|
867
|
-
//#region src/zoo/tasks/audio/audioInput.ts
|
|
868
|
-
/** Audio input helpers: an encoded clip or raw microphone samples in, the
|
|
869
|
-
* 16 kHz mono Float32Array the speech models take out. Both use the
|
|
870
|
-
* browser's own decoder and resampler through an OfflineAudioContext, so
|
|
871
|
-
* they run in the browser only. Importing the file is safe anywhere, the
|
|
872
|
-
* browser objects appear only inside the functions. */
|
|
873
|
-
/** Sample rate the speech models take. */
|
|
874
|
-
const SPEECH_SAMPLE_RATE = 16e3;
|
|
875
|
-
/** Decodes any clip the browser can play (wav, mp3, ogg, ...) to 16 kHz
|
|
876
|
-
* mono samples. Stereo is mixed down to one channel. */
|
|
877
|
-
async function decodeAudio(data) {
|
|
878
|
-
const decoded = await new OfflineAudioContext(1, 1, SPEECH_SAMPLE_RATE).decodeAudioData(data);
|
|
879
|
-
if (decoded.numberOfChannels === 1) return decoded.getChannelData(0);
|
|
880
|
-
const mix = new OfflineAudioContext(1, decoded.length, SPEECH_SAMPLE_RATE);
|
|
881
|
-
const src = mix.createBufferSource();
|
|
882
|
-
src.buffer = decoded;
|
|
883
|
-
src.connect(mix.destination);
|
|
884
|
-
src.start();
|
|
885
|
-
return (await mix.startRendering()).getChannelData(0);
|
|
886
|
-
}
|
|
887
|
-
/** Resamples raw mono samples recorded at `sourceRate` to 16 kHz. Returns
|
|
888
|
-
* the input as is when it is already at 16 kHz. Use it on microphone
|
|
889
|
-
* samples, a lossy re-encode is not needed. */
|
|
890
|
-
async function resampleAudio(samples, sourceRate) {
|
|
891
|
-
if (sourceRate === 16e3) return samples;
|
|
892
|
-
const frames = Math.ceil(samples.length * SPEECH_SAMPLE_RATE / sourceRate);
|
|
893
|
-
const off = new OfflineAudioContext(1, frames, SPEECH_SAMPLE_RATE);
|
|
894
|
-
const buf = off.createBuffer(1, samples.length, sourceRate);
|
|
895
|
-
buf.copyToChannel(samples, 0);
|
|
896
|
-
const src = off.createBufferSource();
|
|
897
|
-
src.buffer = buf;
|
|
898
|
-
src.connect(off.destination);
|
|
899
|
-
src.start();
|
|
900
|
-
return (await off.startRendering()).getChannelData(0);
|
|
2749
|
+
transcribe,
|
|
2750
|
+
dispose: () => {
|
|
2751
|
+
disposed = true;
|
|
2752
|
+
dispose();
|
|
2753
|
+
}
|
|
2754
|
+
};
|
|
901
2755
|
}
|
|
902
2756
|
//#endregion
|
|
903
2757
|
//#region src/zoo/tasks/audio/speechToText.ts
|
|
@@ -907,6 +2761,9 @@ async function resampleAudio(samples, sourceRate) {
|
|
|
907
2761
|
const TICK_SAMPLES = SPEECH_SAMPLE_RATE / 2;
|
|
908
2762
|
/** Speech this long commits even without a pause. */
|
|
909
2763
|
const MAX_SEGMENT_SECONDS = 15;
|
|
2764
|
+
/** Decode budget of a preview, 1.5x the transcriber's default. A preview
|
|
2765
|
+
* often ends mid-word, so the default cuts good text before its EOS. */
|
|
2766
|
+
const PREVIEW_TOKENS_PER_SECOND = 10;
|
|
910
2767
|
/** Loads the checkpoint and the tokenizer, warms every GPU path once, and
|
|
911
2768
|
* returns the runner. With no config, loads Moonshine tiny from the
|
|
912
2769
|
* Hugging Face Hub. */
|
|
@@ -922,7 +2779,7 @@ async function createSpeechToText(config = {}, opts = {}) {
|
|
|
922
2779
|
...opts,
|
|
923
2780
|
cacheId: opts.cacheId ?? cacheId(modelPath)
|
|
924
2781
|
});
|
|
925
|
-
const tokenizer =
|
|
2782
|
+
const tokenizer = createMoonshineTokenizer(await fetchJson(tokenizerPath, {
|
|
926
2783
|
...opts,
|
|
927
2784
|
cacheId: cacheId(tokenizerPath)
|
|
928
2785
|
}));
|
|
@@ -939,10 +2796,10 @@ async function createSpeechToText(config = {}, opts = {}) {
|
|
|
939
2796
|
});
|
|
940
2797
|
let disposed = false;
|
|
941
2798
|
let queue = Promise.resolve();
|
|
942
|
-
const enqueue = (audio) => {
|
|
943
|
-
const run = queue.then(() => transcriber.transcribe(audio));
|
|
2799
|
+
const enqueue = (audio, tokensPerSecond) => {
|
|
2800
|
+
const run = queue.then(() => transcriber.transcribe(audio, tokensPerSecond));
|
|
944
2801
|
queue = run.catch(() => void 0);
|
|
945
|
-
return run;
|
|
2802
|
+
return run.catch(rethrowRunError);
|
|
946
2803
|
};
|
|
947
2804
|
let live;
|
|
948
2805
|
async function* liveLoop(session) {
|
|
@@ -965,11 +2822,16 @@ async function createSpeechToText(config = {}, opts = {}) {
|
|
|
965
2822
|
sampleRate: SPEECH_SAMPLE_RATE,
|
|
966
2823
|
maxSegmentSeconds: MAX_SEGMENT_SECONDS
|
|
967
2824
|
});
|
|
2825
|
+
const commit = final || segment.shouldFinalize;
|
|
968
2826
|
let text = "";
|
|
969
|
-
if (segment.hasSpeech && audio.length >= transcriber.minSamples)
|
|
970
|
-
|
|
2827
|
+
if (segment.hasSpeech && audio.length >= transcriber.minSamples) {
|
|
2828
|
+
const result = await enqueue(audio, commit ? void 0 : PREVIEW_TOKENS_PER_SECOND);
|
|
2829
|
+
text = result.text.trim();
|
|
2830
|
+
const looped = result.ids.at(-1) !== tokenizer.eosId || repeatsPhrase(text);
|
|
2831
|
+
if (!commit && looped) text = last.nonCommitted;
|
|
2832
|
+
} else if (segment.seconds >= MAX_SEGMENT_SECONDS) keepTail(session, SPEECH_SAMPLE_RATE);
|
|
971
2833
|
let committed = last.committed;
|
|
972
|
-
if (
|
|
2834
|
+
if (commit) {
|
|
973
2835
|
if (text) committed = committed ? `${committed} ${text}` : text;
|
|
974
2836
|
text = "";
|
|
975
2837
|
session.chunks = [];
|
|
@@ -990,14 +2852,14 @@ async function createSpeechToText(config = {}, opts = {}) {
|
|
|
990
2852
|
}
|
|
991
2853
|
return {
|
|
992
2854
|
async transcribe(audio) {
|
|
993
|
-
if (disposed) throw new
|
|
994
|
-
if (!(audio instanceof Float32Array)) throw new
|
|
2855
|
+
if (disposed) throw new RunntimeError("RESOURCE_DISPOSED", "speech to text is disposed");
|
|
2856
|
+
if (!(audio instanceof Float32Array)) throw new RunntimeError("INVALID_ARGUMENT", "transcribe: audio must be a Float32Array");
|
|
995
2857
|
if (audio.length < transcriber.minSamples) return "";
|
|
996
2858
|
return (await enqueue(audio)).text;
|
|
997
2859
|
},
|
|
998
2860
|
stream() {
|
|
999
|
-
if (disposed) throw new
|
|
1000
|
-
if (live) throw new
|
|
2861
|
+
if (disposed) throw new RunntimeError("RESOURCE_DISPOSED", "speech to text is disposed");
|
|
2862
|
+
if (live) throw new RunntimeError("INVALID_ARGUMENT", "stream: a stream is open, stop it first");
|
|
1001
2863
|
live = {
|
|
1002
2864
|
chunks: [],
|
|
1003
2865
|
length: 0,
|
|
@@ -1008,8 +2870,8 @@ async function createSpeechToText(config = {}, opts = {}) {
|
|
|
1008
2870
|
return liveLoop(live);
|
|
1009
2871
|
},
|
|
1010
2872
|
streamInsert(samples) {
|
|
1011
|
-
if (!live || live.stopped) throw new
|
|
1012
|
-
if (!(samples instanceof Float32Array)) throw new
|
|
2873
|
+
if (!live || live.stopped) throw new RunntimeError("INVALID_ARGUMENT", "streamInsert: no stream is open");
|
|
2874
|
+
if (!(samples instanceof Float32Array)) throw new RunntimeError("INVALID_ARGUMENT", "streamInsert: samples must be a Float32Array");
|
|
1013
2875
|
live.chunks.push(samples.slice());
|
|
1014
2876
|
live.length += samples.length;
|
|
1015
2877
|
live.fresh += samples.length;
|
|
@@ -1033,7 +2895,7 @@ async function createSpeechToText(config = {}, opts = {}) {
|
|
|
1033
2895
|
};
|
|
1034
2896
|
} catch (err) {
|
|
1035
2897
|
scope.dispose();
|
|
1036
|
-
throw err;
|
|
2898
|
+
throw asLoadError(err, opts.signal);
|
|
1037
2899
|
}
|
|
1038
2900
|
}
|
|
1039
2901
|
/** Joins the chunks into one array of `length` samples. */
|
|
@@ -1046,6 +2908,17 @@ function concat(chunks, length) {
|
|
|
1046
2908
|
}
|
|
1047
2909
|
return out;
|
|
1048
2910
|
}
|
|
2911
|
+
/** True when some three words in a row appear twice. */
|
|
2912
|
+
function repeatsPhrase(text) {
|
|
2913
|
+
const words = text.toLowerCase().match(/[\p{L}\p{N}']+/gu) ?? [];
|
|
2914
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2915
|
+
for (let i = 0; i + 3 <= words.length; i++) {
|
|
2916
|
+
const phrase = words.slice(i, i + 3).join(" ");
|
|
2917
|
+
if (seen.has(phrase)) return true;
|
|
2918
|
+
seen.add(phrase);
|
|
2919
|
+
}
|
|
2920
|
+
return false;
|
|
2921
|
+
}
|
|
1049
2922
|
/** Drops chunks from the front until about `samples` remain. */
|
|
1050
2923
|
function keepTail(session, samples) {
|
|
1051
2924
|
while (session.chunks.length > 1 && session.length - session.chunks[0].length >= samples) session.length -= session.chunks.shift().length;
|
|
@@ -1276,49 +3149,6 @@ const COCO_NAMES = [
|
|
|
1276
3149
|
"toothbrush"
|
|
1277
3150
|
];
|
|
1278
3151
|
//#endregion
|
|
1279
|
-
//#region src/zoo/tasks/cv/ops/point.ts
|
|
1280
|
-
/** The resize as numbers: how many input pixels one image pixel takes, and
|
|
1281
|
-
* where the image's top-left corner lands inside the input. */
|
|
1282
|
-
function resizeTransform(options) {
|
|
1283
|
-
const { from, to, resizeMode } = options;
|
|
1284
|
-
switch (resizeMode) {
|
|
1285
|
-
case "stretch": return {
|
|
1286
|
-
scaleX: from.width / to.width,
|
|
1287
|
-
scaleY: from.height / to.height,
|
|
1288
|
-
offsetX: 0,
|
|
1289
|
-
offsetY: 0
|
|
1290
|
-
};
|
|
1291
|
-
case "letterbox": {
|
|
1292
|
-
const scale = Math.min(from.width / to.width, from.height / to.height);
|
|
1293
|
-
return {
|
|
1294
|
-
scaleX: scale,
|
|
1295
|
-
scaleY: scale,
|
|
1296
|
-
offsetX: (from.width - to.width * scale) / 2,
|
|
1297
|
-
offsetY: (from.height - to.height * scale) / 2
|
|
1298
|
-
};
|
|
1299
|
-
}
|
|
1300
|
-
case "crop": {
|
|
1301
|
-
const side = Math.min(to.width, to.height) * (options.cropFraction ?? 1);
|
|
1302
|
-
const scaleX = from.width / side;
|
|
1303
|
-
const scaleY = from.height / side;
|
|
1304
|
-
return {
|
|
1305
|
-
scaleX,
|
|
1306
|
-
scaleY,
|
|
1307
|
-
offsetX: -(to.width - side) / 2 * scaleX,
|
|
1308
|
-
offsetY: -(to.height - side) / 2 * scaleY
|
|
1309
|
-
};
|
|
1310
|
-
}
|
|
1311
|
-
}
|
|
1312
|
-
}
|
|
1313
|
-
/** Maps a point from model-input coordinates to original-image coordinates. */
|
|
1314
|
-
function scalePoint(point, options) {
|
|
1315
|
-
const { scaleX, scaleY, offsetX, offsetY } = resizeTransform(options);
|
|
1316
|
-
return {
|
|
1317
|
-
x: (point.x - offsetX) / scaleX,
|
|
1318
|
-
y: (point.y - offsetY) / scaleY
|
|
1319
|
-
};
|
|
1320
|
-
}
|
|
1321
|
-
//#endregion
|
|
1322
3152
|
//#region src/zoo/tasks/cv/ops/box.ts
|
|
1323
3153
|
/** Bounding boxes in three formats, plus scaling back onto the original image. */
|
|
1324
3154
|
/** Builds a box from four numbers in the given format. */
|
|
@@ -1346,7 +3176,7 @@ function decodeBox(tuple, format) {
|
|
|
1346
3176
|
w: c,
|
|
1347
3177
|
h: d
|
|
1348
3178
|
};
|
|
1349
|
-
default: throw new
|
|
3179
|
+
default: throw new RunntimeError("INVALID_ARGUMENT", `unknown box format ${String(format)}`);
|
|
1350
3180
|
}
|
|
1351
3181
|
}
|
|
1352
3182
|
/** Maps a box from model-input coordinates to original-image coordinates. */
|
|
@@ -1399,203 +3229,6 @@ function scaleBox(box, options) {
|
|
|
1399
3229
|
}
|
|
1400
3230
|
}
|
|
1401
3231
|
//#endregion
|
|
1402
|
-
//#region src/zoo/tasks/cv/image.ts
|
|
1403
|
-
/** Bytes per pixel for each format. */
|
|
1404
|
-
const FORMAT_CHANNELS = {
|
|
1405
|
-
rgb: 3,
|
|
1406
|
-
rgba: 4,
|
|
1407
|
-
bgr: 3,
|
|
1408
|
-
bgra: 4,
|
|
1409
|
-
gray: 1
|
|
1410
|
-
};
|
|
1411
|
-
/** Throws unless `width` and `height` are positive integers. */
|
|
1412
|
-
function checkImageSize(width, height, what = "image") {
|
|
1413
|
-
if (!Number.isInteger(width) || !Number.isInteger(height) || width <= 0 || height <= 0) throw new Error(`${what}: ${width}x${height} is not a positive size`);
|
|
1414
|
-
}
|
|
1415
|
-
/** Wraps raw bytes as an ImageBuffer. Checks the size and the byte count. */
|
|
1416
|
-
function imageBuffer(data, width, height, format = "rgba") {
|
|
1417
|
-
checkImageSize(width, height);
|
|
1418
|
-
const expected = width * height * FORMAT_CHANNELS[format];
|
|
1419
|
-
if (data.length !== expected) throw new Error(`image: ${data.length} bytes for ${width}x${height} ${format}, expected ${expected}`);
|
|
1420
|
-
return {
|
|
1421
|
-
data,
|
|
1422
|
-
width,
|
|
1423
|
-
height,
|
|
1424
|
-
format,
|
|
1425
|
-
layout: "hwc"
|
|
1426
|
-
};
|
|
1427
|
-
}
|
|
1428
|
-
/** Views canvas ImageData as an ImageBuffer. No copy. */
|
|
1429
|
-
function imageBufferFromImageData(img) {
|
|
1430
|
-
return {
|
|
1431
|
-
data: img.data,
|
|
1432
|
-
width: img.width,
|
|
1433
|
-
height: img.height,
|
|
1434
|
-
format: "rgba",
|
|
1435
|
-
layout: "hwc"
|
|
1436
|
-
};
|
|
1437
|
-
}
|
|
1438
|
-
//#endregion
|
|
1439
|
-
//#region src/zoo/tasks/cv/utils/imagePreprocessor.ts
|
|
1440
|
-
/** CPU image preprocessor: any ImageBuffer in, the model's RGB CHW f32 input
|
|
1441
|
-
* out. Resizes (stretch / letterbox / crop), converts the pixel format,
|
|
1442
|
-
* moves channels first and normalizes, all in one pass over the output. */
|
|
1443
|
-
/** Index of the R, G and B byte inside one pixel of each format. */
|
|
1444
|
-
const RGB_INDEX = {
|
|
1445
|
-
rgb: [
|
|
1446
|
-
0,
|
|
1447
|
-
1,
|
|
1448
|
-
2
|
|
1449
|
-
],
|
|
1450
|
-
rgba: [
|
|
1451
|
-
0,
|
|
1452
|
-
1,
|
|
1453
|
-
2
|
|
1454
|
-
],
|
|
1455
|
-
bgr: [
|
|
1456
|
-
2,
|
|
1457
|
-
1,
|
|
1458
|
-
0
|
|
1459
|
-
],
|
|
1460
|
-
bgra: [
|
|
1461
|
-
2,
|
|
1462
|
-
1,
|
|
1463
|
-
0
|
|
1464
|
-
],
|
|
1465
|
-
gray: [
|
|
1466
|
-
0,
|
|
1467
|
-
0,
|
|
1468
|
-
0
|
|
1469
|
-
]
|
|
1470
|
-
};
|
|
1471
|
-
function perChannel(v, name) {
|
|
1472
|
-
if (typeof v === "number") return [
|
|
1473
|
-
v,
|
|
1474
|
-
v,
|
|
1475
|
-
v
|
|
1476
|
-
];
|
|
1477
|
-
if (v.length !== 3) throw new Error(`normalize.${name}: expected 3 values, got ${v.length}`);
|
|
1478
|
-
return [
|
|
1479
|
-
v[0],
|
|
1480
|
-
v[1],
|
|
1481
|
-
v[2]
|
|
1482
|
-
];
|
|
1483
|
-
}
|
|
1484
|
-
function buildAxis(outLen, srcLen, scale, offset, interpolation) {
|
|
1485
|
-
const i0 = new Int32Array(outLen);
|
|
1486
|
-
const i1 = new Int32Array(outLen);
|
|
1487
|
-
const w = new Float32Array(outLen);
|
|
1488
|
-
const inside = new Uint8Array(outLen);
|
|
1489
|
-
for (let d = 0; d < outLen; d++) {
|
|
1490
|
-
const s = (d + .5 - offset) / scale;
|
|
1491
|
-
if (s < 0 || s >= srcLen) continue;
|
|
1492
|
-
inside[d] = 1;
|
|
1493
|
-
if (interpolation === "nearest") {
|
|
1494
|
-
i0[d] = i1[d] = Math.min(srcLen - 1, Math.floor(s));
|
|
1495
|
-
continue;
|
|
1496
|
-
}
|
|
1497
|
-
const f = s - .5;
|
|
1498
|
-
const a = Math.floor(f);
|
|
1499
|
-
i0[d] = Math.max(0, a);
|
|
1500
|
-
i1[d] = Math.min(srcLen - 1, a + 1);
|
|
1501
|
-
w[d] = a < 0 ? 0 : f - a;
|
|
1502
|
-
}
|
|
1503
|
-
return {
|
|
1504
|
-
i0,
|
|
1505
|
-
i1,
|
|
1506
|
-
w,
|
|
1507
|
-
inside
|
|
1508
|
-
};
|
|
1509
|
-
}
|
|
1510
|
-
function createImagePreprocessor(options, size) {
|
|
1511
|
-
const { width, height, resizeMode } = {
|
|
1512
|
-
...size,
|
|
1513
|
-
resizeMode: options.resizeMode
|
|
1514
|
-
};
|
|
1515
|
-
checkImageSize(width, height, "input size");
|
|
1516
|
-
const interpolation = options.interpolation ?? "linear";
|
|
1517
|
-
const cropFraction = options.cropFraction ?? 1;
|
|
1518
|
-
if (!(cropFraction > 0 && cropFraction <= 1)) throw new Error(`cropFraction: expected a number in (0, 1], got ${cropFraction}`);
|
|
1519
|
-
const padValue = options.padValue ?? 114;
|
|
1520
|
-
const alpha = perChannel(options.normalizeOpts?.alpha ?? 1 / 255, "alpha");
|
|
1521
|
-
const beta = perChannel(options.normalizeOpts?.beta ?? 0, "beta");
|
|
1522
|
-
const hw = width * height;
|
|
1523
|
-
const scaleOptions = (input) => ({
|
|
1524
|
-
from: {
|
|
1525
|
-
width,
|
|
1526
|
-
height
|
|
1527
|
-
},
|
|
1528
|
-
to: {
|
|
1529
|
-
width: input.width,
|
|
1530
|
-
height: input.height
|
|
1531
|
-
},
|
|
1532
|
-
resizeMode,
|
|
1533
|
-
cropFraction
|
|
1534
|
-
});
|
|
1535
|
-
const process = (input, out) => {
|
|
1536
|
-
out ??= new Float32Array(3 * hw);
|
|
1537
|
-
if (out.length !== 3 * hw) throw new Error(`out: ${out.length} floats, expected ${3 * hw}`);
|
|
1538
|
-
checkImageSize(input.width, input.height);
|
|
1539
|
-
const ch = FORMAT_CHANNELS[input.format];
|
|
1540
|
-
const { data } = input;
|
|
1541
|
-
if (data.length !== input.width * input.height * ch) throw new Error(`image: ${data.length} bytes for ${input.width}x${input.height} ${input.format}`);
|
|
1542
|
-
const [ri, gi, bi] = RGB_INDEX[input.format];
|
|
1543
|
-
const [ar, ag, ab] = alpha;
|
|
1544
|
-
const [br, bg, bb] = beta;
|
|
1545
|
-
const t = resizeTransform(scaleOptions(input));
|
|
1546
|
-
if (t.scaleX === 1 && t.scaleY === 1 && t.offsetX === 0 && t.offsetY === 0) {
|
|
1547
|
-
for (let i = 0; i < hw; i++) {
|
|
1548
|
-
const p = i * ch;
|
|
1549
|
-
out[i] = data[p + ri] * ar + br;
|
|
1550
|
-
out[hw + i] = data[p + gi] * ag + bg;
|
|
1551
|
-
out[2 * hw + i] = data[p + bi] * ab + bb;
|
|
1552
|
-
}
|
|
1553
|
-
return out;
|
|
1554
|
-
}
|
|
1555
|
-
const xs = buildAxis(width, input.width, t.scaleX, t.offsetX, interpolation);
|
|
1556
|
-
const ys = buildAxis(height, input.height, t.scaleY, t.offsetY, interpolation);
|
|
1557
|
-
const padR = padValue * ar + br;
|
|
1558
|
-
const padG = padValue * ag + bg;
|
|
1559
|
-
const padB = padValue * ab + bb;
|
|
1560
|
-
const rowStride = input.width * ch;
|
|
1561
|
-
for (let y = 0; y < height; y++) {
|
|
1562
|
-
const rowIn = ys.inside[y];
|
|
1563
|
-
const r0 = ys.i0[y] * rowStride;
|
|
1564
|
-
const r1 = ys.i1[y] * rowStride;
|
|
1565
|
-
const wy = ys.w[y];
|
|
1566
|
-
for (let x = 0; x < width; x++) {
|
|
1567
|
-
const o = y * width + x;
|
|
1568
|
-
if (!rowIn || !xs.inside[x]) {
|
|
1569
|
-
out[o] = padR;
|
|
1570
|
-
out[hw + o] = padG;
|
|
1571
|
-
out[2 * hw + o] = padB;
|
|
1572
|
-
continue;
|
|
1573
|
-
}
|
|
1574
|
-
const c0 = xs.i0[x] * ch;
|
|
1575
|
-
const c1 = xs.i1[x] * ch;
|
|
1576
|
-
const wx = xs.w[x];
|
|
1577
|
-
const w00 = (1 - wy) * (1 - wx);
|
|
1578
|
-
const w01 = (1 - wy) * wx;
|
|
1579
|
-
const w10 = wy * (1 - wx);
|
|
1580
|
-
const w11 = wy * wx;
|
|
1581
|
-
const r = data[r0 + c0 + ri] * w00 + data[r0 + c1 + ri] * w01 + data[r1 + c0 + ri] * w10 + data[r1 + c1 + ri] * w11;
|
|
1582
|
-
const g = data[r0 + c0 + gi] * w00 + data[r0 + c1 + gi] * w01 + data[r1 + c0 + gi] * w10 + data[r1 + c1 + gi] * w11;
|
|
1583
|
-
const b = data[r0 + c0 + bi] * w00 + data[r0 + c1 + bi] * w01 + data[r1 + c0 + bi] * w10 + data[r1 + c1 + bi] * w11;
|
|
1584
|
-
out[o] = r * ar + br;
|
|
1585
|
-
out[hw + o] = g * ag + bg;
|
|
1586
|
-
out[2 * hw + o] = b * ab + bb;
|
|
1587
|
-
}
|
|
1588
|
-
}
|
|
1589
|
-
return out;
|
|
1590
|
-
};
|
|
1591
|
-
return {
|
|
1592
|
-
width,
|
|
1593
|
-
height,
|
|
1594
|
-
process,
|
|
1595
|
-
scaleOptions
|
|
1596
|
-
};
|
|
1597
|
-
}
|
|
1598
|
-
//#endregion
|
|
1599
3232
|
//#region src/zoo/tasks/cv/objectDetection.ts
|
|
1600
3233
|
/** Object detection task: an image in, the objects in it out, as labeled
|
|
1601
3234
|
* boxes. Runs YOLO26 on the initRunntime() device. */
|
|
@@ -1629,11 +3262,11 @@ async function createObjectDetector(config = {}, opts = {}) {
|
|
|
1629
3262
|
return {
|
|
1630
3263
|
labels,
|
|
1631
3264
|
async detectObjects(image, options = {}) {
|
|
1632
|
-
if (disposed) throw new
|
|
3265
|
+
if (disposed) throw new RunntimeError("RESOURCE_DISPOSED", "object detector is disposed");
|
|
1633
3266
|
const { confidenceThreshold = .3, maxDetections = 300 } = options;
|
|
1634
3267
|
const run = queue.then(() => detector.run(preprocessor.process(image, pixels)));
|
|
1635
3268
|
queue = run.catch(() => void 0);
|
|
1636
|
-
const { levels } = await run;
|
|
3269
|
+
const { levels } = await run.catch(rethrowRunError);
|
|
1637
3270
|
const scale = preprocessor.scaleOptions(image);
|
|
1638
3271
|
return decodeDetections(levels, {
|
|
1639
3272
|
numClasses,
|
|
@@ -1667,7 +3300,7 @@ async function createObjectDetector(config = {}, opts = {}) {
|
|
|
1667
3300
|
};
|
|
1668
3301
|
} catch (err) {
|
|
1669
3302
|
scope.dispose();
|
|
1670
|
-
throw err;
|
|
3303
|
+
throw asLoadError(err, opts.signal);
|
|
1671
3304
|
}
|
|
1672
3305
|
}
|
|
1673
3306
|
const clamp$2 = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
|
|
@@ -1705,12 +3338,12 @@ async function createInstanceSegmenter(config = {}, opts = {}) {
|
|
|
1705
3338
|
return {
|
|
1706
3339
|
labels,
|
|
1707
3340
|
async segmentInstances(image, options = {}) {
|
|
1708
|
-
if (disposed) throw new
|
|
3341
|
+
if (disposed) throw new RunntimeError("RESOURCE_DISPOSED", "instance segmenter is disposed");
|
|
1709
3342
|
const { confidenceThreshold = .3, maxDetections = 300 } = options;
|
|
1710
3343
|
const run = queue.then(() => detector.run(preprocessor.process(image, pixels)));
|
|
1711
3344
|
queue = run.catch(() => void 0);
|
|
1712
|
-
const { levels, proto } = await run;
|
|
1713
|
-
if (!proto) throw new
|
|
3345
|
+
const { levels, proto } = await run.catch(rethrowRunError);
|
|
3346
|
+
if (!proto) throw new RunntimeError("EXECUTION_FAILED", "yolo26 segment: the forward pass gave no mask prototypes");
|
|
1714
3347
|
const scale = preprocessor.scaleOptions(image);
|
|
1715
3348
|
const toInput = resizeTransform(scale);
|
|
1716
3349
|
const protoStride = inputSize / proto.w;
|
|
@@ -1752,7 +3385,7 @@ async function createInstanceSegmenter(config = {}, opts = {}) {
|
|
|
1752
3385
|
};
|
|
1753
3386
|
} catch (err) {
|
|
1754
3387
|
scope.dispose();
|
|
1755
|
-
throw err;
|
|
3388
|
+
throw asLoadError(err, opts.signal);
|
|
1756
3389
|
}
|
|
1757
3390
|
}
|
|
1758
3391
|
const clamp$1 = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
|
|
@@ -1820,11 +3453,11 @@ async function createKeypointDetector(config = {}, opts = {}) {
|
|
|
1820
3453
|
landmarkNames: COCO_LANDMARKS,
|
|
1821
3454
|
skeleton: COCO_SKELETON.map(([a, b]) => [COCO_LANDMARKS[a], COCO_LANDMARKS[b]]),
|
|
1822
3455
|
async detectKeypoints(image, options = {}) {
|
|
1823
|
-
if (disposed) throw new
|
|
3456
|
+
if (disposed) throw new RunntimeError("RESOURCE_DISPOSED", "keypoint detector is disposed");
|
|
1824
3457
|
const { confidenceThreshold = .3, maxDetections = 300 } = options;
|
|
1825
3458
|
const run = queue.then(() => detector.run(preprocessor.process(image, pixels)));
|
|
1826
3459
|
queue = run.catch(() => void 0);
|
|
1827
|
-
const { levels } = await run;
|
|
3460
|
+
const { levels } = await run.catch(rethrowRunError);
|
|
1828
3461
|
const scale = preprocessor.scaleOptions(image);
|
|
1829
3462
|
return decodePoses(levels, {
|
|
1830
3463
|
numClasses,
|
|
@@ -1865,17 +3498,97 @@ async function createKeypointDetector(config = {}, opts = {}) {
|
|
|
1865
3498
|
};
|
|
1866
3499
|
} catch (err) {
|
|
1867
3500
|
scope.dispose();
|
|
1868
|
-
throw err;
|
|
3501
|
+
throw asLoadError(err, opts.signal);
|
|
1869
3502
|
}
|
|
1870
3503
|
}
|
|
1871
3504
|
const clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
|
|
1872
3505
|
//#endregion
|
|
3506
|
+
//#region src/zoo/tasks/gpu.ts
|
|
3507
|
+
/** GPU in, GPU out: the types every runner's encode<Verb> method takes, and
|
|
3508
|
+
* how they are unwrapped from TypeGPU and checked. */
|
|
3509
|
+
/** Texture formats a GpuImage can have. sRGB formats are left out: the GPU
|
|
3510
|
+
* decodes them to linear values, which are not the bytes the model expects. */
|
|
3511
|
+
const IMAGE_FORMATS = /* @__PURE__ */ new Set([
|
|
3512
|
+
"rgba8unorm",
|
|
3513
|
+
"bgra8unorm",
|
|
3514
|
+
"rgb10a2unorm",
|
|
3515
|
+
"rgba16float",
|
|
3516
|
+
"rgba32float"
|
|
3517
|
+
]);
|
|
3518
|
+
/** Texture formats a GpuImageTarget can have: the float-renderable ones. */
|
|
3519
|
+
const TARGET_FORMATS = /* @__PURE__ */ new Set([
|
|
3520
|
+
"r8unorm",
|
|
3521
|
+
"r16float",
|
|
3522
|
+
"r32float",
|
|
3523
|
+
"rg8unorm",
|
|
3524
|
+
"rg16float",
|
|
3525
|
+
"rg32float",
|
|
3526
|
+
"rgba8unorm",
|
|
3527
|
+
"rgba8unorm-srgb",
|
|
3528
|
+
"bgra8unorm",
|
|
3529
|
+
"bgra8unorm-srgb",
|
|
3530
|
+
"rgb10a2unorm",
|
|
3531
|
+
"rgba16float",
|
|
3532
|
+
"rgba32float"
|
|
3533
|
+
]);
|
|
3534
|
+
/** Unwraps a source and checks it. A tensor must hold at least `bytes`. */
|
|
3535
|
+
function unwrapSource(source, bytes) {
|
|
3536
|
+
const raw = unwrap(source);
|
|
3537
|
+
if ("mapState" in raw) {
|
|
3538
|
+
checkBuffer(raw, bytes, GPUBufferUsage.COPY_SRC, "COPY_SRC", "source");
|
|
3539
|
+
return { tensor: raw };
|
|
3540
|
+
}
|
|
3541
|
+
if ("format" in raw) checkImage(raw);
|
|
3542
|
+
return { image: raw };
|
|
3543
|
+
}
|
|
3544
|
+
/** Unwraps a target and checks it. A tensor must hold at least `bytes`. */
|
|
3545
|
+
function unwrapTarget(target, bytes) {
|
|
3546
|
+
const raw = unwrap(target);
|
|
3547
|
+
if ("mapState" in raw) {
|
|
3548
|
+
checkBuffer(raw, bytes, GPUBufferUsage.COPY_DST, "COPY_DST", "target");
|
|
3549
|
+
return { tensor: raw };
|
|
3550
|
+
}
|
|
3551
|
+
if (!("format" in raw)) throw new RunntimeError("INVALID_ARGUMENT", "target: an external texture cannot be drawn into");
|
|
3552
|
+
checkTexture(raw, "target", GPUTextureUsage.RENDER_ATTACHMENT, "RENDER_ATTACHMENT");
|
|
3553
|
+
if (!TARGET_FORMATS.has(raw.format)) throw new RunntimeError("INVALID_ARGUMENT", `target: format ${raw.format} is not supported, use one of ${[...TARGET_FORMATS].join(", ")}`);
|
|
3554
|
+
return { image: raw };
|
|
3555
|
+
}
|
|
3556
|
+
/** Runs `record` on the caller's encoder, or on a new one it then submits. */
|
|
3557
|
+
function encodeWith(encoder, record) {
|
|
3558
|
+
const root = defaultRoot();
|
|
3559
|
+
if (encoder !== void 0) {
|
|
3560
|
+
record("resourceType" in encoder ? root.unwrap(encoder) : encoder);
|
|
3561
|
+
return;
|
|
3562
|
+
}
|
|
3563
|
+
const own = root.device.createCommandEncoder();
|
|
3564
|
+
record(own);
|
|
3565
|
+
root.device.queue.submit([own.finish()]);
|
|
3566
|
+
}
|
|
3567
|
+
function unwrap(resource) {
|
|
3568
|
+
if (isTexture(resource)) return defaultRoot().unwrap(resource);
|
|
3569
|
+
if (isBuffer(resource)) return defaultRoot().unwrap(resource);
|
|
3570
|
+
return resource;
|
|
3571
|
+
}
|
|
3572
|
+
function checkImage(texture) {
|
|
3573
|
+
checkTexture(texture, "source", GPUTextureUsage.TEXTURE_BINDING, "TEXTURE_BINDING");
|
|
3574
|
+
if (!IMAGE_FORMATS.has(texture.format)) throw new RunntimeError("INVALID_ARGUMENT", `source: format ${texture.format} is not supported, use one of ${[...IMAGE_FORMATS].join(", ")}`);
|
|
3575
|
+
if (texture.depthOrArrayLayers !== 1) throw new RunntimeError("INVALID_ARGUMENT", `source: ${texture.depthOrArrayLayers} array layers, expected 1`);
|
|
3576
|
+
}
|
|
3577
|
+
function checkTexture(texture, what, usage, usageName) {
|
|
3578
|
+
if (texture.dimension !== "2d" || texture.sampleCount !== 1) throw new RunntimeError("INVALID_ARGUMENT", `${what}: expected a 2d texture with one sample, got ${texture.dimension} with ${texture.sampleCount}`);
|
|
3579
|
+
if (!(texture.usage & usage)) throw new RunntimeError("INVALID_ARGUMENT", `${what}: the texture needs ${usageName} usage`);
|
|
3580
|
+
}
|
|
3581
|
+
function checkBuffer(buffer, bytes, usage, usageName, what) {
|
|
3582
|
+
if (buffer.size < bytes) throw new RunntimeError("INVALID_ARGUMENT", `${what}: buffer holds ${buffer.size} bytes, expected ${bytes}`);
|
|
3583
|
+
if (!(buffer.usage & usage)) throw new RunntimeError("INVALID_ARGUMENT", `${what}: the buffer needs ${usageName} usage`);
|
|
3584
|
+
}
|
|
3585
|
+
//#endregion
|
|
1873
3586
|
//#region src/zoo/tasks/cv/depthEstimation.ts
|
|
1874
3587
|
/** Depth estimation task: an image in, a relative depth map out. Runs
|
|
1875
3588
|
* DepthART on the initRunntime() device. */
|
|
1876
3589
|
/** Loads the checkpoint, warms the GPU path once, and returns the runner. */
|
|
1877
3590
|
async function createDepthEstimator(config = {}, opts = {}) {
|
|
1878
|
-
if (!supportsF16()) throw new
|
|
3591
|
+
if (!supportsF16()) throw new RunntimeError("UNSUPPORTED_DEVICE", "createDepthEstimator: this device has no shader-f16, which the conv kernels need. Request it with tgpu.init({ device: { optionalFeatures: ['shader-f16'] } })");
|
|
1879
3592
|
const scope = createResourceScope();
|
|
1880
3593
|
try {
|
|
1881
3594
|
const { modelPath = models.depthEstimation.DEPTHART.DEFAULT.modelPath, variant, resizeMode = "stretch" } = config;
|
|
@@ -1902,20 +3615,51 @@ async function createDepthEstimator(config = {}, opts = {}) {
|
|
|
1902
3615
|
height: inputSize
|
|
1903
3616
|
});
|
|
1904
3617
|
const pixels = new Float32Array(3 * inputSize * inputSize);
|
|
3618
|
+
const root = defaultRoot();
|
|
3619
|
+
const imageToChw = createImageToChw(root);
|
|
3620
|
+
const mapToTexture = scope.track(createMapToTexture(root));
|
|
3621
|
+
const schemas = {
|
|
3622
|
+
input: d.arrayOf(d.arrayOf(d.arrayOf(d.f32, inputSize), inputSize), 3),
|
|
3623
|
+
output: d.arrayOf(d.arrayOf(d.f32, inputSize), inputSize)
|
|
3624
|
+
};
|
|
3625
|
+
const inputBytes = d.sizeOf(schemas.input);
|
|
3626
|
+
const outputBytes = d.sizeOf(schemas.output);
|
|
1905
3627
|
let disposed = false;
|
|
1906
3628
|
let queue = Promise.resolve();
|
|
1907
3629
|
return {
|
|
1908
3630
|
inputSize,
|
|
3631
|
+
schemas,
|
|
1909
3632
|
async estimateDepth(image) {
|
|
1910
|
-
if (disposed) throw new
|
|
3633
|
+
if (disposed) throw new RunntimeError("RESOURCE_DISPOSED", "depth estimator is disposed");
|
|
1911
3634
|
const run = queue.then(() => estimator.run(preprocessor.process(image, pixels), inputSize, inputSize));
|
|
1912
3635
|
queue = run.catch(() => void 0);
|
|
1913
3636
|
return {
|
|
1914
3637
|
width: inputSize,
|
|
1915
3638
|
height: inputSize,
|
|
1916
|
-
data: await run
|
|
3639
|
+
data: await run.catch(rethrowRunError)
|
|
1917
3640
|
};
|
|
1918
3641
|
},
|
|
3642
|
+
encodeEstimateDepth({ source, target, encoder, normalize = "none" }) {
|
|
3643
|
+
if (disposed) throw new RunntimeError("RESOURCE_DISPOSED", "depth estimator is disposed");
|
|
3644
|
+
const src = unwrapSource(source, inputBytes);
|
|
3645
|
+
const dst = unwrapTarget(target, outputBytes);
|
|
3646
|
+
if ("tensor" in dst && normalize !== "none") throw new RunntimeError("INVALID_ARGUMENT", "normalize: only a texture target is rescaled");
|
|
3647
|
+
try {
|
|
3648
|
+
encodeWith(encoder, (enc) => estimator.encode(enc, (input) => {
|
|
3649
|
+
if ("tensor" in src) enc.copyBufferToBuffer(src.tensor, 0, root.unwrap(input), 0, inputBytes);
|
|
3650
|
+
else imageToChw.encode(enc, src.image, input, preprocessor.gpuArgs);
|
|
3651
|
+
}, (depth) => {
|
|
3652
|
+
if ("tensor" in dst) enc.copyBufferToBuffer(root.unwrap(depth), 0, dst.tensor, 0, outputBytes);
|
|
3653
|
+
else mapToTexture.encode(enc, depth, dst.image, {
|
|
3654
|
+
width: inputSize,
|
|
3655
|
+
height: inputSize,
|
|
3656
|
+
normalize
|
|
3657
|
+
});
|
|
3658
|
+
}));
|
|
3659
|
+
} catch (err) {
|
|
3660
|
+
rethrowRunError(err);
|
|
3661
|
+
}
|
|
3662
|
+
},
|
|
1919
3663
|
dispose() {
|
|
1920
3664
|
disposed = true;
|
|
1921
3665
|
scope.dispose();
|
|
@@ -1923,7 +3667,7 @@ async function createDepthEstimator(config = {}, opts = {}) {
|
|
|
1923
3667
|
};
|
|
1924
3668
|
} catch (err) {
|
|
1925
3669
|
scope.dispose();
|
|
1926
|
-
throw err;
|
|
3670
|
+
throw asLoadError(err, opts.signal);
|
|
1927
3671
|
}
|
|
1928
3672
|
}
|
|
1929
3673
|
//#endregion
|
|
@@ -2956,7 +4700,7 @@ function decodeTopK(logits, k = 5) {
|
|
|
2956
4700
|
/** Loads the checkpoint, warms the GPU path once, and returns the runner.
|
|
2957
4701
|
* Needs a device with `shader-f16`. */
|
|
2958
4702
|
async function createImageClassifier(config = {}, opts = {}) {
|
|
2959
|
-
if (!supportsF16()) throw new
|
|
4703
|
+
if (!supportsF16()) throw new RunntimeError("UNSUPPORTED_DEVICE", "createImageClassifier: this device has no shader-f16, which the conv kernels need. Request it with tgpu.init({ device: { optionalFeatures: ['shader-f16'] } })");
|
|
2960
4704
|
const scope = createResourceScope();
|
|
2961
4705
|
try {
|
|
2962
4706
|
const { modelPath = models.imageClassification.MOBILENETV4.DEFAULT.modelPath, labels = IMAGENET1K_LABELS, resizeMode = "crop" } = config;
|
|
@@ -2968,7 +4712,7 @@ async function createImageClassifier(config = {}, opts = {}) {
|
|
|
2968
4712
|
throwIfAborted(opts.signal);
|
|
2969
4713
|
const classifier = scope.track(await createClassifier(sd, { onProgress: opts.onProgress }));
|
|
2970
4714
|
const { inputSize, numClasses } = classifier;
|
|
2971
|
-
if (labels.length !== numClasses) throw new
|
|
4715
|
+
if (labels.length !== numClasses) throw new RunntimeError("INVALID_ARGUMENT", `labels: ${labels.length} names for a ${numClasses}-class model`);
|
|
2972
4716
|
const preprocessor = createImagePreprocessor({
|
|
2973
4717
|
resizeMode,
|
|
2974
4718
|
cropFraction: MOBILENETV4_CROP_FRACTION,
|
|
@@ -2987,12 +4731,12 @@ async function createImageClassifier(config = {}, opts = {}) {
|
|
|
2987
4731
|
labels,
|
|
2988
4732
|
inputSize,
|
|
2989
4733
|
async classify(image, options = {}) {
|
|
2990
|
-
if (disposed) throw new
|
|
4734
|
+
if (disposed) throw new RunntimeError("RESOURCE_DISPOSED", "image classifier is disposed");
|
|
2991
4735
|
const { topk = numClasses } = options;
|
|
2992
|
-
if (!Number.isInteger(topk) || topk < 1) throw new
|
|
4736
|
+
if (!Number.isInteger(topk) || topk < 1) throw new RunntimeError("INVALID_ARGUMENT", `classify: topk must be a positive integer, got ${topk}`);
|
|
2993
4737
|
const run = queue.then(() => classifier.run(preprocessor.process(image, pixels)));
|
|
2994
4738
|
queue = run.catch(() => void 0);
|
|
2995
|
-
return decodeTopK(await run, Math.min(topk, numClasses)).map(({ classId, confidence }) => ({
|
|
4739
|
+
return decodeTopK(await run.catch(rethrowRunError), Math.min(topk, numClasses)).map(({ classId, confidence }) => ({
|
|
2996
4740
|
label: labels[classId],
|
|
2997
4741
|
classId,
|
|
2998
4742
|
confidence
|
|
@@ -3005,8 +4749,8 @@ async function createImageClassifier(config = {}, opts = {}) {
|
|
|
3005
4749
|
};
|
|
3006
4750
|
} catch (err) {
|
|
3007
4751
|
scope.dispose();
|
|
3008
|
-
throw err;
|
|
4752
|
+
throw asLoadError(err, opts.signal);
|
|
3009
4753
|
}
|
|
3010
4754
|
}
|
|
3011
4755
|
//#endregion
|
|
3012
|
-
export { COCO_LANDMARKS, SPEECH_SAMPLE_RATE, bufferSource, createDepthEstimator, createImageClassifier, createInstanceSegmenter, createKeypointDetector, createObjectDetector, createOpfsCache, createPrivacyFilter, createSpeechToText,
|
|
4756
|
+
export { COCO_LANDMARKS, RUNNTIME_ERROR_CODES, RunntimeError, SPEECH_SAMPLE_RATE, bufferSource, createDepthEstimator, createEmbedder, createImageClassifier, createInstanceSegmenter, createKeypointDetector, createObjectDetector, createOpfsCache, createPrivacyFilter, createSpeechToText, decodeAudio, imageBuffer, imageBufferFromImageData, initRunntime, isRunntimeError, models, resampleAudio, similarity };
|