kobinasam commited on
Commit
a82854a
·
verified ·
1 Parent(s): 1004f98

initial-commit

Browse files
Files changed (3) hide show
  1. README.md +15 -7
  2. app.py +254 -0
  3. requirements.txt +9 -0
README.md CHANGED
@@ -1,15 +1,23 @@
1
  ---
2
- title: No Where Post
3
- emoji: 🏃
4
- colorFrom: gray
5
- colorTo: blue
6
  sdk: gradio
7
- sdk_version: 6.17.3
8
- python_version: '3.12'
9
  app_file: app.py
10
  pinned: false
11
  license: apache-2.0
12
  short_description: Name a place that doesn't exist. Receive its postage stamp.
 
13
  ---
14
 
15
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Nowhere Post
3
+ emoji: 📮
4
+ colorFrom: red
5
+ colorTo: yellow
6
  sdk: gradio
7
+ sdk_version: 6.16.0
 
8
  app_file: app.py
9
  pinned: false
10
  license: apache-2.0
11
  short_description: Name a place that doesn't exist. Receive its postage stamp.
12
+ tags: [small-models-big-adventures, black-forest-labs, flux]
13
  ---
14
 
15
+ # Nowhere Post 📮
16
+
17
+ Name a place that doesn't exist and the post office of Nowhere issues an illustrated
18
+ postage stamp for it. Collect them in your album.
19
+
20
+ Every result is composed into a perforated stamp frame, so it always looks the part.
21
+ Runs on **Black Forest Labs FLUX.1-schnell** (~12B, Apache-2.0, under the 32B ceiling)
22
+ to paint the picture; if no GPU is available it draws a procedural scene instead, so
23
+ the app always boots and demos.
app.py ADDED
@@ -0,0 +1,254 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Nowhere Post
3
+ ============
4
+ Name a place that doesn't exist. The post office of Nowhere issues a stamp for it.
5
+ Collect them in your album.
6
+
7
+ Track 2 toy for the "Small Models Big Adventures" hackathon.
8
+ Model: black-forest-labs/FLUX.1-schnell (~12B, Apache-2.0, under the 32B ceiling).
9
+
10
+ Every result is composed into a perforated postage-stamp frame with PIL, so it
11
+ always looks like a stamp. In model mode FLUX paints the picture; in keeper mode a
12
+ procedural scene is drawn, so the app boots and demos with no GPU.
13
+ """
14
+
15
+ import os
16
+ import random
17
+ import inspect
18
+
19
+ import gradio as gr
20
+ from PIL import Image, ImageDraw, ImageFont
21
+
22
+ _BLOCKS_HAS_CSS = "css" in inspect.signature(gr.Blocks.__init__).parameters
23
+ _LAUNCH_HAS_SSR = "ssr_mode" in inspect.signature(gr.Blocks.launch).parameters
24
+
25
+ MODEL_ID = os.environ.get("POST_MODEL", "black-forest-labs/FLUX.1-schnell")
26
+ DEBUG = os.environ.get("POST_DEBUG", "").strip().lower() in {"1", "true", "yes"}
27
+ ART = 384 # inner picture size (square)
28
+ STEPS = int(os.environ.get("POST_STEPS", "4")) # schnell is happy at 4
29
+
30
+ EXAMPLES = ["the Isle of Lost Umbrellas", "Cloudfall", "the Last Lighthouse",
31
+ "the Market at the Edge of Sleep"]
32
+
33
+ def _noop_gpu(*a, **k):
34
+ def wrap(fn):
35
+ return fn
36
+ return wrap(a[0]) if a and callable(a[0]) else wrap
37
+
38
+ if os.environ.get("SPACES_ZERO_GPU", "").lower() in {"true", "1"}:
39
+ try:
40
+ import spaces
41
+ GPU = spaces.GPU
42
+ except Exception: # noqa: BLE001
43
+ GPU = _noop_gpu
44
+ else:
45
+ GPU = _noop_gpu
46
+
47
+ _pipe = None
48
+ MODE = "keeper"
49
+ _warmed = False
50
+
51
+
52
+ def load_model():
53
+ global _pipe, MODE
54
+ try:
55
+ import torch
56
+ from diffusers import FluxPipeline
57
+ _pipe = FluxPipeline.from_pretrained(MODEL_ID, torch_dtype=torch.bfloat16)
58
+ MODE = "model"
59
+ print(f"[Post] Loaded {MODEL_ID} -- model mode.")
60
+ except Exception as exc: # noqa: BLE001
61
+ MODE = "keeper"
62
+ print(f"[Post] Could not load {MODEL_ID} ({exc}). Keeper mode active.")
63
+
64
+
65
+ load_model()
66
+
67
+
68
+ def _font(size):
69
+ for path in ("DejaVuSans-Bold.ttf", "DejaVuSans.ttf",
70
+ "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"):
71
+ try:
72
+ return ImageFont.truetype(path, size)
73
+ except Exception: # noqa: BLE001
74
+ continue
75
+ return ImageFont.load_default()
76
+
77
+
78
+ @GPU(duration=60)
79
+ def _flux_art(place):
80
+ import torch
81
+ if torch.cuda.is_available() and getattr(_pipe, "device", None) is not None \
82
+ and _pipe.device.type != "cuda":
83
+ _pipe.to("cuda")
84
+ prompt = (f"a vintage postage stamp illustration of {place}, storybook engraving "
85
+ f"style, soft muted inks, fine linework, whimsical, centered scene")
86
+ g = torch.Generator(device=_pipe.device).manual_seed(random.randint(0, 2**31 - 1))
87
+ img = _pipe(prompt, num_inference_steps=STEPS, guidance_scale=0.0,
88
+ height=ART, width=ART, max_sequence_length=256, generator=g).images[0]
89
+ return img
90
+
91
+
92
+ def _procedural_art(place):
93
+ """A seeded little scene for keeper mode, so a stamp always has a picture."""
94
+ rng = random.Random(place.strip().lower())
95
+ palettes = [((214, 198, 150), (122, 150, 120), (90, 110, 140)),
96
+ ((232, 210, 170), (180, 140, 110), (120, 90, 120)),
97
+ ((205, 220, 225), (120, 160, 170), (70, 100, 120)),
98
+ ((235, 200, 160), (200, 120, 90), (110, 70, 90))]
99
+ sky, land, accent = rng.choice(palettes)
100
+ img = Image.new("RGB", (ART, ART), sky)
101
+ d = ImageDraw.Draw(img)
102
+ horizon = int(ART * rng.uniform(0.55, 0.72))
103
+ d.rectangle([0, horizon, ART, ART], fill=land)
104
+ # a sun or moon
105
+ r = rng.randint(28, 46)
106
+ cxp = rng.randint(r, ART - r)
107
+ d.ellipse([cxp - r, horizon - 90 - r, cxp + r, horizon - 90 + r], fill=accent)
108
+ # a few hills / shapes
109
+ for _ in range(rng.randint(2, 4)):
110
+ hw = rng.randint(80, 200)
111
+ hx = rng.randint(-40, ART)
112
+ hh = rng.randint(30, 80)
113
+ d.ellipse([hx, horizon - hh, hx + hw, horizon + hh], fill=accent)
114
+ return img
115
+
116
+
117
+ def compose_stamp(inner, place):
118
+ """Frame a square picture as a perforated postage stamp with a caption."""
119
+ pad, cap = 26, 56
120
+ W, H = ART + pad * 2, ART + pad * 2 + cap
121
+ stamp = Image.new("RGB", (W, H), (253, 250, 240))
122
+ d = ImageDraw.Draw(stamp)
123
+ # perforations
124
+ step = 16
125
+ for x in range(0, W, step):
126
+ d.ellipse([x - 5, -5, x + 5, 5], fill=(243, 234, 215))
127
+ d.ellipse([x - 5, H - 5, x + 5, H + 5], fill=(243, 234, 215))
128
+ for y in range(0, H, step):
129
+ d.ellipse([-5, y - 5, 5, y + 5], fill=(243, 234, 215))
130
+ d.ellipse([W - 5, y - 5, W + 5, y + 5], fill=(243, 234, 215))
131
+ d.rectangle([10, 10, W - 10, H - 10], outline=(170, 100, 28), width=2)
132
+ stamp.paste(inner.resize((ART, ART)), (pad, pad))
133
+ d.rectangle([pad, pad, pad + ART, pad + ART], outline=(170, 100, 28), width=1)
134
+ # header + caption
135
+ fhead, fcap = _font(15), _font(20)
136
+ d.text((pad, 12), "NOWHERE POST", font=fhead, fill=(122, 46, 46))
137
+ denom = random.choice(["1", "2", "3", "5", "7"])
138
+ d.text((W - pad - 30, 12), f"{denom} \u2606", font=fhead, fill=(122, 46, 46))
139
+ name = place if len(place) <= 30 else place[:29] + "\u2026"
140
+ tw = d.textlength(name, font=fcap)
141
+ d.text(((W - tw) / 2, pad + ART + 14), name, font=fcap, fill=(58, 47, 37))
142
+ return stamp
143
+
144
+
145
+ def make_stamp(place):
146
+ inner = None
147
+ if MODE == "model":
148
+ try:
149
+ inner = _flux_art(place)
150
+ except Exception as exc: # noqa: BLE001
151
+ print(f"[Post] generation error: {exc}")
152
+ inner = None
153
+ if inner is None:
154
+ inner = _procedural_art(place)
155
+ return compose_stamp(inner, place.strip())
156
+
157
+
158
+ def on_issue(place, album):
159
+ global _warmed
160
+ place = (place or "").strip()
161
+ if not place:
162
+ yield None, album, album, ""
163
+ return
164
+ if MODE == "model" and not _warmed:
165
+ # show a placeholder frame while FLUX warms up
166
+ warming = compose_stamp(_procedural_art(place), place)
167
+ yield warming, album, album, ""
168
+ stamp = make_stamp(place)
169
+ if MODE == "model":
170
+ _warmed = True
171
+ album = (album or []) + [(stamp, place)]
172
+ yield stamp, [s for s, _ in album], album, ""
173
+
174
+
175
+ def on_reset():
176
+ return None, [], [], ""
177
+
178
+
179
+ def use_example(text):
180
+ return text
181
+
182
+
183
+ CSS = """
184
+ @import url('https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,400;0,9..144,600;1,9..144,400&family=Spectral:ital,wght@0,400;0,500;1,400&display=swap');
185
+ :root{--paper:#f3ead7;--paper-2:#efe3c9;--ink:#2f2a22;--ink-soft:#6b5a47;
186
+ --post:#7a2e2e;--amber-deep:#aa641c;--line:#c9b48f;}
187
+ .gradio-container,.gradio-container.dark,.dark{
188
+ --body-background-fill:transparent;--background-fill-primary:#fffaf0;--background-fill-secondary:#f6edd8;
189
+ --block-background-fill:#fffaf0;--block-border-color:var(--line);--border-color-primary:var(--line);
190
+ --body-text-color:var(--ink);--body-text-color-subdued:var(--ink-soft);
191
+ --block-label-text-color:var(--post);--block-title-text-color:var(--ink);
192
+ --block-label-background-fill:#efe3c9;--block-title-background-fill:transparent;
193
+ --input-background-fill:#fffaf0;--input-border-color:var(--line);--input-placeholder-color:var(--ink-soft);
194
+ --button-primary-background-fill:var(--post);--button-primary-background-fill-hover:#641f1f;
195
+ --button-primary-text-color:#fbf7ec;--button-primary-border-color:#641f1f;
196
+ --button-secondary-background-fill:#efe3c9;--button-secondary-background-fill-hover:#e7d6b3;
197
+ --button-secondary-text-color:var(--ink);--button-secondary-border-color:var(--line);
198
+ --color-accent:var(--amber-deep);--color-accent-soft:#f3e3c4;}
199
+ .gradio-container{background:radial-gradient(120% 80% at 80% -10%,#f7efdd,var(--paper) 55%,var(--paper-2));
200
+ font-family:'Spectral',Georgia,serif !important;color:var(--ink) !important;max-width:960px !important;}
201
+ .gradio-container textarea,.gradio-container input[type="text"],.gradio-container input:not([type]){
202
+ background:#fffaf0 !important;color:var(--ink) !important;-webkit-text-fill-color:var(--ink) !important;border-color:var(--line) !important;}
203
+ .gradio-container textarea::placeholder,.gradio-container input::placeholder{color:var(--ink-soft) !important;-webkit-text-fill-color:var(--ink-soft) !important;opacity:1;}
204
+ .np-title{font-family:'Fraunces',serif;font-weight:600;font-size:2.5rem;line-height:1;margin:.2rem 0 0;}
205
+ .np-title em{font-style:italic;color:var(--post);}
206
+ .np-sub{font-style:italic;color:var(--ink-soft);margin:.35rem 0 1rem;font-size:1.05rem;}
207
+ .np-mode{display:inline-block;font-size:.72rem;letter-spacing:.12em;text-transform:uppercase;color:var(--post);border:1px solid var(--line);border-radius:999px;padding:.15rem .6rem;}
208
+ .np-foot{color:var(--ink-soft);font-size:.82rem;font-style:italic;text-align:center;margin-top:10px;}
209
+ footer{display:none !important;}
210
+ """
211
+
212
+ _bk = {"title": "Nowhere Post"}
213
+ if _BLOCKS_HAS_CSS:
214
+ _bk["css"] = CSS
215
+ _bk["theme"] = gr.themes.Soft()
216
+
217
+ with gr.Blocks(**_bk) as demo:
218
+ album = gr.State([])
219
+ public = "FLUX.1-schnell · on-device" if MODE == "model" else "Nowhere Post"
220
+ mode_label = f"{public} · [{MODE}]" if DEBUG else public
221
+ gr.HTML(f"""
222
+ <div><div class="np-title">Nowhere <em>Post</em></div>
223
+ <div class="np-sub">Name a place that doesn't exist. Receive its postage stamp.</div>
224
+ <span class="np-mode">{mode_label}</span></div>""")
225
+
226
+ with gr.Row():
227
+ with gr.Column(scale=3):
228
+ with gr.Row():
229
+ place = gr.Textbox(placeholder="a place that doesn't exist... (the Isle of Lost Umbrellas)",
230
+ show_label=False, scale=7, autofocus=True)
231
+ go = gr.Button("Issue stamp", variant="primary", scale=2)
232
+ with gr.Row():
233
+ ex_btns = [gr.Button(e, size="sm") for e in EXAMPLES]
234
+ stamp = gr.Image(label="Latest stamp", type="pil", height=460, show_label=False)
235
+ reset = gr.Button("Start a new album", size="sm")
236
+ with gr.Column(scale=2):
237
+ gallery = gr.Gallery(label="Your album", columns=2, height=520, show_label=True)
238
+
239
+ gr.HTML('<div class="np-foot">Every place that isn\'t real still deserves a stamp.</div>')
240
+
241
+ go.click(on_issue, [place, album], [stamp, gallery, album, place])
242
+ place.submit(on_issue, [place, album], [stamp, gallery, album, place])
243
+ reset.click(on_reset, None, [stamp, gallery, album, place])
244
+ for b, e in zip(ex_btns, EXAMPLES):
245
+ b.click(use_example, gr.State(e), place)
246
+
247
+ if __name__ == "__main__":
248
+ _lk = {}
249
+ if not _BLOCKS_HAS_CSS:
250
+ _lk["css"] = CSS
251
+ _lk["theme"] = gr.themes.Soft()
252
+ if _LAUNCH_HAS_SSR:
253
+ _lk["ssr_mode"] = False
254
+ demo.queue(max_size=24).launch(**_lk)
requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ gradio==6.17.3
2
+ diffusers>=0.31.0
3
+ transformers>=4.46.0
4
+ accelerate>=1.0.0
5
+ torch
6
+ sentencepiece
7
+ protobuf
8
+ Pillow
9
+ spaces