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

Keine Bearbeitungszusammenfassung
Keine Bearbeitungszusammenfassung
Zeile 10: Zeile 10:
     modelId: 'Xenova/clip-vit-base-patch32',
     modelId: 'Xenova/clip-vit-base-patch32',
     maxSide: 1024,
     maxSide: 1024,
     debug: true
     debug: true,
 
    // ⇩ Für Debug: Modell auch dann laden, wenn Index keine Embeddings hat
    forceModelWarmup: true
   };
   };


Zeile 55: Zeile 58:
     });
     });


     log('Index geladen:',INDEX.length,'Einträge');
    const withEmb = INDEX_EMB.filter(v=>v && v.length).length;
     log('Index geladen:', INDEX.length, 'Einträge');
    log('Embeddings vorhanden:', withEmb, '/', INDEX.length);
 
    // Exponiere Debug-Infos ins Fenster
    window._LabelScan = window._LabelScan || {};
    window._LabelScan.indexInfo = { total: INDEX.length, withEmbeddings: withEmb };
 
     if(ui) setProgress(0.06);
     if(ui) setProgress(0.06);
     return INDEX;
     return INDEX;
Zeile 68: Zeile 78:
   }
   }


  // ------------------------- CLIP / Transformers -------------------------
   let _clipModulePromise=null;
   let _clipModulePromise=null;
   async function ensureClipExtractor(){
   async function ensureClipExtractor(){
     if(_clipModulePromise) return _clipModulePromise;
     if(_clipModulePromise) return _clipModulePromise;
Zeile 76: Zeile 88:


     _clipModulePromise = (async()=>{
     _clipModulePromise = (async()=>{
       const mod = await import(/* webpackIgnore: true */ CFG.transformersURL);
       try{
        const mod = await import(/* webpackIgnore: true */ CFG.transformersURL);


      mod.env.localModelPath=null;
        mod.env.localModelPath=null;
      mod.env.remoteModels=true;
        mod.env.remoteModels=true;
      mod.env.allowRemoteModels=true;
        mod.env.allowRemoteModels=true;
      mod.env.useBrowserCache=true;
        mod.env.useBrowserCache=true;


      // ✅ FIX: Task geändert
        const pipe = await mod.pipeline(
      const pipe = await mod.pipeline(
          'feature-extraction',
        'feature-extraction',
          CFG.modelId,
        CFG.modelId,
          { quantized:true }
        { quantized:true }
        );
      );


      log('CLIP ready:', pipe.model?.constructor?.name || 'unknown');
        log('CLIP ready:', pipe.model?.constructor?.name || 'unknown');
      return { mod, pipe };
        return { mod, pipe };
      } catch(e){
        err('CLIP load failed:', e);
        throw e;
      }
     })();
     })();


Zeile 114: Zeile 130:
       w=Math.round(w*s); h=Math.round(h*s);
       w=Math.round(w*s); h=Math.round(h*s);
       c.width=w; c.height=h;
       c.width=w; c.height=h;
       c.getContext('2d').drawImage(img,0,0,w,h);
       const g=c.getContext('2d');
      g.imageSmoothingEnabled=true;
      g.drawImage(img,0,0,w,h);
       return c;
       return c;
     }
     }
Zeile 128: Zeile 146:
     const out = await pipe(canvas);
     const out = await pipe(canvas);
     const raw = out && out.data;
     const raw = out && out.data;
     const vec = raw instanceof Float32Array ? raw : new Float32Array(raw);
 
     let vec;
    if (raw instanceof Float32Array) {
      vec = raw;
    } else if (Array.isArray(raw)) {
      // 1D/2D -> Float32
      vec = Array.isArray(raw[0]) ? meanPool2D(raw) : new Float32Array(raw);
    } else {
      throw new Error('Embedding-Format unerwartet');
    }
     return normalize(vec);
     return normalize(vec);
  }
  function meanPool2D(arr2d){
    const rows=arr2d.length;
    const dim=rows?arr2d[0].length:0;
    const sum=new Float32Array(dim);
    for(let r=0;r<rows;r++){
      const row=arr2d[r];
      for(let i=0;i<dim;i++) sum[i]+=row[i]||0;
    }
    for(let i=0;i<dim;i++) sum[i]/=(rows||1);
    return sum;
   }
   }


   function normalize(v){
   function normalize(v){
     let n=0; for(let i=0;i<v.length;i++) n+=v[i]*v[i];
     let n=0; for(let i=0;i<v.length;i++) n+=v[i]*i? v[i]:v[i]*v[i]; // avoid JIT weirdness
    n=0; for(let i=0;i<v.length;i++) n+=v[i]*v[i];
     n=Math.sqrt(n)||1;
     n=Math.sqrt(n)||1;
     const o=new Float32Array(v.length);
     const o=new Float32Array(v.length);
Zeile 139: Zeile 179:
     return o;
     return o;
   }
   }
   function cosine(a,b){ let s=0, L=Math.min(a.length,b.length); for(let i=0;i<L;i++) s+=a[i]*b[i]; return s; }
   function cosine(a,b){ let s=0,L=Math.min(a.length,b.length); for(let i=0;i<L;i++) s+=a[i]*b[i]; return s; }


  // ------------------------- Ranking / Render -------------------------
   function rankByCosine(q){
   function rankByCosine(q){
     const s=[];
     const s=[];
Zeile 165: Zeile 206:
           ${thumb?`<img src="${thumb}" style="width:60px;border-radius:6px;">`:`<div></div>`}
           ${thumb?`<img src="${thumb}" style="width:60px;border-radius:6px;">`:`<div></div>`}
           <div><b><a href="${link}">${mw.html.escape(it.title||'')}</a></b></div>
           <div><b><a href="${link}">${mw.html.escape(it.title||'')}</a></b></div>
           <div style="color:#666">${score.toFixed(3)}</div>
           <div style="color:#666;font-variant-numeric:tabular-nums">${score.toFixed(3)}</div>
         </div>`;
         </div>`;
     });
     });
   }
   }


  // ------------------------- UI Bindings -------------------------
   let BOUND=false;
   let BOUND=false;
   function bindUI(){
   function bindUI(){
     if(BOUND) return;
     if(BOUND) return;
     const btnRun=qs('ados-scan-run');
     const btnRun=qs('ados-scan-run');
     const inCam=qs('ados-scan-file-camera');
     const inCam=qs('ados-scan-file-c
    const inGal=qs('ados-scan-file-gallery');
    const btnReset=qs('ados-scan-reset');
    const btnCam=qs('ados-scan-btn-camera');
    const btnGal=qs('ados-scan-btn-gallery');
    const drop=qs('ados-scan-drop');
 
    if(!btnRun||!inCam||!inGal) return;
 
    btnCam && btnCam.addEventListener('click',()=>inCam.click());
    btnGal && btnGal.addEventListener('click',()=>inGal.click());
 
    const pick=e=>{ const f=e.target.files?.[0]; if(f) showPreview(f); };
    inCam.addEventListener('change',pick);
    inGal.addEventListener('change',pick);
 
    if(drop){
      drop.addEventListener('dragover',ev=>{ev.preventDefault();drop.classList.add('is-over');});
      drop.addEventListener('dragleave',()=>drop.classList.remove('is-over'));
      drop.addEventListener('drop',ev=>{
        ev.preventDefault();drop.classList.remove('is-over');
        const f=ev.dataTransfer?.files?.[0];
        if(f){ const dt=new DataTransfer(); dt.items.add(f); inGal.files=dt.files; showPreview(f); }
      });
    }
 
    btnReset && btnReset.addEventListener('click',()=>{
      setStatus('Bereit.'); setProgress(null);
      qs('ados-scan-preview').innerHTML='<div class="note">Noch keine Vorschau.</div>';
      qs('ados-scan-results').innerHTML='<div class="empty">Hier erscheinen Treffer.</div>';
      inCam.value=''; inGal.value='';
    });
 
    btnRun.addEventListener('click',onRunClick);
 
    BOUND=true;
    log('UI gebunden.');
  }
 
  async function onRunClick(){
    try{
      const inCam=qs('ados-scan-file-camera');
      const inGal=qs('ados-scan-file-gallery');
      const btnRun=qs('ados-scan-run');
 
      const file=inCam.files?.[0]||inGal.files?.[0];
      if(!file){ alert('Bitte zuerst ein Foto auswählen.'); return; }
 
      btnRun.disabled=true;
      await loadIndex({ ui:true });
 
      if(!INDEX_EMB.some(v=>v&&v.length)){
        setStatus('Index enthält keine Embeddings.');
        setProgress(null);
        return;
      }
 
      const q=await embedFileImage(file);
      setProgress(0.70);
      setStatus('Abgleich …');
 
      renderResults(rankByCosine(q));
      setStatus('Fertig.');
      setProgress(null);
    }catch(e){
      err('Fehler',e);
      setStatus('Fehler bei Erkennung.');
      setProgress(null);
    }finally{
      const btnRun=qs('ados-scan-run');
      if(btnRun) btnRun.disabled=false;
    }
  }
 
  function init(){
    if(document.readyState==='loading'){
      document.addEventListener('DOMContentLoaded',bindUI,{once:true});
    } else bindUI();
 
    // Warm load ohne UI
    loadIndex({ ui:false }).catch(err).finally(()=>{
      setStatus('Bereit.');
      setProgress(null);
    });
  }
 
  log('gadget file loaded');
  init();
 
})();