""" 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( """ """, 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('

๐ŸŽฏ AI Resume ATS Analyzer

', 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"""

{results["base_score"]}

Resume Score

""", unsafe_allow_html=True, ) with c2: color = _color_score(results["job_match"]) st.markdown( f"""

{results["job_match"]}%

Job Match

""", unsafe_allow_html=True, ) with c3: color = _color_score(results["ats_score"]) st.markdown( f"""

{results["ats_score"]}%

ATS Score

""", unsafe_allow_html=True, ) with c4: label = results["classification"]["label"] cls_color = results["classification"]["color"] css_class = f"classification-{cls_color}" st.markdown( f"""
{label}

Classification

""", unsafe_allow_html=True, ) st.markdown("
", 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('

๐Ÿ” Detected Information

', 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('

๐Ÿ“‘ Sections Detected

', 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('

โš ๏ธ Missing Sections

', unsafe_allow_html=True) for ms in results["missing_sections"]: st.warning(f"Missing: **{ms}**") with right_col: # โ”€โ”€ Technical Skills โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ st.markdown('

โš™๏ธ Technical Skills Found

', unsafe_allow_html=True) tech_skills = results["skills"].get("technical", []) if tech_skills: badges = "".join( f'{s.title()}' 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('

๐Ÿค Soft Skills Found

', unsafe_allow_html=True) soft_skills = results["skills"].get("soft", []) if soft_skills: badges = "".join( f'{s.title()}' 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('

๐Ÿ”‘ Keyword Overlap with JD

', 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'
{suggestion}
', 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("
", 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( """
๐ŸŽฏ AI Resume ATS Analyzer โ€” Built with open-source AI tools
Powered by FLAN-T5 ยท Sentence-BERT ยท spaCy ยท PyMuPDF
""", unsafe_allow_html=True, )