From faa4868ee28d493589aa11fb4d5bc537f73597e9 Mon Sep 17 00:00:00 2001 From: duffyduck Date: Sat, 31 Oct 2026 19:24:33 +0100 Subject: [PATCH] =?UTF-8?q?feat(diagnostic):=20=E2=84=B9=20im=20Modell-Kat?= =?UTF-8?q?alog=20=E2=80=94=20was=20kann=20das=20Modell,=20ist=20es=20zens?= =?UTF-8?q?iert=3F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Neben jedem Katalog-Eintrag ℹ → Fenster mit: - Kurzbeschreibung auf Deutsch, beim ersten Oeffnen vom LOKALEN Modell aus der Modellkarte (Grundmodell + GGUF-Repo) erzeugt und im Katalog gespeichert (summary_de) — so hat jedes neue Modell eine, ohne Pflege. Hinweis "kann Fehler enthalten". Laeuft kein Modell: Meldung. - Feste deutsche Texte je Modellfamilie (Qwen3-Coder, Foundation-Sec, VulnLLM, Dolphin, Hermes, Lexi, Gemma, Qwen, Llama, Mistral, DeepSeek, Phi), erkannt als ganzes Wort in Name/Grundmodell/Schlagworten. - "unzensiert" / "mit Schutzfiltern" (uncensored/abliterated/heretic/ dolphin/lexi … in Name oder Schlagworten). - Fakten von HF: Grundmodell, Autor, Lizenz, Beliebtheit, Stand, Schlagworte; Auszug der Modellkarte (Link-Absaetze uebersprungen). Server: /api/llm-model-info, /api/llm-model-summary, rvsRequest() als Promise-Variante fuer eigene RVS-Anfragen. Getestet: Info-Teil gegen HuggingFace (4 Modelle). Die Zusammenfassung durch das lokale Modell ungetestet (Karte gerade im Gegenlauf belegt). Co-Authored-By: Claude Opus 5.5 --- diagnostic/index.html | 68 +++++++++++++++++++- diagnostic/server.js | 145 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 212 insertions(+), 1 deletion(-) diff --git a/diagnostic/index.html b/diagnostic/index.html index bebc038..ce6c0e8 100644 --- a/diagnostic/index.html +++ b/diagnostic/index.html @@ -9822,7 +9822,9 @@ : ''; const searchText = [m.id, m.description, m.hfRepo, m.quant].filter(Boolean).join(' '); return '' + - '' + escapeHtml(m.id) + '' + + '' + escapeHtml(m.id) + '' + + (m.hfRepo ? ' ' : '') + + '' + '' + escapeHtml(m.description || m.hfRepo || '') + (m.quant ? ' · ' + escapeHtml(m.quant) + '' : '') + (m.unusable ? ' · nicht ladbar' : '') @@ -10241,6 +10243,70 @@ : ''); } catch (e) { out.innerHTML = '✗ ' + escapeHtml(e.message) + ''; } } + // ℹ: Kurzbeschreibung, zensiert/unzensiert, Fakten von HuggingFace + async function openModelInfo(i) { + const m = _llmCatalog[i]; + if (!m || !m.hfRepo) return; + const box = _modelInfoModal('
Frage HuggingFace nach ' + escapeHtml(m.hfRepo) + ' …
'); + let j; + try { j = await (await fetch('/api/llm-model-info?repo=' + encodeURIComponent(m.hfRepo))).json(); } + catch (e) { j = { ok: false, error: e.message }; } + if (!document.body.contains(box)) return; + if (!j.ok) { _modelInfoModal('
✗ ' + escapeHtml(j.error || 'Fehler') + '
'); return; } + const fam = (j.families || []).map(f => `
${escapeHtml(f.name)}
${escapeHtml(f.text)}
`).join('') + || '
Für diese Modellfamilie habe ich keine Kurzbeschreibung — siehe Modellkarte unten.
'; + const cens = j.uncensored + ? `unzensiert ${escapeHtml(j.uncensoredWhy)} — lehnt kaum etwas ab; du bist selbst verantwortlich, was du damit machst.` + : `mit Schutzfiltern Standard des Herstellers — lehnt heikle Anfragen ab (keine Hinweise auf „uncensored“ o.ä.).`; + _loadModelSummary(m.hfRepo); + const row = (k, v) => v ? `${k}${v}` : ''; + const date = x => x ? new Date(x).toLocaleDateString('de-DE') : ''; + _modelInfoModal(` +

ℹ ${escapeHtml(m.id)}

+ ${escapeHtml(m.hfRepo)} +
${cens}
+

Was kann es?

+
Kurzbeschreibung wird vom lokalen Modell aus der Modellkarte erstellt …
+ ${fam} + + ${row('Größe', m.sizeGB ? '~' + m.sizeGB + ' GB · ' + escapeHtml(m.quant || '') : '')} + ${row('Grundmodell', j.base ? escapeHtml(j.base) : '')} + ${row('Von', escapeHtml(j.author))} + ${row('Lizenz', escapeHtml(j.license))} + ${row('Beliebtheit', '↓ ' + Number(j.downloads).toLocaleString('de') + ' · ♥ ' + j.likes)} + ${row('Stand', date(j.updated))} + ${row('Schlagworte', (j.tags || []).map(t => '' + escapeHtml(t) + '').join(', '))} +
+ ${j.intro ? `
Aus der Modellkarte (${escapeHtml(j.introFrom)}, englisch)
${escapeHtml(j.intro)}
` : ''}`); + } + // Deutsche Kurzbeschreibung: einmal vom lokalen Modell erzeugt, danach aus dem Katalog + async function _loadModelSummary(repo) { + let j; + try { j = await (await fetch('/api/llm-model-summary?repo=' + encodeURIComponent(repo))).json(); } + catch (e) { j = { ok: false, error: e.message }; } + const el = document.getElementById('model-info-summary'); + if (!el) return; + if (j.ok) { + el.style.color = '#E0E0F0'; + el.innerHTML = escapeHtml(j.summary) + `
Zusammengefasst aus der Modellkarte von ${escapeHtml(j.by || 'dem lokalen Modell')} — kann Fehler enthalten.
`; + } else { + el.innerHTML = 'Keine Kurzbeschreibung: ' + escapeHtml(j.error || 'unbekannt') + ' (läuft ein lokales Modell?)'; + } + } + function _modelInfoModal(html) { + let m = document.getElementById('model-info-modal'); + if (!m) { + m = document.createElement('div'); + m.id = 'model-info-modal'; + m.style.cssText = 'position:fixed;inset:0;z-index:9000;background:rgba(0,0,0,.6);display:flex;align-items:center;justify-content:center;padding:16px;'; + m.addEventListener('click', e => { if (e.target === m) m.remove(); }); + document.addEventListener('keydown', function esc(e) { if (e.key === 'Escape') { m.remove(); document.removeEventListener('keydown', esc); } }); + document.body.appendChild(m); + } + m.innerHTML = `
+ ${html}
`; + return m; + } async function addTrendingToCatalog(btn, repo) { btn.disabled = true; btn.textContent = '…'; try { diff --git a/diagnostic/server.js b/diagnostic/server.js index 46d32e3..1448b1f 100644 --- a/diagnostic/server.js +++ b/diagnostic/server.js @@ -660,6 +660,121 @@ async function hfGgufAlternatives(repo) { } catch { return []; } } +// ── ℹ im Modell-Katalog: was kann das Modell, ist es zensiert? ────── +// HuggingFace liefert Grundmodell, Lizenz und Schlagworte, aber keine +// verstaendliche Beschreibung. Darum: kurze deutsche Texte je Modellfamilie +// (erkannt am Namen) + die Fakten von HF + Anfang der Modellkarte. +const MODEL_FAMILIES = [ + ["qwen3-coder", "Qwen3-Coder (Alibaba)", "Aufs Programmieren spezialisiert: Code schreiben, verstehen, umbauen, mit Werkzeugen arbeiten (Agenten). Die 30B-A3B-Fassung ist ein MoE-Modell: pro Wort rechnen nur ~3 Mrd. Parameter — schnell für seine Größe."], + ["foundation-sec", "Foundation-Sec (Cisco Foundation AI)", "Auf IT-Sicherheit trainiert: Schwachstellen, Angriffstechniken (MITRE ATT&CK), Logs und Warnmeldungen auswerten. Die reine „Foundation-Sec-8B“ ist ein Basismodell (setzt nur Text fort) — zum Chatten die Instruct- oder Reasoning-Fassung nehmen."], + ["vulnllm", "VulnLLM-R", "Sucht Sicherheitslücken in Quellcode und begründet Schritt für Schritt (Reasoning), warum eine Stelle verwundbar ist."], + ["dolphin", "Dolphin (Cognitive Computations)", "Bewusst ohne Ablehnungen trainiert: macht, was man verlangt, und lässt sich stark über den Systemprompt steuern. Gut für Aufgaben, die große Anbieter verweigern — man ist aber selbst verantwortlich, was man damit tut."], + ["hermes", "Hermes (Nous Research)", "Sehr gutes Befolgen von Anweisungen, Rollenspiel und Werkzeug-Aufrufe (Function Calling), wenige Ablehnungen. Hermes 4 kann zusätzlich „denken“ (Reasoning)."], + ["lexi", "Lexi (Orenguteng)", "Unzensierter Llama-Ableger für allgemeinen Chat, beantwortet auch Fragen, die das Original ablehnt."], + ["gemma", "Gemma (Google)", "Allround-Modell mit gutem Deutsch; neuere Fassungen verstehen auch Bilder. Eher vorsichtig, lehnt heikle Themen ab."], + ["qwen", "Qwen (Alibaba)", "Starke Allround-Familie: Wissen, Logik, Mathe, Code, viele Sprachen inkl. Deutsch; neuere Fassungen mit Denkmodus. Standard-Fassungen haben Schutzfilter."], + ["llama", "Llama (Meta)", "Bewährtes Allround-Modell und Basis für sehr viele Ableger; ordentliches Deutsch, solide Werkzeug-Aufrufe."], + ["mistral", "Mistral (Mistral AI, Frankreich)", "Gut in europäischen Sprachen, effizient; Mistral Small 24B ist für seine Größe sehr stark."], + ["deepseek", "DeepSeek", "Stark in Logik, Mathe und Code, oft mit ausführlichem Denkweg (Reasoning)."], + ["phi", "Phi (Microsoft)", "Klein und schnell, erstaunlich gut in Logik für die Größe; eher Englisch."], +]; +const UNCENSORED_RE = /uncensor|abliterat|heretic|obliterat|dolphin|lexi|unfiltered|nsfw|no.?refus/i; +async function hfJson(url) { + try { + const r = await fetch(url, { headers: { "User-Agent": "aria-diagnostic" } }); + return r.ok ? await r.json() : null; + } catch { return null; } +} +async function hfReadmeIntro(repo) { + try { + const r = await fetch(`https://huggingface.co/${repo}/raw/main/README.md`, { headers: { "User-Agent": "aria-diagnostic" } }); + if (!r.ok) return ""; + let t = await r.text(); + t = t.replace(/^---[\s\S]*?\n---\n/, ""); // YAML-Kopf + const paras = t.split(/\n\s*\n/).map(p => p.trim()) + .filter(p => p && !/^(#|!\[|<|\||```|\[!\[|>|-{3,})/.test(p) && p.replace(/[^A-Za-z]/g, "").length > 60) + // Absaetze, die fast nur Links sind (Website:, Twitter: …), ueberspringen + .filter(p => p.replace(/https?:\/\/\S+/g, "").replace(/[^A-Za-z]/g, "").length > 80); + return paras.slice(0, 2).join("\n\n").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").slice(0, 900); + } catch { return ""; } +} +// Ganze Modellkarte als Text (ohne YAML/Bilder/HTML), fuer die Zusammenfassung +async function hfReadmeText(repo, max = 7000) { + try { + const r = await fetch(`https://huggingface.co/${repo}/raw/main/README.md`, { headers: { "User-Agent": "aria-diagnostic" } }); + if (!r.ok) return ""; + return (await r.text()).replace(/^---[\s\S]*?\n---\n/, "").replace(/!\[[^\]]*\]\([^)]*\)/g, "") + .replace(/<[^>]+>/g, "").replace(/\n{3,}/g, "\n\n").slice(0, max); + } catch { return ""; } +} +// Deutsche Kurzbeschreibung vom LOKALEN Modell aus der Modellkarte — einmal +// erzeugt, dann im Katalog gespeichert (summary_de). So hat jedes neue Modell +// eine, ohne dass jemand Texte pflegt. +async function llmModelSummary(repo) { + repo = hfRepoFromInput(repo); + const cat = loadLlmCatalog(); + const entry = cat.find(m => m.hfRepo === repo); + if (entry && entry.summary_de) return { summary: entry.summary_de, by: entry.summary_by || "", cached: true }; + const meta = await hfJson(`https://huggingface.co/api/models/${repo}`) || {}; + let base = (meta.cardData || {}).base_model; + base = Array.isArray(base) ? base[0] : base || ""; + const card = [await hfReadmeText(base) , await hfReadmeText(repo, 3000)].filter(Boolean).join("\n\n---\n\n"); + if (!card.trim()) return { summary: "", error: "Keine Modellkarte auf HuggingFace." }; + const tags = (meta.tags || []).filter(t => !/^(base_model|license|region|endpoints)/.test(t)).slice(0, 25).join(", "); + const res = await rvsRequest("llm_request", { + requestId: "modelinfo_" + Date.now(), + messages: [ + { role: "system", content: "Du fasst Modellkarten von KI-Sprachmodellen fuer einen Laien auf Deutsch zusammen. " + + "Nur was in der Karte steht, nichts erfinden. 3 bis 5 kurze Saetze: wofuer das Modell gedacht ist, worin es " + + "besonders gut ist, ob es Ablehnungen/Schutzfilter entfernt hat (uncensored/abliterated), und Besonderheiten " + + "(Denkmodus, Werkzeuge, Sprachen, Groesse). Keine Ueberschrift, kein Markdown." }, + { role: "user", content: `Modell: ${repo}\nGrundmodell: ${base || "-"}\nSchlagworte: ${tags}\n\nModellkarte:\n${card}` }, + ], + max_tokens: 400, temperature: 0.2, model: "", targetInstance: "", + }, "llm_response", 180000); + const text = String(res.content || res.text || "").replace(/[\s\S]*?<\/think>/g, "").trim(); + if (!res.ok || !text) return { summary: "", error: res.error || "Das lokale Modell hat nicht geantwortet." }; + const by = res.model || "lokales Modell"; + if (entry) { + entry.summary_de = text; entry.summary_by = by; entry.summary_at = new Date().toISOString(); + saveLlmCatalog(cat); + } + return { summary: text, by, cached: false }; +} + +async function llmModelInfo(repo) { + repo = hfRepoFromInput(repo); + if (!repo) throw new Error("ungueltiges Repo"); + const meta = await hfJson(`https://huggingface.co/api/models/${repo}`) || {}; + const card = meta.cardData || {}; + let base = card.base_model; + base = Array.isArray(base) ? base[0] : base || ""; + const baseMeta = base ? (await hfJson(`https://huggingface.co/api/models/${base}`) || {}) : {}; + const tags = [...new Set([...(meta.tags || []), ...((card.tags) || [])])] + .filter(t => !/^(base_model|license|region|endpoints|autotrain|text-generation-inference|safetensors|transformers|gguf|conversational|imatrix)/.test(t)); + const hay = [repo, base, ...tags].join(" ").toLowerCase(); + const fams = []; + for (const [key, name, text] of MODEL_FAMILIES) { + // Ganzes Wort am Anfang ("phi" nicht in "dolphin" finden) + const re = new RegExp("(^|[^a-z0-9])" + key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")); + if (name && re.test(hay) && !fams.some(f => f.name === name)) fams.push({ name, text }); + } + // Allgemeine Familien (Qwen/Llama …) nur, wenn keine speziellere passt + const special = fams.filter(f => !/^(Qwen|Llama|Mistral|Gemma) \(/.test(f.name)); + const families = special.length ? [...special, ...fams.filter(f => !special.includes(f)).slice(0, 1)] : fams.slice(0, 1); + const unc = UNCENSORED_RE.exec(hay); + return { + repo, base, author: meta.author || repo.split("/")[0], license: card.license || "", + downloads: meta.downloads || 0, likes: meta.likes || 0, + created: meta.createdAt || "", updated: meta.lastModified || "", + tags: tags.slice(0, 20), families, + uncensored: !!unc, + uncensoredWhy: unc ? `erkannt an „${unc[0]}“ in Name/Schlagworten` : "", + intro: await hfReadmeIntro(base || repo) || await hfReadmeIntro(repo), + introFrom: base || repo, + }; +} + async function addHfRepoToCatalog(repo, description = "") { repo = hfRepoFromInput(repo); if (!/^[\w.-]+\/[\w.-]+$/.test(String(repo || ""))) throw new Error("ungueltiges Repo — Link oder org/name"); @@ -1689,6 +1804,26 @@ function sendToRVS_withResponse(sendType, sendPayload, expectType, clientWs, tim freshWs.on("error", () => {}); } +// Wie sendToRVS_withResponse, aber als Promise fuer den Server selbst: +// eigene Verbindung, wartet auf expectType mit passender requestId. +function rvsRequest(sendType, sendPayload, expectType, timeoutMs = 60000) { + return new Promise((resolve) => { + if (!RVS_HOST || !RVS_TOKEN) return resolve({ ok: false, error: "RVS nicht konfiguriert" }); + const proto = RVS_TLS === "true" ? "wss" : "ws"; + const ws2 = new WebSocket(`${proto}://${RVS_HOST}:${RVS_PORT}?token=${RVS_TOKEN}`); + const done = (v) => { clearTimeout(t); try { ws2.close(); } catch (_) {} resolve(v); }; + const t = setTimeout(() => done({ ok: false, error: "Zeitüberschreitung" }), timeoutMs); + ws2.on("open", () => ws2.send(JSON.stringify({ type: sendType, payload: sendPayload, timestamp: Date.now() }))); + ws2.on("message", (raw) => { + try { + const r = JSON.parse(raw.toString()); + if (r.type === expectType && (r.payload || {}).requestId === sendPayload.requestId) done(r.payload || {}); + } catch {} + }); + ws2.on("error", (e) => done({ ok: false, error: e.message })); + }); +} + function sendToRVS_raw(msgObj) { if (!RVS_HOST || !RVS_TOKEN) return; const payload = JSON.stringify(msgObj); @@ -2504,6 +2639,16 @@ const server = http.createServer((req, res) => { res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ ok: false, error: err.message, items: [] })); }); + } else if (req.url.startsWith("/api/llm-model-info") && req.method === "GET") { + const repo = new URL(req.url, "http://x").searchParams.get("repo") || ""; + llmModelInfo(repo) + .then(info => { res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ ok: true, ...info })); }) + .catch(err => { res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ ok: false, error: err.message })); }); + } else if (req.url.startsWith("/api/llm-model-summary") && req.method === "GET") { + const repo = new URL(req.url, "http://x").searchParams.get("repo") || ""; + llmModelSummary(repo) + .then(r => { res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ ok: !!r.summary, ...r })); }) + .catch(err => { res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ ok: false, error: err.message })); }); } else if (req.url === "/api/llm-catalog/add" && req.method === "POST") { let body = ""; req.on("data", c => { body += c; if (body.length > 10000) req.destroy(); });