lterriel commited on
Commit
35f2afc
·
verified ·
1 Parent(s): 7be1a37

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +1756 -0
app.py ADDED
@@ -0,0 +1,1756 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Torchvision Transforms Playground (Gradio framework)
5
+
6
+ Interactive sandbox to transforming images using torchvision that includes this features:
7
+ - Upload one or multiple images
8
+ - Toggle transforms and tune parameters
9
+ - Preview one example per enabled transform + a final MIX pipeline with multiple random variants
10
+ - See a dynamically generated torchvision Compose code snippet
11
+ - Switch UI language (EN/FR)
12
+ - Disable all transforms in one click
13
+ - Quick links to torchvision documentation per section
14
+
15
+ Usage:
16
+ python3 app.py
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import json
22
+ import random
23
+ from dataclasses import dataclass
24
+ from typing import Any, Dict, List, Tuple, Optional
25
+ from pathlib import Path
26
+
27
+ import gradio as gr
28
+ import torch
29
+ from PIL import Image
30
+ from torchvision.transforms import v2 as T
31
+ from torchvision.transforms.functional import to_pil_image
32
+
33
+ # Assets
34
+ DEFAULT_I18N_PATH = "assets/i18n.json"
35
+ DEFAULT_CSS_PATH = "assets/styles.css"
36
+
37
+
38
+ # Small utilities (image / html / i18n)
39
+ def load_texts_json(path: str) -> str:
40
+ """
41
+ Load i18n JSON string from file.
42
+
43
+ :param path: Path to JSON file.
44
+ :type path: str
45
+ :return: JSON string.
46
+ :rtype: str
47
+ """
48
+ return Path(path).read_text(encoding="utf-8")
49
+
50
+
51
+ def load_css(path: str) -> str:
52
+ """
53
+ Load CSS string from file.
54
+
55
+ :param path: Path to CSS file.
56
+ :type path: str
57
+ :return: CSS string.
58
+ :rtype: str
59
+ """
60
+ return Path(path).read_text(encoding="utf-8")
61
+
62
+
63
+ class I18N:
64
+ """
65
+ Simple i18n manager backed by a JSON string.
66
+
67
+ :param texts_json: JSON string holding UI texts for each language.
68
+ :type texts_json: str
69
+ :param default_lang: Default language key (e.g. "EN").
70
+ :type default_lang: str
71
+ """
72
+
73
+ def __init__(self, texts_json: str, default_lang: str = "EN") -> None:
74
+ self._texts = json.loads(texts_json)
75
+ self._default_lang = default_lang
76
+
77
+ def get(self, lang: str, key: str, default: Optional[str] = None) -> str:
78
+ """
79
+ Get a text key for a given language.
80
+
81
+ :param lang: Language key (e.g. "EN", "FR").
82
+ :type lang: str
83
+ :param key: Text key.
84
+ :type key: str
85
+ :param default: Default value if missing.
86
+ :type default: Optional[str]
87
+ :return: Text value.
88
+ :rtype: str
89
+ """
90
+ return self._texts.get(lang, {}).get(
91
+ key, default if default is not None else ""
92
+ )
93
+
94
+ def section(self, lang: str, section_key: str) -> str:
95
+ """
96
+ Get a section name (translated).
97
+
98
+ :param lang: Language key.
99
+ :type lang: str
100
+ :param section_key: Section identifier (e.g. "geometric").
101
+ :type section_key: str
102
+ :return: Section label.
103
+ :rtype: str
104
+ """
105
+ return (
106
+ self._texts.get(lang, {}).get("sections", {}).get(section_key, section_key)
107
+ )
108
+
109
+ def subtitles(self, lang: str) -> List[str]:
110
+ """
111
+ Get the list of subtitles for a language.
112
+
113
+ :param lang: Language key.
114
+ :type lang: str
115
+ :return: Subtitles list.
116
+ :rtype: List[str]
117
+ """
118
+ return list(self._texts.get(lang, {}).get("app_subtitles", []))
119
+
120
+
121
+ def status_dot(active: bool) -> str:
122
+ """
123
+ Render a small colored dot (HTML).
124
+
125
+ :param active: Whether the section is active.
126
+ :type active: bool
127
+ :return: HTML span for a dot.
128
+ :rtype: str
129
+ """
130
+ color = "#22c55e" if active else "#94a3b8" # green / gray
131
+ return (
132
+ "<span style='display:inline-block;width:10px;height:10px;border-radius:50%;"
133
+ f"background:{color};margin-right:8px;'></span>"
134
+ )
135
+
136
+
137
+ def as_pil_list(gallery_value: Any) -> List[Image.Image]:
138
+ """
139
+ Convert a Gradio Gallery input value to a list of PIL images.
140
+
141
+ :param gallery_value: Gallery value from gr.Gallery.
142
+ :type gallery_value: Any
143
+ :return: List of PIL.Image objects.
144
+ :rtype: List[Image.Image]
145
+ """
146
+ if not gallery_value:
147
+ return []
148
+ imgs: List[Image.Image] = []
149
+ for item in gallery_value:
150
+ if isinstance(item, tuple) and len(item) >= 1:
151
+ imgs.append(item[0])
152
+ else:
153
+ imgs.append(item)
154
+ return imgs
155
+
156
+
157
+ def ensure_pil(x: Any) -> Image.Image:
158
+ """
159
+ Ensure output is a PIL image (convert torch.Tensor if needed).
160
+
161
+ :param x: Transform output (PIL.Image or torch.Tensor).
162
+ :type x: Any
163
+ :return: PIL image.
164
+ :rtype: PIL.Image.Image
165
+ :raises TypeError: If unsupported type.
166
+ """
167
+ if isinstance(x, Image.Image):
168
+ return x
169
+ if isinstance(x, torch.Tensor):
170
+ return to_pil_image(x.clamp(0, 1))
171
+ raise TypeError(f"Unsupported output type: {type(x)}")
172
+
173
+
174
+ @dataclass
175
+ class TransformItem:
176
+ """
177
+ A single transform descriptor.
178
+
179
+ :param name: Display name.
180
+ :type name: str
181
+ :param op: Transform object OR a special sentinel string.
182
+ :type op: Any
183
+ """
184
+
185
+ name: str
186
+ op: Any
187
+
188
+
189
+ class TransformFactory:
190
+ """
191
+ Factory for building:
192
+ - a list of enabled single transforms (one-by-one examples)
193
+ - the final Compose pipeline (MIX)
194
+
195
+ This encapsulates transform construction and keeps UI code clean.
196
+ """
197
+
198
+ TENSOR_ERASE_ONLY = "TENSOR_ERASE_ONLY"
199
+ TENSOR_NORM_ONLY = "TENSOR_NORM_ONLY"
200
+
201
+ def build_single_transforms(self, p: Dict[str, Any]) -> List[TransformItem]:
202
+ """
203
+ Build a list of single transforms (one transform = one operation).
204
+
205
+ :param p: Parameters dict (toggles + params).
206
+ :type p: Dict[str, Any]
207
+ :return: List of enabled transforms.
208
+ :rtype: List[TransformItem]
209
+ """
210
+ L: List[TransformItem] = []
211
+
212
+ # Geometric
213
+ if p["use_pad"]:
214
+ L.append(
215
+ TransformItem(
216
+ "Pad",
217
+ T.Pad(
218
+ padding=int(p["pad_px"]),
219
+ fill=int(p["pad_fill"]),
220
+ padding_mode=p["pad_mode"],
221
+ ),
222
+ )
223
+ )
224
+ if p["use_resize"]:
225
+ L.append(
226
+ TransformItem(
227
+ "Resize", T.Resize((int(p["resize_size"]), int(p["resize_size"])))
228
+ )
229
+ )
230
+ if p["use_center_crop"]:
231
+ L.append(
232
+ TransformItem(
233
+ "CenterCrop",
234
+ T.CenterCrop((int(p["crop_size"]), int(p["crop_size"]))),
235
+ )
236
+ )
237
+ if p["use_five_crop"]:
238
+ L.append(
239
+ TransformItem(
240
+ "FiveCrop",
241
+ T.FiveCrop((int(p["five_crop_size"]), int(p["five_crop_size"]))),
242
+ )
243
+ )
244
+ if p["use_random_perspective"]:
245
+ L.append(
246
+ TransformItem(
247
+ "RandomPerspective",
248
+ T.RandomPerspective(
249
+ distortion_scale=float(p["persp_dist"]), p=float(p["persp_p"])
250
+ ),
251
+ )
252
+ )
253
+ if p["use_random_rotation"]:
254
+ L.append(
255
+ TransformItem(
256
+ "RandomRotation", T.RandomRotation(degrees=int(p["rot_deg"]))
257
+ )
258
+ )
259
+ if p["use_random_affine"]:
260
+ L.append(
261
+ TransformItem(
262
+ "RandomAffine",
263
+ T.RandomAffine(
264
+ degrees=int(p["aff_deg"]),
265
+ translate=(
266
+ float(p["aff_translate"]),
267
+ float(p["aff_translate"]),
268
+ ),
269
+ scale=(float(p["aff_scale_min"]), float(p["aff_scale_max"])),
270
+ shear=int(p["aff_shear"]),
271
+ ),
272
+ )
273
+ )
274
+ if p["use_elastic"]:
275
+ L.append(
276
+ TransformItem(
277
+ "ElasticTransform",
278
+ T.ElasticTransform(
279
+ alpha=float(p["elastic_alpha"]), sigma=float(p["elastic_sigma"])
280
+ ),
281
+ )
282
+ )
283
+ if p["use_random_crop"]:
284
+ L.append(
285
+ TransformItem(
286
+ "RandomCrop",
287
+ T.RandomCrop((int(p["rand_crop_size"]), int(p["rand_crop_size"]))),
288
+ )
289
+ )
290
+ if p["use_rrc"]:
291
+ L.append(
292
+ TransformItem(
293
+ "RandomResizedCrop",
294
+ T.RandomResizedCrop(
295
+ (int(p["rrc_size"]), int(p["rrc_size"])),
296
+ scale=(float(p["rrc_scale_min"]), float(p["rrc_scale_max"])),
297
+ ),
298
+ )
299
+ )
300
+
301
+ # Photometric
302
+ if p["use_grayscale"]:
303
+ L.append(
304
+ TransformItem(
305
+ "Grayscale",
306
+ T.Grayscale(num_output_channels=int(p["gray_channels"])),
307
+ )
308
+ )
309
+ if p["use_cj"]:
310
+ L.append(
311
+ TransformItem(
312
+ "ColorJitter",
313
+ T.ColorJitter(
314
+ brightness=float(p["cj_b"]),
315
+ contrast=float(p["cj_c"]),
316
+ saturation=float(p["cj_s"]),
317
+ hue=float(p["cj_h"]),
318
+ ),
319
+ )
320
+ )
321
+ if p["use_blur"]:
322
+ k = int(p["blur_k"])
323
+ if k % 2 == 0:
324
+ k += 1
325
+ L.append(
326
+ TransformItem(
327
+ "GaussianBlur",
328
+ T.GaussianBlur(
329
+ kernel_size=k,
330
+ sigma=(float(p["blur_sigma_min"]), float(p["blur_sigma_max"])),
331
+ ),
332
+ )
333
+ )
334
+ if p["use_inv"]:
335
+ L.append(TransformItem("RandomInvert", T.RandomInvert(p=float(p["inv_p"]))))
336
+ if p["use_post"]:
337
+ L.append(
338
+ TransformItem(
339
+ "RandomPosterize",
340
+ T.RandomPosterize(bits=int(p["post_bits"]), p=float(p["post_p"])),
341
+ )
342
+ )
343
+ if p["use_sol"]:
344
+ L.append(
345
+ TransformItem(
346
+ "RandomSolarize",
347
+ T.RandomSolarize(
348
+ threshold=int(p["sol_thresh"]), p=float(p["sol_p"])
349
+ ),
350
+ )
351
+ )
352
+ if p["use_sharp"]:
353
+ L.append(
354
+ TransformItem(
355
+ "RandomAdjustSharpness",
356
+ T.RandomAdjustSharpness(
357
+ sharpness_factor=float(p["sharp_factor"]), p=float(p["sharp_p"])
358
+ ),
359
+ )
360
+ )
361
+ if p["use_autoc"]:
362
+ L.append(TransformItem("RandomAutocontrast", T.RandomAutocontrast()))
363
+ if p["use_eq"]:
364
+ L.append(TransformItem("RandomEqualize", T.RandomEqualize()))
365
+ if p["use_jpeg"]:
366
+ L.append(
367
+ TransformItem(
368
+ "JPEG", T.JPEG(quality=(int(p["jpeg_qmin"]), int(p["jpeg_qmax"])))
369
+ )
370
+ )
371
+
372
+ # Policies
373
+ if p["use_autoaugment"]:
374
+ policy = getattr(T.AutoAugmentPolicy, p["aa_policy"])
375
+ L.append(TransformItem("AutoAugment", T.AutoAugment(policy=policy)))
376
+ if p["use_randaugment"]:
377
+ L.append(
378
+ TransformItem(
379
+ "RandAugment",
380
+ T.RandAugment(
381
+ num_ops=int(p["ra_num_ops"]), magnitude=int(p["ra_mag"])
382
+ ),
383
+ )
384
+ )
385
+ if p["use_trivial"]:
386
+ L.append(
387
+ TransformItem(
388
+ "TrivialAugmentWide",
389
+ T.TrivialAugmentWide(num_magnitude_bins=int(p["tw_bins"])),
390
+ )
391
+ )
392
+ if p["use_augmix"]:
393
+ L.append(
394
+ TransformItem(
395
+ "AugMix",
396
+ T.AugMix(
397
+ severity=int(p["am_severity"]),
398
+ mixture_width=int(p["am_width"]),
399
+ chain_depth=int(p["am_depth"]),
400
+ alpha=float(p["am_alpha"]),
401
+ ),
402
+ )
403
+ )
404
+
405
+ # Randomly-applied
406
+ if p["use_hflip"]:
407
+ L.append(
408
+ TransformItem(
409
+ "RandomHorizontalFlip",
410
+ T.RandomHorizontalFlip(p=float(p["hflip_p"])),
411
+ )
412
+ )
413
+ if p["use_vflip"]:
414
+ L.append(
415
+ TransformItem(
416
+ "RandomVerticalFlip", T.RandomVerticalFlip(p=float(p["vflip_p"]))
417
+ )
418
+ )
419
+ if p["use_random_apply"]:
420
+ inner = [T.RandomCrop((int(p["ra_crop"]), int(p["ra_crop"])))]
421
+ L.append(
422
+ TransformItem(
423
+ "RandomApply(RandomCrop)",
424
+ T.RandomApply(transforms=inner, p=float(p["ra_p"])),
425
+ )
426
+ )
427
+
428
+ # Tensor bonus as single examples
429
+ if p["use_erase"]:
430
+ L.append(TransformItem("RandomErasing (tensor)", self.TENSOR_ERASE_ONLY))
431
+ if p["use_norm"]:
432
+ L.append(TransformItem("Normalize (tensor)", self.TENSOR_NORM_ONLY))
433
+
434
+ return L
435
+
436
+ def build_compose(self, p: Dict[str, Any]) -> T.Compose:
437
+ """
438
+ Build the final Compose pipeline (MIX).
439
+
440
+ :param p: Parameters dict.
441
+ :type p: Dict[str, Any]
442
+ :return: Torchvision v2 Compose transform.
443
+ :rtype: torchvision.transforms.v2.Compose
444
+ """
445
+ transforms: List[Any] = []
446
+
447
+ # Geometric
448
+ if p["use_pad"]:
449
+ transforms.append(
450
+ T.Pad(
451
+ padding=int(p["pad_px"]),
452
+ fill=int(p["pad_fill"]),
453
+ padding_mode=p["pad_mode"],
454
+ )
455
+ )
456
+ if p["use_resize"]:
457
+ transforms.append(T.Resize((int(p["resize_size"]), int(p["resize_size"]))))
458
+ if p["use_center_crop"]:
459
+ transforms.append(T.CenterCrop((int(p["crop_size"]), int(p["crop_size"]))))
460
+ if p["use_five_crop"]:
461
+ transforms.append(
462
+ T.FiveCrop((int(p["five_crop_size"]), int(p["five_crop_size"])))
463
+ ) # returns 5
464
+ if p["use_random_perspective"]:
465
+ transforms.append(
466
+ T.RandomPerspective(
467
+ distortion_scale=float(p["persp_dist"]), p=float(p["persp_p"])
468
+ )
469
+ )
470
+ if p["use_random_rotation"]:
471
+ transforms.append(T.RandomRotation(degrees=int(p["rot_deg"])))
472
+ if p["use_random_affine"]:
473
+ transforms.append(
474
+ T.RandomAffine(
475
+ degrees=int(p["aff_deg"]),
476
+ translate=(float(p["aff_translate"]), float(p["aff_translate"])),
477
+ scale=(float(p["aff_scale_min"]), float(p["aff_scale_max"])),
478
+ shear=int(p["aff_shear"]),
479
+ )
480
+ )
481
+ if p["use_elastic"]:
482
+ transforms.append(
483
+ T.ElasticTransform(
484
+ alpha=float(p["elastic_alpha"]), sigma=float(p["elastic_sigma"])
485
+ )
486
+ )
487
+ if p["use_random_crop"]:
488
+ transforms.append(
489
+ T.RandomCrop((int(p["rand_crop_size"]), int(p["rand_crop_size"])))
490
+ )
491
+ if p["use_rrc"]:
492
+ transforms.append(
493
+ T.RandomResizedCrop(
494
+ (int(p["rrc_size"]), int(p["rrc_size"])),
495
+ scale=(float(p["rrc_scale_min"]), float(p["rrc_scale_max"])),
496
+ )
497
+ )
498
+
499
+ # Photometric
500
+ if p["use_grayscale"]:
501
+ transforms.append(T.Grayscale(num_output_channels=int(p["gray_channels"])))
502
+ if p["use_cj"]:
503
+ transforms.append(
504
+ T.ColorJitter(
505
+ brightness=float(p["cj_b"]),
506
+ contrast=float(p["cj_c"]),
507
+ saturation=float(p["cj_s"]),
508
+ hue=float(p["cj_h"]),
509
+ )
510
+ )
511
+ if p["use_blur"]:
512
+ k = int(p["blur_k"])
513
+ if k % 2 == 0:
514
+ k += 1
515
+ transforms.append(
516
+ T.GaussianBlur(
517
+ kernel_size=k,
518
+ sigma=(float(p["blur_sigma_min"]), float(p["blur_sigma_max"])),
519
+ )
520
+ )
521
+ if p["use_inv"]:
522
+ transforms.append(T.RandomInvert(p=float(p["inv_p"])))
523
+ if p["use_post"]:
524
+ transforms.append(
525
+ T.RandomPosterize(bits=int(p["post_bits"]), p=float(p["post_p"]))
526
+ )
527
+ if p["use_sol"]:
528
+ transforms.append(
529
+ T.RandomSolarize(threshold=int(p["sol_thresh"]), p=float(p["sol_p"]))
530
+ )
531
+ if p["use_sharp"]:
532
+ transforms.append(
533
+ T.RandomAdjustSharpness(
534
+ sharpness_factor=float(p["sharp_factor"]), p=float(p["sharp_p"])
535
+ )
536
+ )
537
+ if p["use_autoc"]:
538
+ transforms.append(T.RandomAutocontrast())
539
+ if p["use_eq"]:
540
+ transforms.append(T.RandomEqualize())
541
+ if p["use_jpeg"]:
542
+ transforms.append(
543
+ T.JPEG(quality=(int(p["jpeg_qmin"]), int(p["jpeg_qmax"])))
544
+ )
545
+
546
+ # Policies
547
+ if p["use_autoaugment"]:
548
+ policy = getattr(T.AutoAugmentPolicy, p["aa_policy"])
549
+ transforms.append(T.AutoAugment(policy=policy))
550
+ if p["use_randaugment"]:
551
+ transforms.append(
552
+ T.RandAugment(num_ops=int(p["ra_num_ops"]), magnitude=int(p["ra_mag"]))
553
+ )
554
+ if p["use_trivial"]:
555
+ transforms.append(
556
+ T.TrivialAugmentWide(num_magnitude_bins=int(p["tw_bins"]))
557
+ )
558
+ if p["use_augmix"]:
559
+ transforms.append(
560
+ T.AugMix(
561
+ severity=int(p["am_severity"]),
562
+ mixture_width=int(p["am_width"]),
563
+ chain_depth=int(p["am_depth"]),
564
+ alpha=float(p["am_alpha"]),
565
+ )
566
+ )
567
+
568
+ # Randomly-applied
569
+ if p["use_hflip"]:
570
+ transforms.append(T.RandomHorizontalFlip(p=float(p["hflip_p"])))
571
+ if p["use_vflip"]:
572
+ transforms.append(T.RandomVerticalFlip(p=float(p["vflip_p"])))
573
+ if p["use_random_apply"]:
574
+ inner = [T.RandomCrop((int(p["ra_crop"]), int(p["ra_crop"])))]
575
+ transforms.append(T.RandomApply(transforms=inner, p=float(p["ra_p"])))
576
+
577
+ # Tensor-only
578
+ need_tensor = p["use_erase"] or p["use_norm"]
579
+ if need_tensor:
580
+ transforms.append(T.ToImage())
581
+ transforms.append(T.ToDtype(torch.float32, scale=True))
582
+
583
+ if p["use_erase"]:
584
+ transforms.append(
585
+ T.RandomErasing(
586
+ p=float(p["erase_p"]),
587
+ scale=(
588
+ float(p["erase_scale_min"]),
589
+ float(p["erase_scale_max"]),
590
+ ),
591
+ ratio=(
592
+ float(p["erase_ratio_min"]),
593
+ float(p["erase_ratio_max"]),
594
+ ),
595
+ value="random",
596
+ )
597
+ )
598
+
599
+ if p["use_norm"]:
600
+ mean = [float(x.strip()) for x in str(p["norm_mean"]).split(",")]
601
+ std = [float(x.strip()) for x in str(p["norm_std"]).split(",")]
602
+ transforms.append(T.Normalize(mean=mean, std=std))
603
+
604
+ return T.Compose(transforms)
605
+
606
+ def tensor_only_example(self, p: Dict[str, Any], which: str) -> T.Compose:
607
+ """
608
+ Create a local tensor-only pipeline used for single-transform previews.
609
+
610
+ :param p: Parameters dict.
611
+ :type p: Dict[str, Any]
612
+ :param which: Sentinel ("TENSOR_ERASE_ONLY" or "TENSOR_NORM_ONLY").
613
+ :type which: str
614
+ :return: Compose pipeline that converts image to tensor then applies the tensor op.
615
+ :rtype: torchvision.transforms.v2.Compose
616
+ """
617
+ base = [T.ToImage(), T.ToDtype(torch.float32, scale=True)]
618
+
619
+ if which == self.TENSOR_ERASE_ONLY:
620
+ base.append(
621
+ T.RandomErasing(
622
+ p=float(p["erase_p"]),
623
+ scale=(float(p["erase_scale_min"]), float(p["erase_scale_max"])),
624
+ ratio=(float(p["erase_ratio_min"]), float(p["erase_ratio_max"])),
625
+ value="random",
626
+ )
627
+ )
628
+ elif which == self.TENSOR_NORM_ONLY:
629
+ mean = [float(x.strip()) for x in str(p["norm_mean"]).split(",")]
630
+ std = [float(x.strip()) for x in str(p["norm_std"]).split(",")]
631
+ base.append(T.Normalize(mean=mean, std=std))
632
+ else:
633
+ raise ValueError(f"Unknown tensor sentinel: {which}")
634
+
635
+ return T.Compose(base)
636
+
637
+
638
+ class CodeGenerator:
639
+ """
640
+ Generate the torchvision v2 Compose python code from parameters.
641
+ """
642
+
643
+ def to_code(self, p: Dict[str, Any]) -> str:
644
+ """
645
+ Create a code snippet reflecting the current pipeline in interface.
646
+
647
+ :param p: Parameters dict.
648
+ :type p: Dict[str, Any]
649
+ :return: Python code snippet.
650
+ :rtype: str
651
+ """
652
+ lines: List[str] = [
653
+ "from torchvision.transforms import v2 as T",
654
+ "import torch",
655
+ "",
656
+ "transform = T.Compose([",
657
+ ]
658
+
659
+ def add(s: str) -> None:
660
+ lines.append(f" {s},")
661
+
662
+ # Geometric
663
+ if p["use_pad"]:
664
+ add(
665
+ f"T.Pad(padding={int(p['pad_px'])}, fill={int(p['pad_fill'])}, padding_mode='{p['pad_mode']}')"
666
+ )
667
+ if p["use_resize"]:
668
+ add(f"T.Resize(({int(p['resize_size'])}, {int(p['resize_size'])}))")
669
+ if p["use_center_crop"]:
670
+ add(f"T.CenterCrop(({int(p['crop_size'])}, {int(p['crop_size'])}))")
671
+ if p["use_five_crop"]:
672
+ add(
673
+ f"T.FiveCrop(({int(p['five_crop_size'])}, {int(p['five_crop_size'])})) # returns 5 images"
674
+ )
675
+ if p["use_random_perspective"]:
676
+ add(
677
+ f"T.RandomPerspective(distortion_scale={float(p['persp_dist']):.2f}, p={float(p['persp_p']):.2f})"
678
+ )
679
+ if p["use_random_rotation"]:
680
+ add(f"T.RandomRotation(degrees={int(p['rot_deg'])})")
681
+ if p["use_random_affine"]:
682
+ add(
683
+ "T.RandomAffine("
684
+ f"degrees={int(p['aff_deg'])}, "
685
+ f"translate=({float(p['aff_translate']):.2f}, {float(p['aff_translate']):.2f}), "
686
+ f"scale=({float(p['aff_scale_min']):.2f}, {float(p['aff_scale_max']):.2f}), "
687
+ f"shear={int(p['aff_shear'])}"
688
+ ")"
689
+ )
690
+ if p["use_elastic"]:
691
+ add(
692
+ f"T.ElasticTransform(alpha={float(p['elastic_alpha']):.2f}, sigma={float(p['elastic_sigma']):.2f})"
693
+ )
694
+ if p["use_random_crop"]:
695
+ add(
696
+ f"T.RandomCrop(({int(p['rand_crop_size'])}, {int(p['rand_crop_size'])}))"
697
+ )
698
+ if p["use_rrc"]:
699
+ add(
700
+ "T.RandomResizedCrop("
701
+ f"({int(p['rrc_size'])}, {int(p['rrc_size'])}), "
702
+ f"scale=({float(p['rrc_scale_min']):.3f}, {float(p['rrc_scale_max']):.3f})"
703
+ ")"
704
+ )
705
+
706
+ # Photometric
707
+ if p["use_grayscale"]:
708
+ add(f"T.Grayscale(num_output_channels={int(p['gray_channels'])})")
709
+ if p["use_cj"]:
710
+ add(
711
+ "T.ColorJitter("
712
+ f"brightness={float(p['cj_b']):.2f}, contrast={float(p['cj_c']):.2f}, "
713
+ f"saturation={float(p['cj_s']):.2f}, hue={float(p['cj_h']):.2f}"
714
+ ")"
715
+ )
716
+ if p["use_blur"]:
717
+ k = int(p["blur_k"])
718
+ if k % 2 == 0:
719
+ k += 1
720
+ add(
721
+ f"T.GaussianBlur(kernel_size={k}, sigma=({float(p['blur_sigma_min']):.2f}, {float(p['blur_sigma_max']):.2f}))"
722
+ )
723
+ if p["use_inv"]:
724
+ add(f"T.RandomInvert(p={float(p['inv_p']):.2f})")
725
+ if p["use_post"]:
726
+ add(
727
+ f"T.RandomPosterize(bits={int(p['post_bits'])}, p={float(p['post_p']):.2f})"
728
+ )
729
+ if p["use_sol"]:
730
+ add(
731
+ f"T.RandomSolarize(threshold={int(p['sol_thresh'])}, p={float(p['sol_p']):.2f})"
732
+ )
733
+ if p["use_sharp"]:
734
+ add(
735
+ f"T.RandomAdjustSharpness(sharpness_factor={float(p['sharp_factor']):.2f}, p={float(p['sharp_p']):.2f})"
736
+ )
737
+ if p["use_autoc"]:
738
+ add("T.RandomAutocontrast()")
739
+ if p["use_eq"]:
740
+ add("T.RandomEqualize()")
741
+ if p["use_jpeg"]:
742
+ add(f"T.JPEG(quality=({int(p['jpeg_qmin'])}, {int(p['jpeg_qmax'])}))")
743
+
744
+ # Policies
745
+ if p["use_autoaugment"]:
746
+ add(f"T.AutoAugment(policy=T.AutoAugmentPolicy.{p['aa_policy']})")
747
+ if p["use_randaugment"]:
748
+ add(
749
+ f"T.RandAugment(num_ops={int(p['ra_num_ops'])}, magnitude={int(p['ra_mag'])})"
750
+ )
751
+ if p["use_trivial"]:
752
+ add(f"T.TrivialAugmentWide(num_magnitude_bins={int(p['tw_bins'])})")
753
+ if p["use_augmix"]:
754
+ add(
755
+ "T.AugMix("
756
+ f"severity={int(p['am_severity'])}, mixture_width={int(p['am_width'])}, "
757
+ f"chain_depth={int(p['am_depth'])}, alpha={float(p['am_alpha']):.2f}"
758
+ ")"
759
+ )
760
+
761
+ # Randomly-applied
762
+ if p["use_hflip"]:
763
+ add(f"T.RandomHorizontalFlip(p={float(p['hflip_p']):.2f})")
764
+ if p["use_vflip"]:
765
+ add(f"T.RandomVerticalFlip(p={float(p['vflip_p']):.2f})")
766
+ if p["use_random_apply"]:
767
+ add(
768
+ f"T.RandomApply(transforms=[T.RandomCrop(({int(p['ra_crop'])}, {int(p['ra_crop'])}))], p={float(p['ra_p']):.2f})"
769
+ )
770
+
771
+ # Tensor-only
772
+ need_tensor = p["use_erase"] or p["use_norm"]
773
+ if need_tensor:
774
+ add("T.ToImage()")
775
+ add("T.ToDtype(torch.float32, scale=True)")
776
+
777
+ if p["use_erase"]:
778
+ add(
779
+ "T.RandomErasing("
780
+ f"p={float(p['erase_p']):.2f}, "
781
+ f"scale=({float(p['erase_scale_min']):.3f}, {float(p['erase_scale_max']):.3f}), "
782
+ f"ratio=({float(p['erase_ratio_min']):.2f}, {float(p['erase_ratio_max']):.2f}), "
783
+ 'value="random"'
784
+ ")"
785
+ )
786
+
787
+ if p["use_norm"]:
788
+ add(
789
+ f"T.Normalize(mean=[{p['norm_mean']}], std=[{p['norm_std']}]) # CSV -> list"
790
+ )
791
+
792
+ lines.append("])")
793
+ return "\n".join(lines)
794
+
795
+
796
+ class TransformationEngine:
797
+ """
798
+ Apply transforms:
799
+ - one example per enabled transform
800
+ - final MIX pipeline with N variants (define by user)
801
+
802
+ :param factory: TransformFactory instance.
803
+ :type factory: TransformFactory
804
+ """
805
+
806
+ def __init__(self, factory: TransformFactory) -> None:
807
+ self.factory = factory
808
+
809
+ def apply(
810
+ self,
811
+ gallery_in: Any,
812
+ n_variants: int,
813
+ seed: int,
814
+ reseed_each_variant: bool,
815
+ params: Dict[str, Any],
816
+ ) -> List[Dict[str, Any]]:
817
+ """
818
+ Apply transformations and return HTML.
819
+
820
+ :param gallery_in: Gradio gallery value.
821
+ :type gallery_in: Any
822
+ :param n_variants: Number of variants for the MIX pipeline.
823
+ :type n_variants: int
824
+ :param seed: Base random seed.
825
+ :type seed: int
826
+ :param reseed_each_variant: Whether to re-seed each variant for reproducibility.
827
+ :type reseed_each_variant: bool
828
+ :param params: Transform parameters.
829
+ :type params: Dict[str, Any]
830
+ :return: Rendered HTML results.
831
+ :rtype: str
832
+ """
833
+ images = as_pil_list(gallery_in)
834
+ if not images:
835
+ return ""
836
+
837
+ base_seed = int(seed)
838
+ singles = self.factory.build_single_transforms(params)
839
+ grouped: Dict[int, Dict[str, Any]] = {}
840
+
841
+ for idx, img in enumerate(images):
842
+ grouped[idx] = {"original": img, "singles": [], "mix": []}
843
+
844
+ # one example per transform
845
+ for item in singles:
846
+ tname, tform = item.name, item.op
847
+
848
+ s = base_seed + idx * 10_000 + (abs(hash(tname)) % 10_000)
849
+ random.seed(s)
850
+ torch.manual_seed(s)
851
+
852
+ if tname == "FiveCrop":
853
+ y = tform(img) # tuple of 5
854
+ for i, crop in enumerate(y):
855
+ grouped[idx]["singles"].append(
856
+ (f"FiveCrop #{i + 1}", ensure_pil(crop))
857
+ )
858
+ continue
859
+
860
+ if tform == TransformFactory.TENSOR_ERASE_ONLY:
861
+ tt = self.factory.tensor_only_example(
862
+ params, TransformFactory.TENSOR_ERASE_ONLY
863
+ )
864
+ grouped[idx]["singles"].append((tname, ensure_pil(tt(img))))
865
+ continue
866
+
867
+ if tform == TransformFactory.TENSOR_NORM_ONLY:
868
+ tt = self.factory.tensor_only_example(
869
+ params, TransformFactory.TENSOR_NORM_ONLY
870
+ )
871
+ grouped[idx]["singles"].append((tname, ensure_pil(tt(img))))
872
+ continue
873
+
874
+ grouped[idx]["singles"].append((tname, ensure_pil(tform(img))))
875
+
876
+ # + MIX
877
+ pipe = self.factory.build_compose(params)
878
+
879
+ for v in range(int(n_variants)):
880
+ if reseed_each_variant:
881
+ s = base_seed + idx * 1000 + v
882
+ random.seed(s)
883
+ torch.manual_seed(s)
884
+
885
+ y = pipe(img)
886
+
887
+ # FiveCrop inside Compose returns tuple - for mix we show first crop
888
+ if isinstance(y, (tuple, list)) and len(y) > 0:
889
+ grouped[idx]["mix"].append(
890
+ (f"aug #{v + 1} (FiveCrop→#1)", ensure_pil(y[0]))
891
+ )
892
+ else:
893
+ grouped[idx]["mix"].append((f"aug #{v + 1}", ensure_pil(y)))
894
+
895
+ out = []
896
+ for idx, block in grouped.items():
897
+ out.append(
898
+ {
899
+ "idx": idx,
900
+ "original": block["original"],
901
+ "singles": block.get("singles", []), # list[(name, PIL)]
902
+ "mix": block.get("mix", []), # list[(cap, PIL)]
903
+ }
904
+ )
905
+ return out
906
+
907
+
908
+ # App logic
909
+
910
+
911
+ class TTPApp:
912
+ """
913
+ Main Gradio application class.
914
+
915
+ :param i18n: I18N manager.
916
+ :type i18n: I18N
917
+ :param engine: Transformation engine.
918
+ :type engine: TransformationEngine
919
+ :param codegen: Code generator.
920
+ :type codegen: CodeGenerator
921
+ """
922
+
923
+ def __init__(
924
+ self, i18n: I18N, engine: TransformationEngine, codegen: CodeGenerator
925
+ ) -> None:
926
+ self.i18n = i18n
927
+ self.engine = engine
928
+ self.codegen = codegen
929
+
930
+ # cache (for future?)
931
+ # self._cache_key = None
932
+ # self._cache_singles = None
933
+ # self._cache_pipe = None
934
+
935
+ # populated when building UI
936
+ self._toggles: List[gr.Checkbox] = []
937
+ self._params_inputs: List[gr.components.Component] = []
938
+
939
+ def _active_sections_html(self, lang: str, p: Dict[str, Any]) -> str:
940
+ """
941
+ Build the "active sections" status block.
942
+
943
+ :param p: Parameters dict.
944
+ :type p: Dict[str, Any]
945
+ :return: HTML snippet.
946
+ :rtype: str
947
+ """
948
+ active_geo = any(
949
+ p[k]
950
+ for k in [
951
+ "use_pad",
952
+ "use_resize",
953
+ "use_center_crop",
954
+ "use_five_crop",
955
+ "use_random_perspective",
956
+ "use_random_rotation",
957
+ "use_random_affine",
958
+ "use_elastic",
959
+ "use_random_crop",
960
+ "use_rrc",
961
+ ]
962
+ )
963
+ active_photo = any(
964
+ p[k]
965
+ for k in [
966
+ "use_grayscale",
967
+ "use_cj",
968
+ "use_blur",
969
+ "use_inv",
970
+ "use_post",
971
+ "use_sol",
972
+ "use_sharp",
973
+ "use_autoc",
974
+ "use_eq",
975
+ "use_jpeg",
976
+ ]
977
+ )
978
+ active_aug = any(
979
+ p[k]
980
+ for k in ["use_autoaugment", "use_randaugment", "use_trivial", "use_augmix"]
981
+ )
982
+ active_randomly = any(
983
+ p[k] for k in ["use_hflip", "use_vflip", "use_random_apply"]
984
+ )
985
+ active_tensor = any(p[k] for k in ["use_erase", "use_norm"])
986
+
987
+ return f"""
988
+ <div style="font-size:14px;line-height:1.6">
989
+ <div>{status_dot(active_geo)}<b>{self.i18n.section(lang, 'geometric')}</b></div>
990
+ <div>{status_dot(active_photo)}<b>{self.i18n.section(lang, 'photometric')}</b></div>
991
+ <div>{status_dot(active_aug)}<b>{self.i18n.section(lang, 'policies')}</b></div>
992
+ <div>{status_dot(active_randomly)}<b>{self.i18n.section(lang, 'random_applied')}</b></div>
993
+ <div>{status_dot(active_tensor)}<b>{self.i18n.section(lang, 'tensor_bonus')}</b></div>
994
+ </div>
995
+ """
996
+
997
+ def _collect_params(self, *vals: Any) -> Dict[str, Any]:
998
+ """
999
+ Collect UI values into a params dict (order must match self._params_inputs).
1000
+
1001
+ :param vals: Values from Gradio components.
1002
+ :type vals: Any
1003
+ :return: Parameters dict.
1004
+ :rtype: Dict[str, Any]
1005
+ """
1006
+ keys = [
1007
+ # Geometric
1008
+ "use_pad",
1009
+ "pad_px",
1010
+ "pad_fill",
1011
+ "pad_mode",
1012
+ "use_resize",
1013
+ "resize_size",
1014
+ "use_center_crop",
1015
+ "crop_size",
1016
+ "use_five_crop",
1017
+ "five_crop_size",
1018
+ "use_random_perspective",
1019
+ "persp_p",
1020
+ "persp_dist",
1021
+ "use_random_rotation",
1022
+ "rot_deg",
1023
+ "use_random_affine",
1024
+ "aff_deg",
1025
+ "aff_translate",
1026
+ "aff_scale_min",
1027
+ "aff_scale_max",
1028
+ "aff_shear",
1029
+ "use_elastic",
1030
+ "elastic_alpha",
1031
+ "elastic_sigma",
1032
+ "use_random_crop",
1033
+ "rand_crop_size",
1034
+ "use_rrc",
1035
+ "rrc_size",
1036
+ "rrc_scale_min",
1037
+ "rrc_scale_max",
1038
+ # Photometric
1039
+ "use_grayscale",
1040
+ "gray_channels",
1041
+ "use_cj",
1042
+ "cj_b",
1043
+ "cj_c",
1044
+ "cj_s",
1045
+ "cj_h",
1046
+ "use_blur",
1047
+ "blur_k",
1048
+ "blur_sigma_min",
1049
+ "blur_sigma_max",
1050
+ "use_inv",
1051
+ "inv_p",
1052
+ "use_post",
1053
+ "post_p",
1054
+ "post_bits",
1055
+ "use_sol",
1056
+ "sol_p",
1057
+ "sol_thresh",
1058
+ "use_sharp",
1059
+ "sharp_p",
1060
+ "sharp_factor",
1061
+ "use_autoc",
1062
+ "use_eq",
1063
+ "use_jpeg",
1064
+ "jpeg_qmin",
1065
+ "jpeg_qmax",
1066
+ # Policies
1067
+ "use_autoaugment",
1068
+ "aa_policy",
1069
+ "use_randaugment",
1070
+ "ra_num_ops",
1071
+ "ra_mag",
1072
+ "use_trivial",
1073
+ "tw_bins",
1074
+ "use_augmix",
1075
+ "am_severity",
1076
+ "am_width",
1077
+ "am_depth",
1078
+ "am_alpha",
1079
+ # Randomly-applied
1080
+ "use_hflip",
1081
+ "hflip_p",
1082
+ "use_vflip",
1083
+ "vflip_p",
1084
+ "use_random_apply",
1085
+ "ra_p",
1086
+ "ra_crop",
1087
+ # Tensor-only, bonus
1088
+ "use_erase",
1089
+ "erase_p",
1090
+ "erase_scale_min",
1091
+ "erase_scale_max",
1092
+ "erase_ratio_min",
1093
+ "erase_ratio_max",
1094
+ "use_norm",
1095
+ "norm_mean",
1096
+ "norm_std",
1097
+ ]
1098
+
1099
+ p = dict(zip(keys, vals))
1100
+
1101
+ # Safety: ensure bool toggles are bool
1102
+ for k in list(p.keys()):
1103
+ if k.startswith("use_"):
1104
+ p[k] = bool(p[k])
1105
+
1106
+ return p
1107
+
1108
+ def _disable_all(self) -> List[gr.update]:
1109
+ """
1110
+ Disable all transform toggles.
1111
+
1112
+ :return: List of gr.update objects setting each toggle to False.
1113
+ :rtype: List[gr.update]
1114
+ """
1115
+ return [gr.update(value=False) for _ in self._toggles]
1116
+
1117
+ def _set_language(self, lang: str):
1118
+ # Markdown ONLY (pas de <h1>, pas de <div>)
1119
+ title = f"# {self.i18n.get(lang, 'app_title')}"
1120
+ desc = f"{self.i18n.subtitles(lang)[0]}"
1121
+ return gr.update(value=title), gr.update(value=desc)
1122
+
1123
+ def build(self) -> gr.Blocks:
1124
+ """
1125
+ Build the Gradio interface and wire callbacks.
1126
+
1127
+ :return: Gradio Blocks app.
1128
+ :rtype: gradio.Blocks
1129
+ """
1130
+ i18n = self.i18n
1131
+
1132
+ # Documentation links (simple + stable)
1133
+ DOCS = {
1134
+ "geometric": "https://pytorch.org/vision/stable/transforms.html#geometry",
1135
+ "photometric": "https://pytorch.org/vision/stable/transforms.html#color",
1136
+ "policies": "https://docs.pytorch.org/vision/stable/transforms.html#id8",
1137
+ "random_applied": "https://pytorch.org/vision/stable/transforms.html",
1138
+ "tensor_bonus": "https://docs.pytorch.org/vision/stable/transforms.html#id6",
1139
+ }
1140
+
1141
+ with gr.Blocks(title=i18n.get("EN", "app_title")) as demo:
1142
+ # Header (centered title + subtitle + language)
1143
+
1144
+ title_md = gr.Markdown(value=f"# {i18n.get('EN', 'app_title')}")
1145
+ desc_md = gr.Markdown(value=f"### {i18n.subtitles('EN')[0]}")
1146
+
1147
+ with gr.Sidebar(open=True, width=550, elem_id="controls_sidebar"):
1148
+ globals_title = gr.Markdown(f"### {i18n.get('EN', 'globals_title')}")
1149
+ lang = gr.Radio(
1150
+ ["EN", "FR"], value="EN", label=i18n.get("EN", "language_label")
1151
+ )
1152
+ n_variants = gr.Slider(
1153
+ 1, 8, value=3, step=1, label=i18n.get("EN", "variants_label")
1154
+ )
1155
+ seed = gr.Number(
1156
+ value=42, precision=0, label=i18n.get("EN", "seed_label")
1157
+ )
1158
+ reseed_each_variant = gr.Checkbox(
1159
+ value=True, label=i18n.get("EN", "reseed_label")
1160
+ )
1161
+
1162
+ disable_all_btn = gr.Button(i18n.get("EN", "disable_all"))
1163
+ status_html = gr.HTML(label=i18n.get("EN", "status_label"))
1164
+ code_preview = gr.Code(
1165
+ label=i18n.get("EN", "code_label"), language="python"
1166
+ )
1167
+
1168
+ # Geometric
1169
+ acc_geo = gr.Accordion(
1170
+ label=i18n.section("EN", "geometric"), open=False
1171
+ )
1172
+ with acc_geo:
1173
+ docs_geo_md = gr.Markdown(
1174
+ f"{i18n.get('EN', 'docs_prefix')} [{DOCS['geometric']}]({DOCS['geometric']})"
1175
+ )
1176
+
1177
+ use_pad = gr.Checkbox(value=False, label="Pad")
1178
+ pad_px = gr.Slider(0, 200, value=20, step=1, label="padding (px)")
1179
+ pad_fill = gr.Slider(0, 255, value=0, step=1, label="fill (0-255)")
1180
+ pad_mode = gr.Dropdown(
1181
+ ["constant", "edge", "reflect", "symmetric"],
1182
+ value="constant",
1183
+ label="padding_mode",
1184
+ )
1185
+
1186
+ use_resize = gr.Checkbox(value=False, label="Resize (square)")
1187
+ resize_size = gr.Slider(
1188
+ 64, 1024, value=256, step=1, label="Resize size"
1189
+ )
1190
+
1191
+ use_center_crop = gr.Checkbox(value=False, label="CenterCrop")
1192
+ crop_size = gr.Slider(
1193
+ 32, 1024, value=224, step=1, label="Crop size"
1194
+ )
1195
+
1196
+ use_five_crop = gr.Checkbox(
1197
+ value=False, label="FiveCrop (shows 5 images)"
1198
+ )
1199
+ five_crop_size = gr.Slider(
1200
+ 32, 1024, value=224, step=1, label="FiveCrop size"
1201
+ )
1202
+
1203
+ use_random_perspective = gr.Checkbox(
1204
+ value=False, label="RandomPerspective"
1205
+ )
1206
+ persp_p = gr.Slider(0, 1, value=0.5, step=0.05, label="p")
1207
+ persp_dist = gr.Slider(
1208
+ 0, 1, value=0.5, step=0.05, label="distortion_scale"
1209
+ )
1210
+
1211
+ use_random_rotation = gr.Checkbox(
1212
+ value=False, label="RandomRotation"
1213
+ )
1214
+ rot_deg = gr.Slider(0, 180, value=15, step=1, label="degrees")
1215
+
1216
+ use_random_affine = gr.Checkbox(value=False, label="RandomAffine")
1217
+ aff_deg = gr.Slider(0, 180, value=15, step=1, label="degrees")
1218
+ aff_translate = gr.Slider(
1219
+ 0, 0.5, value=0.1, step=0.01, label="translate (fraction)"
1220
+ )
1221
+ aff_scale_min = gr.Slider(
1222
+ 0.1, 2.0, value=0.9, step=0.05, label="scale min"
1223
+ )
1224
+ aff_scale_max = gr.Slider(
1225
+ 0.1, 2.0, value=1.1, step=0.05, label="scale max"
1226
+ )
1227
+ aff_shear = gr.Slider(0, 45, value=10, step=1, label="shear (deg)")
1228
+
1229
+ use_elastic = gr.Checkbox(value=False, label="ElasticTransform")
1230
+ elastic_alpha = gr.Slider(
1231
+ 0.0, 200.0, value=50.0, step=1.0, label="alpha"
1232
+ )
1233
+ elastic_sigma = gr.Slider(
1234
+ 0.0, 50.0, value=5.0, step=0.5, label="sigma"
1235
+ )
1236
+
1237
+ use_random_crop = gr.Checkbox(value=False, label="RandomCrop")
1238
+ rand_crop_size = gr.Slider(
1239
+ 32, 1024, value=224, step=1, label="RandomCrop size"
1240
+ )
1241
+
1242
+ use_rrc = gr.Checkbox(value=False, label="RandomResizedCrop")
1243
+ rrc_size = gr.Slider(32, 1024, value=224, step=1, label="RRC size")
1244
+ rrc_scale_min = gr.Slider(
1245
+ 0.05, 1.0, value=0.5, step=0.01, label="RRC scale min"
1246
+ )
1247
+ rrc_scale_max = gr.Slider(
1248
+ 0.05, 1.0, value=1.0, step=0.01, label="RRC scale max"
1249
+ )
1250
+
1251
+ # Photometric
1252
+ acc_photo = gr.Accordion(
1253
+ label=i18n.section("EN", "photometric"), open=True
1254
+ )
1255
+ with acc_photo:
1256
+ docs_photo_md = gr.Markdown(
1257
+ f"{i18n.get('EN', 'docs_prefix')} [{DOCS['photometric']}]({DOCS['photometric']})"
1258
+ )
1259
+ use_grayscale = gr.Checkbox(value=False, label="Grayscale")
1260
+ gray_channels = gr.Radio(
1261
+ [1, 3], value=3, label="num_output_channels"
1262
+ )
1263
+
1264
+ use_cj = gr.Checkbox(value=True, label="ColorJitter")
1265
+ cj_b = gr.Slider(0, 2, value=0.2, step=0.05, label="brightness")
1266
+ cj_c = gr.Slider(0, 2, value=0.2, step=0.05, label="contrast")
1267
+ cj_s = gr.Slider(0, 2, value=0.2, step=0.05, label="saturation")
1268
+ cj_h = gr.Slider(0, 0.5, value=0.05, step=0.01, label="hue")
1269
+
1270
+ use_blur = gr.Checkbox(value=False, label="GaussianBlur")
1271
+ blur_k = gr.Slider(
1272
+ 1, 61, value=11, step=2, label="kernel_size (odd)"
1273
+ )
1274
+ blur_sigma_min = gr.Slider(
1275
+ 0.1, 10.0, value=0.1, step=0.1, label="sigma min"
1276
+ )
1277
+ blur_sigma_max = gr.Slider(
1278
+ 0.1, 10.0, value=2.0, step=0.1, label="sigma max"
1279
+ )
1280
+
1281
+ use_inv = gr.Checkbox(value=True, label="RandomInvert")
1282
+ inv_p = gr.Slider(0, 1, value=0.50, step=0.05, label="p")
1283
+
1284
+ use_post = gr.Checkbox(value=False, label="RandomPosterize")
1285
+ post_p = gr.Slider(0, 1, value=0.2, step=0.05, label="p")
1286
+ post_bits = gr.Slider(1, 8, value=4, step=1, label="bits")
1287
+
1288
+ use_sol = gr.Checkbox(value=True, label="RandomSolarize")
1289
+ sol_p = gr.Slider(0, 1, value=0.40, step=0.05, label="p")
1290
+ sol_thresh = gr.Slider(
1291
+ 0, 255, value=128, step=1, label="threshold (0-255)"
1292
+ )
1293
+
1294
+ use_sharp = gr.Checkbox(value=False, label="RandomAdjustSharpness")
1295
+ sharp_p = gr.Slider(0, 1, value=0.5, step=0.05, label="p")
1296
+ sharp_factor = gr.Slider(
1297
+ 0.0, 5.0, value=2.0, step=0.1, label="sharpness_factor"
1298
+ )
1299
+
1300
+ use_autoc = gr.Checkbox(value=True, label="RandomAutocontrast")
1301
+ use_eq = gr.Checkbox(value=True, label="RandomEqualize")
1302
+
1303
+ use_jpeg = gr.Checkbox(value=False, label="JPEG (compression)")
1304
+ jpeg_qmin = gr.Slider(1, 100, value=5, step=1, label="quality min")
1305
+ jpeg_qmax = gr.Slider(1, 100, value=50, step=1, label="quality max")
1306
+
1307
+ # Policies
1308
+ acc_policies = gr.Accordion(
1309
+ label=i18n.section("EN", "policies"), open=False
1310
+ )
1311
+ with acc_policies:
1312
+ docs_acc_md = gr.Markdown(
1313
+ f"{i18n.get('EN', 'docs_prefix')} [{DOCS['policies']}]({DOCS['policies']})"
1314
+ )
1315
+
1316
+ use_autoaugment = gr.Checkbox(value=False, label="AutoAugment")
1317
+ aa_policy = gr.Dropdown(
1318
+ ["CIFAR10", "IMAGENET", "SVHN"],
1319
+ value="IMAGENET",
1320
+ label="policy",
1321
+ )
1322
+
1323
+ use_randaugment = gr.Checkbox(value=False, label="RandAugment")
1324
+ ra_num_ops = gr.Slider(1, 10, value=2, step=1, label="num_ops")
1325
+ ra_mag = gr.Slider(0, 30, value=9, step=1, label="magnitude")
1326
+
1327
+ use_trivial = gr.Checkbox(value=False, label="TrivialAugmentWide")
1328
+ tw_bins = gr.Slider(
1329
+ 1, 50, value=31, step=1, label="num_magnitude_bins"
1330
+ )
1331
+
1332
+ use_augmix = gr.Checkbox(value=False, label="AugMix")
1333
+ am_severity = gr.Slider(1, 10, value=3, step=1, label="severity")
1334
+ am_width = gr.Slider(1, 10, value=3, step=1, label="mixture_width")
1335
+ am_depth = gr.Slider(
1336
+ -1, 10, value=-1, step=1, label="chain_depth (-1 = random)"
1337
+ )
1338
+ am_alpha = gr.Slider(0.0, 5.0, value=1.0, step=0.1, label="alpha")
1339
+
1340
+ # Randomly-applied
1341
+ acc_random = gr.Accordion(
1342
+ label=i18n.section("EN", "random_applied"), open=True
1343
+ )
1344
+ with acc_random:
1345
+ docs_random_md = gr.Markdown(
1346
+ f"{i18n.get('EN', 'docs_prefix')} [{DOCS['random_applied']}]({DOCS['random_applied']})"
1347
+ )
1348
+
1349
+ use_hflip = gr.Checkbox(value=True, label="RandomHorizontalFlip")
1350
+ hflip_p = gr.Slider(0, 1, value=0.5, step=0.05, label="p")
1351
+
1352
+ use_vflip = gr.Checkbox(value=False, label="RandomVerticalFlip")
1353
+ vflip_p = gr.Slider(0, 1, value=0.2, step=0.05, label="p")
1354
+
1355
+ use_random_apply = gr.Checkbox(
1356
+ value=False, label="RandomApply([RandomCrop])"
1357
+ )
1358
+ ra_p = gr.Slider(0, 1, value=0.5, step=0.05, label="p")
1359
+ ra_crop = gr.Slider(
1360
+ 32, 1024, value=64, step=1, label="inner RandomCrop size"
1361
+ )
1362
+
1363
+ # Tensor-only (bonus)
1364
+ acc_tensor = gr.Accordion(
1365
+ label=i18n.section("EN", "tensor_bonus"), open=False
1366
+ )
1367
+ with acc_tensor:
1368
+ docs_tensor_md = gr.Markdown(
1369
+ f"{i18n.get('EN', 'docs_prefix')} [{DOCS['tensor_bonus']}]({DOCS['tensor_bonus']})"
1370
+ )
1371
+
1372
+ use_erase = gr.Checkbox(value=False, label="RandomErasing")
1373
+ erase_p = gr.Slider(0, 1, value=0.25, step=0.05, label="p")
1374
+ erase_scale_min = gr.Slider(
1375
+ 0.0001, 0.5, value=0.02, step=0.01, label="scale min"
1376
+ )
1377
+ erase_scale_max = gr.Slider(
1378
+ 0.0001, 1.0, value=0.2, step=0.01, label="scale max"
1379
+ )
1380
+ erase_ratio_min = gr.Slider(
1381
+ 0.1, 10.0, value=0.3, step=0.1, label="ratio min"
1382
+ )
1383
+ erase_ratio_max = gr.Slider(
1384
+ 0.1, 10.0, value=3.3, step=0.1, label="ratio max"
1385
+ )
1386
+
1387
+ use_norm = gr.Checkbox(value=False, label="Normalize (mean/std)")
1388
+ norm_mean = gr.Textbox(
1389
+ value="0.485,0.456,0.406", label="mean (CSV)"
1390
+ )
1391
+ norm_std = gr.Textbox(value="0.229,0.224,0.225", label="std (CSV)")
1392
+
1393
+ # Main content
1394
+ with gr.Column(scale=9):
1395
+ upload_title_md = gr.Markdown(f"## {i18n.get('EN', 'upload_section')}")
1396
+ gallery_in = gr.Gallery(
1397
+ label=i18n.get("EN", "upload_label"),
1398
+ type="pil",
1399
+ columns=4,
1400
+ height=240,
1401
+ )
1402
+
1403
+ apply_btn = gr.Button(i18n.get("EN", "apply"), variant="primary")
1404
+ results_state = gr.State([])
1405
+ results_title_md = gr.Markdown(
1406
+ f"## {i18n.get('EN', 'results_section')}"
1407
+ )
1408
+
1409
+ @gr.render(inputs=results_state)
1410
+ def render_results(data: Any) -> None:
1411
+ """
1412
+ Render the results accordions + galleries.
1413
+ :param data: Data from results_state.
1414
+ :type data: Any
1415
+ :return: None
1416
+ :rtype: None
1417
+ """
1418
+ if not data:
1419
+ gr.Markdown("")
1420
+ return
1421
+
1422
+ for item in data:
1423
+ with gr.Accordion(label=f"Image #{item['idx']}", open=True):
1424
+ # 1 container == 1 gallery (original + singles + mix)
1425
+ tiles = []
1426
+ tiles.append((item["original"], "Original"))
1427
+
1428
+ # singles: list[(name, PIL)]
1429
+ tiles += [
1430
+ (im, name) for (name, im) in item.get("singles", [])
1431
+ ]
1432
+
1433
+ # mix: list[(cap, PIL)]
1434
+ tiles += [(im, cap) for (cap, im) in item.get("mix", [])]
1435
+
1436
+ gr.Gallery(
1437
+ value=tiles,
1438
+ label=None,
1439
+ columns=4,
1440
+ height=260,
1441
+ preview=True,
1442
+ )
1443
+
1444
+ # use this to disable all
1445
+ self._toggles = [
1446
+ use_pad,
1447
+ use_resize,
1448
+ use_center_crop,
1449
+ use_five_crop,
1450
+ use_random_perspective,
1451
+ use_random_rotation,
1452
+ use_random_affine,
1453
+ use_elastic,
1454
+ use_random_crop,
1455
+ use_rrc,
1456
+ use_grayscale,
1457
+ use_cj,
1458
+ use_blur,
1459
+ use_inv,
1460
+ use_post,
1461
+ use_sol,
1462
+ use_sharp,
1463
+ use_autoc,
1464
+ use_eq,
1465
+ use_jpeg,
1466
+ use_autoaugment,
1467
+ use_randaugment,
1468
+ use_trivial,
1469
+ use_augmix,
1470
+ use_hflip,
1471
+ use_vflip,
1472
+ use_random_apply,
1473
+ use_erase,
1474
+ use_norm,
1475
+ ]
1476
+
1477
+ self._params_inputs = [
1478
+ use_pad,
1479
+ pad_px,
1480
+ pad_fill,
1481
+ pad_mode,
1482
+ use_resize,
1483
+ resize_size,
1484
+ use_center_crop,
1485
+ crop_size,
1486
+ use_five_crop,
1487
+ five_crop_size,
1488
+ use_random_perspective,
1489
+ persp_p,
1490
+ persp_dist,
1491
+ use_random_rotation,
1492
+ rot_deg,
1493
+ use_random_affine,
1494
+ aff_deg,
1495
+ aff_translate,
1496
+ aff_scale_min,
1497
+ aff_scale_max,
1498
+ aff_shear,
1499
+ use_elastic,
1500
+ elastic_alpha,
1501
+ elastic_sigma,
1502
+ use_random_crop,
1503
+ rand_crop_size,
1504
+ use_rrc,
1505
+ rrc_size,
1506
+ rrc_scale_min,
1507
+ rrc_scale_max,
1508
+ use_grayscale,
1509
+ gray_channels,
1510
+ use_cj,
1511
+ cj_b,
1512
+ cj_c,
1513
+ cj_s,
1514
+ cj_h,
1515
+ use_blur,
1516
+ blur_k,
1517
+ blur_sigma_min,
1518
+ blur_sigma_max,
1519
+ use_inv,
1520
+ inv_p,
1521
+ use_post,
1522
+ post_p,
1523
+ post_bits,
1524
+ use_sol,
1525
+ sol_p,
1526
+ sol_thresh,
1527
+ use_sharp,
1528
+ sharp_p,
1529
+ sharp_factor,
1530
+ use_autoc,
1531
+ use_eq,
1532
+ use_jpeg,
1533
+ jpeg_qmin,
1534
+ jpeg_qmax,
1535
+ use_autoaugment,
1536
+ aa_policy,
1537
+ use_randaugment,
1538
+ ra_num_ops,
1539
+ ra_mag,
1540
+ use_trivial,
1541
+ tw_bins,
1542
+ use_augmix,
1543
+ am_severity,
1544
+ am_width,
1545
+ am_depth,
1546
+ am_alpha,
1547
+ use_hflip,
1548
+ hflip_p,
1549
+ use_vflip,
1550
+ vflip_p,
1551
+ use_random_apply,
1552
+ ra_p,
1553
+ ra_crop,
1554
+ use_erase,
1555
+ erase_p,
1556
+ erase_scale_min,
1557
+ erase_scale_max,
1558
+ erase_ratio_min,
1559
+ erase_ratio_max,
1560
+ use_norm,
1561
+ norm_mean,
1562
+ norm_std,
1563
+ ]
1564
+
1565
+ # this for live updates of status + code
1566
+ def _update_all(lang_val: str, *vals: Any) -> Tuple[str, str]:
1567
+ """
1568
+ Update status HTML + code preview.
1569
+
1570
+ :param lang_val: language code.
1571
+ :type lang_val: str
1572
+ :param vals: values from Gradio components.
1573
+ :type vals: Any
1574
+ :return: Tuple of (status HTML, code preview).
1575
+ :rtype: Tuple[str, str]
1576
+ """
1577
+ p = self._collect_params(*vals)
1578
+ status = self._active_sections_html(lang_val, p)
1579
+ code = self.codegen.to_code(p)
1580
+ return status, code
1581
+
1582
+ for comp in self._params_inputs:
1583
+ comp.change(
1584
+ fn=_update_all,
1585
+ inputs=[lang] + self._params_inputs,
1586
+ outputs=[status_html, code_preview],
1587
+ )
1588
+
1589
+ demo.load(
1590
+ fn=_update_all,
1591
+ inputs=[lang] + self._params_inputs,
1592
+ outputs=[status_html, code_preview],
1593
+ )
1594
+
1595
+ disable_all_btn.click(
1596
+ fn=self._disable_all,
1597
+ inputs=[],
1598
+ outputs=self._toggles,
1599
+ )
1600
+
1601
+ # --- apply button ---
1602
+ def _apply(
1603
+ gallery: Any, nvar: int, sd: int, reseed: bool, *vals: Any
1604
+ ) -> str:
1605
+ """
1606
+ Apply transformations.
1607
+
1608
+ :param gallery: the input gallery.
1609
+ :type gallery: Any
1610
+ :param nvar: number of variants.
1611
+ :type nvar: int
1612
+ :param sd: seed.
1613
+ :type sd: int
1614
+ :param reseed: whether to reseed each variant.
1615
+ :type reseed: bool
1616
+ :param vals: values from Gradio components.
1617
+ :type vals: Any
1618
+ :return: Rendered HTML results.
1619
+ :rtype: str
1620
+ """
1621
+ p = self._collect_params(*vals)
1622
+
1623
+ return self.engine.apply(
1624
+ gallery, int(nvar), int(sd), bool(reseed), p
1625
+ )
1626
+
1627
+ apply_btn.click(
1628
+ fn=_apply,
1629
+ inputs=[gallery_in, n_variants, seed, reseed_each_variant]
1630
+ + self._params_inputs,
1631
+ outputs=[results_state],
1632
+ )
1633
+
1634
+ def _on_lang_change(lang_val: str, *vals: Any):
1635
+ """
1636
+ Handle language change: update UI + status + code.
1637
+
1638
+ :param lang_val: language code.
1639
+ :type lang_val: str
1640
+ :param vals: values from Gradio components.
1641
+ :type vals: Any
1642
+ :return: Updated components.
1643
+ :rtype: Tuple[gr.update, ...]
1644
+ """
1645
+ # updates UI
1646
+ t_upd, desc_upd = self._set_language(lang_val)
1647
+
1648
+ # status recalculation
1649
+ p = self._collect_params(*vals)
1650
+ status = self._active_sections_html(lang_val, p)
1651
+ code = self.codegen.to_code(p)
1652
+
1653
+ return (
1654
+ # header
1655
+ t_upd,
1656
+ desc_upd,
1657
+ # globals
1658
+ gr.update(value=f"### {i18n.get(lang_val, 'globals_title')}"),
1659
+ gr.update(label=i18n.get(lang_val, "language_label")),
1660
+ gr.update(value=i18n.get(lang_val, "disable_all")),
1661
+ gr.update(label=i18n.get(lang_val, "variants_label")),
1662
+ gr.update(label=i18n.get(lang_val, "seed_label")),
1663
+ gr.update(label=i18n.get(lang_val, "reseed_label")),
1664
+ gr.update(value=i18n.get(lang_val, "apply")),
1665
+ gr.update(label=i18n.get(lang_val, "upload_label")),
1666
+ # section titles
1667
+ gr.update(value=f"## {i18n.get(lang_val, 'upload_section')}"),
1668
+ gr.update(value=f"## {i18n.get(lang_val, 'results_section')}"),
1669
+ # accordions labels
1670
+ gr.update(label=i18n.section(lang_val, "geometric")),
1671
+ gr.update(label=i18n.section(lang_val, "photometric")),
1672
+ gr.update(label=i18n.section(lang_val, "policies")),
1673
+ gr.update(label=i18n.section(lang_val, "random_applied")),
1674
+ gr.update(label=i18n.section(lang_val, "tensor_bonus")),
1675
+ # docs prefix markdowns
1676
+ gr.update(
1677
+ value=f"{i18n.get(lang_val, 'docs_prefix')} [{DOCS['geometric']}]({DOCS['geometric']})"
1678
+ ),
1679
+ gr.update(
1680
+ value=f"{i18n.get(lang_val, 'docs_prefix')} [{DOCS['photometric']}]({DOCS['photometric']})"
1681
+ ),
1682
+ gr.update(
1683
+ value=f"{i18n.get(lang_val, 'docs_prefix')} [{DOCS['policies']}]({DOCS['policies']})"
1684
+ ),
1685
+ gr.update(
1686
+ value=f"{i18n.get(lang_val, 'docs_prefix')} [{DOCS['random_applied']}]({DOCS['random_applied']})"
1687
+ ),
1688
+ gr.update(
1689
+ value=f"{i18n.get(lang_val, 'docs_prefix')} [{DOCS['tensor_bonus']}]({DOCS['tensor_bonus']})"
1690
+ ),
1691
+ # ✅ status + code recalculés
1692
+ gr.update(value=status),
1693
+ gr.update(value=code),
1694
+ )
1695
+
1696
+ # When language changes: update title/subtitle + some labels
1697
+ lang.change(
1698
+ fn=_on_lang_change,
1699
+ inputs=[lang] + self._params_inputs,
1700
+ outputs=[
1701
+ # header
1702
+ title_md,
1703
+ desc_md,
1704
+ # globals
1705
+ globals_title,
1706
+ lang,
1707
+ disable_all_btn,
1708
+ n_variants,
1709
+ seed,
1710
+ reseed_each_variant,
1711
+ apply_btn,
1712
+ gallery_in,
1713
+ # section titles
1714
+ upload_title_md,
1715
+ results_title_md,
1716
+ # accordion labels
1717
+ acc_geo,
1718
+ acc_photo,
1719
+ acc_policies,
1720
+ acc_random,
1721
+ acc_tensor,
1722
+ # docs markdowns
1723
+ docs_geo_md,
1724
+ docs_photo_md,
1725
+ docs_acc_md,
1726
+ docs_random_md,
1727
+ docs_tensor_md,
1728
+ # status + code gen
1729
+ status_html,
1730
+ code_preview,
1731
+ ],
1732
+ )
1733
+
1734
+ return demo
1735
+
1736
+
1737
+ def main() -> None:
1738
+ """
1739
+ Gradio entrypoint.
1740
+
1741
+ :return: None
1742
+ :rtype: None
1743
+ """
1744
+ texts_json = load_texts_json(DEFAULT_I18N_PATH)
1745
+ i18n = I18N(texts_json, default_lang="EN")
1746
+ factory = TransformFactory()
1747
+ codegen = CodeGenerator()
1748
+ engine = TransformationEngine(factory)
1749
+ app = TTPApp(i18n=i18n, engine=engine, codegen=codegen)
1750
+
1751
+ demo = app.build()
1752
+ demo.launch(css=load_css(DEFAULT_CSS_PATH))
1753
+
1754
+
1755
+ if __name__ == "__main__":
1756
+ main()