Faraz618 commited on
Commit
129fbd5
Β·
verified Β·
1 Parent(s): 42f18ff

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +367 -0
app.py CHANGED
@@ -0,0 +1,367 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ VisualRAG β€” Multi-Modal AI System
3
+ ==================================
4
+ Stack : YOLOv8n Β· CLIP ViT-B/32 Β· FAISS Β· Zephyr-7B Β· Gradio
5
+ Deploy: HuggingFace Spaces (CPU Basic β€” free tier)
6
+
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
+
16
+ import faiss
17
+ import gradio as gr
18
+ import numpy as np
19
+ import torch
20
+ from PIL import Image
21
+ from transformers import CLIPModel, CLIPProcessor
22
+ from ultralytics import YOLO
23
+ from huggingface_hub import InferenceClient
24
+
25
+ # ── Model loading (runs once at Space start-up) ────────────────────────────────
26
+ print("⏳ Loading CLIP ViT-B/32 ...")
27
+ CLIP_MODEL_ID = "openai/clip-vit-base-patch32"
28
+ clip_model = CLIPModel.from_pretrained(CLIP_MODEL_ID)
29
+ clip_processor = CLIPProcessor.from_pretrained(CLIP_MODEL_ID)
30
+ clip_model.eval()
31
+
32
+ print("⏳ Loading YOLOv8n ...")
33
+ yolo = YOLO("yolov8n.pt") # auto-downloads ~6 MB on first run
34
+
35
+ print("⏳ Initialising LLM inference client ...")
36
+ # Uses HF Serverless Inference β€” free for open models, no GPU in Space needed.
37
+ # Add HF_TOKEN as a Space secret for higher rate limits.
38
+ HF_TOKEN = os.environ.get("HF_TOKEN", None)
39
+ llm = InferenceClient(
40
+ model="HuggingFaceH4/zephyr-7b-beta",
41
+ token=HF_TOKEN,
42
+ )
43
+
44
+ print("βœ… All models ready.")
45
+
46
+ # ── FAISS vector store (in-memory, session-scoped) ─────────────────────────────
47
+ EMBED_DIM = 512 # CLIP ViT-B/32 output dimension
48
+ faiss_index = faiss.IndexFlatIP(EMBED_DIM) # inner-product β†’ cosine after L2 norm
49
+ image_store: list[dict] = [] # parallel list to FAISS index rows
50
+
51
+
52
+ # ── Embedding helpers ──────────────────────────────────────────────────────────
53
+
54
+ def embed_image(pil_img: Image.Image) -> np.ndarray:
55
+ """Return L2-normalised 512-d CLIP image embedding."""
56
+ inputs = clip_processor(images=pil_img, return_tensors="pt")
57
+ with torch.no_grad():
58
+ features = clip_model.get_image_features(**inputs)
59
+ emb = features.numpy().astype("float32")
60
+ faiss.normalize_L2(emb)
61
+ return emb
62
+
63
+
64
+ def embed_text(text: str) -> np.ndarray:
65
+ """Return L2-normalised 512-d CLIP text embedding."""
66
+ inputs = clip_processor(text=[text], return_tensors="pt", padding=True, truncation=True)
67
+ with torch.no_grad():
68
+ features = clip_model.get_text_features(**inputs)
69
+ emb = features.numpy().astype("float32")
70
+ faiss.normalize_L2(emb)
71
+ return emb
72
+
73
+
74
+ # ── Computer-vision detection pipeline ────────────────────────────────────────
75
+
76
+ def run_detection(pil_img: Image.Image):
77
+ """
78
+ Run YOLOv8 on a PIL image.
79
+ Returns: (annotated_pil, detections_list, summary_str)
80
+ """
81
+ results = yolo(np.array(pil_img))[0]
82
+ annotated = Image.fromarray(results.plot())
83
+
84
+ detections = []
85
+ if results.boxes is not None:
86
+ for box in results.boxes:
87
+ cls_id = int(box.cls[0])
88
+ conf = float(box.conf[0])
89
+ detections.append({
90
+ "label": yolo.names[cls_id],
91
+ "confidence": round(conf, 3),
92
+ })
93
+
94
+ # Object count summary, e.g. "2 person, 1 car"
95
+ counts: dict[str, int] = {}
96
+ for d in detections:
97
+ counts[d["label"]] = counts.get(d["label"], 0) + 1
98
+ summary = ", ".join(f"{v} {k}" for k, v in counts.items()) or "no objects detected"
99
+
100
+ return annotated, detections, summary
101
+
102
+
103
+ # ── Index pipeline ─────────────────────────────────────────────────────────────
104
+
105
+ def index_image(image_np, note: str):
106
+ """
107
+ Detect objects, compute CLIP embedding, add to FAISS index.
108
+ Triggered by the 'Detect & Index' button.
109
+ """
110
+ if image_np is None:
111
+ return None, "❌ Please upload an image first.", _store_badge()
112
+
113
+ pil_img = Image.fromarray(image_np)
114
+ annotated, detections, summary = run_detection(pil_img)
115
+ embedding = embed_image(pil_img)
116
+
117
+ # Persist to vector store
118
+ faiss_index.add(embedding)
119
+ record = {
120
+ "id": len(image_store),
121
+ "image": pil_img.copy(),
122
+ "annotated": annotated,
123
+ "detections": detections,
124
+ "summary": summary,
125
+ "note": note.strip() or "β€”",
126
+ "ts": datetime.now().strftime("%H:%M:%S"),
127
+ }
128
+ image_store.append(record)
129
+
130
+ msg = f"βœ… Image #{record['id']} indexed Β· Found: {summary}"
131
+ return annotated, msg, _store_badge()
132
+
133
+
134
+ def _store_badge() -> str:
135
+ return f"οΏ½οΏ½οΏ½οΏ½ {len(image_store)} image(s) in vector store"
136
+
137
+
138
+ # ── RAG query pipeline ─────────────────────────────────────────────────────────
139
+
140
+ def query_images(text_query: str, top_k: int):
141
+ """
142
+ 1. Embed query text with CLIP.
143
+ 2. Cosine k-NN search in FAISS.
144
+ 3. Build RAG prompt from retrieved context.
145
+ 4. Generate answer with Zephyr-7B via HF Inference API.
146
+ """
147
+ if not image_store:
148
+ return None, "❌ No images indexed yet β€” go to 'Detect & Index' first.", []
149
+
150
+ query_emb = embed_text(text_query)
151
+ k = min(int(top_k), len(image_store))
152
+ scores, idxs = faiss_index.search(query_emb, k)
153
+
154
+ # Build retrieval results & context string for the LLM
155
+ hits = []
156
+ ctx_lines = []
157
+ for rank, (score, idx) in enumerate(zip(scores[0], idxs[0])):
158
+ if idx < 0:
159
+ continue
160
+ item = image_store[int(idx)]
161
+ hits.append({
162
+ "rank": rank + 1,
163
+ "img_id": int(idx),
164
+ "score": round(float(score), 4),
165
+ "objects": item["summary"],
166
+ "note": item["note"],
167
+ })
168
+ ctx_lines.append(
169
+ f"[Image #{idx}] objects: {item['summary']} | "
170
+ f"note: {item['note']} | indexed at: {item['ts']} | "
171
+ f"cosine similarity: {score:.3f}"
172
+ )
173
+
174
+ context = "\n".join(ctx_lines)
175
+
176
+ # Zephyr-7B chat prompt (ChatML format)
177
+ prompt = (
178
+ "<|system|>\n"
179
+ "You are a concise visual-AI assistant. "
180
+ "Answer the user's question using only the retrieved image context below. "
181
+ "If the context is insufficient, say so.\n"
182
+ "<|user|>\n"
183
+ f"Retrieved context:\n{context}\n\n"
184
+ f"Question: {text_query}\n"
185
+ "<|assistant|>\n"
186
+ )
187
+
188
+ try:
189
+ answer = llm.text_generation(
190
+ prompt,
191
+ max_new_tokens=300,
192
+ temperature=0.2,
193
+ repetition_penalty=1.1,
194
+ stop_sequences=["<|user|>", "<|system|>"],
195
+ ).strip()
196
+ except Exception as exc:
197
+ answer = (
198
+ f"⚠️ LLM call failed ({exc}).\n\n"
199
+ f"Raw retrieval context:\n{context}"
200
+ )
201
+
202
+ best_idx = int(idxs[0][0]) if len(idxs[0]) > 0 and idxs[0][0] >= 0 else None
203
+ best_image = image_store[best_idx]["annotated"] if best_idx is not None else None
204
+
205
+ return best_image, answer, hits
206
+
207
+
208
+ # ── Gradio UI ──────────────────────────────────────────────────────────────────
209
+
210
+ CUSTOM_CSS = """
211
+ .hero { text-align:center; padding:20px 0 4px; }
212
+ .hero h1 { font-size:28px; margin:0; }
213
+ .hero p { color:var(--color-subdued); margin:6px 0 0; }
214
+ .badge-row { display:flex; gap:8px; justify-content:center; flex-wrap:wrap; margin-top:10px; }
215
+ .badge {
216
+ background:var(--color-background-secondary);
217
+ border:1px solid var(--border-color-primary);
218
+ border-radius:20px;
219
+ padding:3px 12px;
220
+ font-size:12px;
221
+ color:var(--color-text-body);
222
+ }
223
+ """
224
+
225
+ with gr.Blocks(
226
+ title="VisualRAG – Multi-Modal AI System",
227
+ theme=gr.themes.Soft(primary_hue="blue"),
228
+ css=CUSTOM_CSS,
229
+ ) as demo:
230
+
231
+ # ── Header ─────────────────────────────────────────────────────────────────
232
+ gr.HTML("""
233
+ <div class="hero">
234
+ <h1>πŸ” VisualRAG</h1>
235
+ <p>Multi-Modal AI Β· Object Detection + Visual Embeddings + RAG + LLM</p>
236
+ <div class="badge-row">
237
+ <span class="badge">YOLOv8</span>
238
+ <span class="badge">CLIP ViT-B/32</span>
239
+ <span class="badge">FAISS</span>
240
+ <span class="badge">Zephyr-7B</span>
241
+ <span class="badge">Gradio</span>
242
+ </div>
243
+ </div>
244
+ """)
245
+
246
+ with gr.Tabs():
247
+
248
+ # ── TAB 1 : Detect & Index ─────────────────────────────────────────────
249
+ with gr.Tab("πŸ“€ Detect & Index"):
250
+ gr.Markdown(
251
+ "Upload any image. YOLOv8 will detect objects, then CLIP encodes it "
252
+ "into a 512-d embedding stored in FAISS for later retrieval."
253
+ )
254
+ with gr.Row():
255
+ with gr.Column(scale=1):
256
+ img_input = gr.Image(
257
+ label="Upload image",
258
+ type="numpy",
259
+ sources=["upload", "webcam"],
260
+ )
261
+ note_input = gr.Textbox(
262
+ label="Context note (optional)",
263
+ placeholder="e.g. 'Warehouse entrance camera, aisle 3'",
264
+ )
265
+ index_btn = gr.Button(
266
+ "πŸ” Detect & Index", variant="primary", size="lg"
267
+ )
268
+
269
+ with gr.Column(scale=1):
270
+ detection_output = gr.Image(label="Detection result (annotated)")
271
+ status_output = gr.Textbox(label="Status", interactive=False)
272
+ store_output = gr.Textbox(label="Vector store", interactive=False, value=_store_badge())
273
+
274
+ index_btn.click(
275
+ fn=index_image,
276
+ inputs=[img_input, note_input],
277
+ outputs=[detection_output, status_output, store_output],
278
+ )
279
+
280
+ # ── TAB 2 : Query (RAG) ────────────────────────────────────────────────
281
+ with gr.Tab("πŸ’¬ Query (RAG)"):
282
+ gr.Markdown(
283
+ "Type any question about your indexed images. "
284
+ "CLIP embeds the query, FAISS retrieves the most similar images, "
285
+ "and Zephyr-7B generates a grounded answer."
286
+ )
287
+ with gr.Row():
288
+ with gr.Column(scale=1):
289
+ query_input = gr.Textbox(
290
+ label="Your question",
291
+ placeholder="e.g. 'How many people are visible?' or 'Are there any vehicles?'",
292
+ lines=3,
293
+ )
294
+ topk_slider = gr.Slider(
295
+ minimum=1, maximum=5, value=3, step=1,
296
+ label="Top-K images to retrieve",
297
+ )
298
+ query_btn = gr.Button(
299
+ "πŸ”Ž Search & Generate Answer", variant="primary", size="lg"
300
+ )
301
+
302
+ with gr.Column(scale=1):
303
+ match_img = gr.Image(label="Best matching image")
304
+ llm_answer = gr.Textbox(
305
+ label="AI Answer (RAG-grounded)",
306
+ lines=6,
307
+ interactive=False,
308
+ )
309
+
310
+ hits_json = gr.JSON(label="Retrieval scores (all top-K)")
311
+
312
+ query_btn.click(
313
+ fn=query_images,
314
+ inputs=[query_input, topk_slider],
315
+ outputs=[match_img, llm_answer, hits_json],
316
+ )
317
+
318
+ # ── TAB 3 : How it works ───────────────────────────────────────────────
319
+ with gr.Tab("πŸ—οΈ How it works"):
320
+ gr.Markdown("""
321
+ ## System overview
322
+
323
+ ### Index pipeline
324
+ ```
325
+ Image β†’ YOLOv8 detection (objects + confidence)
326
+ β†’ CLIP ViT-B/32 image encoder (512-d embedding)
327
+ β†’ L2 normalisation
328
+ β†’ FAISS IndexFlatIP (cosine similarity store)
329
+ ```
330
+
331
+ ### Query / RAG pipeline
332
+ ```
333
+ Text query β†’ CLIP text encoder (512-d embedding)
334
+ β†’ L2 normalisation
335
+ β†’ FAISS k-NN search (cosine similarity, top-K)
336
+ β†’ Build RAG prompt: query + retrieved context
337
+ β†’ Zephyr-7B-Ξ² via HF Serverless Inference API
338
+ β†’ Natural language answer
339
+ ```
340
+
341
+ ## Stack
342
+
343
+ | Component | Technology | Why |
344
+ |---|---|---|
345
+ | Object detection | YOLOv8n (Ultralytics) | Fast CPU inference, production-proven |
346
+ | Visual embedding | CLIP ViT-B/32 | Joint image–text space enables textβ†’image search |
347
+ | Vector index | FAISS IndexFlatIP | Exact cosine k-NN, zero infra |
348
+ | LLM | Zephyr-7B-Ξ² (HF API) | Free serverless, good instruction following |
349
+ | UI | Gradio 4.x | Native HF Spaces support |
350
+
351
+ ## Skills demonstrated (mapped to job description)
352
+ - **Computer vision pipeline**: detect β†’ annotate β†’ embed β†’ index
353
+ - **RAG architecture**: embed β†’ retrieve β†’ generate
354
+ - **Multi-modal reasoning**: text queries over an image knowledge base
355
+ - **Vector database**: FAISS with cosine similarity (L2-normalised inner product)
356
+ - **LLM integration**: HF Inference API with prompt engineering
357
+ - **Production patterns**: stateless functions, error handling, model caching
358
+ """)
359
+
360
+ # ── Footer ─────────────────────────────────────────────────────────────────
361
+ gr.HTML("""
362
+ <div style="text-align:center; padding:16px 0 4px; color:var(--color-subdued); font-size:12px">
363
+ VisualRAG Β· YOLOv8 + CLIP + FAISS + LLM Β· Built for HuggingFace Spaces
364
+ </div>
365
+ """)
366
+
367
+ demo.launch()