Spaces:
Running
Running
File size: 4,108 Bytes
75f7ae6 4eff5b5 75f7ae6 4eff5b5 75f7ae6 4eff5b5 75f7ae6 4eff5b5 75f7ae6 4eff5b5 75f7ae6 4eff5b5 75f7ae6 4eff5b5 75f7ae6 4eff5b5 75f7ae6 4eff5b5 75f7ae6 4eff5b5 75f7ae6 4eff5b5 75f7ae6 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 | /**
* embed-worker.js β Background Web Worker for transformers.js embedding
*
* Runs the ONNX embedding model off the main thread so the UI stays responsive.
* Uses a singleton pattern to ensure the model loads only once.
*
* Protocol:
* Main β Worker:
* { type: 'load', modelId, dtype } β Load/switch model
* { type: 'embed', texts, id } β Embed a batch of texts
* { type: 'unload' } β Release model memory
*
* Worker β Main (all tagged with source: 'vecdb'):
* { source, status: 'progress', file, progress, loaded, total }
* { source, status: 'ready', dim } β Model ready
* { source, status: 'result', id, embeddings, dims }
* { source, status: 'error', id?, message }
* { source, status: 'unloaded' }
*/
import { pipeline } from 'https://cdn.jsdelivr.net/npm/@huggingface/transformers@3';
const MSG_TAG = 'vecdb';
let extractor = null;
let currentModel = null;
let currentDtype = null;
let loadingPromise = null;
function send(msg) {
self.postMessage({ ...msg, source: MSG_TAG });
}
function sendTransfer(msg, transfer) {
self.postMessage({ ...msg, source: MSG_TAG }, transfer);
}
async function loadModel(modelId, dtype) {
// If already loading the same model, wait for it
if (loadingPromise && currentModel === modelId && currentDtype === dtype) {
return loadingPromise;
}
// If switching models, dispose old one
if (extractor) {
try { await extractor.dispose(); } catch {}
extractor = null;
}
currentModel = modelId;
currentDtype = dtype;
loadingPromise = pipeline('feature-extraction', modelId, {
dtype: dtype,
progress_callback: (p) => {
// Only relay download progress events we care about.
// Transformers.js emits: initiate, download, progress, done, ready
// We only forward 'progress' (with loaded/total) so the main thread
// can update the progress bar. Everything else is ignored to avoid
// status collisions (e.g. transformers.js 'ready' vs our 'ready').
if (p.status === 'progress' && p.total > 0) {
send({ status: 'dl-progress', file: p.file, loaded: p.loaded, total: p.total });
}
},
});
extractor = await loadingPromise;
return extractor;
}
self.addEventListener('message', async (event) => {
const { type, id } = event.data;
try {
switch (type) {
case 'load': {
const { modelId, dtype } = event.data;
await loadModel(modelId, dtype);
// Probe dimension by embedding a test string
const test = await extractor(['test'], { pooling: 'mean', normalize: true });
const dim = test.dims[1];
send({ status: 'ready', dim });
break;
}
case 'embed': {
if (!extractor) {
send({ status: 'error', id, message: 'Model not loaded' });
return;
}
const { texts } = event.data;
const output = await extractor(texts, { pooling: 'mean', normalize: true });
// Copy to a fresh Float32Array for zero-copy transfer
const float32 = new Float32Array(output.data);
sendTransfer(
{ status: 'result', id, embeddings: float32, dims: output.dims },
[float32.buffer]
);
break;
}
case 'unload': {
if (extractor) {
try { await extractor.dispose(); } catch {}
extractor = null;
}
currentModel = null;
currentDtype = null;
loadingPromise = null;
send({ status: 'unloaded' });
break;
}
}
} catch (err) {
send({ status: 'error', id, message: err.message || String(err) });
}
});
|