-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathoffscreen.js
More file actions
159 lines (135 loc) · 4.83 KB
/
Copy pathoffscreen.js
File metadata and controls
159 lines (135 loc) · 4.83 KB
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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
// offscreen.js - ML inference for Stash (formerly Cyberbook)
// FEATURES:
// - Lazy model loading with progress reporting
// - Queue management for embedding generation
// - Timeout handling for model loading
// - Persistent model promise (no reset on success)
import { pipeline } from '@huggingface/transformers';
const MODEL_ID = 'Xenova/all-MiniLM-L6-v2';
const MODEL_TIMEOUT_MS = 120000; // 2 minutes for initial download
const EMBEDDING_TIMEOUT_MS = 30000; // 30 seconds per embedding
// State - model promise persists after successful load
let modelPromise = null;
let embeddingQueue = [];
let isProcessingQueue = false;
let loadingProgress = 0;
/**
* Load the embedding model with timeout and progress reporting
* Promise is cached - model only loads once
*/
async function getModel() {
if (modelPromise) {
return modelPromise;
}
console.log('[Stash Offscreen] Loading model...');
const startTime = performance.now();
loadingProgress = 0;
modelPromise = Promise.race([
pipeline('feature-extraction', MODEL_ID, {
dtype: 'q8',
progress_callback: (progress) => {
if (progress.progress !== undefined) {
loadingProgress = Math.round(progress.progress);
// Broadcast progress to popup (if listening)
try {
chrome.runtime.sendMessage({
type: 'MODEL_PROGRESS',
progress: loadingProgress,
status: progress.status || 'loading'
}).catch(() => {}); // Ignore if no listeners
} catch (_) {}
}
}
}),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Model load timeout')), MODEL_TIMEOUT_MS)
)
]).then(model => {
const elapsed = Math.round(performance.now() - startTime);
console.log(`[Stash Offscreen] Model loaded in ${elapsed}ms`);
loadingProgress = 100;
return model;
}).catch(error => {
// Reset promise on failure so retry is possible
modelPromise = null;
loadingProgress = 0;
throw error;
});
return modelPromise;
}
/**
* Generate embedding for text with timeout
*/
async function generateEmbedding(text) {
if (!text || typeof text !== 'string') {
throw new Error('Text is required');
}
const model = await getModel();
// Truncate to reasonable length (model context limit)
const truncated = text.substring(0, 8000);
const result = await Promise.race([
model(truncated, { pooling: 'mean', normalize: true }),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Embedding timeout')), EMBEDDING_TIMEOUT_MS)
)
]);
// Extract embedding array
const embedding = Array.from(result.data);
return embedding;
}
/**
* Queue-based embedding generation to prevent overload
*/
function queueEmbedding(text, resolve, reject) {
embeddingQueue.push({ text, resolve, reject });
processQueue();
}
async function processQueue() {
if (isProcessingQueue || embeddingQueue.length === 0) {
return;
}
isProcessingQueue = true;
while (embeddingQueue.length > 0) {
const { text, resolve, reject } = embeddingQueue.shift();
try {
const embedding = await generateEmbedding(text);
resolve({ success: true, embedding });
} catch (error) {
console.error('[Stash Offscreen] Embedding error:', error);
reject({ success: false, error: error.message });
}
}
isProcessingQueue = false;
}
// Message listener
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.target !== 'offscreen') {
return false;
}
switch (message.type) {
case 'GENERATE_EMBEDDING':
// Use queue for backpressure management
new Promise((resolve, reject) => {
queueEmbedding(message.text, resolve, reject);
}).then(sendResponse).catch(sendResponse);
return true;
case 'WARMUP_MODEL':
// Lazy load - just trigger model load, don't wait
getModel()
.then(() => sendResponse({ success: true }))
.catch(error => sendResponse({ success: false, error: error.message }));
return true;
case 'GET_STATUS':
sendResponse({
modelLoaded: modelPromise !== null,
queueLength: embeddingQueue.length,
isProcessing: isProcessingQueue,
loadingProgress
});
return true;
default:
sendResponse({ error: 'Unknown message type' });
return true;
}
});
console.log('[Stash Offscreen] Ready');