# main.py — AI Interview Confidence Analyzer (Gradio / HuggingFace Spaces) import cv2 import sys import os import time import numpy as np from collections import deque import gradio as gr import tempfile sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "modules")) from modules.face_landmarks import FaceLandmarkExtractor from modules.expression_detection import ExpressionDetector from modules.eye_contact import EyeContactDetector from modules.head_pose import HeadPoseEstimator from modules.llm import ( generate_questions, evaluate_answer, generate_final_summary, ) from modules.resume_parser import resume_to_profile, get_resume_context_for_llm # ── Weights ────────────────────────────────────────────────────────────────── WEIGHTS = { "eye_contact": 30, "expression": 25, "head_stability": 25, "nervousness": 20, } SCORE_HISTORY_LEN = 45 # ── Helpers ─────────────────────────────────────────────────────────────────── def compute_confidence_score(expr_result, eye_result, head_result): eye_score = eye_result.get("score", 0) expr_score = expr_result.get("expression_score", 0) head_score = head_result.get("stability_score", 0) nerv_score = expr_result.get("nervousness_score", 0) nerv_contribution = max(0, 100 - nerv_score) final = ( eye_score * WEIGHTS["eye_contact"] / 100 + expr_score * WEIGHTS["expression"] / 100 + head_score * WEIGHTS["head_stability"] / 100 + nerv_contribution * WEIGHTS["nervousness"] / 100 ) return min(100, max(0, int(final))) def confidence_label(score): if score >= 80: return "High", "#00dc00" if score >= 60: return "Moderate", "#00c896" if score >= 40: return "Low", "#00a5ff" return "Very Low", "#003cff" def analyze_frame(frame): """Run all detectors on a single frame. Returns annotated frame + metrics dict.""" landmark_extractor = FaceLandmarkExtractor() expr_detector = ExpressionDetector(fps=30) eye_detector = EyeContactDetector() head_estimator = HeadPoseEstimator() lm_result = landmark_extractor.extract(frame) if not lm_result["face_detected"]: landmark_extractor.release() return frame, None kp = lm_result["key_points"] landmarks = lm_result["landmarks"] expr_result = expr_detector.detect(kp, frame.shape) eye_result = eye_detector.detect(kp, frame.shape) head_result = head_estimator.detect(landmarks, frame.shape) confidence = compute_confidence_score(expr_result, eye_result, head_result) landmark_extractor.release() metrics = { "confidence": confidence, "expression": expr_result.get("expression", "N/A"), "nervousness": expr_result.get("nervousness_score", 0), "blink_rate": expr_result.get("blink_rate", 0), "gaze": eye_result.get("gaze_direction", "N/A"), "eye_pct": eye_result.get("eye_contact_pct", 0), "direction": head_result.get("direction", "N/A"), "stability": head_result.get("stability_score", 0), "pitch": head_result.get("pitch", 0.0), "yaw": head_result.get("yaw", 0.0), } return lm_result["annotated_frame"], metrics def metrics_to_markdown(metrics, question=""): if metrics is None: return "## No face detected\nPosition yourself properly in the frame." score = metrics["confidence"] label, color = confidence_label(score) bar_filled = "█" * (score // 5) bar_empty = "░" * (20 - score // 5) md = f""" ## Confidence Score: {score}/100 — {label} `{bar_filled}{bar_empty}` {score}% | Metric | Value | |---|---| | Expression | {metrics['expression']} | | Nervousness | {metrics['nervousness']}/100 | | Blink Rate | {metrics['blink_rate']}/min | | Gaze | {metrics['gaze']} | | Eye Contact | {metrics['eye_pct']}% | | Head Direction | {metrics['direction']} | | Head Stability | {metrics['stability']}/100 | | Pitch / Yaw | {metrics['pitch']:.1f} / {metrics['yaw']:.1f} | ### Tips """ if metrics["gaze"] != "Center": md += "- Look directly at the camera\n" if metrics["nervousness"] > 50: md += "- Breathe slowly, you got this\n" if metrics["direction"] != "Forward": md += "- Face forward\n" if metrics["gaze"] == "Center" and metrics["nervousness"] <= 50 and metrics["direction"] == "Forward": md += "- Great posture! Keep it up\n" if question: md += f"\n---\n**Current Question:** {question}" return md # ── Tab 1: Image Analysis ───────────────────────────────────────────────────── def analyze_image(image): if image is None: return None, "Please upload an image." frame = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2BGR) annotated, metrics = analyze_frame(frame) if metrics is None: out_img = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) return out_img, "No face detected in image." out_img = cv2.cvtColor(annotated, cv2.COLOR_BGR2RGB) return out_img, metrics_to_markdown(metrics) # ── Tab 2: Video Analysis ───────────────────────────────────────────────────── def analyze_video(video_path): if video_path is None: return None, "Please upload a video." cap = cv2.VideoCapture(video_path) if not cap.isOpened(): return None, "Could not open video." all_scores = [] expr_counts = {} frame_count = 0 sample_every = 10 # analyze every 10th frame for speed last_annotated = None while True: ret, frame = cap.read() if not ret: break frame_count += 1 if frame_count % sample_every != 0: continue annotated, metrics = analyze_frame(frame) if metrics: all_scores.append(metrics["confidence"]) expr = metrics["expression"] expr_counts[expr] = expr_counts.get(expr, 0) + 1 last_annotated = annotated cap.release() if not all_scores: return None, "No face detected in video." avg = int(np.mean(all_scores)) peak = int(np.max(all_scores)) low = int(np.min(all_scores)) label, _ = confidence_label(avg) expr_breakdown = "\n".join( f"- {e}: {int(c / sum(expr_counts.values()) * 100)}%" for e, c in sorted(expr_counts.items(), key=lambda x: -x[1]) ) summary_md = f""" ## Video Analysis Summary | Metric | Value | |---|---| | Avg Confidence | {avg}/100 [{label}] | | Peak Score | {peak}/100 | | Lowest Score | {low}/100 | | Frames Analyzed | {len(all_scores)} | ### Expression Breakdown {expr_breakdown} """ out_img = cv2.cvtColor(last_annotated, cv2.COLOR_BGR2RGB) if last_annotated is not None else None return out_img, summary_md # ── Tab 3: AI Mock Interview ────────────────────────────────────────────────── def setup_interview(name, job_role, experience, skills, resume_file): """Generate questions from profile or resume.""" if not name or not job_role: return "Please fill Name and Job Role.", gr.update(visible=False), [] profile = { 'name': name, 'job_role': job_role, 'experience': experience, 'skills': skills, 'projects': [], 'education': '', 'summary': '', 'resume_text': '' } if resume_file is not None: try: parsed = resume_to_profile(resume_file.name) profile.update({k: v for k, v in parsed.items() if v}) except Exception as e: print(f"[WARN] Resume parse failed: {e}") resume_ctx = get_resume_context_for_llm(profile) try: questions = generate_questions( profile['name'], profile['job_role'], profile['experience'], profile['skills'], resume_text=resume_ctx, num_questions=3, ) except Exception as e: return f"Question generation failed: {e}", gr.update(visible=False), [] q_display = "\n\n".join([f"**Q{i+1}:** {q}" for i, q in enumerate(questions)]) return ( f"Interview ready! {len(questions)} questions generated.\n\n{q_display}", gr.update(visible=True), questions ) def evaluate_single_answer(question, answer, job_role): """Evaluate one Q&A pair with LLM.""" if not question or not answer: return "Please provide both question and answer." try: feedback = evaluate_answer( question=question, answer=answer, job_role=job_role, followup='', followup_answer='', ) score_str = feedback.get('score_str', '?') strength = feedback.get('strength', '') improve = feedback.get('improvement', '') detail = feedback.get('detail', '') return f""" ## AI Evaluation **Score:** {score_str} **Strength:** {strength} **Improvement:** {improve} **Details:** {detail} """ except Exception as e: return f"Evaluation failed: {e}" # ── Build Gradio UI ─────────────────────────────────────────────────────────── with gr.Blocks( title="AI Interview Confidence Analyzer", theme=gr.themes.Base(primary_hue="purple"), css=""" .score-box { font-size: 1.4em; font-weight: bold; } footer { display: none !important; } """ ) as demo: gr.Markdown(""" # 🎙️ AI Interview Confidence Analyzer Multimodal confidence analysis — Face | Expression | Eye Contact | Head Pose | LLM Evaluation """) with gr.Tabs(): # ── Tab 1: Image ────────────────────────────────────────────────────── with gr.Tab("📸 Analyze Image"): gr.Markdown("Upload a photo from your interview or practice session.") with gr.Row(): img_input = gr.Image(label="Upload Image", type="pil") img_output = gr.Image(label="Annotated Result") img_metrics = gr.Markdown(label="Metrics") img_btn = gr.Button("Analyze", variant="primary") img_btn.click( fn=analyze_image, inputs=img_input, outputs=[img_output, img_metrics] ) # ── Tab 2: Video ────────────────────────────────────────────────────── with gr.Tab("🎬 Analyze Video"): gr.Markdown("Upload a recorded interview video for frame-by-frame analysis.") with gr.Row(): vid_input = gr.Video(label="Upload Video") vid_output = gr.Image(label="Last Analyzed Frame") vid_metrics = gr.Markdown(label="Summary") vid_btn = gr.Button("Analyze Video", variant="primary") vid_btn.click( fn=analyze_video, inputs=vid_input, outputs=[vid_output, vid_metrics] ) # ── Tab 3: AI Mock Interview ────────────────────────────────────────── with gr.Tab("🤖 AI Mock Interview"): gr.Markdown(""" Enter your profile, generate personalized interview questions, then type your answers to get AI feedback. """) with gr.Row(): with gr.Column(): name_input = gr.Textbox(label="Your Name", placeholder="Sunaina") role_input = gr.Textbox(label="Job Role", placeholder="AI/ML Engineer") exp_input = gr.Textbox(label="Experience", placeholder="1 year intern, built RAG systems") skill_input = gr.Textbox(label="Skills", placeholder="Python, LangChain, FastAPI, FAISS") resume_file = gr.File(label="Resume (PDF/DOCX/TXT) — optional", file_types=[".pdf", ".docx", ".txt"]) setup_btn = gr.Button("Generate Questions", variant="primary") with gr.Column(): setup_output = gr.Markdown(label="Questions") questions_state = gr.State([]) setup_btn.click( fn=setup_interview, inputs=[name_input, role_input, exp_input, skill_input, resume_file], outputs=[setup_output, gr.Column(visible=False), questions_state] ) gr.Markdown("---") gr.Markdown("### Evaluate Your Answer") gr.Markdown("Copy a question from above, paste it, write your answer, and get AI feedback.") with gr.Row(): eval_question = gr.Textbox(label="Question", lines=2, placeholder="Paste the question here") eval_role = gr.Textbox(label="Job Role", placeholder="AI/ML Engineer") eval_answer = gr.Textbox(label="Your Answer", lines=5, placeholder="Type your answer here...") eval_btn = gr.Button("Get AI Feedback", variant="primary") eval_output = gr.Markdown(label="AI Feedback") eval_btn.click( fn=evaluate_single_answer, inputs=[eval_question, eval_answer, eval_role], outputs=eval_output ) gr.Markdown(""" --- Built by **Sunaina** | AI/ML Engineer Intern @ Indux Technology | [GitHub](https://github.com/Sunaina792/AI-Interview-system) """) if __name__ == "__main__": demo.launch()