<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="de">
	<id>https://ados-wiki.de/index.php?action=history&amp;feed=atom&amp;title=Hilfe%3ALabelScan-Indexer</id>
	<title>Hilfe:LabelScan-Indexer - Versionsgeschichte</title>
	<link rel="self" type="application/atom+xml" href="https://ados-wiki.de/index.php?action=history&amp;feed=atom&amp;title=Hilfe%3ALabelScan-Indexer"/>
	<link rel="alternate" type="text/html" href="https://ados-wiki.de/index.php?title=Hilfe:LabelScan-Indexer&amp;action=history"/>
	<updated>2026-04-20T03:38:52Z</updated>
	<subtitle>Versionsgeschichte dieser Seite in ADOS Wiki</subtitle>
	<generator>MediaWiki 1.44.0</generator>
	<entry>
		<id>https://ados-wiki.de/index.php?title=Hilfe:LabelScan-Indexer&amp;diff=6502&amp;oldid=prev</id>
		<title>Admin am 9. November 2025 um 14:12 Uhr</title>
		<link rel="alternate" type="text/html" href="https://ados-wiki.de/index.php?title=Hilfe:LabelScan-Indexer&amp;diff=6502&amp;oldid=prev"/>
		<updated>2025-11-09T14:12:43Z</updated>

		<summary type="html">&lt;p&gt;&lt;/p&gt;
&lt;a href=&quot;https://ados-wiki.de/index.php?title=Hilfe:LabelScan-Indexer&amp;amp;diff=6502&amp;amp;oldid=6501&quot;&gt;Änderungen zeigen&lt;/a&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://ados-wiki.de/index.php?title=Hilfe:LabelScan-Indexer&amp;diff=6501&amp;oldid=prev</id>
		<title>Admin am 9. November 2025 um 14:02 Uhr</title>
		<link rel="alternate" type="text/html" href="https://ados-wiki.de/index.php?title=Hilfe:LabelScan-Indexer&amp;diff=6501&amp;oldid=prev"/>
		<updated>2025-11-09T14:02:24Z</updated>

		<summary type="html">&lt;p&gt;&lt;/p&gt;
&lt;table style=&quot;background-color: #fff; color: #202122;&quot; data-mw=&quot;interface&quot;&gt;
				&lt;col class=&quot;diff-marker&quot; /&gt;
				&lt;col class=&quot;diff-content&quot; /&gt;
				&lt;col class=&quot;diff-marker&quot; /&gt;
				&lt;col class=&quot;diff-content&quot; /&gt;
				&lt;tr class=&quot;diff-title&quot; lang=&quot;de&quot;&gt;
				&lt;td colspan=&quot;2&quot; style=&quot;background-color: #fff; color: #202122; text-align: center;&quot;&gt;← Nächstältere Version&lt;/td&gt;
				&lt;td colspan=&quot;2&quot; style=&quot;background-color: #fff; color: #202122; text-align: center;&quot;&gt;Version vom 9. November 2025, 16:02 Uhr&lt;/td&gt;
				&lt;/tr&gt;&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-lineno&quot; id=&quot;mw-diff-left-l1&quot;&gt;Zeile 1:&lt;/td&gt;
&lt;td colspan=&quot;2&quot; class=&quot;diff-lineno&quot;&gt;Zeile 1:&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;&lt;ins style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;{{#tag:html|&lt;/ins&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;&amp;lt;div class=&amp;quot;box&amp;quot; style=&amp;quot;max-width:820px;margin:1rem auto;padding:1rem;border:1px solid #e5e7eb;border-radius:12px;&amp;quot;&amp;gt;&lt;/div&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;&amp;lt;div class=&amp;quot;box&amp;quot; style=&amp;quot;max-width:820px;margin:1rem auto;padding:1rem;border:1px solid #e5e7eb;border-radius:12px;&amp;quot;&amp;gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;   &amp;lt;h2&amp;gt;📦 LabelScan – Indexer (Auto-Save)&amp;lt;/h2&amp;gt;&lt;/div&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;   &amp;lt;h2&amp;gt;📦 LabelScan – Indexer (Auto-Save)&amp;lt;/h2&amp;gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-lineno&quot; id=&quot;mw-diff-left-l270&quot;&gt;Zeile 270:&lt;/td&gt;
&lt;td colspan=&quot;2&quot; class=&quot;diff-lineno&quot;&gt;Zeile 271:&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;})();&lt;/div&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;})();&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;&amp;lt;/script&amp;gt;&lt;/div&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;&amp;lt;/script&amp;gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;&lt;ins style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;}}&lt;/ins&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;/table&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://ados-wiki.de/index.php?title=Hilfe:LabelScan-Indexer&amp;diff=6500&amp;oldid=prev</id>
		<title>Admin: Die Seite wurde neu angelegt: „&lt;div class=&quot;box&quot; style=&quot;max-width:820px;margin:1rem auto;padding:1rem;border:1px solid #e5e7eb;border-radius:12px;&quot;&gt;   &lt;h2&gt;📦 LabelScan – Indexer (Auto-Save)&lt;/h2&gt;   &lt;p&gt;Erzeugt Embeddings lokal im Browser (CLIP) und schreibt sie automatisch nach &lt;code&gt;MediaWiki:Gadget-LabelScan-index.json&lt;/code&gt;.&lt;/p&gt;    &lt;label&gt;&lt;b&gt;Artikel-Titel&lt;/b&gt; (genau wie im Wiki):     &lt;input id=&quot;idx-title&quot; type=&quot;text&quot; style=&quot;width:100%;padding:.5rem;margin:.25rem 0 .75rem;border:1p…“</title>
		<link rel="alternate" type="text/html" href="https://ados-wiki.de/index.php?title=Hilfe:LabelScan-Indexer&amp;diff=6500&amp;oldid=prev"/>
		<updated>2025-11-09T13:56:18Z</updated>

		<summary type="html">&lt;p&gt;Die Seite wurde neu angelegt: „&amp;lt;div class=&amp;quot;box&amp;quot; style=&amp;quot;max-width:820px;margin:1rem auto;padding:1rem;border:1px solid #e5e7eb;border-radius:12px;&amp;quot;&amp;gt;   &amp;lt;h2&amp;gt;📦 LabelScan – Indexer (Auto-Save)&amp;lt;/h2&amp;gt;   &amp;lt;p&amp;gt;Erzeugt Embeddings lokal im Browser (CLIP) und schreibt sie automatisch nach &amp;lt;code&amp;gt;MediaWiki:Gadget-LabelScan-index.json&amp;lt;/code&amp;gt;.&amp;lt;/p&amp;gt;    &amp;lt;label&amp;gt;&amp;lt;b&amp;gt;Artikel-Titel&amp;lt;/b&amp;gt; (genau wie im Wiki):     &amp;lt;input id=&amp;quot;idx-title&amp;quot; type=&amp;quot;text&amp;quot; style=&amp;quot;width:100%;padding:.5rem;margin:.25rem 0 .75rem;border:1p…“&lt;/p&gt;
&lt;p&gt;&lt;b&gt;Neue Seite&lt;/b&gt;&lt;/p&gt;&lt;div&gt;&amp;lt;div class=&amp;quot;box&amp;quot; style=&amp;quot;max-width:820px;margin:1rem auto;padding:1rem;border:1px solid #e5e7eb;border-radius:12px;&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;h2&amp;gt;📦 LabelScan – Indexer (Auto-Save)&amp;lt;/h2&amp;gt;&lt;br /&gt;
  &amp;lt;p&amp;gt;Erzeugt Embeddings lokal im Browser (CLIP) und schreibt sie automatisch nach &amp;lt;code&amp;gt;MediaWiki:Gadget-LabelScan-index.json&amp;lt;/code&amp;gt;.&amp;lt;/p&amp;gt;&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;label&amp;gt;&amp;lt;b&amp;gt;Artikel-Titel&amp;lt;/b&amp;gt; (genau wie im Wiki):&lt;br /&gt;
    &amp;lt;input id=&amp;quot;idx-title&amp;quot; type=&amp;quot;text&amp;quot; style=&amp;quot;width:100%;padding:.5rem;margin:.25rem 0 .75rem;border:1px solid #ddd;border-radius:8px&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;/label&amp;gt;&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;label&amp;gt;&amp;lt;b&amp;gt;Thumb-URL&amp;lt;/b&amp;gt; (optional, 120–300px breit):&lt;br /&gt;
    &amp;lt;input id=&amp;quot;idx-thumb&amp;quot; type=&amp;quot;url&amp;quot; placeholder=&amp;quot;https://ados-wiki.de/images/.../thumb.jpg&amp;quot;&lt;br /&gt;
           style=&amp;quot;width:100%;padding:.5rem;margin:.25rem 0 .75rem;border:1px solid #ddd;border-radius:8px&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;/label&amp;gt;&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;label&amp;gt;&amp;lt;b&amp;gt;Bilddatei&amp;lt;/b&amp;gt; (Frontlabel-Foto):&lt;br /&gt;
    &amp;lt;input id=&amp;quot;idx-file&amp;quot; type=&amp;quot;file&amp;quot; accept=&amp;quot;image/*&amp;quot; style=&amp;quot;display:block;margin:.25rem 0 .75rem;&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;/label&amp;gt;&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;button id=&amp;quot;idx-run&amp;quot; style=&amp;quot;padding:.6rem .9rem;border-radius:10px;background:#2a4b8d;color:#fff;border:none;cursor:pointer&amp;quot;&amp;gt;Embedding erzeugen &amp;amp;amp; speichern&amp;lt;/button&amp;gt;&lt;br /&gt;
  &amp;lt;span id=&amp;quot;idx-status&amp;quot; style=&amp;quot;margin-left:.75rem;color:#555;&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;div id=&amp;quot;idx-preview&amp;quot; style=&amp;quot;margin-top:1rem&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;h3&amp;gt;Zuletzt erzeugter JSON-Eintrag&amp;lt;/h3&amp;gt;&lt;br /&gt;
  &amp;lt;textarea id=&amp;quot;idx-out&amp;quot; rows=&amp;quot;5&amp;quot; style=&amp;quot;width:100%;font-family:ui-monospace,Consolas,monospace;padding:.6rem;border:1px solid #ddd;border-radius:10px&amp;quot;&amp;gt;&amp;lt;/textarea&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;script&amp;gt;&lt;br /&gt;
/* global mw */&lt;br /&gt;
(function(){&lt;br /&gt;
  const INDEX_TITLE = &amp;#039;MediaWiki:Gadget-LabelScan-index.json&amp;#039;;&lt;br /&gt;
&lt;br /&gt;
  // ---- Modell-Config (wie in deinem Gadget) ----&lt;br /&gt;
  const transformersURL = &amp;#039;https://cdn.jsdelivr.net/npm/@xenova/transformers@2.15.0&amp;#039;;&lt;br /&gt;
  const MODEL_ID = &amp;#039;Xenova/clip-vit-base-patch32&amp;#039;;&lt;br /&gt;
  const LOCAL_MODEL_PATH = &amp;#039;/models&amp;#039;;&lt;br /&gt;
&lt;br /&gt;
  // ---- UI helpers ----&lt;br /&gt;
  const $ = id =&amp;gt; document.getElementById(id);&lt;br /&gt;
  const status = (t) =&amp;gt; { const el=$(&amp;#039;idx-status&amp;#039;); if(el) el.textContent=t||&amp;#039;&amp;#039;; };&lt;br /&gt;
&lt;br /&gt;
  // Rechtecheck&lt;br /&gt;
  function hasSysop(){&lt;br /&gt;
    const groups = mw.config.get(&amp;#039;wgUserGroups&amp;#039;) || [];&lt;br /&gt;
    return groups.includes(&amp;#039;sysop&amp;#039;) || groups.includes(&amp;#039;interface-admin&amp;#039;);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // Float32 → base64&lt;br /&gt;
  function float32ToBase64(vec){&lt;br /&gt;
    const bytes = new Uint8Array(vec.buffer);&lt;br /&gt;
    let bin = &amp;#039;&amp;#039;, chunk = 0x8000;&lt;br /&gt;
    for (let i=0; i&amp;lt;bytes.length; i+=chunk) {&lt;br /&gt;
      bin += String.fromCharCode.apply(null, bytes.subarray(i, i+chunk));&lt;br /&gt;
    }&lt;br /&gt;
    return btoa(bin);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // Optional: EXIF-korrekte Canvas-Erzeugung (Fallback ohne OffscreenCanvas)&lt;br /&gt;
  async function fileToCanvasExif(file){&lt;br /&gt;
    if (&amp;#039;createImageBitmap&amp;#039; in window) {&lt;br /&gt;
      const bmp = await createImageBitmap(file, { imageOrientation: &amp;#039;from-image&amp;#039; });&lt;br /&gt;
      // OffscreenCanvas bevorzugen, fallback auf &amp;lt;canvas&amp;gt;&lt;br /&gt;
      if (&amp;#039;OffscreenCanvas&amp;#039; in window) {&lt;br /&gt;
        const c = new OffscreenCanvas(bmp.width, bmp.height);&lt;br /&gt;
        c.getContext(&amp;#039;2d&amp;#039;).drawImage(bmp, 0, 0);&lt;br /&gt;
        return c;&lt;br /&gt;
      } else {&lt;br /&gt;
        const c = document.createElement(&amp;#039;canvas&amp;#039;);&lt;br /&gt;
        c.width = bmp.width; c.height = bmp.height;&lt;br /&gt;
        c.getContext(&amp;#039;2d&amp;#039;).drawImage(bmp, 0, 0);&lt;br /&gt;
        return c;&lt;br /&gt;
      }&lt;br /&gt;
    } else {&lt;br /&gt;
      // klassischer Weg&lt;br /&gt;
      const url = URL.createObjectURL(file);&lt;br /&gt;
      try {&lt;br /&gt;
        const img = await new Promise((res, rej)=&amp;gt;{&lt;br /&gt;
          const im = new Image();&lt;br /&gt;
          im.onload = ()=&amp;gt;res(im);&lt;br /&gt;
          im.onerror = rej;&lt;br /&gt;
          im.src = url;&lt;br /&gt;
        });&lt;br /&gt;
        const c = document.createElement(&amp;#039;canvas&amp;#039;);&lt;br /&gt;
        c.width = img.width; c.height = img.height;&lt;br /&gt;
        c.getContext(&amp;#039;2d&amp;#039;).drawImage(img, 0, 0);&lt;br /&gt;
        return c;&lt;br /&gt;
      } finally {&lt;br /&gt;
        URL.revokeObjectURL(url);&lt;br /&gt;
      }&lt;br /&gt;
    }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ---- Transformers laden (einmalig) ----&lt;br /&gt;
  let _load;&lt;br /&gt;
  async function ensureModel(){&lt;br /&gt;
    if (_load) return _load;&lt;br /&gt;
    _load = (async()=&amp;gt;{&lt;br /&gt;
      const mod = await import(/* webpackIgnore: true */ transformersURL);&lt;br /&gt;
&lt;br /&gt;
      // Nur lokale Modelle (wie beim Gadget)&lt;br /&gt;
      mod.env.allowLocalModels = true;&lt;br /&gt;
      mod.env.allowRemoteModels = false;&lt;br /&gt;
      mod.env.localModelPath = LOCAL_MODEL_PATH;&lt;br /&gt;
&lt;br /&gt;
      // (Optional) WebGPU bevorzugen – fallback bleibt wasm&lt;br /&gt;
      // mod.env.backends = mod.env.backends || {};&lt;br /&gt;
      // mod.env.backends.onnx = mod.env.backends.onnx || {};&lt;br /&gt;
      // mod.env.backends.onnx.preferredBackend = &amp;#039;webgpu&amp;#039;;&lt;br /&gt;
&lt;br /&gt;
      // WASM-Runtime-Pfad (ort-wasm-simd.wasm)&lt;br /&gt;
      mod.env.backends = mod.env.backends || {};&lt;br /&gt;
      mod.env.backends.onnx = mod.env.backends.onnx || {};&lt;br /&gt;
      mod.env.backends.onnx.wasm = mod.env.backends.onnx.wasm || {};&lt;br /&gt;
      mod.env.backends.onnx.wasm.wasmPaths =&lt;br /&gt;
        &amp;#039;https://cdn.jsdelivr.net/npm/@xenova/transformers@2.15.0/dist/&amp;#039;;&lt;br /&gt;
&lt;br /&gt;
      const [processor, model] = await Promise.all([&lt;br /&gt;
        mod.AutoProcessor.from_pretrained(MODEL_ID),&lt;br /&gt;
        mod.CLIPVisionModelWithProjection.from_pretrained(MODEL_ID, { quantized: true })&lt;br /&gt;
      ]);&lt;br /&gt;
&lt;br /&gt;
      return { mod, processor, model };&lt;br /&gt;
    })();&lt;br /&gt;
    return _load;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  async function buildEmbeddingFromFile(file){&lt;br /&gt;
    const { mod, processor, model } = await ensureModel();&lt;br /&gt;
&lt;br /&gt;
    // Canvas (EXIF-korrigiert)&lt;br /&gt;
    const canvas = await fileToCanvasExif(file);&lt;br /&gt;
    // Canvas → Blob → RawImage (robust für Processor)&lt;br /&gt;
    const blob = (canvas.convertToBlob)&lt;br /&gt;
      ? await canvas.convertToBlob({ type:&amp;#039;image/jpeg&amp;#039;, quality:0.95 })&lt;br /&gt;
      : await new Promise(r =&amp;gt; canvas.toBlob(r, &amp;#039;image/jpeg&amp;#039;, 0.95));&lt;br /&gt;
&lt;br /&gt;
    const imageRaw = await mod.RawImage.fromBlob(blob);&lt;br /&gt;
    const inputs = await processor(imageRaw, { return_tensors: &amp;#039;pt&amp;#039; });&lt;br /&gt;
    const out = await model.forward({ pixel_values: inputs.pixel_values });&lt;br /&gt;
    const vec = out?.image_embeds?.data || out?.image_embeds;&lt;br /&gt;
    if (!(vec instanceof Float32Array)) throw new Error(&amp;#039;Embedding-Format unerwartet&amp;#039;);&lt;br /&gt;
&lt;br /&gt;
    // Normieren&lt;br /&gt;
    let n=0; for(let i=0;i&amp;lt;vec.length;i++) n+=vec[i]*vec[i];&lt;br /&gt;
    const norm = Math.sqrt(n)||1;&lt;br /&gt;
    const v = new Float32Array(vec.length);&lt;br /&gt;
    for(let i=0;i&amp;lt;vec.length;i++) v[i]=vec[i]/norm;&lt;br /&gt;
    return v;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ---- Index laden &amp;amp; speichern ----&lt;br /&gt;
  async function fetchIndexJSON(){&lt;br /&gt;
    const url = mw.util.getUrl(INDEX_TITLE, { action:&amp;#039;raw&amp;#039;, ctype:&amp;#039;application/json&amp;#039; });&lt;br /&gt;
    const res = await fetch(url, { cache: &amp;#039;no-store&amp;#039; });&lt;br /&gt;
    if (!res.ok) throw new Error(&amp;#039;Index nicht ladbar: &amp;#039;+res.status);&lt;br /&gt;
    const txt = await res.text();&lt;br /&gt;
    // robust gegen leere/kaputte Inhalte&lt;br /&gt;
    let arr;&lt;br /&gt;
    try { arr = JSON.parse(txt || &amp;#039;[]&amp;#039;); }&lt;br /&gt;
    catch(_){ arr = []; }&lt;br /&gt;
    if (!Array.isArray(arr)) arr = [];&lt;br /&gt;
    return arr;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  async function saveIndexJSON(newArray, summary){&lt;br /&gt;
    await mw.loader.using([&amp;#039;mediawiki.api&amp;#039;]);&lt;br /&gt;
    const api = new mw.Api();&lt;br /&gt;
&lt;br /&gt;
    // Hole aktuelle Seite, um Timestamp für Konflikt-Schutz zu haben&lt;br /&gt;
    const meta = await api.get({&lt;br /&gt;
      action: &amp;#039;query&amp;#039;,&lt;br /&gt;
      prop: &amp;#039;revisions&amp;#039;,&lt;br /&gt;
      titles: INDEX_TITLE,&lt;br /&gt;
      rvprop: &amp;#039;timestamp|content&amp;#039;,&lt;br /&gt;
      format: &amp;#039;json&amp;#039;&lt;br /&gt;
    });&lt;br /&gt;
&lt;br /&gt;
    const pages = meta?.query?.pages || {};&lt;br /&gt;
    const page = pages[Object.keys(pages)[0]];&lt;br /&gt;
    const baseTimestamp = page?.revisions?.[0]?.timestamp;&lt;br /&gt;
&lt;br /&gt;
    const text = JSON.stringify(newArray, null, 2) + &amp;#039;\n&amp;#039;;&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
      const res = await api.postWithToken(&amp;#039;csrf&amp;#039;, {&lt;br /&gt;
        action: &amp;#039;edit&amp;#039;,&lt;br /&gt;
        title: INDEX_TITLE,&lt;br /&gt;
        text,&lt;br /&gt;
        summary: summary || &amp;#039;LabelScan: +1 embedding (Auto-Indexer)&amp;#039;,&lt;br /&gt;
        nocreate: 0,&lt;br /&gt;
        bot: 1,&lt;br /&gt;
        basetimestamp: baseTimestamp&lt;br /&gt;
      });&lt;br /&gt;
      return res;&lt;br /&gt;
    } catch (e) {&lt;br /&gt;
      // einfacher Retry bei Konflikt: neu holen und erneut schreiben&lt;br /&gt;
      if ((e?.details||&amp;#039;&amp;#039;).includes(&amp;#039;editconflict&amp;#039;)) {&lt;br /&gt;
        const fresh = await fetchIndexJSON();&lt;br /&gt;
        const merged = mergeArraysUnique(fresh, newArray); // simple Merge, Dedupe&lt;br /&gt;
        const text2 = JSON.stringify(merged, null, 2) + &amp;#039;\n&amp;#039;;&lt;br /&gt;
        return api.postWithToken(&amp;#039;csrf&amp;#039;, {&lt;br /&gt;
          action: &amp;#039;edit&amp;#039;,&lt;br /&gt;
          title: INDEX_TITLE,&lt;br /&gt;
          text: text2,&lt;br /&gt;
          summary: (summary || &amp;#039;LabelScan: +1 embedding (Auto-Indexer)&amp;#039;) + &amp;#039; (merge)&amp;#039;,&lt;br /&gt;
          nocreate: 0,&lt;br /&gt;
          bot: 1&lt;br /&gt;
        });&lt;br /&gt;
      }&lt;br /&gt;
      throw e;&lt;br /&gt;
    }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // einfache Duplikat-Entfernung (identische title+embed)&lt;br /&gt;
  function mergeArraysUnique(base, add){&lt;br /&gt;
    const seen = new Set(base.map(x =&amp;gt; (x.title||&amp;#039;&amp;#039;)+&amp;#039;|&amp;#039;+(x.embed||&amp;#039;&amp;#039;)));&lt;br /&gt;
    for (const it of add) {&lt;br /&gt;
      const key = (it.title||&amp;#039;&amp;#039;)+&amp;#039;|&amp;#039;+(it.embed||&amp;#039;&amp;#039;);&lt;br /&gt;
      if (!seen.has(key)) { base.push(it); seen.add(key); }&lt;br /&gt;
    }&lt;br /&gt;
    return base;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ---- Klick-Handler ----&lt;br /&gt;
  $(&amp;#039;idx-run&amp;#039;).addEventListener(&amp;#039;click&amp;#039;, async ()=&amp;gt;{&lt;br /&gt;
    try{&lt;br /&gt;
      if (!hasSysop()) {&lt;br /&gt;
        alert(&amp;#039;Du brauchst Admin-Rechte (sysop/interface-admin), um den Index automatisch zu speichern.&amp;#039;);&lt;br /&gt;
        return;&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      const title = $(&amp;#039;idx-title&amp;#039;).value.trim();&lt;br /&gt;
      const thumb = $(&amp;#039;idx-thumb&amp;#039;).value.trim();&lt;br /&gt;
      const file  = $(&amp;#039;idx-file&amp;#039;).files?.[0];&lt;br /&gt;
&lt;br /&gt;
      if (!title) return alert(&amp;#039;Bitte Artikel-Titel eingeben.&amp;#039;);&lt;br /&gt;
      if (!file)  return alert(&amp;#039;Bitte Bilddatei wählen.&amp;#039;);&lt;br /&gt;
&lt;br /&gt;
      status(&amp;#039;Modell laden …&amp;#039;);&lt;br /&gt;
      await ensureModel();&lt;br /&gt;
&lt;br /&gt;
      status(&amp;#039;Embedding berechnen …&amp;#039;);&lt;br /&gt;
      const vec = await buildEmbeddingFromFile(file);&lt;br /&gt;
      const b64 = float32ToBase64(vec);&lt;br /&gt;
&lt;br /&gt;
      // Vorschau&lt;br /&gt;
      $(&amp;#039;idx-preview&amp;#039;).innerHTML = &amp;#039;&amp;#039;;&lt;br /&gt;
      const u = URL.createObjectURL(file);&lt;br /&gt;
      const img = document.createElement(&amp;#039;img&amp;#039;);&lt;br /&gt;
      img.src = u; img.style.maxWidth=&amp;#039;280px&amp;#039;; img.style.borderRadius=&amp;#039;10px&amp;#039;;&lt;br /&gt;
      $(&amp;#039;idx-preview&amp;#039;).appendChild(img);&lt;br /&gt;
&lt;br /&gt;
      const newRow = { title, thumb: thumb || &amp;#039;&amp;#039;, embed: b64 };&lt;br /&gt;
      $(&amp;#039;idx-out&amp;#039;).value = JSON.stringify(newRow);&lt;br /&gt;
&lt;br /&gt;
      status(&amp;#039;Index laden …&amp;#039;);&lt;br /&gt;
      const arr = await fetchIndexJSON();&lt;br /&gt;
&lt;br /&gt;
      const merged = mergeArraysUnique(arr, [newRow]);&lt;br /&gt;
&lt;br /&gt;
      status(&amp;#039;Speichern …&amp;#039;);&lt;br /&gt;
      await saveIndexJSON(merged, `LabelScan: +1 embedding für &amp;quot;${title}&amp;quot;`);&lt;br /&gt;
&lt;br /&gt;
      status(&amp;#039;Gespeichert ✅&amp;#039;);&lt;br /&gt;
    } catch(e){&lt;br /&gt;
      console.error(e);&lt;br /&gt;
      status(&amp;#039;Fehler: &amp;#039; + (e?.message || e));&lt;br /&gt;
      alert(&amp;#039;Fehler beim Speichern:\n&amp;#039; + (e?.message || e));&lt;br /&gt;
    }&lt;br /&gt;
  });&lt;br /&gt;
})();&lt;br /&gt;
&amp;lt;/script&amp;gt;&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
</feed>