AI Resume Matcher and Cover Letter
Build a web app that scores your resume against job postings using AI.
Introduction
30 Second Summary
Tailoring your resume for every job application is tedious and time-consuming. What if you could instantly see how well your resume matches a job posting and get AI-generated materials tailored specifically to that role?
In this project, you will build a web app that reads your resume PDF, scores it against any job description, and uses AI to generate customized application materials. You will work with document parsing, text similarity scoring, and large language model integration.
What You'll Build
A Streamlit web app that extracts resume text from a PDF, calculates a TF-IDF similarity score against a job description, identifies skill gaps, and uses Google Gemini AI to rewrite your resume and generate a cover letter.
By the end of this project, you'll have:
- A working PDF resume parser that extracts text from any resume using PyMuPDF.
- A match scoring engine that uses TF-IDF cosine similarity to quantify how well your resume fits a job description.
- AI-powered resume rewriting and cover letter generation through Google Gemini prompt engineering.
- A polished interactive web interface with file upload, results display, and download buttons.
- Secret Mission: A skill gap action plan tab that generates personalized suggestions for developing your top missing skills.
Are there any prerequisites?
No prior experience with AI APIs or web frameworks is needed. This guide walks you through setting up every tool from scratch, including Python packages, your free Gemini API key, and the Streamlit framework.
Set Up Your Environment
Before you can build an AI-powered resume matcher, you need the right tools installed and configured.
In this step, you will create a project folder, install four Python packages, and grab a free API key from Google. These are the building blocks for everything you will build in the next steps.
In this step, get ready to:
- Install the Python packages your app depends on.
- Get a free Google Gemini API key for AI-powered features.
- Place a test resume PDF in your project folder.
Create a project folder and install Python packages
You need a dedicated folder for this project and four Python libraries that handle PDF parsing, similarity scoring, AI generation, and building a web interface.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal and press Enter to open it.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir resume-matcher
cd resume-matcher
- Confirm the folder was created by running:
pwd
You should see a path ending in resume-matcher in the output.
- Install the four Python packages your app needs by running:
pip install pymupdf==1.28.0 scikit-learn==1.7.2 google-genai==2.10.0 streamlit==1.58.0
What do these packages do?
- pymupdf extracts text from PDF files.
- scikit-learn provides the TF-IDF vectorizer and cosine similarity tools for matching.
- google-genai connects your app to Google's Gemini AI model.
- streamlit turns your Python script into an interactive web app.
You should see several lines of download and install progress, ending with a Successfully installed message listing all four packages.
Seeing 'pip: command not found'?
Your system might use pip3 instead of pip. Try replacing pip with pip3 at the start of the command.
If that also fails, try python3 -m pip install followed by the same package list.
help me fix my pip installation
- Verify all packages installed correctly by running:
python3 -c "import pymupdf; import sklearn; import google.genai; import streamlit; print('All packages installed!')"
You should see All packages installed! printed in the terminal.
Seeing a ModuleNotFoundError?
Make sure the pip you used installs into the same Python that python3 runs. Run python3 -m pip install followed by the package list to ensure they go to the right place.
help me debug my Python package installation
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type VS Code and press Enter to open it.
- Click File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop, select the resume-matcher folder, then click Open.
Why open the folder in VS Code?
Opening the project folder (not just a single file) gives VS Code full context of your project. You will create multiple files in this folder, and VS Code's sidebar will show them all in one place.
Get your free Google Gemini API key
Your app uses Google Gemini to rewrite resumes and generate cover letters. You need an API key to authenticate your requests.
The Gemini API has a free tier that requires no credit card.
- Open your browser and go to aistudio.google.com.
- Sign in with your Google account.
- Click Get API Key in the left sidebar.
- Click Create API key.
- Copy the key that appears. It starts with AIza.
What about API key restrictions?
As of June 19, 2026, unrestricted standard API keys are rejected by Google. Keys with restrictions applied continue to work.
When you create a key in AI Studio, appropriate restrictions are applied automatically. If your key does not work later, check the key settings in AI Studio to confirm restrictions are in place.
Now store your key as an environment variable so your Python scripts can access it during testing.
- In VS Code, open the integrated terminal by pressing Ctrl+` (Windows) or Cmd+` (macOS).
- Set the environment variable by running this command:
macOS
export GEMINI_API_KEY="your-api-key-here"
Windows
$env:GEMINI_API_KEY = "your-api-key-here"
- Replace your-api-key-here with the key you just copied from AI Studio.
This variable lasts one session
The environment variable only exists in this terminal session. If you close the terminal and reopen it later, you will need to run the export command again.
Key not working later?
Make sure you did not accidentally include extra spaces or quotes around the key value. The key should start with AIza and contain no line breaks.
help me troubleshoot my Gemini API key
Prepare a test resume PDF
You need a PDF resume to test your app with. Any resume will work.
- Use your own resume if you have one saved as a PDF.
- If you do not have one handy, create a simple test resume in Google Docs or Word, then save or export it as a PDF.
- Place the PDF file inside your resume-matcher folder. You can drag it into the VS Code sidebar or copy it using your file manager.
Keep the filename simple
Avoid spaces or special characters in the filename. Something like resume.pdf or my-resume.pdf works well.
You should see the PDF file appear in the VS Code sidebar under the resume-matcher folder.
Your environment is ready. Next up, you will build the core logic that parses your resume and scores how well it matches a job description.
Build the Resume Parser and Job Matcher
Your environment is ready with all the packages installed and your API key configured. Now it's time to build the brain of your app.
When you apply for a job, you want to know how well your resume matches the posting. Your app needs to read your resume PDF, pull out the important details, and score how closely it aligns with the job description.
In this step, get ready to:
- Write functions that extract text and skills from a PDF resume.
- Build a scoring system that measures resume-to-job similarity.
- Test your core logic and confirm it identifies skill gaps.
Write the PDF extraction and skill extraction functions
Your app needs a way to read a PDF resume and understand what skills it contains. You'll use PyMuPDF to extract raw text from every page, then scan that text against a database of known skills.
- Create a new file called matcher.py in your project folder.
- Add the imports and skills database by pasting this code:
import os
import pymupdf
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
from google import genai
# --- Skills Database ---
SKILLS_DATABASE = [
"Python", "JavaScript", "TypeScript", "Java", "C++", "C#", "Go", "Rust",
"React", "Angular", "Vue", "Node.js", "Django", "Flask", "FastAPI",
"SQL", "PostgreSQL", "MySQL", "MongoDB", "Redis",
"AWS", "Azure", "Google Cloud", "Docker", "Kubernetes",
"Git", "CI/CD", "Linux", "REST API", "GraphQL",
"Machine Learning", "Deep Learning", "NLP", "Computer Vision",
"TensorFlow", "PyTorch", "Pandas", "NumPy", "Scikit-learn",
"Agile", "Scrum", "Project Management", "Leadership",
"Communication", "Problem Solving", "Teamwork",
"HTML", "CSS", "Tailwind", "Bootstrap",
"Data Analysis", "Data Visualization", "Excel", "Tableau", "Power BI",
"Figma", "Adobe Photoshop", "UI/UX Design",
]
What does this code do?
The imports bring in the libraries you installed in Step 1. You won't use all of them in this step, but they'll be ready for later.
SKILLS_DATABASE is a list of common technical and soft skills your app will search for inside resume and job description text. The app checks whether each skill's name appears in the document.
- Save matcher.py.
- Confirm the file exists by running:
ls matcher.py
You should see matcher.py listed in the output.
- Below the SKILLS_DATABASE list, add the resume parsing functions:
# --- Resume Parsing ---
def extract_text_from_pdf(pdf_path):
"""Extract all text from a PDF file."""
doc = pymupdf.open(pdf_path)
text = "\n".join(page.get_text("text") for page in doc)
doc.close()
return text.strip()
def extract_text_from_pdf_bytes(pdf_bytes):
"""Extract all text from PDF bytes (for Streamlit file upload)."""
doc = pymupdf.open(stream=pdf_bytes, filetype="pdf")
text = "\n".join(page.get_text("text") for page in doc)
doc.close()
return text.strip()
def extract_skills(text, skills_database):
"""Find skills in text that match the skills database."""
text_lower = text.lower()
found_skills = []
for skill in skills_database:
if skill.lower() in text_lower:
found_skills.append(skill)
return found_skills
What does this code do?
- extract_text_from_pdf() opens a PDF from a file path, loops through every page, and joins all the text into one string.
- extract_text_from_pdf_bytes() does the same thing but accepts raw bytes instead of a file path. You'll need this later when handling file uploads in the web interface.
- extract_skills() takes a block of text and compares it against the skills database. It does a case-insensitive check for each skill name and returns every match it finds.
- Save matcher.py.
You won't see output yet. The next substep adds the matching logic, then you'll test everything together.
Seeing a syntax error on save?
- Check that the triple-quoted docstrings use three double quotes (""") on each side.
- Make sure each def line ends with a colon.
- Verify you have a single blank line between each function, not double blank lines.
Help me fix a syntax error in my matcher.py parsing functions.
Write the match scoring and skill gap functions
Extracting skills tells you what's in the resume, but you also need to measure overall similarity. TF-IDF converts documents into numerical vectors based on word importance, and cosine similarity measures how close those vectors are on a scale of 0 to 100%.
- Below the extract_skills function, add the job matching functions:
# --- Job Matching ---
def calculate_match_score(resume_text, job_description):
"""Calculate TF-IDF cosine similarity between resume and job description."""
vectorizer = TfidfVectorizer(stop_words="english")
tfidf_matrix = vectorizer.fit_transform([resume_text, job_description])
score = cosine_similarity(tfidf_matrix[0:1], tfidf_matrix[1:2])[0][0]
return round(score * 100, 1)
def identify_skill_gaps(resume_skills, job_skills):
"""Find matching and missing skills between resume and job."""
resume_skills_lower = {s.lower() for s in resume_skills}
matching = [s for s in job_skills if s.lower() in resume_skills_lower]
missing = [s for s in job_skills if s.lower() not in resume_skills_lower]
return matching, missing
What does this code do?
- calculate_match_score() uses scikit-learn's TfidfVectorizer to convert both texts into vectors, then computes how similar they are. A score of 100% means identical wording. A score of 0% means no overlap at all.
- identify_skill_gaps() compares the skills found in your resume against the skills in the job posting. It returns two lists: skills you already have (matching) and skills the job wants that you're missing.
- Save matcher.py.
You won't see output yet. The next substep adds a test block so you can run everything and confirm it works.
Test the core logic
Your functions are written, but you haven't run them yet. A quick test block at the bottom of the file lets you verify everything works end-to-end.
- At the bottom of matcher.py, add this temporary test block:
# --- Quick Test ---
if __name__ == "__main__":
# Test with your resume PDF
resume_text = extract_text_from_pdf("[[RESUME_PDF=\"your-resume.pdf\"]]")
print("=== Extracted Resume Text (first 500 chars) ===")
print(resume_text[:500])
print()
# Sample job description for testing
job_description = """
We are looking for a Python developer with experience in machine learning,
data analysis, and cloud services. The ideal candidate has experience with
AWS, Docker, Git, and SQL. Strong communication and teamwork skills required.
"""
# Extract skills from both documents
resume_skills = extract_skills(resume_text, SKILLS_DATABASE)
job_skills = extract_skills(job_description, SKILLS_DATABASE)
print(f"=== Resume Skills: {resume_skills}")
print(f"=== Job Skills: {job_skills}")
print()
# Calculate match score
score = calculate_match_score(resume_text, job_description)
print(f"=== Match Score: {score}%")
print()
# Identify skill gaps
matching, missing = identify_skill_gaps(resume_skills, job_skills)
print(f"=== Matching Skills: {matching}")
print(f"=== Missing Skills: {missing}")
What does this code do?
The if __name__ == "__main__": block only runs when you execute the file directly. It won't run when other files import matcher.py later.
This test opens your resume PDF, extracts text, finds skills in both the resume and a sample job posting, scores the match, and prints skill gaps.
- Save matcher.py.
- Run the test by executing:
python matcher.py
You should see extracted resume text, a list of skills found in your resume and the job description, a percentage match score, and lists of matching and missing skills.
Seeing a file not found error?
- Make sure your resume PDF is in the same folder as matcher.py.
- Double-check the filename you typed matches your actual PDF file name exactly, including the .pdf extension.
- File names are case-sensitive on macOS and Linux.
Help me fix a file not found error when running matcher.py.
- Once the test prints results successfully, remove the test block from the bottom of matcher.py (everything from # --- Quick Test --- downward).
- Save matcher.py.
✔️ Awesome, I've got everything!
Great. Make sure you've saved matcher.py with the test block removed.
ⓧ I'd like to double check the full code
import os
import pymupdf
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
from google import genai
# --- Skills Database ---
SKILLS_DATABASE = [
"Python", "JavaScript", "TypeScript", "Java", "C++", "C#", "Go", "Rust",
"React", "Angular", "Vue", "Node.js", "Django", "Flask", "FastAPI",
"SQL", "PostgreSQL", "MySQL", "MongoDB", "Redis",
"AWS", "Azure", "Google Cloud", "Docker", "Kubernetes",
"Git", "CI/CD", "Linux", "REST API", "GraphQL",
"Machine Learning", "Deep Learning", "NLP", "Computer Vision",
"TensorFlow", "PyTorch", "Pandas", "NumPy", "Scikit-learn",
"Agile", "Scrum", "Project Management", "Leadership",
"Communication", "Problem Solving", "Teamwork",
"HTML", "CSS", "Tailwind", "Bootstrap",
"Data Analysis", "Data Visualization", "Excel", "Tableau", "Power BI",
"Figma", "Adobe Photoshop", "UI/UX Design",
]
# --- Resume Parsing ---
def extract_text_from_pdf(pdf_path):
"""Extract all text from a PDF file."""
doc = pymupdf.open(pdf_path)
text = "\n".join(page.get_text("text") for page in doc)
doc.close()
return text.strip()
def extract_text_from_pdf_bytes(pdf_bytes):
"""Extract all text from PDF bytes (for Streamlit file upload)."""
doc = pymupdf.open(stream=pdf_bytes, filetype="pdf")
text = "\n".join(page.get_text("text") for page in doc)
doc.close()
return text.strip()
def extract_skills(text, skills_database):
"""Find skills in text that match the skills database."""
text_lower = text.lower()
found_skills = []
for skill in skills_database:
if skill.lower() in text_lower:
found_skills.append(skill)
return found_skills
# --- Job Matching ---
def calculate_match_score(resume_text, job_description):
"""Calculate TF-IDF cosine similarity between resume and job description."""
vectorizer = TfidfVectorizer(stop_words="english")
tfidf_matrix = vectorizer.fit_transform([resume_text, job_description])
score = cosine_similarity(tfidf_matrix[0:1], tfidf_matrix[1:2])[0][0]
return round(score * 100, 1)
def identify_skill_gaps(resume_skills, job_skills):
"""Find matching and missing skills between resume and job."""
resume_skills_lower = {s.lower() for s in resume_skills}
matching = [s for s in job_skills if s.lower() in resume_skills_lower]
missing = [s for s in job_skills if s.lower() not in resume_skills_lower]
return matching, missing
Your resume parser and job matcher are working. Next up, you'll connect Google Gemini AI to rewrite your resume and generate a tailored cover letter.
Add AI-Powered Resume Rewriting and Cover Letter Generation
Your matcher.py can now extract resume text, identify skills, and score how well a resume matches a job description. That's the analytical foundation for everything else.
Now you'll add the AI layer. Using Google Gemini, you'll write functions that take the analysis results and generate two practical outputs: a rewritten resume tailored to the job, and a custom cover letter.
In this step, get ready to:
- Connect to the Google Gemini API.
- Write functions to rewrite your resume and generate a cover letter using AI.
- Test both AI functions with your resume and a sample job description.
Set up the Gemini client and write the resume rewrite function
The google-genai library you installed in Step 1 gives you a simple client object to send prompts to Gemini. The key to good AI output is crafting a clear, structured prompt that tells the model exactly what you want.
- In matcher.py, add the following code at the bottom of the file, below the identify_skill_gaps function:
# --- AI Generation ---
def get_gemini_client(api_key):
"""Create and return a Gemini client."""
return genai.Client(api_key=api_key)
def rewrite_resume(client, resume_text, job_description, missing_skills):
"""Use Gemini to rewrite resume content tailored to the job."""
prompt = f"""You are a professional resume writer. Rewrite the following resume
to better match the job description below. Keep the same section structure
(Contact Info, Experience, Education, Skills, etc.) but:
1. Enhance bullet points to highlight relevant experience for this role
2. Incorporate these missing skills where the candidate has transferable experience: {', '.join(missing_skills)}
3. Use action verbs and quantify achievements where possible
4. Keep it honest - do not fabricate experience, but reframe existing experience to be more relevant
CURRENT RESUME:
{resume_text}
JOB DESCRIPTION:
{job_description}
Provide the rewritten resume in plain text with clear section headings."""
response = client.models.generate_content(
model="gemini-2.5-flash",
contents=prompt,
)
return response.text
What does this code do?
get_gemini_client(api_key) creates a client connection to Google's Gemini API using your API key. This client object is what you use to send prompts and receive responses.
rewrite_resume() builds a detailed prompt using an f-string that injects the resume text, job description, and missing skills directly into the instructions. The prompt tells Gemini to keep the resume structure honest while reframing experience to be more relevant.
client.models.generate_content() sends the prompt to the gemini-2.5-flash model and returns the AI-generated text via response.text.
- Save matcher.py.
Why prompt engineering matters here
The numbered instructions in the prompt (enhance bullet points, incorporate missing skills, use action verbs, keep it honest) are guardrails that prevent the AI from fabricating experience. Without them, the model might invent qualifications the candidate doesn't have.
The more specific your prompt, the more useful and accurate the output. This is prompt engineering in action.
Write the cover letter generation function
The cover letter function follows the same pattern as the resume rewriter. It sends your resume, the job description, and your matching skills to Gemini with instructions for writing a professional letter.
- In matcher.py, add this function below rewrite_resume:
def generate_cover_letter(client, resume_text, job_description, matching_skills):
"""Use Gemini to generate a tailored cover letter."""
prompt = f"""Write a professional cover letter for the following job based on the
candidate's resume. The letter should:
1. Be 3-4 paragraphs long
2. Highlight these matching skills: {', '.join(matching_skills)}
3. Show enthusiasm for the specific role and company
4. Include a concrete example from the resume that demonstrates relevant experience
5. Be professional but personable in tone
RESUME:
{resume_text}
JOB DESCRIPTION:
{job_description}
Write the cover letter in plain text format, ready to send."""
response = client.models.generate_content(
model="gemini-2.5-flash",
contents=prompt,
)
return response.text
What does this code do?
generate_cover_letter() takes matching skills (the ones you already have) and weaves them into a letter that highlights your strengths for the role. It's the complement to the resume rewriter: one fills gaps, the other amplifies strengths.
The prompt specifies a 3-4 paragraph length and asks for a concrete example from the resume. These constraints keep the output focused and professional rather than generic.
- Save matcher.py.
Seeing a red underline on genai or the import?
Make sure you have from google import genai at the top of your file. This import was already there from Step 1's setup. If your editor shows an error, try restarting VS Code to refresh the language server.
help me fix an import error with google-genai in my matcher.py file
✔️ Awesome, I've got everything!
Great. Make sure you've saved matcher.py before moving on to the test.
ⓧ I'd like to double check the full code
import os
import pymupdf
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
from google import genai
# --- Skills Database ---
SKILLS_DATABASE = [
"Python", "JavaScript", "TypeScript", "Java", "C++", "C#", "Go", "Rust",
"React", "Angular", "Vue", "Node.js", "Django", "Flask", "FastAPI",
"SQL", "PostgreSQL", "MySQL", "MongoDB", "Redis",
"AWS", "Azure", "Google Cloud", "Docker", "Kubernetes",
"Git", "CI/CD", "Linux", "REST API", "GraphQL",
"Machine Learning", "Deep Learning", "NLP", "Computer Vision",
"TensorFlow", "PyTorch", "Pandas", "NumPy", "Scikit-learn",
"Agile", "Scrum", "Project Management", "Leadership",
"Communication", "Problem Solving", "Teamwork",
"HTML", "CSS", "Tailwind", "Bootstrap",
"Data Analysis", "Data Visualization", "Excel", "Tableau", "Power BI",
"Figma", "Adobe Photoshop", "UI/UX Design",
]
# --- Resume Parsing ---
def extract_text_from_pdf(pdf_path):
"""Extract all text from a PDF file."""
doc = pymupdf.open(pdf_path)
text = "\n".join(page.get_text("text") for page in doc)
doc.close()
return text.strip()
def extract_text_from_pdf_bytes(pdf_bytes):
"""Extract all text from PDF bytes (for Streamlit file upload)."""
doc = pymupdf.open(stream=pdf_bytes, filetype="pdf")
text = "\n".join(page.get_text("text") for page in doc)
doc.close()
return text.strip()
def extract_skills(text, skills_database):
"""Find skills in text that match the skills database."""
text_lower = text.lower()
found_skills = []
for skill in skills_database:
if skill.lower() in text_lower:
found_skills.append(skill)
return found_skills
# --- Job Matching ---
def calculate_match_score(resume_text, job_description):
"""Calculate TF-IDF cosine similarity between resume and job description."""
vectorizer = TfidfVectorizer(stop_words="english")
tfidf_matrix = vectorizer.fit_transform([resume_text, job_description])
score = cosine_similarity(tfidf_matrix[0:1], tfidf_matrix[1:2])[0][0]
return round(score * 100, 1)
def identify_skill_gaps(resume_skills, job_skills):
"""Find matching and missing skills between resume and job."""
resume_skills_lower = {s.lower() for s in resume_skills}
matching = [s for s in job_skills if s.lower() in resume_skills_lower]
missing = [s for s in job_skills if s.lower() not in resume_skills_lower]
return matching, missing
# --- AI Generation ---
def get_gemini_client(api_key):
"""Create and return a Gemini client."""
return genai.Client(api_key=api_key)
def rewrite_resume(client, resume_text, job_description, missing_skills):
"""Use Gemini to rewrite resume content tailored to the job."""
prompt = f"""You are a professional resume writer. Rewrite the following resume
to better match the job description below. Keep the same section structure
(Contact Info, Experience, Education, Skills, etc.) but:
1. Enhance bullet points to highlight relevant experience for this role
2. Incorporate these missing skills where the candidate has transferable experience: {', '.join(missing_skills)}
3. Use action verbs and quantify achievements where possible
4. Keep it honest - do not fabricate experience, but reframe existing experience to be more relevant
CURRENT RESUME:
{resume_text}
JOB DESCRIPTION:
{job_description}
Provide the rewritten resume in plain text with clear section headings."""
response = client.models.generate_content(
model="gemini-2.5-flash",
contents=prompt,
)
return response.text
def generate_cover_letter(client, resume_text, job_description, matching_skills):
"""Use Gemini to generate a tailored cover letter."""
prompt = f"""Write a professional cover letter for the following job based on the
candidate's resume. The letter should:
1. Be 3-4 paragraphs long
2. Highlight these matching skills: {', '.join(matching_skills)}
3. Show enthusiasm for the specific role and company
4. Include a concrete example from the resume that demonstrates relevant experience
5. Be professional but personable in tone
RESUME:
{resume_text}
JOB DESCRIPTION:
{job_description}
Write the cover letter in plain text format, ready to send."""
response = client.models.generate_content(
model="gemini-2.5-flash",
contents=prompt,
)
return response.text
Test the AI functions
Time to see Gemini in action. You'll add a temporary test block at the bottom of matcher.py that calls both AI functions and prints the results.
- Add the following test code at the very bottom of matcher.py:
# --- Test Block (temporary) ---
if __name__ == "__main__":
import os
# Extract resume text from your PDF
resume_text = extract_text_from_pdf([[RESUME_PDF="your_resume.pdf"]])
# Sample job description to test against
job_description = """We are looking for a Python developer with experience in
machine learning, REST APIs, and cloud services (AWS).
Strong communication skills required."""
# Run the matching analysis
resume_skills = extract_skills(resume_text, SKILLS_DATABASE)
job_skills = extract_skills(job_description, SKILLS_DATABASE)
matching, missing = identify_skill_gaps(resume_skills, job_skills)
# Test AI generation
api_key = os.environ["GEMINI_API_KEY"]
client = get_gemini_client(api_key)
print("--- Rewritten Resume ---")
rewritten = rewrite_resume(client, resume_text, job_description, missing)
print(rewritten)
print("\n--- Cover Letter ---")
cover_letter = generate_cover_letter(client, resume_text, job_description, matching)
print(cover_letter)
What does this test block do?
The if __name__ == "__main__": guard means this code only runs when you execute the file directly. It won't run when other files import from matcher.py.
The block reuses your resume PDF and a sample job description, runs the skill gap analysis, then passes everything to both AI functions. You'll see the full rewritten resume and cover letter printed in your terminal.
- Save matcher.py.
- Run the test by executing this command in your terminal:
python matcher.py
After a few seconds, you should see a rewritten version of your resume followed by a professional cover letter. Both are tailored to the sample job description.
The API call takes a few seconds
Each Gemini API call takes 3-10 seconds depending on the length of your resume. This is normal. The free tier has a limit of 10 requests per minute, so don't worry about running this a few times for testing.
Seeing an API key error?
If you see a 400 error mentioning the API key, make sure your GEMINI_API_KEY environment variable is set in the same terminal session where you run the script. Run echo $GEMINI_API_KEY to check. If it's empty, set it again with export GEMINI_API_KEY="your-key-here".
If you see a permission error, your key may need restrictions applied in Google AI Studio. Visit aistudio.google.com and check that your key has appropriate restrictions set.
help me fix a Gemini API key error when running my matcher.py script
Your AI functions are working. Next up, you'll wrap everything in a polished Streamlit web interface with file upload, results display, and download buttons.
Build the Streamlit Frontend
Your core logic is working. You can parse resumes, score matches, and generate AI-tailored materials from the terminal.
Now you need a way for anyone to use this tool without touching the command line. Streamlit lets you build an interactive web interface in pure Python. In this step, you will create a polished web app with file upload, live analysis, and download buttons.
In this step, get ready to:
- Create the app layout with a sidebar, file uploader, and job description input.
- Display the match analysis results with metrics and skill lists.
- Add AI-powered resume rewriting and cover letter generation with download buttons.
Create the app layout and file upload
Streamlit apps are just Python scripts. Every time a user interacts with a widget, the entire script reruns top-to-bottom. This means your page layout, sidebar, and inputs all get defined at the top of the file.
- Create a new file called app.py in your project folder.
- Add the imports and page configuration by pasting this code:
import streamlit as st
from matcher import (
extract_text_from_pdf_bytes,
extract_skills,
calculate_match_score,
identify_skill_gaps,
get_gemini_client,
rewrite_resume,
generate_cover_letter,
SKILLS_DATABASE,
)
# --- Page Configuration ---
st.set_page_config(
page_title="AI Resume-Job Matcher",
page_icon="\ud83d\udcc4",
layout="wide",
)
st.title("AI Resume-Job Matcher & Cover Letter Generator")
st.markdown("Upload your resume, paste a job description, and get AI-powered application materials.")
What does this code do?
The import block pulls in every function and constant you built in matcher.py. This keeps your UI code clean and focused on layout.
st.set_page_config must be the first Streamlit command in your script. It sets the browser tab title, favicon, and tells Streamlit to use the full browser width.
- Add the sidebar section below the page configuration code:
# --- Sidebar: API Key ---
with st.sidebar:
st.header("Settings")
api_key = st.text_input(
"Gemini API Key",
type="password",
help="Get your free key at aistudio.google.com",
)
st.markdown("---")
st.markdown("**How it works:**")
st.markdown("1. Upload your resume PDF")
st.markdown("2. Paste the job description")
st.markdown("3. Click 'Run Analysis'")
st.markdown("4. Get your tailored materials!")
What does this code do?
st.sidebar creates a collapsible panel on the left side of the page. The type="password" parameter masks the API key input so it is not visible on screen.
Placing the API key in the sidebar keeps it accessible but out of the main workflow area.
- Add the main content area with the file uploader, text area, and analysis button below the sidebar code:
# --- Main Content ---
col1, col2 = st.columns(2)
with col1:
st.subheader("Your Resume")
uploaded_file = st.file_uploader("Upload your resume PDF", type=["pdf"])
with col2:
st.subheader("Job Description")
job_description = st.text_area(
"Paste the job description here",
height=300,
placeholder="Copy and paste the full job posting...",
)
# --- Run Analysis ---
run_button = st.button("Run Analysis", type="primary", use_container_width=True)
What does this code do?
st.columns(2) splits the page into two equal-width columns. The left column holds the PDF uploader and the right column holds the job description text area.
st.button with type="primary" renders a highlighted blue button. The use_container_width=True parameter stretches it across the full page width.
- Save app.py.
- Launch the app by running this command in your terminal:
streamlit run app.py
- Your browser should open automatically to http://localhost:8501. You should see a wide layout with a sidebar containing the API key input, two columns (resume upload on the left, job description on the right), and a blue Run Analysis button.
App not loading?
- If you see ModuleNotFoundError: No module named 'matcher', make sure your terminal is in the same folder as both app.py and matcher.py.
- If port 8501 is already in use, Streamlit will try the next available port and display it in the terminal output.
help me debug why my Streamlit app is not loading
Display the match analysis results
When the user clicks the button, your app needs to validate inputs, parse the uploaded PDF, calculate the match score, and display results visually. All of this goes inside an if run_button: block.
- Add this code at the bottom of app.py, below the run_button line:
if run_button:
if not uploaded_file:
st.error("Please upload your resume PDF.")
elif not job_description.strip():
st.error("Please paste a job description.")
elif not api_key:
st.error("Please enter your Gemini API key in the sidebar.")
else:
# Parse resume
with st.spinner("Parsing your resume..."):
pdf_bytes = uploaded_file.read()
resume_text = extract_text_from_pdf_bytes(pdf_bytes)
resume_skills = extract_skills(resume_text, SKILLS_DATABASE)
job_skills = extract_skills(job_description, SKILLS_DATABASE)
# Calculate match
score = calculate_match_score(resume_text, job_description)
matching, missing = identify_skill_gaps(resume_skills, job_skills)
# Display match results
st.markdown("---")
st.header("Match Analysis")
metric_col1, metric_col2, metric_col3 = st.columns(3)
with metric_col1:
st.metric("Match Score", f"{score}%")
with metric_col2:
st.metric("Matching Skills", len(matching))
with metric_col3:
st.metric("Missing Skills", len(missing))
skill_col1, skill_col2 = st.columns(2)
with skill_col1:
st.subheader("Matching Skills")
for skill in matching:
st.markdown(f"\u2705 {skill}")
if not matching:
st.markdown("_No matching skills found._")
with skill_col2:
st.subheader("Missing Skills")
for skill in missing:
st.markdown(f"\u274c {skill}")
if not missing:
st.markdown("_No missing skills - great match!_")
What does this code do?
The if/elif/else chain validates that the user has provided all three inputs before doing any work. If something is missing, st.error() shows a red warning message.
st.spinner shows a loading animation while the PDF is being parsed. After parsing, st.metric displays the match score as a large, bold number. The skills are shown in two columns with green checkmarks for matches and red crosses for gaps.
- Save app.py.
- Switch back to your browser. Streamlit should prompt you to rerun the app. Click Rerun (or press R).
- Upload your test resume PDF, paste a sample job description, enter your API key in the sidebar, and click Run Analysis. You should see three metric cards (Match Score, Matching Skills, Missing Skills) and two columns listing your skills with checkmarks and crosses.
Not seeing any skills listed?
- The skill extractor does case-insensitive matching against the SKILLS_DATABASE list. If your resume or job description uses uncommon terms not in the database, no skills will match.
- Try pasting a job description that mentions common skills like Python, SQL, AWS, or React.
help me figure out why my skill matching returns empty results
Add AI generation with download buttons
The match analysis shows you what is missing. Now you will call Gemini to generate application materials the user can download and use immediately.
- In app.py, find the last line of the skills display section (the line that says st.markdown("_No missing skills - great match!_")).
- Add this code directly below it, still inside the else: block (keep the same indentation level):
# AI Generation
st.markdown("---")
st.header("AI-Generated Materials")
client = get_gemini_client(api_key)
with st.spinner("Rewriting your resume for this job..."):
rewritten = rewrite_resume(client, resume_text, job_description, missing)
with st.spinner("Generating your cover letter..."):
cover_letter = generate_cover_letter(
client, resume_text, job_description, matching
)
# Display rewritten resume
with st.expander("Tailored Resume", expanded=True):
st.text(rewritten)
st.download_button(
label="Download Tailored Resume",
data=rewritten,
file_name="rewritten_resume.txt",
mime="text/plain",
)
# Display cover letter
with st.expander("Cover Letter", expanded=True):
st.text(cover_letter)
st.download_button(
label="Download Cover Letter",
data=cover_letter,
file_name="cover_letter.txt",
mime="text/plain",
)
What does this code do?
get_gemini_client(api_key) creates the Gemini client using the key from the sidebar. The two st.spinner blocks show loading messages while each AI call runs.
st.expander creates collapsible sections that start open (expanded=True). Inside each expander, st.download_button lets the user save the generated text as a .txt file with one click.
- Save app.py and click Rerun in your browser.
- Upload your resume, paste a job description, enter your API key, and click Run Analysis again. After the spinners finish, you should see two expandable sections (Tailored Resume and Cover Letter) each with a Download button.
Seeing an API error?
- Double-check that you pasted your full Gemini API key into the sidebar input. The key should start with AIza.
- If you see a permissions error, verify that your API key has restrictions applied in Google AI Studio. Unrestricted keys are rejected.
- If the spinner runs for more than 30 seconds, the Gemini free tier may be rate-limited. Wait a minute and try again.
help me fix my Gemini API error in the Streamlit app
✔️ Awesome, I've got everything!
Great. Double check you have saved app.py and that your app is running at http://localhost:8501.
ⓧ I'd like to double check the full code
import streamlit as st
from matcher import (
extract_text_from_pdf_bytes,
extract_skills,
calculate_match_score,
identify_skill_gaps,
get_gemini_client,
rewrite_resume,
generate_cover_letter,
SKILLS_DATABASE,
)
# --- Page Configuration ---
st.set_page_config(
page_title="AI Resume-Job Matcher",
page_icon="\ud83d\udcc4",
layout="wide",
)
st.title("AI Resume-Job Matcher & Cover Letter Generator")
st.markdown("Upload your resume, paste a job description, and get AI-powered application materials.")
# --- Sidebar: API Key ---
with st.sidebar:
st.header("Settings")
api_key = st.text_input(
"Gemini API Key",
type="password",
help="Get your free key at aistudio.google.com",
)
st.markdown("---")
st.markdown("**How it works:**")
st.markdown("1. Upload your resume PDF")
st.markdown("2. Paste the job description")
st.markdown("3. Click 'Run Analysis'")
st.markdown("4. Get your tailored materials!")
# --- Main Content ---
col1, col2 = st.columns(2)
with col1:
st.subheader("Your Resume")
uploaded_file = st.file_uploader("Upload your resume PDF", type=["pdf"])
with col2:
st.subheader("Job Description")
job_description = st.text_area(
"Paste the job description here",
height=300,
placeholder="Copy and paste the full job posting...",
)
# --- Run Analysis ---
run_button = st.button("Run Analysis", type="primary", use_container_width=True)
if run_button:
if not uploaded_file:
st.error("Please upload your resume PDF.")
elif not job_description.strip():
st.error("Please paste a job description.")
elif not api_key:
st.error("Please enter your Gemini API key in the sidebar.")
else:
# Parse resume
with st.spinner("Parsing your resume..."):
pdf_bytes = uploaded_file.read()
resume_text = extract_text_from_pdf_bytes(pdf_bytes)
resume_skills = extract_skills(resume_text, SKILLS_DATABASE)
job_skills = extract_skills(job_description, SKILLS_DATABASE)
# Calculate match
score = calculate_match_score(resume_text, job_description)
matching, missing = identify_skill_gaps(resume_skills, job_skills)
# Display match results
st.markdown("---")
st.header("Match Analysis")
metric_col1, metric_col2, metric_col3 = st.columns(3)
with metric_col1:
st.metric("Match Score", f"{score}%")
with metric_col2:
st.metric("Matching Skills", len(matching))
with metric_col3:
st.metric("Missing Skills", len(missing))
skill_col1, skill_col2 = st.columns(2)
with skill_col1:
st.subheader("Matching Skills")
for skill in matching:
st.markdown(f"\u2705 {skill}")
if not matching:
st.markdown("_No matching skills found._")
with skill_col2:
st.subheader("Missing Skills")
for skill in missing:
st.markdown(f"\u274c {skill}")
if not missing:
st.markdown("_No missing skills - great match!_")
# AI Generation
st.markdown("---")
st.header("AI-Generated Materials")
client = get_gemini_client(api_key)
with st.spinner("Rewriting your resume for this job..."):
rewritten = rewrite_resume(client, resume_text, job_description, missing)
with st.spinner("Generating your cover letter..."):
cover_letter = generate_cover_letter(
client, resume_text, job_description, matching
)
# Display rewritten resume
with st.expander("Tailored Resume", expanded=True):
st.text(rewritten)
st.download_button(
label="Download Tailored Resume",
data=rewritten,
file_name="rewritten_resume.txt",
mime="text/plain",
)
# Display cover letter
with st.expander("Cover Letter", expanded=True):
st.text(cover_letter)
st.download_button(
label="Download Cover Letter",
data=cover_letter,
file_name="cover_letter.txt",
mime="text/plain",
)
Your resume matcher is live. You have a full end-to-end web app that parses PDFs, scores job fit, and generates tailored application materials with one click. Next up, you will take on a Secret Mission to add a skill gap action plan.
Secret mission
Add a Skill Gap Action Plan Tab
Your app identifies missing skills, but what should you actually do about them? In this secret mission, you'll add a third tab that turns your skill gaps into a personalized 30-day learning plan with prioritized resources, hands-on projects, and weekly milestones.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally and uses the Google Gemini free tier, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- Project folder on your Desktop containing matcher.py and app.py.
- Python packages: pymupdf, scikit-learn, google-genai, streamlit.
- Google Gemini API key.
- Streamlit development server (running process).
Keep everything running
No action needed. Choose this if you plan to keep using or improving your resume matcher app.
- Your project files stay on your Desktop, ready to use anytime.
- Your Gemini API key remains active on the free tier with no charges.
- Your installed Python packages stay available for future projects.
- To relaunch the app later, open your terminal, navigate to your project folder, and run streamlit run app.py.
Pause - I'll come back to this later
Shut down the running Streamlit process to free up memory. Your files and packages stay in place for when you return.
- In the terminal where Streamlit is running, press Ctrl+C to stop the server.
- Your project files and installed packages remain untouched.
- To resume later, navigate back to your project folder and run streamlit run app.py.
Delete - I don't want to use this again
Remove all project resources and start fresh.
- Stop the Streamlit server by pressing Ctrl+C in the terminal where it is running.
- Delete your project folder by running the following command (replace the folder name with yours):
macOS
cd ~/Desktop
rm -rf [[PROJECT_FOLDER="your project folder name"]]
Windows
cd ~\Desktop
rmdir /s /q [[PROJECT_FOLDER="your project folder name"]]
- Uninstall the Python packages by running:
pip uninstall pymupdf scikit-learn google-genai streamlit -y
- Delete your Gemini API key by going to aistudio.google.com, clicking Get API Key, finding your key in the list, and clicking the trash icon to delete it.
- Unset the environment variable in your current terminal session:
macOS
unset GEMINI_API_KEY
Windows
Remove-Item Env:GEMINI_API_KEY
Nice Work!
Nice Work!
You just built a full-stack AI web app that reads your resume, scores it against a job posting, and generates tailored application materials on the spot.
You've learned how to:
- Build a PDF resume parser and TF-IDF cosine similarity scoring system that quantifies how well a resume matches any job description.
- Integrate the Google Gemini API to generate tailored resume rewrites and cover letters using prompt engineering.
- Create a polished Streamlit web interface with file upload, skill gap analysis, and download buttons.
- Secret Mission: Added a tabbed skill development action plan that generates a personalized 30-day learning roadmap powered by AI.
Ready to quiz yourself?