edgeflowjs 0.1.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 +473 -0
- package/dist/backends/index.d.ts +13 -0
- package/dist/backends/index.d.ts.map +1 -0
- package/dist/backends/index.js +32 -0
- package/dist/backends/index.js.map +1 -0
- package/dist/backends/onnx.d.ts +46 -0
- package/dist/backends/onnx.d.ts.map +1 -0
- package/dist/backends/onnx.js +249 -0
- package/dist/backends/onnx.js.map +1 -0
- package/dist/backends/wasm.d.ts +78 -0
- package/dist/backends/wasm.d.ts.map +1 -0
- package/dist/backends/wasm.js +358 -0
- package/dist/backends/wasm.js.map +1 -0
- package/dist/backends/webgpu.d.ts +143 -0
- package/dist/backends/webgpu.d.ts.map +1 -0
- package/dist/backends/webgpu.js +326 -0
- package/dist/backends/webgpu.js.map +1 -0
- package/dist/backends/webnn.d.ts +115 -0
- package/dist/backends/webnn.d.ts.map +1 -0
- package/dist/backends/webnn.js +202 -0
- package/dist/backends/webnn.js.map +1 -0
- package/dist/core/index.d.ts +9 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +14 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/memory.d.ts +234 -0
- package/dist/core/memory.d.ts.map +1 -0
- package/dist/core/memory.js +554 -0
- package/dist/core/memory.js.map +1 -0
- package/dist/core/runtime.d.ts +129 -0
- package/dist/core/runtime.d.ts.map +1 -0
- package/dist/core/runtime.js +352 -0
- package/dist/core/runtime.js.map +1 -0
- package/dist/core/scheduler.d.ts +118 -0
- package/dist/core/scheduler.d.ts.map +1 -0
- package/dist/core/scheduler.js +600 -0
- package/dist/core/scheduler.js.map +1 -0
- package/dist/core/tensor.d.ts +149 -0
- package/dist/core/tensor.d.ts.map +1 -0
- package/dist/core/tensor.js +719 -0
- package/dist/core/tensor.js.map +1 -0
- package/dist/core/types.d.ts +367 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +54 -0
- package/dist/core/types.js.map +1 -0
- package/dist/edgeflow.browser.js +5601 -0
- package/dist/edgeflow.browser.js.map +7 -0
- package/dist/edgeflow.browser.min.js +19 -0
- package/dist/edgeflow.browser.min.js.map +7 -0
- package/dist/index.d.ts +71 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +158 -0
- package/dist/index.js.map +1 -0
- package/dist/pipelines/base.d.ts +122 -0
- package/dist/pipelines/base.d.ts.map +1 -0
- package/dist/pipelines/base.js +155 -0
- package/dist/pipelines/base.js.map +1 -0
- package/dist/pipelines/feature-extraction.d.ts +68 -0
- package/dist/pipelines/feature-extraction.d.ts.map +1 -0
- package/dist/pipelines/feature-extraction.js +197 -0
- package/dist/pipelines/feature-extraction.js.map +1 -0
- package/dist/pipelines/image-classification.d.ts +61 -0
- package/dist/pipelines/image-classification.d.ts.map +1 -0
- package/dist/pipelines/image-classification.js +140 -0
- package/dist/pipelines/image-classification.js.map +1 -0
- package/dist/pipelines/index.d.ts +58 -0
- package/dist/pipelines/index.d.ts.map +1 -0
- package/dist/pipelines/index.js +72 -0
- package/dist/pipelines/index.js.map +1 -0
- package/dist/pipelines/text-classification.d.ts +71 -0
- package/dist/pipelines/text-classification.d.ts.map +1 -0
- package/dist/pipelines/text-classification.js +175 -0
- package/dist/pipelines/text-classification.js.map +1 -0
- package/dist/tools/index.d.ts +143 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +294 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/utils/cache.d.ts +162 -0
- package/dist/utils/cache.d.ts.map +1 -0
- package/dist/utils/cache.js +443 -0
- package/dist/utils/cache.js.map +1 -0
- package/dist/utils/index.d.ts +8 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +12 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/model-loader.d.ts +107 -0
- package/dist/utils/model-loader.d.ts.map +1 -0
- package/dist/utils/model-loader.js +694 -0
- package/dist/utils/model-loader.js.map +1 -0
- package/dist/utils/preprocessor.d.ts +147 -0
- package/dist/utils/preprocessor.d.ts.map +1 -0
- package/dist/utils/preprocessor.js +423 -0
- package/dist/utils/preprocessor.js.map +1 -0
- package/dist/utils/tokenizer.d.ts +140 -0
- package/dist/utils/tokenizer.d.ts.map +1 -0
- package/dist/utils/tokenizer.js +397 -0
- package/dist/utils/tokenizer.js.map +1 -0
- package/package.json +87 -0
|
@@ -0,0 +1,694 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* edgeFlow.js - Advanced Model Loader
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - Preloading: Background model loading
|
|
6
|
+
* - Sharding: Split large files into chunks for download
|
|
7
|
+
* - Resume Download: Continue download from where it left off
|
|
8
|
+
* - Model Caching: IndexedDB storage for large models
|
|
9
|
+
*/
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// IndexedDB Model Cache
|
|
12
|
+
// ============================================================================
|
|
13
|
+
const DB_NAME = 'edgeflow-model-cache';
|
|
14
|
+
const DB_VERSION = 1;
|
|
15
|
+
const STORE_META = 'meta';
|
|
16
|
+
const STORE_CHUNKS = 'chunks';
|
|
17
|
+
const STORE_STATE = 'download-state';
|
|
18
|
+
/**
|
|
19
|
+
* IndexedDB-based model cache for large files
|
|
20
|
+
*/
|
|
21
|
+
class ModelCache {
|
|
22
|
+
db = null;
|
|
23
|
+
dbPromise = null;
|
|
24
|
+
/**
|
|
25
|
+
* Open the database
|
|
26
|
+
*/
|
|
27
|
+
async openDB() {
|
|
28
|
+
if (this.db)
|
|
29
|
+
return this.db;
|
|
30
|
+
if (this.dbPromise)
|
|
31
|
+
return this.dbPromise;
|
|
32
|
+
this.dbPromise = new Promise((resolve, reject) => {
|
|
33
|
+
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
|
34
|
+
request.onupgradeneeded = (event) => {
|
|
35
|
+
const db = event.target.result;
|
|
36
|
+
// Model metadata store
|
|
37
|
+
if (!db.objectStoreNames.contains(STORE_META)) {
|
|
38
|
+
db.createObjectStore(STORE_META, { keyPath: 'url' });
|
|
39
|
+
}
|
|
40
|
+
// Chunk data store
|
|
41
|
+
if (!db.objectStoreNames.contains(STORE_CHUNKS)) {
|
|
42
|
+
const chunkStore = db.createObjectStore(STORE_CHUNKS, { keyPath: ['url', 'index'] });
|
|
43
|
+
chunkStore.createIndex('url', 'url', { unique: false });
|
|
44
|
+
}
|
|
45
|
+
// Download state store (for resume)
|
|
46
|
+
if (!db.objectStoreNames.contains(STORE_STATE)) {
|
|
47
|
+
db.createObjectStore(STORE_STATE, { keyPath: 'url' });
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
request.onsuccess = () => {
|
|
51
|
+
this.db = request.result;
|
|
52
|
+
resolve(this.db);
|
|
53
|
+
};
|
|
54
|
+
request.onerror = () => reject(request.error);
|
|
55
|
+
});
|
|
56
|
+
return this.dbPromise;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Get cached model metadata
|
|
60
|
+
*/
|
|
61
|
+
async getMeta(url) {
|
|
62
|
+
const db = await this.openDB();
|
|
63
|
+
return new Promise((resolve, reject) => {
|
|
64
|
+
const tx = db.transaction(STORE_META, 'readonly');
|
|
65
|
+
const store = tx.objectStore(STORE_META);
|
|
66
|
+
const request = store.get(url);
|
|
67
|
+
request.onsuccess = () => resolve(request.result ?? null);
|
|
68
|
+
request.onerror = () => reject(request.error);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Save model metadata
|
|
73
|
+
*/
|
|
74
|
+
async saveMeta(meta) {
|
|
75
|
+
const db = await this.openDB();
|
|
76
|
+
return new Promise((resolve, reject) => {
|
|
77
|
+
const tx = db.transaction(STORE_META, 'readwrite');
|
|
78
|
+
const store = tx.objectStore(STORE_META);
|
|
79
|
+
store.put(meta);
|
|
80
|
+
tx.oncomplete = () => resolve();
|
|
81
|
+
tx.onerror = () => reject(tx.error);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Save a chunk
|
|
86
|
+
*/
|
|
87
|
+
async saveChunk(url, index, data) {
|
|
88
|
+
const db = await this.openDB();
|
|
89
|
+
return new Promise((resolve, reject) => {
|
|
90
|
+
const tx = db.transaction(STORE_CHUNKS, 'readwrite');
|
|
91
|
+
const store = tx.objectStore(STORE_CHUNKS);
|
|
92
|
+
store.put({ url, index, data });
|
|
93
|
+
tx.oncomplete = () => resolve();
|
|
94
|
+
tx.onerror = () => reject(tx.error);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Get all chunks for a URL
|
|
99
|
+
*/
|
|
100
|
+
async getChunks(url) {
|
|
101
|
+
const db = await this.openDB();
|
|
102
|
+
return new Promise((resolve, reject) => {
|
|
103
|
+
const tx = db.transaction(STORE_CHUNKS, 'readonly');
|
|
104
|
+
const store = tx.objectStore(STORE_CHUNKS);
|
|
105
|
+
const index = store.index('url');
|
|
106
|
+
const request = index.getAll(url);
|
|
107
|
+
request.onsuccess = () => {
|
|
108
|
+
const results = request.result;
|
|
109
|
+
// Sort by index and extract data
|
|
110
|
+
results.sort((a, b) => a.index - b.index);
|
|
111
|
+
resolve(results.map(r => r.data));
|
|
112
|
+
};
|
|
113
|
+
request.onerror = () => reject(request.error);
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Get complete model data (merged chunks)
|
|
118
|
+
*/
|
|
119
|
+
async getModel(url) {
|
|
120
|
+
const meta = await this.getMeta(url);
|
|
121
|
+
if (!meta || !meta.complete)
|
|
122
|
+
return null;
|
|
123
|
+
const chunks = await this.getChunks(url);
|
|
124
|
+
if (chunks.length === 0)
|
|
125
|
+
return null;
|
|
126
|
+
// Merge chunks
|
|
127
|
+
const totalSize = chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0);
|
|
128
|
+
const result = new Uint8Array(totalSize);
|
|
129
|
+
let offset = 0;
|
|
130
|
+
for (const chunk of chunks) {
|
|
131
|
+
result.set(new Uint8Array(chunk), offset);
|
|
132
|
+
offset += chunk.byteLength;
|
|
133
|
+
}
|
|
134
|
+
return result.buffer;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Save download state (for resume)
|
|
138
|
+
*/
|
|
139
|
+
async saveDownloadState(state) {
|
|
140
|
+
const db = await this.openDB();
|
|
141
|
+
return new Promise((resolve, reject) => {
|
|
142
|
+
const tx = db.transaction(STORE_STATE, 'readwrite');
|
|
143
|
+
const store = tx.objectStore(STORE_STATE);
|
|
144
|
+
store.put(state);
|
|
145
|
+
tx.oncomplete = () => resolve();
|
|
146
|
+
tx.onerror = () => reject(tx.error);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Get download state
|
|
151
|
+
*/
|
|
152
|
+
async getDownloadState(url) {
|
|
153
|
+
const db = await this.openDB();
|
|
154
|
+
return new Promise((resolve, reject) => {
|
|
155
|
+
const tx = db.transaction(STORE_STATE, 'readonly');
|
|
156
|
+
const store = tx.objectStore(STORE_STATE);
|
|
157
|
+
const request = store.get(url);
|
|
158
|
+
request.onsuccess = () => resolve(request.result ?? null);
|
|
159
|
+
request.onerror = () => reject(request.error);
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Delete download state
|
|
164
|
+
*/
|
|
165
|
+
async deleteDownloadState(url) {
|
|
166
|
+
const db = await this.openDB();
|
|
167
|
+
return new Promise((resolve, reject) => {
|
|
168
|
+
const tx = db.transaction(STORE_STATE, 'readwrite');
|
|
169
|
+
const store = tx.objectStore(STORE_STATE);
|
|
170
|
+
store.delete(url);
|
|
171
|
+
tx.oncomplete = () => resolve();
|
|
172
|
+
tx.onerror = () => reject(tx.error);
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Delete cached model
|
|
177
|
+
*/
|
|
178
|
+
async deleteModel(url) {
|
|
179
|
+
const db = await this.openDB();
|
|
180
|
+
// Delete metadata
|
|
181
|
+
await new Promise((resolve, reject) => {
|
|
182
|
+
const tx = db.transaction(STORE_META, 'readwrite');
|
|
183
|
+
const store = tx.objectStore(STORE_META);
|
|
184
|
+
store.delete(url);
|
|
185
|
+
tx.oncomplete = () => resolve();
|
|
186
|
+
tx.onerror = () => reject(tx.error);
|
|
187
|
+
});
|
|
188
|
+
// Delete chunks
|
|
189
|
+
const chunks = await this.getChunks(url);
|
|
190
|
+
if (chunks.length > 0) {
|
|
191
|
+
await new Promise((resolve, reject) => {
|
|
192
|
+
const tx = db.transaction(STORE_CHUNKS, 'readwrite');
|
|
193
|
+
const store = tx.objectStore(STORE_CHUNKS);
|
|
194
|
+
const index = store.index('url');
|
|
195
|
+
const request = index.openCursor(IDBKeyRange.only(url));
|
|
196
|
+
request.onsuccess = (event) => {
|
|
197
|
+
const cursor = event.target.result;
|
|
198
|
+
if (cursor) {
|
|
199
|
+
cursor.delete();
|
|
200
|
+
cursor.continue();
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
tx.oncomplete = () => resolve();
|
|
204
|
+
tx.onerror = () => reject(tx.error);
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
// Delete download state
|
|
208
|
+
await this.deleteDownloadState(url);
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Clear all cached models
|
|
212
|
+
*/
|
|
213
|
+
async clear() {
|
|
214
|
+
const db = await this.openDB();
|
|
215
|
+
const stores = [STORE_META, STORE_CHUNKS, STORE_STATE];
|
|
216
|
+
for (const storeName of stores) {
|
|
217
|
+
await new Promise((resolve, reject) => {
|
|
218
|
+
const tx = db.transaction(storeName, 'readwrite');
|
|
219
|
+
const store = tx.objectStore(storeName);
|
|
220
|
+
store.clear();
|
|
221
|
+
tx.oncomplete = () => resolve();
|
|
222
|
+
tx.onerror = () => reject(tx.error);
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Get cache statistics
|
|
228
|
+
*/
|
|
229
|
+
async getStats() {
|
|
230
|
+
const db = await this.openDB();
|
|
231
|
+
return new Promise((resolve, reject) => {
|
|
232
|
+
const tx = db.transaction(STORE_META, 'readonly');
|
|
233
|
+
const store = tx.objectStore(STORE_META);
|
|
234
|
+
const request = store.getAll();
|
|
235
|
+
request.onsuccess = () => {
|
|
236
|
+
const metas = request.result;
|
|
237
|
+
resolve({
|
|
238
|
+
models: metas.filter(m => m.complete).length,
|
|
239
|
+
totalSize: metas.reduce((sum, m) => sum + (m.complete ? m.size : 0), 0),
|
|
240
|
+
});
|
|
241
|
+
};
|
|
242
|
+
request.onerror = () => reject(request.error);
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
// Global cache instance
|
|
247
|
+
const modelCache = new ModelCache();
|
|
248
|
+
// ============================================================================
|
|
249
|
+
// Advanced Model Loader
|
|
250
|
+
// ============================================================================
|
|
251
|
+
/**
|
|
252
|
+
* Check if server supports Range requests
|
|
253
|
+
*/
|
|
254
|
+
async function supportsRangeRequests(url) {
|
|
255
|
+
try {
|
|
256
|
+
const response = await fetch(url, { method: 'HEAD' });
|
|
257
|
+
const acceptRanges = response.headers.get('Accept-Ranges');
|
|
258
|
+
const contentLength = response.headers.get('Content-Length');
|
|
259
|
+
const etag = response.headers.get('ETag') ?? undefined;
|
|
260
|
+
return {
|
|
261
|
+
supports: acceptRanges === 'bytes',
|
|
262
|
+
size: contentLength ? parseInt(contentLength, 10) : 0,
|
|
263
|
+
etag,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
return { supports: false, size: 0 };
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Download a single chunk using Range request
|
|
272
|
+
*/
|
|
273
|
+
async function downloadChunk(url, start, end, timeout) {
|
|
274
|
+
const controller = new AbortController();
|
|
275
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
276
|
+
try {
|
|
277
|
+
const response = await fetch(url, {
|
|
278
|
+
headers: { Range: `bytes=${start}-${end}` },
|
|
279
|
+
signal: controller.signal,
|
|
280
|
+
});
|
|
281
|
+
if (response.status !== 206 && response.status !== 200) {
|
|
282
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
283
|
+
}
|
|
284
|
+
return await response.arrayBuffer();
|
|
285
|
+
}
|
|
286
|
+
finally {
|
|
287
|
+
clearTimeout(timeoutId);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Download model with sharding and resume support
|
|
292
|
+
*/
|
|
293
|
+
async function downloadWithResume(url, options) {
|
|
294
|
+
const { chunkSize = 5 * 1024 * 1024, // 5MB
|
|
295
|
+
parallelConnections = 4, timeout = 30000, onProgress, } = options;
|
|
296
|
+
// Check server capabilities
|
|
297
|
+
const { supports: supportsRange, size: totalSize, etag } = await supportsRangeRequests(url);
|
|
298
|
+
// If no Range support or small file, download normally
|
|
299
|
+
if (!supportsRange || totalSize < chunkSize * 2) {
|
|
300
|
+
return downloadSimple(url, timeout, onProgress);
|
|
301
|
+
}
|
|
302
|
+
// Check for existing download state
|
|
303
|
+
let state = await modelCache.getDownloadState(url);
|
|
304
|
+
// Initialize or reset state if needed
|
|
305
|
+
if (!state || (etag && state.totalSize !== totalSize)) {
|
|
306
|
+
const numChunks = Math.ceil(totalSize / chunkSize);
|
|
307
|
+
const chunks = [];
|
|
308
|
+
for (let i = 0; i < numChunks; i++) {
|
|
309
|
+
const start = i * chunkSize;
|
|
310
|
+
const end = Math.min(start + chunkSize - 1, totalSize - 1);
|
|
311
|
+
chunks.push({ index: i, start, end, downloaded: false });
|
|
312
|
+
}
|
|
313
|
+
state = {
|
|
314
|
+
url,
|
|
315
|
+
totalSize,
|
|
316
|
+
downloadedSize: 0,
|
|
317
|
+
chunks,
|
|
318
|
+
startedAt: Date.now(),
|
|
319
|
+
};
|
|
320
|
+
// Clear any existing chunks
|
|
321
|
+
await modelCache.deleteModel(url);
|
|
322
|
+
}
|
|
323
|
+
// Download remaining chunks
|
|
324
|
+
const pendingChunks = state.chunks.filter(c => !c.downloaded);
|
|
325
|
+
let downloadedSize = state.downloadedSize;
|
|
326
|
+
const startTime = Date.now();
|
|
327
|
+
let lastProgressTime = startTime;
|
|
328
|
+
let lastDownloadedSize = downloadedSize;
|
|
329
|
+
// Progress tracking
|
|
330
|
+
const reportProgress = () => {
|
|
331
|
+
if (!onProgress)
|
|
332
|
+
return;
|
|
333
|
+
const now = Date.now();
|
|
334
|
+
const elapsed = (now - lastProgressTime) / 1000;
|
|
335
|
+
const bytesDownloaded = downloadedSize - lastDownloadedSize;
|
|
336
|
+
const speed = elapsed > 0 ? bytesDownloaded / elapsed : 0;
|
|
337
|
+
const remaining = totalSize - downloadedSize;
|
|
338
|
+
const eta = speed > 0 ? (remaining / speed) * 1000 : 0;
|
|
339
|
+
onProgress({
|
|
340
|
+
loaded: downloadedSize,
|
|
341
|
+
total: totalSize,
|
|
342
|
+
percent: (downloadedSize / totalSize) * 100,
|
|
343
|
+
speed,
|
|
344
|
+
eta,
|
|
345
|
+
currentChunk: state.chunks.filter(c => c.downloaded).length,
|
|
346
|
+
totalChunks: state.chunks.length,
|
|
347
|
+
});
|
|
348
|
+
lastProgressTime = now;
|
|
349
|
+
lastDownloadedSize = downloadedSize;
|
|
350
|
+
};
|
|
351
|
+
// Download chunks in parallel
|
|
352
|
+
const downloadQueue = [...pendingChunks];
|
|
353
|
+
const inProgress = new Map();
|
|
354
|
+
while (downloadQueue.length > 0 || inProgress.size > 0) {
|
|
355
|
+
// Start new downloads up to parallelConnections limit
|
|
356
|
+
while (downloadQueue.length > 0 && inProgress.size < parallelConnections) {
|
|
357
|
+
const chunk = downloadQueue.shift();
|
|
358
|
+
const downloadPromise = (async () => {
|
|
359
|
+
try {
|
|
360
|
+
const data = await downloadChunk(url, chunk.start, chunk.end, timeout);
|
|
361
|
+
await modelCache.saveChunk(url, chunk.index, data);
|
|
362
|
+
chunk.downloaded = true;
|
|
363
|
+
downloadedSize += data.byteLength;
|
|
364
|
+
// Update state periodically
|
|
365
|
+
state.downloadedSize = downloadedSize;
|
|
366
|
+
await modelCache.saveDownloadState(state);
|
|
367
|
+
reportProgress();
|
|
368
|
+
}
|
|
369
|
+
finally {
|
|
370
|
+
inProgress.delete(chunk.index);
|
|
371
|
+
}
|
|
372
|
+
})();
|
|
373
|
+
inProgress.set(chunk.index, downloadPromise);
|
|
374
|
+
}
|
|
375
|
+
// Wait for at least one to complete
|
|
376
|
+
if (inProgress.size > 0) {
|
|
377
|
+
await Promise.race(inProgress.values());
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
// All chunks downloaded, merge them
|
|
381
|
+
const chunks = await modelCache.getChunks(url);
|
|
382
|
+
const result = new Uint8Array(totalSize);
|
|
383
|
+
let offset = 0;
|
|
384
|
+
for (const chunk of chunks) {
|
|
385
|
+
result.set(new Uint8Array(chunk), offset);
|
|
386
|
+
offset += chunk.byteLength;
|
|
387
|
+
}
|
|
388
|
+
// Save metadata and cleanup state
|
|
389
|
+
await modelCache.saveMeta({
|
|
390
|
+
url,
|
|
391
|
+
size: totalSize,
|
|
392
|
+
etag,
|
|
393
|
+
cachedAt: Date.now(),
|
|
394
|
+
chunks: chunks.length,
|
|
395
|
+
complete: true,
|
|
396
|
+
});
|
|
397
|
+
await modelCache.deleteDownloadState(url);
|
|
398
|
+
return result.buffer;
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Simple download without sharding
|
|
402
|
+
*/
|
|
403
|
+
async function downloadSimple(url, timeout, onProgress) {
|
|
404
|
+
const controller = new AbortController();
|
|
405
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
406
|
+
try {
|
|
407
|
+
const response = await fetch(url, { signal: controller.signal });
|
|
408
|
+
if (!response.ok) {
|
|
409
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
410
|
+
}
|
|
411
|
+
const contentLength = response.headers.get('Content-Length');
|
|
412
|
+
const total = contentLength ? parseInt(contentLength, 10) : 0;
|
|
413
|
+
if (!response.body || !onProgress || total === 0) {
|
|
414
|
+
return await response.arrayBuffer();
|
|
415
|
+
}
|
|
416
|
+
// Stream with progress
|
|
417
|
+
const reader = response.body.getReader();
|
|
418
|
+
const chunks = [];
|
|
419
|
+
let loaded = 0;
|
|
420
|
+
const startTime = Date.now();
|
|
421
|
+
while (true) {
|
|
422
|
+
const { done, value } = await reader.read();
|
|
423
|
+
if (done)
|
|
424
|
+
break;
|
|
425
|
+
chunks.push(value);
|
|
426
|
+
loaded += value.length;
|
|
427
|
+
const elapsed = (Date.now() - startTime) / 1000;
|
|
428
|
+
const speed = elapsed > 0 ? loaded / elapsed : 0;
|
|
429
|
+
const remaining = total - loaded;
|
|
430
|
+
const eta = speed > 0 ? (remaining / speed) * 1000 : 0;
|
|
431
|
+
onProgress({
|
|
432
|
+
loaded,
|
|
433
|
+
total,
|
|
434
|
+
percent: (loaded / total) * 100,
|
|
435
|
+
speed,
|
|
436
|
+
eta,
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
// Merge chunks
|
|
440
|
+
const result = new Uint8Array(loaded);
|
|
441
|
+
let offset = 0;
|
|
442
|
+
for (const chunk of chunks) {
|
|
443
|
+
result.set(chunk, offset);
|
|
444
|
+
offset += chunk.length;
|
|
445
|
+
}
|
|
446
|
+
return result.buffer;
|
|
447
|
+
}
|
|
448
|
+
finally {
|
|
449
|
+
clearTimeout(timeoutId);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Preload manager for background model loading
|
|
454
|
+
*/
|
|
455
|
+
class PreloadManager {
|
|
456
|
+
tasks = new Map();
|
|
457
|
+
queue = [];
|
|
458
|
+
maxConcurrent = 2;
|
|
459
|
+
activeCount = 0;
|
|
460
|
+
/**
|
|
461
|
+
* Preload a model in the background
|
|
462
|
+
*/
|
|
463
|
+
preload(url, options = {}) {
|
|
464
|
+
// Check if already preloading
|
|
465
|
+
const existing = this.tasks.get(url);
|
|
466
|
+
if (existing) {
|
|
467
|
+
return existing.promise;
|
|
468
|
+
}
|
|
469
|
+
// Create task
|
|
470
|
+
let resolve;
|
|
471
|
+
let reject;
|
|
472
|
+
const promise = new Promise((res, rej) => {
|
|
473
|
+
resolve = res;
|
|
474
|
+
reject = rej;
|
|
475
|
+
});
|
|
476
|
+
const task = {
|
|
477
|
+
url,
|
|
478
|
+
priority: options.priority ?? 0,
|
|
479
|
+
options,
|
|
480
|
+
promise,
|
|
481
|
+
resolve,
|
|
482
|
+
reject,
|
|
483
|
+
status: 'pending',
|
|
484
|
+
};
|
|
485
|
+
this.tasks.set(url, task);
|
|
486
|
+
// Insert into queue based on priority
|
|
487
|
+
const insertIndex = this.queue.findIndex(u => {
|
|
488
|
+
const t = this.tasks.get(u);
|
|
489
|
+
return t && t.priority < task.priority;
|
|
490
|
+
});
|
|
491
|
+
if (insertIndex === -1) {
|
|
492
|
+
this.queue.push(url);
|
|
493
|
+
}
|
|
494
|
+
else {
|
|
495
|
+
this.queue.splice(insertIndex, 0, url);
|
|
496
|
+
}
|
|
497
|
+
// Process queue
|
|
498
|
+
this.processQueue();
|
|
499
|
+
return promise;
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Process the preload queue
|
|
503
|
+
*/
|
|
504
|
+
async processQueue() {
|
|
505
|
+
while (this.queue.length > 0 && this.activeCount < this.maxConcurrent) {
|
|
506
|
+
const url = this.queue.shift();
|
|
507
|
+
if (!url)
|
|
508
|
+
break;
|
|
509
|
+
const task = this.tasks.get(url);
|
|
510
|
+
if (!task || task.status !== 'pending')
|
|
511
|
+
continue;
|
|
512
|
+
this.activeCount++;
|
|
513
|
+
task.status = 'loading';
|
|
514
|
+
this.downloadTask(task).finally(() => {
|
|
515
|
+
this.activeCount--;
|
|
516
|
+
this.processQueue();
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Download a preload task
|
|
522
|
+
*/
|
|
523
|
+
async downloadTask(task) {
|
|
524
|
+
try {
|
|
525
|
+
const data = await loadModelData(task.url, task.options);
|
|
526
|
+
task.status = 'complete';
|
|
527
|
+
task.resolve(data);
|
|
528
|
+
}
|
|
529
|
+
catch (error) {
|
|
530
|
+
task.status = 'error';
|
|
531
|
+
task.reject(error instanceof Error ? error : new Error(String(error)));
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Check if a model is preloaded
|
|
536
|
+
*/
|
|
537
|
+
isPreloaded(url) {
|
|
538
|
+
const task = this.tasks.get(url);
|
|
539
|
+
return task?.status === 'complete';
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* Get preload status
|
|
543
|
+
*/
|
|
544
|
+
getStatus(url) {
|
|
545
|
+
const task = this.tasks.get(url);
|
|
546
|
+
return task?.status ?? 'not_found';
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Get preloaded model data
|
|
550
|
+
*/
|
|
551
|
+
async get(url) {
|
|
552
|
+
const task = this.tasks.get(url);
|
|
553
|
+
if (!task)
|
|
554
|
+
return null;
|
|
555
|
+
if (task.status === 'complete' || task.status === 'loading') {
|
|
556
|
+
return task.promise;
|
|
557
|
+
}
|
|
558
|
+
return null;
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* Cancel preload
|
|
562
|
+
*/
|
|
563
|
+
cancel(url) {
|
|
564
|
+
const task = this.tasks.get(url);
|
|
565
|
+
if (task && task.status === 'pending') {
|
|
566
|
+
this.tasks.delete(url);
|
|
567
|
+
this.queue = this.queue.filter(u => u !== url);
|
|
568
|
+
task.reject(new Error('Preload cancelled'));
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Clear all preloads
|
|
573
|
+
*/
|
|
574
|
+
clear() {
|
|
575
|
+
for (const [, task] of this.tasks) {
|
|
576
|
+
if (task.status === 'pending') {
|
|
577
|
+
task.reject(new Error('Preload cleared'));
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
this.tasks.clear();
|
|
581
|
+
this.queue = [];
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
// Global preload manager
|
|
585
|
+
const preloadManager = new PreloadManager();
|
|
586
|
+
// ============================================================================
|
|
587
|
+
// Public API
|
|
588
|
+
// ============================================================================
|
|
589
|
+
/**
|
|
590
|
+
* Load model data with caching, sharding, and resume support
|
|
591
|
+
*/
|
|
592
|
+
export async function loadModelData(url, options = {}) {
|
|
593
|
+
const { cache = true, forceDownload = false, resumable = true, } = options;
|
|
594
|
+
// Check cache first
|
|
595
|
+
if (cache && !forceDownload) {
|
|
596
|
+
const cached = await modelCache.getModel(url);
|
|
597
|
+
if (cached) {
|
|
598
|
+
console.log(`✓ Model loaded from cache: ${url}`);
|
|
599
|
+
options.onProgress?.({
|
|
600
|
+
loaded: cached.byteLength,
|
|
601
|
+
total: cached.byteLength,
|
|
602
|
+
percent: 100,
|
|
603
|
+
speed: 0,
|
|
604
|
+
eta: 0,
|
|
605
|
+
});
|
|
606
|
+
return cached;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
// Download with resume support
|
|
610
|
+
let data;
|
|
611
|
+
if (resumable) {
|
|
612
|
+
data = await downloadWithResume(url, options);
|
|
613
|
+
}
|
|
614
|
+
else {
|
|
615
|
+
data = await downloadSimple(url, options.timeout ?? 30000, options.onProgress);
|
|
616
|
+
}
|
|
617
|
+
// Cache the result
|
|
618
|
+
if (cache) {
|
|
619
|
+
// For simple downloads, save as single chunk
|
|
620
|
+
if (!resumable) {
|
|
621
|
+
await modelCache.saveChunk(url, 0, data);
|
|
622
|
+
await modelCache.saveMeta({
|
|
623
|
+
url,
|
|
624
|
+
size: data.byteLength,
|
|
625
|
+
cachedAt: Date.now(),
|
|
626
|
+
chunks: 1,
|
|
627
|
+
complete: true,
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
return data;
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* Preload a model in the background
|
|
635
|
+
*/
|
|
636
|
+
export function preloadModel(url, options = {}) {
|
|
637
|
+
return preloadManager.preload(url, options);
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* Preload multiple models
|
|
641
|
+
*/
|
|
642
|
+
export function preloadModels(urls, options = {}) {
|
|
643
|
+
return Promise.all(urls.map(({ url, priority }) => preloadManager.preload(url, { ...options, priority })));
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Check if a model is cached
|
|
647
|
+
*/
|
|
648
|
+
export async function isModelCached(url) {
|
|
649
|
+
const meta = await modelCache.getMeta(url);
|
|
650
|
+
return meta?.complete ?? false;
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* Get cached model data
|
|
654
|
+
*/
|
|
655
|
+
export async function getCachedModel(url) {
|
|
656
|
+
return modelCache.getModel(url);
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* Delete a cached model
|
|
660
|
+
*/
|
|
661
|
+
export async function deleteCachedModel(url) {
|
|
662
|
+
return modelCache.deleteModel(url);
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* Clear all cached models
|
|
666
|
+
*/
|
|
667
|
+
export async function clearModelCache() {
|
|
668
|
+
return modelCache.clear();
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* Get model cache statistics
|
|
672
|
+
*/
|
|
673
|
+
export async function getModelCacheStats() {
|
|
674
|
+
return modelCache.getStats();
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* Get preload status
|
|
678
|
+
*/
|
|
679
|
+
export function getPreloadStatus(url) {
|
|
680
|
+
return preloadManager.getStatus(url);
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* Cancel a preload
|
|
684
|
+
*/
|
|
685
|
+
export function cancelPreload(url) {
|
|
686
|
+
preloadManager.cancel(url);
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* Get preloaded model (or wait for preload to complete)
|
|
690
|
+
*/
|
|
691
|
+
export async function getPreloadedModel(url) {
|
|
692
|
+
return preloadManager.get(url);
|
|
693
|
+
}
|
|
694
|
+
//# sourceMappingURL=model-loader.js.map
|