Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions src/utils/CrossOriginStorage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
const HASH_ALGORITHM = "SHA-256";

class CrossOriginStorage {
static isAvailable = () =>
typeof navigator !== "undefined" && "crossOriginStorage" in navigator;

match = async (request) => {
const hashValue = await this._getFileHash(request);
if (!hashValue) {
return undefined;
}
const hash = { algorithm: HASH_ALGORITHM, value: hashValue };
try {
// @ts-expect-error
const [handle] = await navigator.crossOriginStorage.requestFileHandles([
hash,
]);
const blob = await handle.getFile();
return new Response(blob);
} catch (err) {
return undefined;
}
};
put = async (request, response) => {
const blob = await response.blob();
const hash = await this._getBlobHash(blob);
// @ts-expect-error
const [handle] = await navigator.crossOriginStorage.requestFileHandles(
[hash],
{ create: true },
);
const writableStream = await handle.createWritable();
await writableStream.write(blob);
await writableStream.close();
};

// Gets the SHA-256 hash for large resources as per
// https://huggingface.co/docs/hub/en/storage-backends#xet.
_getFileHash = async (url) => {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't "see" the requests for the ORT Wasm files. Those should be 100% cached in COS for guaranteed cache hits as any Transformers.js or ONNX Runtime Web uses the same few files.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this needs to happen in ORT, or you can of course do it "by hand".

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Wasm file fetch might happen here (line 12), but not 100% sure.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes we have been meaning to "control" this on the Transformers.js side by loading and caching the binary, then pointing wasmPaths to this buffer.

Just need to get around to adding it :)

if (/\/resolve\/main\/onnx\//.test(url)) {
Copy link

@tomayac tomayac Oct 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is essentially scraping the website. Maybe leave the original comment from my code where this was linked to an explanation on the HF docs. Also see the comment above about future-proofing this for possible algorithm changes.

Copy link

@tomayac tomayac Oct 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (/\/resolve\/main\/onnx\//.test(url)) {
if (/\/resolve\//.test(url)) {

@nico-martin Just realized this needs to be checking for just /resolve/main/ to match non-ONNX models like https://huggingface.co/qualcomm/Depth-Anything-V2/resolve/main/Depth-Anything-V2_float.tflite. Not sure if you want to keep this specific to ONNX models (as is), or if you want to make it generic, so it can resolve with the URL of the raw pointer file for any resources.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nico-martin Actually, the more I play with it, the more corner cases I discover. You can also point at the non-main URL (e.g., https://huggingface.co/qualcomm/Real-ESRGAN-x4plus/resolve/v0.37.0/Real-ESRGAN-x4plus_float.tflite), so you should only check for /resolve/ to make it even more general.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure thi model is supported by transformers.js?
It's tagged as image-to-image but not for transformer.js: https://huggingface.co/models?pipeline_tag=image-to-image&library=transformers.js

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, this is purely about the URL structure, not the model itself. The point is that the original code checked for /resolve/main/, but the example above shows it needs to be just /resolve/.

const rawUrl = url.replace(/\/resolve\//, "/raw/");
const text = await fetch(rawUrl).then((response) => response.text());
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This runs every time, which means you can't run fully offline. Instead, this should cache the mapping url=>hash and return the cached value. I had this in my initial implementation and remember there was some trickery needed to make it work with the actual URLs (I don't remember, but maybe it had to do with the post-redirect URLs that point at the CDN? Just copy what I had, this worked :-)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I did that deliberately. From my point of view, it's a question of separation of concerns/responsibilities. I dont think it is the responsibility of transformers.js to ensure that everything works offline. It is our responsibility to do our best to keep the download payload as little as possible. But here I dont this we need to cache this request since it is tiny.
On the other hand, we would risk that new versions of an ONNX file would not be loaded because the cached SHA value does not change. And it would not be obvious to the user or the app developer why.
In my opinion, if a developer wanted to have a fully offline solution they should solve the offline-caching on a ServiceWorker-level. We could help with that but we should not abstract it away by default.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. And stale-while-revalidate as a caching strategy for these "get SHA-256 hash" routes would work perfectly both for always being offline-capable and for never missing a new model. This should likely be added somewhere as a best practice in the docs, but for here: LGTM.

if (!text.includes("oid sha256:")) {
return null;
}
return text.replace(/.*?\n^oid sha256:(\w+)\n.*?$/gm, "$1") || null;
}
return null;
};

_getBlobHash = async (blob) => {
const hashAlgorithmIdentifier = "SHA-256";

// Get the contents of the blob as binary data contained in an ArrayBuffer.
const arrayBuffer = await blob.arrayBuffer();

// Hash the arrayBuffer using SHA-256.
const hashBuffer = await crypto.subtle.digest(
hashAlgorithmIdentifier,
arrayBuffer,
);

// Convert the ArrayBuffer to a hex string.
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray
.map((byte) => byte.toString(16).padStart(2, "0"))
.join("");

return {
algorithm: hashAlgorithmIdentifier,
value: hashHex,
};
};
}

export default CrossOriginStorage;
5 changes: 5 additions & 0 deletions src/utils/hub.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import path from 'node:path';

import { apis, env } from '../env.js';
import { dispatchCallback } from './core.js';
import CrossOriginStorage from './CrossOriginStorage.js'

/**
* @typedef {boolean|number} ExternalData Whether to load the model using the external data format (used for models >= 2GB in size).
Expand Down Expand Up @@ -479,6 +480,10 @@ export async function getModelFile(path_or_repo_id, filename, fatal = true, opti
filename
);

if(CrossOriginStorage.isAvailable()){
cache = new CrossOriginStorage();
}

/** @type {string} */
let cacheKey;
const proposedCacheKey = cache instanceof FileCache
Expand Down