Zum Inhalt springen

MediaWiki:Gadget-LabelScanIndexer.js

Aus ADOS Wiki
Version vom 9. November 2025, 16:16 Uhr von Admin (Diskussion | Beiträge) (Die Seite wurde neu angelegt: „Gadget: LabelScanIndexer * Lädt auf der Seite Hilfe:LabelScan-Indexer * Erzeugt Embeddings lokal (CLIP) und speichert automatisch in MediaWiki:Gadget-LabelScan-index.json: if (mw.config.get('wgPageName') !== 'Hilfe:LabelScan-Indexer') { // Läuft nur auf der Indexer-Seite return; } (function(){ const INDEX_TITLE = 'MediaWiki:Gadget-LabelScan-index.json'; // Modell / Pfade (müssen zu deinem Setup passen) const transformersURL = 'http…“)
(Unterschied) ← Nächstältere Version | Aktuelle Version (Unterschied) | Nächstjüngere Version → (Unterschied)

Hinweis: Leere nach dem Veröffentlichen den Browser-Cache, um die Änderungen sehen zu können.

  • Firefox/Safari: Umschalttaste drücken und gleichzeitig Aktualisieren anklicken oder entweder Strg+F5 oder Strg+R (⌘+R auf dem Mac) drücken
  • Google Chrome: Umschalttaste+Strg+R (⌘+Umschalttaste+R auf dem Mac) drücken
  • Edge: Strg+F5 drücken oder Strg drücken und gleichzeitig Aktualisieren anklicken
/* Gadget: LabelScanIndexer
 * Lädt auf der Seite Hilfe:LabelScan-Indexer
 * Erzeugt Embeddings lokal (CLIP) und speichert automatisch in MediaWiki:Gadget-LabelScan-index.json
 */

if (mw.config.get('wgPageName') !== 'Hilfe:LabelScan-Indexer') {
  // Läuft nur auf der Indexer-Seite
  return;
}

(function(){
  const INDEX_TITLE = 'MediaWiki:Gadget-LabelScan-index.json';

  // Modell / Pfade (müssen zu deinem Setup passen)
  const transformersURL = 'https://cdn.jsdelivr.net/npm/@xenova/transformers@2.15.0';
  const MODEL_ID = 'Xenova/clip-vit-base-patch32';
  const LOCAL_MODEL_PATH = '/models'; // WICHTIG: Du hast deine Modelle unter /models/… liegen

  const $ = id => document.getElementById(id);
  const status = (t) => { const el=$('idx-status'); if(el) el.textContent=t||''; };

  function hasSysop(){
    const g = mw.config.get('wgUserGroups') || [];
    return g.includes('sysop') || g.includes('interface-admin');
  }

  function float32ToBase64(vec){
    const bytes = new Uint8Array(vec.buffer);
    let bin = '', chunk = 0x8000;
    for (let i=0; i<bytes.length; i+=chunk) {
      bin += String.fromCharCode.apply(null, bytes.subarray(i, i+chunk));
    }
    return btoa(bin);
  }

  async function fileToCanvasExif(file){
    if ('createImageBitmap' in window) {
      const bmp = await createImageBitmap(file, { imageOrientation: 'from-image' });
      if ('OffscreenCanvas' in window) {
        const c = new OffscreenCanvas(bmp.width, bmp.height);
        c.getContext('2d').drawImage(bmp, 0, 0);
        return c;
      } else {
        const c = document.createElement('canvas');
        c.width = bmp.width; c.height = bmp.height;
        c.getContext('2d').drawImage(bmp, 0, 0);
        return c;
      }
    } else {
      const url = URL.createObjectURL(file);
      try {
        const img = await new Promise((res, rej)=>{
          const im = new Image();
          im.onload = ()=>res(im);
          im.onerror = rej;
          im.src = url;
        });
        const c = document.createElement('canvas');
        c.width = img.width; c.height = img.height;
        c.getContext('2d').drawImage(img, 0, 0);
        return c;
      } finally {
        URL.revokeObjectURL(url);
      }
    }
  }

  let _modelPromise;
  async function ensureModel(){
    if (_modelPromise) return _modelPromise;
    _modelPromise = (async()=>{
      const mod = await import(/* webpackIgnore: true */ transformersURL);

      mod.env.allowLocalModels = true;
      mod.env.allowRemoteModels = false;
      mod.env.localModelPath = LOCAL_MODEL_PATH;

      mod.env.backends = mod.env.backends || {};
      mod.env.backends.onnx = mod.env.backends.onnx || {};
      mod.env.backends.onnx.wasm = mod.env.backends.onnx.wasm || {};
      mod.env.backends.onnx.wasm.wasmPaths =
        'https://cdn.jsdelivr.net/npm/@xenova/transformers@2.15.0/dist/';

      const [processor, model] = await Promise.all([
        mod.AutoProcessor.from_pretrained(MODEL_ID),
        mod.CLIPVisionModelWithProjection.from_pretrained(MODEL_ID, { quantized: true })
      ]);

      console.log('[LabelScanIndexer] Modell geladen');
      return { mod, processor, model };
    })();
    return _modelPromise;
  }

  async function buildEmbeddingFromFile(file){
    const { mod, processor, model } = await ensureModel();
    const canvas = await fileToCanvasExif(file);
    const blob = (canvas.convertToBlob)
      ? await canvas.convertToBlob({ type:'image/jpeg', quality:0.95 })
      : await new Promise(r => canvas.toBlob(r, 'image/jpeg', 0.95));
    const raw = await mod.RawImage.fromBlob(blob);
    const inputs = await processor(raw, { return_tensors: 'pt' });
    const out = await model.forward({ pixel_values: inputs.pixel_values });

    const vec = out?.image_embeds?.data || out?.image_embeds;
    if (!(vec instanceof Float32Array)) throw new Error('Embedding-Format unerwartet');

    let n=0; for(let i=0;i<vec.length;i++) n+=vec[i]*vec[i];
    const norm = Math.sqrt(n)||1;
    const v = new Float32Array(vec.length);
    for(let i=0;i<vec.length;i++) v[i]=vec[i]/norm;
    return v;
  }

  async function fetchIndexJSON(){
    const url = mw.util.getUrl(INDEX_TITLE, { action:'raw', ctype:'application/json' });
    const res = await fetch(url, { cache:'no-store' });
    if (!res.ok) throw new Error('Index nicht ladbar: '+res.status);
    try { return JSON.parse(await res.text()) || []; }
    catch(_){ return []; }
  }

  async function saveIndexJSON(newArray){
    await mw.loader.using(['mediawiki.api']);
    const api = new mw.Api();
    const text = JSON.stringify(newArray, null, 2) + '\n';

    return api.postWithToken('csrf', {
      action: 'edit',
      title: INDEX_TITLE,
      text,
      summary: 'LabelScan: +1 embedding (Auto-Indexer)',
      nocreate: 0,
      bot: 1
    });
  }

  $('idx-run').addEventListener('click', async ()=>{
    try{
      if (!hasSysop()) return alert('⚠️ Du brauchst Admin/Interface-Rechte.');

      const title = $('idx-title').value.trim();
      const thumb = $('idx-thumb').value.trim();
      const file  = $('idx-file').files?.[0];

      if (!title) return alert('Titel fehlt.');
      if (!file)  return alert('Bitte Bild wählen.');

      status('Embedding berechnen …');
      const vec = await buildEmbeddingFromFile(file);
      const b64 = float32ToBase64(vec);

      $('idx-out').value = JSON.stringify({title, thumb, embed:b64}, null, 2);

      const arr = await fetchIndexJSON();
      arr.push({ title, thumb, embed:b64 });

      status('Speichern …');
      await saveIndexJSON(arr);

      status('Gespeichert ✅');
    } catch(e){
      console.error(e);
      alert('Fehler: '+e.message);
      status('Fehler ❌');
    }
  });

  console.log('[LabelScanIndexer] bereit');
})();