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
feat: add structured content extraction and batch embedding optimization to highlight()
Fix highlight() hanging on structured text input by addressing 3 root causes:
1. embedBatch() now uses native WASM batch API (single forward pass instead
of N individual embed() calls via Promise.all)
2. highlight() auto-detects content type (plain text, rich-text JSON, HTML,
Markdown) and extracts meaningful text segments. Supports TipTap, Slate.js,
Lexical, Draft.js, and Quill Delta formats. New contentType hint and
contentExtractor callback for custom parsers.
3. Semantic matching phase has 10s timeout - falls back to text-only matches
instead of hanging indefinitely.
Also fixes extractTextContent() array check: uses type-based detection
(typeof data[0] === 'number') instead of length-based (data.length > 10)
so arrays of objects are properly indexed for text search.
New types: ContentType, ContentCategory, ExtractedSegment
New fields: HighlightParams.contentType, HighlightParams.contentExtractor,
Highlight.contentCategory
2026-01-27 10:27:22 -08:00
/ * *
* Batch embed multiple texts using native WASM batch API
*
* Uses the engine ' s embedBatch ( ) for a single WASM forward pass
* instead of N individual embed ( ) calls .
*
* @param texts Array of strings to embed
* @returns Array of embedding vectors ( 384 dimensions each )
* /
async embedBatch ( texts : string [ ] ) : Promise < number [ ] [ ] > {
if ( texts . length === 0 ) return [ ]
const isTestMode =
process . env . BRAINY_UNIT_TEST === 'true' ||
( globalThis as any ) . __BRAINY_UNIT_TEST__
if ( isTestMode ) {
if ( process . env . NODE_ENV === 'production' ) {
throw new Error ( 'CRITICAL: Mock embeddings in production!' )
}
return texts . map ( t = > this . getMockEmbedding ( t ) )
}
await this . init ( )
const results = await this . engine . embedBatch ( texts )
this . embedCount += texts . length
return results
}
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
}