Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 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
32 changes: 26 additions & 6 deletions packages/transformers/src/backends/onnx.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ import { env, apis, LogLevel } from '../env.js';
// In either case, we select the default export if it exists, otherwise we use the named export.
import * as ONNX_NODE from 'onnxruntime-node';
import * as ONNX_WEB from 'onnxruntime-web/webgpu';
import { isBlobURL, loadWasmBinary, loadWasmFactory, toAbsoluteURL } from './utils/cacheWasm.js';
import { loadWasmBinary, loadWasmFactory } from './utils/cacheWasm.js';
import { isBlobURL, toAbsoluteURL } from '../utils/hub/utils.js';
import { logger } from '../utils/logger.js';
export { Tensor } from 'onnxruntime-common';

Expand Down Expand Up @@ -202,14 +203,24 @@ async function ensureWasmLoaded() {
return wasmLoadPromise;
}

// Check if we should load the WASM binary
const shouldUseWasmCache =
env.useWasmCache &&
typeof ONNX_ENV?.wasm?.wasmPaths === 'object' &&
ONNX_ENV?.wasm?.wasmPaths?.wasm &&
ONNX_ENV?.wasm?.wasmPaths?.mjs;

// Check if we should load the WASM binary
if (!shouldUseWasmCache) {
// In Deno's web runtime, the WASM factory must be loaded via blob URL so that
// Node.js detection can be patched out (see loadWasmFactory). Without caching,
// the factory is imported directly from its URL and Deno would crash trying to
// use Node.js APIs. useWasmCache defaults to true in this environment, so this
// only happens if the user explicitly disables it.
if (apis.IS_DENO_WEB_RUNTIME) {
throw new Error(
"env.useWasmCache=false is not supported in Deno's web runtime. Remove the useWasmCache override.",
);
}
wasmLoadPromise = Promise.resolve();
return wasmLoadPromise;
}
Expand All @@ -220,7 +231,10 @@ async function ensureWasmLoaded() {
// shouldUseWasmCache checks for wasmPaths.wasm and wasmPaths.mjs
const urls = /** @type {{ wasm: string, mjs: string }} */ (ONNX_ENV.wasm.wasmPaths);

// Load and cache both the WASM binary and factory
// Load both in parallel; the .mjs blob URL is only kept if wasmBinary succeeded.
// ORT only sets locateFile when wasmBinary is provided (onnxruntime PR https://github.com/microsoft/onnxruntime/pull/27411), which
// prevents new URL(fileName, import.meta.url) from failing inside a blob URL factory.
let wasmBinaryLoaded = false;
await Promise.all([
// Load and cache the WASM binary
urls.wasm && !isBlobURL(urls.wasm)
Expand All @@ -229,14 +243,15 @@ async function ensureWasmLoaded() {
const wasmBinary = await loadWasmBinary(toAbsoluteURL(urls.wasm));
if (wasmBinary) {
ONNX_ENV.wasm.wasmBinary = wasmBinary;
wasmBinaryLoaded = true;
}
} catch (err) {
logger.warn('Failed to pre-load WASM binary:', err);
}
})()
: Promise.resolve(),

// Load and cache the WASM factory
// Load and cache the WASM factory as a blob URL
urls.mjs && !isBlobURL(urls.mjs)
? (async () => {
try {
Expand All @@ -251,6 +266,12 @@ async function ensureWasmLoaded() {
})()
: Promise.resolve(),
]);

// If wasmBinary failed to load, revert wasmPaths.mjs to the original URL (factory can only be loaded from blob if ONNX_ENV.wasm.wasmBinary is set. @see ORT PR #27411)
if (!wasmBinaryLoaded) {
// @ts-ignore
ONNX_ENV.wasm.wasmPaths.mjs = urls.mjs;
}
})();

return wasmLoadPromise;
Expand Down Expand Up @@ -292,8 +313,7 @@ let webInferenceChain = Promise.resolve();
*/
export async function runInferenceSession(session, ortFeed) {
const run = () => session.run(ortFeed);
const output = await (apis.IS_WEB_ENV ? (webInferenceChain = webInferenceChain.then(run)) : run());
return output;
return apis.IS_WEB_ENV ? (webInferenceChain = webInferenceChain.then(run)) : run();
}

/**
Expand Down
62 changes: 20 additions & 42 deletions packages/transformers/src/backends/utils/cacheWasm.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { apis, env } from '../../env.js';
import { getCache } from '../../utils/cache.js';
import { isValidUrl } from '../../utils/hub/utils.js';
import { logger } from '../../utils/logger.js';
import { env } from '../../env.js';

/**
* Loads and caches a file from the given URL.
Expand Down Expand Up @@ -65,58 +64,37 @@ export async function loadWasmBinary(wasmURL) {
}

/**
* Loads and caches the WASM Factory for ONNX Runtime.
* Loads and caches the WASM Factory (.mjs file) for ONNX Runtime.
* Creates a blob URL from cached content (when safe) to bridge Cache API with dynamic imports used in ORT.
* @param {string} libURL The URL of the WASM Factory to load.
* @returns {Promise<string|null>} The blob URL of the WASM Factory, or null if loading failed.
* @returns {Promise<string|null>} The blob URL (if enabled), original URL (if disabled), or null if loading failed.
*/
export async function loadWasmFactory(libURL) {
// We can't use Blob URLs in some environments (Service Workers, Chrome extensions) due to security restrictions on dynamic import() of blob URLs.
// In such cases, just return the original URL and don't bother caching since dynamic import() won't use the Cache API anyway.
// See https://github.com/huggingface/transformers.js/issues/1532.
if (apis.IS_SERVICE_WORKER_ENV || apis.IS_CHROME_AVAILABLE) {
return libURL;
}

// Fetch from cache or network, then create blob URL
const response = await loadAndCacheFile(libURL);
if (!response || typeof response === 'string') return null;

try {
let code = await response.text();
// Fix relative paths when loading factory from blob, overwrite import.meta.url with actual baseURL
const baseUrl = libURL.split('/').slice(0, -1).join('/');
code = code.replaceAll('import.meta.url', `"${baseUrl}"`);

// Patch out the Node.js detection in the factory. Without this, Deno (which exposes
// globalThis.process.versions.node) would enter the Node.js branch and try to use
// Node.js APIs (worker_threads, fs, etc.) that Deno doesn't support.
// Only needed for the asyncify (single-threaded) variant loaded via blob URL. The
// module-level pthread auto-start code is unreachable since asyncify never spawns workers.
// See https://github.com/huggingface/transformers.js/pull/1546/.
code = code.replaceAll('globalThis.process?.versions?.node', 'false');
const blob = new Blob([code], { type: 'text/javascript' });
return URL.createObjectURL(blob);
} catch (error) {
logger.warn('Failed to read WASM binary:', error);
logger.warn('Failed to read WASM factory:', error);
return null;
}
}

/**
* Checks if the given URL is a blob URL (created via URL.createObjectURL).
* Blob URLs should not be cached as they are temporary in-memory references.
* @param {string} url - The URL to check.
* @returns {boolean} True if the URL is a blob URL, false otherwise.
*/
export function isBlobURL(url) {
return isValidUrl(url, ['blob:']);
}

/**
* Converts any URL to an absolute URL if needed.
* If the URL is already absolute (http://, https://, or blob:), returns it unchanged (handled by new URL(...)).
* Otherwise, resolves it relative to the current page location (browser) or module location (Node/Bun/Deno).
* @param {string} url - The URL to convert (can be relative or absolute).
* @returns {string} The absolute URL.
*/
export function toAbsoluteURL(url) {
let baseURL;

if (typeof location !== 'undefined' && location.href) {
// Browser environment: use location.href
baseURL = location.href;
} else if (typeof import.meta !== 'undefined' && import.meta.url) {
// Node.js/Bun/Deno module environment: use import.meta.url
baseURL = import.meta.url;
} else {
// Fallback: if no base is available, return the URL unchanged
return url;
}

return new URL(url, baseURL).href;
}
26 changes: 21 additions & 5 deletions packages/transformers/src/env.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,11 @@ import url from 'node:url';

const VERSION = '4.0.0-next.5';

const HAS_SELF = typeof self !== 'undefined';

const IS_FS_AVAILABLE = !isEmpty(fs);
const IS_PATH_AVAILABLE = !isEmpty(path);
const IS_WEB_CACHE_AVAILABLE = typeof self !== 'undefined' && 'caches' in self;
const IS_WEB_CACHE_AVAILABLE = HAS_SELF && 'caches' in self;

// Runtime detection
const IS_DENO_RUNTIME = typeof globalThis.Deno !== 'undefined';
Expand All @@ -44,7 +46,7 @@ const IS_NODE_ENV = IS_PROCESS_AVAILABLE && process?.release?.name === 'node' &&
// Check if various APIs are available (depends on environment)
const IS_BROWSER_ENV = typeof window !== 'undefined' && typeof window.document !== 'undefined';
const IS_WEBWORKER_ENV =
typeof self !== 'undefined' &&
HAS_SELF &&
['DedicatedWorkerGlobalScope', 'ServiceWorkerGlobalScope', 'SharedWorkerGlobalScope'].includes(
self.constructor?.name,
);
Expand All @@ -54,6 +56,12 @@ const IS_WEBGPU_AVAILABLE = IS_NODE_ENV || (typeof navigator !== 'undefined' &&
const IS_WEBNN_AVAILABLE = typeof navigator !== 'undefined' && 'ml' in navigator;
const IS_CRYPTO_AVAILABLE = typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function';

// @ts-ignore - chrome may not exist in all environments
const IS_CHROME_AVAILABLE = typeof chrome !== 'undefined' && typeof chrome.runtime !== 'undefined' && typeof chrome.runtime.id === 'string';

// @ts-ignore - ServiceWorkerGlobalScope may not exist in all environments
const IS_SERVICE_WORKER_ENV = typeof ServiceWorkerGlobalScope !== 'undefined' && HAS_SELF && self instanceof ServiceWorkerGlobalScope;

/**
* Check if the current environment is Safari browser.
* Works in both browser and web worker contexts.
Expand Down Expand Up @@ -95,6 +103,12 @@ export const apis = Object.freeze({
/** Whether we are running in a web-like environment (browser, web worker, or Deno web runtime) */
IS_WEB_ENV,

/** Whether we are running in a service worker environment */
IS_SERVICE_WORKER_ENV,

/** Whether we are running in Deno's web runtime (CDN imports, Cache API available, no filesystem) */
IS_DENO_WEB_RUNTIME,

/** Whether the Cache API is available */
IS_WEB_CACHE_AVAILABLE,

Expand All @@ -121,6 +135,9 @@ export const apis = Object.freeze({

/** Whether the crypto API is available */
IS_CRYPTO_AVAILABLE,

/** Whether the Chrome runtime API is available */
IS_CHROME_AVAILABLE,
});

const RUNNING_LOCALLY = IS_FS_AVAILABLE && IS_PATH_AVAILABLE;
Expand Down Expand Up @@ -202,9 +219,8 @@ export const LogLevel = Object.freeze({
* @property {boolean} useCustomCache Whether to use a custom cache system (defined by `customCache`), defaults to `false`.
* @property {import('./utils/cache.js').CacheInterface|null} customCache The custom cache to use. Defaults to `null`. Note: this must be an object which
* implements the `match` and `put` functions of the Web Cache API. For more information, see https://developer.mozilla.org/en-US/docs/Web/API/Cache.
* @property {boolean} useWasmCache Whether to pre-load and cache WASM binaries for ONNX Runtime. Defaults to `true` when cache is available.
* This can improve performance by avoiding repeated downloads of WASM files. Note: Only the WASM binary is cached.
* The MJS loader file still requires network access unless you use a Service Worker.
* @property {boolean} useWasmCache Whether to pre-load and cache WASM binaries and the WASM factory (.mjs) for ONNX Runtime.
* Defaults to `true` when cache is available. This can improve performance and enables offline usage by avoiding repeated downloads.
* @property {string} cacheKey The cache key to use for storing models and WASM binaries. Defaults to 'transformers-cache'.
* @property {(input: string | URL, init?: any) => Promise<any>} fetch The fetch function to use. Defaults to `fetch`.
*/
Expand Down
34 changes: 34 additions & 0 deletions packages/transformers/src/utils/hub/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,3 +132,37 @@ export async function readResponse(response, progress_callback, expectedSize) {

return buffer;
}

/**
* Checks if the given URL is a blob URL (created via URL.createObjectURL).
* Blob URLs should not be cached as they are temporary in-memory references.
* @param {string} url - The URL to check.
* @returns {boolean} True if the URL is a blob URL, false otherwise.
*/
export function isBlobURL(url) {
return isValidUrl(url, ['blob:']);
}

/**
* Converts any URL to an absolute URL if needed.
* If the URL is already absolute (http://, https://, or blob:), returns it unchanged (handled by new URL(...)).
* Otherwise, resolves it relative to the current page location (browser) or module location (Node/Bun/Deno).
* @param {string} url - The URL to convert (can be relative or absolute).
* @returns {string} The absolute URL.
*/
export function toAbsoluteURL(url) {
let baseURL;

if (typeof location !== 'undefined' && location.href) {
// Browser environment: use location.href
baseURL = location.href;
} else if (typeof import.meta !== 'undefined' && import.meta.url) {
// Node.js/Bun/Deno module environment: use import.meta.url
baseURL = import.meta.url;
} else {
// Fallback: if no base is available, return the URL unchanged
return url;
}

return new URL(url, baseURL).href;
}