diff --git a/pyproject.toml b/pyproject.toml index 0f57ecf4df..a89d2c2a51 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -88,7 +88,11 @@ huggingfacenotorch = [ "peft>=0.18.0,!=0.11.0", "huggingface_hub>=0.34.0", "hf_transfer", - "diffusers", + # 0.39 is the floor the diffusion stack already assumes: Krea2Pipeline (the Krea 2 + # workflow raises without it), the cache_context child-registry behavior, and the + # Flux2 / Z-Image pipelines. An unversioned entry let an upgrade keep an older + # diffusers, so advertised models failed to load until the user upgraded by hand. + "diffusers>=0.39.0", "transformers>=4.51.3,!=4.52.0,!=4.52.1,!=4.52.2,!=4.52.3,!=4.53.0,!=4.54.0,!=4.55.0,!=4.55.1,!=4.57.0,!=4.57.4,!=4.57.5,!=5.0.0,!=5.1.0,<=5.5.0", "trl>=0.18.2,!=0.19.0,<=0.24.0", "sentence-transformers", diff --git a/studio/backend/hub/services/models/deletion.py b/studio/backend/hub/services/models/deletion.py index efc5013b61..8ef6f0e174 100644 --- a/studio/backend/hub/services/models/deletion.py +++ b/studio/backend/hub/services/models/deletion.py @@ -629,9 +629,14 @@ def _video_blocks_delete(repo_id: str) -> Optional[str]: logger.debug(f"Video backend unavailable during delete guard for {repo_id}: {e}") return None status = backend.status() - if status.get("loaded") and status.get("repo_id"): - if _loaded_id_matches_repo(str(status["repo_id"]), repo_id): - return "Unload the model before deleting" + if status.get("loaded"): + # repo_id names the checkpoint; for a GGUF / single-file load the companion base + # supplies the VAE and text encoders and is just as much part of the live model, + # so refuse it too (the Images guard above does the same via loaded_repo_ids). + for key in ("repo_id", "base_repo"): + held = status.get(key) + if held and _loaded_id_matches_repo(str(held), repo_id): + return "Unload the model before deleting" for lid in getattr(backend, "loading_repo_ids", tuple)(): if _loaded_id_matches_repo(str(lid), repo_id): return "A Video model load is using this repo; wait for it to finish" diff --git a/studio/backend/tests/test_video_routes.py b/studio/backend/tests/test_video_routes.py index c127bc0cbe..3e7d0bb66a 100644 --- a/studio/backend/tests/test_video_routes.py +++ b/studio/backend/tests/test_video_routes.py @@ -734,3 +734,26 @@ def test_export_endpoint_validation(client, monkeypatch): resp = client.get(f"/api/inference/video/gallery/{video['id']}/export?format=webm") assert resp.status_code == 501 assert "PyAV" in resp.json()["detail"] + + +def test_delete_guard_protects_the_loaded_video_companion_base(monkeypatch): + # For a GGUF / single-file video load the companion base supplies the VAE and text + # encoders, so it is as much part of the live model as the checkpoint. Deleting it used + # to sail past the guard, which only compared repo_id. + from hub.services.models import deletion + + class _Backend: + def status(self): + return { + "loaded": True, + "repo_id": "unsloth/LTX-2.3-GGUF", + "base_repo": "unsloth/LTX-2.3", + } + + def loading_repo_ids(self): + return () + + monkeypatch.setattr(video_module, "get_video_backend", lambda: _Backend()) + assert deletion._video_blocks_delete("unsloth/LTX-2.3-GGUF") is not None + assert deletion._video_blocks_delete("unsloth/LTX-2.3") is not None + assert deletion._video_blocks_delete("unsloth/something-else") is None diff --git a/studio/frontend/src/features/model-picker/components/model-selector/pickers.tsx b/studio/frontend/src/features/model-picker/components/model-selector/pickers.tsx index d13d84362c..9a5e972db3 100644 --- a/studio/frontend/src/features/model-picker/components/model-selector/pickers.tsx +++ b/studio/frontend/src/features/model-picker/components/model-selector/pickers.tsx @@ -1921,8 +1921,11 @@ export function HubModelPicker({ ? isKnownGgufRepo(id) : !chatOnly || isRecommendableFormat(id, isKnownGgufRepo(id), isMac), ) - // Member repos of a catalog group collapse into the canonical group row. - .filter((id) => !catalog || !groupForRepoId(id, catalog)) + // Member repos of a catalog group would collapse into the canonical group row -- + // but nothing renders those rows yet (catalogGroupFitsDevice / groupMatchesQuery are + // imported and unused), and a task-scoped picker's `models` is catalogToModelOptions(), + // i.e. group members exclusively. Suppressing them here emptied Recommended on the + // Images and Video pages, so keep the artifacts listed until the grouped UI lands. .filter((id) => !/-FP8[-.]|FP8-Dynamic/i.test(id)); // Sort: GGUFs first, then hub models const gguf: string[] = []; @@ -1970,8 +1973,8 @@ export function HubModelPicker({ : rows.filter((r) => matchesFormatFilter(r.id, r.isGguf, formatFilter)); // Task-scoped pages load single-file GGUF only. if (task) rows = rows.filter((r) => r.isGguf); - // Members already render under their canonical group row. - if (catalog) rows = rows.filter((r) => !groupForRepoId(r.id, catalog)); + // Members would render under their canonical group row, but that row does not exist + // yet (see recommendedIds): filtering here removed curated models from Hub search too. // The "recommended" sort always applies the device-fit filter; the shared // "Fits on device" tick extends it to the other sorts too. if (recommendedSort !== "recommended" && !fitOnDeviceOnly) return rows;