Spaces:
Paused
Paused
File size: 5,305 Bytes
34367da | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 | /**
* Transformers.js Embeddings Service
*
* Uses @xenova/transformers to generate embeddings locally without HuggingFace API.
* Supports sentence-transformers models for semantic similarity.
*
* NOTE: Disabled in Docker/production due to ONNX runtime compatibility issues with Alpine
*/
// Dynamic import to avoid ONNX runtime crash in Docker
let pipelineFactory: any = null;
export interface EmbeddingOptions {
model?: string;
normalize?: boolean;
}
export class TransformersEmbeddings {
private modelName: string;
private extractor: any = null; // Pipeline type from @xenova/transformers
private initialized: boolean = false;
constructor(modelName: string = 'Xenova/all-MiniLM-L6-v2') {
this.modelName = modelName;
}
/**
* Initialize the embedding model
*/
async initialize(): Promise<void> {
if (this.initialized && this.extractor) {
return;
}
// Skip transformers in Docker/production - ONNX runtime has architecture issues on Alpine
const isDocker = process.env.NODE_ENV === 'production' || process.cwd().startsWith('/app');
if (isDocker) {
throw new Error(
'Transformers.js disabled in Docker mode (ONNX runtime incompatibility with Alpine)'
);
}
try {
console.log(`🔄 Loading embedding model: ${this.modelName}`);
// Dynamic import to avoid top-level ONNX load
if (!pipelineFactory) {
const transformers = await import('@xenova/transformers');
pipelineFactory = transformers.pipeline;
}
this.extractor = await pipelineFactory('feature-extraction', this.modelName, {
quantized: true, // Use quantized model for faster loading
});
this.initialized = true;
console.log(`✅ Embedding model loaded: ${this.modelName}`);
} catch (error) {
console.error('❌ Failed to load embedding model:', error);
throw error;
}
}
/**
* Generate embedding for a single text
*/
async embed(text: string, options?: EmbeddingOptions): Promise<number[]> {
if (!this.extractor) {
await this.initialize();
}
if (!this.extractor) {
throw new Error('Embedding model not initialized');
}
try {
const output = await this.extractor(text, {
pooling: 'mean',
normalize: options?.normalize ?? true,
});
// Convert tensor to array
const embedding = Array.from(output.data) as number[];
return embedding;
} catch (error) {
console.error('❌ Failed to generate embedding:', error);
throw error;
}
}
/**
* Generate embeddings for multiple texts
*/
async embedBatch(texts: string[], options?: EmbeddingOptions): Promise<number[][]> {
if (!this.extractor) {
await this.initialize();
}
if (!this.extractor) {
throw new Error('Embedding model not initialized');
}
try {
const embeddings: number[][] = [];
// Process in batches to avoid memory issues
const batchSize = 10;
for (let i = 0; i < texts.length; i += batchSize) {
const batch = texts.slice(i, i + batchSize);
const batchEmbeddings = await Promise.all(batch.map(text => this.embed(text, options)));
embeddings.push(...batchEmbeddings);
}
return embeddings;
} catch (error) {
console.error('❌ Failed to generate batch embeddings:', error);
throw error;
}
}
/**
* Calculate cosine similarity between two embeddings
*/
cosineSimilarity(embedding1: number[], embedding2: number[]): number {
if (embedding1.length !== embedding2.length) {
throw new Error('Embeddings must have the same dimension');
}
let dotProduct = 0;
let norm1 = 0;
let norm2 = 0;
for (let i = 0; i < embedding1.length; i++) {
dotProduct += embedding1[i] * embedding2[i];
norm1 += embedding1[i] * embedding1[i];
norm2 += embedding2[i] * embedding2[i];
}
const similarity = dotProduct / (Math.sqrt(norm1) * Math.sqrt(norm2));
return similarity;
}
/**
* Find most similar embedding in a collection
*/
findMostSimilar(
queryEmbedding: number[],
candidateEmbeddings: number[][],
topK: number = 5
): Array<{ index: number; similarity: number }> {
const similarities = candidateEmbeddings.map((embedding, index) => ({
index,
similarity: this.cosineSimilarity(queryEmbedding, embedding),
}));
return similarities.sort((a, b) => b.similarity - a.similarity).slice(0, topK);
}
/**
* Get embedding dimension
*/
getDimension(): number {
// all-MiniLM-L6-v2 has 384 dimensions
return 384;
}
/**
* Check if model is initialized
*/
isInitialized(): boolean {
return this.initialized && this.extractor !== null;
}
}
// Singleton instance
let transformersEmbeddingsInstance: TransformersEmbeddings | null = null;
export function getTransformersEmbeddings(): TransformersEmbeddings {
if (!transformersEmbeddingsInstance) {
transformersEmbeddingsInstance = new TransformersEmbeddings();
}
return transformersEmbeddingsInstance;
}
|