2026-01-06 17:18:58 -08:00
/ * *
* Universal Model Loader for Candle Embeddings
*
2026-01-27 15:38:21 -08:00
* Model weights are now loaded separately from WASM for faster initialization .
2026-01-06 17:18:58 -08:00
* This reduces WASM compilation time from 139 seconds to ~ 3 - 5 seconds .
*
* Loads model files from appropriate source based on environment :
*
* | Environment | Method |
* | -- -- -- -- -- -- -- -- | -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - |
* | Node . js | fs . readFile ( ) |
* | Bun | Bun . file ( ) |
2026-01-06 18:00:37 -08:00
* | Bun -- compile | Static import with embedded asset |
2026-01-06 17:18:58 -08:00
* | Browser | fetch ( ) |
*
2026-01-06 18:00:37 -08:00
* For Bun -- compile , assets are embedded at compile time using static imports .
2026-01-06 17:18:58 -08:00
* /
// =============================================================================
// Type Declarations
// =============================================================================
declare const Bun : {
file ( path : string ) : { arrayBuffer ( ) : Promise < ArrayBuffer > }
2026-01-06 18:00:37 -08:00
main : string
2026-01-06 17:18:58 -08:00
} | undefined
// =============================================================================
// Environment Detection (evaluated once at module load)
// =============================================================================
const isBun = typeof Bun !== 'undefined'
const isNode = ! isBun && typeof process !== 'undefined' && ! ! process . versions ? . node
2026-01-06 18:00:37 -08:00
// Detect if running in bun --compile binary
// In compiled binaries, Bun.main starts with '/$bunfs/'
const isBunCompiled = isBun && typeof Bun !== 'undefined' && Bun . main ? . startsWith ( '/$bunfs/' )
// =============================================================================
// Embedded Assets for Bun --compile
// =============================================================================
// These static imports tell Bun to embed the files at compile time.
// The paths must be string literals for Bun to resolve them.
// @ts-ignore - Bun-specific import syntax for embedded assets
let embeddedModelPath : string | undefined
// @ts-ignore
let embeddedTokenizerPath : string | undefined
// @ts-ignore
let embeddedConfigPath : string | undefined
// Try to use Bun's embed feature if available
// This block is only executed in Bun environments
if ( isBun ) {
try {
// In Bun --compile, these resolve to embedded asset paths
// In regular Bun runtime, these resolve to filesystem paths
embeddedModelPath = new URL ( '../../../assets/models/all-MiniLM-L6-v2/model.safetensors' , import . meta . url ) . pathname
embeddedTokenizerPath = new URL ( '../../../assets/models/all-MiniLM-L6-v2/tokenizer.json' , import . meta . url ) . pathname
embeddedConfigPath = new URL ( '../../../assets/models/all-MiniLM-L6-v2/config.json' , import . meta . url ) . pathname
} catch {
// Fallback handled in loadBunAssets
}
}
2026-01-06 17:18:58 -08:00
// =============================================================================
// Types
// =============================================================================
export interface ModelAssets {
model : Uint8Array
tokenizer : Uint8Array
config : Uint8Array
}
// =============================================================================
// Public API
// =============================================================================
/ * *
* Load model assets from the appropriate source for the current environment .
*
* This function handles all environment differences internally .
* Model files are loaded in parallel for best performance .
*
* @returns ModelAssets containing model , tokenizer , and config bytes
* @throws Error if model files cannot be loaded
* /
export async function loadModelAssets ( ) : Promise < ModelAssets > {
// Bun (runtime or compiled binary)
if ( isBun ) {
return loadBunAssets ( )
}
// Node.js
if ( isNode ) {
return loadNodeAssets ( )
}
// Browser
return loadBrowserAssets ( )
}
/ * *
* Check if model files are available .
* Useful for debugging .
* /
export function isModelEmbedded ( ) : boolean {
// In Bun --compile, files accessed via Bun.file() are auto-embedded
return isBun
}
// =============================================================================
// Internal Helpers - Bun
// =============================================================================
async function loadBunAssets ( ) : Promise < ModelAssets > {
2026-01-06 18:00:37 -08:00
// For bun --compile, we need to try multiple strategies:
// 1. Try the resolved paths (works in Bun runtime)
// 2. Fall back to node_modules location (for bun --compile when assets are alongside binary)
// 3. Fall back to process.cwd() relative paths
2026-01-06 17:18:58 -08:00
2026-01-06 18:00:37 -08:00
const pathsToTry : string [ ] [ ] = [ ]
// Strategy 1: Pre-resolved paths (works in Bun runtime)
if ( embeddedModelPath && embeddedTokenizerPath && embeddedConfigPath ) {
pathsToTry . push ( [ embeddedModelPath , embeddedTokenizerPath , embeddedConfigPath ] )
}
// Strategy 2: node_modules path relative to CWD (for installed packages)
const nmPath = './node_modules/@soulcraft/brainy/assets/models/all-MiniLM-L6-v2'
pathsToTry . push ( [
` ${ nmPath } /model.safetensors ` ,
` ${ nmPath } /tokenizer.json ` ,
` ${ nmPath } /config.json ` ,
2026-01-06 17:18:58 -08:00
] )
2026-01-06 18:00:37 -08:00
// Strategy 3: assets folder relative to CWD (for local development)
pathsToTry . push ( [
'./assets/models/all-MiniLM-L6-v2/model.safetensors' ,
'./assets/models/all-MiniLM-L6-v2/tokenizer.json' ,
'./assets/models/all-MiniLM-L6-v2/config.json' ,
] )
// Try each strategy
for ( const [ modelPath , tokenizerPath , configPath ] of pathsToTry ) {
try {
const [ model , tokenizer , config ] = await Promise . all ( [
Bun ! . file ( modelPath ) . arrayBuffer ( ) ,
Bun ! . file ( tokenizerPath ) . arrayBuffer ( ) ,
Bun ! . file ( configPath ) . arrayBuffer ( ) ,
] )
// Verify we got valid data (not empty)
if ( model . byteLength > 0 && tokenizer . byteLength > 0 && config . byteLength > 0 ) {
return {
model : new Uint8Array ( model ) ,
tokenizer : new Uint8Array ( tokenizer ) ,
config : new Uint8Array ( config ) ,
}
}
} catch {
// Try next strategy
continue
}
2026-01-06 17:18:58 -08:00
}
2026-01-06 18:00:37 -08:00
// If all strategies fail, provide helpful error message
throw new Error (
'Could not load model assets. For bun --compile, ensure model files are accessible:\n' +
' Option 1: Keep node_modules/@soulcraft/brainy/assets/ alongside your binary\n' +
' Option 2: Copy assets/ folder to your working directory\n' +
' Option 3: Use --asset flag: bun build --compile --asset="./node_modules/@soulcraft/brainy/assets/**/*"'
)
2026-01-06 17:18:58 -08:00
}
// =============================================================================
// Internal Helpers - Node.js
// =============================================================================
async function loadNodeAssets ( ) : Promise < ModelAssets > {
const fs = await import ( 'node:fs' )
const nodePath = await import ( 'node:path' )
const { fileURLToPath } = await import ( 'node:url' )
const thisDir = nodePath . dirname ( fileURLToPath ( import . meta . url ) )
const assetsDir = nodePath . resolve ( thisDir , '../../../assets/models/all-MiniLM-L6-v2' )
// Verify assets directory exists
if ( ! fs . existsSync ( assetsDir ) ) {
throw new Error (
` Model assets not found: ${ assetsDir } \ n ` +
` Ensure @soulcraft/brainy is installed correctly. `
)
}
const [ model , tokenizer , config ] = await Promise . all ( [
fs . promises . readFile ( nodePath . join ( assetsDir , 'model.safetensors' ) ) ,
fs . promises . readFile ( nodePath . join ( assetsDir , 'tokenizer.json' ) ) ,
fs . promises . readFile ( nodePath . join ( assetsDir , 'config.json' ) ) ,
] )
return {
model : new Uint8Array ( model ) ,
tokenizer : new Uint8Array ( tokenizer ) ,
config : new Uint8Array ( config ) ,
}
}
// =============================================================================
// Internal Helpers - Browser
// =============================================================================
async function loadBrowserAssets ( ) : Promise < ModelAssets > {
// In browser, assets are served relative to the WASM location
const baseUrl = new URL ( '../../../assets/models/all-MiniLM-L6-v2/' , import . meta . url )
const [ modelRes , tokenizerRes , configRes ] = await Promise . all ( [
fetch ( new URL ( 'model.safetensors' , baseUrl ) ) ,
fetch ( new URL ( 'tokenizer.json' , baseUrl ) ) ,
fetch ( new URL ( 'config.json' , baseUrl ) ) ,
] )
// Check for errors
if ( ! modelRes . ok ) {
throw new Error ( ` Failed to fetch model: ${ modelRes . status } ${ modelRes . statusText } ` )
}
if ( ! tokenizerRes . ok ) {
throw new Error ( ` Failed to fetch tokenizer: ${ tokenizerRes . status } ${ tokenizerRes . statusText } ` )
}
if ( ! configRes . ok ) {
throw new Error ( ` Failed to fetch config: ${ configRes . status } ${ configRes . statusText } ` )
}
const [ model , tokenizer , config ] = await Promise . all ( [
modelRes . arrayBuffer ( ) ,
tokenizerRes . arrayBuffer ( ) ,
configRes . arrayBuffer ( ) ,
] )
return {
model : new Uint8Array ( model ) ,
tokenizer : new Uint8Array ( tokenizer ) ,
config : new Uint8Array ( config ) ,
}
}