DEV Community

Cover image for Build a Resume Parser & Job Matcher API with FastAPI and NLP
Femi Omoshona
Femi Omoshona

Posted on

Build a Resume Parser & Job Matcher API with FastAPI and NLP

In today’s competitive talent market, recruiters often sift through hundreds of resumes for a single role. Manually comparing each resume against a job description is tedious and inconsistent.

In this article, we’ll build a Resume Parser & Job Matcher a REST API that:

Extracts text from PDF, DOCX, or TXT resumes.
Compares the resume against one or more job descriptions.
Returns a match score (0–100) based on cosine similarity and skill overlap.

We’ll use FastAPI for the web framework, spaCy for text preprocessing, scikit‑learn for TF‑IDF vectorisation, and pdfplumber / python-docx for file parsing.

** What You’ll Learn**

  • How to parse text from different resume formats.
  • How to clean and tokenise text using spaCy.
  • How to compute similarity between documents with TF‑IDF.
  • How to combine multiple heuristics (keyword overlap) into a final score.
  • How to expose everything as a modern, self‑documenting API.

Prerequisites

  • Python 3.9+ installed
  • Basic familiarity with Python and virtual environments
  • A tool to test APIs (cURL, Postman, or just the Swagger UI)

Step 1 – Project Setup
Create a new directory and set up a virtual environment:

# Windows
mkdir resume parser job matcher 
cd resume parser job matcher
python -m venv venv
source venv/bin/activate      # On Windows: venv\Scripts\activate

# macOS / Linux
python3 -m venv venv
source venv/bin/activate
Enter fullscreen mode Exit fullscreen mode

Create a requirements.txt file:
fastapi==0.104.1
uvicorn==0.24.0
python-multipart==0.0.6
pdfplumber==0.10.3
python-docx==1.1.0
spacy==3.7.2
scikit-learn==1.3.2
pydantic==2.5.0

Install everything:

pip install -r requirements.txt

Pip install https://github.com/explosion/spacy-models/releases/download/zh_core_web_trf-3.8.0/zh_core_web_trf-3.8.0.tar.gz                                      
#!/bin/bash

Enter fullscreen mode Exit fullscreen mode

Step 2 – Project Structure
We’ll organise the code like this:

RESUME PARSER JOB MATCHER/
├── utils/
│ ├──init.py
│ ├── generator.py
│ ├── parser.py
│ ├── matcher.py
│ └── skills.py
├── uploads/ # (created automatically)
├── templates/
├── index.html
├── requirements.txt
└── app.py

Step 3 – Resume Parser (utils/parser.py)
This module extracts plain text from uploaded files.

import os
import re
from typing import Dict, Any, Optional
from datetime import datetime
import PyPDF2
import pdfplumber
from docx import Document
from dateutil import parser as date_parser
import spacy

class ResumeParser:
    def __init__(self):
        """Initialize resume parser"""
        try:
            self.nlp = spacy.load("en_core_web_sm")
        except:
            import subprocess
            subprocess.run(["python", "-m", "spacy", "download", "en_core_web_sm"])
            self.nlp = spacy.load("en_core_web_sm")

        # Email pattern
        self.email_pattern = r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b'

        # Phone pattern
        self.phone_pattern = r'(\+?\d{1,3}[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}'

        # Name patterns (simple approach - first proper noun at beginning)
        self.name_patterns = [
            r'^([A-Z][a-z]+(?:\s+[A-Z][a-z]+)+)',  # Capitalized words at start
            r'(?:name|full name)[:\s]*([A-Z][a-z]+(?:\s+[A-Z][a-z]+)+)',
        ]

        # Education patterns
        self.education_keywords = [
            'bachelor', 'master', 'phd', 'doctorate', 'b.tech', 'm.tech',
            'b.e.', 'm.e.', 'b.sc', 'm.sc', 'b.a.', 'm.a.', 'b.b.a.', 'm.b.a.',
            'high school', 'diploma', 'associate degree'
        ]

        # Experience patterns
        self.experience_patterns = [
            r'(\d+)\+?\s*(?:years?|yrs?)',
            r'experience\s*(?:of|:)?\s*(\d+)\+?\s*(?:years?|yrs?)',
        ]

    def parse_pdf(self, file_path: str) -> str:
        """Extract text from PDF file"""
        text = ""
        try:
            # Try pdfplumber first (better for tables)
            with pdfplumber.open(file_path) as pdf:
                for page in pdf.pages:
                    page_text = page.extract_text()
                    if page_text:
                        text += page_text + "\n"

            # Fallback to PyPDF2 if pdfplumber fails
            if not text.strip():
                with open(file_path, 'rb') as file:
                    pdf_reader = PyPDF2.PdfReader(file)
                    for page in pdf_reader.pages:
                        text += page.extract_text() + "\n"
        except Exception as e:
            print(f"Error parsing PDF: {e}")

        return text

    def parse_docx(self, file_path: str) -> str:
        """Extract text from DOCX file"""
        text = ""
        try:
            doc = Document(file_path)
            for paragraph in doc.paragraphs:
                text += paragraph.text + "\n"
        except Exception as e:
            print(f"Error parsing DOCX: {e}")

        return text

    def extract_contact_info(self, text: str) -> Dict[str, Optional[str]]:
        """Extract email, phone, and name from text"""
        contact = {
            'email': None,
            'phone': None,
            'name': None
        }

        # Extract email
        email_match = re.search(self.email_pattern, text)
        if email_match:
            contact['email'] = email_match.group()

        # Extract phone
        phone_match = re.search(self.phone_pattern, text)
        if phone_match:
            contact['phone'] = phone_match.group().strip()

        # Extract name (simplified)
        lines = text.split('\n')
        for i, line in enumerate(lines[:5]):  # Check first 5 lines
            line = line.strip()
            # Name is usually at the top, not too long, with proper capitalization
            if line and len(line.split()) <= 4 and line[0].isupper():
                # Check if it contains any common resume section headers
                if not any(header in line.lower() for header in ['resume', 'cv', 'curriculum', 'vitae', 'email', 'phone']):
                    # Check if each word starts with capital
                    words = line.split()
                    if all(word[0].isupper() for word in words if word):
                        contact['name'] = line
                        break

        return contact

    def extract_education(self, text: str) -> list:
        """Extract education information"""
        education = []
        lines = text.split('\n')

        for i, line in enumerate(lines):
            line_lower = line.lower()
            # Check if line contains education keywords
            if any(keyword in line_lower for keyword in self.education_keywords):
                edu_entry = {
                    'degree': line.strip(),
                    'institution': None,
                    'year': None
                }

                # Look for institution in surrounding lines
                for j in range(max(0, i-2), min(len(lines), i+3)):
                    if j != i and len(lines[j].strip()) > 5:
                        if not any(keyword in lines[j].lower() for keyword in self.education_keywords):
                            edu_entry['institution'] = lines[j].strip()
                            break

                # Look for year
                year_match = re.search(r'(19|20)\d{2}', line)
                if year_match:
                    edu_entry['year'] = year_match.group()

                education.append(edu_entry)

        return education

    def extract_experience(self, text: str) -> Dict[str, Any]:
        """Extract work experience summary"""
        exp_info = {
            'years_of_experience': 0,
            'companies': [],
            'titles': [],
            'summary': ''
        }

        # Extract years of experience
        for pattern in self.experience_patterns:
            match = re.search(pattern, text.lower())
            if match:
                exp_info['years_of_experience'] = int(match.group(1))
                break

        # Extract job titles and companies (simplified)
        lines = text.split('\n')
        job_title_pattern = r'([A-Z][a-z]+(?:\s+[A-Z][a-z]+)*)\s+(?:at|@|-)\s+([A-Z][a-zA-Z\s&]+)'

        for line in lines:
            match = re.search(job_title_pattern, line)
            if match:
                exp_info['titles'].append(match.group(1))
                exp_info['companies'].append(match.group(2).strip())

        return exp_info

    def parse_resume(self, file_path: str) -> Dict[str, Any]:
        """
        Parse resume and extract all information
        """
        # Extract text based on file type
        if file_path.lower().endswith('.pdf'):
            text = self.parse_pdf(file_path)
        elif file_path.lower().endswith('.docx'):
            text = self.parse_docx(file_path)
        else:
            raise ValueError("Unsupported file format. Please use PDF or DOCX.")

        if not text:
            raise ValueError("Could not extract text from file")

        # Extract all information
        result = {
            'file_name': os.path.basename(file_path),
            'full_text': text,
            'contact': self.extract_contact_info(text),
            'education': self.extract_education(text),
            'experience': self.extract_experience(text),
            'raw_text': text  # Keep raw text for skill extraction
        }

        return result
Enter fullscreen mode Exit fullscreen mode

Step 4 – Job Matcher (utils/matcher.py)
This file implements a Job Matching Engine that compares a candidate’s resume with a job description and produces a match score (0–100). It uses multiple weighted criteria to give a balanced evaluation.

Main Purpose
Read structured resume data (skills, experience, education, full text) and job data.

Calculate four independent match scores:

Skill match (50% weight) – compares required skills vs. resume skills.
Experience match (20%) – compares years of experience.
Education match (15%) – compares degree level.
Semantic match (15%) – compares the full‑text using TF‑IDF + cosine similarity.

Combine them into a total score.

Generate recommendations for the candidate (e.g., which skills are missing, how to improve).

import numpy as np
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
from typing import Dict, List, Tuple, Any
import re
from .skills import SkillExtractor

class JobMatcher:
    def __init__(self):
        """Initialize job matcher"""
        self.skill_extractor = SkillExtractor()
        self.tfidf_vectorizer = TfidfVectorizer(
            max_features=1000,
            stop_words='english',
            ngram_range=(1, 2)
        )

        # Weights for different matching criteria
        self.weights = {
            'skill_match': 0.5,
            'experience_match': 0.2,
            'education_match': 0.15,
            'semantic_match': 0.15
        }

    def calculate_skill_match(self, resume_skills: Dict, job_skills: Dict) -> Dict[str, Any]:
        """
        Calculate skill match percentage between resume and job
        """
        # Flatten skills lists
        resume_skill_list = [skill for skills in resume_skills.values() for skill in skills]
        job_skill_list = [skill for skills in job_skills.values() for skill in skills]

        # Calculate matches
        matched_skills = []
        missing_skills = []

        for job_skill in job_skill_list:
            if any(self.skill_similarity(job_skill, res_skill) > 0.8 for res_skill in resume_skill_list):
                matched_skills.append(job_skill)
            else:
                missing_skills.append(job_skill)

        # Calculate scores by category
        category_scores = {}
        for category in resume_skills.keys():
            res_cat_skills = resume_skills[category]
            job_cat_skills = job_skills[category]

            if job_cat_skills:
                matched_cat = [s for s in job_cat_skills if s in res_cat_skills]
                category_scores[category] = len(matched_cat) / len(job_cat_skills)
            else:
                category_scores[category] = 1.0

        # Overall skill match score
        if job_skill_list:
            skill_match_score = len(matched_skills) / len(job_skill_list)
        else:
            skill_match_score = 0.0

        return {
            'score': skill_match_score,
            'matched_skills': matched_skills,
            'missing_skills': missing_skills,
            'category_scores': category_scores
        }

    def skill_similarity(self, skill1: str, skill2: str) -> float:
        """
        Calculate similarity between two skills (handles variations)
        """
        skill1 = skill1.lower().strip()
        skill2 = skill2.lower().strip()

        if skill1 == skill2:
            return 1.0

        # Check if one is substring of the other
        if skill1 in skill2 or skill2 in skill1:
            return 0.9

        # Check word overlap
        words1 = set(skill1.split())
        words2 = set(skill2.split())

        if words1 and words2:
            intersection = words1.intersection(words2)
            if intersection:
                return len(intersection) / max(len(words1), len(words2))

        return 0.0

    def calculate_experience_match(self, resume_exp: Dict, job_requirements: Dict) -> float:
        """
        Calculate experience match score
        """
        resume_years = resume_exp.get('years_of_experience', 0)
        required_years = job_requirements.get('required_experience', 0)

        if required_years == 0:
            return 1.0

        if resume_years >= required_years:
            return 1.0
        else:
            return resume_years / required_years

    def calculate_education_match(self, resume_education: List, job_requirements: Dict) -> float:
        """
        Calculate education match score
        """
        required_degree = job_requirements.get('required_education', '').lower()

        if not required_degree:
            return 1.0

        if not resume_education:
            return 0.0

        # Define education levels
        edu_levels = {
            'phd': 5,
            'doctorate': 5,
            'master': 4,
            'mba': 4,
            'bachelor': 3,
            'associate': 2,
            'diploma': 1,
            'high school': 0
        }

        # Find highest education level in resume
        resume_level = 0
        for edu in resume_education:
            edu_text = edu.get('degree', '').lower()
            for level_name, level_value in edu_levels.items():
                if level_name in edu_text and level_value > resume_level:
                    resume_level = level_value

        # Get required education level
        required_level = 0
        for level_name, level_value in edu_levels.items():
            if level_name in required_degree:
                required_level = level_value
                break

        if resume_level >= required_level:
            return 1.0
        else:
            return resume_level / required_level if required_level > 0 else 0.5

    def calculate_semantic_match(self, resume_text: str, job_text: str) -> float:
        """
        Calculate semantic similarity between resume and job description
        """
        try:
            # Use TF-IDF and cosine similarity
            texts = [resume_text, job_text]
            tfidf_matrix = self.tfidf_vectorizer.fit_transform(texts)
            similarity = cosine_similarity(tfidf_matrix[0:1], tfidf_matrix[1:2])[0][0]
            return float(similarity)
        except:
            return 0.0

    def match_resume_to_job(self, resume_data: Dict, job_data: Dict) -> Dict[str, Any]:
        """
        Match resume to job and return detailed scores
        """
        # Extract skills
        resume_skills = resume_data.get('skills', {})
        job_skills = job_data.get('skills', {})

        # Calculate individual scores
        skill_match = self.calculate_skill_match(resume_skills, job_skills)
        experience_match = self.calculate_experience_match(
            resume_data.get('experience', {}),
            job_data.get('requirements', {})
        )
        education_match = self.calculate_education_match(
            resume_data.get('education', []),
            job_data.get('requirements', {})
        )
        semantic_match = self.calculate_semantic_match(
            resume_data.get('full_text', ''),
            job_data.get('description', '')
        )

        # Calculate weighted total score
        total_score = (
            self.weights['skill_match'] * skill_match['score'] +
            self.weights['experience_match'] * experience_match +
            self.weights['education_match'] * education_match +
            self.weights['semantic_match'] * semantic_match
        )

        # Generate recommendations
        recommendations = self.generate_recommendations(
            skill_match['missing_skills'],
            experience_match,
            education_match
        )

        return {
            'total_score': round(total_score * 100, 2),
            'skill_match': {
                'score': round(skill_match['score'] * 100, 2),
                'matched_skills': skill_match['matched_skills'],
                'missing_skills': skill_match['missing_skills']
            },
            'experience_match': round(experience_match * 100, 2),
            'education_match': round(education_match * 100, 2),
            'semantic_match': round(semantic_match * 100, 2),
            'recommendations': recommendations,
            'breakdown': {
                'skill_weight': self.weights['skill_match'],
                'experience_weight': self.weights['experience_match'],
                'education_weight': self.weights['education_match'],
                'semantic_weight': self.weights['semantic_match']
            }
        }

    def generate_recommendations(self, missing_skills: List[str], 
                                exp_score: float, edu_score: float) -> List[str]:
        """
        Generate recommendations based on missing qualifications
        """
        recommendations = []

        if missing_skills:
            if len(missing_skills) <= 3:
                skills_str = ', '.join(missing_skills)
                recommendations.append(f"Add these key skills to your resume: {skills_str}")
            else:
                recommendations.append(f"Consider gaining experience in: {', '.join(missing_skills[:5])}")
                if len(missing_skills) > 5:
                    recommendations.append(f"And {len(missing_skills) - 5} other skills")

        if exp_score < 0.7:
            recommendations.append("Highlight your relevant work experience more prominently")

        if edu_score < 0.7:
            recommendations.append("Consider adding relevant certifications or courses")

        if not recommendations:
            recommendations.append("Your profile matches well with this position!")

        return recommendations
Enter fullscreen mode Exit fullscreen mode

Step 5 – Generator (utils/generator.py)
This file is a data generation utility – it creates synthetic (fake) resumes and job descriptions that look realistic. It’s designed to help developers test a resume parser / job matcher without needing real, sensitive candidate data.

import random
import json
import os
from datetime import datetime, timedelta
from typing import List, Dict

class SampleDataGenerator:
    def __init__(self):
        """Initialize sample data generator"""
        self.first_names = [
            "James", "Mary", "John", "Patricia", "Robert", "Jennifer", 
            "Michael", "Linda", "William", "Elizabeth", "David", "Susan",
            "Richard", "Jessica", "Joseph", "Sarah", "Thomas", "Karen",
            "Charles", "Nancy", "Christopher", "Lisa", "Daniel", "Betty"
        ]

        self.last_names = [
            "Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia",
            "Miller", "Davis", "Rodriguez", "Martinez", "Hernandez", "Lopez",
            "Gonzalez", "Wilson", "Anderson", "Thomas", "Taylor", "Moore",
            "Jackson", "Martin", "Lee", "Perez", "Thompson", "White"
        ]

        self.companies = [
            "Tech Solutions Inc.", "DataCorp", "InnovateSoft", "CloudNine",
            "AI Dynamics", "WebCraft", "AppGenius", "SecureNet", "DevOps Pro",
            "Analytics Plus", "Smart Systems", "Digital Edge", "Future Tech",
            "Global IT Solutions", "NextGen Software", "Prime Technologies"
        ]

        self.job_titles = [
            "Software Engineer", "Data Scientist", "DevOps Engineer",
            "Frontend Developer", "Backend Developer", "Full Stack Developer",
            "Machine Learning Engineer", "Cloud Architect", "Product Manager",
            "Project Manager", "Business Analyst", "QA Engineer",
            "Systems Administrator", "Network Engineer", "Security Analyst"
        ]

        self.universities = [
            "Stanford University", "MIT", "University of California",
            "Carnegie Mellon", "University of Washington", "Georgia Tech",
            "University of Texas", "Purdue University", "NYU", "Columbia University",
            "University of Michigan", "Cornell University", "Harvard University"
        ]

        self.degree_levels = [
            "Bachelor of Science in", "Master of Science in", 
            "Bachelor of Arts in", "PhD in", "Associate Degree in"
        ]

        self.majors = [
            "Computer Science", "Information Technology", "Data Science",
            "Software Engineering", "Electrical Engineering", "Mathematics",
            "Statistics", "Business Administration", "Information Systems"
        ]

        self.skill_descriptions = {
            "programming_languages": [
                "Proficient in", "Experienced with", "Working knowledge of",
                "Expert in", "Skilled in"
            ],
            "frameworks_libraries": [
                "Extensive experience with", "Built applications using",
                "Developed projects with", "Proficient in"
            ],
            "databases": [
                "Experience with", "Designed and implemented",
                "Managed", "Optimized queries in"
            ],
            "cloud_devops": [
                "Deployed applications on", "Managed infrastructure in",
                "Implemented CI/CD with", "Certified in"
            ]
        }

    def generate_resume_text(self, name: str, title: str) -> str:
        """Generate realistic resume text"""
        skills_db = {
            "programming_languages": ["Python", "Java", "JavaScript", "C++", "Ruby", "Go", "Rust", "TypeScript"],
            "frameworks_libraries": ["Django", "React", "Spring", "TensorFlow", "PyTorch", "Flask", "Node.js"],
            "databases": ["PostgreSQL", "MongoDB", "MySQL", "Redis", "Elasticsearch"],
            "cloud_devops": ["AWS", "Docker", "Kubernetes", "Jenkins", "Terraform", "Azure"]
        }

        # Select random skills
        selected_skills = {}
        for category, skills in skills_db.items():
            num_skills = random.randint(3, 6)
            selected_skills[category] = random.sample(skills, min(num_skills, len(skills)))

        # Generate experience
        num_jobs = random.randint(2, 4)
        total_years = 0
        experience_section = ""

        for i in range(num_jobs):
            company = random.choice(self.companies)
            years = random.randint(1, 4)
            total_years += years

            if i == 0:
                role = title
            else:
                role = random.choice(self.job_titles)

            experience_section += f"""
{role}
{company} | {years} years
• Developed and maintained applications using {', '.join(selected_skills['programming_languages'][:2])}
• Implemented {random.choice(selected_skills['frameworks_libraries'])} for various projects
• Collaborated with cross-functional teams to deliver high-quality software
• {random.choice(['Optimized', 'Improved', 'Enhanced'])} system performance by {random.randint(20, 40)}%
• Managed {random.choice(['database', 'cloud', 'infrastructure'])} using {', '.join(selected_skills['databases'][:1] + selected_skills['cloud_devops'][:1])}
"""

        # Generate education
        education = []
        edu_level = random.choice(self.degree_levels)
        major = random.choice(self.majors)
        uni = random.choice(self.universities)
        grad_year = random.randint(2015, 2023)
        education.append(f"{edu_level} {major}, {uni} - {grad_year}")

        if random.random() > 0.7:  # 30% chance of advanced degree
            edu_level = random.choice(["Master of Science in", "PhD in"])
            major = random.choice(self.majors)
            uni = random.choice(self.universities)
            grad_year = random.randint(2018, 2024)
            education.append(f"{edu_level} {major}, {uni} - {grad_year}")

        # Generate skills section
        skills_section = "\nTECHNICAL SKILLS\n"
        for category, skills in selected_skills.items():
            skills_section += f"{category.replace('_', ' ').title()}: {', '.join(skills)}\n"

        # Generate projects section
        projects_section = "\nPROJECTS\n"
        for i in range(random.randint(2, 4)):
            tech_stack = random.sample(selected_skills['programming_languages'], 2) + \
                        random.sample(selected_skills['frameworks_libraries'], 1)
            projects_section += f"• Project {i+1}: Built using {', '.join(tech_stack)}\n"
            projects_section += f"  {random.choice(['Developed', 'Created', 'Designed'])} a {random.choice(['web app', 'mobile app', 'API', 'data pipeline'])} for {random.choice(['internal use', 'clients', 'open source'])}\n"

        # Generate certifications (optional)
        certs_section = ""
        if random.random() > 0.5:
            certs = random.sample(["AWS Certified", "Azure Certified", "Google Cloud Certified", "Scrum Master", "PMP"], 2)
            certs_section = "\nCERTIFICATIONS\n"
            for cert in certs:
                certs_section += f"{cert}\n"

        # Complete resume text
        resume_text = f"""
{name}
{random.choice(['email@example.com', name.lower().replace(' ', '.') + '@email.com'])} | {random.choice(['123-456-7890', '555-123-4567'])} | {random.choice(['San Francisco, CA', 'New York, NY', 'Seattle, WA', 'Austin, TX'])}

PROFESSIONAL SUMMARY
Experienced {title} with {total_years}+ years of experience in software development. 
Proven track record of delivering high-quality solutions using modern technologies.

EXPERIENCE
{experience_section}

EDUCATION
{chr(10).join(['' + edu for edu in education])}

{certs_section}

{skills_section}

{projects_section}
"""

        return resume_text

    def generate_job_description(self) -> Dict:
        """Generate a realistic job description"""
        title = random.choice(self.job_titles)
        company = random.choice(self.companies)

        skills_db = {
            "programming_languages": ["Python", "Java", "JavaScript", "C++", "Go", "TypeScript"],
            "frameworks_libraries": ["Django", "React", "Spring", "TensorFlow", "Node.js", "Angular"],
            "databases": ["PostgreSQL", "MongoDB", "MySQL", "Redis"],
            "cloud_devops": ["AWS", "Docker", "Kubernetes", "Jenkins", "Git"],
            "soft_skills": ["Communication", "Teamwork", "Problem Solving", "Leadership"],
            "project_management": ["Agile", "Scrum", "JIRA"],
            "data_science": ["Machine Learning", "Data Analysis", "Statistics"]
        }

        # Select required skills
        required_skills = {}
        for category, skills in skills_db.items():
            num_skills = random.randint(2, 5)
            required_skills[category] = random.sample(skills, min(num_skills, len(skills)))

        # Generate requirements
        exp_years = random.choice([2, 3, 5, 5, 7, 10])
        edu_level = random.choice(["Bachelor's degree", "Master's degree"])

        job_text = f"""
Job Title: {title}
Company: {company}
Location: {random.choice(['Remote', 'Hybrid', 'On-site'])} - {random.choice(['San Francisco, CA', 'New York, NY', 'Austin, TX'])}

About the Role:
We are seeking an experienced {title} to join our growing team. 
The ideal candidate will have strong technical skills and a passion for building scalable solutions.

Responsibilities:
• Design and develop high-quality software solutions
• Collaborate with cross-functional teams to deliver features
• Write clean, maintainable, and efficient code
• Participate in code reviews and technical discussions
• Troubleshoot and debug applications
• Stay up-to-date with emerging technologies

Required Qualifications:
• {exp_years}+ years of experience in software development
• {edu_level} in Computer Science or related field
• Strong proficiency in {', '.join(required_skills['programming_languages'][:3])}
• Experience with {', '.join(required_skills['frameworks_libraries'][:2])}
• Knowledge of {', '.join(required_skills['databases'][:2])}
• Familiarity with {', '.join(required_skills['cloud_devops'][:2])}{', '.join(required_skills['soft_skills'][:3])} skills
• Experience with {', '.join(required_skills['project_management'][:2])} methodologies

Preferred Qualifications:
• Master's degree preferred
• Experience with {random.choice(required_skills.get('data_science', ['Machine Learning']))}{random.choice(['AWS', 'Azure', 'Google Cloud'])} certification
• Open source contributions

Benefits:
• Competitive salary and equity
• Health, dental, and vision insurance
• 401(k) matching
• Flexible work hours
• Professional development budget
• Generous PTO
"""

        job_data = {
            'title': title,
            'company': company,
            'description': job_text,
            'skills': required_skills,
            'requirements': {
                'required_experience': exp_years,
                'required_education': edu_level,
                'location': random.choice(['Remote', 'Hybrid', 'On-site'])
            }
        }

        return job_data

    def generate_sample_resumes(self, num_resumes: int = 5) -> List[Dict]:
        """Generate multiple sample resumes"""
        resumes = []

        for i in range(num_resumes):
            name = f"{random.choice(self.first_names)} {random.choice(self.last_names)}"
            title = random.choice(self.job_titles)

            resume_data = {
                'file_name': f"sample_resume_{i+1}.txt",
                'name': name,
                'title': title,
                'text': self.generate_resume_text(name, title)
            }

            resumes.append(resume_data)

        return resumes

    def generate_sample_jobs(self, num_jobs: int = 5) -> List[Dict]:
        """Generate multiple sample job descriptions"""
        jobs = []

        for i in range(num_jobs):
            job_data = self.generate_job_description()
            job_data['file_name'] = f"sample_job_{i+1}.txt"
            jobs.append(job_data)

        return jobs

    def save_sample_data(self, directory: str = 'data'):
        """Save sample data to files"""
        # Create directory if it doesn't exist
        os.makedirs(os.path.join(directory, 'sample_resumes'), exist_ok=True)
        os.makedirs(os.path.join(directory, 'sample_jobs'), exist_ok=True)

        # Generate and save resumes
        resumes = self.generate_sample_resumes(5)
        for i, resume in enumerate(resumes):
            file_path = os.path.join(directory, 'sample_resumes', f"resume_{i+1}.txt")
            with open(file_path, 'w') as f:
                f.write(resume['text'])

            # Also save metadata
            meta_path = os.path.join(directory, 'sample_resumes', f"resume_{i+1}_meta.json")
            with open(meta_path, 'w') as f:
                json.dump({
                    'name': resume['name'],
                    'title': resume['title']
                }, f, indent=2)

        # Generate and save jobs
        jobs = self.generate_sample_jobs(5)
        for i, job in enumerate(jobs):
            file_path = os.path.join(directory, 'sample_jobs', f"job_{i+1}.txt")
            with open(file_path, 'w') as f:
                f.write(job['description'])

            # Also save metadata
            meta_path = os.path.join(directory, 'sample_jobs', f"job_{i+1}_meta.json")
            with open(meta_path, 'w') as f:
                json.dump({
                    'title': job['title'],
                    'company': job['company'],
                    'requirements': job['requirements']
                }, f, indent=2)

        print(f"Generated {len(resumes)} sample resumes and {len(jobs)} sample jobs in {directory}/")
Enter fullscreen mode Exit fullscreen mode

Step 6 – Skills (utils/skills.py)
It implements a skill extraction engine that analyses unstructured text (e.g., resumes, job descriptions) and extracts professional skills, categorising them into groups like programming languages, frameworks, databases, cloud tools, soft skills, etc.
Initialization (init)
When a SkillExtractor object is created:

Loads spaCy model – en_core_web_sm is loaded. If not present, it automatically downloads it.

Builds a skill database – A dictionary skills_db containing 8 categories, each with a list of relevant skills.
Examples:

`programming_languages: python, java, c++,

frameworks_libraries: django, react, tensorflow,

databases: mysql, mongodb, redis,

cloud_devops: aws, docker, kubernetes, git,

soft_skills: communication, leadership, problem solving,

project_management: agile, scrum, jira,

data_science: machine learning, nlp, computer vision,

certifications: aws certified, pmp, scrum master,
`
Flattens all skills into self.all_skills for fast searching.

Defines regex patterns to capture skill‑like phrases (e.g., “proficient in Python”, “Skills: Java, SQL”).

  1. extract_skills(text: str) -> Dict[str, List[str]] This method takes a plain

Step 7 – init (utils/init.py)

# Make utils a Python package
from .parser import ResumeParser
from .skills import SkillExtractor
from .matcher import JobMatcher
from .generator import SampleDataGenerator

__all__ = ['ResumeParser', 'SkillExtractor', 'JobMatcher', 'SampleDataGenerator']
Enter fullscreen mode Exit fullscreen mode

Step 8 – app (app.py)
This file is the main backend of a web application that allows users to:

`Upload a resume (PDF/DOCX)

Enter a job description manually

Get a match score between the resume and the job
`
It uses Flask as the web framework, stores temporary data in server-side sessions, and relies on several custom utility modules (parser, skills, matcher, generator) to handle the core logic.

from flask import Flask, render_template, request, jsonify, session
import os
import json
import tempfile
from werkzeug.utils import secure_filename
from utils.parser import ResumeParser
from utils.skills import SkillExtractor
from utils.matcher import JobMatcher
from utils.generator import SampleDataGenerator

app = Flask(__name__)
app.secret_key = '664fcd6b37618f5da8eab5c65598c9cd99c73b08442005ad3eb7e7d81c204ebf'  # Change this in production
app.config['UPLOAD_FOLDER'] = 'uploads'
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024  # 16MB max file size

# Allowed file extensions
ALLOWED_EXTENSIONS = {'pdf', 'docx'}

# Initialize components
resume_parser = ResumeParser()
skill_extractor = SkillExtractor()
job_matcher = JobMatcher()
data_generator = SampleDataGenerator()

# Ensure upload folder exists
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)

def allowed_file(filename):
    """Check if file extension is allowed"""
    return '.' in filename and \
           filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

@app.route('/')
def index():
    """Home page"""
    return render_template('index.html')

@app.route('/upload_resume', methods=['POST'])
def upload_resume():
    """Upload and parse resume"""
    if 'resume' not in request.files:
        return jsonify({'error': 'No file uploaded'}), 400

    file = request.files['resume']

    if file.filename == '':
        return jsonify({'error': 'No file selected'}), 400

    if not allowed_file(file.filename):
        return jsonify({'error': 'File type not allowed. Please upload PDF or DOCX'}), 400

    try:
        # Save file temporarily
        filename = secure_filename(file.filename)
        filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
        file.save(filepath)

        # Parse resume
        resume_data = resume_parser.parse_resume(filepath)

        # Extract skills
        skills = skill_extractor.extract_skills(resume_data['full_text'])
        resume_data['skills'] = skills
        resume_data['skill_summary'] = skill_extractor.get_skill_summary(skills)

        # Store in session (simplified - in production use database)
        session['resume_data'] = resume_data

        # Clean up
        os.remove(filepath)

        return jsonify({
            'success': True,
            'resume_data': {
                'name': resume_data['contact']['name'],
                'email': resume_data['contact']['email'],
                'skills_summary': resume_data['skill_summary'],
                'experience': resume_data['experience']
            }
        })

    except Exception as e:
        return jsonify({'error': str(e)}), 500

@app.route('/add_job', methods=['POST'])
def add_job():
    """Add job description manually"""
    data = request.json

    if not data or 'description' not in data:
        return jsonify({'error': 'No job description provided'}), 400

    try:
        job_description = data['description']
        job_title = data.get('title', 'Unknown Position')

        # Extract skills from job description
        job_skills = skill_extractor.extract_skills(job_description)

        # Extract requirements (simplified)
        requirements = {
            'required_experience': 0,
            'required_education': ''
        }

        # Try to extract years of experience
        import re
        exp_match = re.search(r'(\d+)[\+]?\s*(?:years?|yrs?).*?(?:experience)', job_description.lower())
        if exp_match:
            requirements['required_experience'] = int(exp_match.group(1))

        # Store job data
        job_data = {
            'title': job_title,
            'description': job_description,
            'skills': job_skills,
            'requirements': requirements
        }

        session['job_data'] = job_data

        return jsonify({
            'success': True,
            'job_data': {
                'title': job_title,
                'skills_summary': skill_extractor.get_skill_summary(job_skills),
                'requirements': requirements
            }
        })

    except Exception as e:
        return jsonify({'error': str(e)}), 500

@app.route('/match', methods=['POST'])
def match():
    """Match resume with job"""
    if 'resume_data' not in session or 'job_data' not in session:
        return jsonify({'error': 'Please upload both resume and job description first'}), 400

    try:
        resume_data = session['resume_data']
        job_data = session['job_data']

        # Perform matching
        match_result = job_matcher.match_resume_to_job(resume_data, job_data)

        return jsonify({
            'success': True,
            'match_result': match_result
        })

    except Exception as e:
        return jsonify({'error': str(e)}), 500

@app.route('/generate_sample', methods=['POST'])
def generate_sample():
    """Generate sample data for testing"""
    try:
        # Generate sample resume
        resumes = data_generator.generate_sample_resumes(1)
        sample_resume = resumes[0]

        # Generate sample job
        jobs = data_generator.generate_sample_jobs(1)
        sample_job = jobs[0]

        # Parse and process resume
        resume_data = {
            'file_name': 'sample_resume.txt',
            'full_text': sample_resume['text'],
            'contact': {
                'name': sample_resume['name'],
                'email': f"{sample_resume['name'].lower().replace(' ', '.')}@example.com",
                'phone': '555-123-4567'
            },
            'skills': skill_extractor.extract_skills(sample_resume['text']),
            'experience': {'years_of_experience': 5, 'companies': [], 'titles': []}
        }

        # Store in session
        session['resume_data'] = resume_data
        session['job_data'] = sample_job

        return jsonify({
            'success': True,
            'message': 'Sample data generated successfully',
            'resume': {
                'name': sample_resume['name'],
                'title': sample_resume['title']
            },
            'job': {
                'title': sample_job['title'],
                'company': sample_job['company']
            }
        })

    except Exception as e:
        return jsonify({'error': str(e)}), 500

@app.route('/clear', methods=['POST'])
def clear_session():
    """Clear session data"""
    session.clear()
    return jsonify({'success': True})

if __name__ == '__main__':
    # Generate sample data if it doesn't exist
    if not os.path.exists('data/sample_resumes'):
        print("Generating sample data...")
        data_generator.save_sample_data()

    app.run(debug=True, port=5000)
Enter fullscreen mode Exit fullscreen mode

Step 9 – index (Template/index.html)

This HTML file is the client‑side user interface for a Resume Parser & Job Matcher web application. It provides an interactive dashboard where users can upload a resume (PDF/DOCX), enter a job description, and receive a detailed match analysis. The page communicates with a backend API (built with FastAPI or Flask) using fetch requests.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Resume Parser & Job Matcher</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;       }

        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            min-height: 100vh;
            padding: 20px;
        }

        .container {
            max-width: 1400px;
            margin: 0 auto;
        }

        h1 {
            text-align: center;
            color: white;
            margin-bottom: 30px;
            font-size: 2.5em;
            text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
        }

        .main-content {
            display: grid;
            grid-template-columns: 1fr 1fr;
            gap: 20px;
            margin-bottom: 20px;
        }

        .section {
            background: white;
            border-radius: 15px;
            padding: 25px;
            box-shadow: 0 10px 30px rgba(0,0,0,0.2);
        }

        .section h2 {
            color: #333;
            margin-bottom: 20px;
            font-size: 1.5em;
            border-bottom: 2px solid #f0f0f0;
            padding-bottom: 10px;
        }

        .upload-area {
            border: 2px dashed #ddd;
            border-radius: 10px;
            padding: 40px;
            text-align: center;
            cursor: pointer;
            transition: all 0.3s;
        }

        .upload-area:hover {
            border-color: #667eea;
            background: #f8f9ff;
        }

        .upload-area i {
            font-size: 48px;
            color: #667eea;
            margin-bottom: 15px;
        }

        .upload-area p {
            color: #666;
            margin-bottom: 10px;
        }

        .upload-area small {
            color: #999;
        }

        .file-input {
            display: none;
        }

        .job-input {
            margin-top: 20px;
        }

        textarea {
            width: 100%;
            height: 200px;
            padding: 15px;
            border: 2px solid #e0e0e0;
            border-radius: 10px;
            font-size: 14px;
            resize: vertical;
            transition: border-color 0.3s;
        }

        textarea:focus {
            outline: none;
            border-color: #667eea;
        }

        .button {
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            border: none;
            padding: 12px 30px;
            border-radius: 25px;
            font-size: 16px;
            cursor: pointer;
            transition: transform 0.3s, box-shadow 0.3s;
            margin-top: 15px;
            width: 100%;
        }

        .button:hover {
            transform: translateY(-2px);
            box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4);
        }

        .button:disabled {
            opacity: 0.6;
            cursor: not-allowed;
        }

        .sample-button {
            background: linear-gradient(135deg, #48c6ef 0%, #6f86d6 100%);
            margin-bottom: 10px;
        }

        .clear-button {
            background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
        }

        .info-card {
            background: #f8f9ff;
            border-radius: 10px;
            padding: 20px;
            margin: 15px 0;
        }

        .info-card h3 {
            color: #333;
            margin-bottom: 10px;
        }

        .skill-tag {
            display: inline-block;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            padding: 5px 15px;
            border-radius: 20px;
            margin: 5px;
            font-size: 14px;
        }

        .skill-category {
            margin: 15px 0;
        }

        .skill-category h4 {
            color: #555;
            margin-bottom: 10px;
            text-transform: capitalize;
        }

        .results-section {
            grid-column: 1 / -1;
        }

        .match-score {
            text-align: center;
            margin: 20px 0;
        }

        .score-circle {
            width: 150px;
            height: 150px;
            border-radius: 50%;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            display: flex;
            align-items: center;
            justify-content: center;
            margin: 0 auto 20px;
            color: white;
            font-size: 36px;
            font-weight: bold;
            box-shadow: 0 10px 30px rgba(102, 126, 234, 0.3);
        }

        .match-breakdown {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
            gap: 15px;
            margin: 20px 0;
        }

        .breakdown-item {
            background: #f8f9ff;
            padding: 15px;
            border-radius: 10px;
            text-align: center;
        }

        .breakdown-item h4 {
            color: #555;
            margin-bottom: 10px;
        }

        .breakdown-item .score {
            font-size: 24px;
            font-weight: bold;
            color: #667eea;
        }

        .recommendations {
            background: #e8f5e9;
            border-radius: 10px;
            padding: 20px;
            margin: 20px 0;
        }

        .recommendations h3 {
            color: #2e7d32;
            margin-bottom: 10px;
        }

        .recommendations ul {
            list-style-type: none;
        }

        .recommendations li {
            padding: 8px 0;
            padding-left: 25px;
            position: relative;
        }

        .recommendations li:before {
            content: "✓";
            color: #2e7d32;
            position: absolute;
            left: 0;
        }

        .loading {
            text-align: center;
            padding: 20px;
            display: none;
        }

        .loading-spinner {
            border: 4px solid #f3f3f3;
            border-top: 4px solid #667eea;
            border-radius: 50%;
            width: 40px;
            height: 40px;
            animation: spin 1s linear infinite;
            margin: 0 auto 10px;
        }

        @keyframes spin {
            0% { transform: rotate(0deg); }
            100% { transform: rotate(360deg); }
        }

        .status-message {
            padding: 15px;
            border-radius: 10px;
            margin: 10px 0;
            display: none;
        }

        .status-success {
            background: #d4edda;
            color: #155724;
            border: 1px solid #c3e6cb;
        }

        .status-error {
            background: #f8d7da;
            color: #721c24;
            border: 1px solid #f5c6cb;
        }

        .button-group {
            display: grid;
            grid-template-columns: 1fr 1fr;
            gap: 10px;
        }

        @media (max-width: 768px) {
            .main-content {
                grid-template-columns: 1fr;
            }
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>📄 Resume Parser & Job Matcher</h1>

        <div class="button-group">
            <button class="button sample-button" onclick="generateSampleData()">🎲 Generate Sample Data</button>
            <button class="button clear-button" onclick="clearAll()">🗑️ Clear All</button>
        </div>

        <div class="main-content">
            <!-- Resume Upload Section -->
            <div class="section">
                <h2>📄 Upload Resume</h2>
                <div class="upload-area" onclick="document.getElementById('resumeInput').click()">
                    <i>📎</i>
                    <p>Click to upload or drag and drop</p>
                    <p>PDF or DOCX (Max 16MB)</p>
                    <small>Supported formats: PDF, DOCX</small>
                </div>
                <input type="file" id="resumeInput" class="file-input" accept=".pdf,.docx" onchange="uploadResume()">

                <div id="resumeInfo" class="info-card" style="display: none;">
                    <h3 id="resumeName"></h3>
                    <p id="resumeEmail"></p>
                    <div id="resumeSkills"></div>
                </div>
            </div>

            <!-- Job Description Section -->
            <div class="section">
                <h2>💼 Job Description</h2>
                <div class="job-input">
                    <input type="text" id="jobTitle" placeholder="Job Title (optional)" style="width: 100%; padding: 10px; margin-bottom: 10px; border: 2px solid #e0e0e0; border-radius: 5px;">
                    <textarea id="jobDescription" placeholder="Paste job description here..."></textarea>
                    <button class="button" onclick="addJobDescription()">Add Job Description</button>
                </div>

                <div id="jobInfo" class="info-card" style="display: none;">
                    <h3 id="jobTitleDisplay"></h3>
                    <div id="jobSkills"></div>
                    <p id="jobRequirements"></p>
                </div>
            </div>
        </div>

        <!-- Match Button -->
        <button class="button" onclick="matchResume()" id="matchButton" style="margin-bottom: 20px;" disabled>🔍 Match Resume with Job</button>

        <!-- Results Section -->
        <div class="section results-section" id="resultsSection" style="display: none;">
            <h2>📊 Match Results</h2>

            <div class="match-score">
                <div class="score-circle" id="totalScore">0%</div>
                <h3>Overall Match</h3>
            </div>

            <div class="match-breakdown" id="breakdown"></div>

            <div class="recommendations" id="recommendations"></div>

            <div id="skillsComparison"></div>
        </div>

        <!-- Loading and Status -->
        <div class="loading" id="loading">
            <div class="loading-spinner"></div>
            <p>Processing...</p>
        </div>

        <div class="status-message" id="statusMessage"></div>
    </div>

    <script>
        let resumeData = null;
        let jobData = null;

        function showStatus(message, isError = false) {
            const statusDiv = document.getElementById('statusMessage');
            statusDiv.textContent = message;
            statusDiv.className = 'status-message ' + (isError ? 'status-error' : 'status-success');
            statusDiv.style.display = 'block';

            setTimeout(() => {
                statusDiv.style.display = 'none';
            }, 5000);
        }

        function showLoading(show) {
            document.getElementById('loading').style.display = show ? 'block' : 'none';
        }

        function displaySkills(skills, containerId) {
            const container = document.getElementById(containerId);
            container.innerHTML = '';

            if (!skills || Object.keys(skills).length === 0) {
                container.innerHTML = '<p>No skills extracted</p>';
                return;
            }

            for (const [category, skillList] of Object.entries(skills)) {
                if (skillList.length > 0) {
                    const categoryDiv = document.createElement('div');
                    categoryDiv.className = 'skill-category';

                    const categoryTitle = document.createElement('h4');
                    categoryTitle.textContent = category.replace(/_/g, ' ');
                    categoryDiv.appendChild(categoryTitle);

                    skillList.forEach(skill => {
                        const tag = document.createElement('span');
                        tag.className = 'skill-tag';
                        tag.textContent = skill;
                        categoryDiv.appendChild(tag);
                    });

                    container.appendChild(categoryDiv);
                }
            }
        }

        async function uploadResume() {
            const fileInput = document.getElementById('resumeInput');
            const file = fileInput.files[0];

            if (!file) return;

            const formData = new FormData();
            formData.append('resume', file);

            showLoading(true);

            try {
                const response = await fetch('/upload_resume', {
                    method: 'POST',
                    body: formData
                });

                const data = await response.json();

                if (data.success) {
                    resumeData = data.resume_data;

                    // Display resume info
                    document.getElementById('resumeName').textContent = resumeData.name || 'Unknown';
                    document.getElementById('resumeEmail').textContent = resumeData.email || 'No email';

                    if (resumeData.skills_summary) {
                        displaySkills(resumeData.skills_summary.skills_by_category || {}, 'resumeSkills');
                    }

                    document.getElementById('resumeInfo').style.display = 'block';

                    // Enable match button if both are present
                    if (jobData) document.getElementById('matchButton').disabled = false;

                    showStatus('Resume uploaded successfully!');
                } else {
                    showStatus(data.error || 'Error uploading resume', true);
                }
            } catch (error) {
                showStatus('Error uploading resume', true);
                console.error(error);
            } finally {
                showLoading(false);
            }
        }

        async function addJobDescription() {
            const title = document.getElementById('jobTitle').value;
            const description = document.getElementById('jobDescription').value;

            if (!description.trim()) {
                showStatus('Please enter a job description', true);
                return;
            }

            showLoading(true);

            try {
                const response = await fetch('/add_job', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json'
                    },
                    body: JSON.stringify({
                        title: title,
                        description: description
                    })
                });

                const data = await response.json();

                if (data.success) {
                    jobData = data.job_data;

                    // Display job info
                    document.getElementById('jobTitleDisplay').textContent = jobData.title || 'Unknown Position';

                    if (jobData.skills_summary) {
                        displaySkills(jobData.skills_summary.skills_by_category || {}, 'jobSkills');
                    }

                    if (jobData.requirements) {
                        document.getElementById('jobRequirements').innerHTML = `
                            <strong>Requirements:</strong><br>
                            Experience: ${jobData.requirements.required_experience || 0} years<br>
                            Education: ${jobData.requirements.required_education || 'Not specified'}
                        `;
                    }

                    document.getElementById('jobInfo').style.display = 'block';

                    // Enable match button if both are present
                    if (resumeData) document.getElementById('matchButton').disabled = false;

                    showStatus('Job description added successfully!');
                } else {
                    showStatus(data.error || 'Error adding job description', true);
                }
            } catch (error) {
                showStatus('Error adding job description', true);
                console.error(error);
            } finally {
                showLoading(false);
            }
        }

        async function matchResume() {
            if (!resumeData || !jobData) {
                showStatus('Please upload both resume and job description first', true);
                return;
            }

            showLoading(true);

            try {
                const response = await fetch('/match', {
                    method: 'POST'
                });

                const data = await response.json();

                if (data.success) {
                    displayMatchResults(data.match_result);
                    document.getElementById('resultsSection').style.display = 'block';
                    showStatus('Matching completed successfully!');
                } else {
                    showStatus(data.error || 'Error matching', true);
                }
            } catch (error) {
                showStatus('Error matching resume with job', true);
                console.error(error);
            } finally {
                showLoading(false);
            }
        }

        function displayMatchResults(results) {
            // Total score
            document.getElementById('totalScore').textContent = results.total_score + '%';

            // Breakdown
            const breakdown = document.getElementById('breakdown');
            breakdown.innerHTML = `
                <div class="breakdown-item">
                    <h4>Skills Match</h4>
                    <div class="score">${results.skill_match.score}%</div>
                </div>
                <div class="breakdown-item">
                    <h4>Experience Match</h4>
                    <div class="score">${results.experience_match}%</div>
                </div>
                <div class="breakdown-item">
                    <h4>Education Match</h4>
                    <div class="score">${results.education_match}%</div>
                </div>
                <div class="breakdown-item">
                    <h4>Semantic Match</h4>
                    <div class="score">${results.semantic_match}%</div>
                </div>
            `;

            // Recommendations
            const recommendations = document.getElementById('recommendations');
            recommendations.innerHTML = `
                <h3>💡 Recommendations</h3>
                <ul>
                    ${results.recommendations.map(rec => `<li>${rec}</li>`).join('')}
                </ul>
            `;

            // Skills comparison
            const skillsComparison = document.getElementById('skillsComparison');
            if (results.skill_match.matched_skills.length > 0 || results.skill_match.missing_skills.length > 0) {
                skillsComparison.innerHTML = `
                    <h3>Skills Analysis</h3>
                    <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-top: 20px;">
                        <div>
                            <h4 style="color: #2e7d32;">✓ Matched Skills (${results.skill_match.matched_skills.length})</h4>
                            <div>
                                ${results.skill_match.matched_skills.map(skill => 
                                    `<span class="skill-tag" style="background: #2e7d32;">${skill}</span>`
                                ).join('')}
                            </div>
                        </div>
                        <div>
                            <h4 style="color: #c62828;">✗ Missing Skills (${results.skill_match.missing_skills.length})</h4>
                            <div>
                                ${results.skill_match.missing_skills.map(skill => 
                                    `<span class="skill-tag" style="background: #c62828;">${skill}</span>`
                                ).join('')}
                            </div>
                        </div>
                    </div>
                `;
            }
        }

        async function generateSampleData() {
            showLoading(true);

            try {
                const response = await fetch('/generate_sample', {
                    method: 'POST'
                });

                const data = await response.json();

                if (data.success) {
                    // Update UI with sample data
                    resumeData = data.resume;
                    jobData = data.job;

                    document.getElementById('resumeName').textContent = data.resume.name;
                    document.getElementById('resumeEmail').textContent = 'sample@email.com';
                    document.getElementById('resumeInfo').style.display = 'block';

                    document.getElementById('jobTitleDisplay').textContent = data.job.title;
                    document.getElementById('jobInfo').style.display = 'block';

                    document.getElementById('matchButton').disabled = false;

                    showStatus('Sample data generated successfully!');
                } else {
                    showStatus(data.error || 'Error generating sample data', true);
                }
            } catch (error) {
                showStatus('Error generating sample data', true);
                console.error(error);
            } finally {
                showLoading(false);
            }
        }

        async function clearAll() {
            try {
                const response = await fetch('/clear', {
                    method: 'POST'
                });

                if (response.ok) {
                    // Reset UI
                    resumeData = null;
                    jobData = null;

                    document.getElementById('resumeInput').value = '';
                    document.getElementById('jobDescription').value = '';
                    document.getElementById('jobTitle').value = '';

                    document.getElementById('resumeInfo').style.display = 'none';
                    document.getElementById('jobInfo').style.display = 'none';
                    document.getElementById('resultsSection').style.display = 'none';
                    document.getElementById('matchButton').disabled = true;

                    showStatus('All data cleared');
                }
            } catch (error) {
                showStatus('Error clearing data', true);
            }
        }

        // Drag and drop functionality
        const dropZone = document.querySelector('.upload-area');

        dropZone.addEventListener('dragover', (e) => {
            e.preventDefault();
            dropZone.style.borderColor = '#667eea';
            dropZone.style.background = '#f8f9ff';
        });

        dropZone.addEventListener('dragleave', (e) => {
            e.preventDefault();
            dropZone.style.borderColor = '#ddd';
            dropZone.style.background = 'white';
        });

        dropZone.addEventListener('drop', (e) => {
            e.preventDefault();
            dropZone.style.borderColor = '#ddd';
            dropZone.style.background = 'white';

            const files = e.dataTransfer.files;
            if (files.length > 0) {
                document.getElementById('resumeInput').files = files;
                uploadResume();
            }
        });
    </script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Generate sample data
python -c "from utils.generator import SampleDataGenerator; SampleDataGenerator().save_sample_data()"

Run the application: python app.py

Open your browser and go to http://localhost:5000

Test the system

Upload a PDF/DOCX resume (or click “Generate Sample Data”).

Add a job description (or use the auto‑generated one).

Click “Get Match Score” to see the result.

Top comments (0)