Nam Fam commited on
Commit
3549ca5
·
1 Parent(s): 91d5eaf
Files changed (5) hide show
  1. Dockerfile +20 -0
  2. README.md +3 -0
  3. app.py +495 -0
  4. assets/sample_rubric.yaml +18 -0
  5. requirements.txt +4 -0
Dockerfile ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use an official Python runtime as a parent image
2
+ FROM python:3.9-slim-buster
3
+
4
+ # Set the working directory in the container
5
+ WORKDIR /app
6
+
7
+ # Copy the requirements file into the container at /app
8
+ COPY requirements.txt .
9
+
10
+ # Install any needed packages specified in requirements.txt
11
+ RUN pip install --no-cache-dir -r requirements.txt
12
+
13
+ # Copy the current directory contents into the container at /app
14
+ COPY . .
15
+
16
+ # Expose the port that Streamlit runs on
17
+ EXPOSE 8501
18
+
19
+ # Run the Streamlit application
20
+ CMD ["streamlit", "run", "app.py", "--server.port=8501", "--server.address=0.0.0.0"]
README.md CHANGED
@@ -4,6 +4,9 @@ emoji: 📚
4
  colorFrom: red
5
  colorTo: gray
6
  sdk: docker
 
 
 
7
  pinned: false
8
  ---
9
 
 
4
  colorFrom: red
5
  colorTo: gray
6
  sdk: docker
7
+ app_port: 8501
8
+ tags:
9
+ - streamlit
10
  pinned: false
11
  ---
12
 
app.py ADDED
@@ -0,0 +1,495 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import yaml
3
+ import requests
4
+ import pypdf
5
+ import docx
6
+ import re
7
+ from io import BytesIO
8
+ import os
9
+
10
+ st.set_page_config(page_title="AI Interview Scorer", page_icon="🤖", layout="wide")
11
+ st.title("Intelcruit: AI Interview Scorer")
12
+ # --- MOCK DATA CONSTANTS (to make frontend self-contained) ---
13
+ MOCK_JOB_DESCRIPTION = """
14
+ Mô tả công việc
15
+
16
+ Sử dụng các công cụ và framework như TensorFlow, PyTorch, và Hugging Face Transformers để xây dựng các mô hình ngôn ngữ.
17
+ Sử dụng các kỹ thuật NLP để phân tích, trích xuất thông tin từ văn bản, và xử lý ngôn ngữ tự nhiên.
18
+ Phát triển các hệ thống truy xuất thông tin từ cơ sở dữ liệu để hỗ trợ quá trình tạo ra câu trả lời chính xác và đầy đủ.
19
+ Sử dụng các kỹ thuật RAG để kết hợp thông tin truy xuất từ các nguồn dữ liệu với khả năng sinh văn bản của mô hình.
20
+ Theo dõi và nghiên cứu các xu hướng và công nghệ mới trong lĩnh vực NLP, Chatbot và RAG.
21
+ Tối ưu hóa thời gian phản hồi và hiệu suất của hệ thống truy xuất thông tin.
22
+
23
+ Yêu cầu ứng viên
24
+
25
+ Có tối thiểu 1 năm kinh nghiệm
26
+ Tốt nghiệp Cao đẳng/Đại học các chuyên ngành Công nghệ Thông tin, Toán Tin, Điện tử Viễn thông, Điều khiển Tự động, hoặc các ngành liên quan.
27
+ Kiến thức chuyên môn:
28
+ Có hiểu biết về Machine Learning và Deep Learning.
29
+ Kinh nghiệm làm việc với các mô hình ngôn ngữ lớn (LLM)
30
+ Có kinh nghiệm làm việc với RESTAPI, Langchain, llamaindex, ...
31
+ Kỹ năng nghiên cứu và nền tảng:
32
+ Khả năng nghiên cứu và áp dụng các công nghệ mới.
33
+ Nền tảng vững chắc về cấu trúc dữ liệu và thuật toán.
34
+ Hiểu biết và có kinh nghiệm lập trình với các ngôn ngữ như C++ và Python.
35
+ Có kinh nghiệm làm việc với cơ sở dữ liệu SQL.
36
+
37
+ Quyền lợi
38
+
39
+ Mức lương: thỏa thuận khi phỏng vấn
40
+ Công ty đóng 100% BHYT, BHXH, BHTN
41
+ Công ty cung cấp thiết bị làm việc
42
+ Review lương 1 - 2 lần/năm theo năng lực
43
+ Thưởng ngày lễ 2/9, 30/04, 1/5, ..., Tết, thưởng lương tháng 13
44
+ Thưởng kết quả kinh doanh toàn công ty cuối năm
45
+ Du lịch 2 lần/năm
46
+ Môi trường làm việc năng động, chuyên nghiệp
47
+
48
+
49
+ """
50
+
51
+ MOCK_RUBRIC_CONTENT = """expertise:
52
+ description: "Đánh giá mức độ thành thạo về chuyên môn AI, bao gồm kiến thức và kinh nghiệm thực tế với NLP, LLM, RAG, và các công cụ như PyTorch, TensorFlow, HuggingFace, LangChain, REST API. Khả năng áp dụng thuật toán, xử lý dữ liệu và tối ưu hóa hệ thống cũng được xem xét."
53
+ weight: 0.7
54
+
55
+ communication:
56
+ description: "Đánh giá khả năng trình bày ý tưởng rõ ràng, trao đổi kỹ thuật hiệu quả, viết tài liệu hoặc báo cáo kỹ thuật dễ hiểu, và khả năng giao tiếp với các thành viên không chuyên kỹ thuật (PM, khách hàng nội bộ)."
57
+ weight: 0.3
58
+
59
+
60
+ """
61
+
62
+ # --- MOCK DATA PATHS ---
63
+ # Get the absolute path of the directory containing the current script (frontend/)
64
+ SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
65
+ # Go up one level to get the project root (intelcruit/)
66
+ PROJECT_ROOT = os.path.dirname(SCRIPT_DIR)
67
+ # Construct the full, robust paths to the mock files
68
+ MOCK_AUDIO_PATH = os.path.join(PROJECT_ROOT, "backend", "examples", "example_interview_audio_tts_ai_engineer.wav")
69
+ MOCK_TRANSCRIPT_PATH = os.path.join(PROJECT_ROOT, "backend", "examples", "example_interview_transcipt.txt")
70
+ MOCK_RESUME_PATH = os.path.join(PROJECT_ROOT, "backend", "examples", "example_resume_ai_engineer.pdf")
71
+ MOCK_JD_PATH = os.path.join(PROJECT_ROOT, "backend", "examples", "example_job_description.txt")
72
+ MOCK_RUBRIC_PATH = os.path.join(PROJECT_ROOT, "backend", "examples", "example_rubric.yaml")
73
+ MOCK_TRANSCRIPT_PLACEHOLDER = "This is a placeholder for the mock transcript. It will be replaced by content from the mock file if available."
74
+
75
+ def load_mock_file(path, mime_type):
76
+ """Loads a mock file from the given path and returns a BytesIO object."""
77
+ if not os.path.exists(path):
78
+ st.warning(f"Mock file not found at: {path}. Please ensure it exists.")
79
+ return None
80
+ with open(path, "rb") as f:
81
+ file_bytes = f.read()
82
+ mock_file = BytesIO(file_bytes)
83
+ mock_file.name = os.path.basename(path)
84
+ mock_file.type = mime_type
85
+ return mock_file
86
+
87
+ # --- API & HELPER FUNCTIONS ---
88
+ def calculate_overall_score(scored_pairs, rubric):
89
+ if not rubric or not scored_pairs:
90
+ return 0, {}
91
+ category_weights = {cat: data.get('weight', 1) for cat, data in rubric.items()}
92
+ total_rubric_weight = sum(category_weights.values())
93
+ if total_rubric_weight == 0:
94
+ return 0, {}
95
+ category_scores = {cat: [] for cat in category_weights.keys()}
96
+ for pair in scored_pairs:
97
+ if 'analysis' in pair and 'scores' in pair['analysis']:
98
+ for score_item in pair['analysis']['scores']:
99
+ category = score_item.get('category')
100
+ score = score_item.get('score')
101
+ if category in category_scores and isinstance(score, (int, float)):
102
+ category_scores[category].append(score)
103
+ avg_category_scores = {}
104
+ for cat, scores in category_scores.items():
105
+ avg_category_scores[cat] = sum(scores) / len(scores) if scores else 0
106
+ weighted_score = sum(avg_score * category_weights.get(cat, 1) for cat, avg_score in avg_category_scores.items())
107
+ final_score_10 = weighted_score / total_rubric_weight
108
+
109
+ # Scale scores to 100
110
+ final_score_100 = final_score_10 * 10
111
+ avg_category_scores_100 = {cat: score * 10 for cat, score in avg_category_scores.items()}
112
+
113
+ return final_score_100, avg_category_scores_100
114
+
115
+ def calculate_resume_overall_score(results):
116
+ """Calculates the overall resume score from the detailed scores."""
117
+ if not results:
118
+ return 0
119
+
120
+ scores = []
121
+ for category in ['experience', 'education', 'skills']:
122
+ # Safely get the score, defaulting to 0 if not found or not a number
123
+ score = results.get(category, {}).get('score')
124
+ if isinstance(score, (int, float)):
125
+ scores.append(score)
126
+
127
+ if not scores:
128
+ return 0
129
+
130
+ # Calculate the average score
131
+ overall_score = sum(scores) / len(scores)
132
+ return round(overall_score, 1)
133
+
134
+ def display_results(results_data, rubric_content):
135
+ try:
136
+ rubric = yaml.safe_load(rubric_content)
137
+ except yaml.YAMLError:
138
+ st.error("Could not parse rubric.")
139
+ rubric = {}
140
+ # st.header("📊 Analysis Report")
141
+ scored_pairs = results_data.get('results', {}).get('scored_qa_pairs', [])
142
+ if not scored_pairs:
143
+ st.warning("No scorable question and answer pairs were found.")
144
+ return
145
+ st.subheader("Summary")
146
+ final_score, avg_category_scores = calculate_overall_score(scored_pairs, rubric)
147
+ st.metric(label="Overall Score", value=f"{final_score:.1f}/100")
148
+ if avg_category_scores:
149
+ st.markdown("**Category Scores:**")
150
+ cols = st.columns(len(avg_category_scores))
151
+ for i, (cat, score) in enumerate(avg_category_scores.items()):
152
+ with cols[i]:
153
+ st.metric(label=cat.replace('_', ' ').title(), value=f"{score:.1f}/100")
154
+ st.markdown("---")
155
+ st.subheader("Detailed Question & Answer Analysis")
156
+ categorized_scores = {}
157
+ for pair in scored_pairs:
158
+ category = pair.get('category', 'general')
159
+ if category not in categorized_scores:
160
+ categorized_scores[category] = []
161
+ categorized_scores[category].append(pair)
162
+ sorted_categories = sorted(categorized_scores.keys(), key=lambda x: (x == 'general', x))
163
+ for category in sorted_categories:
164
+ pairs = categorized_scores[category]
165
+ # st.subheader(f"Category: {category.replace('_', ' ').title()}")
166
+ for pair in pairs:
167
+ with st.expander(f"**{pair['question']}**"):
168
+ st.markdown(f"**Candidate's Answer:** *{pair['answer']}*")
169
+ if 'analysis' in pair and 'scores' in pair['analysis']:
170
+ st.markdown("**AI Analysis:**")
171
+ for score_item in pair['analysis']['scores']:
172
+ category_name = score_item.get('category', 'General').replace('_', ' ').title()
173
+ score = score_item.get('score', 'N/A')
174
+ reasoning = score_item.get('reasoning', 'No reasoning provided.')
175
+ st.markdown(f"**{category_name} Score:** `{score}/10`")
176
+ st.info(f"**Explanation:** {reasoning}")
177
+ elif 'analysis' in pair and 'error' in pair['analysis']:
178
+ st.error(f"Could not score this answer: {pair['analysis']['error']}")
179
+ else:
180
+ st.warning("No analysis available for this Q&A pair.")
181
+
182
+ BASE_URL = "http://127.0.0.1:8000"
183
+ RESUME_EXTRACTION_URL = f"{BASE_URL}/extract_from_resume/"
184
+
185
+ def get_text_from_file(file):
186
+ """Extracts text from an uploaded file (PDF, DOCX, TXT)."""
187
+ text = ""
188
+ try:
189
+ file.seek(0) # Reset file pointer
190
+ if file.type == "application/pdf":
191
+ pdf_reader = pypdf.PdfReader(file)
192
+ raw_text = " ".join(page.extract_text() or "" for page in pdf_reader.pages)
193
+ text = re.sub(r'\s+', ' ', raw_text).strip()
194
+ elif file.type == "application/vnd.openxmlformats-officedocument.wordprocessingml.document":
195
+ doc = docx.Document(file)
196
+ raw_text = " ".join(p.text for p in doc.paragraphs)
197
+ text = re.sub(r'\s+', ' ', raw_text).strip()
198
+ elif file.type == "text/plain":
199
+ text = file.read().decode('utf-8')
200
+ else:
201
+ st.warning(f"Unsupported file type: {file.type}")
202
+ return None
203
+ except Exception as e:
204
+ st.error(f"Error reading file: {e}")
205
+ return None
206
+ return text
207
+
208
+ def display_resume_content(file):
209
+ """Displays the content of the uploaded resume in the UI."""
210
+ resume_text = get_text_from_file(file)
211
+ if resume_text:
212
+ st.text_area("Resume Content", value=resume_text, height=300, disabled=True)
213
+ else:
214
+ st.info("Could not display content for this file type or an error occurred.")
215
+
216
+ def call_analyze_mock_api():
217
+ url = f"{BASE_URL}/analyze_mock/"
218
+ try:
219
+ response = requests.post(url, timeout=120)
220
+ response.raise_for_status()
221
+ return response.json()
222
+ except requests.exceptions.RequestException as e:
223
+ return {"error": f"Failed to connect to backend: {e}"}
224
+
225
+ def call_analyze_interview_api(job_description, rubric_content, audio_file=None, transcript_content=None):
226
+ """Calls the backend to analyze an interview from audio or transcript."""
227
+ api_url = f"{BASE_URL}/analyze/"
228
+ files = {}
229
+ data = {
230
+ 'job_description': job_description,
231
+ 'rubric_content': rubric_content
232
+ }
233
+
234
+ if audio_file:
235
+ files['audio_file'] = (audio_file.name, audio_file.getvalue(), audio_file.type)
236
+ elif transcript_content:
237
+ data['transcript_content'] = transcript_content
238
+ else:
239
+ # This case should ideally be prevented by the UI
240
+ return {"error": "No audio file or transcript was provided."}
241
+
242
+ try:
243
+ # Use a long timeout as analysis can take time
244
+ response = requests.post(api_url, files=files, data=data, timeout=300)
245
+ response.raise_for_status()
246
+ return response.json()
247
+ except requests.exceptions.RequestException as e:
248
+ return {"error": f"API request failed: {e}"}
249
+
250
+ def call_analyze_resume_api(resume_file, job_description):
251
+ """Calls the backend to analyze and score a resume."""
252
+ api_url = f"{BASE_URL}/analyze_resume/"
253
+ files = {'resume_file': (resume_file.name, resume_file.getvalue(), resume_file.type)}
254
+ data = {'job_description': job_description}
255
+ try:
256
+ response = requests.post(api_url, files=files, data=data, timeout=180)
257
+ response.raise_for_status()
258
+ return response.json()
259
+ except requests.exceptions.RequestException as e:
260
+ return {"error": f"API request failed: {e}"}
261
+
262
+ def call_extract_api(resume_file):
263
+ if not resume_file:
264
+ return None
265
+ files = {'resume_file': (resume_file.name, resume_file.getvalue(), resume_file.type)}
266
+ try:
267
+ response = requests.post(RESUME_EXTRACTION_URL, files=files, timeout=60)
268
+ response.raise_for_status()
269
+ return response.json()
270
+ except requests.exceptions.RequestException as e:
271
+ st.error(f"Error connecting to backend for extraction: {e}")
272
+ return {"error": str(e)}
273
+
274
+ # --- PAGE CONFIG & SESSION STATE ---
275
+
276
+ if 'interview_results' not in st.session_state:
277
+ st.session_state.interview_results = None
278
+ if 'use_mock_data' not in st.session_state:
279
+ st.session_state.use_mock_data = False
280
+ if 'candidate_info' not in st.session_state:
281
+ st.session_state.candidate_info = None
282
+ if 'resume_file_name' not in st.session_state:
283
+ st.session_state.resume_file_name = None
284
+ if 'jd_input' not in st.session_state:
285
+ st.session_state.jd_input = None
286
+
287
+ # --- UI LAYOUT ---
288
+ # --- SIDEBAR ---
289
+ st.sidebar.header("⚙️ Configuration")
290
+ st.session_state.use_mock_data = st.sidebar.checkbox("Use Mock Data for Quick Testing", key='use_mock_data_checkbox')
291
+
292
+ if st.session_state.use_mock_data:
293
+ job_description = st.sidebar.text_area("Job Description", value=MOCK_JOB_DESCRIPTION, height=250, disabled=True)
294
+ rubric_content = st.sidebar.text_area("Scoring Rubric (YAML)", value=MOCK_RUBRIC_CONTENT, height=400, disabled=True)
295
+ else:
296
+ job_description = st.sidebar.text_area("Job Description", placeholder="Paste the full job description here...", height=250, key="jd_input")
297
+ rubric_content = st.sidebar.text_area("Scoring Rubric (YAML)", placeholder="Paste the YAML scoring rubric here...", height=400)
298
+
299
+ st.sidebar.markdown("---")
300
+ # st.sidebar.info("Configure the job details here, then manage candidate analysis in the main panel.")
301
+
302
+ # --- MAIN PANEL ---
303
+ # --- Candidate Profile Section ---
304
+ with st.container():
305
+ st.subheader("Candidate Profile")
306
+ profile_col1, profile_col2, profile_col3 = st.columns([1, 1, 2])
307
+ with profile_col1:
308
+
309
+ if st.session_state.use_mock_data:
310
+ uploaded_resume = load_mock_file(MOCK_RESUME_PATH, "application/pdf")
311
+
312
+ else:
313
+ uploaded_resume = st.file_uploader("Upload Candidate Resume/CV", type=["pdf", "docx", "txt"], key="resume_uploader")
314
+
315
+ if uploaded_resume:
316
+ st.image("https://www.w3schools.com/howto/img_avatar.png", width=150)
317
+
318
+
319
+ if uploaded_resume and uploaded_resume.name != st.session_state.get('resume_file_name'):
320
+ st.session_state.resume_file_name = uploaded_resume.name
321
+ st.session_state.candidate_info = None
322
+ with st.spinner("🤖 Extracting key information from resume..."):
323
+ extracted_data = call_extract_api(uploaded_resume)
324
+ if extracted_data and "error" not in extracted_data:
325
+ st.session_state.candidate_info = extracted_data
326
+ st.success("Information extracted successfully!")
327
+ elif extracted_data:
328
+ st.error(f"Extraction failed: {extracted_data.get('error')}")
329
+
330
+ with profile_col2:
331
+ st.markdown("**Basic Information:**")
332
+ if st.session_state.candidate_info:
333
+ info = st.session_state.candidate_info
334
+ st.write(f"**Full Name:** {info.get('full_name', 'N/A')}")
335
+ st.write(f"**Email:** {info.get('email', 'N/A')}")
336
+ st.write(f"**Phone:** {info.get('phone', 'N/A')}")
337
+ else:
338
+ st.info("Upload a resume to extract details.")
339
+
340
+ with profile_col3:
341
+ st.markdown("**Summaries:**")
342
+ if st.session_state.candidate_info:
343
+ info = st.session_state.candidate_info
344
+ with st.expander("Experience"):
345
+ st.markdown(info.get('experience_summary', 'N/A'))
346
+ with st.expander("Education"):
347
+ st.markdown(info.get('education_summary', 'N/A'))
348
+ with st.expander("Skills"):
349
+ st.markdown(info.get('skill_summary', 'N/A'))
350
+ else:
351
+ st.info("Summaries appear after extraction.")
352
+
353
+ if uploaded_resume:
354
+ st.info(f"Using mock resume: {uploaded_resume.name}",)
355
+
356
+ # --- Analysis Tabs ---
357
+ st.subheader("Assessment")
358
+ resume_tab, interview_tab = st.tabs(["📝 Resume/CV Score", "🎙️ Interview Score"])
359
+
360
+ with interview_tab:
361
+ st.header("Interview Analysis")
362
+
363
+ # Add a radio button for input method selection
364
+ input_method = st.radio("Choose input method:", ("Upload Audio", "Enter Transcript"), key="interview_input_method")
365
+
366
+ audio_file = None
367
+ transcript_input = None
368
+
369
+ if input_method == "Upload Audio":
370
+ if st.session_state.use_mock_data:
371
+ audio_file = load_mock_file(MOCK_AUDIO_PATH, "audio/mp3")
372
+ if audio_file:
373
+ st.info(f"Using mock audio: {audio_file.name}")
374
+ st.audio(audio_file)
375
+ # Keep mock transcript display for context if audio is used
376
+ try:
377
+ with open(MOCK_TRANSCRIPT_PATH, 'r', encoding='utf-8') as f:
378
+ mock_transcript_content = f.read()
379
+
380
+ with st.expander("View Mock Transcript"):
381
+ transcript_input = st.text_area("Paste Transcript Here", value=mock_transcript_content, height=300, disabled=True, placeholder=mock_transcript_content)
382
+ except FileNotFoundError:
383
+ st.warning("Mock transcript file not found.")
384
+ transcript_input = st.text_area("Paste Transcript Here", height=300, placeholder="Mock transcript file not found.", disabled=True)
385
+ else:
386
+ audio_file = st.file_uploader("Upload Interview Audio", type=['mp3', 'wav', 'm4a', 'mp4'], key="live_audio_uploader")
387
+ if audio_file:
388
+ st.audio(audio_file)
389
+ else: # input_method == "Enter Transcript"
390
+ if st.session_state.use_mock_data:
391
+ try:
392
+ with open(MOCK_TRANSCRIPT_PATH, 'r', encoding='utf-8') as f:
393
+ transcript_input = f.read()
394
+ st.info("Using mock transcript.")
395
+ transcript_input = st.text_area("Paste Transcript Here", value=transcript_input, height=300, disabled=True)
396
+ except FileNotFoundError:
397
+ st.warning("Mock transcript file not found.")
398
+ transcript_input = st.text_area("Paste Transcript Here", height=300, placeholder="Mock transcript file not found.", disabled=True)
399
+ else:
400
+ with open(MOCK_TRANSCRIPT_PATH, 'r', encoding='utf-8') as f:
401
+ transcript_input = f.read()
402
+ # st.info("Using mock transcript.")
403
+ transcript_input = st.text_area("Paste Transcript Here", height=300, placeholder=transcript_input)
404
+
405
+ # Initialize response_data outside the button block
406
+ response_data = None
407
+
408
+ # Determine if the analyze button should be enabled
409
+ # This needs to be done after transcript_input and audio_file are potentially set
410
+ analyze_button_disabled = (not audio_file and not transcript_input)
411
+
412
+ if st.button("Analyze Interview", key="analyze_interview_btn", disabled=analyze_button_disabled):
413
+ if st.session_state.use_mock_data:
414
+ with st.spinner('Analyzing mock interview...'):
415
+ response_data = call_analyze_mock_api()
416
+ elif audio_file:
417
+ with st.spinner('Analyzing interview... This may take several minutes.'):
418
+ response_data = call_analyze_interview_api(job_description, rubric_content, audio_file=audio_file)
419
+ elif transcript_input:
420
+ with st.spinner('Analyzing interview... This may take several minutes.'):
421
+ response_data = call_analyze_interview_api(job_description, rubric_content, transcript_content=transcript_input)
422
+
423
+ if response_data and "error" not in response_data:
424
+ st.session_state.interview_results = response_data
425
+ st.success('Interview analysis complete!')
426
+ elif response_data:
427
+ st.error(f"API Error: {response_data.get('error')}")
428
+ if 'interview_results' in st.session_state:
429
+ del st.session_state.interview_results
430
+
431
+ if st.session_state.get('interview_results'):
432
+ display_results(st.session_state.interview_results, rubric_content)
433
+
434
+ with resume_tab:
435
+ st.header("Resume Analysis")
436
+
437
+ if uploaded_resume:
438
+ with st.expander("View Uploaded Resume Content"):
439
+ display_resume_content(uploaded_resume)
440
+
441
+ # Button to trigger analysis
442
+ if st.button("Analyze Resume Score", key="analyze_resume_btn", type="primary", disabled=(not uploaded_resume or not job_description)):
443
+ with st.spinner('Analyzing resume... This may take a moment.'):
444
+ # Call the backend API
445
+ api_results = call_analyze_resume_api(uploaded_resume, job_description)
446
+
447
+ # Store results or handle errors
448
+ if api_results and 'error' not in api_results:
449
+ st.session_state.resume_score = api_results
450
+ st.success('Resume analysis complete!')
451
+ else:
452
+ error_message = api_results.get('error', 'An unknown error occurred.') if api_results else 'An unknown error occurred.'
453
+ st.error(f"Analysis failed: {error_message}")
454
+ if 'resume_score' in st.session_state:
455
+ del st.session_state.resume_score # Clear old results on failure
456
+
457
+ # Display results if they exist in the session state
458
+ if 'resume_score' in st.session_state and st.session_state.resume_score:
459
+ results = st.session_state.resume_score.get('results', {})
460
+
461
+ # Safely get nested data
462
+ summary = results.get('overall_summary', 'No summary provided.')
463
+ overall_score = calculate_resume_overall_score(results)
464
+ exp = results.get('experience', {})
465
+ edu = results.get('education', {})
466
+ skills = results.get('skills', {})
467
+
468
+ st.markdown(f"### Overall Score: {overall_score}/100")
469
+ st.markdown("---")
470
+
471
+ st.subheader("Score Breakdown")
472
+ score_col1, score_col2, score_col3 = st.columns(3)
473
+ score_col1.metric(label="Experience Score", value=f"{exp.get('score', 'N/A')}")
474
+ score_col2.metric(label="Education Score", value=f"{edu.get('score', 'N/A')}")
475
+ score_col3.metric(label="Skills Match", value=f"{skills.get('score', 'N/A')}")
476
+
477
+ st.subheader("Overall Summary")
478
+ st.write(summary)
479
+
480
+ st.subheader("Detailed Analysis")
481
+ with st.expander("**Experience Analysis**"):
482
+ st.write(exp.get('justification', 'No justification provided.'))
483
+ with st.expander("**Education Analysis**"):
484
+ st.write(edu.get('justification', 'No justification provided.'))
485
+ with st.expander("**Skills Match Analysis**"):
486
+ st.write(skills.get('justification', 'No justification provided.'))
487
+ else:
488
+ # Default view shown on page load or when no analysis has been run
489
+ # st.info("Upload a CV and provide a Job Description in the sidebar to start the analysis.")
490
+ st.markdown("---")
491
+ # st.subheader("Score Summary")
492
+ # score_col1, score_col2, score_col3 = st.columns(3)
493
+ # score_col1.metric(label="Experience Score", value="N/A")
494
+ # score_col2.metric(label="Education Score", value="N/A")
495
+ # score_col3.metric(label="Skills Match", value="N/A")
assets/sample_rubric.yaml ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Example Rubric: Define criteria and keywords
2
+ # The AI will score the candidate's answers based on these categories.
3
+
4
+ technical_skills:
5
+ description: "Proficiency in core technologies and concepts."
6
+ weight: 0.6
7
+ criteria:
8
+ - "Python proficiency: including standard libraries and data structures."
9
+ - "Experience with FastAPI: building APIs, routing, data models."
10
+ - "Understanding of RESTful principles."
11
+
12
+ communication_skills:
13
+ description: "Clarity, conciseness, and ability to explain complex topics."
14
+ weight: 0.4
15
+ criteria:
16
+ - "Clearly articulates thoughts and ideas."
17
+ - "Provides structured and easy-to-follow answers."
18
+ - "Confidently explains past projects and experiences."
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ streamlit
2
+ requests
3
+ pypdf
4
+ python-docx