MediaWiki:Gadget-LabelScan.js
Hinweis: Leere nach dem Veröffentlichen den Browser-Cache, um die Änderungen sehen zu können.
- Firefox/Safari: Umschalttaste drücken und gleichzeitig Aktualisieren anklicken oder entweder Strg+F5 oder Strg+R (⌘+R auf dem Mac) drücken
- Google Chrome: Umschalttaste+Strg+R (⌘+Umschalttaste+R auf dem Mac) drücken
- Edge: Strg+F5 drücken oder Strg drücken und gleichzeitig Aktualisieren anklicken
/* 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);
})();