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"""
"""
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"""
"""
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
)