Spaces:
Running
Running
Create localmodel.mjs
Browse files- core/localmodel.mjs +54 -0
core/localmodel.mjs
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* Modelo local MiMo en el navegador (wllama 3.5.1, WASM/CPU).
|
| 2 |
+
El chat de este Space no depende de ning煤n servicio externo de modelos:
|
| 3 |
+
el GGUF se descarga desde Hugging Face (aprox. 5,8 GB la primera vez) y se
|
| 4 |
+
reutiliza desde la cach茅 del navegador mientras esta se conserve. */
|
| 5 |
+
const WLLAMA_ESM = 'https://cdn.jsdelivr.net/npm/@wllama/wllama@3.5.1/esm/index.js';
|
| 6 |
+
const WLLAMA_WASM = 'https://cdn.jsdelivr.net/npm/@wllama/wllama@3.5.1/esm/wasm/wllama.wasm';
|
| 7 |
+
const MODEL_URL = 'https://huggingface.co/bartowski/MiMo-V2.6-Distill-Qwen-9B-GGUF/resolve/main/MiMo-V2.6-Distill-Qwen-9B-Q4_K_M.gguf';
|
| 8 |
+
export const APLocalModel = {
|
| 9 |
+
state: 'idle', // idle | downloading | ready | error
|
| 10 |
+
error: '',
|
| 11 |
+
wllama: null,
|
| 12 |
+
label: 'MiMo-V2.6-Distill-Qwen-9B (local en este navegador)',
|
| 13 |
+
async cleanupCache() {
|
| 14 |
+
try { const keys = await caches.keys(); for (const k of keys) await caches.delete(k); } catch (_) {}
|
| 15 |
+
},
|
| 16 |
+
async load(onProgress) {
|
| 17 |
+
if (this.state === 'ready' || this.state === 'downloading') return;
|
| 18 |
+
this.state = 'downloading'; this.error = '';
|
| 19 |
+
try {
|
| 20 |
+
const { Wllama } = await import(WLLAMA_ESM);
|
| 21 |
+
if (this.wllama) { try { await this.wllama.exit(); } catch (_) {} }
|
| 22 |
+
this.wllama = new Wllama({ default: WLLAMA_WASM }, { parallelDownloads: 5 });
|
| 23 |
+
const opts = {
|
| 24 |
+
n_gpu_layers: 0, // WASM/CPU: sin aceleraci贸n garantizada
|
| 25 |
+
n_ctx: 4096, n_batch: 512,
|
| 26 |
+
n_threads: Math.max(1, Math.min(8, navigator.hardwareConcurrency || 4)),
|
| 27 |
+
progressCallback: ({ loaded, total }) => { if (onProgress && total > 0) onProgress(loaded, total); },
|
| 28 |
+
};
|
| 29 |
+
try {
|
| 30 |
+
await this.wllama.loadModelFromUrl(MODEL_URL, opts);
|
| 31 |
+
} catch (first) {
|
| 32 |
+
// Cach茅 incompleta o corrupta: limpiar y reintentar una vez.
|
| 33 |
+
await this.cleanupCache();
|
| 34 |
+
await this.wllama.loadModelFromUrl(MODEL_URL, opts);
|
| 35 |
+
}
|
| 36 |
+
this.state = 'ready';
|
| 37 |
+
} catch (e) {
|
| 38 |
+
this.state = 'error'; this.error = String(e && e.message || e).slice(0, 200);
|
| 39 |
+
throw e;
|
| 40 |
+
}
|
| 41 |
+
},
|
| 42 |
+
async chat(messages, { maxTokens = 640, temperature = 0.2, onToken } = {}) {
|
| 43 |
+
if (this.state !== 'ready') throw new Error('modelo local no cargado');
|
| 44 |
+
const stream = await this.wllama.createChatCompletion({ messages, max_tokens: maxTokens, temperature, stream: true });
|
| 45 |
+
let answer = '';
|
| 46 |
+
for await (const chunk of stream) {
|
| 47 |
+
const delta = (chunk.choices && chunk.choices[0] && chunk.choices[0].delta) || {};
|
| 48 |
+
const tk = delta.content || '';
|
| 49 |
+
if (tk) { answer += tk; if (onToken) onToken(answer); }
|
| 50 |
+
}
|
| 51 |
+
return answer;
|
| 52 |
+
},
|
| 53 |
+
};
|
| 54 |
+
globalThis.APLocalModel = APLocalModel;
|