File size: 14,869 Bytes
768c1da
aa8e154
 
 
 
 
 
 
768c1da
 
aa8e154
 
 
 
 
 
 
 
 
 
 
 
 
 
768c1da
aa8e154
 
 
 
 
 
768c1da
aa8e154
 
768c1da
aa8e154
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
768c1da
 
 
 
aa8e154
 
768c1da
 
 
 
 
 
aa8e154
768c1da
aa8e154
768c1da
 
 
aa8e154
768c1da
 
aa8e154
768c1da
 
 
 
aa8e154
768c1da
aa8e154
768c1da
 
 
 
 
 
 
 
 
 
 
 
 
aa8e154
 
768c1da
 
 
aa8e154
768c1da
 
aa8e154
768c1da
 
aa8e154
768c1da
 
aa8e154
768c1da
aa8e154
768c1da
 
 
 
 
 
 
 
 
 
aa8e154
768c1da
 
 
 
 
 
 
 
 
 
aa8e154
768c1da
 
aa8e154
768c1da
aa8e154
 
768c1da
 
 
 
aa8e154
768c1da
 
aa8e154
768c1da
 
 
aa8e154
768c1da
 
aa8e154
 
768c1da
 
 
 
aa8e154
768c1da
 
 
aa8e154
768c1da
 
 
 
 
aa8e154
768c1da
 
 
 
 
 
 
 
 
 
 
 
 
 
aa8e154
768c1da
aa8e154
768c1da
 
aa8e154
768c1da
 
 
 
aa8e154
768c1da
 
 
aa8e154
 
768c1da
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
aa8e154
768c1da
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
aa8e154
 
768c1da
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
aa8e154
 
768c1da
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
# 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()