
チュートリアル
Twelve Labs を使ったビデオ AI 面接分析ツールの構築

リシケシュ・ヤダフ
このチュートリアルでは、Twelve LabsのPegasusモデルを使用して、録画された面接を自信、明瞭さ、アイコンタクト、身振り手振り(ボディランゲージ)、話すスピード、声のトーンなどの指標で自動的に評価する「AI面接アナライザー」の構築プロセスを解説します。これにより、求職者と採用担当者の双方が、手作業での確認を行うことなく、構造化されたフィードバックを得ることができます。
このチュートリアルでは、Twelve LabsのPegasusモデルを使用して、録画された面接を自信、明瞭さ、アイコンタクト、身振り手振り(ボディランゲージ)、話すスピード、声のトーンなどの指標で自動的に評価する「AI面接アナライザー」の構築プロセスを解説します。これにより、求職者と採用担当者の双方が、手作業での確認を行うことなく、構造化されたフィードバックを得ることができます。

この記事の内容
No headings found on page
ニュースレターに登録する
ニュースレターに登録する
ビデオ理解に関する最新の技術進歩、チュートリアル、業界の動向をお届けします
ビデオ理解に関する最新の技術進歩、チュートリアル、業界の動向をお届けします
AIを活用してビデオを検索、分析、探索します。
2024/09/21
11分
記事へのリンクをコピー
面接室に入ったとき、あなたの何気ない仕草、言葉、そして表情が単に観察されるだけでなく、深く理解されることを想像してみてください。AIがあなた専属のコーチや相談相手となる、未来の就職面接へようこそ。「AI Interview Analyzer(AI面接アナライザー)」が登場し、求職者にとっても採用担当者にとっても、このプロセスに革命を起こそうとしています。
今日の競争が激しい就職活動市場において、重要な面接をうまくこなせるかどうかは、憧れの仕事を手に入れられるか、それとも手の届かないものになってしまうかの分かれ目となります。しかし、もしあなたに秘密兵器があったらどうでしょうか?あなたのパフォーマンスを分析し、強みを浮き彫りにし、改善すべき点を優しく提案してくれるツールがあったら?
まさにそれを実現するのが「AI Interview Analyzer」です。Twelve LabsのPegasus-1モデルを使用することで、かつては熟練した人事プロフェッショナルしか持ち得なかった洞察を提供します。
アプリケーションのデモはこちらで体験できます:Interview Analyzer アプリケーション。また、こちらのReplitテンプレートから実際に動かしてみることも可能です。
前提条件
Twelve Labs Playgroundにサインアップして、APIキーを生成します。
ノートブックとこのアプリケーションのリポジトリは、GitHubにあります。
このチュートリアルのFlaskアプリケーションは、Python、HTML、CSS、JavaScriptを使用しています。

アプリケーションの仕組み
このセクションでは、Twelve Labsを活用した面接準備アプリケーションの開発および利用における、アプリケーションのフロー概要を説明します。
面接プロセスは、ユーザーの準備を促すために、限られた時間だけランダムな適性検査の質問が表示されるところから始まります。ユーザーが回答を終えると、アプリケーションは設定された時間、自動的に面接の録画を開始します。録画終了後、ビデオは即座に処理され、Marengo 2.6 (Embedding Engine)を用いてインデックス登録へと送られます。

Twelve Labsの生成エンジンであるPegasus 1.1に対して実行されたインストラクション・プロンプティングに基づき、ビデオ面接の評価に用いられる様々なパラメータを分析・スコア化します。これらのパラメータには、自信、明瞭さ、視線(アイコンタクト)、ボディーランゲージ、発話速度、声のトーンが含まれます。さらに、面接中に議論されたすべての重要なポイントを書き留めます。企業や組織はこのアプリケーションを活用することで、多数の候補者に対して自動面接を実施し、時間を節約することができます。あるいは、面接練習のフィードバックを提供し、ユーザーがパフォーマンスを向上させるのを助けることもできます。
準備ステップ

Twelve Labs Playgroundにサインアップし、インデックス(Index)を作成します。
分類に合わせて適切なオプションを選択します:(1) ビデオ検索・分類用の「Marengo 2.6 (Embedding Engine)」、または (2) ビデオからテキストへの生成用の「Pegasus 1.1」。これらのエンジンは、ビデオ理解のための強固な基盤を提供します。
Twelve Labs PlaygroundからAPIキーを取得します。
ステップ1で作成したインデックスを開き、INDEX_IDを取得します。IDはURLに含まれています:https://playground.twelvelabs.io/indexes/{index_id}
メインファイルとともに、APIキーとINDEX_IDを設定した
.envファイルを作成します。
Twelvelabs_API=your_api_key_here API_URL=your_api_url_here INDEX_ID=your_index_id_here
これらのステップが完了すれば、いよいよアプリケーションの開発に取り組む準備は完了です。
AI面接アナライザー構築のチュートリアル
このチュートリアルでは、最小限のフロントエンドを持つFlaskアプリケーションを構築します。以下に従うべきディレクトリ構造を示します:
. ├── app.py ├── requirements.txt ├── static │ ├── css │ │ └── style.css │ └── js │ └── main.js ├── templates │ └── index.html └── uploads
1 - Flaskアプリケーションの準備
1.1 - アプリケーションのセットアップ:検証・処理、および面接質問の提供
FlaskコンテナのメインとなるPythonコードは app.py ファイルに記述されています。app.pyを理解するために、セットアップ部分とアップロード・ルート部分の2つに分けて説明します。セットアップではアプリケーションを初期化し、アップロード・ルートでは埋め込み(Embedding)エンジンおよび生成(Generative)エンジンとやり取りを行います。以下は app.py のセットアップ部分のコードです:
# Importing the necessary module import os import json import random from flask import Flask, render_template, request, jsonify from twelvelabs import TwelveLabs from twelvelabs.models.task import Task import requests from dotenv import load_dotenv # Load environment variables load_dotenv() # Initialize the Flask app app = Flask(__name__) # Get the API credentials from env variables API_KEY = os.getenv('API_KEY') API_URL = os.getenv('API_URL') index_id = os.getenv('index_id') # Initialize TwelveLabs client client = TwelveLabs(api_key=API_KEY) # List of the Aptitude Interview Questions INTERVIEW_QUESTIONS = [ "Tell me about yourself.", "What are your greatest strengths?", "What do you consider to be your weaknesses?", "Where do you see yourself in five years?", "Why should we hire you?", "What motivates you?", "What are your career goals?", "How do you work in a team?", "What's your leadership style?" ] # Utility function to check API connection def check_api_connection(): try: response = requests.get(API_URL, headers={"x-api-key": API_KEY}) return response.status_code == 200 except requests.RequestException as e: print(f"API connection check failed. Error: {str(e)}") return False # Utitliy function to process API response def process_api_response(data): # Initialize default processed data structure processed_data = { "confidence": "N/A", "clarity": "N/A", "speech_rate": "N/A", "eye_contact": "N/A", "body_language": "N/A", "voice_tone": "N/A", "imp_points": [] } # Handling the string input (convert to dict, if possible) if isinstance(data, str): try: cleaned_data = data.replace("```json", "").replace("```", "").strip() data = json.loads(cleaned_data) except json.JSONDecodeError as e: print(f"Error decoding JSON - {e}") return processed_data # Extract data from dict if isinstance(data, dict): for key in processed_data.keys(): processed_data[key] = data.get(key, "N/A") return processed_data # Main Page Route @app.route('/') def index(): return render_template('index.html') # API route to get a random interview question from list @app.route('/get_question') def get_question(): question = random.choice(INTERVIEW_QUESTIONS) return jsonify({"question": question})
セットアップでは、Twelve Labs SDK用のクライアントと応答処理用ユーティリティを初期化します。また、適性に基づいた面接質問の生成を開始します。次の重要なステップは、アプリケーションのコア機能に焦点を当てた後半部分を確認することです。
1.2 - アップロード・ルートと埋め込みおよび生成エンジンとの対話
# Route for upload @app.route('/upload', methods=['POST']) def upload(): # Checking of the API connection before proceeding if not check_api_connection(): return jsonify({"error": "Failed to connect to the Twelve Labs API."}), 500 # Validate video file in request if 'video' not in request.files: return jsonify({"error": "No video file provided"}), 400 video = request.files['video'] if video.filename == '': return jsonify({"error": "No video file selected"}), 400 # Save uploaded video file video_path = os.path.join('uploads', 'interview.mp4') video.save(video_path) # Verifying whether the video file was saved successfully if not os.path.exists(video_path): return jsonify({"error": "Failed to save video file"}), 500 file_size = os.path.getsize(video_path) print(f"Uploaded video file size: {file_size} bytes") if file_size == 0: return jsonify({"error": "Uploaded video file is empty"}), 500 try: # Create and wait for indexing task task = client.task.create(index_id=index_id, file=video_path) task.wait_for_done(sleep_interval=5) if task.status != "ready": return jsonify({"error": f"Indexing failed with status {task.status}"}), 500 # Generate text analysis from the video result = client.generate.text( video_id=task.video_id, prompt="""You're an Interviewer, Analyze the video clip of the interview answer for the question - {question}. If the face is not present in the video then do provide the lower points in all categories, Do provide less than 5 for all the other categories if the face is not visible in the video. Do provide the response in the json format with the number assigned as the value. After analyzing from 1-10. The keys of the json as confidence, clarity, speech_rate, eye_contact, body_language, voice_tone, relevant_to_question, imp_points. The imp_points will contain the exact sentence in a summarized points by the speaker, also do remove the filler words and provide it in a list format which is important from video.""" ) # Process and return the API response print("Raw API Response:", json.dumps(result.data, indent=2)) processed_data = process_api_response(result.data) print("Processed data:", json.dumps(processed_data, indent=2)) return jsonify(processed_data), 200 except Exception as e: # To Handle any errors during processing print(f"Error processing video: {str(e)}") return jsonify({"error": f"Error processing video: {str(e)}"}), 500 # Run the Flask app if __name__ == '__main__': os.makedirs('uploads', exist_ok=True) # To ensure whether the uploads directory exists app.run(debug=True)
アップロード・ルートは、Twelve Labs APIを活用して面接ビデオのアップロードおよび処理を行い、ビデオ分析を実行します。ビデオが保存されると、Marengo 2.6 (Embedding Engine)を使用してビデオをインデックス登録するタスクが作成されます。その後、インデックス登録されたビデオは、Pegasus 1.1 (Generative Engine)を搭載した /generate エンドポイントからアクセスされます。
システムはビデオを分析し、簡潔なプロンプトの記述に基づいて回答を提供します。プロンプトの構成は、特徴(評価基準)、目的、および出力形式の3つの要素から成り立っています。
2 - 操作フロー用のシンプルなフロントエンドコード
AI Interview Analyzer ウェブアプリケーションのHTML構造は、スタート(start)、面接(interview)、結果(result)の3つの主要セクションから構成されるシングルページアプリケーションです。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>AI Interview Analyzer</title> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600&display=swap" rel="stylesheet"> <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}"> </head> <body> <div class="container"> <header> <h1>AI Interview Analyzer</h1> <p>Enhance Your Interview Skills with AI Powered Feedback</p> </header> <main> <section id="start-section" class="card"> <h2>Ready to Start Your Interview?</h2> <p>Click the button below to begin your AI powered interview experience.</vp> <button id="startInterviewButton" class="btn btn-primary">Start Interview</button> </section> <section id="interview-section" class="card hidden"> <div id="question-container"> <h2>Interview Question -</h2> <p id="interview-question"></p> </div> <div id="timer-container"> <div id="preparation-timer" class="timer"> Preparation Time - <span id="prep-time">00:20</span> </div> <div id="recording-timer" class="timer hidden"> Recording Time - <span id="rec-time">00:30</span> </div> </div> <div id="video-container" class="hidden"> <video id="videoElement" autoplay muted></video> </div> <div id="status-message" class="status"></div> </section> <section id="result-section" class="card hidden"> <h2>Analysis Results</h2> <div id="result" class="result-container"> <div id="loading-message" class="loading">Analyzing your response...</div> <small>It may take around 3-4 mins</small> </div> </section> </main> </div> <script src="{{ url_for('static', filename='js/main.js') }}"></script> </body> </html>
最小限の構成要素であるフロントエンドセクションは以下の通りです:
スタート・セクション(Start Section): ページを読み込んだときに最初に表示される画面です。歓迎メッセージと面接プロセスを開始するボタンが含まれています。
面接セクション(Interview Section): 面接が始まると表示されます。現在の質問、準備と録画のタイマー、およびユーザーのカメラ映像が表示されます。
結果セクション(Result Section): 面接完了後、AIによる分析結果を表示します。
この構造は、面接の開始からAIのフィードバック取得まで、ステップバイステップの面接プロセスを構成しています。面接ステージ間のスムーズな切り替えは、後述する main.js 内のJavaScriptコードによって管理される非表示用の hidden クラスによって制御されています。
アプリケーションのスタイリングに関しては、こちらから style.css を取得できます。
3 - JavaScriptにおける録画機能とエラーハンドリング
以下のJavaScriptコードスニペットは main.js で、ブラウザ環境におけるメディア録画の設定、分析結果のテキスト処理、およびエラーハンドリングを含んでいます。
document.addEventListener('DOMContentLoaded', () => { // Object to store all DOM elements const elements = { startButton: document.getElementById('startInterviewButton'), sections: { start: document.getElementById('start-section'), interview: document.getElementById('interview-section'), result: document.getElementById('result-section') }, video: document.getElementById('videoElement'), videoContainer: document.getElementById('video-container'), question: document.getElementById('interview-question'), timers: { prep: document.getElementById('preparation-timer'), rec: document.getElementById('recording-timer') }, timerDisplays: { prep: document.getElementById('prep-time'), rec: document.getElementById('rec-time') }, status: document.getElementById('status-message'), result: document.getElementById('result') }; // Variables for media recording let mediaRecorder; let recordedChunks = []; const prepTime = 20; // Interview Preparation time in seconds const recTime = 30; // Video Interview Recording time in seconds let currentTimer; // Function to set up camera and media recorder async function setupCamera() { try { // Request access to user's camera and microphone const stream = await navigator.mediaDevices.getUserMedia({ video: { width: 640, height: 480 }, audio: true }); elements.video.srcObject = stream; // Set up media recorder const options = { mimeType: 'video/mp4' }; mediaRecorder = new MediaRecorder(stream, options); // Event handler for when data is available mediaRecorder.ondataavailable = (event) => { if (event.data.size > 0) { recordedChunks.push(event.data); } }; // Event handler for when recording stops mediaRecorder.onstop = () => { const blob = new Blob(recordedChunks, { type: 'video/mp4' }); console.log('Recording stopped. Blob size:', blob.size, 'bytes'); if (blob.size > 0) { uploadVideo(blob); } else { showError("Recording failed: No data captured."); } }; } catch (error) { console.error('Error accessing camera:', error); showError('Unable to access camera. Please ensure you have given permission and try again.'); } } // Utility function to show a specific section and hide others function showSection(section) { Object.values(elements.sections).forEach(s => s.classList.add('hidden')); elements.sections[section].classList.remove('hidden'); } // Utility function to update timer display function updateTimer(timerElement, time) { const minutes = Math.floor(time / 60); const seconds = time % 60; timerElement.textContent = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; } // Function to start and manage timer function startTimer(phase) { let timeLeft = phase === 'prep' ? prepTime : recTime; updateTimer(elements.timerDisplays[phase], timeLeft); elements.timers[phase].classList.remove('hidden'); return setInterval(() => { timeLeft--; updateTimer(elements.timerDisplays[phase], timeLeft); if (timeLeft <= 0) { clearInterval(currentTimer); elements.timers[phase].classList.add('hidden'); if (phase === 'prep') startRecording(); else stopRecording(); } }, 1000); } // Function to start preparation timer function startPreparationTimer() { showSection('interview'); elements.status.textContent = "Prepare your answer..."; currentTimer = startTimer('prep'); } // Utitlity function to start recording function startRecording() { elements.videoContainer.classList.remove('hidden'); recordedChunks = []; mediaRecorder.start(1000); // Record in 1-second chunks elements.status.textContent = "Recording in progress..."; currentTimer = startTimer('rec'); console.log('Recording started'); } // Utitlity function to stop recording function stopRecording() { mediaRecorder.stop(); elements.status.textContent = "Processing your response..."; showSection('result'); console.log('Recording stopped'); } // Utitlity function to upload recorded video function uploadVideo(blob) { console.log('Uploading video. Blob size:', blob.size, 'bytes'); const formData = new FormData(); formData.append('video', blob, 'interview.mp4'); fetch('/upload', { method: 'POST', body: formData }) .then(response => { if (!response.ok) { return response.json().then(err => { throw new Error(err.error || `HTTP error! status: ${response.status}`); }); } return response.json(); }) .then(data => { console.log('Received data:', data); displayResults(data); }) .catch(error => { console.error('Error:', error); showError(error.message); }); } // Utitlity function to display analysis results function displayResults(data) { let resultHTML = '<h3>Analysis Results:</h3>'; if (data.error) { resultHTML += `<p class="error">Error: ${data.error}</p>`; } else { resultHTML += '<div class="score-grid">'; const metrics = [ { key: 'confidence', label: 'Confidence' }, { key: 'clarity', label: 'Clarity' }, { key: 'speech_rate', label: 'Speech Rate' }, { key: 'eye_contact', label: 'Eye Contact' }, { key: 'body_language', label: 'Body Language' }, { key: 'voice_tone', label: 'Voice Tone' } ]; metrics.forEach(metric => { resultHTML += ` <div class="score"> <span class="score-label">${metric.label}</span> <span class="score-value">${data[metric.key]}/10</span> </div> `; }); resultHTML += '</div>'; if (data.imp_points && data.imp_points.length > 0) { resultHTML += '<h4>Key Points:</h4><ul>'; data.imp_points.forEach(point => { resultHTML += `<li>${point}</li>`; }); resultHTML += '</ul>'; } else { resultHTML += '<p>No key points found in the analysis.</p>'; } } elements.result.innerHTML = resultHTML; } // Utitlity function to display error messages function showError(message) { elements.result.innerHTML = ` <p class="error">Error: ${message}</p> <p>Please try again. If the problem persists, ensure you're recording for the full time and that your video and audio are working correctly.</p> `; } elements.startButton.addEventListener('click', () => { setupCamera().then(() => { fetch('/get_question') .then(response => response.json()) .then(data => { elements.question.textContent = data.question; startPreparationTimer(); }) .catch(error => { console.error('Error fetching question:', error); showError('Failed to fetch interview question. Please try again.'); }); }); }); });
このコードは、ユーザーのカメラとマイクを設定し(setupCamera())、MediaRecorder APIを使用してビデオ録画を管理します。
setupCamera()関数は、ユーザーのカメラとマイクを設定し、MediaRecorder APIを使用してビデオ録画を管理します。ユーザーにストレスのない構造化された面接体験を提供するため、アプリケーションには準備用タイマーと録画用タイマーが実装されています(
startTimer()、startPreparationTimer())。startRecording()やstopRecording()といった関数が、面接の流れを制御します。uploadVideo()関数は、録画されたビデオをサーバーにアップロードし、Pegasus 1.1による分析を用いたインデックス登録およびテキスト生成を行います。面接パフォーマンスのスコアや重要なポイントを含むAIからのフィードバックは、
displayResults()を使用してレンダリングされます。
スタートボタンのイベントリスナーは、クリックされたときに面接のすべてのプロセスを開始します。このスクリプトは、showError() 関数を通じて堅牢なエラーハンドリングを提供し、ユーザーに明確なフィードバックを提示します。
面接ビデオの録画が完了すると、AI Interview Analyzerの出力結果を確認することができます。

これでオブジェクト指向のアプリケーションが利用可能になりました。ぜひ様々なプロンプトを試してみてください。
このチュートリアルを応用するためのその他のアイデア
アプリケーションの動作手順と開発方法を理解することで、革新的なアイデアを実装し、ユーザーのニーズに応えるプロダクトを作成する準備が整います。このチュートリアルブログの内容をベースにして構築できる、同じようなユースケースのアイデアをいくつか紹介します:
📚️ 面接準備(Interview Preparation): 求職者や就職活動中のユーザーが、現実的な環境で面接スキルを練習し、ブラッシュアップするためにAI Interview Analyzerを使用できます。
🤝 採用プロセスの強化(Hiring Process Enhancement): 採用担当者や人事マネージャーは、AI Interview Analyzerを使用して採用プロセスをストリームライン化(効率化)し、より効果的な選考を行うことができます。
🎓 従業員のスキル開発(Employee Skill Development): 社内研修や人材開発を行う部署は、社員のインタービュースキル(プレゼンスキルなど)を向上させるために、このAI Interview Analyzerをプログラムに組み込むことができます。
😊️ 肯定的な候補者体験(Positive Candidate Experience): 求職者はAI Interview Analyzerから貴重な洞察やフィードバックを直接得られるため、採用選考における候補者体験をより好ましいものに改善できます。
結論
このブログ記事は、Twelve Labsを用いて開発された「AI Interview Analyzer」の動作手順とその開発プロセスについて、詳細な解説を提供することを目的としています。チュートリアルを最後までお読みいただきありがとうございました。ユーザー体験をさらに向上させ、様々な課題を解決する皆さんのクリエイティブなアイデアを楽しみにしています。
面接室に入ったとき、あなたの何気ない仕草、言葉、そして表情が単に観察されるだけでなく、深く理解されることを想像してみてください。AIがあなた専属のコーチや相談相手となる、未来の就職面接へようこそ。「AI Interview Analyzer(AI面接アナライザー)」が登場し、求職者にとっても採用担当者にとっても、このプロセスに革命を起こそうとしています。
今日の競争が激しい就職活動市場において、重要な面接をうまくこなせるかどうかは、憧れの仕事を手に入れられるか、それとも手の届かないものになってしまうかの分かれ目となります。しかし、もしあなたに秘密兵器があったらどうでしょうか?あなたのパフォーマンスを分析し、強みを浮き彫りにし、改善すべき点を優しく提案してくれるツールがあったら?
まさにそれを実現するのが「AI Interview Analyzer」です。Twelve LabsのPegasus-1モデルを使用することで、かつては熟練した人事プロフェッショナルしか持ち得なかった洞察を提供します。
アプリケーションのデモはこちらで体験できます:Interview Analyzer アプリケーション。また、こちらのReplitテンプレートから実際に動かしてみることも可能です。
前提条件
Twelve Labs Playgroundにサインアップして、APIキーを生成します。
ノートブックとこのアプリケーションのリポジトリは、GitHubにあります。
このチュートリアルのFlaskアプリケーションは、Python、HTML、CSS、JavaScriptを使用しています。

アプリケーションの仕組み
このセクションでは、Twelve Labsを活用した面接準備アプリケーションの開発および利用における、アプリケーションのフロー概要を説明します。
面接プロセスは、ユーザーの準備を促すために、限られた時間だけランダムな適性検査の質問が表示されるところから始まります。ユーザーが回答を終えると、アプリケーションは設定された時間、自動的に面接の録画を開始します。録画終了後、ビデオは即座に処理され、Marengo 2.6 (Embedding Engine)を用いてインデックス登録へと送られます。

Twelve Labsの生成エンジンであるPegasus 1.1に対して実行されたインストラクション・プロンプティングに基づき、ビデオ面接の評価に用いられる様々なパラメータを分析・スコア化します。これらのパラメータには、自信、明瞭さ、視線(アイコンタクト)、ボディーランゲージ、発話速度、声のトーンが含まれます。さらに、面接中に議論されたすべての重要なポイントを書き留めます。企業や組織はこのアプリケーションを活用することで、多数の候補者に対して自動面接を実施し、時間を節約することができます。あるいは、面接練習のフィードバックを提供し、ユーザーがパフォーマンスを向上させるのを助けることもできます。
準備ステップ

Twelve Labs Playgroundにサインアップし、インデックス(Index)を作成します。
分類に合わせて適切なオプションを選択します:(1) ビデオ検索・分類用の「Marengo 2.6 (Embedding Engine)」、または (2) ビデオからテキストへの生成用の「Pegasus 1.1」。これらのエンジンは、ビデオ理解のための強固な基盤を提供します。
Twelve Labs PlaygroundからAPIキーを取得します。
ステップ1で作成したインデックスを開き、INDEX_IDを取得します。IDはURLに含まれています:https://playground.twelvelabs.io/indexes/{index_id}
メインファイルとともに、APIキーとINDEX_IDを設定した
.envファイルを作成します。
Twelvelabs_API=your_api_key_here API_URL=your_api_url_here INDEX_ID=your_index_id_here
これらのステップが完了すれば、いよいよアプリケーションの開発に取り組む準備は完了です。
AI面接アナライザー構築のチュートリアル
このチュートリアルでは、最小限のフロントエンドを持つFlaskアプリケーションを構築します。以下に従うべきディレクトリ構造を示します:
. ├── app.py ├── requirements.txt ├── static │ ├── css │ │ └── style.css │ └── js │ └── main.js ├── templates │ └── index.html └── uploads
1 - Flaskアプリケーションの準備
1.1 - アプリケーションのセットアップ:検証・処理、および面接質問の提供
FlaskコンテナのメインとなるPythonコードは app.py ファイルに記述されています。app.pyを理解するために、セットアップ部分とアップロード・ルート部分の2つに分けて説明します。セットアップではアプリケーションを初期化し、アップロード・ルートでは埋め込み(Embedding)エンジンおよび生成(Generative)エンジンとやり取りを行います。以下は app.py のセットアップ部分のコードです:
# Importing the necessary module import os import json import random from flask import Flask, render_template, request, jsonify from twelvelabs import TwelveLabs from twelvelabs.models.task import Task import requests from dotenv import load_dotenv # Load environment variables load_dotenv() # Initialize the Flask app app = Flask(__name__) # Get the API credentials from env variables API_KEY = os.getenv('API_KEY') API_URL = os.getenv('API_URL') index_id = os.getenv('index_id') # Initialize TwelveLabs client client = TwelveLabs(api_key=API_KEY) # List of the Aptitude Interview Questions INTERVIEW_QUESTIONS = [ "Tell me about yourself.", "What are your greatest strengths?", "What do you consider to be your weaknesses?", "Where do you see yourself in five years?", "Why should we hire you?", "What motivates you?", "What are your career goals?", "How do you work in a team?", "What's your leadership style?" ] # Utility function to check API connection def check_api_connection(): try: response = requests.get(API_URL, headers={"x-api-key": API_KEY}) return response.status_code == 200 except requests.RequestException as e: print(f"API connection check failed. Error: {str(e)}") return False # Utitliy function to process API response def process_api_response(data): # Initialize default processed data structure processed_data = { "confidence": "N/A", "clarity": "N/A", "speech_rate": "N/A", "eye_contact": "N/A", "body_language": "N/A", "voice_tone": "N/A", "imp_points": [] } # Handling the string input (convert to dict, if possible) if isinstance(data, str): try: cleaned_data = data.replace("```json", "").replace("```", "").strip() data = json.loads(cleaned_data) except json.JSONDecodeError as e: print(f"Error decoding JSON - {e}") return processed_data # Extract data from dict if isinstance(data, dict): for key in processed_data.keys(): processed_data[key] = data.get(key, "N/A") return processed_data # Main Page Route @app.route('/') def index(): return render_template('index.html') # API route to get a random interview question from list @app.route('/get_question') def get_question(): question = random.choice(INTERVIEW_QUESTIONS) return jsonify({"question": question})
セットアップでは、Twelve Labs SDK用のクライアントと応答処理用ユーティリティを初期化します。また、適性に基づいた面接質問の生成を開始します。次の重要なステップは、アプリケーションのコア機能に焦点を当てた後半部分を確認することです。
1.2 - アップロード・ルートと埋め込みおよび生成エンジンとの対話
# Route for upload @app.route('/upload', methods=['POST']) def upload(): # Checking of the API connection before proceeding if not check_api_connection(): return jsonify({"error": "Failed to connect to the Twelve Labs API."}), 500 # Validate video file in request if 'video' not in request.files: return jsonify({"error": "No video file provided"}), 400 video = request.files['video'] if video.filename == '': return jsonify({"error": "No video file selected"}), 400 # Save uploaded video file video_path = os.path.join('uploads', 'interview.mp4') video.save(video_path) # Verifying whether the video file was saved successfully if not os.path.exists(video_path): return jsonify({"error": "Failed to save video file"}), 500 file_size = os.path.getsize(video_path) print(f"Uploaded video file size: {file_size} bytes") if file_size == 0: return jsonify({"error": "Uploaded video file is empty"}), 500 try: # Create and wait for indexing task task = client.task.create(index_id=index_id, file=video_path) task.wait_for_done(sleep_interval=5) if task.status != "ready": return jsonify({"error": f"Indexing failed with status {task.status}"}), 500 # Generate text analysis from the video result = client.generate.text( video_id=task.video_id, prompt="""You're an Interviewer, Analyze the video clip of the interview answer for the question - {question}. If the face is not present in the video then do provide the lower points in all categories, Do provide less than 5 for all the other categories if the face is not visible in the video. Do provide the response in the json format with the number assigned as the value. After analyzing from 1-10. The keys of the json as confidence, clarity, speech_rate, eye_contact, body_language, voice_tone, relevant_to_question, imp_points. The imp_points will contain the exact sentence in a summarized points by the speaker, also do remove the filler words and provide it in a list format which is important from video.""" ) # Process and return the API response print("Raw API Response:", json.dumps(result.data, indent=2)) processed_data = process_api_response(result.data) print("Processed data:", json.dumps(processed_data, indent=2)) return jsonify(processed_data), 200 except Exception as e: # To Handle any errors during processing print(f"Error processing video: {str(e)}") return jsonify({"error": f"Error processing video: {str(e)}"}), 500 # Run the Flask app if __name__ == '__main__': os.makedirs('uploads', exist_ok=True) # To ensure whether the uploads directory exists app.run(debug=True)
アップロード・ルートは、Twelve Labs APIを活用して面接ビデオのアップロードおよび処理を行い、ビデオ分析を実行します。ビデオが保存されると、Marengo 2.6 (Embedding Engine)を使用してビデオをインデックス登録するタスクが作成されます。その後、インデックス登録されたビデオは、Pegasus 1.1 (Generative Engine)を搭載した /generate エンドポイントからアクセスされます。
システムはビデオを分析し、簡潔なプロンプトの記述に基づいて回答を提供します。プロンプトの構成は、特徴(評価基準)、目的、および出力形式の3つの要素から成り立っています。
2 - 操作フロー用のシンプルなフロントエンドコード
AI Interview Analyzer ウェブアプリケーションのHTML構造は、スタート(start)、面接(interview)、結果(result)の3つの主要セクションから構成されるシングルページアプリケーションです。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>AI Interview Analyzer</title> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600&display=swap" rel="stylesheet"> <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}"> </head> <body> <div class="container"> <header> <h1>AI Interview Analyzer</h1> <p>Enhance Your Interview Skills with AI Powered Feedback</p> </header> <main> <section id="start-section" class="card"> <h2>Ready to Start Your Interview?</h2> <p>Click the button below to begin your AI powered interview experience.</vp> <button id="startInterviewButton" class="btn btn-primary">Start Interview</button> </section> <section id="interview-section" class="card hidden"> <div id="question-container"> <h2>Interview Question -</h2> <p id="interview-question"></p> </div> <div id="timer-container"> <div id="preparation-timer" class="timer"> Preparation Time - <span id="prep-time">00:20</span> </div> <div id="recording-timer" class="timer hidden"> Recording Time - <span id="rec-time">00:30</span> </div> </div> <div id="video-container" class="hidden"> <video id="videoElement" autoplay muted></video> </div> <div id="status-message" class="status"></div> </section> <section id="result-section" class="card hidden"> <h2>Analysis Results</h2> <div id="result" class="result-container"> <div id="loading-message" class="loading">Analyzing your response...</div> <small>It may take around 3-4 mins</small> </div> </section> </main> </div> <script src="{{ url_for('static', filename='js/main.js') }}"></script> </body> </html>
最小限の構成要素であるフロントエンドセクションは以下の通りです:
スタート・セクション(Start Section): ページを読み込んだときに最初に表示される画面です。歓迎メッセージと面接プロセスを開始するボタンが含まれています。
面接セクション(Interview Section): 面接が始まると表示されます。現在の質問、準備と録画のタイマー、およびユーザーのカメラ映像が表示されます。
結果セクション(Result Section): 面接完了後、AIによる分析結果を表示します。
この構造は、面接の開始からAIのフィードバック取得まで、ステップバイステップの面接プロセスを構成しています。面接ステージ間のスムーズな切り替えは、後述する main.js 内のJavaScriptコードによって管理される非表示用の hidden クラスによって制御されています。
アプリケーションのスタイリングに関しては、こちらから style.css を取得できます。
3 - JavaScriptにおける録画機能とエラーハンドリング
以下のJavaScriptコードスニペットは main.js で、ブラウザ環境におけるメディア録画の設定、分析結果のテキスト処理、およびエラーハンドリングを含んでいます。
document.addEventListener('DOMContentLoaded', () => { // Object to store all DOM elements const elements = { startButton: document.getElementById('startInterviewButton'), sections: { start: document.getElementById('start-section'), interview: document.getElementById('interview-section'), result: document.getElementById('result-section') }, video: document.getElementById('videoElement'), videoContainer: document.getElementById('video-container'), question: document.getElementById('interview-question'), timers: { prep: document.getElementById('preparation-timer'), rec: document.getElementById('recording-timer') }, timerDisplays: { prep: document.getElementById('prep-time'), rec: document.getElementById('rec-time') }, status: document.getElementById('status-message'), result: document.getElementById('result') }; // Variables for media recording let mediaRecorder; let recordedChunks = []; const prepTime = 20; // Interview Preparation time in seconds const recTime = 30; // Video Interview Recording time in seconds let currentTimer; // Function to set up camera and media recorder async function setupCamera() { try { // Request access to user's camera and microphone const stream = await navigator.mediaDevices.getUserMedia({ video: { width: 640, height: 480 }, audio: true }); elements.video.srcObject = stream; // Set up media recorder const options = { mimeType: 'video/mp4' }; mediaRecorder = new MediaRecorder(stream, options); // Event handler for when data is available mediaRecorder.ondataavailable = (event) => { if (event.data.size > 0) { recordedChunks.push(event.data); } }; // Event handler for when recording stops mediaRecorder.onstop = () => { const blob = new Blob(recordedChunks, { type: 'video/mp4' }); console.log('Recording stopped. Blob size:', blob.size, 'bytes'); if (blob.size > 0) { uploadVideo(blob); } else { showError("Recording failed: No data captured."); } }; } catch (error) { console.error('Error accessing camera:', error); showError('Unable to access camera. Please ensure you have given permission and try again.'); } } // Utility function to show a specific section and hide others function showSection(section) { Object.values(elements.sections).forEach(s => s.classList.add('hidden')); elements.sections[section].classList.remove('hidden'); } // Utility function to update timer display function updateTimer(timerElement, time) { const minutes = Math.floor(time / 60); const seconds = time % 60; timerElement.textContent = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; } // Function to start and manage timer function startTimer(phase) { let timeLeft = phase === 'prep' ? prepTime : recTime; updateTimer(elements.timerDisplays[phase], timeLeft); elements.timers[phase].classList.remove('hidden'); return setInterval(() => { timeLeft--; updateTimer(elements.timerDisplays[phase], timeLeft); if (timeLeft <= 0) { clearInterval(currentTimer); elements.timers[phase].classList.add('hidden'); if (phase === 'prep') startRecording(); else stopRecording(); } }, 1000); } // Function to start preparation timer function startPreparationTimer() { showSection('interview'); elements.status.textContent = "Prepare your answer..."; currentTimer = startTimer('prep'); } // Utitlity function to start recording function startRecording() { elements.videoContainer.classList.remove('hidden'); recordedChunks = []; mediaRecorder.start(1000); // Record in 1-second chunks elements.status.textContent = "Recording in progress..."; currentTimer = startTimer('rec'); console.log('Recording started'); } // Utitlity function to stop recording function stopRecording() { mediaRecorder.stop(); elements.status.textContent = "Processing your response..."; showSection('result'); console.log('Recording stopped'); } // Utitlity function to upload recorded video function uploadVideo(blob) { console.log('Uploading video. Blob size:', blob.size, 'bytes'); const formData = new FormData(); formData.append('video', blob, 'interview.mp4'); fetch('/upload', { method: 'POST', body: formData }) .then(response => { if (!response.ok) { return response.json().then(err => { throw new Error(err.error || `HTTP error! status: ${response.status}`); }); } return response.json(); }) .then(data => { console.log('Received data:', data); displayResults(data); }) .catch(error => { console.error('Error:', error); showError(error.message); }); } // Utitlity function to display analysis results function displayResults(data) { let resultHTML = '<h3>Analysis Results:</h3>'; if (data.error) { resultHTML += `<p class="error">Error: ${data.error}</p>`; } else { resultHTML += '<div class="score-grid">'; const metrics = [ { key: 'confidence', label: 'Confidence' }, { key: 'clarity', label: 'Clarity' }, { key: 'speech_rate', label: 'Speech Rate' }, { key: 'eye_contact', label: 'Eye Contact' }, { key: 'body_language', label: 'Body Language' }, { key: 'voice_tone', label: 'Voice Tone' } ]; metrics.forEach(metric => { resultHTML += ` <div class="score"> <span class="score-label">${metric.label}</span> <span class="score-value">${data[metric.key]}/10</span> </div> `; }); resultHTML += '</div>'; if (data.imp_points && data.imp_points.length > 0) { resultHTML += '<h4>Key Points:</h4><ul>'; data.imp_points.forEach(point => { resultHTML += `<li>${point}</li>`; }); resultHTML += '</ul>'; } else { resultHTML += '<p>No key points found in the analysis.</p>'; } } elements.result.innerHTML = resultHTML; } // Utitlity function to display error messages function showError(message) { elements.result.innerHTML = ` <p class="error">Error: ${message}</p> <p>Please try again. If the problem persists, ensure you're recording for the full time and that your video and audio are working correctly.</p> `; } elements.startButton.addEventListener('click', () => { setupCamera().then(() => { fetch('/get_question') .then(response => response.json()) .then(data => { elements.question.textContent = data.question; startPreparationTimer(); }) .catch(error => { console.error('Error fetching question:', error); showError('Failed to fetch interview question. Please try again.'); }); }); }); });
このコードは、ユーザーのカメラとマイクを設定し(setupCamera())、MediaRecorder APIを使用してビデオ録画を管理します。
setupCamera()関数は、ユーザーのカメラとマイクを設定し、MediaRecorder APIを使用してビデオ録画を管理します。ユーザーにストレスのない構造化された面接体験を提供するため、アプリケーションには準備用タイマーと録画用タイマーが実装されています(
startTimer()、startPreparationTimer())。startRecording()やstopRecording()といった関数が、面接の流れを制御します。uploadVideo()関数は、録画されたビデオをサーバーにアップロードし、Pegasus 1.1による分析を用いたインデックス登録およびテキスト生成を行います。面接パフォーマンスのスコアや重要なポイントを含むAIからのフィードバックは、
displayResults()を使用してレンダリングされます。
スタートボタンのイベントリスナーは、クリックされたときに面接のすべてのプロセスを開始します。このスクリプトは、showError() 関数を通じて堅牢なエラーハンドリングを提供し、ユーザーに明確なフィードバックを提示します。
面接ビデオの録画が完了すると、AI Interview Analyzerの出力結果を確認することができます。

これでオブジェクト指向のアプリケーションが利用可能になりました。ぜひ様々なプロンプトを試してみてください。
このチュートリアルを応用するためのその他のアイデア
アプリケーションの動作手順と開発方法を理解することで、革新的なアイデアを実装し、ユーザーのニーズに応えるプロダクトを作成する準備が整います。このチュートリアルブログの内容をベースにして構築できる、同じようなユースケースのアイデアをいくつか紹介します:
📚️ 面接準備(Interview Preparation): 求職者や就職活動中のユーザーが、現実的な環境で面接スキルを練習し、ブラッシュアップするためにAI Interview Analyzerを使用できます。
🤝 採用プロセスの強化(Hiring Process Enhancement): 採用担当者や人事マネージャーは、AI Interview Analyzerを使用して採用プロセスをストリームライン化(効率化)し、より効果的な選考を行うことができます。
🎓 従業員のスキル開発(Employee Skill Development): 社内研修や人材開発を行う部署は、社員のインタービュースキル(プレゼンスキルなど)を向上させるために、このAI Interview Analyzerをプログラムに組み込むことができます。
😊️ 肯定的な候補者体験(Positive Candidate Experience): 求職者はAI Interview Analyzerから貴重な洞察やフィードバックを直接得られるため、採用選考における候補者体験をより好ましいものに改善できます。
結論
このブログ記事は、Twelve Labsを用いて開発された「AI Interview Analyzer」の動作手順とその開発プロセスについて、詳細な解説を提供することを目的としています。チュートリアルを最後までお読みいただきありがとうございました。ユーザー体験をさらに向上させ、様々な課題を解決する皆さんのクリエイティブなアイデアを楽しみにしています。
面接室に入ったとき、あなたの何気ない仕草、言葉、そして表情が単に観察されるだけでなく、深く理解されることを想像してみてください。AIがあなた専属のコーチや相談相手となる、未来の就職面接へようこそ。「AI Interview Analyzer(AI面接アナライザー)」が登場し、求職者にとっても採用担当者にとっても、このプロセスに革命を起こそうとしています。
今日の競争が激しい就職活動市場において、重要な面接をうまくこなせるかどうかは、憧れの仕事を手に入れられるか、それとも手の届かないものになってしまうかの分かれ目となります。しかし、もしあなたに秘密兵器があったらどうでしょうか?あなたのパフォーマンスを分析し、強みを浮き彫りにし、改善すべき点を優しく提案してくれるツールがあったら?
まさにそれを実現するのが「AI Interview Analyzer」です。Twelve LabsのPegasus-1モデルを使用することで、かつては熟練した人事プロフェッショナルしか持ち得なかった洞察を提供します。
アプリケーションのデモはこちらで体験できます:Interview Analyzer アプリケーション。また、こちらのReplitテンプレートから実際に動かしてみることも可能です。
前提条件
Twelve Labs Playgroundにサインアップして、APIキーを生成します。
ノートブックとこのアプリケーションのリポジトリは、GitHubにあります。
このチュートリアルのFlaskアプリケーションは、Python、HTML、CSS、JavaScriptを使用しています。

アプリケーションの仕組み
このセクションでは、Twelve Labsを活用した面接準備アプリケーションの開発および利用における、アプリケーションのフロー概要を説明します。
面接プロセスは、ユーザーの準備を促すために、限られた時間だけランダムな適性検査の質問が表示されるところから始まります。ユーザーが回答を終えると、アプリケーションは設定された時間、自動的に面接の録画を開始します。録画終了後、ビデオは即座に処理され、Marengo 2.6 (Embedding Engine)を用いてインデックス登録へと送られます。

Twelve Labsの生成エンジンであるPegasus 1.1に対して実行されたインストラクション・プロンプティングに基づき、ビデオ面接の評価に用いられる様々なパラメータを分析・スコア化します。これらのパラメータには、自信、明瞭さ、視線(アイコンタクト)、ボディーランゲージ、発話速度、声のトーンが含まれます。さらに、面接中に議論されたすべての重要なポイントを書き留めます。企業や組織はこのアプリケーションを活用することで、多数の候補者に対して自動面接を実施し、時間を節約することができます。あるいは、面接練習のフィードバックを提供し、ユーザーがパフォーマンスを向上させるのを助けることもできます。
準備ステップ

Twelve Labs Playgroundにサインアップし、インデックス(Index)を作成します。
分類に合わせて適切なオプションを選択します:(1) ビデオ検索・分類用の「Marengo 2.6 (Embedding Engine)」、または (2) ビデオからテキストへの生成用の「Pegasus 1.1」。これらのエンジンは、ビデオ理解のための強固な基盤を提供します。
Twelve Labs PlaygroundからAPIキーを取得します。
ステップ1で作成したインデックスを開き、INDEX_IDを取得します。IDはURLに含まれています:https://playground.twelvelabs.io/indexes/{index_id}
メインファイルとともに、APIキーとINDEX_IDを設定した
.envファイルを作成します。
Twelvelabs_API=your_api_key_here API_URL=your_api_url_here INDEX_ID=your_index_id_here
これらのステップが完了すれば、いよいよアプリケーションの開発に取り組む準備は完了です。
AI面接アナライザー構築のチュートリアル
このチュートリアルでは、最小限のフロントエンドを持つFlaskアプリケーションを構築します。以下に従うべきディレクトリ構造を示します:
. ├── app.py ├── requirements.txt ├── static │ ├── css │ │ └── style.css │ └── js │ └── main.js ├── templates │ └── index.html └── uploads
1 - Flaskアプリケーションの準備
1.1 - アプリケーションのセットアップ:検証・処理、および面接質問の提供
FlaskコンテナのメインとなるPythonコードは app.py ファイルに記述されています。app.pyを理解するために、セットアップ部分とアップロード・ルート部分の2つに分けて説明します。セットアップではアプリケーションを初期化し、アップロード・ルートでは埋め込み(Embedding)エンジンおよび生成(Generative)エンジンとやり取りを行います。以下は app.py のセットアップ部分のコードです:
# Importing the necessary module import os import json import random from flask import Flask, render_template, request, jsonify from twelvelabs import TwelveLabs from twelvelabs.models.task import Task import requests from dotenv import load_dotenv # Load environment variables load_dotenv() # Initialize the Flask app app = Flask(__name__) # Get the API credentials from env variables API_KEY = os.getenv('API_KEY') API_URL = os.getenv('API_URL') index_id = os.getenv('index_id') # Initialize TwelveLabs client client = TwelveLabs(api_key=API_KEY) # List of the Aptitude Interview Questions INTERVIEW_QUESTIONS = [ "Tell me about yourself.", "What are your greatest strengths?", "What do you consider to be your weaknesses?", "Where do you see yourself in five years?", "Why should we hire you?", "What motivates you?", "What are your career goals?", "How do you work in a team?", "What's your leadership style?" ] # Utility function to check API connection def check_api_connection(): try: response = requests.get(API_URL, headers={"x-api-key": API_KEY}) return response.status_code == 200 except requests.RequestException as e: print(f"API connection check failed. Error: {str(e)}") return False # Utitliy function to process API response def process_api_response(data): # Initialize default processed data structure processed_data = { "confidence": "N/A", "clarity": "N/A", "speech_rate": "N/A", "eye_contact": "N/A", "body_language": "N/A", "voice_tone": "N/A", "imp_points": [] } # Handling the string input (convert to dict, if possible) if isinstance(data, str): try: cleaned_data = data.replace("```json", "").replace("```", "").strip() data = json.loads(cleaned_data) except json.JSONDecodeError as e: print(f"Error decoding JSON - {e}") return processed_data # Extract data from dict if isinstance(data, dict): for key in processed_data.keys(): processed_data[key] = data.get(key, "N/A") return processed_data # Main Page Route @app.route('/') def index(): return render_template('index.html') # API route to get a random interview question from list @app.route('/get_question') def get_question(): question = random.choice(INTERVIEW_QUESTIONS) return jsonify({"question": question})
セットアップでは、Twelve Labs SDK用のクライアントと応答処理用ユーティリティを初期化します。また、適性に基づいた面接質問の生成を開始します。次の重要なステップは、アプリケーションのコア機能に焦点を当てた後半部分を確認することです。
1.2 - アップロード・ルートと埋め込みおよび生成エンジンとの対話
# Route for upload @app.route('/upload', methods=['POST']) def upload(): # Checking of the API connection before proceeding if not check_api_connection(): return jsonify({"error": "Failed to connect to the Twelve Labs API."}), 500 # Validate video file in request if 'video' not in request.files: return jsonify({"error": "No video file provided"}), 400 video = request.files['video'] if video.filename == '': return jsonify({"error": "No video file selected"}), 400 # Save uploaded video file video_path = os.path.join('uploads', 'interview.mp4') video.save(video_path) # Verifying whether the video file was saved successfully if not os.path.exists(video_path): return jsonify({"error": "Failed to save video file"}), 500 file_size = os.path.getsize(video_path) print(f"Uploaded video file size: {file_size} bytes") if file_size == 0: return jsonify({"error": "Uploaded video file is empty"}), 500 try: # Create and wait for indexing task task = client.task.create(index_id=index_id, file=video_path) task.wait_for_done(sleep_interval=5) if task.status != "ready": return jsonify({"error": f"Indexing failed with status {task.status}"}), 500 # Generate text analysis from the video result = client.generate.text( video_id=task.video_id, prompt="""You're an Interviewer, Analyze the video clip of the interview answer for the question - {question}. If the face is not present in the video then do provide the lower points in all categories, Do provide less than 5 for all the other categories if the face is not visible in the video. Do provide the response in the json format with the number assigned as the value. After analyzing from 1-10. The keys of the json as confidence, clarity, speech_rate, eye_contact, body_language, voice_tone, relevant_to_question, imp_points. The imp_points will contain the exact sentence in a summarized points by the speaker, also do remove the filler words and provide it in a list format which is important from video.""" ) # Process and return the API response print("Raw API Response:", json.dumps(result.data, indent=2)) processed_data = process_api_response(result.data) print("Processed data:", json.dumps(processed_data, indent=2)) return jsonify(processed_data), 200 except Exception as e: # To Handle any errors during processing print(f"Error processing video: {str(e)}") return jsonify({"error": f"Error processing video: {str(e)}"}), 500 # Run the Flask app if __name__ == '__main__': os.makedirs('uploads', exist_ok=True) # To ensure whether the uploads directory exists app.run(debug=True)
アップロード・ルートは、Twelve Labs APIを活用して面接ビデオのアップロードおよび処理を行い、ビデオ分析を実行します。ビデオが保存されると、Marengo 2.6 (Embedding Engine)を使用してビデオをインデックス登録するタスクが作成されます。その後、インデックス登録されたビデオは、Pegasus 1.1 (Generative Engine)を搭載した /generate エンドポイントからアクセスされます。
システムはビデオを分析し、簡潔なプロンプトの記述に基づいて回答を提供します。プロンプトの構成は、特徴(評価基準)、目的、および出力形式の3つの要素から成り立っています。
2 - 操作フロー用のシンプルなフロントエンドコード
AI Interview Analyzer ウェブアプリケーションのHTML構造は、スタート(start)、面接(interview)、結果(result)の3つの主要セクションから構成されるシングルページアプリケーションです。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>AI Interview Analyzer</title> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600&display=swap" rel="stylesheet"> <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}"> </head> <body> <div class="container"> <header> <h1>AI Interview Analyzer</h1> <p>Enhance Your Interview Skills with AI Powered Feedback</p> </header> <main> <section id="start-section" class="card"> <h2>Ready to Start Your Interview?</h2> <p>Click the button below to begin your AI powered interview experience.</vp> <button id="startInterviewButton" class="btn btn-primary">Start Interview</button> </section> <section id="interview-section" class="card hidden"> <div id="question-container"> <h2>Interview Question -</h2> <p id="interview-question"></p> </div> <div id="timer-container"> <div id="preparation-timer" class="timer"> Preparation Time - <span id="prep-time">00:20</span> </div> <div id="recording-timer" class="timer hidden"> Recording Time - <span id="rec-time">00:30</span> </div> </div> <div id="video-container" class="hidden"> <video id="videoElement" autoplay muted></video> </div> <div id="status-message" class="status"></div> </section> <section id="result-section" class="card hidden"> <h2>Analysis Results</h2> <div id="result" class="result-container"> <div id="loading-message" class="loading">Analyzing your response...</div> <small>It may take around 3-4 mins</small> </div> </section> </main> </div> <script src="{{ url_for('static', filename='js/main.js') }}"></script> </body> </html>
最小限の構成要素であるフロントエンドセクションは以下の通りです:
スタート・セクション(Start Section): ページを読み込んだときに最初に表示される画面です。歓迎メッセージと面接プロセスを開始するボタンが含まれています。
面接セクション(Interview Section): 面接が始まると表示されます。現在の質問、準備と録画のタイマー、およびユーザーのカメラ映像が表示されます。
結果セクション(Result Section): 面接完了後、AIによる分析結果を表示します。
この構造は、面接の開始からAIのフィードバック取得まで、ステップバイステップの面接プロセスを構成しています。面接ステージ間のスムーズな切り替えは、後述する main.js 内のJavaScriptコードによって管理される非表示用の hidden クラスによって制御されています。
アプリケーションのスタイリングに関しては、こちらから style.css を取得できます。
3 - JavaScriptにおける録画機能とエラーハンドリング
以下のJavaScriptコードスニペットは main.js で、ブラウザ環境におけるメディア録画の設定、分析結果のテキスト処理、およびエラーハンドリングを含んでいます。
document.addEventListener('DOMContentLoaded', () => { // Object to store all DOM elements const elements = { startButton: document.getElementById('startInterviewButton'), sections: { start: document.getElementById('start-section'), interview: document.getElementById('interview-section'), result: document.getElementById('result-section') }, video: document.getElementById('videoElement'), videoContainer: document.getElementById('video-container'), question: document.getElementById('interview-question'), timers: { prep: document.getElementById('preparation-timer'), rec: document.getElementById('recording-timer') }, timerDisplays: { prep: document.getElementById('prep-time'), rec: document.getElementById('rec-time') }, status: document.getElementById('status-message'), result: document.getElementById('result') }; // Variables for media recording let mediaRecorder; let recordedChunks = []; const prepTime = 20; // Interview Preparation time in seconds const recTime = 30; // Video Interview Recording time in seconds let currentTimer; // Function to set up camera and media recorder async function setupCamera() { try { // Request access to user's camera and microphone const stream = await navigator.mediaDevices.getUserMedia({ video: { width: 640, height: 480 }, audio: true }); elements.video.srcObject = stream; // Set up media recorder const options = { mimeType: 'video/mp4' }; mediaRecorder = new MediaRecorder(stream, options); // Event handler for when data is available mediaRecorder.ondataavailable = (event) => { if (event.data.size > 0) { recordedChunks.push(event.data); } }; // Event handler for when recording stops mediaRecorder.onstop = () => { const blob = new Blob(recordedChunks, { type: 'video/mp4' }); console.log('Recording stopped. Blob size:', blob.size, 'bytes'); if (blob.size > 0) { uploadVideo(blob); } else { showError("Recording failed: No data captured."); } }; } catch (error) { console.error('Error accessing camera:', error); showError('Unable to access camera. Please ensure you have given permission and try again.'); } } // Utility function to show a specific section and hide others function showSection(section) { Object.values(elements.sections).forEach(s => s.classList.add('hidden')); elements.sections[section].classList.remove('hidden'); } // Utility function to update timer display function updateTimer(timerElement, time) { const minutes = Math.floor(time / 60); const seconds = time % 60; timerElement.textContent = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; } // Function to start and manage timer function startTimer(phase) { let timeLeft = phase === 'prep' ? prepTime : recTime; updateTimer(elements.timerDisplays[phase], timeLeft); elements.timers[phase].classList.remove('hidden'); return setInterval(() => { timeLeft--; updateTimer(elements.timerDisplays[phase], timeLeft); if (timeLeft <= 0) { clearInterval(currentTimer); elements.timers[phase].classList.add('hidden'); if (phase === 'prep') startRecording(); else stopRecording(); } }, 1000); } // Function to start preparation timer function startPreparationTimer() { showSection('interview'); elements.status.textContent = "Prepare your answer..."; currentTimer = startTimer('prep'); } // Utitlity function to start recording function startRecording() { elements.videoContainer.classList.remove('hidden'); recordedChunks = []; mediaRecorder.start(1000); // Record in 1-second chunks elements.status.textContent = "Recording in progress..."; currentTimer = startTimer('rec'); console.log('Recording started'); } // Utitlity function to stop recording function stopRecording() { mediaRecorder.stop(); elements.status.textContent = "Processing your response..."; showSection('result'); console.log('Recording stopped'); } // Utitlity function to upload recorded video function uploadVideo(blob) { console.log('Uploading video. Blob size:', blob.size, 'bytes'); const formData = new FormData(); formData.append('video', blob, 'interview.mp4'); fetch('/upload', { method: 'POST', body: formData }) .then(response => { if (!response.ok) { return response.json().then(err => { throw new Error(err.error || `HTTP error! status: ${response.status}`); }); } return response.json(); }) .then(data => { console.log('Received data:', data); displayResults(data); }) .catch(error => { console.error('Error:', error); showError(error.message); }); } // Utitlity function to display analysis results function displayResults(data) { let resultHTML = '<h3>Analysis Results:</h3>'; if (data.error) { resultHTML += `<p class="error">Error: ${data.error}</p>`; } else { resultHTML += '<div class="score-grid">'; const metrics = [ { key: 'confidence', label: 'Confidence' }, { key: 'clarity', label: 'Clarity' }, { key: 'speech_rate', label: 'Speech Rate' }, { key: 'eye_contact', label: 'Eye Contact' }, { key: 'body_language', label: 'Body Language' }, { key: 'voice_tone', label: 'Voice Tone' } ]; metrics.forEach(metric => { resultHTML += ` <div class="score"> <span class="score-label">${metric.label}</span> <span class="score-value">${data[metric.key]}/10</span> </div> `; }); resultHTML += '</div>'; if (data.imp_points && data.imp_points.length > 0) { resultHTML += '<h4>Key Points:</h4><ul>'; data.imp_points.forEach(point => { resultHTML += `<li>${point}</li>`; }); resultHTML += '</ul>'; } else { resultHTML += '<p>No key points found in the analysis.</p>'; } } elements.result.innerHTML = resultHTML; } // Utitlity function to display error messages function showError(message) { elements.result.innerHTML = ` <p class="error">Error: ${message}</p> <p>Please try again. If the problem persists, ensure you're recording for the full time and that your video and audio are working correctly.</p> `; } elements.startButton.addEventListener('click', () => { setupCamera().then(() => { fetch('/get_question') .then(response => response.json()) .then(data => { elements.question.textContent = data.question; startPreparationTimer(); }) .catch(error => { console.error('Error fetching question:', error); showError('Failed to fetch interview question. Please try again.'); }); }); }); });
このコードは、ユーザーのカメラとマイクを設定し(setupCamera())、MediaRecorder APIを使用してビデオ録画を管理します。
setupCamera()関数は、ユーザーのカメラとマイクを設定し、MediaRecorder APIを使用してビデオ録画を管理します。ユーザーにストレスのない構造化された面接体験を提供するため、アプリケーションには準備用タイマーと録画用タイマーが実装されています(
startTimer()、startPreparationTimer())。startRecording()やstopRecording()といった関数が、面接の流れを制御します。uploadVideo()関数は、録画されたビデオをサーバーにアップロードし、Pegasus 1.1による分析を用いたインデックス登録およびテキスト生成を行います。面接パフォーマンスのスコアや重要なポイントを含むAIからのフィードバックは、
displayResults()を使用してレンダリングされます。
スタートボタンのイベントリスナーは、クリックされたときに面接のすべてのプロセスを開始します。このスクリプトは、showError() 関数を通じて堅牢なエラーハンドリングを提供し、ユーザーに明確なフィードバックを提示します。
面接ビデオの録画が完了すると、AI Interview Analyzerの出力結果を確認することができます。

これでオブジェクト指向のアプリケーションが利用可能になりました。ぜひ様々なプロンプトを試してみてください。
このチュートリアルを応用するためのその他のアイデア
アプリケーションの動作手順と開発方法を理解することで、革新的なアイデアを実装し、ユーザーのニーズに応えるプロダクトを作成する準備が整います。このチュートリアルブログの内容をベースにして構築できる、同じようなユースケースのアイデアをいくつか紹介します:
📚️ 面接準備(Interview Preparation): 求職者や就職活動中のユーザーが、現実的な環境で面接スキルを練習し、ブラッシュアップするためにAI Interview Analyzerを使用できます。
🤝 採用プロセスの強化(Hiring Process Enhancement): 採用担当者や人事マネージャーは、AI Interview Analyzerを使用して採用プロセスをストリームライン化(効率化)し、より効果的な選考を行うことができます。
🎓 従業員のスキル開発(Employee Skill Development): 社内研修や人材開発を行う部署は、社員のインタービュースキル(プレゼンスキルなど)を向上させるために、このAI Interview Analyzerをプログラムに組み込むことができます。
😊️ 肯定的な候補者体験(Positive Candidate Experience): 求職者はAI Interview Analyzerから貴重な洞察やフィードバックを直接得られるため、採用選考における候補者体験をより好ましいものに改善できます。
結論
このブログ記事は、Twelve Labsを用いて開発された「AI Interview Analyzer」の動作手順とその開発プロセスについて、詳細な解説を提供することを目的としています。チュートリアルを最後までお読みいただきありがとうございました。ユーザー体験をさらに向上させ、様々な課題を解決する皆さんのクリエイティブなアイデアを楽しみにしています。




