feat(2.0): Phase 6a — Modell-Install (Download-Jobs + Split + UI)
jobengine portiert (Live-Log + %-Fortschritt aus .incomplete), services/hf.py (resolve_gguf inkl. Split -of-, Groessen), POST /api/models/install (hf download als Job + sofortige Registrierung, Split-fest), GET /api/jobs + cancel. huggingface_hub in requirements. Frontend: Install-Buttons auf Discover-Karten + Live-Download-Fortschrittsbalken (JobsBar). Verifiziert: resolve_gguf gegen echte Repos (Qwen3.6-35B-A3B 23GB single, Qwen3.5-122B-A10B 77GB 3-part split) + Frontend-Build. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
"""HuggingFace-Helfer: GGUF-Dateien eines Repos auflösen (inkl. Split-Teile) + Größen."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import httpx
|
||||
|
||||
|
||||
def hf_bin() -> str:
|
||||
"""Pfad zur `hf`-CLI (bevorzugt neben dem laufenden Python im venv)."""
|
||||
cand = os.path.join(os.path.dirname(sys.executable), "hf")
|
||||
return cand if os.path.exists(cand) else "hf"
|
||||
|
||||
|
||||
def _tree(repo: str) -> list[dict]:
|
||||
url = f"https://huggingface.co/api/models/{repo}/tree/main?recursive=true"
|
||||
with httpx.Client(timeout=20.0) as c:
|
||||
data = c.get(url).json()
|
||||
return data if isinstance(data, list) else []
|
||||
|
||||
|
||||
def _size(entry: dict) -> int:
|
||||
return int(entry.get("size") or (entry.get("lfs") or {}).get("size") or 0)
|
||||
|
||||
|
||||
def resolve_gguf(repo: str, quant: str = "Q4_K_M") -> dict:
|
||||
"""Beste GGUF-Auswahl eines Repos für einen Quant. Behandelt Split-GGUFs
|
||||
(-00001-of-000NN) als Gruppe. Liefert die Datei-/Pattern-Infos für den Download.
|
||||
|
||||
Rückgabe: {files:[paths], first:path, total_bytes:int, mmproj:path|None, split:bool}
|
||||
"""
|
||||
tree = _tree(repo)
|
||||
ggufs = [e for e in tree if str(e.get("path", "")).lower().endswith(".gguf")]
|
||||
q = quant.lower()
|
||||
# mmproj separat (Vision-Projektor)
|
||||
mmproj = next((e["path"] for e in ggufs if "mmproj" in e["path"].lower()), None)
|
||||
model = [e for e in ggufs if "mmproj" not in e["path"].lower()]
|
||||
# bevorzugt den gewünschten Quant
|
||||
pref = [e for e in model if q in e["path"].lower()]
|
||||
chosen = pref or model
|
||||
if not chosen:
|
||||
return {"files": [], "first": None, "total_bytes": 0, "mmproj": mmproj, "split": False}
|
||||
# Split? Wenn die gewählten Dateien -of- enthalten → alle Teile dieser Gruppe.
|
||||
split = any("-of-" in e["path"].lower() for e in chosen)
|
||||
if split:
|
||||
parts = sorted([e for e in chosen if "-of-" in e["path"].lower()], key=lambda e: e["path"])
|
||||
files = [e["path"] for e in parts]
|
||||
first = files[0]
|
||||
total = sum(_size(e) for e in parts)
|
||||
else:
|
||||
# ein einzelnes File: nimm das kleinste passende (typisch genau eins)
|
||||
chosen.sort(key=lambda e: _size(e))
|
||||
first = chosen[0]["path"]
|
||||
files = [first]
|
||||
total = _size(chosen[0])
|
||||
if mmproj:
|
||||
total += next((_size(e) for e in ggufs if e["path"] == mmproj), 0)
|
||||
return {"files": files, "first": first, "total_bytes": total, "mmproj": mmproj, "split": split}
|
||||
Reference in New Issue
Block a user