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

Keine Bearbeitungszusammenfassung
Keine Bearbeitungszusammenfassung
Zeile 1: Zeile 1:
/* LabelScan – visuelle Erkennung über CLIP (no OCR) */
/* global mw */
 
const INDEX_URL = '/wiki?title=MediaWiki:Gadget-LabelScan-index.json&action=raw&ctype=application/json';
 
let ADOS_INDEX = null;
async function loadIndex() {
  if (ADOS_INDEX) return ADOS_INDEX;
  const res = await fetch(INDEX_URL, { cache: 'no-store' });
  if (!res.ok) throw new Error('Index konnte nicht geladen werden: ' + res.status);
  ADOS_INDEX = await res.json();
  return ADOS_INDEX;
}
 
 
const index = await loadIndex(); // <- MUSS vor dem Matching kommen
 
 
 


(function () {
(function () {
   'use strict';
   'use strict';


   console.log('[LabelScan] CLIP-Erkennung Gadget gestartet');
   // -------------------------------------------------------
  // 1) INDEX LADEN
  // -------------------------------------------------------
  const INDEX_URL = '/wiki?title=MediaWiki:Gadget-LabelScan-index.json&action=raw&ctype=application/json';
  let LABEL_INDEX = null;


   // Kategorien, in denen gesucht werden soll
   async function loadIndex() {
  const ADOS_CATEGORIES = [
     if (LABEL_INDEX) return LABEL_INDEX;
     'Alle A Dream of Scotland Abfüllungen',
     const res = await fetch(INDEX_URL);
     'Alle A Dream of Ireland Abfüllungen',
     LABEL_INDEX = await res.json();
     'Alle A Dream of... – Der Rest der Welt Abfüllungen',
     console.log('[LabelScan] Index geladen:', LABEL_INDEX.length, 'Einträge');
     'The Fine Art of Whisky Abfüllungen',
     return LABEL_INDEX;
    '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;
   // 2) KOSINUS-SIMILARITÄT (Bild→Vektor→Vergleich)
   function ensureCLIP() {
  //    -> ultra leichtgewichtige Bildähnlichkeit
     if (clipReady) return clipReady;
  // -------------------------------------------------------
    clipReady = new Promise((resolve, reject) => {
   async function imgToVec(fileOrUrl) {
       mw.loader.using('ext.gadget.aimodels').then(() => {
     return new Promise((resolve, reject) => {
         if (window.CLIP) resolve();
       const img = new Image();
         else reject('CLIP-Modell fehlt');
      img.crossOrigin = 'anonymous';
       });
      img.onload = () => {
        const c = document.createElement('canvas');
        c.width = 16; c.height = 16;
        const ctx = c.getContext('2d');
        ctx.drawImage(img, 0, 0, 16, 16);
        const d = ctx.getImageData(0, 0, 16, 16).data;
        const vec = [];
         for (let i = 0; i < d.length; i += 4) vec.push((d[i] + d[i+1] + d[i+2]) / 3);
         resolve(vec);
       };
      img.onerror = reject;
      img.src = typeof fileOrUrl === 'string' ? fileOrUrl : URL.createObjectURL(fileOrUrl);
     });
     });
    return clipReady;
   }
   }


   // Bild → Embedding
   function cosine(a, b) {
  async function embedImage(file) {
     let dot = 0, na = 0, nb = 0;
     await ensureCLIP();
     for (let i = 0; i < a.length; i++) {
     const img = await CLIP.loadImage(file);
      dot += a[i] * b[i];
     return await CLIP.embedImage(img);
      na += a[i] * a[i];
      nb += b[i] * b[i];
    }
     return dot / (Math.sqrt(na) * Math.sqrt(nb));
   }
   }


   // Wiki-Abfüllungsseiten laden & vorberechnen (macht Cache!)
   // -------------------------------------------------------
   let cache = null;
  // 3) SUCHE BESTES MATCH
   async function loadDatabase() {
   // -------------------------------------------------------
     if (cache) return cache;
   async function findMatch(file) {
     await loadIndex();
    const vec = await imgToVec(file);


     await mw.loader.using('mediawiki.api');
     let best = null;
    const api = new mw.Api();
    for (const entry of LABEL_INDEX) {
      if (!entry.img) continue;
      const v = await imgToVec(entry.img);
      const score = cosine(vec, v);
      if (!best || score > best.score) best = { score, entry };
    }


     const catSearch = ADOS_CATEGORIES.map(c => `incategory:"${c}"`).join(' | ');
     if (!best || best.score < 0.82) return null; // Schwelle fein justierbar
     const result = await api.get({
     return best.entry;
      action: 'query',
  }
      list: 'search',
      srsearch: catSearch,
      srlimit: 500,
      srnamespace: 0,
      formatversion: 2
    });


    cache = result.query.search.map(p => ({
  // -------------------------------------------------------
      title: p.title,
  // 4) UI
      embedding: null
  // -------------------------------------------------------
    }));
  function hasUI() {
     return cache;
     return document.getElementById('ados-scan-run');
   }
   }


   // Erkennen & vergleichen
   function bindUI() {
  async function findMatches(file) {
    if (!hasUI()) return;
     const db = await loadDatabase();
    const fileIn = document.getElementById('ados-scan-file');
     const imgVec = await embedImage(file);
     const runBtn = document.getElementById('ados-scan-run');
     const results = document.getElementById('ados-scan-results');
 
    runBtn.addEventListener('click', async function (ev) {
      ev.preventDefault();
      if (!(fileIn.files && fileIn.files[0])) {
        alert('Bitte ein Bild auswählen.');
        return;
      }
 
      runBtn.disabled = true;
      runBtn.textContent = 'Erkenne…';


    // Falls wir noch keine Embeddings für Seiten haben → schnell "zero-shot prompt"
       const match = await findMatch(fileIn.files[0]);
    db.forEach(p => {
       if (!p.embedding) p.embedding = CLIP.embedTextSync(p.title);
    });


    // Score berechnen
      runBtn.disabled = false;
    const scored = db.map(p => ({
       runBtn.textContent = 'Erkennen & suchen';
       title: p.title,
      score: CLIP.cosineSimilarity(imgVec, p.embedding)
    }));


    scored.sort((a, b) => b.score - a.score);
      if (!match) {
    return scored.slice(0, 8); // Nur Top 8
        results.innerHTML = '<div style="padding:6px;">Keine Treffer. Bitte anderes Foto versuchen.</div>';
  }
        return;
      }


  // UI Rendering
       const link = mw.util.getUrl(match.title.replace(/ /g, '_'));
  function renderResults(items) {
       results.innerHTML = `
    const box = document.getElementById('ados-scan-results');
        <div class="ados-hit">
    if (!box) return;
          <b><a href="${link}">${match.title}</a></b><br/>
    box.innerHTML = '';
          <img src="${match.img}" style="max-width:150px; margin-top:6px;">
    if (!items || items.length === 0) {
        </div>`;
      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) ---
   if (document.readyState === 'loading') {
  document.addEventListener('click', async ev => {
     document.addEventListener('DOMContentLoaded', bindUI);
    const btn = ev.target.closest && ev.target.closest('#ados-scan-run');
  } else bindUI();
    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);
})();
})();