Spaces:
Sleeping
Sleeping
| # app.py | |
| import gradio as gr | |
| import pdfplumber | |
| import spacy | |
| import os | |
| import json | |
| import matplotlib.pyplot as plt | |
| import tempfile | |
| import re | |
| from wordcloud import WordCloud | |
| # Load NLP model | |
| try: | |
| nlp = spacy.load("en_core_web_sm") | |
| except: | |
| from spacy.cli import download | |
| download("en_core_web_sm") | |
| nlp = spacy.load("en_core_web_sm") | |
| # --- Helper functions --- | |
| def extract_text_from_pdf(file): | |
| with pdfplumber.open(file) as pdf: | |
| text = "\n".join([page.extract_text() or "" for page in pdf.pages]) | |
| return clean_text(text) | |
| def clean_text(text): | |
| import re | |
| text = re.sub(r'\n+', '\n', text) | |
| text = re.sub(r'[^\x00-\x7F]+', ' ', text) # remove non-ASCII | |
| text = re.sub(r'\s+', ' ', text) # collapse multiple spaces | |
| return text.strip() | |
| def extract_contact_info(text): | |
| email = re.search(r"[\w.+-]+@[\w-]+\.[\w.-]+", text) | |
| phone = re.search(r"\b\d{4}[-.\s]?\d{7}\b|\b\d{11}\b", text) | |
| return email.group(0) if email else None, phone.group(0) if phone else None | |
| def extract_skills(text): | |
| words = set(re.findall(r"\b\w+\b", text.lower())) | |
| custom_keywords = re.findall(r"(?i)(?<=skills[:\-\n]).+?(?=education|experience|projects|certification|$)", text, re.DOTALL) | |
| skill_keywords = [ | |
| "python", "sql", "ml", "ai", "keras", "tensorflow", "html", "css", "nltk", | |
| "pandas", "numpy", "power bi", "excel", "scikit-learn", "seaborn", "flask", "django" | |
| ] | |
| found = {k for k in skill_keywords if k in words} | |
| for chunk in custom_keywords: | |
| found.update([s.strip().lower() for s in re.split(r",|\n|\||\s{2,}", chunk) if len(s.strip()) >= 2]) | |
| return sorted(set(found)) | |
| def extract_section(text, section_titles): | |
| lines = text.split("\n") | |
| section_data = [] | |
| capture = False | |
| for line in lines: | |
| clean_line = line.strip() | |
| if not clean_line: | |
| continue | |
| # Detect section start | |
| if any(title.lower() in clean_line.lower() for title in section_titles): | |
| capture = True | |
| continue | |
| # Stop if a new main section starts | |
| if capture and re.match(r"^(skills|certifications|projects|summary|languages|contact|references|achievements|publications)\s*[:\-]?$", clean_line, re.IGNORECASE): | |
| break | |
| # Collect section lines | |
| if capture: | |
| section_data.append(clean_line) | |
| return section_data | |
| def match_job_description(skills, job_desc): | |
| job_desc = job_desc.lower() | |
| matched_skills = [s for s in skills if s.lower() in job_desc] | |
| return matched_skills, len(matched_skills), len(skills) | |
| def visualize_skills(skills): | |
| if not skills: | |
| return None | |
| plt.figure(figsize=(6, 4)) | |
| freq = {s: skills.count(s) for s in skills} | |
| plt.bar(freq.keys(), freq.values(), color="skyblue") | |
| plt.xticks(rotation=45) | |
| plt.title("Skills Frequency") | |
| plt.tight_layout() | |
| temp_chart = tempfile.NamedTemporaryFile(delete=False, suffix=".png") | |
| plt.savefig(temp_chart.name) | |
| return temp_chart.name | |
| def parse_resume(file): | |
| text = extract_text_from_pdf(file.name) | |
| doc = nlp(text) | |
| name = next((ent.text for ent in doc.ents if ent.label_ == "PERSON"), "Not Found") | |
| email, phone = extract_contact_info(text) | |
| skills = extract_skills(text) | |
| education = extract_section(text, ["education", "academic background", "qualification", "university", "degree", "bachelor", "master"]) | |
| experience = extract_section(text, ["experience", "professional experience", "work history", "employment", "internship", "projects"]) | |
| print("=== DEBUG TEXT ===") | |
| print(text) | |
| parsed = { | |
| "Name": name, | |
| "Email": email, | |
| "Phone": phone, | |
| "Skills": skills, | |
| "Education": education, | |
| "Experience": experience, | |
| } | |
| chart = visualize_skills(skills) | |
| return parsed, chart | |
| def download_json(data): | |
| with tempfile.NamedTemporaryFile(delete=False, suffix=".json", mode="w") as f: | |
| json.dump(data, f, indent=4) | |
| return f.name | |
| def job_matcher(resume_json, job_desc): | |
| skills = resume_json.get("Skills", []) | |
| matched_skills, matched_count, total_skills = match_job_description(skills, job_desc) | |
| return f"β Matched Skills: {matched_skills}\nπ― Match Score: {matched_count} / {total_skills}" | |
| # --- Gradio Interface --- | |
| with gr.Blocks(title="π§ AI Resume Parser") as demo: | |
| gr.Markdown(""" | |
| # π§ AI Resume Parser (with Skills Chart, JSON Download, and Job Match) | |
| Upload a resume and extract structured data with NLP | |
| """) | |
| with gr.Row(): | |
| resume_input = gr.File(label="Upload Resume (PDF)", file_types=[".pdf"]) | |
| parse_btn = gr.Button("π Parse Resume") | |
| parsed_output = gr.JSON(label="π§Ύ Parsed Resume") | |
| chart_output = gr.Image(label="π Skills Chart") | |
| download_btn = gr.File(label="β¬οΈ Download Resume JSON") | |
| with gr.Accordion("π Match with Job Description", open=False): | |
| job_desc_input = gr.Textbox(label="Paste Job Description", lines=5) | |
| match_btn = gr.Button("π Match Skills") | |
| match_output = gr.Text(label="Match Result") | |
| def full_pipeline(file): | |
| parsed, chart_path = parse_resume(file) | |
| json_path = download_json(parsed) | |
| return parsed, chart_path, json_path | |
| parse_btn.click(fn=full_pipeline, inputs=[resume_input], outputs=[parsed_output, chart_output, download_btn]) | |
| match_btn.click(fn=job_matcher, inputs=[parsed_output, job_desc_input], outputs=[match_output]) | |
| if __name__ == "__main__": | |
| demo.launch() |