chirag1121's picture
Create app.py
d632eb4 verified
"""
app.py β€” AI Resume ATS Analyzer
================================
Main Streamlit application entry point.
Features:
- Upload PDF or DOCX resume
- Automatic text extraction
- Named entity recognition (name, orgs, locations)
- Skills extraction (technical + soft)
- Section detection (Skills, Experience, Projects, Education, Summary)
- Resume base score (0–100) with breakdown
- Job description semantic similarity via Sentence-BERT
- ATS Score = 0.6 Γ— base_score + 0.4 Γ— job_match
- Classification: Good / Average / Poor
- Actionable suggestions
- AI-powered resume rewrite via FLAN-T5
- Manual resume editing + re-evaluation
Run locally:
streamlit run app.py
Deploy on Hugging Face Spaces:
1. Create a new Space (SDK: Streamlit)
2. Upload all project files
3. The Space will install requirements.txt and launch automatically
"""
import streamlit as st
import pandas as pd
import time
# Local utility modules
from utils.parser import parse_resume
from utils.nlp_utils import (
extract_entities,
detect_sections,
extract_skills,
get_missing_sections,
classify_resume,
generate_suggestions,
)
from utils.scorer import compute_base_score, compute_ats_score
from utils.similarity import compute_similarity, keyword_overlap_score
from utils.generator import generate_improved_resume, generate_professional_summary
# ---------------------------------------------------------------------------
# Page configuration
# ---------------------------------------------------------------------------
st.set_page_config(
page_title="AI Resume ATS Analyzer",
page_icon="πŸ“„",
layout="wide",
initial_sidebar_state="expanded",
)
# ---------------------------------------------------------------------------
# Custom CSS β€” clean, professional look
# ---------------------------------------------------------------------------
st.markdown(
"""
<style>
/* Main header */
.main-header {
font-size: 2.4rem;
font-weight: 800;
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin-bottom: 0.2rem;
}
/* Score cards */
.score-card {
background: #f8f9fa;
border-radius: 12px;
padding: 1.2rem;
text-align: center;
border: 1px solid #e9ecef;
box-shadow: 0 2px 6px rgba(0,0,0,0.06);
}
.score-number {
font-size: 2.2rem;
font-weight: 700;
margin: 0;
}
.score-label {
font-size: 0.85rem;
color: #6c757d;
text-transform: uppercase;
letter-spacing: 0.05em;
}
/* Section headers */
.section-header {
font-size: 1.1rem;
font-weight: 600;
color: #343a40;
border-bottom: 2px solid #667eea;
padding-bottom: 0.3rem;
margin-top: 1.5rem;
margin-bottom: 0.8rem;
}
/* Skill badges */
.skill-badge {
display: inline-block;
background: #e8f4fd;
color: #1a6a9a;
border-radius: 20px;
padding: 0.2rem 0.7rem;
margin: 0.15rem;
font-size: 0.82rem;
font-weight: 500;
}
/* Suggestion items */
.suggestion-item {
background: #fffbf0;
border-left: 4px solid #ffc107;
padding: 0.6rem 0.9rem;
border-radius: 0 6px 6px 0;
margin-bottom: 0.5rem;
font-size: 0.9rem;
}
/* Good/Average/Poor banner */
.classification-good {
background: #d4edda; color: #155724;
padding: 0.5rem 1rem; border-radius: 8px;
font-size: 1.1rem; font-weight: 600;
text-align: center;
}
.classification-average {
background: #fff3cd; color: #856404;
padding: 0.5rem 1rem; border-radius: 8px;
font-size: 1.1rem; font-weight: 600;
text-align: center;
}
.classification-poor {
background: #f8d7da; color: #721c24;
padding: 0.5rem 1rem; border-radius: 8px;
font-size: 1.1rem; font-weight: 600;
text-align: center;
}
/* Info boxes */
.stAlert { border-radius: 8px; }
</style>
""",
unsafe_allow_html=True,
)
# ---------------------------------------------------------------------------
# Sidebar β€” About & Instructions
# ---------------------------------------------------------------------------
with st.sidebar:
st.image(
"https://img.icons8.com/color/96/resume.png",
width=70,
)
st.markdown("## πŸ“„ AI Resume Analyzer")
st.markdown(
"""
**How to use:**
1. Upload your resume (PDF or DOCX)
2. Paste the job description
3. Click **Analyze Resume**
4. Review your scores & suggestions
5. Use **Generate Improved Resume** for AI rewrite
6. Edit & **Re-evaluate** anytime
---
**Scoring Formula:**
- Resume Score: up to 100 pts
- Skills (20) + Experience (30)
- Projects (20) + Education (10)
- Length (10) + Diversity (10)
- ATS Score = 0.6 Γ— Resume Score
+ 0.4 Γ— Job Match %
---
**Powered by:**
- πŸ€— FLAN-T5 (text generation)
- πŸ”€ Sentence-BERT (similarity)
- 🧠 spaCy (NER)
- πŸ“„ PyMuPDF + python-docx
"""
)
st.markdown("---")
st.caption("Built with ❀️ using open-source AI")
# ---------------------------------------------------------------------------
# Main UI β€” Header
# ---------------------------------------------------------------------------
st.markdown('<p class="main-header">🎯 AI Resume ATS Analyzer</p>', unsafe_allow_html=True)
st.markdown(
"Upload your resume and a job description to get an **AI-powered ATS score**, "
"skill analysis, and personalized improvement suggestions.",
unsafe_allow_html=True,
)
st.markdown("---")
# ---------------------------------------------------------------------------
# Input Section β€” two columns
# ---------------------------------------------------------------------------
col_upload, col_jd = st.columns([1, 1], gap="large")
with col_upload:
st.markdown("### πŸ“€ Upload Resume")
uploaded_file = st.file_uploader(
"Supported formats: PDF, DOCX",
type=["pdf", "docx"],
help="Upload your resume in PDF or Word format",
)
with col_jd:
st.markdown("### πŸ“‹ Job Description")
job_description = st.text_area(
"Paste the job description here",
height=200,
placeholder=(
"e.g., We are looking for a Python Developer with experience in "
"FastAPI, PostgreSQL, and Docker. You will work on backend systems, "
"REST APIs, and cloud deployments on AWS..."
),
help="The job description is used for semantic matching and tailored suggestions.",
)
# ---------------------------------------------------------------------------
# Session state β€” persist results across reruns
# ---------------------------------------------------------------------------
if "results" not in st.session_state:
st.session_state.results = None
if "resume_text_editable" not in st.session_state:
st.session_state.resume_text_editable = ""
if "improved_resume" not in st.session_state:
st.session_state.improved_resume = ""
# ---------------------------------------------------------------------------
# Helper: run the full analysis pipeline
# ---------------------------------------------------------------------------
def run_analysis(resume_text: str, job_desc: str) -> dict:
"""
Execute the complete resume analysis pipeline.
Args:
resume_text: extracted or edited resume text
job_desc : job description text
Returns:
Dictionary with all analysis results.
"""
# 1. NLP Analysis
with st.spinner("πŸ” Running NLP analysis..."):
entities = extract_entities(resume_text)
sections = detect_sections(resume_text)
skills = extract_skills(resume_text)
missing_sections = get_missing_sections(sections)
# 2. Scoring
with st.spinner("πŸ“Š Computing resume score..."):
score_result = compute_base_score(resume_text, sections, skills)
# 3. Job Description Similarity
with st.spinner("🎯 Matching against job description..."):
if job_desc.strip():
job_match = compute_similarity(resume_text, job_desc)
kw_overlap = keyword_overlap_score(resume_text, job_desc)
else:
job_match = 0.0
kw_overlap = {"overlap_pct": 0.0, "matched": [], "missing": []}
# 4. ATS Score + Classification
ats_score = compute_ats_score(score_result["total"], job_match)
classification = classify_resume(ats_score)
# 5. Suggestions
suggestions = generate_suggestions(
sections, skills, score_result["total"], job_match
)
return {
"entities": entities,
"sections": sections,
"skills": skills,
"missing_sections": missing_sections,
"base_score": score_result["total"],
"score_breakdown": score_result["breakdown"],
"job_match": job_match,
"kw_overlap": kw_overlap,
"ats_score": ats_score,
"classification": classification,
"suggestions": suggestions,
}
# ---------------------------------------------------------------------------
# Helper: render analysis results
# ---------------------------------------------------------------------------
def render_results(results: dict, resume_text: str):
"""Render the full analysis results in the Streamlit UI."""
st.markdown("---")
st.markdown("## πŸ“Š Analysis Results")
# ── Score Dashboard ────────────────────────────────────────────────────
c1, c2, c3, c4 = st.columns(4)
def _color_score(score, thresholds=(70, 45)):
high, mid = thresholds
if score >= high:
return "#28a745"
elif score >= mid:
return "#ffc107"
return "#dc3545"
with c1:
color = _color_score(results["base_score"])
st.markdown(
f"""<div class="score-card">
<p class="score-number" style="color:{color}">{results["base_score"]}</p>
<p class="score-label">Resume Score</p>
</div>""",
unsafe_allow_html=True,
)
with c2:
color = _color_score(results["job_match"])
st.markdown(
f"""<div class="score-card">
<p class="score-number" style="color:{color}">{results["job_match"]}%</p>
<p class="score-label">Job Match</p>
</div>""",
unsafe_allow_html=True,
)
with c3:
color = _color_score(results["ats_score"])
st.markdown(
f"""<div class="score-card">
<p class="score-number" style="color:{color}">{results["ats_score"]}%</p>
<p class="score-label">ATS Score</p>
</div>""",
unsafe_allow_html=True,
)
with c4:
label = results["classification"]["label"]
cls_color = results["classification"]["color"]
css_class = f"classification-{cls_color}"
st.markdown(
f"""<div class="score-card">
<div class="{css_class}" style="margin-top:0.5rem">{label}</div>
<p class="score-label" style="margin-top:0.5rem">Classification</p>
</div>""",
unsafe_allow_html=True,
)
st.markdown("<br>", unsafe_allow_html=True)
# ── Score Breakdown Bar Chart ──────────────────────────────────────────
with st.expander("πŸ“ˆ Score Breakdown", expanded=True):
breakdown = results["score_breakdown"]
max_vals = {
"Skills": 20,
"Experience": 30,
"Projects": 20,
"Education": 10,
"Length": 10,
"Diversity": 10,
}
df = pd.DataFrame(
{
"Category": list(breakdown.keys()),
"Your Score": list(breakdown.values()),
"Max Score": [max_vals.get(k, 10) for k in breakdown.keys()],
}
)
st.bar_chart(df.set_index("Category")[["Your Score", "Max Score"]])
# ── Two column layout for details ─────────────────────────────────────
left_col, right_col = st.columns([1, 1], gap="large")
with left_col:
# ── Detected Information ──────────────────────────────────────────
st.markdown('<p class="section-header">πŸ” Detected Information</p>', unsafe_allow_html=True)
entities = results["entities"]
if entities.get("name"):
st.markdown(f"**πŸ‘€ Candidate Name:** {entities['name']}")
if entities.get("organizations"):
st.markdown(f"**🏒 Organizations:** {', '.join(entities['organizations'][:5])}")
if entities.get("locations"):
st.markdown(f"**πŸ“ Locations:** {', '.join(entities['locations'][:5])}")
# ── Sections Detected ─────────────────────────────────────────────
st.markdown('<p class="section-header">πŸ“‘ Sections Detected</p>', unsafe_allow_html=True)
section_display = {
"skills": "πŸ› οΈ Skills",
"education": "πŸŽ“ Education",
"experience": "πŸ’Ό Experience",
"projects": "πŸš€ Projects",
"summary": "πŸ“ Summary",
"certifications": "πŸ† Certifications",
}
cols = st.columns(2)
for i, (key, label) in enumerate(section_display.items()):
present = results["sections"].get(key, False)
icon = "βœ…" if present else "❌"
cols[i % 2].markdown(f"{icon} {label}")
# ── Missing Sections ──────────────────────────────────────────────
if results["missing_sections"]:
st.markdown('<p class="section-header">⚠️ Missing Sections</p>', unsafe_allow_html=True)
for ms in results["missing_sections"]:
st.warning(f"Missing: **{ms}**")
with right_col:
# ── Technical Skills ──────────────────────────────────────────────
st.markdown('<p class="section-header">βš™οΈ Technical Skills Found</p>', unsafe_allow_html=True)
tech_skills = results["skills"].get("technical", [])
if tech_skills:
badges = "".join(
f'<span class="skill-badge">{s.title()}</span>'
for s in tech_skills
)
st.markdown(badges, unsafe_allow_html=True)
else:
st.info("No technical skills detected. Add a Skills section.")
# ── Soft Skills ───────────────────────────────────────────────────
st.markdown('<p class="section-header">🀝 Soft Skills Found</p>', unsafe_allow_html=True)
soft_skills = results["skills"].get("soft", [])
if soft_skills:
badges = "".join(
f'<span class="skill-badge" style="background:#f0fff0;color:#276749">{s.title()}</span>'
for s in soft_skills
)
st.markdown(badges, unsafe_allow_html=True)
else:
st.info("No soft skills detected.")
# ── Keyword Overlap (if JD provided) ─────────────────────────────
kw = results["kw_overlap"]
if kw["matched"] or kw["missing"]:
st.markdown('<p class="section-header">πŸ”‘ Keyword Overlap with JD</p>', unsafe_allow_html=True)
k1, k2 = st.columns(2)
with k1:
st.metric("Overlap", f"{kw['overlap_pct']}%")
if kw["matched"]:
st.markdown("**βœ… Matched:**")
st.markdown(", ".join(kw["matched"][:10]))
with k2:
if kw["missing"]:
st.markdown("**❌ Missing from JD:**")
st.markdown(", ".join(kw["missing"][:10]))
# ── Suggestions ────────────────────────────────────────────────────────
st.markdown("---")
st.markdown("## πŸ’‘ Improvement Suggestions")
for suggestion in results["suggestions"]:
st.markdown(
f'<div class="suggestion-item">{suggestion}</div>',
unsafe_allow_html=True,
)
# ── Resume Text Preview ───────────────────────────────────────────────
st.markdown("---")
st.markdown("## πŸ“„ Resume Preview")
word_count = len(resume_text.split())
st.caption(f"Word count: {word_count}")
with st.expander("View extracted resume text", expanded=False):
st.text_area(
"Resume Text (read-only preview)",
value=resume_text,
height=300,
disabled=True,
key="preview_text",
)
# ---------------------------------------------------------------------------
# BUTTON 1 β€” Analyze Resume
# ---------------------------------------------------------------------------
st.markdown("<br>", unsafe_allow_html=True)
analyze_col, _, _ = st.columns([1, 1, 1])
with analyze_col:
analyze_clicked = st.button(
"πŸ” Analyze Resume",
type="primary",
use_container_width=True,
disabled=(uploaded_file is None),
)
if analyze_clicked:
if uploaded_file is None:
st.error("Please upload a resume file first.")
else:
# Parse the uploaded file
with st.spinner("πŸ“‚ Extracting text from resume..."):
parse_result = parse_resume(uploaded_file)
if parse_result["error"]:
st.error(f"❌ {parse_result['error']}")
st.stop()
resume_text = parse_result["text"]
if len(resume_text.split()) < 20:
st.warning(
"⚠️ Very little text was extracted. "
"The file might be image-based. Try a text-based PDF or DOCX."
)
# Store editable text in session state
st.session_state.resume_text_editable = resume_text
# Run full pipeline
results = run_analysis(resume_text, job_description)
st.session_state.results = results
st.success("βœ… Analysis complete!")
# Render results if available
if st.session_state.results:
render_results(st.session_state.results, st.session_state.resume_text_editable)
# ---------------------------------------------------------------------------
# BUTTON 2 β€” Generate Improved Resume (AI Rewrite)
# ---------------------------------------------------------------------------
if st.session_state.results:
st.markdown("---")
st.markdown("## πŸ€– AI Resume Improvement")
st.markdown(
"Click below to generate an **ATS-optimized rewrite** of your resume "
"using FLAN-T5. The AI will use professional tone, bullet points, "
"and strong action verbs."
)
gen_col, _ = st.columns([1, 2])
with gen_col:
gen_clicked = st.button(
"✨ Generate Improved Resume",
use_container_width=True,
)
if gen_clicked:
with st.spinner("πŸ€– AI is rewriting your resume... (this may take 30–60 seconds on CPU)"):
improved = generate_improved_resume(
st.session_state.resume_text_editable,
job_description,
st.session_state.results.get("missing_sections", []),
)
st.session_state.improved_resume = improved
if st.session_state.improved_resume:
st.markdown("### βœ… AI-Generated Improved Resume")
st.text_area(
"You can copy this improved resume text:",
value=st.session_state.improved_resume,
height=400,
key="improved_output",
)
st.info(
"πŸ’‘ **Tip:** Copy this improved text, paste it into the editor below, "
"and click **Re-evaluate** to see your new ATS score!"
)
# ── Professional Summary Generator ─────────────────────────────────────
with st.expander("πŸ–ŠοΈ Generate Professional Summary"):
summ_col, _ = st.columns([1, 2])
with summ_col:
target_role = st.text_input(
"Target job title (optional)",
placeholder="e.g., Backend Engineer",
)
summ_btn = st.button("Generate Summary", key="summ_btn")
if summ_btn:
with st.spinner("Generating summary..."):
summary = generate_professional_summary(
name=st.session_state.results["entities"].get("name", ""),
skills=st.session_state.results["skills"].get("technical", []),
experience_present=st.session_state.results["sections"].get(
"experience", False
),
job_title=target_role,
)
st.markdown("**Generated Summary:**")
st.markdown(f"> {summary}")
# ---------------------------------------------------------------------------
# BUTTON 3 β€” Resume Editor + Re-evaluation
# ---------------------------------------------------------------------------
if st.session_state.results:
st.markdown("---")
st.markdown("## ✏️ Edit & Re-evaluate Resume")
st.markdown(
"Edit your resume text below (paste the AI-improved version or make "
"manual changes) and click **Re-evaluate** to get a fresh ATS score."
)
edited_text = st.text_area(
"Edit your resume here:",
value=st.session_state.resume_text_editable,
height=400,
key="editor_area",
help="Make any changes to improve your resume, then click Re-evaluate.",
)
re_eval_col, _ = st.columns([1, 2])
with re_eval_col:
re_eval_clicked = st.button(
"πŸ”„ Re-evaluate Edited Resume",
type="secondary",
use_container_width=True,
)
if re_eval_clicked:
if not edited_text.strip():
st.error("The editor is empty. Please paste your resume text.")
else:
# Update session state
st.session_state.resume_text_editable = edited_text
with st.spinner("πŸ“Š Re-analyzing your edited resume..."):
new_results = run_analysis(edited_text, job_description)
st.session_state.results = new_results
st.success("βœ… Re-evaluation complete!")
st.markdown("### πŸ“Š Updated Results")
render_results(new_results, edited_text)
# ---------------------------------------------------------------------------
# Footer
# ---------------------------------------------------------------------------
st.markdown("---")
st.markdown(
"""
<div style="text-align:center; color:#6c757d; font-size:0.85rem; padding: 1rem 0;">
🎯 AI Resume ATS Analyzer β€” Built with open-source AI tools<br>
Powered by FLAN-T5 Β· Sentence-BERT Β· spaCy Β· PyMuPDF
</div>
""",
unsafe_allow_html=True,
)