facehuggingjay commited on
Commit
2989199
Β·
verified Β·
1 Parent(s): a5eb878

Upload 3 files

Browse files
Files changed (3) hide show
  1. app.py +544 -0
  2. packages.txt +1 -0
  3. requirements.txt +2 -0
app.py ADDED
@@ -0,0 +1,544 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ G'MIC Filter Studio β€” Hugging Face Spaces
3
+ Runs the full modern G'MIC CLI (from apt) with Gradio UI.
4
+ Gives access to 600+ filters including community stdlib.
5
+ """
6
+
7
+ import gradio as gr
8
+ import subprocess
9
+ import tempfile
10
+ import shutil
11
+ import os
12
+ import sys
13
+ from pathlib import Path
14
+ from PIL import Image
15
+
16
+ # ─── PATH TO GMIC ────────────────────────────────────────────────────────────
17
+ GMIC = shutil.which("gmic") or "/usr/bin/gmic"
18
+ STDLIB_UPDATE = os.path.expanduser("~/.config/gmic/update" + "xxx" + ".gmic")
19
+ # We'll resolve the actual update file path at startup
20
+
21
+ UPDATE_FETCHED = False
22
+
23
+
24
+ def _fetch_update_if_needed():
25
+ """Download the G'MIC community filter definitions once at startup."""
26
+ global UPDATE_FETCHED
27
+ if UPDATE_FETCHED:
28
+ return
29
+ try:
30
+ r = subprocess.run(
31
+ [GMIC, "update"],
32
+ capture_output=True, text=True, timeout=30
33
+ )
34
+ UPDATE_FETCHED = True
35
+ except Exception as e:
36
+ print(f"[gmic update] warning: {e}", file=sys.stderr)
37
+ UPDATE_FETCHED = True # continue even if offline
38
+
39
+
40
+ def _run_gmic(input_path: str, command: str, extra_prefix: str = "") -> tuple[str | None, str]:
41
+ """
42
+ Run a G'MIC command on input_path, return (output_path_or_None, log).
43
+ extra_prefix: optional gmic args before the input file (e.g. extra script loading).
44
+ """
45
+ _fetch_update_if_needed()
46
+
47
+ out_path = input_path.replace(".png", "_out.png")
48
+ # Use a fresh temp dir so filenames are safe
49
+ with tempfile.TemporaryDirectory() as td:
50
+ inp = os.path.join(td, "input.png")
51
+ out = os.path.join(td, "output.png")
52
+ shutil.copy(input_path, inp)
53
+
54
+ cmd = [GMIC]
55
+ if extra_prefix:
56
+ cmd += extra_prefix.split()
57
+ cmd += [inp] + command.split() + ["-o", out]
58
+
59
+ try:
60
+ result = subprocess.run(
61
+ cmd,
62
+ capture_output=True, text=True, timeout=120
63
+ )
64
+ log = (result.stdout + "\n" + result.stderr).strip()
65
+
66
+ if os.path.exists(out):
67
+ final = input_path.replace(".png", "_gmic_result.png")
68
+ shutil.copy(out, final)
69
+ return final, log
70
+ else:
71
+ return None, log or "G'MIC produced no output file."
72
+ except subprocess.TimeoutExpired:
73
+ return None, "ERROR: G'MIC timed out (>120s). Try a smaller image or simpler filter."
74
+ except Exception as e:
75
+ return None, f"ERROR: {e}"
76
+
77
+
78
+ def _pil_to_temp(img: Image.Image) -> str:
79
+ tmp = tempfile.mktemp(suffix=".png")
80
+ img.save(tmp, "PNG")
81
+ return tmp
82
+
83
+
84
+ # ─── FILTER DEFINITIONS ──────────────────────────────────────────────────────
85
+ # Each entry: (display_name, command_template, [(param_name, label, min, max, step, default)])
86
+ # {p_name} in command_template gets replaced with slider value.
87
+ # Grouped into tabs.
88
+
89
+ FILTER_GROUPS = {
90
+
91
+ "πŸ“„ Document Repair": [
92
+ ("Repair Scanned Document",
93
+ "fx_repair_scanned_doc {brightness},{contrast},{gamma},{threshold},0",
94
+ [("brightness", "Brightness", -100, 100, 1, 0),
95
+ ("contrast", "Contrast", -100, 100, 1, 20),
96
+ ("gamma", "Gamma", -100, 100, 1, 0),
97
+ ("threshold", "Threshold %", 0, 100, 1, 50)]),
98
+
99
+ ("Clean Text (afre)",
100
+ "afre_cleantext {threshold},{smoothness},{sharpen}",
101
+ [("threshold", "Threshold", 0, 100, 1, 50),
102
+ ("smoothness", "Smoothness", 0, 10, 1, 2),
103
+ ("sharpen", "Sharpen", 0, 300, 5, 50)]),
104
+
105
+ ("Denoise (Iain fast)",
106
+ "iain_fast_denoise_p {strength},{patch},{search}",
107
+ [("strength", "Strength", 1, 50, 1, 15),
108
+ ("patch", "Patch size", 1, 15, 2, 7),
109
+ ("search", "Search size", 5, 30, 1, 21)]),
110
+
111
+ ("Remove Hot Pixels",
112
+ "fx_remove_hotpixels {threshold},{mask}",
113
+ [("threshold", "Threshold", 0, 30, 1, 5),
114
+ ("mask", "Mask size", 1, 5, 1, 2)]),
115
+
116
+ ("Upscale 2Γ— (Scale2x)",
117
+ "fx_scalenx 2", []),
118
+
119
+ ("Deinterlace",
120
+ "deinterlace 0", []),
121
+ ],
122
+
123
+ "πŸ”§ Inpainting": [
124
+ ("Inpaint – Patch-Based",
125
+ "fx_inpaint_patch {patch},{overlap},{blend}",
126
+ [("patch", "Patch size", 5, 50, 1, 15),
127
+ ("overlap", "Overlap", 0, 10, 1, 5),
128
+ ("blend", "Blend radius", 0, 10, 1, 1)]),
129
+
130
+ ("Inpaint – Multi-Scale",
131
+ "fx_inpaint_matchpatch {patch},{nb_scales}",
132
+ [("patch", "Patch size", 5, 60, 1, 11),
133
+ ("nb_scales", "Scales", 1, 5, 1, 3)]),
134
+
135
+ ("Fill Transparent Area",
136
+ "fx_solidify_td {method},{nb_iters}",
137
+ [("method", "Method", 0, 3, 1, 0),
138
+ ("nb_iters", "Iterations", 1, 5, 1, 1)]),
139
+ ],
140
+
141
+ "✨ Enhance / Restore": [
142
+ ("Anisotropic Smooth (edge-preserving)",
143
+ "smooth {amplitude},{sharpness},{anisotropy},{alpha},{sigma}",
144
+ [("amplitude", "Amplitude", 0, 100, 1, 40),
145
+ ("sharpness", "Sharpness", 0, 2, .1, .7),
146
+ ("anisotropy", "Anisotropy", 0, 1, .05, .5),
147
+ ("alpha", "Alpha", 0, 4, .1, .6),
148
+ ("sigma", "Sigma", 0, 4, .1, 1.1)]),
149
+
150
+ ("Smooth Skin",
151
+ "fx_smooth_skin 2,{tolerance},{smoothness},1,1,50,50,5,2,0.2,3,1,0.05,5,0,50,50",
152
+ [("tolerance", "Skin Tolerance", 0, 1, .05, .5),
153
+ ("smoothness", "Smoothness", 0, 5, .1, 1.0)]),
154
+
155
+ ("Retinex (local contrast)",
156
+ "fx_retinex {variance},{part},{colorspace}",
157
+ [("variance", "Variance", 0, 600, 5, 200),
158
+ ("part", "Mix %", 0, 100, 5, 80),
159
+ ("colorspace", "Colorspace", 0, 3, 1, 1)]),
160
+
161
+ ("DCP Dehaze",
162
+ "jeje_dehaze {amount},{radius}",
163
+ [("amount", "Amount", 0, 100, 1, 50),
164
+ ("radius", "Radius", 1, 50, 1, 15)]),
165
+
166
+ ("Sharpen (Texture)",
167
+ "fx_sharpen_texture {amount},{scale}",
168
+ [("amount", "Amount", 0, 300, 5, 100),
169
+ ("scale", "Scale", 0, 5, .1, 1.0)]),
170
+
171
+ ("Unsharp Mask",
172
+ "unsharp {std},{amount},{threshold}",
173
+ [("std", "Std dev", .5, 10, .5, 2),
174
+ ("amount", "Amount", 0, 5, .1, 1.5),
175
+ ("threshold", "Threshold", 0, 50, 1, 5)]),
176
+
177
+ ("Normalize Local",
178
+ "fx_normalize_tiles {size},{overlap},{normalize}",
179
+ [("size", "Tile size", 16, 256, 8, 48),
180
+ ("overlap", "Overlap", 0, 16, 1, 4),
181
+ ("normalize", "Normalize", 0, 5, 1, 2)]),
182
+ ],
183
+
184
+ "🎨 Color & Tone": [
185
+ ("Simulate Film",
186
+ "fx_simulate_film {category},{index},{strength},{brightness},{contrast},{gamma},{hue},{saturation},{normalize},0,0,0",
187
+ [("category", "Category (0–9)", 0, 9, 1, 0),
188
+ ("index", "Film preset", 1, 10, 1, 1),
189
+ ("strength", "Strength", 0, 100, 5, 80),
190
+ ("brightness", "Brightness", -100,100, 1, 0),
191
+ ("contrast", "Contrast", -100,100, 1, 0),
192
+ ("gamma", "Gamma", -100,100, 1, 0),
193
+ ("hue", "Hue", -180,180,5, 0),
194
+ ("saturation", "Saturation", -100,100, 1, 0),
195
+ ("normalize", "Normalize", 0, 3, 1, 0)]),
196
+
197
+ ("Retro Fade",
198
+ "fx_retrofade {strength},{colorfade},{vignette}",
199
+ [("strength", "Strength", 0, 100, 1, 50),
200
+ ("colorfade", "Color Fade", 0, 100, 1, 30),
201
+ ("vignette", "Vignette", 0, 100, 1, 40)]),
202
+
203
+ ("Color Temperature",
204
+ "fx_tk_colortemp {temperature},{strength}",
205
+ [("temperature", "Temperature (K)", 2000, 10000, 100, 6500),
206
+ ("strength", "Strength", 0, 100, 5, 80)]),
207
+
208
+ ("Vibrance",
209
+ "fx_vibrance {amount}",
210
+ [("amount", "Amount", -100, 200, 1, 50)]),
211
+
212
+ ("Color Grading",
213
+ "jl_colorgrading {shadows},{midtones},{highlights},{saturation}",
214
+ [("shadows", "Shadows shift", -50, 50, 1, 0),
215
+ ("midtones", "Midtones shift", -50, 50, 1, 0),
216
+ ("highlights", "Highlights shift", -50, 50, 1, 0),
217
+ ("saturation", "Saturation", -50, 50, 1, 0)]),
218
+
219
+ ("Equalize HSV",
220
+ "fx_hsv_equalizer {strength},{radius}",
221
+ [("strength", "Strength", 0, 100, 1, 80),
222
+ ("radius", "Radius", 0, 50, 1, 20)]),
223
+ ],
224
+
225
+ "πŸ–ŒοΈ Artistic": [
226
+ ("Kuwahara Painting",
227
+ "fx_kuwahara {radius},{sharpness}",
228
+ [("radius", "Radius", 1, 15, 1, 5),
229
+ ("sharpness", "Sharpness", 0, 5, .1, 2.0)]),
230
+
231
+ ("Brushify",
232
+ "fx_brushify {size},{density},{opacity},{sharpness},{angle}",
233
+ [("size", "Brush size", 1, 30, 1, 6),
234
+ ("density", "Density", 0, 100,1, 50),
235
+ ("opacity", "Opacity", 0, 100,5, 80),
236
+ ("sharpness", "Sharpness", 0, 10,.5, 2),
237
+ ("angle", "Angle (Β°)", -180, 180, 5, 0)]),
238
+
239
+ ("Illustration Look",
240
+ "fx_illustration_look {edges},{smoothness},{sharpness}",
241
+ [("edges", "Edges", 0, 100, 5, 50),
242
+ ("smoothness", "Smoothness", 0, 10, .5, 3),
243
+ ("sharpness", "Sharpness", 0, 300, 10,100)]),
244
+
245
+ ("Sketch (Pencil B&W)",
246
+ "fx_sketchbw {amplitude},{edge_threshold},{smooth},{details}",
247
+ [("amplitude", "Amplitude", 0, 100, 1, 40),
248
+ ("edge_threshold", "Edge threshold", 0, 100, 1, 20),
249
+ ("smooth", "Smoothness", 0, 5,.1, .5),
250
+ ("details", "Details", 0, 5,.1, 1)]),
251
+
252
+ ("Cartoon",
253
+ "cartoon {smooth},{sharp},{threshold},{quantize},{coeff}",
254
+ [("smooth", "Smooth", 0, 10, .5, 3),
255
+ ("sharp", "Sharp", 0, 300, 10,200),
256
+ ("threshold", "Threshold", 0, 100, 1, 30),
257
+ ("quantize", "Quantize", 2, 12, 1, 8),
258
+ ("coeff", "Coeff", 0, 3,.1, 1)]),
259
+
260
+ ("Vector Painting",
261
+ "fx_vector_painting {detail}",
262
+ [("detail", "Detail", 1, 15, 1, 7)]),
263
+
264
+ ("Rodilius",
265
+ "fx_rodilius {amplitude},{size},{smoothness},{sharpness}",
266
+ [("amplitude", "Amplitude", 0, 60, 1, 10),
267
+ ("size", "Size", 0, 20, 1, 6),
268
+ ("smoothness", "Smoothness", 0, 4, .1, 1),
269
+ ("sharpness", "Sharpness", 0, 300,10,200)]),
270
+
271
+ ("Poster Edges",
272
+ "fx_poster_edges {edge_th},{smooth},{quantize}",
273
+ [("edge_th", "Edge threshold", 0, 100, 1, 20),
274
+ ("smooth", "Smoothness", 0, 10, .5, 1),
275
+ ("quantize", "Quantize", 2, 12, 1, 8)]),
276
+ ],
277
+
278
+ "πŸ”¬ Analysis": [
279
+ ("Edge Detection",
280
+ "fx_edges {threshold}",
281
+ [("threshold", "Threshold", 0, 100, 1, 15)]),
282
+
283
+ ("Gradient Norm",
284
+ "gradient_norm", []),
285
+
286
+ ("Structure Tensor",
287
+ "structure_tensors 0", []),
288
+
289
+ ("Frequency Split (detail layer)",
290
+ "-blur {radius} +[-2] -[-1] -n 0,255",
291
+ [("radius", "Radius (blur size)", 1, 30, 1, 5)]),
292
+
293
+ ("Local Variance Map",
294
+ "+blur {radius} sqr[0] blur[1] {radius} sqr[1] - sqrt n 0,255",
295
+ [("radius", "Radius", 1, 20, 1, 5)]),
296
+ ],
297
+
298
+ }
299
+
300
+ # ─── CORE GRADIO FUNCTION ────────────────────────────────────────────────────
301
+
302
+ def apply_filter(input_image: Image.Image, filter_group: str,
303
+ filter_name: str, raw_command: str,
304
+ **slider_values) -> tuple[Image.Image | None, str]:
305
+ """
306
+ Main processing function called by Gradio.
307
+ Either runs the raw_command directly, or looks up the preset.
308
+ """
309
+ if input_image is None:
310
+ return None, "Please upload an image first."
311
+
312
+ tmp_in = _pil_to_temp(input_image)
313
+
314
+ # Decide command to run
315
+ if raw_command.strip():
316
+ cmd = raw_command.strip()
317
+ log_prefix = f"[raw] {cmd}"
318
+ else:
319
+ # Find the filter
320
+ filters = FILTER_GROUPS.get(filter_group, [])
321
+ match = None
322
+ for f in filters:
323
+ if f[0] == filter_name:
324
+ match = f
325
+ break
326
+ if match is None:
327
+ return None, f"Filter '{filter_name}' not found in group '{filter_group}'."
328
+
329
+ name, template, params = match
330
+ vals = {}
331
+ for pname, plabel, pmin, pmax, pstep, pdef in params:
332
+ vals[pname] = slider_values.get(f"sl_{pname}", pdef)
333
+ # Build command
334
+ cmd = template
335
+ for k, v in vals.items():
336
+ cmd = cmd.replace("{" + k + "}", str(v))
337
+ log_prefix = f"[{name}] {cmd}"
338
+
339
+ out_path, log = _run_gmic(tmp_in, cmd)
340
+
341
+ try:
342
+ os.unlink(tmp_in)
343
+ except Exception:
344
+ pass
345
+
346
+ if out_path and os.path.exists(out_path):
347
+ result_img = Image.open(out_path)
348
+ result_img = result_img.copy()
349
+ try:
350
+ os.unlink(out_path)
351
+ except Exception:
352
+ pass
353
+ return result_img, log_prefix + "\n\n" + log
354
+ else:
355
+ return None, log_prefix + "\n\nFAILED:\n" + log
356
+
357
+
358
+ # ─── BUILD UI ────────────────────────────────────────────────────────────────
359
+
360
+ def build_ui():
361
+ # All slider names across all filters
362
+ all_params: dict[str, tuple] = {}
363
+ for group_name, filters in FILTER_GROUPS.items():
364
+ for fname, template, params in filters:
365
+ for pname, plabel, pmin, pmax, pstep, pdef in params:
366
+ key = f"sl_{pname}"
367
+ if key not in all_params:
368
+ all_params[key] = (plabel, pmin, pmax, pstep, pdef)
369
+
370
+ CSS = """
371
+ #gmic-header { font-family: monospace; background: #0a0a0a; padding: 14px 20px;
372
+ border-bottom: 2px solid #d4ff00; }
373
+ #gmic-header h1 { color: #d4ff00; font-size: 1.4em; margin: 0; letter-spacing: .08em; }
374
+ #gmic-header p { color: #888; font-size: .8em; margin: 4px 0 0; }
375
+ .note { font-size: .8em; color: #888; font-family: monospace; }
376
+ """
377
+
378
+ with gr.Blocks(css=CSS, title="G'MIC Filter Studio") as demo:
379
+
380
+ gr.HTML("""
381
+ <div id="gmic-header">
382
+ <h1>G'MIC FILTER STUDIO</h1>
383
+ <p>Full G'MIC stdlib + community filters Β· runs on server Β· no size limit</p>
384
+ </div>
385
+ """)
386
+
387
+ with gr.Row():
388
+
389
+ # ── LEFT: inputs ──
390
+ with gr.Column(scale=1, min_width=300):
391
+ input_img = gr.Image(
392
+ type="pil", label="Input Image",
393
+ image_mode="RGB"
394
+ )
395
+
396
+ gr.HTML("<div class='note'>Upload any size. Server-side processing on CPU.</div>")
397
+
398
+ group_dd = gr.Dropdown(
399
+ label="Filter Category",
400
+ choices=list(FILTER_GROUPS.keys()),
401
+ value=list(FILTER_GROUPS.keys())[0]
402
+ )
403
+
404
+ filter_dd = gr.Dropdown(
405
+ label="Filter",
406
+ choices=[f[0] for f in FILTER_GROUPS[list(FILTER_GROUPS.keys())[0]]],
407
+ value=FILTER_GROUPS[list(FILTER_GROUPS.keys())[0]][0][0]
408
+ )
409
+
410
+ # All sliders, shown/hidden via JS class below
411
+ slider_components: dict[str, gr.Slider] = {}
412
+ with gr.Group(visible=True) as param_group:
413
+ gr.Markdown("**Parameters**")
414
+ for key, (plabel, pmin, pmax, pstep, pdef) in all_params.items():
415
+ sl = gr.Slider(
416
+ minimum=pmin, maximum=pmax, step=pstep, value=pdef,
417
+ label=plabel, visible=False, elem_id=key
418
+ )
419
+ slider_components[key] = sl
420
+
421
+ raw_cmd = gr.Textbox(
422
+ label="Raw G'MIC command (overrides preset if non-empty)",
423
+ placeholder="-blur 3 -edges 5\nfx_repair_scanned_doc 0,20,0,50,0\n...",
424
+ lines=3,
425
+ info="Applied directly to the input image. Ctrl+Enter to run."
426
+ )
427
+
428
+ apply_btn = gr.Button("β–Ά Apply Filter", variant="primary")
429
+
430
+ # ── RIGHT: outputs ──
431
+ with gr.Column(scale=1, min_width=300):
432
+ output_img = gr.Image(type="pil", label="Result", interactive=False)
433
+ log_box = gr.Textbox(label="G'MIC log", lines=6, interactive=False)
434
+
435
+ with gr.Accordion("πŸ“– Filter Reference", open=False):
436
+ gr.Markdown("""
437
+ ### Filters unique to G'MIC (not in ImageJ.js / Photopea)
438
+
439
+ | Category | Filter | G'MIC command |
440
+ |---|---|---|
441
+ | Document | **Repair Scanned Document** | `fx_repair_scanned_doc` |
442
+ | Inpainting | Patch-based inpaint | `fx_inpaint_patch` |
443
+ | Inpainting | Multi-scale inpaint | `fx_inpaint_matchpatch` |
444
+ | Smoothing | Anisotropic diffusion | `smooth` |
445
+ | Skin | Smooth Skin | `fx_smooth_skin` |
446
+ | Tone | Retinex | `fx_retinex` |
447
+ | Tone | DCP Dehaze | `jeje_dehaze` |
448
+ | Film | Simulate Film (600+ LUTs) | `fx_simulate_film` |
449
+ | Artistic | Brushify | `fx_brushify` |
450
+ | Artistic | Kuwahara | `fx_kuwahara` |
451
+ | B&W | Engrave / Filaments | `fx_engrave` |
452
+ | Analysis | Frequency split | pipeline |
453
+ | Upscale | Scale2x | `fx_scalenx` |
454
+ | Denoise | Iain Fast Denoise | `iain_fast_denoise_p` |
455
+
456
+ **Tip β€” find any filter's CLI args:**
457
+ In GIMP G'MIC plugin, set *Output Messages β†’ Verbose (layer name)*, apply the filter,
458
+ and the layer name shows the exact CLI command. Or:
459
+ ```
460
+ gmic echo '$${fx_some_filter}'
461
+ ```
462
+ """)
463
+
464
+ # ── Dynamics ──────────────────────────────────────────────
465
+
466
+ def update_filter_list(group_name):
467
+ filters = FILTER_GROUPS.get(group_name, [])
468
+ choices = [f[0] for f in filters]
469
+ val = choices[0] if choices else None
470
+ return gr.Dropdown(choices=choices, value=val)
471
+
472
+ def update_sliders(group_name, filter_name):
473
+ # Find the matching filter
474
+ filters = FILTER_GROUPS.get(group_name, [])
475
+ match = next((f for f in filters if f[0] == filter_name), None)
476
+ updates = {}
477
+ if match:
478
+ _, _, params = match
479
+ active_pnames = {f"sl_{p[0]}" for p in params}
480
+ for key in slider_components:
481
+ if key in active_pnames:
482
+ param = next(p for p in params if f"sl_{p[0]}" == key)
483
+ updates[slider_components[key]] = gr.Slider(
484
+ visible=True, label=param[1],
485
+ minimum=param[2], maximum=param[3],
486
+ step=param[4], value=param[5]
487
+ )
488
+ else:
489
+ updates[slider_components[key]] = gr.Slider(visible=False)
490
+ else:
491
+ for sl in slider_components.values():
492
+ updates[sl] = gr.Slider(visible=False)
493
+ return list(updates.values())
494
+
495
+ group_dd.change(
496
+ update_filter_list,
497
+ inputs=[group_dd],
498
+ outputs=[filter_dd]
499
+ )
500
+ filter_dd.change(
501
+ update_sliders,
502
+ inputs=[group_dd, filter_dd],
503
+ outputs=list(slider_components.values())
504
+ )
505
+ # Initialize slider visibility on page load
506
+ demo.load(
507
+ update_sliders,
508
+ inputs=[group_dd, filter_dd],
509
+ outputs=list(slider_components.values())
510
+ )
511
+
512
+ # Wire apply button
513
+ all_slider_comps = list(slider_components.values())
514
+ all_slider_keys = list(slider_components.keys())
515
+
516
+ def on_apply(input_image, group_name, filter_name, raw_command, *slider_vals):
517
+ sv = {all_slider_keys[i]: slider_vals[i] for i in range(len(slider_vals))}
518
+ return apply_filter(input_image, group_name, filter_name, raw_command, **sv)
519
+
520
+ apply_btn.click(
521
+ on_apply,
522
+ inputs=[input_img, group_dd, filter_dd, raw_cmd] + all_slider_comps,
523
+ outputs=[output_img, log_box]
524
+ )
525
+
526
+ # Also trigger on Ctrl+Enter in raw_cmd
527
+ raw_cmd.submit(
528
+ on_apply,
529
+ inputs=[input_img, group_dd, filter_dd, raw_cmd] + all_slider_comps,
530
+ outputs=[output_img, log_box]
531
+ )
532
+
533
+ return demo
534
+
535
+
536
+ # ─── STARTUP ────────────────────────────────────────────────────────────────
537
+
538
+ if __name__ == "__main__":
539
+ # Prefetch update in background (non-blocking)
540
+ import threading
541
+ threading.Thread(target=_fetch_update_if_needed, daemon=True).start()
542
+
543
+ demo = build_ui()
544
+ demo.launch()
packages.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ gmic
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ gradio>=4.0
2
+ Pillow