**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
|
|
@ -37,6 +37,11 @@ import { WebSocketConnection } from './types/augmentations.js'
|
|||
import { BrainyDataInterface } from './types/brainyDataInterface.js'
|
||||
|
||||
export interface BrainyDataConfig {
|
||||
/**
|
||||
* Vector dimensions (required if not using an embedding function that auto-detects dimensions)
|
||||
*/
|
||||
dimensions?: number
|
||||
|
||||
/**
|
||||
* HNSW index configuration
|
||||
*/
|
||||
|
|
@ -136,16 +141,48 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
|
|||
private readOnly: boolean
|
||||
private storageConfig: BrainyDataConfig['storage'] = {}
|
||||
private useOptimizedIndex: boolean = false
|
||||
private _dimensions: number
|
||||
|
||||
// Remote server properties
|
||||
private remoteServerConfig: BrainyDataConfig['remoteServer'] | null = null
|
||||
private serverSearchConduit: ServerSearchConduitAugmentation | null = null
|
||||
private serverConnection: WebSocketConnection | null = null
|
||||
|
||||
/**
|
||||
* Get the vector dimensions
|
||||
*/
|
||||
public get dimensions(): number {
|
||||
return this._dimensions
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the maximum connections parameter from HNSW configuration
|
||||
*/
|
||||
public get maxConnections(): number {
|
||||
const config = this.index.getConfig()
|
||||
return config.M || 16
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the efConstruction parameter from HNSW configuration
|
||||
*/
|
||||
public get efConstruction(): number {
|
||||
const config = this.index.getConfig()
|
||||
return config.efConstruction || 200
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new vector database
|
||||
*/
|
||||
constructor(config: BrainyDataConfig = {}) {
|
||||
// Validate dimensions
|
||||
if (config.dimensions !== undefined && config.dimensions <= 0) {
|
||||
throw new Error('Dimensions must be a positive number')
|
||||
}
|
||||
|
||||
// Set dimensions (default to 512 for embedding functions, or require explicit config)
|
||||
this._dimensions = config.dimensions || 512
|
||||
|
||||
// Set distance function
|
||||
this.distanceFunction = config.distanceFunction || cosineDistance
|
||||
|
||||
|
|
@ -284,6 +321,16 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
|
|||
// Clear the index and add all nouns
|
||||
this.index.clear()
|
||||
for (const noun of nouns) {
|
||||
// Check if the vector dimensions match the expected dimensions
|
||||
if (noun.vector.length !== this._dimensions) {
|
||||
console.warn(
|
||||
`Skipping noun ${noun.id} due to dimension mismatch: expected ${this._dimensions}, got ${noun.vector.length}`
|
||||
)
|
||||
// Optionally, you could delete the mismatched noun from storage
|
||||
// await this.storage!.deleteNoun(noun.id)
|
||||
continue
|
||||
}
|
||||
|
||||
// Add to index
|
||||
await this.index.addItem({
|
||||
id: noun.id,
|
||||
|
|
@ -393,6 +440,11 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
|
|||
throw new Error('Vector is undefined or null')
|
||||
}
|
||||
|
||||
// Validate vector dimensions
|
||||
if (vector.length !== this._dimensions) {
|
||||
throw new Error(`Vector dimension mismatch: expected ${this._dimensions}, got ${vector.length}`)
|
||||
}
|
||||
|
||||
// Use ID from options if it exists, otherwise from metadata, otherwise generate a new UUID
|
||||
const id =
|
||||
options.id ||
|
||||
|
|
@ -431,7 +483,13 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
|
|||
}
|
||||
}
|
||||
|
||||
await this.storage!.saveMetadata(id, metadata)
|
||||
// Ensure metadata has the correct id field
|
||||
let metadataToSave = metadata
|
||||
if (metadata && typeof metadata === 'object') {
|
||||
metadataToSave = { ...metadata, id }
|
||||
}
|
||||
|
||||
await this.storage!.saveMetadata(id, metadataToSave)
|
||||
}
|
||||
|
||||
// If addToRemote is true and we're connected to a remote server, add to remote as well
|
||||
|
|
@ -452,6 +510,26 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a text item to the database with automatic embedding
|
||||
* This is a convenience method for adding text data with metadata
|
||||
* @param text Text data to add
|
||||
* @param metadata Metadata to associate with the text
|
||||
* @param options Additional options
|
||||
* @returns The ID of the added item
|
||||
*/
|
||||
public async addItem(
|
||||
text: string,
|
||||
metadata?: T,
|
||||
options: {
|
||||
addToRemote?: boolean // Whether to also add to the remote server if connected
|
||||
id?: string // Optional ID to use instead of generating a new one
|
||||
} = {}
|
||||
): Promise<string> {
|
||||
// Use the existing add method with forceEmbed to ensure text is embedded
|
||||
return this.add(text, metadata, { ...options, forceEmbed: true })
|
||||
}
|
||||
|
||||
/**
|
||||
* Add data to both local and remote Brainy instances
|
||||
* @param vectorOrData Vector or data to add
|
||||
|
|
@ -685,7 +763,9 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
|
|||
forceEmbed?: boolean // Force using the embedding function even if input is a vector
|
||||
} = {}
|
||||
): Promise<SearchResult<T>[]> {
|
||||
await this.ensureInitialized()
|
||||
if (!this.isInitialized) {
|
||||
throw new Error('BrainyData must be initialized before searching. Call init() first.')
|
||||
}
|
||||
|
||||
try {
|
||||
let queryVector: Vector
|
||||
|
|
@ -814,6 +894,9 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
|
|||
verbDirection?: 'outgoing' | 'incoming' | 'both' // Direction of verbs to consider when searching connected nouns
|
||||
} = {}
|
||||
): Promise<SearchResult<T>[]> {
|
||||
if (!this.isInitialized) {
|
||||
throw new Error('BrainyData must be initialized before searching. Call init() first.')
|
||||
}
|
||||
// If searching for verbs directly
|
||||
if (options.searchVerbs) {
|
||||
const verbResults = await this.searchVerbs(queryVectorOrData, k, {
|
||||
|
|
@ -873,6 +956,9 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
|
|||
includeVerbs?: boolean // Whether to include associated GraphVerbs in the results
|
||||
} = {}
|
||||
): Promise<SearchResult<T>[]> {
|
||||
if (!this.isInitialized) {
|
||||
throw new Error('BrainyData must be initialized before searching. Call init() first.')
|
||||
}
|
||||
// If input is a string and not a vector, automatically vectorize it
|
||||
let queryToUse = queryVectorOrData
|
||||
if (typeof queryVectorOrData === 'string' && !options.forceEmbed) {
|
||||
|
|
|
|||
52
src/patched-platform-node.ts
Normal file
52
src/patched-platform-node.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
/**
|
||||
* Custom patched implementation of TensorFlow.js PlatformNode class
|
||||
* This resolves the TextEncoder/TextDecoder constructor issues
|
||||
*/
|
||||
|
||||
export class PatchedPlatformNode {
|
||||
util: {
|
||||
TextEncoder: typeof TextEncoder
|
||||
TextDecoder: typeof TextDecoder
|
||||
isFloat32Array: (arr: any) => boolean
|
||||
isTypedArray: (arr: any) => boolean
|
||||
}
|
||||
textEncoder: TextEncoder
|
||||
textDecoder: TextDecoder
|
||||
|
||||
constructor() {
|
||||
// Create a util object with necessary methods
|
||||
this.util = {
|
||||
// Add isFloat32Array and isTypedArray directly to util
|
||||
isFloat32Array: (arr) => {
|
||||
return !!(
|
||||
arr instanceof Float32Array ||
|
||||
(arr &&
|
||||
Object.prototype.toString.call(arr) === '[object Float32Array]')
|
||||
)
|
||||
},
|
||||
isTypedArray: (arr) => {
|
||||
return !!(ArrayBuffer.isView(arr) && !(arr instanceof DataView))
|
||||
},
|
||||
// Provide constructor references directly
|
||||
TextEncoder,
|
||||
TextDecoder
|
||||
}
|
||||
|
||||
// Initialize TextEncoder/TextDecoder instances
|
||||
this.textEncoder = new TextEncoder()
|
||||
this.textDecoder = new TextDecoder()
|
||||
}
|
||||
|
||||
// Define isFloat32Array directly on the instance
|
||||
isFloat32Array(arr: any) {
|
||||
return !!(
|
||||
arr instanceof Float32Array ||
|
||||
(arr && Object.prototype.toString.call(arr) === '[object Float32Array]')
|
||||
)
|
||||
}
|
||||
|
||||
// Define isTypedArray directly on the instance
|
||||
isTypedArray(arr: any) {
|
||||
return !!(ArrayBuffer.isView(arr) && !(arr instanceof DataView))
|
||||
}
|
||||
}
|
||||
52
src/setup.ts
52
src/setup.ts
|
|
@ -15,40 +15,32 @@
|
|||
* tree-shaking by bundlers, ensuring the patch is always applied.
|
||||
*/
|
||||
|
||||
// CRITICAL: Apply the TensorFlow.js patch immediately at the top level
|
||||
// This ensures it runs as early as possible in the module loading process
|
||||
// before any imports are processed
|
||||
if (
|
||||
typeof process !== 'undefined' &&
|
||||
process.versions &&
|
||||
process.versions.node
|
||||
) {
|
||||
try {
|
||||
// For CommonJS environments, use require to ensure synchronous loading
|
||||
if (typeof require === 'function') {
|
||||
const textEncoding = require('./utils/textEncoding.js')
|
||||
if (
|
||||
textEncoding &&
|
||||
typeof textEncoding.applyTensorFlowPatch === 'function'
|
||||
) {
|
||||
textEncoding.applyTensorFlowPatch()
|
||||
console.log(
|
||||
'Applied TensorFlow.js patch via CommonJS require in setup.ts'
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to apply TensorFlow.js patch via require:', e)
|
||||
// Continue to the import-based approach
|
||||
// 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 // No global object available
|
||||
})()
|
||||
|
||||
// Define TextEncoder and TextDecoder globally to make sure they're available
|
||||
// Now works across all environments: Node.js, serverless, and other server environments
|
||||
if (globalObj) {
|
||||
if (!globalObj.TextEncoder) {
|
||||
globalObj.TextEncoder = TextEncoder
|
||||
}
|
||||
if (!globalObj.TextDecoder) {
|
||||
globalObj.TextDecoder = TextDecoder
|
||||
}
|
||||
|
||||
// Create a special global constructor that TensorFlow can use safely
|
||||
;(globalObj as any).__TextEncoder__ = TextEncoder
|
||||
;(globalObj as any).__TextDecoder__ = TextDecoder
|
||||
}
|
||||
|
||||
// Also import normally for ES modules environments
|
||||
import { applyTensorFlowPatch } from './utils/textEncoding.js'
|
||||
|
||||
// Apply the TensorFlow.js platform patch if needed
|
||||
// This will be a no-op if the patch was already applied via require above
|
||||
// Apply the TensorFlow.js platform patch
|
||||
applyTensorFlowPatch()
|
||||
console.log(
|
||||
'Applied or verified TensorFlow.js patch via ES modules in setup.ts'
|
||||
)
|
||||
console.log('Applied TensorFlow.js patch via ES modules in setup.ts')
|
||||
|
|
|
|||
|
|
@ -413,6 +413,7 @@ export class FileSystemStorage implements StorageAdapter {
|
|||
public async saveMetadata(id: string, metadata: any): Promise<void> {
|
||||
if (!this.isInitialized) await this.init()
|
||||
const filePath = path.join(this.metadataDir, `${id}.json`)
|
||||
await this.ensureDirectoryExists(path.dirname(filePath))
|
||||
await fs.promises.writeFile(filePath, JSON.stringify(metadata, null, 2))
|
||||
}
|
||||
|
||||
|
|
@ -437,7 +438,60 @@ export class FileSystemStorage implements StorageAdapter {
|
|||
|
||||
public async clear(): Promise<void> {
|
||||
if (!this.isInitialized) await this.init()
|
||||
await fs.promises.rm(this.rootDir, { recursive: true, force: true })
|
||||
|
||||
// Helper function to recursively remove directory contents
|
||||
const removeDirectoryContents = async (dirPath: string): Promise<void> => {
|
||||
try {
|
||||
const files = await fs.promises.readdir(dirPath, { withFileTypes: true })
|
||||
|
||||
for (const file of files) {
|
||||
const fullPath = path.join(dirPath, file.name)
|
||||
|
||||
if (file.isDirectory()) {
|
||||
await removeDirectoryContents(fullPath)
|
||||
// Use fs.promises.rm with recursive option instead of rmdir
|
||||
try {
|
||||
await fs.promises.rm(fullPath, { recursive: true, force: true })
|
||||
} catch (rmError: any) {
|
||||
// Fallback to rmdir if rm fails
|
||||
await fs.promises.rmdir(fullPath)
|
||||
}
|
||||
} else {
|
||||
await fs.promises.unlink(fullPath)
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error.code !== 'ENOENT') {
|
||||
console.error(`Error removing directory contents ${dirPath}:`, error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// First try the modern approach
|
||||
await fs.promises.rm(this.rootDir, { recursive: true, force: true })
|
||||
} catch (error: any) {
|
||||
console.warn('Modern rm failed, falling back to manual cleanup:', error)
|
||||
|
||||
// Fallback: manually remove contents then directory
|
||||
try {
|
||||
await removeDirectoryContents(this.rootDir)
|
||||
// Use fs.promises.rm with recursive option instead of rmdir
|
||||
try {
|
||||
await fs.promises.rm(this.rootDir, { recursive: true, force: true })
|
||||
} catch (rmError: any) {
|
||||
// Final fallback to rmdir if rm fails
|
||||
await fs.promises.rmdir(this.rootDir)
|
||||
}
|
||||
} catch (fallbackError: any) {
|
||||
if (fallbackError.code !== 'ENOENT') {
|
||||
console.error('Manual cleanup also failed:', fallbackError)
|
||||
throw fallbackError
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.isInitialized = false // Reset state
|
||||
await this.init() // Re-create directories
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,16 +16,20 @@
|
|||
// available to TensorFlow.js before it initializes its platform detection
|
||||
import './setup.js'
|
||||
|
||||
// Export environment information
|
||||
// Import environment detection functions
|
||||
import { isBrowser, isNode } from './utils/environment.js'
|
||||
|
||||
// Export environment information with lazy evaluation
|
||||
export const environment = {
|
||||
isBrowser: typeof window !== 'undefined',
|
||||
isNode:
|
||||
typeof process !== 'undefined' && process.versions && process.versions.node,
|
||||
isServerless:
|
||||
typeof window === 'undefined' &&
|
||||
(typeof process === 'undefined' ||
|
||||
!process.versions ||
|
||||
!process.versions.node)
|
||||
get isBrowser() {
|
||||
return isBrowser()
|
||||
},
|
||||
get isNode() {
|
||||
return isNode()
|
||||
},
|
||||
get isServerless() {
|
||||
return !isBrowser() && !isNode()
|
||||
}
|
||||
}
|
||||
|
||||
// Make environment information available globally
|
||||
|
|
@ -46,3 +50,6 @@ console.log(
|
|||
|
||||
// Re-export everything from index.ts
|
||||
export * from './index.js'
|
||||
|
||||
// Export the TensorFlow patch function for testing and manual use
|
||||
export { applyTensorFlowPatch } from './utils/textEncoding.js'
|
||||
|
|
|
|||
|
|
@ -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