trojblue commited on
Commit
7cb613c
·
verified ·
1 Parent(s): cb50074

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +242 -0
app.py ADDED
@@ -0,0 +1,242 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # annotate_concat_demo.py
2
+ # pip install -U gradio pillow
3
+
4
+ import os
5
+ import time
6
+ from pathlib import Path
7
+ from typing import List, Tuple, Optional
8
+
9
+ import gradio as gr
10
+ from PIL import Image, ImageOps
11
+
12
+ # Your existing implementations are assumed available:
13
+ from unibox.utils.image_utils import (
14
+ concatenate_images_horizontally,
15
+ add_annotation,
16
+ )
17
+
18
+ # ------------------------- helpers -------------------------
19
+
20
+ def _norm_path(f) -> Optional[str]:
21
+ if f is None:
22
+ return None
23
+ if isinstance(f, (str, Path)):
24
+ return str(f)
25
+ if hasattr(f, "name"):
26
+ return str(getattr(f, "name"))
27
+ if isinstance(f, dict):
28
+ return str(f.get("name") or f.get("path") or "")
29
+ return str(f)
30
+
31
+ def _load_images(files) -> List[Tuple[Image.Image, str]]:
32
+ out: List[Tuple[Image.Image, str]] = []
33
+ for f in (files or []):
34
+ p = _norm_path(f)
35
+ if not p:
36
+ continue
37
+ im = Image.open(p)
38
+ # auto-orient, ensure RGB; supports PNG/JPEG/WebP/GIF/BMP/TIFF…
39
+ im = ImageOps.exif_transpose(im).convert("RGB")
40
+ out.append((im, os.path.basename(p)))
41
+ return out
42
+
43
+ def _parse_descriptions(text: str, n: int):
44
+ lines = (text or "").splitlines()
45
+ if len(lines) > n:
46
+ return None, f"Too many description lines ({len(lines)}) for {n} image(s). Provide ≤ one per image."
47
+ lines = lines + [""] * (max(0, n - len(lines))) # pad with blanks
48
+ return lines[:n], None
49
+
50
+ def _build_stats(files, desc_text: str) -> dict:
51
+ pairs = _load_images(files)
52
+ n = len(pairs)
53
+ lines, err = _parse_descriptions(desc_text, n) if n > 0 else ((desc_text or "").splitlines(), None)
54
+ mapping = {}
55
+ for i, (_, fname) in enumerate(pairs):
56
+ mapping[fname] = (lines[i] if isinstance(lines, list) and i < len(lines) else "")
57
+ return {
58
+ "num_images": n,
59
+ "num_descriptions": len((desc_text or "").splitlines()),
60
+ "mapping": mapping,
61
+ **({"error": err} if err else {}),
62
+ }
63
+
64
+ # --------------------- core actions ------------------------
65
+
66
+ def concatenate_with_annotations(
67
+ files,
68
+ desc_text: str,
69
+ max_height: int,
70
+ position: str,
71
+ alignment: str,
72
+ size_adj: str,
73
+ ):
74
+ logs = []
75
+ out_img = None
76
+ out_file = None
77
+
78
+ pairs = _load_images(files)
79
+ if not pairs:
80
+ logs.append("ERROR: Please upload at least one image.")
81
+ return out_img, out_file, "\n".join(logs), _build_stats(files, desc_text)
82
+
83
+ lines, err = _parse_descriptions(desc_text, len(pairs))
84
+ if err:
85
+ logs.append(f"ERROR: {err}")
86
+ return out_img, out_file, "\n".join(logs), _build_stats(files, desc_text)
87
+
88
+ # For left/right, alignment must be center (matches add_annotation behavior)
89
+ if position in ("left", "right"):
90
+ alignment = "center"
91
+
92
+ annotated = []
93
+ for (im, fname), line in zip(pairs, lines):
94
+ if line.strip():
95
+ im2 = add_annotation(
96
+ pil_image=im,
97
+ annotation=line,
98
+ position=position,
99
+ alignment=alignment,
100
+ size=size_adj,
101
+ )
102
+ annotated.append(im2)
103
+ logs.append(f"Annotated: {fname}")
104
+ else:
105
+ annotated.append(im)
106
+ logs.append(f"Skipped (no description): {fname}")
107
+
108
+ started = time.time()
109
+ merged = concatenate_images_horizontally(annotated, max_height=max_height)
110
+ if merged is None:
111
+ logs.append("ERROR: Concatenation produced no result.")
112
+ return None, None, "\n".join(logs), _build_stats(files, desc_text)
113
+
114
+ # Save JPEG with required name
115
+ out_dir = Path("outputs")
116
+ out_dir.mkdir(parents=True, exist_ok=True)
117
+ stamp = time.strftime("%Y%m%d_%H%M%S")
118
+ out_name = f"concatenate_{stamp}.jpg"
119
+ out_path = out_dir / out_name
120
+ merged.save(str(out_path), format="JPEG", quality=95, optimize=True)
121
+
122
+ w, h = merged.size
123
+ size_bytes = out_path.stat().st_size
124
+ latency = time.time() - started
125
+ logs.append(f"Output: {out_name} — {w}×{h}px — {size_bytes} bytes — {latency:.3f}s")
126
+
127
+ return merged, str(out_path), "\n".join(logs), _build_stats(files, desc_text)
128
+
129
+ def check_stats_only(files, desc_text: str, *_):
130
+ stats = _build_stats(files, desc_text)
131
+ log = f"Images: {stats.get('num_images', 0)}; Description lines: {stats.get('num_descriptions', 0)}"
132
+ if "error" in stats:
133
+ log += f"\nERROR: {stats['error']}"
134
+ return None, None, log, stats
135
+
136
+ # ----------------------- UI wiring -------------------------
137
+
138
+ theme = gr.themes.Monochrome(primary_hue="slate", radius_size="sm")
139
+
140
+ with gr.Blocks(
141
+ title="Annotated Concatenation — Demo",
142
+ theme=theme,
143
+ analytics_enabled=False,
144
+ ) as demo:
145
+
146
+ gr.Markdown("# Annotate & Concatenate Images")
147
+ gr.Markdown(
148
+ "Upload images (PNG/JPEG/WebP…), add one description per line (blank = skip), "
149
+ "and concatenate horizontally. The output JPEG is named `concatenate_{timestamp}.jpg`."
150
+ )
151
+
152
+ with gr.Row(variant="panel"):
153
+ with gr.Column(scale=2):
154
+ files_in = gr.Files(
155
+ label="Image files",
156
+ # Explicit list ensures WebP acceptance across Gradio builds
157
+ file_types=[
158
+ ".png", ".jpg", ".jpeg", ".webp", ".gif",
159
+ ".bmp", ".tif", ".tiff", ".jfif"
160
+ ],
161
+ type="filepath",
162
+ interactive=True,
163
+ )
164
+ desc_in = gr.Textbox(
165
+ label="Descriptions (one per line; blank lines allowed to skip)",
166
+ placeholder="e.g.\nLeft image label\n\nRight image label",
167
+ lines=8,
168
+ )
169
+ max_h = gr.Number(
170
+ label="Max height (px) for concatenated image",
171
+ value=1024,
172
+ precision=0,
173
+ minimum=64,
174
+ interactive=True,
175
+ )
176
+
177
+ # Folded by default
178
+ with gr.Accordion("Annotation settings", open=False):
179
+ pos = gr.Dropdown(
180
+ label="Position",
181
+ choices=["top", "bottom", "left", "right"],
182
+ value="bottom",
183
+ )
184
+ align = gr.Radio(
185
+ label="Alignment (applies to top/bottom)",
186
+ choices=["left", "center", "right"],
187
+ value="center",
188
+ )
189
+ size_adj = gr.Radio(
190
+ label="Text size",
191
+ choices=["default", "larger", "smaller", "smallest", "largest"],
192
+ value="default",
193
+ )
194
+
195
+ def _toggle_align(p):
196
+ return gr.update(value="center", interactive=False) if p in ("left", "right") else gr.update(interactive=True)
197
+
198
+ pos.change(_toggle_align, inputs=[pos], outputs=[align])
199
+
200
+ with gr.Row():
201
+ concat_btn = gr.Button("Concatenate image", variant="primary")
202
+ stats_btn = gr.Button("Check stats")
203
+
204
+ with gr.Column(scale=3):
205
+ out_img = gr.Image(
206
+ label="Concatenated image (preview)",
207
+ interactive=False,
208
+ format="jpeg",
209
+ show_download_button=False,
210
+ )
211
+ download_file = gr.File(
212
+ label="Download JPEG (named as saved)",
213
+ interactive=False,
214
+ height=72, # compact
215
+ )
216
+
217
+ with gr.Accordion("Logs", open=False):
218
+ logs_out = gr.Textbox(
219
+ label="Info / Errors",
220
+ lines=10,
221
+ interactive=False,
222
+ )
223
+
224
+ with gr.Accordion("Stats", open=False):
225
+ stats_out = gr.JSON(label="Counts and current filename→description mapping")
226
+
227
+ concat_btn.click(
228
+ concatenate_with_annotations,
229
+ inputs=[files_in, desc_in, max_h, pos, align, size_adj],
230
+ outputs=[out_img, download_file, logs_out, stats_out],
231
+ api_name="concatenate",
232
+ )
233
+
234
+ stats_btn.click(
235
+ check_stats_only,
236
+ inputs=[files_in, desc_in, max_h, pos, align, size_adj],
237
+ outputs=[out_img, download_file, logs_out, stats_out],
238
+ api_name="check_stats",
239
+ )
240
+
241
+ if __name__ == "__main__":
242
+ demo.queue(max_size=8).launch()