|
|
| Zeile 1.245: |
Zeile 1.245: |
|
| |
|
| // ==========================Scan================================== | | // ==========================Scan================================== |
|
| |
| // === LabelScan – Foto->OCR->Suche (UI+Progress) =============================
| |
| mw.loader.using(['mediawiki.api', 'mediawiki.util']).then(function(){
| |
| mw.hook('wikipage.content').add(function($content){
| |
| if (mw.config.get('wgPageName') !== 'LabelScan') return;
| |
|
| |
| var $btnBig = $content.find('#ados-scan-bigbtn');
| |
| var $file = $content.find('#ados-scan-file');
| |
| var $run = $content.find('#ados-scan-run');
| |
| var $drop = $content.find('#ados-scan-drop');
| |
| var $prev = $content.find('#ados-scan-preview');
| |
| var $stat = $content.find('#ados-scan-status');
| |
| var $prog = $content.find('#ados-scan-progress');
| |
| var $res = $content.find('#ados-scan-results');
| |
|
| |
| if (!$btnBig.length || !$file.length || !$run.length) return;
| |
| if ($run.data('bound')) return; // nicht doppelt binden
| |
| $run.data('bound', 1);
| |
|
| |
| function setStatus(t){ $stat.text(t||''); }
| |
| function setProgress(p){
| |
| if (p==null){ $prog.hide().val(0); return; }
| |
| $prog.show().val(Math.max(0, Math.min(1, p)));
| |
| }
| |
| function showPreview(file){
| |
| var url = URL.createObjectURL(file);
| |
| $prev.html('<img alt="Vorschau" src="'+url+'">').attr('aria-hidden','false');
| |
| }
| |
|
| |
| // Datei wählen
| |
| $btnBig.on('click', function(){ $file.trigger('click'); });
| |
| // Drag&Drop
| |
| $drop.on('dragover', function(e){ e.preventDefault(); $drop.addClass('dragover'); });
| |
| $drop.on('dragleave', function(e){ $drop.removeClass('dragover'); });
| |
| $drop.on('drop', function(e){
| |
| e.preventDefault(); $drop.removeClass('dragover');
| |
| if (e.originalEvent && e.originalEvent.dataTransfer && e.originalEvent.dataTransfer.files.length){
| |
| $file[0].files = e.originalEvent.dataTransfer.files;
| |
| showPreview($file[0].files[0]);
| |
| }
| |
| });
| |
| // Vorschau bei Auswahl
| |
| $file.on('change', function(){ if (this.files && this.files[0]) showPreview(this.files[0]); });
| |
|
| |
| // Tesseract lazy-load
| |
| function loadTesseract(){
| |
| return new Promise(function(resolve, reject){
| |
| if (window.Tesseract) return resolve();
| |
| var s = document.createElement('script');
| |
| s.src = 'https://cdn.jsdelivr.net/npm/tesseract.js@5/dist/tesseract.min.js';
| |
| s.async = true;
| |
| s.onload = resolve;
| |
| s.onerror = function(){
| |
| // Fallback-CDN
| |
| var s2 = document.createElement('script');
| |
| s2.src = 'https://unpkg.com/tesseract.js@5/dist/tesseract.min.js';
| |
| s2.async = true;
| |
| s2.onload = resolve;
| |
| s2.onerror = function(){ reject(new Error('Tesseract konnte nicht geladen werden')); };
| |
| document.head.appendChild(s2);
| |
| };
| |
| document.head.appendChild(s);
| |
| });
| |
| }
| |
|
| |
| // Heuristik -> Query
| |
| function extractHints(text){
| |
| const raw = (text||'').replace(/\s+/g,' ').trim();
| |
| const words = Array.from(new Set(raw.match(/\b[A-ZÄÖÜ][A-Za-zÄÖÜäöüß\-]{3,}\b/g) || []));
| |
| const ages = Array.from(new Set(raw.match(/\b([1-9]\d?)\s?(?:years?|yo|jahr|jahre)\b/gi) || []))
| |
| .map(s => (s.match(/[1-9]\d?/)||[])[0]);
| |
| const years = Array.from(new Set(raw.match(/\b(19|20)\d{2}\b/g) || []));
| |
| return {words, ages, years};
| |
| }
| |
| function buildSearchQuery(h){
| |
| const parts = [];
| |
| (h.words||[]).slice(0,5).forEach(w => parts.push('"'+w+'"'));
| |
| (h.ages||[]).forEach(a => parts.push('"'+a+'"'));
| |
| (h.years||[]).forEach(y => parts.push('"'+y+'"'));
| |
| if (!parts.length) parts.push('Whisky');
| |
| return parts.join(' ');
| |
| }
| |
|
| |
| function renderResults(items){
| |
| $res.empty();
| |
| if (!items.length){
| |
| $res.html('<div class="ados-hit">Keine klaren Treffer. Bitte anderes Foto oder manuell suchen.</div>');
| |
| return;
| |
| }
| |
| items.slice(0,8).forEach(function(it){
| |
| var title = it.title || it.Seitentitel || '';
| |
| var link = mw.util.getUrl( title.replace(/ /g,'_') );
| |
| var snip = (it.snippet||'').replace(/<\/?span[^>]*>/g,'').replace(/"/g,'"');
| |
| $res.append(
| |
| '<div class="ados-hit">'
| |
| + '<b><a href="'+link+'">'+mw.html.escape(title)+'</a></b>'
| |
| + (snip ? '<div class="meta" style="color:#666">'+snip+'</div>' : '')
| |
| + '</div>'
| |
| );
| |
| });
| |
| }
| |
|
| |
| async function runOCR(file){
| |
| await loadTesseract();
| |
| setProgress(0);
| |
| const result = await Tesseract.recognize(file, 'deu+eng', {
| |
| logger: function(m){
| |
| if (m && m.status === 'recognizing text' && typeof m.progress === 'number'){
| |
| setProgress(m.progress);
| |
| }
| |
| }
| |
| });
| |
| setProgress(null);
| |
| return (result && result.data && result.data.text) || '';
| |
| }
| |
|
| |
| async function searchWiki(query, limit){
| |
| const api = new mw.Api();
| |
| const res = await api.get({
| |
| action:'query', list:'search', srsearch:query,
| |
| srlimit:limit||10, srwhat:'text', formatversion:2
| |
| });
| |
| return (res.query && res.query.search) || [];
| |
| }
| |
|
| |
| // Start
| |
| $run.on('click', async function(ev){
| |
| ev.preventDefault();
| |
| if (!($file[0].files && $file[0].files[0])){ alert('Bitte ein Foto auswählen oder aufnehmen.'); return; }
| |
| const file = $file[0].files[0];
| |
|
| |
| try{
| |
| $run.prop('disabled', true).text('Erkenne …');
| |
| setStatus('Erkenne Label …');
| |
| const text = await runOCR(file);
| |
|
| |
| setStatus('Suche im Wiki …');
| |
| const hints = extractHints(text);
| |
| const query = buildSearchQuery(hints);
| |
| const hits = await searchWiki(query, 12);
| |
|
| |
| renderResults(hits);
| |
| setStatus('Fertig.');
| |
| } catch(e){
| |
| console.error('[LabelScan] Fehler:', e);
| |
| setStatus('Fehler bei Erkennung/Suche. Bitte erneut versuchen.');
| |
| } finally {
| |
| $run.prop('disabled', false).text('Erkennen & suchen');
| |
| }
| |
| });
| |
|
| |
| console.log('[LabelScan] UI gebunden.');
| |
| });
| |
| });
| |