Spaces:
Sleeping
Sleeping
Upload 19 files
Browse files- AI_Resume.docx +0 -0
- Academic_Template.docx +0 -0
- Modern_Template.docx +0 -0
- Professional_Template.docx +0 -0
- README.md +53 -9
- app.py +691 -0
- average_match_resume.docx +0 -0
- average_match_resume.docx.txt +50 -0
- backend.py +819 -0
- create_templates.py +31 -0
- excellent_match_resume.docx +0 -0
- excellent_match_resume.docx.txt +55 -0
- find_error.py +16 -0
- good_match_resume.docx +0 -0
- good_match_resume.docx.txt +52 -0
- job_desc.txt +43 -0
- poor_match_resume.docx +0 -0
- poor_match_resume.docx.txt +52 -0
- requirements.txt +11 -0
AI_Resume.docx
ADDED
Binary file (56.5 kB). View file
|
|
Academic_Template.docx
ADDED
Binary file (37.5 kB). View file
|
|
Modern_Template.docx
ADDED
Binary file (37.5 kB). View file
|
|
Professional_Template.docx
ADDED
Binary file (37.3 kB). View file
|
|
README.md
CHANGED
@@ -1,14 +1,58 @@
|
|
1 |
---
|
2 |
-
title:
|
3 |
-
emoji:
|
4 |
-
colorFrom:
|
5 |
-
colorTo:
|
6 |
-
sdk:
|
7 |
-
sdk_version:
|
8 |
app_file: app.py
|
9 |
pinned: false
|
10 |
-
license:
|
11 |
-
short_description: AI powered resume analyzer/creator
|
12 |
---
|
13 |
|
14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
---
|
2 |
+
title: Resume Helper
|
3 |
+
emoji: 📄
|
4 |
+
colorFrom: blue
|
5 |
+
colorTo: green
|
6 |
+
sdk: streamlit
|
7 |
+
sdk_version: 1.28.0
|
8 |
app_file: app.py
|
9 |
pinned: false
|
10 |
+
license: mit
|
|
|
11 |
---
|
12 |
|
13 |
+
# Resume Helper
|
14 |
+
|
15 |
+
## Description
|
16 |
+
|
17 |
+
Resume Helper is an AI-powered application that helps you tailor your resume to specific job descriptions. Using OpenAI's GPT-4o model, it analyzes your resume against job requirements, provides a match percentage, identifies gaps, and suggests improvements. You can also generate a tailored version of your resume optimized for the specific job.
|
18 |
+
|
19 |
+
## Features
|
20 |
+
|
21 |
+
- **Resume Analysis**: Get a detailed analysis of how well your resume matches a job description
|
22 |
+
- **Resume Tailoring**: Generate a customized version of your resume for specific job applications
|
23 |
+
- **Match Percentage**: See a quantitative score of how well your resume matches the job requirements
|
24 |
+
- **Skill Breakdown**: View a detailed breakdown of your technical skills, experience, education, and soft skills
|
25 |
+
- **Gap Identification**: Identify missing skills or experiences that are important for the job
|
26 |
+
- **Improvement Suggestions**: Get actionable suggestions to improve your resume
|
27 |
+
- **Template Selection**: Choose from various resume templates
|
28 |
+
- **Verbosity Control**: Select between concise or detailed resume outputs
|
29 |
+
- **Creativity Control**: Adjust how creative the AI should be when tailoring your resume
|
30 |
+
- **Sample Resumes**: Use provided sample resumes to test the application
|
31 |
+
|
32 |
+
## How to Use
|
33 |
+
|
34 |
+
1. **API Key**: Enter your OpenAI API key in the sidebar (or use environment variables)
|
35 |
+
2. **Upload Resume**: Upload your resume in DOCX or PDF format, or select a sample resume
|
36 |
+
3. **Job Description**: Enter the job description you're applying for
|
37 |
+
4. **Template (Optional)**: Select a resume template if desired
|
38 |
+
5. **Verbosity**: Choose between "Concise" or "Elaborate" for your tailored resume
|
39 |
+
6. **Creativity Level**: Adjust how creative the AI should be (0-100%)
|
40 |
+
7. **Analyze**: Click "Analyze Resume" to get a detailed analysis
|
41 |
+
8. **Tailor**: Click "Tailor Resume" to generate a customized version of your resume
|
42 |
+
9. **Download**: Download your tailored resume in DOCX or TXT format
|
43 |
+
|
44 |
+
## Limitations
|
45 |
+
|
46 |
+
- The quality of analysis and tailoring depends on the quality of your resume and the job description
|
47 |
+
- Higher creativity levels may generate content that requires verification
|
48 |
+
- Always review AI-generated content before using it professionally
|
49 |
+
|
50 |
+
## Requirements
|
51 |
+
|
52 |
+
- Python 3.8+
|
53 |
+
- OpenAI API key
|
54 |
+
- Required Python packages (see requirements.txt)
|
55 |
+
|
56 |
+
## License
|
57 |
+
|
58 |
+
This project is licensed under the MIT License.
|
app.py
ADDED
@@ -0,0 +1,691 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import gradio as gr
|
2 |
+
import os
|
3 |
+
import tempfile
|
4 |
+
from backend import (
|
5 |
+
extract_text_from_document,
|
6 |
+
analyze_resume_job_match,
|
7 |
+
tailor_resume,
|
8 |
+
create_word_document,
|
9 |
+
create_tailored_resume_from_template,
|
10 |
+
get_available_templates,
|
11 |
+
set_openai_api_key,
|
12 |
+
set_openai_model
|
13 |
+
)
|
14 |
+
import plotly.graph_objects as go
|
15 |
+
import time as import_time
|
16 |
+
import json
|
17 |
+
|
18 |
+
# Define sample resumes
|
19 |
+
SAMPLE_RESUMES = {
|
20 |
+
"Excellent Match Resume": "excellent_match_resume.docx",
|
21 |
+
"Good Match Resume": "good_match_resume.docx",
|
22 |
+
"Average Match Resume": "average_match_resume.docx",
|
23 |
+
"Poor Match Resume": "poor_match_resume.docx"
|
24 |
+
}
|
25 |
+
|
26 |
+
# Check if sample files exist
|
27 |
+
for sample_name, sample_path in SAMPLE_RESUMES.items():
|
28 |
+
if not os.path.exists(sample_path):
|
29 |
+
print(f"Warning: Sample resume file not found: {sample_path}")
|
30 |
+
|
31 |
+
# Read default job description
|
32 |
+
def get_default_job_description():
|
33 |
+
try:
|
34 |
+
with open("job_desc.txt", "r") as file:
|
35 |
+
return file.read()
|
36 |
+
except:
|
37 |
+
return "Software Architect position requiring cloud expertise, microservices architecture, and leadership skills."
|
38 |
+
|
39 |
+
# Function to get color based on percentage
|
40 |
+
def get_color_for_percentage(percentage):
|
41 |
+
if percentage < 40:
|
42 |
+
return "#FF4B4B" # Red for poor match
|
43 |
+
elif percentage < 60:
|
44 |
+
return "#FFA500" # Orange for average match
|
45 |
+
elif percentage < 80:
|
46 |
+
return "#2E86C1" # Blue for good match
|
47 |
+
else:
|
48 |
+
return "#2ECC71" # Green for excellent match
|
49 |
+
|
50 |
+
# Function to create match gauge chart
|
51 |
+
def create_match_gauge(match_percentage):
|
52 |
+
if match_percentage < 40:
|
53 |
+
color = "#FF4B4B" # Red
|
54 |
+
elif match_percentage < 60:
|
55 |
+
color = "#FFA500" # Orange
|
56 |
+
elif match_percentage < 80:
|
57 |
+
color = "#2E86C1" # Blue
|
58 |
+
else:
|
59 |
+
color = "#2ECC71" # Green
|
60 |
+
|
61 |
+
fig = go.Figure(go.Indicator(
|
62 |
+
mode="gauge+number",
|
63 |
+
value=match_percentage,
|
64 |
+
domain={'x': [0, 1], 'y': [0, 1]},
|
65 |
+
title={'text': "Match Percentage"},
|
66 |
+
gauge={
|
67 |
+
'axis': {'range': [0, 100], 'tickwidth': 1, 'tickcolor': "darkblue"},
|
68 |
+
'bar': {'color': color},
|
69 |
+
'bgcolor': "white",
|
70 |
+
'borderwidth': 2,
|
71 |
+
'bordercolor': "gray",
|
72 |
+
'steps': [
|
73 |
+
{'range': [0, 40], 'color': 'rgba(255, 75, 75, 0.2)'}, # Light red
|
74 |
+
{'range': [40, 60], 'color': 'rgba(255, 165, 0, 0.2)'}, # Light orange
|
75 |
+
{'range': [60, 80], 'color': 'rgba(46, 134, 193, 0.2)'}, # Light blue
|
76 |
+
{'range': [80, 100], 'color': 'rgba(46, 204, 113, 0.2)'} # Light green
|
77 |
+
],
|
78 |
+
}
|
79 |
+
))
|
80 |
+
|
81 |
+
fig.update_layout(
|
82 |
+
height=250,
|
83 |
+
margin=dict(l=20, r=20, t=50, b=20),
|
84 |
+
paper_bgcolor="rgba(0,0,0,0)",
|
85 |
+
plot_bgcolor="rgba(0,0,0,0)",
|
86 |
+
font={'color': "#444", 'family': "Arial"}
|
87 |
+
)
|
88 |
+
|
89 |
+
return fig
|
90 |
+
|
91 |
+
# Function to handle resume upload
|
92 |
+
def handle_resume_upload(file):
|
93 |
+
if file is None:
|
94 |
+
return None, None
|
95 |
+
|
96 |
+
# Save uploaded file
|
97 |
+
temp_dir = tempfile.mkdtemp()
|
98 |
+
temp_path = os.path.join(temp_dir, file.name)
|
99 |
+
with open(temp_path, "wb") as f:
|
100 |
+
f.write(file)
|
101 |
+
|
102 |
+
# Extract text
|
103 |
+
resume_text = extract_text_from_document(temp_path)
|
104 |
+
template_path = temp_path if temp_path.endswith('.docx') else None
|
105 |
+
|
106 |
+
return resume_text, template_path
|
107 |
+
|
108 |
+
# Function to handle sample resume selection
|
109 |
+
def handle_sample_resume(sample_name):
|
110 |
+
if not sample_name:
|
111 |
+
return None, None
|
112 |
+
|
113 |
+
resume_path = SAMPLE_RESUMES[sample_name]
|
114 |
+
if os.path.exists(resume_path):
|
115 |
+
# Extract text
|
116 |
+
resume_text = extract_text_from_document(resume_path)
|
117 |
+
template_path = resume_path
|
118 |
+
return resume_text, template_path
|
119 |
+
else:
|
120 |
+
print(f"Sample resume file not found: {resume_path}")
|
121 |
+
return None, None
|
122 |
+
|
123 |
+
# Function to handle template selection
|
124 |
+
def handle_template_selection(use_template, selected_template):
|
125 |
+
if use_template and selected_template != "None":
|
126 |
+
return selected_template
|
127 |
+
return None
|
128 |
+
|
129 |
+
# Function to analyze resume
|
130 |
+
def analyze_resume(resume_text, job_description, creativity_level):
|
131 |
+
if not resume_text or not job_description:
|
132 |
+
return None, "Please provide both a resume and job description."
|
133 |
+
|
134 |
+
try:
|
135 |
+
analysis_results = analyze_resume_job_match(
|
136 |
+
resume_text,
|
137 |
+
job_description,
|
138 |
+
creativity_level
|
139 |
+
)
|
140 |
+
|
141 |
+
# Create the match gauge chart
|
142 |
+
match_percentage = analysis_results.get("match_percentage", 0)
|
143 |
+
gauge_chart = create_match_gauge(match_percentage)
|
144 |
+
|
145 |
+
# Determine match category and description
|
146 |
+
if match_percentage < 40:
|
147 |
+
match_category = "Poor Match"
|
148 |
+
match_description = "Your resume needs significant improvements to match this job description."
|
149 |
+
elif match_percentage < 60:
|
150 |
+
match_category = "Average Match"
|
151 |
+
match_description = "Your resume partially matches the job description but could use improvements."
|
152 |
+
elif match_percentage < 80:
|
153 |
+
match_category = "Good Match"
|
154 |
+
match_description = "Your resume matches well with the job description with some room for improvement."
|
155 |
+
else:
|
156 |
+
match_category = "Excellent Match"
|
157 |
+
match_description = "Your resume is very well aligned with the job description!"
|
158 |
+
|
159 |
+
# Format the analysis results for display
|
160 |
+
formatted_results = f"## Match Analysis\n\n"
|
161 |
+
formatted_results += f"**Match Category:** {match_category}\n\n"
|
162 |
+
formatted_results += f"**Match Description:** {match_description}\n\n"
|
163 |
+
|
164 |
+
# Add skill breakdown
|
165 |
+
if "skill_breakdown" in analysis_results:
|
166 |
+
formatted_results += "## Skill Breakdown\n\n"
|
167 |
+
skill_breakdown = analysis_results["skill_breakdown"]
|
168 |
+
|
169 |
+
for skill_type, skill_data in skill_breakdown.items():
|
170 |
+
formatted_results += f"### {skill_type.replace('_', ' ').title()}\n"
|
171 |
+
formatted_results += f"**Percentage:** {skill_data.get('percentage', 0)}%\n"
|
172 |
+
formatted_results += f"**Comments:** {skill_data.get('comments', '')}\n\n"
|
173 |
+
|
174 |
+
# Add key matches
|
175 |
+
if "key_matches" in analysis_results:
|
176 |
+
formatted_results += "## Key Matches\n\n"
|
177 |
+
for match in analysis_results["key_matches"]:
|
178 |
+
formatted_results += f"- {match}\n"
|
179 |
+
formatted_results += "\n"
|
180 |
+
|
181 |
+
# Add gaps
|
182 |
+
if "gaps" in analysis_results:
|
183 |
+
formatted_results += "## Gaps Identified\n\n"
|
184 |
+
for gap in analysis_results["gaps"]:
|
185 |
+
formatted_results += f"- {gap}\n"
|
186 |
+
formatted_results += "\n"
|
187 |
+
|
188 |
+
# Add suggestions
|
189 |
+
if "suggestions" in analysis_results:
|
190 |
+
formatted_results += "## Improvement Suggestions\n\n"
|
191 |
+
for suggestion in analysis_results["suggestions"]:
|
192 |
+
formatted_results += f"- {suggestion}\n"
|
193 |
+
formatted_results += "\n"
|
194 |
+
|
195 |
+
# Add summary
|
196 |
+
if "summary" in analysis_results:
|
197 |
+
formatted_results += "## Summary\n\n"
|
198 |
+
formatted_results += analysis_results["summary"]
|
199 |
+
|
200 |
+
return analysis_results, formatted_results
|
201 |
+
except Exception as e:
|
202 |
+
return None, f"Error analyzing resume: {str(e)}"
|
203 |
+
|
204 |
+
# Function to tailor resume
|
205 |
+
def tailor_resume_func(resume_text, job_description, template_path, creativity_level, verbosity):
|
206 |
+
if not resume_text or not job_description:
|
207 |
+
return None, "Please provide both a resume and job description."
|
208 |
+
|
209 |
+
try:
|
210 |
+
tailored_resume = tailor_resume(
|
211 |
+
resume_text,
|
212 |
+
job_description,
|
213 |
+
template_path,
|
214 |
+
creativity_level,
|
215 |
+
verbosity
|
216 |
+
)
|
217 |
+
|
218 |
+
return tailored_resume, "Resume tailored successfully!"
|
219 |
+
except Exception as e:
|
220 |
+
return None, f"Error tailoring resume: {str(e)}"
|
221 |
+
|
222 |
+
# Function to create and download Word document
|
223 |
+
def create_word_doc(tailored_resume, template_path):
|
224 |
+
if not tailored_resume:
|
225 |
+
return None, "No tailored resume to download."
|
226 |
+
|
227 |
+
try:
|
228 |
+
# Create a temporary file
|
229 |
+
temp_dir = tempfile.mkdtemp()
|
230 |
+
output_path = os.path.join(temp_dir, "tailored_resume.docx")
|
231 |
+
|
232 |
+
# Create Word document
|
233 |
+
if template_path and os.path.exists(template_path):
|
234 |
+
success = create_tailored_resume_from_template(
|
235 |
+
tailored_resume,
|
236 |
+
template_path,
|
237 |
+
output_path
|
238 |
+
)
|
239 |
+
else:
|
240 |
+
success = create_word_document(
|
241 |
+
tailored_resume,
|
242 |
+
output_path
|
243 |
+
)
|
244 |
+
|
245 |
+
if success:
|
246 |
+
return output_path, "DOCX file created successfully!"
|
247 |
+
else:
|
248 |
+
return None, "Failed to create DOCX file."
|
249 |
+
except Exception as e:
|
250 |
+
return None, f"Error creating DOCX file: {str(e)}"
|
251 |
+
|
252 |
+
# Function to update API key
|
253 |
+
def update_api_key(api_key):
|
254 |
+
if not api_key:
|
255 |
+
return "Using API key from environment variables if available."
|
256 |
+
|
257 |
+
api_configured = set_openai_api_key(api_key)
|
258 |
+
if api_configured:
|
259 |
+
return "✅ API key configured successfully!"
|
260 |
+
else:
|
261 |
+
return "❌ Failed to configure API key."
|
262 |
+
|
263 |
+
# Function to update model
|
264 |
+
def update_model(model):
|
265 |
+
set_openai_model(model)
|
266 |
+
return f"✅ Model set to {model}"
|
267 |
+
|
268 |
+
# Main function to create the Gradio interface
|
269 |
+
def create_interface():
|
270 |
+
print("Creating interface...")
|
271 |
+
# Define the blocks with custom theme
|
272 |
+
with gr.Blocks(title="Resume Helper", theme=gr.themes.Soft(
|
273 |
+
primary_hue="blue",
|
274 |
+
secondary_hue="indigo",
|
275 |
+
font=[gr.themes.GoogleFont("Poppins"), "ui-sans-serif", "system-ui", "sans-serif"],
|
276 |
+
)) as app:
|
277 |
+
gr.Markdown("# 📄 Resume Helper")
|
278 |
+
gr.Markdown("Upload your resume and get AI-powered analysis and tailoring to match job descriptions.")
|
279 |
+
|
280 |
+
# State variables
|
281 |
+
resume_text = gr.State(None)
|
282 |
+
template_path = gr.State(None)
|
283 |
+
analysis_results = gr.State(None)
|
284 |
+
tailored_resume_text = gr.State(None)
|
285 |
+
|
286 |
+
with gr.Row():
|
287 |
+
# Left column - Inputs
|
288 |
+
with gr.Column(scale=1):
|
289 |
+
# Configuration section
|
290 |
+
with gr.Accordion("⚙️ Configuration", open=False):
|
291 |
+
api_key = gr.Textbox(
|
292 |
+
label="OpenAI API Key",
|
293 |
+
placeholder="Enter your OpenAI API key (optional)",
|
294 |
+
type="password"
|
295 |
+
)
|
296 |
+
api_status = gr.Markdown("Using API key from environment variables if available.")
|
297 |
+
api_key.change(update_api_key, inputs=[api_key], outputs=[api_status])
|
298 |
+
|
299 |
+
model_options = ["gpt-4o-mini", "gpt-4o"]
|
300 |
+
model_selector = gr.Dropdown(
|
301 |
+
label="Select AI Model",
|
302 |
+
choices=model_options,
|
303 |
+
value="gpt-4o-mini",
|
304 |
+
info="Choose the OpenAI model to use. GPT-4o-mini is faster and cheaper, while GPT-4o provides more detailed analysis."
|
305 |
+
)
|
306 |
+
model_status = gr.Markdown("")
|
307 |
+
model_selector.change(update_model, inputs=[model_selector], outputs=[model_status])
|
308 |
+
|
309 |
+
# Resume upload section
|
310 |
+
gr.Markdown("### 📤 Upload Your Resume")
|
311 |
+
resume_option = gr.Radio(
|
312 |
+
label="Choose an option:",
|
313 |
+
choices=["Upload my resume", "Use a sample resume"],
|
314 |
+
value="Upload my resume"
|
315 |
+
)
|
316 |
+
|
317 |
+
# Upload resume file
|
318 |
+
upload_file = gr.File(
|
319 |
+
label="Upload your resume (DOCX, PDF)",
|
320 |
+
file_types=[".docx", ".pdf"],
|
321 |
+
visible=True
|
322 |
+
)
|
323 |
+
|
324 |
+
# Sample resume selection
|
325 |
+
sample_resume = gr.Dropdown(
|
326 |
+
label="Select a sample resume:",
|
327 |
+
choices=list(SAMPLE_RESUMES.keys()),
|
328 |
+
visible=False
|
329 |
+
)
|
330 |
+
|
331 |
+
# Show/hide based on selection
|
332 |
+
def update_resume_option(option):
|
333 |
+
return {
|
334 |
+
upload_file: gr.update(visible=option == "Upload my resume"),
|
335 |
+
sample_resume: gr.update(visible=option == "Use a sample resume")
|
336 |
+
}
|
337 |
+
|
338 |
+
resume_option.change(update_resume_option, inputs=[resume_option], outputs=[upload_file, sample_resume])
|
339 |
+
|
340 |
+
# Template selection
|
341 |
+
gr.Markdown("### 📄 Select Resume Template (Optional)")
|
342 |
+
templates = get_available_templates()
|
343 |
+
print(f"Templates: {templates}")
|
344 |
+
use_template = gr.Checkbox(label="Use a resume template", value=False)
|
345 |
+
template_selector = gr.Dropdown(
|
346 |
+
label="Choose a template:",
|
347 |
+
choices=["None"] + templates,
|
348 |
+
value="None",
|
349 |
+
visible=False
|
350 |
+
)
|
351 |
+
print(f"Template selector: ")
|
352 |
+
|
353 |
+
def update_template_visibility(use_template):
|
354 |
+
return gr.update(visible=use_template)
|
355 |
+
|
356 |
+
use_template.change(update_template_visibility, inputs=[use_template], outputs=[template_selector])
|
357 |
+
|
358 |
+
# Job description
|
359 |
+
gr.Markdown("### 📋 Job Description")
|
360 |
+
job_description = gr.Textbox(
|
361 |
+
label="Job Description",
|
362 |
+
value=get_default_job_description(),
|
363 |
+
lines=10
|
364 |
+
)
|
365 |
+
|
366 |
+
# MOVED FROM RIGHT COLUMN: Resume detail level
|
367 |
+
gr.Markdown("### 📝 Resume Detail Level")
|
368 |
+
verbosity = gr.Radio(
|
369 |
+
label="Choose how detailed your tailored resume should be:",
|
370 |
+
choices=["Concise", "Elaborate"],
|
371 |
+
value="Elaborate"
|
372 |
+
)
|
373 |
+
|
374 |
+
# MOVED FROM RIGHT COLUMN: Creativity level
|
375 |
+
gr.Markdown("### 🎨 Creativity Level")
|
376 |
+
creativity_level = gr.Slider(
|
377 |
+
label="Adjust how creative the AI should be when tailoring your resume",
|
378 |
+
minimum=0,
|
379 |
+
maximum=100,
|
380 |
+
value=30,
|
381 |
+
step=10,
|
382 |
+
info="Higher values mean more creative modifications to your resume"
|
383 |
+
)
|
384 |
+
|
385 |
+
creativity_warning = gr.Markdown(visible=False)
|
386 |
+
|
387 |
+
def update_creativity_warning(level):
|
388 |
+
if level > 70:
|
389 |
+
return gr.update(visible=True, value="⚠️ High creativity levels may generate content that significantly modifies your original resume. Review carefully before using.")
|
390 |
+
else:
|
391 |
+
return gr.update(visible=False)
|
392 |
+
|
393 |
+
creativity_level.change(update_creativity_warning, inputs=[creativity_level], outputs=[creativity_warning])
|
394 |
+
|
395 |
+
# MOVED FROM RIGHT COLUMN: Action buttons
|
396 |
+
gr.Markdown("### 🚀 Actions")
|
397 |
+
with gr.Row():
|
398 |
+
analyze_btn = gr.Button("🔍 Analyze Resume", variant="primary")
|
399 |
+
tailor_btn = gr.Button("✏️ Tailor Resume", variant="primary")
|
400 |
+
reset_btn = gr.Button("🔄 Reset All", variant="secondary")
|
401 |
+
|
402 |
+
# Add loading indicator below the buttons
|
403 |
+
loading_indicator = gr.Markdown(visible=False)
|
404 |
+
|
405 |
+
# Right column - Results
|
406 |
+
with gr.Column(scale=1):
|
407 |
+
# Results section - Now directly in the right column, not in an accordion
|
408 |
+
with gr.Tabs() as results_tabs:
|
409 |
+
# Analysis tab
|
410 |
+
with gr.TabItem("📊 Analysis"):
|
411 |
+
analysis_plot = gr.Plot(label="Match Percentage")
|
412 |
+
analysis_output = gr.Markdown()
|
413 |
+
|
414 |
+
# Tailored Resume tab
|
415 |
+
with gr.TabItem("📝 Tailored Resume"):
|
416 |
+
tailored_resume = gr.Textbox(label="Tailored Resume", lines=15)
|
417 |
+
|
418 |
+
# Create download buttons but initially hide them
|
419 |
+
with gr.Row():
|
420 |
+
download_docx = gr.Button("📄 Download as DOCX", variant="primary", visible=False)
|
421 |
+
download_txt = gr.Button("📝 Download as TXT", variant="primary", visible=False)
|
422 |
+
|
423 |
+
# Create file components but initially hide them
|
424 |
+
docx_file = gr.File(label="Download DOCX", visible=False)
|
425 |
+
txt_file = gr.File(label="Download TXT", visible=False)
|
426 |
+
|
427 |
+
download_status = gr.Markdown()
|
428 |
+
|
429 |
+
# Event handlers
|
430 |
+
def process_resume_input(resume_opt, upload_file, sample_name, use_template_opt, template_selection):
|
431 |
+
if resume_opt == "Upload my resume" and upload_file is not None:
|
432 |
+
resume_text, template_path = handle_resume_upload(upload_file)
|
433 |
+
elif resume_opt == "Use a sample resume" and sample_name:
|
434 |
+
resume_text, template_path = handle_sample_resume(sample_name)
|
435 |
+
else:
|
436 |
+
resume_text, template_path = None, None
|
437 |
+
|
438 |
+
if use_template_opt and template_selection != "None":
|
439 |
+
template_path = template_selection
|
440 |
+
|
441 |
+
return resume_text, template_path
|
442 |
+
|
443 |
+
# Handle file upload
|
444 |
+
upload_file.upload(
|
445 |
+
lambda file: process_resume_input("Upload my resume", file, None, use_template.value, template_selector.value),
|
446 |
+
inputs=[upload_file],
|
447 |
+
outputs=[resume_text, template_path]
|
448 |
+
)
|
449 |
+
|
450 |
+
# Handle sample selection
|
451 |
+
sample_resume.change(
|
452 |
+
lambda sample: process_resume_input("Use a sample resume", None, sample, use_template.value, template_selector.value),
|
453 |
+
inputs=[sample_resume],
|
454 |
+
outputs=[resume_text, template_path]
|
455 |
+
)
|
456 |
+
|
457 |
+
# Handle template selection
|
458 |
+
template_selector.change(
|
459 |
+
lambda template, resume_txt, current_template: (resume_txt, template if template != "None" else current_template),
|
460 |
+
inputs=[template_selector, resume_text, template_path],
|
461 |
+
outputs=[resume_text, template_path]
|
462 |
+
)
|
463 |
+
|
464 |
+
# Analyze button handler with loading indicator
|
465 |
+
def analyze_with_loading(resume_txt, job_desc, creativity):
|
466 |
+
if not resume_txt:
|
467 |
+
return (
|
468 |
+
gr.update(visible=False),
|
469 |
+
None,
|
470 |
+
gr.update(visible=False),
|
471 |
+
"",
|
472 |
+
gr.update(interactive=True),
|
473 |
+
gr.update(interactive=True),
|
474 |
+
gr.update(interactive=True)
|
475 |
+
)
|
476 |
+
|
477 |
+
# Show loading message and disable buttons
|
478 |
+
yield (
|
479 |
+
gr.update(visible=True, value="⏳ Analyzing your resume... This may take a moment."),
|
480 |
+
None,
|
481 |
+
gr.update(visible=False),
|
482 |
+
"",
|
483 |
+
gr.update(interactive=False),
|
484 |
+
gr.update(interactive=False),
|
485 |
+
gr.update(interactive=False)
|
486 |
+
)
|
487 |
+
|
488 |
+
# Perform the actual analysis
|
489 |
+
results, formatted_output = analyze_resume(resume_txt, job_desc, creativity)
|
490 |
+
|
491 |
+
# Hide loading, show results, and re-enable buttons
|
492 |
+
if results:
|
493 |
+
match_percentage = results.get("match_percentage", 0)
|
494 |
+
gauge_chart = create_match_gauge(match_percentage)
|
495 |
+
yield (
|
496 |
+
gr.update(visible=False),
|
497 |
+
results,
|
498 |
+
gauge_chart,
|
499 |
+
formatted_output,
|
500 |
+
gr.update(interactive=True),
|
501 |
+
gr.update(interactive=True),
|
502 |
+
gr.update(interactive=True)
|
503 |
+
)
|
504 |
+
else:
|
505 |
+
yield (
|
506 |
+
gr.update(visible=False),
|
507 |
+
None,
|
508 |
+
gr.update(visible=False),
|
509 |
+
formatted_output,
|
510 |
+
gr.update(interactive=True),
|
511 |
+
gr.update(interactive=True),
|
512 |
+
gr.update(interactive=True)
|
513 |
+
)
|
514 |
+
|
515 |
+
analyze_btn.click(
|
516 |
+
analyze_with_loading,
|
517 |
+
inputs=[resume_text, job_description, creativity_level],
|
518 |
+
outputs=[
|
519 |
+
loading_indicator,
|
520 |
+
analysis_results,
|
521 |
+
analysis_plot,
|
522 |
+
analysis_output,
|
523 |
+
analyze_btn,
|
524 |
+
tailor_btn,
|
525 |
+
reset_btn
|
526 |
+
],
|
527 |
+
queue=True
|
528 |
+
)
|
529 |
+
|
530 |
+
# Tailor button handler with loading indicator
|
531 |
+
def tailor_with_loading(resume_txt, job_desc, template, creativity, verbosity_level):
|
532 |
+
if not resume_txt:
|
533 |
+
return (
|
534 |
+
gr.update(visible=False),
|
535 |
+
None,
|
536 |
+
"",
|
537 |
+
gr.update(visible=False),
|
538 |
+
gr.update(visible=False),
|
539 |
+
gr.update(interactive=True),
|
540 |
+
gr.update(interactive=True),
|
541 |
+
gr.update(interactive=True)
|
542 |
+
)
|
543 |
+
|
544 |
+
# Show loading message and disable buttons
|
545 |
+
yield (
|
546 |
+
gr.update(visible=True, value="⏳ Tailoring your resume... This may take a moment."),
|
547 |
+
None,
|
548 |
+
"",
|
549 |
+
gr.update(visible=False),
|
550 |
+
gr.update(visible=False),
|
551 |
+
gr.update(interactive=False),
|
552 |
+
gr.update(interactive=False),
|
553 |
+
gr.update(interactive=False)
|
554 |
+
)
|
555 |
+
|
556 |
+
# Perform the actual tailoring
|
557 |
+
tailored, message = tailor_resume_func(
|
558 |
+
resume_txt,
|
559 |
+
job_desc,
|
560 |
+
template,
|
561 |
+
creativity,
|
562 |
+
verbosity_level.lower()
|
563 |
+
)
|
564 |
+
|
565 |
+
# Hide loading, show results, and re-enable buttons
|
566 |
+
if tailored:
|
567 |
+
# Show download buttons only when tailored resume is created
|
568 |
+
yield (
|
569 |
+
gr.update(visible=False),
|
570 |
+
tailored,
|
571 |
+
tailored,
|
572 |
+
gr.update(visible=True),
|
573 |
+
gr.update(visible=True),
|
574 |
+
gr.update(interactive=True),
|
575 |
+
gr.update(interactive=True),
|
576 |
+
gr.update(interactive=True)
|
577 |
+
)
|
578 |
+
else:
|
579 |
+
# Hide download buttons if tailoring fails
|
580 |
+
yield (
|
581 |
+
gr.update(visible=False),
|
582 |
+
None,
|
583 |
+
message,
|
584 |
+
gr.update(visible=False),
|
585 |
+
gr.update(visible=False),
|
586 |
+
gr.update(interactive=True),
|
587 |
+
gr.update(interactive=True),
|
588 |
+
gr.update(interactive=True)
|
589 |
+
)
|
590 |
+
|
591 |
+
tailor_btn.click(
|
592 |
+
tailor_with_loading,
|
593 |
+
inputs=[resume_text, job_description, template_path, creativity_level, verbosity],
|
594 |
+
outputs=[
|
595 |
+
loading_indicator,
|
596 |
+
tailored_resume_text,
|
597 |
+
tailored_resume,
|
598 |
+
download_docx,
|
599 |
+
download_txt,
|
600 |
+
analyze_btn,
|
601 |
+
tailor_btn,
|
602 |
+
reset_btn
|
603 |
+
],
|
604 |
+
queue=True
|
605 |
+
)
|
606 |
+
|
607 |
+
# Download handlers
|
608 |
+
def create_docx_handler(tailored_txt, template):
|
609 |
+
if not tailored_txt:
|
610 |
+
return gr.update(visible=False), "No tailored resume to download."
|
611 |
+
|
612 |
+
file_path, message = create_word_doc(tailored_txt, template)
|
613 |
+
if file_path:
|
614 |
+
return gr.update(visible=True, value=file_path), message
|
615 |
+
else:
|
616 |
+
return gr.update(visible=False), message
|
617 |
+
|
618 |
+
download_docx.click(
|
619 |
+
create_docx_handler,
|
620 |
+
inputs=[tailored_resume_text, template_path],
|
621 |
+
outputs=[docx_file, download_status]
|
622 |
+
)
|
623 |
+
|
624 |
+
# Download as TXT
|
625 |
+
def download_txt_handler(tailored_txt):
|
626 |
+
if not tailored_txt:
|
627 |
+
return gr.update(visible=False), "No tailored resume to download."
|
628 |
+
|
629 |
+
# Create a temporary file
|
630 |
+
temp_dir = tempfile.mkdtemp()
|
631 |
+
output_path = os.path.join(temp_dir, "tailored_resume.txt")
|
632 |
+
|
633 |
+
with open(output_path, "w") as f:
|
634 |
+
f.write(tailored_txt)
|
635 |
+
|
636 |
+
return gr.update(visible=True, value=output_path), "TXT file created successfully!"
|
637 |
+
|
638 |
+
download_txt.click(
|
639 |
+
download_txt_handler,
|
640 |
+
inputs=[tailored_resume_text],
|
641 |
+
outputs=[txt_file, download_status]
|
642 |
+
)
|
643 |
+
|
644 |
+
# Reset handler
|
645 |
+
def reset_all():
|
646 |
+
return (
|
647 |
+
None, None, None, None,
|
648 |
+
gr.update(value=None),
|
649 |
+
gr.update(value="Upload my resume"),
|
650 |
+
gr.update(value=None),
|
651 |
+
gr.update(value=None),
|
652 |
+
gr.update(value="None"),
|
653 |
+
gr.update(value=get_default_job_description()),
|
654 |
+
gr.update(value="Elaborate"),
|
655 |
+
gr.update(value=30),
|
656 |
+
gr.update(visible=False),
|
657 |
+
gr.update(value=""),
|
658 |
+
gr.update(value=""),
|
659 |
+
gr.update(visible=False),
|
660 |
+
gr.update(visible=False),
|
661 |
+
gr.update(visible=False),
|
662 |
+
gr.update(visible=False)
|
663 |
+
)
|
664 |
+
|
665 |
+
reset_btn.click(
|
666 |
+
reset_all,
|
667 |
+
inputs=[],
|
668 |
+
outputs=[
|
669 |
+
resume_text, template_path, analysis_results, tailored_resume_text,
|
670 |
+
upload_file, resume_option, sample_resume, use_template, template_selector,
|
671 |
+
job_description, verbosity, creativity_level, creativity_warning,
|
672 |
+
analysis_output, tailored_resume,
|
673 |
+
download_docx, download_txt, docx_file, txt_file
|
674 |
+
]
|
675 |
+
)
|
676 |
+
|
677 |
+
# Footer
|
678 |
+
gr.Markdown("---")
|
679 |
+
gr.Markdown("### 📝 Disclaimer")
|
680 |
+
gr.Markdown("""
|
681 |
+
This tool uses AI to analyze and tailor resumes. While it strives for accuracy, please review all generated content before using it professionally.
|
682 |
+
Higher creativity levels may generate content that requires more thorough verification. Always ensure that your resume accurately represents your skills and experience.
|
683 |
+
""")
|
684 |
+
print("Interface created successfully")
|
685 |
+
return app
|
686 |
+
|
687 |
+
# Launch the app
|
688 |
+
if __name__ == "__main__":
|
689 |
+
app = create_interface()
|
690 |
+
app.queue() # Enable the queue for the app
|
691 |
+
app.launch()
|
average_match_resume.docx
ADDED
Binary file (37.8 kB). View file
|
|
average_match_resume.docx.txt
ADDED
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
DAVID CHEN
|
2 |
+
Software Developer
|
3 |
+
[email protected] | (555) 456-7890 | linkedin.com/in/davidchen
|
4 |
+
Boston, MA | Remote Available
|
5 |
+
|
6 |
+
PROFESSIONAL SUMMARY
|
7 |
+
Software Developer with 6 years of experience building web applications and services. Experienced in full-stack development with a focus on backend systems and API development. Seeking to leverage my technical skills in a software architecture role.
|
8 |
+
|
9 |
+
SKILLS
|
10 |
+
• Programming: Java, JavaScript, TypeScript, Node.js
|
11 |
+
• Web Technologies: React, Angular, HTML, CSS
|
12 |
+
• Databases: MySQL, PostgreSQL, MongoDB
|
13 |
+
• Cloud: Basic AWS (EC2, S3)
|
14 |
+
• Tools: Git, JIRA, VS Code
|
15 |
+
• Testing: Jest, JUnit, Selenium
|
16 |
+
• Methodologies: Agile, Scrum
|
17 |
+
|
18 |
+
PROFESSIONAL EXPERIENCE
|
19 |
+
|
20 |
+
SENIOR SOFTWARE DEVELOPER | WebTech Solutions | 2019 - Present
|
21 |
+
• Develop and maintain full-stack web applications using React, Node.js, and PostgreSQL
|
22 |
+
• Design and implement RESTful APIs for internal and external service integration
|
23 |
+
• Collaborate with product managers to understand business requirements
|
24 |
+
• Participate in code reviews and provide feedback to team members
|
25 |
+
• Implement automated testing using Jest and Selenium
|
26 |
+
• Deploy applications to AWS using EC2 and S3
|
27 |
+
• Mentor junior developers on coding standards and best practices
|
28 |
+
|
29 |
+
SOFTWARE DEVELOPER | Digital Creations | 2016 - 2019
|
30 |
+
• Developed front-end interfaces using Angular and TypeScript
|
31 |
+
• Created and maintained backend services using Java Spring Boot
|
32 |
+
• Implemented database schemas and queries in MySQL
|
33 |
+
• Participated in agile development processes, including daily stand-ups and sprint planning
|
34 |
+
• Collaborated with UX designers to implement user interfaces
|
35 |
+
• Fixed bugs and improved application performance
|
36 |
+
• Documented code and created technical specifications
|
37 |
+
|
38 |
+
JUNIOR DEVELOPER | Tech Startups Inc. | 2015 - 2016
|
39 |
+
• Assisted in the development of web applications using JavaScript and PHP
|
40 |
+
• Created responsive web designs using HTML and CSS
|
41 |
+
• Fixed bugs and implemented minor features
|
42 |
+
• Participated in code reviews and team meetings
|
43 |
+
• Learned and applied new technologies and frameworks
|
44 |
+
|
45 |
+
EDUCATION
|
46 |
+
• Bachelor of Science in Computer Science, Boston University, 2015
|
47 |
+
|
48 |
+
CERTIFICATIONS
|
49 |
+
• Oracle Certified Professional, Java SE 8 Programmer
|
50 |
+
• MongoDB Certified Developer
|
backend.py
ADDED
@@ -0,0 +1,819 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import docx2txt
|
3 |
+
import docx
|
4 |
+
from docx import Document
|
5 |
+
import openai
|
6 |
+
from dotenv import load_dotenv
|
7 |
+
import json
|
8 |
+
import tempfile
|
9 |
+
import re
|
10 |
+
|
11 |
+
# Add these imports for PDF support
|
12 |
+
import PyPDF2
|
13 |
+
import io
|
14 |
+
|
15 |
+
# Load environment variables
|
16 |
+
load_dotenv()
|
17 |
+
|
18 |
+
# Set up OpenAI API key
|
19 |
+
openai.api_key = os.getenv("OPENAI_API_KEY")
|
20 |
+
|
21 |
+
# Default model
|
22 |
+
DEFAULT_MODEL = "gpt-4o"
|
23 |
+
current_model = DEFAULT_MODEL
|
24 |
+
|
25 |
+
# Function to set OpenAI API key
|
26 |
+
def set_openai_api_key(api_key=None):
|
27 |
+
"""
|
28 |
+
Set the OpenAI API key from the provided key or environment variable.
|
29 |
+
|
30 |
+
Args:
|
31 |
+
api_key: Optional API key to use. If None, will use the environment variable.
|
32 |
+
|
33 |
+
Returns:
|
34 |
+
bool: True if API key is set, False otherwise
|
35 |
+
"""
|
36 |
+
if api_key:
|
37 |
+
openai.api_key = api_key
|
38 |
+
return True
|
39 |
+
else:
|
40 |
+
env_api_key = os.getenv("OPENAI_API_KEY")
|
41 |
+
if env_api_key:
|
42 |
+
openai.api_key = env_api_key
|
43 |
+
return True
|
44 |
+
return False
|
45 |
+
|
46 |
+
# Function to set OpenAI model
|
47 |
+
def set_openai_model(model_name="gpt-4o"):
|
48 |
+
"""
|
49 |
+
Set the OpenAI model to use for API calls.
|
50 |
+
|
51 |
+
Args:
|
52 |
+
model_name: Name of the model to use (e.g., "gpt-4o", "gpt-4o-mini")
|
53 |
+
|
54 |
+
Returns:
|
55 |
+
str: The name of the model that was set
|
56 |
+
"""
|
57 |
+
global current_model
|
58 |
+
current_model = model_name
|
59 |
+
return current_model
|
60 |
+
|
61 |
+
# Set up OpenAI API key from environment variable initially
|
62 |
+
set_openai_api_key()
|
63 |
+
|
64 |
+
def extract_text_from_document(file_path):
|
65 |
+
"""
|
66 |
+
Extract text from a document file (DOCX or PDF).
|
67 |
+
|
68 |
+
Args:
|
69 |
+
file_path: Path to the document file
|
70 |
+
|
71 |
+
Returns:
|
72 |
+
str: Extracted text from the document
|
73 |
+
"""
|
74 |
+
try:
|
75 |
+
# Check file extension
|
76 |
+
if file_path.lower().endswith('.docx'):
|
77 |
+
# Extract text from DOCX
|
78 |
+
return docx2txt.process(file_path)
|
79 |
+
elif file_path.lower().endswith('.pdf'):
|
80 |
+
# Extract text from PDF
|
81 |
+
text = ""
|
82 |
+
with open(file_path, 'rb') as file:
|
83 |
+
pdf_reader = PyPDF2.PdfReader(file)
|
84 |
+
for page_num in range(len(pdf_reader.pages)):
|
85 |
+
page = pdf_reader.pages[page_num]
|
86 |
+
text += page.extract_text() + "\n\n"
|
87 |
+
return text
|
88 |
+
else:
|
89 |
+
raise ValueError(f"Unsupported file format: {os.path.splitext(file_path)[1]}")
|
90 |
+
except Exception as e:
|
91 |
+
print(f"Error extracting text from document: {e}")
|
92 |
+
return None
|
93 |
+
|
94 |
+
def parse_resume(file_path):
|
95 |
+
"""
|
96 |
+
Parse a resume file and extract its content.
|
97 |
+
|
98 |
+
Args:
|
99 |
+
file_path: Path to the resume file
|
100 |
+
|
101 |
+
Returns:
|
102 |
+
str: Extracted text from the resume
|
103 |
+
"""
|
104 |
+
try:
|
105 |
+
return extract_text_from_document(file_path)
|
106 |
+
except Exception as e:
|
107 |
+
print(f"Error parsing resume: {e}")
|
108 |
+
return None
|
109 |
+
|
110 |
+
def analyze_resume_job_match(resume_text, job_description, creativity_level=30):
|
111 |
+
"""
|
112 |
+
Analyze the match between a resume and job description using GPT-4o.
|
113 |
+
|
114 |
+
Args:
|
115 |
+
resume_text: Raw text of the resume
|
116 |
+
job_description: Job description text
|
117 |
+
creativity_level: Level of creativity/modification allowed (0-100)
|
118 |
+
|
119 |
+
Returns:
|
120 |
+
dict: Analysis results including match percentage, gaps, and suggestions
|
121 |
+
"""
|
122 |
+
try:
|
123 |
+
print(f"Analyzing resume match with creativity level: {creativity_level}")
|
124 |
+
print(f"Resume text length: {len(resume_text)}")
|
125 |
+
print(f"Job description length: {len(job_description)}")
|
126 |
+
|
127 |
+
# Adjust system message based on creativity level
|
128 |
+
if creativity_level < 20:
|
129 |
+
system_message = "You are a conservative resume analyzer. Focus only on exact matches between the resume and job description. Be strict in your evaluation."
|
130 |
+
elif creativity_level < 50:
|
131 |
+
system_message = "You are a balanced resume analyzer. Evaluate the resume against the job description with a moderate level of flexibility, recognizing transferable skills."
|
132 |
+
elif creativity_level < 80:
|
133 |
+
system_message = "You are a creative resume analyzer. Be generous in your evaluation, recognizing potential and transferable skills even when not explicitly stated."
|
134 |
+
else:
|
135 |
+
system_message = "You are an optimistic resume analyzer. Focus on potential rather than exact matches. Be very generous in your evaluation and provide ambitious suggestions for improvement."
|
136 |
+
|
137 |
+
prompt = f"""
|
138 |
+
Analyze the following resume and job description:
|
139 |
+
|
140 |
+
RESUME:
|
141 |
+
{resume_text}
|
142 |
+
|
143 |
+
JOB DESCRIPTION:
|
144 |
+
{job_description}
|
145 |
+
|
146 |
+
CREATIVITY LEVEL: {creativity_level}% (where 0% means strictly factual and 100% means highly creative)
|
147 |
+
|
148 |
+
Provide a detailed analysis in JSON format with the following structure:
|
149 |
+
1. "match_percentage": A numerical percentage (0-100) representing how well the resume matches the job description. Use job skills keyword used in job description to match with the contents of the resume to come up with the match percentage.
|
150 |
+
2. "key_matches": List of skills and experiences in the resume that match the job requirements.
|
151 |
+
3. "gaps": List of skills or experiences mentioned in the job description that are missing from the resume.
|
152 |
+
4. "suggestions": Specific suggestions to improve the resume for this job. Ensure the suggestions are based on the job description and the resume and contain the exact keywords from the job description.
|
153 |
+
5. "summary": A brief summary of the overall match and main recommendations.
|
154 |
+
6. "skill_breakdown": An object containing categories of skills from the job description and how well the candidate matches each category:
|
155 |
+
- "technical_skills": Assessment of technical skills match (percentage and comments)
|
156 |
+
- "experience": Assessment of experience requirements match (percentage and comments)
|
157 |
+
- "education": Assessment of education requirements match (percentage and comments)
|
158 |
+
- "soft_skills": Assessment of soft skills/leadership match (percentage and comments)
|
159 |
+
|
160 |
+
Adjust your analysis based on the creativity level. Higher creativity means being more generous with matches and more ambitious with suggestions.
|
161 |
+
|
162 |
+
IMPORTANT: For each resume, provide a unique and accurate match percentage based on the actual content. Do not use the same percentage for different resumes. The excellent match resume should have a high percentage (80-95%), good match should be moderate-high (65-80%), average match should be moderate (40-65%), and poor match should be low (below 40%).
|
163 |
+
|
164 |
+
Return ONLY the JSON object without any additional text.
|
165 |
+
"""
|
166 |
+
|
167 |
+
response = openai.chat.completions.create(
|
168 |
+
model=current_model,
|
169 |
+
messages=[
|
170 |
+
{"role": "system", "content": system_message},
|
171 |
+
{"role": "user", "content": prompt}
|
172 |
+
],
|
173 |
+
response_format={"type": "json_object"}
|
174 |
+
)
|
175 |
+
|
176 |
+
analysis = json.loads(response.choices[0].message.content)
|
177 |
+
print(f"Analysis complete. Match percentage: {analysis.get('match_percentage', 0)}%")
|
178 |
+
return analysis
|
179 |
+
|
180 |
+
except Exception as e:
|
181 |
+
print(f"Error analyzing resume: {e}")
|
182 |
+
return {
|
183 |
+
"match_percentage": 0,
|
184 |
+
"key_matches": [],
|
185 |
+
"gaps": ["Error analyzing resume"],
|
186 |
+
"suggestions": ["Please try again"],
|
187 |
+
"summary": f"Error: {str(e)}",
|
188 |
+
"skill_breakdown": {
|
189 |
+
"technical_skills": {"percentage": 0, "comments": "Error analyzing resume"},
|
190 |
+
"experience": {"percentage": 0, "comments": "Error analyzing resume"},
|
191 |
+
"education": {"percentage": 0, "comments": "Error analyzing resume"},
|
192 |
+
"soft_skills": {"percentage": 0, "comments": "Error analyzing resume"}
|
193 |
+
}
|
194 |
+
}
|
195 |
+
|
196 |
+
def tailor_resume(resume_text, job_description, template_path=None, creativity_level=30, verbosity="elaborate"):
|
197 |
+
"""
|
198 |
+
Generate a tailored resume based on the job description using GPT-4o.
|
199 |
+
|
200 |
+
Args:
|
201 |
+
resume_text: Raw text of the original resume
|
202 |
+
job_description: Job description text
|
203 |
+
template_path: Optional path to a resume template
|
204 |
+
creativity_level: Level of creativity/modification allowed (0-100)
|
205 |
+
verbosity: Level of detail in the resume ('concise' or 'elaborate')
|
206 |
+
|
207 |
+
Returns:
|
208 |
+
str: Tailored resume content
|
209 |
+
"""
|
210 |
+
try:
|
211 |
+
# If a template is provided, read its structure
|
212 |
+
template_structure = ""
|
213 |
+
template_sections = []
|
214 |
+
|
215 |
+
if template_path:
|
216 |
+
# Extract the template structure and sections
|
217 |
+
doc = Document(template_path)
|
218 |
+
for para in doc.paragraphs:
|
219 |
+
if para.text.strip():
|
220 |
+
template_structure += para.text + "\n"
|
221 |
+
# Identify section headings (usually in all caps or with specific styles)
|
222 |
+
if para.style.name.startswith('Heading') or para.text.isupper() or (para.runs and para.runs[0].bold):
|
223 |
+
template_sections.append(para.text.strip())
|
224 |
+
|
225 |
+
# Adjust system message based on creativity level
|
226 |
+
if creativity_level < 20:
|
227 |
+
system_message = "You are a conservative resume editor. Only reorganize existing content to better match the job description. Do not add any new experiences or skills that aren't explicitly mentioned in the original resume."
|
228 |
+
elif creativity_level < 50:
|
229 |
+
system_message = "You are a balanced resume editor. Enhance existing content with better wording and highlight relevant skills. Make minor improvements but keep all content factual and based on the original resume."
|
230 |
+
elif creativity_level < 80:
|
231 |
+
system_message = "You are a creative resume editor. Significantly enhance the resume with improved wording and may suggest minor additions or extensions of existing experiences to better match the job description."
|
232 |
+
else:
|
233 |
+
system_message = "You are an aggressive resume optimizer. Optimize the resume to perfectly match the job description, including suggesting new skills and experiences that would make the candidate more competitive, while maintaining some connection to their actual background. Ensure exact keywords are included from the job description in the new resume. "
|
234 |
+
|
235 |
+
# Adjust system message based on creativity level and verbosity
|
236 |
+
if creativity_level < 20:
|
237 |
+
base_message = "You are a conservative resume editor. Only reorganize existing content to better match the job description. Do not add any new experiences or skills that aren't explicitly mentioned in the original resume."
|
238 |
+
elif creativity_level < 50:
|
239 |
+
base_message = "You are a balanced resume editor. Enhance existing content with better wording and highlight relevant skills. Make minor improvements but keep all content factual and based on the original resume."
|
240 |
+
elif creativity_level < 80:
|
241 |
+
base_message = "You are a creative resume editor. Significantly enhance the resume with improved wording and may suggest minor additions or extensions of existing experiences to better match the job description."
|
242 |
+
else:
|
243 |
+
base_message = "You are an aggressive resume optimizer. Optimize the resume to perfectly match the job description, including suggesting new skills and experiences that would make the candidate more competitive, while maintaining some connection to their actual background. Ensure exact keywords are included from the job description in the new resume."
|
244 |
+
|
245 |
+
# Add verbosity instructions to the system message
|
246 |
+
if verbosity == "concise":
|
247 |
+
system_message = base_message + " Create a concise resume with brief bullet points, focusing only on the most relevant information. Aim for a shorter resume that can be quickly scanned by recruiters."
|
248 |
+
else: # elaborate
|
249 |
+
system_message = base_message + " Create a detailed resume that thoroughly explains experiences and skills, providing context and specific achievements. Use comprehensive bullet points to showcase the candidate's qualifications."
|
250 |
+
|
251 |
+
prompt = f"""
|
252 |
+
Create a tailored version of this resume to better match the job description:
|
253 |
+
|
254 |
+
ORIGINAL RESUME:
|
255 |
+
{resume_text}
|
256 |
+
|
257 |
+
JOB DESCRIPTION:
|
258 |
+
{job_description}
|
259 |
+
|
260 |
+
{("TEMPLATE STRUCTURE TO FOLLOW:" + chr(10) + template_structure) if template_path else ""}
|
261 |
+
|
262 |
+
{("TEMPLATE SECTIONS TO INCLUDE:" + chr(10) + chr(10).join(template_sections)) if template_sections else ""}
|
263 |
+
|
264 |
+
CREATIVITY LEVEL: {creativity_level}% (where 0% means strictly factual and 100% means highly creative)
|
265 |
+
VERBOSITY: {verbosity.upper()} (CONCISE means brief and to-the-point, ELABORATE means detailed and comprehensive)
|
266 |
+
|
267 |
+
Create a tailored resume that:
|
268 |
+
1. Highlights relevant skills and experiences that match the job description
|
269 |
+
2. Uses keywords from the job description
|
270 |
+
3. Quantifies achievements where possible
|
271 |
+
4. Removes or downplays irrelevant information
|
272 |
+
5. Adjusts content based on the specified creativity level
|
273 |
+
|
274 |
+
IMPORTANT FORMATTING INSTRUCTIONS:
|
275 |
+
- Format your response with clear section headings in ALL CAPS
|
276 |
+
- Use bullet points (•) for listing items and achievements
|
277 |
+
- If a template is provided, follow its exact section structure and organization
|
278 |
+
- Maintain the same section headings as in the template when possible
|
279 |
+
- For each section, provide content that matches the requested verbosity level:
|
280 |
+
* CONCISE: Use 1-2 line bullet points, focus only on the most relevant achievements
|
281 |
+
* ELABORATE: Use detailed bullet points with context and specific metrics
|
282 |
+
|
283 |
+
Return the complete tailored resume content in a professional format.
|
284 |
+
"""
|
285 |
+
|
286 |
+
response = openai.chat.completions.create(
|
287 |
+
model=current_model,
|
288 |
+
messages=[
|
289 |
+
{"role": "system", "content": system_message},
|
290 |
+
{"role": "user", "content": prompt}
|
291 |
+
]
|
292 |
+
)
|
293 |
+
|
294 |
+
tailored_resume = response.choices[0].message.content
|
295 |
+
return tailored_resume
|
296 |
+
|
297 |
+
except Exception as e:
|
298 |
+
print(f"Error tailoring resume: {e}")
|
299 |
+
return f"Error tailoring resume: {str(e)}"
|
300 |
+
|
301 |
+
def create_word_document(content, output_path, template_path=None):
|
302 |
+
"""
|
303 |
+
Create a Word document with the given content, optionally using a template.
|
304 |
+
|
305 |
+
Args:
|
306 |
+
content: Text content for the document
|
307 |
+
output_path: Path to save the document
|
308 |
+
template_path: Optional path to a template document to use as a base
|
309 |
+
|
310 |
+
Returns:
|
311 |
+
bool: Success status
|
312 |
+
"""
|
313 |
+
try:
|
314 |
+
# If a template is provided, use it as the base document
|
315 |
+
if template_path and os.path.exists(template_path):
|
316 |
+
# Create a new document instead of modifying the template directly
|
317 |
+
doc = Document()
|
318 |
+
original_template = Document(template_path)
|
319 |
+
|
320 |
+
# Copy all styles from the template to the new document
|
321 |
+
for style in original_template.styles:
|
322 |
+
if style.name not in doc.styles:
|
323 |
+
try:
|
324 |
+
doc.styles.add_style(style.name, style.type)
|
325 |
+
except:
|
326 |
+
pass # Style might already exist or be built-in
|
327 |
+
|
328 |
+
# Copy document properties and sections settings
|
329 |
+
# We'll only add sections as needed, not at the beginning
|
330 |
+
if len(original_template.sections) > 0:
|
331 |
+
# Copy properties from the first section
|
332 |
+
section = original_template.sections[0]
|
333 |
+
# Use the existing first section in the new document
|
334 |
+
new_section = doc.sections[0]
|
335 |
+
new_section.page_height = section.page_height
|
336 |
+
new_section.page_width = section.page_width
|
337 |
+
new_section.left_margin = section.left_margin
|
338 |
+
new_section.right_margin = section.right_margin
|
339 |
+
new_section.top_margin = section.top_margin
|
340 |
+
new_section.bottom_margin = section.bottom_margin
|
341 |
+
new_section.header_distance = section.header_distance
|
342 |
+
new_section.footer_distance = section.footer_distance
|
343 |
+
|
344 |
+
# Copy additional sections if needed
|
345 |
+
for i in range(1, len(original_template.sections)):
|
346 |
+
section = original_template.sections[i]
|
347 |
+
new_section = doc.add_section()
|
348 |
+
new_section.page_height = section.page_height
|
349 |
+
new_section.page_width = section.page_width
|
350 |
+
new_section.left_margin = section.left_margin
|
351 |
+
new_section.right_margin = section.right_margin
|
352 |
+
new_section.top_margin = section.top_margin
|
353 |
+
new_section.bottom_margin = section.bottom_margin
|
354 |
+
new_section.header_distance = section.header_distance
|
355 |
+
new_section.footer_distance = section.footer_distance
|
356 |
+
|
357 |
+
# Copy complex elements like headers and footers
|
358 |
+
copy_template_complex_elements(original_template, doc)
|
359 |
+
|
360 |
+
# Split content by sections (using headings as delimiters)
|
361 |
+
sections = []
|
362 |
+
current_section = []
|
363 |
+
lines = content.split('\n')
|
364 |
+
|
365 |
+
for line in lines:
|
366 |
+
line = line.strip()
|
367 |
+
if not line:
|
368 |
+
continue
|
369 |
+
|
370 |
+
# Check if this is a heading (all caps or ends with a colon)
|
371 |
+
if line.isupper() or (line.endswith(':') and len(line) < 50):
|
372 |
+
if current_section:
|
373 |
+
sections.append(current_section)
|
374 |
+
current_section = [line]
|
375 |
+
else:
|
376 |
+
current_section.append(line)
|
377 |
+
|
378 |
+
if current_section:
|
379 |
+
sections.append(current_section)
|
380 |
+
|
381 |
+
# Find template headings to match with our content sections
|
382 |
+
template_headings = []
|
383 |
+
template_heading_styles = {}
|
384 |
+
for para in original_template.paragraphs:
|
385 |
+
if para.style.name.startswith('Heading') or para.text.isupper() or (para.runs and para.runs[0].bold):
|
386 |
+
template_headings.append(para.text.strip())
|
387 |
+
template_heading_styles[para.text.strip()] = para.style.name
|
388 |
+
|
389 |
+
# Add content to the document with appropriate formatting
|
390 |
+
for section in sections:
|
391 |
+
if not section:
|
392 |
+
continue
|
393 |
+
|
394 |
+
# First line of each section is treated as a heading
|
395 |
+
heading = section[0]
|
396 |
+
|
397 |
+
# Try to find a matching heading style from the template
|
398 |
+
heading_style = 'Heading 1' # Default
|
399 |
+
for template_heading in template_headings:
|
400 |
+
if template_heading.upper() == heading.upper() or template_heading.upper() in heading.upper() or heading.upper() in template_heading.upper():
|
401 |
+
heading_style = template_heading_styles.get(template_heading, 'Heading 1')
|
402 |
+
break
|
403 |
+
|
404 |
+
# Add the heading with the appropriate style
|
405 |
+
p = doc.add_paragraph()
|
406 |
+
try:
|
407 |
+
p.style = heading_style
|
408 |
+
except:
|
409 |
+
p.style = 'Heading 1' # Fallback
|
410 |
+
|
411 |
+
run = p.add_run(heading)
|
412 |
+
run.bold = True
|
413 |
+
|
414 |
+
# Add the rest of the section content
|
415 |
+
for line in section[1:]:
|
416 |
+
if line.startswith('•') or line.startswith('-') or line.startswith('*'):
|
417 |
+
# This is a bullet point
|
418 |
+
p = doc.add_paragraph(line[1:].strip(), style='List Bullet')
|
419 |
+
else:
|
420 |
+
p = doc.add_paragraph(line)
|
421 |
+
else:
|
422 |
+
# Create a new document with basic formatting
|
423 |
+
doc = Document()
|
424 |
+
|
425 |
+
# Split content by lines and add to document with basic formatting
|
426 |
+
paragraphs = content.split('\n')
|
427 |
+
for para in paragraphs:
|
428 |
+
para = para.strip()
|
429 |
+
if not para:
|
430 |
+
continue
|
431 |
+
|
432 |
+
# Check if this is a heading (all caps or ends with a colon)
|
433 |
+
if para.isupper() or (para.endswith(':') and len(para) < 50):
|
434 |
+
p = doc.add_paragraph()
|
435 |
+
p.style = 'Heading 1'
|
436 |
+
run = p.add_run(para)
|
437 |
+
run.bold = True
|
438 |
+
elif para.startswith('•') or para.startswith('-') or para.startswith('*'):
|
439 |
+
# This is a bullet point
|
440 |
+
p = doc.add_paragraph(para[1:].strip(), style='List Bullet')
|
441 |
+
else:
|
442 |
+
doc.add_paragraph(para)
|
443 |
+
|
444 |
+
doc.save(output_path)
|
445 |
+
return True
|
446 |
+
|
447 |
+
except Exception as e:
|
448 |
+
print(f"Error creating Word document: {e}")
|
449 |
+
return False
|
450 |
+
|
451 |
+
def get_available_templates():
|
452 |
+
"""
|
453 |
+
Get a list of available resume templates from the current working directory.
|
454 |
+
|
455 |
+
Returns:
|
456 |
+
list: List of template file paths
|
457 |
+
"""
|
458 |
+
# First, copy any templates from templates directory if they don't exist
|
459 |
+
#copy_templates_to_current_directory()
|
460 |
+
|
461 |
+
templates = []
|
462 |
+
|
463 |
+
# Check current directory for templates
|
464 |
+
for file in os.listdir("."):
|
465 |
+
if file.endswith("_Template.docx") or file.endswith("Template.docx"):
|
466 |
+
templates.append(file)
|
467 |
+
|
468 |
+
print(f"Found templates in current directory: {templates}")
|
469 |
+
return templates
|
470 |
+
|
471 |
+
def copy_templates_to_current_directory():
|
472 |
+
"""
|
473 |
+
Copy templates from the templates directory to the current working directory
|
474 |
+
if they don't already exist.
|
475 |
+
|
476 |
+
Returns:
|
477 |
+
list: List of copied template file paths
|
478 |
+
"""
|
479 |
+
copied_templates = []
|
480 |
+
templates_dir = "templates"
|
481 |
+
|
482 |
+
# Check if templates directory exists
|
483 |
+
if not os.path.exists(templates_dir):
|
484 |
+
print(f"Templates directory '{templates_dir}' not found.")
|
485 |
+
return copied_templates
|
486 |
+
|
487 |
+
# Get list of template files in templates directory
|
488 |
+
template_files = [f for f in os.listdir(templates_dir) if f.endswith(".docx")]
|
489 |
+
|
490 |
+
# Copy each template file to current directory if it doesn't exist
|
491 |
+
for template_file in template_files:
|
492 |
+
source_path = os.path.join(templates_dir, template_file)
|
493 |
+
dest_path = template_file
|
494 |
+
|
495 |
+
if not os.path.exists(dest_path):
|
496 |
+
try:
|
497 |
+
import shutil
|
498 |
+
shutil.copy2(source_path, dest_path)
|
499 |
+
copied_templates.append(dest_path)
|
500 |
+
print(f"Copied template '{template_file}' to current directory.")
|
501 |
+
except Exception as e:
|
502 |
+
print(f"Error copying template '{template_file}': {e}")
|
503 |
+
else:
|
504 |
+
print(f"Template '{template_file}' already exists in current directory.")
|
505 |
+
|
506 |
+
return copied_templates
|
507 |
+
|
508 |
+
def copy_template_complex_elements(source_doc, target_doc):
|
509 |
+
"""
|
510 |
+
Copy complex elements like headers and footers from source document to target document.
|
511 |
+
|
512 |
+
Args:
|
513 |
+
source_doc: Source Document object
|
514 |
+
target_doc: Target Document object
|
515 |
+
"""
|
516 |
+
try:
|
517 |
+
# Copy headers and footers
|
518 |
+
for i, section in enumerate(target_doc.sections):
|
519 |
+
# Skip if source doesn't have this many sections
|
520 |
+
if i >= len(source_doc.sections):
|
521 |
+
break
|
522 |
+
|
523 |
+
# Copy header
|
524 |
+
if section.header.is_linked_to_previous == False:
|
525 |
+
# Check if there's at least one paragraph in the header
|
526 |
+
if len(section.header.paragraphs) == 0:
|
527 |
+
section.header.add_paragraph()
|
528 |
+
|
529 |
+
# Copy text and style from source header paragraphs
|
530 |
+
for j, para in enumerate(source_doc.sections[i].header.paragraphs):
|
531 |
+
if j < len(section.header.paragraphs):
|
532 |
+
section.header.paragraphs[j].text = para.text
|
533 |
+
try:
|
534 |
+
section.header.paragraphs[j].style = para.style
|
535 |
+
except Exception:
|
536 |
+
pass # Style might not be compatible
|
537 |
+
else:
|
538 |
+
new_para = section.header.add_paragraph(para.text)
|
539 |
+
try:
|
540 |
+
new_para.style = para.style
|
541 |
+
except Exception:
|
542 |
+
pass
|
543 |
+
|
544 |
+
# Copy footer
|
545 |
+
if section.footer.is_linked_to_previous == False:
|
546 |
+
# Check if there's at least one paragraph in the footer
|
547 |
+
if len(section.footer.paragraphs) == 0:
|
548 |
+
section.footer.add_paragraph()
|
549 |
+
|
550 |
+
# Copy text and style from source footer paragraphs
|
551 |
+
for j, para in enumerate(source_doc.sections[i].footer.paragraphs):
|
552 |
+
if j < len(section.footer.paragraphs):
|
553 |
+
section.footer.paragraphs[j].text = para.text
|
554 |
+
try:
|
555 |
+
section.footer.paragraphs[j].style = para.style
|
556 |
+
except Exception:
|
557 |
+
pass # Style might not be compatible
|
558 |
+
else:
|
559 |
+
new_para = section.footer.add_paragraph(para.text)
|
560 |
+
try:
|
561 |
+
new_para.style = para.style
|
562 |
+
except Exception:
|
563 |
+
pass
|
564 |
+
except Exception as e:
|
565 |
+
print(f"Error copying complex elements: {e}")
|
566 |
+
|
567 |
+
def create_tailored_resume_from_template(content, template_path, output_path):
|
568 |
+
"""
|
569 |
+
Create a tailored resume by directly modifying a template document.
|
570 |
+
This preserves all formatting, tables, and styles from the original template.
|
571 |
+
|
572 |
+
Args:
|
573 |
+
content: Structured content for the resume (text)
|
574 |
+
template_path: Path to the template document
|
575 |
+
output_path: Path to save the output document
|
576 |
+
|
577 |
+
Returns:
|
578 |
+
bool: Success status
|
579 |
+
"""
|
580 |
+
try:
|
581 |
+
if not os.path.exists(template_path):
|
582 |
+
return False
|
583 |
+
|
584 |
+
# Create a copy of the template
|
585 |
+
doc = Document(template_path)
|
586 |
+
|
587 |
+
# Parse the content into sections
|
588 |
+
sections = {}
|
589 |
+
current_section = None
|
590 |
+
current_content = []
|
591 |
+
|
592 |
+
for line in content.split('\n'):
|
593 |
+
line = line.strip()
|
594 |
+
if not line:
|
595 |
+
continue
|
596 |
+
|
597 |
+
# Check if this is a heading (all caps or ends with a colon)
|
598 |
+
if line.isupper() or (line.endswith(':') and len(line) < 50):
|
599 |
+
# Save the previous section
|
600 |
+
if current_section and current_content:
|
601 |
+
sections[current_section] = current_content
|
602 |
+
|
603 |
+
# Start a new section
|
604 |
+
current_section = line
|
605 |
+
current_content = []
|
606 |
+
else:
|
607 |
+
if current_section:
|
608 |
+
current_content.append(line)
|
609 |
+
|
610 |
+
# Save the last section
|
611 |
+
if current_section and current_content:
|
612 |
+
sections[current_section] = current_content
|
613 |
+
|
614 |
+
# Find all paragraphs in the template that are headings or potential section markers
|
615 |
+
template_sections = {}
|
616 |
+
for i, para in enumerate(doc.paragraphs):
|
617 |
+
if para.style.name.startswith('Heading') or para.text.isupper() or para.runs and para.runs[0].bold:
|
618 |
+
template_sections[para.text.strip()] = i
|
619 |
+
|
620 |
+
# Create a new document to avoid duplicate content
|
621 |
+
new_doc = Document()
|
622 |
+
|
623 |
+
# Copy all styles from the template to the new document
|
624 |
+
for style in doc.styles:
|
625 |
+
if style.name not in new_doc.styles:
|
626 |
+
try:
|
627 |
+
new_doc.styles.add_style(style.name, style.type)
|
628 |
+
except:
|
629 |
+
pass # Style might already exist or be built-in
|
630 |
+
|
631 |
+
# Copy document properties and sections settings
|
632 |
+
# We'll only add sections as needed, not at the beginning
|
633 |
+
if len(doc.sections) > 0:
|
634 |
+
# Copy properties from the first section
|
635 |
+
section = doc.sections[0]
|
636 |
+
# Use the existing first section in the new document
|
637 |
+
new_section = new_doc.sections[0]
|
638 |
+
new_section.page_height = section.page_height
|
639 |
+
new_section.page_width = section.page_width
|
640 |
+
new_section.left_margin = section.left_margin
|
641 |
+
new_section.right_margin = section.right_margin
|
642 |
+
new_section.top_margin = section.top_margin
|
643 |
+
new_section.bottom_margin = section.bottom_margin
|
644 |
+
new_section.header_distance = section.header_distance
|
645 |
+
new_section.footer_distance = section.footer_distance
|
646 |
+
|
647 |
+
# Copy additional sections if needed
|
648 |
+
for i in range(1, len(doc.sections)):
|
649 |
+
section = doc.sections[i]
|
650 |
+
new_section = new_doc.add_section()
|
651 |
+
new_section.page_height = section.page_height
|
652 |
+
new_section.page_width = section.page_width
|
653 |
+
new_section.left_margin = section.left_margin
|
654 |
+
new_section.right_margin = section.right_margin
|
655 |
+
new_section.top_margin = section.top_margin
|
656 |
+
new_section.bottom_margin = section.bottom_margin
|
657 |
+
new_section.header_distance = section.header_distance
|
658 |
+
new_section.footer_distance = section.footer_distance
|
659 |
+
|
660 |
+
# Copy complex elements like headers and footers
|
661 |
+
copy_template_complex_elements(doc, new_doc)
|
662 |
+
|
663 |
+
# Replace content in the template with our tailored content
|
664 |
+
# First, create a mapping between our sections and template sections
|
665 |
+
section_mapping = {}
|
666 |
+
for our_section in sections.keys():
|
667 |
+
best_match = None
|
668 |
+
best_score = 0
|
669 |
+
|
670 |
+
for template_section in template_sections.keys():
|
671 |
+
# Calculate similarity between section headings
|
672 |
+
if template_section.upper() == our_section.upper():
|
673 |
+
# Exact match
|
674 |
+
best_match = template_section
|
675 |
+
break
|
676 |
+
elif template_section.upper() in our_section.upper() or our_section.upper() in template_section.upper():
|
677 |
+
# Partial match
|
678 |
+
score = len(set(template_section.upper()) & set(our_section.upper())) / max(len(template_section), len(our_section))
|
679 |
+
if score > best_score:
|
680 |
+
best_score = score
|
681 |
+
best_match = template_section
|
682 |
+
|
683 |
+
if best_match and best_score > 0.5:
|
684 |
+
section_mapping[our_section] = best_match
|
685 |
+
|
686 |
+
# Add content to the new document based on the template structure
|
687 |
+
for our_section, content_lines in sections.items():
|
688 |
+
# Add section heading
|
689 |
+
p = new_doc.add_paragraph()
|
690 |
+
|
691 |
+
# Try to find a matching heading style from the template
|
692 |
+
if our_section in section_mapping:
|
693 |
+
template_section = section_mapping[our_section]
|
694 |
+
section_index = template_sections[template_section]
|
695 |
+
template_para = doc.paragraphs[section_index]
|
696 |
+
|
697 |
+
try:
|
698 |
+
p.style = template_para.style.name
|
699 |
+
except:
|
700 |
+
p.style = 'Heading 1' # Fallback
|
701 |
+
|
702 |
+
# Copy formatting from template paragraph
|
703 |
+
for run in template_para.runs:
|
704 |
+
if run.text.strip():
|
705 |
+
p_run = p.add_run(our_section)
|
706 |
+
p_run.bold = run.bold
|
707 |
+
p_run.italic = run.italic
|
708 |
+
p_run.underline = run.underline
|
709 |
+
if run.font.name:
|
710 |
+
p_run.font.name = run.font.name
|
711 |
+
if run.font.size:
|
712 |
+
p_run.font.size = run.font.size
|
713 |
+
if run.font.color.rgb:
|
714 |
+
p_run.font.color.rgb = run.font.color.rgb
|
715 |
+
break
|
716 |
+
else:
|
717 |
+
# If no runs with text, add a default run
|
718 |
+
p_run = p.add_run(our_section)
|
719 |
+
p_run.bold = True
|
720 |
+
else:
|
721 |
+
# No matching template section, use default formatting
|
722 |
+
p.style = 'Heading 1'
|
723 |
+
p_run = p.add_run(our_section)
|
724 |
+
p_run.bold = True
|
725 |
+
|
726 |
+
# Add section content
|
727 |
+
for line in content_lines:
|
728 |
+
if line.startswith('•') or line.startswith('-') or line.startswith('*'):
|
729 |
+
# This is a bullet point
|
730 |
+
p = new_doc.add_paragraph(line[1:].strip(), style='List Bullet')
|
731 |
+
else:
|
732 |
+
p = new_doc.add_paragraph(line)
|
733 |
+
|
734 |
+
new_doc.save(output_path)
|
735 |
+
return True
|
736 |
+
|
737 |
+
except Exception as e:
|
738 |
+
print(f"Error creating tailored resume from template: {e}")
|
739 |
+
return False
|
740 |
+
|
741 |
+
def convert_text_to_word(text_file_path, output_docx_path):
|
742 |
+
"""
|
743 |
+
Convert a text file to a Word document.
|
744 |
+
|
745 |
+
Args:
|
746 |
+
text_file_path: Path to the text file
|
747 |
+
output_docx_path: Path where the Word document will be saved
|
748 |
+
|
749 |
+
Returns:
|
750 |
+
bool: True if successful, False otherwise
|
751 |
+
"""
|
752 |
+
try:
|
753 |
+
# Read the text file
|
754 |
+
with open(text_file_path, 'r', encoding='utf-8') as file:
|
755 |
+
content = file.read()
|
756 |
+
|
757 |
+
# Create a new Word document
|
758 |
+
doc = Document()
|
759 |
+
|
760 |
+
# Split the content by double newlines to identify paragraphs
|
761 |
+
paragraphs = content.split('\n\n')
|
762 |
+
|
763 |
+
# Process each paragraph
|
764 |
+
for i, para_text in enumerate(paragraphs):
|
765 |
+
# Skip empty paragraphs
|
766 |
+
if not para_text.strip():
|
767 |
+
continue
|
768 |
+
|
769 |
+
# Check if this looks like a heading (all caps or ends with a colon)
|
770 |
+
is_heading = para_text.isupper() or para_text.strip().endswith(':')
|
771 |
+
|
772 |
+
# Add the paragraph to the document
|
773 |
+
paragraph = doc.add_paragraph(para_text.strip())
|
774 |
+
|
775 |
+
# Apply formatting based on position and content
|
776 |
+
if i == 0: # First paragraph is likely the name
|
777 |
+
paragraph.style = 'Title'
|
778 |
+
elif is_heading:
|
779 |
+
paragraph.style = 'Heading 2'
|
780 |
+
else:
|
781 |
+
paragraph.style = 'Normal'
|
782 |
+
|
783 |
+
# Save the document
|
784 |
+
doc.save(output_docx_path)
|
785 |
+
return True
|
786 |
+
|
787 |
+
except Exception as e:
|
788 |
+
print(f"Error converting text to Word: {e}")
|
789 |
+
return False
|
790 |
+
|
791 |
+
def convert_sample_resumes():
|
792 |
+
"""
|
793 |
+
Convert all sample text resumes to Word documents.
|
794 |
+
|
795 |
+
Returns:
|
796 |
+
list: Paths to the created Word documents
|
797 |
+
"""
|
798 |
+
sample_files = [
|
799 |
+
"excellent_match_resume.docx.txt",
|
800 |
+
"good_match_resume.docx.txt",
|
801 |
+
"average_match_resume.docx.txt",
|
802 |
+
"poor_match_resume.docx.txt"
|
803 |
+
]
|
804 |
+
|
805 |
+
created_files = []
|
806 |
+
|
807 |
+
for text_file in sample_files:
|
808 |
+
if os.path.exists(text_file):
|
809 |
+
output_path = text_file.replace('.docx.txt', '.docx')
|
810 |
+
print(f"Converting {text_file} to {output_path}...")
|
811 |
+
if convert_text_to_word(text_file, output_path):
|
812 |
+
created_files.append(output_path)
|
813 |
+
print(f"Successfully created {output_path}")
|
814 |
+
else:
|
815 |
+
print(f"Failed to create {output_path}")
|
816 |
+
else:
|
817 |
+
print(f"Sample file not found: {text_file}")
|
818 |
+
|
819 |
+
return created_files
|
create_templates.py
ADDED
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import sys
|
3 |
+
|
4 |
+
# Add the templates directory to the Python path
|
5 |
+
templates_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "templates")
|
6 |
+
sys.path.append(templates_dir)
|
7 |
+
|
8 |
+
# Import the template creation functions
|
9 |
+
from create_templates import (
|
10 |
+
create_professional_template,
|
11 |
+
create_modern_template,
|
12 |
+
create_academic_template
|
13 |
+
)
|
14 |
+
|
15 |
+
def main():
|
16 |
+
"""Create resume templates in the current working directory."""
|
17 |
+
print("Creating resume templates in the current directory...")
|
18 |
+
|
19 |
+
# Create templates
|
20 |
+
create_professional_template()
|
21 |
+
create_modern_template()
|
22 |
+
create_academic_template()
|
23 |
+
|
24 |
+
print("All templates created successfully in the current directory!")
|
25 |
+
|
26 |
+
# List created templates
|
27 |
+
templates = [f for f in os.listdir(".") if f.endswith("_Template.docx")]
|
28 |
+
print(f"Templates available: {templates}")
|
29 |
+
|
30 |
+
if __name__ == "__main__":
|
31 |
+
main()
|
excellent_match_resume.docx
ADDED
Binary file (38.3 kB). View file
|
|
excellent_match_resume.docx.txt
ADDED
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
MICHAEL JOHNSON
|
2 |
+
Senior Software Architect
|
3 |
+
[email protected] | (555) 123-4567 | linkedin.com/in/michaeljohnson
|
4 |
+
San Francisco, CA | Remote Available
|
5 |
+
|
6 |
+
PROFESSIONAL SUMMARY
|
7 |
+
Innovative Software Architect with 12+ years of experience designing and implementing scalable, high-performance enterprise applications. Expert in cloud architecture, microservices, and distributed systems with a proven track record of delivering complex solutions. Skilled in translating business requirements into robust technical designs while mentoring development teams and promoting best practices.
|
8 |
+
|
9 |
+
SKILLS
|
10 |
+
• Cloud Platforms: AWS (Certified Solutions Architect), Azure, GCP
|
11 |
+
• Architecture: Microservices, Event-driven, Serverless, API Design (REST, GraphQL)
|
12 |
+
• Containerization: Docker, Kubernetes, ECS
|
13 |
+
• Programming: Java, Python, TypeScript, C#
|
14 |
+
• DevOps: CI/CD, Jenkins, GitHub Actions, ArgoCD
|
15 |
+
• Infrastructure as Code: Terraform, CloudFormation
|
16 |
+
• Databases: PostgreSQL, MongoDB, DynamoDB, Cassandra
|
17 |
+
• Messaging: Kafka, RabbitMQ, AWS Kinesis
|
18 |
+
• Security: OAuth, JWT, API Gateway
|
19 |
+
|
20 |
+
PROFESSIONAL EXPERIENCE
|
21 |
+
|
22 |
+
SENIOR SOFTWARE ARCHITECT | Cloudwave Technologies | 2019 - Present
|
23 |
+
• Led the architectural redesign of a monolithic application into a microservices architecture, resulting in 40% improved scalability and 30% reduced deployment time
|
24 |
+
• Designed and implemented cloud-native solutions on AWS using containerization (Docker, Kubernetes) and serverless technologies (Lambda, API Gateway)
|
25 |
+
• Created comprehensive architecture documentation and technical specifications for enterprise applications
|
26 |
+
• Established CI/CD pipelines using Jenkins and GitHub Actions, reducing deployment errors by 75%
|
27 |
+
• Mentored a team of 15 developers on best practices in software architecture and cloud technologies
|
28 |
+
• Implemented event-driven architecture using Kafka for real-time data processing, handling 10M+ daily events
|
29 |
+
• Designed and optimized database schemas across SQL and NoSQL databases for high-performance applications
|
30 |
+
|
31 |
+
LEAD SOFTWARE ENGINEER | TechInnovate Solutions | 2015 - 2019
|
32 |
+
• Architected and developed a distributed system handling 5,000+ concurrent users with 99.99% uptime
|
33 |
+
• Led the migration of on-premises applications to AWS, reducing infrastructure costs by 35%
|
34 |
+
• Designed RESTful and GraphQL APIs for internal and external consumption
|
35 |
+
• Implemented microservices using Spring Boot and Docker, improving system modularity and maintainability
|
36 |
+
• Created architecture blueprints and technical documentation for development teams
|
37 |
+
• Collaborated with product managers to translate business requirements into technical solutions
|
38 |
+
• Introduced automated testing frameworks, increasing code coverage from 60% to 90%
|
39 |
+
|
40 |
+
SOFTWARE ENGINEER | Global Systems Inc. | 2011 - 2015
|
41 |
+
• Developed enterprise Java applications using Spring Framework
|
42 |
+
• Designed and implemented database schemas and stored procedures in PostgreSQL
|
43 |
+
• Created front-end interfaces using Angular and React
|
44 |
+
• Participated in agile development processes, including daily stand-ups and sprint planning
|
45 |
+
• Contributed to architectural decisions for new product features
|
46 |
+
|
47 |
+
EDUCATION
|
48 |
+
• Master of Science in Computer Science, Stanford University, 2011
|
49 |
+
• Bachelor of Science in Software Engineering, University of California, Berkeley, 2009
|
50 |
+
|
51 |
+
CERTIFICATIONS
|
52 |
+
• AWS Certified Solutions Architect - Professional
|
53 |
+
• Microsoft Azure Solutions Architect Expert
|
54 |
+
• Certified Kubernetes Administrator (CKA)
|
55 |
+
• Professional Scrum Master I (PSM I)
|
find_error.py
ADDED
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import re
|
2 |
+
|
3 |
+
def find_problematic_fstrings(file_path):
|
4 |
+
with open(file_path, 'r', encoding='utf-8') as file:
|
5 |
+
lines = file.readlines()
|
6 |
+
|
7 |
+
for i, line in enumerate(lines):
|
8 |
+
# Look for f-strings with backslashes
|
9 |
+
if 'f"' in line or "f'" in line:
|
10 |
+
# Check if there's a backslash in the f-string
|
11 |
+
match = re.search(r'f["\'].*?\\.*?["\']', line)
|
12 |
+
if match:
|
13 |
+
print(f"Potential problematic f-string at line {i+1}: {line.strip()}")
|
14 |
+
|
15 |
+
if __name__ == "__main__":
|
16 |
+
find_problematic_fstrings("backend.py")
|
good_match_resume.docx
ADDED
Binary file (38 kB). View file
|
|
good_match_resume.docx.txt
ADDED
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
SARAH WILLIAMS
|
2 |
+
Software Architect
|
3 |
+
[email protected] | (555) 987-6543 | linkedin.com/in/sarahwilliams
|
4 |
+
Chicago, IL | Remote Available
|
5 |
+
|
6 |
+
PROFESSIONAL SUMMARY
|
7 |
+
Dedicated Software Architect with 9 years of experience in designing and implementing software solutions for enterprise applications. Skilled in cloud architecture and microservices with a focus on scalable and maintainable systems. Strong communicator with experience guiding development teams and collaborating with stakeholders.
|
8 |
+
|
9 |
+
SKILLS
|
10 |
+
• Cloud Platforms: AWS, Azure
|
11 |
+
• Architecture: Microservices, Service-oriented Architecture
|
12 |
+
• Containerization: Docker, Kubernetes
|
13 |
+
• Programming: Java, Python, JavaScript
|
14 |
+
• DevOps: CI/CD, Jenkins
|
15 |
+
• Databases: MySQL, MongoDB
|
16 |
+
• API Design: REST
|
17 |
+
• Tools: Git, JIRA, Confluence
|
18 |
+
|
19 |
+
PROFESSIONAL EXPERIENCE
|
20 |
+
|
21 |
+
SOFTWARE ARCHITECT | Enterprise Solutions Inc. | 2018 - Present
|
22 |
+
• Design and develop software architecture for enterprise applications serving 1M+ users
|
23 |
+
• Create detailed architecture specifications and documentation for development teams
|
24 |
+
• Lead the migration from monolithic to microservices architecture, improving system scalability
|
25 |
+
• Implement containerization using Docker and Kubernetes for consistent deployment
|
26 |
+
• Collaborate with product managers to translate business requirements into technical solutions
|
27 |
+
• Guide a team of 8 developers in implementing architectural designs
|
28 |
+
• Evaluate and select appropriate technologies and frameworks for new projects
|
29 |
+
• Ensure system scalability, reliability, and security through proper architectural patterns
|
30 |
+
|
31 |
+
SENIOR SOFTWARE DEVELOPER | TechSolutions Corp | 2015 - 2018
|
32 |
+
• Developed and maintained enterprise Java applications using Spring Framework
|
33 |
+
• Participated in architectural decisions for new features and system improvements
|
34 |
+
• Implemented RESTful APIs for internal and external service integration
|
35 |
+
• Designed database schemas and optimized queries for performance
|
36 |
+
• Collaborated with QA team to ensure code quality and test coverage
|
37 |
+
• Mentored junior developers on coding standards and best practices
|
38 |
+
• Participated in agile development processes, including sprint planning and retrospectives
|
39 |
+
|
40 |
+
SOFTWARE DEVELOPER | Digital Innovations | 2013 - 2015
|
41 |
+
• Developed web applications using Java, JavaScript, and HTML/CSS
|
42 |
+
• Created and maintained database schemas in MySQL
|
43 |
+
• Implemented unit and integration tests to ensure code quality
|
44 |
+
• Participated in code reviews and provided feedback to team members
|
45 |
+
• Collaborated with UX designers to implement user interfaces
|
46 |
+
|
47 |
+
EDUCATION
|
48 |
+
• Bachelor of Science in Computer Science, University of Illinois, 2013
|
49 |
+
|
50 |
+
CERTIFICATIONS
|
51 |
+
• AWS Certified Solutions Architect - Associate
|
52 |
+
• Certified Scrum Developer
|
job_desc.txt
ADDED
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
Software Architect
|
2 |
+
Location: Remote (US)
|
3 |
+
Salary: $150,000 - $180,000
|
4 |
+
|
5 |
+
About the Role:
|
6 |
+
We are seeking an experienced Software Architect to design and oversee the implementation of high-performance, scalable software systems. The ideal candidate will have a strong background in cloud architecture, microservices, and distributed systems, with a proven track record of delivering complex software solutions.
|
7 |
+
|
8 |
+
Key Responsibilities:
|
9 |
+
• Design and develop software architecture for enterprise applications
|
10 |
+
• Create detailed architecture specifications and documentation
|
11 |
+
• Collaborate with product managers to translate business requirements into technical solutions
|
12 |
+
• Guide development teams in implementing architectural designs
|
13 |
+
• Evaluate and select appropriate technologies and frameworks
|
14 |
+
• Ensure system scalability, reliability, and security
|
15 |
+
• Mentor junior developers and promote best practices
|
16 |
+
• Stay current with emerging technologies and industry trends
|
17 |
+
|
18 |
+
Required Skills and Experience:
|
19 |
+
• 8+ years of software development experience
|
20 |
+
• 5+ years in software architecture roles
|
21 |
+
• Expert knowledge of cloud platforms (AWS, Azure, or GCP)
|
22 |
+
• Strong experience with microservices architecture
|
23 |
+
• Proficiency in at least one major programming language (Java, Python, C#, or JavaScript/TypeScript)
|
24 |
+
• Experience with containerization (Docker, Kubernetes)
|
25 |
+
• Knowledge of CI/CD pipelines and DevOps practices
|
26 |
+
• Understanding of database design (SQL and NoSQL)
|
27 |
+
• Experience with API design and implementation (REST, GraphQL)
|
28 |
+
• Strong communication and leadership skills
|
29 |
+
|
30 |
+
Preferred Qualifications:
|
31 |
+
• Experience with event-driven architecture
|
32 |
+
• Knowledge of serverless computing
|
33 |
+
• Experience with data streaming platforms (Kafka, Kinesis)
|
34 |
+
• Understanding of machine learning frameworks
|
35 |
+
• Experience with infrastructure as code (Terraform, CloudFormation)
|
36 |
+
• Relevant certifications (AWS Solutions Architect, Azure Solutions Architect, etc.)
|
37 |
+
• Experience in agile development methodologies
|
38 |
+
|
39 |
+
Education:
|
40 |
+
• Bachelor's degree in Computer Science, Software Engineering, or related field
|
41 |
+
• Master's degree preferred
|
42 |
+
|
43 |
+
We offer competitive compensation, excellent benefits, and the opportunity to work on cutting-edge technologies in a collaborative environment. Join our team and help shape the future of our software platforms!
|
poor_match_resume.docx
ADDED
Binary file (37.8 kB). View file
|
|
poor_match_resume.docx.txt
ADDED
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
JENNIFER SMITH
|
2 |
+
Frontend Developer
|
3 |
+
[email protected] | (555) 234-5678 | linkedin.com/in/jennifersmith
|
4 |
+
New York, NY | On-site Only
|
5 |
+
|
6 |
+
PROFESSIONAL SUMMARY
|
7 |
+
Creative Frontend Developer with 3 years of experience crafting engaging user interfaces and responsive web designs. Passionate about creating intuitive user experiences with modern web technologies. Strong attention to detail and commitment to writing clean, maintainable code.
|
8 |
+
|
9 |
+
SKILLS
|
10 |
+
• Frontend: HTML5, CSS3, JavaScript, React, Vue.js
|
11 |
+
• Design: Figma, Adobe XD, Photoshop
|
12 |
+
• CSS Frameworks: Bootstrap, Tailwind CSS
|
13 |
+
• Version Control: Git, GitHub
|
14 |
+
• Tools: VS Code, Chrome DevTools
|
15 |
+
• Basic Knowledge: Node.js, Express
|
16 |
+
|
17 |
+
PROFESSIONAL EXPERIENCE
|
18 |
+
|
19 |
+
FRONTEND DEVELOPER | Creative Web Solutions | 2020 - Present
|
20 |
+
• Develop responsive web interfaces using HTML, CSS, and JavaScript
|
21 |
+
• Create interactive UI components using React and Vue.js
|
22 |
+
• Collaborate with designers to implement pixel-perfect designs
|
23 |
+
• Optimize website performance and loading times
|
24 |
+
• Ensure cross-browser compatibility and responsive design
|
25 |
+
• Implement animations and transitions for enhanced user experience
|
26 |
+
• Participate in code reviews and provide feedback to team members
|
27 |
+
|
28 |
+
JUNIOR WEB DEVELOPER | Digital Design Agency | 2019 - 2020
|
29 |
+
• Assisted in the development of client websites using HTML, CSS, and JavaScript
|
30 |
+
• Created responsive layouts using Bootstrap
|
31 |
+
• Fixed UI bugs and implemented minor features
|
32 |
+
• Collaborated with designers to implement visual elements
|
33 |
+
• Participated in client meetings and gathered requirements
|
34 |
+
• Learned and applied new frontend technologies and frameworks
|
35 |
+
|
36 |
+
WEB DESIGN INTERN | Tech Startups Inc. | 2018 - 2019
|
37 |
+
• Assisted in designing and developing website mockups
|
38 |
+
• Created UI elements using Photoshop and Illustrator
|
39 |
+
• Implemented basic web pages using HTML and CSS
|
40 |
+
• Learned about responsive design principles
|
41 |
+
• Participated in team meetings and brainstorming sessions
|
42 |
+
|
43 |
+
EDUCATION
|
44 |
+
• Bachelor of Fine Arts in Graphic Design, New York University, 2018
|
45 |
+
|
46 |
+
CERTIFICATIONS
|
47 |
+
• Meta Frontend Developer Professional Certificate
|
48 |
+
• FreeCodeCamp Responsive Web Design Certification
|
49 |
+
|
50 |
+
PORTFOLIO
|
51 |
+
• Personal website: jennifersmith.design
|
52 |
+
• GitHub: github.com/jennifersmith
|
requirements.txt
ADDED
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
gradio>=4.0.0
|
2 |
+
openai>=1.0.0
|
3 |
+
python-docx>=0.8.11
|
4 |
+
docx2txt>=0.8
|
5 |
+
PyPDF2>=3.0.0
|
6 |
+
plotly>=5.14.0
|
7 |
+
python-dotenv>=1.0.0
|
8 |
+
requests>=2.28.0
|
9 |
+
numpy>=1.24.0
|
10 |
+
pandas>=2.0.0
|
11 |
+
matplotlib>=3.7.0
|