Spaces:
Running
Running
| import { pipeline, env, cos_sim } from 'https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.0.0-alpha.19/dist/transformers.min.js'; | |
| // Cấu hình | |
| env.allowLocalModels = false; | |
| env.useBrowserCache = true; | |
| // Hàm log helper gửi về Main Thread | |
| function log(msg) { | |
| self.postMessage({ type: 'log', payload: msg }); | |
| } | |
| const EMBEDDING_MODEL_ID = 'onnx-community/embeddinggemma-300m-ONNX'; | |
| // Dùng ID Granite 4.0 (Nếu lỗi 404 hãy thử ibm-granite/granite-3.0-2b-instruct) | |
| const LLM_MODEL_ID = 'ibm-granite/granite-4.0-350m-instruct'; | |
| let extractor = null; | |
| let generator = null; | |
| let vectorStore = []; | |
| async function initModels() { | |
| try { | |
| log("🚀 Bắt đầu tải Embedding Model (Gemma)..."); | |
| self.postMessage({ type: 'status', payload: "Đang tải Embedding..." }); | |
| extractor = await pipeline('feature-extraction', EMBEDDING_MODEL_ID, { | |
| device: 'webgpu', | |
| dtype: 'fp32', | |
| progress_callback: (data) => self.postMessage({ type: 'download_progress', payload: data }) | |
| }); | |
| log("✅ Embedding Model đã tải xong."); | |
| log("🚀 Bắt đầu tải LLM (Granite 4.0)..."); | |
| self.postMessage({ type: 'status', payload: "Đang tải LLM..." }); | |
| generator = await pipeline('text-generation', LLM_MODEL_ID, { | |
| device: 'webgpu', | |
| dtype: 'q4', | |
| use_external_data_format: true, | |
| progress_callback: (data) => self.postMessage({ type: 'download_progress', payload: data }) | |
| }); | |
| log("✅ LLM đã tải xong."); | |
| self.postMessage({ type: 'init_complete' }); | |
| } catch (e) { | |
| self.postMessage({ type: 'error', payload: e.message }); | |
| } | |
| } | |
| // Hàm chia nhỏ văn bản (Chunking) có log | |
| function chunkText(text, chunkSize = 300, overlap = 50) { | |
| log(`Bắt đầu chia nhỏ văn bản dài ${text.length} ký tự...`); | |
| const sentences = text.match(/[^.!?]+[.!?]+|[^.!?]+$/g) || [text]; | |
| let chunks = []; | |
| let currentChunk = ""; | |
| for (let sentence of sentences) { | |
| if ((currentChunk + sentence).length > chunkSize) { | |
| chunks.push(currentChunk.trim()); | |
| currentChunk = sentence.slice(-overlap); | |
| } else { | |
| currentChunk += " " + sentence; | |
| } | |
| } | |
| if (currentChunk) chunks.push(currentChunk.trim()); | |
| log(`-> Đã chia thành ${chunks.length} chunks.`); | |
| return chunks; | |
| } | |
| // Tạo Embeddings | |
| async function ingestText(text) { | |
| const chunks = chunkText(text); | |
| vectorStore = []; | |
| log("🔄 Đang tạo vector cho từng chunk (Quá trình này tốn GPU)..."); | |
| let startTime = performance.now(); | |
| for (let i = 0; i < chunks.length; i++) { | |
| const output = await extractor(chunks[i], { pooling: 'mean', normalize: true }); | |
| vectorStore.push({ | |
| text: chunks[i], | |
| vector: output.data | |
| }); | |
| // Log mỗi 5 chunks để không spam | |
| if ((i + 1) % 5 === 0) log(`Processed ${i + 1}/${chunks.length} chunks...`); | |
| } | |
| let endTime = performance.now(); | |
| log(`✅ Indexing hoàn tất trong ${((endTime - startTime)/1000).toFixed(2)}s.`); | |
| } | |
| async function retrieve(query) { | |
| log(`🔍 Đang tìm kiếm ngữ cảnh cho: "${query}"`); | |
| const queryOutput = await extractor(query, { pooling: 'mean', normalize: true }); | |
| const queryVector = queryOutput.data; | |
| const scored = vectorStore.map(item => ({ | |
| text: item.text, | |
| score: cos_sim(queryVector, item.vector) | |
| })); | |
| scored.sort((a, b) => b.score - a.score); | |
| const top3 = scored.slice(0, 3); | |
| log(`-> Tìm thấy 3 đoạn khớp nhất (Scores: ${top3.map(i => i.score.toFixed(2)).join(', ')})`); | |
| return top3.map(i => i.text).join("\n\n"); | |
| } | |
| self.onmessage = async (e) => { | |
| if (!extractor || !generator) { | |
| await initModels(); | |
| } | |
| const { type, payload } = e.data; | |
| if (type === 'ingest_text') { | |
| await ingestText(payload); | |
| } else if (type === 'query') { | |
| if (vectorStore.length === 0) { | |
| log("⚠️ Cảnh báo: Chưa có dữ liệu PDF nào được index."); | |
| } | |
| const context = await retrieve(payload); | |
| const prompt = `<|system|> | |
| Bạn là trợ lý AI hữu ích. Trả lời dựa trên ngữ cảnh sau bằng Tiếng Việt: | |
| ${context} | |
| <|user|> | |
| ${payload} | |
| <|assistant|>`; | |
| log("🤖 LLM đang suy nghĩ..."); | |
| try { | |
| const output = await generator(prompt, { | |
| max_new_tokens: 256, | |
| temperature: 0.6, | |
| do_sample: true, | |
| // Log mỗi khi sinh ra token mới (cẩn thận spam nếu bật) | |
| // callback_function: (x) => log("Generated token...") | |
| }); | |
| let answer = output[0].generated_text; | |
| if (answer.includes("<|assistant|>")) { | |
| answer = answer.split("<|assistant|>")[1]; | |
| } | |
| self.postMessage({ type: 'answer', payload: answer }); | |
| } catch (err) { | |
| self.postMessage({ type: 'error', payload: err.message }); | |
| } | |
| } | |
| }; | |
| initModels(); |