Spaces:
Sleeping
Sleeping
import os | |
import json | |
import torch | |
import clip | |
import faiss | |
import numpy as np | |
from PIL import Image | |
import gradio as gr | |
import openai | |
import requests | |
from tqdm import tqdm | |
from io import BytesIO | |
# βββββββββββββββββββββββββββββββββββββββββββββ | |
# π§ STEP 1: LOAD CLIP MODEL | |
# βββββββββββββββββββββββββββββββββββββββββββββ | |
device = "cuda" if torch.cuda.is_available() else "cpu" | |
model, preprocess = clip.load("ViT-B/32", device=device) | |
# βββββββββββββββββββββββββββββββββββββββββββββ | |
# π¦ STEP 2: LOAD PROFILE DATA FROM JSON | |
# βββββββββββββββββββββββββββββββββββββββββββββ | |
def load_profile_data(json_file_path=None, json_data=None): | |
"""Load profile data either from a file or directly from JSON data""" | |
if json_file_path and os.path.exists(json_file_path): | |
with open(json_file_path, 'r') as f: | |
profiles = json.load(f) | |
elif json_data: | |
profiles = json_data | |
else: | |
# Sample data structure as fallback | |
profiles = [ | |
{ | |
"Id": "sample-id", | |
"Name": "Sample Profile", | |
"Age": 25, | |
"Bio": "Sample bio", | |
"Photos": [ | |
"https://example.com/sample.jpg" | |
] | |
} | |
] | |
return profiles | |
# βββββββββββββββββββββββββββββββββββββββββββββ | |
# πΌοΈ STEP 3: DOWNLOAD AND PROCESS IMAGES | |
# βββββββββββββββββββββββββββββββββββββββββββββ | |
def download_and_process_image(url): | |
"""Download image from URL and return PIL Image""" | |
try: | |
response = requests.get(url, timeout=10) | |
response.raise_for_status() | |
img = Image.open(BytesIO(response.content)).convert("RGB") | |
return img | |
except Exception as e: | |
print(f"β οΈ Error downloading image from {url}: {e}") | |
return None | |
def generate_embeddings(profiles, max_images=500): | |
"""Generate CLIP embeddings for profile images""" | |
embeddings = [] | |
image_urls = [] | |
profile_info = [] # Store name, age, etc. for each image | |
image_count = 0 | |
print(f"π§ Generating CLIP embeddings for profile images...") | |
for profile in tqdm(profiles, desc="Processing profiles"): | |
name = profile.get("Name", "Unknown") | |
age = profile.get("Age", "?") | |
for photo_url in profile.get("Photos", []): | |
if image_count >= max_images: | |
break | |
try: | |
img = download_and_process_image(photo_url) | |
if img is None: | |
continue | |
img_input = preprocess(img).unsqueeze(0).to(device) | |
with torch.no_grad(): | |
emb = model.encode_image(img_input).cpu().numpy().flatten() | |
emb /= np.linalg.norm(emb) | |
embeddings.append(emb) | |
image_urls.append(photo_url) | |
profile_info.append({ | |
"Name": name, | |
"Age": age, | |
"Id": profile.get("Id", "Unknown"), | |
"Bio": profile.get("Bio", "") | |
}) | |
image_count += 1 | |
except Exception as e: | |
print(f"β οΈ Error with {photo_url}: {e}") | |
if image_count >= max_images: | |
break | |
if embeddings: | |
embeddings = np.vstack(embeddings).astype("float32") | |
else: | |
embeddings = np.array([]).astype("float32") | |
print(f"β Finished embedding {len(embeddings)} images.") | |
return embeddings, image_urls, profile_info | |
# βββββββββββββββββββββββββββββββββββββββββββββ | |
# β‘ STEP 4: BUILD FAISS INDEX | |
# βββββββββββββββββββββββββββββββββββββββββββββ | |
def build_faiss_index(embeddings): | |
"""Build FAISS index from embeddings""" | |
if len(embeddings) == 0: | |
return None | |
dimension = embeddings.shape[1] | |
index = faiss.IndexFlatIP(dimension) | |
index.add(embeddings) | |
return index | |
# βββββββββββββββββββββββββββββββββββββββββββββ | |
# π STEP 5: LOAD OPENAI API KEY | |
# βββββββββββββββββββββββββββββββββββββββββββββ | |
def init_openai(): | |
openai.api_key = os.getenv("OPENAI_API_KEY") | |
if not openai.api_key: | |
print("β οΈ Warning: OPENAI_API_KEY not found. GPT-4 analysis will not be available.") | |
# βββββββββββββββββββββββββββββββββββββββββββββ | |
# π STEP 6: SEARCH FUNCTIONALITY | |
# βββββββββββββββββββββββββββββββββββββββββββββ | |
def search_similar_faces(user_image, index, image_urls, profile_info, top_k=5): | |
"""Search for similar faces using CLIP + FAISS""" | |
if index is None: | |
return [], [], 0, "No index available. Please load profile data first." | |
try: | |
user_image = user_image.convert("RGB") | |
tensor = preprocess(user_image).unsqueeze(0).to(device) | |
with torch.no_grad(): | |
query_emb = model.encode_image(tensor).cpu().numpy().astype("float32") | |
query_emb /= np.linalg.norm(query_emb) | |
except Exception as e: | |
return [], [], 0, f"Image preprocessing failed: {e}" | |
scores, indices = index.search(query_emb, top_k) | |
scores, indices = scores.flatten(), indices.flatten() | |
matching_images = [] | |
match_details = [] | |
for i in range(len(indices)): | |
idx = indices[i] | |
score = scores[i] | |
try: | |
url = image_urls[idx] | |
info = profile_info[idx] | |
img = download_and_process_image(url) | |
if img: | |
matching_images.append(img) | |
match_details.append({ | |
"url": url, | |
"score": score, | |
"info": info | |
}) | |
except Exception as e: | |
print(f"β οΈ Error processing match at index {idx}: {e}") | |
risk_score = min(100, int(np.mean(scores) * 100)) if scores.size > 0 else 0 | |
return matching_images, match_details, risk_score | |
# βββββββββββββββββββββββββββββββββββββββββββββ | |
# π§ STEP 7: GPT-4 ANALYSIS | |
# βββββββββββββββββββββββββββββββββββββββββββββ | |
def generate_gpt4_analysis(match_details): | |
"""Generate fun analysis using GPT-4""" | |
if not openai.api_key: | |
return "GPT-4 analysis not available (API key not configured)" | |
if not match_details: | |
return "No matches found for analysis" | |
try: | |
names = [f"{d['info']['Name']} ({d['info']['Age']})" for d in match_details] | |
prompt = ( | |
f"The uploaded face matches closely with: {', '.join(names)}. " | |
f"Based on this, should the user be suspicious? " | |
f"Analyze like a funny but smart AI dating detective. Keep it concise." | |
) | |
response = openai.chat.completions.create( | |
model="gpt-4", | |
messages=[ | |
{"role": "system", "content": "You're a playful but intelligent AI face-matching analyst."}, | |
{"role": "user", "content": prompt} | |
] | |
) | |
return response.choices[0].message.content | |
except Exception as e: | |
return f"(OpenAI error): {e}" | |
# βββββββββββββββββββββββββββββββββββββββββββββ | |
# ποΈ STEP 8: APPLICATION CLASS | |
# βββββββββββββββββββββββββββββββββββββββββββββ | |
class TinderScanner: | |
def __init__(self): | |
self.index = None | |
self.image_urls = [] | |
self.profile_info = [] | |
self.profiles = [] | |
# Initialize OpenAI | |
init_openai() | |
def load_data(self, json_text=None, json_file=None): | |
"""Load profile data and build index""" | |
try: | |
if json_text: | |
json_data = json.loads(json_text) | |
self.profiles = load_profile_data(json_data=json_data) | |
elif json_file: | |
self.profiles = load_profile_data(json_file_path=json_file) | |
else: | |
return "Please provide either JSON text or a JSON file" | |
embeddings, self.image_urls, self.profile_info = generate_embeddings(self.profiles) | |
if len(embeddings) > 0: | |
self.index = build_faiss_index(embeddings) | |
return f"β Successfully loaded {len(self.profiles)} profiles with {len(self.image_urls)} photos" | |
else: | |
return "β οΈ No valid images found in the provided data" | |
except Exception as e: | |
return f"β Error loading data: {e}" | |
def scan_face(self, user_image, json_input=None): | |
"""Process a user image and find matches""" | |
# Load data if provided and not already loaded | |
if json_input and not self.index: | |
load_result = self.load_data(json_text=json_input) | |
if "Successfully" not in load_result: | |
return [], "", "", load_result | |
if not self.index: | |
return [], "", "", "Please load profile data first by providing JSON input" | |
if user_image is None: | |
return [], "", "", "Please upload a face image" | |
images, match_details, risk_score = search_similar_faces( | |
user_image, self.index, self.image_urls, self.profile_info | |
) | |
# Format match captions | |
captions = [] | |
for detail in match_details: | |
info = detail["info"] | |
captions.append(f"{info['Name']} ({info['Age']}) - Score: {detail['score']:.2f}") | |
# Generate GPT-4 analysis | |
explanation = generate_gpt4_analysis(match_details) | |
return images, "\n".join(captions), f"{risk_score}/100", explanation | |
# βββββββββββββββββββββββββββββββββββββββββββββ | |
# π₯οΈ STEP 9: GRADIO UI | |
# βββββββββββββββββββββββββββββββββββββββββββββ | |
def create_ui(): | |
scanner = TinderScanner() | |
with gr.Blocks(title="Enhanced Tinder Scanner") as demo: | |
gr.Markdown("# π Tinder Scanner Pro β Face Match Detector") | |
gr.Markdown("Scan a face image to find visual matches in Tinder profiles and get a cheeky GPT-4 analysis.") | |
with gr.Tabs(): | |
with gr.TabItem("Setup Data"): | |
with gr.Row(): | |
with gr.Column(): | |
json_input = gr.Textbox( | |
label="JSON Profile Data", | |
placeholder='Paste JSON data here. Format: [{"Id": "...", "Name": "...", "Age": 25, "Photos": ["url1", "url2"]}]', | |
lines=10 | |
) | |
load_btn = gr.Button("Load Profile Data", variant="primary") | |
data_status = gr.Textbox(label="Status") | |
load_btn.click( | |
fn=scanner.load_data, | |
inputs=[json_input], | |
outputs=[data_status] | |
) | |
with gr.TabItem("Scan Face"): | |
with gr.Row(): | |
with gr.Column(): | |
user_image = gr.Image(type="pil", label="Upload a Face Image") | |
scan_btn = gr.Button("Scan Face", variant="primary") | |
with gr.Column(): | |
matches_gallery = gr.Gallery(label="π Top Matches", columns=[5], height="auto") | |
match_details = gr.Textbox(label="Match Details") | |
risk_score = gr.Textbox(label="π¨ Similarity Score") | |
gpt_analysis = gr.Textbox(label="π§ GPT-4 Analysis") | |
scan_btn.click( | |
fn=scanner.scan_face, | |
inputs=[user_image, json_input], | |
outputs=[matches_gallery, match_details, risk_score, gpt_analysis] | |
) | |
return demo | |
# βββββββββββββββββββββββββββββββββββββββββββββ | |
# π STEP 10: MAIN EXECUTION | |
# βββββββββββββββββββββββββββββββββββββββββββββ | |
if __name__ == "__main__": | |
demo = create_ui() | |
demo.launch() |