feat(quick-wins): HF-Link-Suche, Download-ETA, Job-Verlauf loeschbar
- Cookbook-Profisuche: ganze HF-URL oder "owner/repo" direkt einfuegbar
(parseHfRepo) -> exakter Treffer statt unscharfer Suche; limit 12->40,
damit auch grosse/seltenere Modelle (z.B. Llama-4-Scout) erscheinen.
- jobengine.attach_download_progress: geglaettete Rate (EMA) + Restzeit
(eta_s) je Download; Aktivitaet zeigt "noch ~X min · Y MB/s".
- Jobs: erledigte/abgebrochene Eintraege einzeln loeschbar (DELETE
/api/jobs/{id}) + "Verlauf leeren" (POST /api/jobs/clear); laufende
bleiben geschuetzt.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -113,9 +113,12 @@ def attach_download_progress(job_id: str, local_dir: str, total_bytes: int) -> N
|
||||
job = JOBS.get(job_id)
|
||||
if job is not None:
|
||||
job["progress"] = 0
|
||||
job["total_bytes"] = total_bytes
|
||||
|
||||
def _watch():
|
||||
pat = os.path.join(local_dir, ".cache", "huggingface", "download", "*.incomplete")
|
||||
prev_t, prev_b = None, None
|
||||
rate = 0.0 # geglaettete Download-Rate (Bytes/s), EMA gegen Zappeln
|
||||
while True:
|
||||
j = JOBS.get(job_id)
|
||||
if not j or j["state"] in ("done", "failed", "canceled"):
|
||||
@@ -125,12 +128,22 @@ def attach_download_progress(job_id: str, local_dir: str, total_bytes: int) -> N
|
||||
cur = sum(os.path.getsize(f) for f in inc) if inc else 0
|
||||
if cur:
|
||||
j["progress"] = min(99, int(cur * 100 / total_bytes))
|
||||
j["done_bytes"] = cur
|
||||
now = time.time()
|
||||
if prev_t is not None and now > prev_t and cur >= prev_b:
|
||||
inst = (cur - prev_b) / (now - prev_t)
|
||||
rate = inst if rate == 0 else 0.3 * inst + 0.7 * rate
|
||||
if rate > 0:
|
||||
j["rate_bps"] = rate
|
||||
j["eta_s"] = int((total_bytes - cur) / rate)
|
||||
prev_t, prev_b = now, cur
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
time.sleep(1.0)
|
||||
j = JOBS.get(job_id)
|
||||
if j and j["state"] == "done":
|
||||
j["progress"] = 100
|
||||
j.pop("eta_s", None)
|
||||
|
||||
threading.Thread(target=_watch, daemon=True).start()
|
||||
|
||||
@@ -156,6 +169,25 @@ def cancel_job(job_id: str) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
def delete_job(job_id: str) -> bool:
|
||||
"""Einen abgeschlossenen Job aus dem Verlauf entfernen. Laufende Jobs werden
|
||||
NICHT geloescht (erst abbrechen). Liefert False, wenn unbekannt oder noch aktiv."""
|
||||
job = JOBS.get(job_id)
|
||||
if not job or job["state"] not in ("done", "failed", "canceled"):
|
||||
return False
|
||||
JOBS.pop(job_id, None)
|
||||
return True
|
||||
|
||||
|
||||
def clear_finished() -> int:
|
||||
"""Alle abgeschlossenen Jobs (fertig/fehler/abgebrochen) aus dem Verlauf raeumen.
|
||||
Laufende bleiben. Liefert die Anzahl entfernter Eintraege."""
|
||||
done = [jid for jid, j in JOBS.items() if j["state"] in ("done", "failed", "canceled")]
|
||||
for jid in done:
|
||||
JOBS.pop(jid, None)
|
||||
return len(done)
|
||||
|
||||
|
||||
def start_job(args: list[str], label: str, env: dict | None = None,
|
||||
stdin_data: str | None = None, log_cmd: str | None = None) -> str:
|
||||
job_id = uuid.uuid4().hex[:12]
|
||||
|
||||
Reference in New Issue
Block a user