feat(diagnostic): Katalog von HuggingFace — Trends, Code, unzensiert; taeglich; aufraeumen
"Von HuggingFace aktualisieren" holte nur die 40 meistgeladenen GGUF aller Zeiten — neue und unzensierte Modelle kamen so kaum vor. Jetzt: - Blickwinkel: beliebt, im Trend (trendingScore), Code, unzensiert (uncensored/heretic/abliterated). - Nur Sprachmodelle (pipeline_tag + Namensfilter gegen Bild/Audio/Musik/ Video/Embedding) und hoechstens LLM_CATALOG_MAX_GB (30). - Markierungen in der Liste: "unzensiert", "neu" (letzte 7 Tage). - Einmal am Tag automatisch (LLM_CATALOG_REFRESH_HOURS, 0 = aus). - Aufraeumen: HF-Eintraege aelter als LLM_CATALOG_KEEP_DAYS (30), auf keiner Box und nicht mehr gelistet, fliegen raus; kuratierte und von Hand hinzugefuegte bleiben. Getestet gegen HuggingFace mit Test-Katalog: 43 neue in 2 s, keine Bild-/Audio-Modelle mehr, groesstes 29,2 GB. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -9706,7 +9706,7 @@
|
||||
const j = await r.json();
|
||||
_llmCatalog = (j && j.models) || _llmCatalog;
|
||||
renderLlmCatalog();
|
||||
if (st) { st.textContent = j.ok ? `✓ ${j.added || 0} neue Modelle von HuggingFace` : `✗ ${j.error || 'Fehler'}`; st.style.color = j.ok ? '#3FFF3F' : '#FF6B6B'; }
|
||||
if (st) { st.textContent = j.ok ? `✓ ${j.added || 0} neue Modelle von HuggingFace` + (j.removed ? `, ${j.removed} alte aufgeräumt` : '') + ' (läuft auch von selbst einmal am Tag)' : `✗ ${j.error || 'Fehler'}`; st.style.color = j.ok ? '#3FFF3F' : '#FF6B6B'; }
|
||||
} catch (e) { if (st) { st.textContent = '✗ ' + e.message; st.style.color = '#FF6B6B'; } }
|
||||
}
|
||||
// online LLM-Boxen (fuer die Ziel-Auswahl + Verfuegbarkeit)
|
||||
@@ -9823,6 +9823,8 @@
|
||||
const searchText = [m.id, m.description, m.hfRepo, m.quant].filter(Boolean).join(' ');
|
||||
return '<tr data-search="' + escapeHtml(searchText) + '" style="border-bottom:1px solid #1E1E2E;">' +
|
||||
'<td ' + td + 'white-space:nowrap;"><b>' + escapeHtml(m.id) + '</b>'
|
||||
+ (m.uncensored ? ' <span title="Unzensierte Fassung — lehnt kaum etwas ab" style="font-size:10px;color:#FFB020;border:1px solid #FFB020;border-radius:8px;padding:0 5px;">unzensiert</span>' : '')
|
||||
+ (m.added_at && (Date.now() - new Date(m.added_at).getTime()) < 7 * 864e5 ? ' <span title="In den letzten 7 Tagen in den Katalog gekommen" style="font-size:10px;color:#4DB8FF;border:1px solid #4DB8FF;border-radius:8px;padding:0 5px;">neu</span>' : '')
|
||||
+ (m.hfRepo ? ' <button class="btn secondary" onclick="openModelInfo(' + i + ')" title="Was kann das Modell? Zensiert oder nicht?" style="padding:0 6px;font-size:11px;margin-left:4px;">ℹ</button>' : '')
|
||||
+ '</td>' +
|
||||
'<td ' + td + 'color:#9a9ab5;min-width:220px;">' + escapeHtml(m.description || m.hfRepo || '')
|
||||
|
||||
+69
-17
@@ -517,22 +517,46 @@ async function hfRepoQuant(repo, wanted = "Q4_K_M") {
|
||||
// Katalog (kuratierte Eintraege + Beschreibungen bleiben erhalten). Fuer jedes
|
||||
// HF-Repo wird die TATSAECHLICH vorhandene Quantisierung und Groesse
|
||||
// eingetragen — auch bei schon vorhandenen HF-Eintraegen.
|
||||
// "Von HuggingFace aktualisieren": mehrere Blickwinkel statt nur der
|
||||
// meistgeladenen aller Zeiten (da gewinnen alte Modelle). Neu dazu:
|
||||
// was gerade im Trend ist, Coder-Modelle und unzensierte Fassungen.
|
||||
// Nur was eine Box laden kann (GGUF) und hoechstens LLM_CATALOG_MAX_GB.
|
||||
const LLM_CATALOG_MAX_GB = Number(process.env.LLM_CATALOG_MAX_GB || 30);
|
||||
const NON_LLM_RE = /image|audio|tts|speech|music|yue|whisper|diffusion|flux|sdxl|wan2|video|embed|rerank|ocr/i;
|
||||
const HF_REFRESH_QUERIES = [
|
||||
["beliebt", "search=GGUF&sort=downloads&direction=-1&limit=30"],
|
||||
["im Trend", "filter=gguf&sort=trendingScore&direction=-1&limit=30"],
|
||||
["Code", "search=coder&filter=gguf&sort=trendingScore&direction=-1&limit=12"],
|
||||
["unzensiert", "search=uncensored&filter=gguf&sort=trendingScore&direction=-1&limit=12"],
|
||||
["unzensiert", "search=heretic&filter=gguf&sort=trendingScore&direction=-1&limit=12"],
|
||||
["unzensiert", "search=abliterated&filter=gguf&sort=trendingScore&direction=-1&limit=12"],
|
||||
];
|
||||
async function refreshLlmCatalogFromHF() {
|
||||
const url = "https://huggingface.co/api/models?search=GGUF&sort=downloads&direction=-1&limit=40";
|
||||
const r = await fetch(url, { headers: { "User-Agent": "aria-diagnostic" } });
|
||||
if (!r.ok) throw new Error(`HF API ${r.status}`);
|
||||
const list = await r.json();
|
||||
const existing = loadLlmCatalog();
|
||||
const byId = new Map(existing.map(m => [m.id, m]));
|
||||
let added = 0;
|
||||
for (const m of (Array.isArray(list) ? list : [])) {
|
||||
const repo = m.id || m.modelId;
|
||||
if (!repo || !/gguf/i.test(repo)) continue;
|
||||
const fresh = new Map(); // repo → {cat, downloads}
|
||||
for (const [cat, q] of HF_REFRESH_QUERIES) {
|
||||
const list = await hfJson(`https://huggingface.co/api/models?${q}`);
|
||||
for (const m of Array.isArray(list) ? list : []) {
|
||||
const repo = m.id || m.modelId;
|
||||
if (!repo || fresh.has(repo)) continue;
|
||||
// Nur Sprachmodelle: Bild-, Audio-, Musik- und Sprach-Modelle kann die
|
||||
// LLM-Box nicht als Chat-Modell laden.
|
||||
const pt = String(m.pipeline_tag || "");
|
||||
if (pt && !["text-generation", "image-text-to-text", "conversational"].includes(pt)) continue;
|
||||
if (NON_LLM_RE.test(repo)) continue;
|
||||
fresh.set(repo, { cat, downloads: m.downloads || 0 });
|
||||
}
|
||||
}
|
||||
if (!fresh.size) throw new Error("HuggingFace hat nichts geliefert (erreichbar?)");
|
||||
const known = new Set(existing.map(m => m.hfRepo));
|
||||
for (const [repo, f] of fresh) {
|
||||
if (known.has(repo)) continue;
|
||||
const id = slugModelId(repo);
|
||||
if (byId.has(id)) continue; // kuratierte/vorhandene nicht ueberschreiben
|
||||
const entry = { id, hfRepo: repo, quant: "Q4_K_M", ctx: 8192, sizeGB: 0,
|
||||
description: `HuggingFace · ${(m.downloads || 0).toLocaleString("de")} Downloads`, source: "hf", _new: true };
|
||||
byId.set(id, entry);
|
||||
if (byId.has(id)) continue; // kuratierte/vorhandene nicht ueberschreiben
|
||||
byId.set(id, { id, hfRepo: repo, quant: "Q4_K_M", ctx: 8192, sizeGB: 0,
|
||||
description: `HuggingFace · ${f.cat} · ${(f.downloads).toLocaleString("de")} Downloads`, source: "hf",
|
||||
uncensored: UNCENSORED_RE.test(repo), added_at: new Date().toISOString().slice(0, 10), _new: true });
|
||||
}
|
||||
// Quantisierung + Groesse pruefen: alle HF-Eintraege (kuratierte haben
|
||||
// gepflegte Werte). Je 6 gleichzeitig, damit HF nicht drosselt.
|
||||
@@ -540,18 +564,46 @@ async function refreshLlmCatalogFromHF() {
|
||||
for (let i = 0; i < toCheck.length; i += 6) {
|
||||
await Promise.all(toCheck.slice(i, i + 6).map(async e => {
|
||||
const q = await hfRepoQuant(e.hfRepo, e.quant || "Q4_K_M");
|
||||
if (!q) {
|
||||
// Kein ladbares Modell im Repo: neue nicht aufnehmen, alte markieren.
|
||||
if (e._new) byId.delete(e.id); else e.unusable = true;
|
||||
if (!q || (e._new && q.sizeGB > LLM_CATALOG_MAX_GB)) {
|
||||
// Nicht ladbar oder fuer unsere Karten viel zu gross: neue nicht
|
||||
// aufnehmen, alte markieren.
|
||||
if (e._new) byId.delete(e.id); else if (!q) e.unusable = true;
|
||||
return;
|
||||
}
|
||||
e.quant = q.quant; e.sizeGB = q.sizeGB; e.quants = q.quants; delete e.unusable;
|
||||
}));
|
||||
}
|
||||
let added = 0;
|
||||
for (const e of byId.values()) { if (e._new) { added++; delete e._new; } }
|
||||
// Aufraeumen: von HF uebernommen, vor > LLM_CATALOG_KEEP_DAYS Tagen, auf
|
||||
// keiner Box (Platte/Karte), heute nicht mehr gelistet → raus. Kuratierte
|
||||
// und von Hand hinzugefuegte bleiben immer.
|
||||
const keepDays = Number(process.env.LLM_CATALOG_KEEP_DAYS || 30);
|
||||
const onBox = new Set();
|
||||
for (const w of workers.values()) {
|
||||
for (const d of ((w.disk && w.disk.models) || [])) onBox.add(d.repo);
|
||||
for (const sl of (w.slots || [])) if (sl.model) onBox.add(sl.model);
|
||||
}
|
||||
let removed = 0;
|
||||
for (const [id, e] of [...byId]) {
|
||||
const age = e.added_at ? (Date.now() - new Date(e.added_at).getTime()) / 864e5 : 0;
|
||||
if (e.source === "hf" && !/Hand/.test(e.description || "") && age > keepDays
|
||||
&& !fresh.has(e.hfRepo) && !onBox.has(e.hfRepo) && !onBox.has(e.id)) {
|
||||
byId.delete(id); removed++;
|
||||
}
|
||||
}
|
||||
const merged = Array.from(byId.values());
|
||||
saveLlmCatalog(merged);
|
||||
return { models: merged, added };
|
||||
return { models: merged, added, removed };
|
||||
}
|
||||
// Einmal am Tag von selbst (LLM_CATALOG_REFRESH_HOURS, 0 = aus)
|
||||
const LLM_CATALOG_REFRESH_HOURS = Number(process.env.LLM_CATALOG_REFRESH_HOURS ?? 24);
|
||||
if (LLM_CATALOG_REFRESH_HOURS > 0) {
|
||||
setInterval(() => {
|
||||
refreshLlmCatalogFromHF()
|
||||
.then(r => log("info", "llm", `LLM-Katalog automatisch aktualisiert: +${r.added}, -${r.removed}`))
|
||||
.catch(err => log("warn", "llm", `LLM-Katalog automatisch: ${err.message}`));
|
||||
}, LLM_CATALOG_REFRESH_HOURS * 3600 * 1000);
|
||||
}
|
||||
|
||||
// ── Belege: Werkzeug-Aufrufe zu einer ARIA-Antwort ─────────────────
|
||||
@@ -2609,7 +2661,7 @@ const server = http.createServer((req, res) => {
|
||||
refreshLlmCatalogFromHF()
|
||||
.then(r => {
|
||||
res.writeHead(200, { "Content-Type": "application/json" });
|
||||
res.end(JSON.stringify({ ok: true, models: r.models, added: r.added }));
|
||||
res.end(JSON.stringify({ ok: true, models: r.models, added: r.added, removed: r.removed }));
|
||||
log("info", "llm", `LLM-Katalog von HuggingFace aktualisiert: +${r.added} Modelle`);
|
||||
})
|
||||
.catch(err => {
|
||||
|
||||
Reference in New Issue
Block a user