MediaWiki:Gadget-LabelScan.js: Unterschied zwischen den Versionen
Erscheinungsbild
Admin (Diskussion | Beiträge) Keine Bearbeitungszusammenfassung |
Admin (Diskussion | Beiträge) Keine Bearbeitungszusammenfassung |
||
| Zeile 1: | Zeile 1: | ||
/* LabelScan | /* LabelScan – visuelle Erkennung über CLIP (no OCR) */ | ||
(function () { | (function () { | ||
'use strict'; | 'use strict'; | ||
console.log('[LabelScan] CLIP-Erkennung Gadget gestartet'); | |||
console.log('[LabelScan | |||
// | // Kategorien, in denen gesucht werden soll | ||
const ADOS_CATEGORIES = [ | |||
'Alle A Dream of Scotland Abfüllungen', | |||
// | 'Alle A Dream of Ireland Abfüllungen', | ||
if ( | 'Alle A Dream of... – Der Rest der Welt Abfüllungen', | ||
'The Fine Art of Whisky Abfüllungen', | |||
'Die Whisky Elfen Abfüllungen', | |||
'Friendly Mr. Z Whiskytainment Abfüllungen', | |||
'Alle Rumbastic Abfüllungen', | |||
'Cigar Malt Übersicht', | |||
'The Tasteful 8', | |||
'Còmhlan Abfüllungen', | |||
'The Forbidden Kingdom', | |||
'Sonderabfüllungen' | |||
]; | |||
// Laden des CLIP-Modells | |||
let clipReady = null; | |||
function ensureCLIP() { | |||
if (clipReady) return clipReady; | |||
clipReady = new Promise((resolve, reject) => { | |||
mw.loader.using('ext.gadget.aimodels').then(() => { | |||
if (window.CLIP) resolve(); | |||
else reject('CLIP-Modell fehlt'); | |||
}); | |||
}); | |||
return clipReady; | |||
} | |||
// Bild → Embedding | |||
async function embedImage(file) { | |||
await ensureCLIP(); | |||
const img = await CLIP.loadImage(file); | |||
return await CLIP.embedImage(img); | |||
} | |||
// Wiki-Abfüllungsseiten laden & vorberechnen (macht Cache!) | |||
let cache = null; | |||
async function loadDatabase() { | |||
if (cache) return cache; | |||
await mw.loader.using('mediawiki.api'); | |||
const api = new mw.Api(); | |||
const catSearch = ADOS_CATEGORIES.map(c => `incategory:"${c}"`).join(' | '); | |||
const result = await api.get({ | |||
action: 'query', | |||
list: 'search', | |||
srsearch: catSearch, | |||
srlimit: 500, | |||
srnamespace: 0, | |||
formatversion: 2 | |||
}); | |||
cache = result.query.search.map(p => ({ | |||
title: p.title, | |||
embedding: null | |||
})); | |||
return cache; | |||
} | |||
// Erkennen & vergleichen | |||
async function findMatches(file) { | |||
const db = await loadDatabase(); | |||
const imgVec = await embedImage(file); | |||
// Falls wir noch keine Embeddings für Seiten haben → schnell "zero-shot prompt" | |||
db.forEach(p => { | |||
if (!p.embedding) p.embedding = CLIP.embedTextSync(p.title); | |||
}); | |||
// Score berechnen | |||
const scored = db.map(p => ({ | |||
title: p.title, | |||
score: CLIP.cosineSimilarity(imgVec, p.embedding) | |||
})); | |||
scored.sort((a, b) => b.score - a.score); | |||
return scored.slice(0, 8); // Nur Top 8 | |||
} | |||
// UI Rendering | |||
function renderResults(items) { | |||
const box = document.getElementById('ados-scan-results'); | |||
if (!box) return; | |||
box.innerHTML = ''; | |||
if (!items || items.length === 0) { | |||
box.innerHTML = '<div class="ados-hit">Keine klaren Treffer gefunden.</div>'; | |||
return; | |||
} | } | ||
items.forEach(it => { | |||
const link = mw.util.getUrl(it.title.replace(/ /g, '_')); | |||
box.innerHTML += `<div class="ados-hit"> | |||
<b><a href="${link}">${mw.html.escape(it.title)}</a></b> | |||
<div class="meta">Ähnlichkeit: ${(it.score * 100).toFixed(1)}%</div> | |||
</div>`; | |||
}); | }); | ||
} | } | ||
// | // --- Button Binding (funktioniert sicher, da Binding OK geprüft) --- | ||
document.addEventListener('click', async ev => { | |||
const btn = ev.target.closest && ev.target.closest('#ados-scan-run'); | |||
if (!document.getElementById('ados-scan- | if (!btn) return; | ||
ev.preventDefault(); | |||
const fileIn = document.getElementById('ados-scan-file'); | |||
const status = document.getElementById('ados-scan-status'); | |||
if (!fileIn.files || !fileIn.files[0]) { | |||
alert('Bitte ein Label-Foto auswählen.'); | |||
return; | |||
} | |||
const file = fileIn.files[0]; | |||
status.textContent = '🔍 Erkenne Bildstil…'; | |||
btn.disabled = true; | |||
try { | |||
const matches = await findMatches(file); | |||
renderResults(matches); | |||
status.textContent = '✅ Fertig.'; | |||
} catch (err) { | |||
console.error(err); | |||
status.textContent = '❌ Fehler.'; | |||
} | } | ||
}, | |||
btn.disabled = false; | |||
}, true); | |||
})(); | })(); | ||
Version vom 6. November 2025, 22:07 Uhr
/* LabelScan – visuelle Erkennung über CLIP (no OCR) */
(function () {
'use strict';
console.log('[LabelScan] CLIP-Erkennung Gadget gestartet');
// Kategorien, in denen gesucht werden soll
const ADOS_CATEGORIES = [
'Alle A Dream of Scotland Abfüllungen',
'Alle A Dream of Ireland Abfüllungen',
'Alle A Dream of... – Der Rest der Welt Abfüllungen',
'The Fine Art of Whisky Abfüllungen',
'Die Whisky Elfen Abfüllungen',
'Friendly Mr. Z Whiskytainment Abfüllungen',
'Alle Rumbastic Abfüllungen',
'Cigar Malt Übersicht',
'The Tasteful 8',
'Còmhlan Abfüllungen',
'The Forbidden Kingdom',
'Sonderabfüllungen'
];
// Laden des CLIP-Modells
let clipReady = null;
function ensureCLIP() {
if (clipReady) return clipReady;
clipReady = new Promise((resolve, reject) => {
mw.loader.using('ext.gadget.aimodels').then(() => {
if (window.CLIP) resolve();
else reject('CLIP-Modell fehlt');
});
});
return clipReady;
}
// Bild → Embedding
async function embedImage(file) {
await ensureCLIP();
const img = await CLIP.loadImage(file);
return await CLIP.embedImage(img);
}
// Wiki-Abfüllungsseiten laden & vorberechnen (macht Cache!)
let cache = null;
async function loadDatabase() {
if (cache) return cache;
await mw.loader.using('mediawiki.api');
const api = new mw.Api();
const catSearch = ADOS_CATEGORIES.map(c => `incategory:"${c}"`).join(' | ');
const result = await api.get({
action: 'query',
list: 'search',
srsearch: catSearch,
srlimit: 500,
srnamespace: 0,
formatversion: 2
});
cache = result.query.search.map(p => ({
title: p.title,
embedding: null
}));
return cache;
}
// Erkennen & vergleichen
async function findMatches(file) {
const db = await loadDatabase();
const imgVec = await embedImage(file);
// Falls wir noch keine Embeddings für Seiten haben → schnell "zero-shot prompt"
db.forEach(p => {
if (!p.embedding) p.embedding = CLIP.embedTextSync(p.title);
});
// Score berechnen
const scored = db.map(p => ({
title: p.title,
score: CLIP.cosineSimilarity(imgVec, p.embedding)
}));
scored.sort((a, b) => b.score - a.score);
return scored.slice(0, 8); // Nur Top 8
}
// UI Rendering
function renderResults(items) {
const box = document.getElementById('ados-scan-results');
if (!box) return;
box.innerHTML = '';
if (!items || items.length === 0) {
box.innerHTML = '<div class="ados-hit">Keine klaren Treffer gefunden.</div>';
return;
}
items.forEach(it => {
const link = mw.util.getUrl(it.title.replace(/ /g, '_'));
box.innerHTML += `<div class="ados-hit">
<b><a href="${link}">${mw.html.escape(it.title)}</a></b>
<div class="meta">Ähnlichkeit: ${(it.score * 100).toFixed(1)}%</div>
</div>`;
});
}
// --- Button Binding (funktioniert sicher, da Binding OK geprüft) ---
document.addEventListener('click', async ev => {
const btn = ev.target.closest && ev.target.closest('#ados-scan-run');
if (!btn) return;
ev.preventDefault();
const fileIn = document.getElementById('ados-scan-file');
const status = document.getElementById('ados-scan-status');
if (!fileIn.files || !fileIn.files[0]) {
alert('Bitte ein Label-Foto auswählen.');
return;
}
const file = fileIn.files[0];
status.textContent = '🔍 Erkenne Bildstil…';
btn.disabled = true;
try {
const matches = await findMatches(file);
renderResults(matches);
status.textContent = '✅ Fertig.';
} catch (err) {
console.error(err);
status.textContent = '❌ Fehler.';
}
btn.disabled = false;
}, true);
})();