Zum Inhalt springen

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

Aus ADOS Wiki
Der Seiteninhalt wurde durch einen anderen Text ersetzt: „global mw: console.log('[LabelScan] smoke test loaded'); document.addEventListener('DOMContentLoaded', function () { console.log('[LabelScan] DOM ready'); var btn = document.getElementById('ados-scan-run'); if (btn) { btn.addEventListener('click', function () { alert('Click OK – Gadget greift!'); }, { once: true }); } });“
Markierung: Ersetzt
Keine Bearbeitungszusammenfassung
Zeile 1: Zeile 1:
/* global mw */
/* global mw */
console.log('[LabelScan] smoke test loaded');
(function () {
document.addEventListener('DOMContentLoaded', function () {
  'use strict';
  console.log('[LabelScan] DOM ready');
 
   var btn = document.getElementById('ados-scan-run');
  // ---- Basistools ----
   if (btn) {
  function $(id){return document.getElementById(id);}
     btn.addEventListener('click', function () {
  function esc(s){return mw.html.escape(String(s||''));}
      alert('Click OK – Gadget greift!');
  function setStatus(t){ var el=$('ados-scan-status'); if(el) el.textContent=t||''; }
     }, { once: true });
   function setProgress(p){ var 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));}}
 
  // ---- Preview ----
   function showPreview(file){
     try{ var url=URL.createObjectURL(file);
      var prev=$('ados-scan-preview');
      if(prev){ prev.innerHTML='<img alt="Vorschau" style="max-width:100%;height:auto;border-radius:8px;" src="'+url+'">'; prev.setAttribute('aria-hidden','false'); }
     }catch(e){ console.warn('[LabelScan] Preview fail:',e); }
   }
   }
});
 
  // ---- CLIP bereitstellen (transformers.js + Pipeline) ----
  let CLIP_READY=null;
  async function ensureClipExtractor(){
    if(CLIP_READY) return CLIP_READY;
    CLIP_READY = new Promise((resolve,reject)=>{
      if(!window.transformers){
        const s=document.createElement('script');
        s.src='https://cdn.jsdelivr.net/npm/@xenova/transformers/dist/transformers.min.js';
        s.async=true;
        s.onload=async ()=>{ try{
          const pipe=await window.transformers.pipeline('image-feature-extraction','Xenova/clip-vit-base-patch32');
          resolve(pipe);
        }catch(e){reject(e);} };
        s.onerror=reject;
        document.head.appendChild(s);
      }else{
        window.transformers.pipeline('image-feature-extraction','Xenova/clip-vit-base-patch32').then(resolve,reject);
      }
    });
    return CLIP_READY;
  }
 
  // ---- Index laden (mit embed) ----
  function decodeEmbed(b64){
    const bin=atob(b64), len=bin.length, bytes=new Uint8Array(len);
    for(let i=0;i<len;i++) bytes[i]=bin.charCodeAt(i);
    return new Float32Array(bytes.buffer);
  }
  function normalizeVec(v){
    let n=0; for(let i=0;i<v.length;i++) n+=v[i]*v[i];
    n=Math.sqrt(n)||1; const out=new Float32Array(v.length);
    for(let i=0;i<v.length;i++) out[i]=v[i]/n; return out;
  }
  function cosine(a,b){ let s=0; for(let i=0;i<a.length;i++) s+=a[i]*b[i]; return s; }
 
  let ADOS_INDEX=null;
  async function loadLabelIndex(){
    if(ADOS_INDEX) return ADOS_INDEX;
    const page='MediaWiki:Gadget-LabelScan-index.json';
    const url=mw.util.getUrl(page,{action:'raw',ctype:'application/json',maxage:0,smaxage:0,_:Date.now()});
    const res=await fetch(url,{cache:'no-store'});
    const txt=await res.text();
    const json=JSON.parse(txt.replace(/^\uFEFF/,''));
    if(!Array.isArray(json)||!json.length||typeof json[0].embed!=='string'){
      throw new Error('Index leer/ohne embed');
    }
    ADOS_INDEX = json.map(it=>({title:it.title, thumb:it.thumb, vec: normalizeVec(decodeEmbed(it.embed))}));
    console.log('[LabelScan] Index OK:', ADOS_INDEX.length, 'items; dim=', ADOS_INDEX[0].vec.length);
    return ADOS_INDEX;
  }
 
  async function embedFileImage(file){
    const extractor=await ensureClipExtractor();
    const url=URL.createObjectURL(file);
    try{
      const feat=await extractor(url);
      const v=normalizeVec(feat.data);
      const norm=Math.sqrt(v.reduce((a,c)=>a+c*c,0)).toFixed(3);
      console.log('[LabelScan] query norm ~', norm, 'first3=', Array.from(v.slice(0,3)).map(x=>x.toFixed(3)).join(', '));
      return v;
    }finally{ URL.revokeObjectURL(url); }
  }
 
  function rankMatches(qvec, index, topK, threshold){
    const scored=index.map(it=>({it, s: cosine(qvec,it.vec)})).sort((a,b)=>b.s-a.s);
    const out=[]; for(const r of scored){ if(threshold!=null && r.s<threshold) break; out.push({title:r.it.title,thumb:r.it.thumb,score:r.s}); if(out.length>=topK) break; }
    return out;
  }
 
  function renderResults(items){
    const box=$('ados-scan-results'); if(!box) return;
    box.innerHTML='';
    if(!items||!items.length){ box.innerHTML='<div class="ados-hit">Keine klaren Treffer. Bitte anderes Foto oder manuell suchen.</div>'; return; }
    items.forEach(it=>{
      const link=mw.util.getUrl(it.title.replace(/ /g,'_'));
      const row=document.createElement('div'); row.className='ados-hit';
      row.innerHTML =
        '<div style="display:flex;gap:10px;align-items:flex-start;">' +
        (it.thumb?`<img src="${it.thumb}" alt="" style="width:64px;height:auto;border-radius:6px;">`:'') +
        `<div><b><a href="${link}">${esc(it.title)}</a></b>` +
        (typeof it.score==='number'?`<div class="meta">Score: ${(it.score*100).toFixed(1)}%</div>`:'') +
        '</div></div>';
      box.appendChild(row);
    });
  }
 
  // ---- Binding ----
  function hasUI(){ return $('ados-scan-file') && $('ados-scan-run'); }
 
  let BOUND=false;
  function bind(){
    if(BOUND||!hasUI()) return;
    const fileIn=$('ados-scan-file'), runBtn=$('ados-scan-run');
    const photoBtn=$('ados-scan-photo'), pickBtn=$('ados-scan-pick'), bigBtn=$('ados-scan-bigbtn');
    if(!fileIn||!runBtn) return;
 
    if(runBtn.dataset.bound==='1') return;
    runBtn.dataset.bound='1'; BOUND=true;
 
    // Buttons Kamera/Bild
    function clickInput(withCapture){
      try {
        if(withCapture){ fileIn.setAttribute('capture','environment'); }
        else { fileIn.removeAttribute('capture'); }
      } catch {}
      fileIn.click();
    }
    if(photoBtn) photoBtn.addEventListener('click', ()=> clickInput(true));
    if(pickBtn)  pickBtn.addEventListener('click', ()=> clickInput(false));
    if(bigBtn)  bigBtn.addEventListener('click', ()=> clickInput(true));
 
    fileIn.addEventListener('change', function(){ if(this.files&&this.files[0]) showPreview(this.files[0]); });
 
    runBtn.addEventListener('click', async (ev)=>{
      ev.preventDefault();
      const f=fileIn.files && fileIn.files[0];
      if(!f){ alert('Bitte ein Bild wählen oder aufnehmen.'); return; }
 
      runBtn.disabled=true; if(photoBtn) photoBtn.disabled=true; if(pickBtn) pickBtn.disabled=true;
      try{
        setStatus('Modell laden …');
        await ensureClipExtractor();
 
        setStatus('Index laden …');
        const index=await loadLabelIndex();
 
        setStatus('Bild einlesen …'); setProgress(0.1);
        const qvec=await embedFileImage(f);
 
        setStatus('Vergleiche …'); setProgress(0.2);
        const matches=rankMatches(qvec, index, 6, 0.82);
 
        renderResults(matches);
        setStatus(matches.length?'Fertig.':'Keine klaren Treffer – anderes Foto probieren.');
      }catch(e){
        console.error('[LabelScan] Fehler:', e);
        setStatus('Fehler bei Erkennung/Suche.');
      }finally{
        setProgress(null);
        runBtn.disabled=false; if(photoBtn) photoBtn.disabled=false; if(pickBtn) pickBtn.disabled=false;
      }
    });
 
    console.log('[LabelScan] Gadget gebunden.');
  }
 
  if(document.readyState==='loading'){ document.addEventListener('DOMContentLoaded', bind); } else { bind(); }
  setTimeout(bind,250); setTimeout(bind,1000);
  const mo=new MutationObserver(()=>{ if(!BOUND) bind(); });
  mo.observe(document.documentElement||document.body,{childList:true,subtree:true});
 
})();

Version vom 8. November 2025, 16:18 Uhr

/* global mw */
(function () {
  'use strict';

  // ---- Basistools ----
  function $(id){return document.getElementById(id);}
  function esc(s){return mw.html.escape(String(s||''));}
  function setStatus(t){ var el=$('ados-scan-status'); if(el) el.textContent=t||''; }
  function setProgress(p){ var 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));}}

  // ---- Preview ----
  function showPreview(file){
    try{ var url=URL.createObjectURL(file);
      var prev=$('ados-scan-preview');
      if(prev){ prev.innerHTML='<img alt="Vorschau" style="max-width:100%;height:auto;border-radius:8px;" src="'+url+'">'; prev.setAttribute('aria-hidden','false'); }
    }catch(e){ console.warn('[LabelScan] Preview fail:',e); }
  }

  // ---- CLIP bereitstellen (transformers.js + Pipeline) ----
  let CLIP_READY=null;
  async function ensureClipExtractor(){
    if(CLIP_READY) return CLIP_READY;
    CLIP_READY = new Promise((resolve,reject)=>{
      if(!window.transformers){
        const s=document.createElement('script');
        s.src='https://cdn.jsdelivr.net/npm/@xenova/transformers/dist/transformers.min.js';
        s.async=true;
        s.onload=async ()=>{ try{
          const pipe=await window.transformers.pipeline('image-feature-extraction','Xenova/clip-vit-base-patch32');
          resolve(pipe);
        }catch(e){reject(e);} };
        s.onerror=reject;
        document.head.appendChild(s);
      }else{
        window.transformers.pipeline('image-feature-extraction','Xenova/clip-vit-base-patch32').then(resolve,reject);
      }
    });
    return CLIP_READY;
  }

  // ---- Index laden (mit embed) ----
  function decodeEmbed(b64){
    const bin=atob(b64), len=bin.length, bytes=new Uint8Array(len);
    for(let i=0;i<len;i++) bytes[i]=bin.charCodeAt(i);
    return new Float32Array(bytes.buffer);
  }
  function normalizeVec(v){
    let n=0; for(let i=0;i<v.length;i++) n+=v[i]*v[i];
    n=Math.sqrt(n)||1; const out=new Float32Array(v.length);
    for(let i=0;i<v.length;i++) out[i]=v[i]/n; return out;
  }
  function cosine(a,b){ let s=0; for(let i=0;i<a.length;i++) s+=a[i]*b[i]; return s; }

  let ADOS_INDEX=null;
  async function loadLabelIndex(){
    if(ADOS_INDEX) return ADOS_INDEX;
    const page='MediaWiki:Gadget-LabelScan-index.json';
    const url=mw.util.getUrl(page,{action:'raw',ctype:'application/json',maxage:0,smaxage:0,_:Date.now()});
    const res=await fetch(url,{cache:'no-store'});
    const txt=await res.text();
    const json=JSON.parse(txt.replace(/^\uFEFF/,''));
    if(!Array.isArray(json)||!json.length||typeof json[0].embed!=='string'){
      throw new Error('Index leer/ohne embed');
    }
    ADOS_INDEX = json.map(it=>({title:it.title, thumb:it.thumb, vec: normalizeVec(decodeEmbed(it.embed))}));
    console.log('[LabelScan] Index OK:', ADOS_INDEX.length, 'items; dim=', ADOS_INDEX[0].vec.length);
    return ADOS_INDEX;
  }

  async function embedFileImage(file){
    const extractor=await ensureClipExtractor();
    const url=URL.createObjectURL(file);
    try{
      const feat=await extractor(url);
      const v=normalizeVec(feat.data);
      const norm=Math.sqrt(v.reduce((a,c)=>a+c*c,0)).toFixed(3);
      console.log('[LabelScan] query norm ~', norm, 'first3=', Array.from(v.slice(0,3)).map(x=>x.toFixed(3)).join(', '));
      return v;
    }finally{ URL.revokeObjectURL(url); }
  }

  function rankMatches(qvec, index, topK, threshold){
    const scored=index.map(it=>({it, s: cosine(qvec,it.vec)})).sort((a,b)=>b.s-a.s);
    const out=[]; for(const r of scored){ if(threshold!=null && r.s<threshold) break; out.push({title:r.it.title,thumb:r.it.thumb,score:r.s}); if(out.length>=topK) break; }
    return out;
  }

  function renderResults(items){
    const box=$('ados-scan-results'); if(!box) return;
    box.innerHTML='';
    if(!items||!items.length){ box.innerHTML='<div class="ados-hit">Keine klaren Treffer. Bitte anderes Foto oder manuell suchen.</div>'; return; }
    items.forEach(it=>{
      const link=mw.util.getUrl(it.title.replace(/ /g,'_'));
      const row=document.createElement('div'); row.className='ados-hit';
      row.innerHTML =
        '<div style="display:flex;gap:10px;align-items:flex-start;">' +
        (it.thumb?`<img src="${it.thumb}" alt="" style="width:64px;height:auto;border-radius:6px;">`:'') +
        `<div><b><a href="${link}">${esc(it.title)}</a></b>` +
        (typeof it.score==='number'?`<div class="meta">Score: ${(it.score*100).toFixed(1)}%</div>`:'') +
        '</div></div>';
      box.appendChild(row);
    });
  }

  // ---- Binding ----
  function hasUI(){ return $('ados-scan-file') && $('ados-scan-run'); }

  let BOUND=false;
  function bind(){
    if(BOUND||!hasUI()) return;
    const fileIn=$('ados-scan-file'), runBtn=$('ados-scan-run');
    const photoBtn=$('ados-scan-photo'), pickBtn=$('ados-scan-pick'), bigBtn=$('ados-scan-bigbtn');
    if(!fileIn||!runBtn) return;

    if(runBtn.dataset.bound==='1') return;
    runBtn.dataset.bound='1'; BOUND=true;

    // Buttons Kamera/Bild
    function clickInput(withCapture){
      try {
        if(withCapture){ fileIn.setAttribute('capture','environment'); }
        else { fileIn.removeAttribute('capture'); }
      } catch {}
      fileIn.click();
    }
    if(photoBtn) photoBtn.addEventListener('click', ()=> clickInput(true));
    if(pickBtn)  pickBtn.addEventListener('click', ()=> clickInput(false));
    if(bigBtn)   bigBtn.addEventListener('click', ()=> clickInput(true));

    fileIn.addEventListener('change', function(){ if(this.files&&this.files[0]) showPreview(this.files[0]); });

    runBtn.addEventListener('click', async (ev)=>{
      ev.preventDefault();
      const f=fileIn.files && fileIn.files[0];
      if(!f){ alert('Bitte ein Bild wählen oder aufnehmen.'); return; }

      runBtn.disabled=true; if(photoBtn) photoBtn.disabled=true; if(pickBtn) pickBtn.disabled=true;
      try{
        setStatus('Modell laden …');
        await ensureClipExtractor();

        setStatus('Index laden …');
        const index=await loadLabelIndex();

        setStatus('Bild einlesen …'); setProgress(0.1);
        const qvec=await embedFileImage(f);

        setStatus('Vergleiche …'); setProgress(0.2);
        const matches=rankMatches(qvec, index, 6, 0.82);

        renderResults(matches);
        setStatus(matches.length?'Fertig.':'Keine klaren Treffer – anderes Foto probieren.');
      }catch(e){
        console.error('[LabelScan] Fehler:', e);
        setStatus('Fehler bei Erkennung/Suche.');
      }finally{
        setProgress(null);
        runBtn.disabled=false; if(photoBtn) photoBtn.disabled=false; if(pickBtn) pickBtn.disabled=false;
      }
    });

    console.log('[LabelScan] Gadget gebunden.');
  }

  if(document.readyState==='loading'){ document.addEventListener('DOMContentLoaded', bind); } else { bind(); }
  setTimeout(bind,250); setTimeout(bind,1000);
  const mo=new MutationObserver(()=>{ if(!BOUND) bind(); });
  mo.observe(document.documentElement||document.body,{childList:true,subtree:true});

})();