jev-web 0.3.0 → 0.3.2
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/package.json +1 -1
- package/src/laya.mjs +152 -21
package/package.json
CHANGED
package/src/laya.mjs
CHANGED
|
@@ -211,26 +211,11 @@ export function layaAnswersFromLogits({
|
|
|
211
211
|
* Falls back to `res.json()` when the body isn't streamable (Node fakes/tests).
|
|
212
212
|
*/
|
|
213
213
|
async function fetchJson(url, file, { fetchImpl, onProgress }) {
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
const chunks = [];
|
|
220
|
-
let loaded = 0;
|
|
221
|
-
for (;;) {
|
|
222
|
-
const { done, value } = await reader.read();
|
|
223
|
-
if (done) break;
|
|
224
|
-
chunks.push(value);
|
|
225
|
-
loaded += value.length;
|
|
226
|
-
onProgress({ phase: "tokenizer", file, loaded, total: total || null });
|
|
227
|
-
}
|
|
228
|
-
const buf = new Uint8Array(loaded);
|
|
229
|
-
let off = 0;
|
|
230
|
-
for (const c of chunks) { buf.set(c, off); off += c.length; }
|
|
231
|
-
return JSON.parse(new TextDecoder().decode(buf));
|
|
232
|
-
}
|
|
233
|
-
return res.json();
|
|
214
|
+
const bytes = await fetchCachedBytes(url, {
|
|
215
|
+
fetchImpl,
|
|
216
|
+
onProgress: (p) => onProgress?.({ ...p, phase: "tokenizer", file }),
|
|
217
|
+
});
|
|
218
|
+
return JSON.parse(new TextDecoder().decode(bytes));
|
|
234
219
|
}
|
|
235
220
|
|
|
236
221
|
/**
|
|
@@ -300,6 +285,146 @@ const now = () => (typeof performance !== "undefined" ? performance.now() : Date
|
|
|
300
285
|
const hfUrl = (model, revision, file) =>
|
|
301
286
|
`https://huggingface.co/${model}/resolve/${revision ? `${revision}/` : ""}${file}`;
|
|
302
287
|
|
|
288
|
+
const MODEL_CACHE = "kf-models-v1";
|
|
289
|
+
|
|
290
|
+
async function modelCacheStore() {
|
|
291
|
+
try {
|
|
292
|
+
return typeof caches !== "undefined" ? caches : null;
|
|
293
|
+
} catch {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** Ask for persistent storage once so big weights survive eviction pressure. */
|
|
299
|
+
export async function requestPersistentStorage() {
|
|
300
|
+
try {
|
|
301
|
+
return (await globalThis.navigator?.storage?.persist?.()) ?? false;
|
|
302
|
+
} catch {
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Fetch bytes with a Cache-API layer. Hugging Face redirects to a *signed*
|
|
309
|
+
* CDN URL that changes per request, so the browser HTTP cache never hits for
|
|
310
|
+
* the 468 MB encoder weights; this caches them under the stable model URL
|
|
311
|
+
* instead (Cache API), so a page refresh is free.
|
|
312
|
+
*
|
|
313
|
+
* @returns {Promise<Uint8Array>}
|
|
314
|
+
*/
|
|
315
|
+
export async function fetchCachedBytes(url, {
|
|
316
|
+
fetchImpl = fetch,
|
|
317
|
+
onProgress = null,
|
|
318
|
+
cacheName = MODEL_CACHE,
|
|
319
|
+
force = false,
|
|
320
|
+
persist = false,
|
|
321
|
+
} = {}) {
|
|
322
|
+
const store = await modelCacheStore();
|
|
323
|
+
if (persist) await requestPersistentStorage();
|
|
324
|
+
if (store && !force) {
|
|
325
|
+
try {
|
|
326
|
+
const hit = await store.match(url);
|
|
327
|
+
if (hit) {
|
|
328
|
+
const data = new Uint8Array(await hit.arrayBuffer());
|
|
329
|
+
onProgress?.({ phase: "cache", file: String(url).split("/").pop(), loaded: data.length, total: data.length });
|
|
330
|
+
return data;
|
|
331
|
+
}
|
|
332
|
+
} catch {
|
|
333
|
+
/* cache read failed — fall through to the network */
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
const res = await fetchImpl(url);
|
|
337
|
+
if (!res?.ok) throw new Error(`fetch ${url} failed (HTTP ${res?.status ?? "no response"})`);
|
|
338
|
+
const data = await readBytesWithProgress(res, url, onProgress);
|
|
339
|
+
if (store) {
|
|
340
|
+
try {
|
|
341
|
+
await store.put(url, new Response(data, {
|
|
342
|
+
headers: { "content-type": "application/octet-stream", "content-length": String(data.length) },
|
|
343
|
+
}));
|
|
344
|
+
} catch {
|
|
345
|
+
/* quota exceeded / private mode — caching is best-effort */
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return data;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async function readBytesWithProgress(res, url, onProgress) {
|
|
352
|
+
const total = Number(res.headers?.get?.("content-length")) || 0;
|
|
353
|
+
const file = String(url).split("/").pop();
|
|
354
|
+
if (res.body?.getReader) {
|
|
355
|
+
const reader = res.body.getReader();
|
|
356
|
+
let received = 0;
|
|
357
|
+
let data = total ? new Uint8Array(total) : null;
|
|
358
|
+
const chunks = total ? null : [];
|
|
359
|
+
for (;;) {
|
|
360
|
+
const { done, value } = await reader.read();
|
|
361
|
+
if (done) break;
|
|
362
|
+
if (data) {
|
|
363
|
+
if (received + value.length > data.length) {
|
|
364
|
+
// content-length can describe the *encoded* size (gzip/br), so the
|
|
365
|
+
// decoded stream may be larger — grow instead of overflowing.
|
|
366
|
+
const grown = new Uint8Array(Math.max(data.length * 2, received + value.length));
|
|
367
|
+
grown.set(data.subarray(0, received));
|
|
368
|
+
data = grown;
|
|
369
|
+
}
|
|
370
|
+
data.set(value, received);
|
|
371
|
+
} else {
|
|
372
|
+
chunks.push(value);
|
|
373
|
+
}
|
|
374
|
+
received += value.length;
|
|
375
|
+
onProgress?.({ phase: "download", file, loaded: received, total: total || null });
|
|
376
|
+
}
|
|
377
|
+
if (!data) {
|
|
378
|
+
data = new Uint8Array(received);
|
|
379
|
+
let off = 0;
|
|
380
|
+
for (const c of chunks) { data.set(c, off); off += c.length; }
|
|
381
|
+
} else {
|
|
382
|
+
// the buffer may have grown past the decoded length (content-length is
|
|
383
|
+
// the *encoded* size) — always trim to what was actually received
|
|
384
|
+
data = data.subarray(0, received);
|
|
385
|
+
}
|
|
386
|
+
return data;
|
|
387
|
+
}
|
|
388
|
+
let data;
|
|
389
|
+
if (typeof res.arrayBuffer === "function") {
|
|
390
|
+
data = new Uint8Array(await res.arrayBuffer());
|
|
391
|
+
} else if (typeof res.text === "function") {
|
|
392
|
+
data = new TextEncoder().encode(await res.text());
|
|
393
|
+
} else if (typeof res.json === "function") {
|
|
394
|
+
data = new TextEncoder().encode(JSON.stringify(await res.json()));
|
|
395
|
+
} else {
|
|
396
|
+
throw new Error(`fetch ${url}: response has no readable body`);
|
|
397
|
+
}
|
|
398
|
+
onProgress?.({ phase: "download", file, loaded: data.length, total: data.length });
|
|
399
|
+
return data;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* ONNX external-data loader.
|
|
404
|
+
*
|
|
405
|
+
* The pinned Laya export stores weights in `<model>.onnx.data` siblings
|
|
406
|
+
* (encoder ~468 MB, head ~53 MB). onnxruntime-web cannot resolve those from a
|
|
407
|
+
* URL by itself (`Module.MountedFiles is not available`); it needs the bytes
|
|
408
|
+
* passed via `session_options.externalData`. Fetch the sibling (streaming,
|
|
409
|
+
* with progress) and hand it over.
|
|
410
|
+
*
|
|
411
|
+
* @returns {Promise<{path:string, data:Uint8Array}|null>} null when the model
|
|
412
|
+
* has no external data (404) or the fetch fails.
|
|
413
|
+
*/
|
|
414
|
+
export async function fetchOnnxExternalData(url, { fetchImpl = fetch, onProgress = null, force = false } = {}) {
|
|
415
|
+
const dataUrl = `${url}.data`;
|
|
416
|
+
const path = `${String(url).split("/").pop()}.data`;
|
|
417
|
+
try {
|
|
418
|
+
// Probe cheaply first: a 404 means the model has no external data.
|
|
419
|
+
const head = await fetchImpl(dataUrl, { method: "HEAD" }).catch(() => null);
|
|
420
|
+
if (head && !head.ok) return null;
|
|
421
|
+
const data = await fetchCachedBytes(dataUrl, { fetchImpl, onProgress, force, persist: true });
|
|
422
|
+
return { path, data };
|
|
423
|
+
} catch {
|
|
424
|
+
return null;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
303
428
|
/**
|
|
304
429
|
* Create a Laya decider.
|
|
305
430
|
*
|
|
@@ -376,7 +501,13 @@ export async function createLayaDecider({
|
|
|
376
501
|
const encUrl = encoderUrl ?? hfUrl(model, revision, `${subfolder}/encoder_q8.onnx`);
|
|
377
502
|
const headUrlResolved = headUrl ?? hfUrl(model, revision, `${subfolder}/head_q8.onnx`);
|
|
378
503
|
const createSession = sessionFactory
|
|
379
|
-
?? ((url, opts) =>
|
|
504
|
+
?? (async (url, opts) => {
|
|
505
|
+
const report2 = (p) => onProgress?.(p);
|
|
506
|
+
const modelBytes = await fetchCachedBytes(url, { fetchImpl, onProgress: report2, persist: true });
|
|
507
|
+
const external = await fetchOnnxExternalData(url, { fetchImpl, onProgress: report2 });
|
|
508
|
+
const sessionOpts = external ? { ...opts, externalData: [external] } : opts;
|
|
509
|
+
return ortMod.InferenceSession.create(modelBytes, sessionOpts);
|
|
510
|
+
});
|
|
380
511
|
const loadPair = async (providers) => {
|
|
381
512
|
const opts = { executionProviders: providers, graphOptimizationLevel: "all" };
|
|
382
513
|
const enc = await createSession(encUrl, { ...opts, progress_callback: report("encoder") });
|