fix(vision): mmproj-Projektor wird jetzt automatisch mitgeladen
Bisher: install-recipe + install-model luden nur die Haupt-GGUF, aber nicht den mmproj-Projektor → Vision-Modelle starteten ohne --mmproj und konnten keine Bilder verarbeiten (obwohl der Hinweis sagte 'wird automatisch ergänzt'). Fix: - _pick_mmproj(repo): findet die mmproj-F16/BF16-Datei im HF-Repo - install-recipe: lädt mmproj mit hf download (multi-file) + ergänzt --mmproj/--jinja im cmd - install-model: gleicher Fix - Requirement-Text präzisiert (explizit 'mmproj-F16 wird mitgeladen') hf-cli unterstützt mehrere Dateien pro Download-Aufruf → ein einziger Job für Haupt-GGUF + Projektor. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+37
-4
@@ -273,9 +273,14 @@ def install_recipe(req: InstallRecipeReq):
|
|||||||
file = _pick_gguf(m["repo"], m.get("quant", "Q4_K_M"))
|
file = _pick_gguf(m["repo"], m.get("quant", "Q4_K_M"))
|
||||||
if not file:
|
if not file:
|
||||||
continue # kein GGUF im Repo gefunden -> Modell ueberspringen (Rest installiert trotzdem)
|
continue # kein GGUF im Repo gefunden -> Modell ueberspringen (Rest installiert trotzdem)
|
||||||
|
# Vision-Modelle: mmproj-Projektor mitholen (nötig für --mmproj in llama.cpp).
|
||||||
|
mmproj = _pick_mmproj(m["repo"])
|
||||||
target = MODELS_DIR / m["repo"].split("/")[-1]
|
target = MODELS_DIR / m["repo"].split("/")[-1]
|
||||||
target.mkdir(parents=True, exist_ok=True)
|
target.mkdir(parents=True, exist_ok=True)
|
||||||
args = [hf_bin(), "download", m["repo"], file, "--local-dir", str(target)]
|
args = [hf_bin(), "download", m["repo"], file]
|
||||||
|
if mmproj:
|
||||||
|
args.append(mmproj) # hf-cli unterstützt mehrere Dateien in einem Aufruf
|
||||||
|
args += ["--local-dir", str(target)]
|
||||||
jid = start_job(args, f"download {m['name']}", env=env)
|
jid = start_job(args, f"download {m['name']}", env=env)
|
||||||
JOBS[jid]["result_path"] = str(target / file)
|
JOBS[jid]["result_path"] = str(target / file)
|
||||||
attach_download_progress(jid, str(target), hf_file_size(m["repo"], file))
|
attach_download_progress(jid, str(target), hf_file_size(m["repo"], file))
|
||||||
@@ -284,6 +289,8 @@ def install_recipe(req: InstallRecipeReq):
|
|||||||
ctx = max_ctx_for(m["params_b"], m["quant"], ram_gb)
|
ctx = max_ctx_for(m["params_b"], m["quant"], ram_gb)
|
||||||
path = str(target / file)
|
path = str(target / file)
|
||||||
cmd = CMD_TEMPLATE.replace("{model}", path).replace("{ctx}", str(ctx))
|
cmd = CMD_TEMPLATE.replace("{model}", path).replace("{ctx}", str(ctx))
|
||||||
|
if mmproj:
|
||||||
|
cmd = cmd.rstrip() + f" --mmproj {target / mmproj} --jinja"
|
||||||
# Schluessel = sprechender Modellname; Rolle (aus dem Rezept) als Alias.
|
# Schluessel = sprechender Modellname; Rolle (aus dem Rezept) als Alias.
|
||||||
mid = model_id_from_path(path)
|
mid = model_id_from_path(path)
|
||||||
cfg["models"][mid] = {"cmd": LiteralScalarString(cmd + "\n"), "ttl": DEFAULT_TTL}
|
cfg["models"][mid] = {"cmd": LiteralScalarString(cmd + "\n"), "ttl": DEFAULT_TTL}
|
||||||
@@ -321,6 +328,26 @@ def _pick_gguf(repo: str, quant: str = "Q4_K_M") -> str | None:
|
|||||||
return (pref or nosplit or ggufs)[0]
|
return (pref or nosplit or ggufs)[0]
|
||||||
|
|
||||||
|
|
||||||
|
def _pick_mmproj(repo: str) -> str | None:
|
||||||
|
"""Sucht den mmproj-Projektor (Vision-Modelle) im Repo — F16/BF16 bevorzugt.
|
||||||
|
Gibt None zurueck, wenn kein Projektor vorhanden (= kein Vision-Modell)."""
|
||||||
|
try:
|
||||||
|
with httpx.Client(timeout=10.0) as c:
|
||||||
|
tree = c.get(f"https://huggingface.co/api/models/{repo}/tree/main").json()
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
return None
|
||||||
|
candidates = [
|
||||||
|
f["path"] for f in tree
|
||||||
|
if isinstance(f, dict) and "mmproj" in str(f.get("path", "")).lower()
|
||||||
|
and str(f.get("path", "")).endswith(".gguf")
|
||||||
|
]
|
||||||
|
if not candidates:
|
||||||
|
return None
|
||||||
|
# F16/BF16 hat die beste Projektor-Qualität; nimm das erste wenn keins passt.
|
||||||
|
pref = [c for c in candidates if "f16" in c.lower() or "bf16" in c.lower()]
|
||||||
|
return (pref or candidates)[0]
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Automatische Modell-Entdeckung ("aktuell beste Modelle", live von HF + Cache).
|
# Automatische Modell-Entdeckung ("aktuell beste Modelle", live von HF + Cache).
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -353,7 +380,7 @@ def _model_requirements(role: str, quant: str, fit: dict) -> list[str]:
|
|||||||
"""Ehrliche Voraussetzungen/Hinweise pro Modell (Klartext fuer Anfaenger)."""
|
"""Ehrliche Voraussetzungen/Hinweise pro Modell (Klartext fuer Anfaenger)."""
|
||||||
reqs = []
|
reqs = []
|
||||||
if role == "vision":
|
if role == "vision":
|
||||||
reqs.append("Bild-Modell: Projektor (mmproj) + --jinja werden beim Einpflegen automatisch ergänzt.")
|
reqs.append("Bild-Modell: Projektor (mmproj-F16) wird automatisch mitgeladen und --jinja gesetzt.")
|
||||||
if quant.upper() in ("FP16", "BF16", "F16", "F32"):
|
if quant.upper() in ("FP16", "BF16", "F16", "F32"):
|
||||||
reqs.append("Unquantisiert — sehr groß. Eine Q4/Q5-Variante ist meist die bessere Wahl.")
|
reqs.append("Unquantisiert — sehr groß. Eine Q4/Q5-Variante ist meist die bessere Wahl.")
|
||||||
if fit["level"] == "too_tight":
|
if fit["level"] == "too_tight":
|
||||||
@@ -508,20 +535,26 @@ def install_model(req: InstallModelReq):
|
|||||||
file = _pick_gguf(req.repo, req.quant)
|
file = _pick_gguf(req.repo, req.quant)
|
||||||
if not file:
|
if not file:
|
||||||
raise HTTPException(404, "Keine GGUF-Datei im Repo gefunden.")
|
raise HTTPException(404, "Keine GGUF-Datei im Repo gefunden.")
|
||||||
|
mmproj = _pick_mmproj(req.repo)
|
||||||
ram_gb = psutil.virtual_memory().total / (1024 ** 3)
|
ram_gb = psutil.virtual_memory().total / (1024 ** 3)
|
||||||
env = dict(HF_DOWNLOAD_ENV)
|
env = dict(HF_DOWNLOAD_ENV)
|
||||||
if req.hf_token:
|
if req.hf_token:
|
||||||
env["HF_TOKEN"] = req.hf_token
|
env["HF_TOKEN"] = req.hf_token
|
||||||
target = MODELS_DIR / req.repo.split("/")[-1]
|
target = MODELS_DIR / req.repo.split("/")[-1]
|
||||||
target.mkdir(parents=True, exist_ok=True)
|
target.mkdir(parents=True, exist_ok=True)
|
||||||
jid = start_job([hf_bin(), "download", req.repo, file, "--local-dir", str(target)],
|
args = [hf_bin(), "download", req.repo, file]
|
||||||
f"download {req.repo.split('/')[-1]}", env=env)
|
if mmproj:
|
||||||
|
args.append(mmproj)
|
||||||
|
args += ["--local-dir", str(target)]
|
||||||
|
jid = start_job(args, f"download {req.repo.split('/')[-1]}", env=env)
|
||||||
JOBS[jid]["result_path"] = str(target / file)
|
JOBS[jid]["result_path"] = str(target / file)
|
||||||
attach_download_progress(jid, str(target), hf_file_size(req.repo, file))
|
attach_download_progress(jid, str(target), hf_file_size(req.repo, file))
|
||||||
cfg = read_config()
|
cfg = read_config()
|
||||||
ctx = max_ctx_for(req.params_b, req.quant, ram_gb)
|
ctx = max_ctx_for(req.params_b, req.quant, ram_gb)
|
||||||
path = str(target / file)
|
path = str(target / file)
|
||||||
cmd = CMD_TEMPLATE.replace("{model}", path).replace("{ctx}", str(ctx))
|
cmd = CMD_TEMPLATE.replace("{model}", path).replace("{ctx}", str(ctx))
|
||||||
|
if mmproj:
|
||||||
|
cmd = cmd.rstrip() + f" --mmproj {target / mmproj} --jinja"
|
||||||
mid = model_id_from_path(path)
|
mid = model_id_from_path(path)
|
||||||
cfg["models"][mid] = {"cmd": LiteralScalarString(cmd + "\n"), "ttl": DEFAULT_TTL}
|
cfg["models"][mid] = {"cmd": LiteralScalarString(cmd + "\n"), "ttl": DEFAULT_TTL}
|
||||||
set_role_alias(cfg, mid, req.role)
|
set_role_alias(cfg, mid, req.role)
|
||||||
|
|||||||
Reference in New Issue
Block a user