husseinelsaadi commited on
Commit
308d699
·
1 Parent(s): 5cee863

full new update

Browse files
Dockerfile CHANGED
@@ -2,8 +2,8 @@ FROM python:3.10-slim
2
 
3
  # Install OS dependencies
4
  RUN apt-get update && apt-get install -y \
5
- ffmpeg libsndfile1 libgl1 git curl \
6
- build-essential && \
7
  rm -rf /var/lib/apt/lists/*
8
 
9
  # Set working directory
@@ -19,8 +19,9 @@ RUN pip install -r requirements.txt
19
  # Copy everything to the container
20
  COPY . .
21
 
22
- # Create necessary directories
23
- RUN mkdir -p static/audio temp backend/instance uploads/resumes data/resumes
 
24
 
25
  # Expose port
26
  EXPOSE 7860
 
2
 
3
  # Install OS dependencies
4
  RUN apt-get update && apt-get install -y \
5
+ ffmpeg libsndfile1 libsndfile1-dev libgl1 git curl \
6
+ build-essential pkg-config && \
7
  rm -rf /var/lib/apt/lists/*
8
 
9
  # Set working directory
 
19
  # Copy everything to the container
20
  COPY . .
21
 
22
+ # Create necessary directories with proper permissions
23
+ RUN mkdir -p static/audio temp backend/instance uploads/resumes data/resumes /tmp/audio /tmp/interview_temp && \
24
+ chmod 777 /tmp/audio /tmp/interview_temp
25
 
26
  # Expose port
27
  EXPOSE 7860
backend/routes/interview_api.py CHANGED
@@ -1,6 +1,7 @@
1
  import os
2
  import uuid
3
  import json
 
4
  from flask import Blueprint, request, jsonify, send_file, url_for, current_app
5
  from flask_login import login_required, current_user
6
  from backend.models.database import db, Job, Application
@@ -21,140 +22,210 @@ def start_interview():
21
  resume/profile and the selected job. Always returns a JSON payload
22
  containing the question text and, if available, a URL to an audio
23
  rendition of the question.
24
-
25
- Previously this endpoint returned a raw audio file when TTS generation
26
- succeeded. This prevented the client from displaying the actual question
27
- and forced it to fall back to a hard‑coded default. By always returning
28
- structured JSON we ensure the UI can show the generated question and
29
- optionally play the associated audio.
30
  """
31
- data = request.get_json() or {}
32
- job_id = data.get("job_id")
33
-
34
- # Validate the job and the user's application
35
- job = Job.query.get_or_404(job_id)
36
- application = Application.query.filter_by(
37
- user_id=current_user.id,
38
- job_id=job_id
39
- ).first()
40
- if not application or not application.extracted_features:
41
- return jsonify({"error": "No application/profile data found."}), 400
42
-
43
- # Parse the candidate's profile
44
- try:
45
- profile = json.loads(application.extracted_features)
46
- except Exception:
47
- return jsonify({"error": "Invalid profile JSON"}), 500
48
-
49
- # Generate the first question using the LLM
50
- question = generate_first_question(profile, job)
51
-
52
- # Attempt to generate a TTS audio file for the question. If successful
53
- # we'll return a URL that the client can call to retrieve it; otherwise
54
- # audio_url remains None.
55
- audio_url = None
56
  try:
57
- audio_dir = "/tmp/audio"
58
- os.makedirs(audio_dir, exist_ok=True)
59
- filename = f"q_{uuid.uuid4().hex}.wav"
60
- audio_path = os.path.join(audio_dir, filename)
61
- audio_out = edge_tts_to_file_sync(question, audio_path)
62
- if audio_out and os.path.exists(audio_path):
63
- audio_url = url_for("interview_api.get_audio", filename=filename)
64
- except Exception:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  audio_url = None
66
-
67
- return jsonify({
68
- "question": question,
69
- "audio_url": audio_url
70
- })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
 
72
 
73
  @interview_api.route("/transcribe_audio", methods=["POST"])
74
  @login_required
75
  def transcribe_audio():
76
- audio_file = request.files.get("audio")
77
- if not audio_file:
78
- return jsonify({"error": "No audio file received."}), 400
79
-
80
- # Use /tmp directory which is writable in Hugging Face Spaces
81
- temp_dir = "/tmp/interview_temp"
82
- os.makedirs(temp_dir, exist_ok=True)
83
-
84
- filename = f"user_audio_{uuid.uuid4().hex}.webm"
85
- path = os.path.join(temp_dir, filename)
86
- audio_file.save(path)
87
-
88
- transcript = whisper_stt(path)
89
-
90
- # Clean up
91
  try:
92
- os.remove(path)
93
- except:
94
- pass
95
-
96
- return jsonify({"transcript": transcript})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
 
98
  @interview_api.route("/process_answer", methods=["POST"])
99
  @login_required
100
  def process_answer():
101
  """
102
  Process a user's answer and return a follow‑up question along with an
103
- evaluation. Always responds with JSON containing:
104
-
105
- - success: boolean indicating the operation succeeded
106
- - next_question: the text of the next question
107
- - audio_url: optional URL to the TTS audio for the next question
108
- - evaluation: a dict with a score and feedback
109
- - is_complete: boolean indicating if the interview is finished
110
-
111
- Returning JSON even when audio generation succeeds simplifies client
112
- handling and prevents errors when parsing the response.
113
  """
114
- data = request.get_json() or {}
115
- answer = data.get("answer", "")
116
- question_idx = data.get("questionIndex", 0)
117
-
118
- # Construct the next question. In a full implementation this would
119
- # depend on the user's answer and job description.
120
- next_question_text = f"Follow‑up question {question_idx + 2}: Can you elaborate on your experience with relevant technologies?"
121
-
122
- # Stubbed evaluation of the answer. Replace with a call to evaluate_answer()
123
- evaluation_result = {
124
- "score": "medium",
125
- "feedback": "Good answer, but be more specific."
126
- }
127
-
128
- # Determine completion (3 questions in total, zero‑based index)
129
- is_complete = question_idx >= 2
130
-
131
- # Try to generate audio for the next question
132
- audio_url = None
133
  try:
134
- audio_dir = "/tmp/audio"
135
- os.makedirs(audio_dir, exist_ok=True)
136
- filename = f"q_{uuid.uuid4().hex}.wav"
137
- audio_path = os.path.join(audio_dir, filename)
138
- audio_out = edge_tts_to_file_sync(next_question_text, audio_path)
139
- if audio_out and os.path.exists(audio_path):
140
- audio_url = url_for("interview_api.get_audio", filename=filename)
141
- except Exception:
 
 
 
 
 
 
 
 
 
142
  audio_url = None
143
-
144
- return jsonify({
145
- "success": True,
146
- "next_question": next_question_text,
147
- "audio_url": audio_url,
148
- "evaluation": evaluation_result,
149
- "is_complete": is_complete
150
- })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
 
152
  @interview_api.route("/audio/<string:filename>", methods=["GET"])
153
  @login_required
154
  def get_audio(filename: str):
155
  """Serve previously generated TTS audio from the /tmp/audio directory."""
156
- safe_name = os.path.basename(filename)
157
- audio_path = os.path.join("/tmp/audio", safe_name)
158
- if not os.path.exists(audio_path):
159
- return jsonify({"error": "Audio file not found."}), 404
160
- return send_file(audio_path, mimetype="audio/wav", as_attachment=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import os
2
  import uuid
3
  import json
4
+ import logging
5
  from flask import Blueprint, request, jsonify, send_file, url_for, current_app
6
  from flask_login import login_required, current_user
7
  from backend.models.database import db, Job, Application
 
22
  resume/profile and the selected job. Always returns a JSON payload
23
  containing the question text and, if available, a URL to an audio
24
  rendition of the question.
 
 
 
 
 
 
25
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  try:
27
+ data = request.get_json() or {}
28
+ job_id = data.get("job_id")
29
+
30
+ # Validate the job and the user's application
31
+ job = Job.query.get_or_404(job_id)
32
+ application = Application.query.filter_by(
33
+ user_id=current_user.id,
34
+ job_id=job_id
35
+ ).first()
36
+
37
+ if not application or not application.extracted_features:
38
+ return jsonify({"error": "No application/profile data found."}), 400
39
+
40
+ # Parse the candidate's profile
41
+ try:
42
+ profile = json.loads(application.extracted_features)
43
+ except Exception as e:
44
+ logging.error(f"Invalid profile JSON: {e}")
45
+ return jsonify({"error": "Invalid profile JSON"}), 500
46
+
47
+ # Generate the first question using the LLM
48
+ question = generate_first_question(profile, job)
49
+
50
+ if not question:
51
+ question = "Tell me about yourself and why you're interested in this position."
52
+
53
+ # Attempt to generate a TTS audio file for the question
54
  audio_url = None
55
+ try:
56
+ audio_dir = "/tmp/audio"
57
+ os.makedirs(audio_dir, exist_ok=True)
58
+ filename = f"q_{uuid.uuid4().hex}.wav"
59
+ audio_path = os.path.join(audio_dir, filename)
60
+
61
+ audio_result = edge_tts_to_file_sync(question, audio_path)
62
+ if audio_result and os.path.exists(audio_path) and os.path.getsize(audio_path) > 1000:
63
+ audio_url = url_for("interview_api.get_audio", filename=filename)
64
+ logging.info(f"Audio generated successfully: {audio_url}")
65
+ else:
66
+ logging.warning("Audio generation failed or file too small")
67
+ except Exception as e:
68
+ logging.error(f"Error generating TTS audio: {e}")
69
+ audio_url = None
70
+
71
+ return jsonify({
72
+ "question": question,
73
+ "audio_url": audio_url
74
+ })
75
+
76
+ except Exception as e:
77
+ logging.error(f"Error in start_interview: {e}")
78
+ return jsonify({"error": "Internal server error"}), 500
79
 
80
 
81
  @interview_api.route("/transcribe_audio", methods=["POST"])
82
  @login_required
83
  def transcribe_audio():
84
+ """Transcribe uploaded audio with better error handling"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  try:
86
+ audio_file = request.files.get("audio")
87
+ if not audio_file:
88
+ return jsonify({"error": "No audio file received."}), 400
89
+
90
+ # Check if file has content
91
+ audio_file.seek(0, 2) # Seek to end
92
+ file_size = audio_file.tell()
93
+ audio_file.seek(0) # Seek back to start
94
+
95
+ if file_size == 0:
96
+ logging.error("Received empty audio file")
97
+ return jsonify({"error": "Empty audio file received."}), 400
98
+
99
+ logging.info(f"Received audio file: {file_size} bytes")
100
+
101
+ # Use /tmp directory which is writable in Hugging Face Spaces
102
+ temp_dir = "/tmp/interview_temp"
103
+ os.makedirs(temp_dir, exist_ok=True)
104
+
105
+ # Keep original extension for better compatibility
106
+ original_filename = audio_file.filename or "recording.webm"
107
+ file_extension = os.path.splitext(original_filename)[1] or ".webm"
108
+ filename = f"user_audio_{uuid.uuid4().hex}{file_extension}"
109
+ path = os.path.join(temp_dir, filename)
110
+
111
+ # Save the file
112
+ audio_file.save(path)
113
+
114
+ # Verify file was saved
115
+ if not os.path.exists(path) or os.path.getsize(path) == 0:
116
+ logging.error(f"Failed to save audio file or file is empty: {path}")
117
+ return jsonify({"error": "Failed to save audio file."}), 500
118
+
119
+ logging.info(f"Audio file saved: {path} ({os.path.getsize(path)} bytes)")
120
+
121
+ # Transcribe the audio
122
+ transcript = whisper_stt(path)
123
+
124
+ # Clean up
125
+ try:
126
+ os.remove(path)
127
+ except Exception as e:
128
+ logging.warning(f"Could not remove temp file {path}: {e}")
129
+
130
+ if not transcript or not transcript.strip():
131
+ return jsonify({"error": "No speech detected in audio. Please try again."}), 400
132
+
133
+ return jsonify({"transcript": transcript})
134
+
135
+ except Exception as e:
136
+ logging.error(f"Error in transcribe_audio: {e}")
137
+ return jsonify({"error": "Error processing audio. Please try again."}), 500
138
 
139
  @interview_api.route("/process_answer", methods=["POST"])
140
  @login_required
141
  def process_answer():
142
  """
143
  Process a user's answer and return a follow‑up question along with an
144
+ evaluation. Always responds with JSON.
 
 
 
 
 
 
 
 
 
145
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
146
  try:
147
+ data = request.get_json() or {}
148
+ answer = data.get("answer", "").strip()
149
+ question_idx = data.get("questionIndex", 0)
150
+
151
+ if not answer:
152
+ return jsonify({"error": "No answer provided."}), 400
153
+
154
+ # Get the current question for evaluation context
155
+ current_question = data.get("current_question", "Tell me about yourself")
156
+
157
+ # Evaluate the answer
158
+ evaluation_result = evaluate_answer(current_question, answer)
159
+
160
+ # Determine completion (3 questions in total, zero‑based index)
161
+ is_complete = question_idx >= 2
162
+
163
+ next_question_text = None
164
  audio_url = None
165
+
166
+ if not is_complete:
167
+ # Generate next question based on question index
168
+ if question_idx == 0:
169
+ next_question_text = "Can you describe a challenging project you've worked on and how you overcame the difficulties?"
170
+ elif question_idx == 1:
171
+ next_question_text = "What are your career goals and how does this position align with them?"
172
+ else:
173
+ next_question_text = "Do you have any questions about the role or our company?"
174
+
175
+ # Try to generate audio for the next question
176
+ try:
177
+ audio_dir = "/tmp/audio"
178
+ os.makedirs(audio_dir, exist_ok=True)
179
+ filename = f"q_{uuid.uuid4().hex}.wav"
180
+ audio_path = os.path.join(audio_dir, filename)
181
+
182
+ audio_result = edge_tts_to_file_sync(next_question_text, audio_path)
183
+ if audio_result and os.path.exists(audio_path) and os.path.getsize(audio_path) > 1000:
184
+ audio_url = url_for("interview_api.get_audio", filename=filename)
185
+ logging.info(f"Next question audio generated: {audio_url}")
186
+ except Exception as e:
187
+ logging.error(f"Error generating next question audio: {e}")
188
+ audio_url = None
189
+
190
+ return jsonify({
191
+ "success": True,
192
+ "next_question": next_question_text,
193
+ "audio_url": audio_url,
194
+ "evaluation": evaluation_result,
195
+ "is_complete": is_complete
196
+ })
197
+
198
+ except Exception as e:
199
+ logging.error(f"Error in process_answer: {e}")
200
+ return jsonify({"error": "Error processing answer. Please try again."}), 500
201
 
202
  @interview_api.route("/audio/<string:filename>", methods=["GET"])
203
  @login_required
204
  def get_audio(filename: str):
205
  """Serve previously generated TTS audio from the /tmp/audio directory."""
206
+ try:
207
+ # Sanitize filename to prevent directory traversal
208
+ safe_name = os.path.basename(filename)
209
+ if not safe_name.endswith('.wav'):
210
+ return jsonify({"error": "Invalid audio file format."}), 400
211
+
212
+ audio_path = os.path.join("/tmp/audio", safe_name)
213
+
214
+ if not os.path.exists(audio_path):
215
+ logging.warning(f"Audio file not found: {audio_path}")
216
+ return jsonify({"error": "Audio file not found."}), 404
217
+
218
+ if os.path.getsize(audio_path) == 0:
219
+ logging.warning(f"Audio file is empty: {audio_path}")
220
+ return jsonify({"error": "Audio file is empty."}), 404
221
+
222
+ return send_file(
223
+ audio_path,
224
+ mimetype="audio/wav",
225
+ as_attachment=False,
226
+ conditional=True # Enable range requests for better audio streaming
227
+ )
228
+
229
+ except Exception as e:
230
+ logging.error(f"Error serving audio file {filename}: {e}")
231
+ return jsonify({"error": "Error serving audio file."}), 500
backend/services/interview_engine.py CHANGED
@@ -5,6 +5,8 @@ import edge_tts
5
  from faster_whisper import WhisperModel
6
  from langchain_groq import ChatGroq
7
  import logging
 
 
8
 
9
  # Initialize models
10
  chat_groq_api = os.getenv("GROQ_API_KEY")
@@ -22,9 +24,15 @@ whisper_model = None
22
  def load_whisper_model():
23
  global whisper_model
24
  if whisper_model is None:
25
- device = "cuda" if os.system("nvidia-smi") == 0 else "cpu"
26
- compute_type = "float16" if device == "cuda" else "int8"
27
- whisper_model = WhisperModel("base", device=device, compute_type=compute_type)
 
 
 
 
 
 
28
  return whisper_model
29
 
30
  def generate_first_question(profile, job):
@@ -38,115 +46,252 @@ def generate_first_question(profile, job):
38
  - Education: {profile.get('education', [])}
39
 
40
  Generate an appropriate opening interview question that is professional and relevant.
41
- Keep it concise and clear.
42
  """
43
 
44
  response = groq_llm.invoke(prompt)
45
- return response.content.strip()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  except Exception as e:
47
  logging.error(f"Error generating first question: {e}")
48
  return "Tell me about yourself and why you're interested in this position."
49
 
50
  def edge_tts_to_file_sync(text, output_path, voice="en-US-AriaNeural"):
51
- """Synchronous wrapper for edge-tts"""
52
  try:
 
 
 
 
 
53
  # Ensure the directory exists and is writable
54
  directory = os.path.dirname(output_path)
55
  if not directory:
56
- directory = "/tmp" # Fallback to /tmp if no directory specified
57
  output_path = os.path.join(directory, os.path.basename(output_path))
58
 
59
  os.makedirs(directory, exist_ok=True)
60
 
61
- # Test write permissions
62
  test_file = os.path.join(directory, f"test_{os.getpid()}.tmp")
63
  try:
64
  with open(test_file, 'w') as f:
65
  f.write("test")
66
  os.remove(test_file)
 
67
  except (PermissionError, OSError) as e:
68
  logging.error(f"Directory {directory} is not writable: {e}")
69
  # Fallback to /tmp
70
- directory = "/tmp"
71
  output_path = os.path.join(directory, os.path.basename(output_path))
72
  os.makedirs(directory, exist_ok=True)
73
 
74
  async def generate_audio():
75
- communicate = edge_tts.Communicate(text, voice)
76
- await communicate.save(output_path)
 
 
 
 
 
77
 
78
  # Run async function in sync context
79
  try:
80
  loop = asyncio.get_event_loop()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  except RuntimeError:
 
82
  loop = asyncio.new_event_loop()
83
  asyncio.set_event_loop(loop)
84
-
85
- loop.run_until_complete(generate_audio())
 
 
86
 
87
  # Verify file was created and has content
88
- if os.path.exists(output_path) and os.path.getsize(output_path) > 0:
89
- return output_path
 
 
 
 
 
 
90
  else:
91
- logging.error(f"Audio file was not created or is empty: {output_path}")
92
  return None
93
 
94
  except Exception as e:
95
  logging.error(f"Error in TTS generation: {e}")
96
  return None
97
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  def whisper_stt(audio_path):
99
- """Speech-to-text using Faster-Whisper"""
100
  try:
101
  if not audio_path or not os.path.exists(audio_path):
102
  logging.error(f"Audio file does not exist: {audio_path}")
103
  return ""
104
 
105
  # Check if file has content
106
- if os.path.getsize(audio_path) == 0:
 
107
  logging.error(f"Audio file is empty: {audio_path}")
108
  return ""
109
 
 
 
 
 
 
 
 
 
 
 
 
110
  model = load_whisper_model()
111
- segments, _ = model.transcribe(audio_path)
112
- transcript = " ".join(segment.text for segment in segments)
113
- return transcript.strip()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
  except Exception as e:
115
  logging.error(f"Error in STT: {e}")
116
  return ""
117
 
118
- def evaluate_answer(question, answer, ref_answer, job_role, seniority):
119
- """Evaluate candidate's answer"""
120
  try:
 
 
 
 
 
 
121
  prompt = f"""
122
  You are evaluating a candidate's answer for a {seniority} {job_role} position.
123
 
124
  Question: {question}
125
  Candidate Answer: {answer}
126
- Reference Answer: {ref_answer}
127
 
128
  Evaluate based on technical correctness, clarity, and relevance.
129
- Respond with JSON format:
130
- {{
131
- "Score": "Poor|Medium|Good|Excellent",
132
- "Reasoning": "brief explanation",
133
- "Improvements": ["suggestion1", "suggestion2"]
134
- }}
 
135
  """
136
 
137
  response = groq_llm.invoke(prompt)
138
- # Extract JSON from response
139
- start_idx = response.find("{")
140
- end_idx = response.rfind("}") + 1
141
- if start_idx >= 0 and end_idx > start_idx:
142
- json_str = response[start_idx:end_idx]
143
- return json.loads(json_str)
144
  else:
145
- raise ValueError("No valid JSON found in response")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
146
  except Exception as e:
147
  logging.error(f"Error evaluating answer: {e}")
148
  return {
149
- "Score": "Medium",
150
- "Reasoning": "Evaluation failed",
151
- "Improvements": ["Please be more specific"]
152
  }
 
5
  from faster_whisper import WhisperModel
6
  from langchain_groq import ChatGroq
7
  import logging
8
+ import tempfile
9
+ import shutil
10
 
11
  # Initialize models
12
  chat_groq_api = os.getenv("GROQ_API_KEY")
 
24
  def load_whisper_model():
25
  global whisper_model
26
  if whisper_model is None:
27
+ try:
28
+ device = "cuda" if os.system("nvidia-smi") == 0 else "cpu"
29
+ compute_type = "float16" if device == "cuda" else "int8"
30
+ whisper_model = WhisperModel("base", device=device, compute_type=compute_type)
31
+ logging.info(f"Whisper model loaded on {device} with {compute_type}")
32
+ except Exception as e:
33
+ logging.error(f"Error loading Whisper model: {e}")
34
+ # Fallback to CPU
35
+ whisper_model = WhisperModel("base", device="cpu", compute_type="int8")
36
  return whisper_model
37
 
38
  def generate_first_question(profile, job):
 
46
  - Education: {profile.get('education', [])}
47
 
48
  Generate an appropriate opening interview question that is professional and relevant.
49
+ Keep it concise and clear. Respond with ONLY the question text, no additional formatting.
50
  """
51
 
52
  response = groq_llm.invoke(prompt)
53
+
54
+ # Fix: Handle AIMessage object properly
55
+ if hasattr(response, 'content'):
56
+ question = response.content.strip()
57
+ elif isinstance(response, str):
58
+ question = response.strip()
59
+ else:
60
+ question = str(response).strip()
61
+
62
+ # Ensure we have a valid question
63
+ if not question or len(question) < 10:
64
+ question = "Tell me about yourself and why you're interested in this position."
65
+
66
+ logging.info(f"Generated question: {question}")
67
+ return question
68
+
69
  except Exception as e:
70
  logging.error(f"Error generating first question: {e}")
71
  return "Tell me about yourself and why you're interested in this position."
72
 
73
  def edge_tts_to_file_sync(text, output_path, voice="en-US-AriaNeural"):
74
+ """Synchronous wrapper for edge-tts with better error handling"""
75
  try:
76
+ # Ensure text is not empty
77
+ if not text or not text.strip():
78
+ logging.error("Empty text provided for TTS")
79
+ return None
80
+
81
  # Ensure the directory exists and is writable
82
  directory = os.path.dirname(output_path)
83
  if not directory:
84
+ directory = "/tmp/audio"
85
  output_path = os.path.join(directory, os.path.basename(output_path))
86
 
87
  os.makedirs(directory, exist_ok=True)
88
 
89
+ # Test write permissions with a temporary file
90
  test_file = os.path.join(directory, f"test_{os.getpid()}.tmp")
91
  try:
92
  with open(test_file, 'w') as f:
93
  f.write("test")
94
  os.remove(test_file)
95
+ logging.info(f"Directory {directory} is writable")
96
  except (PermissionError, OSError) as e:
97
  logging.error(f"Directory {directory} is not writable: {e}")
98
  # Fallback to /tmp
99
+ directory = "/tmp/audio"
100
  output_path = os.path.join(directory, os.path.basename(output_path))
101
  os.makedirs(directory, exist_ok=True)
102
 
103
  async def generate_audio():
104
+ try:
105
+ communicate = edge_tts.Communicate(text, voice)
106
+ await communicate.save(output_path)
107
+ logging.info(f"TTS audio saved to: {output_path}")
108
+ except Exception as e:
109
+ logging.error(f"Error in async TTS generation: {e}")
110
+ raise
111
 
112
  # Run async function in sync context
113
  try:
114
  loop = asyncio.get_event_loop()
115
+ if loop.is_running():
116
+ # If loop is already running, create a new one in a thread
117
+ import threading
118
+ import concurrent.futures
119
+
120
+ def run_in_thread():
121
+ new_loop = asyncio.new_event_loop()
122
+ asyncio.set_event_loop(new_loop)
123
+ try:
124
+ new_loop.run_until_complete(generate_audio())
125
+ finally:
126
+ new_loop.close()
127
+
128
+ with concurrent.futures.ThreadPoolExecutor() as executor:
129
+ future = executor.submit(run_in_thread)
130
+ future.result(timeout=30) # 30 second timeout
131
+ else:
132
+ loop.run_until_complete(generate_audio())
133
  except RuntimeError:
134
+ # No event loop exists
135
  loop = asyncio.new_event_loop()
136
  asyncio.set_event_loop(loop)
137
+ try:
138
+ loop.run_until_complete(generate_audio())
139
+ finally:
140
+ loop.close()
141
 
142
  # Verify file was created and has content
143
+ if os.path.exists(output_path):
144
+ file_size = os.path.getsize(output_path)
145
+ if file_size > 1000: # At least 1KB for a valid audio file
146
+ logging.info(f"TTS file created successfully: {output_path} ({file_size} bytes)")
147
+ return output_path
148
+ else:
149
+ logging.error(f"TTS file is too small: {output_path} ({file_size} bytes)")
150
+ return None
151
  else:
152
+ logging.error(f"TTS file was not created: {output_path}")
153
  return None
154
 
155
  except Exception as e:
156
  logging.error(f"Error in TTS generation: {e}")
157
  return None
158
 
159
+ def convert_webm_to_wav(webm_path, wav_path):
160
+ """Convert WebM audio to WAV using ffmpeg if available"""
161
+ try:
162
+ import subprocess
163
+ result = subprocess.run([
164
+ 'ffmpeg', '-i', webm_path, '-ar', '16000', '-ac', '1', '-y', wav_path
165
+ ], capture_output=True, text=True, timeout=30)
166
+
167
+ if result.returncode == 0 and os.path.exists(wav_path) and os.path.getsize(wav_path) > 0:
168
+ logging.info(f"Successfully converted {webm_path} to {wav_path}")
169
+ return wav_path
170
+ else:
171
+ logging.error(f"FFmpeg conversion failed: {result.stderr}")
172
+ return None
173
+ except (subprocess.TimeoutExpired, FileNotFoundError, Exception) as e:
174
+ logging.error(f"Error converting audio: {e}")
175
+ return None
176
+
177
  def whisper_stt(audio_path):
178
+ """Speech-to-text using Faster-Whisper with better error handling"""
179
  try:
180
  if not audio_path or not os.path.exists(audio_path):
181
  logging.error(f"Audio file does not exist: {audio_path}")
182
  return ""
183
 
184
  # Check if file has content
185
+ file_size = os.path.getsize(audio_path)
186
+ if file_size == 0:
187
  logging.error(f"Audio file is empty: {audio_path}")
188
  return ""
189
 
190
+ logging.info(f"Processing audio file: {audio_path} ({file_size} bytes)")
191
+
192
+ # If the file is WebM, try to convert it to WAV
193
+ if audio_path.endswith('.webm'):
194
+ wav_path = audio_path.replace('.webm', '.wav')
195
+ converted_path = convert_webm_to_wav(audio_path, wav_path)
196
+ if converted_path:
197
+ audio_path = converted_path
198
+ else:
199
+ logging.warning("Could not convert WebM to WAV, trying with original file")
200
+
201
  model = load_whisper_model()
202
+
203
+ # Add timeout and better error handling
204
+ try:
205
+ segments, info = model.transcribe(
206
+ audio_path,
207
+ language="en", # Specify language for better performance
208
+ task="transcribe",
209
+ vad_filter=True, # Voice activity detection
210
+ vad_parameters=dict(min_silence_duration_ms=500)
211
+ )
212
+
213
+ transcript_parts = []
214
+ for segment in segments:
215
+ if hasattr(segment, 'text') and segment.text.strip():
216
+ transcript_parts.append(segment.text.strip())
217
+
218
+ transcript = " ".join(transcript_parts)
219
+
220
+ if transcript:
221
+ logging.info(f"Transcription successful: '{transcript[:100]}...'")
222
+ else:
223
+ logging.warning("No speech detected in audio file")
224
+
225
+ return transcript.strip()
226
+
227
+ except Exception as e:
228
+ logging.error(f"Error during transcription: {e}")
229
+ return ""
230
+
231
  except Exception as e:
232
  logging.error(f"Error in STT: {e}")
233
  return ""
234
 
235
+ def evaluate_answer(question, answer, job_role="Software Developer", seniority="Mid-level"):
236
+ """Evaluate candidate's answer with better error handling"""
237
  try:
238
+ if not answer or not answer.strip():
239
+ return {
240
+ "score": "Poor",
241
+ "feedback": "No answer provided."
242
+ }
243
+
244
  prompt = f"""
245
  You are evaluating a candidate's answer for a {seniority} {job_role} position.
246
 
247
  Question: {question}
248
  Candidate Answer: {answer}
 
249
 
250
  Evaluate based on technical correctness, clarity, and relevance.
251
+ Provide a brief evaluation in 1-2 sentences.
252
+
253
+ Rate the answer as one of: Poor, Medium, Good, Excellent
254
+
255
+ Respond in this exact format:
256
+ Score: [Poor/Medium/Good/Excellent]
257
+ Feedback: [Your brief feedback here]
258
  """
259
 
260
  response = groq_llm.invoke(prompt)
261
+
262
+ # Handle AIMessage object properly
263
+ if hasattr(response, 'content'):
264
+ response_text = response.content.strip()
265
+ elif isinstance(response, str):
266
+ response_text = response.strip()
267
  else:
268
+ response_text = str(response).strip()
269
+
270
+ # Parse the response
271
+ lines = response_text.split('\n')
272
+ score = "Medium" # default
273
+ feedback = "Good answer, but could be more detailed." # default
274
+
275
+ for line in lines:
276
+ line = line.strip()
277
+ if line.startswith('Score:'):
278
+ score = line.replace('Score:', '').strip()
279
+ elif line.startswith('Feedback:'):
280
+ feedback = line.replace('Feedback:', '').strip()
281
+
282
+ # Ensure score is valid
283
+ valid_scores = ["Poor", "Medium", "Good", "Excellent"]
284
+ if score not in valid_scores:
285
+ score = "Medium"
286
+
287
+ return {
288
+ "score": score,
289
+ "feedback": feedback
290
+ }
291
+
292
  except Exception as e:
293
  logging.error(f"Error evaluating answer: {e}")
294
  return {
295
+ "score": "Medium",
296
+ "feedback": "Unable to evaluate answer at this time."
 
297
  }
backend/templates/interview.html CHANGED
@@ -498,6 +498,7 @@
498
  this.isRecording = false;
499
  this.mediaRecorder = null;
500
  this.audioChunks = [];
 
501
  this.interviewData = {
502
  questions: [],
503
  answers: [],
@@ -525,10 +526,23 @@
525
  }
526
 
527
  bindEvents() {
528
- this.micButton.addEventListener('mousedown', () => this.startRecording());
529
- this.micButton.addEventListener('mouseup', () => this.stopRecording());
530
- this.micButton.addEventListener('mouseleave', () => this.stopRecording());
 
 
 
 
 
 
 
 
 
 
 
 
531
 
 
532
  this.micButton.addEventListener('touchstart', (e) => {
533
  e.preventDefault();
534
  this.startRecording();
@@ -565,6 +579,7 @@
565
 
566
  async initializeInterview() {
567
  try {
 
568
  const response = await fetch('/api/start_interview', {
569
  method: 'POST',
570
  headers: {
@@ -574,26 +589,29 @@
574
  });
575
 
576
  if (!response.ok) {
 
 
577
  throw new Error(`HTTP error! status: ${response.status}`);
578
  }
579
 
580
- // Always expect a JSON payload describing the question and optional audio URL
581
  const data = await response.json();
 
 
582
  if (data.error) {
583
  this.showError(data.error);
584
  return;
585
  }
586
 
587
- // Display the actual question text and play audio if provided
 
588
  this.displayQuestion(data.question, data.audio_url);
589
  this.interviewData.questions.push(data.question);
590
  } catch (error) {
591
  console.error('Error starting interview:', error);
592
- this.showError('Failed to start interview. Please try again.');
593
  }
594
  }
595
 
596
-
597
  displayQuestion(question, audioUrl = null) {
598
  // Remove loading message
599
  const loadingMsg = document.getElementById('loadingMessage');
@@ -605,11 +623,11 @@
605
  const messageDiv = document.createElement('div');
606
  messageDiv.className = 'ai-message';
607
  messageDiv.innerHTML = `
608
- <div class="ai-avatar talking">AI</div>
609
- <div class="message-bubble">
610
- <p>${question}</p>
611
- </div>
612
- `;
613
  this.chatArea.appendChild(messageDiv);
614
  this.chatArea.scrollTop = this.chatArea.scrollHeight;
615
 
@@ -618,17 +636,25 @@
618
 
619
  // Play audio if available
620
  if (audioUrl) {
 
621
  this.playQuestionAudio(audioUrl);
622
  } else {
623
- // Enable controls if no audio
624
  setTimeout(() => this.enableControls(), 1000);
625
  }
626
  }
627
 
628
  playQuestionAudio(audioUrl) {
 
 
 
 
629
  this.ttsAudio.src = audioUrl;
 
 
630
  this.ttsAudio.play().catch(error => {
631
  console.error('Audio play error:', error);
 
632
  this.enableControls();
633
  });
634
  }
@@ -637,31 +663,61 @@
637
  this.micButton.disabled = false;
638
  this.recordingStatus.textContent = 'Click and hold to record your answer';
639
 
640
- // Remove talking animation from avatar
641
  const avatars = this.chatArea.querySelectorAll('.ai-avatar');
642
  avatars.forEach(avatar => avatar.classList.remove('talking'));
643
  }
644
 
645
  async startRecording() {
646
- if (this.isRecording) return;
647
 
648
  try {
649
- const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
650
- this.mediaRecorder = new MediaRecorder(stream, {
651
- mimeType: 'audio/webm;codecs=opus'
 
 
 
 
 
652
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
653
  this.audioChunks = [];
654
 
655
  this.mediaRecorder.ondataavailable = (event) => {
656
- this.audioChunks.push(event.data);
 
 
 
657
  };
658
 
659
  this.mediaRecorder.onstop = () => {
 
 
660
  this.processRecording();
 
 
 
 
 
661
  stream.getTracks().forEach(track => track.stop());
662
  };
663
 
664
- this.mediaRecorder.start();
665
  this.isRecording = true;
666
 
667
  // Update UI
@@ -672,12 +728,14 @@
672
  } catch (error) {
673
  console.error('Error starting recording:', error);
674
  this.recordingStatus.textContent = 'Microphone access denied. Please allow microphone access and try again.';
 
675
  }
676
  }
677
 
678
  stopRecording() {
679
  if (!this.isRecording || !this.mediaRecorder) return;
680
 
 
681
  this.mediaRecorder.stop();
682
  this.isRecording = false;
683
 
@@ -685,27 +743,50 @@
685
  this.micButton.classList.remove('recording');
686
  this.micIcon.textContent = '🎤';
687
  this.recordingStatus.textContent = 'Processing audio...';
 
688
  }
689
 
690
  async processRecording() {
691
- const audioBlob = new Blob(this.audioChunks, { type: 'audio/wav' });
692
- const formData = new FormData();
693
- formData.append('audio', audioBlob, 'recording.wav');
694
-
695
  try {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
696
  const response = await fetch('/api/transcribe_audio', {
697
  method: 'POST',
698
  body: formData
699
  });
700
 
701
  if (!response.ok) {
 
 
702
  throw new Error(`HTTP error! status: ${response.status}`);
703
  }
704
 
705
  const data = await response.json();
 
706
 
707
  if (data.error) {
708
  this.recordingStatus.textContent = data.error;
 
709
  return;
710
  }
711
 
@@ -714,13 +795,16 @@
714
  this.confirmButton.disabled = false;
715
  this.retryButton.style.display = 'inline-flex';
716
  this.recordingStatus.textContent = 'Transcription complete. Review and confirm your answer.';
 
717
  } else {
718
  this.recordingStatus.textContent = 'No speech detected. Please try recording again.';
 
719
  }
720
 
721
  } catch (error) {
722
  console.error('Error processing recording:', error);
723
  this.recordingStatus.textContent = 'Error processing audio. Please try again.';
 
724
  }
725
  }
726
 
@@ -729,12 +813,15 @@
729
  this.confirmButton.disabled = true;
730
  this.retryButton.style.display = 'none';
731
  this.recordingStatus.textContent = 'Click and hold to record your answer';
 
732
  }
733
 
734
  async submitAnswer() {
735
  const answer = this.transcriptArea.textContent.trim();
736
  if (!answer) return;
737
 
 
 
738
  // Show loading state
739
  this.confirmButton.disabled = true;
740
  this.confirmLoading.style.display = 'inline-block';
@@ -751,18 +838,22 @@
751
  },
752
  body: JSON.stringify({
753
  answer: answer,
754
- questionIndex: this.currentQuestionIndex
 
755
  })
756
  });
757
 
758
  if (!response.ok) {
 
 
759
  throw new Error(`HTTP error! status: ${response.status}`);
760
  }
761
 
762
- // Parse JSON response
763
  const data = await response.json();
 
 
764
  if (!data.success) {
765
- this.showError('Failed to process answer. Please try again.');
766
  return;
767
  }
768
 
@@ -771,11 +862,12 @@
771
  this.interviewData.evaluations.push(data.evaluation || {});
772
 
773
  if (data.is_complete) {
774
- // Interview finished, show summary
775
  this.showInterviewSummary();
776
  } else {
777
- // Advance to next question
778
  this.currentQuestionIndex++;
 
779
  this.displayQuestion(data.next_question, data.audio_url);
780
  this.interviewData.questions.push(data.next_question);
781
  this.resetForNextQuestion();
@@ -794,10 +886,10 @@
794
  const messageDiv = document.createElement('div');
795
  messageDiv.className = 'user-message';
796
  messageDiv.innerHTML = `
797
- <div class="user-bubble">
798
- <p>${message}</p>
799
- </div>
800
- `;
801
  this.chatArea.appendChild(messageDiv);
802
  this.chatArea.scrollTop = this.chatArea.scrollHeight;
803
  }
@@ -807,6 +899,7 @@
807
  this.confirmButton.disabled = true;
808
  this.retryButton.style.display = 'none';
809
  this.recordingStatus.textContent = 'Wait for the next question...';
 
810
  this.micButton.disabled = true;
811
  }
812
 
@@ -819,14 +912,14 @@
819
  const evaluation = this.interviewData.evaluations[index] || {};
820
 
821
  summaryHtml += `
822
- <div class="summary-item">
823
- <h4>Question ${index + 1}:</h4>
824
- <p><strong>Q:</strong> ${question}</p>
825
- <p><strong>A:</strong> ${answer}</p>
826
- <p><strong>Score:</strong> <span class="evaluation-score">${evaluation.score || 'N/A'}</span></p>
827
- <p><strong>Feedback:</strong> ${evaluation.feedback || 'No feedback provided'}</p>
828
- </div>
829
- `;
830
  });
831
 
832
  summaryContent.innerHTML = summaryHtml;
@@ -837,6 +930,8 @@
837
  }
838
 
839
  showError(message) {
 
 
840
  // Create error message element
841
  const errorDiv = document.createElement('div');
842
  errorDiv.className = 'error-message';
@@ -864,6 +959,7 @@
864
 
865
  // Initialize the interview when page loads
866
  document.addEventListener('DOMContentLoaded', () => {
 
867
  new AIInterviewer();
868
  });
869
 
 
498
  this.isRecording = false;
499
  this.mediaRecorder = null;
500
  this.audioChunks = [];
501
+ this.currentQuestion = "";
502
  this.interviewData = {
503
  questions: [],
504
  answers: [],
 
526
  }
527
 
528
  bindEvents() {
529
+ // Mouse events for desktop
530
+ this.micButton.addEventListener('mousedown', (e) => {
531
+ e.preventDefault();
532
+ this.startRecording();
533
+ });
534
+
535
+ this.micButton.addEventListener('mouseup', (e) => {
536
+ e.preventDefault();
537
+ this.stopRecording();
538
+ });
539
+
540
+ this.micButton.addEventListener('mouseleave', (e) => {
541
+ e.preventDefault();
542
+ this.stopRecording();
543
+ });
544
 
545
+ // Touch events for mobile
546
  this.micButton.addEventListener('touchstart', (e) => {
547
  e.preventDefault();
548
  this.startRecording();
 
579
 
580
  async initializeInterview() {
581
  try {
582
+ console.log('Starting interview...');
583
  const response = await fetch('/api/start_interview', {
584
  method: 'POST',
585
  headers: {
 
589
  });
590
 
591
  if (!response.ok) {
592
+ const errorText = await response.text();
593
+ console.error('Server response:', response.status, errorText);
594
  throw new Error(`HTTP error! status: ${response.status}`);
595
  }
596
 
 
597
  const data = await response.json();
598
+ console.log('Received interview data:', data);
599
+
600
  if (data.error) {
601
  this.showError(data.error);
602
  return;
603
  }
604
 
605
+ // Store the current question for evaluation
606
+ this.currentQuestion = data.question;
607
  this.displayQuestion(data.question, data.audio_url);
608
  this.interviewData.questions.push(data.question);
609
  } catch (error) {
610
  console.error('Error starting interview:', error);
611
+ this.showError('Failed to start interview. Please check your connection and try again.');
612
  }
613
  }
614
 
 
615
  displayQuestion(question, audioUrl = null) {
616
  // Remove loading message
617
  const loadingMsg = document.getElementById('loadingMessage');
 
623
  const messageDiv = document.createElement('div');
624
  messageDiv.className = 'ai-message';
625
  messageDiv.innerHTML = `
626
+ <div class="ai-avatar">AI</div>
627
+ <div class="message-bubble">
628
+ <p>${question}</p>
629
+ </div>
630
+ `;
631
  this.chatArea.appendChild(messageDiv);
632
  this.chatArea.scrollTop = this.chatArea.scrollHeight;
633
 
 
636
 
637
  // Play audio if available
638
  if (audioUrl) {
639
+ console.log('Playing audio:', audioUrl);
640
  this.playQuestionAudio(audioUrl);
641
  } else {
642
+ console.log('No audio URL provided, enabling controls');
643
  setTimeout(() => this.enableControls(), 1000);
644
  }
645
  }
646
 
647
  playQuestionAudio(audioUrl) {
648
+ // Add talking animation immediately
649
+ const avatars = this.chatArea.querySelectorAll('.ai-avatar');
650
+ avatars.forEach(avatar => avatar.classList.add('talking'));
651
+
652
  this.ttsAudio.src = audioUrl;
653
+ this.ttsAudio.load(); // Ensure audio is loaded
654
+
655
  this.ttsAudio.play().catch(error => {
656
  console.error('Audio play error:', error);
657
+ avatars.forEach(avatar => avatar.classList.remove('talking'));
658
  this.enableControls();
659
  });
660
  }
 
663
  this.micButton.disabled = false;
664
  this.recordingStatus.textContent = 'Click and hold to record your answer';
665
 
666
+ // Remove talking animation from all avatars
667
  const avatars = this.chatArea.querySelectorAll('.ai-avatar');
668
  avatars.forEach(avatar => avatar.classList.remove('talking'));
669
  }
670
 
671
  async startRecording() {
672
+ if (this.isRecording || this.micButton.disabled) return;
673
 
674
  try {
675
+ console.log('Starting recording...');
676
+ const stream = await navigator.mediaDevices.getUserMedia({
677
+ audio: {
678
+ echoCancellation: true,
679
+ noiseSuppression: true,
680
+ autoGainControl: true,
681
+ sampleRate: 16000
682
+ }
683
  });
684
+
685
+ // Use webm format with opus codec for better compatibility
686
+ const options = {
687
+ mimeType: 'audio/webm;codecs=opus'
688
+ };
689
+
690
+ // Fallback for browsers that don't support webm
691
+ if (!MediaRecorder.isTypeSupported(options.mimeType)) {
692
+ options.mimeType = 'audio/webm';
693
+ }
694
+ if (!MediaRecorder.isTypeSupported(options.mimeType)) {
695
+ delete options.mimeType;
696
+ }
697
+
698
+ this.mediaRecorder = new MediaRecorder(stream, options);
699
  this.audioChunks = [];
700
 
701
  this.mediaRecorder.ondataavailable = (event) => {
702
+ if (event.data.size > 0) {
703
+ this.audioChunks.push(event.data);
704
+ console.log('Audio chunk received:', event.data.size, 'bytes');
705
+ }
706
  };
707
 
708
  this.mediaRecorder.onstop = () => {
709
+ console.log('Recording stopped, processing...');
710
+ stream.getTracks().forEach(track => track.stop());
711
  this.processRecording();
712
+ };
713
+
714
+ this.mediaRecorder.onerror = (event) => {
715
+ console.error('MediaRecorder error:', event.error);
716
+ this.recordingStatus.textContent = 'Recording error. Please try again.';
717
  stream.getTracks().forEach(track => track.stop());
718
  };
719
 
720
+ this.mediaRecorder.start(1000); // Collect data every second
721
  this.isRecording = true;
722
 
723
  // Update UI
 
728
  } catch (error) {
729
  console.error('Error starting recording:', error);
730
  this.recordingStatus.textContent = 'Microphone access denied. Please allow microphone access and try again.';
731
+ this.recordingStatus.style.color = '#ff4757';
732
  }
733
  }
734
 
735
  stopRecording() {
736
  if (!this.isRecording || !this.mediaRecorder) return;
737
 
738
+ console.log('Stopping recording...');
739
  this.mediaRecorder.stop();
740
  this.isRecording = false;
741
 
 
743
  this.micButton.classList.remove('recording');
744
  this.micIcon.textContent = '🎤';
745
  this.recordingStatus.textContent = 'Processing audio...';
746
+ this.recordingStatus.style.color = '#666';
747
  }
748
 
749
  async processRecording() {
 
 
 
 
750
  try {
751
+ if (this.audioChunks.length === 0) {
752
+ console.error('No audio chunks recorded');
753
+ this.recordingStatus.textContent = 'No audio recorded. Please try again.';
754
+ return;
755
+ }
756
+
757
+ console.log('Processing', this.audioChunks.length, 'audio chunks');
758
+
759
+ // Create blob from audio chunks
760
+ const audioBlob = new Blob(this.audioChunks, { type: 'audio/webm' });
761
+ console.log('Created audio blob:', audioBlob.size, 'bytes');
762
+
763
+ if (audioBlob.size === 0) {
764
+ console.error('Audio blob is empty');
765
+ this.recordingStatus.textContent = 'No audio data captured. Please try again.';
766
+ return;
767
+ }
768
+
769
+ const formData = new FormData();
770
+ formData.append('audio', audioBlob, 'recording.webm');
771
+
772
+ console.log('Sending audio for transcription...');
773
  const response = await fetch('/api/transcribe_audio', {
774
  method: 'POST',
775
  body: formData
776
  });
777
 
778
  if (!response.ok) {
779
+ const errorText = await response.text();
780
+ console.error('Transcription error:', response.status, errorText);
781
  throw new Error(`HTTP error! status: ${response.status}`);
782
  }
783
 
784
  const data = await response.json();
785
+ console.log('Transcription response:', data);
786
 
787
  if (data.error) {
788
  this.recordingStatus.textContent = data.error;
789
+ this.recordingStatus.style.color = '#ff4757';
790
  return;
791
  }
792
 
 
795
  this.confirmButton.disabled = false;
796
  this.retryButton.style.display = 'inline-flex';
797
  this.recordingStatus.textContent = 'Transcription complete. Review and confirm your answer.';
798
+ this.recordingStatus.style.color = '#4CAF50';
799
  } else {
800
  this.recordingStatus.textContent = 'No speech detected. Please try recording again.';
801
+ this.recordingStatus.style.color = '#ff4757';
802
  }
803
 
804
  } catch (error) {
805
  console.error('Error processing recording:', error);
806
  this.recordingStatus.textContent = 'Error processing audio. Please try again.';
807
+ this.recordingStatus.style.color = '#ff4757';
808
  }
809
  }
810
 
 
813
  this.confirmButton.disabled = true;
814
  this.retryButton.style.display = 'none';
815
  this.recordingStatus.textContent = 'Click and hold to record your answer';
816
+ this.recordingStatus.style.color = '#666';
817
  }
818
 
819
  async submitAnswer() {
820
  const answer = this.transcriptArea.textContent.trim();
821
  if (!answer) return;
822
 
823
+ console.log('Submitting answer:', answer);
824
+
825
  // Show loading state
826
  this.confirmButton.disabled = true;
827
  this.confirmLoading.style.display = 'inline-block';
 
838
  },
839
  body: JSON.stringify({
840
  answer: answer,
841
+ questionIndex: this.currentQuestionIndex,
842
+ current_question: this.currentQuestion
843
  })
844
  });
845
 
846
  if (!response.ok) {
847
+ const errorText = await response.text();
848
+ console.error('Process answer error:', response.status, errorText);
849
  throw new Error(`HTTP error! status: ${response.status}`);
850
  }
851
 
 
852
  const data = await response.json();
853
+ console.log('Process answer response:', data);
854
+
855
  if (!data.success) {
856
+ this.showError(data.error || 'Failed to process answer. Please try again.');
857
  return;
858
  }
859
 
 
862
  this.interviewData.evaluations.push(data.evaluation || {});
863
 
864
  if (data.is_complete) {
865
+ console.log('Interview completed');
866
  this.showInterviewSummary();
867
  } else {
868
+ console.log('Moving to next question');
869
  this.currentQuestionIndex++;
870
+ this.currentQuestion = data.next_question;
871
  this.displayQuestion(data.next_question, data.audio_url);
872
  this.interviewData.questions.push(data.next_question);
873
  this.resetForNextQuestion();
 
886
  const messageDiv = document.createElement('div');
887
  messageDiv.className = 'user-message';
888
  messageDiv.innerHTML = `
889
+ <div class="user-bubble">
890
+ <p>${message}</p>
891
+ </div>
892
+ `;
893
  this.chatArea.appendChild(messageDiv);
894
  this.chatArea.scrollTop = this.chatArea.scrollHeight;
895
  }
 
899
  this.confirmButton.disabled = true;
900
  this.retryButton.style.display = 'none';
901
  this.recordingStatus.textContent = 'Wait for the next question...';
902
+ this.recordingStatus.style.color = '#666';
903
  this.micButton.disabled = true;
904
  }
905
 
 
912
  const evaluation = this.interviewData.evaluations[index] || {};
913
 
914
  summaryHtml += `
915
+ <div class="summary-item">
916
+ <h4>Question ${index + 1}:</h4>
917
+ <p><strong>Q:</strong> ${question}</p>
918
+ <p><strong>A:</strong> ${answer}</p>
919
+ <p><strong>Score:</strong> <span class="evaluation-score">${evaluation.score || 'N/A'}</span></p>
920
+ <p><strong>Feedback:</strong> ${evaluation.feedback || 'No feedback provided'}</p>
921
+ </div>
922
+ `;
923
  });
924
 
925
  summaryContent.innerHTML = summaryHtml;
 
930
  }
931
 
932
  showError(message) {
933
+ console.error('Showing error:', message);
934
+
935
  // Create error message element
936
  const errorDiv = document.createElement('div');
937
  errorDiv.className = 'error-message';
 
959
 
960
  // Initialize the interview when page loads
961
  document.addEventListener('DOMContentLoaded', () => {
962
+ console.log('DOM loaded, initializing AI Interviewer...');
963
  new AIInterviewer();
964
  });
965
 
requirements.txt CHANGED
@@ -53,4 +53,10 @@ edge-tts==6.1.2
53
 
54
  # Additional Flask dependencies
55
  gunicorn
56
- python-dotenv
 
 
 
 
 
 
 
53
 
54
  # Additional Flask dependencies
55
  gunicorn
56
+ python-dotenv
57
+
58
+ # Audio format conversion (critical for WebM/WAV handling)
59
+ pydub>=0.25.1
60
+
61
+ # Better error handling for API calls
62
+ requests>=2.31.0