diff --git a/diagnostic/index.html b/diagnostic/index.html index 7f09b8f..bebc038 100644 --- a/diagnostic/index.html +++ b/diagnostic/index.html @@ -960,6 +960,12 @@ den Fortschritt. Danach ist es im Modell-Dropdown oben waehlbar. Achte auf den VRAM der Box (Spalte VRAM).

+
+ + +
+
(lade Katalog…)
@@ -10206,6 +10212,35 @@ '' + act + ''; }).join('') + ''; } + // Modell per Link/Repo in den Katalog. Ohne GGUF: GGUF-Fassungen vorschlagen. + async function addCatalogByLink(repo) { + const inp = document.getElementById('llm-catalog-add'); + const out = document.getElementById('llm-catalog-add-result'); + const val = repo || (inp.value || '').trim(); + if (!val) return; + out.innerHTML = 'Prüfe ' + escapeHtml(val) + ' auf HuggingFace …'; + try { + const j = await (await fetch('/api/llm-catalog/add', { method: 'POST', headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ hfRepo: val, manual: true }) })).json(); + if (j.ok) { + _llmCatalog = j.models || _llmCatalog; + renderLlmCatalog(); + inp.value = ''; + out.innerHTML = '✓ ' + escapeHtml(j.id) + (j.existed ? ' war schon im Katalog' : ' ist jetzt im Katalog') + ' — unten Ziel-Karte wählen und laden.'; + const s = document.getElementById('llm-catalog-search'); + if (s) { s.value = j.id; filterLlmCatalog(); } + return; + } + const alts = j.alternatives || []; + out.innerHTML = '⚠ ' + escapeHtml(j.error || 'Fehler') + '' + + (alts.length ? '
GGUF-Fassungen desselben Modells:
' + alts.map(a => + `
+ ${escapeHtml(a.repo)} + ↓ ${Number(a.downloads).toLocaleString('de')} +
`).join('') + : ''); + } catch (e) { out.innerHTML = '✗ ' + escapeHtml(e.message) + ''; } + } async function addTrendingToCatalog(btn, repo) { btn.disabled = true; btn.textContent = '…'; try { diff --git a/diagnostic/server.js b/diagnostic/server.js index 92f80c4..46d32e3 100644 --- a/diagnostic/server.js +++ b/diagnostic/server.js @@ -640,17 +640,42 @@ async function discoverGguf(cat, trusted) { } // Ein HF-Repo in den Katalog uebernehmen (danach dort wie gewohnt "Laden") -async function addHfRepoToCatalog(repo) { - if (!/^[\w.-]+\/[\w.-]+$/.test(String(repo || ""))) throw new Error("ungueltiges Repo"); +// Link oder "org/name" → "org/name" (huggingface.co/org/name/tree/main … geht auch) +function hfRepoFromInput(input) { + const t = String(input || "").trim().replace(/^https?:\/\/(www\.)?huggingface\.co\//i, "").replace(/[?#].*$/, ""); + const m = t.match(/^([\w.-]+)\/([\w.-]+)/); + return m ? `${m[1]}/${m[2]}` : ""; +} +// Repo ohne GGUF (z.B. nur safetensors): GGUF-Fassungen desselben Modells +// auf HuggingFace suchen — meist von mradermacher, bartowski, unsloth & Co. +async function hfGgufAlternatives(repo) { + const name = String(repo).split("/")[1] || ""; + if (!name) return []; + try { + const r = await fetch(`https://huggingface.co/api/models?search=${encodeURIComponent(name)}&filter=gguf&sort=downloads&direction=-1&limit=12`, + { headers: { "User-Agent": "aria-diagnostic" } }); + if (!r.ok) return []; + return (await r.json()).filter(m => m.id && m.id !== repo) + .map(m => ({ repo: m.id, downloads: m.downloads || 0, likes: m.likes || 0 })).slice(0, 8); + } catch { return []; } +} + +async function addHfRepoToCatalog(repo, description = "") { + repo = hfRepoFromInput(repo); + if (!/^[\w.-]+\/[\w.-]+$/.test(String(repo || ""))) throw new Error("ungueltiges Repo — Link oder org/name"); const q = await hfRepoQuant(repo, "Q4_K_M"); - if (!q) throw new Error("kein ladbares GGUF-Modell in diesem Repo"); + if (!q) { + const err = new Error("In diesem Repo gibt es kein GGUF — das braucht die Box (llama.cpp)."); + err.alternatives = await hfGgufAlternatives(repo); + throw err; + } const cat = loadLlmCatalog(); let id = slugModelId(repo); const same = cat.find(m => m.hfRepo === repo); if (same) return { models: cat, id: same.id, existed: true }; while (cat.some(m => m.id === id)) id += "-2"; cat.push({ id, hfRepo: repo, quant: q.quant, ctx: 8192, sizeGB: q.sizeGB, quants: q.quants, - description: "Aus den HuggingFace-Trends übernommen", source: "hf" }); + description: description || "Aus den HuggingFace-Trends übernommen", source: "hf" }); saveLlmCatalog(cat); return { models: cat, id, existed: false }; } @@ -2483,9 +2508,9 @@ const server = http.createServer((req, res) => { let body = ""; req.on("data", c => { body += c; if (body.length > 10000) req.destroy(); }); req.on("end", () => { - let repo = ""; - try { repo = JSON.parse(body || "{}").hfRepo; } catch {} - addHfRepoToCatalog(repo) + let repo = "", manual = false; + try { const b = JSON.parse(body || "{}"); repo = b.hfRepo; manual = !!b.manual; } catch {} + addHfRepoToCatalog(repo, manual ? "Von Hand hinzugefügt" : "") .then(r => { res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ ok: true, ...r })); @@ -2493,7 +2518,7 @@ const server = http.createServer((req, res) => { }) .catch(err => { res.writeHead(200, { "Content-Type": "application/json" }); - res.end(JSON.stringify({ ok: false, error: err.message })); + res.end(JSON.stringify({ ok: false, error: err.message, alternatives: err.alternatives || [] })); }); }); } else if (req.url === "/api/local-llm-config" && req.method === "GET") {