react-native-executorch 0.10.5 → 0.10.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.
@@ -14,9 +14,11 @@ const IS_ANDROID = Platform.OS === 'android';
14
14
  // Persistent, per-app directory where downloaded model assets are cached.
15
15
  // iOS: internal DocumentDir (not CacheDir) so the OS won't evict large models
16
16
  // between runs and force a costly re-download.
17
- // Android: the app-private EXTERNAL files dir (getExternalFilesDir), so the
18
- // system DownloadManager can write there and same-volume moves stay cheap
19
- // even for multi-GB files. Falls back to DocumentDir if unmounted.
17
+ // Android: the app-private EXTERNAL files dir (getExternalFilesDir). It was
18
+ // picked so the system DownloadManager could stage into it; that backend
19
+ // is gone, but every model already on disk lives here, so moving the
20
+ // cache now would silently orphan all of them and re-download multi-GB
21
+ // files. Falls back to DocumentDir if unmounted.
20
22
  const ANDROID_DIRECTORY = RNBlobUtil.fs.dirs.SDCardDir || RNBlobUtil.fs.dirs.DocumentDir;
21
23
  const RNE_DIRECTORY = IS_ANDROID
22
24
  ? `${ANDROID_DIRECTORY}/react-native-executorch`
@@ -31,9 +33,7 @@ export interface DownloadOptions {
31
33
  onProgress?: (progress: number) => void;
32
34
  /**
33
35
  * Aborts the download. The bytes fetched so far are kept so a later
34
- * {@link download} of the same source resumes instead of restarting, except on
35
- * Android without the optional background downloader, where the system
36
- * DownloadManager discards a cancelled transfer.
36
+ * {@link download} of the same source resumes instead of restarting.
37
37
  */
38
38
  signal?: AbortSignal;
39
39
  /**
@@ -176,24 +176,6 @@ async function foldResumedChunkIntoPartial(
176
176
  await RNBlobUtil.fs.unlink(chunkPath).catch(() => {});
177
177
  }
178
178
 
179
- // DownloadManager's byte counter is 64-bit, but blob-util reads it out of the
180
- // cursor with `getInt`, so what reaches JS is the low 32 bits reinterpreted as
181
- // a signed int: past 2 GB it arrives NEGATIVE and wraps every 4 GB after that.
182
- // Multi-GB LLM models spend most of their download inside that range, which is
183
- // what collapsed their progress bar. The counter only ever grows, so the
184
- // discarded high bits can be rebuilt by counting how often the low ones wrap.
185
- const UINT32 = 0x100000000;
186
- function reassemble32BitCounter(): (raw: number) => number {
187
- let wraps = 0;
188
- let previous = 0;
189
- return (raw) => {
190
- const low = raw < 0 ? raw + UINT32 : raw;
191
- if (low < previous) wraps += 1;
192
- previous = low;
193
- return low + wraps * UINT32;
194
- };
195
- }
196
-
197
179
  // Reports absolute bytes for one file. `total` is 0 when the transfer does not
198
180
  // know the length yet — the receiver keeps using whatever length it already
199
181
  // had rather than treating the file as complete.
@@ -249,8 +231,7 @@ async function downloadUrl(url: string, cb: DownloadUrlCallbacks): Promise<strin
249
231
  // something a later download would CONTINUE from: the staged `.partial` and,
250
232
  // with the background downloader in play, a paused task holding resume data.
251
233
  // Clear it, or "download it again" quietly resumes the very attempt the
252
- // caller is trying to replace. The DownloadManager backend needs nothing
253
- // here — it unlinks its own staging file before every transfer.
234
+ // caller is trying to replace.
254
235
  await discardPartialDownload(dest);
255
236
  } else if (await RNBlobUtil.fs.exists(dest)) {
256
237
  // Cache hit — nothing to download.
@@ -296,30 +277,19 @@ async function startDownload(url: string, dest: string, entry: InFlightDownload)
296
277
  };
297
278
 
298
279
  // The optional background downloader wins on BOTH platforms when the app has
299
- // it, so a caller that installs it gets one transfer mechanism that behaves
300
- // the same everywhere instead of a per-platform one it cannot see.
301
- //
302
- // Without it each platform falls back to the best it can do on its own, and
303
- // only iOS loses background transfers by doing so — blob-util's in-process
304
- // reader is broken on Android (RonRadtke/react-native-blob-util#475: it stops
305
- // after 8 KB), so the system DownloadManager is not a preference there but the
306
- // only backend that works at all.
280
+ // it; without it both fall back to the same in-process stream. One mechanism
281
+ // plus one shared fallback, so a caller sees the same behavior everywhere.
307
282
  const backgroundDownloader = loadBackgroundDownloader();
308
283
 
309
- let path: string;
310
- if (backgroundDownloader) {
311
- path = await downloadUrlViaBackgroundSession(backgroundDownloader, url, dest, cb);
312
- } else if (IS_ANDROID) {
313
- path = await downloadUrlViaAndroidDownloadManager(url, dest, cb);
314
- } else {
315
- path = await downloadUrlViaIosStream(url, dest, cb);
316
- }
284
+ const path = backgroundDownloader
285
+ ? await downloadUrlViaBackgroundSession(backgroundDownloader, url, dest, cb)
286
+ : await downloadUrlViaStream(url, dest, cb);
317
287
 
318
- // Neither backend is guaranteed to emit a last sample at 100%: blob-util
319
- // throttles progress events, and DownloadManager is polled, so the final
320
- // chunk usually lands between two polls. Without this a finished file stays
321
- // stuck a little short of its own size, and in a multi-file config that
322
- // shortfall is what the user sees while the remaining files download.
288
+ // Neither backend is guaranteed to emit a last sample at 100%: blob-util only
289
+ // started reporting the final event reliably in 0.25.0, and the background
290
+ // downloader throttles its own. Without this a finished file stays stuck a
291
+ // little short of its own size, and in a multi-file config that shortfall is
292
+ // what the user sees while the remaining files download.
323
293
  const finalSize = await fileSize(path);
324
294
  if (finalSize > 0) cb.onBytes?.(finalSize, finalSize);
325
295
 
@@ -347,72 +317,6 @@ function joinDownload(entry: InFlightDownload, cb: DownloadUrlCallbacks): Promis
347
317
  });
348
318
  }
349
319
 
350
- // Android fallback used when that optional dependency is absent: the system
351
- // DownloadManager streams to app-private external storage. blob-util's
352
- // in-process reader cannot stand in for it — upstream #475 makes that path stop
353
- // after 8 KB — and DownloadManager also handles files larger than 2 GB, keeps
354
- // downloading while the app is in the background or killed, and resumes across
355
- // transient network drops on its own, so no manual Range logic.
356
- async function downloadUrlViaAndroidDownloadManager(
357
- url: string,
358
- dest: string,
359
- cb: DownloadUrlCallbacks
360
- ): Promise<string> {
361
- const tmp = `${dest}.downloading`;
362
- await RNBlobUtil.fs.unlink(tmp).catch(() => {});
363
-
364
- if (cb.signal?.aborted) throw abortError();
365
-
366
- const task = RNBlobUtil.config({
367
- addAndroidDownloads: {
368
- useDownloadManager: true,
369
- path: tmp,
370
- notification: false,
371
- mediaScannable: false,
372
- mime: 'application/octet-stream',
373
- },
374
- }).fetch('GET', url);
375
-
376
- const onAbort = () => task.cancel();
377
- cb.signal?.addEventListener('abort', onAbort);
378
-
379
- // DownloadManager reports total as -1 until the size is known; pass that on
380
- // as 0 rather than echoing the received count, which would otherwise look
381
- // like a file that is complete at every sample.
382
- const absoluteBytes = reassemble32BitCounter();
383
- task.progress({ count: 100 }, (received, total) => {
384
- const tot = Number(total);
385
- cb.onBytes?.(absoluteBytes(Number(received)), tot > 0 ? tot : 0);
386
- });
387
-
388
- try {
389
- await task;
390
- } catch (e) {
391
- await RNBlobUtil.fs.unlink(tmp).catch(() => {});
392
- throw cb.signal?.aborted ? abortError() : e;
393
- } finally {
394
- cb.signal?.removeEventListener('abort', onAbort);
395
- }
396
-
397
- // DownloadManager doesn't surface an HTTP status; an empty file means failure.
398
- const size = await fileSize(tmp);
399
- if (size <= 0) {
400
- await RNBlobUtil.fs.unlink(tmp).catch(() => {});
401
- throw RnExecuTorchError('DOWNLOAD_FAILED', `Download of ${url} failed (empty response).`);
402
- }
403
- // A non-empty file is not necessarily a complete one, and DownloadManager
404
- // gives us no status to check. Promoting a short file would cache it under
405
- // its final name forever — the existence-only cache check can't tell the
406
- // difference, and a truncated .pte only fails much later, at load.
407
- const expected = await expectedBytesFor(url, cb.expectedBytes, cb.signal);
408
- if (isTruncated(size, expected)) {
409
- await RNBlobUtil.fs.unlink(tmp).catch(() => {});
410
- throw incompleteError(url, size, expected);
411
- }
412
- await RNBlobUtil.fs.mv(tmp, dest);
413
- return dest;
414
- }
415
-
416
320
  // One background task per destination file, under an id that stays the same
417
321
  // across app launches: that is what lets a later call adopt a transfer this
418
322
  // process never started.
@@ -579,14 +483,19 @@ async function downloadUrlViaBackgroundSession(
579
483
  return dest;
580
484
  }
581
485
 
582
- // iOS fallback used when that optional dependency is absent: blob-util streams
583
- // via the iOS URL session straight to disk. It does NOT survive the app being
584
- // suspended — iOS tears the connection down about a second later — so an
585
- // interrupted transfer is picked up by the next `download` call instead.
586
- // Interrupted downloads resume from a `.partial` file via an HTTP Range request.
587
- // `canResume` is set to `false` on an internal retry to avoid recursing forever
588
- // if partial-file assembly ever fails.
589
- async function downloadUrlViaIosStream(
486
+ // The fallback used on either platform when that optional dependency is absent:
487
+ // blob-util streams the response straight to disk, in process. It does NOT
488
+ // survive the app being suspended — iOS tears the connection down about a
489
+ // second later, and Android may kill the process outright — so an interrupted
490
+ // transfer is picked up by the next `download` call instead: it resumes from a
491
+ // `.partial` file via an HTTP Range request. `canResume` is set to `false` on an
492
+ // internal retry to avoid recursing forever if partial-file assembly ever fails.
493
+ //
494
+ // This path needs react-native-blob-util >=0.24.11 on Android. 0.24.10's reader
495
+ // stopped after exactly 8 KB with "Download interrupted"
496
+ // (RonRadtke/react-native-blob-util#475), which is why Android used the system
497
+ // DownloadManager until that fix shipped.
498
+ async function downloadUrlViaStream(
590
499
  url: string,
591
500
  dest: string,
592
501
  cb: DownloadUrlCallbacks,
@@ -638,9 +547,9 @@ async function downloadUrlViaIosStream(
638
547
  });
639
548
  }
640
549
 
641
- // Same granularity as Android. blob-util still floors the rate at one event
642
- // per 250 ms, so this only means a large file advances in ~1% steps instead
643
- // of the 5% ones that made a multi-GB download look stalled between jumps.
550
+ // blob-util floors the rate at one event per 250 ms regardless, so this only
551
+ // means a large file advances in ~1% steps instead of the 5% ones that made a
552
+ // multi-GB download look stalled between jumps.
644
553
  task.progress({ count: 100 }, (received, total) => {
645
554
  const recv = Number(received);
646
555
  const tot = Number(total);
@@ -733,14 +642,14 @@ async function downloadUrlViaIosStream(
733
642
  // once as a plain full download so correctness never depends on resume.
734
643
  await RNBlobUtil.fs.unlink(part).catch(() => {});
735
644
  await RNBlobUtil.fs.unlink(target).catch(() => {});
736
- if (canResume) return downloadUrlViaIosStream(url, dest, cb, false);
645
+ if (canResume) return downloadUrlViaStream(url, dest, cb, false);
737
646
  throw assemblyErr;
738
647
  }
739
648
 
740
649
  if (restart) {
741
650
  await RNBlobUtil.fs.unlink(part).catch(() => {});
742
651
  await RNBlobUtil.fs.unlink(target).catch(() => {});
743
- return downloadUrlViaIosStream(url, dest, cb, false);
652
+ return downloadUrlViaStream(url, dest, cb, false);
744
653
  }
745
654
 
746
655
  // The transfer can also report success while the body was cut short — a
@@ -756,7 +665,7 @@ async function downloadUrlViaIosStream(
756
665
  // start over rather than fail. A fresh transfer that overshoots is a wrong
757
666
  // expectation instead, and falls through to be accepted.
758
667
  await RNBlobUtil.fs.unlink(part).catch(() => {});
759
- return downloadUrlViaIosStream(url, dest, cb, false);
668
+ return downloadUrlViaStream(url, dest, cb, false);
760
669
  }
761
670
  if (isTruncated(assembled, expected)) {
762
671
  // Short: keep the partial so the next call resumes and finishes it.
@@ -859,10 +768,8 @@ function substituteRemoteSources<T>(node: T, resolved: ReadonlyMap<string, strin
859
768
  * and survive it being killed. The fetcher uses it automatically on both platforms
860
769
  * when it is present, so the behavior is the same on each; nothing else changes.
861
770
  *
862
- * Without it the fetcher falls back to what each platform can do on its own: the
863
- * system DownloadManager on Android, which still continues in the background,
864
- * and on iOS a streaming request that stops when the app is suspended and is
865
- * resumed by the next `download` call.
771
+ * Without it both platforms fall back to the same streaming request, which stops
772
+ * when the app is suspended and is resumed by the next `download` call.
866
773
  * @category Utils / Functions
867
774
  * @typeParam T The shape of the value being resolved.
868
775
  * @param source A URL, a local path, or any nested object/array holding them.
@@ -10,7 +10,7 @@ const DOWNLOAD_EVENT_ENDPOINT = 'https://ai.swmansion.com/telemetry/downloads/ap
10
10
  // self-referencing import would need package `exports` support in the consuming
11
11
  // bundler to load at all. A wrong value here is analytics noise; a failed
12
12
  // import would break the bundle.
13
- const LIB_VERSION = '0.10.5';
13
+ const LIB_VERSION = '0.10.6';
14
14
 
15
15
  // Anonymous analytics are on by default; apps opt out via setTelemetryEnabled.
16
16
  let telemetryEnabled = true;
package/src/models.ts CHANGED
@@ -922,10 +922,15 @@ const LFM2_5_EMBEDDING_350M_MLX_INT4: TextEmbedderModel = {
922
922
  // =============================================================================
923
923
  // Image Embeddings
924
924
  // =============================================================================
925
+ // The exported image towers expect CLIP-normalized pixels (mean
926
+ // [0.48145466, 0.4578275, 0.40821073], std [0.26862954, 0.26130258, 0.27577711]).
925
927
  const CLIP_IMAGE_EMBEDDINGS_OPTS = {
926
928
  resizeMode: 'stretch' as const,
927
929
  interpolation: 'linear' as const,
928
- normalizeOpts: { alpha: 1 / 255.0, beta: 0.0 },
930
+ normalizeOpts: {
931
+ alpha: [1 / (255.0 * 0.26862954), 1 / (255.0 * 0.26130258), 1 / (255.0 * 0.27577711)],
932
+ beta: [-0.48145466 / 0.26862954, -0.4578275 / 0.26130258, -0.40821073 / 0.27577711],
933
+ },
929
934
  };
930
935
  const CLIP_VIT_BASE_PATCH32_IMAGE_XNNPACK_FP32: ImageEmbedderModel = {
931
936
  modelPath: `${BASE_URL}-clip-vit-base-patch32/${NEXT_VERSION_TAG}/xnnpack/clip_vit_base_patch32_image_xnnpack_fp32.pte`,