hexware commited on
Commit
50532ba
·
verified ·
1 Parent(s): da0db23

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +562 -669
app.py CHANGED
@@ -4,7 +4,6 @@ import numpy as np
4
  import random
5
  import tempfile
6
  import zipfile
7
- import threading
8
 
9
  import spaces
10
  import torch
@@ -25,55 +24,9 @@ login(token=os.environ.get("hf"))
25
  dtype = torch.bfloat16
26
  device = "cuda" if torch.cuda.is_available() else "cpu"
27
 
28
- # ----------------------------
29
- # Pipeline singleton (fast path)
30
- # ----------------------------
31
- _PIPELINE = None
32
- _PIPELINE_LOCK = threading.Lock()
33
-
34
-
35
- def _enable_fast_cuda_settings():
36
- if not torch.cuda.is_available():
37
- return
38
- try:
39
- torch.backends.cuda.matmul.allow_tf32 = True
40
- torch.backends.cudnn.allow_tf32 = True
41
- torch.backends.cudnn.benchmark = True
42
- torch.set_float32_matmul_precision("high")
43
- try:
44
- torch.backends.cuda.enable_flash_sdp(True)
45
- torch.backends.cuda.enable_mem_efficient_sdp(True)
46
- torch.backends.cuda.enable_math_sdp(False)
47
- except Exception:
48
- pass
49
- except Exception:
50
- pass
51
-
52
-
53
- def get_pipeline():
54
- global _PIPELINE
55
- if _PIPELINE is not None:
56
- return _PIPELINE
57
-
58
- with _PIPELINE_LOCK:
59
- if _PIPELINE is not None:
60
- return _PIPELINE
61
-
62
- _enable_fast_cuda_settings()
63
-
64
- pipe = QwenImageLayeredPipeline.from_pretrained(
65
- "Qwen/Qwen-Image-Layered",
66
- torch_dtype=dtype,
67
- )
68
-
69
- # Fastest mode: keep weights on GPU if available
70
- if device == "cuda":
71
- pipe.to("cuda")
72
- else:
73
- pipe.to("cpu")
74
-
75
- _PIPELINE = pipe
76
- return _PIPELINE
77
 
78
 
79
  def ensure_dirname(path: str):
@@ -115,6 +68,29 @@ def imagelist_to_pptx(img_files):
115
  return tmp.name
116
 
117
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
  def _clamp_int(x, default: int, lo: int, hi: int) -> int:
119
  try:
120
  v = int(x)
@@ -123,72 +99,95 @@ def _clamp_int(x, default: int, lo: int, hi: int) -> int:
123
  return max(lo, min(hi, v))
124
 
125
 
126
- def _normalize_rgba(pil: Image.Image) -> Image.Image:
127
- if pil.mode != "RGBA":
128
- pil = pil.convert("RGB").convert("RGBA")
129
- return pil
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
 
131
 
132
  def _history_choices(history):
133
- """
134
- history: list[dict] with keys:
135
- id, parent, title, layers(list[PIL]), meta(optional)
136
- """
137
  choices = []
138
- by_id = {n["id"]: n for n in history}
139
- for i, node in enumerate(history):
140
- n_layers = len(node.get("layers", []) or [])
141
- parent = node.get("parent")
142
-
143
- depth = 0
144
- pid = parent
145
- seen = set()
146
- while pid and pid in by_id and pid not in seen:
147
- seen.add(pid)
148
- depth += 1
149
- pid = by_id[pid].get("parent")
150
-
151
- prefix = " " * min(depth, 6)
152
- label = f"{prefix}{i+1}. {node.get('title','Node')} (layers={n_layers})"
153
- choices.append((label, node["id"]))
154
  return choices
155
 
156
 
157
- def _find_node(history, node_id):
158
- for n in history:
159
- if n.get("id") == node_id:
160
- return n
161
- return None
162
 
 
 
 
 
163
 
164
- def _layers_to_temp_pngs(layers):
165
- temp_files = []
166
- for img in layers:
167
- tmp = tempfile.NamedTemporaryFile(suffix=".png", delete=False)
168
- _normalize_rgba(img).save(tmp.name)
169
- temp_files.append(tmp.name)
170
- return temp_files
 
 
171
 
172
 
173
- def _export_zip_from_layers(layers):
174
- temp_files = _layers_to_temp_pngs(layers)
175
- with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tmpzip:
176
- with zipfile.ZipFile(tmpzip.name, "w", zipfile.ZIP_DEFLATED) as zipf:
177
- for i, p in enumerate(temp_files):
178
- zipf.write(p, f"layer_{i+1}.png")
179
- return tmpzip.name
180
 
181
 
182
- def _export_pptx_from_layers(layers):
183
- temp_files = _layers_to_temp_pngs(layers)
184
- return imagelist_to_pptx(temp_files)
185
 
186
 
187
- # ----------------------------
188
- # ZeroGPU duration helper
189
- # ----------------------------
190
- def get_duration(
191
- input_image=None,
192
  seed=777,
193
  randomize_seed=False,
194
  prompt=None,
@@ -200,16 +199,23 @@ def get_duration(
200
  use_en_prompt=True,
201
  resolution=640,
202
  gpu_duration=1000,
203
- **kwargs,
204
  ):
205
  return _clamp_int(gpu_duration, default=1000, lo=20, hi=1500)
206
 
207
 
208
- # ----------------------------
209
- # GPU ops
210
- # ----------------------------
211
- @spaces.GPU(duration=get_duration)
212
- def run_decompose_gpu(
 
 
 
 
 
 
 
 
213
  input_image,
214
  seed=777,
215
  randomize_seed=False,
@@ -223,35 +229,28 @@ def run_decompose_gpu(
223
  resolution=640,
224
  gpu_duration=1000,
225
  ):
 
226
  if randomize_seed:
227
  seed = random.randint(0, MAX_SEED)
228
 
229
- resolution = _clamp_int(resolution, default=640, lo=640, hi=1024)
230
- if resolution not in (640, 1024):
231
- resolution = 640
232
 
233
- if isinstance(input_image, list):
234
- input_image = input_image[0]
235
-
236
- if isinstance(input_image, str):
237
- pil_image = Image.open(input_image)
238
- elif isinstance(input_image, Image.Image):
239
- pil_image = input_image
240
- elif isinstance(input_image, np.ndarray):
241
- pil_image = Image.fromarray(input_image)
242
- else:
243
- raise ValueError(f"Unsupported input_image type: {type(input_image)}")
244
-
245
- pil_image = _normalize_rgba(pil_image)
246
-
247
- pipe = get_pipeline()
248
-
249
- gen_device = "cuda" if torch.cuda.is_available() else "cpu"
250
- generator = torch.Generator(device=gen_device).manual_seed(int(seed))
251
 
252
  inputs = {
253
  "image": pil_image,
254
- "generator": generator,
255
  "true_cfg_scale": true_guidance_scale,
256
  "prompt": prompt,
257
  "negative_prompt": neg_prompt,
@@ -263,471 +262,349 @@ def run_decompose_gpu(
263
  "use_en_prompt": use_en_prompt,
264
  }
265
 
 
 
 
266
  with torch.inference_mode():
267
- if torch.cuda.is_available():
268
- with torch.autocast("cuda", dtype=torch.bfloat16):
269
- out = pipe(**inputs)
270
- else:
271
- out = pipe(**inputs)
 
 
 
 
 
 
 
 
 
 
 
272
 
273
- layers_out = out.images[0]
274
- layers_out = [_normalize_rgba(x) for x in layers_out]
275
- return layers_out
276
 
 
 
 
277
 
278
- @spaces.GPU(duration=get_duration)
279
- def run_refine_gpu(
280
- base_layers,
281
- selected_index: int,
282
- seed=777,
283
- randomize_seed=False,
284
- prompt=None,
285
- neg_prompt=" ",
286
- true_guidance_scale=4.0,
287
- num_inference_steps=50,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
288
  sub_layers=3,
289
- cfg_norm=True,
290
- use_en_prompt=True,
291
- resolution=640,
292
  gpu_duration=1000,
293
  ):
294
- if not base_layers or not isinstance(base_layers, list):
295
- raise ValueError("No base layers to refine. Run Decompose first.")
296
 
297
- if randomize_seed:
298
- seed = random.randint(0, MAX_SEED)
 
299
 
300
- resolution = _clamp_int(resolution, default=640, lo=640, hi=1024)
301
- if resolution not in (640, 1024):
302
- resolution = 640
303
 
304
- sub_layers = _clamp_int(sub_layers, default=3, lo=2, hi=10)
 
 
305
 
306
- idx = _clamp_int(selected_index, default=0, lo=0, hi=len(base_layers) - 1)
307
- selected_layer = _normalize_rgba(base_layers[idx])
308
 
309
- pipe = get_pipeline()
 
 
 
 
 
 
 
 
 
310
 
311
- gen_device = "cuda" if torch.cuda.is_available() else "cpu"
312
- generator = torch.Generator(device=gen_device).manual_seed(int(seed))
313
 
314
  inputs = {
315
  "image": selected_layer,
316
- "generator": generator,
317
  "true_cfg_scale": true_guidance_scale,
318
  "prompt": prompt,
319
  "negative_prompt": neg_prompt,
320
  "num_inference_steps": num_inference_steps,
321
  "num_images_per_prompt": 1,
322
- "layers": sub_layers,
323
- "resolution": resolution,
324
- "cfg_normalize": cfg_norm,
325
  "use_en_prompt": use_en_prompt,
326
  }
327
 
328
- with torch.inference_mode():
329
- if torch.cuda.is_available():
330
- with torch.autocast("cuda", dtype=torch.bfloat16):
331
- out = pipe(**inputs)
332
- else:
333
- out = pipe(**inputs)
334
-
335
- refined = out.images[0]
336
- refined = [_normalize_rgba(x) for x in refined]
337
- return refined
338
 
 
 
 
339
 
340
- # ----------------------------
341
- # Gradio glue (history + UX)
342
- # ----------------------------
343
- def _init_state():
344
- return {
345
- "history": [],
346
- "active_node_id": None,
347
- "selected_layer_idx": 0,
348
  }
349
 
350
-
351
- def _set_active_node(state, node_id):
352
- state["active_node_id"] = node_id
353
- state["selected_layer_idx"] = 0
354
- return state
355
-
356
-
357
- def _node_layers_and_picker_updates(node):
358
- layers_out = node.get("layers") or []
359
- layer_choices = [(f"Layer {i+1}", i) for i in range(len(layers_out))]
360
- return layers_out, layer_choices
361
-
362
-
363
- def on_decompose_click(
364
- input_image,
365
- seed,
366
- randomize_seed,
367
- prompt,
368
- neg_prompt,
369
- true_guidance_scale,
370
- num_inference_steps,
371
- layer,
372
- cfg_norm,
373
- use_en_prompt,
374
- resolution,
375
- gpu_duration,
376
- state,
377
- ):
378
- if state is None:
379
- state = _init_state()
380
-
381
- layers_out = run_decompose_gpu(
382
- input_image=input_image,
383
- seed=seed,
384
- randomize_seed=randomize_seed,
385
- prompt=prompt,
386
- neg_prompt=neg_prompt,
387
- true_guidance_scale=true_guidance_scale,
388
- num_inference_steps=num_inference_steps,
389
- layer=layer,
390
- cfg_norm=cfg_norm,
391
- use_en_prompt=use_en_prompt,
392
- resolution=resolution,
393
- gpu_duration=gpu_duration,
394
  )
395
 
396
- node_id = random_str(10)
397
- node = {
398
- "id": node_id,
399
- "parent": None,
400
- "title": "Decompose",
401
- "layers": layers_out,
402
- "meta": {"type": "decompose"},
403
- }
404
 
405
- state["history"].append(node)
406
- _set_active_node(state, node_id)
407
 
408
- choices = _history_choices(state["history"])
409
- _, layer_choices = _node_layers_and_picker_updates(node)
 
410
 
411
- return (
412
- state,
413
- choices,
414
- node_id, # selected history node
415
- layers_out, # base gallery
416
- layers_out, # picker gallery
417
- layer_choices, # dropdown choices
418
- 0, # dropdown selected index
419
- gr.Accordion.update(open=False),
420
- [], # refined gallery cleared
421
- node.get("title", ""),
422
  )
423
 
424
-
425
- def on_history_change(node_id, state):
426
- if state is None:
427
- state = _init_state()
428
-
429
- node = _find_node(state["history"], node_id)
430
- if not node:
431
- return (
432
- state,
433
- [],
434
- [],
435
- [],
436
- 0,
437
- gr.Accordion.update(open=False),
438
- [],
439
- "",
440
- )
441
-
442
- _set_active_node(state, node_id)
443
- layers_out, layer_choices = _node_layers_and_picker_updates(node)
444
-
445
  return (
446
- state,
447
- layers_out,
448
- layers_out,
449
- layer_choices,
450
- 0,
451
- gr.Accordion.update(open=False),
452
- [],
453
- node.get("title", ""),
 
 
 
 
454
  )
455
 
456
 
457
- def on_picker_select(evt: gr.SelectData, state):
458
- if state is None:
459
- state = _init_state()
460
- idx = int(evt.index) if evt and evt.index is not None else 0
461
- state["selected_layer_idx"] = idx
462
- return state, idx
463
 
464
 
465
- def on_layer_dropdown_change(layer_idx, state):
466
- if state is None:
467
- state = _init_state()
 
468
  try:
469
- idx = int(layer_idx)
 
470
  except Exception:
471
- idx = 0
472
- state["selected_layer_idx"] = idx
473
- return state
474
-
475
-
476
- def _append_refine_node(state, parent_node, selected_idx, sub_layers_value, refined_layers):
477
- new_id = random_str(10)
478
- new_node = {
479
- "id": new_id,
480
- "parent": parent_node["id"],
481
- "title": f"Refine: Layer {selected_idx+1}",
482
- "layers": refined_layers,
483
- "meta": {
484
- "type": "refine",
485
- "refine_from": parent_node["id"],
486
- "refine_layer_idx": int(selected_idx),
487
- "sub_layers": int(sub_layers_value),
488
- },
489
- }
490
- state["history"].append(new_node)
491
- _set_active_node(state, new_id)
492
- return new_node
493
 
494
 
495
- def on_refine_click(
496
- seed,
497
- randomize_seed,
498
- prompt,
499
- neg_prompt,
500
- true_guidance_scale,
501
- num_inference_steps,
502
- cfg_norm,
503
- use_en_prompt,
504
- resolution,
505
- gpu_duration,
506
- sub_layers,
507
- state,
508
- history_node_id,
509
- layer_dropdown_idx,
510
- ):
511
- if state is None:
512
- state = _init_state()
513
 
514
- node = _find_node(state["history"], history_node_id)
515
  if not node:
516
- raise gr.Error("No active node selected. Run Decompose first.")
517
-
518
- base_layers = node.get("layers") or []
519
- if not base_layers:
520
- raise gr.Error("Selected node has no layers to refine.")
 
 
 
 
 
 
 
 
521
 
522
- try:
523
- selected_idx = int(layer_dropdown_idx)
524
- except Exception:
525
- selected_idx = int(state.get("selected_layer_idx", 0) or 0)
526
-
527
- refined_layers = run_refine_gpu(
528
- base_layers=base_layers,
529
- selected_index=selected_idx,
530
- seed=seed,
531
- randomize_seed=randomize_seed,
532
- prompt=prompt,
533
- neg_prompt=neg_prompt,
534
- true_guidance_scale=true_guidance_scale,
535
- num_inference_steps=num_inference_steps,
536
- sub_layers=sub_layers,
537
- cfg_norm=cfg_norm,
538
- use_en_prompt=use_en_prompt,
539
- resolution=resolution,
540
- gpu_duration=gpu_duration,
541
- )
542
 
543
- new_node = _append_refine_node(
544
- state=state,
545
- parent_node=node,
546
- selected_idx=selected_idx,
547
- sub_layers_value=sub_layers,
548
- refined_layers=refined_layers,
549
  )
550
 
551
- choices = _history_choices(state["history"])
552
- _, layer_choices = _node_layers_and_picker_updates(new_node)
553
-
554
  return (
555
- state,
556
- choices,
557
- new_node["id"],
558
- refined_layers, # base gallery shows refined node
559
- refined_layers, # picker shows refined node
560
- layer_choices,
561
- 0,
562
- gr.Accordion.update(open=True),
563
- refined_layers, # refined gallery
564
- new_node.get("title", ""),
 
565
  )
566
 
567
 
568
- def on_back_to_parent_click(state, history_node_id):
569
- if state is None:
570
- state = _init_state()
571
-
572
- node = _find_node(state["history"], history_node_id)
573
- if not node:
574
- raise gr.Error("Select a node in History.")
575
-
576
- parent_id = node.get("parent")
577
- if not parent_id:
578
- # already root
579
- layers_out, layer_choices = _node_layers_and_picker_updates(node)
580
  return (
581
- state,
582
- history_node_id,
583
- layers_out,
584
- layers_out,
585
- layer_choices,
586
- 0,
587
- gr.Accordion.update(open=False),
588
  [],
589
- node.get("title", ""),
 
 
 
 
 
 
 
590
  )
591
 
592
- parent = _find_node(state["history"], parent_id)
 
593
  if not parent:
594
- raise gr.Error("Parent not found in history (corrupted history).")
 
595
 
596
- _set_active_node(state, parent_id)
597
- layers_out, layer_choices = _node_layers_and_picker_updates(parent)
598
 
599
- return (
600
- state,
601
- parent_id,
602
- layers_out,
603
- layers_out,
604
- layer_choices,
605
- 0,
606
- gr.Accordion.update(open=False),
607
- [],
608
- parent.get("title", ""),
609
- )
610
 
611
-
612
- def on_redo_refine_click(
613
- seed,
614
- randomize_seed,
615
- prompt,
616
- neg_prompt,
617
- true_guidance_scale,
618
- num_inference_steps,
619
- cfg_norm,
620
- use_en_prompt,
621
- resolution,
622
- gpu_duration,
623
- state,
624
- history_node_id,
625
- ):
626
- if state is None:
627
- state = _init_state()
628
-
629
- node = _find_node(state["history"], history_node_id)
630
  if not node:
631
- raise gr.Error("Select a node in History.")
 
 
 
632
 
633
- meta = node.get("meta") or {}
634
- if meta.get("type") != "refine":
635
- raise gr.Error("Redo refine работает только для refine-узлов (не для Decompose).")
636
 
637
- parent_id = meta.get("refine_from") or node.get("parent")
638
- if not parent_id:
639
- raise gr.Error("Refine node has no parent info.")
640
 
641
- parent = _find_node(state["history"], parent_id)
642
- if not parent:
643
- raise gr.Error("Parent not found in history.")
644
-
645
- base_layers = parent.get("layers") or []
646
- if not base_layers:
647
- raise gr.Error("Parent node has no layers.")
648
-
649
- selected_idx = int(meta.get("refine_layer_idx", 0))
650
- sub_layers_value = int(meta.get("sub_layers", 3))
651
-
652
- refined_layers = run_refine_gpu(
653
- base_layers=base_layers,
654
- selected_index=selected_idx,
655
- seed=seed,
656
- randomize_seed=randomize_seed,
657
- prompt=prompt,
658
- neg_prompt=neg_prompt,
659
- true_guidance_scale=true_guidance_scale,
660
- num_inference_steps=num_inference_steps,
661
- sub_layers=sub_layers_value,
662
- cfg_norm=cfg_norm,
663
- use_en_prompt=use_en_prompt,
664
- resolution=resolution,
665
- gpu_duration=gpu_duration,
666
- )
667
 
668
- new_node = _append_refine_node(
669
- state=state,
670
- parent_node=parent,
671
- selected_idx=selected_idx,
672
- sub_layers_value=sub_layers_value,
673
- refined_layers=refined_layers,
 
 
 
 
 
 
 
 
674
  )
675
 
676
- choices = _history_choices(state["history"])
677
- _, layer_choices = _node_layers_and_picker_updates(new_node)
678
 
679
- return (
680
- state,
681
- choices,
682
- new_node["id"],
683
- refined_layers,
684
- refined_layers,
685
- layer_choices,
686
- 0,
687
- gr.Accordion.update(open=True),
688
- refined_layers,
689
- new_node.get("title", ""),
690
- )
691
 
 
 
692
 
693
- def on_rename_node_click(state, history_node_id, new_name):
694
- if state is None:
695
- state = _init_state()
696
 
697
- node = _find_node(state["history"], history_node_id)
 
 
 
698
  if not node:
699
- raise gr.Error("Select a node in History.")
700
-
701
  new_name = (new_name or "").strip()
702
  if not new_name:
703
- # no-op
704
- choices = _history_choices(state["history"])
705
- return state, choices, history_node_id, node.get("title", "")
 
706
 
707
- node["title"] = new_name
708
- choices = _history_choices(state["history"])
709
- return state, choices, history_node_id, node.get("title", "")
710
 
711
-
712
- def on_export_click(state, node_id, export_kind: str):
713
- if state is None:
714
- state = _init_state()
715
- node = _find_node(state["history"], node_id)
716
  if not node:
717
- raise gr.Error("Select a node in History to export.")
718
- layers = node.get("layers") or []
719
- if not layers:
720
- raise gr.Error("Selected node has no layers to export.")
721
- if export_kind == "pptx":
722
- return _export_pptx_from_layers(layers)
723
- if export_kind == "zip":
724
- return _export_zip_from_layers(layers)
725
- raise gr.Error("Unknown export kind.")
726
-
727
-
728
- # ----------------------------
729
- # UI
730
- # ----------------------------
731
  ensure_dirname(LOG_DIR)
732
 
733
  examples = [
@@ -747,13 +624,17 @@ examples = [
747
  ]
748
 
749
  with gr.Blocks() as demo:
750
- state = gr.State(_init_state())
 
 
 
751
 
752
  with gr.Column(elem_id="col-container"):
753
  gr.HTML(
754
  '<img src="https://qianwen-res.oss-cn-beijing.aliyuncs.com/Qwen-Image/layered/qwen-image-layered-logo.png" '
755
  'alt="Qwen-Image-Layered Logo" width="600" style="display: block; margin: 0 auto;">'
756
  )
 
757
  gr.Markdown(
758
  """
759
  The text prompt is intended to describe the overall content of the input image—including elements that may be partially occluded (e.g., you may specify the text hidden behind a foreground object). It is not designed to control the semantic content of individual layers explicitly.
@@ -764,7 +645,7 @@ The text prompt is intended to describe the overall content of the input image
764
  with gr.Column(scale=1):
765
  input_image = gr.Image(label="Input Image", image_mode="RGBA")
766
 
767
- with gr.Accordion("Advanced Settings", open=False):
768
  prompt = gr.Textbox(
769
  label="Prompt (Optional)",
770
  placeholder="Please enter the prompt to descibe the image. (Optional)",
@@ -778,7 +659,13 @@ The text prompt is intended to describe the overall content of the input image
778
  lines=2,
779
  )
780
 
781
- seed = gr.Slider(label="Seed", minimum=0, maximum=MAX_SEED, step=1, value=0)
 
 
 
 
 
 
782
  randomize_seed = gr.Checkbox(label="Randomize seed", value=True)
783
 
784
  true_guidance_scale = gr.Slider(
@@ -811,7 +698,9 @@ The text prompt is intended to describe the overall content of the input image
811
  value=640,
812
  )
813
 
814
- cfg_norm = gr.Checkbox(label="Whether enable CFG normalization", value=True)
 
 
815
  use_en_prompt = gr.Checkbox(
816
  label="Automatic caption language if no prompt provided, True for EN, False for ZH",
817
  value=True,
@@ -824,52 +713,23 @@ The text prompt is intended to describe the overall content of the input image
824
  placeholder="e.g. 60, 120, 300, 1000, 1500",
825
  )
826
 
827
- decompose_btn = gr.Button("Decompose!", variant="primary")
828
-
829
- with gr.Accordion("History", open=True):
830
- history_dropdown = gr.Dropdown(
831
- label="Nodes",
832
- choices=[],
833
- value=None,
834
- interactive=True,
835
- )
836
-
837
- with gr.Row():
838
- back_parent_btn = gr.Button("← Back to parent")
839
- redo_refine_btn = gr.Button("↺ Redo refine")
840
-
841
- branch_name = gr.Textbox(
842
- label="Branch name",
843
- value="",
844
- lines=1,
845
- placeholder="Rename selected node...",
846
- )
847
- rename_btn = gr.Button("Rename selected node")
848
-
849
- with gr.Row():
850
- export_pptx_btn = gr.Button("Export PPTX (selected node)")
851
- export_zip_btn = gr.Button("Export ZIP (selected node)")
852
 
853
- export_pptx_file = gr.File(label="Download PPTX")
854
- export_zip_file = gr.File(label="Download ZIP")
855
-
856
- with gr.Accordion("Refine layer", open=True):
857
  gr.Markdown("Pick a layer visually (like Photoshop), then refine it into sub-layers.")
858
 
859
- layer_picker = gr.Gallery(
860
- label="Layer Picker (click a thumbnail)",
 
861
  columns=8,
862
  rows=1,
863
- height="auto",
864
  format="png",
865
- show_label=True,
866
  )
867
 
868
- layer_idx_dropdown = gr.Dropdown(
869
- label="Refine layer index",
870
  choices=[],
871
- value=0,
872
- interactive=True,
873
  )
874
 
875
  sub_layers = gr.Slider(
@@ -880,23 +740,69 @@ The text prompt is intended to describe the overall content of the input image
880
  value=3,
881
  )
882
 
883
- refine_btn = gr.Button("Refine selected layer", variant="secondary")
884
 
885
  with gr.Column(scale=2):
886
- base_gallery = gr.Gallery(label="Current node layers", columns=4, rows=1, format="png")
 
 
 
 
 
 
 
 
 
 
 
 
887
 
888
- refined_accordion = gr.Accordion("Refined layers", open=False)
 
 
 
 
 
 
 
 
 
889
  with refined_accordion:
890
- refined_gallery = gr.Gallery(label="Refined layers output", columns=4, rows=1, format="png")
 
 
 
 
 
 
891
 
 
892
  gr.Examples(
893
  examples=examples,
894
  inputs=[input_image],
 
 
 
895
  cache_examples=False,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
896
  )
897
 
898
  # Decompose
899
- decompose_btn.click(
900
  fn=on_decompose_click,
901
  inputs=[
902
  input_image,
@@ -911,151 +817,138 @@ The text prompt is intended to describe the overall content of the input image
911
  use_en_prompt,
912
  resolution,
913
  gpu_duration,
914
- state,
915
  ],
916
  outputs=[
917
- state,
918
- history_dropdown,
 
 
919
  history_dropdown,
920
- base_gallery,
921
- layer_picker,
922
- layer_idx_dropdown,
923
- layer_idx_dropdown,
924
- refined_accordion,
925
- refined_gallery,
926
- branch_name,
927
- ],
928
- )
929
-
930
- # History change
931
- history_dropdown.change(
932
- fn=on_history_change,
933
- inputs=[history_dropdown, state],
934
- outputs=[
935
- state,
936
- base_gallery,
937
- layer_picker,
938
- layer_idx_dropdown,
939
- layer_idx_dropdown,
940
- refined_accordion,
941
  refined_gallery,
942
- branch_name,
 
 
943
  ],
944
  )
945
 
946
- # Picker click
947
- layer_picker.select(
948
- fn=on_picker_select,
949
- inputs=[state],
950
- outputs=[state, layer_idx_dropdown],
951
- )
952
-
953
- # Dropdown change -> state sync
954
- layer_idx_dropdown.change(
955
- fn=on_layer_dropdown_change,
956
- inputs=[layer_idx_dropdown, state],
957
- outputs=[state],
958
- )
959
-
960
  # Refine
961
- refine_btn.click(
962
  fn=on_refine_click,
963
  inputs=[
964
- seed,
965
- randomize_seed,
966
- prompt,
967
- neg_prompt,
968
- true_guidance_scale,
969
- num_inference_steps,
970
- cfg_norm,
971
- use_en_prompt,
972
- resolution,
973
- gpu_duration,
974
  sub_layers,
975
- state,
976
- history_dropdown,
977
- layer_idx_dropdown,
978
  ],
979
  outputs=[
980
- state,
 
 
 
981
  history_dropdown,
982
- history_dropdown,
983
- base_gallery,
984
- layer_picker,
985
- layer_idx_dropdown,
986
- layer_idx_dropdown,
987
- refined_accordion,
988
  refined_gallery,
989
- branch_name,
 
 
990
  ],
991
  )
992
 
993
- # Back to parent
994
- back_parent_btn.click(
995
- fn=on_back_to_parent_click,
996
- inputs=[state, history_dropdown],
997
  outputs=[
998
- state,
 
 
999
  history_dropdown,
1000
- base_gallery,
1001
- layer_picker,
1002
- layer_idx_dropdown,
1003
- layer_idx_dropdown,
1004
- refined_accordion,
1005
  refined_gallery,
1006
- branch_name,
 
 
1007
  ],
1008
  )
1009
 
1010
- # Redo refine (same parent/index/sub_layers as the selected refine node)
1011
- redo_refine_btn.click(
1012
- fn=on_redo_refine_click,
1013
- inputs=[
1014
- seed,
1015
- randomize_seed,
1016
- prompt,
1017
- neg_prompt,
1018
- true_guidance_scale,
1019
- num_inference_steps,
1020
- cfg_norm,
1021
- use_en_prompt,
1022
- resolution,
1023
- gpu_duration,
1024
- state,
1025
  history_dropdown,
 
 
 
 
 
 
 
1026
  ],
 
 
 
 
 
 
1027
  outputs=[
1028
- state,
 
 
 
1029
  history_dropdown,
1030
- history_dropdown,
1031
- base_gallery,
1032
- layer_picker,
1033
- layer_idx_dropdown,
1034
- layer_idx_dropdown,
1035
- refined_accordion,
1036
  refined_gallery,
1037
- branch_name,
 
 
1038
  ],
1039
  )
1040
 
1041
- # Rename selected node
1042
- rename_btn.click(
1043
- fn=on_rename_node_click,
1044
- inputs=[state, history_dropdown, branch_name],
1045
- outputs=[state, history_dropdown, history_dropdown, branch_name],
 
 
 
 
 
 
 
 
 
 
 
 
1046
  )
1047
 
1048
- # Export selected node
1049
- export_pptx_btn.click(
1050
- fn=lambda st, node_id: on_export_click(st, node_id, "pptx"),
1051
- inputs=[state, history_dropdown],
1052
- outputs=[export_pptx_file],
1053
  )
1054
 
1055
- export_zip_btn.click(
1056
- fn=lambda st, node_id: on_export_click(st, node_id, "zip"),
1057
- inputs=[state, history_dropdown],
1058
- outputs=[export_zip_file],
 
1059
  )
1060
 
1061
  if __name__ == "__main__":
 
4
  import random
5
  import tempfile
6
  import zipfile
 
7
 
8
  import spaces
9
  import torch
 
24
  dtype = torch.bfloat16
25
  device = "cuda" if torch.cuda.is_available() else "cpu"
26
 
27
+ pipeline = QwenImageLayeredPipeline.from_pretrained(
28
+ "Qwen/Qwen-Image-Layered", torch_dtype=dtype
29
+ ).to(device)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
 
31
 
32
  def ensure_dirname(path: str):
 
68
  return tmp.name
69
 
70
 
71
+ def export_zip_from_pil(images):
72
+ paths = []
73
+ for img in images:
74
+ tmp = tempfile.NamedTemporaryFile(suffix=".png", delete=False)
75
+ img.save(tmp.name)
76
+ paths.append(tmp.name)
77
+
78
+ with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tmpzip:
79
+ with zipfile.ZipFile(tmpzip.name, "w", zipfile.ZIP_DEFLATED) as zipf:
80
+ for i, p in enumerate(paths):
81
+ zipf.write(p, f"layer_{i+1}.png")
82
+ return tmpzip.name
83
+
84
+
85
+ def export_pptx_from_pil(images):
86
+ paths = []
87
+ for img in images:
88
+ tmp = tempfile.NamedTemporaryFile(suffix=".png", delete=False)
89
+ img.save(tmp.name)
90
+ paths.append(tmp.name)
91
+ return imagelist_to_pptx(paths)
92
+
93
+
94
  def _clamp_int(x, default: int, lo: int, hi: int) -> int:
95
  try:
96
  v = int(x)
 
99
  return max(lo, min(hi, v))
100
 
101
 
102
+ def _norm_resolution(x):
103
+ x = _clamp_int(x, default=640, lo=640, hi=1024)
104
+ if x not in (640, 1024):
105
+ return 640
106
+ return x
107
+
108
+
109
+ def _norm_image(input_image):
110
+ if isinstance(input_image, list):
111
+ input_image = input_image[0]
112
+
113
+ if isinstance(input_image, str):
114
+ return Image.open(input_image).convert("RGB").convert("RGBA")
115
+ if isinstance(input_image, Image.Image):
116
+ return input_image.convert("RGB").convert("RGBA")
117
+ if isinstance(input_image, np.ndarray):
118
+ return Image.fromarray(input_image).convert("RGB").convert("RGBA")
119
+
120
+ raise ValueError(f"Unsupported input_image type: {type(input_image)}")
121
+
122
+
123
+ def _make_node(
124
+ name,
125
+ parent_id,
126
+ images,
127
+ params,
128
+ refine_meta=None,
129
+ ):
130
+ node_id = random_str(10)
131
+ return {
132
+ "id": node_id,
133
+ "name": name,
134
+ "parent": parent_id,
135
+ "children": [],
136
+ "images": images, # list[PIL.Image]
137
+ "params": params, # dict
138
+ "refine_meta": refine_meta, # dict | None
139
+ }
140
 
141
 
142
  def _history_choices(history):
143
+ # Dropdown choices: list of (label, value)
144
+ nodes = history.get("nodes", {})
145
+ order = history.get("order", [])
 
146
  choices = []
147
+ for nid in order:
148
+ n = nodes.get(nid)
149
+ if not n:
150
+ continue
151
+ cnt = len(n.get("images") or [])
152
+ label = f"{n.get('name','node')} · {cnt} layers · {nid}"
153
+ choices.append((label, nid))
 
 
 
 
 
 
 
 
 
154
  return choices
155
 
156
 
157
+ def _chips_for_node(history, node_id):
158
+ nodes = history.get("nodes", {})
159
+ if node_id not in nodes:
160
+ return ""
 
161
 
162
+ n = nodes[node_id]
163
+ parent = n.get("parent")
164
+ children = n.get("children") or []
165
+ root = history.get("root")
166
 
167
+ tags = []
168
+ if node_id == root:
169
+ tags.append("[root]")
170
+ if parent:
171
+ tags.append(f"[parent: {parent}]")
172
+ else:
173
+ tags.append("[parent: —]")
174
+ tags.append(f"[children: {len(children)}]")
175
+ return " ".join(tags)
176
 
177
 
178
+ def _get_current_node(history, node_id):
179
+ nodes = history.get("nodes", {})
180
+ return nodes.get(node_id)
 
 
 
 
181
 
182
 
183
+ def _generator_for_seed(seed):
184
+ gen_device = "cuda" if torch.cuda.is_available() else "cpu"
185
+ return torch.Generator(device=gen_device).manual_seed(seed)
186
 
187
 
188
+ # Dynamic duration callable: must accept the same args as on_decompose_click(). It returns seconds.
189
+ def get_duration_decompose(
190
+ input_image,
 
 
191
  seed=777,
192
  randomize_seed=False,
193
  prompt=None,
 
199
  use_en_prompt=True,
200
  resolution=640,
201
  gpu_duration=1000,
 
202
  ):
203
  return _clamp_int(gpu_duration, default=1000, lo=20, hi=1500)
204
 
205
 
206
+ # Dynamic duration callable for refine (same args + refine-specific)
207
+ def get_duration_refine(
208
+ history,
209
+ current_node_id,
210
+ refine_layer_index=0,
211
+ sub_layers=3,
212
+ gpu_duration=1000,
213
+ ):
214
+ return _clamp_int(gpu_duration, default=1000, lo=20, hi=1500)
215
+
216
+
217
+ @spaces.GPU(duration=get_duration_decompose)
218
+ def on_decompose_click(
219
  input_image,
220
  seed=777,
221
  randomize_seed=False,
 
229
  resolution=640,
230
  gpu_duration=1000,
231
  ):
232
+ # Seed
233
  if randomize_seed:
234
  seed = random.randint(0, MAX_SEED)
235
 
236
+ resolution = _norm_resolution(resolution)
237
+ pil_image = _norm_image(input_image)
 
238
 
239
+ params = {
240
+ "seed": seed,
241
+ "prompt": prompt,
242
+ "negative_prompt": neg_prompt,
243
+ "true_cfg_scale": true_guidance_scale,
244
+ "num_inference_steps": num_inference_steps,
245
+ "layers": layer,
246
+ "resolution": resolution,
247
+ "cfg_normalize": cfg_norm,
248
+ "use_en_prompt": use_en_prompt,
249
+ }
 
 
 
 
 
 
 
250
 
251
  inputs = {
252
  "image": pil_image,
253
+ "generator": _generator_for_seed(seed),
254
  "true_cfg_scale": true_guidance_scale,
255
  "prompt": prompt,
256
  "negative_prompt": neg_prompt,
 
262
  "use_en_prompt": use_en_prompt,
263
  }
264
 
265
+ print("DECOMPOSE INPUTS:", inputs)
266
+ print("REQUESTED GPU DURATION:", gpu_duration)
267
+
268
  with torch.inference_mode():
269
+ out = pipeline(**inputs)
270
+ output_images = out.images[0] # list of PIL
271
+
272
+ # New history (reset)
273
+ history = {"nodes": {}, "order": [], "root": None}
274
+
275
+ root_node = _make_node(
276
+ name="Decompose (root)",
277
+ parent_id=None,
278
+ images=output_images,
279
+ params=params,
280
+ refine_meta=None,
281
+ )
282
+ history["nodes"][root_node["id"]] = root_node
283
+ history["order"].append(root_node["id"])
284
+ history["root"] = root_node["id"]
285
 
286
+ current_node_id = root_node["id"]
 
 
287
 
288
+ # History UI
289
+ choices = _history_choices(history)
290
+ chips = _chips_for_node(history, current_node_id)
291
 
292
+ # Layer selection defaults
293
+ refine_layer_index = 0
294
+ refine_layer_dropdown_choices = [f"Layer {i+1}" for i in range(len(output_images))]
295
+ refine_layer_dropdown_value = (
296
+ refine_layer_dropdown_choices[0] if refine_layer_dropdown_choices else None
297
+ )
298
+
299
+ # Clear exports on new run
300
+ export_pptx = None
301
+ export_zip = None
302
+
303
+ # Refined output empty
304
+ refined_gallery = []
305
+
306
+ return (
307
+ history,
308
+ current_node_id,
309
+ output_images, # decomposed gallery
310
+ output_images, # picker gallery (1 row)
311
+ gr.update(choices=choices, value=current_node_id), # history dropdown
312
+ gr.update(value=refine_layer_index), # refine layer index state
313
+ gr.update(choices=refine_layer_dropdown_choices, value=refine_layer_dropdown_value),
314
+ chips,
315
+ refined_gallery,
316
+ export_pptx,
317
+ export_zip,
318
+ gr.update(open=False), # refined accordion closed
319
+ )
320
+
321
+
322
+ @spaces.GPU(duration=get_duration_refine)
323
+ def on_refine_click(
324
+ history,
325
+ current_node_id,
326
+ refine_layer_index=0,
327
  sub_layers=3,
 
 
 
328
  gpu_duration=1000,
329
  ):
330
+ if not history or not current_node_id:
331
+ raise gr.Error("No active decomposition yet. Run Decompose first.")
332
 
333
+ node = _get_current_node(history, current_node_id)
334
+ if not node:
335
+ raise gr.Error("Current node not found in history.")
336
 
337
+ images = node.get("images") or []
338
+ if not images:
339
+ raise gr.Error("Current node has no images to refine.")
340
 
341
+ idx = _clamp_int(refine_layer_index, default=0, lo=0, hi=max(0, len(images) - 1))
342
+ if idx >= len(images):
343
+ idx = 0
344
 
345
+ selected_layer = images[idx]
 
346
 
347
+ # Reuse params from this node (no separate refine steps/resolution/cfg)
348
+ p = node.get("params") or {}
349
+ seed = p.get("seed", 777)
350
+ prompt = p.get("prompt", None)
351
+ neg_prompt = p.get("negative_prompt", " ")
352
+ true_guidance_scale = p.get("true_cfg_scale", 4.0)
353
+ num_inference_steps = p.get("num_inference_steps", 50)
354
+ resolution = p.get("resolution", 640)
355
+ cfg_norm = p.get("cfg_normalize", True)
356
+ use_en_prompt = p.get("use_en_prompt", True)
357
 
358
+ sub_layers = _clamp_int(sub_layers, default=3, lo=2, hi=10)
 
359
 
360
  inputs = {
361
  "image": selected_layer,
362
+ "generator": _generator_for_seed(seed),
363
  "true_cfg_scale": true_guidance_scale,
364
  "prompt": prompt,
365
  "negative_prompt": neg_prompt,
366
  "num_inference_steps": num_inference_steps,
367
  "num_images_per_prompt": 1,
368
+ "layers": sub_layers, # <-- sub-layers
369
+ "resolution": resolution, # <-- reuse
370
+ "cfg_normalize": cfg_norm, # <-- reuse
371
  "use_en_prompt": use_en_prompt,
372
  }
373
 
374
+ print("REFINE INPUTS:", inputs)
375
+ print("REQUESTED GPU DURATION:", gpu_duration)
 
 
 
 
 
 
 
 
376
 
377
+ with torch.inference_mode():
378
+ out = pipeline(**inputs)
379
+ refined_images = out.images[0]
380
 
381
+ refine_meta = {
382
+ "from_node": current_node_id,
383
+ "layer_index": idx,
384
+ "sub_layers": sub_layers,
 
 
 
 
385
  }
386
 
387
+ child = _make_node(
388
+ name=f"Refine L{idx+1} → {sub_layers}",
389
+ parent_id=current_node_id,
390
+ images=refined_images,
391
+ params=p,
392
+ refine_meta=refine_meta,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
393
  )
394
 
395
+ # Insert child into history
396
+ history["nodes"][child["id"]] = child
397
+ history["order"].append(child["id"])
398
+ history["nodes"][current_node_id].setdefault("children", []).append(child["id"])
 
 
 
 
399
 
400
+ # Move current to child
401
+ current_node_id = child["id"]
402
 
403
+ # Update history dropdown
404
+ choices = _history_choices(history)
405
+ chips = _chips_for_node(history, current_node_id)
406
 
407
+ # Update layer pickers for new current node
408
+ refine_layer_index = 0
409
+ refine_layer_dropdown_choices = [f"Layer {i+1}" for i in range(len(refined_images))]
410
+ refine_layer_dropdown_value = (
411
+ refine_layer_dropdown_choices[0] if refine_layer_dropdown_choices else None
 
 
 
 
 
 
412
  )
413
 
414
+ # Auto open refined accordion (and collapse refined selection is handled via updates)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
415
  return (
416
+ history,
417
+ current_node_id,
418
+ refined_images, # decomposed gallery now shows current node
419
+ refined_images, # picker gallery
420
+ gr.update(choices=choices, value=current_node_id),
421
+ gr.update(value=refine_layer_index),
422
+ gr.update(choices=refine_layer_dropdown_choices, value=refine_layer_dropdown_value),
423
+ chips,
424
+ refined_images, # refined gallery
425
+ None, # export pptx reset
426
+ None, # export zip reset
427
+ gr.update(open=True), # refined accordion open ✅ (replaced Accordion.update)
428
  )
429
 
430
 
431
+ def on_picker_select(evt: gr.SelectData):
432
+ # evt.index for Gallery is int when selecting an item
433
+ try:
434
+ return int(evt.index)
435
+ except Exception:
436
+ return 0
437
 
438
 
439
+ def on_refine_layer_dropdown_change(label):
440
+ # label is "Layer K"
441
+ if not label:
442
+ return 0
443
  try:
444
+ k = int(str(label).split()[-1])
445
+ return max(0, k - 1)
446
  except Exception:
447
+ return 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
448
 
449
 
450
+ def on_history_change(history, node_id):
451
+ if not history or not node_id:
452
+ return (
453
+ None,
454
+ [],
455
+ [],
456
+ gr.update(),
457
+ gr.update(value=0),
458
+ gr.update(choices=[], value=None),
459
+ "",
460
+ [],
461
+ None,
462
+ None,
463
+ gr.update(open=False), # refined accordion closed
464
+ )
 
 
 
465
 
466
+ node = _get_current_node(history, node_id)
467
  if not node:
468
+ return (
469
+ node_id,
470
+ [],
471
+ [],
472
+ gr.update(),
473
+ gr.update(value=0),
474
+ gr.update(choices=[], value=None),
475
+ "",
476
+ [],
477
+ None,
478
+ None,
479
+ gr.update(open=False),
480
+ )
481
 
482
+ imgs = node.get("images") or []
483
+ chips = _chips_for_node(history, node_id)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
484
 
485
+ refine_layer_index = 0
486
+ refine_layer_dropdown_choices = [f"Layer {i+1}" for i in range(len(imgs))]
487
+ refine_layer_dropdown_value = (
488
+ refine_layer_dropdown_choices[0] if refine_layer_dropdown_choices else None
 
 
489
  )
490
 
491
+ # Keep refined panel closed when user jumps around history
 
 
492
  return (
493
+ node_id,
494
+ imgs,
495
+ imgs,
496
+ gr.update(choices=_history_choices(history), value=node_id),
497
+ gr.update(value=refine_layer_index),
498
+ gr.update(choices=refine_layer_dropdown_choices, value=refine_layer_dropdown_value),
499
+ chips,
500
+ [],
501
+ None,
502
+ None,
503
+ gr.update(open=False), # ✅ replaced Accordion.update
504
  )
505
 
506
 
507
+ def on_back_to_parent(history, current_node_id):
508
+ if not history or not current_node_id:
 
 
 
 
 
 
 
 
 
 
509
  return (
510
+ current_node_id,
511
+ [],
 
 
 
 
 
512
  [],
513
+ gr.update(),
514
+ gr.update(value=0),
515
+ gr.update(choices=[], value=None),
516
+ "",
517
+ [],
518
+ None,
519
+ None,
520
+ gr.update(open=False),
521
  )
522
 
523
+ node = _get_current_node(history, current_node_id)
524
+ parent = node.get("parent") if node else None
525
  if not parent:
526
+ # already at root or missing parent
527
+ parent = current_node_id
528
 
529
+ return on_history_change(history, parent)
 
530
 
 
 
 
 
 
 
 
 
 
 
 
531
 
532
+ def on_redo_refine(history, current_node_id, gpu_duration=1000):
533
+ # If current node is a refined node, redo the same refine from its parent with same meta
534
+ if not history or not current_node_id:
535
+ raise gr.Error("No active node.")
536
+ node = _get_current_node(history, current_node_id)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
537
  if not node:
538
+ raise gr.Error("Node not found.")
539
+ meta = node.get("refine_meta")
540
+ if not meta:
541
+ raise gr.Error("This node has no refine metadata to redo (not a refined node).")
542
 
543
+ parent_id = meta.get("from_node")
544
+ layer_index = meta.get("layer_index", 0)
545
+ sub_layers = meta.get("sub_layers", 3)
546
 
547
+ # Temporarily switch to parent for redo logic
548
+ return on_refine_click(history, parent_id, layer_index, sub_layers, gpu_duration)
 
549
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
550
 
551
+ def on_duplicate_node(history, current_node_id):
552
+ if not history or not current_node_id:
553
+ raise gr.Error("No active node to duplicate.")
554
+
555
+ node = _get_current_node(history, current_node_id)
556
+ if not node:
557
+ raise gr.Error("Node not found.")
558
+
559
+ dup = _make_node(
560
+ name=f"{node.get('name','node')} (copy)",
561
+ parent_id=node.get("parent"),
562
+ images=node.get("images") or [],
563
+ params=node.get("params") or {},
564
+ refine_meta=node.get("refine_meta"),
565
  )
566
 
567
+ history["nodes"][dup["id"]] = dup
568
+ history["order"].append(dup["id"])
569
 
570
+ # Attach to same parent if any
571
+ parent = dup.get("parent")
572
+ if parent and parent in history["nodes"]:
573
+ history["nodes"][parent].setdefault("children", []).append(dup["id"])
 
 
 
 
 
 
 
 
574
 
575
+ # Jump to duplicated node
576
+ return on_history_change(history, dup["id"])
577
 
 
 
 
578
 
579
+ def on_rename_node(history, current_node_id, new_name):
580
+ if not history or not current_node_id:
581
+ raise gr.Error("No active node.")
582
+ node = _get_current_node(history, current_node_id)
583
  if not node:
584
+ raise gr.Error("Node not found.")
 
585
  new_name = (new_name or "").strip()
586
  if not new_name:
587
+ raise gr.Error("Name cannot be empty.")
588
+ node["name"] = new_name
589
+ # Update dropdown label list
590
+ return gr.update(choices=_history_choices(history), value=current_node_id)
591
 
 
 
 
592
 
593
+ def on_export_current(history, current_node_id):
594
+ if not history or not current_node_id:
595
+ raise gr.Error("No active node.")
596
+ node = _get_current_node(history, current_node_id)
 
597
  if not node:
598
+ raise gr.Error("Node not found.")
599
+ imgs = node.get("images") or []
600
+ if not imgs:
601
+ raise gr.Error("Node has no images to export.")
602
+
603
+ pptx_path = export_pptx_from_pil(imgs)
604
+ zip_path = export_zip_from_pil(imgs)
605
+ return pptx_path, zip_path
606
+
607
+
 
 
 
 
608
  ensure_dirname(LOG_DIR)
609
 
610
  examples = [
 
624
  ]
625
 
626
  with gr.Blocks() as demo:
627
+ # Server-side state
628
+ history_state = gr.State(None)
629
+ current_node_id_state = gr.State(None)
630
+ refine_layer_index_state = gr.State(0)
631
 
632
  with gr.Column(elem_id="col-container"):
633
  gr.HTML(
634
  '<img src="https://qianwen-res.oss-cn-beijing.aliyuncs.com/Qwen-Image/layered/qwen-image-layered-logo.png" '
635
  'alt="Qwen-Image-Layered Logo" width="600" style="display: block; margin: 0 auto;">'
636
  )
637
+
638
  gr.Markdown(
639
  """
640
  The text prompt is intended to describe the overall content of the input image—including elements that may be partially occluded (e.g., you may specify the text hidden behind a foreground object). It is not designed to control the semantic content of individual layers explicitly.
 
645
  with gr.Column(scale=1):
646
  input_image = gr.Image(label="Input Image", image_mode="RGBA")
647
 
648
+ with gr.Accordion("Settings", open=False):
649
  prompt = gr.Textbox(
650
  label="Prompt (Optional)",
651
  placeholder="Please enter the prompt to descibe the image. (Optional)",
 
659
  lines=2,
660
  )
661
 
662
+ seed = gr.Slider(
663
+ label="Seed",
664
+ minimum=0,
665
+ maximum=MAX_SEED,
666
+ step=1,
667
+ value=0,
668
+ )
669
  randomize_seed = gr.Checkbox(label="Randomize seed", value=True)
670
 
671
  true_guidance_scale = gr.Slider(
 
698
  value=640,
699
  )
700
 
701
+ cfg_norm = gr.Checkbox(
702
+ label="Whether enable CFG normalization", value=True
703
+ )
704
  use_en_prompt = gr.Checkbox(
705
  label="Automatic caption language if no prompt provided, True for EN, False for ZH",
706
  value=True,
 
713
  placeholder="e.g. 60, 120, 300, 1000, 1500",
714
  )
715
 
716
+ run_button = gr.Button("Decompose!", variant="primary")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
717
 
718
+ with gr.Accordion("Refine (Recursive)", open=True):
 
 
 
719
  gr.Markdown("Pick a layer visually (like Photoshop), then refine it into sub-layers.")
720
 
721
+ # One-row “Photoshop-like” picker gallery
722
+ picker_gallery = gr.Gallery(
723
+ label="Layer picker (click a layer)",
724
  columns=8,
725
  rows=1,
 
726
  format="png",
 
727
  )
728
 
729
+ refine_layer_dropdown = gr.Dropdown(
730
+ label="Refine layer (fallback)",
731
  choices=[],
732
+ value=None,
 
733
  )
734
 
735
  sub_layers = gr.Slider(
 
740
  value=3,
741
  )
742
 
743
+ refine_button = gr.Button("Refine selected layer", variant="secondary")
744
 
745
  with gr.Column(scale=2):
746
+ # History / navigation
747
+ with gr.Accordion("History", open=True):
748
+ history_dropdown = gr.Dropdown(
749
+ label="Nodes",
750
+ choices=[],
751
+ value=None,
752
+ )
753
+ chips_md = gr.Markdown("")
754
+
755
+ with gr.Row():
756
+ back_button = gr.Button("← back to parent")
757
+ redo_button = gr.Button("↺ redo refine")
758
+ dup_button = gr.Button("Duplicate node (branch)")
759
 
760
+ with gr.Row():
761
+ rename_text = gr.Textbox(label="Branch name", value="", lines=1)
762
+ rename_button = gr.Button("Rename")
763
+
764
+ # Main outputs
765
+ decomp_accordion = gr.Accordion("Layers (Current node)", open=True)
766
+ with decomp_accordion:
767
+ gallery = gr.Gallery(label="Layers", columns=4, rows=1, format="png")
768
+
769
+ refined_accordion = gr.Accordion("Refined layers (Latest refine)", open=False)
770
  with refined_accordion:
771
+ refined_gallery = gr.Gallery(label="Refined", columns=4, rows=1, format="png")
772
+
773
+ with gr.Row():
774
+ export_button = gr.Button("Export ZIP/PPTX (current node)")
775
+ with gr.Row():
776
+ export_file = gr.File(label="Download PPTX")
777
+ export_zip_file = gr.File(label="Download ZIP")
778
 
779
+ # Examples (run decompose)
780
  gr.Examples(
781
  examples=examples,
782
  inputs=[input_image],
783
+ outputs=[gallery, export_file, export_zip_file],
784
+ fn=lambda img: ([], None, None), # keep examples UI; actual run via click
785
+ examples_per_page=14,
786
  cache_examples=False,
787
+ run_on_click=False,
788
+ )
789
+
790
+ # Picker selection -> refine_layer_index_state
791
+ picker_gallery.select(
792
+ fn=on_picker_select,
793
+ inputs=None,
794
+ outputs=refine_layer_index_state,
795
+ )
796
+
797
+ # Dropdown selection -> refine_layer_index_state
798
+ refine_layer_dropdown.change(
799
+ fn=on_refine_layer_dropdown_change,
800
+ inputs=refine_layer_dropdown,
801
+ outputs=refine_layer_index_state,
802
  )
803
 
804
  # Decompose
805
+ run_button.click(
806
  fn=on_decompose_click,
807
  inputs=[
808
  input_image,
 
817
  use_en_prompt,
818
  resolution,
819
  gpu_duration,
 
820
  ],
821
  outputs=[
822
+ history_state,
823
+ current_node_id_state,
824
+ gallery,
825
+ picker_gallery,
826
  history_dropdown,
827
+ refine_layer_index_state,
828
+ refine_layer_dropdown,
829
+ chips_md,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
830
  refined_gallery,
831
+ export_file,
832
+ export_zip_file,
833
+ refined_accordion, # gr.update(open=...) returned
834
  ],
835
  )
836
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
837
  # Refine
838
+ refine_button.click(
839
  fn=on_refine_click,
840
  inputs=[
841
+ history_state,
842
+ current_node_id_state,
843
+ refine_layer_index_state,
 
 
 
 
 
 
 
844
  sub_layers,
845
+ gpu_duration,
 
 
846
  ],
847
  outputs=[
848
+ history_state,
849
+ current_node_id_state,
850
+ gallery,
851
+ picker_gallery,
852
  history_dropdown,
853
+ refine_layer_index_state,
854
+ refine_layer_dropdown,
855
+ chips_md,
 
 
 
856
  refined_gallery,
857
+ export_file,
858
+ export_zip_file,
859
+ refined_accordion, # ✅ uses gr.update(open=True)
860
  ],
861
  )
862
 
863
+ # History jump
864
+ history_dropdown.change(
865
+ fn=on_history_change,
866
+ inputs=[history_state, history_dropdown],
867
  outputs=[
868
+ current_node_id_state,
869
+ gallery,
870
+ picker_gallery,
871
  history_dropdown,
872
+ refine_layer_index_state,
873
+ refine_layer_dropdown,
874
+ chips_md,
 
 
875
  refined_gallery,
876
+ export_file,
877
+ export_zip_file,
878
+ refined_accordion, # ✅ uses gr.update(open=False)
879
  ],
880
  )
881
 
882
+ # Back to parent
883
+ back_button.click(
884
+ fn=on_back_to_parent,
885
+ inputs=[history_state, current_node_id_state],
886
+ outputs=[
887
+ current_node_id_state,
888
+ gallery,
889
+ picker_gallery,
 
 
 
 
 
 
 
890
  history_dropdown,
891
+ refine_layer_index_state,
892
+ refine_layer_dropdown,
893
+ chips_md,
894
+ refined_gallery,
895
+ export_file,
896
+ export_zip_file,
897
+ refined_accordion,
898
  ],
899
+ )
900
+
901
+ # Redo refine
902
+ redo_button.click(
903
+ fn=on_redo_refine,
904
+ inputs=[history_state, current_node_id_state, gpu_duration],
905
  outputs=[
906
+ history_state,
907
+ current_node_id_state,
908
+ gallery,
909
+ picker_gallery,
910
  history_dropdown,
911
+ refine_layer_index_state,
912
+ refine_layer_dropdown,
913
+ chips_md,
 
 
 
914
  refined_gallery,
915
+ export_file,
916
+ export_zip_file,
917
+ refined_accordion,
918
  ],
919
  )
920
 
921
+ # Duplicate node (branch)
922
+ dup_button.click(
923
+ fn=on_duplicate_node,
924
+ inputs=[history_state, current_node_id_state],
925
+ outputs=[
926
+ current_node_id_state,
927
+ gallery,
928
+ picker_gallery,
929
+ history_dropdown,
930
+ refine_layer_index_state,
931
+ refine_layer_dropdown,
932
+ chips_md,
933
+ refined_gallery,
934
+ export_file,
935
+ export_zip_file,
936
+ refined_accordion,
937
+ ],
938
  )
939
 
940
+ # Rename
941
+ rename_button.click(
942
+ fn=on_rename_node,
943
+ inputs=[history_state, current_node_id_state, rename_text],
944
+ outputs=[history_dropdown],
945
  )
946
 
947
+ # Export
948
+ export_button.click(
949
+ fn=on_export_current,
950
+ inputs=[history_state, current_node_id_state],
951
+ outputs=[export_file, export_zip_file],
952
  )
953
 
954
  if __name__ == "__main__":