Spaces:
Running on Zero
Running on Zero
ci: ruff + pytest on push/pr (l1+l2, no gpu deps)
Browse filesAdd .github/workflows/ci.yml running ruff format, ruff check, and pytest
against a minimal dep set (no torch/diffsynth/realesrgan/controlnet_aux).
Fix all ruff format + lint violations found during CI replication (import
order, unused imports, EN-dash RUF001, RUF059 unused unpack variables).
Skip auto_device test when torch is absent so CI passes without a GPU.
- .github/workflows/ci.yml +40 -0
- app.py +57 -26
- backend.py +23 -15
- lora.py +5 -1
- models.py +22 -17
- modes.py +21 -9
- preprocessors.py +3 -0
- tests/test_backend.py +32 -17
- tests/test_lora.py +32 -17
- tests/test_models.py +4 -0
- tests/test_modes.py +61 -20
- tests/test_scaffold.py +13 -4
- tests/test_tooltips.py +20 -4
- tests/test_upscale.py +2 -1
- tooltips.py +17 -16
- upscale.py +4 -3
.github/workflows/ci.yml
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: CI
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
push:
|
| 5 |
+
branches: [main]
|
| 6 |
+
pull_request:
|
| 7 |
+
|
| 8 |
+
jobs:
|
| 9 |
+
lint-and-test:
|
| 10 |
+
runs-on: ubuntu-latest
|
| 11 |
+
steps:
|
| 12 |
+
- uses: actions/checkout@v4
|
| 13 |
+
|
| 14 |
+
- name: Set up Python
|
| 15 |
+
uses: actions/setup-python@v5
|
| 16 |
+
with:
|
| 17 |
+
python-version: "3.11"
|
| 18 |
+
|
| 19 |
+
- name: Cache pip
|
| 20 |
+
uses: actions/cache@v4
|
| 21 |
+
with:
|
| 22 |
+
path: ~/.cache/pip
|
| 23 |
+
key: pip-${{ runner.os }}-${{ hashFiles('requirements.txt') }}
|
| 24 |
+
|
| 25 |
+
- name: Install
|
| 26 |
+
run: |
|
| 27 |
+
python -m pip install -U pip
|
| 28 |
+
pip install ruff pytest pytest-mock pillow numpy gradio==5.50.0 safetensors opencv-python-headless
|
| 29 |
+
|
| 30 |
+
- name: Ruff format
|
| 31 |
+
run: ruff format --check .
|
| 32 |
+
|
| 33 |
+
- name: Ruff lint
|
| 34 |
+
run: ruff check .
|
| 35 |
+
|
| 36 |
+
- name: Pytest (L1+L2 — no GPU)
|
| 37 |
+
run: pytest -q --tb=short
|
| 38 |
+
env:
|
| 39 |
+
# Skip tests that need diffsynth / realesrgan / controlnet_aux installed
|
| 40 |
+
PYTEST_DISABLE_PLUGIN_AUTOLOAD: 1
|
app.py
CHANGED
|
@@ -3,12 +3,12 @@
|
|
| 3 |
On HF Spaces, ``_bootstrap`` runs once on import to mirror the read-only preload
|
| 4 |
cache into a writable tree.
|
| 5 |
"""
|
|
|
|
| 6 |
from __future__ import annotations
|
| 7 |
|
| 8 |
import os
|
| 9 |
import random
|
| 10 |
from pathlib import Path
|
| 11 |
-
from typing import Any
|
| 12 |
|
| 13 |
import gradio as gr
|
| 14 |
|
|
@@ -18,9 +18,9 @@ import models
|
|
| 18 |
import theme
|
| 19 |
import ui
|
| 20 |
|
| 21 |
-
|
| 22 |
# ----- HF Spaces bootstrap ---------------------------------------------------
|
| 23 |
|
|
|
|
| 24 |
def _bootstrap() -> None:
|
| 25 |
"""Mirror the preload_from_hub cache once, then point HF env at the mirror."""
|
| 26 |
if not models.on_spaces():
|
|
@@ -49,6 +49,7 @@ def get_backend() -> backend.ZImageStudioBackend:
|
|
| 49 |
|
| 50 |
# ----- Generation event handlers --------------------------------------------
|
| 51 |
|
|
|
|
| 52 |
def _maybe_random_seed(seed: int) -> int:
|
| 53 |
return seed if seed and seed > 0 else random.randint(1, 2_147_483_647)
|
| 54 |
|
|
@@ -64,46 +65,53 @@ def _coerce_lora(lora_path: str | None) -> Path | None:
|
|
| 64 |
def _esrgan_path() -> str:
|
| 65 |
"""Locate the preloaded RealESRGAN_x4plus.pth."""
|
| 66 |
from huggingface_hub import hf_hub_download
|
|
|
|
| 67 |
return hf_hub_download("xinntao/Real-ESRGAN", "RealESRGAN_x4plus.pth")
|
| 68 |
|
| 69 |
|
| 70 |
-
def on_t2i_generate(prompt, negative_prompt, model, steps, cfg,
|
| 71 |
-
width, height, seed, lora_path, lora_strength):
|
| 72 |
try:
|
| 73 |
lora_p = _coerce_lora(lora_path)
|
| 74 |
except lora_mod.LoRAValidationError as e:
|
| 75 |
raise gr.Error(str(e)) from e
|
| 76 |
|
| 77 |
params = dict(
|
| 78 |
-
prompt=prompt,
|
| 79 |
-
|
| 80 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
seed=_maybe_random_seed(int(seed)),
|
| 82 |
-
lora_path=lora_p,
|
|
|
|
| 83 |
)
|
| 84 |
image, meta = get_backend().generate(mode="t2i", params=params)
|
| 85 |
return image, meta
|
| 86 |
|
| 87 |
|
| 88 |
-
def on_controlnet_generate(prompt, input_image, preprocessor, controlnet_scale,
|
| 89 |
-
steps, seed, lora_path, lora_strength):
|
| 90 |
try:
|
| 91 |
lora_p = _coerce_lora(lora_path)
|
| 92 |
except lora_mod.LoRAValidationError as e:
|
| 93 |
raise gr.Error(str(e)) from e
|
| 94 |
|
| 95 |
params = dict(
|
| 96 |
-
prompt=prompt,
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
)
|
| 101 |
image, meta = get_backend().generate(mode="controlnet", params=params)
|
| 102 |
return image, meta
|
| 103 |
|
| 104 |
|
| 105 |
-
def on_upscale_generate(prompt, input_image, refine_steps, refine_denoise,
|
| 106 |
-
seed, lora_path, lora_strength):
|
| 107 |
try:
|
| 108 |
lora_p = _coerce_lora(lora_path)
|
| 109 |
except lora_mod.LoRAValidationError as e:
|
|
@@ -115,7 +123,8 @@ def on_upscale_generate(prompt, input_image, refine_steps, refine_denoise,
|
|
| 115 |
refine_steps=int(refine_steps),
|
| 116 |
refine_denoise=float(refine_denoise),
|
| 117 |
seed=_maybe_random_seed(int(seed)),
|
| 118 |
-
lora_path=lora_p,
|
|
|
|
| 119 |
esrgan_model_path=_esrgan_path(),
|
| 120 |
)
|
| 121 |
image, meta = get_backend().generate(mode="upscale", params=params)
|
|
@@ -169,9 +178,18 @@ def build_app() -> gr.Blocks:
|
|
| 169 |
t = ui.build_t2i_tab()
|
| 170 |
t["generate_btn"].click(
|
| 171 |
fn=on_t2i_generate,
|
| 172 |
-
inputs=[
|
| 173 |
-
|
| 174 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 175 |
outputs=[t["output_image"], t["output_meta"]],
|
| 176 |
)
|
| 177 |
|
|
@@ -179,9 +197,16 @@ def build_app() -> gr.Blocks:
|
|
| 179 |
c = ui.build_controlnet_tab()
|
| 180 |
c["generate_btn"].click(
|
| 181 |
fn=on_controlnet_generate,
|
| 182 |
-
inputs=[
|
| 183 |
-
|
| 184 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 185 |
outputs=[c["output_image"], c["output_meta"]],
|
| 186 |
)
|
| 187 |
|
|
@@ -189,9 +214,15 @@ def build_app() -> gr.Blocks:
|
|
| 189 |
u = ui.build_upscale_tab()
|
| 190 |
u["generate_btn"].click(
|
| 191 |
fn=on_upscale_generate,
|
| 192 |
-
inputs=[
|
| 193 |
-
|
| 194 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 195 |
outputs=[u["output_image"], u["output_meta"]],
|
| 196 |
)
|
| 197 |
return demo
|
|
|
|
| 3 |
On HF Spaces, ``_bootstrap`` runs once on import to mirror the read-only preload
|
| 4 |
cache into a writable tree.
|
| 5 |
"""
|
| 6 |
+
|
| 7 |
from __future__ import annotations
|
| 8 |
|
| 9 |
import os
|
| 10 |
import random
|
| 11 |
from pathlib import Path
|
|
|
|
| 12 |
|
| 13 |
import gradio as gr
|
| 14 |
|
|
|
|
| 18 |
import theme
|
| 19 |
import ui
|
| 20 |
|
|
|
|
| 21 |
# ----- HF Spaces bootstrap ---------------------------------------------------
|
| 22 |
|
| 23 |
+
|
| 24 |
def _bootstrap() -> None:
|
| 25 |
"""Mirror the preload_from_hub cache once, then point HF env at the mirror."""
|
| 26 |
if not models.on_spaces():
|
|
|
|
| 49 |
|
| 50 |
# ----- Generation event handlers --------------------------------------------
|
| 51 |
|
| 52 |
+
|
| 53 |
def _maybe_random_seed(seed: int) -> int:
|
| 54 |
return seed if seed and seed > 0 else random.randint(1, 2_147_483_647)
|
| 55 |
|
|
|
|
| 65 |
def _esrgan_path() -> str:
|
| 66 |
"""Locate the preloaded RealESRGAN_x4plus.pth."""
|
| 67 |
from huggingface_hub import hf_hub_download
|
| 68 |
+
|
| 69 |
return hf_hub_download("xinntao/Real-ESRGAN", "RealESRGAN_x4plus.pth")
|
| 70 |
|
| 71 |
|
| 72 |
+
def on_t2i_generate(prompt, negative_prompt, model, steps, cfg, width, height, seed, lora_path, lora_strength):
|
|
|
|
| 73 |
try:
|
| 74 |
lora_p = _coerce_lora(lora_path)
|
| 75 |
except lora_mod.LoRAValidationError as e:
|
| 76 |
raise gr.Error(str(e)) from e
|
| 77 |
|
| 78 |
params = dict(
|
| 79 |
+
prompt=prompt,
|
| 80 |
+
negative_prompt=negative_prompt or "",
|
| 81 |
+
model=model,
|
| 82 |
+
steps=int(steps),
|
| 83 |
+
cfg=float(cfg),
|
| 84 |
+
width=int(width),
|
| 85 |
+
height=int(height),
|
| 86 |
seed=_maybe_random_seed(int(seed)),
|
| 87 |
+
lora_path=lora_p,
|
| 88 |
+
lora_strength=float(lora_strength),
|
| 89 |
)
|
| 90 |
image, meta = get_backend().generate(mode="t2i", params=params)
|
| 91 |
return image, meta
|
| 92 |
|
| 93 |
|
| 94 |
+
def on_controlnet_generate(prompt, input_image, preprocessor, controlnet_scale, steps, seed, lora_path, lora_strength):
|
|
|
|
| 95 |
try:
|
| 96 |
lora_p = _coerce_lora(lora_path)
|
| 97 |
except lora_mod.LoRAValidationError as e:
|
| 98 |
raise gr.Error(str(e)) from e
|
| 99 |
|
| 100 |
params = dict(
|
| 101 |
+
prompt=prompt,
|
| 102 |
+
input_image=input_image,
|
| 103 |
+
preprocessor=preprocessor,
|
| 104 |
+
controlnet_scale=float(controlnet_scale),
|
| 105 |
+
steps=int(steps),
|
| 106 |
+
seed=_maybe_random_seed(int(seed)),
|
| 107 |
+
lora_path=lora_p,
|
| 108 |
+
lora_strength=float(lora_strength),
|
| 109 |
)
|
| 110 |
image, meta = get_backend().generate(mode="controlnet", params=params)
|
| 111 |
return image, meta
|
| 112 |
|
| 113 |
|
| 114 |
+
def on_upscale_generate(prompt, input_image, refine_steps, refine_denoise, seed, lora_path, lora_strength):
|
|
|
|
| 115 |
try:
|
| 116 |
lora_p = _coerce_lora(lora_path)
|
| 117 |
except lora_mod.LoRAValidationError as e:
|
|
|
|
| 123 |
refine_steps=int(refine_steps),
|
| 124 |
refine_denoise=float(refine_denoise),
|
| 125 |
seed=_maybe_random_seed(int(seed)),
|
| 126 |
+
lora_path=lora_p,
|
| 127 |
+
lora_strength=float(lora_strength),
|
| 128 |
esrgan_model_path=_esrgan_path(),
|
| 129 |
)
|
| 130 |
image, meta = get_backend().generate(mode="upscale", params=params)
|
|
|
|
| 178 |
t = ui.build_t2i_tab()
|
| 179 |
t["generate_btn"].click(
|
| 180 |
fn=on_t2i_generate,
|
| 181 |
+
inputs=[
|
| 182 |
+
t["prompt"],
|
| 183 |
+
t["negative_prompt"],
|
| 184 |
+
t["model_state"],
|
| 185 |
+
t["steps"],
|
| 186 |
+
t["cfg"],
|
| 187 |
+
t["width"],
|
| 188 |
+
t["height"],
|
| 189 |
+
t["seed"],
|
| 190 |
+
t["lora_path"],
|
| 191 |
+
t["lora_strength"],
|
| 192 |
+
],
|
| 193 |
outputs=[t["output_image"], t["output_meta"]],
|
| 194 |
)
|
| 195 |
|
|
|
|
| 197 |
c = ui.build_controlnet_tab()
|
| 198 |
c["generate_btn"].click(
|
| 199 |
fn=on_controlnet_generate,
|
| 200 |
+
inputs=[
|
| 201 |
+
c["prompt"],
|
| 202 |
+
c["input_image"],
|
| 203 |
+
c["preprocessor"],
|
| 204 |
+
c["controlnet_scale"],
|
| 205 |
+
c["steps"],
|
| 206 |
+
c["seed"],
|
| 207 |
+
c["lora_path"],
|
| 208 |
+
c["lora_strength"],
|
| 209 |
+
],
|
| 210 |
outputs=[c["output_image"], c["output_meta"]],
|
| 211 |
)
|
| 212 |
|
|
|
|
| 214 |
u = ui.build_upscale_tab()
|
| 215 |
u["generate_btn"].click(
|
| 216 |
fn=on_upscale_generate,
|
| 217 |
+
inputs=[
|
| 218 |
+
u["prompt"],
|
| 219 |
+
u["input_image"],
|
| 220 |
+
u["refine_steps"],
|
| 221 |
+
u["refine_denoise"],
|
| 222 |
+
u["seed"],
|
| 223 |
+
u["lora_path"],
|
| 224 |
+
u["lora_strength"],
|
| 225 |
+
],
|
| 226 |
outputs=[u["output_image"], u["output_meta"]],
|
| 227 |
)
|
| 228 |
return demo
|
backend.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
| 1 |
"""ZImageStudioBackend — wraps the DiffSynth pipeline; applies @spaces.GPU on HF Spaces."""
|
|
|
|
| 2 |
from __future__ import annotations
|
| 3 |
|
| 4 |
import os
|
|
@@ -12,17 +13,16 @@ except ImportError:
|
|
| 12 |
|
| 13 |
import modes
|
| 14 |
|
| 15 |
-
|
| 16 |
_BASE_DURATION_S: dict[str, int] = {
|
| 17 |
-
"t2i":
|
| 18 |
-
"controlnet": 30,
|
| 19 |
-
"upscale":
|
| 20 |
}
|
| 21 |
_PER_STEP_S: dict[tuple[str, str], float] = {
|
| 22 |
-
("t2i", "Base"):
|
| 23 |
("t2i", "Turbo"): 1.6,
|
| 24 |
("controlnet", "Turbo"): 2.0,
|
| 25 |
-
("upscale", "Turbo"):
|
| 26 |
}
|
| 27 |
|
| 28 |
|
|
@@ -51,8 +51,11 @@ def _identity(fn):
|
|
| 51 |
|
| 52 |
|
| 53 |
_ON_SPACES = bool(os.environ.get("SPACES_ZERO_GPU"))
|
| 54 |
-
_GPU =
|
| 55 |
-
|
|
|
|
|
|
|
|
|
|
| 56 |
|
| 57 |
|
| 58 |
def _build_pipeline() -> Any:
|
|
@@ -66,10 +69,14 @@ def _build_pipeline() -> Any:
|
|
| 66 |
vram_cfg: dict[str, Any] = {}
|
| 67 |
if device != "cpu":
|
| 68 |
vram_cfg = dict(
|
| 69 |
-
offload_dtype=torch.bfloat16,
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
)
|
| 74 |
|
| 75 |
pipe = ZImagePipeline.from_pretrained(
|
|
@@ -77,7 +84,8 @@ def _build_pipeline() -> Any:
|
|
| 77 |
device=device,
|
| 78 |
model_configs=models.build_diffsynth_configs(vram_cfg=vram_cfg),
|
| 79 |
tokenizer_config=models.build_diffsynth_configs(
|
| 80 |
-
(models.TOKENIZER_CONFIG,),
|
|
|
|
| 81 |
)[0],
|
| 82 |
vram_limit=models.vram_limit_for(device),
|
| 83 |
)
|
|
@@ -85,9 +93,9 @@ def _build_pipeline() -> Any:
|
|
| 85 |
|
| 86 |
|
| 87 |
_DISPATCH = {
|
| 88 |
-
"t2i":
|
| 89 |
"controlnet": modes.call_controlnet,
|
| 90 |
-
"upscale":
|
| 91 |
}
|
| 92 |
|
| 93 |
|
|
|
|
| 1 |
"""ZImageStudioBackend — wraps the DiffSynth pipeline; applies @spaces.GPU on HF Spaces."""
|
| 2 |
+
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
import os
|
|
|
|
| 13 |
|
| 14 |
import modes
|
| 15 |
|
|
|
|
| 16 |
_BASE_DURATION_S: dict[str, int] = {
|
| 17 |
+
"t2i": 20, # fixed setup + decode
|
| 18 |
+
"controlnet": 30, # + preprocessor + control patch
|
| 19 |
+
"upscale": 50, # + realesrgan pixel-space step
|
| 20 |
}
|
| 21 |
_PER_STEP_S: dict[tuple[str, str], float] = {
|
| 22 |
+
("t2i", "Base"): 2.4,
|
| 23 |
("t2i", "Turbo"): 1.6,
|
| 24 |
("controlnet", "Turbo"): 2.0,
|
| 25 |
+
("upscale", "Turbo"): 1.6,
|
| 26 |
}
|
| 27 |
|
| 28 |
|
|
|
|
| 51 |
|
| 52 |
|
| 53 |
_ON_SPACES = bool(os.environ.get("SPACES_ZERO_GPU"))
|
| 54 |
+
_GPU = (
|
| 55 |
+
spaces.GPU(duration=lambda *a, **kw: duration_for(*a[1:3], **kw))
|
| 56 |
+
if (spaces is not None and _ON_SPACES)
|
| 57 |
+
else _identity
|
| 58 |
+
)
|
| 59 |
|
| 60 |
|
| 61 |
def _build_pipeline() -> Any:
|
|
|
|
| 69 |
vram_cfg: dict[str, Any] = {}
|
| 70 |
if device != "cpu":
|
| 71 |
vram_cfg = dict(
|
| 72 |
+
offload_dtype=torch.bfloat16,
|
| 73 |
+
offload_device="cpu",
|
| 74 |
+
onload_dtype=torch.bfloat16,
|
| 75 |
+
onload_device="cpu",
|
| 76 |
+
preparing_dtype=torch.bfloat16,
|
| 77 |
+
preparing_device=device,
|
| 78 |
+
computation_dtype=torch.bfloat16,
|
| 79 |
+
computation_device=device,
|
| 80 |
)
|
| 81 |
|
| 82 |
pipe = ZImagePipeline.from_pretrained(
|
|
|
|
| 84 |
device=device,
|
| 85 |
model_configs=models.build_diffsynth_configs(vram_cfg=vram_cfg),
|
| 86 |
tokenizer_config=models.build_diffsynth_configs(
|
| 87 |
+
(models.TOKENIZER_CONFIG,),
|
| 88 |
+
vram_cfg=None,
|
| 89 |
)[0],
|
| 90 |
vram_limit=models.vram_limit_for(device),
|
| 91 |
)
|
|
|
|
| 93 |
|
| 94 |
|
| 95 |
_DISPATCH = {
|
| 96 |
+
"t2i": modes.call_t2i,
|
| 97 |
"controlnet": modes.call_controlnet,
|
| 98 |
+
"upscale": modes.call_upscale,
|
| 99 |
}
|
| 100 |
|
| 101 |
|
lora.py
CHANGED
|
@@ -1,12 +1,14 @@
|
|
| 1 |
"""LoRA file validation and apply/revert context manager."""
|
|
|
|
| 2 |
from __future__ import annotations
|
| 3 |
|
| 4 |
import json
|
| 5 |
import struct
|
|
|
|
| 6 |
from contextlib import contextmanager
|
| 7 |
from dataclasses import dataclass
|
| 8 |
from pathlib import Path
|
| 9 |
-
from typing import Any
|
| 10 |
|
| 11 |
ZIMAGE_LORA_PREFIXES = ("transformer.", "dit.", "model.transformer.")
|
| 12 |
|
|
@@ -97,6 +99,7 @@ def applied_lora(pipe: Any, path: Path | str | None, strength: float) -> Iterato
|
|
| 97 |
def _apply_lora_impl(pipe: Any, path: Path | str, strength: float) -> None:
|
| 98 |
"""Apply a LoRA to ``pipe.dit``. Imports DiffSynth lazily for testability."""
|
| 99 |
from diffsynth.utils.lora import merge_lora
|
|
|
|
| 100 |
merge_lora(pipe.dit, str(path), alpha=float(strength))
|
| 101 |
|
| 102 |
|
|
@@ -108,6 +111,7 @@ def _revert_lora_impl(pipe: Any) -> None:
|
|
| 108 |
"""
|
| 109 |
try:
|
| 110 |
from diffsynth.utils.lora import unmerge_lora
|
|
|
|
| 111 |
unmerge_lora(pipe.dit)
|
| 112 |
return
|
| 113 |
except ImportError:
|
|
|
|
| 1 |
"""LoRA file validation and apply/revert context manager."""
|
| 2 |
+
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
import json
|
| 6 |
import struct
|
| 7 |
+
from collections.abc import Iterator
|
| 8 |
from contextlib import contextmanager
|
| 9 |
from dataclasses import dataclass
|
| 10 |
from pathlib import Path
|
| 11 |
+
from typing import Any
|
| 12 |
|
| 13 |
ZIMAGE_LORA_PREFIXES = ("transformer.", "dit.", "model.transformer.")
|
| 14 |
|
|
|
|
| 99 |
def _apply_lora_impl(pipe: Any, path: Path | str, strength: float) -> None:
|
| 100 |
"""Apply a LoRA to ``pipe.dit``. Imports DiffSynth lazily for testability."""
|
| 101 |
from diffsynth.utils.lora import merge_lora
|
| 102 |
+
|
| 103 |
merge_lora(pipe.dit, str(path), alpha=float(strength))
|
| 104 |
|
| 105 |
|
|
|
|
| 111 |
"""
|
| 112 |
try:
|
| 113 |
from diffsynth.utils.lora import unmerge_lora
|
| 114 |
+
|
| 115 |
unmerge_lora(pipe.dit)
|
| 116 |
return
|
| 117 |
except ImportError:
|
models.py
CHANGED
|
@@ -1,8 +1,9 @@
|
|
| 1 |
"""Device autodetect, ZImagePipeline ModelConfig registry, and (Task 4) HF cache mirror."""
|
|
|
|
| 2 |
from __future__ import annotations
|
| 3 |
|
| 4 |
import os
|
| 5 |
-
from dataclasses import dataclass
|
| 6 |
from pathlib import Path
|
| 7 |
from typing import Any
|
| 8 |
|
|
@@ -17,6 +18,7 @@ def on_spaces() -> bool:
|
|
| 17 |
def auto_device() -> str:
|
| 18 |
"""Detect the best available compute device."""
|
| 19 |
import torch
|
|
|
|
| 20 |
if torch.cuda.is_available():
|
| 21 |
return "cuda"
|
| 22 |
if torch.backends.mps.is_available():
|
|
@@ -35,8 +37,9 @@ def vram_limit_for(device: str, free_gb: float | None = None) -> float:
|
|
| 35 |
return 0.0
|
| 36 |
if free_gb is None:
|
| 37 |
import torch
|
|
|
|
| 38 |
if device == "cuda":
|
| 39 |
-
free_gb = torch.cuda.mem_get_info()[1] / (1024
|
| 40 |
else: # mps
|
| 41 |
# torch.mps has no mem_get_info on most builds; fall back to a safe constant.
|
| 42 |
free_gb = 24.0
|
|
@@ -55,6 +58,7 @@ class ModelConfig:
|
|
| 55 |
``diffsynth.core.ModelConfig`` instance is built on demand by
|
| 56 |
:func:`build_diffsynth_configs`.
|
| 57 |
"""
|
|
|
|
| 58 |
model_id: str
|
| 59 |
origin_file_pattern: str
|
| 60 |
description: str = ""
|
|
@@ -62,23 +66,24 @@ class ModelConfig:
|
|
| 62 |
|
| 63 |
MODEL_CONFIGS: tuple[ModelConfig, ...] = (
|
| 64 |
# Base
|
| 65 |
-
ModelConfig("Tongyi-MAI/Z-Image", "transformer/*.safetensors",
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
ModelConfig(
|
| 70 |
-
|
|
|
|
| 71 |
# Turbo (transformer only — encoder + VAE come from the Z-Image entry above)
|
| 72 |
-
ModelConfig("Tongyi-MAI/Z-Image-Turbo", "transformer/*.safetensors",
|
| 73 |
-
"Z-Image-Turbo transformer (8 steps, cfg=1)"),
|
| 74 |
# ControlNet Union 2.1 (eager preload per spec; can move to lazy if RAM is tight)
|
| 75 |
-
ModelConfig(
|
| 76 |
-
|
| 77 |
-
|
|
|
|
|
|
|
| 78 |
)
|
| 79 |
|
| 80 |
-
TOKENIZER_CONFIG = ModelConfig("Tongyi-MAI/Z-Image", "tokenizer/",
|
| 81 |
-
"Qwen3-4B tokenizer")
|
| 82 |
|
| 83 |
|
| 84 |
def build_diffsynth_configs(
|
|
@@ -91,9 +96,9 @@ def build_diffsynth_configs(
|
|
| 91 |
block (offload_dtype, offload_device, etc.) that DiffSynth's low-VRAM examples use.
|
| 92 |
"""
|
| 93 |
from diffsynth.core import ModelConfig as DSConfig
|
|
|
|
| 94 |
return [
|
| 95 |
-
DSConfig(model_id=c.model_id, origin_file_pattern=c.origin_file_pattern, **(vram_cfg or {}))
|
| 96 |
-
for c in configs
|
| 97 |
]
|
| 98 |
|
| 99 |
|
|
|
|
| 1 |
"""Device autodetect, ZImagePipeline ModelConfig registry, and (Task 4) HF cache mirror."""
|
| 2 |
+
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
import os
|
| 6 |
+
from dataclasses import dataclass
|
| 7 |
from pathlib import Path
|
| 8 |
from typing import Any
|
| 9 |
|
|
|
|
| 18 |
def auto_device() -> str:
|
| 19 |
"""Detect the best available compute device."""
|
| 20 |
import torch
|
| 21 |
+
|
| 22 |
if torch.cuda.is_available():
|
| 23 |
return "cuda"
|
| 24 |
if torch.backends.mps.is_available():
|
|
|
|
| 37 |
return 0.0
|
| 38 |
if free_gb is None:
|
| 39 |
import torch
|
| 40 |
+
|
| 41 |
if device == "cuda":
|
| 42 |
+
free_gb = torch.cuda.mem_get_info()[1] / (1024**3)
|
| 43 |
else: # mps
|
| 44 |
# torch.mps has no mem_get_info on most builds; fall back to a safe constant.
|
| 45 |
free_gb = 24.0
|
|
|
|
| 58 |
``diffsynth.core.ModelConfig`` instance is built on demand by
|
| 59 |
:func:`build_diffsynth_configs`.
|
| 60 |
"""
|
| 61 |
+
|
| 62 |
model_id: str
|
| 63 |
origin_file_pattern: str
|
| 64 |
description: str = ""
|
|
|
|
| 66 |
|
| 67 |
MODEL_CONFIGS: tuple[ModelConfig, ...] = (
|
| 68 |
# Base
|
| 69 |
+
ModelConfig("Tongyi-MAI/Z-Image", "transformer/*.safetensors", "Z-Image base transformer (25 steps, cfg=4)"),
|
| 70 |
+
ModelConfig(
|
| 71 |
+
"Tongyi-MAI/Z-Image", "text_encoder/*.safetensors", "Qwen3-4B text encoder — shared between base + turbo"
|
| 72 |
+
),
|
| 73 |
+
ModelConfig(
|
| 74 |
+
"Tongyi-MAI/Z-Image", "vae/diffusion_pytorch_model.safetensors", "Flux-family VAE — shared between base + turbo"
|
| 75 |
+
),
|
| 76 |
# Turbo (transformer only — encoder + VAE come from the Z-Image entry above)
|
| 77 |
+
ModelConfig("Tongyi-MAI/Z-Image-Turbo", "transformer/*.safetensors", "Z-Image-Turbo transformer (8 steps, cfg=1)"),
|
|
|
|
| 78 |
# ControlNet Union 2.1 (eager preload per spec; can move to lazy if RAM is tight)
|
| 79 |
+
ModelConfig(
|
| 80 |
+
"PAI/Z-Image-Turbo-Fun-Controlnet-Union-2.1",
|
| 81 |
+
"Z-Image-Turbo-Fun-Controlnet-Union-2.1-8steps.safetensors",
|
| 82 |
+
"ControlNet Union 2.1 — canny/depth/pose",
|
| 83 |
+
),
|
| 84 |
)
|
| 85 |
|
| 86 |
+
TOKENIZER_CONFIG = ModelConfig("Tongyi-MAI/Z-Image", "tokenizer/", "Qwen3-4B tokenizer")
|
|
|
|
| 87 |
|
| 88 |
|
| 89 |
def build_diffsynth_configs(
|
|
|
|
| 96 |
block (offload_dtype, offload_device, etc.) that DiffSynth's low-VRAM examples use.
|
| 97 |
"""
|
| 98 |
from diffsynth.core import ModelConfig as DSConfig
|
| 99 |
+
|
| 100 |
return [
|
| 101 |
+
DSConfig(model_id=c.model_id, origin_file_pattern=c.origin_file_pattern, **(vram_cfg or {})) for c in configs
|
|
|
|
| 102 |
]
|
| 103 |
|
| 104 |
|
modes.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
| 1 |
"""Mode handlers — pure functions over a ZImagePipeline + params dict."""
|
|
|
|
| 2 |
from __future__ import annotations
|
| 3 |
|
| 4 |
from pathlib import Path
|
|
@@ -24,7 +25,7 @@ except ImportError:
|
|
| 24 |
class T2IParams(TypedDict, total=False):
|
| 25 |
prompt: str
|
| 26 |
negative_prompt: str
|
| 27 |
-
model: str
|
| 28 |
steps: int
|
| 29 |
cfg: float
|
| 30 |
width: int
|
|
@@ -66,9 +67,13 @@ def call_t2i(pipe: Any, params: T2IParams) -> tuple[Image.Image, dict[str, Any]]
|
|
| 66 |
image = pipe(**kwargs)
|
| 67 |
|
| 68 |
meta = dict(
|
| 69 |
-
mode="t2i",
|
| 70 |
-
|
| 71 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
lora=str(params.get("lora_path")) if params.get("lora_path") else None,
|
| 73 |
lora_strength=params.get("lora_strength", 0.0),
|
| 74 |
)
|
|
@@ -103,11 +108,15 @@ def call_controlnet(pipe: Any, params: dict[str, Any]) -> tuple[Image.Image, dic
|
|
| 103 |
image = pipe(**kwargs)
|
| 104 |
|
| 105 |
meta = dict(
|
| 106 |
-
mode="controlnet",
|
|
|
|
| 107 |
preprocessor=preproc_mode,
|
| 108 |
controlnet_scale=cn_input.scale,
|
| 109 |
-
steps=kwargs["num_inference_steps"],
|
| 110 |
-
|
|
|
|
|
|
|
|
|
|
| 111 |
lora=str(params.get("lora_path")) if params.get("lora_path") else None,
|
| 112 |
lora_strength=params.get("lora_strength", 0.0),
|
| 113 |
)
|
|
@@ -138,10 +147,13 @@ def call_upscale(pipe: Any, params: dict[str, Any]) -> tuple[Image.Image, dict[s
|
|
| 138 |
image = pipe(**kwargs)
|
| 139 |
|
| 140 |
meta = dict(
|
| 141 |
-
mode="upscale",
|
|
|
|
| 142 |
refine_steps=kwargs["num_inference_steps"],
|
| 143 |
refine_denoise=kwargs["denoising_strength"],
|
| 144 |
-
seed=kwargs["seed"],
|
|
|
|
|
|
|
| 145 |
lora=str(params.get("lora_path")) if params.get("lora_path") else None,
|
| 146 |
lora_strength=params.get("lora_strength", 0.0),
|
| 147 |
)
|
|
|
|
| 1 |
"""Mode handlers — pure functions over a ZImagePipeline + params dict."""
|
| 2 |
+
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
from pathlib import Path
|
|
|
|
| 25 |
class T2IParams(TypedDict, total=False):
|
| 26 |
prompt: str
|
| 27 |
negative_prompt: str
|
| 28 |
+
model: str # "Base" | "Turbo"
|
| 29 |
steps: int
|
| 30 |
cfg: float
|
| 31 |
width: int
|
|
|
|
| 67 |
image = pipe(**kwargs)
|
| 68 |
|
| 69 |
meta = dict(
|
| 70 |
+
mode="t2i",
|
| 71 |
+
model=model_name,
|
| 72 |
+
steps=kwargs["num_inference_steps"],
|
| 73 |
+
cfg=kwargs["cfg_scale"],
|
| 74 |
+
seed=kwargs["seed"],
|
| 75 |
+
width=kwargs["width"],
|
| 76 |
+
height=kwargs["height"],
|
| 77 |
lora=str(params.get("lora_path")) if params.get("lora_path") else None,
|
| 78 |
lora_strength=params.get("lora_strength", 0.0),
|
| 79 |
)
|
|
|
|
| 108 |
image = pipe(**kwargs)
|
| 109 |
|
| 110 |
meta = dict(
|
| 111 |
+
mode="controlnet",
|
| 112 |
+
model="Turbo",
|
| 113 |
preprocessor=preproc_mode,
|
| 114 |
controlnet_scale=cn_input.scale,
|
| 115 |
+
steps=kwargs["num_inference_steps"],
|
| 116 |
+
cfg=1.0,
|
| 117 |
+
seed=kwargs["seed"],
|
| 118 |
+
width=kwargs["width"],
|
| 119 |
+
height=kwargs["height"],
|
| 120 |
lora=str(params.get("lora_path")) if params.get("lora_path") else None,
|
| 121 |
lora_strength=params.get("lora_strength", 0.0),
|
| 122 |
)
|
|
|
|
| 147 |
image = pipe(**kwargs)
|
| 148 |
|
| 149 |
meta = dict(
|
| 150 |
+
mode="upscale",
|
| 151 |
+
model="Turbo",
|
| 152 |
refine_steps=kwargs["num_inference_steps"],
|
| 153 |
refine_denoise=kwargs["denoising_strength"],
|
| 154 |
+
seed=kwargs["seed"],
|
| 155 |
+
width=upscaled.size[0],
|
| 156 |
+
height=upscaled.size[1],
|
| 157 |
lora=str(params.get("lora_path")) if params.get("lora_path") else None,
|
| 158 |
lora_strength=params.get("lora_strength", 0.0),
|
| 159 |
)
|
preprocessors.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
| 1 |
"""ControlNet preprocessors — lazy imports so an unused mode pays no cost."""
|
|
|
|
| 2 |
from __future__ import annotations
|
| 3 |
|
| 4 |
from typing import Any
|
|
@@ -25,6 +26,7 @@ def run(mode: str, image: Image.Image | None) -> Image.Image:
|
|
| 25 |
def _run_canny(image: Image.Image) -> Image.Image:
|
| 26 |
import cv2
|
| 27 |
import numpy as np
|
|
|
|
| 28 |
arr = np.array(image.convert("RGB"))
|
| 29 |
gray = cv2.cvtColor(arr, cv2.COLOR_RGB2GRAY)
|
| 30 |
edges = cv2.Canny(gray, threshold1=100, threshold2=200)
|
|
@@ -55,5 +57,6 @@ def _get_processor(name: str) -> Any:
|
|
| 55 |
"""Lazy-init and cache a controlnet_aux Processor."""
|
| 56 |
if name not in _PROCESSOR_CACHE:
|
| 57 |
from controlnet_aux.processor import Processor
|
|
|
|
| 58 |
_PROCESSOR_CACHE[name] = Processor(name)
|
| 59 |
return _PROCESSOR_CACHE[name]
|
|
|
|
| 1 |
"""ControlNet preprocessors — lazy imports so an unused mode pays no cost."""
|
| 2 |
+
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
from typing import Any
|
|
|
|
| 26 |
def _run_canny(image: Image.Image) -> Image.Image:
|
| 27 |
import cv2
|
| 28 |
import numpy as np
|
| 29 |
+
|
| 30 |
arr = np.array(image.convert("RGB"))
|
| 31 |
gray = cv2.cvtColor(arr, cv2.COLOR_RGB2GRAY)
|
| 32 |
edges = cv2.Canny(gray, threshold1=100, threshold2=200)
|
|
|
|
| 57 |
"""Lazy-init and cache a controlnet_aux Processor."""
|
| 58 |
if name not in _PROCESSOR_CACHE:
|
| 59 |
from controlnet_aux.processor import Processor
|
| 60 |
+
|
| 61 |
_PROCESSOR_CACHE[name] = Processor(name)
|
| 62 |
return _PROCESSOR_CACHE[name]
|
tests/test_backend.py
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import backend
|
| 2 |
|
| 3 |
|
|
@@ -23,8 +28,9 @@ def test_duration_clamps_at_60():
|
|
| 23 |
|
| 24 |
def test_duration_multiplier_scales_up():
|
| 25 |
base = backend.duration_for(mode="t2i", params=dict(model="Turbo", steps=8, width=1024, height=1024))
|
| 26 |
-
retry = backend.duration_for(
|
| 27 |
-
|
|
|
|
| 28 |
assert retry > base
|
| 29 |
|
| 30 |
|
|
@@ -34,12 +40,6 @@ def test_duration_upscale_has_realesrgan_overhead():
|
|
| 34 |
assert upsc > t2i
|
| 35 |
|
| 36 |
|
| 37 |
-
from unittest.mock import MagicMock
|
| 38 |
-
|
| 39 |
-
import pytest
|
| 40 |
-
from PIL import Image
|
| 41 |
-
|
| 42 |
-
|
| 43 |
@pytest.fixture
|
| 44 |
def fake_backend(monkeypatch):
|
| 45 |
"""A ZImageStudioBackend whose constructor doesn't actually build a pipeline."""
|
|
@@ -54,9 +54,18 @@ def fake_backend(monkeypatch):
|
|
| 54 |
def test_backend_generate_routes_t2i(fake_backend):
|
| 55 |
img, meta = fake_backend.generate(
|
| 56 |
mode="t2i",
|
| 57 |
-
params=dict(
|
| 58 |
-
|
| 59 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
)
|
| 61 |
assert isinstance(img, Image.Image)
|
| 62 |
assert meta["mode"] == "t2i"
|
|
@@ -64,13 +73,19 @@ def test_backend_generate_routes_t2i(fake_backend):
|
|
| 64 |
|
| 65 |
|
| 66 |
def test_backend_generate_routes_controlnet(fake_backend, monkeypatch):
|
| 67 |
-
monkeypatch.setattr(backend.modes, "preprocessors",
|
| 68 |
-
|
| 69 |
-
img, meta = fake_backend.generate(
|
| 70 |
mode="controlnet",
|
| 71 |
-
params=dict(
|
| 72 |
-
|
| 73 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
)
|
| 75 |
assert meta["mode"] == "controlnet"
|
| 76 |
|
|
|
|
| 1 |
+
from unittest.mock import MagicMock
|
| 2 |
+
|
| 3 |
+
import pytest
|
| 4 |
+
from PIL import Image
|
| 5 |
+
|
| 6 |
import backend
|
| 7 |
|
| 8 |
|
|
|
|
| 28 |
|
| 29 |
def test_duration_multiplier_scales_up():
|
| 30 |
base = backend.duration_for(mode="t2i", params=dict(model="Turbo", steps=8, width=1024, height=1024))
|
| 31 |
+
retry = backend.duration_for(
|
| 32 |
+
mode="t2i", params=dict(model="Turbo", steps=8, width=1024, height=1024), multiplier=2.0
|
| 33 |
+
)
|
| 34 |
assert retry > base
|
| 35 |
|
| 36 |
|
|
|
|
| 40 |
assert upsc > t2i
|
| 41 |
|
| 42 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
@pytest.fixture
|
| 44 |
def fake_backend(monkeypatch):
|
| 45 |
"""A ZImageStudioBackend whose constructor doesn't actually build a pipeline."""
|
|
|
|
| 54 |
def test_backend_generate_routes_t2i(fake_backend):
|
| 55 |
img, meta = fake_backend.generate(
|
| 56 |
mode="t2i",
|
| 57 |
+
params=dict(
|
| 58 |
+
prompt="cat",
|
| 59 |
+
negative_prompt="",
|
| 60 |
+
model="Turbo",
|
| 61 |
+
steps=8,
|
| 62 |
+
cfg=1.0,
|
| 63 |
+
width=1024,
|
| 64 |
+
height=1024,
|
| 65 |
+
seed=42,
|
| 66 |
+
lora_path=None,
|
| 67 |
+
lora_strength=0.0,
|
| 68 |
+
),
|
| 69 |
)
|
| 70 |
assert isinstance(img, Image.Image)
|
| 71 |
assert meta["mode"] == "t2i"
|
|
|
|
| 73 |
|
| 74 |
|
| 75 |
def test_backend_generate_routes_controlnet(fake_backend, monkeypatch):
|
| 76 |
+
monkeypatch.setattr(backend.modes, "preprocessors", type("P", (), {"run": staticmethod(lambda m, i: i)}))
|
| 77 |
+
_img, meta = fake_backend.generate(
|
|
|
|
| 78 |
mode="controlnet",
|
| 79 |
+
params=dict(
|
| 80 |
+
prompt="cat",
|
| 81 |
+
input_image=Image.new("RGB", (64, 64)),
|
| 82 |
+
preprocessor="Canny",
|
| 83 |
+
controlnet_scale=1.0,
|
| 84 |
+
steps=9,
|
| 85 |
+
seed=0,
|
| 86 |
+
lora_path=None,
|
| 87 |
+
lora_strength=0.0,
|
| 88 |
+
),
|
| 89 |
)
|
| 90 |
assert meta["mode"] == "controlnet"
|
| 91 |
|
tests/test_lora.py
CHANGED
|
@@ -15,11 +15,14 @@ def _write_safetensors(path: Path, header: dict) -> None:
|
|
| 15 |
|
| 16 |
def test_sniff_valid_zimage_lora_returns_metadata(tmp_path):
|
| 17 |
p = tmp_path / "ok.safetensors"
|
| 18 |
-
_write_safetensors(
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
|
|
|
|
|
|
|
|
|
| 23 |
info = lora.sniff(p)
|
| 24 |
assert info.rank == 64
|
| 25 |
assert info.target == "transformer"
|
|
@@ -36,9 +39,12 @@ def test_sniff_rejects_non_safetensors(tmp_path):
|
|
| 36 |
|
| 37 |
def test_sniff_rejects_non_zimage_keys(tmp_path):
|
| 38 |
p = tmp_path / "wrong.safetensors"
|
| 39 |
-
_write_safetensors(
|
| 40 |
-
|
| 41 |
-
|
|
|
|
|
|
|
|
|
|
| 42 |
with pytest.raises(lora.LoRAValidationError) as exc:
|
| 43 |
lora.sniff(p)
|
| 44 |
msg = str(exc.value).lower()
|
|
@@ -47,23 +53,29 @@ def test_sniff_rejects_non_zimage_keys(tmp_path):
|
|
| 47 |
|
| 48 |
class _FakePipe:
|
| 49 |
"""Minimal stand-in for DiffSynth's ZImagePipeline.dit hook surface."""
|
|
|
|
| 50 |
def __init__(self):
|
| 51 |
-
self.applied = []
|
| 52 |
self.reverted = []
|
| 53 |
|
| 54 |
|
| 55 |
def test_applied_lora_calls_apply_then_revert(tmp_path, monkeypatch):
|
| 56 |
p = tmp_path / "ok.safetensors"
|
| 57 |
-
_write_safetensors(
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
|
|
|
|
|
|
|
|
|
| 61 |
pipe = _FakePipe()
|
| 62 |
|
| 63 |
def fake_apply(pipe, path, strength):
|
| 64 |
pipe.applied.append((str(path), strength))
|
|
|
|
| 65 |
def fake_revert(pipe):
|
| 66 |
pipe.reverted.append(True)
|
|
|
|
| 67 |
monkeypatch.setattr(lora, "_apply_lora_impl", fake_apply)
|
| 68 |
monkeypatch.setattr(lora, "_revert_lora_impl", fake_revert)
|
| 69 |
|
|
@@ -88,10 +100,13 @@ def test_applied_lora_with_none_is_a_noop(tmp_path, monkeypatch):
|
|
| 88 |
|
| 89 |
def test_applied_lora_reverts_on_exception(tmp_path, monkeypatch):
|
| 90 |
p = tmp_path / "ok.safetensors"
|
| 91 |
-
_write_safetensors(
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
|
|
|
|
|
|
|
|
|
| 95 |
pipe = _FakePipe()
|
| 96 |
monkeypatch.setattr(lora, "_apply_lora_impl", lambda pipe, p, s: pipe.applied.append((p, s)))
|
| 97 |
monkeypatch.setattr(lora, "_revert_lora_impl", lambda pipe: pipe.reverted.append(True))
|
|
|
|
| 15 |
|
| 16 |
def test_sniff_valid_zimage_lora_returns_metadata(tmp_path):
|
| 17 |
p = tmp_path / "ok.safetensors"
|
| 18 |
+
_write_safetensors(
|
| 19 |
+
p,
|
| 20 |
+
{
|
| 21 |
+
"transformer.layer1.lora_A.weight": {"dtype": "BF16", "shape": [64, 3840]},
|
| 22 |
+
"transformer.layer1.lora_B.weight": {"dtype": "BF16", "shape": [3840, 64]},
|
| 23 |
+
"__metadata__": {"rank": "64"},
|
| 24 |
+
},
|
| 25 |
+
)
|
| 26 |
info = lora.sniff(p)
|
| 27 |
assert info.rank == 64
|
| 28 |
assert info.target == "transformer"
|
|
|
|
| 39 |
|
| 40 |
def test_sniff_rejects_non_zimage_keys(tmp_path):
|
| 41 |
p = tmp_path / "wrong.safetensors"
|
| 42 |
+
_write_safetensors(
|
| 43 |
+
p,
|
| 44 |
+
{
|
| 45 |
+
"down_blocks.0.weight": {"dtype": "F32", "shape": [320, 320]},
|
| 46 |
+
},
|
| 47 |
+
)
|
| 48 |
with pytest.raises(lora.LoRAValidationError) as exc:
|
| 49 |
lora.sniff(p)
|
| 50 |
msg = str(exc.value).lower()
|
|
|
|
| 53 |
|
| 54 |
class _FakePipe:
|
| 55 |
"""Minimal stand-in for DiffSynth's ZImagePipeline.dit hook surface."""
|
| 56 |
+
|
| 57 |
def __init__(self):
|
| 58 |
+
self.applied = [] # list of (path, strength) tuples
|
| 59 |
self.reverted = []
|
| 60 |
|
| 61 |
|
| 62 |
def test_applied_lora_calls_apply_then_revert(tmp_path, monkeypatch):
|
| 63 |
p = tmp_path / "ok.safetensors"
|
| 64 |
+
_write_safetensors(
|
| 65 |
+
p,
|
| 66 |
+
{
|
| 67 |
+
"transformer.x.lora_A.weight": {"dtype": "BF16", "shape": [32, 3840]},
|
| 68 |
+
"transformer.x.lora_B.weight": {"dtype": "BF16", "shape": [3840, 32]},
|
| 69 |
+
},
|
| 70 |
+
)
|
| 71 |
pipe = _FakePipe()
|
| 72 |
|
| 73 |
def fake_apply(pipe, path, strength):
|
| 74 |
pipe.applied.append((str(path), strength))
|
| 75 |
+
|
| 76 |
def fake_revert(pipe):
|
| 77 |
pipe.reverted.append(True)
|
| 78 |
+
|
| 79 |
monkeypatch.setattr(lora, "_apply_lora_impl", fake_apply)
|
| 80 |
monkeypatch.setattr(lora, "_revert_lora_impl", fake_revert)
|
| 81 |
|
|
|
|
| 100 |
|
| 101 |
def test_applied_lora_reverts_on_exception(tmp_path, monkeypatch):
|
| 102 |
p = tmp_path / "ok.safetensors"
|
| 103 |
+
_write_safetensors(
|
| 104 |
+
p,
|
| 105 |
+
{
|
| 106 |
+
"transformer.x.lora_A.weight": {"dtype": "BF16", "shape": [16, 3840]},
|
| 107 |
+
"transformer.x.lora_B.weight": {"dtype": "BF16", "shape": [3840, 16]},
|
| 108 |
+
},
|
| 109 |
+
)
|
| 110 |
pipe = _FakePipe()
|
| 111 |
monkeypatch.setattr(lora, "_apply_lora_impl", lambda pipe, p, s: pipe.applied.append((p, s)))
|
| 112 |
monkeypatch.setattr(lora, "_revert_lora_impl", lambda pipe: pipe.reverted.append(True))
|
tests/test_models.py
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
|
|
| 1 |
import os
|
| 2 |
from unittest import mock
|
| 3 |
|
|
|
|
|
|
|
| 4 |
import models
|
| 5 |
|
| 6 |
|
|
|
|
| 7 |
def test_auto_device_returns_cuda_or_mps_or_cpu():
|
| 8 |
dev = models.auto_device()
|
| 9 |
assert dev in ("cuda", "mps", "cpu")
|
|
|
|
| 1 |
+
import importlib
|
| 2 |
import os
|
| 3 |
from unittest import mock
|
| 4 |
|
| 5 |
+
import pytest
|
| 6 |
+
|
| 7 |
import models
|
| 8 |
|
| 9 |
|
| 10 |
+
@pytest.mark.skipif(importlib.util.find_spec("torch") is None, reason="torch not installed")
|
| 11 |
def test_auto_device_returns_cuda_or_mps_or_cpu():
|
| 12 |
dev = models.auto_device()
|
| 13 |
assert dev in ("cuda", "mps", "cpu")
|
tests/test_modes.py
CHANGED
|
@@ -23,10 +23,13 @@ def test_t2i_turbo_builds_minimal_call(fake_pipe):
|
|
| 23 |
prompt="a cat",
|
| 24 |
negative_prompt="",
|
| 25 |
model="Turbo",
|
| 26 |
-
steps=8,
|
| 27 |
-
|
|
|
|
|
|
|
| 28 |
seed=42,
|
| 29 |
-
lora_path=None,
|
|
|
|
| 30 |
),
|
| 31 |
)
|
| 32 |
fake_pipe.assert_called_once()
|
|
@@ -47,10 +50,16 @@ def test_t2i_base_passes_negative_prompt_and_cfg4(fake_pipe):
|
|
| 47 |
modes.call_t2i(
|
| 48 |
fake_pipe,
|
| 49 |
params=dict(
|
| 50 |
-
prompt="a cat",
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
),
|
| 55 |
)
|
| 56 |
kwargs = fake_pipe.call_args.kwargs
|
|
@@ -62,8 +71,18 @@ def test_t2i_base_passes_negative_prompt_and_cfg4(fake_pipe):
|
|
| 62 |
def test_t2i_swaps_transformer_via_model_pool(fake_pipe):
|
| 63 |
modes.call_t2i(
|
| 64 |
fake_pipe,
|
| 65 |
-
params=dict(
|
| 66 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
)
|
| 68 |
fake_pipe.model_pool.fetch_model.assert_called()
|
| 69 |
call = fake_pipe.model_pool.fetch_model.call_args
|
|
@@ -72,13 +91,15 @@ def test_t2i_swaps_transformer_via_model_pool(fake_pipe):
|
|
| 72 |
|
| 73 |
def test_controlnet_calls_preprocessor_then_pipeline(fake_pipe, monkeypatch):
|
| 74 |
canny_called = []
|
|
|
|
| 75 |
def fake_run(mode, img):
|
| 76 |
canny_called.append((mode, img.size))
|
| 77 |
return img # passthrough for test
|
|
|
|
| 78 |
monkeypatch.setattr(modes, "preprocessors", type("P", (), {"run": staticmethod(fake_run)}))
|
| 79 |
|
| 80 |
input_image = Image.new("RGB", (1024, 1024))
|
| 81 |
-
|
| 82 |
fake_pipe,
|
| 83 |
params=dict(
|
| 84 |
prompt="cinematic portrait",
|
|
@@ -87,7 +108,8 @@ def test_controlnet_calls_preprocessor_then_pipeline(fake_pipe, monkeypatch):
|
|
| 87 |
controlnet_scale=1.0,
|
| 88 |
steps=9,
|
| 89 |
seed=42,
|
| 90 |
-
lora_path=None,
|
|
|
|
| 91 |
),
|
| 92 |
)
|
| 93 |
|
|
@@ -106,22 +128,31 @@ def test_controlnet_rejects_missing_input_image(fake_pipe):
|
|
| 106 |
with pytest.raises(ValueError):
|
| 107 |
modes.call_controlnet(
|
| 108 |
fake_pipe,
|
| 109 |
-
params=dict(
|
| 110 |
-
|
| 111 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
)
|
| 113 |
|
| 114 |
|
| 115 |
def test_upscale_runs_realesrgan_then_pipeline(fake_pipe, monkeypatch):
|
| 116 |
calls = {"upscale": None}
|
|
|
|
| 117 |
def fake_2x(img, model_path):
|
| 118 |
calls["upscale"] = (img.size, str(model_path))
|
| 119 |
w, h = img.size
|
| 120 |
return img.resize((w * 2, h * 2), Image.LANCZOS)
|
|
|
|
| 121 |
monkeypatch.setattr(modes, "upscale", type("U", (), {"realesrgan_2x": staticmethod(fake_2x)}))
|
| 122 |
|
| 123 |
input_image = Image.new("RGB", (512, 512))
|
| 124 |
-
|
| 125 |
fake_pipe,
|
| 126 |
params=dict(
|
| 127 |
prompt="masterpiece, 8k",
|
|
@@ -129,7 +160,8 @@ def test_upscale_runs_realesrgan_then_pipeline(fake_pipe, monkeypatch):
|
|
| 129 |
refine_steps=5,
|
| 130 |
refine_denoise=0.33,
|
| 131 |
seed=42,
|
| 132 |
-
lora_path=None,
|
|
|
|
| 133 |
esrgan_model_path="/fake/path/RealESRGAN_x4plus.pth",
|
| 134 |
),
|
| 135 |
)
|
|
@@ -145,7 +177,16 @@ def test_upscale_runs_realesrgan_then_pipeline(fake_pipe, monkeypatch):
|
|
| 145 |
|
| 146 |
def test_upscale_rejects_missing_image(fake_pipe):
|
| 147 |
with pytest.raises(ValueError):
|
| 148 |
-
modes.call_upscale(
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
prompt="a cat",
|
| 24 |
negative_prompt="",
|
| 25 |
model="Turbo",
|
| 26 |
+
steps=8,
|
| 27 |
+
cfg=1.0,
|
| 28 |
+
width=1024,
|
| 29 |
+
height=1024,
|
| 30 |
seed=42,
|
| 31 |
+
lora_path=None,
|
| 32 |
+
lora_strength=0.0,
|
| 33 |
),
|
| 34 |
)
|
| 35 |
fake_pipe.assert_called_once()
|
|
|
|
| 50 |
modes.call_t2i(
|
| 51 |
fake_pipe,
|
| 52 |
params=dict(
|
| 53 |
+
prompt="a cat",
|
| 54 |
+
negative_prompt="blurry, lowres",
|
| 55 |
+
model="Base",
|
| 56 |
+
steps=25,
|
| 57 |
+
cfg=4.0,
|
| 58 |
+
width=1024,
|
| 59 |
+
height=1024,
|
| 60 |
+
seed=42,
|
| 61 |
+
lora_path=None,
|
| 62 |
+
lora_strength=0.0,
|
| 63 |
),
|
| 64 |
)
|
| 65 |
kwargs = fake_pipe.call_args.kwargs
|
|
|
|
| 71 |
def test_t2i_swaps_transformer_via_model_pool(fake_pipe):
|
| 72 |
modes.call_t2i(
|
| 73 |
fake_pipe,
|
| 74 |
+
params=dict(
|
| 75 |
+
prompt="x",
|
| 76 |
+
negative_prompt="",
|
| 77 |
+
model="Base",
|
| 78 |
+
steps=25,
|
| 79 |
+
cfg=4.0,
|
| 80 |
+
width=1024,
|
| 81 |
+
height=1024,
|
| 82 |
+
seed=0,
|
| 83 |
+
lora_path=None,
|
| 84 |
+
lora_strength=0.0,
|
| 85 |
+
),
|
| 86 |
)
|
| 87 |
fake_pipe.model_pool.fetch_model.assert_called()
|
| 88 |
call = fake_pipe.model_pool.fetch_model.call_args
|
|
|
|
| 91 |
|
| 92 |
def test_controlnet_calls_preprocessor_then_pipeline(fake_pipe, monkeypatch):
|
| 93 |
canny_called = []
|
| 94 |
+
|
| 95 |
def fake_run(mode, img):
|
| 96 |
canny_called.append((mode, img.size))
|
| 97 |
return img # passthrough for test
|
| 98 |
+
|
| 99 |
monkeypatch.setattr(modes, "preprocessors", type("P", (), {"run": staticmethod(fake_run)}))
|
| 100 |
|
| 101 |
input_image = Image.new("RGB", (1024, 1024))
|
| 102 |
+
_out, meta = modes.call_controlnet(
|
| 103 |
fake_pipe,
|
| 104 |
params=dict(
|
| 105 |
prompt="cinematic portrait",
|
|
|
|
| 108 |
controlnet_scale=1.0,
|
| 109 |
steps=9,
|
| 110 |
seed=42,
|
| 111 |
+
lora_path=None,
|
| 112 |
+
lora_strength=0.0,
|
| 113 |
),
|
| 114 |
)
|
| 115 |
|
|
|
|
| 128 |
with pytest.raises(ValueError):
|
| 129 |
modes.call_controlnet(
|
| 130 |
fake_pipe,
|
| 131 |
+
params=dict(
|
| 132 |
+
prompt="x",
|
| 133 |
+
input_image=None,
|
| 134 |
+
preprocessor="Canny",
|
| 135 |
+
controlnet_scale=1.0,
|
| 136 |
+
steps=9,
|
| 137 |
+
seed=0,
|
| 138 |
+
lora_path=None,
|
| 139 |
+
lora_strength=0.0,
|
| 140 |
+
),
|
| 141 |
)
|
| 142 |
|
| 143 |
|
| 144 |
def test_upscale_runs_realesrgan_then_pipeline(fake_pipe, monkeypatch):
|
| 145 |
calls = {"upscale": None}
|
| 146 |
+
|
| 147 |
def fake_2x(img, model_path):
|
| 148 |
calls["upscale"] = (img.size, str(model_path))
|
| 149 |
w, h = img.size
|
| 150 |
return img.resize((w * 2, h * 2), Image.LANCZOS)
|
| 151 |
+
|
| 152 |
monkeypatch.setattr(modes, "upscale", type("U", (), {"realesrgan_2x": staticmethod(fake_2x)}))
|
| 153 |
|
| 154 |
input_image = Image.new("RGB", (512, 512))
|
| 155 |
+
_out, meta = modes.call_upscale(
|
| 156 |
fake_pipe,
|
| 157 |
params=dict(
|
| 158 |
prompt="masterpiece, 8k",
|
|
|
|
| 160 |
refine_steps=5,
|
| 161 |
refine_denoise=0.33,
|
| 162 |
seed=42,
|
| 163 |
+
lora_path=None,
|
| 164 |
+
lora_strength=0.0,
|
| 165 |
esrgan_model_path="/fake/path/RealESRGAN_x4plus.pth",
|
| 166 |
),
|
| 167 |
)
|
|
|
|
| 177 |
|
| 178 |
def test_upscale_rejects_missing_image(fake_pipe):
|
| 179 |
with pytest.raises(ValueError):
|
| 180 |
+
modes.call_upscale(
|
| 181 |
+
fake_pipe,
|
| 182 |
+
params=dict(
|
| 183 |
+
prompt="x",
|
| 184 |
+
input_image=None,
|
| 185 |
+
refine_steps=5,
|
| 186 |
+
refine_denoise=0.33,
|
| 187 |
+
seed=0,
|
| 188 |
+
lora_path=None,
|
| 189 |
+
lora_strength=0.0,
|
| 190 |
+
esrgan_model_path="/fake.pth",
|
| 191 |
+
),
|
| 192 |
+
)
|
tests/test_scaffold.py
CHANGED
|
@@ -1,26 +1,35 @@
|
|
| 1 |
from pathlib import Path
|
| 2 |
-
import re
|
| 3 |
|
| 4 |
REPO = Path(__file__).resolve().parents[1]
|
| 5 |
|
|
|
|
| 6 |
def test_required_files_exist():
|
| 7 |
for rel in [
|
| 8 |
-
"pyproject.toml",
|
| 9 |
-
"
|
| 10 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
]:
|
| 12 |
assert (REPO / rel).exists(), f"missing {rel}"
|
| 13 |
|
|
|
|
| 14 |
def test_pyproject_targets_py311():
|
| 15 |
text = (REPO / "pyproject.toml").read_text()
|
| 16 |
assert "python = " not in text # not poetry
|
| 17 |
assert "py311" in text # ruff target-version
|
| 18 |
|
|
|
|
| 19 |
def test_requirements_has_core_deps():
|
| 20 |
text = (REPO / "requirements.txt").read_text().lower()
|
| 21 |
for dep in ["diffsynth-studio", "gradio", "spaces", "controlnet-aux", "torch", "safetensors", "ruff", "pytest"]:
|
| 22 |
assert dep in text, f"missing dep: {dep}"
|
| 23 |
|
|
|
|
| 24 |
def test_license_is_mit():
|
| 25 |
text = (REPO / "LICENSE").read_text()
|
| 26 |
assert "MIT License" in text
|
|
|
|
| 1 |
from pathlib import Path
|
|
|
|
| 2 |
|
| 3 |
REPO = Path(__file__).resolve().parents[1]
|
| 4 |
|
| 5 |
+
|
| 6 |
def test_required_files_exist():
|
| 7 |
for rel in [
|
| 8 |
+
"pyproject.toml",
|
| 9 |
+
"requirements.txt",
|
| 10 |
+
"setup.sh",
|
| 11 |
+
"LICENSE",
|
| 12 |
+
"CLAUDE.md",
|
| 13 |
+
"README.md",
|
| 14 |
+
".gitignore",
|
| 15 |
+
"tests/__init__.py",
|
| 16 |
+
"tests/conftest.py",
|
| 17 |
]:
|
| 18 |
assert (REPO / rel).exists(), f"missing {rel}"
|
| 19 |
|
| 20 |
+
|
| 21 |
def test_pyproject_targets_py311():
|
| 22 |
text = (REPO / "pyproject.toml").read_text()
|
| 23 |
assert "python = " not in text # not poetry
|
| 24 |
assert "py311" in text # ruff target-version
|
| 25 |
|
| 26 |
+
|
| 27 |
def test_requirements_has_core_deps():
|
| 28 |
text = (REPO / "requirements.txt").read_text().lower()
|
| 29 |
for dep in ["diffsynth-studio", "gradio", "spaces", "controlnet-aux", "torch", "safetensors", "ruff", "pytest"]:
|
| 30 |
assert dep in text, f"missing dep: {dep}"
|
| 31 |
|
| 32 |
+
|
| 33 |
def test_license_is_mit():
|
| 34 |
text = (REPO / "LICENSE").read_text()
|
| 35 |
assert "MIT License" in text
|
tests/test_tooltips.py
CHANGED
|
@@ -1,19 +1,35 @@
|
|
| 1 |
import tooltips
|
| 2 |
|
| 3 |
REQUIRED_KEYS = {
|
| 4 |
-
"prompt",
|
| 5 |
-
"
|
| 6 |
-
"
|
| 7 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
}
|
| 9 |
|
|
|
|
| 10 |
def test_tooltips_has_all_required_keys():
|
| 11 |
assert REQUIRED_KEYS <= set(tooltips.TOOLTIPS)
|
| 12 |
|
|
|
|
| 13 |
def test_tooltips_values_are_non_empty_strings():
|
| 14 |
for key, val in tooltips.TOOLTIPS.items():
|
| 15 |
assert isinstance(val, str) and val.strip(), f"{key} is empty or non-string"
|
| 16 |
|
|
|
|
| 17 |
def test_tooltips_values_are_short_enough_for_a_tooltip():
|
| 18 |
for key, val in tooltips.TOOLTIPS.items():
|
| 19 |
assert len(val) <= 200, f"{key} is too long for a tooltip ({len(val)} chars)"
|
|
|
|
| 1 |
import tooltips
|
| 2 |
|
| 3 |
REQUIRED_KEYS = {
|
| 4 |
+
"prompt",
|
| 5 |
+
"negative_prompt",
|
| 6 |
+
"model",
|
| 7 |
+
"lora",
|
| 8 |
+
"lora_strength",
|
| 9 |
+
"steps",
|
| 10 |
+
"cfg",
|
| 11 |
+
"width",
|
| 12 |
+
"height",
|
| 13 |
+
"seed",
|
| 14 |
+
"controlnet_image",
|
| 15 |
+
"controlnet_preprocessor",
|
| 16 |
+
"controlnet_scale",
|
| 17 |
+
"upscale_image",
|
| 18 |
+
"refine_steps",
|
| 19 |
+
"refine_denoise",
|
| 20 |
+
"output",
|
| 21 |
}
|
| 22 |
|
| 23 |
+
|
| 24 |
def test_tooltips_has_all_required_keys():
|
| 25 |
assert REQUIRED_KEYS <= set(tooltips.TOOLTIPS)
|
| 26 |
|
| 27 |
+
|
| 28 |
def test_tooltips_values_are_non_empty_strings():
|
| 29 |
for key, val in tooltips.TOOLTIPS.items():
|
| 30 |
assert isinstance(val, str) and val.strip(), f"{key} is empty or non-string"
|
| 31 |
|
| 32 |
+
|
| 33 |
def test_tooltips_values_are_short_enough_for_a_tooltip():
|
| 34 |
for key, val in tooltips.TOOLTIPS.items():
|
| 35 |
assert len(val) <= 200, f"{key} is too long for a tooltip ({len(val)} chars)"
|
tests/test_upscale.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
| 1 |
-
from unittest import mock
|
| 2 |
import pytest
|
| 3 |
from PIL import Image
|
| 4 |
|
|
@@ -12,9 +11,11 @@ def small_image():
|
|
| 12 |
|
| 13 |
def test_realesrgan_2x_produces_2x_image(small_image, monkeypatch):
|
| 14 |
"""RealESRGAN runs 4x then we scale down 0.5 → net 2x."""
|
|
|
|
| 15 |
def fake_run_4x(_model_path, image):
|
| 16 |
w, h = image.size
|
| 17 |
return image.resize((w * 4, h * 4), Image.LANCZOS)
|
|
|
|
| 18 |
monkeypatch.setattr(upscale, "_realesrgan_4x", fake_run_4x)
|
| 19 |
|
| 20 |
out = upscale.realesrgan_2x(small_image, model_path="/dev/null")
|
|
|
|
|
|
|
| 1 |
import pytest
|
| 2 |
from PIL import Image
|
| 3 |
|
|
|
|
| 11 |
|
| 12 |
def test_realesrgan_2x_produces_2x_image(small_image, monkeypatch):
|
| 13 |
"""RealESRGAN runs 4x then we scale down 0.5 → net 2x."""
|
| 14 |
+
|
| 15 |
def fake_run_4x(_model_path, image):
|
| 16 |
w, h = image.size
|
| 17 |
return image.resize((w * 4, h * 4), Image.LANCZOS)
|
| 18 |
+
|
| 19 |
monkeypatch.setattr(upscale, "_realesrgan_4x", fake_run_4x)
|
| 20 |
|
| 21 |
out = upscale.realesrgan_2x(small_image, model_path="/dev/null")
|
tooltips.py
CHANGED
|
@@ -3,24 +3,25 @@
|
|
| 3 |
Kept separate from ``ui.py`` so copy edits don't touch component wiring. Every
|
| 4 |
key here MUST be referenced from a labeled component in ``ui.py`` (and vice versa).
|
| 5 |
"""
|
|
|
|
| 6 |
from __future__ import annotations
|
| 7 |
|
| 8 |
TOOLTIPS: dict[str, str] = {
|
| 9 |
-
"prompt":
|
| 10 |
-
"negative_prompt":
|
| 11 |
-
"model":
|
| 12 |
-
"lora":
|
| 13 |
-
"lora_strength":
|
| 14 |
-
"steps":
|
| 15 |
-
"cfg":
|
| 16 |
-
"width":
|
| 17 |
-
"height":
|
| 18 |
-
"seed":
|
| 19 |
-
"controlnet_image":
|
| 20 |
"controlnet_preprocessor": "Canny = edges, Depth = depth map, Pose = body pose, Pre-processed = use image as-is.",
|
| 21 |
-
"controlnet_scale":
|
| 22 |
-
"upscale_image":
|
| 23 |
-
"refine_steps":
|
| 24 |
-
"refine_denoise":
|
| 25 |
-
"output":
|
| 26 |
}
|
|
|
|
| 3 |
Kept separate from ``ui.py`` so copy edits don't touch component wiring. Every
|
| 4 |
key here MUST be referenced from a labeled component in ``ui.py`` (and vice versa).
|
| 5 |
"""
|
| 6 |
+
|
| 7 |
from __future__ import annotations
|
| 8 |
|
| 9 |
TOOLTIPS: dict[str, str] = {
|
| 10 |
+
"prompt": "What to generate. Be specific: subject, style, lighting, camera angle.",
|
| 11 |
+
"negative_prompt": "What to avoid (Base only). e.g. 'blurry, low quality, distorted'.",
|
| 12 |
+
"model": "Base = 25 steps, higher quality. Turbo = 8 steps, fast.",
|
| 13 |
+
"lora": "Optional .safetensors LoRA file. Trained on Z-Image base or turbo.",
|
| 14 |
+
"lora_strength": "LoRA influence. 0.6-1.0 typical. Higher = more LoRA, less base model.",
|
| 15 |
+
"steps": "Denoising steps. Turbo: 6-10. Base: 20-30. More = better detail, slower.",
|
| 16 |
+
"cfg": "Classifier-free guidance. Turbo: locked at 1.0. Base: 3-5 typical.",
|
| 17 |
+
"width": "Output width in pixels. Multiples of 64. Higher = more memory.",
|
| 18 |
+
"height": "Output height in pixels. Multiples of 64.",
|
| 19 |
+
"seed": "0 = random each run. Pin a number to reproduce an image exactly.",
|
| 20 |
+
"controlnet_image": "Control image — the structural reference for the output.",
|
| 21 |
"controlnet_preprocessor": "Canny = edges, Depth = depth map, Pose = body pose, Pre-processed = use image as-is.",
|
| 22 |
+
"controlnet_scale": "How strongly the control image guides the output. 0.6-1.2 typical.",
|
| 23 |
+
"upscale_image": "Input image to upscale 2x.",
|
| 24 |
+
"refine_steps": "Steps for the Z-Image-Turbo refinement pass after RealESRGAN. 3-8 typical.",
|
| 25 |
+
"refine_denoise": "How much the refinement alters pixels. 0.2-0.4 typical. Higher = more detail change.",
|
| 26 |
+
"output": "Generated image. Right-click to download full resolution.",
|
| 27 |
}
|
upscale.py
CHANGED
|
@@ -3,6 +3,7 @@
|
|
| 3 |
This module only handles the *pixel-space* upscale. The Z-Image-Turbo refinement
|
| 4 |
pass (img2img at denoise=0.33) lives in :mod:`modes` since it shares the pipeline.
|
| 5 |
"""
|
|
|
|
| 6 |
from __future__ import annotations
|
| 7 |
|
| 8 |
from pathlib import Path
|
|
@@ -26,8 +27,8 @@ _MODEL_CACHE: dict[str, Any] = {}
|
|
| 26 |
def _realesrgan_4x(model_path: Path | str, image: Image.Image) -> Image.Image:
|
| 27 |
"""Run RealESRGAN x4plus on ``image``. Caches the model in-process."""
|
| 28 |
import numpy as np
|
| 29 |
-
from realesrgan import RealESRGANer
|
| 30 |
from basicsr.archs.rrdbnet_arch import RRDBNet
|
|
|
|
| 31 |
|
| 32 |
key = str(model_path)
|
| 33 |
if key not in _MODEL_CACHE:
|
|
@@ -36,10 +37,10 @@ def _realesrgan_4x(model_path: Path | str, image: Image.Image) -> Image.Image:
|
|
| 36 |
scale=4,
|
| 37 |
model_path=key,
|
| 38 |
model=net,
|
| 39 |
-
tile=512,
|
| 40 |
tile_pad=10,
|
| 41 |
pre_pad=0,
|
| 42 |
-
half=False,
|
| 43 |
gpu_id=None,
|
| 44 |
)
|
| 45 |
|
|
|
|
| 3 |
This module only handles the *pixel-space* upscale. The Z-Image-Turbo refinement
|
| 4 |
pass (img2img at denoise=0.33) lives in :mod:`modes` since it shares the pipeline.
|
| 5 |
"""
|
| 6 |
+
|
| 7 |
from __future__ import annotations
|
| 8 |
|
| 9 |
from pathlib import Path
|
|
|
|
| 27 |
def _realesrgan_4x(model_path: Path | str, image: Image.Image) -> Image.Image:
|
| 28 |
"""Run RealESRGAN x4plus on ``image``. Caches the model in-process."""
|
| 29 |
import numpy as np
|
|
|
|
| 30 |
from basicsr.archs.rrdbnet_arch import RRDBNet
|
| 31 |
+
from realesrgan import RealESRGANer
|
| 32 |
|
| 33 |
key = str(model_path)
|
| 34 |
if key not in _MODEL_CACHE:
|
|
|
|
| 37 |
scale=4,
|
| 38 |
model_path=key,
|
| 39 |
model=net,
|
| 40 |
+
tile=512, # split into tiles to avoid OOM on large inputs
|
| 41 |
tile_pad=10,
|
| 42 |
pre_pad=0,
|
| 43 |
+
half=False, # bf16 elsewhere; keep this fp32 for stability
|
| 44 |
gpu_id=None,
|
| 45 |
)
|
| 46 |
|