Skip to content

inline HF model download badge on image + video gen forms#517

Merged
atomantic merged 5 commits into
mainfrom
feat/inline-hf-model-download-badge
May 28, 2026
Merged

inline HF model download badge on image + video gen forms#517
atomantic merged 5 commits into
mainfrom
feat/inline-hf-model-download-badge

Conversation

@atomantic

Copy link
Copy Markdown
Owner

Summary

Surfaces whether the picked HF model's weights are already in the local cache (~/.cache/huggingface/hub/) before the user hits Render — so a multi-GB pull is an opt-in pre-fetch instead of a silent surprise mid-generation. Cached models get a green "Available · 7.8 GB" badge under the model picker; missing ones get a "Download (~est)" button that pre-fetches the repo over SSE with a file-by-file progress bar. Lazy download remains the existing fallback for users who skip the pre-fetch.

The video form also surfaces the active text encoder (a separate ~7–25 GB Gemma pull) with its own button, since the encoder is a shared dependency across every video render.

What ships

Server:

  • server/lib/hfCache.js — async hub-cache inspector. Walks the latest snapshot dir, follows blob symlinks, treats dangling links (partial downloads) as "not cached" so users get the Download button instead of an "Available" badge that fails at render time.
  • server/lib/hfDownload.js — spawns scripts/hf_download_repo.py in the FLUX.2 venv (fallback: mflux pythonPath, gated on FLUX.2 venv health so a broken venv doesn't trap every download). Parses STAGE: / DOWNLOAD: / USER_ERROR: lines into SSE-friendly events.
  • server/lib/sseDownload.js — shared SSE driver for both image and video routes (and any future HF pre-fetch endpoint). Owns the cross-route in-flight Map keyed by repo so a FLUX repo referenced by both pages can't spawn two python children. req.on('close') registers before the cache-inspect await so a mid-await disconnect still kills the child.
  • server/lib/mediaModels.jsrepoForModel() maps mflux's legacy dev / schnell ids to canonical Black Forest Labs repos so they get the same badge as every other entry. isHfRepoId() distinguishes org/name from localPath text-encoder entries.
  • Routes: GET /api/{image,video}-gen/models/status (per-model {cached, sizeBytes}); GET /api/{image,video}-gen/models/:id/download (SSE); GET /api/video-gen/text-encoder/download (SSE). The status routes parallelize N inspections with Promise.all.

Client:

  • client/src/hooks/useModelDownloadStatus.js — fetches status + drives the SSE pre-download. Memoizes the active model's enriched status so a new SSE frame only re-renders the active badge, not the whole ImageGenControls subtree.
  • client/src/components/media/ModelDownloadBadge.jsx — three states: cached (green "Available · 7.8 GB"), unknown repo (no badge), needs-download (button → inline progress bar + stage label + cancel).
  • ImageGen.jsx, VideoGen.jsx, ImageGenControls.jsx — wire the badge in. Callers that don't pass the props (Universe Builder batch render, etc.) simply get no badge.

Helper script:

  • scripts/hf_download_repo.py — per-file hf_hub_download with STAGE: marker emission so the SSE bridge can drive a real progress bar. Sequential rather than snapshot_download() so the UI can show per-file granular progress; trade-off documented inline.

Context

This is the "rest of #515" — the fix-local-python-setup author deferred the hfDownload.js work out of that PR (see commit e0765909's body: "in a file not yet on origin and not part of this PR's surface; left for whichever branch ships hfDownload") and it ships here. The codex review finding about hfDownload.js falling back to a broken FLUX.2 venv is already addressed by the isFlux2VenvHealthy() gate in resolveHfDownloadPython().

Test plan

  • server: 8073 passing (+ 8 new hfCache scenarios)
  • client: 602 passing
  • Local smoke: image-gen page with FLUX.1-dev shows "Available" badge after first generation; uncached model shows Download button + progress; cancel during download kills the python child cleanly
  • Video-gen page surfaces text-encoder Download button when Gemma isn't cached; LM Studio localPath entry shows as Available without a Download button

atomantic added 4 commits May 28, 2026 13:38
Surface whether the picked model's weights are already in the local HF
cache (~/.cache/huggingface/hub/) before the user hits Render. Cached
models get a green "Available · 7.8 GB" badge; missing ones get a
"Download (~est)" button that pre-fetches the repo over SSE with a live
file-by-file progress bar — so a multi-GB pull is opt-in, not a silent
lazy-download surprise at render time. Lazy download remains the
fallback for users who skip the pre-fetch.

The video form also surfaces the active text encoder (a separate
~7–25 GB Gemma pull) with its own button, since the encoder is a
shared dependency across every video render.

Server:
- server/lib/hfCache.js — async hub-cache inspector. Walks the latest
  snapshot dir, follows blob symlinks, treats dangling links (partial
  downloads) as "not cached" so users get the Download button instead
  of an "Available" badge that fails at render time.
- server/lib/hfDownload.js — spawns scripts/hf_download_repo.py in the
  FLUX.2 venv (fallback: mflux pythonPath, gated on FLUX.2 venv health
  so a broken venv doesn't trap every download). Parses STAGE: /
  DOWNLOAD: / USER_ERROR: lines from the helper into SSE-friendly
  stage/progress/complete/error events.
- server/lib/sseDownload.js — shared SSE driver for both image and
  video routes (and any future HF pre-fetch endpoint). Owns the
  cross-route in-flight Map keyed by repo so a FLUX repo referenced by
  both pages can't spawn two python children.
- server/lib/mediaModels.js — repoForModel() maps mflux's legacy 'dev'
  / 'schnell' ids to their canonical Black Forest Labs repos so they
  get the same badge as every other entry. isHfRepoId() distinguishes
  'org/name' from localPath text-encoder entries.
- Routes: GET /api/{image,video}-gen/models/status returns per-model
  {cached, sizeBytes}; GET /api/{image,video}-gen/models/:id/download
  is the SSE endpoint. Video also exposes
  GET /api/video-gen/text-encoder/download for the Gemma pre-fetch.
  The status route parallelizes N inspections with Promise.all.

Client:
- client/src/hooks/useModelDownloadStatus.js — fetch status + drive the
  SSE pre-download. Memoizes the active model's enriched status so a
  new SSE frame only re-renders the active badge, not the whole
  ImageGenControls subtree.
- client/src/components/media/ModelDownloadBadge.jsx — three states:
  cached (green "Available · 7.8 GB"), unknown repo (no badge), and
  needs-download (button → inline progress bar + stage label + cancel).
- ImageGen.jsx, VideoGen.jsx, ImageGenControls.jsx — wire the badge
  in. Universe Builder batch-render and any other caller that doesn't
  pass the props simply gets no badge.

Helper script:
- scripts/hf_download_repo.py — per-file hf_hub_download with STAGE:
  marker emission so the SSE bridge can drive a real progress bar.
  Sequential rather than snapshot_download() so the UI can show
  per-file granular progress; trade-off documented inline.

Tests: server 8073 passing (+8 hfCache scenarios), client 602.
…upe flux2 python lookup

Two small fixes from the pre-PR self-review:

* sseDownload: register `req.on('close')` BEFORE the inspectModelCache
  await so a client disconnect during the (potentially cold-cache,
  double-digit ms) inspection still hits the kill path. Without this,
  closing the tab mid-await landed after spawn with no listener
  registered, leaving an orphan python child to finish on its own.
* hfDownload.resolveHfDownloadPython: cache resolveFlux2Python() in a
  local instead of calling it twice — tiny but the second call was just
  paying a sync file-existence check for no reason.

Tests unchanged.
…honor XDG_CACHE_HOME

Three findings from the codex review pass:

* hfDownload.js: when the EventSource closes during
  `resolveHfDownloadPython()` (FLUX.2 health probe, several hundred ms
  cold) or `getHfTokenInfo()`, `kill()` could only set a flag — the
  IIFE still proceeded to spawn the python child after the await, so a
  cancelled-by-the-user download still ran a multi-GB HF pull headlessly
  and held its inFlight slot until completion. Added two `if (killed)`
  short-circuits inside the IIFE (before and after the token-info
  await), plus a re-check immediately after spawn returns to catch a
  race in the spawn callback itself.

* useModelDownloadStatus.js: a terminal `{type:'error'}` SSE frame
  (gated repo, missing HF token, broken venv) was silently dropped —
  the close-effect cleared activeModelId, the badge re-rendered as the
  plain Download button, and the user saw nothing actionable. Toast the
  error message before clearing state so the user actually learns why
  the pre-download failed.

* hfCache.js: huggingface_hub honors `$XDG_CACHE_HOME/huggingface/hub`
  before the `~/.cache` fallback. Without this branch, Linux installs
  with `XDG_CACHE_HOME` set would write to one path and our cache
  inspector would check another — the status endpoint would
  permanently report `cached:false` even after a successful download.
  Added the XDG branch in the documented huggingface_hub precedence
  order; test updated.

Tests: server 8073 + 1 new XDG case = 8074; client 602 unchanged.
… blob stats

* hfDownload.js: chunk boundaries from pipe reads don't align to line
  boundaries. The previous `chunk.toString().split(/[\r\n]+/)` treated
  each chunk as if it ended cleanly on a newline — a STAGE:download:…
  or USER_ERROR:… marker split across two chunks would be routed to
  the generic log path with its wire shape destroyed (no typed-error
  capture, no progress event). Added per-stream rolling buffers,
  splitting on `\r?\n` and holding back the trailing partial until the
  next chunk arrives. Also flush remaining buffers on close so a
  helper that exits without a final newline doesn't drop its last
  line (rare, but possible on SIGKILL).

* hfCache.js: weight-file stats inside inspectModelCache ran
  sequentially. Large FLUX / HiDream / Wan snapshots ship hundreds of
  shards and the serial awaits compound into a real /models/status
  hitch. Parallelized via Promise.all; the cancel-on-broken-blob
  semantics are preserved (we scan the resolved stats in order and
  return at the first 0-size / failed entry).

Skipped the third gemini finding about `latestSnapshotDir` having a
"race condition" on shared latestMs/latest writes inside the Promise.all
map — single-threaded JS guarantees the read-modify-write between the
two synchronous statements after the stat await runs atomically, so no
interleaving is possible. Left as-is.

Tests unchanged.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an inline HuggingFace cache-awareness + opt-in prefetch flow to the Image Gen and Video Gen model pickers, so users can see (and optionally download) multi‑GB model weights before starting a render. This is implemented via new server cache-inspection + SSE download endpoints and a shared client hook/badge component to render “Available” vs “Download” with live progress.

Changes:

  • Server: implement HF hub cache inspection and an SSE-driven repo prefetch pipeline (shared across image/video routes with in-flight dedupe).
  • Client: add a reusable download-status hook + badge UI and wire it into ImageGen/VideoGen pickers (including video text-encoder prefetch).
  • Tests/docs/changelog: add hfCache unit tests, update route tests/mocks, and document the new library modules and release note.

Reviewed changes

Copilot reviewed 20 out of 20 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
server/routes/videoGen.test.js Updates fileUtils PATHS mock to include new PATHS fields needed by download helper.
server/routes/videoGen.js Adds /models/status, /models/:id/download (SSE), and /text-encoder/download (SSE) endpoints for video gen.
server/routes/imageGen.js Adds /models/status and /models/:id/download (SSE) endpoints for image gen.
server/lib/sseDownload.js New shared SSE download driver with cross-route in-flight dedupe + disconnect cancellation.
server/lib/README.md Documents new hfCache, hfDownload, and sseDownload modules.
server/lib/mediaModels.js Adds repoForModel() mapping + isHfRepoId() helper for distinguishing HF repo ids vs local paths.
server/lib/index.js Exports the new HF cache/download utilities from the server lib barrel.
server/lib/hfDownload.js New Node-side spawner/parser for hf_download_repo.py, emitting SSE-friendly events.
server/lib/hfCache.test.js New unit tests validating cache-root resolution and cache inspection scenarios (complete/partial/nested).
server/lib/hfCache.js New async inspector for HF hub cache snapshots + weight size aggregation.
scripts/hf_download_repo.py New Python helper that lists repo files and downloads sequentially with progress markers for SSE.
client/src/services/apiImageVideo.js Adds API helpers for fetching model cache statuses (image + video).
client/src/pages/VideoGen.jsx Wires model + text-encoder cache status and download badge into Video Gen UI.
client/src/pages/ImageGen.jsx Installs the model download-status hook and passes status/handlers down to controls (local mode only).
client/src/hooks/useModelDownloadStatus.js New hook to fetch statuses and drive SSE downloads with auto-refresh + error toast.
client/src/hooks/README.md Documents the new hook in the hooks index table.
client/src/hooks/index.js Exports useModelDownloadStatus.
client/src/components/media/ModelDownloadBadge.jsx New UI component for cached/unknown/needs-download states with progress + cancel.
client/src/components/imageGen/ImageGenControls.jsx Adds optional badge rendering under model select when props are provided.
.changelog/NEXT.md Adds release note entry describing the new inline status + prefetch feature and endpoints.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread server/lib/mediaModels.js Outdated
Comment thread scripts/hf_download_repo.py Outdated
Comment thread server/lib/hfDownload.js Outdated
Comment thread client/src/components/media/ModelDownloadBadge.jsx Outdated
…c fixes

* isHfRepoId would accept `C:/Users/foo/model` as a repo id (no leading
  `/` or `~`, includes `/`) — on a Windows install where the active
  text encoder is a localPath like `C:/lmstudio/.../gemma-3-12b-it`,
  the video status endpoint would call inspectModelCache against that
  path and the /text-encoder/download route would try to spawn the HF
  helper. Tightened to reject backslash anywhere, drive-letter prefix
  (`X:`), and any value that isn't exactly one `/` (canonical
  `org/name`). Added cross-platform unit coverage.

* scripts/hf_download_repo.py docstring referenced
  `/models/:repoId/download`; the actual route is `/models/:id/download`
  (the route resolves model id → HF repo server-side before invoking
  this helper), plus the separate `/text-encoder/download`.

* hfDownload.js wire-protocol comment locked
  `USER_ERROR:<kind>:<repo>` but the python helper also emits
  `:<filename>` for per-file download failures. Generalized to
  `<detail>` with a note about which kinds carry what.

* ModelDownloadBadge.jsx header had a typo "Rendering Render is NOT
  blocked"; fixed to "Hitting Render is NOT blocked".

Tests: server 8074 + 1 new isHfRepoId case = 8075; client 602.
@atomantic atomantic merged commit 8b0e098 into main May 28, 2026
2 checks passed
@atomantic atomantic deleted the feat/inline-hf-model-download-badge branch May 28, 2026 21:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants