hmb HF Staff Claude Sonnet 4.6 commited on
Commit
520ec96
·
1 Parent(s): 151ec23

Add engraving style via ZeroGPU, polish UI colours

Browse files

- Add instruct-pix2pix engraving style powered by @spaces.GPU
- Remove token input; Space runs under owner account
- Replace black tones with warm browns in buttons/selected states
- Change stamp collection area to parchment background
- Remove felt board inner shadow and box shadow

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Files changed (3) hide show
  1. README.md +4 -4
  2. app.py +680 -0
  3. requirements.txt +7 -0
README.md CHANGED
@@ -1,10 +1,10 @@
1
  ---
2
  title: Stamp Maker
3
- emoji: 📊
4
- colorFrom: blue
5
- colorTo: blue
6
  sdk: gradio
7
- sdk_version: 6.8.0
8
  app_file: app.py
9
  pinned: false
10
  ---
 
1
  ---
2
  title: Stamp Maker
3
+ emoji: 🪄
4
+ colorFrom: pink
5
+ colorTo: orange
6
  sdk: gradio
7
+ sdk_version: 6.5.0
8
  app_file: app.py
9
  pinned: false
10
  ---
app.py ADDED
@@ -0,0 +1,680 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ from PIL import Image, ImageOps, ImageDraw, ImageEnhance, ImageFont
3
+ import numpy as np
4
+ import base64, io, random
5
+
6
+ try:
7
+ import spaces
8
+ import torch
9
+ from diffusers import StableDiffusionInstructPix2PixPipeline
10
+ HAS_SPACES = True
11
+ except ImportError:
12
+ HAS_SPACES = False
13
+
14
+ _pipe = None
15
+
16
+ def _load_pipe():
17
+ global _pipe
18
+ if _pipe is None:
19
+ _pipe = StableDiffusionInstructPix2PixPipeline.from_pretrained(
20
+ "timbrooks/instruct-pix2pix",
21
+ torch_dtype=torch.float16,
22
+ safety_checker=None,
23
+ ).to("cuda")
24
+ _pipe.set_progress_bar_config(disable=True)
25
+ return _pipe
26
+
27
+ def _engrave_fn(img):
28
+ pipe = _load_pipe()
29
+ return pipe(
30
+ "convert to vintage postage stamp intaglio engraving, fine crosshatch etching lines, monochrome ink illustration",
31
+ image=img,
32
+ num_inference_steps=20,
33
+ image_guidance_scale=1.5,
34
+ guidance_scale=7,
35
+ ).images[0]
36
+
37
+ if HAS_SPACES:
38
+ _engrave_fn = spaces.GPU(_engrave_fn)
39
+
40
+ # ── Palette ───────────────────────────────────────────────────────────────────
41
+ COLORS = {
42
+ "coral": "#FF6B6B",
43
+ "mint": "#00C9A7",
44
+ "lavender": "#9B72CF",
45
+ "gold": "#E8A838",
46
+ "sky": "#4FC3F7",
47
+ "rose": "#F48FB1",
48
+ "sage": "#66BB6A",
49
+ }
50
+
51
+ # ── Helpers ───────────────────────────────────────────────────────────────────
52
+ def hex_rgb(h):
53
+ h = h.lstrip("#")
54
+ return int(h[:2], 16), int(h[2:4], 16), int(h[4:6], 16)
55
+
56
+ def to_b64(img):
57
+ buf = io.BytesIO()
58
+ img.save(buf, "PNG")
59
+ return base64.b64encode(buf.getvalue()).decode()
60
+
61
+ def load_font(size=11):
62
+ for path in [
63
+ "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
64
+ "/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf",
65
+ "/System/Library/Fonts/Helvetica.ttc",
66
+ ]:
67
+ try:
68
+ return ImageFont.truetype(path, size)
69
+ except OSError:
70
+ pass
71
+ return ImageFont.load_default()
72
+
73
+ # ── Stylisation ───────────────────────────────────────────────────────────────
74
+ def stylize(img, style, hex_color):
75
+ r, g, b = hex_rgb(hex_color)
76
+ img = img.convert("RGB")
77
+
78
+ if style in ("engraving", "engraving ✦"):
79
+ if HAS_SPACES:
80
+ try:
81
+ return _engrave_fn(img).convert("RGB")
82
+ except Exception:
83
+ pass
84
+ # local fallback: high-contrast sharpened grayscale
85
+ return ImageOps.autocontrast(ImageEnhance.Sharpness(img.convert("L")).enhance(3.0), cutoff=2).convert("RGB")
86
+
87
+ if style == "duotone":
88
+ gray = ImageOps.autocontrast(img.convert("L"), cutoff=2)
89
+ gray = ImageOps.posterize(gray, 3)
90
+ t = np.array(gray, dtype=np.float32) / 255.0
91
+ return Image.fromarray(np.dstack([
92
+ np.clip(t * 255 + (1-t) * r, 0, 255),
93
+ np.clip(t * 255 + (1-t) * g, 0, 255),
94
+ np.clip(t * 255 + (1-t) * b, 0, 255),
95
+ ]).astype(np.uint8))
96
+
97
+ if style == "grayscale":
98
+ return ImageOps.autocontrast(img.convert("L"), cutoff=2).convert("RGB")
99
+
100
+ if style == "pop art":
101
+ return ImageEnhance.Color(ImageOps.posterize(img, 2)).enhance(2.5)
102
+
103
+ if style == "sepia":
104
+ t = np.array(ImageOps.autocontrast(img.convert("L")), dtype=np.float32) / 255.0
105
+ return Image.fromarray(np.dstack([
106
+ np.clip(t*240,0,255), np.clip(t*200,0,255), np.clip(t*145,0,255),
107
+ ]).astype(np.uint8))
108
+
109
+ return img
110
+
111
+ # ── Stamp image ───────────────────────────────────────────────────────────────
112
+ def build_stamp(arr, color_name, style, label):
113
+ if arr is None:
114
+ return None
115
+
116
+ hex_c = COLORS.get(color_name, "#FF6B6B")
117
+ r, g, b = hex_rgb(hex_c)
118
+ W, H = 280, 330
119
+ BRD, LH, PR, PG = 22, (30 if label.strip() else 0), 5, 16
120
+ aw, ah = W - 2*BRD, H - 2*BRD - LH - 8
121
+
122
+ photo = Image.fromarray(arr).convert("RGB")
123
+ photo.thumbnail((aw, ah), Image.LANCZOS)
124
+ photo = stylize(photo, style, hex_c)
125
+
126
+ stamp = Image.new("RGBA", (W, H), (r, g, b, 255))
127
+ draw = ImageDraw.Draw(stamp)
128
+ draw.rectangle([BRD-4, BRD-4, W-BRD+3, H-BRD+3], fill=(255,255,255,255))
129
+
130
+ px, py = BRD + (aw - photo.width)//2, BRD + 4
131
+ stamp.paste(photo.convert("RGBA"), (px, py))
132
+
133
+ if label.strip():
134
+ font = load_font(11)
135
+ draw.text((W//2, H - BRD - LH//2 + 2), label.upper()[:24],
136
+ fill=(r, g, b, 255), font=font, anchor="mm")
137
+
138
+ pc = (249, 245, 239, 255)
139
+ for x in range(PG, W, PG):
140
+ draw.ellipse([x-PR, -PR, x+PR, PR], fill=pc)
141
+ draw.ellipse([x-PR, H-PR, x+PR, H+PR], fill=pc)
142
+ for y in range(PG, H, PG):
143
+ draw.ellipse([-PR, y-PR, PR, y+PR], fill=pc)
144
+ draw.ellipse([W-PR, y-PR, W+PR, y+PR], fill=pc)
145
+
146
+ return stamp
147
+
148
+ # ── Gallery HTML ──────────────────────────────────────────────────────────────
149
+ _DATES = ["MAR 2026", "FEB 2026", "JAN 2026", "DEC 2025", "NOV 2025"]
150
+
151
+ def gallery_html(stamps):
152
+ style_block = """<style>
153
+ @import url('https://fonts.googleapis.com/css2?family=DM+Serif+Display&family=Space+Mono:wght@400;700&family=DM+Sans:wght@400;500;600&display=swap');
154
+
155
+ .felt-board {
156
+ background: #F7F0E6;
157
+ background-image:
158
+ repeating-linear-gradient(0deg, transparent, transparent 39px, rgba(0,0,0,.03) 39px, rgba(0,0,0,.03) 40px),
159
+ repeating-linear-gradient(90deg, transparent, transparent 39px, rgba(0,0,0,.03) 39px, rgba(0,0,0,.03) 40px);
160
+ min-height: 460px;
161
+ border-radius: 12px;
162
+ padding: 48px 36px;
163
+ display: flex;
164
+ flex-wrap: wrap;
165
+ gap: 36px;
166
+ align-items: flex-start;
167
+ position: relative;
168
+ border: 1px solid #D4C3A8;
169
+ box-shadow: none;
170
+ }
171
+
172
+ .felt-board::before {
173
+ content: '';
174
+ position: absolute;
175
+ inset: 6px;
176
+ border: 1px solid rgba(255,255,255,.04);
177
+ border-radius: 8px;
178
+ pointer-events: none;
179
+ }
180
+
181
+ .stamp-pin {
182
+ transform: rotate(var(--rot, 0deg));
183
+ transition: transform .35s cubic-bezier(.34,1.56,.64,1), filter .35s ease;
184
+ cursor: pointer;
185
+ position: relative;
186
+ }
187
+ .stamp-pin:hover {
188
+ transform: rotate(0deg) scale(1.12) translateY(-10px) !important;
189
+ z-index: 100;
190
+ }
191
+ .stamp-pin img { display: block; width: 122px; border-radius: 1px; }
192
+
193
+ .postmark {
194
+ position: absolute;
195
+ top: 50%; left: 55%;
196
+ transform: translate(-50%, -50%) rotate(-22deg);
197
+ width: 58px; height: 58px;
198
+ border: 2.5px solid var(--mk-color, rgba(40,10,10,.55));
199
+ border-radius: 50%;
200
+ display: flex; flex-direction: column;
201
+ align-items: center; justify-content: center;
202
+ pointer-events: none;
203
+ opacity: var(--mk-vis, 0);
204
+ transition: opacity .2s;
205
+ font-family: 'Space Mono', monospace;
206
+ font-size: 5.5px;
207
+ font-weight: 700;
208
+ letter-spacing: .06em;
209
+ color: var(--mk-color, rgba(40,10,10,.55));
210
+ line-height: 1.4;
211
+ text-align: center;
212
+ text-transform: uppercase;
213
+ }
214
+ .postmark::before, .postmark::after {
215
+ content: '─ ─ ─ ─';
216
+ font-size: 5px;
217
+ letter-spacing: 1px;
218
+ display: block;
219
+ color: inherit;
220
+ opacity: .7;
221
+ }
222
+ .stamp-pin:hover .postmark { opacity: 1; }
223
+
224
+ .board-empty {
225
+ width: 100%;
226
+ display: flex; flex-direction: column;
227
+ align-items: center; justify-content: center;
228
+ min-height: 360px;
229
+ gap: 14px;
230
+ font-family: 'DM Sans', system-ui;
231
+ color: #3A4F35;
232
+ }
233
+ .board-empty .empty-icon { font-size: 2.2rem; opacity: .6; }
234
+ .board-empty p { font-size: .85rem; font-weight: 500; letter-spacing: .04em; text-transform: uppercase; opacity: .5; margin: 0; }
235
+
236
+ .collection-bar {
237
+ display: flex; align-items: center; justify-content: space-between;
238
+ padding: 0 2px 10px;
239
+ }
240
+ .collection-bar .col-label {
241
+ font-family: 'Space Mono', monospace;
242
+ font-size: .68rem;
243
+ font-weight: 700;
244
+ letter-spacing: .12em;
245
+ text-transform: uppercase;
246
+ color: #3A4F35;
247
+ }
248
+ .collection-bar .col-count {
249
+ font-family: 'Space Mono', monospace;
250
+ font-size: .68rem;
251
+ color: #3A4F35;
252
+ letter-spacing: .08em;
253
+ }
254
+ </style>"""
255
+
256
+ if not stamps:
257
+ return f"""{style_block}
258
+ <div class="felt-board">
259
+ <div class="board-empty">
260
+ <div class="empty-icon">✉</div>
261
+ <p>your collection is waiting</p>
262
+ </div>
263
+ </div>"""
264
+
265
+ cards = ""
266
+ mk_colors = ["rgba(120,20,20,.55)", "rgba(20,40,120,.5)", "rgba(20,80,30,.5)"]
267
+ for i, s in enumerate(stamps):
268
+ date = _DATES[i % len(_DATES)]
269
+ mk_c = mk_colors[i % len(mk_colors)]
270
+ cards += (
271
+ f'<div class="stamp-pin" style="--rot:{s["rot"]}deg">'
272
+ f'<img src="data:image/png;base64,{s["b64"]}" />'
273
+ f'</div>'
274
+ )
275
+
276
+ n = len(stamps)
277
+ return f"""{style_block}
278
+ <div class="collection-bar">
279
+ <span class="col-label">collection</span>
280
+ <span class="col-count">{n:02d} stamp{"s" if n!=1 else ""}</span>
281
+ </div>
282
+ <div class="felt-board">{cards}</div>"""
283
+
284
+ # ── App logic ─────────────────────────────────────────────────────────────────
285
+ def preview_stamp(arr, color, style, label):
286
+ return build_stamp(arr, color, style, label)
287
+
288
+ def add_stamp(arr, color, style, label, stamps):
289
+ img = build_stamp(arr, color, style, label)
290
+ if img is None:
291
+ return stamps, gallery_html(stamps)
292
+ rot = round(random.uniform(-6, 6), 1)
293
+ updated = stamps + [{"b64": to_b64(img), "rot": rot, "label": label.strip(), "color": color}]
294
+ return updated, gallery_html(updated)
295
+
296
+ def clear_stamps():
297
+ return [], gallery_html([])
298
+
299
+ # ── CSS ───────────────────────────────────────────────────────────────────────
300
+ CSS = """
301
+ @import url('https://fonts.googleapis.com/css2?family=DM+Serif+Display&family=Space+Mono:wght@400;700&family=DM+Sans:wght@400;500;600&display=swap');
302
+
303
+ /* ─── Reset canvas ─── */
304
+ body, .gradio-container, .main { background: #F7F0E6 !important; }
305
+ footer { display: none !important; }
306
+ .gradio-container { max-width: 1080px !important; margin: 0 auto !important; padding: 0 !important; }
307
+
308
+ /* ─── Blocks / panels ─── */
309
+ .block, .form {
310
+ background: transparent !important;
311
+ border: none !important;
312
+ box-shadow: none !important;
313
+ padding: 0 !important;
314
+ overflow: visible !important;
315
+ }
316
+ .gap, .contain, .row { overflow: visible !important; }
317
+ .gap, .contain { gap: 16px !important; }
318
+
319
+ /* ─── Image component: kill dark backgrounds everywhere ─── */
320
+ [data-testid="image"],
321
+ [data-testid="image"] > div,
322
+ [data-testid="image"] .wrap,
323
+ [data-testid="image"] .image-container,
324
+ [data-testid="image"] > div > div,
325
+ [data-testid="image"] .upload-container,
326
+ [data-testid="image"] .svelte-1ipelgc {
327
+ background: #F0E8D8 !important;
328
+ }
329
+ /* Upload zone text — blanket override for all text nodes inside */
330
+ [data-testid="image"] * {
331
+ color: #3D2E1E !important;
332
+ opacity: 1 !important;
333
+ }
334
+
335
+ /* Upload zone: fill full area, dashed border, no offset */
336
+ [data-testid="image"] .wrap {
337
+ border: 2px dashed #C4B49A !important;
338
+ border-radius: 12px !important;
339
+ width: 100% !important;
340
+ height: 100% !important;
341
+ box-sizing: border-box !important;
342
+ position: relative !important;
343
+ left: auto !important;
344
+ top: auto !important;
345
+ transform: none !important;
346
+ }
347
+ /* Center the drop target content */
348
+ [data-testid="image"] .wrap > div {
349
+ width: 100% !important;
350
+ height: 100% !important;
351
+ display: flex !important;
352
+ flex-direction: column !important;
353
+ align-items: center !important;
354
+ justify-content: center !important;
355
+ }
356
+ /* Source selector bar at bottom */
357
+ [data-testid="image"] .source-selection {
358
+ background: rgba(240,232,216,.9) !important;
359
+ border-top: 1px solid #D4C3A8 !important;
360
+ border-radius: 0 0 10px 10px !important;
361
+ }
362
+ [data-testid="image"] .source-selection button {
363
+ color: #8A7660 !important;
364
+ }
365
+ /* Hide the floating "photo" label button — label is shown by our HTML header instead */
366
+ [data-testid="image"] .label-wrap {
367
+ display: none !important;
368
+ }
369
+ /* Toolbar icon buttons */
370
+ [data-testid="image"] .toolbar,
371
+ [data-testid="image"] .icon-button-wrapper,
372
+ [data-testid="image"] .tool-button,
373
+ [data-testid="image"] button,
374
+ [data-testid="image"] .svelte-button {
375
+ background: transparent !important;
376
+ background-color: transparent !important;
377
+ border: none !important;
378
+ box-shadow: none !important;
379
+ color: #8A7660 !important;
380
+ }
381
+
382
+ /* ─── Labels (component titles like "colour", "style", "label") ─── */
383
+ label > span,
384
+ .block > label > span,
385
+ .form > label > span,
386
+ fieldset > span,
387
+ .gradio-radio > label,
388
+ [data-testid="radio-group"] > span,
389
+ .radio-group > span,
390
+ span.svelte-radiogroup,
391
+ .wrap ~ span,
392
+ .container > span:first-child {
393
+ font-family: 'Space Mono', monospace !important;
394
+ font-size: .65rem !important;
395
+ font-weight: 700 !important;
396
+ letter-spacing: .14em !important;
397
+ text-transform: uppercase !important;
398
+ color: #3D2E1E !important;
399
+ opacity: 1 !important;
400
+ }
401
+
402
+ /* ─── Radio: colour swatches ─── */
403
+ .color-pick .wrap { display: flex !important; flex-wrap: wrap !important; gap: 6px !important; }
404
+ .color-pick .wrap label {
405
+ display: flex !important; align-items: center !important; gap: 7px !important;
406
+ padding: 5px 12px 5px 7px !important;
407
+ border-radius: 20px !important;
408
+ border: 1.5px solid #D4C3A8 !important;
409
+ background: white !important;
410
+ cursor: pointer !important;
411
+ font-family: 'DM Sans', system-ui !important;
412
+ font-size: .78rem !important;
413
+ font-weight: 600 !important;
414
+ color: #3D3020 !important;
415
+ transition: border-color .15s, box-shadow .15s !important;
416
+ user-select: none !important;
417
+ }
418
+ .color-pick .wrap label:hover { border-color: #A89070 !important; box-shadow: 0 2px 8px rgba(0,0,0,.08) !important; }
419
+ .color-pick .wrap label:has(input:checked) { border-color: #5C3A1E !important; box-shadow: 2px 2px 0px #5C3A1E !important; }
420
+ .color-pick .wrap label input[type=radio] { display: none !important; }
421
+ .color-pick .wrap label::before {
422
+ content: '' !important; display: block !important;
423
+ width: 12px !important; height: 12px !important;
424
+ border-radius: 50% !important; flex-shrink: 0 !important;
425
+ }
426
+ .color-pick .wrap label:nth-child(1)::before { background: #FF6B6B !important; }
427
+ .color-pick .wrap label:nth-child(2)::before { background: #00C9A7 !important; }
428
+ .color-pick .wrap label:nth-child(3)::before { background: #9B72CF !important; }
429
+ .color-pick .wrap label:nth-child(4)::before { background: #E8A838 !important; }
430
+ .color-pick .wrap label:nth-child(5)::before { background: #4FC3F7 !important; }
431
+ .color-pick .wrap label:nth-child(6)::before { background: #F48FB1 !important; }
432
+ .color-pick .wrap label:nth-child(7)::before { background: #66BB6A !important; }
433
+
434
+ /* ─── Radio: style pills ─── */
435
+ .style-pick .wrap { display: flex !important; gap: 6px !important; flex-wrap: wrap !important; }
436
+ .style-pick .wrap label {
437
+ padding: 5px 14px !important;
438
+ border-radius: 6px !important;
439
+ border: 1.5px solid #D4C3A8 !important;
440
+ background: white !important;
441
+ cursor: pointer !important;
442
+ font-family: 'Space Mono', monospace !important;
443
+ font-size: .68rem !important;
444
+ font-weight: 700 !important;
445
+ letter-spacing: .06em !important;
446
+ color: #2A1E10 !important;
447
+ transition: all .15s !important;
448
+ user-select: none !important;
449
+ }
450
+ .style-pick .wrap label:hover { background: #F5EDD9 !important; }
451
+ .style-pick .wrap label:has(input:checked),
452
+ .style-pick .wrap label.selected {
453
+ background: #5C3A1E !important;
454
+ color: #E8C97A !important;
455
+ border-color: #5C3A1E !important;
456
+ }
457
+ /* Ensure checked text beats the base color rule */
458
+ .style-pick .wrap label:has(input:checked) span,
459
+ .style-pick .wrap label:has(input:checked)::first-line {
460
+ color: #E8C97A !important;
461
+ }
462
+ .style-pick .wrap label input[type=radio] { display: none !important; }
463
+
464
+ /* ─── Textbox ─── */
465
+ input[type=text], textarea {
466
+ font-family: 'Space Mono', monospace !important;
467
+ font-size: .8rem !important;
468
+ border: 1.5px solid #D4C3A8 !important;
469
+ border-radius: 8px !important;
470
+ background: white !important;
471
+ color: #5C3A1E !important;
472
+ padding: 10px 14px !important;
473
+ }
474
+ input[type=text]:focus, textarea:focus {
475
+ border-color: #5C3A1E !important;
476
+ box-shadow: 2px 2px 0px #5C3A1E !important;
477
+ outline: none !important;
478
+ }
479
+
480
+ /* ─── Buttons ─── */
481
+ button.primary {
482
+ background: #5C3A1E !important;
483
+ color: #E8C97A !important;
484
+ border: none !important;
485
+ border-radius: 8px !important;
486
+ font-family: 'Space Mono', monospace !important;
487
+ font-size: .75rem !important;
488
+ font-weight: 700 !important;
489
+ letter-spacing: .1em !important;
490
+ text-transform: uppercase !important;
491
+ padding: 12px 20px !important;
492
+ box-shadow: 3px 3px 0px #8A7660 !important;
493
+ transition: transform .12s, box-shadow .12s !important;
494
+ }
495
+ button.primary:hover { transform: translate(-1px, -1px) !important; box-shadow: 4px 4px 0px #8A7660 !important; }
496
+ button.primary:active { transform: translate(2px, 2px) !important; box-shadow: 1px 1px 0px #8A7660 !important; }
497
+
498
+ button.secondary {
499
+ background: white !important;
500
+ color: #3D2E1E !important;
501
+ border: 1.5px solid #D4C3A8 !important;
502
+ border-radius: 8px !important;
503
+ font-family: 'Space Mono', monospace !important;
504
+ font-size: .68rem !important;
505
+ font-weight: 700 !important;
506
+ letter-spacing: .08em !important;
507
+ text-transform: uppercase !important;
508
+ padding: 10px 14px !important;
509
+ transition: all .15s !important;
510
+ }
511
+ button.secondary:hover { border-color: #8A7660 !important; color: #3D3020 !important; }
512
+
513
+ /* ─── Preview image ─── */
514
+ [data-testid="image"].preview-img .wrap {
515
+ border-radius: 12px !important;
516
+ overflow: hidden !important;
517
+ border: 2px solid #D4C3A8 !important;
518
+ background: #F0E8D8 !important;
519
+ }
520
+
521
+ /* ─── Preview output: remove black toolbar/label/buttons ─── */
522
+ .preview-img [data-testid="image"] .toolbar,
523
+ .preview-img .toolbar,
524
+ .preview-img .icon-button-wrapper,
525
+ .preview-img button,
526
+ .preview-img .label-wrap,
527
+ .preview-img .label-wrap span,
528
+ .preview-img .download-button {
529
+ background: rgba(240,232,216,.9) !important;
530
+ background-color: rgba(240,232,216,.9) !important;
531
+ color: #8A7660 !important;
532
+ border-color: #D4C3A8 !important;
533
+ box-shadow: none !important;
534
+ }
535
+ """
536
+
537
+ # ── Decorative HTML pieces ────────────────────────────────────────────────────
538
+ HEADER_HTML = """
539
+ <div style="font-family:'Space Mono',monospace;padding:0 0 28px;">
540
+
541
+ <!-- top rule -->
542
+ <div style="display:flex;align-items:center;gap:12px;margin-bottom:20px;opacity:.4">
543
+ <div style="flex:1;height:1px;background:repeating-linear-gradient(90deg,#8A7660 0,#8A7660 6px,transparent 6px,transparent 12px)"></div>
544
+ <span style="font-size:.6rem;letter-spacing:.18em;color:#8A7660;white-space:nowrap">PAR AVION · AIRMAIL · 航空郵便</span>
545
+ <div style="flex:1;height:1px;background:repeating-linear-gradient(90deg,#8A7660 0,#8A7660 6px,transparent 6px,transparent 12px)"></div>
546
+ </div>
547
+
548
+ <div style="display:flex;align-items:flex-end;justify-content:space-between;gap:24px;flex-wrap:wrap;">
549
+
550
+ <!-- title block -->
551
+ <div>
552
+ <div style="font-family:'DM Serif Display',Georgia,serif;font-size:3.2rem;line-height:.95;letter-spacing:-.02em;color:#5C3A1E">
553
+ stamp<br>maker
554
+ </div>
555
+ <div style="margin-top:10px;font-size:.62rem;letter-spacing:.18em;color:#8A7660;text-transform:uppercase">
556
+ ✦ turn photos into collectibles ✦
557
+ </div>
558
+ </div>
559
+
560
+ <!-- postmark decoration -->
561
+ <div style="display:flex;gap:16px;align-items:center;opacity:.65;flex-shrink:0">
562
+ <div style="width:72px;height:72px;border:2.5px solid #8A7660;border-radius:50%;display:flex;flex-direction:column;align-items:center;justify-content:center;font-size:5.5px;letter-spacing:.1em;color:#6A5A45;text-align:center;line-height:1.7;text-transform:uppercase;position:relative">
563
+ <div style="position:absolute;top:-1px;left:50%;transform:translateX(-50%);width:80%;height:2px;background:#F7F0E6"></div>
564
+ <div style="position:absolute;bottom:-1px;left:50%;transform:translateX(-50%);width:80%;height:2px;background:#F7F0E6"></div>
565
+ FIRST CLASS<br><span style="font-size:7px;font-weight:700">2026</span><br>HAND STAMPED
566
+ </div>
567
+ <div style="display:flex;flex-direction:column;gap:3px">
568
+ <div style="width:48px;height:1.5px;background:#8A7660;border-radius:1px"></div>
569
+ <div style="width:40px;height:1.5px;background:#8A7660;border-radius:1px"></div>
570
+ <div style="width:44px;height:1.5px;background:#8A7660;border-radius:1px"></div>
571
+ </div>
572
+ <div style="writing-mode:vertical-rl;font-size:.55rem;letter-spacing:.2em;color:#8A7660;text-transform:uppercase;opacity:.8">
573
+ philatelist studio
574
+ </div>
575
+ </div>
576
+ </div>
577
+
578
+ <!-- bottom rule -->
579
+ <div style="margin-top:20px;display:flex;align-items:center;gap:12px;opacity:.35">
580
+ <div style="flex:1;border-top:1.5px solid #8A7660"></div>
581
+ <div style="width:6px;height:6px;border:1.5px solid #8A7660;border-radius:50%"></div>
582
+ <div style="flex:1;border-top:1.5px solid #8A7660"></div>
583
+ </div>
584
+ </div>
585
+ """
586
+
587
+ SECTION_DIVIDER = """
588
+ <div style="margin:8px 0 4px;display:flex;align-items:center;gap:10px;opacity:.3">
589
+ <div style="flex:1;height:1px;background:repeating-linear-gradient(90deg,#8A7660 0,#8A7660 4px,transparent 4px,transparent 8px)"></div>
590
+ <span style="font-family:'Space Mono',monospace;font-size:.55rem;letter-spacing:.2em;color:#3D2E1E;text-transform:uppercase;white-space:nowrap">your collection</span>
591
+ <div style="flex:1;height:1px;background:repeating-linear-gradient(90deg,#8A7660 0,#8A7660 4px,transparent 4px,transparent 8px)"></div>
592
+ </div>
593
+ """
594
+
595
+ CONTROLS_HEADER = """
596
+ <div style="font-family:'Space Mono',monospace;font-size:.6rem;letter-spacing:.18em;text-transform:uppercase;color:#3D2E1E;margin-bottom:20px;margin-top:8px;">
597
+ ── studio controls ──
598
+ </div>
599
+ """
600
+
601
+ PREVIEW_HEADER = """
602
+ <div style="font-family:'Space Mono',monospace;font-size:.6rem;letter-spacing:.18em;text-transform:uppercase;color:#3D2E1E;margin-bottom:2px;">
603
+ ── preview ──
604
+ </div>
605
+ """
606
+
607
+ # ── UI ────────────────────────────────────────────────────────────────────────
608
+ with gr.Blocks(css=CSS, title="stamp maker ✦") as demo:
609
+ state = gr.State([])
610
+
611
+ gr.HTML(HEADER_HTML)
612
+
613
+ with gr.Row(equal_height=False):
614
+
615
+ # ── Controls ──
616
+ with gr.Column(scale=5, min_width=300):
617
+ gr.HTML(CONTROLS_HEADER)
618
+ img_in = gr.Image(
619
+ label="photo",
620
+ type="numpy",
621
+ sources=["upload", "webcam"],
622
+ height=220,
623
+ show_label=False,
624
+ )
625
+ color_in = gr.Radio(
626
+ choices=list(COLORS.keys()),
627
+ value="coral",
628
+ label="colour",
629
+ elem_classes=["color-pick"],
630
+ )
631
+ style_in = gr.Radio(
632
+ choices=["duotone", "grayscale", "pop art", "sepia", "engraving ✦"],
633
+ value="duotone",
634
+ label="style",
635
+ elem_classes=["style-pick"],
636
+ )
637
+ label_in = gr.Textbox(
638
+ label="stamp label",
639
+ show_label=False,
640
+ placeholder="stamp label e.g. tokyo / 2026",
641
+ max_lines=1,
642
+ )
643
+ with gr.Row():
644
+ add_btn = gr.Button("add to collection ✦", variant="primary", scale=3)
645
+ clear_btn = gr.Button("clear all", variant="secondary", scale=1)
646
+
647
+ # ── Preview ──
648
+ with gr.Column(scale=4, min_width=260):
649
+ gr.HTML(PREVIEW_HEADER)
650
+ preview_out = gr.Image(
651
+ label="stamp preview",
652
+ type="pil",
653
+ interactive=False,
654
+ show_label=False,
655
+ height=380,
656
+ buttons=["download"],
657
+ elem_classes=["preview-img"],
658
+ )
659
+
660
+ gr.HTML(SECTION_DIVIDER)
661
+ gallery_out = gr.HTML(value=gallery_html([]))
662
+
663
+ # ── Events ──
664
+ for ctrl in [img_in, color_in, style_in, label_in]:
665
+ ctrl.change(
666
+ fn=preview_stamp,
667
+ inputs=[img_in, color_in, style_in, label_in],
668
+ outputs=preview_out,
669
+ )
670
+
671
+ add_btn.click(
672
+ fn=add_stamp,
673
+ inputs=[img_in, color_in, style_in, label_in, state],
674
+ outputs=[state, gallery_out],
675
+ )
676
+
677
+ clear_btn.click(fn=clear_stamps, outputs=[state, gallery_out])
678
+
679
+ if __name__ == "__main__":
680
+ demo.launch()
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ gradio>=6.5.0
2
+ pillow>=10.0.0
3
+ numpy>=1.24.0
4
+ diffusers>=0.27.0
5
+ transformers>=4.40.0
6
+ accelerate>=0.30.0
7
+ torch