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 @@
: '(keine Box online) ';
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(); });