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

Keine Bearbeitungszusammenfassung
Keine Bearbeitungszusammenfassung
Markierung: Zurückgesetzt
Zeile 1: Zeile 1:
/* global mw */
/* global mw */
(() => {
(function(){
   'use strict';
   'use strict';


   // ------------------------------------------------------------
   // ---------- Konfiguration ----------
   // Konfiguration
   function log(){ console.log('[LabelScan]', ...arguments); }
   // ------------------------------------------------------------
   function err(){ console.error('[LabelScan] Fehler', ...arguments); }
 
   const CFG = {
   const CFG = {
     // Wo liegt die JSON mit Index (Titel, Thumb, EMBEDDING)?
     // ESM-Build (wichtig!):
    indexTitle: (window.LabelScanConfig && window.LabelScanConfig.indexTitle) ||
     transformersURL: 'https://cdn.jsdelivr.net/npm/@xenova/transformers@2.15.0/dist/transformers.min.js',
                'MediaWiki:Gadget-LabelScan-index.json',
     modelId: 'Xenova/clip-vit-base-patch32',
    // Top-N Treffer anzeigen:
     topKByPhash: 24,        // wie viele pHash-Kandidaten für CLIP nachladen
    topK: 8,
     showN: 8,               // wie viele Treffer anzeigen
    // CLIP-Model:
     indexUrl: mw.util.getUrl('MediaWiki:Gadget-LabelScan-index.json', { action:'raw', ctype:'application/json' })
     transformersURL: 'https://cdn.jsdelivr.net/npm/@xenova/transformers@2.15.0',
     modelId: 'Xenova/clip-vit-base-patch32', // robust & kompakt (quantized)
     // Max-Seitenkante beim Downscaling (Speed/Qualität):
     maxSide: 1024,
    // Logging:
     debug: true
   };
   };


  function log(...args) { if (CFG.debug) console.log('[LabelScan]', ...args); }
   // ---------- UI Helfer ----------
  function warn(...args){ if (CFG.debug) console.warn('[LabelScan]', ...args); }
   function $(id){ return document.getElementById(id); }
  function err(...args) { console.error('[LabelScan]', ...args); }
   function setStatus(t){ const el=$('ados-scan-status'); if(el) el.textContent=t||''; }
 
   function setProgress(p){
   // ------------------------------------------------------------
     const bar=$('ados-scan-progress');
  // UI Helpers
    if(!bar) return;
  // ------------------------------------------------------------
     if(p==null){ bar.hidden=true; bar.value=0; return; }
   function qs(id) { return document.getElementById(id); }
     bar.hidden=false; bar.value=Math.max(0,Math.min(1,p));
   function setStatus(txt) { const el = qs('ados-scan-status'); if (el) el.textContent = txt || ''; }
   function setProgress(p) {
     const bar = qs('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)); }
   }
   }
   function showPreview(file) {
   function showPreview(file){
     const url = URL.createObjectURL(file);
     const url=URL.createObjectURL(file);
     const prev = qs('ados-scan-preview');
     const prev=$('ados-scan-preview');
     if (prev) {
     if(prev){
       prev.innerHTML = '<img alt="Vorschau" style="max-width:100%;height:auto;border-radius:8px;" src="'+url+'">';
       prev.innerHTML = '<img alt="Vorschau" style="max-width:100%;height:auto;border-radius:8px;" />';
      const img=prev.querySelector('img'); img.src=url;
       prev.setAttribute('aria-hidden','false');
       prev.setAttribute('aria-hidden','false');
     }
     }
  }
  function renderResults(items){
    const box=$('ados-scan-results'); if(!box) return;
    box.innerHTML='';
    if(!items || !items.length){
      box.innerHTML='<div class="empty">Keine klaren Treffer. Bitte anderes Foto oder manuell suchen.</div>';
      return;
    }
    items.slice(0, CFG.showN).forEach(it=>{
      const url = mw.util.getUrl(String(it.title||'').replace(/ /g,'_'));
      const div=document.createElement('div');
      div.className='ados-hit';
      div.style.margin='6px 0';
      div.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;border:1px solid #eee;">` : '') +
          `<div><div><b><a href="${url}">${mw.html.escape(it.title||'')}</a></b></div>` +
          `<div class="meta" style="color:#666;font-size:90%;">Score: ${(it.score||0).toFixed(3)}</div></div>` +
        '</div>';
      box.appendChild(div);
    });
   }
   }


   // ------------------------------------------------------------
   // ---------- Index laden ----------
   // Index laden (JSON: Titel, Thumb, embed(Base64 Float32))
   let _indexPromise=null, INDEX=[];
  // ------------------------------------------------------------
  async function ensureIndex(){
  let INDEX = [];
    if(_indexPromise) return _indexPromise;
   let INDEX_EMB = []; // Array<Float32Array>
    _indexPromise = fetch(CFG.indexUrl, { cache:'reload' })
      .then(r=>{ if(!r.ok) throw new Error('Index HTTP '+r.status); return r.json(); })
      .then(data=>{
        if(!Array.isArray(data)) throw new Error('Index ist kein Array');
        INDEX=data.filter(x=>x && x.title && x.thumb && x.phash);
        log('Index geladen:', INDEX.length, 'Einträge');
        return INDEX;
      })
      .catch(e=>{ err(e); INDEX=[]; return INDEX; });
    return _indexPromise;
   }


   async function loadIndex() {
   // ---------- pHash ----------
    if (INDEX.length) return INDEX;
  // erwartet 16-hex (64bit) oder 32-hex (128bit); wir normalisieren auf 64bit Vergleich
     setStatus('Index laden …');
  function hexToBigInt(h){ try{ return BigInt('0x'+String(h).trim()); } catch(_){ return null; } }
     setProgress(0.03);
  function hamming64(aHex,bHex){
 
     const a=hexToBigInt(aHex), b=hexToBigInt(bHex);
     // Roh-URL bauen
     if(a===null || b===null) return 64;
     const rawURL = mw.util.getUrl(CFG.indexTitle, { action:'raw', ctype:'application/json' });
     let x=a^b, d=0n;
     const res = await fetch(rawURL, { cache:'reload' });
     while(x){ d += (x & 1n); x >>= 1n; }
    if (!res.ok) throw new Error('Index nicht ladbar: ' + res.status);
     return Number(d);
     const json = await res.json();
  }
  function phashScore(a,b){ // 1..0
    const d=hamming64(a,b);
     const max=64;
    return 1 - (d/max);
  }


    if (!Array.isArray(json)) throw new Error('Index ist keine Array-JSON');
  // ---------- Bild laden ----------
    INDEX = json;
  function fileToImage(file){
 
     return new Promise((res,rej)=>{
    // Embeddings dekodieren (falls vorhanden)
       const img=new Image();
     INDEX_EMB = INDEX.map((it, i) => {
      img.onload=()=>res(img);
       if (typeof it.embed === 'string' && it.embed.length) {
       img.onerror=rej;
        try { return base64ToFloat32(it.embed); }
       img.src=URL.createObjectURL(file);
        catch(e){ warn('Embed-Decode-Fehler bei Index', i, it.title, e); return null; }
       }
       return null;
     });
     });
    log('Index geladen:', INDEX.length, 'Einträge');
    setProgress(0.06);
    return INDEX;
   }
   }
 
   function urlToImage(url){
  // Base64 -> Float32Array
     return new Promise((res,rej)=>{
   function base64ToFloat32(b64) {
      const img=new Image();
     const bin = atob(b64);
      img.crossOrigin='anonymous';
    const len = bin.length;
      img.onload=()=>res(img);
    const buf = new ArrayBuffer(len);
      img.onerror=rej;
    const view = new Uint8Array(buf);
      img.src=url;
    for (let i=0;i<len;i++) view[i] = bin.charCodeAt(i);
     });
     return new Float32Array(buf);
   }
   }


// ------------------------------------------------------------
  // ---------- CLIP laden ----------
// CLIP: Feature-Extraction (nur Bild, ohne Tokenizer)
  let _clipModulePromise=null;
// ------------------------------------------------------------
  async function ensureClipExtractor(){
let _clipModulePromise = null;
    if(_clipModulePromise) return _clipModulePromise;
async function ensureClipExtractor() {
  if (_clipModulePromise) return _clipModulePromise;


  setStatus('Modell laden …');
    setStatus('Modell laden …'); setProgress(0.08);
  setProgress(0.08);


  _clipModulePromise = (async () => {
    _clipModulePromise = (async ()=>{
    const ESM_URL = CFG.transformersURL;
      const mod = await import(/* webpackIgnore: true */ CFG.transformersURL);
    const mod = await import(/* webpackIgnore: true */ ESM_URL);


    // NUR remote laden
      // Nur Remote, im Browser cachen
    mod.env.localModelPath = null;
      mod.env.localModelPath = null;
    mod.env.remoteModels   = true;
      mod.env.remoteModels = true;
    mod.env.allowRemoteModels = true;
      mod.env.allowRemoteModels = true;
    mod.env.useBrowserCache = true;
      mod.env.useBrowserCache = true;


    // Hier liegt der Trick: Wir nutzen explizit "image-feature-extraction",
      const pipe = await mod.pipeline('image-feature-extraction', CFG.modelId, { quantized:true });
    // damit kein Tokenizer geladen wird.
      log('CLIP ready:', pipe.model?.constructor?.name || 'unknown');
    const pipe = await mod.pipeline(
      return { mod, pipe };
      'image-feature-extraction',
     })().catch(e=>{ err(e); throw e; });
      CFG.modelId,
      { quantized: true }
     );


    log('CLIP ready:', pipe.model?.constructor?.name || 'unknown');
     return _clipModulePromise;
     return { mod, pipe };
   }
   })();


   return _clipModulePromise;
   // ---------- Embeddings & Cosine ----------
}
  function cosine(a,b){
    let dot=0, na=0, nb=0;
    for(let i=0;i<a.length;i++){ const x=a[i], y=b[i]; dot+=x*y; na+=x*x; nb+=y*y; }
    if(na===0 || nb===0) return 0;
    return dot / (Math.sqrt(na)*Math.sqrt(nb));
  }
  async function embedImage(img){
    const { pipe } = await ensureClipExtractor();
    // transformers akzeptiert HTMLImageElement direkt:
    const out = await pipe(img);
    // out ist typischerweise Float32Array
    return Array.from(out.data || out);
  }
  async function embedURL(url){
    const img = await urlToImage(url);
    return embedImage(img);
  }
  async function embedFile(file){
    const img = await fileToImage(file);
    return embedImage(img);
  }


   // Datei -> HTMLImageElement -> Canvas (Downscale) -> Embedding (Float32Array, normiert)
   // ---------- Matching Pipeline ----------
   async function embedFileImage(file) {
   async function matchImage(file){
     // Datei als HTMLImageElement laden
     await ensureIndex();
    function loadImageFromFile(f) {
    if(!INDEX.length) throw new Error('Index leer.');
      return new Promise((resolve, reject) => {
        const url = URL.createObjectURL(f);
        const img = new Image();
        img.crossOrigin = 'anonymous';
        img.onload = () => { URL.revokeObjectURL(url); resolve(img); };
        img.onerror = (e)=> { URL.revokeObjectURL(url); reject(e); };
        img.src = url;
      });
    }
    // Canvas-Downscale
    function toCanvas(img, maxSide) {
      const c = document.createElement('canvas');
      let { width: w, height: h } = img;
      const scale = Math.min(1, maxSide / Math.max(w, h));
      w = Math.round(w * scale);
      h = Math.round(h * scale);
      c.width = w; c.height = h;
      const ctx = c.getContext('2d');
      ctx.imageSmoothingEnabled = true;
      ctx.drawImage(img, 0, 0, w, h);
      return c;
    }
    const { pipe } = await ensureClipExtractor();


     setStatus('Bild vorbereiten …');
     // Vorschau
     setProgress(0.20);
     showPreview(file);


     const img = await loadImageFromFile(file);
    // pHash-Kandidaten
     const canvas = toCanvas(img, CFG.maxSide);
    setStatus('Vorab-Abgleich (pHash) …'); setProgress(0.18);
     const userPhash = null; // (Optional: clientseitig pHash berechnen – hier nicht nötig)
    // Wenn wir keinen pHash des Uploads haben, nehmen wir alle & sortieren später nach CLIP.
    // Für schnellen Vorfilter sortieren wir grob nach Titel-Länge (kein harter Nutzen) → oder zufällig mischen
    // Besser: Wir lassen pHash-Score=0.5 fallback, oder ignorieren pHash.
    // Hier: pHash nicht vorhanden → wir nutzen alle Kandidaten, schneiden aber hart auf topKByPhash zu.
     let prelim = INDEX.map(x=>({ item:x, pScore:0.5 }));


     setStatus('Bild analysieren …');
     // Optional: Falls du clientseitig pHash ergänzt, hier pScore via phashScore(user, x.phash) setzen.
    setProgress(0.38);


     const out = await pipe(canvas);
     // leichte Bevorzugung kurzer Thumbnails (heuristisch nicht nötig) – wir gehen direkt weiter
    prelim = prelim.slice(0, Math.max(CFG.topKByPhash, 12));


     // Ausgabe → Float32Array normieren
     // CLIP des Uploads
     const d = out && out.data;
     setStatus('Bild verstehen (KI) …'); setProgress(0.38);
    let vec;
     const userVec = await embedFile(file);
    if (d instanceof Float32Array) {
      vec = d;
    } else if (Array.isArray(d)) {
      // 2D → mitteln
      vec = Array.isArray(d[0]) ? meanPool2D(d) : new Float32Array(d);
     } else {
      throw new Error('Embedding-Format unerwartet');
    }
    return normalize(vec);
  }


  function meanPool2D(arr2d) {
    // CLIP für Kandidaten
     const rows = arr2d.length;
    setStatus('Kandidaten bewerten …'); setProgress(0.55);
     const dim = rows ? arr2d[0].length : 0;
     let done=0;
     const sum = new Float32Array(dim);
     const scored = [];
    for (let r=0;r<rows;r++) {
     for(const k of prelim){
       const row = arr2d[r];
      try{
      for (let i=0;i<dim;i++) sum[i] += row[i] || 0;
        const v = await embedURL(k.item.thumb);
        const c = cosine(userVec, v);      // 0..1
        const s = 0.6*c + 0.4*k.pScore;   // Kombi aus CLIP (60%) und pHash (40%)
        scored.push({ title:k.item.title, thumb:k.item.thumb, score:s });
      }catch(e){
        // Bild konnte nicht geladen werden → überspringen
       }finally{
        done++; setProgress(0.55 + 0.35*(done/prelim.length));
      }
     }
     }
    for (let i=0;i<dim;i++) sum[i] /= (rows || 1);
    return sum;
  }
  function normalize(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; const L=Math.min(a.length,b.length);
    for (let i=0;i<L;i++) s += a[i]*b[i];
    return s; // bei normierten Vektoren = cos
  }


  // ------------------------------------------------------------
     scored.sort((a,b)=>b.score-a.score);
  // Ranking & Rendering
     return scored;
  // ------------------------------------------------------------
  function rankByCosine(queryVec) {
    const scores = [];
    for (let i=0;i<INDEX.length;i++) {
      const vec = INDEX_EMB[i];
      if (!vec) continue; // ohne Embedding nicht vergleichbar
      const score = cosine(queryVec, vec);
      scores.push({ i, score });
    }
    // absteigend
     scores.sort((a,b)=> b.score - a.score);
     return scores.slice(0, CFG.topK);
   }
   }


   function renderResults(ranked) {
  // ---------- Bindings ----------
     const box = qs('ados-scan-results');
   function bindUI(){
     if (!box) return;
     const btnCam  = $('ados-scan-btn-camera');
     box.innerHTML = '';
     const btnGal  = $('ados-scan-btn-gallery');
     const inCam    = $('ados-scan-file-camera');
    const inGal    = $('ados-scan-file-gallery');
    const btnRun  = $('ados-scan-run');
    const btnReset = $('ados-scan-reset');
    const drop    = $('ados-scan-drop');


     if (!ranked || !ranked.length) {
     if(!btnRun || !btnReset || !btnCam || !btnGal || !inCam || !inGal){
       box.innerHTML = '<div class="empty">Keine klaren Treffer. Bitte anderes Foto oder näher am Frontlabel.</div>';
       log('UI unvollständig – Seite lädt evtl. ohne HTML-Wrapper <html></html>?');
       return;
       return;
     }
     }


     ranked.forEach(({ i, score }) => {
     // Buttons → Inputs
      const it = INDEX[i];
    btnCam.addEventListener('click', ()=> inCam.click());
      const link = mw.util.getUrl((it.title || '').replace(/ /g,'_'));
     btnGal.addEventListener('click', ()=> inGal.click());
      const thumb = it.thumb || '';
      const row = document.createElement('div');
      row.className = 'ados-hit';
      row.style.display = 'grid';
      row.style.gridTemplateColumns = '60px 1fr auto';
      row.style.alignItems = 'center';
      row.style.gap = '10px';
      row.style.padding = '.35rem 0';
      row.innerHTML =
        (thumb ? `<div><img src="${thumb}" alt="" style="width:60px;height:auto;border-radius:6px;"></div>` : '<div></div>') +
        `<div><b><a href="${link}">${escapeHtml(it.title || '')}</a></b></div>` +
        `<div style="color:#666;font-variant-numeric:tabular-nums">${score.toFixed(3)}</div>`;
      box.appendChild(row);
     });
  }
 
  function escapeHtml(s){ return mw.html.escape(String(s||'')); }
 
  // ------------------------------------------------------------
  // Bindings
  // ------------------------------------------------------------
  let BOUND = false;
  function bindUI() {
    if (BOUND) return;
    const btnCam  = qs('ados-scan-btn-camera');
    const btnGal  = qs('ados-scan-btn-gallery');
    const inCam  = qs('ados-scan-file-camera');
    const inGal   = qs('ados-scan-file-gallery');
    const btnRun  = qs('ados-scan-run');
    const btnReset= qs('ados-scan-reset');
    const drop    = qs('ados-scan-drop');
 
    if (!btnRun || !inCam || !inGal) return;


    // Buttons triggern jeweilige Inputs
     function onPick(ev){
    if (btnCam)  btnCam.addEventListener('click', ()=> inCam.click());
       const f = ev.target.files && ev.target.files[0];
    if (btnGal)  btnGal.addEventListener('click', ()=> inGal.click());
       if(f){ showPreview(f); setStatus('Bereit zum Erkennen.'); }
 
    // Vorschau setzen
     function onPick(e) {
       const f = e.target.files && e.target.files[0];
       if (f) showPreview(f);
     }
     }
     inCam.addEventListener('change', onPick);
     inCam.addEventListener('change', onPick);
     inGal.addEventListener('change', onPick);
     inGal.addEventListener('change', onPick);


     // Drag & Drop nur als Zusatz
     // Drag&Drop
     if (drop) {
     if(drop){
       drop.addEventListener('dragover', ev => { ev.preventDefault(); drop.classList.add('is-over'); });
       drop.addEventListener('dragover', e=>{ e.preventDefault(); drop.classList.add('is-over'); });
       drop.addEventListener('dragleave', ()=> drop.classList.remove('is-over'));
       drop.addEventListener('dragleave', ()=> drop.classList.remove('is-over'));
       drop.addEventListener('drop', ev => {
       drop.addEventListener('drop', e=>{
         ev.preventDefault(); drop.classList.remove('is-over');
         e.preventDefault(); drop.classList.remove('is-over');
         const f = ev.dataTransfer && ev.dataTransfer.files && ev.dataTransfer.files[0];
         if(e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files[0]){
        if (f) {
          const f=e.dataTransfer.files[0];
           // in "Gallery"-Input setzen, damit onRunClick es findet
           // in Galerie-Input setzen (nur zur Verwaltung), Vorschau zeigen
           const dt = new DataTransfer();
           const dt = new DataTransfer(); dt.items.add(f);
          dt.items.add(f);
           inGal.files = dt.files;
           inGal.files = dt.files;
           showPreview(f);
           showPreview(f);
          setStatus('Bereit zum Erkennen.');
         }
         }
       });
       });
Zeile 293: Zeile 245:


     // Reset
     // Reset
     if (btnReset) btnReset.addEventListener('click', () => {
     btnReset.addEventListener('click', ()=>{
       const p = qs('ados-scan-preview'); if (p) p.innerHTML = '<div class="note">Noch keine Vorschau. Wähle ein Foto.</div>';
      inCam.value=''; inGal.value='';
      inCam.value = ''; inGal.value = '';
       const p=$('ados-scan-preview'); if(p) p.innerHTML='<div class="note">Noch keine Vorschau. Wähle ein Foto.</div>';
       const r = qs('ados-scan-results'); if (r) r.innerHTML = '<div class="empty">Hier erscheinen passende Abfüllungen mit Link ins Wiki.</div>';
       const r=$('ados-scan-results'); if(r) r.innerHTML='<div class="empty">Hier erscheinen passende Abfüllungen mit Link ins Wiki.</div>';
       setStatus('Bereit.'); setProgress(null);
       setStatus('Bereit.'); setProgress(null);
     });
     });


     // Start
     // Run
     btnRun.addEventListener('click', onRunClick);
     btnRun.addEventListener('click', async ()=>{
      try{
        const file = inCam.files?.[0] || inGal.files?.[0];
        if(!file){ alert('Bitte zuerst ein Foto aufnehmen oder auswählen.'); return; }
        btnRun.disabled=true; setStatus('Starte …'); setProgress(0.05);


    BOUND = true;
        const hits = await matchImage(file);
    log('UI gebunden.');
        renderResults(hits);
  }
        setStatus('Fertig.');
 
        setProgress(null);
  async function onRunClick() {
       }catch(e){
    try {
        err(e);
      const inCam  = qs('ados-scan-file-camera');
         setStatus('Fehler bei der Erkennung/Suche.');
      const inGal  = qs('ados-scan-file-gallery');
      const btnRun  = qs('ados-scan-run');
 
      const file = (inCam.files && inCam.files[0]) || (inGal.files && inGal.files[0]);
       if (!file) { alert('Bitte zuerst ein Foto auswählen oder aufnehmen.'); return; }
 
      btnRun.disabled = true;
      setStatus('Vorbereitung …');
      setProgress(0.02);
 
      // Index sicher laden
      await loadIndex();
 
      // Mind. ein Eintrag mit Embedding vorhanden?
      if (!INDEX_EMB.some(v => v && v.length)) {
         setStatus('Index enthält keine Embeddings – bitte Index mit Embeddings neu erzeugen.');
         setProgress(null);
         setProgress(null);
         btnRun.disabled = false;
      }finally{
        return;
         btnRun.disabled=false;
       }
       }
    });


      // Embedding für Query-Bild
     log('UI gebunden.');
      const qVec = await embedFileImage(file);    // 0.20–0.40 in ensure / embed
      setProgress(0.70);
 
      // Ranking
      setStatus('Abgleich mit Datenbank …');
      const ranked = rankByCosine(qVec);
 
      setProgress(0.95);
      renderResults(ranked);
      setStatus('Fertig.');
      setProgress(null);
     } catch (e) {
      err('Fehler', e);
      setStatus('Fehler bei Erkennung/Abgleich. Bitte erneut versuchen.');
      setProgress(null);
    } finally {
      const btnRun = qs('ados-scan-run');
      if (btnRun) btnRun.disabled = false;
    }
   }
   }


   // ------------------------------------------------------------
   // ---------- Init ----------
  // Init
   function init(){
  // ------------------------------------------------------------
     log('gadget file loaded');
   function init() {
    ensureIndex(); // schon mal laden
     // Bind nach DOM fertig
     if(document.readyState==='loading'){
     if (document.readyState === 'loading') {
       document.addEventListener('DOMContentLoaded', bindUI);
       document.addEventListener('DOMContentLoaded', bindUI, { once: true });
     }else{
     } else {
       bindUI();
       bindUI();
     }
     }
    // Fallbacks
    setTimeout(bindUI, 250);
    setTimeout(bindUI, 1000);
    // (Optional) Index „warm“ laden (nicht blockierend)
    loadIndex().catch(err);
   }
   }


  // Start
  log('gadget file loaded');
   init();
   init();
})();
})();