inline HF model download badge on image + video gen forms#517
Merged
Conversation
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.
Contributor
There was a problem hiding this comment.
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.
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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— spawnsscripts/hf_download_repo.pyin the FLUX.2 venv (fallback: mflux pythonPath, gated on FLUX.2 venv health so a broken venv doesn't trap every download). ParsesSTAGE:/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-flightMapkeyed 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.js—repoForModel()maps mflux's legacydev/schnellids to canonical Black Forest Labs repos so they get the same badge as every other entry.isHfRepoId()distinguishesorg/namefrom localPath text-encoder entries.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 withPromise.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 wholeImageGenControlssubtree.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-filehf_hub_downloadwithSTAGE:marker emission so the SSE bridge can drive a real progress bar. Sequential rather thansnapshot_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-setupauthor deferred thehfDownload.jswork out of that PR (see commite0765909'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 abouthfDownload.jsfalling back to a broken FLUX.2 venv is already addressed by theisFlux2VenvHealthy()gate inresolveHfDownloadPython().Test plan
server: 8073 passing (+ 8 newhfCachescenarios)client: 602 passing