The __words__ keyword index stores 50-5000 entries per entity (one per word), which inflated avg entries/entity well above the corruption threshold of 100. This caused: 1. validateConsistency() to falsely detect corruption on every startup, triggering unnecessary clearAllIndexData() + rebuild() cycles 2. getStats() to log false "Metadata index may be corrupted" warnings and report inflated totalEntries/totalIds stats Both methods now skip __words__ when counting, so stats and health checks reflect metadata fields only (noun, type, createdAt, etc.). Keyword search is unaffected since the __words__ field index itself is not modified.
247 lines
8.7 KiB
TypeScript
247 lines
8.7 KiB
TypeScript
/**
|
|
* Universal Model Loader for Candle Embeddings
|
|
*
|
|
* Model weights are now loaded separately from WASM for faster initialization.
|
|
* 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() |
|
|
* | Bun --compile | Static import with embedded asset |
|
|
* | Browser | fetch() |
|
|
*
|
|
* For Bun --compile, assets are embedded at compile time using static imports.
|
|
*/
|
|
|
|
// =============================================================================
|
|
// Type Declarations
|
|
// =============================================================================
|
|
|
|
declare const Bun: {
|
|
file(path: string): { arrayBuffer(): Promise<ArrayBuffer> }
|
|
main: string
|
|
} | undefined
|
|
|
|
// =============================================================================
|
|
// Environment Detection (evaluated once at module load)
|
|
// =============================================================================
|
|
|
|
const isBun = typeof Bun !== 'undefined'
|
|
const isNode = !isBun && typeof process !== 'undefined' && !!process.versions?.node
|
|
|
|
// 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
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// 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> {
|
|
// 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
|
|
|
|
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`,
|
|
])
|
|
|
|
// 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
|
|
}
|
|
}
|
|
|
|
// 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/**/*"'
|
|
)
|
|
}
|
|
|
|
// =============================================================================
|
|
// 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),
|
|
}
|
|
}
|
|
|