Spaces:
Sleeping
Sleeping
import os | |
import io | |
import streamlit as st | |
import docx | |
import docx2txt | |
import tempfile | |
import time | |
import re | |
import pandas as pd | |
from functools import lru_cache | |
# Try different import approaches | |
try: | |
from transformers import pipeline | |
has_pipeline = True | |
except ImportError: | |
from transformers import AutoModelForSequenceClassification, AutoTokenizer, AutoModelForSeq2SeqLM | |
import torch | |
has_pipeline = False | |
st.warning("Using basic transformers functionality instead of pipeline API") | |
# Set page title and hide sidebar | |
st.set_page_config( | |
page_title="Resume-Job Fit Analyzer", | |
initial_sidebar_state="collapsed" | |
) | |
# Hide sidebar completely with custom CSS | |
st.markdown(""" | |
<style> | |
[data-testid="collapsedControl"] {display: none;} | |
section[data-testid="stSidebar"] {display: none;} | |
</style> | |
""", unsafe_allow_html=True) | |
##################################### | |
# Preload Models | |
##################################### | |
def load_models(): | |
"""Load models at startup""" | |
with st.spinner("Loading AI models... This may take a minute on first run."): | |
models = {} | |
# Load summarization model | |
if has_pipeline: | |
# Use pipeline if available | |
models['summarizer'] = pipeline( | |
"summarization", | |
model="facebook/bart-base", | |
max_length=100, | |
truncation=True | |
) | |
else: | |
# Fall back to basic model loading | |
try: | |
models['summarizer_model'] = AutoModelForSeq2SeqLM.from_pretrained("facebook/bart-base") | |
models['summarizer_tokenizer'] = AutoTokenizer.from_pretrained("facebook/bart-base") | |
except Exception as e: | |
st.error(f"Error loading summarization model: {e}") | |
models['summarizer_model'] = None | |
models['summarizer_tokenizer'] = None | |
# Load sentiment model for evaluation | |
if has_pipeline: | |
# Use pipeline if available | |
models['evaluator'] = pipeline( | |
"sentiment-analysis", | |
model="distilbert/distilbert-base-uncased-finetuned-sst-2-english" | |
) | |
else: | |
# Fall back to basic model loading | |
try: | |
models['evaluator_model'] = AutoModelForSequenceClassification.from_pretrained( | |
"distilbert/distilbert-base-uncased-finetuned-sst-2-english" | |
) | |
models['evaluator_tokenizer'] = AutoTokenizer.from_pretrained( | |
"distilbert/distilbert-base-uncased-finetuned-sst-2-english" | |
) | |
except Exception as e: | |
st.error(f"Error loading sentiment model: {e}") | |
models['evaluator_model'] = None | |
models['evaluator_tokenizer'] = None | |
return models | |
# Custom text summarization function that works with or without pipeline | |
def summarize_text(text, models, max_length=100): | |
"""Summarize text using available models""" | |
# Truncate input to prevent issues with long texts | |
input_text = text[:1024] # Limit input length | |
if has_pipeline and 'summarizer' in models: | |
# Use pipeline if available | |
try: | |
summary = models['summarizer'](input_text)[0]['summary_text'] | |
return summary | |
except Exception as e: | |
st.warning(f"Error in pipeline summarization: {e}") | |
# Fall back to manual model inference | |
if 'summarizer_model' in models and 'summarizer_tokenizer' in models and models['summarizer_model']: | |
try: | |
tokenizer = models['summarizer_tokenizer'] | |
model = models['summarizer_model'] | |
# Prepare inputs | |
inputs = tokenizer(input_text, return_tensors="pt", truncation=True, max_length=1024) | |
# Generate summary | |
summary_ids = model.generate( | |
inputs.input_ids, | |
max_length=max_length, | |
min_length=30, | |
num_beams=4, | |
early_stopping=True | |
) | |
summary = tokenizer.decode(summary_ids[0], skip_special_tokens=True) | |
return summary | |
except Exception as e: | |
st.warning(f"Error in manual summarization: {e}") | |
# If all else fails, extract first few sentences | |
return basic_summarize(text, max_length) | |
# Basic text summarization as last fallback | |
def basic_summarize(text, max_length=100): | |
"""Basic text summarization by extracting key sentences""" | |
# Split into sentences | |
sentences = re.split(r'(?<!\w\.\w.)(?<![A-Z][a-z]\.)(?<=\.|\?)\s', text) | |
# Score sentences by position (earlier is better) and length | |
scored_sentences = [] | |
for i, sentence in enumerate(sentences): | |
# Skip very short sentences | |
if len(sentence.split()) < 4: | |
continue | |
# Simple scoring: earlier sentences get higher scores, penalize very long sentences | |
score = 1.0 / (i + 1) - (0.01 * max(0, len(sentence.split()) - 20)) | |
scored_sentences.append((score, sentence)) | |
# Sort by score | |
scored_sentences.sort(reverse=True) | |
# Get top sentences until we reach max_length | |
summary_sentences = [] | |
current_length = 0 | |
for _, sentence in scored_sentences: | |
if current_length + len(sentence.split()) <= max_length: | |
summary_sentences.append(sentence) | |
current_length += len(sentence.split()) | |
else: | |
break | |
# Re-order sentences to match original order if we have more than one | |
if summary_sentences: | |
original_order = [] | |
for sentence in summary_sentences: | |
original_order.append((sentences.index(sentence), sentence)) | |
original_order.sort() | |
summary_sentences = [s for _, s in original_order] | |
# Combine into a summary | |
summary = " ".join(summary_sentences) | |
return summary | |
# Custom classification function for job fit assessment | |
def evaluate_job_fit(resume_summary, job_requirements, models): | |
""" | |
Use the sentiment model to evaluate job fit with multiple analyses | |
This function deliberately takes time to do a more thorough analysis, creating | |
multiple perspectives for the sentiment model to evaluate. | |
""" | |
start_time = time.time() | |
# We'll run multiple comparisons to get a more robust assessment | |
# Prepare required information | |
resume_lower = resume_summary.lower() | |
required_skills = job_requirements["required_skills"] | |
years_required = job_requirements["years_experience"] | |
job_title = job_requirements["title"] | |
job_summary = job_requirements["summary"] | |
# Extract skills mentioned in resume | |
skills_in_resume = [] | |
for skill in required_skills: | |
if skill.lower() in resume_lower: | |
skills_in_resume.append(skill) | |
# Skills match percentage | |
skills_match_percentage = int((len(skills_in_resume) / max(1, len(required_skills))) * 100) | |
# Extract years of experience from resume | |
experience_years = 0 | |
year_patterns = [ | |
r'(\d+)\s*(?:\+)?\s*years?\s*(?:of)?\s*experience', | |
r'experience\s*(?:of)?\s*(\d+)\s*(?:\+)?\s*years?' | |
] | |
for pattern in year_patterns: | |
exp_match = re.search(pattern, resume_lower) | |
if exp_match: | |
try: | |
experience_years = int(exp_match.group(1)) | |
break | |
except: | |
pass | |
# If we couldn't find explicit years, try to count based on work history | |
if experience_years == 0: | |
# Try to extract from work experience section | |
work_exp_match = re.search(r'work experience:(.*?)(?=\n\n|$)', resume_summary, re.IGNORECASE | re.DOTALL) | |
if work_exp_match: | |
work_text = work_exp_match.group(1).lower() | |
years = re.findall(r'(\d{4})\s*-\s*(\d{4}|present|current)', work_text) | |
total_years = 0 | |
for year_range in years: | |
start_year = int(year_range[0]) | |
if year_range[1].isdigit(): | |
end_year = int(year_range[1]) | |
else: | |
end_year = 2025 # Assume "present" is current year | |
total_years += (end_year - start_year) | |
experience_years = total_years | |
# Check experience match | |
experience_match = "sufficient" if experience_years >= years_required else "insufficient" | |
# Create multiple comparison texts to evaluate from different angles | |
# Each formatted to bias the sentiment model in a different way | |
# 1. Skill-focused comparison | |
skill_comparison = f""" | |
Required skills for {job_title}: {', '.join(required_skills)} | |
Skills found in candidate resume: {', '.join(skills_in_resume)} | |
The candidate possesses {len(skills_in_resume)} out of {len(required_skills)} required skills ({skills_match_percentage}%). | |
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. | |
""" | |
# 2. Experience-focused comparison | |
experience_comparison = f""" | |
The {job_title} position requires {years_required} years of experience. | |
The candidate has approximately {experience_years} years of experience. | |
Based on experience alone, the candidate {'meets' if experience_years >= years_required else 'does not meet'} the experience requirements for this position. | |
""" | |
# 3. Overall job fit comparison | |
overall_comparison = f""" | |
Job: {job_title} | |
Job description summary: {job_summary} | |
Candidate summary: {resume_summary[:300]} | |
Skills match: {skills_match_percentage}% | |
Experience match: {experience_years}/{years_required} years | |
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. | |
""" | |
# Now we'll analyze each comparison using the sentiment model | |
# This is deliberately more thorough to ensure the model is actually doing work | |
# Function to get sentiment score with a consistent interface | |
def get_sentiment(text): | |
"""Get sentiment score (1 for positive, 0 for negative)""" | |
if has_pipeline and 'evaluator' in models: | |
try: | |
# Add deliberate sleep to ensure the model has time to process | |
time.sleep(0.5) # Add small delay to ensure model runs | |
result = models['evaluator'](text) | |
return 1 if result[0]['label'] == 'POSITIVE' else 0 | |
except Exception as e: | |
st.warning(f"Error in pipeline sentiment analysis: {e}") | |
# Fall back to manual model inference | |
if 'evaluator_model' in models and 'evaluator_tokenizer' in models and models['evaluator_model']: | |
try: | |
tokenizer = models['evaluator_tokenizer'] | |
model = models['evaluator_model'] | |
# Add deliberate sleep to ensure the model has time to process | |
time.sleep(0.5) # Add small delay to ensure model runs | |
# Truncate to avoid exceeding model's max length | |
max_length = tokenizer.model_max_length if hasattr(tokenizer, 'model_max_length') else 512 | |
truncated_text = " ".join(text.split()[:max_length]) | |
inputs = tokenizer(truncated_text, return_tensors="pt", truncation=True, max_length=max_length) | |
with torch.no_grad(): | |
outputs = model(**inputs) | |
probabilities = torch.nn.functional.softmax(outputs.logits, dim=-1) | |
prediction = torch.argmax(probabilities, dim=-1).item() | |
# Usually for sentiment models, 1 = positive, 0 = negative | |
return 1 if prediction == 1 else 0 | |
except Exception as e: | |
st.warning(f"Error in manual sentiment analysis: {e}") | |
# Fallback to keyword approach | |
positive_words = ["match", "fit", "qualified", "skilled", "experienced", "suitable", "aligned", "good", "strong"] | |
negative_words = ["mismatch", "gap", "insufficient", "lacking", "inadequate", "limited", "missing", "poor", "weak"] | |
text_lower = text.lower() | |
positive_count = sum(text_lower.count(word) for word in positive_words) | |
negative_count = sum(text_lower.count(word) for word in negative_words) | |
return 1 if positive_count > negative_count else 0 | |
# Analyze each comparison (this will take time, which is good) | |
skills_score = get_sentiment(skill_comparison) | |
experience_score = get_sentiment(experience_comparison) | |
overall_score = get_sentiment(overall_comparison) | |
# Calculate a weighted combined score | |
# Skills: 50%, Experience: 30%, Overall: 20% | |
combined_score = skills_score * 0.5 + experience_score * 0.3 + overall_score * 0.2 | |
# Now determine the final score (0, 1, or 2) | |
if combined_score >= 0.7 and skills_match_percentage >= 70 and experience_match == "sufficient": | |
final_score = 2 # Strong fit | |
elif combined_score >= 0.4 or (skills_match_percentage >= 50 and experience_match == "sufficient"): | |
final_score = 1 # Potential fit | |
else: | |
final_score = 0 # Not fit | |
# Generate assessment text based on the score | |
if final_score == 2: | |
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." | |
elif final_score == 1: | |
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}." | |
else: | |
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}." | |
execution_time = time.time() - start_time | |
return assessment, final_score, execution_time | |
##################################### | |
# Function: Extract Text from File | |
##################################### | |
def extract_text_from_file(file_obj): | |
""" | |
Extract text from .docx and .doc files. | |
Returns the extracted text or an error message if extraction fails. | |
""" | |
filename = file_obj.name | |
ext = os.path.splitext(filename)[1].lower() | |
text = "" | |
if ext == ".docx": | |
try: | |
document = docx.Document(file_obj) | |
text = "\n".join(para.text for para in document.paragraphs if para.text.strip()) | |
except Exception as e: | |
text = f"Error processing DOCX file: {e}" | |
elif ext == ".doc": | |
try: | |
# For .doc files, we need to save to a temp file | |
with tempfile.NamedTemporaryFile(delete=False, suffix='.doc') as temp_file: | |
temp_file.write(file_obj.getvalue()) | |
temp_path = temp_file.name | |
# Use docx2txt which is generally faster | |
try: | |
text = docx2txt.process(temp_path) | |
except Exception: | |
text = "Could not process .doc file. Please convert to .docx format." | |
# Clean up temp file | |
os.unlink(temp_path) | |
except Exception as e: | |
text = f"Error processing DOC file: {e}" | |
elif ext == ".txt": | |
try: | |
text = file_obj.getvalue().decode("utf-8") | |
except Exception as e: | |
text = f"Error processing TXT file: {e}" | |
else: | |
text = "Unsupported file type. Please upload a .docx, .doc, or .txt file." | |
# Limit text size for faster processing | |
return text[:15000] if text else text | |
##################################### | |
# Functions for Information Extraction | |
##################################### | |
# Cache the extraction functions to avoid reprocessing | |
def extract_name(text_start): | |
"""Extract candidate name from the beginning of resume text""" | |
# Only use the first 500 characters to speed up processing | |
lines = text_start.split('\n') | |
# Check first few non-empty lines for potential names | |
potential_name_lines = [line.strip() for line in lines[:5] if line.strip()] | |
if potential_name_lines: | |
# First line is often the name if it's short and doesn't contain common headers | |
first_line = potential_name_lines[0] | |
if 5 <= len(first_line) <= 40 and not any(x in first_line.lower() for x in ["resume", "cv", "curriculum", "vitae", "profile"]): | |
return first_line | |
# Look for lines that might contain a name | |
for line in potential_name_lines[:3]: | |
if len(line.split()) <= 4 and not any(x in line.lower() for x in ["address", "phone", "email", "resume", "cv"]): | |
return line | |
return "Unknown (please extract from resume)" | |
def extract_skills_and_work(text): | |
"""Extract both skills and work experience at once to save processing time""" | |
# Common skill categories - reduced keyword list for speed | |
skill_categories = { | |
"Programming": ["Python", "Java", "JavaScript", "HTML", "CSS", "SQL", "C++", "C#", "Go"], | |
"Data Science": ["Machine Learning", "Data Analysis", "Statistics", "TensorFlow", "PyTorch", "AI", "Algorithms"], | |
"Database": ["SQL", "MySQL", "MongoDB", "Database", "NoSQL", "PostgreSQL"], | |
"Web Development": ["React", "Angular", "Node.js", "Frontend", "Backend", "Full-Stack"], | |
"Software Development": ["Agile", "Scrum", "Git", "DevOps", "Docker", "System Design"], | |
"Cloud": ["AWS", "Azure", "Google Cloud", "Cloud Computing"], | |
"Security": ["Cybersecurity", "Network Security", "Encryption", "Security"], | |
"Business": ["Project Management", "Business Analysis", "Leadership", "Teamwork"], | |
"Design": ["UX/UI", "User Experience", "Design Thinking", "Adobe"] | |
} | |
# Work experience extraction | |
work_headers = [ | |
"work experience", "professional experience", "employment history", | |
"work history", "experience" | |
] | |
next_section_headers = [ | |
"education", "skills", "certifications", "projects", "achievements" | |
] | |
# Process everything at once | |
lines = text.split('\n') | |
text_lower = text.lower() | |
# Skills extraction | |
found_skills = [] | |
for category, skills in skill_categories.items(): | |
category_skills = [] | |
for skill in skills: | |
if skill.lower() in text_lower: | |
category_skills.append(skill) | |
if category_skills: | |
found_skills.append(f"{category}: {', '.join(category_skills)}") | |
# Work experience extraction - simplified approach | |
work_section = [] | |
in_work_section = False | |
for idx, line in enumerate(lines): | |
line_lower = line.lower().strip() | |
# Start of work section | |
if not in_work_section: | |
if any(header in line_lower for header in work_headers): | |
in_work_section = True | |
continue | |
# End of work section | |
elif in_work_section: | |
if any(header in line_lower for header in next_section_headers): | |
break | |
if line.strip(): | |
work_section.append(line.strip()) | |
# Simplified work formatting | |
if not work_section: | |
work_experience = "Work experience not clearly identified" | |
else: | |
# Just take the first 5-7 lines of the work section as a summary | |
work_lines = [] | |
company_count = 0 | |
current_company = "" | |
for line in work_section: | |
# New company entry often has a date | |
if re.search(r'(19|20)\d{2}', line): | |
company_count += 1 | |
if company_count <= 3: # Limit to 3 most recent positions | |
current_company = line | |
work_lines.append(f"**{line}**") | |
else: | |
break | |
elif company_count <= 3 and len(work_lines) < 10: # Limit total lines | |
work_lines.append(line) | |
work_experience = "\n• " + "\n• ".join(work_lines[:7]) if work_lines else "Work experience not clearly structured" | |
skills_formatted = "\n• " + "\n• ".join(found_skills) if found_skills else "No specific technical skills clearly identified" | |
return skills_formatted, work_experience | |
##################################### | |
# Function: Summarize Resume Text | |
##################################### | |
def summarize_resume_text(resume_text, models): | |
""" | |
Generates a structured summary of the resume text | |
""" | |
start_time = time.time() | |
# Use our summarize_text function which handles both pipeline and non-pipeline cases | |
base_summary = summarize_text(resume_text, models, max_length=100) | |
# Extract name from the beginning of the resume | |
name = extract_name(resume_text[:500]) | |
# Extract skills and work experience | |
skills, work_experience = extract_skills_and_work(resume_text) | |
# Extract education level - simplified approach | |
education_level = "Not specified" | |
education_terms = ["bachelor", "master", "phd", "doctorate", "mba", "degree"] | |
for term in education_terms: | |
if term in resume_text.lower(): | |
education_level = "Higher education degree mentioned" | |
break | |
# Format the structured summary | |
formatted_summary = f"Name: {name}\n\n" | |
formatted_summary += f"Summary: {base_summary}\n\n" | |
formatted_summary += f"Previous Work Experience: {work_experience}\n\n" | |
formatted_summary += f"Skills: {skills}\n\n" | |
formatted_summary += f"Education: {education_level}" | |
execution_time = time.time() - start_time | |
return formatted_summary, execution_time | |
##################################### | |
# Function: Extract Job Requirements | |
##################################### | |
def extract_job_requirements(job_description, models): | |
""" | |
Extract key requirements from a job description | |
""" | |
# Common technical skills to look for | |
tech_skills = [ | |
"Python", "Java", "C++", "JavaScript", "TypeScript", "Go", "Rust", "SQL", "Ruby", "PHP", "Swift", "Kotlin", | |
"React", "Angular", "Vue", "Node.js", "HTML", "CSS", "Django", "Flask", "Spring", "REST API", "GraphQL", | |
"Machine Learning", "TensorFlow", "PyTorch", "Data Science", "AI", "Big Data", "Deep Learning", "NLP", | |
"AWS", "Azure", "GCP", "Docker", "Kubernetes", "CI/CD", "Jenkins", "GitHub Actions", "Terraform", | |
"MySQL", "PostgreSQL", "MongoDB", "Redis", "Elasticsearch", "DynamoDB", "Cassandra" | |
] | |
# Clean the text for processing | |
clean_job_text = job_description.lower() | |
# Extract job title | |
title_patterns = [ | |
r'^([^:.\n]+?)(position|role|job|opening|vacancy)', | |
r'^([^:.\n]+?)\n', | |
r'(hiring|looking for(?: a| an)?|recruiting)(?: a| an)? ([^:.\n]+?)(:-|[.:]|\n|$)' | |
] | |
job_title = "Not specified" | |
for pattern in title_patterns: | |
title_match = re.search(pattern, clean_job_text, re.IGNORECASE) | |
if title_match: | |
potential_title = title_match.group(1).strip() if len(title_match.groups()) >= 1 else title_match.group(2).strip() | |
if 3 <= len(potential_title) <= 50: # Reasonable title length | |
job_title = potential_title.capitalize() | |
break | |
# Extract years of experience | |
exp_patterns = [ | |
r'(\d+)(?:\+)?\s*(?:years|yrs)(?:\s*of)?\s*(?:experience|exp)', | |
r'experience\s*(?:of)?\s*(\d+)(?:\+)?\s*(?:years|yrs)' | |
] | |
years_required = 0 | |
for pattern in exp_patterns: | |
exp_match = re.search(pattern, clean_job_text, re.IGNORECASE) | |
if exp_match: | |
try: | |
years_required = int(exp_match.group(1)) | |
break | |
except: | |
pass | |
# Extract required skills | |
required_skills = [skill for skill in tech_skills if re.search(r'\b' + re.escape(skill.lower()) + r'\b', clean_job_text)] | |
# Create a simple summary of the job using the summarize_text function | |
job_summary = summarize_text(job_description, models, max_length=100) | |
# Format the job requirements | |
job_requirements = { | |
"title": job_title, | |
"years_experience": years_required, | |
"required_skills": required_skills, | |
"summary": job_summary | |
} | |
return job_requirements | |
##################################### | |
# Function: Analyze Job Fit | |
##################################### | |
def analyze_job_fit(resume_summary, job_description, models): | |
""" | |
Analyze how well the candidate fits the job requirements. | |
Returns a fit score (0-2) and an assessment. | |
""" | |
start_time = time.time() | |
# Extract job requirements | |
job_requirements = extract_job_requirements(job_description, models) | |
# Use our more thorough evaluation function | |
assessment, fit_score, execution_time = evaluate_job_fit(resume_summary, job_requirements, models) | |
return assessment, fit_score, execution_time | |
# Load models at startup | |
models = load_models() | |
##################################### | |
# Main Streamlit Interface | |
##################################### | |
st.title("Resume-Job Fit Analyzer") | |
st.markdown( | |
""" | |
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. | |
""" | |
) | |
# Resume upload | |
uploaded_file = st.file_uploader("Upload your resume (.docx, .doc, or .txt)", type=["docx", "doc", "txt"]) | |
# Job description input | |
job_description = st.text_area("Enter Job Description", height=200, placeholder="Paste the job description here...") | |
# Process button with optimized flow | |
if uploaded_file is not None and job_description and st.button("Analyze Job Fit"): | |
# Create a placeholder for the progress bar | |
progress_bar = st.progress(0) | |
status_text = st.empty() | |
# Step 1: Extract text | |
status_text.text("Step 1/3: Extracting text from resume...") | |
resume_text = extract_text_from_file(uploaded_file) | |
progress_bar.progress(25) | |
if resume_text.startswith("Error") or resume_text == "Unsupported file type. Please upload a .docx, .doc, or .txt file.": | |
st.error(resume_text) | |
else: | |
# Step 2: Generate summary | |
status_text.text("Step 2/3: Analyzing resume and generating summary...") | |
summary, summarization_time = summarize_resume_text(resume_text, models) | |
progress_bar.progress(50) | |
# Display summary | |
st.subheader("Your Resume Summary") | |
st.markdown(summary) | |
# Step 3: Generate job fit assessment | |
status_text.text("Step 3/3: Evaluating job fit (this will take a moment)...") | |
assessment, fit_score, assessment_time = analyze_job_fit(summary, job_description, models) | |
progress_bar.progress(100) | |
# Clear status messages | |
status_text.empty() | |
# Display job fit results | |
st.subheader("Job Fit Assessment") | |
# Display fit score with label | |
fit_labels = { | |
0: "NOT FIT ❌", | |
1: "POTENTIAL FIT ⚠️", | |
2: "STRONG FIT ✅" | |
} | |
# Show the score prominently | |
st.markdown(f"## {fit_labels[fit_score]}") | |
# Display assessment | |
st.markdown(assessment) | |
st.info(f"Analysis completed in {(summarization_time + assessment_time):.2f} seconds") | |
# Add potential next steps based on the fit score | |
st.subheader("Recommended Next Steps") | |
if fit_score == 2: | |
st.markdown(""" | |
- Apply for this position as you appear to be a strong match | |
- Prepare for interviews by focusing on your relevant experience | |
- Highlight your matching skills in your cover letter | |
""") | |
elif fit_score == 1: | |
st.markdown(""" | |
- Consider applying but address skill gaps in your cover letter | |
- Emphasize transferable skills and relevant experience | |
- Prepare to discuss how you can quickly develop missing skills | |
""") | |
else: | |
st.markdown(""" | |
- Look for positions better aligned with your current skills | |
- If interested in this field, focus on developing the required skills | |
- Consider similar roles with fewer experience requirements | |
""") |