ans123 commited on
Commit
1cc356f
Β·
verified Β·
1 Parent(s): 6e47969

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +339 -0
app.py ADDED
@@ -0,0 +1,339 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import zipfile
3
+ import torch
4
+ import clip
5
+ import numpy as np
6
+ from PIL import Image
7
+ import gradio as gr
8
+ import openai
9
+ from tqdm import tqdm
10
+ from glob import glob
11
+ import psycopg2
12
+ from psycopg2.extras import execute_values
13
+ import json
14
+ import time
15
+
16
+ # ─────────────────────────────────────────────
17
+ # πŸ“‚ STEP 1: UNZIP TO CORRECT STRUCTURE
18
+ # ─────────────────────────────────────────────
19
+ zip_name = "lfw-faces.zip"
20
+ unzip_dir = "lfw-faces"
21
+
22
+ if not os.path.exists(unzip_dir):
23
+ print("πŸ”“ Unzipping...")
24
+ with zipfile.ZipFile(zip_name, "r") as zip_ref:
25
+ zip_ref.extractall(unzip_dir)
26
+ print("βœ… Unzipped into:", unzip_dir)
27
+
28
+ # True image root after unzip
29
+ img_root = os.path.join(unzip_dir, "lfw-deepfunneled")
30
+
31
+ # ─────────────────────────────────────────────
32
+ # πŸ—„οΈ STEP 2: DATABASE SETUP
33
+ # ─────────────────────────────────────────────
34
+ def setup_database():
35
+ """Setup PostgreSQL with pgvector extension"""
36
+ # Database configuration
37
+ DB_CONFIG = {
38
+ "dbname": "face_matcher",
39
+ "user": "postgres",
40
+ "password": "postgres", # Change this to your actual password
41
+ "host": "localhost",
42
+ "port": "5432"
43
+ }
44
+
45
+ try:
46
+ # Connect to PostgreSQL server to create database if it doesn't exist
47
+ conn = psycopg2.connect(
48
+ dbname="postgres",
49
+ user=DB_CONFIG["user"],
50
+ password=DB_CONFIG["password"],
51
+ host=DB_CONFIG["host"]
52
+ )
53
+ conn.autocommit = True
54
+ cur = conn.cursor()
55
+
56
+ # Create database if it doesn't exist
57
+ cur.execute(f"SELECT 1 FROM pg_catalog.pg_database WHERE datname = '{DB_CONFIG['dbname']}'")
58
+ exists = cur.fetchone()
59
+ if not exists:
60
+ cur.execute(f"CREATE DATABASE {DB_CONFIG['dbname']}")
61
+ print(f"Database {DB_CONFIG['dbname']} created.")
62
+
63
+ cur.close()
64
+ conn.close()
65
+
66
+ # Connect to the face_matcher database
67
+ conn = psycopg2.connect(**DB_CONFIG)
68
+ conn.autocommit = True
69
+ cur = conn.cursor()
70
+
71
+ # Create pgvector extension if it doesn't exist
72
+ cur.execute("CREATE EXTENSION IF NOT EXISTS vector")
73
+
74
+ # Create faces table if it doesn't exist
75
+ cur.execute("""
76
+ CREATE TABLE IF NOT EXISTS faces (
77
+ id SERIAL PRIMARY KEY,
78
+ path TEXT UNIQUE NOT NULL,
79
+ name TEXT NOT NULL,
80
+ embedding vector(512),
81
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
82
+ )
83
+ """)
84
+
85
+ # Create index on the embedding column
86
+ cur.execute("CREATE INDEX IF NOT EXISTS faces_embedding_idx ON faces USING ivfflat (embedding vector_ip_ops)")
87
+
88
+ print("βœ… Database setup complete.")
89
+ return conn
90
+ except Exception as e:
91
+ print(f"❌ Database setup failed: {e}")
92
+ return None
93
+
94
+ # ─────────────────────────────────────────────
95
+ # 🧠 STEP 3: LOAD CLIP MODEL
96
+ # ─────────────────────────────────────────────
97
+ device = "cuda" if torch.cuda.is_available() else "cpu"
98
+ model, preprocess = clip.load("ViT-B/32", device=device)
99
+ print(f"βœ… CLIP model loaded on {device}")
100
+
101
+ # ─────────────────────────────────────────────
102
+ # πŸ“Š STEP 4: EMBEDDING FUNCTIONS
103
+ # ─────────────────────────────────────────────
104
+ def embed_image(image_path):
105
+ """Generate CLIP embedding for a single image"""
106
+ try:
107
+ img = Image.open(image_path).convert("RGB")
108
+ img_input = preprocess(img).unsqueeze(0).to(device)
109
+ with torch.no_grad():
110
+ emb = model.encode_image(img_input).cpu().numpy().flatten()
111
+ emb /= np.linalg.norm(emb)
112
+ return emb
113
+ except Exception as e:
114
+ print(f"⚠️ Error embedding {image_path}: {e}")
115
+ return None
116
+
117
+ def populate_database(conn, limit=500):
118
+ """Populate database with images and their embeddings"""
119
+ # Collect all .jpg files inside subfolders
120
+ all_images = sorted(glob(os.path.join(img_root, "*", "*.jpg")))
121
+ selected_images = all_images[:limit]
122
+
123
+ if len(selected_images) == 0:
124
+ raise RuntimeError("❌ No image files found in unzipped structure!")
125
+
126
+ cur = conn.cursor()
127
+
128
+ # Check which images are already in the database
129
+ cur.execute("SELECT path FROM faces")
130
+ existing_paths = set(path[0] for path in cur.fetchall())
131
+
132
+ # Filter out images that are already in the database
133
+ new_images = [path for path in selected_images if path not in existing_paths]
134
+
135
+ if not new_images:
136
+ print("βœ… All images are already in the database.")
137
+ return
138
+
139
+ print(f"🧠 Generating CLIP embeddings for {len(new_images)} new images...")
140
+
141
+ # Process images in batches to avoid memory issues
142
+ batch_size = 50
143
+ for i in range(0, len(new_images), batch_size):
144
+ batch = new_images[i:i+batch_size]
145
+ data_to_insert = []
146
+
147
+ for fpath in tqdm(batch, desc=f"Embedding batch {i//batch_size + 1}"):
148
+ try:
149
+ emb = embed_image(fpath)
150
+ if emb is not None:
151
+ name = os.path.splitext(os.path.basename(fpath))[0].replace("_", " ")
152
+ data_to_insert.append((fpath, name, emb.tolist()))
153
+ except Exception as e:
154
+ print(f"⚠️ Error with {fpath}: {e}")
155
+
156
+ # Insert batch into database
157
+ if data_to_insert:
158
+ execute_values(
159
+ cur,
160
+ "INSERT INTO faces (path, name, embedding) VALUES %s ON CONFLICT (path) DO NOTHING",
161
+ [(d[0], d[1], d[2]) for d in data_to_insert],
162
+ template="(%s, %s, %s::vector)"
163
+ )
164
+ conn.commit()
165
+
166
+ # Count total faces in database
167
+ cur.execute("SELECT COUNT(*) FROM faces")
168
+ total_faces = cur.fetchone()[0]
169
+ print(f"βœ… Database now contains {total_faces} faces.")
170
+
171
+ # ─────────────────────────────────────────────
172
+ # πŸ” STEP 5: LOAD OPENAI API KEY
173
+ # ─────────────────────────────────────────────
174
+ openai.api_key = os.getenv("OPENAI_API_KEY")
175
+
176
+ # ─────────────────────────────────────────────
177
+ # πŸ” STEP 6: FACE MATCHING FUNCTION
178
+ # ─────────────────────────────────────────────
179
+ def scan_face(user_image, conn):
180
+ """Scan a face image and find matches in the database"""
181
+ if user_image is None:
182
+ return [], "", "", "Please upload a face image."
183
+
184
+ try:
185
+ user_image = user_image.convert("RGB")
186
+ tensor = preprocess(user_image).unsqueeze(0).to(device)
187
+ with torch.no_grad():
188
+ query_emb = model.encode_image(tensor).cpu().numpy().flatten()
189
+ query_emb /= np.linalg.norm(query_emb)
190
+ except Exception as e:
191
+ return [], "", "", f"Image preprocessing failed: {e}"
192
+
193
+ # Query database for similar faces
194
+ cur = conn.cursor()
195
+ emb_list = query_emb.tolist()
196
+ cur.execute("""
197
+ SELECT path, name, embedding <-> %s::vector AS distance
198
+ FROM faces
199
+ ORDER BY distance
200
+ LIMIT 5
201
+ """, (emb_list,))
202
+
203
+ results = cur.fetchall()
204
+
205
+ gallery, captions, names = [], [], []
206
+ scores = []
207
+
208
+ for path, name, distance in results:
209
+ try:
210
+ # Convert distance to similarity score (1 - distance)
211
+ similarity = 1 - distance
212
+ scores.append(similarity)
213
+
214
+ img = Image.open(path)
215
+ gallery.append(img)
216
+ captions.append(f"{name} (Score: {similarity:.2f})")
217
+ names.append(name)
218
+ except Exception as e:
219
+ captions.append(f"⚠️ Error loading match image: {e}")
220
+
221
+ risk_score = min(100, int(np.mean(scores) * 100)) if scores else 0
222
+
223
+ # 🧠 GPT-4 EXPLANATION
224
+ try:
225
+ prompt = (
226
+ f"The uploaded face matches closely with: {', '.join(names)}. "
227
+ f"Based on this, should the user be suspicious? Analyze like a funny but smart AI dating detective."
228
+ )
229
+ response = openai.chat.completions.create(
230
+ model="gpt-4",
231
+ messages=[
232
+ {"role": "system", "content": "You're a playful but intelligent AI face-matching analyst."},
233
+ {"role": "user", "content": prompt}
234
+ ]
235
+ )
236
+ explanation = response.choices[0].message.content
237
+ except Exception as e:
238
+ explanation = f"(OpenAI error): {e}"
239
+
240
+ return gallery, "\n".join(captions), f"{risk_score}/100", explanation
241
+
242
+ # ─────────────────────────────────────────────
243
+ # 🌱 STEP 7: ADD NEW FACE FUNCTION
244
+ # ─────────────────────────────────────────────
245
+ def add_new_face(image, name, conn):
246
+ """Add a new face to the database"""
247
+ if image is None or not name:
248
+ return "Please provide both an image and a name."
249
+
250
+ try:
251
+ # Save image to a temporary file
252
+ timestamp = int(time.time())
253
+ os.makedirs("uploaded_faces", exist_ok=True)
254
+ path = f"uploaded_faces/{name.replace(' ', '_')}_{timestamp}.jpg"
255
+ image.save(path)
256
+
257
+ # Generate embedding
258
+ emb = embed_image(path)
259
+ if emb is None:
260
+ return "Failed to generate embedding for the image."
261
+
262
+ # Add to database
263
+ cur = conn.cursor()
264
+ cur.execute(
265
+ "INSERT INTO faces (path, name, embedding) VALUES (%s, %s, %s::vector)",
266
+ (path, name, emb.tolist())
267
+ )
268
+ conn.commit()
269
+
270
+ return f"βœ… Added {name} to the database successfully!"
271
+ except Exception as e:
272
+ return f"❌ Failed to add face: {e}"
273
+
274
+ # ─────────────────────────────────────────────
275
+ # πŸŽ›οΈ STEP 8: GRADIO UI
276
+ # ─────────────────────────────────────────────
277
+ def create_ui():
278
+ """Create Gradio UI with both scan and add functionality"""
279
+ # Setup database connection
280
+ conn = setup_database()
281
+ if conn is None:
282
+ raise RuntimeError("❌ Database connection failed. Please check your PostgreSQL installation and pgvector extension.")
283
+
284
+ # Populate database with initial images
285
+ populate_database(conn)
286
+
287
+ # Wrapper functions for Gradio that use the database connection
288
+ def scan_face_wrapper(image):
289
+ return scan_face(image, conn)
290
+
291
+ def add_face_wrapper(image, name):
292
+ return add_new_face(image, name, conn)
293
+
294
+ with gr.Blocks(title="Tinder Scanner – Real Face Match Detector") as demo:
295
+ gr.Markdown("# Tinder Scanner – Real Face Match Detector")
296
+ gr.Markdown("Scan a face image to find visual matches using CLIP and PostgreSQL, and get a cheeky GPT-4 analysis.")
297
+
298
+ with gr.Tab("Scan Face"):
299
+ with gr.Row():
300
+ with gr.Column():
301
+ input_image = gr.Image(type="pil", label="Upload a Face Image")
302
+ scan_button = gr.Button("πŸ” Scan Face")
303
+
304
+ with gr.Column():
305
+ gallery = gr.Gallery(label="πŸ” Top Matches", columns=[5], height="auto")
306
+ captions = gr.Textbox(label="Match Names + Similarity Scores")
307
+ risk_score = gr.Textbox(label="🚨 Cheating Risk Score")
308
+ explanation = gr.Textbox(label="🧠 GPT-4 Explanation", lines=5)
309
+
310
+ scan_button.click(
311
+ fn=scan_face_wrapper,
312
+ inputs=[input_image],
313
+ outputs=[gallery, captions, risk_score, explanation]
314
+ )
315
+
316
+ with gr.Tab("Add New Face"):
317
+ with gr.Row():
318
+ with gr.Column():
319
+ new_image = gr.Image(type="pil", label="Upload New Face Image")
320
+ new_name = gr.Textbox(label="Person's Name")
321
+ add_button = gr.Button("βž• Add to Database")
322
+
323
+ with gr.Column():
324
+ result = gr.Textbox(label="Result")
325
+
326
+ add_button.click(
327
+ fn=add_face_wrapper,
328
+ inputs=[new_image, new_name],
329
+ outputs=result
330
+ )
331
+
332
+ return demo
333
+
334
+ # ─────────────────────────────────────────────
335
+ # πŸš€ MAIN EXECUTION
336
+ # ─────────────────────────────────────────────
337
+ if __name__ == "__main__":
338
+ demo = create_ui()
339
+ demo.launch()