feat: echter Download-Fortschritt via Datei-Polling

`hf` gibt im Nicht-TTY-Modus keinen Fortschritt aus (am Bosgame verifiziert:
0 CR-Frames). Stattdessen pollt jobengine.attach_download_progress die
wachsende <local-dir>/.cache/huggingface/download/*.incomplete-Datei gegen die
Gesamtgroesse aus der HF-Tree-API (cookbook.hf_file_size) -> exaktes %.

- attach_download_progress an /api/download, install-recipe, install-model
- Frontend (Aktivitaet + Server-Karte): nutzt job.progress bevorzugt,
  Log-%-Parsing bleibt Fallback fuer Tools, die selbst Prozente ausgeben

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-06-21 18:26:27 +02:00
parent 2cde6ba5a5
commit 3cf36d436b
5 changed files with 68 additions and 7 deletions
+17 -1
View File
@@ -14,7 +14,7 @@ from auth import auth
from hw_math import evaluate_fit, max_ctx_for
from config import MODELS_DIR, CMD_TEMPLATE, DEFAULT_TTL, HF_DOWNLOAD_ENV, hf_bin
from llamaswap import read_config, write_config
from jobengine import start_job, JOBS
from jobengine import start_job, JOBS, attach_download_progress
from recipes import RECIPES, UPGRADES
router = APIRouter(prefix="/api/cookbook", dependencies=[Depends(auth)])
@@ -163,6 +163,7 @@ def install_recipe(req: InstallRecipeReq):
args = [hf_bin(), "download", m["repo"], file, "--local-dir", str(target)]
jid = start_job(args, f"download {m['name']}", env=env)
JOBS[jid]["result_path"] = str(target / file)
attach_download_progress(jid, str(target), hf_file_size(m["repo"], file))
job_ids.append(jid)
# Eintrag jetzt schon schreiben — optimaler Kontext, aber gedeckelt fuer schnellen Erststart.
ctx = min(max_ctx_for(m["params_b"], m["quant"], ram_gb), 32768)
@@ -173,6 +174,20 @@ def install_recipe(req: InstallRecipeReq):
return {"job_ids": job_ids, "count": len(job_ids)}
def hf_file_size(repo: str, file: str) -> int:
"""Groesse einer Datei im HF-Repo (Bytes) fuer die Fortschrittsanzeige. 0 wenn unbekannt.
GGUFs sind LFS -> ggf. unter 'lfs.size'."""
try:
with httpx.Client(timeout=10.0) as c:
tree = c.get(f"https://huggingface.co/api/models/{repo}/tree/main").json()
for f in tree:
if isinstance(f, dict) and f.get("path") == file:
return int(f.get("size") or (f.get("lfs") or {}).get("size") or 0)
except Exception: # noqa: BLE001
return 0
return 0
def _pick_gguf(repo: str, quant: str = "Q4_K_M") -> str | None:
"""Beste GGUF-Datei eines Repos auflösen: bevorzugt gewünschten Quant, keine Split-Teile."""
try:
@@ -234,6 +249,7 @@ def install_model(req: InstallModelReq):
jid = start_job([hf_bin(), "download", req.repo, file, "--local-dir", str(target)],
f"download {req.repo.split('/')[-1]}", env=env)
JOBS[jid]["result_path"] = str(target / file)
attach_download_progress(jid, str(target), hf_file_size(req.repo, file))
cfg = read_config()
ctx = min(max_ctx_for(req.params_b, req.quant, ram_gb), 32768)
cmd = CMD_TEMPLATE.replace("{model}", str(target / file)).replace("{ctx}", str(ctx))