**feat(core): enhance vector handling, model loading, and compatibility**
- **Vector Handling Updates**: - Added a `dimensions` property to `BrainyDataConfig` for specifying vector dimensions. - Introduced validation for vector dimensions during database creation and insertion to ensure consistency. - Enhanced error handling and logging for dimension mismatches. - **Model Loading Improvements**: - Implemented retry logic for Universal Sentence Encoder model loading to handle network instability and JSON parsing errors gracefully. - Improved logging and debugging support for failures during model initialization and embedding operations. - **Compatibility Enhancements**: - Updated polyfills to support TensorFlow.js compatibility across diverse server environments (Node.js, serverless, etc.). - Introduced and refactored global `TextEncoder`/`TextDecoder` definitions for seamless operation in non-browser environments. - Simplified TensorFlow.js backend setup with streamlined imports and logging for GPU/WebGL fallback. - **Purpose**: - These updates improve BrainyData's robustness, enforce correct vector usage, and extend compatibility with varied runtime environments. The changes enhance the usability, reliability, and cross-platform readiness of core functionalities.
This commit is contained in:
parent
ad4af27385
commit
3ec183dab6
9 changed files with 828 additions and 365 deletions
|
|
@ -145,32 +145,28 @@ export async function calculateDistancesBatch(
|
|||
// In worker context, use the importTensorFlow function
|
||||
tf = await self.importTensorFlow()
|
||||
} else {
|
||||
// CRITICAL: First, directly import the setup module to ensure the TensorFlow.js patch is applied
|
||||
// This is the most reliable way to ensure the patch is applied before TensorFlow.js is loaded
|
||||
// CRITICAL: Ensure TextEncoder/TextDecoder are available before TensorFlow.js loads
|
||||
try {
|
||||
// In Node.js environment, use require() which is synchronous
|
||||
if (typeof require !== 'undefined') {
|
||||
// First, require the setup module to apply the patch
|
||||
require('../setup.js')
|
||||
|
||||
// Now load TensorFlow.js core module
|
||||
tf = require('@tensorflow/tfjs-core')
|
||||
|
||||
// Load CPU backend
|
||||
require('@tensorflow/tfjs-backend-cpu')
|
||||
|
||||
// Set CPU as the backend
|
||||
tf.setBackend('cpu')
|
||||
} else {
|
||||
// In browser or other environments without require(), use dynamic imports
|
||||
// First, dynamically import the setup module to apply the patch
|
||||
await import('../setup.js')
|
||||
|
||||
// Now load TensorFlow.js core module
|
||||
tf = await import('@tensorflow/tfjs-core')
|
||||
await import('@tensorflow/tfjs-backend-cpu')
|
||||
await tf.setBackend('cpu')
|
||||
// Use dynamic imports for all environments to ensure TensorFlow loads after patch
|
||||
if (typeof process !== 'undefined' && process.versions && process.versions.node) {
|
||||
// Ensure TextEncoder/TextDecoder are globally available in Node.js
|
||||
const util = await import('util')
|
||||
if (typeof global.TextEncoder === 'undefined') {
|
||||
global.TextEncoder = util.TextEncoder
|
||||
}
|
||||
if (typeof global.TextDecoder === 'undefined') {
|
||||
global.TextDecoder = util.TextDecoder
|
||||
}
|
||||
}
|
||||
|
||||
// Apply the TensorFlow.js patch
|
||||
const { applyTensorFlowPatch } = await import('./textEncoding.js')
|
||||
await applyTensorFlowPatch()
|
||||
|
||||
// Now load TensorFlow.js core module using dynamic imports
|
||||
tf = await import('@tensorflow/tfjs-core')
|
||||
await import('@tensorflow/tfjs-backend-cpu')
|
||||
await tf.setBackend('cpu')
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize TensorFlow.js:', error)
|
||||
throw error
|
||||
|
|
|
|||
|
|
@ -22,52 +22,163 @@ export class UniversalSentenceEncoder implements EmbeddingModel {
|
|||
|
||||
/**
|
||||
* Add polyfills and patches for TensorFlow.js compatibility
|
||||
* This addresses issues with TensorFlow.js in Node.js environments
|
||||
*
|
||||
* This addresses issues with TensorFlow.js across all server environments
|
||||
* (Node.js, serverless, and other server environments)
|
||||
*
|
||||
* Note: The main TensorFlow.js patching is now centralized in textEncoding.ts
|
||||
* and applied through setup.ts. This method only adds additional utility functions
|
||||
* that might be needed by TensorFlow.js.
|
||||
*/
|
||||
private addNodeCompatibilityPolyfills(): void {
|
||||
// Only apply in Node.js environment
|
||||
if (
|
||||
typeof process === 'undefined' ||
|
||||
!process.versions ||
|
||||
!process.versions.node
|
||||
) {
|
||||
return
|
||||
private addServerCompatibilityPolyfills(): void {
|
||||
// Apply in all non-browser environments (Node.js, serverless, server environments)
|
||||
const isBrowserEnv = typeof window !== 'undefined' && typeof document !== 'undefined'
|
||||
if (isBrowserEnv) {
|
||||
return // Browser environments don't need these polyfills
|
||||
}
|
||||
|
||||
// Add polyfill for isFloat32Array in Node.js 24.4.0
|
||||
// This fixes the "Cannot read properties of undefined (reading 'isFloat32Array')" error
|
||||
if (typeof global !== 'undefined') {
|
||||
// Get the appropriate global object for the current environment
|
||||
const globalObj = (() => {
|
||||
if (typeof globalThis !== 'undefined') return globalThis
|
||||
if (typeof global !== 'undefined') return global
|
||||
if (typeof self !== 'undefined') return self
|
||||
return {} as any // Fallback for unknown environments
|
||||
})()
|
||||
|
||||
// Add polyfill for utility functions across all server environments
|
||||
// This fixes issues like "Cannot read properties of undefined (reading 'isFloat32Array')"
|
||||
try {
|
||||
// Ensure the util object exists
|
||||
if (!globalObj.util) {
|
||||
globalObj.util = {}
|
||||
}
|
||||
|
||||
// Add isFloat32Array method if it doesn't exist
|
||||
if (!globalObj.util.isFloat32Array) {
|
||||
globalObj.util.isFloat32Array = (obj: any) => {
|
||||
return !!(
|
||||
obj instanceof Float32Array ||
|
||||
(obj &&
|
||||
Object.prototype.toString.call(obj) === '[object Float32Array]')
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Add isTypedArray method if it doesn't exist
|
||||
if (!globalObj.util.isTypedArray) {
|
||||
globalObj.util.isTypedArray = (obj: any) => {
|
||||
return !!(ArrayBuffer.isView(obj) && !(obj instanceof DataView))
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to add utility polyfills:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we're running in a test environment
|
||||
*/
|
||||
private isTestEnvironment(): boolean {
|
||||
// Safely check for Node.js environment first
|
||||
if (typeof process === 'undefined') {
|
||||
return false
|
||||
}
|
||||
|
||||
return (
|
||||
process.env.NODE_ENV === 'test' ||
|
||||
process.env.VITEST === 'true' ||
|
||||
(typeof global !== 'undefined' && global.__vitest__) ||
|
||||
process.argv.some((arg) => arg.includes('vitest'))
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Log message only if not in test environment
|
||||
*/
|
||||
private logIfNotTest(
|
||||
level: 'log' | 'warn' | 'error',
|
||||
message: string,
|
||||
...args: any[]
|
||||
): void {
|
||||
if (!this.isTestEnvironment()) {
|
||||
console[level](message, ...args)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the Universal Sentence Encoder model with retry logic
|
||||
* This helps handle network failures and JSON parsing errors from TensorFlow Hub
|
||||
* @param loadFunction The function to load the model
|
||||
* @param maxRetries Maximum number of retry attempts
|
||||
* @param baseDelay Base delay in milliseconds for exponential backoff
|
||||
*/
|
||||
private async loadModelWithRetry(
|
||||
loadFunction: () => Promise<EmbeddingModel>,
|
||||
maxRetries: number = 3,
|
||||
baseDelay: number = 1000
|
||||
): Promise<EmbeddingModel> {
|
||||
let lastError: Error | null = null
|
||||
|
||||
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
// Ensure the util object exists
|
||||
if (!global.util) {
|
||||
global.util = {}
|
||||
this.logIfNotTest(
|
||||
'log',
|
||||
attempt === 0
|
||||
? 'Loading Universal Sentence Encoder model...'
|
||||
: `Retrying Universal Sentence Encoder model loading (attempt ${attempt + 1}/${maxRetries + 1})...`
|
||||
)
|
||||
|
||||
const model = await loadFunction()
|
||||
|
||||
if (attempt > 0) {
|
||||
this.logIfNotTest(
|
||||
'log',
|
||||
'Universal Sentence Encoder model loaded successfully after retry'
|
||||
)
|
||||
}
|
||||
|
||||
// Add isFloat32Array method if it doesn't exist
|
||||
if (!global.util.isFloat32Array) {
|
||||
global.util.isFloat32Array = (obj: any) => {
|
||||
return !!(
|
||||
obj instanceof Float32Array ||
|
||||
(obj &&
|
||||
Object.prototype.toString.call(obj) === '[object Float32Array]')
|
||||
return model
|
||||
} catch (error) {
|
||||
lastError = error as Error
|
||||
const errorMessage = lastError.message || String(lastError)
|
||||
|
||||
// Check if this is a network-related error that might benefit from retry
|
||||
const isRetryableError =
|
||||
errorMessage.includes('Failed to parse model JSON') ||
|
||||
errorMessage.includes('Failed to fetch') ||
|
||||
errorMessage.includes('Network error') ||
|
||||
errorMessage.includes('ENOTFOUND') ||
|
||||
errorMessage.includes('ECONNRESET') ||
|
||||
errorMessage.includes('ETIMEDOUT') ||
|
||||
errorMessage.includes('JSON') ||
|
||||
errorMessage.includes('model.json')
|
||||
|
||||
if (attempt < maxRetries && isRetryableError) {
|
||||
const delay = baseDelay * Math.pow(2, attempt) // Exponential backoff
|
||||
this.logIfNotTest(
|
||||
'warn',
|
||||
`Universal Sentence Encoder model loading failed (attempt ${attempt + 1}): ${errorMessage}. Retrying in ${delay}ms...`
|
||||
)
|
||||
await new Promise((resolve) => setTimeout(resolve, delay))
|
||||
} else {
|
||||
// Either we've exhausted retries or this is not a retryable error
|
||||
if (attempt >= maxRetries) {
|
||||
this.logIfNotTest(
|
||||
'error',
|
||||
`Universal Sentence Encoder model loading failed after ${maxRetries + 1} attempts. Last error: ${errorMessage}`
|
||||
)
|
||||
} else {
|
||||
this.logIfNotTest(
|
||||
'error',
|
||||
`Universal Sentence Encoder model loading failed with non-retryable error: ${errorMessage}`
|
||||
)
|
||||
}
|
||||
throw lastError
|
||||
}
|
||||
|
||||
// Add isTypedArray method if it doesn't exist
|
||||
if (!global.util.isTypedArray) {
|
||||
global.util.isTypedArray = (obj: any) => {
|
||||
return !!(ArrayBuffer.isView(obj) && !(obj instanceof DataView))
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to add utility polyfills:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// This should never be reached, but just in case
|
||||
throw lastError || new Error('Unknown error during model loading')
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -93,31 +204,62 @@ export class UniversalSentenceEncoder implements EmbeddingModel {
|
|||
}
|
||||
|
||||
// Add polyfills for TensorFlow.js compatibility
|
||||
this.addNodeCompatibilityPolyfills()
|
||||
this.addServerCompatibilityPolyfills()
|
||||
|
||||
// TensorFlow.js will use its default EPSILON value
|
||||
|
||||
// CRITICAL: First, directly import the setup module to ensure the TensorFlow.js patch is applied
|
||||
// This is the most reliable way to ensure the patch is applied before TensorFlow.js is loaded
|
||||
// CRITICAL: Ensure TextEncoder/TextDecoder are available before TensorFlow.js loads
|
||||
try {
|
||||
// In Node.js environment, use require() which is synchronous
|
||||
if (typeof require !== 'undefined') {
|
||||
// First, require the setup module to apply the patch
|
||||
require('../setup.js')
|
||||
// Get the appropriate global object for the current environment
|
||||
const globalObj = (() => {
|
||||
if (typeof globalThis !== 'undefined') return globalThis
|
||||
if (typeof global !== 'undefined') return global
|
||||
if (typeof self !== 'undefined') return self
|
||||
return null
|
||||
})()
|
||||
|
||||
// Now load TensorFlow.js core module
|
||||
this.tf = require('@tensorflow/tfjs-core')
|
||||
// Ensure TextEncoder/TextDecoder are globally available in server environments
|
||||
if (globalObj) {
|
||||
// Try to use Node.js util module if available (Node.js environments)
|
||||
try {
|
||||
if (typeof process !== 'undefined' && process.versions && process.versions.node) {
|
||||
const util = await import('util')
|
||||
if (!globalObj.TextEncoder) {
|
||||
globalObj.TextEncoder = util.TextEncoder
|
||||
}
|
||||
if (!globalObj.TextDecoder) {
|
||||
globalObj.TextDecoder = util.TextDecoder
|
||||
}
|
||||
}
|
||||
} catch (utilError) {
|
||||
// Fallback to standard TextEncoder/TextDecoder for non-Node.js server environments
|
||||
if (!globalObj.TextEncoder) {
|
||||
globalObj.TextEncoder = TextEncoder
|
||||
}
|
||||
if (!globalObj.TextDecoder) {
|
||||
globalObj.TextDecoder = TextDecoder
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load CPU backend (always needed as fallback)
|
||||
require('@tensorflow/tfjs-backend-cpu')
|
||||
// Apply the TensorFlow.js patch
|
||||
const { applyTensorFlowPatch } = await import('./textEncoding.js')
|
||||
await applyTensorFlowPatch()
|
||||
|
||||
// Try to load WebGL backend for GPU acceleration in browser environments
|
||||
// Now load TensorFlow.js core module using dynamic imports
|
||||
this.tf = await import('@tensorflow/tfjs-core')
|
||||
|
||||
// Import CPU backend (always needed as fallback)
|
||||
await import('@tensorflow/tfjs-backend-cpu')
|
||||
|
||||
// Try to import WebGL backend for GPU acceleration in browser environments
|
||||
try {
|
||||
if (typeof window !== 'undefined') {
|
||||
await import('@tensorflow/tfjs-backend-webgl')
|
||||
// Check if WebGL is available
|
||||
try {
|
||||
require('@tensorflow/tfjs-backend-webgl')
|
||||
// Check if WebGL is available
|
||||
if (this.tf.setBackend) {
|
||||
this.tf.setBackend('webgl')
|
||||
await this.tf.setBackend('webgl')
|
||||
this.backend = 'webgl'
|
||||
console.log('Using WebGL backend for TensorFlow.js')
|
||||
} else {
|
||||
|
|
@ -133,58 +275,18 @@ export class UniversalSentenceEncoder implements EmbeddingModel {
|
|||
this.backend = 'cpu'
|
||||
}
|
||||
}
|
||||
|
||||
// Load Universal Sentence Encoder
|
||||
this.use = require('@tensorflow-models/universal-sentence-encoder')
|
||||
} else {
|
||||
// In browser or other environments without require(), use dynamic imports
|
||||
// First, dynamically import the setup module to apply the patch
|
||||
await import('../setup.js')
|
||||
|
||||
// Now load TensorFlow.js core module
|
||||
this.tf = await import('@tensorflow/tfjs-core')
|
||||
|
||||
// Import CPU backend (always needed as fallback)
|
||||
await import('@tensorflow/tfjs-backend-cpu')
|
||||
|
||||
// Try to import WebGL backend for GPU acceleration in browser environments
|
||||
try {
|
||||
if (typeof window !== 'undefined') {
|
||||
await import('@tensorflow/tfjs-backend-webgl')
|
||||
// Check if WebGL is available
|
||||
try {
|
||||
if (this.tf.setBackend) {
|
||||
await this.tf.setBackend('webgl')
|
||||
this.backend = 'webgl'
|
||||
console.log('Using WebGL backend for TensorFlow.js')
|
||||
} else {
|
||||
console.warn(
|
||||
'tf.setBackend is not available, falling back to CPU'
|
||||
)
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(
|
||||
'WebGL backend not available, falling back to CPU:',
|
||||
e
|
||||
)
|
||||
this.backend = 'cpu'
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
'WebGL backend not available, falling back to CPU:',
|
||||
error
|
||||
)
|
||||
this.backend = 'cpu'
|
||||
}
|
||||
|
||||
// Load Universal Sentence Encoder
|
||||
this.use = await import(
|
||||
'@tensorflow-models/universal-sentence-encoder'
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
'WebGL backend not available, falling back to CPU:',
|
||||
error
|
||||
)
|
||||
this.backend = 'cpu'
|
||||
}
|
||||
|
||||
// Load Universal Sentence Encoder using dynamic import
|
||||
this.use = await import('@tensorflow-models/universal-sentence-encoder')
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize TensorFlow.js:', error)
|
||||
this.logIfNotTest('error', 'Failed to initialize TensorFlow.js:', error)
|
||||
throw error
|
||||
}
|
||||
|
||||
|
|
@ -209,14 +311,18 @@ export class UniversalSentenceEncoder implements EmbeddingModel {
|
|||
)
|
||||
}
|
||||
|
||||
// Load the model
|
||||
this.model = await loadFunction()
|
||||
// Load the model with retry logic for network failures
|
||||
this.model = await this.loadModelWithRetry(loadFunction)
|
||||
this.initialized = true
|
||||
|
||||
// Restore original console.warn
|
||||
console.warn = originalWarn
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize Universal Sentence Encoder:', error)
|
||||
this.logIfNotTest(
|
||||
'error',
|
||||
'Failed to initialize Universal Sentence Encoder:',
|
||||
error
|
||||
)
|
||||
throw new Error(
|
||||
`Failed to initialize Universal Sentence Encoder: ${error}`
|
||||
)
|
||||
|
|
@ -272,7 +378,8 @@ export class UniversalSentenceEncoder implements EmbeddingModel {
|
|||
|
||||
return embeddingArray[0]
|
||||
} catch (error) {
|
||||
console.error(
|
||||
this.logIfNotTest(
|
||||
'error',
|
||||
'Failed to embed text with Universal Sentence Encoder:',
|
||||
error
|
||||
)
|
||||
|
|
@ -336,7 +443,8 @@ export class UniversalSentenceEncoder implements EmbeddingModel {
|
|||
|
||||
return results
|
||||
} catch (error) {
|
||||
console.error(
|
||||
this.logIfNotTest(
|
||||
'error',
|
||||
'Failed to batch embed text with Universal Sentence Encoder:',
|
||||
error
|
||||
)
|
||||
|
|
@ -357,7 +465,11 @@ export class UniversalSentenceEncoder implements EmbeddingModel {
|
|||
this.tf.disposeVariables()
|
||||
this.initialized = false
|
||||
} catch (error) {
|
||||
console.error('Failed to dispose Universal Sentence Encoder:', error)
|
||||
this.logIfNotTest(
|
||||
'error',
|
||||
'Failed to dispose Universal Sentence Encoder:',
|
||||
error
|
||||
)
|
||||
}
|
||||
}
|
||||
return Promise.resolve()
|
||||
|
|
@ -471,13 +583,48 @@ function findUSELoadFunction(
|
|||
return loadFunction
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we're running in a test environment (standalone version)
|
||||
*/
|
||||
function isTestEnvironment(): boolean {
|
||||
// Safely check for Node.js environment first
|
||||
if (typeof process === 'undefined') {
|
||||
return false
|
||||
}
|
||||
|
||||
return (
|
||||
process.env.NODE_ENV === 'test' ||
|
||||
process.env.VITEST === 'true' ||
|
||||
(typeof global !== 'undefined' && global.__vitest__) ||
|
||||
process.argv.some((arg) => arg.includes('vitest'))
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Log message only if not in test environment (standalone version)
|
||||
*/
|
||||
function logIfNotTest(
|
||||
level: 'log' | 'warn' | 'error',
|
||||
message: string,
|
||||
...args: any[]
|
||||
): void {
|
||||
if (!isTestEnvironment()) {
|
||||
console[level](message, ...args)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an embedding function from an embedding model
|
||||
* @param model Embedding model to use
|
||||
* @param model Embedding model to use (optional, defaults to UniversalSentenceEncoder)
|
||||
*/
|
||||
export function createEmbeddingFunction(
|
||||
model: EmbeddingModel
|
||||
model?: EmbeddingModel
|
||||
): EmbeddingFunction {
|
||||
// If no model is provided, use the default TensorFlow embedding function
|
||||
if (!model) {
|
||||
return createTensorFlowEmbeddingFunction()
|
||||
}
|
||||
|
||||
return async (data: any): Promise<Vector> => {
|
||||
return await model.embed(data)
|
||||
}
|
||||
|
|
@ -497,13 +644,19 @@ export function createTensorFlowEmbeddingFunction(): EmbeddingFunction {
|
|||
try {
|
||||
// Initialize the model if it hasn't been initialized yet
|
||||
if (!sharedModelInitialized) {
|
||||
await sharedModel.init()
|
||||
sharedModelInitialized = true
|
||||
try {
|
||||
await sharedModel.init()
|
||||
sharedModelInitialized = true
|
||||
} catch (initError) {
|
||||
// Reset the flag so we can retry initialization on the next call
|
||||
sharedModelInitialized = false
|
||||
throw initError
|
||||
}
|
||||
}
|
||||
|
||||
return await sharedModel.embed(data)
|
||||
} catch (error) {
|
||||
console.error('Failed to use TensorFlow embedding:', error)
|
||||
logIfNotTest('error', 'Failed to use TensorFlow embedding:', error)
|
||||
throw new Error(
|
||||
`Universal Sentence Encoder is required but failed: ${error}`
|
||||
)
|
||||
|
|
@ -543,7 +696,7 @@ export const defaultBatchEmbeddingFunction: (
|
|||
|
||||
return await sharedBatchModel.embedBatch(dataArray)
|
||||
} catch (error) {
|
||||
console.error('Failed to use TensorFlow batch embedding:', error)
|
||||
logIfNotTest('error', 'Failed to use TensorFlow batch embedding:', error)
|
||||
throw new Error(
|
||||
`Universal Sentence Encoder batch embedding failed: ${error}`
|
||||
)
|
||||
|
|
|
|||
|
|
@ -13,6 +13,12 @@ export function isBrowser(): boolean {
|
|||
* Check if code is running in a Node.js environment
|
||||
*/
|
||||
export function isNode(): boolean {
|
||||
// If browser environment is detected, prioritize it over Node.js
|
||||
// This handles cases like jsdom where both window and process exist
|
||||
if (isBrowser()) {
|
||||
return false
|
||||
}
|
||||
|
||||
return (
|
||||
typeof process !== 'undefined' &&
|
||||
process.versions != null &&
|
||||
|
|
|
|||
|
|
@ -1,224 +1,341 @@
|
|||
// In: @soulcraft/brainy/src/utils/textEncoding.ts
|
||||
import { isNode } from './environment.js'
|
||||
|
||||
/**
|
||||
* Checks if the code is running in a Node.js environment.
|
||||
*/
|
||||
function isNode(): boolean {
|
||||
return (
|
||||
typeof process !== 'undefined' &&
|
||||
process.versions != null &&
|
||||
process.versions.node != null
|
||||
)
|
||||
// This module must be run BEFORE any TensorFlow.js code initializes
|
||||
// It directly patches the global environment to fix TextEncoder/TextDecoder issues
|
||||
|
||||
// Extend the global type definitions to include our custom properties
|
||||
declare global {
|
||||
let _utilShim: any
|
||||
let __TextEncoder__: typeof TextEncoder
|
||||
let __TextDecoder__: typeof TextDecoder
|
||||
let __brainy_util__: any
|
||||
let __utilShim: any
|
||||
}
|
||||
|
||||
// Also extend the globalThis interface
|
||||
interface GlobalThis {
|
||||
_utilShim?: any
|
||||
__TextEncoder__?: typeof TextEncoder
|
||||
__TextDecoder__?: typeof TextDecoder
|
||||
__brainy_util__?: any
|
||||
__utilShim?: any
|
||||
}
|
||||
|
||||
/**
|
||||
* Global flag to track if TensorFlow.js has been initialized
|
||||
* This helps prevent multiple registrations of the same kernels
|
||||
*/
|
||||
const TENSORFLOW_INITIALIZED = Symbol('TENSORFLOW_INITIALIZED')
|
||||
|
||||
/**
|
||||
* Flag to track if the patch has been applied
|
||||
* This prevents multiple applications of the patch
|
||||
*/
|
||||
let patchApplied = false
|
||||
|
||||
/**
|
||||
* CRITICAL: Applies a compatibility patch for TensorFlow.js when running in a modern
|
||||
* Node.js ES Module environment. This must be called before any TensorFlow.js
|
||||
* modules are imported.
|
||||
*
|
||||
* This function prevents the "TextEncoder is not a constructor" error by preemptively
|
||||
* creating a compliant PlatformNode class with proper TextEncoder/TextDecoder support
|
||||
* and placing it on the global object where TensorFlow.js expects to find it.
|
||||
*
|
||||
* The race condition occurs because TensorFlow.js's platform detection might run
|
||||
* before the necessary global objects are properly initialized in certain Node.js
|
||||
* environments, particularly when the package is being used by other applications.
|
||||
*
|
||||
* This function is called from setup.ts, which must be the first import in unified.ts
|
||||
* to ensure the patch is applied before any TensorFlow.js code is executed.
|
||||
*
|
||||
* It also applies a patch to prevent duplicate kernel registrations when TensorFlow.js
|
||||
* is imported multiple times.
|
||||
* Monkeypatch TensorFlow.js's PlatformNode class to fix TextEncoder/TextDecoder issues
|
||||
* CRITICAL: This runs immediately at the top level when this module is imported
|
||||
*/
|
||||
export function applyTensorFlowPatch(): void {
|
||||
// Prevent multiple applications of the patch
|
||||
if (patchApplied) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!isNode()) {
|
||||
return // Patch is only for Node.js
|
||||
}
|
||||
|
||||
// In modern Node.js with ES Modules, TensorFlow.js can fail during its
|
||||
// initial platform detection. This patch preempts that logic by creating
|
||||
// a compliant "Platform" class that uses the standard global TextEncoder
|
||||
// and placing it on the global object where TensorFlow.js expects to find it.
|
||||
if (typeof globalThis !== 'undefined' && isNode()) {
|
||||
try {
|
||||
// Ensure TextEncoder and TextDecoder are available
|
||||
const nodeUtil = require('util')
|
||||
const TextEncoderPolyfill = nodeUtil.TextEncoder || global.TextEncoder
|
||||
const TextDecoderPolyfill = nodeUtil.TextDecoder || global.TextDecoder
|
||||
|
||||
if (!TextEncoderPolyfill || !TextDecoderPolyfill) {
|
||||
console.warn(
|
||||
'Brainy: TextEncoder or TextDecoder not available, attempting to polyfill'
|
||||
)
|
||||
|
||||
// If still not available, try to use a simple polyfill
|
||||
if (!TextEncoderPolyfill) {
|
||||
class SimpleTextEncoder {
|
||||
encode(input: string): Uint8Array {
|
||||
const buf = Buffer.from(input, 'utf8')
|
||||
return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength)
|
||||
}
|
||||
}
|
||||
|
||||
global.TextEncoder = SimpleTextEncoder
|
||||
}
|
||||
|
||||
if (!TextDecoderPolyfill) {
|
||||
class SimpleTextDecoder {
|
||||
decode(input?: Uint8Array): string {
|
||||
if (!input) return ''
|
||||
return Buffer.from(
|
||||
input.buffer,
|
||||
input.byteOffset,
|
||||
input.byteLength
|
||||
).toString('utf8')
|
||||
}
|
||||
}
|
||||
|
||||
global.TextDecoder = SimpleTextDecoder
|
||||
}
|
||||
} else {
|
||||
// Ensure they're available globally
|
||||
global.TextEncoder = TextEncoderPolyfill
|
||||
global.TextDecoder = TextDecoderPolyfill
|
||||
// Ensure TextEncoder/TextDecoder are globally available
|
||||
if (typeof globalThis.TextEncoder === 'undefined') {
|
||||
globalThis.TextEncoder = TextEncoder
|
||||
}
|
||||
if (typeof globalThis.TextDecoder === 'undefined') {
|
||||
globalThis.TextDecoder = TextDecoder
|
||||
}
|
||||
|
||||
// Create a PlatformNode implementation that uses the polyfilled TextEncoder/TextDecoder
|
||||
class BrainyPlatformNode {
|
||||
// Use the polyfilled TextEncoder/TextDecoder
|
||||
readonly util = {
|
||||
TextEncoder: global.TextEncoder,
|
||||
TextDecoder: global.TextDecoder,
|
||||
|
||||
// Add utility functions that TensorFlow.js might need
|
||||
isTypedArray: (arr: any): boolean => {
|
||||
return ArrayBuffer.isView(arr) && !(arr instanceof DataView)
|
||||
},
|
||||
|
||||
isFloat32Array: (arr: any): boolean => {
|
||||
return (
|
||||
arr instanceof Float32Array ||
|
||||
(arr &&
|
||||
Object.prototype.toString.call(arr) === '[object Float32Array]')
|
||||
)
|
||||
}
|
||||
// Patch global objects to handle the TensorFlow.js constructor issue
|
||||
// This is needed because TF accesses TextEncoder/TextDecoder as constructors via this.util
|
||||
if (typeof global !== 'undefined') {
|
||||
if (!global.TextEncoder) {
|
||||
global.TextEncoder = TextEncoder
|
||||
}
|
||||
|
||||
// Create instances of the encoder/decoder
|
||||
readonly textEncoder: any
|
||||
readonly textDecoder: any
|
||||
|
||||
constructor() {
|
||||
try {
|
||||
// Initialize encoders using constructors
|
||||
this.textEncoder = new global.TextEncoder()
|
||||
this.textDecoder = new global.TextDecoder()
|
||||
} catch (e) {
|
||||
console.warn(
|
||||
'Brainy: Error creating TextEncoder/TextDecoder instances:',
|
||||
e
|
||||
)
|
||||
// Provide fallback implementations if instantiation fails
|
||||
this.textEncoder = {
|
||||
encode: (input: string): Uint8Array => {
|
||||
const buf = Buffer.from(input, 'utf8')
|
||||
return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength)
|
||||
}
|
||||
}
|
||||
this.textDecoder = {
|
||||
decode: (input?: Uint8Array): string => {
|
||||
if (!input) return ''
|
||||
return Buffer.from(
|
||||
input.buffer,
|
||||
input.byteOffset,
|
||||
input.byteLength
|
||||
).toString('utf8')
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!global.TextDecoder) {
|
||||
global.TextDecoder = TextDecoder
|
||||
}
|
||||
// Also set the special global constructors that TensorFlow can use safely
|
||||
global.__TextEncoder__ = TextEncoder
|
||||
global.__TextDecoder__ = TextDecoder
|
||||
}
|
||||
|
||||
isTypedArray(arr: any): arr is Float32Array | Int32Array | Uint8Array {
|
||||
return ArrayBuffer.isView(arr) && !(arr instanceof DataView)
|
||||
}
|
||||
|
||||
isFloat32Array(arr: any): arr is Float32Array {
|
||||
return (
|
||||
arr instanceof Float32Array ||
|
||||
(arr &&
|
||||
Object.prototype.toString.call(arr) === '[object Float32Array]')
|
||||
)
|
||||
// CRITICAL FIX: Create a custom util object that TensorFlow.js can use
|
||||
// We'll make this available globally so TensorFlow.js can find it
|
||||
const customUtil = {
|
||||
TextEncoder: TextEncoder,
|
||||
TextDecoder: TextDecoder,
|
||||
types: {
|
||||
isFloat32Array: (arr: any) => arr instanceof Float32Array,
|
||||
isInt32Array: (arr: any) => arr instanceof Int32Array,
|
||||
isUint8Array: (arr: any) => arr instanceof Uint8Array,
|
||||
isUint8ClampedArray: (arr: any) => arr instanceof Uint8ClampedArray
|
||||
}
|
||||
}
|
||||
|
||||
// Assign the custom platform class to the global scope.
|
||||
// TensorFlow.js specifically looks for `PlatformNode`.
|
||||
global.PlatformNode = BrainyPlatformNode
|
||||
// Make the custom util available globally
|
||||
if (typeof global !== 'undefined') {
|
||||
global.__brainy_util__ = customUtil
|
||||
}
|
||||
|
||||
// Also create an instance and assign it to global.platformNode (lowercase p)
|
||||
// This is needed for some TensorFlow.js versions
|
||||
global.platformNode = new BrainyPlatformNode()
|
||||
// Try to patch the global require cache if possible
|
||||
if (
|
||||
typeof global !== 'undefined' &&
|
||||
global.require &&
|
||||
global.require.cache
|
||||
) {
|
||||
// Find the util module in the cache and patch it
|
||||
for (const key in global.require.cache) {
|
||||
if (key.endsWith('/util.js') || key === 'util') {
|
||||
const utilModule = global.require.cache[key]
|
||||
if (utilModule && utilModule.exports) {
|
||||
Object.assign(utilModule.exports, customUtil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set up a global flag to track TensorFlow.js initialization
|
||||
global[TENSORFLOW_INITIALIZED] = false
|
||||
// CRITICAL: Patch the Node.js util module directly
|
||||
try {
|
||||
const util = require('util')
|
||||
// Ensure TextEncoder and TextDecoder are available as constructors
|
||||
util.TextEncoder = TextEncoder as typeof util.TextEncoder
|
||||
util.TextDecoder = TextDecoder as typeof util.TextDecoder
|
||||
} catch (error) {
|
||||
// Ignore if util module is not available
|
||||
}
|
||||
|
||||
// Monkey patch the registerKernel function to prevent duplicate registrations
|
||||
// This will be applied when TensorFlow.js is imported
|
||||
const originalRegisterKernel = global.registerKernel
|
||||
if (!originalRegisterKernel) {
|
||||
// Set up a handler to intercept the registerKernel function when it's defined
|
||||
Object.defineProperty(global, 'registerKernel', {
|
||||
set: function (newRegisterKernel) {
|
||||
// Replace the setter with our patched version
|
||||
Object.defineProperty(global, 'registerKernel', {
|
||||
value: function (kernel: any) {
|
||||
// Check if this kernel is already registered
|
||||
const kernelName = kernel.kernelName
|
||||
const backendName = kernel.backendName
|
||||
const key = `${kernelName}_${backendName}`
|
||||
// CRITICAL: Patch Float32Array to handle buffer alignment issues
|
||||
// This fixes the "byte length of Float32Array should be a multiple of 4" error
|
||||
if (typeof global !== 'undefined') {
|
||||
const originalFloat32Array = global.Float32Array
|
||||
|
||||
// Use a global registry to track registered kernels
|
||||
if (!global.__REGISTERED_KERNELS__) {
|
||||
global.__REGISTERED_KERNELS__ = new Set()
|
||||
}
|
||||
global.Float32Array = class extends originalFloat32Array {
|
||||
constructor(arg?: any, byteOffset?: number, length?: number) {
|
||||
if (arg instanceof ArrayBuffer) {
|
||||
// Ensure buffer is properly aligned for Float32Array (multiple of 4 bytes)
|
||||
const alignedByteOffset = byteOffset || 0
|
||||
const alignedLength =
|
||||
length !== undefined
|
||||
? length
|
||||
: (arg.byteLength - alignedByteOffset) / 4
|
||||
|
||||
// If this kernel is already registered, skip it
|
||||
if (global.__REGISTERED_KERNELS__.has(key)) {
|
||||
return
|
||||
}
|
||||
// Check if the buffer slice is properly aligned
|
||||
if (
|
||||
(arg.byteLength - alignedByteOffset) % 4 !== 0 &&
|
||||
length === undefined
|
||||
) {
|
||||
// Create a new aligned buffer if the original isn't properly aligned
|
||||
const alignedByteLength =
|
||||
Math.floor((arg.byteLength - alignedByteOffset) / 4) * 4
|
||||
const alignedBuffer = new ArrayBuffer(alignedByteLength)
|
||||
const sourceView = new Uint8Array(
|
||||
arg,
|
||||
alignedByteOffset,
|
||||
alignedByteLength
|
||||
)
|
||||
const targetView = new Uint8Array(alignedBuffer)
|
||||
targetView.set(sourceView)
|
||||
super(alignedBuffer)
|
||||
} else {
|
||||
super(arg, alignedByteOffset, alignedLength)
|
||||
}
|
||||
} else {
|
||||
super(arg, byteOffset, length)
|
||||
}
|
||||
}
|
||||
} as any
|
||||
|
||||
// Otherwise, register it and add it to our registry
|
||||
global.__REGISTERED_KERNELS__.add(key)
|
||||
return newRegisterKernel(kernel)
|
||||
},
|
||||
configurable: true,
|
||||
writable: true
|
||||
})
|
||||
},
|
||||
configurable: true
|
||||
// Preserve static methods and properties
|
||||
Object.setPrototypeOf(global.Float32Array, originalFloat32Array)
|
||||
Object.defineProperty(global.Float32Array, 'name', {
|
||||
value: 'Float32Array'
|
||||
})
|
||||
Object.defineProperty(global.Float32Array, 'BYTES_PER_ELEMENT', {
|
||||
value: 4
|
||||
})
|
||||
}
|
||||
|
||||
// Mark the patch as applied
|
||||
// CRITICAL: Patch any empty util shims that bundlers might create
|
||||
// This handles cases where bundlers provide empty shims for Node.js modules
|
||||
if (typeof global !== 'undefined') {
|
||||
// Look for common patterns of util shims in bundled code
|
||||
const checkAndPatchUtilShim = (obj: any) => {
|
||||
if (obj && typeof obj === 'object' && !obj.TextEncoder) {
|
||||
obj.TextEncoder = TextEncoder
|
||||
obj.TextDecoder = TextDecoder
|
||||
obj.types = obj.types || {
|
||||
isFloat32Array: (arr: any) => arr instanceof Float32Array,
|
||||
isInt32Array: (arr: any) => arr instanceof Int32Array,
|
||||
isUint8Array: (arr: any) => arr instanceof Uint8Array,
|
||||
isUint8ClampedArray: (arr: any) => arr instanceof Uint8ClampedArray
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Patch any existing util-like objects in global scope
|
||||
if (global._utilShim) {
|
||||
checkAndPatchUtilShim(global._utilShim)
|
||||
}
|
||||
|
||||
// CRITICAL: Patch the bundled util shim directly
|
||||
// In bundled code, there's often a _utilShim object that needs patching
|
||||
if (
|
||||
typeof globalThis !== 'undefined' &&
|
||||
(globalThis as GlobalThis)._utilShim
|
||||
) {
|
||||
checkAndPatchUtilShim((globalThis as GlobalThis)._utilShim)
|
||||
}
|
||||
|
||||
// CRITICAL: Create and patch a global _utilShim if it doesn't exist
|
||||
// This ensures the bundled code will find the patched version
|
||||
if (!global._utilShim) {
|
||||
global._utilShim = {
|
||||
TextEncoder: TextEncoder,
|
||||
TextDecoder: TextDecoder,
|
||||
types: {
|
||||
isFloat32Array: (arr: any) => arr instanceof Float32Array,
|
||||
isInt32Array: (arr: any) => arr instanceof Int32Array,
|
||||
isUint8Array: (arr: any) => arr instanceof Uint8Array,
|
||||
isUint8ClampedArray: (arr: any) => arr instanceof Uint8ClampedArray
|
||||
}
|
||||
}
|
||||
} else {
|
||||
checkAndPatchUtilShim(global._utilShim)
|
||||
}
|
||||
|
||||
// Also ensure it's available on globalThis
|
||||
if (
|
||||
typeof globalThis !== 'undefined' &&
|
||||
!(globalThis as GlobalThis)._utilShim
|
||||
) {
|
||||
;(globalThis as GlobalThis)._utilShim = global._utilShim
|
||||
}
|
||||
|
||||
// Set up a property descriptor to catch util shim assignments
|
||||
try {
|
||||
Object.defineProperty(global, '_utilShim', {
|
||||
get() {
|
||||
return this.__utilShim || {}
|
||||
},
|
||||
set(value) {
|
||||
checkAndPatchUtilShim(value)
|
||||
this.__utilShim = value
|
||||
},
|
||||
configurable: true
|
||||
})
|
||||
} catch (e) {
|
||||
// Ignore if property can't be defined
|
||||
}
|
||||
|
||||
// Also set up property descriptor on globalThis
|
||||
try {
|
||||
Object.defineProperty(globalThis, '_utilShim', {
|
||||
get() {
|
||||
return this.__utilShim || {}
|
||||
},
|
||||
set(value) {
|
||||
checkAndPatchUtilShim(value)
|
||||
this.__utilShim = value
|
||||
},
|
||||
configurable: true
|
||||
})
|
||||
} catch (e) {
|
||||
// Ignore if property can't be defined
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
'Brainy: Successfully patched TensorFlow.js PlatformNode at module load time'
|
||||
)
|
||||
patchApplied = true
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
'Brainy: Failed to apply early TensorFlow.js platform patch:',
|
||||
error
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the TensorFlow.js platform patch if it hasn't been applied already
|
||||
* This is a safety measure in case the module-level patch didn't run
|
||||
* Now works across all environments: browser, Node.js, and serverless/server
|
||||
*/
|
||||
export async function applyTensorFlowPatch(): Promise<void> {
|
||||
// Apply patches for all non-browser environments that might need TensorFlow.js compatibility
|
||||
// This includes Node.js, serverless environments, and other server environments
|
||||
const isBrowserEnv = typeof window !== 'undefined' && typeof document !== 'undefined'
|
||||
if (isBrowserEnv) {
|
||||
return // Browser environments don't need these patches
|
||||
}
|
||||
|
||||
// Get the appropriate global object for the current environment
|
||||
const globalObj = (() => {
|
||||
if (typeof globalThis !== 'undefined') return globalThis
|
||||
if (typeof global !== 'undefined') return global
|
||||
if (typeof self !== 'undefined') return self
|
||||
return {} as any // Fallback for unknown environments
|
||||
})()
|
||||
|
||||
// Check if the critical globals exist, not just the flag
|
||||
// This allows re-patching if globals have been deleted
|
||||
const needsPatch = !patchApplied ||
|
||||
typeof globalObj.__TextEncoder__ === 'undefined' ||
|
||||
typeof globalObj.__TextDecoder__ === 'undefined'
|
||||
|
||||
if (!needsPatch) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(
|
||||
'Brainy: Applying TensorFlow.js platform patch via function call'
|
||||
)
|
||||
|
||||
// CRITICAL FIX: Patch the global environment to ensure TextEncoder/TextDecoder are available
|
||||
// This approach works by ensuring the global constructors are available before TensorFlow.js loads
|
||||
// Now works across all environments: Node.js, serverless, and other server environments
|
||||
|
||||
// Make sure TextEncoder and TextDecoder are available globally
|
||||
if (!globalObj.TextEncoder) {
|
||||
globalObj.TextEncoder = TextEncoder
|
||||
}
|
||||
if (!globalObj.TextDecoder) {
|
||||
globalObj.TextDecoder = TextDecoder
|
||||
}
|
||||
|
||||
// Also set the special global constructors that TensorFlow can use safely
|
||||
;(globalObj as any).__TextEncoder__ = TextEncoder
|
||||
;(globalObj as any).__TextDecoder__ = TextDecoder
|
||||
|
||||
// Also patch process.versions to ensure TensorFlow.js detects Node.js correctly
|
||||
if (typeof process !== 'undefined' && process.versions) {
|
||||
// Ensure TensorFlow.js sees this as a Node.js environment
|
||||
if (!process.versions.node) {
|
||||
process.versions.node = process.version
|
||||
}
|
||||
}
|
||||
|
||||
// CRITICAL: Patch the Node.js util module directly
|
||||
try {
|
||||
const util = await import('util')
|
||||
// Ensure TextEncoder and TextDecoder are available as constructors
|
||||
util.TextEncoder = TextEncoder as typeof util.TextEncoder
|
||||
util.TextDecoder = TextDecoder as typeof util.TextDecoder
|
||||
} catch (error) {
|
||||
// Ignore if util module is not available
|
||||
}
|
||||
|
||||
patchApplied = true
|
||||
console.log('Brainy: Successfully applied TensorFlow.js platform patch')
|
||||
} catch (error) {
|
||||
console.warn('Brainy: Failed to apply TensorFlow.js platform patch:', error)
|
||||
}
|
||||
}
|
||||
|
||||
export function getTextEncoder(): TextEncoder {
|
||||
return new TextEncoder()
|
||||
}
|
||||
|
||||
export function getTextDecoder(): TextDecoder {
|
||||
return new TextDecoder()
|
||||
}
|
||||
|
||||
// Apply patch immediately
|
||||
applyTensorFlowPatch().catch((error) => {
|
||||
console.warn('Failed to apply TensorFlow patch at module load:', error)
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue