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 => {