diff --git a/diagnostic/index.html b/diagnostic/index.html
index ce6c0e8..2a6696b 100644
--- a/diagnostic/index.html
+++ b/diagnostic/index.html
@@ -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 '
' +
'| ' + escapeHtml(m.id) + ''
+ + (m.uncensored ? ' unzensiert' : '')
+ + (m.added_at && (Date.now() - new Date(m.added_at).getTime()) < 7 * 864e5 ? ' neu' : '')
+ (m.hfRepo ? ' ' : '')
+ ' | ' +
'' + escapeHtml(m.description || m.hfRepo || '')
diff --git a/diagnostic/server.js b/diagnostic/server.js
index 1448b1f..1170690 100644
--- a/diagnostic/server.js
+++ b/diagnostic/server.js
@@ -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 => {
|