@omote/core 0.5.4 → 0.5.6

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/dist/index.d.mts CHANGED
@@ -584,6 +584,14 @@ interface FullFacePipelineOptions {
584
584
  chunkSize?: number;
585
585
  /** A2E inference engine */
586
586
  lam: A2EBackend;
587
+ /**
588
+ * Identity/style index for the A2E model (default: 0).
589
+ *
590
+ * The LAM model uses a 12-class one-hot identity vector as style conditioning.
591
+ * Different indices produce different expression intensity across face regions.
592
+ * Only affects Wav2Vec2Inference (GPU). Wav2ArkitCpuInference has identity 11 baked in.
593
+ */
594
+ identityIndex?: number;
587
595
  /** Per-character expression weight scaling */
588
596
  profile?: ExpressionProfile;
589
597
  /**
@@ -2259,6 +2267,17 @@ interface A2EProcessorConfig {
2259
2267
  sampleRate?: number;
2260
2268
  /** Samples per inference chunk (default: 16000 = 1s) */
2261
2269
  chunkSize?: number;
2270
+ /**
2271
+ * Identity/style index for the A2E model (default: 0).
2272
+ *
2273
+ * The LAM model uses a one-hot identity vector (12 classes, indices 0-11) as
2274
+ * style conditioning alongside audio features. Different indices produce
2275
+ * different expression intensity across face regions (brows, eyes, cheeks).
2276
+ *
2277
+ * Only affects Wav2Vec2Inference (GPU model). Wav2ArkitCpuInference has
2278
+ * identity 11 baked into the model weights.
2279
+ */
2280
+ identityIndex?: number;
2262
2281
  /** Callback fired with each blendshape frame (push mode) */
2263
2282
  onFrame?: (frame: Float32Array) => void;
2264
2283
  /** Error callback */
@@ -2268,6 +2287,7 @@ declare class A2EProcessor {
2268
2287
  private readonly backend;
2269
2288
  private readonly sampleRate;
2270
2289
  private readonly chunkSize;
2290
+ private readonly identityIndex;
2271
2291
  private readonly onFrame?;
2272
2292
  private readonly onError?;
2273
2293
  private bufferCapacity;
@@ -3110,6 +3130,12 @@ interface FetchWithCacheOptions {
3110
3130
  validateStale?: boolean;
3111
3131
  /** Progress callback during download */
3112
3132
  onProgress?: (loaded: number, total: number) => void;
3133
+ /** Timeout per fetch attempt in ms. Default: 120_000 (2 min) */
3134
+ timeoutMs?: number;
3135
+ /** Max retry attempts on failure. Default: 2 (3 total attempts) */
3136
+ maxRetries?: number;
3137
+ /** AbortSignal to cancel the entire operation */
3138
+ signal?: AbortSignal;
3113
3139
  }
3114
3140
  /**
3115
3141
  * Fetch a model with caching
package/dist/index.d.ts CHANGED
@@ -584,6 +584,14 @@ interface FullFacePipelineOptions {
584
584
  chunkSize?: number;
585
585
  /** A2E inference engine */
586
586
  lam: A2EBackend;
587
+ /**
588
+ * Identity/style index for the A2E model (default: 0).
589
+ *
590
+ * The LAM model uses a 12-class one-hot identity vector as style conditioning.
591
+ * Different indices produce different expression intensity across face regions.
592
+ * Only affects Wav2Vec2Inference (GPU). Wav2ArkitCpuInference has identity 11 baked in.
593
+ */
594
+ identityIndex?: number;
587
595
  /** Per-character expression weight scaling */
588
596
  profile?: ExpressionProfile;
589
597
  /**
@@ -2259,6 +2267,17 @@ interface A2EProcessorConfig {
2259
2267
  sampleRate?: number;
2260
2268
  /** Samples per inference chunk (default: 16000 = 1s) */
2261
2269
  chunkSize?: number;
2270
+ /**
2271
+ * Identity/style index for the A2E model (default: 0).
2272
+ *
2273
+ * The LAM model uses a one-hot identity vector (12 classes, indices 0-11) as
2274
+ * style conditioning alongside audio features. Different indices produce
2275
+ * different expression intensity across face regions (brows, eyes, cheeks).
2276
+ *
2277
+ * Only affects Wav2Vec2Inference (GPU model). Wav2ArkitCpuInference has
2278
+ * identity 11 baked into the model weights.
2279
+ */
2280
+ identityIndex?: number;
2262
2281
  /** Callback fired with each blendshape frame (push mode) */
2263
2282
  onFrame?: (frame: Float32Array) => void;
2264
2283
  /** Error callback */
@@ -2268,6 +2287,7 @@ declare class A2EProcessor {
2268
2287
  private readonly backend;
2269
2288
  private readonly sampleRate;
2270
2289
  private readonly chunkSize;
2290
+ private readonly identityIndex;
2271
2291
  private readonly onFrame?;
2272
2292
  private readonly onError?;
2273
2293
  private bufferCapacity;
@@ -3110,6 +3130,12 @@ interface FetchWithCacheOptions {
3110
3130
  validateStale?: boolean;
3111
3131
  /** Progress callback during download */
3112
3132
  onProgress?: (loaded: number, total: number) => void;
3133
+ /** Timeout per fetch attempt in ms. Default: 120_000 (2 min) */
3134
+ timeoutMs?: number;
3135
+ /** Max retry attempts on failure. Default: 2 (3 total attempts) */
3136
+ maxRetries?: number;
3137
+ /** AbortSignal to cancel the entire operation */
3138
+ signal?: AbortSignal;
3113
3139
  }
3114
3140
  /**
3115
3141
  * Fetch a model with caching
package/dist/index.js CHANGED
@@ -916,6 +916,7 @@ var A2EProcessor = class {
916
916
  this.backend = config.backend;
917
917
  this.sampleRate = config.sampleRate ?? 16e3;
918
918
  this.chunkSize = config.chunkSize ?? config.backend.chunkSize ?? 16e3;
919
+ this.identityIndex = config.identityIndex ?? 0;
919
920
  this.onFrame = config.onFrame;
920
921
  this.onError = config.onError;
921
922
  this.bufferCapacity = this.chunkSize * 2;
@@ -1123,7 +1124,7 @@ var A2EProcessor = class {
1123
1124
  const { chunk, timestamp } = this.pendingChunks.shift();
1124
1125
  try {
1125
1126
  const t0 = performance.now();
1126
- const result = await this.backend.infer(chunk);
1127
+ const result = await this.backend.infer(chunk, this.identityIndex);
1127
1128
  const inferMs = Math.round(performance.now() - t0);
1128
1129
  const actualDuration = chunk.length / this.sampleRate;
1129
1130
  const actualFrameCount = Math.ceil(actualDuration * FRAME_RATE);
@@ -2218,6 +2219,16 @@ function getModelCache() {
2218
2219
  return cacheInstance;
2219
2220
  }
2220
2221
  var MAX_CACHE_SIZE_BYTES = 500 * 1024 * 1024;
2222
+ function fetchWithTimeout(url, timeoutMs, signal) {
2223
+ const controller = new AbortController();
2224
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
2225
+ const onCallerAbort = () => controller.abort();
2226
+ signal?.addEventListener("abort", onCallerAbort, { once: true });
2227
+ return fetch(url, { signal: controller.signal }).finally(() => {
2228
+ clearTimeout(timer);
2229
+ signal?.removeEventListener("abort", onCallerAbort);
2230
+ });
2231
+ }
2221
2232
  async function fetchWithCache(url, optionsOrProgress) {
2222
2233
  let options = {};
2223
2234
  if (typeof optionsOrProgress === "function") {
@@ -2271,61 +2282,84 @@ async function fetchWithCache(url, optionsOrProgress) {
2271
2282
  }
2272
2283
  span?.setAttributes({ "fetch.cache_hit": false });
2273
2284
  console.log(`[ModelCache] Cache miss, fetching: ${url}`);
2274
- try {
2275
- const response = await fetch(url);
2276
- if (!response.ok) {
2277
- throw new Error(`Failed to fetch ${url}: ${response.status}`);
2278
- }
2279
- const contentLength = response.headers.get("content-length");
2280
- const total = contentLength ? parseInt(contentLength, 10) : 0;
2281
- const etag = response.headers.get("etag") ?? void 0;
2282
- const tooLargeForCache = total > MAX_CACHE_SIZE_BYTES;
2283
- if (tooLargeForCache) {
2284
- console.log(`[ModelCache] File too large for IndexedDB (${(total / 1024 / 1024).toFixed(0)}MB > 500MB), using HTTP cache only`);
2285
- }
2286
- if (!response.body) {
2287
- const data2 = await response.arrayBuffer();
2285
+ const timeout = options.timeoutMs ?? 12e4;
2286
+ const maxRetries = options.maxRetries ?? 2;
2287
+ let lastError = null;
2288
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
2289
+ if (options.signal?.aborted) {
2290
+ throw new Error(`Fetch aborted for ${url}`);
2291
+ }
2292
+ if (attempt > 0) {
2293
+ const backoff = Math.min(2e3 * Math.pow(2, attempt - 1), 16e3);
2294
+ console.log(`[ModelCache] Retry ${attempt}/${maxRetries} after ${backoff}ms: ${url}`);
2295
+ await new Promise((r) => setTimeout(r, backoff));
2296
+ }
2297
+ try {
2298
+ const response = await fetchWithTimeout(url, timeout, options.signal);
2299
+ if (!response.ok) {
2300
+ throw new Error(`Failed to fetch ${url}: ${response.status}`);
2301
+ }
2302
+ const contentLength = response.headers.get("content-length");
2303
+ const total = contentLength ? parseInt(contentLength, 10) : 0;
2304
+ const etag = response.headers.get("etag") ?? void 0;
2305
+ const tooLargeForCache = total > MAX_CACHE_SIZE_BYTES;
2306
+ if (tooLargeForCache) {
2307
+ console.log(`[ModelCache] File too large for IndexedDB (${(total / 1024 / 1024).toFixed(0)}MB > 500MB), using HTTP cache only`);
2308
+ }
2309
+ if (!response.body) {
2310
+ const data2 = await response.arrayBuffer();
2311
+ if (!tooLargeForCache) {
2312
+ await cache.set(cacheKey, data2, etag, version);
2313
+ }
2314
+ span?.setAttributes({
2315
+ "fetch.size_bytes": data2.byteLength,
2316
+ "fetch.cached_to_indexeddb": !tooLargeForCache,
2317
+ ...attempt > 0 && { "fetch.retry_count": attempt }
2318
+ });
2319
+ span?.end();
2320
+ return data2;
2321
+ }
2322
+ const reader = response.body.getReader();
2323
+ const chunks = [];
2324
+ let loaded = 0;
2325
+ while (true) {
2326
+ const { done, value } = await reader.read();
2327
+ if (done) break;
2328
+ chunks.push(value);
2329
+ loaded += value.length;
2330
+ onProgress?.(loaded, total || loaded);
2331
+ }
2332
+ const data = new Uint8Array(loaded);
2333
+ let offset = 0;
2334
+ for (const chunk of chunks) {
2335
+ data.set(chunk, offset);
2336
+ offset += chunk.length;
2337
+ }
2338
+ const buffer = data.buffer;
2288
2339
  if (!tooLargeForCache) {
2289
- await cache.set(cacheKey, data2, etag, version);
2340
+ await cache.set(cacheKey, buffer, etag, version);
2341
+ console.log(`[ModelCache] Cached: ${url} (${(buffer.byteLength / 1024 / 1024).toFixed(1)}MB)`);
2290
2342
  }
2291
2343
  span?.setAttributes({
2292
- "fetch.size_bytes": data2.byteLength,
2293
- "fetch.cached_to_indexeddb": !tooLargeForCache
2344
+ "fetch.size_bytes": buffer.byteLength,
2345
+ "fetch.cached_to_indexeddb": !tooLargeForCache,
2346
+ ...attempt > 0 && { "fetch.retry_count": attempt }
2294
2347
  });
2295
2348
  span?.end();
2296
- return data2;
2297
- }
2298
- const reader = response.body.getReader();
2299
- const chunks = [];
2300
- let loaded = 0;
2301
- while (true) {
2302
- const { done, value } = await reader.read();
2303
- if (done) break;
2304
- chunks.push(value);
2305
- loaded += value.length;
2306
- onProgress?.(loaded, total || loaded);
2307
- }
2308
- const data = new Uint8Array(loaded);
2309
- let offset = 0;
2310
- for (const chunk of chunks) {
2311
- data.set(chunk, offset);
2312
- offset += chunk.length;
2313
- }
2314
- const buffer = data.buffer;
2315
- if (!tooLargeForCache) {
2316
- await cache.set(cacheKey, buffer, etag, version);
2317
- console.log(`[ModelCache] Cached: ${url} (${(buffer.byteLength / 1024 / 1024).toFixed(1)}MB)`);
2349
+ return buffer;
2350
+ } catch (error) {
2351
+ lastError = error instanceof Error ? error : new Error(String(error));
2352
+ if (options.signal?.aborted) {
2353
+ span?.endWithError(lastError);
2354
+ throw lastError;
2355
+ }
2356
+ if (attempt < maxRetries) {
2357
+ console.warn(`[ModelCache] Attempt ${attempt + 1} failed for ${url}: ${lastError.message}`);
2358
+ }
2318
2359
  }
2319
- span?.setAttributes({
2320
- "fetch.size_bytes": buffer.byteLength,
2321
- "fetch.cached_to_indexeddb": !tooLargeForCache
2322
- });
2323
- span?.end();
2324
- return buffer;
2325
- } catch (error) {
2326
- span?.endWithError(error instanceof Error ? error : new Error(String(error)));
2327
- throw error;
2328
2360
  }
2361
+ span?.endWithError(lastError);
2362
+ throw lastError;
2329
2363
  }
2330
2364
  async function preloadModels(urls, onProgress) {
2331
2365
  const cache = getModelCache();
@@ -2574,6 +2608,15 @@ function getSessionOptions(backend) {
2574
2608
  graphOptimizationLevel: "all"
2575
2609
  };
2576
2610
  }
2611
+ function withTimeout(promise, ms, label) {
2612
+ return new Promise((resolve, reject) => {
2613
+ const timer = setTimeout(
2614
+ () => reject(new Error(`${label} timed out after ${ms}ms`)),
2615
+ ms
2616
+ );
2617
+ promise.then(resolve, reject).finally(() => clearTimeout(timer));
2618
+ });
2619
+ }
2577
2620
 
2578
2621
  // src/inference/blendshapeUtils.ts
2579
2622
  var LAM_BLENDSHAPES = [
@@ -2846,7 +2889,11 @@ var _Wav2Vec2Inference = class _Wav2Vec2Inference {
2846
2889
  )
2847
2890
  });
2848
2891
  try {
2849
- this.session = await this.ort.InferenceSession.create(modelUrl, sessionOptions);
2892
+ this.session = await withTimeout(
2893
+ this.ort.InferenceSession.create(modelUrl, sessionOptions),
2894
+ 18e4,
2895
+ "Wav2Vec2 InferenceSession.create (iOS URL pass-through)"
2896
+ );
2850
2897
  } catch (sessionErr) {
2851
2898
  logger3.error("iOS: InferenceSession.create() failed", {
2852
2899
  error: sessionErr instanceof Error ? sessionErr.message : String(sessionErr),
@@ -3247,6 +3294,7 @@ var FullFacePipeline = class extends EventEmitter {
3247
3294
  backend: options.lam,
3248
3295
  sampleRate,
3249
3296
  chunkSize,
3297
+ identityIndex: options.identityIndex,
3250
3298
  onError: (error) => {
3251
3299
  logger4.error("A2E inference error", { message: error.message, stack: error.stack });
3252
3300
  this.emit("error", error);
@@ -3933,9 +3981,10 @@ var _SenseVoiceInference = class _SenseVoiceInference {
3933
3981
  logger5.info("iOS: passing model URL directly to ORT (low-memory path)", {
3934
3982
  modelUrl: this.config.modelUrl
3935
3983
  });
3936
- this.session = await this.ort.InferenceSession.create(
3937
- this.config.modelUrl,
3938
- sessionOptions
3984
+ this.session = await withTimeout(
3985
+ this.ort.InferenceSession.create(this.config.modelUrl, sessionOptions),
3986
+ 18e4,
3987
+ "SenseVoice InferenceSession.create (iOS URL pass-through)"
3939
3988
  );
3940
3989
  } else {
3941
3990
  const cache = getModelCache();
@@ -4188,6 +4237,12 @@ var WORKER_SCRIPT = `
4188
4237
  var ort = null;
4189
4238
  var session = null;
4190
4239
  var tokenMap = null;
4240
+
4241
+ function fetchWithTimeout(url, timeoutMs) {
4242
+ var controller = new AbortController();
4243
+ var timer = setTimeout(function() { controller.abort(); }, timeoutMs);
4244
+ return fetch(url, { signal: controller.signal }).finally(function() { clearTimeout(timer); });
4245
+ }
4191
4246
  var negMean = null;
4192
4247
  var invStddev = null;
4193
4248
  var languageId = 0;
@@ -4631,7 +4686,7 @@ async function loadOrt(wasmPaths) {
4631
4686
  var ortUrl = wasmPaths + 'ort.wasm.min.js';
4632
4687
 
4633
4688
  // Load the script by fetching and executing it
4634
- var response = await fetch(ortUrl);
4689
+ var response = await fetchWithTimeout(ortUrl, 30000);
4635
4690
  var scriptText = await response.text();
4636
4691
 
4637
4692
  // Create a blob URL for the script
@@ -4657,7 +4712,7 @@ async function loadOrt(wasmPaths) {
4657
4712
  */
4658
4713
  async function loadModel(modelUrl, tokensUrl, isIOSDevice, lang, textNorm) {
4659
4714
  // 1. Fetch and parse tokens.txt
4660
- var tokensResponse = await fetch(tokensUrl);
4715
+ var tokensResponse = await fetchWithTimeout(tokensUrl, 30000);
4661
4716
  if (!tokensResponse.ok) {
4662
4717
  throw new Error('Failed to fetch tokens.txt: ' + tokensResponse.status + ' ' + tokensResponse.statusText);
4663
4718
  }
@@ -4680,7 +4735,7 @@ async function loadModel(modelUrl, tokensUrl, isIOSDevice, lang, textNorm) {
4680
4735
  session = await ort.InferenceSession.create(modelUrl, sessionOptions);
4681
4736
  } else {
4682
4737
  // Desktop: fetch ArrayBuffer for potential caching
4683
- var modelResponse = await fetch(modelUrl);
4738
+ var modelResponse = await fetchWithTimeout(modelUrl, 120000);
4684
4739
  if (!modelResponse.ok) {
4685
4740
  throw new Error('Failed to fetch model: ' + modelResponse.status + ' ' + modelResponse.statusText);
4686
4741
  }
@@ -5191,6 +5246,12 @@ var WORKER_SCRIPT2 = `
5191
5246
 
5192
5247
  var ort = null;
5193
5248
 
5249
+ function fetchWithTimeout(url, timeoutMs) {
5250
+ var controller = new AbortController();
5251
+ var timer = setTimeout(function() { controller.abort(); }, timeoutMs);
5252
+ return fetch(url, { signal: controller.signal }).finally(function() { clearTimeout(timer); });
5253
+ }
5254
+
5194
5255
  // SenseVoice state
5195
5256
  var svSession = null;
5196
5257
  var svTokenMap = null;
@@ -5505,7 +5566,7 @@ function symmetrizeBlendshapes(frame) {
5505
5566
  async function loadOrt(wasmPaths, isIOSDevice) {
5506
5567
  if (ort) return;
5507
5568
  var ortUrl = wasmPaths + 'ort.wasm.min.js';
5508
- var response = await fetch(ortUrl);
5569
+ var response = await fetchWithTimeout(ortUrl, 30000);
5509
5570
  var scriptText = await response.text();
5510
5571
  var blob = new Blob([scriptText], { type: 'application/javascript' });
5511
5572
  var blobUrl = URL.createObjectURL(blob);
@@ -5523,7 +5584,7 @@ async function loadOrt(wasmPaths, isIOSDevice) {
5523
5584
  // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
5524
5585
 
5525
5586
  async function svLoad(msg) {
5526
- var tokensResponse = await fetch(msg.tokensUrl);
5587
+ var tokensResponse = await fetchWithTimeout(msg.tokensUrl, 30000);
5527
5588
  if (!tokensResponse.ok) throw new Error('Failed to fetch tokens.txt: ' + tokensResponse.status);
5528
5589
  var tokensText = await tokensResponse.text();
5529
5590
  svTokenMap = parseTokensFile(tokensText);
@@ -5534,7 +5595,7 @@ async function svLoad(msg) {
5534
5595
  if (msg.isIOS) {
5535
5596
  svSession = await ort.InferenceSession.create(msg.modelUrl, sessionOptions);
5536
5597
  } else {
5537
- var modelResponse = await fetch(msg.modelUrl);
5598
+ var modelResponse = await fetchWithTimeout(msg.modelUrl, 120000);
5538
5599
  if (!modelResponse.ok) throw new Error('Failed to fetch model: ' + modelResponse.status);
5539
5600
  var modelBuffer = await modelResponse.arrayBuffer();
5540
5601
  svSession = await ort.InferenceSession.create(new Uint8Array(modelBuffer), sessionOptions);
@@ -5609,11 +5670,11 @@ async function cpuLoad(msg) {
5609
5670
  }
5610
5671
  cpuSession = await ort.InferenceSession.create(msg.modelUrl, sessionOptions);
5611
5672
  } else {
5612
- var graphResponse = await fetch(msg.modelUrl);
5673
+ var graphResponse = await fetchWithTimeout(msg.modelUrl, 120000);
5613
5674
  if (!graphResponse.ok) throw new Error('Failed to fetch model graph: ' + graphResponse.status);
5614
5675
  var graphBuffer = await graphResponse.arrayBuffer();
5615
5676
  if (msg.externalDataUrl && dataFilename) {
5616
- var dataResponse = await fetch(msg.externalDataUrl);
5677
+ var dataResponse = await fetchWithTimeout(msg.externalDataUrl, 120000);
5617
5678
  if (!dataResponse.ok) throw new Error('Failed to fetch external data: ' + dataResponse.status);
5618
5679
  var dataBuffer = await dataResponse.arrayBuffer();
5619
5680
  sessionOptions.externalData = [{ path: dataFilename, data: new Uint8Array(dataBuffer) }];
@@ -5665,7 +5726,7 @@ async function vadLoad(msg) {
5665
5726
  vadChunkSize = vadSampleRate === 16000 ? 512 : 256;
5666
5727
  vadContextSize = vadSampleRate === 16000 ? 64 : 32;
5667
5728
 
5668
- var response = await fetch(msg.modelUrl);
5729
+ var response = await fetchWithTimeout(msg.modelUrl, 60000);
5669
5730
  if (!response.ok) throw new Error('Failed to fetch VAD model: ' + response.status);
5670
5731
  var modelBuffer = await response.arrayBuffer();
5671
5732
  vadSession = await ort.InferenceSession.create(new Uint8Array(modelBuffer), {
@@ -6513,7 +6574,11 @@ var _Wav2ArkitCpuInference = class _Wav2ArkitCpuInference {
6513
6574
  // URL string — ORT fetches directly into WASM
6514
6575
  }];
6515
6576
  }
6516
- this.session = await this.ort.InferenceSession.create(modelUrl, sessionOptions);
6577
+ this.session = await withTimeout(
6578
+ this.ort.InferenceSession.create(modelUrl, sessionOptions),
6579
+ 18e4,
6580
+ "Wav2ArkitCpu InferenceSession.create (iOS URL pass-through)"
6581
+ );
6517
6582
  } else {
6518
6583
  const cache = getModelCache();
6519
6584
  const isCached = await cache.has(modelUrl);
@@ -6781,6 +6846,12 @@ var WORKER_SCRIPT3 = `
6781
6846
  var ort = null;
6782
6847
  var session = null;
6783
6848
 
6849
+ function fetchWithTimeout(url, timeoutMs) {
6850
+ var controller = new AbortController();
6851
+ var timer = setTimeout(function() { controller.abort(); }, timeoutMs);
6852
+ return fetch(url, { signal: controller.signal }).finally(function() { clearTimeout(timer); });
6853
+ }
6854
+
6784
6855
  // Precomputed symmetric index pairs from LAM_BLENDSHAPES alphabetical ordering
6785
6856
  // Used to average left/right blendshape pairs for symmetrized output
6786
6857
  const SYMMETRIC_INDEX_PAIRS = [
@@ -6830,7 +6901,7 @@ async function loadOrt(wasmPaths) {
6830
6901
  const ortUrl = wasmPaths + 'ort.wasm.min.js';
6831
6902
 
6832
6903
  // Load the script by fetching and executing it
6833
- const response = await fetch(ortUrl);
6904
+ const response = await fetchWithTimeout(ortUrl, 30000);
6834
6905
  const scriptText = await response.text();
6835
6906
 
6836
6907
  // Create a blob URL for the script
@@ -6872,7 +6943,7 @@ async function loadModel(modelUrl, externalDataUrl, isIOS) {
6872
6943
  session = await ort.InferenceSession.create(modelUrl, sessionOptions);
6873
6944
  } else {
6874
6945
  // Desktop: fetch model graph as ArrayBuffer
6875
- const graphResponse = await fetch(modelUrl);
6946
+ const graphResponse = await fetchWithTimeout(modelUrl, 120000);
6876
6947
  if (!graphResponse.ok) {
6877
6948
  throw new Error('Failed to fetch model graph: ' + graphResponse.status + ' ' + graphResponse.statusText);
6878
6949
  }
@@ -6880,7 +6951,7 @@ async function loadModel(modelUrl, externalDataUrl, isIOS) {
6880
6951
 
6881
6952
  // Fetch external data file if present
6882
6953
  if (externalDataUrl && dataFilename) {
6883
- const dataResponse = await fetch(externalDataUrl);
6954
+ const dataResponse = await fetchWithTimeout(externalDataUrl, 120000);
6884
6955
  if (!dataResponse.ok) {
6885
6956
  throw new Error('Failed to fetch external data: ' + dataResponse.status + ' ' + dataResponse.statusText);
6886
6957
  }
@@ -8015,6 +8086,13 @@ var WORKER_SCRIPT4 = `
8015
8086
 
8016
8087
  var ort = null;
8017
8088
  var session = null;
8089
+
8090
+ function fetchWithTimeout(url, timeoutMs) {
8091
+ var controller = new AbortController();
8092
+ var timer = setTimeout(function() { controller.abort(); }, timeoutMs);
8093
+ return fetch(url, { signal: controller.signal }).finally(function() { clearTimeout(timer); });
8094
+ }
8095
+
8018
8096
  var sampleRate = 16000;
8019
8097
  var chunkSize = 512;
8020
8098
  var contextSize = 64;
@@ -8030,7 +8108,7 @@ async function loadOrt(wasmPaths) {
8030
8108
  const ortUrl = wasmPaths + 'ort.wasm.min.js';
8031
8109
 
8032
8110
  // Load the script by fetching and executing it
8033
- const response = await fetch(ortUrl);
8111
+ const response = await fetchWithTimeout(ortUrl, 30000);
8034
8112
  const scriptText = await response.text();
8035
8113
 
8036
8114
  // Create a blob URL for the script
@@ -8060,7 +8138,7 @@ async function loadModel(modelUrl, sr) {
8060
8138
  contextSize = sr === 16000 ? 64 : 32;
8061
8139
 
8062
8140
  // Fetch model data
8063
- const response = await fetch(modelUrl);
8141
+ const response = await fetchWithTimeout(modelUrl, 60000);
8064
8142
  if (!response.ok) {
8065
8143
  throw new Error('Failed to fetch model: ' + response.status + ' ' + response.statusText);
8066
8144
  }