**feat(models): enhance loader reliability and compatibility**

- **Compatibility Enhancements**:
  - Added support to detect and inject missing `"format"` field in `model.json` files for TensorFlow.js compatibility.
  - Modified model loading logic to handle both `tfjs-graph-model` and `tfjs-layers-model` formats.

- **New Features**:
  - Introduced additional fallback paths for locating models to increase reliability in varying environments.
  - Added support for mock implementations of the Universal Sentence Encoder in test environments.

- **Bug Fixes**:
  - Fixed module loading resolution in `FileSystemStorage` with improved initialization and error handling for Node.js environments.
  - Resolved issues with test assertions to improve validation logic in core tests.

**Purpose**: Improve model loading reliability, expand compatibility with TensorFlow.js models, and enhance test environment support.
This commit is contained in:
David Snelling 2025-08-01 18:31:37 -07:00
parent 58091a0015
commit 38c28ae038
6 changed files with 566 additions and 384 deletions

View file

@ -5,11 +5,11 @@
import { EmbeddingFunction, EmbeddingModel, Vector } from '../coreTypes.js'
import { executeInThread } from './workerUtils.js'
import { isBrowser } from './environment.js'
import {
RobustModelLoader,
ModelLoadOptions,
import {
RobustModelLoader,
ModelLoadOptions,
createRobustModelLoader,
getUniversalSentenceEncoderFallbacks
getUniversalSentenceEncoderFallbacks
} from './robustModelLoader.js'
/**
@ -40,7 +40,7 @@ export class UniversalSentenceEncoder implements EmbeddingModel {
*/
constructor(options: UniversalSentenceEncoderOptions = {}) {
this.verbose = options.verbose !== undefined ? options.verbose : true
// Create robust model loader with enhanced reliability features
this.robustLoader = createRobustModelLoader({
maxRetries: options.maxRetries ?? 3,
@ -48,7 +48,8 @@ export class UniversalSentenceEncoder implements EmbeddingModel {
maxRetryDelay: options.maxRetryDelay ?? 30000,
timeout: options.timeout ?? 60000,
useExponentialBackoff: options.useExponentialBackoff ?? true,
fallbackUrls: options.fallbackUrls ?? getUniversalSentenceEncoderFallbacks(),
fallbackUrls:
options.fallbackUrls ?? getUniversalSentenceEncoderFallbacks(),
verbose: this.verbose,
preferLocalModel: options.preferLocalModel ?? true
})
@ -146,28 +147,34 @@ export class UniversalSentenceEncoder implements EmbeddingModel {
private async loadModelFromLocal(
loadFunction: () => Promise<EmbeddingModel>
): Promise<EmbeddingModel> {
this.logger('log', 'Loading Universal Sentence Encoder model with robust loader...')
this.logger(
'log',
'Loading Universal Sentence Encoder model with robust loader...'
)
try {
// Use the robust model loader to handle all retry logic, timeouts, and fallbacks
const model = await this.robustLoader.loadModel(
loadFunction,
'universal-sentence-encoder'
)
this.logger('log', 'Successfully loaded Universal Sentence Encoder model')
return model
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
this.logger('error', `Failed to load Universal Sentence Encoder model: ${errorMessage}`)
const errorMessage =
error instanceof Error ? error.message : String(error)
this.logger(
'error',
`Failed to load Universal Sentence Encoder model: ${errorMessage}`
)
// Log loading statistics for debugging
const stats = this.robustLoader.getLoadingStats()
if (Object.keys(stats).length > 0) {
this.logger('log', 'Loading attempt statistics:', stats)
}
throw error
}
}
@ -176,6 +183,31 @@ export class UniversalSentenceEncoder implements EmbeddingModel {
* Initialize the embedding model
*/
public async init(): Promise<void> {
// Use a mock implementation in test environments
if (this.isTestEnvironment()) {
this.logger('log', 'Using mock Universal Sentence Encoder for tests')
// Create a mock model that returns fixed embeddings
this.model = {
embed: async (sentences: string | string[]) => {
// Create a tensor-like object with a mock array method
return {
array: async () => {
// Return fixed embeddings for each input sentence
const inputArray = Array.isArray(sentences)
? sentences
: [sentences]
return inputArray.map(() =>
new Array(512).fill(0).map((_, i) => (i % 2 === 0 ? 0.1 : -0.1))
)
},
dispose: () => {}
}
}
}
this.initialized = true
return
}
try {
// Save original console.warn
const originalWarn = console.warn

View file

@ -11,6 +11,10 @@
import { EmbeddingModel } from '../coreTypes.js'
// Import the findUSELoadFunction from embedding.ts
// We need to access it directly since it's not exported
// For now, we'll implement a similar function locally
export interface ModelLoadOptions {
/** Maximum number of retry attempts */
maxRetries?: number
@ -181,6 +185,9 @@ export class RobustModelLoader {
// Look for bundled model in multiple possible locations
const possiblePaths = [
path.join(__dirname, '..', '..', 'models', 'bundled', 'universal-sentence-encoder'),
path.join(__dirname, '..', '..', 'brainy-models-package', 'models', 'universal-sentence-encoder'),
path.join(process.cwd(), 'brainy-models-package', 'models', 'universal-sentence-encoder'),
path.join(__dirname, '..', '..', 'node_modules', '@soulcraft', 'brainy-models', 'models', 'universal-sentence-encoder'),
path.join(__dirname, '..', '..', 'node_modules', '@soulcraft', 'brainy-models', 'universal-sentence-encoder'),
path.join(process.cwd(), 'node_modules', '@soulcraft', 'brainy-models', 'universal-sentence-encoder')
]
@ -192,7 +199,31 @@ export class RobustModelLoader {
// Load TensorFlow.js if not already loaded
const tf = await import('@tensorflow/tfjs')
const model = await tf.loadLayersModel(`file://${modelJsonPath}`)
// Read the model.json to check the format
const modelJsonContent = JSON.parse(fs.readFileSync(modelJsonPath, 'utf8'))
// Ensure the format field exists for TensorFlow.js compatibility
if (!modelJsonContent.format) {
modelJsonContent.format = 'tfjs-graph-model'
try {
fs.writeFileSync(modelJsonPath, JSON.stringify(modelJsonContent, null, 2))
this.log(`✅ Added missing "format" field to model.json for TensorFlow.js compatibility`)
} catch (writeError) {
this.log(`⚠️ Could not write format field to model.json: ${writeError}`)
}
}
const modelFormat = modelJsonContent.format || 'tfjs-graph-model'
let model
if (modelFormat === 'tfjs-graph-model') {
// Use loadGraphModel for graph models
model = await tf.loadGraphModel(`file://${modelJsonPath}`)
} else {
// Use loadLayersModel for layers models (default)
model = await tf.loadLayersModel(`file://${modelJsonPath}`)
}
// Return a wrapper that matches the Universal Sentence Encoder interface
return this.createModelWrapper(model)