Spaces:
Sleeping
Sleeping
| # 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() |