Build AI Interview Coach with Gemini API & Webcam
Blog Post

Build AI Interview Coach with Gemini API & Webcam

Jake McCluskey
Back to blog

: concrete examples vs. vague statements - Completeness: covers all aspects of the question - STAR method (for behavioral questions): Situation, Task, Action, Result

        Return complete analysis as valid JSON.
        """
        
        response = self.analyzer.analyze_segment(segment_frames, segment_audio)
        results.append(json.loads(response))
        
        # Respect rate limits (15 RPM on free tier)
        time.sleep(4)
        
    return self._generate_report(results)
    
def _generate_report(self, segment_results):
    """
    Aggregates segment scores into final report card
    """
    report = {
        'timestamp': datetime.now().isoformat(),
        'overall_scores': {},
        'segment_breakdown': segment_results,
        'trends': {},
        'feedback': {}
    }
    
    # Calculate average scores across all segments
    metrics = ['eye_contact', 'body_language', 'pacing', 'tone', 
               'filler_words', 'clarity', 'answer_quality']
    
    for metric in metrics:
        scores = [seg.get(metric, 0) for seg in segment_results]
        report['overall_scores'][metric] = {
            'average': sum(scores) / len(scores),
            'min': min(scores),
            'max': max(scores),
            'trend': 'improving' if scores[-1] > scores[0] else 'declining'
        }
    
    # Generate actionable feedback
    report['feedback'] = self._generate_feedback(report['overall_scores'])
    
    return report
    
def _generate_feedback(self, scores):
    """
    Creates specific improvement recommendations
    """
    feedback = {
        'strengths': [],
        'priority_improvements': [],
        'specific_tips': []
    }
    
    # Identify top 2 strengths (scores >= 8)
    strong_areas = [(k, v['average']) for k, v in scores.items() 
                    if v['average'] >= 8.0]
    strong_areas.sort(key=lambda x: x[1], reverse=True)
    feedback['strengths'] = [area[0] for area in strong_areas[:2]]
    
    # Identify top 2-3 improvement areas (scores < 7)
    weak_areas = [(k, v['average']) for k, v in scores.items() 
                  if v['average'] < 7.0]
    weak_areas.sort(key=lambda x: x[1])
    feedback['priority_improvements'] = [area[0] for area in weak_areas[:3]]
    
    # Generate specific tips based on weak areas
    tip_map = {
        'eye_contact': "Practice looking at the camera lens, not your own image on screen. Try the 5-second rule: hold eye contact for 5 seconds, then briefly look away.",
        'pacing': "Your speaking rate needs adjustment. Record yourself and count words per minute, aiming for 140-160 WPM. Use a metronome app to practice consistent pacing.",
        'filler_words': "You're overusing filler words. Try pausing silently instead of saying 'um' or 'uh.' Practice with a friend who snaps their fingers every time you use a filler.",
        'tone': "Your pitch variance is limited. Practice emphasizing key words in your answers. Read children's books aloud to exaggerate vocal variety, then dial it back to natural levels.",
        'answer_quality': "Structure your answers using STAR method: Situation, Task, Action, Result. Write out 10 common interview questions and script STAR responses for each.",
        'body_language': "You're showing nervous habits. Before interviews, do power poses for 2 minutes. Keep hands visible and use purposeful gestures to emphasize points.",
        'clarity': "Work on articulation. Practice tongue twisters daily. Slow down slightly and finish your sentences with clear, downward inflections."
    }
    
    for area in feedback['priority_improvements']:
        if area in tip_map:
            feedback['specific_tips'].append(tip_map[area])
    
    return feedback

def _create_wav_from_chunks(self, audio_chunks):
    """Combines audio chunks into WAV file bytes"""
    # Implementation details for WAV file creation
    pass

def _extract_audio_segment(self, audio_data, start_sec, duration_sec):
    """Extracts specific time range from audio data"""
    # Implementation details for audio segmentation
    pass

How Do You Handle the Free Tier Rate Limits and Optimize Costs

The free tier's 15 requests per minute limit means you'll need 4-second delays between API calls when processing a multi-segment interview. For a 5-minute interview (5 segments), total processing time will be around 60 seconds (8-12 seconds per request plus 4-second delays). That's still faster than real-time analysis.

You can optimize by reducing frame sampling. Instead of sending all 60 frames from a 1-minute segment, send every third frame (20 frames total). Testing shows this reduces processing time by about 30% with minimal impact on visual analysis accuracy, and honestly, most people won't notice the difference in scoring.

For longer practice sessions, implement local caching. Store processed segments so you don't re-analyze the same footage if you want to review it later. You can also batch multiple questions into a single longer segment (up to 60 seconds) rather than analyzing each 30-second answer separately.

The free tier's 1,500 requests per day allows roughly 300 five-minute interview sessions daily, which is more than sufficient for personal use. If you're building this for a team or coaching service, you'll need to track usage per user and implement queuing to stay within limits.

How Do You Build the User Interface and Display Results

A simple Streamlit interface works well for this application. You'll create a page with a video preview window, question display area, recording controls, and results dashboard. Streamlit handles the webcam integration through its built-in components, which simplifies the frontend code significantly.

The results dashboard should display your five core metrics as progress bars with color coding (green for 8-10, yellow for 6-7, red for below 6). Include a timeline view that shows score variations across segments, so you can see if you got stronger or weaker as the interview progressed. Most people fade after the 3-minute mark.

For each metric, provide expandable detail sections with specific observations from Gemini's analysis. If the system flagged a moment where you looked away for 8 seconds, show the timestamp and a thumbnail of that frame. If it detected 7 filler words in a 60-second segment, display the transcript with those words highlighted.

Here's a basic Streamlit implementation:

import streamlit as st
import time

def main():
    st.title("AI Interview Coach")
    st.write("Practice your interview skills with real-time feedback")
    
    # Sidebar for configuration
    with st.sidebar:
        st.header("Settings")
        api_key = st.text_input("Gemini API Key", type="password")
        
        st.header("Interview Questions")
        questions = []
        num_questions = st.number_input("Number of questions", 1, 10, 3)
        
        for i in range(num_questions):
            q = st.text_area(f"Question {i+1}", 
                           value=f"Tell me about a challenging project you worked on.")
            questions.append(q)
    
    # Main area
    if not api_key:
        st.warning("Please enter your Gemini API key in the sidebar")
        return
    
    coach = InterviewCoach(api_key)
    
    col1, col2 = st.columns([2, 1])
    
    with col1:
        st.header("Interview Session")
        
        # Video preview
        camera = st.camera_input("Camera Preview")
        
        if st.button("Start Interview", type="primary"):
            with st.spinner("Recording and analyzing..."):
                results = coach.conduct_interview(questions, duration_minutes=len(questions))
                st.session_state['results'] = results
    
    with col2:
        st.header("Quick Tips")
        st.info("""
        - Look directly at the camera
        - Speak at 140-160 words per minute
        - Use STAR method for behavioral questions
        - Limit filler words to 3 per minute
        - Sit up straight with open posture
        """)
    
    # Results display
    if 'results' in st.session_state:
        display_results(st.session_state['results'])

def display_results(results):
    st.header("Performance Report Card")
    
    # Overall scores
    st.subheader("Overall Scores")
    
    scores = results['overall_scores']
    
    col1, col2, col3 = st.columns(3)
    
    with col1:
        display_metric_card("Eye Contact", scores['eye_contact'])
        display_metric_card("Pacing", scores['pacing'])
    
    with col2:
        display_metric_card("Tone", scores['tone'])
        display_metric_card("Filler Words", scores['filler_words'])
    
    with col3:
        display_metric_card("Body Language", scores['body_language'])
        display_metric_card("Answer Quality", scores['answer_quality'])
    
    # Trends
    st.subheader("Performance Trends")
    
    trend_data = []
    for metric, data in scores.items():
        trend_data.append({
            'Metric': metric.replace('_', ' ').title(),
            'Trend': '📈' if data['trend'] == 'improving' else '📉',
            'Change': f"{data['max'] - data['min']:.1f} points"
        })
    
    st.table(trend_data)
    
    # Feedback
    st.subheader("Personalized Feedback")
    
    feedback = results['feedback']
    
    if feedback['strengths']:
        st.success(f"**Your Strengths:** {', '.join(feedback['strengths'])}")
    
    if feedback['priority_improvements']:
        st.warning(f"**Focus On:** {', '.join(feedback['priority_improvements'])}")
    
    st.subheader("Action Items")
    for i, tip in enumerate(feedback['specific_tips'], 1):
        st.write(f"{i}. {tip}")
    
    # Detailed segment breakdown
    with st.expander("View Detailed Segment Analysis"):
        for i, segment in enumerate(results['segment_breakdown'], 1):
            st.write(f"**Segment {i}**")
            st.json(segment)

def display_metric_card(name, score_data):
    avg_score = score_data['average']
    
    # Color coding
    if avg_score >= 8:
        color = "green"
    elif avg_score >= 6:
        color = "orange"
    else:
        color = "red"
    
    st.metric(
        label=name,
        value=f"{avg_score:.1f}/10",
        delta=f"{score_data['trend']}"
    )
    
    st.progress(avg_score / 10)

if __name__ == "__main__":
    main()

What Are Common Issues and How Do You Debug Them

The most frequent problem is API timeout errors when processing longer video segments. If a request takes more than 60 seconds, it'll fail. Solution: reduce frame sampling from 1 FPS to 0.5 FPS (one frame every 2 seconds), which cuts processing time roughly in half.

Audio sync issues can occur if your video and audio capture threads drift out of alignment. You'll notice this when Gemini's transcript doesn't match what you actually said at specific timestamps. Fix this by adding timestamp markers to both streams and implementing a synchronization check every 10 seconds during recording.

Low-quality scores across all metrics often indicate poor lighting or audio input levels. Before starting an interview, run a quick calibration check: capture 5 seconds of footage and verify that faces are clearly visible and audio levels peak around 70-80% of maximum. Bad input data will tank your scores no matter how good your actual performance is.

Rate limit errors (429 status codes) mean you're exceeding 15 requests per minute. Add exponential backoff: if a request fails, wait 5 seconds and retry, doubling the wait time with each subsequent failure up to a maximum of 60 seconds.

Here's error handling code:

import time
from google.api_core import exceptions

def analyze_with_retry(analyzer, frames, audio, max_retries=3):
    """
    Wrapper for API calls with exponential backoff
    """
    for attempt in range(max_retries):
        try:
            result = analyzer.analyze_segment(frames, audio)
            return result
            
        except exceptions.ResourceExhausted:
            # Rate limit hit
            wait_time = min(5 * (2 ** attempt), 60)
            print(f"Rate limit reached. Waiting {wait_time} seconds...")
            time.sleep(wait_time)
            
        except exceptions.DeadlineExceeded:
            # Request timeout
            print("Request timeout. Reducing frame count...")
            frames = frames[::2]  # Use every other frame
            
        except Exception as e:
            print(f"Unexpected error: {e}")
            if attempt == max_retries - 1:
                raise
    
    return None

def validate_input_quality(frame, audio_chunk):
    """
    Checks if input data meets minimum quality standards
    """
    issues = []
    
    # Check frame brightness
    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
    brightness = gray.mean()
    if brightness < 50:
        issues.append("Lighting too dark (increase brightness)")
    elif brightness > 200:
        issues.append("Overexposed video (reduce brightness)")
    
    # Check audio levels
    audio_array = np.frombuffer(audio_chunk, dtype=np.int16)
    audio_level = np.abs(audio_array).mean()
    max_level = np.iinfo(np.int16).max
    
    if audio_level < max_level * 0.1:
        issues.append("Audio too quiet (move closer to mic)")
    elif audio_level > max_level * 0.9:
        issues.append("Audio clipping (reduce input volume)")
    
    return issues

How Can You Extend This System for Advanced Features

You can add question-specific scoring by training custom evaluation criteria for different interview types. Technical interviews need different metrics than behavioral interviews. For technical questions, you'd instruct Gemini to check for specific concepts, algorithms, or frameworks in the answer. For behavioral questions, you'd weight STAR method compliance more heavily.

Comparative analysis lets you track improvement over multiple practice sessions. Store results in a local SQLite database with timestamps, then generate progress charts showing how your scores change over weeks of practice. You'll see which areas improve fastest and which remain stubborn weak points.

Real interviewer simulation adds another layer: use Gemini to generate follow-up questions based on your answers, creating a dynamic conversation instead of a static question list. The system analyzes your response, identifies areas that need clarification, and asks a relevant follow-up. This mimics real interview dynamics better than pre-written questions.

Peer comparison (anonymized) could work if you're building this for a study group or bootcamp. Aggregate scores across users to show percentile rankings: "Your pacing is in the 78th percentile among users." This gives context for whether a score of 7.2 is actually good or needs work.

Here's a framework for question-specific scoring:

class QuestionTypeScorer:
    def __init__(self):
        self.scoring_templates = {
            'behavioral': {
                'required_elements': ['situation', 'task', 'action', 'result'],
                'weights': {
                    'structure': 0.3,
                    'specificity': 0.3,
Ready to stop reading and start shipping?

Get a free AI-powered SEO audit of your site

We'll crawl your site, benchmark your local pack, and hand you a prioritized fix list in minutes. No call required.

Run my free audit
WANT THE SHORTCUT

Need help applying this to your business?

The post above is the framework. Spend 30 minutes with me and we'll map it to your specific stack, budget, and timeline. No pitch, just a real scoping conversation.