from datetime import datetime from config import Config from evaluator import ClaudeEvaluator, evaluate_answer from models import Answer, InterviewReport, Question, QuestionResult from reporter import ReportGenerator from speech import ( DryRunRecognizer, DryRunSynthesizer, SpeechRecognizer, SpeechSynthesizer, ) class Interviewer: def __init__(self, config: Config, questions: list[Question]): self.config = config self.questions = questions if config.dry_run: self.recognizer = DryRunRecognizer() self.synthesizer = DryRunSynthesizer() else: self.recognizer = SpeechRecognizer( model_name=config.whisper_model, sample_rate=config.sample_rate, silence_timeout=config.silence_timeout, max_duration=config.max_answer_duration, ) self.synthesizer = SpeechSynthesizer() self.claude = ClaudeEvaluator( api_key=config.anthropic_api_key, model=config.claude_model, ) self.reporter = ReportGenerator(output_dir=config.output_dir) def run(self) -> InterviewReport: candidate_name = self._greet() results: list[QuestionResult] = [] for i, question in enumerate(self.questions, start=1): print(f"\n--- 質問 {i}/{len(self.questions)} [{question.category}] ---") answer = self._ask_question(question) if not answer.transcribed_text.strip(): print(" 回答なし。スキップします。") answer = Answer(transcribed_text="(回答なし)", audio_duration_sec=0.0) print(f" 📝 書き起こし: {answer.transcribed_text}") print(" 評価中...") result = evaluate_answer(question, answer, self.claude) results.append(result) print(f" スコア: {result.total_score:.1f}/{question.max_score}点") print(f" 💬 {result.ai_feedback}") # 追加質問の判定 if question.follow_up and result.total_score < question.max_score * 0.4: follow_up_result = self._ask_follow_up(question, result) if follow_up_result: results.append(follow_up_result) if i < len(self.questions): self.synthesizer.speak("ありがとうございます。次の質問に進みます。") self._close() total_score = sum(r.total_score for r in results) max_possible = sum(r.question.max_score for r in results) percentage = total_score / max_possible if max_possible > 0 else 0 is_passed = percentage >= self.config.pass_threshold report = InterviewReport( candidate_name=candidate_name, interview_date=datetime.now(), results=results, total_score=total_score, max_possible_score=max_possible, pass_threshold=self.config.pass_threshold, is_passed=is_passed, ) filepath = self.reporter.generate(report) self.reporter.print_summary(report) print(f"\n📄 レポート保存先: {filepath}") return report def _greet(self) -> str: self.synthesizer.speak( "こんにちは。これからAI面接を開始します。" "各質問に対して、音声でお答えください。" "準備ができましたら、お名前をお聞かせください。" ) print("\n候補者のお名前:") if self.config.dry_run: name = input("> ").strip() else: name, _ = self.recognizer.listen() name = name.strip() if not name: name = "名前未入力" print(f"候補者: {name}") self.synthesizer.speak(f"{name}さん、よろしくお願いいたします。それでは面接を始めましょう。") return name def _ask_question(self, question: Question) -> Answer: self.synthesizer.speak(question.question_text) text, duration = self.recognizer.listen() # 空の場合は1回リトライ if not text.strip(): self.synthesizer.speak("聞き取れませんでした。もう一度お答えください。") text, duration = self.recognizer.listen() return Answer(transcribed_text=text, audio_duration_sec=duration) def _ask_follow_up( self, question: Question, original_result: QuestionResult ) -> QuestionResult | None: print(f" 📌 追加質問: {question.follow_up}") self.synthesizer.speak(question.follow_up) text, duration = self.recognizer.listen() if not text.strip(): return None print(f" 📝 追加回答: {text}") print(" 追加回答を評価中...") # 追加質問の評価(元の質問の半分の配点) follow_up_question = Question( id=question.id, category=question.category, question_text=question.follow_up, expected_keywords=question.expected_keywords, keyword_weight=question.keyword_weight, ai_weight=question.ai_weight, improv_weight=question.improv_weight, max_score=question.max_score // 2, scoring_criteria=question.scoring_criteria + "(追加質問への回答)", follow_up="", ) answer = Answer(transcribed_text=text, audio_duration_sec=duration) result = evaluate_answer(follow_up_question, answer, self.claude) print(f" 追加スコア: {result.total_score:.1f}/{follow_up_question.max_score}点") return result def _close(self) -> None: self.synthesizer.speak( "以上で面接は終了です。" "本日はお時間をいただきありがとうございました。" "結果は後日お知らせいたします。お疲れ様でした。" ) print("\n面接終了。レポートを生成中...")