2025-09-02 10:00:52 -07:00
/ * *
* Unified Embedding Manager
2025-12-17 17:42:37 -08:00
*
2025-09-02 10:00:52 -07:00
* THE single source of truth for all embedding operations in Brainy .
2026-01-06 12:52:34 -08:00
* Uses Candle WASM inference for universal compatibility .
2025-12-17 17:42:37 -08:00
*
2025-09-02 10:00:52 -07:00
* Features :
* - Singleton pattern ensures ONE model instance
2026-01-06 12:52:34 -08:00
* - Candle WASM ( no transformers . js or ONNX Runtime dependency )
2025-12-17 17:42:37 -08:00
* - Bundled model ( no runtime downloads )
* - Works everywhere : Node.js , Bun , Bun -- compile , browsers
2025-09-02 10:00:52 -07:00
* - Memory monitoring
* /
import { Vector , EmbeddingFunction } from '../coreTypes.js'
2025-12-17 17:42:37 -08:00
import { WASMEmbeddingEngine } from './wasm/index.js'
2025-09-02 10:00:52 -07:00
// Types
export type ModelPrecision = 'q8' | 'fp32'
interface EmbeddingStats {
initialized : boolean
precision : ModelPrecision
modelName : string
embedCount : number
initTime : number | null
memoryMB : number | null
}
// Global state for true singleton across entire process
let globalInstance : EmbeddingManager | null = null
let globalInitPromise : Promise < void > | null = null
/ * *
* Unified Embedding Manager - Clean , simple , reliable
2025-12-17 17:42:37 -08:00
*
2026-01-06 12:52:34 -08:00
* Now powered by Candle WASM for universal compatibility .
2025-09-02 10:00:52 -07:00
* /
export class EmbeddingManager {
2025-12-17 17:42:37 -08:00
private engine : WASMEmbeddingEngine
private precision : ModelPrecision = 'q8'
private modelName = 'all-MiniLM-L6-v2'
2025-09-02 10:00:52 -07:00
private initialized = false
private initTime : number | null = null
private embedCount = 0
private locked = false
2025-12-17 17:42:37 -08:00
2025-09-02 10:00:52 -07:00
private constructor ( ) {
2025-12-17 17:42:37 -08:00
this . engine = WASMEmbeddingEngine . getInstance ( )
console . log ( '🎯 EmbeddingManager: Using Q8 precision (WASM)' )
2025-09-02 10:00:52 -07:00
}
2025-12-17 17:42:37 -08:00
2025-09-02 10:00:52 -07:00
/ * *
* Get the singleton instance
* /
static getInstance ( ) : EmbeddingManager {
if ( ! globalInstance ) {
globalInstance = new EmbeddingManager ( )
}
return globalInstance
}
2025-12-17 17:42:37 -08:00
2025-09-02 10:00:52 -07:00
/ * *
* Initialize the model ( happens once )
* /
async init ( ) : Promise < void > {
// In unit test mode, skip real model initialization
2025-12-17 17:42:37 -08:00
const isTestMode =
process . env . BRAINY_UNIT_TEST === 'true' ||
( globalThis as any ) . __BRAINY_UNIT_TEST__
2025-09-11 16:23:32 -07:00
if ( isTestMode ) {
2025-12-17 17:42:37 -08:00
// Production safeguard
2025-09-11 16:23:32 -07:00
if ( process . env . NODE_ENV === 'production' ) {
throw new Error (
'CRITICAL: Mock embeddings detected in production environment! ' +
'BRAINY_UNIT_TEST or __BRAINY_UNIT_TEST__ is set while NODE_ENV=production. ' +
'This is a security risk. Remove test flags before deploying to production.'
)
}
2025-12-17 17:42:37 -08:00
2025-09-02 10:00:52 -07:00
if ( ! this . initialized ) {
this . initialized = true
this . initTime = 1 // Mock init time
console . log ( '🧪 EmbeddingManager: Using mocked embeddings for unit tests' )
}
return
}
2025-12-17 17:42:37 -08:00
2025-09-02 10:00:52 -07:00
// Already initialized
2025-12-17 17:42:37 -08:00
if ( this . initialized && this . engine . isInitialized ( ) ) {
2025-09-02 10:00:52 -07:00
return
}
2025-12-17 17:42:37 -08:00
2025-09-02 10:00:52 -07:00
// Initialization in progress
if ( globalInitPromise ) {
await globalInitPromise
return
}
2025-12-17 17:42:37 -08:00
2025-09-02 10:00:52 -07:00
// Start initialization
globalInitPromise = this . performInit ( )
2025-12-17 17:42:37 -08:00
2025-09-02 10:00:52 -07:00
try {
await globalInitPromise
} finally {
globalInitPromise = null
}
}
2025-12-17 17:42:37 -08:00
2025-09-02 10:00:52 -07:00
/ * *
* Perform actual initialization
* /
private async performInit ( ) : Promise < void > {
const startTime = Date . now ( )
2025-12-17 17:42:37 -08:00
2025-09-02 10:00:52 -07:00
try {
2025-12-17 17:42:37 -08:00
// Initialize WASM engine (handles all model loading)
await this . engine . initialize ( )
2025-09-02 10:00:52 -07:00
// Lock precision after successful initialization
this . locked = true
this . initialized = true
this . initTime = Date . now ( ) - startTime
2025-12-17 17:42:37 -08:00
2025-09-02 10:00:52 -07:00
// Log success
const memoryMB = this . getMemoryUsage ( )
2025-09-11 16:23:32 -07:00
console . log ( ` 📊 Precision: Q8 | Memory: ${ memoryMB } MB ` )
2025-12-17 17:42:37 -08:00
console . log ( '🔒 Configuration locked' )
2025-09-02 10:00:52 -07:00
} catch ( error ) {
this . initialized = false
2025-12-17 17:42:37 -08:00
throw new Error (
` Failed to initialize embedding model: ${ error instanceof Error ? error.message : String ( error ) } `
)
2025-09-02 10:00:52 -07:00
}
}
2025-12-17 17:42:37 -08:00
2025-09-02 10:00:52 -07:00
/ * *
* Generate embeddings
* /
2025-10-17 12:29:27 -07:00
async embed ( text : string | string [ ] | Record < string , unknown > ) : Promise < Vector > {
2025-12-17 17:42:37 -08:00
// Check for unit test environment
const isTestMode =
process . env . BRAINY_UNIT_TEST === 'true' ||
( globalThis as any ) . __BRAINY_UNIT_TEST__
2025-09-25 10:47:44 -07:00
2025-09-11 16:23:32 -07:00
if ( isTestMode ) {
if ( process . env . NODE_ENV === 'production' ) {
throw new Error ( 'CRITICAL: Mock embeddings in production!' )
}
2025-09-02 10:00:52 -07:00
return this . getMockEmbedding ( text )
}
2025-09-25 10:47:44 -07:00
2025-09-02 10:00:52 -07:00
// Ensure initialized
await this . init ( )
2025-09-25 10:47:44 -07:00
2025-12-17 17:42:37 -08:00
// Normalize input to string
2025-09-25 10:47:44 -07:00
let input : string
if ( Array . isArray ( text ) ) {
2025-12-17 17:42:37 -08:00
input = text . map ( ( t ) = > ( typeof t === 'string' ? t : String ( t ) ) ) . join ( ' ' )
2025-09-25 10:47:44 -07:00
} else if ( typeof text === 'string' ) {
input = text
2025-10-17 12:29:27 -07:00
} else if ( typeof text === 'object' ) {
input = JSON . stringify ( text )
2025-09-25 10:47:44 -07:00
} else {
2025-10-17 12:29:27 -07:00
console . warn ( 'EmbeddingManager.embed received unexpected input type:' , typeof text )
2025-09-25 10:47:44 -07:00
input = String ( text )
}
2025-12-17 17:42:37 -08:00
// Generate embedding using WASM engine
const embedding = await this . engine . embed ( input )
2025-09-02 10:00:52 -07:00
// Validate dimensions
if ( embedding . length !== 384 ) {
console . warn ( ` Unexpected embedding dimension: ${ embedding . length } ` )
if ( embedding . length < 384 ) {
return [ . . . embedding , . . . new Array ( 384 - embedding . length ) . fill ( 0 ) ]
} else {
return embedding . slice ( 0 , 384 )
}
}
2025-12-17 17:42:37 -08:00
2025-09-02 10:00:52 -07:00
this . embedCount ++
return embedding
}
2025-12-17 17:42:37 -08:00
2025-09-02 10:00:52 -07:00
/ * *
* Generate mock embeddings for unit tests
* /
2025-10-17 12:29:27 -07:00
private getMockEmbedding ( text : string | string [ ] | Record < string , unknown > ) : Vector {
2025-09-02 10:00:52 -07:00
const input = Array . isArray ( text ) ? text . join ( ' ' ) : text
const str = typeof input === 'string' ? input : JSON.stringify ( input )
const vector = new Array ( 384 ) . fill ( 0 )
2025-10-17 12:29:27 -07:00
2025-09-02 10:00:52 -07:00
// Create semi-realistic embeddings based on text content
for ( let i = 0 ; i < Math . min ( str . length , 384 ) ; i ++ ) {
vector [ i ] = ( str . charCodeAt ( i % str . length ) % 256 ) / 256
}
2025-10-17 12:29:27 -07:00
2025-09-02 10:00:52 -07:00
// Add position-based variation
for ( let i = 0 ; i < 384 ; i ++ ) {
vector [ i ] += Math . sin ( i * 0.1 + str . length ) * 0.1
}
2025-10-17 12:29:27 -07:00
2025-09-02 10:00:52 -07:00
this . embedCount ++
return vector
}
2025-12-17 17:42:37 -08:00
2025-09-02 10:00:52 -07:00
/ * *
* Get embedding function for compatibility
* /
getEmbeddingFunction ( ) : EmbeddingFunction {
2025-10-17 12:29:27 -07:00
return async ( data : string | string [ ] | Record < string , unknown > ) : Promise < Vector > = > {
2025-09-02 10:00:52 -07:00
return await this . embed ( data )
}
}
2025-09-17 14:53:54 -07:00
2025-09-02 10:00:52 -07:00
/ * *
* Get memory usage in MB
* /
private getMemoryUsage ( ) : number | null {
if ( typeof process !== 'undefined' && process . memoryUsage ) {
const usage = process . memoryUsage ( )
return Math . round ( usage . heapUsed / 1024 / 1024 )
}
return null
}
2025-12-17 17:42:37 -08:00
2025-09-02 10:00:52 -07:00
/ * *
* Get current statistics
* /
getStats ( ) : EmbeddingStats {
2025-12-17 17:42:37 -08:00
const engineStats = this . engine . getStats ( )
2025-09-02 10:00:52 -07:00
return {
initialized : this.initialized ,
precision : this.precision ,
modelName : this.modelName ,
2025-12-17 17:42:37 -08:00
embedCount : this.embedCount + engineStats . embedCount ,
2025-09-02 10:00:52 -07:00
initTime : this.initTime ,
2025-12-17 17:42:37 -08:00
memoryMB : this.getMemoryUsage ( ) ,
2025-09-02 10:00:52 -07:00
}
}
2025-12-17 17:42:37 -08:00
2025-09-02 10:00:52 -07:00
/ * *
* Check if initialized
* /
isInitialized ( ) : boolean {
return this . initialized
}
2025-12-17 17:42:37 -08:00
2025-09-02 10:00:52 -07:00
/ * *
* Get current precision
* /
getPrecision ( ) : ModelPrecision {
return this . precision
}
2025-12-17 17:42:37 -08:00
2025-09-02 10:00:52 -07:00
/ * *
* Validate precision matches expected
* /
validatePrecision ( expected : ModelPrecision ) : void {
if ( this . locked && expected !== this . precision ) {
throw new Error (
` Precision mismatch! System using ${ this . precision . toUpperCase ( ) } ` +
` but ${ expected . toUpperCase ( ) } was requested. Cannot mix precisions. `
)
}
}
}
// Export singleton instance and convenience functions
export const embeddingManager = EmbeddingManager . getInstance ( )
/ * *
* Direct embed function
* /
2025-12-17 17:42:37 -08:00
export async function embed (
text : string | string [ ] | Record < string , unknown >
) : Promise < Vector > {
2025-09-02 10:00:52 -07:00
return await embeddingManager . embed ( text )
}
/ * *
* Get embedding function for compatibility
* /
export function getEmbeddingFunction ( ) : EmbeddingFunction {
return embeddingManager . getEmbeddingFunction ( )
}
/ * *
* Get statistics
* /
export function getEmbeddingStats ( ) : EmbeddingStats {
return embeddingManager . getStats ( )
2025-12-17 17:42:37 -08:00
}