Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
@@ -14,14 +14,10 @@ try:
|
|
14 |
from transformers import pipeline
|
15 |
has_pipeline = True
|
16 |
except ImportError:
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
st.warning("Using basic transformers functionality instead of pipeline API")
|
22 |
-
except ImportError:
|
23 |
-
st.error("Transformers library not properly installed. Some features will be limited.")
|
24 |
-
has_pipeline = False
|
25 |
|
26 |
# Set page title and hide sidebar
|
27 |
st.set_page_config(
|
@@ -46,6 +42,25 @@ def load_models():
|
|
46 |
with st.spinner("Loading AI models... This may take a minute on first run."):
|
47 |
models = {}
|
48 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
49 |
# Load sentiment model for evaluation
|
50 |
if has_pipeline:
|
51 |
# Use pipeline if available
|
@@ -63,13 +78,53 @@ def load_models():
|
|
63 |
"distilbert/distilbert-base-uncased-finetuned-sst-2-english"
|
64 |
)
|
65 |
except Exception as e:
|
66 |
-
st.error(f"Error loading
|
67 |
models['evaluator_model'] = None
|
68 |
models['evaluator_tokenizer'] = None
|
69 |
|
70 |
return models
|
71 |
|
72 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
73 |
def basic_summarize(text, max_length=100):
|
74 |
"""Basic text summarization by extracting key sentences"""
|
75 |
# Split into sentences
|
@@ -112,49 +167,188 @@ def basic_summarize(text, max_length=100):
|
|
112 |
summary = " ".join(summary_sentences)
|
113 |
return summary
|
114 |
|
115 |
-
# Custom
|
116 |
-
def
|
117 |
-
"""
|
|
|
118 |
|
119 |
-
|
120 |
-
|
121 |
-
|
122 |
-
|
123 |
-
return result[0]['label'] == 'POSITIVE'
|
124 |
-
except Exception as e:
|
125 |
-
st.warning(f"Error in pipeline sentiment analysis: {e}")
|
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 |
-
negative_words = ["mismatch", "gap", "insufficient", "lacking", "inadequate", "limited", "missing", "poor", "weak"]
|
152 |
|
153 |
-
|
154 |
-
|
155 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
156 |
|
157 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
158 |
|
159 |
#####################################
|
160 |
# Function: Extract Text from File
|
@@ -326,8 +520,8 @@ def summarize_resume_text(resume_text, models):
|
|
326 |
"""
|
327 |
start_time = time.time()
|
328 |
|
329 |
-
#
|
330 |
-
base_summary =
|
331 |
|
332 |
# Extract name from the beginning of the resume
|
333 |
name = extract_name(resume_text[:500])
|
@@ -357,7 +551,7 @@ def summarize_resume_text(resume_text, models):
|
|
357 |
#####################################
|
358 |
# Function: Extract Job Requirements
|
359 |
#####################################
|
360 |
-
def extract_job_requirements(job_description):
|
361 |
"""
|
362 |
Extract key requirements from a job description
|
363 |
"""
|
@@ -408,8 +602,8 @@ def extract_job_requirements(job_description):
|
|
408 |
# Extract required skills
|
409 |
required_skills = [skill for skill in tech_skills if re.search(r'\b' + re.escape(skill.lower()) + r'\b', clean_job_text)]
|
410 |
|
411 |
-
# Create a simple summary of the job
|
412 |
-
job_summary =
|
413 |
|
414 |
# Format the job requirements
|
415 |
job_requirements = {
|
@@ -432,103 +626,12 @@ def analyze_job_fit(resume_summary, job_description, models):
|
|
432 |
start_time = time.time()
|
433 |
|
434 |
# Extract job requirements
|
435 |
-
job_requirements = extract_job_requirements(job_description)
|
436 |
-
|
437 |
-
# Now prepare a comparison text for sentiment analysis
|
438 |
-
resume_lower = resume_summary.lower()
|
439 |
-
|
440 |
-
# Extract skills mentioned in resume
|
441 |
-
skills_in_resume = []
|
442 |
-
for skill in job_requirements["required_skills"]:
|
443 |
-
if skill.lower() in resume_lower:
|
444 |
-
skills_in_resume.append(skill)
|
445 |
-
|
446 |
-
# Count how many required skills are found in the resume
|
447 |
-
skills_match_percentage = int((len(skills_in_resume) / max(1, len(job_requirements["required_skills"]))) * 100)
|
448 |
-
|
449 |
-
# Check for years of experience match
|
450 |
-
years_required = job_requirements["years_experience"]
|
451 |
|
452 |
-
#
|
453 |
-
|
454 |
-
year_patterns = [
|
455 |
-
r'(\d+)\s*(?:\+)?\s*years?\s*(?:of)?\s*experience',
|
456 |
-
r'experience\s*(?:of)?\s*(\d+)\s*(?:\+)?\s*years?'
|
457 |
-
]
|
458 |
|
459 |
-
|
460 |
-
exp_match = re.search(pattern, resume_lower)
|
461 |
-
if exp_match:
|
462 |
-
try:
|
463 |
-
experience_years = int(exp_match.group(1))
|
464 |
-
break
|
465 |
-
except:
|
466 |
-
pass
|
467 |
-
|
468 |
-
# If we couldn't find explicit years, try to count based on work history
|
469 |
-
if experience_years == 0:
|
470 |
-
# Try to extract from work experience section
|
471 |
-
work_exp_match = re.search(r'work experience:(.*?)(?=\n\n|$)', resume_summary, re.IGNORECASE | re.DOTALL)
|
472 |
-
if work_exp_match:
|
473 |
-
work_text = work_exp_match.group(1).lower()
|
474 |
-
years = re.findall(r'(\d{4})\s*-\s*(\d{4}|present|current)', work_text)
|
475 |
-
|
476 |
-
total_years = 0
|
477 |
-
for year_range in years:
|
478 |
-
start_year = int(year_range[0])
|
479 |
-
if year_range[1].isdigit():
|
480 |
-
end_year = int(year_range[1])
|
481 |
-
else:
|
482 |
-
end_year = 2025 # Assume "present" is current year
|
483 |
-
|
484 |
-
total_years += (end_year - start_year)
|
485 |
-
|
486 |
-
experience_years = total_years
|
487 |
-
|
488 |
-
# Check experience match
|
489 |
-
experience_match = "sufficient" if experience_years >= years_required else "insufficient"
|
490 |
-
|
491 |
-
# Prepare a comparison summary for sentiment analysis
|
492 |
-
comparison_text = f"""
|
493 |
-
Job title: {job_requirements['title']}
|
494 |
-
|
495 |
-
Job summary: {job_requirements['summary']}
|
496 |
-
|
497 |
-
Candidate summary: {resume_summary[:500]}
|
498 |
-
|
499 |
-
Required skills: {', '.join(job_requirements['required_skills'])}
|
500 |
-
Skills in resume: {', '.join(skills_in_resume)}
|
501 |
-
Skills match: {skills_match_percentage}%
|
502 |
-
|
503 |
-
Required experience: {years_required} years
|
504 |
-
Candidate experience: {experience_years} years
|
505 |
-
Experience match: {experience_match}
|
506 |
-
|
507 |
-
Overall assessment: The candidate's skills and experience {"appear to match well with" if skills_match_percentage >= 60 and experience_match == "sufficient" else "have some gaps compared to"} the job requirements.
|
508 |
-
"""
|
509 |
-
|
510 |
-
# Use sentiment analysis function to evaluate the comparison
|
511 |
-
is_positive = analyze_sentiment(comparison_text, models)
|
512 |
-
|
513 |
-
# Derive final score based on sentiment and match metrics
|
514 |
-
if is_positive and skills_match_percentage >= 70 and experience_match == "sufficient":
|
515 |
-
final_score = 2 # Strong fit
|
516 |
-
elif is_positive and skills_match_percentage >= 50:
|
517 |
-
final_score = 1 # Potential fit
|
518 |
-
else:
|
519 |
-
final_score = 0 # Not fit
|
520 |
-
|
521 |
-
# Generate assessment text based on the score
|
522 |
-
if final_score == 2:
|
523 |
-
assessment = f"{final_score}: The candidate is a strong match for this {job_requirements['title']} position. They have the required {experience_years} years of experience and demonstrate proficiency in key skills including {', '.join(skills_in_resume[:5])}. Their background aligns well with the job requirements."
|
524 |
-
elif final_score == 1:
|
525 |
-
assessment = f"{final_score}: The candidate shows potential for this {job_requirements['title']} position, but has some skill gaps. They match on {skills_match_percentage}% of required skills including {', '.join(skills_in_resume[:3]) if skills_in_resume else 'minimal required skills'}, and their experience is {experience_match}."
|
526 |
-
else:
|
527 |
-
assessment = f"{final_score}: The candidate does not appear to be a good match for this {job_requirements['title']} position. Their profile shows limited alignment with key requirements, matching only {skills_match_percentage}% of required skills, and their experience level is {experience_match}."
|
528 |
-
|
529 |
-
execution_time = time.time() - start_time
|
530 |
-
|
531 |
-
return assessment, final_score, execution_time
|
532 |
|
533 |
# Load models at startup
|
534 |
models = load_models()
|
@@ -573,7 +676,7 @@ if uploaded_file is not None and job_description and st.button("Analyze Job Fit"
|
|
573 |
st.markdown(summary)
|
574 |
|
575 |
# Step 3: Generate job fit assessment
|
576 |
-
status_text.text("Step 3/3: Evaluating job fit...")
|
577 |
assessment, fit_score, assessment_time = analyze_job_fit(summary, job_description, models)
|
578 |
progress_bar.progress(100)
|
579 |
|
|
|
14 |
from transformers import pipeline
|
15 |
has_pipeline = True
|
16 |
except ImportError:
|
17 |
+
from transformers import AutoModelForSequenceClassification, AutoTokenizer, AutoModelForSeq2SeqLM
|
18 |
+
import torch
|
19 |
+
has_pipeline = False
|
20 |
+
st.warning("Using basic transformers functionality instead of pipeline API")
|
|
|
|
|
|
|
|
|
21 |
|
22 |
# Set page title and hide sidebar
|
23 |
st.set_page_config(
|
|
|
42 |
with st.spinner("Loading AI models... This may take a minute on first run."):
|
43 |
models = {}
|
44 |
|
45 |
+
# Load summarization model
|
46 |
+
if has_pipeline:
|
47 |
+
# Use pipeline if available
|
48 |
+
models['summarizer'] = pipeline(
|
49 |
+
"summarization",
|
50 |
+
model="facebook/bart-base",
|
51 |
+
max_length=100,
|
52 |
+
truncation=True
|
53 |
+
)
|
54 |
+
else:
|
55 |
+
# Fall back to basic model loading
|
56 |
+
try:
|
57 |
+
models['summarizer_model'] = AutoModelForSeq2SeqLM.from_pretrained("facebook/bart-base")
|
58 |
+
models['summarizer_tokenizer'] = AutoTokenizer.from_pretrained("facebook/bart-base")
|
59 |
+
except Exception as e:
|
60 |
+
st.error(f"Error loading summarization model: {e}")
|
61 |
+
models['summarizer_model'] = None
|
62 |
+
models['summarizer_tokenizer'] = None
|
63 |
+
|
64 |
# Load sentiment model for evaluation
|
65 |
if has_pipeline:
|
66 |
# Use pipeline if available
|
|
|
78 |
"distilbert/distilbert-base-uncased-finetuned-sst-2-english"
|
79 |
)
|
80 |
except Exception as e:
|
81 |
+
st.error(f"Error loading sentiment model: {e}")
|
82 |
models['evaluator_model'] = None
|
83 |
models['evaluator_tokenizer'] = None
|
84 |
|
85 |
return models
|
86 |
|
87 |
+
# Custom text summarization function that works with or without pipeline
|
88 |
+
def summarize_text(text, models, max_length=100):
|
89 |
+
"""Summarize text using available models"""
|
90 |
+
# Truncate input to prevent issues with long texts
|
91 |
+
input_text = text[:1024] # Limit input length
|
92 |
+
|
93 |
+
if has_pipeline and 'summarizer' in models:
|
94 |
+
# Use pipeline if available
|
95 |
+
try:
|
96 |
+
summary = models['summarizer'](input_text)[0]['summary_text']
|
97 |
+
return summary
|
98 |
+
except Exception as e:
|
99 |
+
st.warning(f"Error in pipeline summarization: {e}")
|
100 |
+
|
101 |
+
# Fall back to manual model inference
|
102 |
+
if 'summarizer_model' in models and 'summarizer_tokenizer' in models and models['summarizer_model']:
|
103 |
+
try:
|
104 |
+
tokenizer = models['summarizer_tokenizer']
|
105 |
+
model = models['summarizer_model']
|
106 |
+
|
107 |
+
# Prepare inputs
|
108 |
+
inputs = tokenizer(input_text, return_tensors="pt", truncation=True, max_length=1024)
|
109 |
+
|
110 |
+
# Generate summary
|
111 |
+
summary_ids = model.generate(
|
112 |
+
inputs.input_ids,
|
113 |
+
max_length=max_length,
|
114 |
+
min_length=30,
|
115 |
+
num_beams=4,
|
116 |
+
early_stopping=True
|
117 |
+
)
|
118 |
+
|
119 |
+
summary = tokenizer.decode(summary_ids[0], skip_special_tokens=True)
|
120 |
+
return summary
|
121 |
+
except Exception as e:
|
122 |
+
st.warning(f"Error in manual summarization: {e}")
|
123 |
+
|
124 |
+
# If all else fails, extract first few sentences
|
125 |
+
return basic_summarize(text, max_length)
|
126 |
+
|
127 |
+
# Basic text summarization as last fallback
|
128 |
def basic_summarize(text, max_length=100):
|
129 |
"""Basic text summarization by extracting key sentences"""
|
130 |
# Split into sentences
|
|
|
167 |
summary = " ".join(summary_sentences)
|
168 |
return summary
|
169 |
|
170 |
+
# Custom classification function for job fit assessment
|
171 |
+
def evaluate_job_fit(resume_summary, job_requirements, models):
|
172 |
+
"""
|
173 |
+
Use the sentiment model to evaluate job fit with multiple analyses
|
174 |
|
175 |
+
This function deliberately takes time to do a more thorough analysis, creating
|
176 |
+
multiple perspectives for the sentiment model to evaluate.
|
177 |
+
"""
|
178 |
+
start_time = time.time()
|
|
|
|
|
|
|
179 |
|
180 |
+
# We'll run multiple comparisons to get a more robust assessment
|
181 |
+
|
182 |
+
# Prepare required information
|
183 |
+
resume_lower = resume_summary.lower()
|
184 |
+
required_skills = job_requirements["required_skills"]
|
185 |
+
years_required = job_requirements["years_experience"]
|
186 |
+
job_title = job_requirements["title"]
|
187 |
+
job_summary = job_requirements["summary"]
|
188 |
+
|
189 |
+
# Extract skills mentioned in resume
|
190 |
+
skills_in_resume = []
|
191 |
+
for skill in required_skills:
|
192 |
+
if skill.lower() in resume_lower:
|
193 |
+
skills_in_resume.append(skill)
|
194 |
+
|
195 |
+
# Skills match percentage
|
196 |
+
skills_match_percentage = int((len(skills_in_resume) / max(1, len(required_skills))) * 100)
|
197 |
+
|
198 |
+
# Extract years of experience from resume
|
199 |
+
experience_years = 0
|
200 |
+
year_patterns = [
|
201 |
+
r'(\d+)\s*(?:\+)?\s*years?\s*(?:of)?\s*experience',
|
202 |
+
r'experience\s*(?:of)?\s*(\d+)\s*(?:\+)?\s*years?'
|
203 |
+
]
|
204 |
+
|
205 |
+
for pattern in year_patterns:
|
206 |
+
exp_match = re.search(pattern, resume_lower)
|
207 |
+
if exp_match:
|
208 |
+
try:
|
209 |
+
experience_years = int(exp_match.group(1))
|
210 |
+
break
|
211 |
+
except:
|
212 |
+
pass
|
213 |
+
|
214 |
+
# If we couldn't find explicit years, try to count based on work history
|
215 |
+
if experience_years == 0:
|
216 |
+
# Try to extract from work experience section
|
217 |
+
work_exp_match = re.search(r'work experience:(.*?)(?=\n\n|$)', resume_summary, re.IGNORECASE | re.DOTALL)
|
218 |
+
if work_exp_match:
|
219 |
+
work_text = work_exp_match.group(1).lower()
|
220 |
+
years = re.findall(r'(\d{4})\s*-\s*(\d{4}|present|current)', work_text)
|
221 |
|
222 |
+
total_years = 0
|
223 |
+
for year_range in years:
|
224 |
+
start_year = int(year_range[0])
|
225 |
+
if year_range[1].isdigit():
|
226 |
+
end_year = int(year_range[1])
|
227 |
+
else:
|
228 |
+
end_year = 2025 # Assume "present" is current year
|
229 |
+
|
230 |
+
total_years += (end_year - start_year)
|
231 |
|
232 |
+
experience_years = total_years
|
233 |
+
|
234 |
+
# Check experience match
|
235 |
+
experience_match = "sufficient" if experience_years >= years_required else "insufficient"
|
236 |
|
237 |
+
# Create multiple comparison texts to evaluate from different angles
|
238 |
+
# Each formatted to bias the sentiment model in a different way
|
|
|
239 |
|
240 |
+
# 1. Skill-focused comparison
|
241 |
+
skill_comparison = f"""
|
242 |
+
Required skills for {job_title}: {', '.join(required_skills)}
|
243 |
+
|
244 |
+
Skills found in candidate resume: {', '.join(skills_in_resume)}
|
245 |
+
|
246 |
+
The candidate possesses {len(skills_in_resume)} out of {len(required_skills)} required skills ({skills_match_percentage}%).
|
247 |
+
|
248 |
+
Based on skills alone, the candidate is {'well-qualified' if skills_match_percentage >= 70 else 'partially qualified' if skills_match_percentage >= 50 else 'not well qualified'} for this position.
|
249 |
+
"""
|
250 |
+
|
251 |
+
# 2. Experience-focused comparison
|
252 |
+
experience_comparison = f"""
|
253 |
+
The {job_title} position requires {years_required} years of experience.
|
254 |
+
|
255 |
+
The candidate has approximately {experience_years} years of experience.
|
256 |
|
257 |
+
Based on experience alone, the candidate {'meets' if experience_years >= years_required else 'does not meet'} the experience requirements for this position.
|
258 |
+
"""
|
259 |
+
|
260 |
+
# 3. Overall job fit comparison
|
261 |
+
overall_comparison = f"""
|
262 |
+
Job: {job_title}
|
263 |
+
|
264 |
+
Job description summary: {job_summary}
|
265 |
+
|
266 |
+
Candidate summary: {resume_summary[:300]}
|
267 |
+
|
268 |
+
Skills match: {skills_match_percentage}%
|
269 |
+
Experience match: {experience_years}/{years_required} years
|
270 |
+
|
271 |
+
Overall assessment: The candidate's profile {'appears to fit' if skills_match_percentage >= 60 and experience_match == "sufficient" else 'has some gaps compared to'} the key requirements for this position.
|
272 |
+
"""
|
273 |
+
|
274 |
+
# Now we'll analyze each comparison using the sentiment model
|
275 |
+
# This is deliberately more thorough to ensure the model is actually doing work
|
276 |
+
|
277 |
+
# Function to get sentiment score with a consistent interface
|
278 |
+
def get_sentiment(text):
|
279 |
+
"""Get sentiment score (1 for positive, 0 for negative)"""
|
280 |
+
if has_pipeline and 'evaluator' in models:
|
281 |
+
try:
|
282 |
+
# Add deliberate sleep to ensure the model has time to process
|
283 |
+
time.sleep(0.5) # Add small delay to ensure model runs
|
284 |
+
result = models['evaluator'](text)
|
285 |
+
return 1 if result[0]['label'] == 'POSITIVE' else 0
|
286 |
+
except Exception as e:
|
287 |
+
st.warning(f"Error in pipeline sentiment analysis: {e}")
|
288 |
+
|
289 |
+
# Fall back to manual model inference
|
290 |
+
if 'evaluator_model' in models and 'evaluator_tokenizer' in models and models['evaluator_model']:
|
291 |
+
try:
|
292 |
+
tokenizer = models['evaluator_tokenizer']
|
293 |
+
model = models['evaluator_model']
|
294 |
+
|
295 |
+
# Add deliberate sleep to ensure the model has time to process
|
296 |
+
time.sleep(0.5) # Add small delay to ensure model runs
|
297 |
+
|
298 |
+
# Truncate to avoid exceeding model's max length
|
299 |
+
max_length = tokenizer.model_max_length if hasattr(tokenizer, 'model_max_length') else 512
|
300 |
+
truncated_text = " ".join(text.split()[:max_length])
|
301 |
+
|
302 |
+
inputs = tokenizer(truncated_text, return_tensors="pt", truncation=True, max_length=max_length)
|
303 |
+
with torch.no_grad():
|
304 |
+
outputs = model(**inputs)
|
305 |
+
|
306 |
+
probabilities = torch.nn.functional.softmax(outputs.logits, dim=-1)
|
307 |
+
prediction = torch.argmax(probabilities, dim=-1).item()
|
308 |
+
|
309 |
+
# Usually for sentiment models, 1 = positive, 0 = negative
|
310 |
+
return 1 if prediction == 1 else 0
|
311 |
+
except Exception as e:
|
312 |
+
st.warning(f"Error in manual sentiment analysis: {e}")
|
313 |
+
|
314 |
+
# Fallback to keyword approach
|
315 |
+
positive_words = ["match", "fit", "qualified", "skilled", "experienced", "suitable", "aligned", "good", "strong"]
|
316 |
+
negative_words = ["mismatch", "gap", "insufficient", "lacking", "inadequate", "limited", "missing", "poor", "weak"]
|
317 |
+
|
318 |
+
text_lower = text.lower()
|
319 |
+
positive_count = sum(text_lower.count(word) for word in positive_words)
|
320 |
+
negative_count = sum(text_lower.count(word) for word in negative_words)
|
321 |
+
|
322 |
+
return 1 if positive_count > negative_count else 0
|
323 |
+
|
324 |
+
# Analyze each comparison (this will take time, which is good)
|
325 |
+
skills_score = get_sentiment(skill_comparison)
|
326 |
+
experience_score = get_sentiment(experience_comparison)
|
327 |
+
overall_score = get_sentiment(overall_comparison)
|
328 |
+
|
329 |
+
# Calculate a weighted combined score
|
330 |
+
# Skills: 50%, Experience: 30%, Overall: 20%
|
331 |
+
combined_score = skills_score * 0.5 + experience_score * 0.3 + overall_score * 0.2
|
332 |
+
|
333 |
+
# Now determine the final score (0, 1, or 2)
|
334 |
+
if combined_score >= 0.7 and skills_match_percentage >= 70 and experience_match == "sufficient":
|
335 |
+
final_score = 2 # Strong fit
|
336 |
+
elif combined_score >= 0.4 or (skills_match_percentage >= 50 and experience_match == "sufficient"):
|
337 |
+
final_score = 1 # Potential fit
|
338 |
+
else:
|
339 |
+
final_score = 0 # Not fit
|
340 |
+
|
341 |
+
# Generate assessment text based on the score
|
342 |
+
if final_score == 2:
|
343 |
+
assessment = f"{final_score}: The candidate is a strong match for this {job_title} position. They have the required {experience_years} years of experience and demonstrate proficiency in key skills including {', '.join(skills_in_resume[:5])}. Their background aligns well with the job requirements."
|
344 |
+
elif final_score == 1:
|
345 |
+
assessment = f"{final_score}: The candidate shows potential for this {job_title} position, but has some skill gaps. They match on {skills_match_percentage}% of required skills including {', '.join(skills_in_resume[:3]) if skills_in_resume else 'minimal required skills'}, and their experience is {experience_match}."
|
346 |
+
else:
|
347 |
+
assessment = f"{final_score}: The candidate does not appear to be a good match for this {job_title} position. Their profile shows limited alignment with key requirements, matching only {skills_match_percentage}% of required skills, and their experience level is {experience_match}."
|
348 |
+
|
349 |
+
execution_time = time.time() - start_time
|
350 |
+
|
351 |
+
return assessment, final_score, execution_time
|
352 |
|
353 |
#####################################
|
354 |
# Function: Extract Text from File
|
|
|
520 |
"""
|
521 |
start_time = time.time()
|
522 |
|
523 |
+
# Use our summarize_text function which handles both pipeline and non-pipeline cases
|
524 |
+
base_summary = summarize_text(resume_text, models, max_length=100)
|
525 |
|
526 |
# Extract name from the beginning of the resume
|
527 |
name = extract_name(resume_text[:500])
|
|
|
551 |
#####################################
|
552 |
# Function: Extract Job Requirements
|
553 |
#####################################
|
554 |
+
def extract_job_requirements(job_description, models):
|
555 |
"""
|
556 |
Extract key requirements from a job description
|
557 |
"""
|
|
|
602 |
# Extract required skills
|
603 |
required_skills = [skill for skill in tech_skills if re.search(r'\b' + re.escape(skill.lower()) + r'\b', clean_job_text)]
|
604 |
|
605 |
+
# Create a simple summary of the job using the summarize_text function
|
606 |
+
job_summary = summarize_text(job_description, models, max_length=100)
|
607 |
|
608 |
# Format the job requirements
|
609 |
job_requirements = {
|
|
|
626 |
start_time = time.time()
|
627 |
|
628 |
# Extract job requirements
|
629 |
+
job_requirements = extract_job_requirements(job_description, models)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
630 |
|
631 |
+
# Use our more thorough evaluation function
|
632 |
+
assessment, fit_score, execution_time = evaluate_job_fit(resume_summary, job_requirements, models)
|
|
|
|
|
|
|
|
|
633 |
|
634 |
+
return assessment, fit_score, execution_time
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
635 |
|
636 |
# Load models at startup
|
637 |
models = load_models()
|
|
|
676 |
st.markdown(summary)
|
677 |
|
678 |
# Step 3: Generate job fit assessment
|
679 |
+
status_text.text("Step 3/3: Evaluating job fit (this will take a moment)...")
|
680 |
assessment, fit_score, assessment_time = analyze_job_fit(summary, job_description, models)
|
681 |
progress_bar.progress(100)
|
682 |
|