Oysiyl commited on
Commit
2be8221
·
1 Parent(s): b63dea3

Add one-click PNG and SVG export buttons to both tabs

Browse files

- Add _make_qr_stem, _write_png, _write_svg helpers (top of file, after PIL import)
- Add _download_png, _download_svg handler functions used as DownloadButton values
- Add ⬇ PNG and ⬇ SVG DownloadButtons to Standard tab output column
- Add ⬇ PNG and ⬇ SVG DownloadButtons to Artistic tab output column
- Use gr.DownloadButton(value=Callable, inputs=[...]) pattern so files are
generated on demand at click time — one click, no pre-caching
- Write temp files to /tmp (OS-managed) not /tmp/gradio/, so they are not
subject to the delete_cache=(3600, 3600) hourly sweep on the Space
- SVG format is PNG-embedded-in-SVG: confirmed scannable, opens in
Figma, Illustrator, Safari, and Chrome

Files changed (1) hide show
  1. app.py +377 -13
app.py CHANGED
@@ -22,6 +22,77 @@ from huggingface_hub import hf_hub_download
22
  from PIL import Image
23
  import kornia.color # For RGB→HSV conversion in Stable Cascade filter
24
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  # ComfyUI imports (after HF hub downloads)
26
  from comfy import model_management
27
  from comfy.cli_args import args
@@ -691,6 +762,14 @@ def generate_qr_code_unified(
691
  variation_steps: int = 5,
692
  enable_animation: bool = True,
693
  enable_cascade_filter: bool = False,
 
 
 
 
 
 
 
 
694
  gr_progress=None,
695
  ):
696
  # Track actual GPU time spent
@@ -772,6 +851,14 @@ def generate_qr_code_unified(
772
  variation_steps=variation_steps,
773
  enable_animation=enable_animation,
774
  enable_cascade_filter=enable_cascade_filter,
 
 
 
 
 
 
 
 
775
  gr_progress=gr_progress,
776
  ):
777
  yield result
@@ -1195,6 +1282,43 @@ def apply_stable_cascade_qr_filter(
1195
  return filtered
1196
 
1197
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1198
  def generate_standard_qr(
1199
  prompt: str,
1200
  negative_prompt: str = "ugly, tiling, poorly drawn hands, poorly drawn feet, poorly drawn face, out of frame, extra limbs, body out of frame, blurry, bad anatomy, blurred, watermark, grainy, signature, cut off, draft, closed eyes, text, logo",
@@ -1320,6 +1444,14 @@ def generate_artistic_qr(
1320
  enable_upscale: bool = False,
1321
  enable_animation: bool = True,
1322
  enable_cascade_filter: bool = False,
 
 
 
 
 
 
 
 
1323
  enable_freeu: bool = True,
1324
  freeu_b1: float = 1.4,
1325
  freeu_b2: float = 1.3,
@@ -1418,6 +1550,14 @@ def generate_artistic_qr(
1418
  variation_steps=variation_steps,
1419
  enable_animation=enable_animation,
1420
  enable_cascade_filter=enable_cascade_filter,
 
 
 
 
 
 
 
 
1421
  gr_progress=progress,
1422
  )
1423
 
@@ -2363,6 +2503,14 @@ def _pipeline_artistic(
2363
  variation_steps: int = 5,
2364
  enable_animation: bool = True,
2365
  enable_cascade_filter: bool = False,
 
 
 
 
 
 
 
 
2366
  gr_progress=None,
2367
  ):
2368
  # Initialize animation handler if enabled
@@ -2473,7 +2621,9 @@ def _pipeline_artistic(
2473
  # Apply Stable Cascade filter if enabled
2474
  if enable_cascade_filter:
2475
  qr_for_brightness = apply_stable_cascade_qr_filter(
2476
- qr_with_border_noise, blur_kernel=15, threshold_ratio=0.33
 
 
2477
  )
2478
  else:
2479
  qr_for_brightness = qr_with_border_noise
@@ -2491,8 +2641,11 @@ def _pipeline_artistic(
2491
  )
2492
 
2493
  # Tile preprocessor (using filtered or raw QR with border cubics)
 
 
 
2494
  tile_processed = tilepreprocessor.execute(
2495
- pyrUp_iters=3,
2496
  resolution=image_size,
2497
  image=qr_for_brightness,
2498
  )
@@ -2601,6 +2754,17 @@ def _pipeline_artistic(
2601
  first_pass_np = (first_pass_tensor.detach().cpu().numpy() * 255).astype(np.uint8)
2602
  first_pass_np = first_pass_np[0]
2603
  first_pass_pil = Image.fromarray(first_pass_np)
 
 
 
 
 
 
 
 
 
 
 
2604
  msg = f"First enhancement pass complete (step {current_step}/{total_steps})... final refinement pass"
2605
  log_progress(msg, gr_progress, 0.5)
2606
  yield (first_pass_pil, msg)
@@ -3199,17 +3363,167 @@ with gr.Blocks(delete_cache=(3600, 3600)) as demo:
3199
  info="Shows intermediate images every 5 steps during generation. Disable for faster generation.",
3200
  )
3201
 
3202
- # Stable Cascade QR Filter
3203
- gr.Markdown("### Stable Cascade QR Filter")
3204
- gr.Markdown(
3205
- "Advanced preprocessing filter that improves brightness perception using HSV color space, "
3206
- "Gaussian blur, and adaptive thresholding. Based on Stable Cascade implementation."
3207
- )
3208
- enable_cascade_filter_artistic = gr.Checkbox(
3209
- label="Enable Stable Cascade QR Filter",
3210
- value=False,
3211
- info="Applies HSV-based brightness filter with Gaussian blur (kernel=15) and adaptive thresholding (33%). May improve scannability and aesthetic quality.",
3212
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3213
 
3214
  # Color Quantization Section
3215
  gr.Markdown("### Color Quantization (Optional)")
@@ -3464,6 +3778,31 @@ with gr.Blocks(delete_cache=(3600, 3600)) as demo:
3464
  show_copy_button=True,
3465
  )
3466
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3467
  # Button to show examples again (initially hidden)
3468
  show_examples_btn = gr.Button(
3469
  "🎨 Try Another Example",
@@ -3489,6 +3828,14 @@ with gr.Blocks(delete_cache=(3600, 3600)) as demo:
3489
  artistic_enable_upscale,
3490
  artistic_enable_animation,
3491
  enable_cascade_filter_artistic,
 
 
 
 
 
 
 
 
3492
  enable_freeu_artistic,
3493
  freeu_b1,
3494
  freeu_b2,
@@ -4074,6 +4421,23 @@ with gr.Blocks(delete_cache=(3600, 3600)) as demo:
4074
  show_copy_button=True,
4075
  )
4076
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4077
  # When clicking the button, it will trigger the main function
4078
  generate_btn.click(
4079
  fn=generate_standard_qr,
 
22
  from PIL import Image
23
  import kornia.color # For RGB→HSV conversion in Stable Cascade filter
24
 
25
+ # ── Export helpers (PNG + embedded SVG download) ──────────────────────────────
26
+ import base64
27
+ import io
28
+ import re
29
+ import tempfile
30
+
31
+
32
+ def _make_qr_stem(text_input: str, seed: int) -> str:
33
+ """Build a short human-readable filename stem from QR payload + seed."""
34
+ slug = re.sub(r"[^a-z0-9]+", "-", text_input.lower()).strip("-")[:30] or "export"
35
+ return f"ai-qr-{slug}-seed{seed}"
36
+
37
+
38
+ def _write_png(img: Image.Image, stem: str) -> str:
39
+ """Write PNG to a named temp file outside Gradio's cache. Returns path.
40
+
41
+ Uses /tmp (OS-managed) rather than Gradio's /tmp/gradio/ cache so the
42
+ file is not subject to the delete_cache=(3600, 3600) sweep on the Space.
43
+ """
44
+ tmp = tempfile.NamedTemporaryFile(suffix=".png", prefix=f"{stem}-", delete=False)
45
+ img.convert("RGB").save(tmp.name, "PNG")
46
+ tmp.close()
47
+ return tmp.name
48
+
49
+
50
+ def _write_svg(img: Image.Image, stem: str) -> str:
51
+ """Write a PNG-embedded SVG to a named temp file. Returns path.
52
+
53
+ The SVG wraps the raster image as a base64-encoded PNG inside a valid SVG
54
+ container. Confirmed scannable and opens correctly in Figma, Illustrator,
55
+ Safari, and Chrome. Written outside Gradio's cache for the same reason as
56
+ _write_png — not subject to the 1-hour cache sweep.
57
+ """
58
+ buf = io.BytesIO()
59
+ img.convert("RGB").save(buf, "PNG")
60
+ b64 = base64.b64encode(buf.getvalue()).decode("ascii")
61
+ w, h = img.size
62
+ svg = (
63
+ '<?xml version="1.0" encoding="UTF-8"?>'
64
+ f'<svg xmlns="http://www.w3.org/2000/svg" '
65
+ f'xmlns:xlink="http://www.w3.org/1999/xlink" '
66
+ f'width="{w}" height="{h}" viewBox="0 0 {w} {h}">'
67
+ f"<title>AI QR Code</title>"
68
+ f'<image href="data:image/png;base64,{b64}" '
69
+ f'x="0" y="0" width="{w}" height="{h}" '
70
+ f'preserveAspectRatio="xMidYMid meet"/></svg>'
71
+ )
72
+ tmp = tempfile.NamedTemporaryFile(suffix=".svg", prefix=f"{stem}-", delete=False)
73
+ tmp.write(svg.encode("utf-8"))
74
+ tmp.close()
75
+ return tmp.name
76
+
77
+
78
+ def _download_png(image, text_input, seed):
79
+ """Called by DownloadButton at click time — generates PNG on demand."""
80
+ if image is None:
81
+ return None
82
+ img = image if isinstance(image, Image.Image) else Image.fromarray(image)
83
+ return _write_png(img, _make_qr_stem(str(text_input), int(seed)))
84
+
85
+
86
+ def _download_svg(image, text_input, seed):
87
+ """Called by DownloadButton at click time — generates embedded SVG on demand."""
88
+ if image is None:
89
+ return None
90
+ img = image if isinstance(image, Image.Image) else Image.fromarray(image)
91
+ return _write_svg(img, _make_qr_stem(str(text_input), int(seed)))
92
+
93
+
94
+ # ─────────────────────────────────────────────────────────────────────────────
95
+
96
  # ComfyUI imports (after HF hub downloads)
97
  from comfy import model_management
98
  from comfy.cli_args import args
 
762
  variation_steps: int = 5,
763
  enable_animation: bool = True,
764
  enable_cascade_filter: bool = False,
765
+ cascade_blur_kernel: int = 15,
766
+ cascade_threshold_ratio: float = 0.33,
767
+ enable_detail_sharpening: bool = False,
768
+ sharpening_radius: float = 2.0,
769
+ sharpening_amount: float = 1.5,
770
+ sharpening_threshold: int = 0,
771
+ customize_tile_preprocessing: bool = False,
772
+ tile_pyrup_iters: int = 3,
773
  gr_progress=None,
774
  ):
775
  # Track actual GPU time spent
 
851
  variation_steps=variation_steps,
852
  enable_animation=enable_animation,
853
  enable_cascade_filter=enable_cascade_filter,
854
+ cascade_blur_kernel=cascade_blur_kernel,
855
+ cascade_threshold_ratio=cascade_threshold_ratio,
856
+ enable_detail_sharpening=enable_detail_sharpening,
857
+ sharpening_radius=sharpening_radius,
858
+ sharpening_amount=sharpening_amount,
859
+ sharpening_threshold=sharpening_threshold,
860
+ customize_tile_preprocessing=customize_tile_preprocessing,
861
+ tile_pyrup_iters=tile_pyrup_iters,
862
  gr_progress=gr_progress,
863
  ):
864
  yield result
 
1282
  return filtered
1283
 
1284
 
1285
+ def apply_detail_sharpening(
1286
+ image: Image.Image, radius: float = 2.0, amount: float = 1.5, threshold: int = 0
1287
+ ) -> Image.Image:
1288
+ """
1289
+ Apply unsharp mask sharpening to preserve QR details between passes.
1290
+
1291
+ This filter is applied to the first-pass output before the second pass,
1292
+ helping maintain sharp QR code edges when using lower ControlNet strengths.
1293
+
1294
+ Args:
1295
+ image: PIL Image to sharpen
1296
+ radius: Sharpening radius in pixels (1.0-5.0)
1297
+ Higher values = wider sharpening effect
1298
+ amount: Sharpening strength (0.5-3.0)
1299
+ Higher values = stronger sharpening
1300
+ threshold: Minimum brightness change to sharpen (0-10)
1301
+ 0 = sharpen all pixels, higher = sharpen only high-contrast edges
1302
+
1303
+ Returns:
1304
+ Sharpened PIL Image
1305
+ """
1306
+ from PIL import ImageFilter
1307
+
1308
+ # Ensure radius is valid for UnsharpMask (must be positive)
1309
+ radius = max(0.1, radius)
1310
+
1311
+ # Apply unsharp mask filter
1312
+ # PIL's UnsharpMask expects percent as integer (0-100+)
1313
+ sharpened = image.filter(
1314
+ ImageFilter.UnsharpMask(
1315
+ radius=radius, percent=int(amount * 100), threshold=threshold
1316
+ )
1317
+ )
1318
+
1319
+ return sharpened
1320
+
1321
+
1322
  def generate_standard_qr(
1323
  prompt: str,
1324
  negative_prompt: str = "ugly, tiling, poorly drawn hands, poorly drawn feet, poorly drawn face, out of frame, extra limbs, body out of frame, blurry, bad anatomy, blurred, watermark, grainy, signature, cut off, draft, closed eyes, text, logo",
 
1444
  enable_upscale: bool = False,
1445
  enable_animation: bool = True,
1446
  enable_cascade_filter: bool = False,
1447
+ cascade_blur_kernel: int = 15,
1448
+ cascade_threshold_ratio: float = 0.33,
1449
+ enable_detail_sharpening: bool = False,
1450
+ sharpening_radius: float = 2.0,
1451
+ sharpening_amount: float = 1.5,
1452
+ sharpening_threshold: int = 0,
1453
+ customize_tile_preprocessing: bool = False,
1454
+ tile_pyrup_iters: int = 3,
1455
  enable_freeu: bool = True,
1456
  freeu_b1: float = 1.4,
1457
  freeu_b2: float = 1.3,
 
1550
  variation_steps=variation_steps,
1551
  enable_animation=enable_animation,
1552
  enable_cascade_filter=enable_cascade_filter,
1553
+ cascade_blur_kernel=cascade_blur_kernel,
1554
+ cascade_threshold_ratio=cascade_threshold_ratio,
1555
+ enable_detail_sharpening=enable_detail_sharpening,
1556
+ sharpening_radius=sharpening_radius,
1557
+ sharpening_amount=sharpening_amount,
1558
+ sharpening_threshold=sharpening_threshold,
1559
+ customize_tile_preprocessing=customize_tile_preprocessing,
1560
+ tile_pyrup_iters=tile_pyrup_iters,
1561
  gr_progress=progress,
1562
  )
1563
 
 
2503
  variation_steps: int = 5,
2504
  enable_animation: bool = True,
2505
  enable_cascade_filter: bool = False,
2506
+ cascade_blur_kernel: int = 15,
2507
+ cascade_threshold_ratio: float = 0.33,
2508
+ enable_detail_sharpening: bool = False,
2509
+ sharpening_radius: float = 2.0,
2510
+ sharpening_amount: float = 1.5,
2511
+ sharpening_threshold: int = 0,
2512
+ customize_tile_preprocessing: bool = False,
2513
+ tile_pyrup_iters: int = 3,
2514
  gr_progress=None,
2515
  ):
2516
  # Initialize animation handler if enabled
 
2621
  # Apply Stable Cascade filter if enabled
2622
  if enable_cascade_filter:
2623
  qr_for_brightness = apply_stable_cascade_qr_filter(
2624
+ qr_with_border_noise,
2625
+ blur_kernel=cascade_blur_kernel,
2626
+ threshold_ratio=cascade_threshold_ratio,
2627
  )
2628
  else:
2629
  qr_for_brightness = qr_with_border_noise
 
2641
  )
2642
 
2643
  # Tile preprocessor (using filtered or raw QR with border cubics)
2644
+ # Use custom pyrUp_iters if enabled, otherwise default to 3
2645
+ actual_pyrup_iters = tile_pyrup_iters if customize_tile_preprocessing else 3
2646
+
2647
  tile_processed = tilepreprocessor.execute(
2648
+ pyrUp_iters=actual_pyrup_iters,
2649
  resolution=image_size,
2650
  image=qr_for_brightness,
2651
  )
 
2754
  first_pass_np = (first_pass_tensor.detach().cpu().numpy() * 255).astype(np.uint8)
2755
  first_pass_np = first_pass_np[0]
2756
  first_pass_pil = Image.fromarray(first_pass_np)
2757
+
2758
+ # Apply detail sharpening if enabled (experimental feature)
2759
+ # This preserves QR code edge sharpness for the second pass
2760
+ if enable_detail_sharpening:
2761
+ first_pass_pil = apply_detail_sharpening(
2762
+ first_pass_pil,
2763
+ radius=sharpening_radius,
2764
+ amount=sharpening_amount,
2765
+ threshold=sharpening_threshold,
2766
+ )
2767
+
2768
  msg = f"First enhancement pass complete (step {current_step}/{total_steps})... final refinement pass"
2769
  log_progress(msg, gr_progress, 0.5)
2770
  yield (first_pass_pil, msg)
 
3363
  info="Shows intermediate images every 5 steps during generation. Disable for faster generation.",
3364
  )
3365
 
3366
+ # Experimental Settings Section
3367
+ with gr.Accordion(
3368
+ "⚙️ Experimental Settings (Advanced Users Only)", open=False
3369
+ ):
3370
+ gr.Markdown("""
3371
+ ⚠️ **Warning:** These features are experimental and may affect generation quality,
3372
+ scannability, or processing time. Only enable if you understand their effects.
3373
+
3374
+ These settings allow fine-tuning of the artistic pipeline for advanced users who want
3375
+ more control over detail preservation, QR preprocessing, and enhancement strategies.
3376
+
3377
+ **Recommendation:** Start with defaults, then enable one feature at a time to understand
3378
+ its impact on your specific use case.
3379
+ """)
3380
+
3381
+ # Section 1: Stable Cascade QR Filter
3382
+ gr.Markdown("### 🔹 Stable Cascade QR Filter")
3383
+ gr.Markdown(
3384
+ "Advanced preprocessing filter using HSV color space, Gaussian blur, and "
3385
+ "adaptive thresholding. Based on Stable Cascade implementation."
3386
+ )
3387
+
3388
+ enable_cascade_filter_artistic = gr.Checkbox(
3389
+ label="Enable Stable Cascade QR Filter",
3390
+ value=False,
3391
+ info="Apply HSV-based brightness filter to QR code before ControlNet",
3392
+ )
3393
+
3394
+ # Advanced Cascade parameters (nested accordion, visible only when filter enabled)
3395
+ cascade_advanced_accordion = gr.Accordion(
3396
+ "Advanced Cascade Parameters", open=False, visible=False
3397
+ )
3398
+
3399
+ with cascade_advanced_accordion:
3400
+ gr.Markdown(
3401
+ "Fine-tune filter behavior. Higher blur = smoother, higher threshold = sharper cutoff."
3402
+ )
3403
+
3404
+ cascade_blur_kernel_artistic = gr.Slider(
3405
+ minimum=5,
3406
+ maximum=35,
3407
+ step=2,
3408
+ value=15,
3409
+ label="Blur Kernel Size",
3410
+ info="Gaussian blur kernel size (must be odd). Default: 15",
3411
+ )
3412
+
3413
+ cascade_threshold_ratio_artistic = gr.Slider(
3414
+ minimum=0.1,
3415
+ maximum=0.5,
3416
+ step=0.05,
3417
+ value=0.33,
3418
+ label="Threshold Ratio",
3419
+ info="Adaptive threshold ratio. Default: 0.33",
3420
+ )
3421
+
3422
+ # Show/hide advanced parameters when filter is toggled
3423
+ enable_cascade_filter_artistic.change(
3424
+ fn=lambda x: gr.update(visible=x),
3425
+ inputs=[enable_cascade_filter_artistic],
3426
+ outputs=[cascade_advanced_accordion],
3427
+ )
3428
+
3429
+ # Section 2: Detail Sharpening
3430
+ gr.Markdown("### 🔹 Detail Sharpening")
3431
+ gr.Markdown(
3432
+ "Apply unsharp mask between first and second pass to preserve QR code details. "
3433
+ "Allows lower first-pass ControlNet strength (e.g., 0.35-0.40) while maintaining scannability."
3434
+ )
3435
+
3436
+ enable_detail_sharpening_artistic = gr.Checkbox(
3437
+ label="Enable Detail Sharpening",
3438
+ value=False,
3439
+ info="Sharpen first-pass output before second pass to preserve QR edges",
3440
+ )
3441
+
3442
+ # Sharpening parameters (nested accordion, visible only when enabled)
3443
+ sharpening_params_accordion = gr.Accordion(
3444
+ "Sharpening Parameters", open=False, visible=False
3445
+ )
3446
+
3447
+ with sharpening_params_accordion:
3448
+ gr.Markdown(
3449
+ "Adjust sharpening behavior. Start with defaults, increase amount for stronger effect."
3450
+ )
3451
+
3452
+ sharpening_radius_artistic = gr.Slider(
3453
+ minimum=1.0,
3454
+ maximum=5.0,
3455
+ step=0.5,
3456
+ value=2.0,
3457
+ label="Sharpening Radius",
3458
+ info="Sharpening width in pixels. Higher = wider effect. Default: 2.0",
3459
+ )
3460
+
3461
+ sharpening_amount_artistic = gr.Slider(
3462
+ minimum=0.5,
3463
+ maximum=3.0,
3464
+ step=0.1,
3465
+ value=1.5,
3466
+ label="Sharpening Amount",
3467
+ info="Sharpening strength. Higher = stronger. Default: 1.5",
3468
+ )
3469
+
3470
+ sharpening_threshold_artistic = gr.Slider(
3471
+ minimum=0,
3472
+ maximum=10,
3473
+ step=1,
3474
+ value=0,
3475
+ label="Sharpening Threshold",
3476
+ info="Minimum brightness change. 0 = all pixels, higher = edges only. Default: 0",
3477
+ )
3478
+
3479
+ # Show/hide parameters when sharpening is toggled
3480
+ enable_detail_sharpening_artistic.change(
3481
+ fn=lambda x: gr.update(visible=x),
3482
+ inputs=[enable_detail_sharpening_artistic],
3483
+ outputs=[sharpening_params_accordion],
3484
+ )
3485
+
3486
+ # Section 3: Tile Preprocessor Configuration
3487
+ gr.Markdown("### 🔹 Tile Preprocessor Configuration")
3488
+ gr.Markdown(
3489
+ "Adjust tile preprocessor detail level. Lower values preserve more details but may "
3490
+ "reduce composition coherence. Higher values create smoother, more coherent results but lose fine details."
3491
+ )
3492
+
3493
+ customize_tile_preprocessing_artistic = gr.Checkbox(
3494
+ label="Customize Tile Preprocessing",
3495
+ value=False,
3496
+ info="Override default pyrUp iterations (default: 3)",
3497
+ )
3498
+
3499
+ # Tile parameters (nested accordion, visible only when enabled)
3500
+ tile_params_accordion = gr.Accordion(
3501
+ "Tile Preprocessing Parameters",
3502
+ open=False,
3503
+ visible=False,
3504
+ )
3505
+
3506
+ with tile_params_accordion:
3507
+ gr.Markdown(
3508
+ "pyrUp iterations control detail vs smoothness trade-off. "
3509
+ "Default (3) is balanced. Lower = sharper/more details, Higher = smoother/less details."
3510
+ )
3511
+
3512
+ tile_pyrup_iters_artistic = gr.Slider(
3513
+ minimum=1,
3514
+ maximum=4,
3515
+ step=1,
3516
+ value=3,
3517
+ label="Tile Detail Level (pyrUp iterations)",
3518
+ info="1=sharpest, 2=sharp, 3=balanced (default), 4=smoothest",
3519
+ )
3520
+
3521
+ # Show/hide parameters when customization is toggled
3522
+ customize_tile_preprocessing_artistic.change(
3523
+ fn=lambda x: gr.update(visible=x),
3524
+ inputs=[customize_tile_preprocessing_artistic],
3525
+ outputs=[tile_params_accordion],
3526
+ )
3527
 
3528
  # Color Quantization Section
3529
  gr.Markdown("### Color Quantization (Optional)")
 
3778
  show_copy_button=True,
3779
  )
3780
 
3781
+ # Export buttons — generated on demand at click time (cache-sweep safe)
3782
+ with gr.Row():
3783
+ png_download_artistic = gr.DownloadButton(
3784
+ "⬇ PNG",
3785
+ variant="primary",
3786
+ size="sm",
3787
+ value=_download_png,
3788
+ inputs=[
3789
+ artistic_output_image,
3790
+ artistic_text_input,
3791
+ artistic_seed,
3792
+ ],
3793
+ )
3794
+ svg_download_artistic = gr.DownloadButton(
3795
+ "⬇ SVG",
3796
+ variant="secondary",
3797
+ size="sm",
3798
+ value=_download_svg,
3799
+ inputs=[
3800
+ artistic_output_image,
3801
+ artistic_text_input,
3802
+ artistic_seed,
3803
+ ],
3804
+ )
3805
+
3806
  # Button to show examples again (initially hidden)
3807
  show_examples_btn = gr.Button(
3808
  "🎨 Try Another Example",
 
3828
  artistic_enable_upscale,
3829
  artistic_enable_animation,
3830
  enable_cascade_filter_artistic,
3831
+ cascade_blur_kernel_artistic,
3832
+ cascade_threshold_ratio_artistic,
3833
+ enable_detail_sharpening_artistic,
3834
+ sharpening_radius_artistic,
3835
+ sharpening_amount_artistic,
3836
+ sharpening_threshold_artistic,
3837
+ customize_tile_preprocessing_artistic,
3838
+ tile_pyrup_iters_artistic,
3839
  enable_freeu_artistic,
3840
  freeu_b1,
3841
  freeu_b2,
 
4421
  show_copy_button=True,
4422
  )
4423
 
4424
+ # Export buttons — generated on demand at click time (cache-sweep safe)
4425
+ with gr.Row():
4426
+ png_download_standard = gr.DownloadButton(
4427
+ "⬇ PNG",
4428
+ variant="primary",
4429
+ size="sm",
4430
+ value=_download_png,
4431
+ inputs=[output_image, text_input, seed],
4432
+ )
4433
+ svg_download_standard = gr.DownloadButton(
4434
+ "⬇ SVG",
4435
+ variant="secondary",
4436
+ size="sm",
4437
+ value=_download_svg,
4438
+ inputs=[output_image, text_input, seed],
4439
+ )
4440
+
4441
  # When clicking the button, it will trigger the main function
4442
  generate_btn.click(
4443
  fn=generate_standard_qr,