-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathapp.py
More file actions
469 lines (387 loc) · 15.6 KB
/
app.py
File metadata and controls
469 lines (387 loc) · 15.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
import streamlit as st
import asyncio
import os
import time
from datetime import datetime
from utils import (
transcribe_with_speechmatics,
extract_resume_info_using_llm,
get_ai_greeting_message,
get_final_thanks_message,
speak_text,
analyze_candidate_response_and_generate_new_question,
get_feedback_of_candidate_response,
get_overall_evaluation_score,
save_interview_data,
load_content_streamlit,
)
MAX_QUESTIONS = 5
# Configuration and Styling
def setup_page_config():
"""Setup page configuration and custom CSS"""
st.set_page_config(page_title="AI Interview App", layout="wide")
st.markdown(
"""
<style>
.audio-section {
background-color: #f8f9fa;
border-radius: 10px;
padding: 20px;
margin-top: 20px;
text-align: center;
border: 2px solid #e9ecef;
}
.interview-progress {
background-color: #e8f5e8;
border-radius: 10px;
padding: 15px;
margin-bottom: 20px;
text-align: center;
}
</style>
""",
unsafe_allow_html=True,
)
def initialize_session_state():
"""Initialize all session state variables"""
defaults = {
"interview_started": False,
"name": "",
"resume_highlights": "",
"job_description": "",
"qa_index": 1,
"conversations": [],
"current_question": "",
"question_spoken": False,
"awaiting_response": False,
"processing_audio": False,
"messages": [],
"interview_completed": False,
"max_questions": MAX_QUESTIONS,
"ai_voice": "Alex (Male)",
"thanks_message_prepared": False,
"thanks_message_spoken": False,
"show_final_results": False,
}
for key, value in defaults.items():
if key not in st.session_state:
st.session_state[key] = value
def get_ai_voice_details():
"""Get AI voice configuration"""
return {
"Alex (Male)": {"name": "Alex", "code": "en-US-GuyNeural"},
"Aria (Female)": {"name": "Aria", "code": "en-US-AriaNeural"},
"Natasha (Female)": {"name": "Natasha", "code": "en-AU-NatashaNeural"},
"Sonia (Female)": {"name": "Sonia", "code": "en-GB-SoniaNeural"},
}
def get_instructions():
"""Get instructions for the user"""
content = """
#### Please follow these steps to use the AI Interview App:
1. **Upload Resume**: Upload your resume in PDF format.
2. **Job Description**: Paste the job description.
3. **Start Interview**: Click the "Start Interview" button to begin the AI-driven interview.
4. **Maximum Questions**: You can set the maximum number of questions for the interview. (default: 5)
5. **Voice Selection**: Choose the AI voice for the interview.(default: Alex (Male))
6. **Answer Questions**: Answer the questions posed by the AI interviewer.
7. **Review Results**: After the interview, review your performance and feedback.
**Note**: The AI interviewer will ask you questions based on your resume and the job description.
I hope you find the experience helpful!
"""
return st.markdown(content)
def render_sidebar():
"""Render sidebar with candidate information and settings"""
st.sidebar.title("Candidate Information")
# File upload
uploaded_resume = st.sidebar.file_uploader("Upload your Resume (PDF)", type=["pdf"])
# Job description
job_description = st.sidebar.text_area("Paste the Job Description")
# Settings
max_questions = st.sidebar.number_input(
"Maximum number of questions",
min_value=1,
max_value=10,
value=MAX_QUESTIONS,
)
st.session_state["max_questions"] = max_questions
ai_voice = st.sidebar.radio(
"Select AI Interviewer Voice",
["Alex (Male)", "Aria (Female)", "Natasha (Female)", "Sonia (Female)"],
)
st.session_state["ai_voice"] = ai_voice
submit = st.sidebar.button("Submit")
return uploaded_resume, job_description, submit
def process_resume_submission(uploaded_resume, job_description):
"""Process resume and job description submission"""
with st.spinner("Processing resume..."):
resume_content = load_content_streamlit(uploaded_resume)
name, resume_highlights = extract_resume_info_using_llm(resume_content)
# Store in session state
st.session_state["name"] = name
st.session_state["resume_highlights"] = resume_highlights
st.session_state["job_description"] = job_description
# Reset interview state
reset_interview_state()
st.success(f"Candidate Name: {name}")
def reset_interview_state():
"""Reset interview-related session state"""
interview_keys = [
"interview_started",
"qa_index",
"conversations",
"current_question",
"question_spoken",
"awaiting_response",
"processing_audio",
"messages",
"interview_completed",
"thanks_message_prepared",
"thanks_message_spoken",
"show_final_results",
]
for key in interview_keys:
if key == "interview_started" or key == "interview_completed":
st.session_state[key] = False
elif key in ["qa_index"]:
st.session_state[key] = 1
elif key in ["conversations", "messages"]:
st.session_state[key] = []
elif key in ["current_question"]:
st.session_state[key] = ""
else:
st.session_state[key] = False
def start_interview():
"""Initialize and start the interview"""
st.session_state["interview_started"] = True
reset_interview_state()
st.session_state["interview_started"] = True # Reset above sets this to False
# Get greeting message
ai_voice_details = get_ai_voice_details()
interviewer_name = ai_voice_details[st.session_state["ai_voice"]]["name"]
greeting_message = get_ai_greeting_message(
st.session_state["name"], interviewer_name=interviewer_name
)
st.session_state["current_question"] = greeting_message
st.session_state["messages"].append(
{"role": "assistant", "content": greeting_message}
)
def display_chat_messages():
"""Display all chat messages from history"""
for message in st.session_state["messages"]:
with st.chat_message(message["role"]):
st.markdown(message["content"])
def speak_current_question():
"""Speak the current question if not already spoken"""
if st.session_state["current_question"] and not st.session_state["question_spoken"]:
with st.spinner("AI Interviewer is speaking..."):
ai_voice_details = get_ai_voice_details()
speak_text(
st.session_state["current_question"],
voice=ai_voice_details[st.session_state["ai_voice"]]["code"],
)
st.session_state["question_spoken"] = True
st.session_state["awaiting_response"] = True
st.rerun()
def generate_next_question():
"""Generate and prepare the next question"""
if st.session_state["conversations"]:
last_conv = st.session_state["conversations"][-1]
next_question, _ = asyncio.run(analyze_candidate_response_and_generate_new_question(
last_conv["Question"],
last_conv["Candidate Answer"],
st.session_state["job_description"],
st.session_state["resume_highlights"],
))
else:
next_question = "Tell me about yourself and your experience."
st.session_state["current_question"] = next_question
st.session_state["messages"].append({"role": "assistant", "content": next_question})
st.session_state["question_spoken"] = False
st.session_state["awaiting_response"] = False
def process_candidate_response(transcript):
"""Process candidate's response and move to next state"""
# Add candidate's answer to chat
st.session_state["messages"].append({"role": "user", "content": transcript})
# Generate feedback for this response
if st.session_state["qa_index"] < st.session_state["max_questions"] - 1:
# Not the last question - generate next question and feedback
next_question, feedback = asyncio.run(analyze_candidate_response_and_generate_new_question(
st.session_state["current_question"],
transcript,
st.session_state["job_description"],
st.session_state["resume_highlights"],
))
else:
# Last question - only generate feedback
feedback = asyncio.run(get_feedback_of_candidate_response(
st.session_state["current_question"],
transcript,
st.session_state["job_description"],
st.session_state["resume_highlights"],
))
# Store conversation
st.session_state["conversations"].append(
{
"Question": st.session_state["current_question"],
"Candidate Answer": transcript,
"Evaluation": feedback["score"],
"Feedback": feedback["feedback"],
}
)
# Move to next question or complete interview
st.session_state["qa_index"] += 1
st.session_state["processing_audio"] = False
st.session_state["awaiting_response"] = False
if st.session_state["qa_index"] <= st.session_state["max_questions"]:
# Prepare next question
generate_next_question()
st.success("✅ Answer recorded! Preparing next question...")
else:
# Interview completed - prepare thanks message
st.session_state["interview_completed"] = True
prepare_thanks_message()
def prepare_thanks_message():
"""Prepare and display thanks message"""
if not st.session_state["thanks_message_prepared"]:
final_note = get_final_thanks_message(st.session_state["name"])
st.session_state["messages"].append(
{"role": "assistant", "content": final_note}
)
st.session_state["thanks_message_prepared"] = True
st.session_state["qa_index"] -= 1
st.rerun()
def speak_thanks_message():
"""Speak the thanks message"""
if (
st.session_state["thanks_message_prepared"]
and not st.session_state["thanks_message_spoken"]
):
# Get the last message (thanks message)
thanks_message = st.session_state["messages"][-1]["content"]
with st.spinner("AI Interviewer is giving final remarks..."):
ai_voice_details = get_ai_voice_details()
speak_text(
thanks_message,
voice=ai_voice_details[st.session_state["ai_voice"]]["code"],
)
st.session_state["thanks_message_spoken"] = True
st.success("🎉 Interview completed! Thank you for your time.")
# Now show final results
st.session_state["show_final_results"] = True
st.rerun()
def handle_audio_recording():
"""Handle audio recording and processing"""
if not (
st.session_state["awaiting_response"]
and not st.session_state["processing_audio"]
):
return
st.write("**🎙️ Please record your answer to the question above**")
audio_key = f"audio_input_{st.session_state['qa_index']}_{len(st.session_state['messages'])}"
audio_data = st.audio_input("Record your answer", key=audio_key)
if audio_data is not None:
st.session_state["processing_audio"] = True
with st.spinner("Processing your answer..."):
# Save audio file
name = st.session_state["name"]
filename = f"audio/{name}/{name}_{st.session_state['qa_index'] + 1}.wav"
os.makedirs(os.path.dirname(filename), exist_ok=True)
with open(filename, "wb") as f:
f.write(audio_data.read())
# Transcribe audio
transcript = transcribe_with_speechmatics(filename)
if transcript and transcript.strip():
process_candidate_response(transcript)
st.rerun()
else:
st.error("No speech detected in audio. Please try recording again.")
st.session_state["processing_audio"] = False
def display_final_results():
"""Display final interview results"""
if (
not st.session_state["show_final_results"]
or not st.session_state["conversations"]
):
return
with st.spinner("Calculating final score..."):
final_score = get_overall_evaluation_score(st.session_state["conversations"])
# Save interview data
now = datetime.now().isoformat() + "Z"
interview_data = {
"name": st.session_state["name"],
"createdAt": now,
"updatedAt": now,
"id": 1,
"job_description": st.session_state["job_description"],
"resume_highlights": st.session_state["resume_highlights"],
"conversations": st.session_state["conversations"],
"overall_score": round(final_score, 2),
}
save_interview_data(interview_data, candidate_name=st.session_state["name"])
# Display results
st.subheader("🎉 Interview Results")
st.markdown(f"**Candidate:** {st.session_state['name']}")
st.markdown(f"**Overall Score:** {final_score:.2f}/10")
# Show detailed summary
st.subheader("Detailed Interview Summary")
for i, conv in enumerate(st.session_state["conversations"], 1):
with st.expander(f"Question {i} (Score: {conv['Evaluation']}/10)"):
st.write(f"**Q:** {conv['Question']}")
st.write(f"**A:** {conv['Candidate Answer']}")
st.write(f"**Feedback:** {conv['Feedback']}")
# New interview option
if st.button("Start New Interview"):
for key in list(st.session_state.keys()):
del st.session_state[key]
st.rerun()
def render_interview_progress():
"""Render interview progress indicator"""
if st.session_state.get("interview_started", False):
progress_text = f"Question {st.session_state['qa_index']} of {st.session_state['max_questions']}"
st.markdown(
f'<div class="interview-progress"><strong>{progress_text}</strong></div>',
unsafe_allow_html=True,
)
def main():
"""Main application function"""
# Setup
setup_page_config()
initialize_session_state()
# Header
st.title("🤖 AI Interview System")
insturctions = st.empty()
if not st.session_state["interview_started"]:
insturctions = get_instructions()
# Sidebar
uploaded_resume, job_description, submit = render_sidebar()
# Process submission
if submit and uploaded_resume and job_description:
insturctions.empty()
process_resume_submission(uploaded_resume, job_description)
# Start interview button
if st.session_state["name"] and not st.session_state["interview_started"]:
if st.button("Start Interview"):
start_interview()
st.rerun()
# Interview section
if st.session_state.get("interview_started", False):
render_interview_progress()
# Show chat history
st.subheader("Interview Chat")
display_chat_messages()
# Handle different interview states
if not st.session_state["interview_completed"]:
# Active interview
speak_current_question()
handle_audio_recording()
elif not st.session_state["thanks_message_prepared"]:
# Interview just completed - prepare thanks
prepare_thanks_message()
elif not st.session_state["thanks_message_spoken"]:
# Thanks message prepared but not spoken
speak_thanks_message()
else:
# Everything done - show results
display_final_results()
if __name__ == "__main__":
main()