Jonathandav commited on
Commit
07c7af8
Β·
verified Β·
1 Parent(s): ed73d88

Upload 3 files

Browse files
Files changed (3) hide show
  1. README.md +6 -9
  2. app.py +77 -51
  3. requirements.txt +3 -4
README.md CHANGED
@@ -2,10 +2,10 @@
2
  title: Wonder Finder
3
  emoji: 🌍
4
  colorFrom: yellow
5
- colorTo: yellow
6
  sdk: gradio
7
- sdk_version: 4.44.0
8
- python_version: "3.12"
9
  app_file: app.py
10
  pinned: false
11
  license: mit
@@ -22,18 +22,15 @@ Visual recommender for the 12 Wonders of the World, powered by CLIP embeddings.
22
  ## How it works
23
  1. The catalog (11,544 images across 12 wonder classes) is pre-embedded using CLIP ViT-B/32.
24
  2. User input (image or text) is embedded into the same 512-D space.
25
- 3. Cosine similarity ranks the catalog; the top 3 results are returned with a diversity filter to avoid duplicates.
26
 
27
  ## Dataset
28
  [chavajaz/wonders_dataset](https://huggingface.co/datasets/chavajaz/wonders_dataset) β€” CC0-1.0 licensed, ~960 images per class on average.
29
 
30
  ## Model
31
- [openai/clip-vit-base-patch32](https://huggingface.co/openai/clip-vit-base-patch32) β€” chosen for its joint image-text embedding space, which enables both image and text input through a single model.
32
 
33
  ## Cluster analysis
34
  K-Means at k=12 on the embeddings achieved **ARI = 0.890** and **NMI = 0.927** against ground-truth wonder labels, indicating CLIP's pretrained space already separates the 12 wonders almost perfectly without supervision.
35
 
36
- ## Files
37
- - `app.py` β€” the Gradio application
38
- - `requirements.txt` β€” pinned dependencies
39
- - `wonders_embeddings.parquet` β€” precomputed CLIP embeddings (one row per catalog image, column `embedding`, aligned 1:1 with the dataset in `train β†’ validation β†’ test` order)
 
2
  title: Wonder Finder
3
  emoji: 🌍
4
  colorFrom: yellow
5
+ colorTo: red
6
  sdk: gradio
7
+ sdk_version: 4.44.1
8
+ python_version: "3.10"
9
  app_file: app.py
10
  pinned: false
11
  license: mit
 
22
  ## How it works
23
  1. The catalog (11,544 images across 12 wonder classes) is pre-embedded using CLIP ViT-B/32.
24
  2. User input (image or text) is embedded into the same 512-D space.
25
+ 3. Cosine similarity ranks the catalog; top 3 results are returned with a diversity filter to avoid duplicates.
26
 
27
  ## Dataset
28
  [chavajaz/wonders_dataset](https://huggingface.co/datasets/chavajaz/wonders_dataset) β€” CC0-1.0 licensed, ~960 images per class on average.
29
 
30
  ## Model
31
+ [openai/clip-vit-base-patch32](https://huggingface.co/openai/clip-vit-base-patch32) β€” chosen for its joint image–text embedding space, which enables both image and text input through a single model.
32
 
33
  ## Cluster analysis
34
  K-Means at k=12 on the embeddings achieved **ARI = 0.890** and **NMI = 0.927** against ground-truth wonder labels, indicating CLIP's pretrained space already separates the 12 wonders almost perfectly without supervision.
35
 
36
+ Built as the final project for Assignment 3.
 
 
 
app.py CHANGED
@@ -1,12 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import gradio as gr
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  import torch
3
  import numpy as np
4
  import pandas as pd
 
5
  from transformers import CLIPProcessor, CLIPModel
6
  from datasets import load_dataset, concatenate_datasets
7
 
8
  # ============================================================
9
- # LOAD EVERYTHING ON STARTUP (runs once when the Space boots)
10
  # ============================================================
11
 
12
  print("Loading CLIP model...")
@@ -18,28 +83,13 @@ processor = CLIPProcessor.from_pretrained(MODEL_NAME)
18
 
19
  print("Loading dataset...")
20
  ds = load_dataset("chavajaz/wonders_dataset")
21
- # Concatenate whatever splits exist, in a stable order, instead of assuming
22
- # train/validation/test are all present.
23
- split_order = [s for s in ["train", "validation", "test"] if s in ds]
24
- split_order += [s for s in ds.keys() if s not in split_order]
25
- splits = [ds[s] for s in split_order]
26
- full_ds = concatenate_datasets(splits) if len(splits) > 1 else splits[0]
27
  class_names = full_ds.features["label"].names
28
 
29
  print("Loading precomputed embeddings...")
30
  embeddings_df = pd.read_parquet("wonders_embeddings.parquet")
31
  image_embeddings = np.array(embeddings_df["embedding"].tolist(), dtype=np.float32)
32
  EMBEDDINGS_TENSOR = torch.tensor(image_embeddings, device=device, dtype=torch.float32)
33
- # Defensively L2-normalize so cosine similarity and the diversity threshold
34
- # are correct even if the stored vectors weren't normalized.
35
- EMBEDDINGS_TENSOR = EMBEDDINGS_TENSOR / EMBEDDINGS_TENSOR.norm(dim=-1, keepdim=True).clamp_min(1e-12)
36
-
37
- if len(full_ds) != EMBEDDINGS_TENSOR.shape[0]:
38
- print(
39
- f"WARNING: dataset has {len(full_ds)} images but the embeddings file has "
40
- f"{EMBEDDINGS_TENSOR.shape[0]} rows. They must line up 1:1 and be in the "
41
- f"same order for results to be correct."
42
- )
43
 
44
  print(f"Ready. {len(full_ds)} images, embeddings {image_embeddings.shape}, on {device}")
45
 
@@ -106,7 +156,7 @@ def recommend(query_embedding, top_k=3, diversity_threshold=0.98):
106
 
107
  def recommend_from_image(input_image):
108
  if input_image is None:
109
- return [], "βœ‹ Please upload an image to find matching wonders."
110
  query_emb = embed_image(input_image)
111
  results = recommend(query_emb, top_k=3)
112
  gallery_items = [
@@ -115,14 +165,14 @@ def recommend_from_image(input_image):
115
  ]
116
  medals = ["πŸ₯‡", "πŸ₯ˆ", "πŸ₯‰"]
117
  summary = "Your top 3 wonder matches:\n\n" + "\n".join(
118
- f"{medals[i]} {r['label_name'].replace('_', ' ').title():<22} similarity {r['score']:.3f}"
119
  for i, r in enumerate(results)
120
  )
121
  return gallery_items, summary
122
 
123
  def recommend_from_text(text_query):
124
  if not text_query or not text_query.strip():
125
- return [], "βœ‹ Please describe what you're looking for."
126
  query_emb = embed_text(text_query)
127
  results = recommend(query_emb, top_k=3)
128
  gallery_items = [
@@ -131,7 +181,7 @@ def recommend_from_text(text_query):
131
  ]
132
  medals = ["πŸ₯‡", "πŸ₯ˆ", "πŸ₯‰"]
133
  summary = f'Best matches for "{text_query}":\n\n' + "\n".join(
134
- f"{medals[i]} {r['label_name'].replace('_', ' ').title():<22} similarity {r['score']:.3f}"
135
  for i, r in enumerate(results)
136
  )
137
  return gallery_items, summary
@@ -148,13 +198,11 @@ CUSTOM_CSS = """
148
  font-family: 'Nunito', 'Quicksand', -apple-system, sans-serif !important;
149
  }
150
 
151
- /* Headings get the rounder, friendlier Quicksand */
152
  h1, h2, h3, h4 {
153
  font-family: 'Quicksand', sans-serif !important;
154
  letter-spacing: 0.3px !important;
155
  }
156
 
157
- /* ---------- HEADER ---------- */
158
  #header-block {
159
  background: linear-gradient(135deg, #8B4513 0%, #A0522D 50%, #CD853F 100%);
160
  padding: 36px 28px;
@@ -182,7 +230,6 @@ h1, h2, h3, h4 {
182
  opacity: 0.95;
183
  }
184
 
185
- /* ---------- TABS (the big upgrade) ---------- */
186
  .tab-nav {
187
  background: transparent !important;
188
  border-bottom: none !important;
@@ -221,7 +268,6 @@ h1, h2, h3, h4 {
221
  transform: translateY(-2px);
222
  }
223
 
224
- /* ---------- BUTTONS ---------- */
225
  button.primary, .gr-button-primary {
226
  background: linear-gradient(135deg, #8B4513 0%, #A0522D 100%) !important;
227
  border: none !important;
@@ -239,7 +285,6 @@ button.primary:hover, .gr-button-primary:hover {
239
  box-shadow: 0 6px 16px rgba(139, 69, 19, 0.45) !important;
240
  }
241
 
242
- /* ---------- INPUTS / PANELS ---------- */
243
  .gr-box, .gr-form, .gr-panel {
244
  background: #FFF8E7 !important;
245
  border: 2px solid #D2B48C !important;
@@ -276,7 +321,6 @@ textarea:focus, input[type="text"]:focus {
276
  padding: 10px !important;
277
  }
278
 
279
- /* ---------- FOOTER ---------- */
280
  #footer-block {
281
  margin-top: 28px;
282
  padding: 22px 24px;
@@ -298,9 +342,6 @@ textarea:focus, input[type="text"]:focus {
298
  }
299
  """
300
 
301
- # Build sample image indices defensively so a smaller dataset can't crash startup.
302
- SAMPLE_IDX = [i for i in [50, 2000, 5000, 7500, 10000] if i < len(full_ds)]
303
-
304
  with gr.Blocks(css=CUSTOM_CSS, theme=gr.themes.Soft(
305
  primary_hue="orange", secondary_hue="amber", neutral_hue="stone",
306
  ), title="Wonder Finder") as demo:
@@ -324,12 +365,6 @@ with gr.Blocks(css=CUSTOM_CSS, theme=gr.themes.Soft(
324
  with gr.Column(scale=2):
325
  img_gallery = gr.Gallery(label="Top 3 Matches", columns=3, rows=1, height=320, object_fit="cover")
326
  img_summary = gr.Textbox(label="πŸ“Š Match Details", lines=6, show_copy_button=True)
327
- if SAMPLE_IDX:
328
- gr.Examples(
329
- examples=[[full_ds[i]["image"]] for i in SAMPLE_IDX],
330
- inputs=img_input,
331
- label="✨ Or try these sample images:",
332
- )
333
  img_btn.click(recommend_from_image, inputs=img_input, outputs=[img_gallery, img_summary])
334
 
335
  with gr.Tab("πŸ’¬ Search by Description"):
@@ -345,19 +380,6 @@ with gr.Blocks(css=CUSTOM_CSS, theme=gr.themes.Soft(
345
  with gr.Column(scale=2):
346
  text_gallery = gr.Gallery(label="Top 3 Matches", columns=3, rows=1, height=320, object_fit="cover")
347
  text_summary = gr.Textbox(label="πŸ“Š Match Details", lines=6, show_copy_button=True)
348
- gr.Examples(
349
- examples=[
350
- ["ancient stone pyramid in the desert"],
351
- ["tall modern skyscraper at night"],
352
- ["waterfall in the tropical jungle"],
353
- ["ancient Roman amphitheater"],
354
- ["statue of a religious figure with outstretched arms"],
355
- ["a misty stone monument at sunrise"],
356
- ["white marble palace with a dome"],
357
- ],
358
- inputs=text_input,
359
- label="✨ Or try these example queries:",
360
- )
361
  text_btn.click(recommend_from_text, inputs=text_input, outputs=[text_gallery, text_summary])
362
 
363
  gr.HTML("""
@@ -369,6 +391,10 @@ with gr.Blocks(css=CUSTOM_CSS, theme=gr.themes.Soft(
369
  </div>
370
  """)
371
 
372
-
373
  if __name__ == "__main__":
374
- demo.launch()
 
 
 
 
 
 
1
+ """
2
+ Wonder Finder β€” Visual recommender for the 12 Wonders of the World.
3
+ HF Spaces deployment.
4
+
5
+ Notes on defensive patches:
6
+ - gradio 4.44.x has a known bug in gradio_client/utils.py where api_info
7
+ schema generation crashes on `additionalProperties: True` (boolean schema).
8
+ - The bug raises gradio_client.utils.APIInfoParseError, which is NOT a
9
+ subclass of TypeError/KeyError/AttributeError β€” so naive try/except misses it.
10
+ - We patch at THREE layers: get_type, _json_schema_to_python_type, and
11
+ Blocks.get_api_info. Each catches Exception (the broadest possible).
12
+ """
13
+
14
+ import os
15
+
16
+ # Belt-and-suspenders env vars
17
+ os.environ["GRADIO_ANALYTICS_ENABLED"] = "False"
18
+ os.environ["GRADIO_SERVER_NAME"] = "0.0.0.0"
19
+
20
+ # ============================================================
21
+ # DEFENSIVE PATCHES β€” must run before any Gradio component init
22
+ # ============================================================
23
+
24
+ import gradio_client.utils as _gcu
25
+
26
+ # Patch 1: _json_schema_to_python_type β€” the inner recursive function
27
+ _original_json_schema = _gcu._json_schema_to_python_type
28
+ def _safe_json_schema(schema, defs=None):
29
+ # Handle boolean schemas (the actual bug trigger)
30
+ if isinstance(schema, bool):
31
+ return "Any"
32
+ if not isinstance(schema, dict):
33
+ return "Any"
34
+ try:
35
+ return _original_json_schema(schema, defs)
36
+ except Exception:
37
+ return "Any"
38
+ _gcu._json_schema_to_python_type = _safe_json_schema
39
+
40
+ # Patch 2: get_type β€” wraps the entry-point type checker
41
+ _original_get_type = _gcu.get_type
42
+ def _safe_get_type(schema):
43
+ if not isinstance(schema, dict):
44
+ return "Any"
45
+ try:
46
+ return _original_get_type(schema)
47
+ except Exception:
48
+ return "Any"
49
+ _gcu.get_type = _safe_get_type
50
+
51
+ # Patch 3: top-level api_info generator β€” safety net for anything we missed
52
  import gradio as gr
53
+ import gradio.blocks as _gradio_blocks
54
+ _original_get_api_info = _gradio_blocks.Blocks.get_api_info
55
+ def _safe_get_api_info(self):
56
+ try:
57
+ return _original_get_api_info(self)
58
+ except Exception:
59
+ return {"named_endpoints": {}, "unnamed_endpoints": {}}
60
+ _gradio_blocks.Blocks.get_api_info = _safe_get_api_info
61
+
62
+ # ============================================================
63
+ # REGULAR IMPORTS
64
+ # ============================================================
65
+
66
  import torch
67
  import numpy as np
68
  import pandas as pd
69
+ from PIL import Image
70
  from transformers import CLIPProcessor, CLIPModel
71
  from datasets import load_dataset, concatenate_datasets
72
 
73
  # ============================================================
74
+ # LOAD EVERYTHING ON STARTUP
75
  # ============================================================
76
 
77
  print("Loading CLIP model...")
 
83
 
84
  print("Loading dataset...")
85
  ds = load_dataset("chavajaz/wonders_dataset")
86
+ full_ds = concatenate_datasets([ds["train"], ds["validation"], ds["test"]])
 
 
 
 
 
87
  class_names = full_ds.features["label"].names
88
 
89
  print("Loading precomputed embeddings...")
90
  embeddings_df = pd.read_parquet("wonders_embeddings.parquet")
91
  image_embeddings = np.array(embeddings_df["embedding"].tolist(), dtype=np.float32)
92
  EMBEDDINGS_TENSOR = torch.tensor(image_embeddings, device=device, dtype=torch.float32)
 
 
 
 
 
 
 
 
 
 
93
 
94
  print(f"Ready. {len(full_ds)} images, embeddings {image_embeddings.shape}, on {device}")
95
 
 
156
 
157
  def recommend_from_image(input_image):
158
  if input_image is None:
159
+ return [], "Please upload an image to find matching wonders."
160
  query_emb = embed_image(input_image)
161
  results = recommend(query_emb, top_k=3)
162
  gallery_items = [
 
165
  ]
166
  medals = ["πŸ₯‡", "πŸ₯ˆ", "πŸ₯‰"]
167
  summary = "Your top 3 wonder matches:\n\n" + "\n".join(
168
+ f"{medals[i]} {r['label_name'].replace('_', ' ').title()} β€” similarity {r['score']:.3f}"
169
  for i, r in enumerate(results)
170
  )
171
  return gallery_items, summary
172
 
173
  def recommend_from_text(text_query):
174
  if not text_query or not text_query.strip():
175
+ return [], "Please describe what you're looking for."
176
  query_emb = embed_text(text_query)
177
  results = recommend(query_emb, top_k=3)
178
  gallery_items = [
 
181
  ]
182
  medals = ["πŸ₯‡", "πŸ₯ˆ", "πŸ₯‰"]
183
  summary = f'Best matches for "{text_query}":\n\n' + "\n".join(
184
+ f"{medals[i]} {r['label_name'].replace('_', ' ').title()} β€” similarity {r['score']:.3f}"
185
  for i, r in enumerate(results)
186
  )
187
  return gallery_items, summary
 
198
  font-family: 'Nunito', 'Quicksand', -apple-system, sans-serif !important;
199
  }
200
 
 
201
  h1, h2, h3, h4 {
202
  font-family: 'Quicksand', sans-serif !important;
203
  letter-spacing: 0.3px !important;
204
  }
205
 
 
206
  #header-block {
207
  background: linear-gradient(135deg, #8B4513 0%, #A0522D 50%, #CD853F 100%);
208
  padding: 36px 28px;
 
230
  opacity: 0.95;
231
  }
232
 
 
233
  .tab-nav {
234
  background: transparent !important;
235
  border-bottom: none !important;
 
268
  transform: translateY(-2px);
269
  }
270
 
 
271
  button.primary, .gr-button-primary {
272
  background: linear-gradient(135deg, #8B4513 0%, #A0522D 100%) !important;
273
  border: none !important;
 
285
  box-shadow: 0 6px 16px rgba(139, 69, 19, 0.45) !important;
286
  }
287
 
 
288
  .gr-box, .gr-form, .gr-panel {
289
  background: #FFF8E7 !important;
290
  border: 2px solid #D2B48C !important;
 
321
  padding: 10px !important;
322
  }
323
 
 
324
  #footer-block {
325
  margin-top: 28px;
326
  padding: 22px 24px;
 
342
  }
343
  """
344
 
 
 
 
345
  with gr.Blocks(css=CUSTOM_CSS, theme=gr.themes.Soft(
346
  primary_hue="orange", secondary_hue="amber", neutral_hue="stone",
347
  ), title="Wonder Finder") as demo:
 
365
  with gr.Column(scale=2):
366
  img_gallery = gr.Gallery(label="Top 3 Matches", columns=3, rows=1, height=320, object_fit="cover")
367
  img_summary = gr.Textbox(label="πŸ“Š Match Details", lines=6, show_copy_button=True)
 
 
 
 
 
 
368
  img_btn.click(recommend_from_image, inputs=img_input, outputs=[img_gallery, img_summary])
369
 
370
  with gr.Tab("πŸ’¬ Search by Description"):
 
380
  with gr.Column(scale=2):
381
  text_gallery = gr.Gallery(label="Top 3 Matches", columns=3, rows=1, height=320, object_fit="cover")
382
  text_summary = gr.Textbox(label="πŸ“Š Match Details", lines=6, show_copy_button=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
383
  text_btn.click(recommend_from_text, inputs=text_input, outputs=[text_gallery, text_summary])
384
 
385
  gr.HTML("""
 
391
  </div>
392
  """)
393
 
 
394
  if __name__ == "__main__":
395
+ demo.launch(
396
+ server_name="0.0.0.0",
397
+ server_port=7860,
398
+ show_api=False,
399
+ share=False,
400
+ )
requirements.txt CHANGED
@@ -1,10 +1,9 @@
1
- gradio==4.44.0
2
- pydantic==2.9.2
3
  transformers==4.45.2
4
  torch==2.4.1
5
  datasets==3.0.0
6
  pillow==10.4.0
7
  numpy==1.26.4
8
  pandas==2.2.2
9
- pyarrow==17.0.0
10
- huggingface-hub==0.25.0
 
1
+ gradio==4.44.1
2
+ gradio-client==1.3.0
3
  transformers==4.45.2
4
  torch==2.4.1
5
  datasets==3.0.0
6
  pillow==10.4.0
7
  numpy==1.26.4
8
  pandas==2.2.2
9
+ huggingface-hub==0.25.2