File size: 3,191 Bytes
dae5ea5
 
 
 
 
 
 
 
 
 
 
 
 
23eaea0
 
 
 
 
 
 
 
 
dae5ea5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23eaea0
dae5ea5
 
23eaea0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
dae5ea5
 
 
 
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
/* Modelo local MiMo en el navegador (wllama 3.5.1, WASM/CPU).
   El chat de este Space no depende de ningún servicio externo de modelos:
   el GGUF se descarga desde Hugging Face (aprox. 5,8 GB la primera vez) y se
   reutiliza desde la caché del navegador mientras esta se conserve. */
const WLLAMA_ESM = 'https://cdn.jsdelivr.net/npm/@wllama/wllama@3.5.1/esm/index.js';
const WLLAMA_WASM = 'https://cdn.jsdelivr.net/npm/@wllama/wllama@3.5.1/esm/wasm/wllama.wasm';
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';
export const APLocalModel = {
  state: 'idle', // idle | downloading | ready | error
  error: '',
  wllama: null,
  label: 'MiMo-V2.6-Distill-Qwen-9B (local en este navegador)',
  async cleanupCache() {
    // Solo la caché del propio modelo/wllama de ESTE origen; nada más.
    try {
      for (const name of await caches.keys()) {
        const c = await caches.open(name);
        for (const req of await c.keys()) {
          if (/wllama|MiMo-V2\.6/i.test(req.url)) await c.delete(req);
        }
      }
    } catch (_) {}
  },
  async load(onProgress) {
    if (this.state === 'ready' || this.state === 'downloading') return;
    this.state = 'downloading'; this.error = '';
    try {
      const { Wllama } = await import(WLLAMA_ESM);
      if (this.wllama) { try { await this.wllama.exit(); } catch (_) {} }
      this.wllama = new Wllama({ default: WLLAMA_WASM }, { parallelDownloads: 5 });
      const opts = {
        n_gpu_layers: 0, // WASM/CPU: sin aceleración garantizada
        n_ctx: 4096, n_batch: 512,
        n_threads: Math.max(1, Math.min(8, navigator.hardwareConcurrency || 4)),
        progressCallback: ({ loaded, total }) => { if (onProgress && total > 0) onProgress(loaded, total); },
      };
      try {
        await this.wllama.loadModelFromUrl(MODEL_URL, opts);
      } catch (first) {
        // Caché incompleta o corrupta: limpiar y reintentar una vez.
        await this.cleanupCache();
        await this.wllama.loadModelFromUrl(MODEL_URL, opts);
      }
      this.state = 'ready';
    } catch (e) {
      this.state = 'error'; this.error = String(e && e.message || e).slice(0, 200);
      throw e;
    }
  },
  stop() { this._abort = true; },
  async chat(messages, { maxTokens = 640, temperature = 0.2, onToken } = {}) {
    if (this.state !== 'ready') throw new Error('modelo local no cargado');
    this.state = 'generating'; this._abort = false;
    let stream = null;
    try {
      stream = await this.wllama.createChatCompletion({ messages, max_tokens: maxTokens, temperature, stream: true });
      let answer = '';
      for await (const chunk of stream) {
        if (this._abort) break; // salir del bucle cierra el iterador y frena la generación
        const delta = (chunk.choices && chunk.choices[0] && chunk.choices[0].delta) || {};
        const tk = delta.content || '';
        if (tk) { answer += tk; if (onToken) onToken(answer); }
      }
      return answer;
    } finally {
      this._abort = false;
      if (this.state === 'generating') this.state = 'ready';
    }
  },
};
globalThis.APLocalModel = APLocalModel;