Spaces:
Sleeping
Sleeping
Download app.py from Thestrelema/ATS: direct link, hf CLI and curl.
- Browser
- Download file 11.3 kB
-
https://huggingface.co/spaces/Thestrelema/ATS/resolve/main/app.py
- Command line
-
hf download hf://spaces/Thestrelema/ATS/app.py
-
curl -L -o app.py https://huggingface.co/spaces/Thestrelema/ATS/resolve/main/app.py
11.3 kB
| import os | |
| # ---------------- SAFE CACHE ---------------- | |
| os.environ["HF_HOME"] = "/tmp/hf" | |
| os.environ["TRANSFORMERS_CACHE"] = "/tmp/hf" | |
| os.environ["TORCH_HOME"] = "/tmp/torch" | |
| # ---------------- IMPORTS ---------------- | |
| import streamlit as st | |
| import pdfplumber | |
| import re | |
| import pandas as pd | |
| import spacy | |
| import json | |
| from keybert import KeyBERT | |
| from sentence_transformers import SentenceTransformer, util | |
| from streamlit_lottie import st_lottie | |
| # ========================================================= | |
| # PAGE CONFIG | |
| # ========================================================= | |
| st.set_page_config( | |
| page_title="AI Resume ATS Matcher", | |
| page_icon="π", | |
| layout="wide" | |
| ) | |
| # ========================================================= | |
| # CUSTOM CSS (LIGHT MODERN UI) | |
| # ========================================================= | |
| st.markdown(""" | |
| <style> | |
| /* Main Background */ | |
| .stApp { | |
| background-color: #f8fafc; | |
| } | |
| /* Header */ | |
| .main-title { | |
| font-size: 42px; | |
| font-weight: 800; | |
| color: #111827; | |
| text-align: center; | |
| margin-bottom: 5px; | |
| } | |
| .sub-title { | |
| font-size: 18px; | |
| color: #6b7280; | |
| text-align: center; | |
| margin-bottom: 30px; | |
| } | |
| /* Upload Box */ | |
| .upload-box { | |
| background: white; | |
| padding: 25px; | |
| border-radius: 18px; | |
| border: 1px solid #e5e7eb; | |
| box-shadow: 0px 2px 10px rgba(0,0,0,0.04); | |
| margin-bottom: 20px; | |
| } | |
| /* Candidate Card */ | |
| .candidate-card { | |
| background: white; | |
| padding: 20px; | |
| border-radius: 18px; | |
| border: 1px solid #e5e7eb; | |
| margin-bottom: 20px; | |
| box-shadow: 0px 2px 10px rgba(0,0,0,0.05); | |
| } | |
| /* Skill Tags */ | |
| .skill-tag { | |
| display: inline-block; | |
| background: #eff6ff; | |
| color: #2563eb; | |
| padding: 6px 12px; | |
| border-radius: 30px; | |
| margin: 4px; | |
| font-size: 13px; | |
| font-weight: 500; | |
| } | |
| /* Buttons */ | |
| .stButton > button { | |
| width: 100%; | |
| height: 55px; | |
| border-radius: 14px; | |
| background-color: #2563eb; | |
| color: white; | |
| font-size: 18px; | |
| font-weight: 700; | |
| border: none; | |
| } | |
| .stButton > button:hover { | |
| background-color: #1d4ed8; | |
| color: white; | |
| } | |
| /* Dataframe */ | |
| [data-testid="stDataFrame"] { | |
| border-radius: 15px; | |
| overflow: hidden; | |
| } | |
| /* Hide Streamlit Footer */ | |
| footer { | |
| visibility: hidden; | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # ========================================================= | |
| # LOAD MODELS | |
| # ========================================================= | |
| def load_models(): | |
| model = SentenceTransformer("all-MiniLM-L6-v2") | |
| nlp = spacy.load("en_core_web_sm") | |
| kw_model = KeyBERT(model=model) | |
| return model, nlp, kw_model | |
| model, nlp, kw_model = load_models() | |
| # ========================================================= | |
| # LOAD ANIMATION | |
| # ========================================================= | |
| def load_lottiefile(filepath): | |
| try: | |
| with open(filepath, "r") as f: | |
| return json.load(f) | |
| except: | |
| return None | |
| lottie_animation = load_lottiefile("Animation.json") | |
| # ========================================================= | |
| # HEADER | |
| # ========================================================= | |
| if lottie_animation: | |
| st_lottie(lottie_animation, height=180) | |
| st.markdown( | |
| '<div class="main-title">π€ AI Resume ATS </div>', | |
| unsafe_allow_html=True | |
| ) | |
| st.markdown( | |
| '<div class="sub-title">Upload multiple resumes and instantly rank the best candidates</div>', | |
| unsafe_allow_html=True | |
| ) | |
| # ========================================================= | |
| # HELPERS | |
| # ========================================================= | |
| def extract_text_from_pdf(pdf_file): | |
| text = "" | |
| with pdfplumber.open(pdf_file) as pdf: | |
| for page in pdf.pages: | |
| page_text = page.extract_text() | |
| if page_text: | |
| text += page_text + "\n" | |
| return text.strip() | |
| def clean_text(text): | |
| return re.sub(r"\s+", " ", text).strip() | |
| def extract_skills_auto(text, top_n=15): | |
| text = text[:3000] | |
| keywords = kw_model.extract_keywords( | |
| text, | |
| keyphrase_ngram_range=(1, 3), | |
| stop_words='english', | |
| top_n=top_n | |
| ) | |
| return [kw[0] for kw in keywords] | |
| def calculate_similarity(text1, text2): | |
| emb1 = model.encode(text1, convert_to_tensor=True) | |
| emb2 = model.encode(text2, convert_to_tensor=True) | |
| score = util.pytorch_cos_sim( | |
| emb1, | |
| emb2 | |
| ).item() | |
| return round(score * 100, 2) | |
| # ========================================================= | |
| # INPUT SECTION | |
| # ========================================================= | |
| #st.markdown('<div class="upload-box">', unsafe_allow_html=True) | |
| st.markdown("### π Upload Resumes") | |
| resume_files = st.file_uploader( | |
| "Upload Multiple PDF Resumes", | |
| type=["pdf"], | |
| accept_multiple_files=True | |
| ) | |
| st.markdown("### π Paste Job Description") | |
| job_description = st.text_area( | |
| "Enter Job Description", | |
| height=220, | |
| placeholder="Paste the job description here..." | |
| ) | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| # ========================================================= | |
| # MATCH BUTTON | |
| # ========================================================= | |
| if st.button("π Match Resumes"): | |
| if resume_files and job_description: | |
| with st.spinner("Analyzing resumes..."): | |
| jd_skills = extract_skills_auto(job_description) | |
| results = [] | |
| progress_bar = st.progress(0) | |
| total_files = len(resume_files) | |
| # ========================================================= | |
| # PROCESS RESUMES | |
| # ========================================================= | |
| for idx, resume_file in enumerate(resume_files): | |
| try: | |
| resume_text = clean_text( | |
| extract_text_from_pdf(resume_file) | |
| ) | |
| resume_skills = extract_skills_auto( | |
| resume_text | |
| ) | |
| score = calculate_similarity( | |
| " ".join(resume_skills), | |
| " ".join(jd_skills) | |
| ) | |
| results.append({ | |
| "Resume": resume_file.name, | |
| "ATS Score": score, | |
| "Skills": resume_skills[:8] | |
| }) | |
| except Exception as e: | |
| results.append({ | |
| "Resume": resume_file.name, | |
| "ATS Score": 0, | |
| "Skills": [f"Error: {str(e)}"] | |
| }) | |
| progress_bar.progress((idx + 1) / total_files) | |
| # ========================================================= | |
| # SORT RESULTS | |
| # ========================================================= | |
| results = sorted( | |
| results, | |
| key=lambda x: x["ATS Score"], | |
| reverse=True | |
| ) | |
| # ========================================================= | |
| # TOP STATS | |
| # ========================================================= | |
| st.markdown("## π ATS Analytics") | |
| col1, col2, col3 = st.columns(3) | |
| with col1: | |
| st.metric( | |
| "Total Resumes", | |
| len(results) | |
| ) | |
| with col2: | |
| st.metric( | |
| "Top ATS Score", | |
| f"{results[0]['ATS Score']}%" | |
| ) | |
| with col3: | |
| avg_score = round( | |
| sum(r["ATS Score"] for r in results) / len(results), | |
| 2 | |
| ) | |
| st.metric( | |
| "Average Score", | |
| f"{avg_score}%" | |
| ) | |
| st.divider() | |
| # ========================================================= | |
| # TABLE | |
| # ========================================================= | |
| st.markdown("## π Candidate Rankings") | |
| table_data = [] | |
| for idx, item in enumerate(results, start=1): | |
| table_data.append({ | |
| "Rank": idx, | |
| "Resume": item["Resume"], | |
| "ATS Score": f"{item['ATS Score']}%" | |
| }) | |
| df = pd.DataFrame(table_data) | |
| st.dataframe( | |
| df, | |
| use_container_width=True | |
| ) | |
| # ========================================================= | |
| # TOP CANDIDATES | |
| # ========================================================= | |
| st.markdown("## β Best Candidates") | |
| top_candidates = results[:5] | |
| for idx, candidate in enumerate(top_candidates, start=1): | |
| score = candidate["ATS Score"] | |
| # Status | |
| if score >= 85: | |
| status = "Excellent Match β " | |
| elif score >= 65: | |
| status = "Good Match π" | |
| else: | |
| status = "Average Match β οΈ" | |
| # CARD | |
| st.markdown( | |
| '<div class="candidate-card">', | |
| unsafe_allow_html=True | |
| ) | |
| col1, col2 = st.columns([1, 5]) | |
| # LEFT | |
| with col1: | |
| st.metric( | |
| label=f"π Rank #{idx}", | |
| value=f"{score}%" | |
| ) | |
| # RIGHT | |
| with col2: | |
| st.markdown( | |
| f"### π {candidate['Resume']}" | |
| ) | |
| st.progress(score / 100) | |
| st.markdown( | |
| f"**Status:** {status}" | |
| ) | |
| st.markdown("#### π Matching Skills") | |
| skill_html = "" | |
| for skill in candidate["Skills"]: | |
| skill_html += f""" | |
| <span class="skill-tag"> | |
| {skill} | |
| </span> | |
| """ | |
| st.markdown( | |
| skill_html, | |
| unsafe_allow_html=True | |
| ) | |
| st.markdown( | |
| '</div>', | |
| unsafe_allow_html=True | |
| ) | |
| # ========================================================= | |
| # DOWNLOAD CSV | |
| # ========================================================= | |
| csv = pd.DataFrame(results).to_csv(index=False).encode("utf-8") | |
| st.download_button( | |
| label="π₯ Download ATS Report CSV", | |
| data=csv, | |
| file_name="ats_report.csv", | |
| mime="text/csv" | |
| ) | |
| else: | |
| st.warning( | |
| "β οΈ Please upload resumes and enter a job description." | |
| ) | |
| # ========================================================= | |
| # FOOTER | |
| # ========================================================= | |
| st.markdown(""" | |
| <br><br> | |
| <hr> | |
| <p style='text-align:center; color:gray;'> | |
| Developed by <b>Ashvinkumar Bari</b> β’ AI Resume Screening Platform | |
| </p> | |
| """, unsafe_allow_html=True) |