@hanzo/browser-extension 1.9.31 → 1.9.33

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.
Files changed (62) hide show
  1. package/package.json +2 -1
  2. package/src/answer/AnswerEngine.tsx +553 -0
  3. package/src/manifest-firefox.json +10 -2
  4. package/src/manifest.json +17 -2
  5. package/src/newtab.css +277 -0
  6. package/src/newtab.html +13 -0
  7. package/src/popup.html +28 -0
  8. package/dist/browser-extension/README.md +0 -43
  9. package/dist/browser-extension/background-firefox.js +0 -4556
  10. package/dist/browser-extension/background.js +0 -7245
  11. package/dist/browser-extension/browser-control.js +0 -1317
  12. package/dist/browser-extension/chrome/ai-worker.js +0 -571
  13. package/dist/browser-extension/chrome/background.js +0 -7245
  14. package/dist/browser-extension/chrome/callback.html +0 -17
  15. package/dist/browser-extension/chrome/content-script.js +0 -2789
  16. package/dist/browser-extension/chrome/icon128.png +0 -0
  17. package/dist/browser-extension/chrome/icon16.png +0 -0
  18. package/dist/browser-extension/chrome/icon48.png +0 -0
  19. package/dist/browser-extension/chrome/manifest.json +0 -66
  20. package/dist/browser-extension/chrome/popup.css +0 -761
  21. package/dist/browser-extension/chrome/popup.html +0 -353
  22. package/dist/browser-extension/chrome/popup.js +0 -1650
  23. package/dist/browser-extension/chrome/sidebar.css +0 -1792
  24. package/dist/browser-extension/chrome/sidebar.html +0 -463
  25. package/dist/browser-extension/chrome/sidebar.js +0 -26541
  26. package/dist/browser-extension/cli.js +0 -233
  27. package/dist/browser-extension/firefox/ai-worker.js +0 -571
  28. package/dist/browser-extension/firefox/background.js +0 -4556
  29. package/dist/browser-extension/firefox/callback.html +0 -17
  30. package/dist/browser-extension/firefox/content-script.js +0 -2789
  31. package/dist/browser-extension/firefox/icon128.png +0 -0
  32. package/dist/browser-extension/firefox/icon16.png +0 -0
  33. package/dist/browser-extension/firefox/icon48.png +0 -0
  34. package/dist/browser-extension/firefox/manifest.json +0 -77
  35. package/dist/browser-extension/firefox/popup.css +0 -761
  36. package/dist/browser-extension/firefox/popup.html +0 -353
  37. package/dist/browser-extension/firefox/popup.js +0 -1650
  38. package/dist/browser-extension/firefox/sidebar.css +0 -1792
  39. package/dist/browser-extension/firefox/sidebar.html +0 -463
  40. package/dist/browser-extension/firefox/sidebar.js +0 -26541
  41. package/dist/browser-extension/icon128.png +0 -0
  42. package/dist/browser-extension/icon16.png +0 -0
  43. package/dist/browser-extension/icon48.png +0 -0
  44. package/dist/browser-extension/manifest.json +0 -66
  45. package/dist/browser-extension/package.json +0 -41
  46. package/dist/browser-extension/popup.js +0 -1650
  47. package/dist/browser-extension/safari/Info.plist +0 -21
  48. package/dist/browser-extension/safari/ai-worker.js +0 -571
  49. package/dist/browser-extension/safari/background.js +0 -7245
  50. package/dist/browser-extension/safari/callback.html +0 -17
  51. package/dist/browser-extension/safari/content-script.js +0 -2789
  52. package/dist/browser-extension/safari/icon128.png +0 -0
  53. package/dist/browser-extension/safari/icon16.png +0 -0
  54. package/dist/browser-extension/safari/icon48.png +0 -0
  55. package/dist/browser-extension/safari/popup.css +0 -761
  56. package/dist/browser-extension/safari/popup.html +0 -353
  57. package/dist/browser-extension/safari/popup.js +0 -1650
  58. package/dist/browser-extension/safari/sidebar.css +0 -1792
  59. package/dist/browser-extension/safari/sidebar.html +0 -463
  60. package/dist/browser-extension/safari/sidebar.js +0 -26541
  61. package/dist/browser-extension/sidebar.js +0 -26541
  62. package/dist/browser-extension/webgpu-ai.js +0 -568
@@ -1,568 +0,0 @@
1
- // src/webgpu-ai.ts
2
- var WebGPUAI = class {
3
- constructor() {
4
- this.device = null;
5
- this.models = /* @__PURE__ */ new Map();
6
- // ---------------------------------------------------------------------------
7
- // BPE Tokenizer (byte-level, GPT-2 compatible)
8
- // ---------------------------------------------------------------------------
9
- this.byteEncoder = {};
10
- this.byteDecoder = {};
11
- this.byteEncoderInitialized = false;
12
- // ---------------------------------------------------------------------------
13
- // WGSL Compute Shaders
14
- // ---------------------------------------------------------------------------
15
- this.matmulShader = `
16
- @group(0) @binding(0) var<storage, read> weights: array<f32>;
17
- @group(0) @binding(1) var<storage, read> input_tokens: array<f32>;
18
- @group(0) @binding(2) var<storage, read_write> output_logits: array<f32>;
19
- @group(0) @binding(3) var<storage, read> params: array<u32>;
20
-
21
- // params[0] = input_size
22
- // params[1] = output_size (vocab_size)
23
- // params[2] = weights_offset
24
-
25
- @compute @workgroup_size(256)
26
- fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
27
- let out_idx = gid.x;
28
- let input_size = params[0];
29
- let output_size = params[1];
30
- let w_offset = params[2];
31
-
32
- if (out_idx >= output_size) {
33
- return;
34
- }
35
-
36
- var sum: f32 = 0.0;
37
- for (var j: u32 = 0u; j < input_size; j = j + 1u) {
38
- let w_idx = w_offset + out_idx * input_size + j;
39
- if (w_idx < arrayLength(&weights)) {
40
- sum = sum + weights[w_idx] * input_tokens[j];
41
- }
42
- }
43
- output_logits[out_idx] = sum;
44
- }
45
- `;
46
- this.softmaxShader = `
47
- @group(0) @binding(0) var<storage, read_write> data: array<f32>;
48
- @group(0) @binding(1) var<storage, read> size_buf: array<u32>;
49
-
50
- @compute @workgroup_size(1)
51
- fn main() {
52
- let size = size_buf[0];
53
-
54
- var max_val: f32 = data[0];
55
- for (var i: u32 = 1u; i < size; i = i + 1u) {
56
- if (data[i] > max_val) {
57
- max_val = data[i];
58
- }
59
- }
60
-
61
- var sum: f32 = 0.0;
62
- for (var i: u32 = 0u; i < size; i = i + 1u) {
63
- let e = exp(data[i] - max_val);
64
- data[i] = e;
65
- sum = sum + e;
66
- }
67
-
68
- for (var i: u32 = 0u; i < size; i = i + 1u) {
69
- data[i] = data[i] / sum;
70
- }
71
- }
72
- `;
73
- }
74
- async initialize() {
75
- if (!navigator.gpu) {
76
- console.warn("[Hanzo AI] WebGPU not supported in this browser");
77
- return false;
78
- }
79
- try {
80
- const adapter = await navigator.gpu.requestAdapter({
81
- powerPreference: "high-performance"
82
- });
83
- if (!adapter) {
84
- console.warn("[Hanzo AI] No GPU adapter found");
85
- return false;
86
- }
87
- this.device = await adapter.requestDevice({
88
- requiredLimits: {
89
- maxStorageBufferBindingSize: adapter.limits?.maxStorageBufferBindingSize || 134217728,
90
- maxBufferSize: adapter.limits?.maxBufferSize || 268435456
91
- }
92
- });
93
- this.device.lost.then((info) => {
94
- console.error("[Hanzo AI] GPU device lost:", info.message);
95
- this.device = null;
96
- });
97
- console.log("[Hanzo AI] WebGPU initialized successfully");
98
- const features = adapter.features;
99
- console.log("[Hanzo AI] GPU Features:", Array.from(features));
100
- return true;
101
- } catch (error) {
102
- console.error("[Hanzo AI] WebGPU initialization failed:", error);
103
- return false;
104
- }
105
- }
106
- initByteEncoder() {
107
- if (this.byteEncoderInitialized) return;
108
- const bs = [];
109
- for (let i = 33; i <= 126; i++) bs.push(i);
110
- for (let i = 161; i <= 172; i++) bs.push(i);
111
- for (let i = 174; i <= 255; i++) bs.push(i);
112
- const cs = bs.slice();
113
- let n = 0;
114
- for (let b = 0; b < 256; b++) {
115
- if (!bs.includes(b)) {
116
- bs.push(b);
117
- cs.push(256 + n);
118
- n++;
119
- }
120
- }
121
- for (let i = 0; i < bs.length; i++) {
122
- this.byteEncoder[bs[i]] = String.fromCharCode(cs[i]);
123
- this.byteDecoder[String.fromCharCode(cs[i])] = bs[i];
124
- }
125
- this.byteEncoderInitialized = true;
126
- }
127
- tokenize(text, model) {
128
- this.initByteEncoder();
129
- if (!model.vocab.length) {
130
- const tokens2 = [];
131
- for (let i = 0; i < text.length; i++) {
132
- tokens2.push(text.charCodeAt(i));
133
- }
134
- return tokens2;
135
- }
136
- const bytes = new TextEncoder().encode(text);
137
- let word = "";
138
- for (const b of bytes) {
139
- word += this.byteEncoder[b] || String.fromCharCode(b);
140
- }
141
- const tokens = [];
142
- let pos = 0;
143
- while (pos < word.length) {
144
- let bestLen = 0;
145
- let bestIdx = -1;
146
- const maxLen = Math.min(word.length - pos, 50);
147
- for (let len = maxLen; len >= 1; len--) {
148
- const candidate = word.substring(pos, pos + len);
149
- const idx = model.vocabIndex.get(candidate);
150
- if (idx !== void 0) {
151
- bestLen = len;
152
- bestIdx = idx;
153
- break;
154
- }
155
- }
156
- if (bestIdx !== -1) {
157
- tokens.push(bestIdx);
158
- pos += bestLen;
159
- } else {
160
- tokens.push(word.charCodeAt(pos) % Math.max(model.vocab.length, 1));
161
- pos++;
162
- }
163
- }
164
- return tokens;
165
- }
166
- detokenize(tokenIds, model) {
167
- this.initByteEncoder();
168
- if (!model.vocab.length) {
169
- const arr = tokenIds instanceof Int32Array ? Array.from(tokenIds) : tokenIds;
170
- return String.fromCharCode(...arr.filter((t) => t > 0 && t < 65536));
171
- }
172
- let byteStr = "";
173
- for (const id of tokenIds) {
174
- if (id >= 0 && id < model.vocab.length) {
175
- byteStr += model.vocab[id];
176
- }
177
- }
178
- const decoded = [];
179
- for (const ch of byteStr) {
180
- if (ch in this.byteDecoder) {
181
- decoded.push(this.byteDecoder[ch]);
182
- } else {
183
- decoded.push(ch.charCodeAt(0) & 255);
184
- }
185
- }
186
- return new TextDecoder().decode(new Uint8Array(decoded));
187
- }
188
- // ---------------------------------------------------------------------------
189
- // Model Loading
190
- // ---------------------------------------------------------------------------
191
- async loadModel(config) {
192
- if (!this.device) {
193
- throw new Error("WebGPU not initialized");
194
- }
195
- console.log(`[Hanzo AI] Loading model: ${config.name}`);
196
- const modelData = await this._fetchWithCache(config.url, config.name);
197
- const modelBuffer = this.device.createBuffer({
198
- size: modelData.byteLength,
199
- usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
200
- label: `model-${config.name}`
201
- });
202
- this.device.queue.writeBuffer(modelBuffer, 0, modelData);
203
- let vocab = [];
204
- const vocabIndex = /* @__PURE__ */ new Map();
205
- if (config.vocabUrl) {
206
- try {
207
- const vocabResp = await fetch(config.vocabUrl);
208
- if (vocabResp.ok) {
209
- const vocabData = await vocabResp.json();
210
- if (Array.isArray(vocabData)) {
211
- vocab = vocabData;
212
- } else if (typeof vocabData === "object") {
213
- const entries = Object.entries(vocabData);
214
- entries.sort((a, b) => a[1] - b[1]);
215
- vocab = entries.map((e) => e[0]);
216
- }
217
- for (let i = 0; i < vocab.length; i++) {
218
- vocabIndex.set(vocab[i], i);
219
- }
220
- }
221
- } catch (e) {
222
- console.warn(`[Hanzo AI] Vocabulary load failed for ${config.name}:`, e);
223
- }
224
- }
225
- this.models.set(config.name, {
226
- buffer: modelBuffer,
227
- config,
228
- vocab,
229
- vocabIndex
230
- });
231
- console.log(`[Hanzo AI] Model ${config.name} loaded: ${modelData.byteLength} bytes, vocab: ${vocab.length}`);
232
- }
233
- // ---------------------------------------------------------------------------
234
- // Inference
235
- // ---------------------------------------------------------------------------
236
- async runInference(modelName, input) {
237
- const model = this.models.get(modelName);
238
- if (!model || !this.device) {
239
- throw new Error(`Model ${modelName} not loaded or GPU unavailable`);
240
- }
241
- const tokens = this.tokenize(input, model);
242
- const vocabSize = Math.max(model.vocab.length, 256);
243
- const inputSize = Math.min(tokens.length, 2048);
244
- const maxTokens = model.config.maxTokens || 128;
245
- const inputData = new Float32Array(inputSize);
246
- for (let i = 0; i < inputSize; i++) {
247
- inputData[i] = tokens[i] / vocabSize;
248
- }
249
- const inputBuffer = this.device.createBuffer({
250
- size: inputData.byteLength,
251
- usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
252
- label: "input"
253
- });
254
- this.device.queue.writeBuffer(inputBuffer, 0, inputData);
255
- const outputByteSize = vocabSize * 4;
256
- const outputBuffer = this.device.createBuffer({
257
- size: outputByteSize,
258
- usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
259
- label: "output"
260
- });
261
- const paramsData = new Uint32Array([inputSize, vocabSize, 0]);
262
- const paramsBuffer = this.device.createBuffer({
263
- size: paramsData.byteLength,
264
- usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
265
- label: "params"
266
- });
267
- this.device.queue.writeBuffer(paramsBuffer, 0, paramsData);
268
- const readBuffer = this.device.createBuffer({
269
- size: outputByteSize,
270
- usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
271
- label: "readback"
272
- });
273
- const matmulModule = this.device.createShaderModule({ code: this.matmulShader });
274
- const matmulLayout = this.device.createBindGroupLayout({
275
- entries: [
276
- { binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } },
277
- { binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } },
278
- { binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } },
279
- { binding: 3, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } }
280
- ]
281
- });
282
- const matmulPipeline = this.device.createComputePipeline({
283
- layout: this.device.createPipelineLayout({ bindGroupLayouts: [matmulLayout] }),
284
- compute: { module: matmulModule, entryPoint: "main" }
285
- });
286
- const matmulBindGroup = this.device.createBindGroup({
287
- layout: matmulLayout,
288
- entries: [
289
- { binding: 0, resource: { buffer: model.buffer } },
290
- { binding: 1, resource: { buffer: inputBuffer } },
291
- { binding: 2, resource: { buffer: outputBuffer } },
292
- { binding: 3, resource: { buffer: paramsBuffer } }
293
- ]
294
- });
295
- const softmaxModule = this.device.createShaderModule({ code: this.softmaxShader });
296
- const softmaxLayout = this.device.createBindGroupLayout({
297
- entries: [
298
- { binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } },
299
- { binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } }
300
- ]
301
- });
302
- const sizeBuf = this.device.createBuffer({
303
- size: 4,
304
- usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
305
- });
306
- this.device.queue.writeBuffer(sizeBuf, 0, new Uint32Array([vocabSize]));
307
- const softmaxPipeline = this.device.createComputePipeline({
308
- layout: this.device.createPipelineLayout({ bindGroupLayouts: [softmaxLayout] }),
309
- compute: { module: softmaxModule, entryPoint: "main" }
310
- });
311
- const softmaxBindGroup = this.device.createBindGroup({
312
- layout: softmaxLayout,
313
- entries: [
314
- { binding: 0, resource: { buffer: outputBuffer } },
315
- { binding: 1, resource: { buffer: sizeBuf } }
316
- ]
317
- });
318
- const generatedTokens = [];
319
- const temperature = 0.7;
320
- for (let step = 0; step < maxTokens; step++) {
321
- const encoder = this.device.createCommandEncoder();
322
- const matmulPass = encoder.beginComputePass();
323
- matmulPass.setPipeline(matmulPipeline);
324
- matmulPass.setBindGroup(0, matmulBindGroup);
325
- matmulPass.dispatchWorkgroups(Math.ceil(vocabSize / 256));
326
- matmulPass.end();
327
- const softmaxPass = encoder.beginComputePass();
328
- softmaxPass.setPipeline(softmaxPipeline);
329
- softmaxPass.setBindGroup(0, softmaxBindGroup);
330
- softmaxPass.dispatchWorkgroups(1);
331
- softmaxPass.end();
332
- encoder.copyBufferToBuffer(outputBuffer, 0, readBuffer, 0, outputByteSize);
333
- this.device.queue.submit([encoder.finish()]);
334
- await readBuffer.mapAsync(GPUMapMode.READ);
335
- const probs = new Float32Array(readBuffer.getMappedRange().slice(0));
336
- readBuffer.unmap();
337
- const nextToken = this.sampleToken(probs, temperature);
338
- generatedTokens.push(nextToken);
339
- if (nextToken === 0 || nextToken === 2) break;
340
- const newInput = new Float32Array([nextToken / vocabSize]);
341
- this.device.queue.writeBuffer(inputBuffer, 0, newInput);
342
- paramsData[0] = 1;
343
- this.device.queue.writeBuffer(paramsBuffer, 0, paramsData);
344
- }
345
- inputBuffer.destroy();
346
- outputBuffer.destroy();
347
- paramsBuffer.destroy();
348
- readBuffer.destroy();
349
- sizeBuf.destroy();
350
- return this.detokenize(generatedTokens, model);
351
- }
352
- sampleToken(probs, temperature) {
353
- if (temperature <= 0) {
354
- let maxIdx = 0;
355
- let maxVal = probs[0];
356
- for (let i = 1; i < probs.length; i++) {
357
- if (probs[i] > maxVal) {
358
- maxVal = probs[i];
359
- maxIdx = i;
360
- }
361
- }
362
- return maxIdx;
363
- }
364
- const scaled = new Float32Array(probs.length);
365
- let sum = 0;
366
- for (let i = 0; i < probs.length; i++) {
367
- scaled[i] = Math.exp(Math.log(Math.max(probs[i], 1e-10)) / temperature);
368
- sum += scaled[i];
369
- }
370
- for (let i = 0; i < scaled.length; i++) {
371
- scaled[i] /= sum;
372
- }
373
- const r = Math.random();
374
- let cumulative = 0;
375
- for (let i = 0; i < scaled.length; i++) {
376
- cumulative += scaled[i];
377
- if (r < cumulative) return i;
378
- }
379
- return scaled.length - 1;
380
- }
381
- // ---------------------------------------------------------------------------
382
- // Embedding computation (for DOM element analysis)
383
- // ---------------------------------------------------------------------------
384
- async computeEmbedding(modelName, text) {
385
- const model = this.models.get(modelName);
386
- if (!model || !this.device) {
387
- throw new Error(`Model ${modelName} not loaded or GPU unavailable`);
388
- }
389
- const tokens = this.tokenize(text, model);
390
- const inputSize = Math.min(tokens.length, 512);
391
- const embeddingDim = 256;
392
- const inputData = new Float32Array(inputSize);
393
- for (let i = 0; i < inputSize; i++) {
394
- inputData[i] = tokens[i];
395
- }
396
- const inputBuffer = this.device.createBuffer({
397
- size: inputData.byteLength,
398
- usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
399
- });
400
- this.device.queue.writeBuffer(inputBuffer, 0, inputData);
401
- const embeddingBuffer = this.device.createBuffer({
402
- size: embeddingDim * 4,
403
- usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST
404
- });
405
- const paramData = new Uint32Array([inputSize, embeddingDim, 0]);
406
- const paramBuffer = this.device.createBuffer({
407
- size: paramData.byteLength,
408
- usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
409
- });
410
- this.device.queue.writeBuffer(paramBuffer, 0, paramData);
411
- const shaderModule = this.device.createShaderModule({ code: this.matmulShader });
412
- const layout = this.device.createBindGroupLayout({
413
- entries: [
414
- { binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } },
415
- { binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } },
416
- { binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } },
417
- { binding: 3, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } }
418
- ]
419
- });
420
- const pipeline = this.device.createComputePipeline({
421
- layout: this.device.createPipelineLayout({ bindGroupLayouts: [layout] }),
422
- compute: { module: shaderModule, entryPoint: "main" }
423
- });
424
- const bindGroup = this.device.createBindGroup({
425
- layout,
426
- entries: [
427
- { binding: 0, resource: { buffer: model.buffer } },
428
- { binding: 1, resource: { buffer: inputBuffer } },
429
- { binding: 2, resource: { buffer: embeddingBuffer } },
430
- { binding: 3, resource: { buffer: paramBuffer } }
431
- ]
432
- });
433
- const readBuffer = this.device.createBuffer({
434
- size: embeddingDim * 4,
435
- usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
436
- });
437
- const encoder = this.device.createCommandEncoder();
438
- const pass = encoder.beginComputePass();
439
- pass.setPipeline(pipeline);
440
- pass.setBindGroup(0, bindGroup);
441
- pass.dispatchWorkgroups(Math.ceil(embeddingDim / 256));
442
- pass.end();
443
- encoder.copyBufferToBuffer(embeddingBuffer, 0, readBuffer, 0, embeddingDim * 4);
444
- this.device.queue.submit([encoder.finish()]);
445
- await readBuffer.mapAsync(GPUMapMode.READ);
446
- const embedding = new Float32Array(readBuffer.getMappedRange().slice(0));
447
- readBuffer.unmap();
448
- let norm = 0;
449
- for (let i = 0; i < embedding.length; i++) {
450
- norm += embedding[i] * embedding[i];
451
- }
452
- norm = Math.sqrt(norm);
453
- if (norm > 0) {
454
- for (let i = 0; i < embedding.length; i++) {
455
- embedding[i] /= norm;
456
- }
457
- }
458
- inputBuffer.destroy();
459
- embeddingBuffer.destroy();
460
- paramBuffer.destroy();
461
- readBuffer.destroy();
462
- return embedding;
463
- }
464
- // ---------------------------------------------------------------------------
465
- // Cached, progress-reporting fetch — used by loadModel().
466
- // ---------------------------------------------------------------------------
467
- /**
468
- * Fetch a model blob with persistent caching. Backed by the service-worker
469
- * Cache API so the bytes survive worker restarts and only download once
470
- * per URL. Streams the body to log progress every ~5% so big artifacts
471
- * (huggingface.co/<repo>/resolve/main/<file>) don't appear to hang.
472
- *
473
- * Cache invalidation: the URL is the key; bump the URL (e.g. point to a
474
- * different revision sha) to force a re-fetch. Use ``WebGPUAI.evictCache``
475
- * to wipe everything when models go bad.
476
- */
477
- async _fetchWithCache(url, label) {
478
- const cacheName = "hanzo-webgpu-models-v1";
479
- const cache = typeof caches !== "undefined" ? await caches.open(cacheName) : null;
480
- if (cache) {
481
- const hit = await cache.match(url);
482
- if (hit) {
483
- console.log(`[Hanzo AI] Cache hit: ${label} (${url})`);
484
- return await hit.arrayBuffer();
485
- }
486
- }
487
- console.log(`[Hanzo AI] Fetching: ${label} (${url})`);
488
- const response = await fetch(url);
489
- if (!response.ok) {
490
- throw new Error(`Failed to fetch ${label}: ${response.status} ${response.statusText}`);
491
- }
492
- const total = Number(response.headers.get("content-length") || 0);
493
- let received = 0;
494
- let nextLog = total > 0 ? Math.floor(total * 0.05) : Infinity;
495
- const chunks = [];
496
- if (response.body) {
497
- const reader = response.body.getReader();
498
- while (true) {
499
- const { done, value } = await reader.read();
500
- if (done) break;
501
- chunks.push(value);
502
- received += value.byteLength;
503
- if (total > 0 && received >= nextLog) {
504
- const pct = Math.floor(received / total * 100);
505
- console.log(`[Hanzo AI] ${label}: ${pct}% (${received}/${total} bytes)`);
506
- nextLog = received + Math.floor(total * 0.05);
507
- }
508
- }
509
- } else {
510
- chunks.push(new Uint8Array(await response.arrayBuffer()));
511
- }
512
- const blob = new Uint8Array(received || chunks.reduce((n, c) => n + c.byteLength, 0));
513
- let offset = 0;
514
- for (const c of chunks) {
515
- blob.set(c, offset);
516
- offset += c.byteLength;
517
- }
518
- console.log(`[Hanzo AI] Fetched: ${label} (${blob.byteLength} bytes)`);
519
- if (cache) {
520
- try {
521
- await cache.put(url, new Response(blob, {
522
- headers: { "content-type": "application/octet-stream" }
523
- }));
524
- console.log(`[Hanzo AI] Cached: ${label}`);
525
- } catch (e) {
526
- console.warn(`[Hanzo AI] Cache write failed for ${label}:`, e);
527
- }
528
- }
529
- return blob.buffer;
530
- }
531
- /** Build an HF resolve URL for a given repo / file / revision. */
532
- static huggingFaceURL(repo, file, revision = "main") {
533
- return `https://huggingface.co/${repo}/resolve/${revision}/${file}`;
534
- }
535
- /** Wipe every cached model blob. Useful when a model artifact is rotated. */
536
- static async evictCache() {
537
- if (typeof caches === "undefined") return;
538
- await caches.delete("hanzo-webgpu-models-v1");
539
- console.log("[Hanzo AI] WebGPU model cache evicted");
540
- }
541
- // ---------------------------------------------------------------------------
542
- // Status / Cleanup
543
- // ---------------------------------------------------------------------------
544
- getStatus() {
545
- return {
546
- initialized: !!this.device,
547
- models: Array.from(this.models.keys())
548
- };
549
- }
550
- async unloadModel(name) {
551
- const model = this.models.get(name);
552
- if (model) {
553
- model.buffer.destroy();
554
- this.models.delete(name);
555
- console.log(`[Hanzo AI] Model ${name} unloaded`);
556
- }
557
- }
558
- destroy() {
559
- for (const [name, model] of this.models) {
560
- model.buffer.destroy();
561
- }
562
- this.models.clear();
563
- this.device = null;
564
- }
565
- };
566
- export {
567
- WebGPUAI
568
- };