Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
@@ -6,8 +6,6 @@ import docx2txt
|
|
6 |
import tempfile
|
7 |
import time
|
8 |
import re
|
9 |
-
import math
|
10 |
-
import concurrent.futures
|
11 |
import pandas as pd
|
12 |
from functools import lru_cache
|
13 |
from transformers import pipeline
|
@@ -127,58 +125,6 @@ def extract_name(text_start):
|
|
127 |
|
128 |
return "Unknown (please extract from resume)"
|
129 |
|
130 |
-
def extract_age(text):
|
131 |
-
"""Extract candidate age from resume text"""
|
132 |
-
# Simplified: just check a few common patterns
|
133 |
-
age_patterns = [
|
134 |
-
r'age:?\s*(\d{1,2})',
|
135 |
-
r'(\d{1,2})\s*years\s*old',
|
136 |
-
]
|
137 |
-
|
138 |
-
text_lower = text.lower()
|
139 |
-
for pattern in age_patterns:
|
140 |
-
matches = re.search(pattern, text_lower)
|
141 |
-
if matches:
|
142 |
-
return matches.group(1)
|
143 |
-
|
144 |
-
return "Not specified"
|
145 |
-
|
146 |
-
def extract_industry(text, base_summary):
|
147 |
-
"""Extract expected job industry from resume"""
|
148 |
-
# Simplified industry keywords focused on the most common ones
|
149 |
-
industry_keywords = {
|
150 |
-
"technology": ["software", "programming", "developer", "IT", "tech", "computer"],
|
151 |
-
"finance": ["banking", "financial", "accounting", "finance", "analyst"],
|
152 |
-
"healthcare": ["medical", "health", "hospital", "clinical", "nurse", "doctor"],
|
153 |
-
"education": ["teaching", "teacher", "professor", "education", "university"],
|
154 |
-
"marketing": ["marketing", "advertising", "digital marketing", "social media"],
|
155 |
-
"engineering": ["engineer", "engineering"],
|
156 |
-
"data science": ["data science", "machine learning", "AI", "analytics"],
|
157 |
-
"information systems": ["information systems", "ERP", "systems management"]
|
158 |
-
}
|
159 |
-
|
160 |
-
# Count occurrences of industry keywords - using the summary to speed up
|
161 |
-
combined_text = base_summary.lower()
|
162 |
-
|
163 |
-
counts = {}
|
164 |
-
for industry, keywords in industry_keywords.items():
|
165 |
-
counts[industry] = sum(combined_text.count(keyword.lower()) for keyword in keywords)
|
166 |
-
|
167 |
-
# Get the industry with the highest count
|
168 |
-
if counts:
|
169 |
-
likely_industry = max(counts.items(), key=lambda x: x[1])
|
170 |
-
if likely_industry[1] > 0:
|
171 |
-
return likely_industry[0].capitalize()
|
172 |
-
|
173 |
-
# Check for educational background that might indicate industry
|
174 |
-
degrees = ["computer science", "business", "engineering", "medicine", "education", "finance", "marketing"]
|
175 |
-
|
176 |
-
for degree in degrees:
|
177 |
-
if degree in combined_text:
|
178 |
-
return f"{degree.capitalize()}-related field"
|
179 |
-
|
180 |
-
return "Not clearly specified"
|
181 |
-
|
182 |
def extract_skills_and_work(text):
|
183 |
"""Extract both skills and work experience at once to save processing time"""
|
184 |
# Common skill categories - reduced keyword list for speed
|
@@ -282,26 +228,26 @@ def summarize_resume_text(resume_text):
|
|
282 |
text_to_summarize = resume_text[:min(len(resume_text), max_input_length)]
|
283 |
base_summary = models['summarizer'](text_to_summarize)[0]['summary_text']
|
284 |
|
285 |
-
# Extract
|
286 |
-
|
287 |
-
|
288 |
-
|
289 |
-
|
290 |
-
|
291 |
-
|
292 |
-
|
293 |
-
|
294 |
-
|
295 |
-
|
296 |
-
|
297 |
-
|
298 |
|
299 |
# Format the structured summary
|
300 |
-
formatted_summary = f"Name: {name}\n"
|
301 |
-
formatted_summary += f"
|
302 |
-
formatted_summary += f"Expected Job Industry: {industry}\n\n"
|
303 |
formatted_summary += f"Previous Work Experience: {work_experience}\n\n"
|
304 |
-
formatted_summary += f"Skills: {skills}"
|
|
|
305 |
|
306 |
execution_time = time.time() - start_time
|
307 |
|
@@ -312,19 +258,16 @@ def summarize_resume_text(resume_text):
|
|
312 |
#####################################
|
313 |
def extract_job_requirements(job_description):
|
314 |
"""
|
315 |
-
Extract key requirements
|
316 |
"""
|
317 |
-
# Common technical
|
318 |
-
|
319 |
-
"
|
320 |
-
"
|
321 |
-
"
|
322 |
-
"
|
323 |
-
"
|
324 |
-
|
325 |
-
|
326 |
-
# Common soft skills to look for
|
327 |
-
soft_skills = ["Communication", "Leadership", "Teamwork", "Problem-solving", "Critical thinking", "Adaptability", "Creativity", "Time management"]
|
328 |
|
329 |
# Clean the text for processing
|
330 |
clean_job_text = job_description.lower()
|
@@ -361,47 +304,18 @@ def extract_job_requirements(job_description):
|
|
361 |
except:
|
362 |
pass
|
363 |
|
364 |
-
# Extract
|
365 |
-
|
366 |
-
all_tech_skills = []
|
367 |
|
368 |
-
|
369 |
-
|
370 |
-
for skill in skills:
|
371 |
-
if re.search(r'\b' + re.escape(skill.lower()) + r'\b', clean_job_text):
|
372 |
-
category_skills.append(skill)
|
373 |
-
all_tech_skills.append(skill)
|
374 |
-
|
375 |
-
if category_skills:
|
376 |
-
found_tech_skills[category] = category_skills
|
377 |
-
|
378 |
-
# Extract soft skills
|
379 |
-
found_soft_skills = []
|
380 |
-
for skill in soft_skills:
|
381 |
-
if re.search(r'\b' + re.escape(skill.lower()) + r'\b', clean_job_text):
|
382 |
-
found_soft_skills.append(skill)
|
383 |
-
|
384 |
-
# Extract educational requirements
|
385 |
-
edu_patterns = [
|
386 |
-
r"bachelor'?s degree|bs|b\.s\.",
|
387 |
-
r"master'?s degree|ms|m\.s\.",
|
388 |
-
r"phd|ph\.d\.|doctorate",
|
389 |
-
r"mba|m\.b\.a\."
|
390 |
-
]
|
391 |
-
|
392 |
-
education_required = []
|
393 |
-
for pattern in edu_patterns:
|
394 |
-
if re.search(pattern, clean_job_text, re.IGNORECASE):
|
395 |
-
edu_match = re.search(pattern, clean_job_text, re.IGNORECASE).group(0)
|
396 |
-
education_required.append(edu_match.capitalize())
|
397 |
|
398 |
# Format the job requirements
|
399 |
job_requirements = {
|
400 |
"title": job_title,
|
401 |
"years_experience": years_required,
|
402 |
-
"
|
403 |
-
"
|
404 |
-
"education": education_required,
|
405 |
}
|
406 |
|
407 |
return job_requirements
|
@@ -412,94 +326,24 @@ def extract_job_requirements(job_description):
|
|
412 |
def analyze_job_fit(resume_summary, job_description):
|
413 |
"""
|
414 |
Analyze how well the candidate fits the job requirements with the DistilBERT sentiment model.
|
|
|
415 |
"""
|
416 |
start_time = time.time()
|
417 |
|
418 |
# Extract job requirements
|
419 |
job_requirements = extract_job_requirements(job_description)
|
420 |
|
421 |
-
#
|
422 |
resume_lower = resume_summary.lower()
|
423 |
-
job_lower = job_description.lower()
|
424 |
-
|
425 |
-
# Define keyword categories based on the job description
|
426 |
-
# We'll dynamically build these based on the job requirements
|
427 |
-
skill_keywords = {
|
428 |
-
"technical_skills": job_requirements["technical_skills"],
|
429 |
-
"soft_skills": job_requirements["soft_skills"],
|
430 |
-
"education": job_requirements["education"],
|
431 |
-
}
|
432 |
|
433 |
-
#
|
434 |
-
|
435 |
-
|
436 |
-
|
437 |
-
|
438 |
-
"collaboration": ["team", "collaborate", "cooperation", "cross-functional", "communication", "stakeholder"]
|
439 |
-
}
|
440 |
|
441 |
-
#
|
442 |
-
|
443 |
-
|
444 |
-
# Category weights with descriptive labels
|
445 |
-
category_weights = {
|
446 |
-
"technical_skills": {"weight": 0.40, "label": "Technical Skills"},
|
447 |
-
"soft_skills": {"weight": 0.15, "label": "Soft Skills"},
|
448 |
-
"education": {"weight": 0.10, "label": "Education"},
|
449 |
-
"problem_solving": {"weight": 0.15, "label": "Problem Solving"},
|
450 |
-
"domain_knowledge": {"weight": 0.10, "label": "Domain Knowledge"},
|
451 |
-
"collaboration": {"weight": 0.10, "label": "Collaboration"}
|
452 |
-
}
|
453 |
-
|
454 |
-
# Calculate category scores and store detailed information
|
455 |
-
category_scores = {}
|
456 |
-
category_details = {}
|
457 |
-
found_skills = {}
|
458 |
-
|
459 |
-
for category, keywords in skill_keywords.items():
|
460 |
-
if not keywords: # Skip empty categories
|
461 |
-
category_scores[category] = 0.0
|
462 |
-
category_details[category] = {
|
463 |
-
"raw_percentage": 0,
|
464 |
-
"adjusted_score": 0,
|
465 |
-
"matching_keywords": [],
|
466 |
-
"total_keywords": 0,
|
467 |
-
"matches": 0
|
468 |
-
}
|
469 |
-
found_skills[category] = []
|
470 |
-
continue
|
471 |
-
|
472 |
-
# Find the specific matching keywords for feedback
|
473 |
-
category_matches = []
|
474 |
-
for keyword in keywords:
|
475 |
-
if keyword.lower() in resume_lower:
|
476 |
-
category_matches.append(keyword)
|
477 |
-
|
478 |
-
found_skills[category] = category_matches
|
479 |
-
|
480 |
-
# Count matches but cap at a reasonable level
|
481 |
-
matches = len(category_matches)
|
482 |
-
total_keywords = len(keywords)
|
483 |
-
|
484 |
-
# Calculate raw percentage for this category
|
485 |
-
raw_percentage = int((matches / max(1, total_keywords)) * 100)
|
486 |
-
|
487 |
-
# Apply logarithmic scaling for more realistic scores
|
488 |
-
if matches == 0:
|
489 |
-
adjusted_score = 0.0
|
490 |
-
else:
|
491 |
-
# Logarithmic scaling to prevent perfect scores
|
492 |
-
adjusted_score = min(0.95, (math.log(matches + 1) / math.log(min(total_keywords, 8) + 1)))
|
493 |
-
|
494 |
-
# Store both raw and adjusted scores for feedback
|
495 |
-
category_scores[category] = adjusted_score
|
496 |
-
category_details[category] = {
|
497 |
-
"raw_percentage": raw_percentage,
|
498 |
-
"adjusted_score": int(adjusted_score * 100),
|
499 |
-
"matching_keywords": category_matches,
|
500 |
-
"total_keywords": total_keywords,
|
501 |
-
"matches": matches
|
502 |
-
}
|
503 |
|
504 |
# Check for years of experience match
|
505 |
years_required = job_requirements["years_experience"]
|
@@ -540,96 +384,53 @@ def analyze_job_fit(resume_summary, job_description):
|
|
540 |
|
541 |
experience_years = total_years
|
542 |
|
543 |
-
#
|
544 |
-
if years_required
|
545 |
-
if experience_years >= years_required:
|
546 |
-
exp_score = 1.0
|
547 |
-
else:
|
548 |
-
exp_score = experience_years / years_required
|
549 |
-
else:
|
550 |
-
exp_score = 1.0 # If no specific years required, assume full match
|
551 |
-
|
552 |
-
category_scores["experience"] = exp_score
|
553 |
-
category_details["experience"] = {
|
554 |
-
"raw_percentage": int(exp_score * 100),
|
555 |
-
"adjusted_score": int(exp_score * 100),
|
556 |
-
"candidate_years": experience_years,
|
557 |
-
"required_years": years_required
|
558 |
-
}
|
559 |
-
|
560 |
-
# Calculate weighted score
|
561 |
-
weighted_score = 0
|
562 |
-
for category, score in category_scores.items():
|
563 |
-
if category in category_weights:
|
564 |
-
weighted_score += score * category_weights[category]["weight"]
|
565 |
|
566 |
-
#
|
567 |
-
|
568 |
-
|
569 |
-
# Apply final curve to keep scores in a realistic range
|
570 |
-
match_percentage = min(95, max(35, int(weighted_score * 100)))
|
571 |
-
|
572 |
-
# Prepare input for sentiment analysis
|
573 |
-
# Create a structured summary of the match for sentiment model
|
574 |
-
match_summary = f"""
|
575 |
Job title: {job_requirements['title']}
|
576 |
-
Match percentage: {match_percentage}%
|
577 |
|
578 |
-
|
579 |
-
Required technical skills: {', '.join(job_requirements['technical_skills'][:5])}
|
580 |
-
Candidate has: {', '.join(found_skills['technical_skills'][:5])}
|
581 |
|
582 |
-
|
583 |
-
Required years: {job_requirements['years_experience']}
|
584 |
-
Candidate years: {experience_years}
|
585 |
|
586 |
-
|
|
|
|
|
587 |
|
588 |
-
|
589 |
-
|
|
|
590 |
|
591 |
-
|
592 |
-
|
593 |
|
594 |
-
#
|
595 |
-
|
596 |
-
# POSITIVE = 1 (good fit)
|
597 |
-
score_mapping = {
|
598 |
-
"NEGATIVE": 0,
|
599 |
-
"POSITIVE": 1
|
600 |
-
}
|
601 |
|
602 |
-
#
|
603 |
-
sentiment_score =
|
604 |
|
605 |
-
#
|
606 |
-
if sentiment_score == 1 and
|
607 |
-
final_score = 2 #
|
608 |
-
elif sentiment_score == 1:
|
609 |
-
final_score = 1 #
|
610 |
else:
|
611 |
-
final_score = 0 #
|
612 |
-
|
613 |
-
# Map to fit status
|
614 |
-
fit_status_map = {
|
615 |
-
0: "NOT FIT",
|
616 |
-
1: "POTENTIAL FIT",
|
617 |
-
2: "STRONG FIT"
|
618 |
-
}
|
619 |
|
620 |
-
|
621 |
-
|
622 |
-
# Generate assessment summary based on the score
|
623 |
if final_score == 2:
|
624 |
-
assessment = f"{final_score}: The candidate is a
|
625 |
elif final_score == 1:
|
626 |
-
assessment = f"{final_score}: The candidate shows potential for this {job_requirements['title']} position,
|
627 |
else:
|
628 |
-
assessment = f"{final_score}: The candidate does not appear to be a
|
629 |
|
630 |
execution_time = time.time() - start_time
|
631 |
|
632 |
-
return assessment, final_score,
|
633 |
|
634 |
#####################################
|
635 |
# Main Streamlit Interface
|
@@ -637,10 +438,7 @@ def analyze_job_fit(resume_summary, job_description):
|
|
637 |
st.title("Resume-Job Fit Analyzer")
|
638 |
st.markdown(
|
639 |
"""
|
640 |
-
Upload your resume file in **.docx**, **.doc**, or **.txt** format and enter a job description to see how well you match with the job requirements.
|
641 |
-
1. Extracts text from your resume.
|
642 |
-
2. Uses AI to generate a structured candidate summary.
|
643 |
-
3. Analyzes how well your profile fits the specific job requirements.
|
644 |
"""
|
645 |
)
|
646 |
|
@@ -672,11 +470,10 @@ if uploaded_file is not None and job_description and st.button("Analyze Job Fit"
|
|
672 |
# Display summary
|
673 |
st.subheader("Your Resume Summary")
|
674 |
st.markdown(summary)
|
675 |
-
st.info(f"Summary generated in {summarization_time:.2f} seconds")
|
676 |
|
677 |
# Step 3: Generate job fit assessment
|
678 |
status_text.text("Step 3/3: Evaluating job fit...")
|
679 |
-
assessment, fit_score,
|
680 |
progress_bar.progress(100)
|
681 |
|
682 |
# Clear status messages
|
@@ -689,92 +486,35 @@ if uploaded_file is not None and job_description and st.button("Analyze Job Fit"
|
|
689 |
fit_labels = {
|
690 |
0: "NOT FIT ❌",
|
691 |
1: "POTENTIAL FIT ⚠️",
|
692 |
-
2: "
|
693 |
}
|
694 |
|
695 |
# Show the score prominently
|
696 |
-
st.markdown(f"##
|
697 |
-
|
698 |
-
# Display match percentage
|
699 |
-
if match_percentage >= 85:
|
700 |
-
st.success(f"**Match Score:** {match_percentage}% 🌟")
|
701 |
-
elif match_percentage >= 70:
|
702 |
-
st.success(f"**Match Score:** {match_percentage}% ✅")
|
703 |
-
elif match_percentage >= 50:
|
704 |
-
st.warning(f"**Match Score:** {match_percentage}% ⚠️")
|
705 |
-
else:
|
706 |
-
st.error(f"**Match Score:** {match_percentage}% 🔍")
|
707 |
|
708 |
# Display assessment
|
709 |
-
st.markdown("### Assessment")
|
710 |
st.markdown(assessment)
|
711 |
|
712 |
-
|
713 |
-
st.markdown("### Score Breakdown")
|
714 |
-
|
715 |
-
# Create a neat table with category scores
|
716 |
-
breakdown_data = []
|
717 |
-
for category, details in category_details.items():
|
718 |
-
if category == "experience":
|
719 |
-
label = "Experience"
|
720 |
-
matching_info = f"{details['candidate_years']} years (Required: {details['required_years']} years)"
|
721 |
-
else:
|
722 |
-
# Get the nice label for the category
|
723 |
-
label = {"technical_skills": "Technical Skills",
|
724 |
-
"soft_skills": "Soft Skills",
|
725 |
-
"education": "Education",
|
726 |
-
"problem_solving": "Problem Solving",
|
727 |
-
"domain_knowledge": "Domain Knowledge",
|
728 |
-
"collaboration": "Collaboration"}[category]
|
729 |
-
|
730 |
-
matching_info = ", ".join(details["matching_keywords"][:3]) if details.get("matching_keywords") else "None detected"
|
731 |
-
|
732 |
-
# Add formatted breakdown row
|
733 |
-
breakdown_data.append({
|
734 |
-
"Category": label,
|
735 |
-
"Score": f"{details['adjusted_score']}%",
|
736 |
-
"Matching Items": matching_info
|
737 |
-
})
|
738 |
-
|
739 |
-
# Convert to DataFrame and display
|
740 |
-
breakdown_df = pd.DataFrame(breakdown_data)
|
741 |
-
# Remove the index column entirely
|
742 |
-
st.table(breakdown_df.set_index('Category').reset_index()) # This removes the numerical index
|
743 |
-
|
744 |
-
# Show a note about how scores are calculated
|
745 |
-
with st.expander("How are these scores calculated?"):
|
746 |
-
st.markdown("""
|
747 |
-
- **Technical Skills** (40% of total): Evaluates programming languages, software tools, and technical requirements
|
748 |
-
- **Soft Skills** (15% of total): Assesses communication, teamwork, and interpersonal abilities
|
749 |
-
- **Education** (10% of total): Compares educational requirements with candidate's background
|
750 |
-
- **Problem Solving** (15% of total): Measures analytical thinking and approach to challenges
|
751 |
-
- **Domain Knowledge** (10% of total): Evaluates industry-specific experience and knowledge
|
752 |
-
- **Collaboration** (10% of total): Assesses team skills and cross-functional collaboration
|
753 |
-
- **Experience** (20% overall modifier): Years of relevant experience compared to job requirements
|
754 |
-
|
755 |
-
Scores are calculated based on keyword matches in your resume, with diminishing returns applied (first few skills matter more than later ones).
|
756 |
-
""")
|
757 |
-
|
758 |
-
st.info(f"Assessment completed in {assessment_time:.2f} seconds")
|
759 |
|
760 |
# Add potential next steps based on the fit score
|
761 |
st.subheader("Recommended Next Steps")
|
762 |
|
763 |
if fit_score == 2:
|
764 |
st.markdown("""
|
765 |
-
-
|
766 |
-
- Prepare for
|
767 |
-
-
|
768 |
""")
|
769 |
elif fit_score == 1:
|
770 |
st.markdown("""
|
771 |
-
-
|
772 |
-
-
|
773 |
-
- Prepare to discuss how
|
774 |
""")
|
775 |
else:
|
776 |
st.markdown("""
|
777 |
-
-
|
778 |
-
-
|
779 |
-
-
|
780 |
""")
|
|
|
6 |
import tempfile
|
7 |
import time
|
8 |
import re
|
|
|
|
|
9 |
import pandas as pd
|
10 |
from functools import lru_cache
|
11 |
from transformers import pipeline
|
|
|
125 |
|
126 |
return "Unknown (please extract from resume)"
|
127 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
128 |
def extract_skills_and_work(text):
|
129 |
"""Extract both skills and work experience at once to save processing time"""
|
130 |
# Common skill categories - reduced keyword list for speed
|
|
|
228 |
text_to_summarize = resume_text[:min(len(resume_text), max_input_length)]
|
229 |
base_summary = models['summarizer'](text_to_summarize)[0]['summary_text']
|
230 |
|
231 |
+
# Extract name from the beginning of the resume
|
232 |
+
name = extract_name(resume_text[:500])
|
233 |
+
|
234 |
+
# Extract skills and work experience
|
235 |
+
skills, work_experience = extract_skills_and_work(resume_text)
|
236 |
+
|
237 |
+
# Extract education level - simplified approach
|
238 |
+
education_level = "Not specified"
|
239 |
+
education_terms = ["bachelor", "master", "phd", "doctorate", "mba", "degree"]
|
240 |
+
for term in education_terms:
|
241 |
+
if term in resume_text.lower():
|
242 |
+
education_level = "Higher education degree mentioned"
|
243 |
+
break
|
244 |
|
245 |
# Format the structured summary
|
246 |
+
formatted_summary = f"Name: {name}\n\n"
|
247 |
+
formatted_summary += f"Summary: {base_summary}\n\n"
|
|
|
248 |
formatted_summary += f"Previous Work Experience: {work_experience}\n\n"
|
249 |
+
formatted_summary += f"Skills: {skills}\n\n"
|
250 |
+
formatted_summary += f"Education: {education_level}"
|
251 |
|
252 |
execution_time = time.time() - start_time
|
253 |
|
|
|
258 |
#####################################
|
259 |
def extract_job_requirements(job_description):
|
260 |
"""
|
261 |
+
Extract key requirements from a job description
|
262 |
"""
|
263 |
+
# Common technical skills to look for
|
264 |
+
tech_skills = [
|
265 |
+
"Python", "Java", "C++", "JavaScript", "TypeScript", "Go", "Rust", "SQL", "Ruby", "PHP", "Swift", "Kotlin",
|
266 |
+
"React", "Angular", "Vue", "Node.js", "HTML", "CSS", "Django", "Flask", "Spring", "REST API", "GraphQL",
|
267 |
+
"Machine Learning", "TensorFlow", "PyTorch", "Data Science", "AI", "Big Data", "Deep Learning", "NLP",
|
268 |
+
"AWS", "Azure", "GCP", "Docker", "Kubernetes", "CI/CD", "Jenkins", "GitHub Actions", "Terraform",
|
269 |
+
"MySQL", "PostgreSQL", "MongoDB", "Redis", "Elasticsearch", "DynamoDB", "Cassandra"
|
270 |
+
]
|
|
|
|
|
|
|
271 |
|
272 |
# Clean the text for processing
|
273 |
clean_job_text = job_description.lower()
|
|
|
304 |
except:
|
305 |
pass
|
306 |
|
307 |
+
# Extract required skills
|
308 |
+
required_skills = [skill for skill in tech_skills if re.search(r'\b' + re.escape(skill.lower()) + r'\b', clean_job_text)]
|
|
|
309 |
|
310 |
+
# Create a simple summary of the job
|
311 |
+
job_summary = models['summarizer'](job_description[:1024])[0]['summary_text']
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
312 |
|
313 |
# Format the job requirements
|
314 |
job_requirements = {
|
315 |
"title": job_title,
|
316 |
"years_experience": years_required,
|
317 |
+
"required_skills": required_skills,
|
318 |
+
"summary": job_summary
|
|
|
319 |
}
|
320 |
|
321 |
return job_requirements
|
|
|
326 |
def analyze_job_fit(resume_summary, job_description):
|
327 |
"""
|
328 |
Analyze how well the candidate fits the job requirements with the DistilBERT sentiment model.
|
329 |
+
Returns a fit score (0-2) and an assessment.
|
330 |
"""
|
331 |
start_time = time.time()
|
332 |
|
333 |
# Extract job requirements
|
334 |
job_requirements = extract_job_requirements(job_description)
|
335 |
|
336 |
+
# Now prepare a comparison text for sentiment analysis
|
337 |
resume_lower = resume_summary.lower()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
338 |
|
339 |
+
# Extract skills mentioned in resume
|
340 |
+
skills_in_resume = []
|
341 |
+
for skill in job_requirements["required_skills"]:
|
342 |
+
if skill.lower() in resume_lower:
|
343 |
+
skills_in_resume.append(skill)
|
|
|
|
|
344 |
|
345 |
+
# Count how many required skills are found in the resume
|
346 |
+
skills_match_percentage = int((len(skills_in_resume) / max(1, len(job_requirements["required_skills"]))) * 100)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
347 |
|
348 |
# Check for years of experience match
|
349 |
years_required = job_requirements["years_experience"]
|
|
|
384 |
|
385 |
experience_years = total_years
|
386 |
|
387 |
+
# Check experience match
|
388 |
+
experience_match = "sufficient" if experience_years >= years_required else "insufficient"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
389 |
|
390 |
+
# Prepare a comparison summary for sentiment analysis
|
391 |
+
comparison_text = f"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
392 |
Job title: {job_requirements['title']}
|
|
|
393 |
|
394 |
+
Job summary: {job_requirements['summary']}
|
|
|
|
|
395 |
|
396 |
+
Candidate summary: {resume_summary[:500]}
|
|
|
|
|
397 |
|
398 |
+
Required skills: {', '.join(job_requirements['required_skills'])}
|
399 |
+
Skills in resume: {', '.join(skills_in_resume)}
|
400 |
+
Skills match: {skills_match_percentage}%
|
401 |
|
402 |
+
Required experience: {years_required} years
|
403 |
+
Candidate experience: {experience_years} years
|
404 |
+
Experience match: {experience_match}
|
405 |
|
406 |
+
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.
|
407 |
+
"""
|
408 |
|
409 |
+
# Use sentiment analysis model to evaluate the comparison
|
410 |
+
sentiment_result = models['evaluator'](comparison_text)
|
|
|
|
|
|
|
|
|
|
|
411 |
|
412 |
+
# Map sentiment to score: NEGATIVE = 0, POSITIVE = 1
|
413 |
+
sentiment_score = 1 if sentiment_result[0]['label'] == 'POSITIVE' else 0
|
414 |
|
415 |
+
# Derive final score based on sentiment and match metrics
|
416 |
+
if sentiment_score == 1 and skills_match_percentage >= 70 and experience_match == "sufficient":
|
417 |
+
final_score = 2 # Good fit
|
418 |
+
elif sentiment_score == 1 and skills_match_percentage >= 50:
|
419 |
+
final_score = 1 # Potential fit
|
420 |
else:
|
421 |
+
final_score = 0 # Not fit
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
422 |
|
423 |
+
# Generate assessment text based on the score
|
|
|
|
|
424 |
if final_score == 2:
|
425 |
+
assessment = f"{final_score}: The candidate is a good 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."
|
426 |
elif final_score == 1:
|
427 |
+
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}."
|
428 |
else:
|
429 |
+
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}."
|
430 |
|
431 |
execution_time = time.time() - start_time
|
432 |
|
433 |
+
return assessment, final_score, execution_time
|
434 |
|
435 |
#####################################
|
436 |
# Main Streamlit Interface
|
|
|
438 |
st.title("Resume-Job Fit Analyzer")
|
439 |
st.markdown(
|
440 |
"""
|
441 |
+
Upload your resume file in **.docx**, **.doc**, or **.txt** format and enter a job description to see how well you match with the job requirements.
|
|
|
|
|
|
|
442 |
"""
|
443 |
)
|
444 |
|
|
|
470 |
# Display summary
|
471 |
st.subheader("Your Resume Summary")
|
472 |
st.markdown(summary)
|
|
|
473 |
|
474 |
# Step 3: Generate job fit assessment
|
475 |
status_text.text("Step 3/3: Evaluating job fit...")
|
476 |
+
assessment, fit_score, assessment_time = analyze_job_fit(summary, job_description)
|
477 |
progress_bar.progress(100)
|
478 |
|
479 |
# Clear status messages
|
|
|
486 |
fit_labels = {
|
487 |
0: "NOT FIT ❌",
|
488 |
1: "POTENTIAL FIT ⚠️",
|
489 |
+
2: "GOOD FIT ✅"
|
490 |
}
|
491 |
|
492 |
# Show the score prominently
|
493 |
+
st.markdown(f"## {fit_labels[fit_score]}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
494 |
|
495 |
# Display assessment
|
|
|
496 |
st.markdown(assessment)
|
497 |
|
498 |
+
st.info(f"Analysis completed in {(summarization_time + assessment_time):.2f} seconds")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
499 |
|
500 |
# Add potential next steps based on the fit score
|
501 |
st.subheader("Recommended Next Steps")
|
502 |
|
503 |
if fit_score == 2:
|
504 |
st.markdown("""
|
505 |
+
- Apply for this position as you appear to be a good match
|
506 |
+
- Prepare for interviews by focusing on your relevant experience
|
507 |
+
- Highlight your matching skills in your cover letter
|
508 |
""")
|
509 |
elif fit_score == 1:
|
510 |
st.markdown("""
|
511 |
+
- Consider applying but address skill gaps in your cover letter
|
512 |
+
- Emphasize transferable skills and relevant experience
|
513 |
+
- Prepare to discuss how you can quickly develop missing skills
|
514 |
""")
|
515 |
else:
|
516 |
st.markdown("""
|
517 |
+
- Look for positions better aligned with your current skills
|
518 |
+
- If interested in this field, focus on developing the required skills
|
519 |
+
- Consider similar roles with fewer experience requirements
|
520 |
""")
|