Viper51 commited on
Commit
4b4bcc2
·
verified ·
1 Parent(s): 3abfe59

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +341 -38
src/streamlit_app.py CHANGED
@@ -1,40 +1,343 @@
1
- import altair as alt
2
- import numpy as np
3
- import pandas as pd
4
  import streamlit as st
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
- """
7
- # Welcome to Streamlit!
8
-
9
- Edit `/streamlit_app.py` to customize this app to your heart's desire :heart:.
10
- If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
11
- forums](https://discuss.streamlit.io).
12
-
13
- In the meantime, below is an example of what you can do with just a few lines of code:
14
- """
15
-
16
- num_points = st.slider("Number of points in spiral", 1, 10000, 1100)
17
- num_turns = st.slider("Number of turns in spiral", 1, 300, 31)
18
-
19
- indices = np.linspace(0, 1, num_points)
20
- theta = 2 * np.pi * num_turns * indices
21
- radius = indices
22
-
23
- x = radius * np.cos(theta)
24
- y = radius * np.sin(theta)
25
-
26
- df = pd.DataFrame({
27
- "x": x,
28
- "y": y,
29
- "idx": indices,
30
- "rand": np.random.randn(num_points),
31
- })
32
-
33
- st.altair_chart(alt.Chart(df, height=700, width=700)
34
- .mark_point(filled=True)
35
- .encode(
36
- x=alt.X("x", axis=None),
37
- y=alt.Y("y", axis=None),
38
- color=alt.Color("idx", legend=None, scale=alt.Scale()),
39
- size=alt.Size("rand", legend=None, scale=alt.Scale(range=[1, 150])),
40
- ))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import streamlit as st
2
+ from PyPDF2 import PdfReader
3
+ import google.generativeai as genai
4
+ from langchain_google_genai import ChatGoogleGenerativeAI
5
+ from langchain_core.prompts import PromptTemplate
6
+ from pydantic import BaseModel, Field
7
+ from typing import Optional
8
+ from gtts import gTTS
9
+ import speech_recognition as sr
10
+ import os
11
+ import io
12
+ import tempfile
13
+ from streamlit_mic_recorder import mic_recorder # Key component for browser audio
14
 
15
+ # --- Configuration & Secrets ---
16
+
17
+ # Load API Key from Streamlit/Hugging Face Secrets
18
+ # DO NOT hardcode your key. Add it to your HF Space's secrets.
19
+ try:
20
+ GOOGLE_API_KEY = st.secrets["GOOGLE_API_KEY"]
21
+ genai.configure(api_key=GOOGLE_API_KEY)
22
+ os.environ["GOOGLE_API_KEY"] = GOOGLE_API_KEY
23
+ except KeyError:
24
+ st.error("GOOGLE_API_KEY not found in Streamlit secrets. Please add it to your Hugging Face Space secrets.", icon="🚨")
25
+ st.stop()
26
+ except Exception as e:
27
+ st.error(f"Error configuring Google API: {e}", icon="🚨")
28
+ st.stop()
29
+
30
+ # --- Pydantic Models (from your code) ---
31
+
32
+ class questions(BaseModel):
33
+ questions: list[str] = Field(description="List of questions")
34
+
35
+ class introduction(BaseModel):
36
+ intro: Optional[str] = Field(description="Give AI agent's intro")
37
+ question: str = Field(description="Question asked by AI agent")
38
+ followup: Optional[str] = Field(description="The followup question to user's answer")
39
+
40
+ class evaluation(BaseModel):
41
+ marks: int = Field(description="Marks out of 100")
42
+ followup: Optional[str] = Field(description="The followup question")
43
+ review: Optional[str] = Field(description="Short Review of the answer")
44
+
45
+ # --- AI & Logic Functions (from your code, slightly modified) ---
46
+
47
+ @st.cache_resource
48
+ def get_llm():
49
+ """Cached function to initialize the LLM."""
50
+ return ChatGoogleGenerativeAI(model="gemini-2.5-flash", temperature=1.0)
51
+
52
+ @st.cache_resource
53
+ def get_models(_llm):
54
+ """Cached function to get structured output models."""
55
+ generate_questions_resume_model = _llm.with_structured_output(questions)
56
+ intro_model = _llm.with_structured_output(introduction)
57
+ evaluate_answers_model = _llm.with_structured_output(evaluation)
58
+ return generate_questions_resume_model, intro_model, evaluate_answers_model
59
+
60
+ def read_resume(uploaded_file):
61
+ """Reads a PDF file uploaded via Streamlit."""
62
+ try:
63
+ reader = PdfReader(uploaded_file)
64
+ text = ""
65
+ for page in reader.pages:
66
+ text += page.extract_text() or "" # Add check for None
67
+ return text
68
+ except Exception as e:
69
+ st.error(f"Error reading PDF: {e}")
70
+ return None
71
+
72
+ def generate_questions_from_resume(resume_text, model):
73
+ """Generates interview questions from resume text."""
74
+ parse_resume_prompt_template = PromptTemplate(
75
+ template="""Generate 4-8 interview questions about the Experience and Projects section from this given text of from a resume.
76
+ Try to cover all projects and experience. Generate some conceptual questions too. Don't generate unnecessary questions.
77
+ Resume:\n{text}""",
78
+ input_variables=['text']
79
+ )
80
+ generate_question_from_resume_chain = parse_resume_prompt_template | model
81
+ output = generate_question_from_resume_chain.invoke({'text': resume_text})
82
+ return output.questions
83
+
84
+ def get_introduction(model):
85
+ """Gets the AI's intro and first question."""
86
+ introduction_prompt = PromptTemplate(template="""Introduce yourself to the user telling the user that you are a AI agent. And ask the user to give introduction""")
87
+ intro_chain = introduction_prompt | model
88
+ output = intro_chain.invoke({})
89
+ return output
90
+
91
+ def ask_followup(user_intro, model):
92
+ """Asks a followup to the user's intro."""
93
+ intro_followup = PromptTemplate(template="""The user has given the following introduction of himself/herself. Ask a followup about his intro to make the user comfortable. Intro given by the user: {intro}""",
94
+ input_variables=['intro'])
95
+ followup_chain = intro_followup | model
96
+ output = followup_chain.invoke({'intro': user_intro})
97
+ return output.followup
98
+
99
+ def evaluate_answer(question, answer, model):
100
+ """Evaluates the user's answer."""
101
+ evaluate_answer_prompt = PromptTemplate(template="""You are given a question and an answer. Evaluate the answer honestly on the question out of 100.
102
+ Also generate a very short review on the answer telling the candidate about his answer. If he is wrong but close to the correct answer, give subtle hints.
103
+ If a good followup question can be asked generate it but only if it is a genuine question.\nQuestion: {question}\n\n Answer: {answer}""",
104
+ input_variables=['question', 'answer'])
105
+ evaluate_chain = evaluate_answer_prompt | model
106
+ output = evaluate_chain.invoke({'question': question, 'answer': answer})
107
+ return output
108
+
109
+ # --- Streamlit Audio/Visual Functions ---
110
+
111
+ def text_to_speech_and_display(text, autoplay=True):
112
+ """Converts text to speech, displays text, and plays audio."""
113
+ if not text:
114
+ return
115
+
116
+ try:
117
+ # Display the caption
118
+ st.session_state.chat_history.append(f"**Interviewer:** {text}")
119
+
120
+ # Generate audio
121
+ tts = gTTS(text=text, lang='en', slow=False)
122
+ audio_fp = io.BytesIO()
123
+ tts.write_to_fp(audio_fp)
124
+ audio_fp.seek(0)
125
+
126
+ # Display audio player
127
+ st.audio(audio_fp, format='audio/mp3', autoplay=autoplay)
128
+
129
+ except Exception as e:
130
+ st.error(f"Error in text-to-speech: {e}")
131
+
132
+ def speech_to_text(audio_bytes):
133
+ """Converts recorded audio bytes to text using SpeechRecognition."""
134
+ if not audio_bytes:
135
+ return "No audio recorded."
136
+
137
+ r = sr.Recognizer()
138
+
139
+ # Need to save bytes to a temporary WAV file
140
+ # because recognizer.recognize_google requires a file path or AudioData
141
+ try:
142
+ with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as temp_wav:
143
+ temp_wav.write(audio_bytes)
144
+ temp_wav_path = temp_wav.name
145
+
146
+ # Use the temp file
147
+ with sr.AudioFile(temp_wav_path) as source:
148
+ audio_data = r.record(source)
149
+
150
+ # Recognize speech
151
+ text = r.recognize_google(audio_data)
152
+ st.session_state.chat_history.append(f"**You:** {text}")
153
+ return text
154
+
155
+ except sr.UnknownValueError:
156
+ st.warning("Could not understand audio.")
157
+ return None
158
+ except sr.RequestError as e:
159
+ st.error(f"Speech recognition service error: {e}")
160
+ return None
161
+ except Exception as e:
162
+ st.error(f"Error processing audio: {e}")
163
+ return None
164
+ finally:
165
+ # Clean up the temp file
166
+ if 'temp_wav_path' in locals() and os.path.exists(temp_wav_path):
167
+ os.remove(temp_wav_path)
168
+
169
+ # --- Main Streamlit App ---
170
+
171
+ st.set_page_config(page_title="AI Interviewer", layout="wide")
172
+ st.title("🤖 AI Interviewer")
173
+
174
+ # Initialize LLM and models
175
+ llm = get_llm()
176
+ gen_q_model, intro_model, eval_model = get_models(llm)
177
+
178
+ # --- Session State Initialization ---
179
+ # This is crucial for making the app work step-by-step
180
+ if 'stage' not in st.session_state:
181
+ st.session_state.stage = 'start'
182
+ if 'chat_history' not in st.session_state:
183
+ st.session_state.chat_history = []
184
+ if 'questions' not in st.session_state:
185
+ st.session_state.questions = []
186
+ if 'q_index' not in st.session_state:
187
+ st.session_state.q_index = 0
188
+ if 'current_question' not in st.session_state:
189
+ st.session_state.current_question = ""
190
+ if 'total_marks' not in st.session_state:
191
+ st.session_state.total_marks = 0
192
+ if 'num_questions' not in st.session_state:
193
+ st.session_state.num_questions = 0
194
+
195
+ # --- App Logic (State Machine) ---
196
+
197
+ # --- STAGE 0: Start (File Upload) ---
198
+ if st.session_state.stage == 'start':
199
+ st.info("Welcome! Please upload your resume (PDF) to begin the interview.")
200
+ uploaded_file = st.file_uploader("Upload your Resume (PDF)", type=["pdf"])
201
+
202
+ if uploaded_file:
203
+ with st.spinner("Analyzing your resume... This may take a moment."):
204
+ resume_text = read_resume(uploaded_file)
205
+ if resume_text:
206
+ # 1. Generate Questions
207
+ st.session_state.questions = generate_questions_from_resume(resume_text, gen_q_model)
208
+ if not st.session_state.questions:
209
+ st.error("Could not generate questions from the resume. Please try another file.")
210
+ st.session_state.stage = 'start'
211
+ else:
212
+ # 2. Get AI Introduction
213
+ intro_output = get_introduction(intro_model)
214
+ st.session_state.current_question = intro_output.question
215
+
216
+ # 3. Move to next stage and display intro
217
+ st.session_state.stage = 'awaiting_intro'
218
+ text_to_speech_and_display(intro_output.intro)
219
+ text_to_speech_and_display(intro_output.question)
220
+ st.rerun() # Rerun to update the UI
221
+
222
+ # --- Main Interview Area (Stages > 0) ---
223
+ if st.session_state.stage != 'start':
224
+
225
+ # --- Chat History Display ---
226
+ st.subheader("Interview Transcript")
227
+ chat_container = st.container(height=300, border=True)
228
+ with chat_container:
229
+ for entry in st.session_state.chat_history:
230
+ st.markdown(entry)
231
+
232
+ st.divider()
233
+
234
+ # --- Audio Recorder ---
235
+ # This component returns audio bytes when the user stops recording
236
+ st.write("Your turn to speak:")
237
+ audio_bytes = mic_recorder(
238
+ start_prompt="Start Recording",
239
+ stop_prompt="Stop Recording",
240
+ key='recorder'
241
+ )
242
+
243
+ # --- End Interview Button ---
244
+ if st.button("End Interview", type="primary"):
245
+ st.session_state.stage = 'finished'
246
+ st.rerun()
247
+
248
+ # --- Process Recorded Audio ---
249
+ if audio_bytes:
250
+ with st.spinner("Transcribing your answer..."):
251
+ user_text = speech_to_text(audio_bytes['bytes'])
252
+
253
+ if user_text:
254
+ # --- STAGE 1: Process User's Introduction ---
255
+ if st.session_state.stage == 'awaiting_intro':
256
+ with st.spinner("Thinking of a followup..."):
257
+ followup = ask_followup(user_text, intro_model)
258
+ st.session_state.current_question = followup
259
+ text_to_speech_and_display(followup)
260
+ st.session_state.stage = 'awaiting_intro_followup'
261
+ st.rerun()
262
+
263
+ # --- STAGE 2: Process Followup to Intro ---
264
+ elif st.session_state.stage == 'awaiting_intro_followup':
265
+ text_to_speech_and_display("OK, Great. Let's start the interview with questions from your resume.")
266
+ st.session_state.stage = 'asking_question' # Move to main questions
267
+ st.rerun()
268
+
269
+ # --- STAGE 4: Process Answer to a Main Question ---
270
+ elif st.session_state.stage == 'awaiting_answer':
271
+ with st.spinner("Evaluating your answer..."):
272
+ question_asked = st.session_state.current_question
273
+ output = evaluate_answer(question_asked, user_text, eval_model)
274
+
275
+ st.session_state.total_marks += output.marks
276
+ st.session_state.num_questions += 1
277
+
278
+ if output.review:
279
+ text_to_speech_and_display(output.review)
280
+
281
+ if output.followup:
282
+ # Ask followup question
283
+ st.session_state.current_question = output.followup
284
+ text_to_speech_and_display(output.followup)
285
+ st.session_state.stage = 'awaiting_followup_answer'
286
+ else:
287
+ # Move to next question
288
+ st.session_state.q_index += 1
289
+ st.session_state.stage = 'asking_question'
290
+ st.rerun()
291
+
292
+ # --- STAGE 5: Process Answer to a Followup Question ---
293
+ elif st.session_state.stage == 'awaiting_followup_answer':
294
+ with st.spinner("Evaluating your answer..."):
295
+ question_asked = st.session_state.current_question
296
+ output = evaluate_answer(question_asked, user_text, eval_model)
297
+
298
+ st.session_state.total_marks += output.marks
299
+ st.session_state.num_questions += 1
300
+
301
+ if output.review:
302
+ text_to_speech_and_display(output.review)
303
+
304
+ # Always move to the next main question after a followup
305
+ st.session_state.q_index += 1
306
+ st.session_state.stage = 'asking_question'
307
+ st.rerun()
308
+
309
+ # --- STAGE 3: Ask a New Question ---
310
+ if st.session_state.stage == 'asking_question':
311
+ if st.session_state.q_index < len(st.session_state.questions):
312
+ # Ask the next question
313
+ question = st.session_state.questions[st.session_state.q_index]
314
+ st.session_state.current_question = question
315
+ text_to_speech_and_display(question)
316
+ st.session_state.stage = 'awaiting_answer'
317
+ else:
318
+ # No more questions
319
+ st.session_state.stage = 'finished'
320
+ st.rerun()
321
+
322
+ # --- STAGE 6: Finished ---
323
+ if st.session_state.stage == 'finished':
324
+ st.balloons()
325
+ st.success("Interview Complete!")
326
+
327
+ final_score = 0
328
+ if st.session_state.num_questions > 0:
329
+ final_score = st.session_state.total_marks / st.session_state.num_questions
330
+
331
+ st.subheader("Final Report")
332
+ st.markdown(f"**Total Questions Answered:** {st.session_state.num_questions}")
333
+ st.markdown(f"**Average Score:** {final_score:.2f} / 100")
334
+
335
+ st.subheader("Full Transcript")
336
+ for entry in st.session_state.chat_history:
337
+ st.markdown(entry)
338
+
339
+ if st.button("Start New Interview"):
340
+ # Clear all session state
341
+ for key in st.session_state.keys():
342
+ del st.session_state[key]
343
+ st.rerun()