#!/usr/bin/env python3 """ AI Mock Interviewer — 语音面试官 功能: - 语音提问(OpenAI TTS) - 语音回答(Groq STT) - Claude 评估 + 追问 - 面试结束生成评分报告 用法: python interviewer.py # 默认算法面试 python interviewer.py --type ml # ML面试 python interviewer.py --type system # 系统设计 python interviewer.py --type behavioral # 行为面试 python interviewer.py --lang zh # 中文面试 """ import anthropic import json import os import subprocess import sys import time import threading import tempfile import speech_recognition as sr # ── Config ── INTERVIEW_TYPES = { "algo": { "name": "Algorithm & Data Structures", "name_zh": "算法与数据结构", "system": """You are a senior technical interviewer at a top tech company (Google/Meta level). You are conducting a 30-minute algorithm and data structures interview. Rules: - Ask ONE question at a time - Start easy, increase difficulty based on candidate's performance - After the candidate answers, give brief feedback then ask a follow-up or new question - Ask about time/space complexity - If the candidate is stuck, give a hint - After 5-6 questions, wrap up with final feedback - Be professional but friendly - Keep responses concise (2-3 sentences max for feedback)""" }, "ml": { "name": "Machine Learning", "name_zh": "机器学习", "system": """You are a senior ML engineer conducting a machine learning interview. Cover: supervised/unsupervised learning, neural networks, transformers, training optimization, model evaluation, deployment at scale. Rules: - Ask ONE question at a time - Mix theory and practical experience questions - Ask follow-ups based on answers - After 5-6 questions, wrap up - Keep responses concise""" }, "system": { "name": "System Design", "name_zh": "系统设计", "system": """You are a senior architect conducting a system design interview. Pick a system to design (URL shortener, chat app, news feed, etc.) Rules: - Present the problem, let candidate drive the design - Ask clarifying questions about their choices - Probe on scalability, reliability, trade-offs - Discuss database choices, caching, load balancing - One main design problem for the full interview - Keep responses concise""" }, "behavioral": { "name": "Behavioral", "name_zh": "行为面试", "system": """You are an engineering manager conducting a behavioral interview. Use the STAR method to evaluate answers. Rules: - Ask about teamwork, conflict, leadership, failure, growth - Ask ONE question at a time - Follow up with "Tell me more about..." or "What would you do differently?" - After 5-6 questions, wrap up - Keep responses concise and encouraging""" }, } class AIInterviewer: def __init__(self, interview_type="algo", lang="en"): self.type = interview_type self.lang = lang self.config = INTERVIEW_TYPES[interview_type] self.conversation = [] self.scores = [] self.turn = 0 self.max_turns = 6 self.running = True # API clients self._init_clients() # Audio self.recognizer = sr.Recognizer() self.recognizer.energy_threshold = 300 self.recognizer.dynamic_energy_threshold = True self.recognizer.pause_threshold = 2.0 def _init_clients(self): """Initialize API clients.""" # Claude (via OAuth) token = self._read_keychain_token() if token: self.claude = anthropic.Anthropic( api_key=None, auth_token=token, default_headers={ "anthropic-beta": "claude-code-20250219,oauth-2025-04-20," "fine-grained-tool-streaming-2025-05-14," "interleaved-thinking-2025-05-14", "anthropic-dangerous-direct-browser-access": "true", "user-agent": "claude-cli/2.1.77", "x-app": "cli", }, ) else: self.claude = None # Groq (STT) self.groq_key = self._read_key("groq") # OpenAI (TTS) self.openai_key = self._read_key("openai") def _read_key(self, name): kf = os.path.expanduser(f"~/.{name}_key") if os.path.exists(kf): return open(kf).read().strip() return "" def _read_keychain_token(self): try: raw = subprocess.check_output( ["security", "find-generic-password", "-s", "Claude Code-credentials", "-w"], stderr=subprocess.DEVNULL, ).decode().strip() data = json.loads(raw) return data.get("claudeAiOauth", {}).get("accessToken", "") except Exception: return "" def speak(self, text): """TTS: speak the text aloud.""" if self.openai_key: self._speak_openai(text) else: self._speak_macos(text) def _speak_openai(self, text): """OpenAI TTS (most natural).""" try: import requests voice = "nova" if self.lang == "zh" else "shimmer" resp = requests.post( "https://api.openai.com/v1/audio/speech", headers={"Authorization": f"Bearer {self.openai_key}", "Content-Type": "application/json"}, json={"model": "tts-1-hd", "input": text, "voice": voice, "speed": 1.0, "response_format": "mp3"}, timeout=15, ) if resp.status_code == 200: tmp = tempfile.NamedTemporaryFile(suffix=".mp3", delete=False) tmp.write(resp.content) tmp.close() subprocess.run(["afplay", tmp.name], capture_output=True) os.unlink(tmp.name) return except Exception: pass self._speak_macos(text) def _speak_macos(self, text): """Fallback: macOS say command.""" voice = "Tingting" if self.lang == "zh" else "Samantha" subprocess.run(["say", "-v", voice, "-r", "190", text], capture_output=True) def listen(self) -> str: """STT: listen for user's spoken answer.""" print(" 🎙 Listening... (speak now, pause 2s to finish)") mic = sr.Microphone() with mic as source: self.recognizer.adjust_for_ambient_noise(source, duration=0.5) try: audio = self.recognizer.listen(source, timeout=30, phrase_time_limit=60) except sr.WaitTimeoutError: return "" # Transcribe with Groq (fastest) if self.groq_key: return self._transcribe_groq(audio) else: return self._transcribe_google(audio) def _transcribe_groq(self, audio) -> str: from groq import Groq client = Groq(api_key=self.groq_key) tmp = tempfile.NamedTemporaryFile(suffix=".wav", delete=False) tmp.write(audio.get_wav_data()) tmp.close() try: with open(tmp.name, "rb") as f: t = client.audio.transcriptions.create( file=("audio.wav", f), model="whisper-large-v3", response_format="text", temperature=0.0, ) return t.strip() if isinstance(t, str) else t.text.strip() finally: os.unlink(tmp.name) def _transcribe_google(self, audio) -> str: try: lang = "zh-CN" if self.lang == "zh" else "en-US" return self.recognizer.recognize_google(audio, language=lang) except Exception: return "" def ask_claude(self, user_message: str) -> str: """Send message to Claude and get response.""" if not self.claude: return "Error: Claude API not available" self.conversation.append({"role": "user", "content": user_message}) lang_instruction = "回答请用中文。" if self.lang == "zh" else "" system_blocks = [ {"type": "text", "text": "You are Claude Code, Anthropic's official CLI for Claude."}, {"type": "text", "text": self.config["system"] + "\n" + lang_instruction}, ] resp = self.claude.messages.create( model="claude-sonnet-4-6", max_tokens=500, system=system_blocks, messages=self.conversation, ) reply = resp.content[0].text self.conversation.append({"role": "assistant", "content": reply}) return reply def evaluate_answer(self, question: str, answer: str) -> dict: """Get Claude to evaluate the candidate's answer.""" eval_prompt = f"""Evaluate this interview answer on a scale of 1-10. Question: {question} Answer: {answer} Return JSON only: {{"score": N, "strengths": "...", "improvements": "..."}}""" self.conversation.append({"role": "user", "content": eval_prompt}) system_blocks = [ {"type": "text", "text": "You are Claude Code, Anthropic's official CLI for Claude."}, {"type": "text", "text": "You are an interview evaluator. Return only valid JSON."}, ] resp = self.claude.messages.create( model="claude-haiku-4-5", max_tokens=200, system=system_blocks, messages=[{"role": "user", "content": eval_prompt}], ) try: text = resp.content[0].text # Extract JSON from response if "{" in text: json_str = text[text.index("{"):text.rindex("}") + 1] return json.loads(json_str) except Exception: pass return {"score": 5, "strengths": "Good attempt", "improvements": "Could elaborate more"} def generate_report(self) -> str: """Generate final interview report.""" avg_score = sum(s["score"] for s in self.scores) / max(len(self.scores), 1) all_strengths = [s["strengths"] for s in self.scores] all_improvements = [s["improvements"] for s in self.scores] report = f""" {'='*60} INTERVIEW REPORT — {self.config['name']} {'='*60} Overall Score: {avg_score:.1f} / 10 Questions Asked: {len(self.scores)} Strengths: """ for i, s in enumerate(all_strengths, 1): report += f" {i}. {s}\n" report += "\n Areas for Improvement:\n" for i, s in enumerate(all_improvements, 1): report += f" {i}. {s}\n" report += f""" Individual Scores: """ for i, s in enumerate(self.scores, 1): report += f" Q{i}: {s['score']}/10\n" if avg_score >= 8: verdict = "STRONG HIRE ✅" elif avg_score >= 6: verdict = "HIRE (with reservations) ⚠️" elif avg_score >= 4: verdict = "NO HIRE (needs improvement) ❌" else: verdict = "STRONG NO HIRE ❌❌" report += f""" Verdict: {verdict} {'='*60} """ return report def run(self): """Main interview loop.""" type_name = self.config["name_zh"] if self.lang == "zh" else self.config["name"] print(f"\n{'='*60}") print(f" 🎤 AI Mock Interview — {type_name}") print(f" Language: {'中文' if self.lang == 'zh' else 'English'}") print(f" Questions: ~{self.max_turns}") print(f" TTS: {'OpenAI HD' if self.openai_key else 'macOS'}") print(f" STT: {'Groq' if self.groq_key else 'Google'}") print(f"{'='*60}\n") # Opening if self.lang == "zh": opening = "你好!欢迎参加今天的面试。我是你的面试官。准备好了吗?" else: opening = "Hello! Welcome to the interview. I'm your interviewer today. Are you ready to begin?" print(f" 🤖 Interviewer: {opening}") self.speak(opening) # Wait for ready print() ready = self.listen() print(f" 👤 You: {ready}") # Ask first question first_q = self.ask_claude("The candidate is ready. Ask your first interview question.") print(f"\n 🤖 Interviewer: {first_q}") self.speak(first_q) last_question = first_q # Interview loop while self.turn < self.max_turns and self.running: self.turn += 1 print(f"\n --- Question {self.turn}/{self.max_turns} ---") # Listen for answer answer = self.listen() if not answer: print(" (no response detected, skipping...)") continue print(f" 👤 You: {answer}") # Evaluate silently score = self.evaluate_answer(last_question, answer) self.scores.append(score) print(f" 📊 Score: {score['score']}/10") # Get next response from Claude (feedback + next question) response = self.ask_claude(answer) print(f"\n 🤖 Interviewer: {response}") self.speak(response) last_question = response # Wrap up if self.lang == "zh": closing = "面试到此结束。感谢你的参与!我会生成一份评估报告。" else: closing = "That concludes our interview. Thank you for your time! Let me generate your evaluation report." print(f"\n 🤖 Interviewer: {closing}") self.speak(closing) # Generate report report = self.generate_report() print(report) # Save report report_file = os.path.expanduser(f"~/ai-interviewer/reports/report_{int(time.time())}.txt") os.makedirs(os.path.dirname(report_file), exist_ok=True) with open(report_file, "w") as f: f.write(report) print(f" Report saved to: {report_file}") def main(): import argparse parser = argparse.ArgumentParser(description="AI Mock Interviewer") parser.add_argument("--type", choices=["algo", "ml", "system", "behavioral"], default="algo") parser.add_argument("--lang", choices=["en", "zh"], default="en") parser.add_argument("--turns", type=int, default=6) args = parser.parse_args() interviewer = AIInterviewer(interview_type=args.type, lang=args.lang) interviewer.max_turns = args.turns interviewer.run() if __name__ == "__main__": main()