|
|
@@ -1,35 +1,535 @@ |
|
1 |
+"""Gradio demo: ennustab tänavapildi järgi riigi. |
|
2 |
+ |
|
3 |
+Kujundus laenab trükiatlaselt, sest leht ongi sisuliselt atlase register: |
|
4 |
+riikide loend arvudega. Soe kaardipaber, pruunikas graveeringutint, terrakota |
|
5 |
+reljeefivärv, koordinaadivõrk ainult päise all, ja register juhtpunktidega. |
|
6 |
+ |
|
7 |
+Kõik arvud loetakse treeningu väljunditest, et demo ei saaks lubada rohkem kui |
|
8 |
+mudel tegelikult oskab. |
|
9 |
+""" |
|
10 |
+ |
|
11 |
+import json |
| 1 |
12 |
import os |
|
13 |
+from pathlib import Path |
| 2 |
14 |
|
| 3 |
15 |
import gradio as gr |
|
16 |
+import pandas as pd |
| 4 |
17 |
import timm |
| 5 |
18 |
import torch |
| 6 |
19 |
|
| 7 |
20 |
from countrysense.data import build_transforms |
| 8 |
21 |
from countrysense.model import load_checkpoint |
| 9 |
22 |
|
| 10 |
|
-CHECKPOINT = os.environ.get("COUNTRYSENSE_CKPT", "runs/clip_vit_b16/best.pt") |
|
23 |
+CHECKPOINT = Path(os.environ.get("COUNTRYSENSE_CKPT", "runs/clip_vit_b16/best.pt")) |
|
24 |
+RUN_DIR = CHECKPOINT.parent |
| 11 |
25 |
|
| 12 |
26 |
device = torch.device("cuda" if torch.cuda.is_available() else "cpu") |
| 13 |
|
-model, classes = load_checkpoint(CHECKPOINT, device) |
|
27 |
+model, classes = load_checkpoint(str(CHECKPOINT), device) |
| 14 |
28 |
data_cfg = timm.data.resolve_model_data_config(model.backbone) |
| 15 |
29 |
tfm = build_transforms(data_cfg["mean"], data_cfg["std"], |
| 16 |
30 |
data_cfg["input_size"][-1], train=False) |
| 17 |
31 |
|
| 18 |
32 |
|
|
33 |
+def _read_metrics(): |
|
34 |
+ try: |
|
35 |
+ return json.loads((RUN_DIR / "metrics.json").read_text(encoding="utf-8")) |
|
36 |
+ except (OSError, ValueError): |
|
37 |
+ return {} |
|
38 |
+ |
|
39 |
+ |
|
40 |
+def _read_per_country(): |
|
41 |
+ try: |
|
42 |
+ df = pd.read_csv(RUN_DIR / "per_country.csv") |
|
43 |
+ except (OSError, ValueError): |
|
44 |
+ return pd.DataFrame(columns=["country", "accuracy", "support"]) |
|
45 |
+ return df.sort_values("accuracy", ascending=False).reset_index(drop=True) |
|
46 |
+ |
|
47 |
+ |
|
48 |
+METRICS = _read_metrics() |
|
49 |
+PER_COUNTRY = _read_per_country() |
|
50 |
+ |
|
51 |
+ |
|
52 |
+def _pct(value, digits=1): |
|
53 |
+ return f"{value * 100:.{digits}f}".replace(".", ",") + "%" |
|
54 |
+ |
|
55 |
+ |
|
56 |
+# ── Ennustus ────────────────────────────────────────────────────────────────── |
|
57 |
+ |
|
58 |
+EMPTY = '<p class="empty">Lisa kõrvale tänavapilt.</p>' |
|
59 |
+ |
|
60 |
+ |
| 19 |
61 |
def predict(img): |
|
62 |
+ if img is None: |
|
63 |
+ return EMPTY |
|
64 |
+ |
| 20 |
65 |
with torch.no_grad(): |
| 21 |
66 |
probs = model(tfm(img.convert("RGB")).unsqueeze(0).to(device)).softmax(1)[0] |
| 22 |
|
- return {classes[i]: float(p) for p, i in |
| 23 |
|
- zip(*[t.tolist() for t in probs.topk(min(5, len(classes)))])} |
|
67 |
+ top = probs.topk(min(5, len(classes))) |
|
68 |
+ ranked = [(classes[i], p) for p, i in |
|
69 |
+ zip(top.values.tolist(), top.indices.tolist())] |
|
70 |
+ |
|
71 |
+ lead, lead_p = ranked[0] |
|
72 |
+ note = ("kindel määrang" if lead_p >= 0.7 else |
|
73 |
+ "kõhklev määrang" if lead_p >= 0.4 else "nõrk määrang, ära usalda") |
|
74 |
+ |
|
75 |
+ rest = "".join( |
|
76 |
+ f""" |
|
77 |
+ <li class="alt"> |
|
78 |
+ <span class="alt__name">{country}</span> |
|
79 |
+ <span class="alt__bar"><i style="width:{p * 100:.1f}%"></i></span> |
|
80 |
+ <span class="alt__pct num">{_pct(p)}</span> |
|
81 |
+ </li> |
|
82 |
+ """ |
|
83 |
+ for country, p in ranked[1:] |
|
84 |
+ ) |
|
85 |
+ |
|
86 |
+ return f""" |
|
87 |
+<div class="out"> |
|
88 |
+ <p class="eyebrow eyebrow--mark">Määrang</p> |
|
89 |
+ <p class="out__lead">{lead}</p> |
|
90 |
+ <p class="out__meta"><span class="num">{_pct(lead_p)}</span> · <em>{note}</em></p> |
|
91 |
+ <ul class="alts">{rest}</ul> |
|
92 |
+</div> |
|
93 |
+""" |
|
94 |
+ |
|
95 |
+ |
|
96 |
+# ── Staatiline sisu ─────────────────────────────────────────────────────────── |
|
97 |
+ |
|
98 |
+def _head_html(): |
|
99 |
+ stats = [ |
|
100 |
+ ("top-1", _pct(METRICS["top1"]) if "top1" in METRICS else "—"), |
|
101 |
+ ("top-5", _pct(METRICS["top5"]) if "top5" in METRICS else "—"), |
|
102 |
+ ("makro-F1", f"{METRICS['macro_f1']:.3f}".replace(".", ",") |
|
103 |
+ if "macro_f1" in METRICS else "—"), |
|
104 |
+ ("klasse", str(METRICS.get("n_classes", len(classes)))), |
|
105 |
+ ("testipilte", f"{METRICS['n_test']:,}".replace(",", " ") |
|
106 |
+ if "n_test" in METRICS else "—"), |
|
107 |
+ ] |
|
108 |
+ cells = "".join( |
|
109 |
+ f'<div class="stat"><p class="eyebrow">{label}</p>' |
|
110 |
+ f'<p class="stat__v num">{value}</p></div>' |
|
111 |
+ for label, value in stats |
|
112 |
+ ) |
|
113 |
+ return f""" |
|
114 |
+<header class="mast"> |
|
115 |
+ <div class="mast__grid" aria-hidden="true"></div> |
|
116 |
+ <div class="mast__body"> |
|
117 |
+ <p class="eyebrow eyebrow--mark">Peenhäälestatud CLIP ViT-B/16</p> |
|
118 |
+ <h1 class="mast__title">CountrySense</h1> |
|
119 |
+ <p class="mast__lede"> |
|
120 |
+ Määrab ühe tänavapildi järgi riigi. <em>Riigi tase, mitte koordinaadid.</em> |
|
121 |
+ Arvud all on mõõdetud kõrvale pandud testihulgal, mida mudel treeningu ajal |
|
122 |
+ ei näinud. |
|
123 |
+ </p> |
|
124 |
+ </div> |
|
125 |
+ <div class="stats">{cells}</div> |
|
126 |
+</header> |
|
127 |
+""" |
|
128 |
+ |
|
129 |
+ |
|
130 |
+CAVEATS = [ |
|
131 |
+ ("Mudel valib alati mõne registris oleva riigi", |
|
132 |
+ "Kui pilt on tehtud kuskil mujal, tuleb ikkagi enesekindel vastus ja see on " |
|
133 |
+ "vale. Vaata registrit enne, kui määrangut usud."), |
|
134 |
+ ("Ootab tänavavaate kaadrit", |
|
135 |
+ "Treeningpildid on GeoGuessri Street View kaadrid: väljas, päevavalguses, " |
|
136 |
+ "tee tasandilt. Toapildid, lähivõtted, öised kaadrid ja ekraanitõmmised " |
|
137 |
+ "muust kontekstist jäävad ootuspäraselt kehvaks."), |
|
138 |
+ ("Naaberriigid lähevad segamini", |
|
139 |
+ "Ida-Euroopa on nõrgim koht. Läti, Slovakkia ja Ukraina tabavus on " |
|
140 |
+ "testihulgal null: need lähevad segamini Venemaa, Poola ja üksteisega."), |
|
141 |
+ ("Klassid ei ole ühesuurused", |
|
142 |
+ "Testipilte on riigi kohta 11 kuni 200. Väikese valimiga riigi protsent on " |
|
143 |
+ "lärmakas, mistõttu on registris iga rea juures ka valimi suurus."), |
|
144 |
+ ("Koordinaadid on teadlikult väljas", |
|
145 |
+ "Laius- ja pikkuskraadi määramine nõuab miljoneid pilte ja arvutusvõimsust, " |
|
146 |
+ "mida sellel projektil ei ole. Riigi tasand on aus ülesanne, mille sai " |
|
147 |
+ "korralikult lahendada. Piirkonna tasand on järgmine samm."), |
|
148 |
+] |
|
149 |
+ |
|
150 |
+ |
|
151 |
+def _caveats_html(): |
|
152 |
+ items = "".join( |
|
153 |
+ f""" |
|
154 |
+ <li class="note"> |
|
155 |
+ <span class="note__n num">{n:02d}</span> |
|
156 |
+ <div> |
|
157 |
+ <h3>{title}</h3> |
|
158 |
+ <p>{body}</p> |
|
159 |
+ </div> |
|
160 |
+ </li> |
|
161 |
+ """ |
|
162 |
+ for n, (title, body) in enumerate(CAVEATS, start=1) |
|
163 |
+ ) |
|
164 |
+ return f""" |
|
165 |
+<section class="sec"> |
|
166 |
+ <h2 class="sec__h"><span>Mida peaks teadma</span></h2> |
|
167 |
+ <ol class="notes">{items}</ol> |
|
168 |
+</section> |
|
169 |
+""" |
|
170 |
+ |
|
171 |
+ |
|
172 |
+def _index_html(): |
|
173 |
+ if PER_COUNTRY.empty: |
|
174 |
+ return "" |
|
175 |
+ |
|
176 |
+ rows = "".join( |
|
177 |
+ f""" |
|
178 |
+ <li class="gz__row"> |
|
179 |
+ <span class="gz__name">{row.country}</span> |
|
180 |
+ <span class="gz__dots" aria-hidden="true"></span> |
|
181 |
+ <span class="gz__pct num">{_pct(row.accuracy, 0)}</span> |
|
182 |
+ <span class="gz__n num">{int(row.support)}</span> |
|
183 |
+ </li> |
|
184 |
+ """ |
|
185 |
+ for row in PER_COUNTRY.itertuples() |
|
186 |
+ ) |
|
187 |
+ return f""" |
|
188 |
+<section class="sec"> |
|
189 |
+ <h2 class="sec__h"><span>Register</span><b class="num">{len(PER_COUNTRY)} riiki</b></h2> |
|
190 |
+ <p class="sec__note"> |
|
191 |
+ Toorandmestikus oli 124 riiki. Puhastus viskas välja rikutud, liiga väikesed, |
|
192 |
+ tumedad, heledad ja udused pildid ning tajuräsi järgi korduvad kaadrid. |
|
193 |
+ Riigid, millele jäi alla 100 pildi, langesid välja tervikuna. Alles jäi |
|
194 |
+ {len(PER_COUNTRY)}. Järjestatud tabavuse, mitte tähestiku järgi. Teine veerg |
|
195 |
+ on tabavus, kolmas testipiltide arv. |
|
196 |
+ </p> |
|
197 |
+ <ul class="gz">{rows}</ul> |
|
198 |
+</section> |
|
199 |
+""" |
|
200 |
+ |
|
201 |
+ |
|
202 |
+FOOTER = """ |
|
203 |
+<footer class="foot"> |
|
204 |
+ <p> |
|
205 |
+ Kahefaasiline treening: kõigepealt lineaarne proovipea külmutatud põhivõrgu |
|
206 |
+ peal, et juhuslikult lähtestatud pea gradiendid eeltreenitud kaale ära ei |
|
207 |
+ lõhuks, seejärel neli viimast transformeri plokki lahti madalama |
|
208 |
+ õppekiirusega. Augmentatsioonis ei ole horisontaalpeegeldust: kummal pool |
|
209 |
+ teed sõidetakse, on päris geograafiline vihje ja peegeldus rikub selle ära. |
|
210 |
+ </p> |
|
211 |
+</footer> |
|
212 |
+""" |
|
213 |
+ |
|
214 |
+ |
|
215 |
+# ── Kujundus ────────────────────────────────────────────────────────────────── |
| 24 |
216 |
|
|
217 |
+CSS = """ |
|
218 |
+@import url('https://fonts.googleapis.com/css2?family=Spectral:ital,wght@0,300;0,400;0,500;0,600;1,300;1,400&family=Archivo+Narrow:wght@400;500;600&display=swap'); |
| 25 |
219 |
|
| 26 |
|
-demo = gr.Interface( |
| 27 |
|
- fn=predict, |
| 28 |
|
- inputs=gr.Image(type="pil"), |
| 29 |
|
- outputs=gr.Label(num_top_classes=5), |
| 30 |
|
- title="CountrySense", |
| 31 |
|
- description="Ennustab tänavapildi järgi riigi. Riigi tase, mitte koordinaadid.", |
|
220 |
+:root { |
|
221 |
+ --paper: #f5f1e7; |
|
222 |
+ --ink: #262019; |
|
223 |
+ --soft: #625849; |
|
224 |
+ --faint: #9c9075; |
|
225 |
+ --rule: #d9cfb8; |
|
226 |
+ --rule-soft: #e6ddc9; |
|
227 |
+ --mark: #9c4a2a; |
|
228 |
+ --sea: #2f6273; |
|
229 |
+} |
|
230 |
+ |
|
231 |
+gradio-app, .gradio-container, body { |
|
232 |
+ background: var(--paper) !important; |
|
233 |
+ color: var(--ink) !important; |
|
234 |
+ font-family: 'Spectral', Georgia, serif !important; |
|
235 |
+} |
|
236 |
+ |
|
237 |
+.gradio-container { |
|
238 |
+ max-width: 1000px !important; |
|
239 |
+ margin: 0 auto !important; |
|
240 |
+ padding: 0 34px 90px !important; |
|
241 |
+} |
|
242 |
+ |
|
243 |
+footer.built-with, .built-with, footer svg { display: none !important; } |
|
244 |
+::selection { background: var(--mark); color: var(--paper); } |
|
245 |
+ |
|
246 |
+/* Kaardisilt: kitsas grotesk versaalis, hõredalt. Kõik, mida silm otsib. */ |
|
247 |
+.eyebrow { |
|
248 |
+ font-family: 'Archivo Narrow', sans-serif; |
|
249 |
+ font-size: 11.5px; |
|
250 |
+ font-weight: 600; |
|
251 |
+ letter-spacing: 0.17em; |
|
252 |
+ text-transform: uppercase; |
|
253 |
+ color: var(--faint); |
|
254 |
+ margin: 0; |
|
255 |
+} |
|
256 |
+.eyebrow--mark { color: var(--mark); } |
|
257 |
+ |
|
258 |
+/* Arvud kitsas grotesk, tabulaarselt, et veerud ei triiviks. */ |
|
259 |
+.num { |
|
260 |
+ font-family: 'Archivo Narrow', sans-serif; |
|
261 |
+ font-variant-numeric: tabular-nums; |
|
262 |
+ font-weight: 500; |
|
263 |
+} |
|
264 |
+ |
|
265 |
+/* ── Päis ─────────────────────────────────────────────────────────────── */ |
|
266 |
+ |
|
267 |
+.mast { position: relative; padding: 66px 0 0; } |
|
268 |
+ |
|
269 |
+/* Koordinaadivõrk: peenjooned iga 32 px, iga neljas veidi tugevam. Ainult |
|
270 |
+ päise all, sest see on kaardi element, mitte tapeet. */ |
|
271 |
+.mast__grid { |
|
272 |
+ position: absolute; |
|
273 |
+ inset: 0 -34px auto; |
|
274 |
+ height: 260px; |
|
275 |
+ background-image: |
|
276 |
+ linear-gradient(var(--rule-soft) 1px, transparent 1px), |
|
277 |
+ linear-gradient(90deg, var(--rule-soft) 1px, transparent 1px), |
|
278 |
+ linear-gradient(var(--rule) 1px, transparent 1px), |
|
279 |
+ linear-gradient(90deg, var(--rule) 1px, transparent 1px); |
|
280 |
+ background-size: 32px 32px, 32px 32px, 128px 128px, 128px 128px; |
|
281 |
+ opacity: 0.5; |
|
282 |
+ -webkit-mask-image: linear-gradient(#000, transparent 88%); |
|
283 |
+ mask-image: linear-gradient(#000, transparent 88%); |
|
284 |
+ pointer-events: none; |
|
285 |
+} |
|
286 |
+.mast__body { position: relative; } |
|
287 |
+ |
|
288 |
+.mast__title { |
|
289 |
+ font-family: 'Spectral', Georgia, serif; |
|
290 |
+ font-size: clamp(46px, 7vw, 68px); |
|
291 |
+ font-weight: 400; |
|
292 |
+ line-height: 1; |
|
293 |
+ letter-spacing: -0.015em; |
|
294 |
+ margin: 20px 0 20px; |
|
295 |
+ color: var(--ink); |
|
296 |
+} |
|
297 |
+.mast__lede { |
|
298 |
+ max-width: 56ch; |
|
299 |
+ font-size: 17px; |
|
300 |
+ font-weight: 300; |
|
301 |
+ line-height: 1.62; |
|
302 |
+ color: var(--soft); |
|
303 |
+ margin: 0 0 40px; |
|
304 |
+} |
|
305 |
+.mast__lede em { color: var(--ink); font-style: italic; } |
|
306 |
+ |
|
307 |
+.stats { |
|
308 |
+ position: relative; |
|
309 |
+ display: flex; |
|
310 |
+ flex-wrap: wrap; |
|
311 |
+ border-top: 1px solid var(--rule); |
|
312 |
+ border-bottom: 1px solid var(--rule); |
|
313 |
+} |
|
314 |
+.stat { flex: 1 1 124px; padding: 15px 0 14px; } |
|
315 |
+.stat__v { font-size: 21px; margin: 7px 0 0; color: var(--ink); } |
|
316 |
+ |
|
317 |
+/* ── Määrang ──────────────────────────────────────────────────────────── */ |
|
318 |
+ |
|
319 |
+.stage { margin-top: 38px !important; gap: 32px !important; } |
|
320 |
+.stage .block { |
|
321 |
+ background: transparent !important; |
|
322 |
+ border: 1px solid var(--rule) !important; |
|
323 |
+ border-radius: 0 !important; |
|
324 |
+ box-shadow: none !important; |
|
325 |
+} |
|
326 |
+.stage button { border-radius: 0 !important; font-family: 'Archivo Narrow', sans-serif !important; } |
|
327 |
+ |
|
328 |
+.empty { |
|
329 |
+ font-family: 'Archivo Narrow', sans-serif; |
|
330 |
+ font-size: 13px; |
|
331 |
+ letter-spacing: 0.05em; |
|
332 |
+ color: var(--faint); |
|
333 |
+ margin: 4px 0 0; |
|
334 |
+} |
|
335 |
+ |
|
336 |
+.out__lead { |
|
337 |
+ font-size: clamp(34px, 5vw, 44px); |
|
338 |
+ font-weight: 400; |
|
339 |
+ line-height: 1.05; |
|
340 |
+ letter-spacing: -0.012em; |
|
341 |
+ margin: 12px 0 8px; |
|
342 |
+ color: var(--ink); |
|
343 |
+} |
|
344 |
+.out__meta { |
|
345 |
+ font-size: 15px; |
|
346 |
+ font-weight: 300; |
|
347 |
+ color: var(--soft); |
|
348 |
+ margin: 0 0 22px; |
|
349 |
+} |
|
350 |
+.out__meta em { font-style: italic; } |
|
351 |
+ |
|
352 |
+.alts { list-style: none; margin: 0; padding: 0; } |
|
353 |
+.alt { |
|
354 |
+ display: grid; |
|
355 |
+ grid-template-columns: auto 1fr 52px; |
|
356 |
+ align-items: center; |
|
357 |
+ gap: 0 14px; |
|
358 |
+ padding: 8px 0; |
|
359 |
+ border-top: 1px solid var(--rule-soft); |
|
360 |
+} |
|
361 |
+.alt__name { font-size: 15px; font-weight: 300; color: var(--soft); white-space: nowrap; } |
|
362 |
+.alt__bar { display: block; height: 2px; background: var(--rule-soft); } |
|
363 |
+.alt__bar i { display: block; height: 100%; background: var(--sea); } |
|
364 |
+.alt__pct { font-size: 13.5px; color: var(--soft); text-align: right; } |
|
365 |
+ |
|
366 |
+/* ── Sektsioonid ──────────────────────────────────────────────────────── */ |
|
367 |
+ |
|
368 |
+.sec { margin-top: 76px; } |
|
369 |
+ |
|
370 |
+/* Legendi pealkiri: versaal, mille kõrvale jookseb joon üle laiuse. */ |
|
371 |
+.sec__h { |
|
372 |
+ display: flex; |
|
373 |
+ align-items: center; |
|
374 |
+ gap: 16px; |
|
375 |
+ margin: 0 0 24px; |
|
376 |
+ font-family: 'Archivo Narrow', sans-serif; |
|
377 |
+ font-size: 12.5px; |
|
378 |
+ font-weight: 600; |
|
379 |
+ letter-spacing: 0.17em; |
|
380 |
+ text-transform: uppercase; |
|
381 |
+ color: var(--ink); |
|
382 |
+} |
|
383 |
+.sec__h::after { |
|
384 |
+ content: ''; |
|
385 |
+ flex: 1; |
|
386 |
+ height: 1px; |
|
387 |
+ background: var(--rule); |
|
388 |
+} |
|
389 |
+.sec__h b { |
|
390 |
+ order: 3; |
|
391 |
+ font-size: 12px; |
|
392 |
+ font-weight: 500; |
|
393 |
+ letter-spacing: 0.06em; |
|
394 |
+ text-transform: none; |
|
395 |
+ color: var(--mark); |
|
396 |
+} |
|
397 |
+.sec__note { |
|
398 |
+ max-width: 74ch; |
|
399 |
+ font-size: 15.5px; |
|
400 |
+ font-weight: 300; |
|
401 |
+ line-height: 1.68; |
|
402 |
+ color: var(--soft); |
|
403 |
+ margin: 0 0 30px; |
|
404 |
+} |
|
405 |
+ |
|
406 |
+.notes { list-style: none; margin: 0; padding: 0; } |
|
407 |
+.note { |
|
408 |
+ display: grid; |
|
409 |
+ grid-template-columns: 48px 1fr; |
|
410 |
+ gap: 0 16px; |
|
411 |
+ padding: 19px 0; |
|
412 |
+ border-top: 1px solid var(--rule-soft); |
|
413 |
+} |
|
414 |
+.note:first-child { border-top: 0; padding-top: 0; } |
|
415 |
+.note__n { font-size: 12.5px; color: var(--mark); padding-top: 5px; } |
|
416 |
+.note h3 { |
|
417 |
+ font-size: 16.5px; |
|
418 |
+ font-weight: 500; |
|
419 |
+ letter-spacing: -0.005em; |
|
420 |
+ margin: 0 0 7px; |
|
421 |
+ color: var(--ink); |
|
422 |
+} |
|
423 |
+.note p { |
|
424 |
+ max-width: 68ch; |
|
425 |
+ font-size: 15.5px; |
|
426 |
+ font-weight: 300; |
|
427 |
+ line-height: 1.68; |
|
428 |
+ color: var(--soft); |
|
429 |
+ margin: 0; |
|
430 |
+} |
|
431 |
+ |
|
432 |
+/* ── Register ─────────────────────────────────────────────────────────── */ |
|
433 |
+ |
|
434 |
+.gz { |
|
435 |
+ list-style: none; |
|
436 |
+ margin: 0; |
|
437 |
+ padding: 0; |
|
438 |
+ display: grid; |
|
439 |
+ grid-template-columns: 1fr 1fr; |
|
440 |
+ gap: 0 52px; |
|
441 |
+} |
|
442 |
+.gz__row { |
|
443 |
+ display: grid; |
|
444 |
+ grid-template-columns: auto 1fr auto auto; |
|
445 |
+ align-items: baseline; |
|
446 |
+ gap: 0 8px; |
|
447 |
+ padding: 6px 0; |
|
448 |
+} |
|
449 |
+.gz__name { font-size: 15.5px; font-weight: 300; color: var(--ink); white-space: nowrap; } |
|
450 |
+/* Juhtpunktid, nagu atlase registris: seovad nime numbriga üle tühja ruumi. */ |
|
451 |
+.gz__dots { |
|
452 |
+ border-bottom: 1px dotted var(--rule); |
|
453 |
+ transform: translateY(-4px); |
|
454 |
+ min-width: 14px; |
|
455 |
+} |
|
456 |
+.gz__pct { font-size: 13.5px; color: var(--soft); width: 42px; text-align: right; } |
|
457 |
+.gz__n { font-size: 12.5px; color: var(--faint); width: 34px; text-align: right; } |
|
458 |
+ |
|
459 |
+/* ── Jalus ────────────────────────────────────────────────────────────── */ |
|
460 |
+ |
|
461 |
+.foot { margin-top: 68px; padding-top: 22px; border-top: 1px solid var(--rule); } |
|
462 |
+.foot p { |
|
463 |
+ max-width: 78ch; |
|
464 |
+ font-size: 14.5px; |
|
465 |
+ font-weight: 300; |
|
466 |
+ line-height: 1.72; |
|
467 |
+ color: var(--faint); |
|
468 |
+ margin: 0; |
|
469 |
+} |
|
470 |
+ |
|
471 |
+@media (max-width: 760px) { |
|
472 |
+ .gradio-container { padding: 0 20px 58px !important; } |
|
473 |
+ .mast { padding-top: 42px; } |
|
474 |
+ .mast__grid { inset: 0 -20px auto; } |
|
475 |
+ .stat { flex-basis: 50%; } |
|
476 |
+ .gz { grid-template-columns: 1fr; gap: 0; } |
|
477 |
+ .note { grid-template-columns: 34px 1fr; } |
|
478 |
+} |
|
479 |
+""" |
|
480 |
+ |
|
481 |
+THEME = gr.themes.Base( |
|
482 |
+ primary_hue=gr.themes.colors.orange, |
|
483 |
+ neutral_hue=gr.themes.colors.stone, |
|
484 |
+ font=gr.themes.GoogleFont("Spectral"), |
|
485 |
+ font_mono=gr.themes.GoogleFont("Archivo Narrow"), |
|
486 |
+).set( |
|
487 |
+ body_background_fill="#f5f1e7", |
|
488 |
+ background_fill_primary="#f5f1e7", |
|
489 |
+ background_fill_secondary="#efe9db", |
|
490 |
+ border_color_primary="#d9cfb8", |
|
491 |
+ body_text_color="#262019", |
|
492 |
+ body_text_color_subdued="#625849", |
|
493 |
+ button_primary_background_fill="#9c4a2a", |
|
494 |
+ button_primary_text_color="#f5f1e7", |
|
495 |
+ block_radius="0px", |
|
496 |
+ button_large_radius="0px", |
|
497 |
+ button_small_radius="0px", |
|
498 |
+ input_radius="0px", |
|
499 |
+ block_shadow="none", |
| 32 |
500 |
) |
| 33 |
501 |
|
|
502 |
+with gr.Blocks(title="CountrySense", analytics_enabled=False) as demo: |
|
503 |
+ gr.HTML(_head_html()) |
|
504 |
+ |
|
505 |
+ with gr.Row(elem_classes="stage", equal_height=False): |
|
506 |
+ with gr.Column(scale=5): |
|
507 |
+ image = gr.Image(type="pil", height=300, show_label=False, |
|
508 |
+ sources=["upload", "clipboard"]) |
|
509 |
+ with gr.Column(scale=6): |
|
510 |
+ result = gr.HTML(EMPTY) |
|
511 |
+ |
|
512 |
+ image.change(predict, inputs=image, outputs=result) |
|
513 |
+ |
|
514 |
+ gr.HTML(_caveats_html()) |
|
515 |
+ gr.HTML(_index_html()) |
|
516 |
+ gr.HTML(FOOTER) |
|
517 |
+ |
|
518 |
+ |
| 34 |
519 |
if __name__ == "__main__": |
| 35 |
|
- demo.launch() |
|
520 |
+ # Üks ennustus korraga. Mudel võtab protsessorimälust ligi gigabaidi ja |
|
521 |
+ # paralleelsed päringud kahekordistaksid selle ilma midagi juurde andmata. |
|
522 |
+ demo.queue(default_concurrency_limit=1, max_size=16) |
|
523 |
+ |
|
524 |
+ # Gradio 6: theme ja css käivad launch()-i, mitte Blocks()-i. |
|
525 |
+ demo.launch( |
|
526 |
+ theme=THEME, |
|
527 |
+ css=CSS, |
|
528 |
+ # Vaikimisi jääb kohalik käivitus ainult sellesse masinasse. Konteiner |
|
529 |
+ # seab COUNTRYSENSE_HOST=0.0.0.0, sest pöördproksi jõuab kohale ainult |
|
530 |
+ # üle konteinerivõrgu. |
|
531 |
+ server_name=os.environ.get("COUNTRYSENSE_HOST", "127.0.0.1"), |
|
532 |
+ server_port=int(os.environ.get("PORT", "7860")), |
|
533 |
+ # Demo võtab vastu tänavapilte, mitte suvalisi faile. |
|
534 |
+ max_file_size="12mb", |
|
535 |
+ ) |