MediaWiki:Gadget-LabelScan.js: Unterschied zwischen den Versionen
Erscheinungsbild
Admin (Diskussion | Beiträge) 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 |
Admin (Diskussion | Beiträge) Keine Bearbeitungszusammenfassung |
||
| Zeile 1: | Zeile 1: | ||
/* global mw */ | /* global mw */ | ||
(function () { | |||
'use strict'; | |||
var | // ---- 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}); | |||
})(); | |||
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});
})();