Studio diffusion: image workflows (safetensors, image-conditioned, editing) + Images UI

Backend:
- Load non-GGUF safetensors models: full bnb-4bit pipelines and single-file
  fp8 transformers, gated to the unsloth org plus a curated allowlist.
- Image-conditioned workflows built with Pipeline.from_pipe so they reuse the
  loaded transformer/VAE/text-encoder with no extra VRAM: img2img, inpaint,
  outpaint, and a hires-fix upscale pass.
- Instruction editing as its own family kind (Qwen-Image-Edit-2511,
  FLUX.1-Kontext-dev) and FLUX.2-klein reference conditioning (single and
  multi-reference) plus klein inpaint.
- Auto-resize odd-sized inputs to a multiple of 16 (and resize the matched
  mask) so img2img/inpaint/edit no longer reject non-/16 uploads. Bound the
  decoded image size and cap upscale output to avoid OOM on large inputs.
- Fixes: from_pipe defaulting to a float32 recast that crashed torchao
  quantized transformers; image-conditioned calls forcing the slider size
  onto the input image. Native sd.cpp engine rejects image-conditioned and
  reference requests it cannot serve.

Frontend:
- Redesigned Images page with capability-gated workflow tabs (Create,
  Transform, Inpaint, Extend, Upscale, Reference, Edit), a brush mask editor,
  client-side outpaint, and a multi-reference picker.
- Advanced options moved to a right-docked panel mirroring Chat: closed by
  default, toggled by a single fixed top-bar button that stays in place.

sd.cpp installer: pin the release, verify each download's sha256, add a
download timeout, and make the source repo configurable for a future mirror.
This commit is contained in:
Daniel Han 2026-06-30 23:09:58 +00:00
commit b14e2f9be7
14 changed files with 2920 additions and 183 deletions

File diff suppressed because it is too large Load diff

View file

@ -99,7 +99,9 @@ def _activate(name: str, reason: Optional[str]) -> Any:
return get_active_diffusion_engine()
def select_and_activate_engine(fam: DiffusionFamily, *, hf_token: Optional[str] = None) -> Any:
def select_and_activate_engine(
fam: DiffusionFamily, *, hf_token: Optional[str] = None, model_kind: Optional[str] = None
) -> Any:
"""Pick + activate the engine for loading ``fam`` on this host; return the engine.
Falls back to diffusers (recording a reason) whenever the native route is
@ -107,6 +109,12 @@ def select_and_activate_engine(fam: DiffusionFamily, *, hf_token: Optional[str]
native asset mapping, or the sd-cli binary is unavailable -- always BEFORE the
slow load begins, so a fallback never strands a half-native load.
"""
# Non-GGUF loads (a single-file safetensors transformer, or a full diffusers
# pipeline) only run on diffusers: the native sd.cpp engine consumes single-file
# GGUF checkpoints only, so force diffusers before the device/native checks below.
if model_kind and model_kind != "gguf":
return _activate(ENGINE_DIFFUSERS, f"non-GGUF load ({model_kind}) requires diffusers")
forced, sd_cpp_pref, mps_enabled = _engine_config()
if forced == ENGINE_DIFFUSERS:

View file

@ -28,6 +28,32 @@ class DiffusionFamily:
# Pipeline kwarg carrying the guidance value. Most use "guidance_scale";
# Qwen-Image's distilled guidance is off, so its real CFG is "true_cfg_scale".
cfg_kwarg: str = "guidance_scale"
# Optional diffusers pipeline classes for image-conditioned workflows. The backend
# builds these around the ALREADY-loaded transformer/VAE/text-encoder via
# ``Pipeline.from_pipe`` (no extra weights, no reload), so a family only needs the
# class name here to gain the workflow. None = the family does not support it (the
# UI gates the workflow off). The base text-to-image pipeline is ``pipeline_class``.
img2img_pipeline_class: Optional[str] = None
inpaint_pipeline_class: Optional[str] = None
# True when the inpaint pipeline keeps the input canvas size, so it can also drive
# outpaint (extend), where the padded canvas is LARGER than the original. False for
# FLUX.2 (its pipelines scale any >1MP input down to ~1MP, which shrinks an outpaint
# canvas back and defeats the extend). Such families get Inpaint but not Extend.
inpaint_preserves_size: bool = True
# True for instruction-editing families (Qwen-Image-Edit / FLUX Kontext): the model's
# OWN pipeline (``pipeline_class``) is the edit pipeline -- it takes an input image plus
# a text instruction and has no plain text-to-image mode. So these expose only the
# "edit" workflow, require an input image at generate time, and the loaded pipe is used
# directly (no from_pipe). ``base_repo`` here is the matching diffusers repo that
# supplies the VAE / text-encoder / processor / scheduler for the GGUF transformer.
edit: bool = False
# True for families whose OWN text-to-image pipeline ALSO accepts reference image(s)
# (FLUX.2: Flux2KleinPipeline takes an optional ``image`` arg). Unlike ``edit`` these
# families still do plain text-to-image (no image), and unlike img2img the conditioning
# is reference-based, not a denoise blend: there is no ``strength`` and the output size
# comes from the requested width/height, not the reference's size. The loaded pipe is
# used directly (no from_pipe). Exposes a "reference" workflow alongside "txt2img".
reference: bool = False
# Extra lowercased substrings (besides ``name``) that map a repo id here.
aliases: tuple[str, ...] = field(default_factory = tuple)
# True for families whose activations overflow float16's finite range
@ -79,6 +105,8 @@ _FAMILIES: tuple[DiffusionFamily, ...] = (
transformer_class = "FluxTransformer2DModel",
base_repo = "black-forest-labs/FLUX.1-schnell",
aliases = ("flux1", "flux-1"),
img2img_pipeline_class = "FluxImg2ImgPipeline",
inpaint_pipeline_class = "FluxInpaintPipeline",
sd_cpp_vae = ("black-forest-labs/FLUX.1-schnell", "ae.safetensors"),
sd_cpp_text_encoders = (
("comfyanonymous/flux_text_encoders", "clip_l.safetensors", "clip_l"),
@ -94,6 +122,13 @@ _FAMILIES: tuple[DiffusionFamily, ...] = (
transformer_class = "Flux2Transformer2DModel",
base_repo = "black-forest-labs/FLUX.2-klein-4B",
aliases = ("flux2-klein",),
# Flux2KleinPipeline natively accepts reference image(s) via its `image` arg, so it
# exposes a "reference" workflow on top of plain text-to-image. It has a dedicated
# inpaint pipeline too (no img2img one), so it also gets inpaint + extend (outpaint).
reference = True,
inpaint_pipeline_class = "Flux2KleinInpaintPipeline",
# FLUX.2 scales >1MP inputs down to ~1MP, so outpaint (a larger canvas) can't grow.
inpaint_preserves_size = False,
# FLUX.2 uses a distinct 32-channel autoencoder; sd-cli needs the latent
# format override. The single-file VAE ships in Comfy-Org/flux2-dev (the
# klein-4B repo only has a sharded diffusers VAE). Shares Qwen3-4B with z-image.
@ -103,6 +138,39 @@ _FAMILIES: tuple[DiffusionFamily, ...] = (
("Comfy-Org/z_image_turbo", "split_files/text_encoders/qwen_3_4b.safetensors", "llm"),
),
),
DiffusionFamily(
# Instruction editing with FLUX. FluxKontextPipeline takes an input image + an edit
# instruction; the GGUF transformer is the standard FluxTransformer2DModel, with the
# T5/CLIP text encoders + VAE from the base diffusers repo. cfg defaults to
# guidance_scale (FLUX). Most-specific aliases first so detect_family prefers this
# over the plain "flux.1" family and un-rejects the "kontext" keyword for it.
name = "flux.1-kontext",
pipeline_class = "FluxKontextPipeline",
transformer_class = "FluxTransformer2DModel",
base_repo = "black-forest-labs/FLUX.1-Kontext-dev",
aliases = ("flux.1-kontext-dev", "flux1-kontext", "flux-kontext", "kontext"),
edit = True,
),
DiffusionFamily(
# Instruction editing (image-in + text-instruction-out). The 2511 checkpoint ships
# as QwenImageEditPlusPipeline (multi-image-capable); the GGUF transformer is the
# standard QwenImageTransformer2DModel, with the VAE / Qwen2.5-VL text-encoder /
# image processor / scheduler coming from the base diffusers repo. Most-specific
# aliases first so detect_family prefers this over the plain "qwen-image" family.
name = "qwen-image-edit",
pipeline_class = "QwenImageEditPlusPipeline",
transformer_class = "QwenImageTransformer2DModel",
base_repo = "Qwen/Qwen-Image-Edit-2511",
cfg_kwarg = "true_cfg_scale",
aliases = (
"qwen-image-edit-2511",
"qwen-image-edit-2509",
"qwen-image-edit",
"qwen_image_edit",
"qwenimageedit",
),
edit = True,
),
DiffusionFamily(
name = "qwen-image",
pipeline_class = "QwenImagePipeline",
@ -110,6 +178,8 @@ _FAMILIES: tuple[DiffusionFamily, ...] = (
base_repo = "Qwen/Qwen-Image",
cfg_kwarg = "true_cfg_scale",
aliases = ("qwen_image", "qwenimage"),
img2img_pipeline_class = "QwenImageImg2ImgPipeline",
inpaint_pipeline_class = "QwenImageInpaintPipeline",
sd_cpp_vae = ("Comfy-Org/Qwen-Image_ComfyUI", "split_files/vae/qwen_image_vae.safetensors"),
# The Qwen2.5-VL text encoder as a Q4_K_M GGUF keeps the CPU RAM win (the
# bf16 safetensors encoder is ~15 GB). sd-cli's --qwen2vl is an alias of --llm.
@ -130,6 +200,8 @@ _FAMILIES: tuple[DiffusionFamily, ...] = (
transformer_class = "ZImageTransformer2DModel",
base_repo = "Tongyi-MAI/Z-Image-Turbo",
aliases = ("zimage", "z_image"),
img2img_pipeline_class = "ZImageImg2ImgPipeline",
inpaint_pipeline_class = "ZImageInpaintPipeline",
# Z-Image's MLP down-projections peak near 9e5, which overflows float16.
fp16_incompatible = True,
sd_cpp_vae = ("Comfy-Org/z_image_turbo", "split_files/vae/ae.safetensors"),
@ -141,15 +213,33 @@ _FAMILIES: tuple[DiffusionFamily, ...] = (
# Editing / inpaint checkpoints share an arch keyword but need a different
# pipeline and an input image, which this text-to-image backend doesn't drive.
_EDIT_KEYWORDS = ("edit", "kontext", "inpaint")
# "layered" rejects Qwen-Image-Layered: its transformer sets additional_t_cond=True
# and expects an extra addition_t_cond input that the standard QwenImagePipeline
# never supplies, so it loads but crashes at the first denoise step. Rejecting it
# here fails the load fast with a clear message and hides it from the picker.
_EDIT_KEYWORDS = ("edit", "kontext", "inpaint", "layered")
def _best_family_match(needle: str) -> Optional[DiffusionFamily]:
"""The family whose name/alias is the LONGEST substring of ``needle``. Longest =
most specific, so an edit checkpoint ('...qwen-image-edit-2511...') matches the
'qwen-image-edit' family rather than the generic 'qwen-image' one."""
best: Optional[tuple[DiffusionFamily, int]] = None
for fam in _FAMILIES:
for token in (fam.name, *fam.aliases):
if token in needle and (best is None or len(token) > best[1]):
best = (fam, len(token))
return best[0] if best else None
def detect_family(repo_id: str, override: Optional[str] = None) -> Optional[DiffusionFamily]:
"""Resolve a ``DiffusionFamily`` from a repo id, or an explicit override.
``override`` matches a family ``name`` or alias exactly; otherwise the repo
id is scanned for the first family whose name/alias appears in it. Image
editing checkpoints are rejected (None) since this backend is text-to-image.
``override`` matches a family ``name`` or alias exactly. Otherwise the most-specific
family whose name/alias is a substring of the repo id wins. Supported editing families
(Qwen-Image-Edit) match here; unsupported editing/inpaint/layered checkpoints that only
share a base family's arch keyword are still rejected (None), because they need a
different pipeline + input this backend's base text-to-image path doesn't drive.
"""
if override:
key = override.strip().lower()
@ -158,11 +248,18 @@ def detect_family(repo_id: str, override: Optional[str] = None) -> Optional[Diff
return fam
return None
needle = repo_id.lower()
if any(kw in needle for kw in _EDIT_KEYWORDS):
return None
for fam in _FAMILIES:
if fam.name in needle or any(alias in needle for alias in fam.aliases):
return fam
match = _best_family_match(needle)
if match is not None:
# Don't let a generic base family (e.g. qwen-image) swallow a variant it can't run
# (qwen-image-LAYERED, ...-Inpaint): if the id still carries a reject keyword the
# matched family does not itself declare, reject so the load fails fast + clearly.
matched_tokens = (match.name, *match.aliases)
if any(
kw in needle and not any(kw in tok for tok in matched_tokens)
for kw in _EDIT_KEYWORDS
):
return None
return match
return None

View file

@ -262,6 +262,17 @@ def estimate_gguf_dense_mib(storage_mib: Optional[int], quant: Optional[str]) ->
return int(storage_mib * 4.0) # unknown: assume 4-bit-ish
def estimate_safetensors_dense_mib(storage_mib: Optional[int]) -> Optional[int]:
"""Resident size of a safetensors checkpoint, in MiB.
Unlike a GGUF (which is dequantised to bf16/fp16 on load, so a 4-bit file
expands ~4x), a safetensors checkpoint loads near its on-disk size: a dense
bf16 file is already bf16, and a bnb-4bit / fp8 file stays compressed in VRAM.
So the on-disk size is the estimate, returned unchanged (None passes through).
"""
return storage_mib
def estimate_image_runtime_mib(
*,
width: Optional[int],

View file

@ -232,6 +232,9 @@ class SdCppDiffusionBackend:
attention_backend: Optional[str] = None,
transformer_cache: Optional[str] = None,
transformer_cache_threshold: Optional[float] = None,
# Accepted for a uniform engine interface; the native engine is GGUF-only, so a
# non-GGUF kind never routes here (the router forces diffusers for those).
model_kind: Optional[str] = None,
) -> dict[str, Any]:
"""Validate, then fetch assets on a daemon thread. Returns at once."""
# An empty / whitespace token is "no token": passing "" verbatim to HfApi /
@ -454,11 +457,28 @@ class SdCppDiffusionBackend:
guidance: float = 0.0,
seed: Optional[int] = None,
batch_size: int = 1,
# Accepted for a uniform engine interface. The native engine is text-to-image
# only for now (sd-cli's init-img/mask plumbing is not wired), so an image-
# conditioned request is rejected clearly rather than silently dropping the input.
init_image: Optional[str] = None,
mask_image: Optional[str] = None,
strength: Optional[float] = None,
# Accepted for the uniform engine interface; upscale needs an init image, so the
# init_image guard below rejects it on the native engine like img2img/inpaint.
upscale: Optional[float] = None,
# Reference workflow is GPU/diffusers-only (FLUX.2); accepted for interface parity.
reference_images: Optional[list[str]] = None,
) -> dict[str, Any]:
import tempfile
from PIL import Image
if init_image is not None or mask_image is not None or reference_images:
raise ValueError(
"img2img / inpaint / reference are not yet supported on the native sd.cpp "
"engine; run on a GPU (diffusers) for image-conditioned workflows."
)
cancel = threading.Event()
with self._generate_lock:
with self._lock:

View file

@ -1689,9 +1689,19 @@ class AnthropicMessagesResponse(BaseModel):
class DiffusionLoadRequest(BaseModel):
"""Request to load a local diffusion (text-to-image) checkpoint."""
model_path: str = Field(..., description = "Diffusion GGUF repo id or local path")
gguf_filename: str = Field(
..., description = "The chosen single-file GGUF quant inside model_path"
model_path: str = Field(..., description = "Diffusion repo id or local path")
gguf_filename: Optional[str] = Field(
None,
description = "The chosen single-file checkpoint (GGUF or safetensors) inside "
"model_path. Required for the gguf / single_file kinds; omit for a full pipeline.",
)
model_kind: Optional[Literal["gguf", "single_file", "pipeline"]] = Field(
None,
description = "How to load the model (null = auto-detect from gguf_filename): gguf "
"(single-file GGUF transformer, dequantised on-device), single_file (single-file "
"safetensors transformer, e.g. fp8), or pipeline (a full diffusers repo via "
"from_pretrained, embedded quant auto-applied). Non-GGUF kinds are restricted to "
"unsloth/* repos (or a local path).",
)
base_repo: Optional[str] = Field(
None, description = "Companion diffusers repo for VAE/text-encoders (default: family base)"
@ -1708,10 +1718,12 @@ class DiffusionLoadRequest(BaseModel):
"cut), low_vram (offload every component, lowest VRAM, slower). "
"Overrides cpu_offload when set.",
)
speed_mode: Optional[Literal["off", "default", "max"]] = Field(
speed_mode: Optional[Literal["off", "eager", "default", "max"]] = Field(
None,
description = "Opt-in speed optims (default off -> bit-identical output): "
"default (channels_last + regional torch.compile where eligible), "
"eager (channels_last + cudnn + attention + fused RMSNorm/AdaLayerNorm patches, "
"NO torch.compile -> fast first image, no compile tax), "
"default (also regional torch.compile where eligible), "
"max (also TF32 + fused QKV).",
)
text_encoder_quant: Optional[Literal["fp8", "nvfp4"]] = Field(
@ -1807,6 +1819,55 @@ class DiffusionGenerateRequest(BaseModel):
batch_size: int = Field(
1, ge = 1, le = 32, description = "Images generated in one forward pass (VRAM-heavy)"
)
# Image-conditioned workflows (base64 or data-URL). An init_image alone runs img2img;
# init_image + mask_image runs inpaint. Both require a model family with the matching
# pipeline (img2img/inpaint) or the load is rejected with a clear message.
# Cap each base64 image string so a single request can't buffer a multi-GB payload (the
# decoded dimensions are bounded separately in the backend). ~32 MiB comfortably fits a
# full 4096px image yet rejects abuse.
init_image: Optional[str] = Field(
None,
max_length = 32 * 1024 * 1024,
description = "Base64/data-URL source image for img2img or inpaint (omit for txt2img)",
)
mask_image: Optional[str] = Field(
None,
max_length = 32 * 1024 * 1024,
description = "Base64/data-URL mask for inpaint (white = repaint, black = keep). "
"Requires init_image.",
)
strength: Optional[float] = Field(
None,
ge = 0.0,
le = 1.0,
description = "img2img/inpaint denoise strength: 0 keeps the source, 1 fully "
"redraws it. Ignored for txt2img.",
)
upscale: Optional[float] = Field(
None,
ge = 1.0,
le = 4.0,
description = "Upscale (hires fix) factor for an init_image: enlarges the source "
"by this multiple and re-denoises at low strength. Requires init_image; "
"ignored for txt2img/inpaint/edit.",
)
reference_images: Optional[list[str]] = Field(
None,
max_length = 3,
description = "Additional reference images (base64/data-URL) for the FLUX.2 reference "
"workflow, combined with init_image. Up to 3; ignored by other workflows.",
)
@field_validator("reference_images")
@classmethod
def _bounded_reference_items(cls, value: Optional[list[str]]) -> Optional[list[str]]:
# Each reference is a base64 image; bound its length like init_image/mask_image so a
# request carrying several references can't buffer a multi-GB payload.
if value is not None:
for item in value:
if len(item) > 32 * 1024 * 1024:
raise ValueError("each reference image must be at most 32 MiB (base64)")
return value
@field_validator("width", "height")
@classmethod
@ -1880,6 +1941,9 @@ class DiffusionStatusResponse(BaseModel):
base_repo: Optional[str] = Field(None, description = "Companion diffusers base repo")
device: Optional[str] = Field(None, description = "Device the pipeline is on")
dtype: Optional[str] = Field(None, description = "Compute dtype")
model_kind: Optional[str] = Field(
None, description = "Resolved load kind: gguf | single_file | pipeline (gates GGUF-only UI)"
)
cpu_offload: bool = Field(False, description = "Whether CPU offload is engaged")
offload_policy: Optional[str] = Field(
None, description = "Resolved offload policy: none | group | model | sequential"
@ -1904,6 +1968,11 @@ class DiffusionStatusResponse(BaseModel):
"_native_cudnn), or null for the default SDPA",
)
transformer_cache: Optional[str] = Field(None, description = "Step cache engaged: fbcache | null")
workflows: list[str] = Field(
default_factory = list,
description = "Image workflows the loaded family supports (drives UI tab gating): "
"txt2img, img2img, inpaint. Empty when nothing is loaded or on the native engine.",
)
engine: Optional[str] = Field(None, description = "Active diffusion engine: diffusers | sd_cpp")
fallback_reason: Optional[str] = Field(
None,

View file

@ -10057,7 +10057,7 @@ async def _openai_passthrough_non_streaming(
async def load_diffusion_model(
request: DiffusionLoadRequest, current_subject: str = Depends(get_current_subject)
):
from core.inference.diffusion import get_diffusion_backend
from core.inference.diffusion import get_diffusion_backend, resolve_model_kind
from core.inference.diffusion_device import resolve_diffusion_device_target
from core.inference.diffusion_engine_router import (
active_engine_name,
@ -10070,19 +10070,25 @@ async def load_diffusion_model(
backend = get_diffusion_backend()
try:
# Resolve the load kind once (gguf / single_file / pipeline) so validation,
# engine selection, and the load all agree. A bad explicit kind raises here -> 400.
kind = resolve_model_kind(request.gguf_filename, request.model_kind)
# Validate cheaply BEFORE touching the GPU: an unloadable pick (bad family,
# missing local GGUF) must not evict a working chat model and then 400. The
# validated family also drives engine selection below.
# missing local GGUF, a non-unsloth non-GGUF repo) must not evict a working chat
# model and then 400. The validated family also drives engine selection below.
fam = await asyncio.to_thread(
backend.validate_load_request,
request.model_path,
gguf_filename = request.gguf_filename,
family_override = request.family_override,
model_kind = kind,
)
# Pick the engine for this host (diffusers on GPU, native sd.cpp with no GPU),
# installing the sd-cli binary if needed -- all BEFORE evicting chat, so a
# native fallback never strands a half-loaded state.
engine = await asyncio.to_thread(select_and_activate_engine, fam, hf_token = request.hf_token)
# native fallback never strands a half-loaded state. Non-GGUF kinds force diffusers.
engine = await asyncio.to_thread(
select_and_activate_engine, fam, hf_token = request.hf_token, model_kind = kind
)
# Take the GPU from the chat backend only when this load will actually use it.
# diffusers always does; a *force-native* sd.cpp load on a CUDA/XPU/MPS box does
# too. But a native sd.cpp load on a pure-CPU host never touches the GPU, so
@ -10110,6 +10116,7 @@ async def load_diffusion_model(
attention_backend = request.attention_backend,
transformer_cache = request.transformer_cache,
transformer_cache_threshold = request.transformer_cache_threshold,
model_kind = kind,
)
return DiffusionStatusResponse(**annotate_status(status_dict))
except (ValueError, FileNotFoundError) as exc:
@ -10138,7 +10145,16 @@ async def generate_diffusion_image(
guidance = request.guidance,
seed = request.seed,
batch_size = request.batch_size,
init_image = request.init_image,
mask_image = request.mask_image,
strength = request.strength,
upscale = request.upscale,
reference_images = request.reference_images,
)
except ValueError as exc:
# Bad client input (undecodable image/mask, or a workflow the loaded family
# doesn't support) — a 400 with the reason, not a generic 500.
raise HTTPException(status_code = 400, detail = str(exc))
except RuntimeError as exc:
# Only "no model loaded" / cancelled are client-state (409). The native
# sd.cpp engine also raises RuntimeError for execution failures (nonzero
@ -10146,10 +10162,10 @@ async def generate_diffusion_image(
msg = str(exc)
if "No diffusion model is loaded" in msg or "cancelled" in msg.lower():
raise HTTPException(status_code = 409, detail = msg)
logger.error("diffusion.generate_failed: %s", exc)
logger.error("diffusion.generate_failed: %s", exc, exc_info = True)
raise HTTPException(status_code = 500, detail = "Image generation failed.")
except Exception as exc:
logger.error("diffusion.generate_failed: %s", exc)
logger.error("diffusion.generate_failed: %s", exc, exc_info = True)
raise HTTPException(status_code = 500, detail = "Image generation failed.")
# Persist each image with its full recipe embedded. The diffusers batch shares

View file

@ -50,9 +50,24 @@ def test_detect_family_from_repo_id():
# Qwen-Image guides via true_cfg_scale, not guidance_scale.
assert detect_family("unsloth/Qwen-Image-2512-GGUF").cfg_kwarg == "true_cfg_scale"
assert detect_family("unsloth/Z-Image-GGUF").cfg_kwarg == "guidance_scale"
# Image-editing checkpoints are rejected (text-to-image backend only).
assert detect_family("unsloth/Qwen-Image-Edit-2511-GGUF") is None
assert detect_family("unsloth/FLUX.1-Kontext-dev-GGUF") is None
# Qwen-Image-Edit is a SUPPORTED instruction-editing family (its own edit pipeline);
# the most-specific match wins so it doesn't fall back to the generic qwen-image.
edit = detect_family("unsloth/Qwen-Image-Edit-2511-GGUF")
assert edit.name == "qwen-image-edit"
assert edit.pipeline_class == "QwenImageEditPlusPipeline"
assert edit.edit is True
assert detect_family("unsloth/Qwen-Image-Edit-2509-GGUF").name == "qwen-image-edit"
# FLUX Kontext is a SUPPORTED editing family (FluxKontextPipeline); the "kontext"
# keyword is un-rejected for it, and it must win over the generic "flux.1" match.
kontext = detect_family("unsloth/FLUX.1-Kontext-dev-GGUF")
assert kontext.name == "flux.1-kontext"
assert kontext.pipeline_class == "FluxKontextPipeline"
assert kontext.edit is True
assert kontext.cfg_kwarg == "guidance_scale"
# A plain FLUX.1 checkpoint must still resolve to the base flux.1 family, not kontext.
assert detect_family("unsloth/FLUX.1-dev-GGUF").name == "flux.1"
# A plain Qwen-Image checkpoint must still resolve to the base family, not edit.
assert detect_family("unsloth/Qwen-Image-2512-GGUF").name == "qwen-image"
assert detect_family("meta-llama/Llama-3-8B") is None
@ -194,6 +209,86 @@ class _FakeTransformer:
return object()
class _FakeImg2ImgPipe:
"""An img2img pipeline call: records the image-conditioned kwargs. Its signature
declares image/strength but NOT width/height, mirroring real img2img pipelines
(which derive the output size from the input image)."""
last_kwargs: dict = {}
def __call__(
self,
*,
prompt = None,
image = None,
strength = None,
negative_prompt = None,
callback_on_step_end = None,
guidance_scale = None,
true_cfg_scale = None,
**kwargs,
):
_FakeImg2ImgPipe.last_kwargs = {
"prompt": prompt,
"image": image,
"strength": strength,
**kwargs,
}
n = kwargs.get("num_images_per_prompt", 1)
return types.SimpleNamespace(images = [_FakeImage() for _ in range(n)])
class _FakeImg2ImgPipeline:
built_from: object = None
from_pipe_kwargs: dict = {}
@classmethod
def from_pipe(cls, base_pipe, **kwargs):
_FakeImg2ImgPipeline.built_from = base_pipe
_FakeImg2ImgPipeline.from_pipe_kwargs = kwargs
return _FakeImg2ImgPipe()
class _FakeInpaintPipe:
"""An inpaint pipeline call: records image + mask_image + strength. Real inpaint
pipelines take both an init image and a grayscale mask and derive output size from
the input, so width/height are not in its signature."""
last_kwargs: dict = {}
def __call__(
self,
*,
prompt = None,
image = None,
mask_image = None,
strength = None,
negative_prompt = None,
callback_on_step_end = None,
guidance_scale = None,
true_cfg_scale = None,
**kwargs,
):
_FakeInpaintPipe.last_kwargs = {
"prompt": prompt,
"image": image,
"mask_image": mask_image,
"strength": strength,
**kwargs,
}
n = kwargs.get("num_images_per_prompt", 1)
return types.SimpleNamespace(images = [_FakeImage() for _ in range(n)])
class _FakeInpaintPipeline:
built_from: object = None
@classmethod
def from_pipe(cls, base_pipe, **kwargs):
_FakeInpaintPipeline.built_from = base_pipe
return _FakeInpaintPipe()
@pytest.fixture
def fake_runtime(monkeypatch):
torch = types.ModuleType("torch")
@ -210,9 +305,15 @@ def fake_runtime(monkeypatch):
diffusers.GGUFQuantizationConfig = lambda compute_dtype = None: ("quant", compute_dtype)
diffusers.ZImagePipeline = _FakePipeline
diffusers.ZImageTransformer2DModel = _FakeTransformer
diffusers.ZImageImg2ImgPipeline = _FakeImg2ImgPipeline
diffusers.ZImageInpaintPipeline = _FakeInpaintPipeline
# Qwen-Image too, so the true_cfg_scale cfg-kwarg path is exercisable.
diffusers.QwenImagePipeline = _FakePipeline
diffusers.QwenImageTransformer2DModel = _FakeTransformer
diffusers.QwenImageImg2ImgPipeline = _FakeImg2ImgPipeline
diffusers.QwenImageInpaintPipeline = _FakeInpaintPipeline
# Instruction-editing pipeline (Qwen-Image-Edit): its own pipeline IS the loaded one.
diffusers.QwenImageEditPlusPipeline = _FakePipeline
monkeypatch.setitem(sys.modules, "torch", torch)
monkeypatch.setitem(sys.modules, "diffusers", diffusers)
@ -221,6 +322,10 @@ def fake_runtime(monkeypatch):
monkeypatch.setattr("core.inference.diffusion.clear_gpu_cache", lambda: None)
_FakePipeline.last = {}
_FakeTransformer.last = {}
_FakeImg2ImgPipeline.built_from = None
_FakeImg2ImgPipe.last_kwargs = {}
_FakeInpaintPipeline.built_from = None
_FakeInpaintPipe.last_kwargs = {}
yield
@ -273,6 +378,453 @@ def test_load_generate_unload_gguf(fake_runtime, tmp_path):
assert backend.is_loaded is False
def _tiny_png_b64() -> str:
import base64
import io
from PIL import Image
buf = io.BytesIO()
Image.new("RGB", (64, 64), (120, 30, 30)).save(buf, format = "PNG")
return base64.b64encode(buf.getvalue()).decode()
def test_generate_img2img_uses_from_pipe(fake_runtime, tmp_path):
"""An init_image routes generate() through the family's img2img pipeline, built via
Pipeline.from_pipe around the loaded pipe (no reload), with image + strength passed
and width/height dropped (the img2img pipe derives size from the input image)."""
(tmp_path / "model.gguf").write_bytes(b"x")
backend = DiffusionBackend()
backend.load_pipeline(
str(tmp_path), gguf_filename = "model.gguf", base_repo = "base/repo", family_override = "z-image"
)
# The loaded family advertises the image-conditioned workflows for UI gating
# (upscale rides the img2img pipeline, so it appears whenever img2img does).
assert backend.status()["workflows"] == ["txt2img", "img2img", "upscale", "inpaint", "outpaint"]
loaded_pipe = backend._state.pipe
out = backend.generate(
prompt = "a car at sunset", steps = 4, guidance = 0.0, seed = 3,
init_image = _tiny_png_b64(), strength = 0.5,
)
assert len(out["images"]) == 1
# from_pipe was handed the loaded text-to-image pipe (component reuse, no reload).
assert _FakeImg2ImgPipeline.built_from is loaded_pipe
# ...and with torch_dtype=None so from_pipe SKIPS its default float32 recast, which
# both upcasts the reused bf16 modules and crashes on torchao-quantized weights.
assert _FakeImg2ImgPipeline.from_pipe_kwargs.get("torch_dtype", "MISSING") is None
call = _FakeImg2ImgPipe.last_kwargs
assert call["image"] is not None # decoded source image passed through
assert call["strength"] == 0.5
assert "width" not in call and "height" not in call # img2img derives size from image
# A txt2img call after it still uses the base pipe (no image kwarg).
backend.generate(prompt = "plain", steps = 4, seed = 1)
assert backend._state.pipe.last_kwargs.get("image") is None
def test_generate_img2img_unsupported_family_raises(fake_runtime, tmp_path, monkeypatch):
"""A family with no image-conditioning at all (no img2img/inpaint/edit/reference) rejects
an init_image with a clear error rather than failing deep in the pipeline."""
from core.inference.diffusion_families import DiffusionFamily
# A synthetic txt2img-only family: no img2img/inpaint pipeline, not edit, not reference.
# (Every shipped family now supports some image workflow, so build one for this case.)
plain = DiffusionFamily(
name = "plain-test",
pipeline_class = "ZImagePipeline",
transformer_class = "ZImageTransformer2DModel",
base_repo = "base/repo",
)
monkeypatch.setattr(
"core.inference.diffusion.detect_family", lambda repo_id, override = None: plain
)
(tmp_path / "model.gguf").write_bytes(b"x")
backend = DiffusionBackend()
backend.load_pipeline(str(tmp_path), gguf_filename = "model.gguf", base_repo = "base/repo")
assert backend.status()["workflows"] == ["txt2img"]
with pytest.raises(ValueError, match = "img2img"):
backend.generate(prompt = "x", steps = 4, init_image = _tiny_png_b64())
def test_generate_upscale_enlarges_and_low_strength(fake_runtime, tmp_path):
"""An init_image + upscale factor routes generate() through the family's img2img
pipeline (hires fix): the source is enlarged to size*factor (rounded to /16) before the
denoise, the strength defaults low, and the factor is capped so a huge value can't OOM."""
(tmp_path / "model.gguf").write_bytes(b"x")
backend = DiffusionBackend()
backend.load_pipeline(
str(tmp_path), gguf_filename = "model.gguf", base_repo = "base/repo", family_override = "z-image"
)
# Upscale rides the img2img pipeline, so it is advertised alongside img2img.
assert "upscale" in backend.status()["workflows"]
loaded_pipe = backend._state.pipe
out = backend.generate(
prompt = "a crisp photo", steps = 4, guidance = 0.0, seed = 3,
init_image = _tiny_png_b64(), upscale = 2.0, # 64 -> 128, no explicit strength
)
assert len(out["images"]) == 1
# Reuses the resident modules via from_pipe (no reload, no extra VRAM).
assert _FakeImg2ImgPipeline.built_from is loaded_pipe
call = _FakeImg2ImgPipe.last_kwargs
# The image handed to the pipe is the ENLARGED source (64 * 2 = 128, already /16).
assert call["image"].size == (128, 128)
# Strength defaults to the hires-fix value when the caller sends none.
assert call["strength"] == 0.35
# The factor is capped at 4x so a large request can't blow up the VAE/transformer.
backend.generate(
prompt = "x", steps = 4, seed = 1, init_image = _tiny_png_b64(), upscale = 99.0,
)
assert _FakeImg2ImgPipe.last_kwargs["image"].size == (256, 256) # 64 * 4 (capped)
# An explicit strength overrides the hires-fix default.
backend.generate(
prompt = "x", steps = 4, seed = 1, init_image = _tiny_png_b64(),
upscale = 1.5, strength = 0.2,
)
assert _FakeImg2ImgPipe.last_kwargs["strength"] == 0.2
# 64 * 1.5 = 96, already a multiple of 16.
assert _FakeImg2ImgPipe.last_kwargs["image"].size == (96, 96)
def _png_b64(side: int) -> str:
import base64
import io
from PIL import Image
buf = io.BytesIO()
Image.new("RGB", (side, side), (10, 20, 30)).save(buf, format = "PNG")
return base64.b64encode(buf.getvalue()).decode()
def test_decode_image_rejects_oversized(fake_runtime, tmp_path):
"""An input image larger than the per-side cap is rejected with a clear error (protects
img2img / inpaint / reference from decompression-bomb / OOM inputs), not a 500."""
(tmp_path / "model.gguf").write_bytes(b"x")
backend = DiffusionBackend()
backend.load_pipeline(
str(tmp_path), gguf_filename = "model.gguf", base_repo = "base/repo", family_override = "z-image"
)
with pytest.raises(ValueError, match = "too large"):
backend.generate(prompt = "x", steps = 4, init_image = _png_b64(4112)) # > 4096/side
def test_upscale_output_is_capped(fake_runtime, tmp_path):
"""Upscale bounds the absolute output side to 2048 even when input*factor exceeds it, so a
large upload at 4x can't OOM the VAE/transformer."""
(tmp_path / "model.gguf").write_bytes(b"x")
backend = DiffusionBackend()
backend.load_pipeline(
str(tmp_path), gguf_filename = "model.gguf", base_repo = "base/repo", family_override = "z-image"
)
backend.generate(prompt = "x", steps = 4, seed = 1, init_image = _png_b64(1024), upscale = 4.0)
# 1024 * 4 = 4096 -> clamped to 2048 (longest side), still a multiple of 16.
assert _FakeImg2ImgPipe.last_kwargs["image"].size == (2048, 2048)
def _mask_b64(side: int) -> str:
import base64
import io
from PIL import Image
buf = io.BytesIO()
img = Image.new("L", (side, side), 0)
for y in range(side // 4, 3 * side // 4):
for x in range(side // 4, 3 * side // 4):
img.putpixel((x, y), 255)
img.save(buf, format = "PNG")
return base64.b64encode(buf.getvalue()).decode()
def test_img2img_snaps_non_multiple_of_16(fake_runtime, tmp_path):
"""An odd-sized img2img upload (not divisible by 16) is auto-resized to the nearest
multiple of 16 so the pipeline's divisibility check passes instead of erroring."""
(tmp_path / "model.gguf").write_bytes(b"x")
backend = DiffusionBackend()
backend.load_pipeline(
str(tmp_path), gguf_filename = "model.gguf", base_repo = "base/repo", family_override = "z-image"
)
backend.generate(prompt = "x", steps = 4, seed = 1, init_image = _png_b64(186), strength = 0.5)
# 186 / 16 = 11.625 -> round to 12 -> 192.
assert _FakeImg2ImgPipe.last_kwargs["image"].size == (192, 192)
def test_inpaint_snaps_image_and_mask_together(fake_runtime, tmp_path):
"""Inpaint snaps the odd-sized input to /16 AND resizes the mask to match, so the image
and mask stay aligned (a mismatch would crash the inpaint pipeline)."""
(tmp_path / "model.gguf").write_bytes(b"x")
backend = DiffusionBackend()
backend.load_pipeline(
str(tmp_path), gguf_filename = "model.gguf", base_repo = "base/repo", family_override = "z-image"
)
backend.generate(
prompt = "x", steps = 4, seed = 1,
init_image = _png_b64(186), mask_image = _mask_b64(186), strength = 0.5,
)
assert _FakeInpaintPipe.last_kwargs["image"].size == (192, 192)
assert _FakeInpaintPipe.last_kwargs["mask_image"].size == (192, 192)
def test_generate_reference_uses_loaded_pipe_at_slider_size(fake_runtime, tmp_path):
"""A reference family (FLUX.2-klein) advertises txt2img + reference, and a generate with
an init_image passes it as the loaded pipe's `image` arg (no from_pipe, no strength) while
the output size stays the REQUESTED slider size (the pipe resizes the reference itself)."""
import diffusers
diffusers.Flux2KleinPipeline = _FakePipeline
diffusers.Flux2KleinInpaintPipeline = _FakeInpaintPipeline
diffusers.Flux2Transformer2DModel = _FakeTransformer
(tmp_path / "model.gguf").write_bytes(b"x")
backend = DiffusionBackend()
backend.load_pipeline(
str(tmp_path), gguf_filename = "model.gguf", base_repo = "base/repo",
family_override = "flux.2-klein",
)
# FLUX.2-klein: txt2img + reference (own pipe) + inpaint (dedicated pipe). No img2img class,
# so no img2img/upscale.
assert backend.status()["workflows"] == ["txt2img", "reference", "inpaint"]
loaded_pipe = backend._state.pipe
out = backend.generate(
prompt = "a portrait in this style", steps = 6, guidance = 4.0, seed = 5,
width = 768, height = 512, init_image = _tiny_png_b64(), strength = 0.5,
)
assert len(out["images"]) == 1
call = loaded_pipe.last_kwargs
assert call["image"] is not None # reference handed to the loaded pipe
assert call["width"] == 768 and call["height"] == 512 # OUTPUT size = sliders, not input
assert "strength" not in call # reference conditioning has no strength
assert "mask_image" not in call
# Guidance flows via guidance_scale (FLUX.2 default behaviour).
assert call["guidance_scale"] == 4.0
# Multi-reference: extra reference_images are combined with init_image into a LIST so the
# model can blend several references (subject + style).
backend.generate(
prompt = "combine these", steps = 6, seed = 9, width = 1024, height = 1024,
init_image = _tiny_png_b64(), reference_images = [_tiny_png_b64(), _tiny_png_b64()],
)
img_arg = loaded_pipe.last_kwargs["image"]
assert isinstance(img_arg, list) and len(img_arg) == 3 # primary + 2 extras
# Branch ordering: an init image + MASK on a reference family must route to inpaint (the
# dedicated pipeline), NOT be swallowed by the reference branch (which ignores the mask).
backend.generate(
prompt = "repaint here", steps = 6, seed = 2,
init_image = _tiny_png_b64(), mask_image = _tiny_mask_b64(), strength = 0.8,
)
assert _FakeInpaintPipeline.built_from is loaded_pipe # built via from_pipe off the load
assert _FakeInpaintPipe.last_kwargs["mask_image"] is not None
assert _FakeInpaintPipe.last_kwargs["strength"] == 0.8
# Without an init image the same family does plain txt2img (no image arg).
backend.generate(prompt = "just text", steps = 6, seed = 1)
assert backend._state.pipe.last_kwargs.get("image") is None
def _tiny_mask_b64() -> str:
import base64
import io
from PIL import Image
buf = io.BytesIO()
# A grayscale mask: white square (repaint) on black (keep).
img = Image.new("L", (64, 64), 0)
for y in range(16, 48):
for x in range(16, 48):
img.putpixel((x, y), 255)
img.save(buf, format = "PNG")
return base64.b64encode(buf.getvalue()).decode()
def test_generate_inpaint_uses_from_pipe(fake_runtime, tmp_path):
"""An init_image + mask_image routes generate() through the family's inpaint pipeline,
built via Pipeline.from_pipe around the loaded pipe (no reload), with the decoded image
+ mask + strength passed through and width/height dropped (size derives from the input)."""
(tmp_path / "model.gguf").write_bytes(b"x")
backend = DiffusionBackend()
backend.load_pipeline(
str(tmp_path), gguf_filename = "model.gguf", base_repo = "base/repo", family_override = "z-image"
)
loaded_pipe = backend._state.pipe
out = backend.generate(
prompt = "a red door", steps = 4, guidance = 0.0, seed = 5,
init_image = _tiny_png_b64(), mask_image = _tiny_mask_b64(), strength = 0.7,
)
assert len(out["images"]) == 1
# The inpaint pipe (not img2img) was selected and built from the loaded pipe.
assert _FakeInpaintPipeline.built_from is loaded_pipe
assert _FakeImg2ImgPipeline.built_from is None
call = _FakeInpaintPipe.last_kwargs
assert call["image"] is not None and call["mask_image"] is not None
assert call["strength"] == 0.7
assert "width" not in call and "height" not in call # inpaint derives size from image
def test_image_conditioned_passes_image_size_not_slider(fake_runtime, tmp_path):
"""When the workflow pipe DOES accept width/height, an image-conditioned call must pass
the INPUT IMAGE's size, never the txt2img slider size -- otherwise a non-slider-sized
input (e.g. a 1536px outpaint canvas with a 1024 slider) mismatches the latents
("tensor a (128) must match tensor b (192)"). Covers Transform + Extend with any size."""
import base64
import io
from PIL import Image
class _SizePipe:
last: dict = {}
def __call__(
self,
*,
prompt = None,
image = None,
strength = None,
width = None,
height = None,
negative_prompt = None,
callback_on_step_end = None,
guidance_scale = None,
true_cfg_scale = None,
**kwargs,
):
_SizePipe.last = {"width": width, "height": height}
n = kwargs.get("num_images_per_prompt", 1)
return types.SimpleNamespace(images = [_FakeImage() for _ in range(n)])
class _SizePipeline:
@classmethod
def from_pipe(cls, base_pipe, **kwargs):
return _SizePipe()
import diffusers
diffusers.ZImageImg2ImgPipeline = _SizePipeline
(tmp_path / "model.gguf").write_bytes(b"x")
backend = DiffusionBackend()
backend.load_pipeline(
str(tmp_path), gguf_filename = "model.gguf", base_repo = "base/repo", family_override = "z-image"
)
buf = io.BytesIO()
Image.new("RGB", (96, 64), (10, 20, 30)).save(buf, format = "PNG") # non-square, non-slider
b64 = base64.b64encode(buf.getvalue()).decode()
backend.generate(prompt = "x", steps = 4, width = 1024, height = 1024, init_image = b64, strength = 0.5)
# The pipe got the IMAGE's 96x64, not the 1024x1024 slider.
assert _SizePipe.last == {"width": 96, "height": 64}
def test_edit_family_uses_own_pipeline_and_requires_image(fake_runtime, tmp_path):
"""An instruction-editing family (Qwen-Image-Edit) exposes only the 'edit' workflow,
runs the image through its OWN loaded pipeline (no from_pipe), and rejects a call with
no input image."""
(tmp_path / "model.gguf").write_bytes(b"x")
backend = DiffusionBackend()
backend.load_pipeline(
str(tmp_path), gguf_filename = "model.gguf", base_repo = "Qwen/Qwen-Image-Edit-2511",
family_override = "qwen-image-edit",
)
# Edit families advertise only the edit workflow (no txt2img / img2img / inpaint).
assert backend.status()["workflows"] == ["edit"]
loaded_pipe = backend._state.pipe
out = backend.generate(
prompt = "make it night", steps = 8, guidance = 4.0, seed = 1,
init_image = _tiny_png_b64(),
)
assert len(out["images"]) == 1
# The loaded pipe handled it directly -- no from_pipe img2img/inpaint was built.
assert backend._state.pipe is loaded_pipe
assert _FakeImg2ImgPipeline.built_from is None and _FakeInpaintPipeline.built_from is None
assert loaded_pipe.last_kwargs.get("image") is not None
# An edit model with no input image fails fast with a clear message.
with pytest.raises(ValueError, match = "image"):
backend.generate(prompt = "make it night", steps = 8)
def test_load_pipeline_kind_uses_from_pretrained(fake_runtime):
"""A full-pipeline (no single-file) load on an unsloth/* repo builds the pipe with
pipeline_cls.from_pretrained(repo_id) -- NO single-file transformer build, NO GGUF
quant config -- so an embedded bnb-4bit config is reloaded by diffusers itself."""
backend = DiffusionBackend()
status = backend.load_pipeline(
"unsloth/Z-Image-Turbo-unsloth-bnb-4bit", family_override = "z-image"
)
assert status["loaded"] is True
assert status["family"] == "z-image"
# from_pretrained pointed at the repo itself (it IS its own base), with no transformer.
assert _FakePipeline.last["base"] == "unsloth/Z-Image-Turbo-unsloth-bnb-4bit"
assert "transformer" not in _FakePipeline.last
# The GGUF single-file build path was never taken.
assert _FakeTransformer.last == {}
def test_load_single_file_safetensors_no_gguf_config(fake_runtime, tmp_path):
"""A single-file *.safetensors transformer is built with from_single_file WITHOUT the
GGUF dequant config (it carries its own dtype), then assembled from the base repo."""
(tmp_path / "model.safetensors").write_bytes(b"weights")
backend = DiffusionBackend()
status = backend.load_pipeline(
str(tmp_path),
gguf_filename = "model.safetensors",
base_repo = "base/repo",
family_override = "qwen-image",
)
assert status["loaded"] is True
assert _FakeTransformer.last["path"] == str((tmp_path / "model.safetensors").resolve())
assert _FakeTransformer.last["subfolder"] == "transformer"
# No GGUF quant config on the safetensors path (the GGUF path sets one).
assert "quantization_config" not in _FakeTransformer.last
assert _FakePipeline.last["base"] == "base/repo"
assert "transformer" in _FakePipeline.last
def test_load_pipeline_rejects_non_unsloth_repo(fake_runtime):
backend = DiffusionBackend()
with pytest.raises(ValueError, match = "unsloth"):
backend.load_pipeline("randomorg/Z-Image-bnb-4bit", family_override = "z-image")
def test_detect_family_rejects_layered():
# Qwen-Image-Layered needs a dedicated pipeline (additional_t_cond); it must be
# rejected so it fails fast at load instead of crashing at the first denoise step.
assert detect_family("unsloth/Qwen-Image-Layered-GGUF") is None
assert detect_family("unsloth/qwen_image_layered") is None
def test_failed_load_rolls_back_eager_patches(fake_runtime, tmp_path, monkeypatch):
"""A load failure AFTER the eager patches install but BEFORE the _LoadState commit must
roll the process-wide patches back, so the next bit-identical `off` load is not
contaminated (the asymmetric-cleanup bug the reviewers flagged)."""
from core.inference import diffusion as diff_mod
from core.inference import diffusion_eager_patches as ep
(tmp_path / "model.gguf").write_bytes(b"x")
ep.uninstall_patches() # clean slate
def _boom(*_a, **_k):
raise RuntimeError("placement boom")
# apply_memory_plan runs AFTER the patches are installed, before _LoadState commits.
monkeypatch.setattr(diff_mod, "apply_memory_plan", _boom)
backend = DiffusionBackend()
with pytest.raises(RuntimeError):
backend.load_pipeline(
str(tmp_path),
gguf_filename = "model.gguf",
family_override = "z-image",
base_repo = "base/repo",
speed_mode = "eager", # != off -> installs the shared patches
)
assert ep.is_installed() is False # rolled back by the load-failure finally
assert backend.is_loaded is False
def test_cpu_offload_ignored_off_cuda(fake_runtime, tmp_path):
(tmp_path / "model.gguf").write_bytes(b"x")
backend = DiffusionBackend()
@ -319,8 +871,10 @@ def test_resolve_base_repo_prefers_caller_then_hf_tag_then_fallback(monkeypatch)
def test_load_without_gguf_raises():
backend = DiffusionBackend()
with pytest.raises(ValueError):
backend.load_pipeline("unsloth/Z-Image-Turbo-GGUF") # no gguf_filename
# No gguf_filename -> a full-pipeline load, gated to unsloth/*; a non-unsloth repo
# is rejected before any GPU/network work.
with pytest.raises(ValueError, match = "unsloth"):
backend.load_pipeline("some-org/Z-Image-bnb-4bit")
def test_load_unknown_family_raises():
@ -685,8 +1239,24 @@ def test_callback_cancellation_interrupts_denoise(fake_runtime):
def test_validate_load_request(tmp_path):
backend = DiffusionBackend()
with pytest.raises(ValueError, match = "gguf_filename"):
backend.validate_load_request("unsloth/Z-Image-Turbo-GGUF")
# No filename + unsloth repo -> a full-pipeline load (allowed for unsloth/*).
assert (
backend.validate_load_request("unsloth/Z-Image-Turbo-unsloth-bnb-4bit").name == "z-image"
)
# No filename + non-unsloth repo -> a pipeline load, gated to unsloth/* -> rejected.
with pytest.raises(ValueError, match = "unsloth"):
backend.validate_load_request("some-org/Z-Image-bnb-4bit")
# An explicit gguf/single_file kind still requires a single-file name.
with pytest.raises(ValueError, match = "single-file"):
backend.validate_load_request("unsloth/Z-Image-Turbo-GGUF", model_kind = "gguf")
# A pipeline kind must NOT carry a single-file name.
with pytest.raises(ValueError, match = "pipeline"):
backend.validate_load_request(
"unsloth/Z-Image-Turbo-bnb-4bit", gguf_filename = "q.gguf", model_kind = "pipeline"
)
# A single-file safetensors load is also gated to unsloth/* repos.
with pytest.raises(ValueError, match = "unsloth"):
backend.validate_load_request("some-org/Z-Image", gguf_filename = "model.safetensors")
with pytest.raises(ValueError, match = "family"):
backend.validate_load_request("meta/Llama-3", gguf_filename = "q.gguf")
assert (

View file

@ -35,13 +35,21 @@ class _FakeBackend:
*,
gguf_filename = None,
family_override = None,
model_kind = None,
):
# Mirror the real backend's cheap validation so the route's
# validate-before-evict ordering is exercised.
from core.inference.diffusion import resolve_model_kind
from core.inference.diffusion_families import detect_family
if not gguf_filename:
raise ValueError("gguf_filename is required.")
kind = resolve_model_kind(gguf_filename, model_kind)
if kind in ("gguf", "single_file") and not gguf_filename:
raise ValueError("a single-file checkpoint name is required.")
# Non-GGUF loads are gated to unsloth/* (or a local path), like the real backend.
if kind != "gguf" and not model_path.lower().startswith("unsloth/"):
raise ValueError(
f"Non-GGUF diffusion loads are restricted to unsloth/* repos; got '{model_path}'."
)
fam = detect_family(model_path, family_override)
if fam is None:
raise ValueError(f"Could not infer a diffusion family for '{model_path}'.")
@ -254,10 +262,24 @@ def test_generate_rejects_non_multiple_of_16(client):
assert ok.status_code == 200
def test_load_requires_gguf_filename(client):
# gguf_filename is now mandatory — a load without it is a 422.
def test_non_gguf_load_restricted_to_unsloth(client):
# gguf_filename is optional now; with none, the load is a full-pipeline kind, which
# is gated to unsloth/* repos. A non-unsloth repo (no filename) is rejected -> 400.
resp = client.post("/api/inference/images/load", json = {"model_path": "x/z-image"})
assert resp.status_code == 422
assert resp.status_code == 400
assert "unsloth" in resp.json()["detail"].lower()
def test_pipeline_load_allowed_for_unsloth_repo(client):
# An unsloth/* repo with no filename loads as a full diffusers pipeline (kind auto
# = pipeline); the route forwards model_kind="pipeline" to begin_load.
resp = client.post(
"/api/inference/images/load", json = {"model_path": "unsloth/Z-Image-Turbo-unsloth-bnb-4bit"}
)
assert resp.status_code == 200
backend = diffusion_module.get_diffusion_backend()
assert backend.last_load_kwargs["model_kind"] == "pipeline"
assert backend.last_load_kwargs.get("gguf_filename") is None
def test_generate_without_load_returns_409(client):

View file

@ -17,7 +17,26 @@ _STUDIO = Path(__file__).resolve().parents[2]
if str(_STUDIO) not in sys.path:
sys.path.insert(0, str(_STUDIO))
from install_sd_cpp_prebuilt import default_install_dir, resolve_release_asset # noqa: E402
import hashlib # noqa: E402
import io # noqa: E402
import json # noqa: E402
import urllib.error # noqa: E402
import zipfile # noqa: E402
import pytest # noqa: E402
import install_sd_cpp_prebuilt as sdmod # noqa: E402
from install_sd_cpp_prebuilt import ( # noqa: E402
DEFAULT_REPO,
DEFAULT_TAG,
_fetch_release,
_pinned_tag,
_repo,
_verify_sha256,
default_install_dir,
install,
resolve_release_asset,
)
# A real stable-diffusion.cpp latest-release asset list.
_ASSETS = [
@ -114,3 +133,127 @@ def test_default_install_dir_is_sibling_of_llama(monkeypatch):
d = default_install_dir()
assert d.name == "stable-diffusion.cpp"
assert d.parent.name == ".unsloth"
# ── version pin + source repo (reproducibility) ─────────────────────────────
def test_pinned_tag_default_and_override(monkeypatch):
monkeypatch.delenv("UNSLOTH_SD_CPP_TAG", raising = False)
assert _pinned_tag() == DEFAULT_TAG # pinned, not "latest"
monkeypatch.setenv("UNSLOTH_SD_CPP_TAG", "master-999-deadbee")
assert _pinned_tag() == "master-999-deadbee"
monkeypatch.setenv("UNSLOTH_SD_CPP_TAG", "") # explicit empty -> track latest
assert _pinned_tag() is None
def test_repo_default_and_mirror_override(monkeypatch):
monkeypatch.delenv("UNSLOTH_SD_CPP_REPO", raising = False)
assert _repo() == DEFAULT_REPO == "leejet/stable-diffusion.cpp"
monkeypatch.setenv("UNSLOTH_SD_CPP_REPO", "unslothai/stable-diffusion.cpp")
assert _repo() == "unslothai/stable-diffusion.cpp"
# ── sha256 integrity check ──────────────────────────────────────────────────
def test_verify_sha256_accepts_matching_digest(tmp_path):
f = tmp_path / "asset.zip"
f.write_bytes(b"hello sd-cli")
digest = "sha256:" + hashlib.sha256(b"hello sd-cli").hexdigest()
_verify_sha256(f, digest) # no raise
def test_verify_sha256_rejects_mismatch(tmp_path):
f = tmp_path / "asset.zip"
f.write_bytes(b"tampered")
bad = "sha256:" + hashlib.sha256(b"original").hexdigest()
with pytest.raises(RuntimeError, match = "sha256 mismatch"):
_verify_sha256(f, bad)
def test_verify_sha256_skips_when_absent_or_unknown(tmp_path):
f = tmp_path / "asset.zip"
f.write_bytes(b"x")
_verify_sha256(f, None) # no digest published -> warn + proceed (no raise)
_verify_sha256(f, "md5:abc") # unrecognised algo -> skip (no raise)
# ── _fetch_release: pinned-tag 404 -> latest fallback ───────────────────────
def test_fetch_release_falls_back_to_latest_on_404(monkeypatch):
calls: list[str] = []
class _Resp:
def __enter__(self):
return self
def __exit__(self, *a):
return False
def read(self):
return json.dumps({"tag_name": "latest-xyz", "assets": []}).encode()
def fake_urlopen(req, timeout = 30.0):
url = getattr(req, "full_url", req)
calls.append(url)
if "/tags/" in url:
raise urllib.error.HTTPError(url, 404, "not found", None, None)
return _Resp()
monkeypatch.setattr(sdmod.urllib.request, "urlopen", fake_urlopen)
rel = _fetch_release("gone-tag", repo = "leejet/stable-diffusion.cpp")
assert rel["tag_name"] == "latest-xyz"
assert any("/tags/gone-tag" in c for c in calls) and any(c.endswith("/latest") for c in calls)
def test_fetch_release_propagates_non_404(monkeypatch):
def fake_urlopen(req, timeout = 30.0):
url = getattr(req, "full_url", req)
raise urllib.error.HTTPError(url, 403, "rate limited", None, None)
monkeypatch.setattr(sdmod.urllib.request, "urlopen", fake_urlopen)
with pytest.raises(urllib.error.HTTPError):
_fetch_release("any-tag")
# ── install(): download -> verify -> extract -> locate (offline) ────────────
def _zip_with_sd_cli() -> bytes:
buf = io.BytesIO()
with zipfile.ZipFile(buf, "w") as zf:
zf.writestr("build/bin/sd-cli", b"#!/bin/sh\necho sd-cli\n")
return buf.getvalue()
def _stub_release(monkeypatch, *, zip_bytes: bytes, digest: str):
name = "sd-master-deadbee-bin-Linux-Ubuntu-24.04-x86_64.zip"
release = {
"tag_name": "master-1-deadbee",
"assets": [
{"name": name, "browser_download_url": f"https://example.invalid/{name}", "digest": digest}
],
}
monkeypatch.setattr(sdmod, "_fetch_release", lambda *a, **k: release)
monkeypatch.setattr(sdmod, "_download", lambda url, dest, **k: dest.write_bytes(zip_bytes))
monkeypatch.setattr(sdmod.platform, "system", lambda: "Linux")
monkeypatch.setattr(sdmod.platform, "machine", lambda: "x86_64")
return name
def test_install_downloads_verifies_extracts(tmp_path, monkeypatch):
zb = _zip_with_sd_cli()
name = _stub_release(monkeypatch, zip_bytes = zb, digest = "sha256:" + hashlib.sha256(zb).hexdigest())
sd_cli = install(install_dir = tmp_path)
assert sd_cli.name == "sd-cli" and sd_cli.is_file()
assert not (tmp_path / name).exists() # archive cleaned up after extract
def test_install_sha256_mismatch_raises_and_cleans_up(tmp_path, monkeypatch):
zb = _zip_with_sd_cli()
name = _stub_release(monkeypatch, zip_bytes = zb, digest = "sha256:" + "0" * 64)
with pytest.raises(RuntimeError, match = "sha256 mismatch"):
install(install_dir = tmp_path)
assert not (tmp_path / name).exists() # the finally: drops the bad archive

View file

@ -989,11 +989,17 @@ export const IMAGE_GEN_TASKS = [
// which the text-to-image backend rejects (mirrors its _EDIT_KEYWORDS). Hidden by
// id so they don't show in the Images picker only to 400 on load. Keeping the
// image-to-image task itself is required: some supported models (FLUX.2-klein)
// carry that tag too.
const IMAGE_EDIT_KEYWORDS = ["edit", "kontext", "inpaint"] as const;
// carry that tag too. "layered" hides Qwen-Image-Layered, which needs a dedicated
// pipeline (additional_t_cond) the standard text-to-image path can't drive.
const IMAGE_EDIT_KEYWORDS = ["edit", "kontext", "inpaint", "layered"] as const;
// Editing families the backend now SUPPORTS (their own Edit workflow) -- must not be
// hidden even though their id contains an edit keyword. Mirrors the backend's
// qwen-image-edit family in diffusion_families.py.
const SUPPORTED_EDIT_KEYWORDS = ["qwen-image-edit", "kontext"] as const;
function isImageEditModel(repoId: string | null | undefined): boolean {
if (!repoId) return false;
const id = repoId.toLowerCase();
if (SUPPORTED_EDIT_KEYWORDS.some((kw) => id.includes(kw))) return false;
return IMAGE_EDIT_KEYWORDS.some((kw) => id.includes(kw));
}
@ -1676,6 +1682,19 @@ export function HubModelPicker({
isChatSupported,
]);
// Curated non-GGUF (safetensors) models for the Images picker. The HF listing +
// Recommended gate only surface GGUF on a GPU host (isRecommendableFormat), so a
// bnb-4bit / fp8 safetensors model would never appear there. These curated entries
// (the non-GGUF ModelOptions passed in) are shown explicitly above the GGUF rows so
// the user can pick a full diffusers pipeline. Only the Images picker (task set)
// curates them; already-downloaded ones show under Downloaded instead.
const curatedSafetensorsRows = useMemo(() => {
if (!task) return [];
return models.filter(
(m) => m.isGguf === false && !downloadedSet.has(m.id.toLowerCase()),
);
}, [models, task, downloadedSet]);
// Per-row meta + VRAM badge from the recommended listing's own metadata.
const recommendedMeta = useMemo(() => {
const map = new Map<
@ -3244,6 +3263,30 @@ export function HubModelPicker({
{showRecommendedSection ? (
<>
{/* Curated safetensors models (full diffusers pipelines / single-file
fp8). Shown above the GGUF rows; clicking loads directly (no quant
expander), the same path as a non-GGUF Recommended row. */}
{curatedSafetensorsRows.map((m) => {
const optionKey = makeModelOptionKey("curated-safetensors", m.id);
return (
<div key={m.id}>
<ModelRow
label={m.id}
hideOwner={true}
downloaded={downloadedSet.has(m.id.toLowerCase())}
capabilities={capsById.get(m.id)}
meta={m.description ?? "Safetensors"}
selected={value === m.id}
optionProps={hubModelList.getOptionProps(
optionKey,
value === m.id,
)}
onClick={() => handleModelClick(m.id)}
gpuGb={gpu.available ? gpu.memoryTotalGb : undefined}
/>
</div>
);
})}
{recommendedSearch.isLoading &&
recommendedRows.length === 0 ? (
<div className="flex items-center gap-2 px-5 py-3">
@ -3252,7 +3295,8 @@ export function HubModelPicker({
Loading models
</span>
</div>
) : recommendedRows.length === 0 ? (
) : recommendedRows.length === 0 &&
curatedSafetensorsRows.length === 0 ? (
<div className="px-2.5 py-2 text-xs text-muted-foreground">
No models found.
</div>

View file

@ -11,7 +11,13 @@ export interface DiffusionStatus {
base_repo: string | null;
device: string | null;
dtype: string | null;
// Resolved load kind: "gguf" | "single_file" | "pipeline". Gates GGUF-only controls
// (the dense transformer_quant fast path only engages on gguf). Null when not loaded.
model_kind?: string | null;
cpu_offload: boolean;
// Image workflows the loaded family supports (drives tab gating): txt2img, img2img,
// inpaint. Absent/empty when nothing is loaded or on the native sd.cpp engine.
workflows?: string[];
}
export interface DiffusionGenerateProgress {
@ -32,11 +38,33 @@ export interface DiffusionLoadProgress {
export interface DiffusionLoadRequest {
model_path: string;
gguf_filename: string;
// Optional now: required for the gguf / single_file kinds, omitted for a full
// pipeline (a diffusers repo loaded via from_pretrained).
gguf_filename?: string;
// How to load the model (omit to auto-detect from gguf_filename): "gguf" (single-file
// GGUF transformer), "single_file" (single-file safetensors transformer, e.g. fp8), or
// "pipeline" (a full diffusers repo). Non-GGUF kinds are restricted to unsloth/* repos.
model_kind?: "gguf" | "single_file" | "pipeline";
base_repo?: string;
family_override?: string;
hf_token?: string;
cpu_offload?: boolean;
// Advanced (load-time) tuning. All optional; omit for the backend's auto defaults.
speed_mode?: "off" | "eager" | "default" | "max";
transformer_quant?: "auto" | "int8" | "fp8" | "nvfp4" | "mxfp8";
attention_backend?:
| "auto"
| "native"
| "cudnn"
| "flash"
| "flash2"
| "flash3"
| "flash4"
| "sage"
| "xformers"
| "aiter";
memory_mode?: "auto" | "fast" | "balanced" | "low_vram";
transformer_cache?: "off" | "fbcache";
}
export interface DiffusionGenerateRequest {
@ -48,6 +76,16 @@ export interface DiffusionGenerateRequest {
guidance?: number;
seed?: number;
batch_size?: number;
// Image-conditioned workflows. init_image alone = img2img; init_image + mask_image =
// inpaint. Base64 or data-URL. strength is the denoise amount (0 keeps source, 1 redraws).
init_image?: string;
mask_image?: string;
strength?: number;
// Upscale (hires fix): factor > 1 with an init_image enlarges the source and re-denoises
// it at low strength. Requires init_image; ignored for txt2img/inpaint/edit.
upscale?: number;
// Additional reference images for the FLUX.2 reference workflow, combined with init_image.
reference_images?: string[];
}
// A persisted image's full generation recipe (also embedded in the PNG).

File diff suppressed because it is too large Load diff

View file

@ -25,18 +25,40 @@ Usage:
from __future__ import annotations
import argparse
import hashlib
import json
import os
import platform
import shutil
import stat
import sys
import urllib.error
import urllib.request
import zipfile
from pathlib import Path
from typing import Optional, Sequence
REPO = "leejet/stable-diffusion.cpp"
RELEASES_API = f"https://api.github.com/repos/{REPO}/releases/latest"
# Default upstream source. Overridable with UNSLOTH_SD_CPP_REPO so a pinned unslothai
# mirror (built the same way as unslothai/llama.cpp's prebuilts) can be used without a
# code change once it exists; otherwise this falls back to leejet upstream.
DEFAULT_REPO = "leejet/stable-diffusion.cpp"
# Pinned release tag for REPRODUCIBILITY: "releases/latest" silently swaps the binary
# under users on every upstream push. Override with UNSLOTH_SD_CPP_TAG; set it empty to
# track latest. If the pinned tag is gone upstream, install falls back to latest.
DEFAULT_TAG = "master-737-3b6c9ca"
# Back-compat alias (some callers/tests import REPO).
REPO = DEFAULT_REPO
def _repo() -> str:
return (os.environ.get("UNSLOTH_SD_CPP_REPO") or DEFAULT_REPO).strip() or DEFAULT_REPO
def _pinned_tag() -> Optional[str]:
"""The release tag to install: env override, else the pinned default; '' = latest."""
val = os.environ.get("UNSLOTH_SD_CPP_TAG", DEFAULT_TAG).strip()
return val or None
# accelerator -> the token that must appear in a Linux/Windows asset name.
_LINUX_ACCEL_TOKEN = {"rocm": "rocm", "vulkan": "vulkan"}
@ -108,14 +130,59 @@ def resolve_release_asset(
return sel[0] if sel else None
def _fetch_latest_release(*, token: Optional[str] = None, timeout: float = 30.0) -> dict:
"""GET the latest-release JSON from GitHub (token optional, lifts rate limit)."""
req = urllib.request.Request(RELEASES_API, headers = {"Accept": "application/vnd.github+json"})
def _fetch_release(
tag: Optional[str] = None, *, repo: Optional[str] = None,
token: Optional[str] = None, timeout: float = 30.0,
) -> dict:
"""GET a release JSON from GitHub. With ``tag`` set, fetch that exact release (and fall
back to latest if the tag is gone upstream); otherwise fetch latest. ``token`` is
optional and lifts the API rate limit."""
repo = repo or _repo()
token = token or os.environ.get("GH_TOKEN") or os.environ.get("GITHUB_TOKEN")
if token:
req.add_header("Authorization", f"Bearer {token}")
with urllib.request.urlopen(req, timeout = timeout) as resp: # noqa: S310 (fixed https host)
return json.loads(resp.read().decode("utf-8"))
def _get(url: str) -> dict:
req = urllib.request.Request(url, headers = {"Accept": "application/vnd.github+json"})
if token:
req.add_header("Authorization", f"Bearer {token}")
with urllib.request.urlopen(req, timeout = timeout) as resp: # noqa: S310 (fixed https host)
return json.loads(resp.read().decode("utf-8"))
base = f"https://api.github.com/repos/{repo}/releases"
if tag:
try:
return _get(f"{base}/tags/{tag}")
except urllib.error.HTTPError as exc: # pinned tag removed upstream -> latest
if exc.code != 404:
raise
print(f"sd-cli: pinned tag {tag} not found on {repo}; falling back to latest", flush = True)
return _get(f"{base}/latest")
# Back-compat alias: the old name fetched latest.
def _fetch_latest_release(*, token: Optional[str] = None, timeout: float = 30.0) -> dict:
return _fetch_release(None, token = token, timeout = timeout)
def _verify_sha256(path: Path, expected_digest: Optional[str]) -> None:
"""Verify ``path`` against a GitHub asset ``digest`` ('sha256:<hex>'). Integrity check
against a corrupted/tampered download before we extract + execute the binary. When the
release publishes no digest (older releases), warn and proceed rather than hard-fail."""
if not expected_digest:
print(f"sd-cli: WARNING no digest for {path.name}; cannot verify integrity", flush = True)
return
algo, _, want = expected_digest.partition(":")
if algo.lower() != "sha256" or not want:
print(f"sd-cli: WARNING unrecognised digest {expected_digest!r}; skipping check", flush = True)
return
h = hashlib.sha256()
with open(path, "rb") as f:
for chunk in iter(lambda: f.read(1 << 20), b""):
h.update(chunk)
got = h.hexdigest()
if got != want.lower():
raise RuntimeError(
f"sha256 mismatch for {path.name}: expected {want.lower()}, got {got}"
)
def default_install_dir() -> Path:
@ -132,6 +199,14 @@ def _make_executable(path: Path) -> None:
path.chmod(mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
def _download(url: str, dest: Path, *, timeout: float = 300.0) -> None:
"""Stream a release asset to ``dest`` with a timeout. ``urlretrieve`` has no timeout,
so a stalled connection would hang the lazy first-load (ensure_sd_cpp_binary) forever.
Anonymous, matching the public release URL -- the API fetch carries any token."""
with urllib.request.urlopen(url, timeout = timeout) as resp, open(dest, "wb") as f: # noqa: S310
shutil.copyfileobj(resp, f)
def _locate_sd_cli(root: Path) -> Optional[Path]:
name = "sd-cli.exe" if sys.platform == "win32" else "sd-cli"
for p in root.rglob(name):
@ -152,7 +227,8 @@ def install(
build from source) or the archive has no ``sd-cli``.
"""
target = install_dir or default_install_dir()
release = _fetch_latest_release(token = token)
release = _fetch_release(_pinned_tag(), token = token)
print(f"sd-cli: source {_repo()} release {release.get('tag_name', '?')}", flush = True)
names = [a["name"] for a in release.get("assets", [])]
chosen = resolve_release_asset(
names,
@ -164,17 +240,24 @@ def install(
raise RuntimeError(
f"No prebuilt sd-cli for {platform.system()}/{platform.machine()} "
f"(accelerator={accelerator}). Build from source: "
f"https://github.com/{REPO}"
f"https://github.com/{_repo()}"
)
url = next(a["browser_download_url"] for a in release["assets"] if a["name"] == chosen)
asset = next(a for a in release["assets"] if a["name"] == chosen)
url = asset["browser_download_url"]
target.mkdir(parents = True, exist_ok = True)
archive = target / chosen
print(f"downloading {chosen} -> {archive}", flush = True)
urllib.request.urlretrieve(url, archive) # noqa: S310 (github release URL)
print("extracting ...", flush = True)
with zipfile.ZipFile(archive) as zf:
zf.extractall(target)
archive.unlink(missing_ok = True)
try:
_download(url, archive)
# Verify integrity BEFORE extracting + executing.
_verify_sha256(archive, asset.get("digest"))
print("extracting ...", flush = True)
with zipfile.ZipFile(archive) as zf:
zf.extractall(target)
finally:
# Always drop the archive: on a sha256 mismatch / corrupt zip / network error it
# must not linger (and a stale partial would defeat a later retry).
archive.unlink(missing_ok = True)
sd_cli = _locate_sd_cli(target)
if not sd_cli:
raise RuntimeError(f"archive {chosen} contained no sd-cli binary")
@ -196,7 +279,7 @@ def main(argv: Optional[list[str]] = None) -> int:
args = p.parse_args(argv)
if args.print_asset:
release = _fetch_latest_release()
release = _fetch_release(_pinned_tag())
names = [a["name"] for a in release.get("assets", [])]
chosen = resolve_release_asset(
names,