techfreakworm commited on
Commit
9a5065c
·
unverified ·
1 Parent(s): ceadaef

ci: ruff + pytest on push/pr (l1+l2, no gpu deps)

Browse files

Add .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 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, negative_prompt=negative_prompt or "",
79
- model=model, steps=int(steps), cfg=float(cfg),
80
- width=int(width), height=int(height),
 
 
 
 
81
  seed=_maybe_random_seed(int(seed)),
82
- lora_path=lora_p, lora_strength=float(lora_strength),
 
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, input_image=input_image,
97
- preprocessor=preprocessor, controlnet_scale=float(controlnet_scale),
98
- steps=int(steps), seed=_maybe_random_seed(int(seed)),
99
- lora_path=lora_p, lora_strength=float(lora_strength),
 
 
 
 
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, lora_strength=float(lora_strength),
 
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=[t["prompt"], t["negative_prompt"], t["model_state"],
173
- t["steps"], t["cfg"], t["width"], t["height"], t["seed"],
174
- t["lora_path"], t["lora_strength"]],
 
 
 
 
 
 
 
 
 
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=[c["prompt"], c["input_image"],
183
- c["preprocessor"], c["controlnet_scale"],
184
- c["steps"], c["seed"], c["lora_path"], c["lora_strength"]],
 
 
 
 
 
 
 
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=[u["prompt"], u["input_image"],
193
- u["refine_steps"], u["refine_denoise"],
194
- u["seed"], u["lora_path"], u["lora_strength"]],
 
 
 
 
 
 
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": 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,8 +51,11 @@ def _identity(fn):
51
 
52
 
53
  _ON_SPACES = bool(os.environ.get("SPACES_ZERO_GPU"))
54
- _GPU = spaces.GPU(duration=lambda *a, **kw: duration_for(*a[1:3], **kw)) \
55
- if (spaces is not None and _ON_SPACES) else _identity
 
 
 
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, offload_device="cpu",
70
- onload_dtype=torch.bfloat16, onload_device="cpu",
71
- preparing_dtype=torch.bfloat16, preparing_device=device,
72
- computation_dtype=torch.bfloat16, computation_device=device,
 
 
 
 
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,), vram_cfg=None,
 
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": modes.call_t2i,
89
  "controlnet": modes.call_controlnet,
90
- "upscale": modes.call_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, Iterator
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, field
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 ** 3)
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
- "Z-Image base transformer (25 steps, cfg=4)"),
67
- ModelConfig("Tongyi-MAI/Z-Image", "text_encoder/*.safetensors",
68
- "Qwen3-4B text encoder — shared between base + turbo"),
69
- ModelConfig("Tongyi-MAI/Z-Image", "vae/diffusion_pytorch_model.safetensors",
70
- "Flux-family VAE — shared between base + turbo"),
 
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("PAI/Z-Image-Turbo-Fun-Controlnet-Union-2.1",
76
- "Z-Image-Turbo-Fun-Controlnet-Union-2.1-8steps.safetensors",
77
- "ControlNet Union 2.1 — canny/depth/pose"),
 
 
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 # "Base" | "Turbo"
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", model=model_name,
70
- steps=kwargs["num_inference_steps"], cfg=kwargs["cfg_scale"],
71
- seed=kwargs["seed"], width=kwargs["width"], height=kwargs["height"],
 
 
 
 
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", model="Turbo",
 
107
  preprocessor=preproc_mode,
108
  controlnet_scale=cn_input.scale,
109
- steps=kwargs["num_inference_steps"], cfg=1.0,
110
- seed=kwargs["seed"], width=kwargs["width"], height=kwargs["height"],
 
 
 
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", model="Turbo",
 
142
  refine_steps=kwargs["num_inference_steps"],
143
  refine_denoise=kwargs["denoising_strength"],
144
- seed=kwargs["seed"], width=upscaled.size[0], height=upscaled.size[1],
 
 
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(mode="t2i", params=dict(model="Turbo", steps=8, width=1024, height=1024),
27
- multiplier=2.0)
 
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(prompt="cat", negative_prompt="", model="Turbo",
58
- steps=8, cfg=1.0, width=1024, height=1024, seed=42,
59
- lora_path=None, lora_strength=0.0),
 
 
 
 
 
 
 
 
 
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
- type("P", (), {"run": staticmethod(lambda m, i: i)}))
69
- img, meta = fake_backend.generate(
70
  mode="controlnet",
71
- params=dict(prompt="cat", input_image=Image.new("RGB", (64, 64)),
72
- preprocessor="Canny", controlnet_scale=1.0,
73
- steps=9, seed=0, lora_path=None, lora_strength=0.0),
 
 
 
 
 
 
 
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(p, {
19
- "transformer.layer1.lora_A.weight": {"dtype": "BF16", "shape": [64, 3840]},
20
- "transformer.layer1.lora_B.weight": {"dtype": "BF16", "shape": [3840, 64]},
21
- "__metadata__": {"rank": "64"},
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(p, {
40
- "down_blocks.0.weight": {"dtype": "F32", "shape": [320, 320]},
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 = [] # list of (path, strength) tuples
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(p, {
58
- "transformer.x.lora_A.weight": {"dtype": "BF16", "shape": [32, 3840]},
59
- "transformer.x.lora_B.weight": {"dtype": "BF16", "shape": [3840, 32]},
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(p, {
92
- "transformer.x.lora_A.weight": {"dtype": "BF16", "shape": [16, 3840]},
93
- "transformer.x.lora_B.weight": {"dtype": "BF16", "shape": [3840, 16]},
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, cfg=1.0,
27
- width=1024, height=1024,
 
 
28
  seed=42,
29
- lora_path=None, lora_strength=0.0,
 
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", negative_prompt="blurry, lowres",
51
- model="Base", steps=25, cfg=4.0,
52
- width=1024, height=1024, seed=42,
53
- lora_path=None, lora_strength=0.0,
 
 
 
 
 
 
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(prompt="x", negative_prompt="", model="Base", steps=25, cfg=4.0,
66
- width=1024, height=1024, seed=0, lora_path=None, lora_strength=0.0),
 
 
 
 
 
 
 
 
 
 
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
- out, meta = modes.call_controlnet(
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, lora_strength=0.0,
 
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(prompt="x", input_image=None, preprocessor="Canny",
110
- controlnet_scale=1.0, steps=9, seed=0,
111
- lora_path=None, lora_strength=0.0),
 
 
 
 
 
 
 
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
- out, meta = modes.call_upscale(
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, lora_strength=0.0,
 
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(fake_pipe, params=dict(prompt="x", input_image=None,
149
- refine_steps=5, refine_denoise=0.33, seed=0,
150
- lora_path=None, lora_strength=0.0,
151
- esrgan_model_path="/fake.pth"))
 
 
 
 
 
 
 
 
 
 
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", "requirements.txt", "setup.sh",
9
- "LICENSE", "CLAUDE.md", "README.md", ".gitignore",
10
- "tests/__init__.py", "tests/conftest.py",
 
 
 
 
 
 
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", "negative_prompt", "model", "lora", "lora_strength",
5
- "steps", "cfg", "width", "height", "seed",
6
- "controlnet_image", "controlnet_preprocessor", "controlnet_scale",
7
- "upscale_image", "refine_steps", "refine_denoise", "output",
 
 
 
 
 
 
 
 
 
 
 
 
 
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": "What to generate. Be specific: subject, style, lighting, camera angle.",
10
- "negative_prompt": "What to avoid (Base only). e.g. 'blurry, low quality, distorted'.",
11
- "model": "Base = 25 steps, higher quality. Turbo = 8 steps, fast.",
12
- "lora": "Optional .safetensors LoRA file. Trained on Z-Image base or turbo.",
13
- "lora_strength": "LoRA influence. 0.61.0 typical. Higher = more LoRA, less base model.",
14
- "steps": "Denoising steps. Turbo: 610. Base: 2030. More = better detail, slower.",
15
- "cfg": "Classifier-free guidance. Turbo: locked at 1.0. Base: 35 typical.",
16
- "width": "Output width in pixels. Multiples of 64. Higher = more memory.",
17
- "height": "Output height in pixels. Multiples of 64.",
18
- "seed": "0 = random each run. Pin a number to reproduce an image exactly.",
19
- "controlnet_image": "Control image — the structural reference for the output.",
20
  "controlnet_preprocessor": "Canny = edges, Depth = depth map, Pose = body pose, Pre-processed = use image as-is.",
21
- "controlnet_scale": "How strongly the control image guides the output. 0.61.2 typical.",
22
- "upscale_image": "Input image to upscale 2x.",
23
- "refine_steps": "Steps for the Z-Image-Turbo refinement pass after RealESRGAN. 38 typical.",
24
- "refine_denoise": "How much the refinement alters pixels. 0.20.4 typical. Higher = more detail change.",
25
- "output": "Generated image. Right-click to download full resolution.",
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, # split into tiles to avoid OOM on large inputs
40
  tile_pad=10,
41
  pre_pad=0,
42
- half=False, # bf16 elsewhere; keep this fp32 for stability
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