joeaa17 commited on
Commit
081422c
Β·
verified Β·
1 Parent(s): 54ea6c8

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +214 -208
app.py CHANGED
@@ -1,81 +1,48 @@
1
  #!/usr/bin/env python
2
 
3
- import spaces
4
- import os
5
- import random
6
-
7
  import gradio as gr
8
- import numpy as np
9
- import PIL.Image
10
- import torch
11
- import torchvision.transforms.functional as TF
12
-
13
- from diffusers import ControlNetModel, StableDiffusionXLControlNetPipeline, AutoencoderKL
14
- from diffusers import DDIMScheduler, EulerAncestralDiscreteScheduler
15
- from controlnet_aux import PidiNetDetector, HEDdetector
16
- from diffusers.utils import load_image
17
- from huggingface_hub import HfApi
18
  from pathlib import Path
19
  from PIL import Image, ImageOps
20
- import torch
21
- import numpy as np
22
- import cv2
23
- import os
24
- import random
 
 
 
 
 
25
  from gradio_imageslider import ImageSlider
26
 
 
 
 
27
  js_func = """
28
  function refresh() {
29
  const url = new URL(window.location);
30
-
31
  if (url.searchParams.get('__theme') !== 'dark') {
32
  url.searchParams.set('__theme', 'dark');
33
  window.location.href = url.href;
34
  }
35
  }
36
  """
37
- def nms(x, t, s):
38
- x = cv2.GaussianBlur(x.astype(np.float32), (0, 0), s)
39
-
40
- f1 = np.array([[0, 0, 0], [1, 1, 1], [0, 0, 0]], dtype=np.uint8)
41
- f2 = np.array([[0, 1, 0], [0, 1, 0], [0, 1, 0]], dtype=np.uint8)
42
- f3 = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]], dtype=np.uint8)
43
- f4 = np.array([[0, 0, 1], [0, 1, 0], [1, 0, 0]], dtype=np.uint8)
44
-
45
- y = np.zeros_like(x)
46
-
47
- for f in [f1, f2, f3, f4]:
48
- np.putmask(y, cv2.dilate(x, kernel=f) == x, x)
49
-
50
- z = np.zeros_like(y, dtype=np.uint8)
51
- z[y > t] = 255
52
- return z
53
 
54
- def HWC3(x):
55
- assert x.dtype == np.uint8
56
- if x.ndim == 2:
57
- x = x[:, :, None]
58
- assert x.ndim == 3
59
- H, W, C = x.shape
60
- assert C == 1 or C == 3 or C == 4
61
- if C == 3:
62
- return x
63
- if C == 1:
64
- return np.concatenate([x, x, x], axis=2)
65
- if C == 4:
66
- color = x[:, :, 0:3].astype(np.float32)
67
- alpha = x[:, :, 3:4].astype(np.float32) / 255.0
68
- y = color * alpha + 255.0 * (1.0 - alpha)
69
- y = y.clip(0, 255).astype(np.uint8)
70
- return y
71
-
72
- DESCRIPTION = '''# Scribble SDXL πŸ–‹οΈπŸŒ„
73
- sketch to image with SDXL, using [@xinsir](https://huggingface.co/xinsir) [scribble sdxl controlnet](https://huggingface.co/xinsir/controlnet-scribble-sdxl-1.0), [sdxl controlnet canny](https://huggingface.co/xinsir/controlnet-canny-sdxl-1.0)
74
  '''
75
 
76
  if not torch.cuda.is_available():
77
- DESCRIPTION += "\n<p>Running on CPU πŸ₯Ά This demo does not work on CPU.</p>"
78
 
 
 
 
79
  style_list = [
80
  {
81
  "name": "(No style)",
@@ -114,7 +81,7 @@ style_list = [
114
  },
115
  {
116
  "name": "Fantasy art",
117
- "prompt": "ethereal fantasy concept art of {prompt} . magnificent, celestial, ethereal, painterly, epic, majestic, magical, fantasy art, cover art, dreamy",
118
  "negative_prompt": "photographic, realistic, realism, 35mm film, dslr, cropped, frame, text, deformed, glitch, noise, noisy, off-center, deformed, cross-eyed, closed eyes, bad anatomy, ugly, disfigured, sloppy, duplicate, mutated, black and white",
119
  },
120
  {
@@ -128,204 +95,243 @@ style_list = [
128
  "negative_prompt": "ugly, deformed, noisy, blurry, low contrast, realism, photorealistic, Western comic style",
129
  },
130
  ]
131
-
132
- styles = {k["name"]: (k["prompt"], k["negative_prompt"]) for k in style_list}
133
  STYLE_NAMES = list(styles.keys())
134
  DEFAULT_STYLE_NAME = "(No style)"
135
 
136
-
137
  def apply_style(style_name: str, positive: str, negative: str = "") -> tuple[str, str]:
138
  p, n = styles.get(style_name, styles[DEFAULT_STYLE_NAME])
139
- return p.replace("{prompt}", positive), n + negative
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
 
 
 
 
 
 
 
 
 
 
 
 
 
141
 
 
 
 
 
 
 
 
 
 
 
 
 
142
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
143
 
144
- eulera_scheduler = EulerAncestralDiscreteScheduler.from_pretrained("stabilityai/stable-diffusion-xl-base-1.0", subfolder="scheduler")
145
-
 
146
 
147
- controlnet = ControlNetModel.from_pretrained(
148
- "xinsir/controlnet-scribble-sdxl-1.0",
149
- torch_dtype=torch.float16
150
  )
151
  controlnet_canny = ControlNetModel.from_pretrained(
152
- "xinsir/controlnet-canny-sdxl-1.0",
153
- torch_dtype=torch.float16
 
 
154
  )
155
- # when test with other base model, you need to change the vae also.
156
- vae = AutoencoderKL.from_pretrained("madebyollin/sdxl-vae-fp16-fix", torch_dtype=torch.float16)
157
 
158
- pipe = StableDiffusionXLControlNetPipeline.from_pretrained(
159
  "stabilityai/stable-diffusion-xl-base-1.0",
160
- controlnet=controlnet,
161
  vae=vae,
162
- torch_dtype=torch.float16,
163
- scheduler=eulera_scheduler,
164
  )
165
- pipe.to(device)
166
- # Load model.
167
  pipe_canny = StableDiffusionXLControlNetPipeline.from_pretrained(
168
  "stabilityai/stable-diffusion-xl-base-1.0",
169
  controlnet=controlnet_canny,
170
  vae=vae,
171
- safety_checker=None,
172
- torch_dtype=torch.float16,
173
- scheduler=eulera_scheduler,
174
  )
175
 
176
- pipe_canny.to(device)
 
 
 
 
 
 
 
177
 
178
  MAX_SEED = np.iinfo(np.int32).max
179
- processor = HEDdetector.from_pretrained('lllyasviel/Annotators')
180
- def nms(x, t, s):
181
- x = cv2.GaussianBlur(x.astype(np.float32), (0, 0), s)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
182
 
183
- f1 = np.array([[0, 0, 0], [1, 1, 1], [0, 0, 0]], dtype=np.uint8)
184
- f2 = np.array([[0, 1, 0], [0, 1, 0], [0, 1, 0]], dtype=np.uint8)
185
- f3 = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]], dtype=np.uint8)
186
- f4 = np.array([[0, 0, 1], [0, 1, 0], [1, 0, 0]], dtype=np.uint8)
187
 
188
- y = np.zeros_like(x)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
 
190
- for f in [f1, f2, f3, f4]:
191
- np.putmask(y, cv2.dilate(x, kernel=f) == x, x)
192
 
193
- z = np.zeros_like(y, dtype=np.uint8)
194
- z[y > t] = 255
195
- return z
 
196
 
197
  def randomize_seed_fn(seed: int, randomize_seed: bool) -> int:
198
- if randomize_seed:
199
- seed = random.randint(0, MAX_SEED)
200
- return seed
201
 
202
  @spaces.GPU
203
  def run(
204
- image: PIL.Image.Image,
205
  prompt: str,
206
  negative_prompt: str,
207
  style_name: str = DEFAULT_STYLE_NAME,
208
- num_steps: int = 25,
209
- guidance_scale: float = 5,
210
  controlnet_conditioning_scale: float = 1.0,
211
  seed: int = 0,
212
  use_hed: bool = False,
213
  use_canny: bool = False,
214
  progress=gr.Progress(track_tqdm=True),
215
- ) -> PIL.Image.Image:
216
- width, height = image['composite'].size
217
- ratio = np.sqrt(1024. * 1024. / (width * height))
218
- new_width, new_height = int(width * ratio), int(height * ratio)
219
- image = image['composite'].resize((new_width, new_height))
220
-
221
- if use_canny:
222
- controlnet_img = np.array(image)
223
- controlnet_img = cv2.Canny(controlnet_img, 100, 200)
224
- controlnet_img = HWC3(controlnet_img)
225
- image = Image.fromarray(controlnet_img)
226
-
227
- elif not use_hed:
228
- controlnet_img = image
229
- else:
230
- controlnet_img = processor(image, scribble=False)
231
- # following is some processing to simulate human sketch draw, different threshold can generate different width of lines
232
- controlnet_img = np.array(controlnet_img)
233
- controlnet_img = nms(controlnet_img, 127, 3)
234
- controlnet_img = cv2.GaussianBlur(controlnet_img, (0, 0), 3)
235
-
236
- # higher threshold, thiner line
237
- random_val = int(round(random.uniform(0.01, 0.10), 2) * 255)
238
- controlnet_img[controlnet_img > random_val] = 255
239
- controlnet_img[controlnet_img < 255] = 0
240
- image = Image.fromarray(controlnet_img)
241
-
242
-
243
- prompt, negative_prompt = apply_style(style_name, prompt, negative_prompt)
244
-
245
- generator = torch.Generator(device=device).manual_seed(seed)
246
- if use_canny:
247
- out = pipe_canny(
248
- prompt=prompt,
249
- negative_prompt=negative_prompt,
250
- image=image,
251
- num_inference_steps=num_steps,
252
- generator=generator,
253
- controlnet_conditioning_scale=controlnet_conditioning_scale,
254
- guidance_scale=guidance_scale,
255
- width=new_width,
256
- height=new_height,
257
  ).images[0]
258
- else:
259
- out = pipe(
260
- prompt=prompt,
261
- negative_prompt=negative_prompt,
262
- image=image,
263
- num_inference_steps=num_steps,
264
- generator=generator,
265
- controlnet_conditioning_scale=controlnet_conditioning_scale,
266
- guidance_scale=guidance_scale,
267
- width=new_width,
268
- height=new_height,).images[0]
269
-
270
- return (controlnet_img, out)
271
 
 
 
 
 
 
 
272
 
273
- with gr.Blocks(css="style.css", js=js_func) as demo:
 
 
 
274
  gr.Markdown(DESCRIPTION, elem_id="description")
275
- gr.DuplicateButton(
276
- value="Duplicate Space for private use",
277
- elem_id="duplicate-button",
278
- visible=os.getenv("SHOW_DUPLICATE_BUTTON") == "1",
279
- )
280
 
281
  with gr.Row():
282
  with gr.Column():
283
  with gr.Group():
284
- image = gr.ImageEditor(type="pil", image_mode="L", crop_size=(512, 512))
285
- prompt = gr.Textbox(label="Prompt")
286
  style = gr.Dropdown(label="Style", choices=STYLE_NAMES, value=DEFAULT_STYLE_NAME)
287
- use_hed = gr.Checkbox(label="use HED detector", value=False, info="check this box if you upload an image and want to turn it to a sketch")
288
- use_canny = gr.Checkbox(label="use Canny", value=False, info="check this to use ControlNet canny instead of scribble")
289
  run_button = gr.Button("Run")
290
  with gr.Accordion("Advanced options", open=False):
291
  negative_prompt = gr.Textbox(
292
  label="Negative prompt",
293
  value="longbody, lowres, bad anatomy, bad hands, missing fingers, extra digit, fewer digits, cropped, worst quality, low quality",
294
  )
295
- num_steps = gr.Slider(
296
- label="Number of steps",
297
- minimum=1,
298
- maximum=50,
299
- step=1,
300
- value=25,
301
- )
302
- guidance_scale = gr.Slider(
303
- label="Guidance scale",
304
- minimum=0.1,
305
- maximum=10.0,
306
- step=0.1,
307
- value=5,
308
- )
309
  controlnet_conditioning_scale = gr.Slider(
310
- label="controlnet conditioning scale",
311
- minimum=0.5,
312
- maximum=5.0,
313
- step=0.1,
314
- value=0.9,
315
  )
316
- seed = gr.Slider(
317
- label="Seed",
318
- minimum=0,
319
- maximum=MAX_SEED,
320
- step=1,
321
- value=0,
322
- )
323
- randomize_seed = gr.Checkbox(label="Randomize seed", value=True)
324
-
325
  with gr.Column():
326
  with gr.Group():
327
- image_slider = ImageSlider(position=0.5)
328
-
329
 
330
  inputs = [
331
  image,
@@ -337,19 +343,19 @@ with gr.Blocks(css="style.css", js=js_func) as demo:
337
  controlnet_conditioning_scale,
338
  seed,
339
  use_hed,
340
- use_canny
341
  ]
342
  outputs = [image_slider]
 
 
343
  run_button.click(
344
- fn=randomize_seed_fn,
345
- inputs=[seed, randomize_seed],
346
- outputs=seed,
347
- queue=False,
348
- api_name=False,
349
- ).then(lambda x: None, inputs=None, outputs=image_slider).then(
350
- fn=run, inputs=inputs, outputs=outputs
351
- )
352
-
353
-
354
-
355
- demo.queue().launch()
 
1
  #!/usr/bin/env python
2
 
3
+ import os, random, numpy as np, cv2, torch
 
 
 
4
  import gradio as gr
 
 
 
 
 
 
 
 
 
 
5
  from pathlib import Path
6
  from PIL import Image, ImageOps
7
+ import PIL.Image
8
+
9
+ import spaces
10
+ from diffusers import (
11
+ ControlNetModel,
12
+ StableDiffusionXLControlNetPipeline,
13
+ AutoencoderKL,
14
+ EulerAncestralDiscreteScheduler,
15
+ )
16
+ from controlnet_aux import HEDdetector
17
  from gradio_imageslider import ImageSlider
18
 
19
+ # ──────────────────────────────────────────────────────────────────────────────
20
+ # Small JS helper to force dark theme (kept from your version)
21
+ # ──────────────────────────────────────────────────────────────────────────────
22
  js_func = """
23
  function refresh() {
24
  const url = new URL(window.location);
 
25
  if (url.searchParams.get('__theme') !== 'dark') {
26
  url.searchParams.set('__theme', 'dark');
27
  window.location.href = url.href;
28
  }
29
  }
30
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
 
32
+ # ──────────────────────────────────────────────────────────────────────────────
33
+ # UI text
34
+ # ──────────────────────────────────────────────────────────────────────────────
35
+ DESCRIPTION = '''# Scribble SDXL πŸ–‹οΈπŸŒ„ β€” live updates
36
+ Sketch β†’ image with SDXL ControlNet (scribble/canny). Now with **auto re-inference** when you draw or tweak settings (debounced).
37
+ Models: [xinsir/controlnet-scribble-sdxl-1.0], [xinsir/controlnet-canny-sdxl-1.0], base [stabilityai/stable-diffusion-xl-base-1.0]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  '''
39
 
40
  if not torch.cuda.is_available():
41
+ DESCRIPTION += "\n<p>Running on CPU πŸ₯Ά This demo is intended for GPU Spaces for good latency.</p>"
42
 
43
+ # ──────────────────────────────────────────────────────────────────────────────
44
+ # Styles (unchanged, but refactored into a compact mapping)
45
+ # ──────────────────────────────────────────────────────────────────────────────
46
  style_list = [
47
  {
48
  "name": "(No style)",
 
81
  },
82
  {
83
  "name": "Fantasy art",
84
+ "prompt": "ethereal fantasy concept art of {prompt} . magnificent, celestial, ethereal, painterly, epic, majestic, magical, fantasy art, cover art, dreamy",
85
  "negative_prompt": "photographic, realistic, realism, 35mm film, dslr, cropped, frame, text, deformed, glitch, noise, noisy, off-center, deformed, cross-eyed, closed eyes, bad anatomy, ugly, disfigured, sloppy, duplicate, mutated, black and white",
86
  },
87
  {
 
95
  "negative_prompt": "ugly, deformed, noisy, blurry, low contrast, realism, photorealistic, Western comic style",
96
  },
97
  ]
98
+ styles = {s["name"]: (s["prompt"], s["negative_prompt"]) for s in style_list}
 
99
  STYLE_NAMES = list(styles.keys())
100
  DEFAULT_STYLE_NAME = "(No style)"
101
 
 
102
  def apply_style(style_name: str, positive: str, negative: str = "") -> tuple[str, str]:
103
  p, n = styles.get(style_name, styles[DEFAULT_STYLE_NAME])
104
+ return p.replace("{prompt}", positive), (n + " " + (negative or "")).strip()
105
+
106
+ # ──────────────────────────────────────────────────────────────────────────────
107
+ # Utilities
108
+ # ──────────────────────────────────────────────────────────────────────────────
109
+ def HWC3(x: np.ndarray) -> np.ndarray:
110
+ assert x.dtype == np.uint8
111
+ if x.ndim == 2:
112
+ x = x[:, :, None]
113
+ H, W, C = x.shape
114
+ assert C in (1, 3, 4)
115
+ if C == 3:
116
+ return x
117
+ if C == 1:
118
+ return np.concatenate([x, x, x], axis=2)
119
+ # C == 4
120
+ color = x[:, :, 0:3].astype(np.float32)
121
+ alpha = x[:, :, 3:4].astype(np.float32) / 255.0
122
+ y = color * alpha + 255.0 * (1.0 - alpha)
123
+ y = y.clip(0, 255).astype(np.uint8)
124
+ return y
125
 
126
+ def nms(x, t, s):
127
+ x = cv2.GaussianBlur(x.astype(np.float32), (0, 0), s)
128
+ f1 = np.array([[0,0,0],[1,1,1],[0,0,0]], dtype=np.uint8)
129
+ f2 = np.array([[0,1,0],[0,1,0],[0,1,0]], dtype=np.uint8)
130
+ f3 = np.array([[1,0,0],[0,1,0],[0,0,1]], dtype=np.uint8)
131
+ f4 = np.array([[0,0,1],[0,1,0],[1,0,0]], dtype=np.uint8)
132
+ y = np.zeros_like(x)
133
+ for f in [f1,f2,f3,f4]:
134
+ np.putmask(y, cv2.dilate(x, kernel=f) == x, x)
135
+ z = np.zeros_like(y, dtype=np.uint8)
136
+ z[y > t] = 255
137
+ return z
138
 
139
+ def clamp_size_to_megapixels(w: int, h: int, max_mpx: float = 1.0) -> tuple[int, int]:
140
+ """Scale so that w*h β‰ˆ max_mpx*1e6 (default ~1024x1024 area)."""
141
+ area = w * h
142
+ target = max_mpx * 1_000_000.0
143
+ if area <= target:
144
+ return w, h
145
+ r = (target / area) ** 0.5
146
+ return max(64, int(w * r)) // 8 * 8, max(64, int(h * r)) // 8 * 8 # SDXL likes multiples of 8
147
+
148
+ # ──────────────────────────────────────────────────────────────────────────────
149
+ # Load models once
150
+ # ──────────────────────────────────────────────────────────────────────────────
151
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
152
 
153
+ scheduler = EulerAncestralDiscreteScheduler.from_pretrained(
154
+ "stabilityai/stable-diffusion-xl-base-1.0", subfolder="scheduler"
155
+ )
156
 
157
+ controlnet_scribble = ControlNetModel.from_pretrained(
158
+ "xinsir/controlnet-scribble-sdxl-1.0", torch_dtype=torch.float16 if device.type=="cuda" else torch.float32
 
159
  )
160
  controlnet_canny = ControlNetModel.from_pretrained(
161
+ "xinsir/controlnet-canny-sdxl-1.0", torch_dtype=torch.float16 if device.type=="cuda" else torch.float32
162
+ )
163
+ vae = AutoencoderKL.from_pretrained(
164
+ "madebyollin/sdxl-vae-fp16-fix", torch_dtype=torch.float16 if device.type=="cuda" else torch.float32
165
  )
 
 
166
 
167
+ pipe_scribble = StableDiffusionXLControlNetPipeline.from_pretrained(
168
  "stabilityai/stable-diffusion-xl-base-1.0",
169
+ controlnet=controlnet_scribble,
170
  vae=vae,
171
+ torch_dtype=torch.float16 if device.type=="cuda" else torch.float32,
172
+ scheduler=scheduler,
173
  )
 
 
174
  pipe_canny = StableDiffusionXLControlNetPipeline.from_pretrained(
175
  "stabilityai/stable-diffusion-xl-base-1.0",
176
  controlnet=controlnet_canny,
177
  vae=vae,
178
+ torch_dtype=torch.float16 if device.type=="cuda" else torch.float32,
179
+ scheduler=scheduler,
 
180
  )
181
 
182
+ for p in (pipe_scribble, pipe_canny):
183
+ if device.type == "cuda":
184
+ try:
185
+ p.enable_xformers_memory_efficient_attention()
186
+ except Exception:
187
+ pass
188
+ p.enable_attention_slicing()
189
+ p.to(device)
190
 
191
  MAX_SEED = np.iinfo(np.int32).max
192
+ hed = HEDdetector.from_pretrained("lllyasviel/Annotators")
193
+
194
+ # ──────────────────────────────────────────────────────────────────────────────
195
+ # Core inference
196
+ # ──────────────────────────────────────────────────────────────────────────────
197
+ def _prepare_control_image(image_editor_value, use_hed: bool, use_canny: bool) -> Image.Image:
198
+ """
199
+ Accepts the dict from gr.ImageEditor (contains 'composite'), or a PIL.Image.
200
+ Returns a PIL.Image with control map (scribble/canny/hed result).
201
+ """
202
+ if image_editor_value is None:
203
+ return None
204
+
205
+ if isinstance(image_editor_value, dict) and "composite" in image_editor_value:
206
+ img = image_editor_value["composite"]
207
+ elif isinstance(image_editor_value, PIL.Image.Image):
208
+ img = image_editor_value
209
+ else:
210
+ return None
211
 
212
+ # Convert to RGB for detectors
213
+ if img.mode != "RGB":
214
+ img = img.convert("RGB")
 
215
 
216
+ if use_canny:
217
+ arr = np.array(img)
218
+ edge = cv2.Canny(arr, 100, 200)
219
+ edge = HWC3(edge)
220
+ return Image.fromarray(edge)
221
+
222
+ if use_hed:
223
+ control = hed(img, scribble=False)
224
+ control = np.array(control)
225
+ control = nms(control, 127, 3)
226
+ control = cv2.GaussianBlur(control, (0, 0), 3)
227
+
228
+ # Simulate human sketch width with a soft random threshold
229
+ thr = int(round(random.uniform(0.01, 0.10), 2) * 255)
230
+ control[control > thr] = 255
231
+ control[control < 255] = 0
232
+ return Image.fromarray(control)
233
+
234
+ # Default: use the editor composite as "scribble"
235
+ return img
236
+
237
+ def _image_size_from_editor(image_editor_value, target_mpx=1.0) -> tuple[int, int]:
238
+ if image_editor_value is None:
239
+ return 1024, 1024
240
+ if isinstance(image_editor_value, dict) and "composite" in image_editor_value:
241
+ w, h = image_editor_value["composite"].size
242
+ elif isinstance(image_editor_value, PIL.Image.Image):
243
+ w, h = image_editor_value.size
244
+ else:
245
+ w, h = 1024, 1024
246
+ return clamp_size_to_megapixels(w, h, max_mpx=target_mpx)
247
 
248
+ def _pick_pipe(use_canny: bool):
249
+ return pipe_canny if use_canny else pipe_scribble
250
 
251
+ def _maybe_seed(seed: int):
252
+ if seed is None or seed < 0:
253
+ return None
254
+ return torch.Generator(device=device).manual_seed(int(seed))
255
 
256
  def randomize_seed_fn(seed: int, randomize_seed: bool) -> int:
257
+ return random.randint(0, MAX_SEED) if randomize_seed else int(seed)
 
 
258
 
259
  @spaces.GPU
260
  def run(
261
+ image, # dict from ImageEditor or PIL.Image
262
  prompt: str,
263
  negative_prompt: str,
264
  style_name: str = DEFAULT_STYLE_NAME,
265
+ num_steps: int = 12,
266
+ guidance_scale: float = 5.0,
267
  controlnet_conditioning_scale: float = 1.0,
268
  seed: int = 0,
269
  use_hed: bool = False,
270
  use_canny: bool = False,
271
  progress=gr.Progress(track_tqdm=True),
272
+ ):
273
+ if image is None or (isinstance(prompt, str) and prompt.strip() == ""):
274
+ return (None, None)
275
+
276
+ # Prepare control image + target size (β‰ˆ1MP for speed)
277
+ ctrl_img = _prepare_control_image(image, use_hed=use_hed, use_canny=use_canny)
278
+ w, h = _image_size_from_editor(image, target_mpx=1.0)
279
+
280
+ # Style injection
281
+ prompt_styled, neg_styled = apply_style(style_name, prompt, negative_prompt or "")
282
+
283
+ g = _maybe_seed(seed)
284
+ pipe = _pick_pipe(use_canny)
285
+
286
+ out = pipe(
287
+ prompt=prompt_styled,
288
+ negative_prompt=neg_styled,
289
+ image=ctrl_img,
290
+ num_inference_steps=int(num_steps),
291
+ controlnet_conditioning_scale=float(controlnet_conditioning_scale),
292
+ guidance_scale=float(guidance_scale),
293
+ generator=g,
294
+ width=w, height=h,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
295
  ).images[0]
 
 
 
 
 
 
 
 
 
 
 
 
 
296
 
297
+ # Return (control, output) for ImageSlider
298
+ if isinstance(ctrl_img, Image.Image):
299
+ ci = ctrl_img
300
+ else:
301
+ ci = Image.fromarray(ctrl_img) if ctrl_img is not None else None
302
+ return (ci, out)
303
 
304
+ # ──────────────────────────────────────────────────────────────────────────────
305
+ # UI (with live updates wired via .change on inputs)
306
+ # ──────────────────────────────────────────────────────────────────────────────
307
+ with gr.Blocks(css="style.css", js=js_func, title="Scribble SDXL β€” Live") as demo:
308
  gr.Markdown(DESCRIPTION, elem_id="description")
 
 
 
 
 
309
 
310
  with gr.Row():
311
  with gr.Column():
312
  with gr.Group():
313
+ image = gr.ImageEditor(type="pil", image_mode="L", crop_size=(512, 512), label="Draw / Edit")
314
+ prompt = gr.Textbox(label="Prompt", value="a detailed robot mascot, studio lighting, clean lines")
315
  style = gr.Dropdown(label="Style", choices=STYLE_NAMES, value=DEFAULT_STYLE_NAME)
316
+ use_hed = gr.Checkbox(label="Use HED detector (turn photo β†’ sketch)", value=False)
317
+ use_canny = gr.Checkbox(label="Use Canny (ControlNet Canny)", value=False)
318
  run_button = gr.Button("Run")
319
  with gr.Accordion("Advanced options", open=False):
320
  negative_prompt = gr.Textbox(
321
  label="Negative prompt",
322
  value="longbody, lowres, bad anatomy, bad hands, missing fingers, extra digit, fewer digits, cropped, worst quality, low quality",
323
  )
324
+ num_steps = gr.Slider(label="Steps (lower = faster)", minimum=4, maximum=40, step=1, value=12)
325
+ guidance_scale = gr.Slider(label="Guidance", minimum=0.1, maximum=12.0, step=0.1, value=5.0)
 
 
 
 
 
 
 
 
 
 
 
 
326
  controlnet_conditioning_scale = gr.Slider(
327
+ label="Control strength", minimum=0.5, maximum=2.0, step=0.05, value=1.0
 
 
 
 
328
  )
329
+ seed = gr.Slider(label="Seed (-1 random)", minimum=-1, maximum=MAX_SEED, step=1, value=-1)
330
+ randomize_seed = gr.Checkbox(label="Randomize seed on Run", value=True)
331
+
 
 
 
 
 
 
332
  with gr.Column():
333
  with gr.Group():
334
+ image_slider = ImageSlider(position=0.5, label="Control ↔ Output")
 
335
 
336
  inputs = [
337
  image,
 
343
  controlnet_conditioning_scale,
344
  seed,
345
  use_hed,
346
+ use_canny,
347
  ]
348
  outputs = [image_slider]
349
+
350
+ # Manual "Run" flow (seed randomization, clear slider, then infer)
351
  run_button.click(
352
+ fn=randomize_seed_fn, inputs=[seed, randomize_seed], outputs=seed, queue=False, api_name=False
353
+ ).then(lambda: None, inputs=None, outputs=image_slider).then(fn=run, inputs=inputs, outputs=outputs)
354
+
355
+ # ── Live re-inference hooks (debounced) ───────────────────────────────────
356
+ # Fire when drawing or tweaking settings. 'every' = debounce seconds.
357
+ for comp in [image, prompt, negative_prompt, style, num_steps, guidance_scale,
358
+ controlnet_conditioning_scale, seed, use_hed, use_canny]:
359
+ comp.change(fn=run, inputs=inputs, outputs=outputs, every=0.5, queue=True)
360
+
361
+ demo.queue(concurrency_count=2, max_size=20).launch()