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) });
    }
});