MediaWiki:Gadget-LabelScan.js: Unterschied zwischen den Versionen

Keine Bearbeitungszusammenfassung
Der Seiteninhalt wurde durch einen anderen Text ersetzt: „LabelScan Gadget (Syntax-Test): (function () { 'use strict'; function log(msg) { console.log('[LabelScan TEST]', msg); } // Test UI Hooks document.addEventListener('DOMContentLoaded', function () { const btn = document.querySelector('#ados-scan-run'); if (!btn) { log('UI nicht gefunden (das ist ok, wenn du nicht auf der LabelScan Seite bist)'); return;…“
Markierung: Ersetzt
Zeile 1: Zeile 1:
/* LabelScan – Bildähnlichkeit mit CLIP (UMD Loader, robust)
/* LabelScan Gadget (Syntax-Test) */
  Lädt @xenova/transformers (UMD) per <script>, baut einen lokalen Bildindex
  aus Abfüllungs-Thumbnails und sucht die ähnlichsten Seiten.
*/
/* global mw */
(() => {
  'use strict';


  // ======== KONFIG ========
(function () {
  const CATEGORIES = [
     'use strict';
    'Alle A Dream of Scotland Abfüllungen',
    'Alle A Dream of Ireland Abfüllungen',
    'Alle A Dream of... – Der Rest der Welt Abfüllungen',
    'Cigar Malt Übersicht',
    'Rumbastic Abfüllungen',
    'The Tasteful 8',
    'Còmhlan Abfüllungen',
    'Friendly Mr. Z Whiskytainment Abfüllungen',
     'Die Whisky Elfen Abfüllungen',
    'The Fine Art of Whisky Abfüllungen',
    'The Forbidden Kingdom',
    'Sonderabfüllungen'
  ];
  const THUMB_SIZE = 512;
  const TOP_K = 8;
  const IDB = { name: 'ados-labelscan', store: 'index', version: 2 }; // <- Version erhöht


  const UMD_URL = 'https://cdn.jsdelivr.net/npm/@xenova/transformers@3.0.0/dist/transformers.umd.min.js';
     function log(msg) {
 
         console.log('[LabelScan TEST]', msg);
  // ======== UI Helpers ========
  const $ = (s) => document.querySelector(s);
  function setStatus(t){ const el = $('#ados-scan-status'); if (el) el.textContent = t || ''; }
  function setProgress(p){ const bar=$('#ados-scan-progress'); if(!bar) return; if(p==null){bar.hidden=true;bar.value=0;}else{bar.hidden=false;bar.value=Math.max(0,Math.min(1,p));}}
  function showPreview(file){ const url=URL.createObjectURL(file); const box=$('#ados-scan-preview'); if(box) box.innerHTML=`<img alt="Vorschau" src="${url}" style="max-width:260px;border-radius:8px">`; }
  function renderResults(items){
    const box = $('#ados-scan-results'); if(!box) return; box.innerHTML='';
    if(!items || !items.length){ box.innerHTML='<div class="ados-hit">Keine Treffer gefunden.</div>'; return; }
    items.forEach(it=>{
      const url = mw.util.getUrl(it.title.replace(/ /g,'_'));
      const div = document.createElement('div');
      div.className = 'ados-hit';
      div.innerHTML = `
        <a class="thumb" href="${url}"><img alt="" src="${it.thumb}" loading="lazy"></a>
        <div class="meta"><b><a href="${url}">${mw.html.escape(it.title)}</a></b>
        <div class="sub">Ähnlichkeit: ${(it.score*100).toFixed(1)}%</div></div>`;
      box.appendChild(div);
    });
  }
 
  // ======== IndexedDB Mini ========
  function idbOpen(){
    return new Promise((res,rej)=>{
      const req = indexedDB.open(IDB.name, IDB.version);
      req.onupgradeneeded = (e)=>{ const db=req.result; if(e.oldVersion<1) db.createObjectStore(IDB.store,{keyPath:'key'}); };
      req.onsuccess=()=>res(req.result); req.onerror=()=>rej(req.error);
    });
  }
  async function idbGet(key){ const db=await idbOpen(); return new Promise((res,rej)=>{ const tx=db.transaction(IDB.store,'readonly'); const st=tx.objectStore(IDB.store); const r=st.get(key); r.onsuccess=()=>res(r.result?r.result.val:null); r.onerror=()=>rej(r.error);});}
  async function idbSet(key,val){ const db=await idbOpen(); return new Promise((res,rej)=>{ const tx=db.transaction(IDB.store,'readwrite'); const st=tx.objectStore(IDB.store); const r=st.put({key,val,ts:Date.now()}); r.onsuccess=()=>res(); r.onerror=()=>rej(r.error);});}
 
  // ======== Transformers (UMD) laden ========
  let clipReady=null;
  function ensureTransformersUMD(){
    if (clipReady) return clipReady;
    clipReady = new Promise((resolve, reject)=>{
      if (window.transformers && window.transformers.pipeline) return resolve(window.transformers);
      const s=document.createElement('script');
      s.src = UMD_URL;
      s.async = true;
      s.onload = ()=> resolve(window.transformers);
      s.onerror = ()=> reject(new Error('Transformers UMD konnte nicht geladen werden (CSP/CDN geblockt?)'));
      document.head.appendChild(s);
    });
    return clipReady;
  }
 
  // ======== Mathe ========
  function cosine(a,b){ let s=0; for(let i=0;i<a.length;i++) s += a[i]*b[i]; // Vektoren sind schon normalisiert
    // auf 0..1 hübschen (optional)
    return Math.max(0, Math.min(1, (s+1)/2));
  }
 
  // ======== MW API ========
  async function apiGet(p){ await mw.loader.using('mediawiki.api'); const api=new mw.Api(); return api.get(p); }
  async function pagesFromCategory(cat){
    const pages=[]; let cont; do{
      const r=await apiGet({ action:'query', list:'categorymembers', cmtitle:'Category:'+cat, cmtype:'page', cmlimit:'max', ...(cont||{}) });
      (r.query?.categorymembers||[]).forEach(it=>pages.push(it.title));
      cont=r.continue;
     }while(cont);
    return pages;
  }
  async function pageThumbs(titles){
    const out=[]; const chunk=(a,n)=>a.length?[a.slice(0,n),...chunk(a.slice(n),n)]:[];
    for(const batch of chunk(titles,40)){
      const r = await apiGet({ action:'query', prop:'pageimages', piprop:'thumbnail', pithumbsize:THUMB_SIZE, titles:batch.join('|'), formatversion:2 });
      (r.query?.pages||[]).forEach(p=>{ const th=p.thumbnail?.source; if(th) out.push({title:p.title, thumb:th}); });
    }
    return out;
  }
  async function buildGallery(){
    const set=new Set();
    for(const c of CATEGORIES){ const list=await pagesFromCategory(c); list.forEach(t=>set.add(t)); }
    const titles=[...set];
    return pageThumbs(titles);
  }
 
  // ======== Index bauen/laden ========
  async function ensureIndex(report){
    let idx = await idbGet('index-v2');
    if (idx?.items?.length){ report?.(1,1,'Index aus Cache'); return idx; }
 
    report?.(0,1,'Lade Wiki-Bilder …');
    const gallery = await buildGallery();
    if (!gallery.length) return { items: [] };
 
    setStatus('Lade CLIP-Modell … (einmalig)');
    const tf = await ensureTransformersUMD();
    // Optional: Pfade für WASM setzen (nur Info-Log)
    try {
      tf.env.allowRemoteModels = true; // default, aber explizit
    } catch(e){ /* ignore */ }
 
    const pipe = await tf.pipeline('image-feature-extraction', 'Xenova/clip-vit-base-patch32');
 
    const items=[];
    for(let i=0;i<gallery.length;i++){
      const g=gallery[i];
      report?.(i, gallery.length, `Embedding ${i+1}/${gallery.length}: ${g.title}`);
      try{
        const emb = await pipe(g.thumb, { pooling:'mean', normalize:true }); // Float32Array
        items.push({ title:g.title, thumb:g.thumb, vec:Array.from(emb.data) });
      }catch(e){
         console.warn('[LabelScan] Embedding-Fehler', g.title, e);
      }
     }
     }
    const index={ builtAt:Date.now(), items };
    await idbSet('index-v2', index);
    report?.(1,1,'Index gespeichert');
    return index;
  }
  // ======== Suche ========
  async function runSearch(file){
    setProgress(0); setStatus('Baue/ lade Bild-Index …');
    const index = await ensureIndex((i,n,msg)=>{ setStatus(msg||'Index…'); setProgress(n? i/n : null); });
    if(!index.items.length){ renderResults([]); setProgress(null); setStatus('Kein Bildmaterial gefunden.'); return; }
    setStatus('Berechne Embedding vom Foto …'); setProgress(0.1);
    const tf = await ensureTransformersUMD();
    const pipe = await tf.pipeline('image-feature-extraction', 'Xenova/clip-vit-base-patch32');


     const dataURL = await new Promise((res,rej)=>{ const r=new FileReader(); r.onload=()=>res(r.result); r.onerror=rej; r.readAsDataURL(file); });
     // Test UI Hooks
    document.addEventListener('DOMContentLoaded', function () {
        const btn = document.querySelector('#ados-scan-run');
        if (!btn) {
            log('UI nicht gefunden (das ist ok, wenn du nicht auf der LabelScan Seite bist)');
            return;
        }


    const q = await pipe(dataURL, { pooling:'mean', normalize:true });
        btn.addEventListener('click', function (ev) {
    const qv = q.data;
            ev.preventDefault();
            alert("✅ Gadget funktioniert! (Syntax OK)\nJetzt können wir die echte Erkennung einbauen.");
        });


    setStatus('Finde ähnlichste Abfüllungen …'); setProgress(0.2);
         log('✅ Gadget wurde erfolgreich geladen.');
    const scored = index.items.map(it=>({ title:it.title, thumb:it.thumb, score:cosine(qv, it.vec) }))
                              .sort((a,b)=>b.score-a.score)
                              .slice(0, TOP_K);
    renderResults(scored);
    setProgress(null); setStatus('Fertig.');
  }
 
  // ======== Binding ========
  function bind(){
    const runBtn=$('#ados-scan-run'), fileIn=$('#ados-scan-file'), bigBtn=$('#ados-scan-bigbtn');
    if(!runBtn||!fileIn) return;
    if(runBtn.dataset.bound==='1') return; runBtn.dataset.bound='1';
 
    bigBtn && bigBtn.addEventListener('click', ()=>fileIn.click());
    fileIn.addEventListener('change', function(){ if(this.files && this.files[0]) showPreview(this.files[0]); });
 
    runBtn.addEventListener('click', async (ev)=>{
      ev.preventDefault();
      if(!(fileIn.files && fileIn.files[0])){ alert('Bitte ein Foto auswählen oder aufnehmen.'); return; }
      runBtn.disabled=true; runBtn.textContent='Erkenne …';
      try { await runSearch(fileIn.files[0]); }
      catch(e){
        console.error('[LabelScan] Fehler:', e);
         setStatus(
          'Fehler beim Laden/Verarbeiten. Prüfe Konsole. '+
          'Häufige Ursachen: CDN/CSP blockiert oder Netzwerk.'
        );
      } finally {
        runBtn.disabled=false; runBtn.textContent='🔍 Erkennen & suchen';
      }
     });
     });
  }
  if(document.readyState==='loading') document.addEventListener('DOMContentLoaded', bind); else bind();
  new MutationObserver(bind).observe(document.documentElement, { childList:true, subtree:true });
})();
})();