diff --git a/packages/transformers/src/backends/onnx.js b/packages/transformers/src/backends/onnx.js index 524e228e0..13b1a7482 100644 --- a/packages/transformers/src/backends/onnx.js +++ b/packages/transformers/src/backends/onnx.js @@ -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'; @@ -202,14 +203,23 @@ 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; } @@ -220,7 +230,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) @@ -229,6 +242,7 @@ 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); @@ -236,7 +250,7 @@ async function ensureWasmLoaded() { })() : 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 { @@ -251,6 +265,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; @@ -292,8 +312,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(); } /** diff --git a/packages/transformers/src/backends/utils/cacheWasm.js b/packages/transformers/src/backends/utils/cacheWasm.js index 8d5a81760..a0c096567 100644 --- a/packages/transformers/src/backends/utils/cacheWasm.js +++ b/packages/transformers/src/backends/utils/cacheWasm.js @@ -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. @@ -65,58 +64,38 @@ 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} The blob URL of the WASM Factory, or null if loading failed. + * @returns {Promise} 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}"`); + + // Handle the case where we are importing the bundled version of the library in Deno (e.g., via CDN or local file), + // where we need to patch out 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 aren't used in the bundled web version. + // 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/ for more information. + // + // NOTE: This does not affect default usage via Deno (i.e., imported via npm: prefix), since we'll be using onnxruntime-node (Native) instead of onnxruntime-web (WASM). 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; -} diff --git a/packages/transformers/src/env.js b/packages/transformers/src/env.js index 70005605a..8b3f8755b 100644 --- a/packages/transformers/src/env.js +++ b/packages/transformers/src/env.js @@ -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'; @@ -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, ); @@ -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. @@ -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, @@ -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; @@ -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} fetch The fetch function to use. Defaults to `fetch`. */ diff --git a/packages/transformers/src/utils/hub/utils.js b/packages/transformers/src/utils/hub/utils.js index f95573ca0..e05ee27ee 100644 --- a/packages/transformers/src/utils/hub/utils.js +++ b/packages/transformers/src/utils/hub/utils.js @@ -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; +}