from __future__ import annotations # Block TensorFlow before any other import (crashes on machines without AVX/Rosetta) import os os.environ.setdefault("USE_TF", "0") os.environ.setdefault("USE_JAX", "0") os.environ.setdefault("USE_TORCH", "1") os.environ.setdefault("TRANSFORMERS_NO_TF", "1") os.environ.setdefault("TRANSFORMERS_NO_JAX", "1") import sys from pathlib import Path import cv2 import gradio as gr import numpy as np # ── model path ───────────────────────────────────────────────────────────── MODEL_PATH = os.environ.get( "MODEL_PATH", str(Path(__file__).parent / "models" / "face_model_best.pth"), ) MODEL_AVAILABLE = Path(MODEL_PATH).exists() sys.path.insert(0, str(Path(__file__).parent)) # Lazy-load predictor only when model is available _predictor = None def get_predictor(): global _predictor if _predictor is None and MODEL_AVAILABLE: from src.inference.predictor import Predictor _predictor = Predictor(model_path=MODEL_PATH) return _predictor # ── emotion-only fallback (no trained model needed) ─────────────────────── _emotion_detector = None _face_detector = None def get_emotion_detector(): global _emotion_detector if _emotion_detector is None: from src.inference.emotion_detector import EmotionDetector _emotion_detector = EmotionDetector() return _emotion_detector def get_face_detector(): global _face_detector if _face_detector is None: from src.inference.face_detector import FaceDetector _face_detector = FaceDetector(confidence_threshold=0.6) return _face_detector # ── inference ────────────────────────────────────────────────────────────── def analyze(image: np.ndarray): """ Entry point called by Gradio. Returns: (annotated_image, gender_html, age_html, emotion_html, aged_image) """ try: return _analyze_inner(image) except Exception as exc: import traceback traceback.print_exc() err = _card(f"Error: {exc}", "Check Space logs for details") blank = _blank("Error") return blank, err, err, err, blank def _analyze_inner(image: np.ndarray): if image is None: empty = _blank("No image received") return empty, "–", "–", "–", empty # Ensure RGB if image.ndim == 2: image = cv2.cvtColor(image, cv2.COLOR_GRAY2RGB) elif image.shape[2] == 4: image = cv2.cvtColor(image, cv2.COLOR_RGBA2RGB) predictor = get_predictor() if predictor is not None: # Full pipeline results = predictor.predict_image(image) annotated = predictor.annotate(image) if not results: return annotated, _card("–", "No face detected"), "–", "–", _blank("No face detected") r = results[0] # use first detected face gender_html = _gender_card(r["gender"], r["gender_conf"]) age_html = _age_card(r["age"]) emotion_html = _emotion_card(r["emotion"], r["emotion_conf"], r["emotion_probs"]) aged_img = r["aged_face"] else: # Fallback: face detect + emotion only (no trained weights) bgr = cv2.cvtColor(image, cv2.COLOR_RGB2BGR) fd = get_face_detector() crops, boxes = fd.crop_faces(bgr) annotated = image.copy() if not crops: empty = _blank("No face detected") return annotated, _card("–", "No face detected"), "–", "–", empty for x1, y1, x2, y2 in boxes: cv2.rectangle(annotated, (x1, y1), (x2, y2), (52, 152, 219), 2) em = get_emotion_detector() emotion, conf = em.top_emotion(crops[0]) probs = em.predict(crops[0]) gender_html = _card("⚠️ Model not trained yet", "Upload weights to models/") age_html = _card("⚠️ Model not trained yet", "Upload weights to models/") emotion_html = _emotion_card(emotion, conf * 100, probs) from src.inference.age_progression import age_to_70 aged_img = age_to_70(crops[0], current_age=30) return annotated, gender_html, age_html, emotion_html, aged_img # ── HTML helpers ─────────────────────────────────────────────────────────── def _blank(msg: str) -> np.ndarray: canvas = np.ones((200, 300, 3), dtype=np.uint8) * 240 cv2.putText(canvas, msg, (20, 110), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (120, 120, 120), 1) return canvas def _card(title: str, subtitle: str = "") -> str: return f"""
{title}
{subtitle}
""" def _gender_card(gender: str, conf: float) -> str: icon = "♂" if gender == "Male" else "♀" color = "#3498db" if gender == "Male" else "#e74c3c" bar = int(conf) return f"""
{icon} {gender}
{conf:.1f}% confidence
""" def _age_card(age: float) -> str: if age < 18: label, color = "Child / Teen", "#27ae60" elif age < 35: label, color = "Young Adult", "#2ecc71" elif age < 55: label, color = "Middle-aged", "#f39c12" else: label, color = "Senior", "#e74c3c" return f"""
{age:.0f}
years old · {label}
""" def _emotion_card(emotion: str, conf: float, probs: dict) -> str: ICONS = { "Happy": ("😊", "#f1c40f"), "Sad": ("😢", "#3498db"), "Angry": ("😠", "#e74c3c"), "Fear": ("😨", "#9b59b6"), "Surprise": ("😮", "#e67e22"), "Disgust": ("🤢", "#27ae60"), "Neutral": ("😐", "#95a5a6"), } icon, color = ICONS.get(emotion, ("🙂", "#95a5a6")) bars = "" for lbl, prob in sorted(probs.items(), key=lambda x: -x[1]): w = int(prob * 100) ic, co = ICONS.get(lbl, ("", "#bbb")) bars += f"""
{ic} {lbl}
{w}%
""" return f"""
{icon} {emotion}
{conf:.1f}% confidence
{bars}
""" # ── CSS ──────────────────────────────────────────────────────────────────── CSS = """ /* ---------- globals ---------- */ body, .gradio-container { font-family: 'Inter', system-ui, sans-serif !important; } .gradio-container { max-width: 1100px !important; margin: 0 auto !important; } /* ---------- hero ---------- */ .hero { background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%); border-radius: 20px; padding: 40px 30px 30px; margin-bottom: 24px; text-align: center; color: white; } .hero h1 { font-size: clamp(2rem, 5vw, 3.2rem); font-weight: 800; letter-spacing: -1px; margin: 0 0 10px; background: linear-gradient(90deg, #00d2ff, #a8edea); -webkit-background-clip: text; -webkit-text-fill-color: transparent; } .hero p { font-size: clamp(0.9rem, 2.5vw, 1.05rem); color: #a8c0d6; max-width: 620px; margin: 0 auto 18px; line-height: 1.6; } .badge { display: inline-block; background: rgba(255,255,255,0.12); border: 1px solid rgba(255,255,255,0.2); border-radius: 99px; padding: 4px 14px; font-size: 0.78rem; color: #a8edea; margin: 0 4px; } /* ---------- panels ---------- */ .panel { background: white; border-radius: 16px; padding: 20px; box-shadow: 0 2px 12px rgba(0,0,0,0.07); } .section-label { font-size: 0.72rem; font-weight: 700; letter-spacing: 1.2px; text-transform: uppercase; color: #95a5a6; margin-bottom: 8px; } /* ---------- webcam button ---------- */ .analyze-btn { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important; color: white !important; font-weight: 700 !important; font-size: 1rem !important; border-radius: 12px !important; padding: 14px !important; border: none !important; width: 100% !important; cursor: pointer !important; transition: opacity 0.2s !important; } .analyze-btn:hover { opacity: 0.88 !important; } /* ---------- aged-face label ---------- */ .aged-label { font-size: 0.82rem; color: #7f8c8d; text-align: center; margin-top: 6px; } /* ---------- footer ---------- */ .footer { text-align: center; font-size: 0.78rem; color: #bbb; margin-top: 16px; padding-bottom: 10px; } /* ---------- responsive ---------- */ @media (max-width: 700px) { .hero { padding: 28px 16px 20px; } .gradio-container { padding: 8px !important; } } """ HERO_HTML = """

🧠 FaceInsight AI

Point your camera at a face — or upload a photo — and instantly see gender, age, emotion, and a preview of how the person will look at age 70.

✦ Real-time webcam ✦ Works on mobile & desktop ✦ No data stored
""" HOW_HTML = """
How to use:
1️⃣ Click Allow when your browser asks for camera access.
2️⃣ Align your face in the frame — good lighting helps!
3️⃣ Press Analyze Face or upload a photo from your gallery.
4️⃣ Results appear instantly on the right. The side panel shows a simulated portrait of you at 70.
""" # ── build Gradio app ─────────────────────────────────────────────────────── def build_app() -> gr.Blocks: with gr.Blocks(title="FaceInsight_AI") as demo: gr.HTML(HERO_HTML) with gr.Row(equal_height=False): # ── LEFT COLUMN ─────────────────────────────────────────────── with gr.Column(scale=5, min_width=280): gr.HTML(HOW_HTML) image_input = gr.Image( sources = ["webcam", "upload"], type = "numpy", label = "📷 Camera / Upload", height = 360, show_label = True, ) analyze_btn = gr.Button( "🔍 Analyze Face", elem_classes = ["analyze-btn"], variant = "primary", ) # ── RIGHT COLUMN ────────────────────────────────────────────── with gr.Column(scale=7, min_width=320): annotated_out = gr.Image( label = "🎯 Detected faces", height = 320, ) with gr.Row(): gender_out = gr.HTML(label="Gender") age_out = gr.HTML(label="Age") emotion_out = gr.HTML(label="Emotion") # ── AGED FACE (full width below) ────────────────────────────────── with gr.Row(): with gr.Column(scale=1): gr.HTML("""
🕰️ Simulated portrait — age 70
This is an artistic simulation, not medical or forensic analysis.
""") aged_out = gr.Image( label = "You at 70", height = 300, ) # ── EXAMPLES ────────────────────────────────────────────────────── gr.Examples( examples = [["examples/sample1.jpg"]], inputs = [image_input], outputs = [annotated_out, gender_out, age_out, emotion_out, aged_out], fn = analyze, cache_examples = False, label = "Try a sample image", ) if Path("examples/sample1.jpg").exists() else None # ── wire up ─────────────────────────────────────────────────────── analyze_btn.click( fn = analyze, inputs = [image_input], outputs = [annotated_out, gender_out, age_out, emotion_out, aged_out], ) # Run on webcam changes only when image is not None image_input.change( fn = lambda img: analyze(img) if img is not None else (None,)*5, inputs = [image_input], outputs = [annotated_out, gender_out, age_out, emotion_out, aged_out], ) gr.HTML(""" """) return demo if __name__ == "__main__": app = build_app() app.launch( server_name = "0.0.0.0", server_port = int(os.environ.get("PORT", 7860)), css = CSS, share = True, # creates a public gradio.live tunnel URL )