kagikush commited on
Commit
4fa0ce3
Β·
verified Β·
1 Parent(s): 06e5ba1

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +245 -299
app.py CHANGED
@@ -41,144 +41,7 @@ def cosine_search(query_vec: np.ndarray, top_k: int) -> pd.DataFrame:
41
  return out
42
 
43
 
44
- # ─── Rendering ─────────────────────────────────────────────────────────────────
45
- RESULT_CSS = """
46
- <style>
47
- @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;900&display=swap');
48
-
49
- .sgu-results-wrap {
50
- font-family: 'Inter', sans-serif;
51
- max-width: 980px;
52
- margin: 0 auto;
53
- padding: 40px 24px 80px;
54
- animation: sguFadeIn .6s ease-out;
55
- }
56
- @keyframes sguFadeIn { from { opacity:0; transform: translateY(20px);} to { opacity:1; transform:none;} }
57
-
58
- .sgu-results-header {
59
- font-family: 'Inter', sans-serif;
60
- font-weight: 900;
61
- font-size: 2.4rem;
62
- letter-spacing: -0.02em;
63
- color: #0a1628;
64
- text-align: center;
65
- margin: 0 0 8px;
66
- }
67
- .sgu-results-sub {
68
- font-family: 'Inter', sans-serif;
69
- font-weight: 400;
70
- font-size: 0.95rem;
71
- color: #0a1628;
72
- opacity: 0.65;
73
- text-align: center;
74
- margin: 0 0 56px;
75
- letter-spacing: 0.01em;
76
- }
77
-
78
- .sgu-list { display: flex; flex-direction: column; gap: 20px; }
79
-
80
- .sgu-row {
81
- display: grid;
82
- grid-template-columns: 80px 1fr;
83
- align-items: center;
84
- background: rgba(255, 255, 255, 0.55);
85
- backdrop-filter: blur(10px);
86
- -webkit-backdrop-filter: blur(10px);
87
- border: 1px solid rgba(10, 22, 40, 0.08);
88
- border-radius: 20px;
89
- padding: 18px 22px;
90
- text-decoration: none;
91
- color: inherit;
92
- transition: transform .25s cubic-bezier(.2,.8,.2,1), box-shadow .25s ease, background .25s ease;
93
- }
94
- .sgu-row:hover {
95
- transform: translateY(-3px);
96
- background: rgba(255, 255, 255, 0.85);
97
- box-shadow: 0 18px 50px -20px rgba(10, 22, 40, 0.25);
98
- }
99
-
100
- .sgu-num {
101
- font-family: 'Inter', sans-serif;
102
- font-weight: 900;
103
- font-size: 2.6rem;
104
- color: #0a1628;
105
- line-height: 1;
106
- letter-spacing: -0.04em;
107
- font-variant-numeric: tabular-nums;
108
- }
109
-
110
- .sgu-body { display: grid; grid-template-columns: 200px 1fr auto; gap: 22px; align-items: center; }
111
-
112
- .sgu-thumb-wrap {
113
- width: 200px; height: 94px;
114
- border-radius: 12px; overflow: hidden;
115
- background: #0a1628;
116
- flex-shrink: 0;
117
- }
118
- .sgu-thumb-wrap img {
119
- width: 100%; height: 100%; object-fit: cover; display: block;
120
- transition: transform .35s ease;
121
- }
122
- .sgu-row:hover .sgu-thumb-wrap img { transform: scale(1.06); }
123
-
124
- .sgu-info { min-width: 0; }
125
- .sgu-title {
126
- font-family: 'Inter', sans-serif;
127
- font-weight: 700; font-size: 1.15rem;
128
- color: #0a1628;
129
- margin: 0 0 4px;
130
- letter-spacing: -0.01em;
131
- white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
132
- }
133
- .sgu-genre {
134
- font-family: 'Inter', sans-serif;
135
- font-weight: 400; font-size: 0.78rem;
136
- color: #0a1628; opacity: 0.55;
137
- margin: 0;
138
- white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
139
- }
140
-
141
- .sgu-match {
142
- text-align: right;
143
- font-family: 'Inter', sans-serif;
144
- flex-shrink: 0;
145
- }
146
- .sgu-match-pct {
147
- font-weight: 700; font-size: 1.4rem;
148
- color: #1976d2;
149
- letter-spacing: -0.02em;
150
- line-height: 1;
151
- font-variant-numeric: tabular-nums;
152
- }
153
- .sgu-match-label {
154
- font-weight: 400; font-size: 0.65rem;
155
- color: #0a1628; opacity: 0.5;
156
- text-transform: uppercase;
157
- letter-spacing: 0.15em;
158
- margin-top: 4px;
159
- }
160
-
161
- .sgu-empty {
162
- font-family: 'Inter', sans-serif;
163
- text-align: center;
164
- color: #0a1628; opacity: 0.55;
165
- padding: 80px 20px;
166
- font-size: 0.95rem;
167
- }
168
-
169
- @media (max-width: 720px) {
170
- .sgu-row { grid-template-columns: 50px 1fr; padding: 14px 16px; }
171
- .sgu-num { font-size: 1.8rem; }
172
- .sgu-body { grid-template-columns: 110px 1fr; gap: 14px; }
173
- .sgu-thumb-wrap { width: 110px; height: 52px; }
174
- .sgu-title { font-size: .95rem; }
175
- .sgu-match { display: none; }
176
- .sgu-results-header { font-size: 1.8rem; }
177
- }
178
- </style>
179
- """
180
-
181
-
182
  def build_row(rank: int, row) -> str:
183
  genres = (", ".join(row["genres"])
184
  if hasattr(row["genres"], "__iter__") and not isinstance(row["genres"], str)
@@ -190,242 +53,328 @@ def build_row(rank: int, row) -> str:
190
  return f"""
191
  <a class="sgu-row" href="{search_url}" target="_blank" rel="noopener">
192
  <div class="sgu-num">{rank:02d}</div>
193
- <div class="sgu-body">
194
- <div class="sgu-thumb-wrap">
195
- <img src="{img_url}" alt="{name_safe}" loading="lazy"
196
- onerror="this.parentElement.style.background='#0a1628'; this.style.display='none';"/>
197
- </div>
198
- <div class="sgu-info">
199
- <h3 class="sgu-title">{name_safe}</h3>
200
- <p class="sgu-genre">{genres}</p>
201
- </div>
202
- <div class="sgu-match">
203
- <div class="sgu-match-pct">{sim_pct}</div>
204
- <div class="sgu-match-label">match</div>
205
- </div>
206
  </div>
207
- </a>
208
- """
 
 
 
209
 
210
 
211
  def search_games(query_image, top_k: int):
212
  if query_image is None:
213
- return RESULT_CSS + '<div class="sgu-results-wrap"><p class="sgu-empty">Upload a game image above to begin exploring.</p></div>'
214
 
215
  try:
216
  query_vec = embed_image(query_image)
217
  except Exception as e:
218
- return RESULT_CSS + f'<div class="sgu-results-wrap"><p class="sgu-empty">Embedding error: {e}</p></div>'
219
 
220
- results = cosine_search(query_vec, top_k=top_k)
221
  rows = "".join(build_row(i + 1, r) for i, (_, r) in enumerate(results.iterrows()))
222
 
223
- return RESULT_CSS + f"""
224
  <div class="sgu-results-wrap">
225
  <h2 class="sgu-results-header">Your Universe</h2>
226
- <p class="sgu-results-sub">{top_k} games sharing the visual DNA of your upload</p>
227
  <div class="sgu-list">{rows}</div>
228
  </div>
229
  """
230
 
231
 
232
- # ─── UI ────────────────────────────────────────────────────────────────────────
233
-
234
  APP_CSS = """
235
  @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap');
236
 
237
- /* Mint background flooding the whole app */
238
- body, .gradio-container, gradio-app {
239
  background: #5DDBC7 !important;
240
  font-family: 'Inter', sans-serif !important;
 
241
  }
242
- .gradio-container { max-width: 100% !important; padding: 0 !important; }
243
- footer { display: none !important; }
244
 
245
- /* Hero section */
246
- #sgu-hero {
247
  text-align: center;
248
- padding: 80px 24px 40px;
249
  max-width: 760px;
250
  margin: 0 auto;
251
  }
252
- #sgu-headline {
253
- font-family: 'Inter', sans-serif;
254
  font-weight: 900;
255
- font-size: clamp(4rem, 11vw, 8.5rem);
256
  line-height: 0.92;
257
  letter-spacing: -0.04em;
258
  color: #0a1628;
259
  margin: 0 0 28px;
260
  }
261
- #sgu-tagline {
262
- font-family: 'Inter', sans-serif;
263
  font-weight: 500;
264
  font-size: 1.05rem;
265
  color: #0a1628;
266
  line-height: 1.55;
267
- margin: 0 0 24px;
268
- letter-spacing: 0;
269
  }
270
- #sgu-explainer {
271
- font-family: 'Inter', sans-serif;
272
  font-weight: 400;
273
  font-size: 0.92rem;
274
  color: #0a1628;
275
  opacity: 0.72;
276
  line-height: 1.65;
277
  max-width: 560px;
278
- margin: 0 auto 56px;
279
- letter-spacing: 0.005em;
280
  }
281
 
282
- /* The big blue circle = upload zone */
283
- #sgu-circle-wrap {
284
- display: flex;
285
- justify-content: center;
286
- padding: 0 24px 40px;
287
- }
288
- #sgu-circle-wrap .gr-image,
289
- #sgu-circle-wrap [data-testid="image"] {
290
- width: 360px !important;
291
- height: 360px !important;
 
 
 
 
 
 
 
 
 
 
 
292
  border-radius: 50% !important;
 
293
  background: #2D7DD2 !important;
294
  border: none !important;
295
- box-shadow:
296
- 0 30px 80px -20px rgba(45, 125, 210, 0.5),
297
- 0 0 0 0 rgba(45, 125, 210, 0.4);
298
- transition: transform .3s ease, box-shadow .3s ease;
299
- overflow: hidden !important;
300
- position: relative;
301
  }
302
- #sgu-circle-wrap .gr-image:hover,
303
- #sgu-circle-wrap [data-testid="image"]:hover {
304
- transform: scale(1.02);
305
- box-shadow: 0 40px 100px -20px rgba(45, 125, 210, 0.6);
306
- }
307
- #sgu-circle-wrap label { display: none !important; }
308
- #sgu-circle-wrap .image-container,
309
- #sgu-circle-wrap .image-frame,
310
- #sgu-circle-wrap .preview,
311
- #sgu-circle-wrap [data-testid="image"] > div {
312
- background: transparent !important;
313
- border: none !important;
314
  width: 100% !important;
315
  height: 100% !important;
316
- border-radius: 50% !important;
317
- overflow: hidden !important;
318
- }
319
- #sgu-circle-wrap .upload-container,
320
- #sgu-circle-wrap .wrap {
321
- background: transparent !important;
322
  border: none !important;
323
- height: 100% !important;
324
  border-radius: 50% !important;
 
 
 
 
 
325
  }
326
 
327
- /* Hide gradio's default upload UI text, replace with our own */
328
- #sgu-circle-wrap .upload-text,
329
- #sgu-circle-wrap [data-testid="image"] .or,
330
- #sgu-circle-wrap [data-testid="image"] svg {
331
- display: none !important;
332
- }
333
 
334
- /* Custom prompt text inside the circle */
335
- #sgu-circle-wrap [data-testid="image"]::before {
336
- content: "Describe the game aesthetic or paste a photo of your favorite game";
337
  position: absolute;
338
- top: 50%; left: 50%;
339
- transform: translate(-50%, -50%);
340
- width: 70%;
341
  text-align: center;
342
  font-family: 'Inter', sans-serif;
343
  font-weight: 500;
344
  font-size: 1rem;
345
  line-height: 1.5;
346
  color: #ffffff;
 
347
  pointer-events: none;
348
- z-index: 0;
349
  }
350
- /* Hide custom prompt once image uploaded */
351
- #sgu-circle-wrap [data-testid="image"]:has(img)::before { display: none; }
 
352
 
353
- /* Style uploaded image to fill the circle */
354
- #sgu-circle-wrap [data-testid="image"] img {
355
  width: 100% !important;
356
  height: 100% !important;
357
  object-fit: cover !important;
358
  border-radius: 50% !important;
 
359
  }
360
 
361
- /* Hide image action buttons (clear/edit) β€” keep it minimal */
362
- #sgu-circle-wrap .icon-buttons,
363
- #sgu-circle-wrap [aria-label="Clear"],
364
- #sgu-circle-wrap button[title="Clear"] {
365
- background: rgba(255,255,255,0.9) !important;
366
- border-radius: 50% !important;
367
- top: 12px !important;
368
- right: 12px !important;
369
- }
370
-
371
- /* Controls row */
372
- #sgu-controls {
373
- max-width: 480px;
374
- margin: 0 auto;
375
- padding: 0 24px 24px;
376
  }
377
- #sgu-controls .gr-slider label,
378
- #sgu-controls .gr-slider .label-wrap {
379
  color: #0a1628 !important;
380
- font-family: 'Inter', sans-serif !important;
381
  font-weight: 500 !important;
382
- font-size: 0.8rem !important;
383
- letter-spacing: 0.12em !important;
384
  text-transform: uppercase !important;
385
  }
386
- #sgu-controls input[type="range"] {
387
- accent-color: #2D7DD2 !important;
388
- }
389
- #sgu-controls .gr-slider {
390
- background: transparent !important;
391
- border: none !important;
392
- }
393
 
394
- /* Explore button */
395
- #sgu-explore-wrap {
396
- display: flex;
397
- justify-content: center;
398
- padding: 16px 24px 80px;
 
399
  }
400
- #sgu-explore-btn {
401
  background: #0a1628 !important;
402
  color: #5DDBC7 !important;
403
  font-family: 'Inter', sans-serif !important;
404
  font-weight: 700 !important;
405
- font-size: 0.85rem !important;
406
- letter-spacing: 0.25em !important;
407
  text-transform: uppercase !important;
408
  padding: 18px 56px !important;
409
  border-radius: 999px !important;
410
  border: none !important;
411
  cursor: pointer !important;
412
  transition: transform .2s ease, background .2s ease !important;
 
413
  }
414
- #sgu-explore-btn:hover {
415
  transform: translateY(-2px) !important;
416
- background: #1976d2 !important;
417
- color: #ffffff !important;
418
  }
419
 
420
- /* Divider between hero and results */
421
- #sgu-divider {
422
- width: 60px; height: 1px;
423
- background: #0a1628; opacity: 0.2;
424
- margin: 0 auto 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
425
  }
426
  """
427
 
428
 
 
429
  with gr.Blocks(
430
  theme=gr.themes.Base(),
431
  title="Steam Graphic Universe",
@@ -433,15 +382,14 @@ with gr.Blocks(
433
  analytics_enabled=False,
434
  ) as demo:
435
 
436
- # Hero
437
  gr.HTML("""
438
- <div id="sgu-hero">
439
- <h1 id="sgu-headline">Let's<br/>Play!</h1>
440
- <p id="sgu-tagline">
441
  You decide the games you want to play<br/>
442
  We are here to help you make the best decision
443
  </p>
444
- <p id="sgu-explainer">
445
  Upload an image of any game and we'll surface the closest visual matches
446
  from a universe of over 9,800 Steam titles. Behind the scenes a CLIP vision
447
  model converts your image into a high-dimensional aesthetic fingerprint,
@@ -451,29 +399,28 @@ with gr.Blocks(
451
  </div>
452
  """)
453
 
454
- with gr.Column(elem_id="sgu-circle-wrap"):
455
- query_image = gr.Image(
456
- type="pil",
457
- show_label=False,
458
- show_download_button=False,
459
- container=False,
460
- sources=["upload", "clipboard"],
461
- )
462
-
463
- with gr.Column(elem_id="sgu-controls"):
464
- top_k_slider = gr.Slider(
465
- minimum=3, maximum=12, step=1, value=6,
466
- label="Number of matches",
467
- show_label=True,
468
- )
469
 
470
- with gr.Row(elem_id="sgu-explore-wrap"):
471
- search_btn = gr.Button("Explore", elem_id="sgu-explore-btn")
 
 
 
472
 
473
- gr.HTML('<div id="sgu-divider"></div>')
 
474
 
475
  output_html = gr.HTML(
476
- value=RESULT_CSS + '<div class="sgu-results-wrap"><p class="sgu-empty">Upload a game image above to begin exploring.</p></div>'
477
  )
478
 
479
  search_btn.click(
@@ -482,6 +429,5 @@ with gr.Blocks(
482
  outputs=output_html,
483
  )
484
 
485
-
486
  if __name__ == "__main__":
487
- demo.launch()
 
41
  return out
42
 
43
 
44
+ # ─── HTML rendering of results ─────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  def build_row(rank: int, row) -> str:
46
  genres = (", ".join(row["genres"])
47
  if hasattr(row["genres"], "__iter__") and not isinstance(row["genres"], str)
 
53
  return f"""
54
  <a class="sgu-row" href="{search_url}" target="_blank" rel="noopener">
55
  <div class="sgu-num">{rank:02d}</div>
56
+ <div class="sgu-thumb"><img src="{img_url}" alt="{name_safe}" loading="lazy"/></div>
57
+ <div class="sgu-info">
58
+ <h3 class="sgu-title">{name_safe}</h3>
59
+ <p class="sgu-genre">{genres}</p>
 
 
 
 
 
 
 
 
 
60
  </div>
61
+ <div class="sgu-match">
62
+ <div class="sgu-match-pct">{sim_pct}</div>
63
+ <div class="sgu-match-label">match</div>
64
+ </div>
65
+ </a>"""
66
 
67
 
68
  def search_games(query_image, top_k: int):
69
  if query_image is None:
70
+ return '<div class="sgu-results-wrap"><p class="sgu-empty">Upload a game image above to begin exploring.</p></div>'
71
 
72
  try:
73
  query_vec = embed_image(query_image)
74
  except Exception as e:
75
+ return f'<div class="sgu-results-wrap"><p class="sgu-empty">Error: {e}</p></div>'
76
 
77
+ results = cosine_search(query_vec, top_k=int(top_k))
78
  rows = "".join(build_row(i + 1, r) for i, (_, r) in enumerate(results.iterrows()))
79
 
80
+ return f"""
81
  <div class="sgu-results-wrap">
82
  <h2 class="sgu-results-header">Your Universe</h2>
83
+ <p class="sgu-results-sub">{int(top_k)} games sharing the visual DNA of your upload</p>
84
  <div class="sgu-list">{rows}</div>
85
  </div>
86
  """
87
 
88
 
89
+ # ─── Styling ───────────────────────────────────────────────────────────────────
 
90
  APP_CSS = """
91
  @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap');
92
 
93
+ /* ─── flood mint everywhere ─── */
94
+ html, body, .gradio-container, gradio-app, .main, .app, .contain {
95
  background: #5DDBC7 !important;
96
  font-family: 'Inter', sans-serif !important;
97
+ color: #0a1628;
98
  }
99
+ .gradio-container { max-width: 100% !important; padding: 0 !important; margin: 0 !important; }
100
+ footer, .footer, #footer { display: none !important; }
101
 
102
+ /* ─── HERO ─── */
103
+ .sgu-hero {
104
  text-align: center;
105
+ padding: 70px 24px 32px;
106
  max-width: 760px;
107
  margin: 0 auto;
108
  }
109
+ .sgu-headline {
 
110
  font-weight: 900;
111
+ font-size: clamp(4rem, 11vw, 8rem);
112
  line-height: 0.92;
113
  letter-spacing: -0.04em;
114
  color: #0a1628;
115
  margin: 0 0 28px;
116
  }
117
+ .sgu-tagline {
 
118
  font-weight: 500;
119
  font-size: 1.05rem;
120
  color: #0a1628;
121
  line-height: 1.55;
122
+ margin: 0 0 22px;
 
123
  }
124
+ .sgu-explainer {
 
125
  font-weight: 400;
126
  font-size: 0.92rem;
127
  color: #0a1628;
128
  opacity: 0.72;
129
  line-height: 1.65;
130
  max-width: 560px;
131
+ margin: 0 auto 0;
 
132
  }
133
 
134
+ /* ─── CIRCLE: native gr.Image, sized & shaped via outer wrapper ─── */
135
+ #circle-zone {
136
+ display: flex !important;
137
+ justify-content: center !important;
138
+ align-items: center !important;
139
+ margin: 50px auto 30px !important;
140
+ padding: 0 !important;
141
+ width: 320px !important;
142
+ height: 320px !important;
143
+ flex: none !important;
144
+ min-width: 0 !important;
145
+ }
146
+ #circle-zone > div,
147
+ #circle-zone .block,
148
+ #circle-zone .form,
149
+ #circle-zone .gradio-image,
150
+ #circle-zone .image-container {
151
+ width: 320px !important;
152
+ height: 320px !important;
153
+ min-width: 0 !important;
154
+ max-width: 320px !important;
155
  border-radius: 50% !important;
156
+ overflow: hidden !important;
157
  background: #2D7DD2 !important;
158
  border: none !important;
159
+ box-shadow: 0 20px 60px -15px rgba(45,125,210,.45);
160
+ padding: 0 !important;
161
+ margin: 0 auto !important;
 
 
 
162
  }
163
+ #circle-zone label,
164
+ #circle-zone .label-wrap { display: none !important; }
165
+
166
+ /* The drop area inside */
167
+ #circle-zone .upload-container,
168
+ #circle-zone [data-testid="image"],
169
+ #circle-zone .wrap {
 
 
 
 
 
170
  width: 100% !important;
171
  height: 100% !important;
172
+ background: #2D7DD2 !important;
 
 
 
 
 
173
  border: none !important;
 
174
  border-radius: 50% !important;
175
+ display: flex !important;
176
+ align-items: center !important;
177
+ justify-content: center !important;
178
+ cursor: pointer !important;
179
+ color: #fff !important;
180
  }
181
 
182
+ /* Hide Gradio's default upload icon/text, replace with custom prompt */
183
+ #circle-zone .upload-text,
184
+ #circle-zone .or,
185
+ #circle-zone svg { display: none !important; }
 
 
186
 
187
+ #circle-zone .upload-container::after,
188
+ #circle-zone [data-testid="image"]::after {
189
+ content: "Upload a photo of your favorite game";
190
  position: absolute;
 
 
 
191
  text-align: center;
192
  font-family: 'Inter', sans-serif;
193
  font-weight: 500;
194
  font-size: 1rem;
195
  line-height: 1.5;
196
  color: #ffffff;
197
+ width: 70%;
198
  pointer-events: none;
 
199
  }
200
+ /* Hide that text once an image is loaded */
201
+ #circle-zone:has(img)::after,
202
+ #circle-zone [data-testid="image"]:has(img)::after { display: none !important; }
203
 
204
+ /* Make uploaded image fill the circle */
205
+ #circle-zone img {
206
  width: 100% !important;
207
  height: 100% !important;
208
  object-fit: cover !important;
209
  border-radius: 50% !important;
210
+ display: block !important;
211
  }
212
 
213
+ /* ─── SLIDER ─── */
214
+ #slider-zone {
215
+ max-width: 380px !important;
216
+ margin: 0 auto !important;
217
+ padding: 8px 24px 0 !important;
 
 
 
 
 
 
 
 
 
 
218
  }
219
+ #slider-zone label,
220
+ #slider-zone .label-wrap span {
221
  color: #0a1628 !important;
 
222
  font-weight: 500 !important;
223
+ font-size: 0.7rem !important;
224
+ letter-spacing: 0.18em !important;
225
  text-transform: uppercase !important;
226
  }
227
+ #slider-zone input[type="range"] { accent-color: #0a1628 !important; }
228
+ #slider-zone .block { background: transparent !important; border: none !important; padding: 0 !important; }
 
 
 
 
 
229
 
230
+ /* ─── EXPLORE BUTTON ─── */
231
+ #explore-zone {
232
+ display: flex !important;
233
+ justify-content: center !important;
234
+ padding: 28px 24px 60px !important;
235
+ background: transparent !important;
236
  }
237
+ #explore-zone button, #explore-btn {
238
  background: #0a1628 !important;
239
  color: #5DDBC7 !important;
240
  font-family: 'Inter', sans-serif !important;
241
  font-weight: 700 !important;
242
+ font-size: 0.78rem !important;
243
+ letter-spacing: 0.28em !important;
244
  text-transform: uppercase !important;
245
  padding: 18px 56px !important;
246
  border-radius: 999px !important;
247
  border: none !important;
248
  cursor: pointer !important;
249
  transition: transform .2s ease, background .2s ease !important;
250
+ box-shadow: 0 10px 30px -10px rgba(10,22,40,.35);
251
  }
252
+ #explore-zone button:hover, #explore-btn:hover {
253
  transform: translateY(-2px) !important;
254
+ background: #163251 !important;
 
255
  }
256
 
257
+ /* ─── RESULTS ─── */
258
+ .sgu-results-wrap {
259
+ max-width: 920px;
260
+ margin: 0 auto;
261
+ padding: 30px 24px 80px;
262
+ animation: sguFade .5s ease-out;
263
+ }
264
+ @keyframes sguFade { from {opacity:0;transform:translateY(16px);} to {opacity:1;transform:none;} }
265
+
266
+ .sgu-results-header {
267
+ font-weight: 900;
268
+ font-size: 2.2rem;
269
+ letter-spacing: -0.02em;
270
+ color: #0a1628;
271
+ text-align: center;
272
+ margin: 0 0 6px;
273
+ }
274
+ .sgu-results-sub {
275
+ font-weight: 400;
276
+ font-size: 0.9rem;
277
+ color: #0a1628;
278
+ opacity: 0.6;
279
+ text-align: center;
280
+ margin: 0 0 48px;
281
+ }
282
+
283
+ .sgu-list { display: flex; flex-direction: column; gap: 14px; }
284
+
285
+ .sgu-row {
286
+ display: grid;
287
+ grid-template-columns: 56px 180px 1fr auto;
288
+ align-items: center;
289
+ gap: 22px;
290
+ background: rgba(255, 255, 255, 0.55);
291
+ backdrop-filter: blur(8px);
292
+ -webkit-backdrop-filter: blur(8px);
293
+ border: 1px solid rgba(10,22,40,.08);
294
+ border-radius: 18px;
295
+ padding: 16px 22px;
296
+ text-decoration: none;
297
+ color: inherit;
298
+ transition: transform .25s cubic-bezier(.2,.8,.2,1), box-shadow .25s ease, background .25s ease;
299
+ }
300
+ .sgu-row:hover {
301
+ transform: translateY(-3px);
302
+ background: rgba(255,255,255,0.85);
303
+ box-shadow: 0 18px 50px -20px rgba(10,22,40,.25);
304
+ }
305
+
306
+ .sgu-num {
307
+ font-weight: 900;
308
+ font-size: 2.2rem;
309
+ color: #0a1628;
310
+ line-height: 1;
311
+ letter-spacing: -0.04em;
312
+ font-variant-numeric: tabular-nums;
313
+ text-align: center;
314
+ }
315
+
316
+ .sgu-thumb {
317
+ width: 180px; height: 84px;
318
+ border-radius: 10px;
319
+ overflow: hidden;
320
+ background: #0a1628;
321
+ }
322
+ .sgu-thumb img {
323
+ width: 100%; height: 100%; object-fit: cover; display: block;
324
+ transition: transform .35s ease;
325
+ }
326
+ .sgu-row:hover .sgu-thumb img { transform: scale(1.06); }
327
+
328
+ .sgu-info { min-width: 0; }
329
+ .sgu-title {
330
+ font-weight: 700; font-size: 1.1rem;
331
+ color: #0a1628;
332
+ margin: 0 0 4px;
333
+ letter-spacing: -0.01em;
334
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
335
+ }
336
+ .sgu-genre {
337
+ font-weight: 400; font-size: 0.76rem;
338
+ color: #0a1628; opacity: 0.55;
339
+ margin: 0;
340
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
341
+ }
342
+
343
+ .sgu-match { text-align: right; }
344
+ .sgu-match-pct {
345
+ font-weight: 700; font-size: 1.3rem;
346
+ color: #1976d2;
347
+ letter-spacing: -0.02em;
348
+ line-height: 1;
349
+ font-variant-numeric: tabular-nums;
350
+ }
351
+ .sgu-match-label {
352
+ font-weight: 400; font-size: 0.62rem;
353
+ color: #0a1628; opacity: 0.5;
354
+ text-transform: uppercase;
355
+ letter-spacing: 0.18em;
356
+ margin-top: 4px;
357
+ }
358
+
359
+ .sgu-empty {
360
+ text-align: center;
361
+ color: #0a1628; opacity: 0.55;
362
+ padding: 60px 20px;
363
+ font-size: 0.95rem;
364
+ }
365
+
366
+ @media (max-width: 720px) {
367
+ .sgu-row { grid-template-columns: 40px 100px 1fr; gap: 12px; padding: 12px 14px; }
368
+ .sgu-num { font-size: 1.6rem; }
369
+ .sgu-thumb { width: 100px; height: 48px; }
370
+ .sgu-title { font-size: .92rem; }
371
+ .sgu-match { display: none; }
372
+ .sgu-results-header { font-size: 1.7rem; }
373
  }
374
  """
375
 
376
 
377
+ # ─── UI ────────────────────────────────────────────────────────────────────────
378
  with gr.Blocks(
379
  theme=gr.themes.Base(),
380
  title="Steam Graphic Universe",
 
382
  analytics_enabled=False,
383
  ) as demo:
384
 
 
385
  gr.HTML("""
386
+ <div class="sgu-hero">
387
+ <h1 class="sgu-headline">Let's<br/>Play!</h1>
388
+ <p class="sgu-tagline">
389
  You decide the games you want to play<br/>
390
  We are here to help you make the best decision
391
  </p>
392
+ <p class="sgu-explainer">
393
  Upload an image of any game and we'll surface the closest visual matches
394
  from a universe of over 9,800 Steam titles. Behind the scenes a CLIP vision
395
  model converts your image into a high-dimensional aesthetic fingerprint,
 
399
  </div>
400
  """)
401
 
402
+ # Image uploader β€” let Gradio handle the upload natively, we just shape the wrapper
403
+ query_image = gr.Image(
404
+ type="pil",
405
+ show_label=False,
406
+ show_download_button=False,
407
+ sources=["upload", "clipboard"],
408
+ elem_id="circle-zone",
409
+ height=320,
410
+ width=320,
411
+ )
 
 
 
 
 
412
 
413
+ top_k_slider = gr.Slider(
414
+ minimum=3, maximum=12, step=1, value=6,
415
+ label="Number of matches",
416
+ elem_id="slider-zone",
417
+ )
418
 
419
+ with gr.Row(elem_id="explore-zone"):
420
+ search_btn = gr.Button("Explore", elem_id="explore-btn")
421
 
422
  output_html = gr.HTML(
423
+ value='<div class="sgu-results-wrap"><p class="sgu-empty">Upload a game image above to begin exploring.</p></div>'
424
  )
425
 
426
  search_btn.click(
 
429
  outputs=output_html,
430
  )
431
 
 
432
  if __name__ == "__main__":
433
+ demo.launch()