transparent image rail; hide fine-tuned shortcut; custom folders filtered to diffusion GGUFs

This commit is contained in:
oobabooga 2026-06-24 16:00:17 -03:00
commit efdc5ad9a2
5 changed files with 83 additions and 51 deletions

View file

@ -183,6 +183,12 @@ class LocalModelInfo(BaseModel):
None,
description = "Unix timestamp of latest observed update",
)
task: Optional[str] = Field(
None,
description = "HF pipeline task inferred from a GGUF's architecture "
"('text-to-image' for diffusion, 'text-generation' otherwise). Lets the "
"Images picker show only diffusion GGUFs.",
)
class LocalModelListResponse(BaseModel):

View file

@ -843,6 +843,11 @@ async def list_local_models(
reverse = True,
)
models = [m for m in models if not _is_hidden_model(m.id, m.path)]
# Tag each GGUF with its task so the Images picker can filter to diffusion.
models = [
m.model_copy(update = {"task": _local_model_task(m.path, m.model_format)})
for m in models
]
return LocalModelListResponse(
models_dir = str(models_root),
@ -3117,6 +3122,12 @@ def _gguf_architecture(path: str) -> Optional[str]:
return None
def _arch_to_task(arch: Optional[str]) -> Optional[str]:
if arch is None:
return None
return "text-to-image" if arch.lower() in _DIFFUSION_GGUF_ARCHS else "text-generation"
def _repo_gguf_task(repo_info) -> Optional[str]:
"""HF pipeline task of a cached GGUF repo, from its architecture:
'text-to-image' for diffusion archs, else 'text-generation' (None if unreadable)."""
@ -3124,10 +3135,29 @@ def _repo_gguf_task(repo_info) -> Optional[str]:
for path in _iter_gguf_paths(Path(repo_info.repo_path)):
if _is_mmproj_filename(path.name):
continue
arch = _gguf_architecture(str(path))
if arch is None:
task = _arch_to_task(_gguf_architecture(str(path)))
if task is not None:
return task
except Exception:
pass
return None
def _local_model_task(path: str, model_format: Optional[str]) -> Optional[str]:
"""Same classification for a local model: read its GGUF architecture. The
path may be the .gguf file itself or a folder containing one."""
if model_format != "gguf":
return None
try:
p = Path(path)
if p.suffix.lower() == ".gguf" and p.is_file():
return _arch_to_task(_gguf_architecture(str(p)))
for f in _iter_gguf_paths(p):
if _is_mmproj_filename(f.name):
continue
return "text-to-image" if arch.lower() in _DIFFUSION_GGUF_ARCHS else "text-generation"
task = _arch_to_task(_gguf_architecture(str(f)))
if task is not None:
return task
except Exception:
pass
return None

View file

@ -1745,20 +1745,20 @@ export function HubModelPicker({
normalizeForSearch(
`${m.model_id ?? ""} ${m.display_name} ${m.id}`,
).includes(localQuery);
// A task filter (the Images page) wants diffusion GGUFs only; local folders /
// LM Studio / fine-tuned models aren't that, so drop them in that mode.
// A task filter (the Images page) wants diffusion GGUFs only, so local models
// are filtered to that task (by the GGUF architecture the backend reports).
const sortedLmStudio = useMemo(
() =>
task
? []
: sortLocalModels(
lmStudioModels.filter(
(m) =>
localModelMatchesFormat(m, formatFilter) && matchesLocalQuery(m),
),
downloadedSort,
loadTimes,
),
sortLocalModels(
lmStudioModels.filter(
(m) =>
(!task || taskMatchesFilter(m.task, task)) &&
localModelMatchesFormat(m, formatFilter) &&
matchesLocalQuery(m),
),
downloadedSort,
loadTimes,
),
// eslint-disable-next-line react-hooks/exhaustive-deps
[lmStudioModels, downloadedSort, formatFilter, loadTimes, localQuery, task],
);
@ -1767,20 +1767,19 @@ export function HubModelPicker({
// rule). An MLX build a Mac user dropped in ./models stays selectable.
const sortedLocalDir = useMemo(
() =>
task
? []
: sortLocalModels(
localDirModels.filter(
(m) =>
(!chatOnly ||
localModelIsGguf(m) ||
(isMac && localModelIsMlx(m))) &&
localModelMatchesFormat(m, formatFilter) &&
matchesLocalQuery(m),
),
downloadedSort,
loadTimes,
),
sortLocalModels(
localDirModels.filter(
(m) =>
(!task || taskMatchesFilter(m.task, task)) &&
(!chatOnly ||
localModelIsGguf(m) ||
(isMac && localModelIsMlx(m))) &&
localModelMatchesFormat(m, formatFilter) &&
matchesLocalQuery(m),
),
downloadedSort,
loadTimes,
),
// eslint-disable-next-line react-hooks/exhaustive-deps
[
localDirModels,
@ -1795,16 +1794,16 @@ export function HubModelPicker({
);
const sortedCustomFolderModels = useMemo(
() =>
task
? []
: sortLocalModels(
customFolderModels.filter(
(m) =>
localModelMatchesFormat(m, formatFilter) && matchesLocalQuery(m),
),
customSort,
loadTimes,
),
sortLocalModels(
customFolderModels.filter(
(m) =>
(!task || taskMatchesFilter(m.task, task)) &&
localModelMatchesFormat(m, formatFilter) &&
matchesLocalQuery(m),
),
customSort,
loadTimes,
),
// eslint-disable-next-line react-hooks/exhaustive-deps
[customFolderModels, customSort, formatFilter, loadTimes, localQuery, task],
);
@ -2208,9 +2207,7 @@ export function HubModelPicker({
// On Device owns the downloaded and custom-folder models; the Unsloth tab
// searches the HF listing (below). Both filter locally by the query.
const showDownloaded = section === "downloaded";
// Custom Folders (server-side scan dirs) hold arbitrary local models, not
// diffusion GGUFs — hide the whole section under a task filter (Images).
const showCustom = section === "downloaded" && !task;
const showCustom = section === "downloaded";
const showRecommendedSection = !showHfSection && section === "recommended";
const downloadedEmpty =
visibleCachedGguf.length === 0 &&
@ -2611,6 +2608,7 @@ export function HubModelPicker({
</TooltipContent>
</Tooltip>
) : null}
{!task && (
<Tooltip delayDuration={0}>
<TooltipTrigger asChild={true}>
<button
@ -2632,6 +2630,7 @@ export function HubModelPicker({
Go to fine-tuned models
</TooltipContent>
</Tooltip>
)}
<Tooltip delayDuration={0}>
<TooltipTrigger asChild={true}>
<button

View file

@ -289,6 +289,10 @@ export interface LocalModelInfo {
// classify scanned folders whose name lacks a -GGUF suffix.
model_format?: string | null;
updated_at?: number | null;
// HF pipeline task inferred from the GGUF architecture, so the Images picker
// can filter local models to diffusion ("text-to-image"). Optional for
// older-backend compatibility.
task?: string | null;
}
interface LocalModelListResponse {

View file

@ -17,7 +17,6 @@ import {
import { Slider } from "@/components/ui/slider";
import { Spinner } from "@/components/ui/spinner";
import { Textarea } from "@/components/ui/textarea";
import { SectionCard } from "@/components/section-card";
import { ModelSelector } from "@/components/assistant-ui/model-selector";
import type {
ModelOption,
@ -312,13 +311,7 @@ export function ImagesPage() {
{/* ── Controls rail + preview canvas ─────────────────── */}
<div className="flex min-h-0 min-w-0 flex-1 gap-4 overflow-hidden px-4 pb-4 sm:px-6 sm:pb-6">
<SectionCard
icon={<HugeiconsIcon icon={ImageAdd02Icon} className="size-5" strokeWidth={1.5} />}
title="Generate"
description="Prompt and settings"
accent="indigo"
className="w-[360px] shrink-0 gap-4 overflow-y-auto"
>
<div className="flex w-[340px] shrink-0 flex-col gap-4 overflow-y-auto">
<Field label="Prompt">
<Textarea rows={4} value={prompt} onChange={(e) => setPrompt(e.target.value)} />
</Field>
@ -370,7 +363,7 @@ export function ImagesPage() {
{busy === "generating" ? <Spinner className="mr-2 size-4" /> : null}
Generate
</Button>
</SectionCard>
</div>
<div className="bg-card corner-squircle relative flex min-w-0 flex-1 flex-col overflow-hidden rounded-3xl ring-1 ring-foreground/10">
<div className="relative flex flex-1 items-center justify-center overflow-auto p-6">