FaceInsight_AI / app.py
vaisagan's picture
Upload app.py with huggingface_hub
4fa966a verified
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"""
<div style="background:#f8f9fa;border-radius:12px;padding:16px;text-align:center">
<div style="font-size:1.4rem;font-weight:700;color:#2c3e50">{title}</div>
<div style="font-size:0.85rem;color:#7f8c8d;margin-top:4px">{subtitle}</div>
</div>"""
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"""
<div style="background:#f8f9fa;border-radius:12px;padding:16px">
<div style="font-size:2.2rem;text-align:center;color:{color}">{icon} {gender}</div>
<div style="background:#e0e0e0;border-radius:99px;height:8px;margin-top:10px">
<div style="width:{bar}%;background:{color};height:8px;border-radius:99px"></div>
</div>
<div style="text-align:right;font-size:0.8rem;color:#7f8c8d;margin-top:4px">{conf:.1f}% confidence</div>
</div>"""
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"""
<div style="background:#f8f9fa;border-radius:12px;padding:16px;text-align:center">
<div style="font-size:2.6rem;font-weight:800;color:{color}">{age:.0f}</div>
<div style="font-size:0.9rem;color:#7f8c8d">years old · {label}</div>
</div>"""
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"""
<div style="margin-top:5px">
<div style="display:flex;align-items:center;gap:6px;font-size:0.78rem">
<span>{ic}</span>
<span style="width:70px;color:#555">{lbl}</span>
<div style="flex:1;background:#e0e0e0;border-radius:99px;height:6px">
<div style="width:{w}%;background:{co};height:6px;border-radius:99px"></div>
</div>
<span style="width:35px;text-align:right;color:#7f8c8d">{w}%</span>
</div>
</div>"""
return f"""
<div style="background:#f8f9fa;border-radius:12px;padding:16px">
<div style="font-size:1.8rem;text-align:center">{icon} <span style="color:{color};font-weight:700">{emotion}</span></div>
<div style="font-size:0.82rem;color:#7f8c8d;text-align:center;margin-bottom:8px">{conf:.1f}% confidence</div>
{bars}
</div>"""
# ── 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 = """
<div class="hero">
<h1>🧠 FaceInsight AI</h1>
<p>
Point your camera at a face — or upload a photo — and instantly see
<strong>gender</strong>, <strong>age</strong>, <strong>emotion</strong>,
and a preview of <strong>how the person will look at age&nbsp;70</strong>.
</p>
<span class="badge">✦ Real-time webcam</span>
<span class="badge">✦ Works on mobile & desktop</span>
<span class="badge">✦ No data stored</span>
</div>
"""
HOW_HTML = """
<div style="background:#eaf4ff;border-radius:12px;padding:14px 18px;font-size:0.86rem;color:#2c3e50;line-height:1.7">
<strong>How to use:</strong><br>
1️⃣ Click <em>Allow</em> when your browser asks for camera access.<br>
2️⃣ Align your face in the frame — good lighting helps!<br>
3️⃣ Press <strong>Analyze Face</strong> or upload a photo from your gallery.<br>
4️⃣ Results appear instantly on the right. The side panel shows a simulated portrait of you at 70.
</div>
"""
# ── 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("""
<div style="text-align:center;margin:20px 0 8px">
<span style="font-size:1.1rem;font-weight:700;color:#2c3e50">🕰️ Simulated portrait — age 70</span><br>
<span style="font-size:0.82rem;color:#95a5a6">This is an artistic simulation, not medical or forensic analysis.</span>
</div>""")
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("""
<div class="footer">
FaceInsight AI · Trained on UTKFace (White/Black = US · Indian) ·
Emotion: FER-2013 · Age-at-70 is an artistic effect only ·
No images are stored or transmitted.
</div>""")
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
)