techfreakworm commited on
Commit
76862de
·
unverified ·
1 Parent(s): 5ac741c

fix: post-review polish — slider defaults, error handling, comment

Browse files

- I5: t2i steps/cfg sliders update via gr.change() when model selector
toggles base/turbo (was: stuck at turbo defaults, base generations
silently used cfg=1 steps=8 → bad output).
- I3: controlnet preprocessor wraps in try/except with raw-input
fallback + stderr warning.
- I1: zerogpu 'gpu task aborted' retries once with 2x duration
via __retry_multiplier__ in params. duration_for honors it.
- I4: 'Eager backend boot' comment renamed to 'Lazy backend singleton'
to match the actual lazy get_backend() behavior.
- bonus: fix pre-existing ruff failures in test_smoke_gpu.py
(unused meta vars, unsorted local imports).

Files changed (7) hide show
  1. app.py +16 -4
  2. backend.py +20 -1
  3. modes.py +9 -1
  4. tests/test_app.py +13 -0
  5. tests/test_backend.py +49 -0
  6. tests/test_modes.py +26 -0
  7. tests/test_smoke_gpu.py +54 -23
app.py CHANGED
@@ -35,7 +35,7 @@ def _bootstrap() -> None:
35
  _bootstrap()
36
 
37
 
38
- # ----- Eager backend boot ----------------------------------------------------
39
 
40
  _BACKEND: backend.ZImageStudioBackend | None = None
41
 
@@ -62,6 +62,13 @@ def _coerce_lora(lora_path: str | None) -> Path | None:
62
  return p
63
 
64
 
 
 
 
 
 
 
 
65
  def _esrgan_path() -> str:
66
  """Locate the preloaded RealESRGAN_x4plus.pth."""
67
  from huggingface_hub import hf_hub_download
@@ -87,7 +94,7 @@ def on_t2i_generate(prompt, negative_prompt, model, steps, cfg, width, height, s
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
 
@@ -107,7 +114,7 @@ def on_controlnet_generate(prompt, input_image, preprocessor, controlnet_scale,
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
 
@@ -127,7 +134,7 @@ def on_upscale_generate(prompt, input_image, refine_steps, refine_denoise, seed,
127
  lora_strength=float(lora_strength),
128
  esrgan_model_path=_esrgan_path(),
129
  )
130
- image, meta = get_backend().generate(mode="upscale", params=params)
131
  return image, meta
132
 
133
 
@@ -192,6 +199,11 @@ def build_app() -> gr.Blocks:
192
  ],
193
  outputs=[t["output_image"], t["output_meta"]],
194
  )
 
 
 
 
 
195
 
196
  with gr.Tab("ControlNet"):
197
  c = ui.build_controlnet_tab()
 
35
  _bootstrap()
36
 
37
 
38
+ # ----- Lazy backend singleton ------------------------------------------------
39
 
40
  _BACKEND: backend.ZImageStudioBackend | None = None
41
 
 
62
  return p
63
 
64
 
65
+ def _on_model_change(model_name: str) -> tuple[int, float]:
66
+ """When the user clicks Base / Turbo in the custom selector, update steps + cfg."""
67
+ if model_name == "Base":
68
+ return 25, 4.0
69
+ return 8, 1.0 # Turbo
70
+
71
+
72
  def _esrgan_path() -> str:
73
  """Locate the preloaded RealESRGAN_x4plus.pth."""
74
  from huggingface_hub import hf_hub_download
 
94
  lora_path=lora_p,
95
  lora_strength=float(lora_strength),
96
  )
97
+ image, meta = backend.generate_with_retry(get_backend(), mode="t2i", params=params)
98
  return image, meta
99
 
100
 
 
114
  lora_path=lora_p,
115
  lora_strength=float(lora_strength),
116
  )
117
+ image, meta = backend.generate_with_retry(get_backend(), mode="controlnet", params=params)
118
  return image, meta
119
 
120
 
 
134
  lora_strength=float(lora_strength),
135
  esrgan_model_path=_esrgan_path(),
136
  )
137
+ image, meta = backend.generate_with_retry(get_backend(), mode="upscale", params=params)
138
  return image, meta
139
 
140
 
 
199
  ],
200
  outputs=[t["output_image"], t["output_meta"]],
201
  )
202
+ t["model_state"].change(
203
+ fn=_on_model_change,
204
+ inputs=[t["model_state"]],
205
+ outputs=[t["steps"], t["cfg"]],
206
+ )
207
 
208
  with gr.Tab("ControlNet"):
209
  c = ui.build_controlnet_tab()
backend.py CHANGED
@@ -37,12 +37,14 @@ def duration_for(
37
  width = int(params.get("width", 1024))
38
  height = int(params.get("height", 1024))
39
 
 
 
40
  base = _BASE_DURATION_S.get(mode, 30)
41
  per_step = _PER_STEP_S.get((mode, model), _PER_STEP_S.get((mode, "Turbo"), 1.6))
42
  size_factor = (width * height) / (1024 * 1024)
43
  cold_buffer = 15 # CPU→GPU copy on first call after a quiet period
44
 
45
- est = (base + per_step * steps + cold_buffer) * size_factor * multiplier
46
  return max(60, min(int(est), 180))
47
 
48
 
@@ -111,3 +113,20 @@ class ZImageStudioBackend:
111
  if handler is None:
112
  raise ValueError(f"unknown mode: {mode!r}; expected one of {list(_DISPATCH)}")
113
  return handler(self.pipeline, params)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  width = int(params.get("width", 1024))
38
  height = int(params.get("height", 1024))
39
 
40
+ eff_multiplier = float(params.get("__retry_multiplier__", multiplier))
41
+
42
  base = _BASE_DURATION_S.get(mode, 30)
43
  per_step = _PER_STEP_S.get((mode, model), _PER_STEP_S.get((mode, "Turbo"), 1.6))
44
  size_factor = (width * height) / (1024 * 1024)
45
  cold_buffer = 15 # CPU→GPU copy on first call after a quiet period
46
 
47
+ est = (base + per_step * steps + cold_buffer) * size_factor * eff_multiplier
48
  return max(60, min(int(est), 180))
49
 
50
 
 
113
  if handler is None:
114
  raise ValueError(f"unknown mode: {mode!r}; expected one of {list(_DISPATCH)}")
115
  return handler(self.pipeline, params)
116
+
117
+
118
+ def generate_with_retry(
119
+ backend_instance: ZImageStudioBackend,
120
+ mode: str,
121
+ params: dict[str, Any],
122
+ ) -> tuple[Any, dict[str, Any]]:
123
+ """Call backend_instance.generate; on ZeroGPU timeout, retry once with 2x duration budget."""
124
+ try:
125
+ return backend_instance.generate(mode, params)
126
+ except Exception as e:
127
+ msg = str(e).lower()
128
+ if "gpu task aborted" in msg or ("gpu" in msg and "aborted" in msg):
129
+ retry_params = dict(params)
130
+ retry_params["__retry_multiplier__"] = 2.0
131
+ return backend_instance.generate(mode, retry_params)
132
+ raise
modes.py CHANGED
@@ -87,7 +87,15 @@ def call_controlnet(pipe: Any, params: dict[str, Any]) -> tuple[Image.Image, dic
87
  raise ValueError("ControlNet mode requires an input image")
88
 
89
  preproc_mode = params.get("preprocessor", "Canny")
90
- control_image = preprocessors.run(preproc_mode, input_image)
 
 
 
 
 
 
 
 
91
 
92
  _swap_transformer(pipe, "Turbo")
93
 
 
87
  raise ValueError("ControlNet mode requires an input image")
88
 
89
  preproc_mode = params.get("preprocessor", "Canny")
90
+ try:
91
+ control_image = preprocessors.run(preproc_mode, input_image)
92
+ except Exception as e:
93
+ import sys
94
+
95
+ print(
96
+ f"[modes] preprocessor {preproc_mode!r} failed: {e}; falling back to raw input", file=sys.stderr, flush=True
97
+ )
98
+ control_image = input_image
99
 
100
  _swap_transformer(pipe, "Turbo")
101
 
tests/test_app.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import app
2
+
3
+
4
+ def test_on_model_change_returns_base_defaults():
5
+ assert app._on_model_change("Base") == (25, 4.0)
6
+
7
+
8
+ def test_on_model_change_returns_turbo_defaults():
9
+ assert app._on_model_change("Turbo") == (8, 1.0)
10
+
11
+
12
+ def test_on_model_change_unknown_falls_back_to_turbo():
13
+ assert app._on_model_change("Edit") == (8, 1.0)
tests/test_backend.py CHANGED
@@ -93,3 +93,52 @@ def test_backend_generate_routes_controlnet(fake_backend, monkeypatch):
93
  def test_backend_generate_unknown_mode_raises(fake_backend):
94
  with pytest.raises(ValueError):
95
  fake_backend.generate(mode="dance", params={})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
  def test_backend_generate_unknown_mode_raises(fake_backend):
94
  with pytest.raises(ValueError):
95
  fake_backend.generate(mode="dance", params={})
96
+
97
+
98
+ def test_generate_with_retry_retries_on_gpu_aborted(fake_backend, monkeypatch):
99
+ call_count = {"n": 0}
100
+ original_generate = fake_backend.generate
101
+
102
+ def flaky(mode, params):
103
+ call_count["n"] += 1
104
+ if call_count["n"] == 1:
105
+ from gradio.exceptions import Error
106
+
107
+ raise Error("GPU task aborted")
108
+ return original_generate(mode, params)
109
+
110
+ fake_backend.generate = flaky
111
+
112
+ _img, meta = backend.generate_with_retry(
113
+ fake_backend,
114
+ mode="t2i",
115
+ params=dict(
116
+ prompt="x",
117
+ negative_prompt="",
118
+ model="Turbo",
119
+ steps=8,
120
+ cfg=1.0,
121
+ width=1024,
122
+ height=1024,
123
+ seed=0,
124
+ lora_path=None,
125
+ lora_strength=0.0,
126
+ ),
127
+ )
128
+ assert call_count["n"] == 2 # one fail + one retry
129
+ assert meta["mode"] == "t2i"
130
+
131
+
132
+ def test_generate_with_retry_does_not_retry_other_errors(fake_backend):
133
+ fake_backend.generate = lambda *a, **kw: (_ for _ in ()).throw(ValueError("not a gpu issue"))
134
+ with pytest.raises(ValueError):
135
+ backend.generate_with_retry(fake_backend, mode="t2i", params={})
136
+
137
+
138
+ def test_duration_honors_retry_multiplier_in_params():
139
+ normal = backend.duration_for(mode="t2i", params=dict(model="Turbo", steps=8, width=1024, height=1024))
140
+ retry = backend.duration_for(
141
+ mode="t2i",
142
+ params=dict(model="Turbo", steps=8, width=1024, height=1024, __retry_multiplier__=2.0),
143
+ )
144
+ assert retry > normal
tests/test_modes.py CHANGED
@@ -190,3 +190,29 @@ def test_upscale_rejects_missing_image(fake_pipe):
190
  esrgan_model_path="/fake.pth",
191
  ),
192
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
190
  esrgan_model_path="/fake.pth",
191
  ),
192
  )
193
+
194
+
195
+ def test_controlnet_falls_back_when_preprocessor_raises(fake_pipe, monkeypatch):
196
+ def boom(mode, img):
197
+ raise RuntimeError("preprocessor exploded")
198
+
199
+ monkeypatch.setattr(modes, "preprocessors", type("P", (), {"run": staticmethod(boom)}))
200
+
201
+ input_image = Image.new("RGB", (512, 512))
202
+ _out, _meta = modes.call_controlnet(
203
+ fake_pipe,
204
+ params=dict(
205
+ prompt="x",
206
+ input_image=input_image,
207
+ preprocessor="Canny",
208
+ controlnet_scale=1.0,
209
+ steps=9,
210
+ seed=0,
211
+ lora_path=None,
212
+ lora_strength=0.0,
213
+ ),
214
+ )
215
+ # Pipeline still ran — fallback to raw input
216
+ kwargs = fake_pipe.call_args.kwargs
217
+ cn_in = kwargs["controlnet_inputs"]
218
+ assert cn_in[0].image is input_image # the raw input, not a preprocessed image
tests/test_smoke_gpu.py CHANGED
@@ -7,17 +7,27 @@ pytestmark = pytest.mark.gpu
7
  def real_backend():
8
  """Build a real backend with real weights. ~30 GB download on first run."""
9
  import backend
 
10
  return backend.ZImageStudioBackend()
11
 
12
 
13
  def test_t2i_turbo_produces_image(real_backend):
14
  from PIL import Image
 
15
  image, meta = real_backend.generate(
16
  mode="t2i",
17
- params=dict(prompt="a red apple on a wooden table",
18
- negative_prompt="", model="Turbo",
19
- steps=8, cfg=1.0, width=384, height=384, seed=42,
20
- lora_path=None, lora_strength=0.0),
 
 
 
 
 
 
 
 
21
  )
22
  assert isinstance(image, Image.Image)
23
  assert image.size == (384, 384)
@@ -26,42 +36,63 @@ def test_t2i_turbo_produces_image(real_backend):
26
 
27
  def test_t2i_base_produces_image(real_backend):
28
  from PIL import Image
29
- image, meta = real_backend.generate(
 
30
  mode="t2i",
31
- params=dict(prompt="a red apple on a wooden table",
32
- negative_prompt="blurry", model="Base",
33
- steps=15, cfg=4.0, width=384, height=384, seed=42,
34
- lora_path=None, lora_strength=0.0),
 
 
 
 
 
 
 
 
35
  )
36
  assert isinstance(image, Image.Image)
37
 
38
 
39
  def test_controlnet_produces_image(real_backend):
40
- from PIL import Image
41
  import numpy as np
 
 
42
  arr = np.random.randint(0, 255, (384, 384, 3), dtype=np.uint8)
43
- image, meta = real_backend.generate(
44
  mode="controlnet",
45
- params=dict(prompt="a portrait of a person, dramatic light",
46
- input_image=Image.fromarray(arr),
47
- preprocessor="Canny", controlnet_scale=1.0,
48
- steps=9, seed=42, lora_path=None, lora_strength=0.0),
 
 
 
 
 
 
49
  )
50
  assert isinstance(image, Image.Image)
51
 
52
 
53
  def test_upscale_produces_image(real_backend, tmp_path):
54
- from PIL import Image
55
  import numpy as np
56
  from huggingface_hub import hf_hub_download
 
 
57
  arr = np.random.randint(0, 255, (256, 256, 3), dtype=np.uint8)
58
- image, meta = real_backend.generate(
59
  mode="upscale",
60
- params=dict(prompt="masterpiece, 8k",
61
- input_image=Image.fromarray(arr),
62
- refine_steps=5, refine_denoise=0.33, seed=42,
63
- lora_path=None, lora_strength=0.0,
64
- esrgan_model_path=hf_hub_download("xinntao/Real-ESRGAN",
65
- "RealESRGAN_x4plus.pth")),
 
 
 
 
66
  )
67
  assert image.size == (512, 512)
 
7
  def real_backend():
8
  """Build a real backend with real weights. ~30 GB download on first run."""
9
  import backend
10
+
11
  return backend.ZImageStudioBackend()
12
 
13
 
14
  def test_t2i_turbo_produces_image(real_backend):
15
  from PIL import Image
16
+
17
  image, meta = real_backend.generate(
18
  mode="t2i",
19
+ params=dict(
20
+ prompt="a red apple on a wooden table",
21
+ negative_prompt="",
22
+ model="Turbo",
23
+ steps=8,
24
+ cfg=1.0,
25
+ width=384,
26
+ height=384,
27
+ seed=42,
28
+ lora_path=None,
29
+ lora_strength=0.0,
30
+ ),
31
  )
32
  assert isinstance(image, Image.Image)
33
  assert image.size == (384, 384)
 
36
 
37
  def test_t2i_base_produces_image(real_backend):
38
  from PIL import Image
39
+
40
+ image, _meta = real_backend.generate(
41
  mode="t2i",
42
+ params=dict(
43
+ prompt="a red apple on a wooden table",
44
+ negative_prompt="blurry",
45
+ model="Base",
46
+ steps=15,
47
+ cfg=4.0,
48
+ width=384,
49
+ height=384,
50
+ seed=42,
51
+ lora_path=None,
52
+ lora_strength=0.0,
53
+ ),
54
  )
55
  assert isinstance(image, Image.Image)
56
 
57
 
58
  def test_controlnet_produces_image(real_backend):
 
59
  import numpy as np
60
+ from PIL import Image
61
+
62
  arr = np.random.randint(0, 255, (384, 384, 3), dtype=np.uint8)
63
+ image, _meta = real_backend.generate(
64
  mode="controlnet",
65
+ params=dict(
66
+ prompt="a portrait of a person, dramatic light",
67
+ input_image=Image.fromarray(arr),
68
+ preprocessor="Canny",
69
+ controlnet_scale=1.0,
70
+ steps=9,
71
+ seed=42,
72
+ lora_path=None,
73
+ lora_strength=0.0,
74
+ ),
75
  )
76
  assert isinstance(image, Image.Image)
77
 
78
 
79
  def test_upscale_produces_image(real_backend, tmp_path):
 
80
  import numpy as np
81
  from huggingface_hub import hf_hub_download
82
+ from PIL import Image
83
+
84
  arr = np.random.randint(0, 255, (256, 256, 3), dtype=np.uint8)
85
+ image, _meta = real_backend.generate(
86
  mode="upscale",
87
+ params=dict(
88
+ prompt="masterpiece, 8k",
89
+ input_image=Image.fromarray(arr),
90
+ refine_steps=5,
91
+ refine_denoise=0.33,
92
+ seed=42,
93
+ lora_path=None,
94
+ lora_strength=0.0,
95
+ esrgan_model_path=hf_hub_download("xinntao/Real-ESRGAN", "RealESRGAN_x4plus.pth"),
96
+ ),
97
  )
98
  assert image.size == (512, 512)