Guard in-flight video loads from deletion, resolve cached hub GGUFs by arch, seed gen controls

Three video-tab fixes:
- delete-cached refused a loaded video repo but not one a background load is still downloading
  (status().loaded is False in that window); add VideoBackend.loading_repo_ids() mirroring the
  image backend and check it in the route so deleting mid-download can no longer yank blobs.
- the cached-gguf picker tags a cached HUB GGUF by its general.architecture, but the loader's
  arch fallback only read a LOCAL file, so an opaquely-named cached hub LTX GGUF the picker
  offered 400d on load; read the arch from the cached blob (network-free) too.
- on a mount with a model already loaded (refresh / load from another client) steps and guidance
  stuck at the pre-load default, so a base checkpoint silently generated a degraded clip; seed
  them from the backend-authoritative status.defaults once per newly-loaded model.
This commit is contained in:
Daniel Han 2026-07-06 14:14:15 +00:00
commit bb937296e7
4 changed files with 123 additions and 14 deletions

View file

@ -119,18 +119,26 @@ def _is_trusted_video_repo(repo_id: str) -> bool:
return rid.startswith("unsloth/") or rid in _TRUSTED_NON_GGUF_VIDEO_REPOS
def _local_gguf_arch(repo_id: str, gguf_filename: str) -> Optional[str]:
"""``general.architecture`` of a LOCAL GGUF pick (``repo_id`` is a directory), or None.
The Video picker admits a local GGUF by its arch (not its name), so a renamed file whose
path carries no family token still shows up; reading the arch lets the loader resolve the
same family the picker offered. Header-only, bounds-checked read (no-op for a hub repo id
whose path does not exist)."""
def _picked_gguf_arch(repo_id: str, gguf_filename: str) -> Optional[str]:
"""``general.architecture`` of a picked GGUF, or None. The Video picker admits a GGUF by its
arch (not its name) -- for a LOCAL dir (``repo_id`` is a directory) AND for a cached HUB repo
(the cached-gguf listing tags it by arch too), so a renamed/opaquely-named file whose path
carries no family token still shows up; reading the arch lets the loader resolve the same
family the picker offered. Reads the local file when present, else the cached hub blob
(network-free via try_to_load_from_cache). Header-only, bounds-checked."""
try:
from pathlib import Path
path = Path(repo_id).expanduser() / gguf_filename
if not path.is_file():
return None
# Not a local dir: resolve a cached HUB blob from the HF cache (no network). The
# cached-gguf picker only offers already-downloaded repos, so the blob is on disk.
from huggingface_hub import try_to_load_from_cache
cached = try_to_load_from_cache(repo_id, gguf_filename)
if not isinstance(cached, str):
return None
path = Path(cached)
from utils.models.gguf_metadata import read_gguf_general_metadata
arch = (read_gguf_general_metadata(str(path)) or {}).get("general.architecture")
@ -152,12 +160,12 @@ def _detect_load_family(
else None
)
if fam is None and gguf_filename and not family_override:
# The picker admits a local GGUF by its general.architecture, but its path/name may
# carry no whole-segment family token (e.g. a renamed "model.gguf"), so the name-based
# detection above misses it. Resolve the same family the picker offered by reading the
# arch -- its string ("ltxv") is a family alias. A video arch with no backend family
# (e.g. "wan") still yields None, so an unsupported pick 400s exactly as before.
arch = _local_gguf_arch(repo_id, gguf_filename)
# The picker admits a GGUF (local dir OR cached hub repo) by its general.architecture, but
# its path/name may carry no whole-segment family token (e.g. a renamed "model.gguf"), so
# the name-based detection above misses it. Resolve the same family the picker offered by
# reading the arch -- its string ("ltxv") is a family alias. A video arch with no backend
# family (e.g. "wan") still yields None, so an unsupported pick 400s exactly as before.
arch = _picked_gguf_arch(repo_id, gguf_filename)
if arch:
fam = detect_video_family(repo_id, override = arch)
return fam
@ -613,6 +621,19 @@ class VideoBackend:
expected_bytes = int(expected) if expected else None,
)
def loading_repo_ids(self) -> tuple[str, ...]:
"""Repo ids an in-flight background load is downloading (empty when idle).
The delete-cached guard needs this: during a load ``status()["loaded"]`` is
still False, but deleting the target repo (or its companion base) would yank
blobs and snapshot files from under the download/assembly. Mirrors the image
backend's guard (DiffusionBackend.loading_repo_ids)."""
with self._lock:
loading = self._loading
if loading is None or loading.error is not None:
return ()
return tuple(r for r in (loading.repo_id, loading.base_repo) if r)
# ── the load itself ──────────────────────────────────────────────────────
def load_pipeline(

View file

@ -3521,7 +3521,9 @@ async def delete_cached_model(
# pipeline -- the same invariant the three guards above enforce. Repo-level match.
try:
from core.inference.video import get_video_backend
video_status = get_video_backend().status()
video_backend = get_video_backend()
video_status = video_backend.status()
if video_status.get("loaded") and video_status.get("repo_id"):
loaded_id = str(video_status["repo_id"]).lower()
if loaded_id == repo_id.lower() or loaded_id.startswith(repo_id.lower()):
@ -3529,6 +3531,16 @@ async def delete_cached_model(
status_code = 400,
detail = "Unload the model before deleting",
)
# Also refuse while a background VIDEO load is DOWNLOADING this repo (or its companion
# base): status().loaded is still False in that window, but deleting would remove blobs
# from under the in-flight download/assembly -- same as the Images guard above.
for lid in getattr(video_backend, "loading_repo_ids", tuple)():
lid = str(lid).lower()
if lid == repo_id.lower() or lid.startswith(repo_id.lower()):
raise HTTPException(
status_code = 400,
detail = "A Video model load is using this repo; wait for it to finish",
)
except HTTPException:
raise
except Exception:

View file

@ -243,6 +243,62 @@ def test_detect_load_family_filename_fallback():
assert _detect_load_family("someorg/quants", "ltx-2-19b-Q4_K_M.gguf", "bogus") is None
def test_detect_load_family_cached_hub_arch_fallback(monkeypatch):
# A CACHED HUB GGUF is admitted to the picker by its general.architecture (the cached-gguf
# listing tags it text-to-video), but an opaque repo id + renamed file carry no family token,
# so name detection misses. The local-file arch read misses too (a hub repo id is not a local
# dir), so without a cache fallback the loader 400s a SUPPORTED checkpoint the picker offered.
import huggingface_hub
import utils.models.gguf_metadata as gguf_meta
# No local file at Path(repo_id)/filename; resolve the arch from the cached blob instead.
monkeypatch.setattr(
huggingface_hub,
"try_to_load_from_cache",
lambda repo_id, filename, **kw: "/fake/cache/blobs/model.gguf",
)
monkeypatch.setattr(
gguf_meta, "read_gguf_general_metadata", lambda path: {"general.architecture": "ltxv"}
)
fam = _detect_load_family("someorg/opaque-quants", "model.gguf", None)
assert fam is not None and fam.name == "ltx-2"
# A cache MISS (blob not present -> None) still yields None (400 exactly as before).
monkeypatch.setattr(huggingface_hub, "try_to_load_from_cache", lambda *a, **k: None)
assert _detect_load_family("someorg/opaque-quants", "model.gguf", None) is None
# A recognised-but-unsupported video arch (wan has no backend family in this build) stays None,
# so an unsupported cached pick 400s just like the local-dir case.
monkeypatch.setattr(
huggingface_hub, "try_to_load_from_cache", lambda *a, **k: "/fake/cache/blobs/model.gguf"
)
monkeypatch.setattr(
gguf_meta, "read_gguf_general_metadata", lambda path: {"general.architecture": "wan"}
)
assert _detect_load_family("someorg/opaque-quants", "model.gguf", None) is None
def test_loading_repo_ids_guards_in_flight_delete():
# During a background load status()["loaded"] is still False, but the target repo (+ its
# companion base) is being downloaded, so the delete-cached guard needs loading_repo_ids to
# refuse deletion and avoid yanking blobs from under the in-flight download/assembly.
from core.inference.video import _VideoLoadingState
backend = VideoBackend()
assert backend.loading_repo_ids() == () # idle: nothing to guard
backend._loading = _VideoLoadingState(repo_id = "org/ckpt", base_repo = "Lightricks/LTX-2")
assert set(backend.loading_repo_ids()) == {"org/ckpt", "Lightricks/LTX-2"}
# An errored load is no longer in flight -> the files are safe to delete.
backend._loading = _VideoLoadingState(
repo_id = "org/ckpt", base_repo = "Lightricks/LTX-2", error = "boom"
)
assert backend.loading_repo_ids() == ()
# A load whose base equals the repo (or is empty) yields just the one id.
backend._loading = _VideoLoadingState(repo_id = "org/ckpt", base_repo = "")
assert backend.loading_repo_ids() == ("org/ckpt",)
def test_load_generate_unload_gguf(fake_runtime, tmp_path):
backend = VideoBackend()
status = _load_gguf(backend, tmp_path)

View file

@ -613,6 +613,26 @@ export function VideoPage({ active = true }: { active?: boolean }) {
});
}, [durationOptions, loadedFamily, familyDefaultFrames]);
// Seed steps/guidance from the loaded model's backend-authoritative defaults. On mount with a
// model already loaded (browser refresh, or a load from another client) only refreshStatus runs
// -- handleModelSelect never fires -- so the controls otherwise stick at the pre-load DEFAULT_GEN
// (8/1) and a base checkpoint that wants 40/4 silently generates a degraded clip. Key on the repo
// id so it fires once per newly-loaded model (a distilled vs base checkpoint of the same family
// has different defaults); a later user edit is not clobbered because the key only changes when
// the loaded model changes, and a gallery restore (which keeps the same repo) is left untouched.
const loadedModelKey = status?.loaded ? status.repo_id : null;
const defaultSteps = status?.defaults?.steps;
const defaultGuidance = status?.defaults?.guidance;
const prevLoadedModelRef = useRef<string | null>(null);
useEffect(() => {
const modelChanged = loadedModelKey !== prevLoadedModelRef.current;
prevLoadedModelRef.current = loadedModelKey;
if (modelChanged && loadedModelKey && defaultSteps != null && defaultGuidance != null) {
setSteps(defaultSteps);
setGuidance(defaultGuidance);
}
}, [loadedModelKey, defaultSteps, defaultGuidance]);
// Fetch (once) the object URL for a record's MP4; cached across remounts. Same
// auth-protected blob pattern the images gallery uses.
const ensureSrc = useCallback(async (video: GalleryVideo) => {