Faraz618 commited on
Commit
71e10db
Β·
verified Β·
1 Parent(s): 1c5a37c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +79 -138
app.py CHANGED
@@ -7,51 +7,23 @@ Deploy: HuggingFace Spaces (CPU Basic β€” free tier)
7
  Pipeline
8
  Index : Image β†’ YOLOv8 detection β†’ CLIP embedding β†’ FAISS vector store
9
  Query : Text β†’ CLIP text embedding β†’ cosine k-NN β†’ LLM answer generation
 
 
 
 
 
 
 
 
10
  """
11
 
12
  import json
13
  import os
14
  from datetime import datetime
15
- from typing import List, Dict, Tuple, Optional
16
 
17
  import faiss
18
  import numpy as np
19
  import torch
20
-
21
- # ── Monkey-patch gradio_client 4.44.x schema-introspection bug ────────────────
22
- # gradio_client/utils.py calls `if "const" in schema` where schema can be a
23
- # plain bool (valid JSON Schema: additionalProperties=True/False).
24
- # The `in` operator on a bool raises TypeError and crashes the whole Space.
25
- # We patch both affected functions before importing gradio.
26
- try:
27
- import gradio_client.utils as _gcu
28
-
29
- # Patch 1: get_type() β€” guard against non-dict schema argument
30
- _orig_get_type = _gcu.get_type
31
-
32
- def _safe_get_type(schema):
33
- if not isinstance(schema, dict):
34
- return "Any"
35
- return _orig_get_type(schema)
36
-
37
- _gcu.get_type = _safe_get_type
38
-
39
- # Patch 2: _json_schema_to_python_type() β€” normalise bool additionalProperties
40
- _orig_j2p = _gcu._json_schema_to_python_type
41
-
42
- def _safe_j2p(schema, defs=None):
43
- if not isinstance(schema, dict):
44
- return "Any"
45
- if isinstance(schema.get("additionalProperties"), bool):
46
- schema = {**schema, "additionalProperties": {}}
47
- return _orig_j2p(schema, defs)
48
-
49
- _gcu._json_schema_to_python_type = _safe_j2p
50
- print("βœ… gradio_client monkey-patch applied.")
51
- except Exception as _e:
52
- print(f"⚠️ gradio_client patch skipped: {_e}")
53
- # ──────────────────────────────────────────────────────────────────────────────
54
-
55
  import gradio as gr
56
  from PIL import Image
57
  from transformers import CLIPModel, CLIPProcessor
@@ -60,35 +32,32 @@ from huggingface_hub import InferenceClient
60
 
61
  # ── Model loading (runs once at Space start-up) ────────────────────────────────
62
  print("⏳ Loading CLIP ViT-B/32 ...")
63
- CLIP_MODEL_ID = "openai/clip-vit-base-patch32"
64
  clip_model = CLIPModel.from_pretrained(CLIP_MODEL_ID)
65
  clip_processor = CLIPProcessor.from_pretrained(CLIP_MODEL_ID)
66
  clip_model.eval()
67
 
68
  print("⏳ Loading YOLOv8n ...")
69
- yolo = YOLO("yolov8n.pt") # auto-downloads ~6 MB on first run
70
 
71
  print("⏳ Initialising LLM inference client ...")
72
- # Uses HF Serverless Inference β€” free for open models, no GPU in Space needed.
73
- # Add HF_TOKEN as a Space secret for higher rate limits.
74
  HF_TOKEN = os.environ.get("HF_TOKEN", None)
75
- llm = InferenceClient(
76
- model="HuggingFaceH4/zephyr-7b-beta",
77
- token=HF_TOKEN,
78
- )
79
 
80
  print("βœ… All models ready.")
81
 
82
  # ── FAISS vector store (in-memory, session-scoped) ─────────────────────────────
83
- EMBED_DIM = 512 # CLIP ViT-B/32 output dimension
84
- faiss_index = faiss.IndexFlatIP(EMBED_DIM) # inner-product β†’ cosine after L2 norm
85
- image_store: List[Dict] = [] # parallel list to FAISS index rows
86
 
87
 
88
  # ── Embedding helpers ──────────────────────────────────────────────────────────
89
 
90
  def embed_image(pil_img: Image.Image) -> np.ndarray:
91
- """Return L2-normalised 512-d CLIP image embedding."""
92
  inputs = clip_processor(images=pil_img, return_tensors="pt")
93
  with torch.no_grad():
94
  features = clip_model.get_image_features(**inputs)
@@ -98,10 +67,9 @@ def embed_image(pil_img: Image.Image) -> np.ndarray:
98
 
99
 
100
  def embed_text(text: str) -> np.ndarray:
101
- """Return L2-normalised 512-d CLIP text embedding."""
102
- inputs = clip_processor(
103
- text=[text], return_tensors="pt", padding=True, truncation=True
104
- )
105
  with torch.no_grad():
106
  features = clip_model.get_text_features(**inputs)
107
  emb = features.numpy().astype("float32")
@@ -111,26 +79,23 @@ def embed_text(text: str) -> np.ndarray:
111
 
112
  # ── Computer-vision detection pipeline ────────────────────────────────────────
113
 
114
- def run_detection(pil_img: Image.Image) -> Tuple[Image.Image, List[Dict], str]:
115
  """
116
- Run YOLOv8 on a PIL image.
117
- Returns: (annotated_pil, detections_list, summary_str)
118
  """
119
- results = yolo(np.array(pil_img))[0]
120
- annotated = Image.fromarray(results.plot())
121
 
122
- detections: List[Dict] = []
123
  if results.boxes is not None:
124
  for box in results.boxes:
125
- cls_id = int(box.cls[0])
126
- conf = float(box.conf[0])
127
  detections.append({
128
- "label": yolo.names[cls_id],
129
- "confidence": round(conf, 3),
130
  })
131
 
132
- # Object count summary, e.g. "2 person, 1 car"
133
- counts: Dict[str, int] = {}
134
  for d in detections:
135
  counts[d["label"]] = counts.get(d["label"], 0) + 1
136
  summary = ", ".join(f"{v} {k}" for k, v in counts.items()) or "no objects detected"
@@ -141,10 +106,7 @@ def run_detection(pil_img: Image.Image) -> Tuple[Image.Image, List[Dict], str]:
141
  # ── Index pipeline ─────────────────────────────────────────────────────────────
142
 
143
  def index_image(image_np, note: str):
144
- """
145
- Detect objects, compute CLIP embedding, add to FAISS index.
146
- Triggered by the 'Detect & Index' button.
147
- """
148
  if image_np is None:
149
  return None, "❌ Please upload an image first.", _store_badge()
150
 
@@ -152,21 +114,20 @@ def index_image(image_np, note: str):
152
  annotated, detections, summary = run_detection(pil_img)
153
  embedding = embed_image(pil_img)
154
 
155
- # Persist to vector store
156
  faiss_index.add(embedding)
157
- record = {
158
  "id": len(image_store),
159
  "image": pil_img.copy(),
160
- "annotated": annotated,
161
  "detections": detections,
162
  "summary": summary,
163
  "note": note.strip() or "β€”",
164
  "ts": datetime.now().strftime("%H:%M:%S"),
165
- }
166
- image_store.append(record)
167
 
168
- msg = f"βœ… Image #{record['id']} indexed Β· Found: {summary}"
169
- return np.array(annotated), msg, _store_badge()
 
170
 
171
 
172
  def _store_badge() -> str:
@@ -177,24 +138,20 @@ def _store_badge() -> str:
177
 
178
  def query_images(text_query: str, top_k: int):
179
  """
180
- 1. Embed query text with CLIP.
181
- 2. Cosine k-NN search in FAISS.
182
- 3. Build RAG prompt from retrieved context.
183
- 4. Generate answer with Zephyr-7B via HF Inference API.
184
  """
185
  if not image_store:
186
- return None, "❌ No images indexed yet β€” go to 'Detect & Index' first.", []
187
 
188
  if not text_query.strip():
189
- return None, "❌ Please enter a question.", []
190
 
191
  query_emb = embed_text(text_query)
192
  k = min(int(top_k), len(image_store))
193
  scores, idxs = faiss_index.search(query_emb, k)
194
 
195
- # Build retrieval results & context string for the LLM
196
- hits: List[Dict] = []
197
- ctx_lines: List[str] = []
198
  for rank, (score, idx) in enumerate(zip(scores[0], idxs[0])):
199
  if idx < 0:
200
  continue
@@ -214,7 +171,6 @@ def query_images(text_query: str, top_k: int):
214
 
215
  context = "\n".join(ctx_lines)
216
 
217
- # Zephyr-7B chat prompt (ChatML format)
218
  prompt = (
219
  "<|system|>\n"
220
  "You are a concise visual-AI assistant. "
@@ -235,15 +191,11 @@ def query_images(text_query: str, top_k: int):
235
  stop_sequences=["<|user|>", "<|system|>"],
236
  ).strip()
237
  except Exception as exc:
238
- answer = (
239
- f"⚠️ LLM call failed ({exc}).\n\n"
240
- f"Raw retrieval context:\n{context}"
241
- )
242
 
243
  best_idx = int(idxs[0][0]) if len(idxs[0]) > 0 and idxs[0][0] >= 0 else None
244
- best_image = (
245
- np.array(image_store[best_idx]["annotated"]) if best_idx is not None else None
246
- )
247
 
248
  return best_image, answer, json.dumps(hits, indent=2)
249
 
@@ -271,7 +223,6 @@ with gr.Blocks(
271
  css=CUSTOM_CSS,
272
  ) as demo:
273
 
274
- # ── Header ─────────────────────────────────────────────────────────────────
275
  gr.HTML("""
276
  <div class="hero">
277
  <h1>πŸ” VisualRAG</h1>
@@ -281,18 +232,18 @@ with gr.Blocks(
281
  <span class="badge">CLIP ViT-B/32</span>
282
  <span class="badge">FAISS</span>
283
  <span class="badge">Zephyr-7B</span>
284
- <span class="badge">Gradio</span>
285
  </div>
286
  </div>
287
  """)
288
 
289
  with gr.Tabs():
290
 
291
- # ── TAB 1 : Detect & Index ─────────────────────────────────────────────
292
  with gr.Tab("πŸ“€ Detect & Index"):
293
  gr.Markdown(
294
- "Upload any image. YOLOv8 will detect objects, then CLIP encodes it "
295
- "into a 512-d embedding stored in FAISS for later retrieval."
296
  )
297
  with gr.Row():
298
  with gr.Column(scale=1):
@@ -305,17 +256,14 @@ with gr.Blocks(
305
  label="Context note (optional)",
306
  placeholder="e.g. 'Warehouse entrance camera, aisle 3'",
307
  )
308
- index_btn = gr.Button(
309
- "πŸ” Detect & Index", variant="primary", size="lg"
310
- )
311
 
312
  with gr.Column(scale=1):
 
313
  detection_output = gr.Image(label="Detection result (annotated)")
314
  status_output = gr.Textbox(label="Status", interactive=False)
315
  store_output = gr.Textbox(
316
- label="Vector store",
317
- interactive=False,
318
- value=_store_badge(),
319
  )
320
 
321
  index_btn.click(
@@ -324,18 +272,18 @@ with gr.Blocks(
324
  outputs=[detection_output, status_output, store_output],
325
  )
326
 
327
- # ── TAB 2 : Query (RAG) ────────────────────────────────────────────────
328
  with gr.Tab("πŸ’¬ Query (RAG)"):
329
  gr.Markdown(
330
- "Type any question about your indexed images. "
331
- "CLIP embeds the query, FAISS retrieves the most similar images, "
332
  "and Zephyr-7B generates a grounded answer."
333
  )
334
  with gr.Row():
335
  with gr.Column(scale=1):
336
  query_input = gr.Textbox(
337
  label="Your question",
338
- placeholder="e.g. 'How many people are visible?' or 'Are there any vehicles?'",
339
  lines=3,
340
  )
341
  topk_slider = gr.Slider(
@@ -349,43 +297,39 @@ with gr.Blocks(
349
  with gr.Column(scale=1):
350
  match_img = gr.Image(label="Best matching image")
351
  llm_answer = gr.Textbox(
352
- label="AI Answer (RAG-grounded)",
353
- lines=6,
354
- interactive=False,
355
  )
356
 
357
- hits_json = gr.Textbox(
358
- label="Retrieval scores (all top-K)",
359
- interactive=False,
360
- lines=6,
361
  )
362
 
363
  query_btn.click(
364
  fn=query_images,
365
  inputs=[query_input, topk_slider],
366
- outputs=[match_img, llm_answer, hits_json],
367
  )
368
 
369
- # ── TAB 3 : How it works ───────────────────────────────────────────────
370
  with gr.Tab("πŸ—οΈ How it works"):
371
  gr.Markdown("""
372
- ## System Overview
373
 
374
- ### Index Pipeline
375
  ```
376
- Image β†’ YOLOv8 detection (objects + confidence)
377
- β†’ CLIP ViT-B/32 image encoder (512-d embedding)
378
  β†’ L2 normalisation
379
- β†’ FAISS IndexFlatIP (cosine similarity store)
380
  ```
381
 
382
- ### Query / RAG Pipeline
383
  ```
384
- Text query β†’ CLIP text encoder (512-d embedding)
385
  β†’ L2 normalisation
386
  β†’ FAISS k-NN search (cosine similarity, top-K)
387
- β†’ Build RAG prompt: query + retrieved context
388
- β†’ Zephyr-7B-Ξ² via HF Serverless Inference API
389
  β†’ Natural language answer
390
  ```
391
 
@@ -396,22 +340,19 @@ Text query β†’ CLIP text encoder (512-d embedding)
396
  | Object detection | YOLOv8n (Ultralytics) | Fast CPU inference, production-proven |
397
  | Visual embedding | CLIP ViT-B/32 | Joint image–text space enables textβ†’image search |
398
  | Vector index | FAISS IndexFlatIP | Exact cosine k-NN, zero infra |
399
- | LLM | Zephyr-7B-Ξ² (HF API) | Free serverless, good instruction following |
400
- | UI | Gradio 4.x | Native HF Spaces support |
401
-
402
- ## Skills Demonstrated
403
- - **Computer vision pipeline**: detect β†’ annotate β†’ embed β†’ index
404
- - **RAG architecture**: embed β†’ retrieve β†’ generate
405
- - **Multi-modal reasoning**: text queries over an image knowledge base
406
- - **Vector database**: FAISS with cosine similarity (L2-normalised inner product)
407
- - **LLM integration**: HF Inference API with prompt engineering
408
- - **Production patterns**: stateless functions, error handling, model caching
409
  """)
410
 
411
- # ── Footer ─────────────────────────────────────────────────────────────────
412
  gr.HTML("""
413
- <div style="text-align:center; padding:16px 0 4px; color:var(--color-subdued); font-size:12px">
414
- VisualRAG Β· YOLOv8 + CLIP + FAISS + LLM Β· Built for HuggingFace Spaces
415
  </div>
416
  """)
417
 
 
7
  Pipeline
8
  Index : Image β†’ YOLOv8 detection β†’ CLIP embedding β†’ FAISS vector store
9
  Query : Text β†’ CLIP text embedding β†’ cosine k-NN β†’ LLM answer generation
10
+
11
+ FIXES vs previous version
12
+ 1. Removed gradio_client monkey-patch β€” it targeted private functions that
13
+ were renamed in gradio 4.44.x, causing silent schema corruption on import.
14
+ 2. requirements.txt now includes opencv-python-headless (YOLO crashes without it)
15
+ and explicit gradio==4.44.1 pin.
16
+ 3. gr.Image outputs return PIL Images consistently (Gradio accepts both PIL and
17
+ numpy; PIL is safer across versions).
18
  """
19
 
20
  import json
21
  import os
22
  from datetime import datetime
 
23
 
24
  import faiss
25
  import numpy as np
26
  import torch
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  import gradio as gr
28
  from PIL import Image
29
  from transformers import CLIPModel, CLIPProcessor
 
32
 
33
  # ── Model loading (runs once at Space start-up) ────────────────────────────────
34
  print("⏳ Loading CLIP ViT-B/32 ...")
35
+ CLIP_MODEL_ID = "openai/clip-vit-base-patch32"
36
  clip_model = CLIPModel.from_pretrained(CLIP_MODEL_ID)
37
  clip_processor = CLIPProcessor.from_pretrained(CLIP_MODEL_ID)
38
  clip_model.eval()
39
 
40
  print("⏳ Loading YOLOv8n ...")
41
+ yolo = YOLO("yolov8n.pt") # auto-downloads ~6 MB on first run
42
 
43
  print("⏳ Initialising LLM inference client ...")
44
+ # Free HF Serverless Inference β€” model runs on HF servers, not in the Space.
45
+ # Add HF_TOKEN as a Space Secret (Settings β†’ Secrets) for higher rate limits.
46
  HF_TOKEN = os.environ.get("HF_TOKEN", None)
47
+ llm = InferenceClient(model="HuggingFaceH4/zephyr-7b-beta", token=HF_TOKEN)
 
 
 
48
 
49
  print("βœ… All models ready.")
50
 
51
  # ── FAISS vector store (in-memory, session-scoped) ─────────────────────────────
52
+ EMBED_DIM = 512 # CLIP ViT-B/32 output dimension
53
+ faiss_index = faiss.IndexFlatIP(EMBED_DIM) # cosine similarity via L2-normalised inner product
54
+ image_store = [] # parallel list: one dict per indexed image
55
 
56
 
57
  # ── Embedding helpers ──────────────────────────────────────────────────────────
58
 
59
  def embed_image(pil_img: Image.Image) -> np.ndarray:
60
+ """Return L2-normalised 512-d CLIP image embedding (shape: 1 Γ— 512)."""
61
  inputs = clip_processor(images=pil_img, return_tensors="pt")
62
  with torch.no_grad():
63
  features = clip_model.get_image_features(**inputs)
 
67
 
68
 
69
  def embed_text(text: str) -> np.ndarray:
70
+ """Return L2-normalised 512-d CLIP text embedding (shape: 1 Γ— 512)."""
71
+ inputs = clip_processor(text=[text], return_tensors="pt",
72
+ padding=True, truncation=True)
 
73
  with torch.no_grad():
74
  features = clip_model.get_text_features(**inputs)
75
  emb = features.numpy().astype("float32")
 
79
 
80
  # ── Computer-vision detection pipeline ────────────────────────────────────────
81
 
82
+ def run_detection(pil_img: Image.Image):
83
  """
84
+ Run YOLOv8n on a PIL image.
85
+ Returns: (annotated_PIL, detections_list, summary_str)
86
  """
87
+ results = yolo(np.array(pil_img))[0]
88
+ annotated = Image.fromarray(results.plot()) # PIL Image with bounding boxes
89
 
90
+ detections = []
91
  if results.boxes is not None:
92
  for box in results.boxes:
 
 
93
  detections.append({
94
+ "label": yolo.names[int(box.cls[0])],
95
+ "confidence": round(float(box.conf[0]), 3),
96
  })
97
 
98
+ counts = {}
 
99
  for d in detections:
100
  counts[d["label"]] = counts.get(d["label"], 0) + 1
101
  summary = ", ".join(f"{v} {k}" for k, v in counts.items()) or "no objects detected"
 
106
  # ── Index pipeline ─────────────────────────────────────────────────────────────
107
 
108
  def index_image(image_np, note: str):
109
+ """Detect β†’ embed β†’ store. Called by the 'Detect & Index' button."""
 
 
 
110
  if image_np is None:
111
  return None, "❌ Please upload an image first.", _store_badge()
112
 
 
114
  annotated, detections, summary = run_detection(pil_img)
115
  embedding = embed_image(pil_img)
116
 
 
117
  faiss_index.add(embedding)
118
+ image_store.append({
119
  "id": len(image_store),
120
  "image": pil_img.copy(),
121
+ "annotated": annotated, # PIL Image
122
  "detections": detections,
123
  "summary": summary,
124
  "note": note.strip() or "β€”",
125
  "ts": datetime.now().strftime("%H:%M:%S"),
126
+ })
 
127
 
128
+ msg = f"βœ… Image #{len(image_store) - 1} indexed Β· Found: {summary}"
129
+ # FIX 3: return PIL Image directly β€” avoids potential numpy dtype mismatches
130
+ return annotated, msg, _store_badge()
131
 
132
 
133
  def _store_badge() -> str:
 
138
 
139
  def query_images(text_query: str, top_k: int):
140
  """
141
+ Text β†’ CLIP embed β†’ FAISS k-NN β†’ RAG prompt β†’ Zephyr-7B answer.
 
 
 
142
  """
143
  if not image_store:
144
+ return None, "❌ No images indexed yet β€” upload images in the 'Detect & Index' tab first.", ""
145
 
146
  if not text_query.strip():
147
+ return None, "❌ Please enter a question.", ""
148
 
149
  query_emb = embed_text(text_query)
150
  k = min(int(top_k), len(image_store))
151
  scores, idxs = faiss_index.search(query_emb, k)
152
 
153
+ hits = []
154
+ ctx_lines = []
 
155
  for rank, (score, idx) in enumerate(zip(scores[0], idxs[0])):
156
  if idx < 0:
157
  continue
 
171
 
172
  context = "\n".join(ctx_lines)
173
 
 
174
  prompt = (
175
  "<|system|>\n"
176
  "You are a concise visual-AI assistant. "
 
191
  stop_sequences=["<|user|>", "<|system|>"],
192
  ).strip()
193
  except Exception as exc:
194
+ answer = f"⚠️ LLM call failed ({exc}).\n\nRaw retrieval context:\n{context}"
 
 
 
195
 
196
  best_idx = int(idxs[0][0]) if len(idxs[0]) > 0 and idxs[0][0] >= 0 else None
197
+ # FIX 3 continued: return PIL Image directly
198
+ best_image = image_store[best_idx]["annotated"] if best_idx is not None else None
 
199
 
200
  return best_image, answer, json.dumps(hits, indent=2)
201
 
 
223
  css=CUSTOM_CSS,
224
  ) as demo:
225
 
 
226
  gr.HTML("""
227
  <div class="hero">
228
  <h1>πŸ” VisualRAG</h1>
 
232
  <span class="badge">CLIP ViT-B/32</span>
233
  <span class="badge">FAISS</span>
234
  <span class="badge">Zephyr-7B</span>
235
+ <span class="badge">Gradio 4.44.1</span>
236
  </div>
237
  </div>
238
  """)
239
 
240
  with gr.Tabs():
241
 
242
+ # ── TAB 1: Detect & Index ──────────────────────────────────────────────
243
  with gr.Tab("πŸ“€ Detect & Index"):
244
  gr.Markdown(
245
+ "Upload any image. YOLOv8n detects objects, then CLIP ViT-B/32 encodes it "
246
+ "into a 512-d embedding stored in FAISS."
247
  )
248
  with gr.Row():
249
  with gr.Column(scale=1):
 
256
  label="Context note (optional)",
257
  placeholder="e.g. 'Warehouse entrance camera, aisle 3'",
258
  )
259
+ index_btn = gr.Button("πŸ” Detect & Index", variant="primary", size="lg")
 
 
260
 
261
  with gr.Column(scale=1):
262
+ # type not specified on output gr.Image β†’ Gradio auto-detects PIL or numpy
263
  detection_output = gr.Image(label="Detection result (annotated)")
264
  status_output = gr.Textbox(label="Status", interactive=False)
265
  store_output = gr.Textbox(
266
+ label="Vector store", interactive=False, value=_store_badge()
 
 
267
  )
268
 
269
  index_btn.click(
 
272
  outputs=[detection_output, status_output, store_output],
273
  )
274
 
275
+ # ── TAB 2: Query (RAG) ────────────────────────────────────────────────
276
  with gr.Tab("πŸ’¬ Query (RAG)"):
277
  gr.Markdown(
278
+ "Ask any question about your indexed images. CLIP embeds the query, "
279
+ "FAISS retrieves the most similar images by cosine similarity, "
280
  "and Zephyr-7B generates a grounded answer."
281
  )
282
  with gr.Row():
283
  with gr.Column(scale=1):
284
  query_input = gr.Textbox(
285
  label="Your question",
286
+ placeholder="e.g. 'How many people are visible?' or 'Are there any vehicles?'",
287
  lines=3,
288
  )
289
  topk_slider = gr.Slider(
 
297
  with gr.Column(scale=1):
298
  match_img = gr.Image(label="Best matching image")
299
  llm_answer = gr.Textbox(
300
+ label="AI Answer (RAG-grounded)", lines=6, interactive=False
 
 
301
  )
302
 
303
+ hits_out = gr.Textbox(
304
+ label="Retrieval scores (all top-K)", interactive=False, lines=8
 
 
305
  )
306
 
307
  query_btn.click(
308
  fn=query_images,
309
  inputs=[query_input, topk_slider],
310
+ outputs=[match_img, llm_answer, hits_out],
311
  )
312
 
313
+ # ── TAB 3: How it works ───────────────────────────────────────────────
314
  with gr.Tab("πŸ—οΈ How it works"):
315
  gr.Markdown("""
316
+ ## System overview
317
 
318
+ ### Index pipeline
319
  ```
320
+ Image β†’ YOLOv8n detection (objects + confidence scores)
321
+ β†’ CLIP ViT-B/32 image encoder β†’ 512-d embedding
322
  β†’ L2 normalisation
323
+ β†’ FAISS IndexFlatIP (cosine similarity store)
324
  ```
325
 
326
+ ### Query / RAG pipeline
327
  ```
328
+ Text query β†’ CLIP text encoder β†’ 512-d query embedding
329
  β†’ L2 normalisation
330
  β†’ FAISS k-NN search (cosine similarity, top-K)
331
+ β†’ RAG prompt = query + retrieved context
332
+ β†’ Zephyr-7B-Ξ² (HF Serverless Inference API)
333
  β†’ Natural language answer
334
  ```
335
 
 
340
  | Object detection | YOLOv8n (Ultralytics) | Fast CPU inference, production-proven |
341
  | Visual embedding | CLIP ViT-B/32 | Joint image–text space enables textβ†’image search |
342
  | Vector index | FAISS IndexFlatIP | Exact cosine k-NN, zero infra |
343
+ | LLM | Zephyr-7B-Ξ² (HF API) | Free serverless, strong instruction following |
344
+ | UI | Gradio 4.44.1 | Native HF Spaces support |
345
+
346
+ ## What was fixed in this version
347
+ - `opencv-python-headless` added to requirements β€” YOLO needs `cv2` at startup
348
+ - `gradio==4.44.1` pinned explicitly in requirements
349
+ - Removed fragile `gradio_client` monkey-patch β€” caused silent schema corruption in 4.44.x
350
+ - Return types unified to PIL Image (avoids numpy dtype edge cases across Gradio versions)
 
 
351
  """)
352
 
 
353
  gr.HTML("""
354
+ <div style="text-align:center; padding:14px 0 4px; color:var(--color-subdued); font-size:12px">
355
+ VisualRAG Β· YOLOv8 + CLIP + FAISS + LLM Β· HuggingFace Spaces
356
  </div>
357
  """)
358