2025-08-26 12:32:21 -07:00
#!/usr/bin/env node
/ * *
* Download and bundle models for offline usage
* /
const fs = require ( 'fs' ) . promises
const path = require ( 'path' )
const MODEL _NAME = 'Xenova/all-MiniLM-L6-v2'
const OUTPUT _DIR = './models'
2025-08-29 11:09:40 -07:00
// Parse command line arguments for model type selection
const args = process . argv . slice ( 2 )
const downloadType = args . includes ( 'fp32' ) ? 'fp32' :
args . includes ( 'q8' ) ? 'q8' : 'both'
2025-08-26 12:32:21 -07:00
async function downloadModels ( ) {
// Use dynamic import for ES modules in CommonJS
const { pipeline , env } = await import ( '@huggingface/transformers' )
// Configure transformers.js to use local cache
env . cacheDir = './models-cache'
env . allowRemoteModels = true
2025-08-29 11:09:40 -07:00
2025-08-26 12:32:21 -07:00
try {
2025-08-29 11:09:40 -07:00
console . log ( '🧠 Brainy Model Downloader v2.8.0' )
console . log ( '===================================' )
2025-08-26 12:32:21 -07:00
console . log ( ` Model: ${ MODEL _NAME } ` )
2025-08-29 11:09:40 -07:00
console . log ( ` Type: ${ downloadType } (fp32, q8, or both) ` )
2025-08-26 12:32:21 -07:00
console . log ( ` Cache: ${ env . cacheDir } ` )
2025-08-29 11:09:40 -07:00
console . log ( '' )
2025-08-26 12:32:21 -07:00
// Create output directory
await fs . mkdir ( OUTPUT _DIR , { recursive : true } )
2025-08-29 11:09:40 -07:00
// Download models based on type
if ( downloadType === 'both' || downloadType === 'fp32' ) {
console . log ( '📥 Downloading FP32 model (full precision, 90MB)...' )
await downloadModelVariant ( 'fp32' )
}
2025-08-26 12:32:21 -07:00
2025-08-29 11:09:40 -07:00
if ( downloadType === 'both' || downloadType === 'q8' ) {
console . log ( '📥 Downloading Q8 model (quantized, 23MB)...' )
await downloadModelVariant ( 'q8' )
}
2025-08-26 12:32:21 -07:00
// Copy ALL model files from cache to our models directory
2025-08-29 11:09:40 -07:00
console . log ( '📋 Copying model files to bundle directory...' )
2025-08-26 12:32:21 -07:00
const cacheDir = path . resolve ( env . cacheDir )
const outputDir = path . resolve ( OUTPUT _DIR )
console . log ( ` From: ${ cacheDir } ` )
console . log ( ` To: ${ outputDir } ` )
// Copy the entire cache directory structure to ensure we get ALL files
// including tokenizer.json, config.json, and all ONNX model files
const modelCacheDir = path . join ( cacheDir , 'Xenova' , 'all-MiniLM-L6-v2' )
if ( await dirExists ( modelCacheDir ) ) {
const targetModelDir = path . join ( outputDir , 'Xenova' , 'all-MiniLM-L6-v2' )
console . log ( ` Copying complete model: Xenova/all-MiniLM-L6-v2 ` )
await copyDirectory ( modelCacheDir , targetModelDir )
} else {
throw new Error ( ` Model cache directory not found: ${ modelCacheDir } ` )
}
console . log ( '✅ Model bundling complete!' )
console . log ( ` Total size: ${ await calculateDirectorySize ( outputDir ) } MB ` )
console . log ( ` Location: ${ outputDir } ` )
2025-08-29 11:09:40 -07:00
// Create a marker file with downloaded model info
const markerData = {
model : MODEL _NAME ,
bundledAt : new Date ( ) . toISOString ( ) ,
version : '2.8.0' ,
downloadType : downloadType ,
models : { }
}
// Check which models were downloaded
const fp32Path = path . join ( outputDir , 'Xenova/all-MiniLM-L6-v2/onnx/model.onnx' )
const q8Path = path . join ( outputDir , 'Xenova/all-MiniLM-L6-v2/onnx/model_quantized.onnx' )
if ( await fileExists ( fp32Path ) ) {
const stats = await fs . stat ( fp32Path )
markerData . models . fp32 = {
file : 'onnx/model.onnx' ,
size : stats . size ,
sizeFormatted : ` ${ Math . round ( stats . size / ( 1024 * 1024 ) ) } MB `
}
}
if ( await fileExists ( q8Path ) ) {
const stats = await fs . stat ( q8Path )
markerData . models . q8 = {
file : 'onnx/model_quantized.onnx' ,
size : stats . size ,
sizeFormatted : ` ${ Math . round ( stats . size / ( 1024 * 1024 ) ) } MB `
}
}
2025-08-26 12:32:21 -07:00
await fs . writeFile (
path . join ( outputDir , '.brainy-models-bundled' ) ,
2025-08-29 11:09:40 -07:00
JSON . stringify ( markerData , null , 2 )
2025-08-26 12:32:21 -07:00
)
2025-08-29 11:09:40 -07:00
console . log ( '' )
console . log ( '✅ Download complete! Available models:' )
if ( markerData . models . fp32 ) {
console . log ( ` • FP32: ${ markerData . models . fp32 . sizeFormatted } (full precision) ` )
}
if ( markerData . models . q8 ) {
console . log ( ` • Q8: ${ markerData . models . q8 . sizeFormatted } (quantized, 75% smaller) ` )
}
console . log ( '' )
console . log ( 'Air-gap deployment ready! 🚀' )
2025-08-26 12:32:21 -07:00
} catch ( error ) {
console . error ( '❌ Error downloading models:' , error )
process . exit ( 1 )
}
}
2025-08-29 11:09:40 -07:00
// Download a specific model variant
async function downloadModelVariant ( dtype ) {
const { pipeline } = await import ( '@huggingface/transformers' )
try {
// Load the model to force download
const extractor = await pipeline ( 'feature-extraction' , MODEL _NAME , {
dtype : dtype ,
cache _dir : './models-cache'
} )
// Test the model
const testResult = await extractor ( [ 'Hello world!' ] , {
pooling : 'mean' ,
normalize : true
} )
console . log ( ` ✅ ${ dtype . toUpperCase ( ) } model downloaded and tested ( ${ testResult . data . length } dimensions) ` )
// Dispose to free memory
if ( extractor . dispose ) {
await extractor . dispose ( )
}
} catch ( error ) {
console . error ( ` ❌ Failed to download ${ dtype } model: ` , error )
throw error
}
}
2025-08-26 12:32:21 -07:00
async function findModelDirectories ( baseDir , modelName ) {
const dirs = [ ]
try {
// Convert model name to expected directory structure
const modelPath = modelName . replace ( '/' , '--' )
async function searchDirectory ( currentDir ) {
try {
const entries = await fs . readdir ( currentDir , { withFileTypes : true } )
for ( const entry of entries ) {
if ( entry . isDirectory ( ) ) {
const fullPath = path . join ( currentDir , entry . name )
// Check if this directory contains model files
if ( entry . name . includes ( modelPath ) || entry . name === 'onnx' ) {
const hasModelFiles = await containsModelFiles ( fullPath )
if ( hasModelFiles ) {
dirs . push ( fullPath )
}
}
// Recursively search subdirectories
await searchDirectory ( fullPath )
}
}
} catch ( error ) {
// Ignore access errors
}
}
await searchDirectory ( baseDir )
} catch ( error ) {
console . warn ( 'Warning: Error searching for model directories:' , error )
}
return dirs
}
async function containsModelFiles ( dir ) {
try {
const files = await fs . readdir ( dir )
return files . some ( file =>
file . endsWith ( '.onnx' ) ||
file . endsWith ( '.json' ) ||
file === 'config.json' ||
file === 'tokenizer.json'
)
} catch ( error ) {
return false
}
}
async function dirExists ( dir ) {
try {
const stats = await fs . stat ( dir )
return stats . isDirectory ( )
} catch ( error ) {
return false
}
}
2025-08-29 11:09:40 -07:00
async function fileExists ( file ) {
try {
const stats = await fs . stat ( file )
return stats . isFile ( )
} catch ( error ) {
return false
}
}
2025-08-26 12:32:21 -07:00
async function copyDirectory ( src , dest ) {
await fs . mkdir ( dest , { recursive : true } )
const entries = await fs . readdir ( src , { withFileTypes : true } )
for ( const entry of entries ) {
const srcPath = path . join ( src , entry . name )
const destPath = path . join ( dest , entry . name )
if ( entry . isDirectory ( ) ) {
await copyDirectory ( srcPath , destPath )
} else {
await fs . copyFile ( srcPath , destPath )
}
}
}
async function calculateDirectorySize ( dir ) {
let size = 0
async function calculateSize ( currentDir ) {
try {
const entries = await fs . readdir ( currentDir , { withFileTypes : true } )
for ( const entry of entries ) {
const fullPath = path . join ( currentDir , entry . name )
if ( entry . isDirectory ( ) ) {
await calculateSize ( fullPath )
} else {
const stats = await fs . stat ( fullPath )
size += stats . size
}
}
} catch ( error ) {
// Ignore access errors
}
}
await calculateSize ( dir )
return Math . round ( size / ( 1024 * 1024 ) )
}
// Run the download
downloadModels ( ) . catch ( error => {
console . error ( 'Fatal error:' , error )
process . exit ( 1 )
} )