Spaces:
Running
Running
Upload 25 files
Browse files- README.md +28 -12
- __init__.py +0 -0
- app.py +139 -0
- config.yaml +18 -0
- core/__init__.py +10 -0
- core/analyzer.py +41 -0
- core/form_filler/background.js +10 -0
- core/form_filler/content.js +14 -0
- core/form_filler/manifest.json +17 -0
- core/form_filler/popup.html +10 -0
- core/optimizer.py +39 -0
- core/tracker.py +57 -0
- credentials.json +13 -0
- data_models/__init__.py +5 -0
- data_models/job_description.py +32 -0
- data_models/resume.py +23 -0
- requirement.txt +22 -0
- setup.py +16 -0
- templates/Ramen_DXC.docx +0 -0
- templates/professional.docx +0 -0
- utils/__init__.py +5 -0
- utils/config_manager.py +9 -0
- utils/file_handlers.py +69 -0
- utils/file_handlers1.py +55 -0
- utils/logger.py +18 -0
README.md
CHANGED
@@ -1,12 +1,28 @@
|
|
1 |
-
|
2 |
-
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# ATS Optimizer Pro
|
2 |
+
|
3 |
+
Complete solution to optimize resumes for applicant tracking systems.
|
4 |
+
|
5 |
+
## Features
|
6 |
+
|
7 |
+
1. **Format-Preserving Analysis**
|
8 |
+
- Parses PDF/Word resumes while keeping original formatting
|
9 |
+
- Advanced section detection
|
10 |
+
|
11 |
+
2. **5-Factor ATS Scoring**
|
12 |
+
- Keyword matching (TF-IDF + exact match)
|
13 |
+
- Section completeness
|
14 |
+
- Semantic similarity (Qdrant vectors)
|
15 |
+
- Experience matching
|
16 |
+
- Education verification
|
17 |
+
|
18 |
+
3. **AI-Powered Optimization**
|
19 |
+
- DeepSeek API integration
|
20 |
+
- 3-step iterative refinement
|
21 |
+
- Format-aware rewriting
|
22 |
+
|
23 |
+
## Setup
|
24 |
+
|
25 |
+
1. Install requirements:
|
26 |
+
```bash
|
27 |
+
pip install -r requirements.txt
|
28 |
+
python -m spacy download en_core_web_md
|
__init__.py
ADDED
File without changes
|
app.py
ADDED
@@ -0,0 +1,139 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import streamlit as st
|
2 |
+
from ats_optimizer.core import ResumeAnalyzer, ResumeOptimizer
|
3 |
+
from ats_optimizer.data_models import Resume, JobDescription
|
4 |
+
from ats_optimizer.utils.file_handlers import FileHandler # Updated import
|
5 |
+
# from ats_optimizer.utils import FileHandler, Config
|
6 |
+
from ats_optimizer.utils.config_manager import Config
|
7 |
+
from pathlib import Path
|
8 |
+
import tempfile
|
9 |
+
import sys
|
10 |
+
|
11 |
+
# Fix module imports for Hugging Face
|
12 |
+
sys.path.append(str(Path(__file__).parent))
|
13 |
+
|
14 |
+
# def main():
|
15 |
+
# # Initialize components
|
16 |
+
# config = Config('config.yaml')
|
17 |
+
# analyzer = ResumeAnalyzer()
|
18 |
+
# optimizer = ResumeOptimizer(config.deepseek_api_key)
|
19 |
+
|
20 |
+
# # Streamlit UI
|
21 |
+
# st.title("🚀 ATS Optimizer Pro")
|
22 |
+
# st.markdown("Upload your resume and job description to analyze and optimize for ATS compatibility")
|
23 |
+
|
24 |
+
# # File upload section
|
25 |
+
# with st.expander("Upload Files", expanded=True):
|
26 |
+
# col1, col2 = st.columns(2)
|
27 |
+
# with col1:
|
28 |
+
# resume_file = st.file_uploader("Resume", type=["pdf", "docx"], key="resume_upload")
|
29 |
+
# with col2:
|
30 |
+
# jd_file = st.file_uploader("Job Description", type=["pdf", "docx", "txt"], key="jd_upload")
|
31 |
+
|
32 |
+
# if st.button("Analyze", type="primary"):
|
33 |
+
# if resume_file and jd_file:
|
34 |
+
# with st.spinner("Processing files..."):
|
35 |
+
# try:
|
36 |
+
# # Save uploaded files
|
37 |
+
# with tempfile.TemporaryDirectory() as temp_dir:
|
38 |
+
# resume_path = FileHandler.save_uploaded_file(resume_file, temp_dir)
|
39 |
+
# jd_path = FileHandler.save_uploaded_file(jd_file, temp_dir)
|
40 |
+
|
41 |
+
# if not resume_path or not jd_path:
|
42 |
+
# st.error("Failed to process uploaded files")
|
43 |
+
# return
|
44 |
+
|
45 |
+
# # Analyze documents
|
46 |
+
# resume = analyzer.parse_resume(resume_path)
|
47 |
+
# jd = analyzer.parse_jd(jd_path)
|
48 |
+
|
49 |
+
# # Calculate score
|
50 |
+
# score = analyzer.calculate_ats_score(resume, jd)
|
51 |
+
|
52 |
+
# # Display results
|
53 |
+
# st.subheader("📊 Analysis Results")
|
54 |
+
# st.metric("Overall ATS Score", f"{score['overall_score']:.1f}%")
|
55 |
+
|
56 |
+
# with st.expander("Detailed Scores"):
|
57 |
+
# st.write(f"Keyword Match: {score['keyword_score']:.1f}%")
|
58 |
+
# st.write(f"Section Completeness: {score['section_score']:.1f}%")
|
59 |
+
# st.write(f"Experience Match: {score['experience_score']:.1f}%")
|
60 |
+
|
61 |
+
# # Optimization section
|
62 |
+
# st.subheader("🛠 Optimization")
|
63 |
+
# if st.button("Optimize Resume", key="optimize_btn"):
|
64 |
+
# with st.spinner("Rewriting resume..."):
|
65 |
+
# optimized = optimizer.rewrite_resume(resume, jd)
|
66 |
+
# temp_output = Path(temp_dir) / "optimized_resume.docx"
|
67 |
+
# FileHandler.save_resume(optimized, str(temp_output))
|
68 |
+
|
69 |
+
# st.success("Optimization complete!")
|
70 |
+
# with open(temp_output, "rb") as f:
|
71 |
+
# st.download_button(
|
72 |
+
# "Download Optimized Resume",
|
73 |
+
# data=f,
|
74 |
+
# file_name="optimized_resume.docx",
|
75 |
+
# mime="application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
76 |
+
# )
|
77 |
+
|
78 |
+
# except Exception as e:
|
79 |
+
# st.error(f"An error occurred: {str(e)}")
|
80 |
+
# st.stop()
|
81 |
+
# else:
|
82 |
+
# st.warning("Please upload both resume and job description files")
|
83 |
+
|
84 |
+
# if __name__ == "__main__":
|
85 |
+
# main()
|
86 |
+
|
87 |
+
def main():
|
88 |
+
# Initialize components
|
89 |
+
config = Config('config.yaml')
|
90 |
+
analyzer = ResumeAnalyzer()
|
91 |
+
optimizer = ResumeOptimizer(config.deepseek_api_key)
|
92 |
+
|
93 |
+
# Streamlit UI
|
94 |
+
st.title("ATS Optimizer Pro")
|
95 |
+
|
96 |
+
# File upload
|
97 |
+
resume_file = st.file_uploader("Upload Resume", type=["pdf", "docx"])
|
98 |
+
jd_file = st.file_uploader("Upload Job Description", type=["pdf", "docx", "txt"])
|
99 |
+
|
100 |
+
if st.button("Analyze"):
|
101 |
+
if resume_file and jd_file:
|
102 |
+
with st.spinner("Processing..."):
|
103 |
+
try:
|
104 |
+
# Create temp directory
|
105 |
+
with tempfile.TemporaryDirectory() as temp_dir:
|
106 |
+
# Save uploaded files
|
107 |
+
resume_path = FileHandler.save_uploaded_file(resume_file, temp_dir)
|
108 |
+
jd_path = FileHandler.save_uploaded_file(jd_file, temp_dir)
|
109 |
+
|
110 |
+
if not resume_path or not jd_path:
|
111 |
+
st.error("Failed to process uploaded files")
|
112 |
+
return
|
113 |
+
|
114 |
+
# Analyze documents
|
115 |
+
resume = analyzer.parse_resume(resume_path)
|
116 |
+
jd = analyzer.parse_jd(jd_path)
|
117 |
+
|
118 |
+
# Calculate score
|
119 |
+
score = analyzer.calculate_ats_score(resume, jd)
|
120 |
+
|
121 |
+
# Display results
|
122 |
+
st.subheader("Analysis Results")
|
123 |
+
st.json(score)
|
124 |
+
|
125 |
+
# Optimization
|
126 |
+
if st.button("Optimize Resume"):
|
127 |
+
optimized = optimizer.rewrite_resume(resume, jd)
|
128 |
+
output_path = os.path.join(temp_dir, "optimized_resume.docx")
|
129 |
+
if FileHandler.save_resume(optimized, output_path):
|
130 |
+
with open(output_path, "rb") as f:
|
131 |
+
st.download_button(
|
132 |
+
"Download Optimized Resume",
|
133 |
+
data=f,
|
134 |
+
file_name="optimized_resume.docx"
|
135 |
+
)
|
136 |
+
except Exception as e:
|
137 |
+
st.error(f"An error occurred: {str(e)}")
|
138 |
+
else:
|
139 |
+
st.warning("Please upload both resume and job description files")
|
config.yaml
ADDED
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
deepseek:
|
2 |
+
api_key: "sk-cf0b89ca56e44cb9980113909ebe687e"
|
3 |
+
model: "deepseek-chat"
|
4 |
+
temperature: 0.7
|
5 |
+
|
6 |
+
google_sheets:
|
7 |
+
enabled: true # Set to true if using Google Sheets or else false
|
8 |
+
credentials: "credentials.json"
|
9 |
+
# sheet_id: "your-sheet-id"
|
10 |
+
sheet_name: "Job Applications"
|
11 |
+
|
12 |
+
scoring:
|
13 |
+
weights:
|
14 |
+
keyword: 0.3
|
15 |
+
section: 0.2
|
16 |
+
vector: 0.25
|
17 |
+
experience: 0.15
|
18 |
+
education: 0.1
|
core/__init__.py
ADDED
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# from .analyzer import ResumeAnalyzer
|
2 |
+
# from .optimizer import ResumeOptimizer
|
3 |
+
# from .tracker import ApplicationTracker
|
4 |
+
|
5 |
+
from ats_optimizer.core.analyzer import ResumeAnalyzer
|
6 |
+
from ats_optimizer.core.optimizer import ResumeOptimizer
|
7 |
+
from ats_optimizer.core.tracker import ApplicationTracker
|
8 |
+
|
9 |
+
__all__ = ['ResumeAnalyzer', 'ResumeOptimizer', 'ApplicationTracker']
|
10 |
+
# __all__ = ['ResumeAnalyzer', 'ResumeOptimizer'] #if doesnt want to use tracker & comment above import too
|
core/analyzer.py
ADDED
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from qdrant_client import QdrantClient
|
2 |
+
from ats_optimizer.data_models.resume import Resume
|
3 |
+
from ats_optimizer.data_models.job_description import JobDescription
|
4 |
+
# from utils.logger import logger
|
5 |
+
# from ..utils.logger import logger
|
6 |
+
from ats_optimizer.utils.logger import logger
|
7 |
+
|
8 |
+
class ResumeAnalyzer:
|
9 |
+
def __init__(self):
|
10 |
+
self.client = QdrantClient(":memory:")
|
11 |
+
self.client.set_model("BAAI/bge-base-en")
|
12 |
+
|
13 |
+
def parse_resume(self, file_path: str) -> Resume:
|
14 |
+
"""Step 1: Parse resume with formatting preservation"""
|
15 |
+
# Uses python-docx for Word docs, pdfminer for PDFs
|
16 |
+
raw_text, formatting = FileHandler.extract_with_formatting(file_path)
|
17 |
+
return Resume(raw_text, formatting)
|
18 |
+
|
19 |
+
def parse_jd(self, file_path: str) -> JobDescription:
|
20 |
+
"""Step 2: Analyze job description"""
|
21 |
+
raw_text = FileHandler.extract_text(file_path)
|
22 |
+
return JobDescription(raw_text)
|
23 |
+
|
24 |
+
def calculate_ats_score(self, resume: Resume, jd: JobDescription) -> dict:
|
25 |
+
"""Step 3: Comprehensive ATS scoring"""
|
26 |
+
# Enhanced scoring with 5 factors
|
27 |
+
scores = {
|
28 |
+
'keyword': self._keyword_match_score(resume, jd),
|
29 |
+
'section': self._section_completeness(resume, jd),
|
30 |
+
'vector': self._vector_similarity(resume, jd),
|
31 |
+
'experience': self._experience_match(resume, jd),
|
32 |
+
'education': self._education_match(resume, jd)
|
33 |
+
}
|
34 |
+
scores['overall'] = sum(w * s for w, s in [
|
35 |
+
(0.3, scores['keyword']),
|
36 |
+
(0.2, scores['section']),
|
37 |
+
(0.25, scores['vector']),
|
38 |
+
(0.15, scores['experience']),
|
39 |
+
(0.1, scores['education'])
|
40 |
+
])
|
41 |
+
return scores
|
core/form_filler/background.js
ADDED
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
2 |
+
if (request.action === "fillForm") {
|
3 |
+
chrome.tabs.query({active: true, currentWindow: true}, (tabs) => {
|
4 |
+
chrome.tabs.sendMessage(tabs[0].id, {
|
5 |
+
action: "fillFields",
|
6 |
+
resumeData: request.data
|
7 |
+
});
|
8 |
+
});
|
9 |
+
}
|
10 |
+
});
|
core/form_filler/content.js
ADDED
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
2 |
+
if (msg.action === "fillFields") {
|
3 |
+
const fields = {
|
4 |
+
'input[name*="name"]': msg.resumeData.name,
|
5 |
+
'input[name*="email"]': msg.resumeData.email,
|
6 |
+
'textarea[name*="experience"]': msg.resumeData.experience
|
7 |
+
};
|
8 |
+
|
9 |
+
Object.entries(fields).forEach(([selector, value]) => {
|
10 |
+
const el = document.querySelector(selector);
|
11 |
+
if (el) el.value = value;
|
12 |
+
});
|
13 |
+
}
|
14 |
+
});
|
core/form_filler/manifest.json
ADDED
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"manifest_version": 3,
|
3 |
+
"name": "ATS AutoFill",
|
4 |
+
"version": "1.0",
|
5 |
+
"permissions": ["activeTab", "storage"],
|
6 |
+
"action": {
|
7 |
+
"default_popup": "popup.html",
|
8 |
+
"default_icon": "icon.png"
|
9 |
+
},
|
10 |
+
"background": {
|
11 |
+
"service_worker": "background.js"
|
12 |
+
},
|
13 |
+
"content_scripts": [{
|
14 |
+
"matches": ["*://*/*"],
|
15 |
+
"js": ["content.js"]
|
16 |
+
}]
|
17 |
+
}
|
core/form_filler/popup.html
ADDED
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html>
|
3 |
+
<head>
|
4 |
+
<title>ATS AutoFill</title>
|
5 |
+
<script src="popup.js"></script>
|
6 |
+
</head>
|
7 |
+
<body>
|
8 |
+
<button id="fillButton">Fill Form</button>
|
9 |
+
</body>
|
10 |
+
</html>
|
core/optimizer.py
ADDED
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import requests
|
2 |
+
from docx import Document
|
3 |
+
from data_models.resume import Resume
|
4 |
+
|
5 |
+
from ats_optimizer.data_models.job_description import JobDescription
|
6 |
+
from ats_optimizer.data_models.resume import Resume
|
7 |
+
|
8 |
+
class ResumeOptimizer:
|
9 |
+
def __init__(self, api_key: str):
|
10 |
+
self.api_key = api_key
|
11 |
+
# self.template = "templates/professional.docx"
|
12 |
+
self.template = "templates/Ramen_DXC.docx"
|
13 |
+
|
14 |
+
def rewrite_resume(self, resume: Resume, jd: JobDescription) -> Resume:
|
15 |
+
"""Step 4: AI rewriting with formatting preservation"""
|
16 |
+
prompt = self._build_optimization_prompt(resume, jd)
|
17 |
+
|
18 |
+
response = requests.post(
|
19 |
+
"https://api.deepseek.com/v1/chat/completions",
|
20 |
+
headers={"Authorization": f"Bearer {self.api_key}"},
|
21 |
+
json={
|
22 |
+
"model": "deepseek-chat",
|
23 |
+
"messages": [{
|
24 |
+
"role": "user",
|
25 |
+
"content": prompt
|
26 |
+
}],
|
27 |
+
"temperature": 0.7
|
28 |
+
}
|
29 |
+
)
|
30 |
+
|
31 |
+
# Apply optimized content to original format
|
32 |
+
optimized_content = response.json()["choices"][0]["message"]["content"]
|
33 |
+
return self._apply_formatting(resume, optimized_content)
|
34 |
+
|
35 |
+
def _apply_formatting(self, original: Resume, new_content: str) -> Resume:
|
36 |
+
"""Preserve original formatting with new content"""
|
37 |
+
doc = Document(original.file_path)
|
38 |
+
# Advanced formatting preservation logic
|
39 |
+
return Resume.from_docx(doc, new_content)
|
core/tracker.py
ADDED
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import gspread
|
2 |
+
from oauth2client.service_account import ServiceAccountCredentials
|
3 |
+
from pathlib import Path
|
4 |
+
import logging
|
5 |
+
|
6 |
+
logger = logging.getLogger(__name__)
|
7 |
+
|
8 |
+
'''
|
9 |
+
class ApplicationTracker:
|
10 |
+
def __init__(self, creds_path: str):
|
11 |
+
self.scope = ["https://www.googleapis.com/auth/spreadsheets"]
|
12 |
+
self.creds = ServiceAccountCredentials.from_json_keyfile_name(creds_path, self.scope)
|
13 |
+
|
14 |
+
def track(self, application_data: dict):
|
15 |
+
"""Track application in Google Sheets with enhanced fields"""
|
16 |
+
client = gspread.authorize(self.creds)
|
17 |
+
sheet = client.open("Job Applications").sheet1
|
18 |
+
|
19 |
+
sheet.append_row([
|
20 |
+
application_data['company'],
|
21 |
+
application_data['position'],
|
22 |
+
application_data['date'],
|
23 |
+
application_data['status'],
|
24 |
+
application_data['score'],
|
25 |
+
application_data['url'],
|
26 |
+
application_data['resume_version'],
|
27 |
+
application_data['notes']
|
28 |
+
])
|
29 |
+
'''
|
30 |
+
class ApplicationTracker:
|
31 |
+
def __init__(self, creds_path: str):
|
32 |
+
if not Path(creds_path).exists():
|
33 |
+
raise FileNotFoundError(f"Credentials file not found at {creds_path}")
|
34 |
+
|
35 |
+
try:
|
36 |
+
self.scope = ["https://www.googleapis.com/auth/spreadsheets"]
|
37 |
+
self.creds = ServiceAccountCredentials.from_json_keyfile_name(creds_path, self.scope)
|
38 |
+
self.client = gspread.authorize(self.creds)
|
39 |
+
except Exception as e:
|
40 |
+
logger.error(f"Google Sheets auth failed: {e}")
|
41 |
+
raise
|
42 |
+
|
43 |
+
def track(self, application_data: dict, sheet_name="Job Applications"):
|
44 |
+
try:
|
45 |
+
sheet = self.client.open(sheet_name).sheet1
|
46 |
+
sheet.append_row([
|
47 |
+
application_data.get('company', ''),
|
48 |
+
application_data.get('position', ''),
|
49 |
+
application_data.get('date_applied', ''),
|
50 |
+
application_data.get('status', 'Applied'),
|
51 |
+
application_data.get('score', ''),
|
52 |
+
application_data.get('url', '')
|
53 |
+
])
|
54 |
+
return True
|
55 |
+
except Exception as e:
|
56 |
+
logger.error(f"Tracking failed: {e}")
|
57 |
+
return False
|
credentials.json
ADDED
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"type": "service_account",
|
3 |
+
"project_id": "formal-theater-458022-d2",
|
4 |
+
"private_key_id": "e7494ef90b6d43b0dc15ce9f9db0922225a48694",
|
5 |
+
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDDGjEU29ETZSyx\nqr8zakizg+q3IqWq3pQdMVHH72GLq/vugkZiqao11YdFK3igyKIRApacpLAACjmm\nEoUwcKEh1R7WKExbAvyFTLIm65j9wZ2Z2LeAuRRZFjU2uQ+iCXOTQA7YgIPU3sMg\nLI+cMAITr0C4hLRnjgUcPeNQRG8M/Io3Oq4KeTDvEKS99aqVvA7zgGH98awdBd2c\ncciYGHaTUq5sJxK72AKFnPxrLHgbq9hiuc5fw7YdCVzmuVY/LXkwSkosbIsIrxUx\nPHwlnYdoWHZ0aI76x3qluw+MUqZoIEp3ctkIfHaZbLf4yuqTcbUuQ7RtkPFLifge\ndFsM7BnhAgMBAAECggEASLQr8h/wC5A6VYLReXFz4iGYh+JLZh9HhpFobl8QNKJE\nYZ7+Z6neGe2WWPpYG2JosnoKchkU1Q76aJ6iL2jpQthOg3PE8G1ueKYaBVLqUjWi\na0BNMZTGtmQGNHxGDRYEkazfW2KYvey9PfIdGhDx1TALqDcbmzNbSCjv2muGDoou\nzmEcAzdil5M0UNVctqxqAOk4PK3l59dSUdRgJLP6LsceT1xqNDw9Ps9x8vOUY3iq\n0kkOeXu+eTVIkgTxIsJGnBQ0wr+Ao5r12zJ8Y/iyXDVN/RTOl+aakegYiIYjeVZu\nEPyZ2fMNnQCtfSGzoLrcHyFxkhh1Fl2E4ZhW4hWh+QKBgQDgjtlnhBdhNuBGHOp0\nknRKWlhtRXzuIDklCmIS/vK0ZXpCF2wacXo7UKHQ7oVPsA5+0MqeV9teu8hqQm/Y\nZRipHU911zlsR81ZejR4WQ8xDci4Ewpt5JVkx+DfPlNXuPapgGTtg/KBDfKHWnnv\nNr/BSeRSNb5iSPkS/j4ENnrhEwKBgQDea4U0cy9J2YvBhOmb/Bk9fOIBcnlz/B2v\nWt/+ddIZnW1IOYgSd/W8W3GyQ803OzzuHa+y0vLNnGBwapOMPJ8foagy36XYbRxr\nSu8He+oTTJTgwidEnUz1DS8B3IIgi+FJK8YLUL8hVupuBqqI/r9G/wtULTFutzvD\n5/N/rCaruwKBgADOIlNvstHDa5x0wBZ46/fUSRrjM+Z6sRnD5sQgq+gfsQeJo/aY\nT5Lk4B+qq0m03OhxgTh+Iig9ziMrZ9FD04nPtBg9FFSiEUdv275Ou3I2lXCriM8K\nEcsRuGm0hIH9BM1oy3PalEUIMsVvep5z+M4NoMb2sF8T2ejKhphnRZuHAoGAL+rM\nFMOn8WoLwNJIndFPAr8v1Y36+nDbWFbkoOZzMA+JZqD2Xrw3VbABq50NzhNWChqd\nKpJlusQwxqc/SFwbD+581RD3osvG7pqDKoKYqDW8cTuCyDZ3SOfhM6503lwkWeYz\nUWbA9obKFJAdF0yCmuIBZ84gszCIkKkc/WlyH1cCgYEAtkmceBmt/YCPnaVDhk/L\n4hWqcVHqD2RU4DPPhjIXppjahDrcOzjD7Lt9swKK3QZEE840DcIAKVU140V2Neie\nOXqzgrLn/dIe0j15FK4lmY9GhCVU3kTx08JwxEiBIDoi0Z6F926C9dvjGxarpsi7\n77pNNpxzZ9i6mx9dK9ECuMI=\n-----END PRIVATE KEY-----\n",
|
6 |
+
"client_email": "[email protected]",
|
7 |
+
"client_id": "102212927624577642135",
|
8 |
+
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
9 |
+
"token_uri": "https://oauth2.googleapis.com/token",
|
10 |
+
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
11 |
+
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/ats-optimizer%40formal-theater-458022-d2.iam.gserviceaccount.com",
|
12 |
+
"universe_domain": "googleapis.com"
|
13 |
+
}
|
data_models/__init__.py
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# from .resume import Resume
|
2 |
+
from .resume import Resume
|
3 |
+
from .job_description import JobDescription
|
4 |
+
|
5 |
+
__all__ = ['Resume', 'JobDescription']
|
data_models/job_description.py
ADDED
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from dataclasses import dataclass
|
2 |
+
from typing import Dict, List
|
3 |
+
|
4 |
+
@dataclass
|
5 |
+
class JobDescription:
|
6 |
+
# raw_text: str
|
7 |
+
# extracted_keywords: List[str]
|
8 |
+
# keyterms: List[tuple]
|
9 |
+
# entities: List[str]
|
10 |
+
raw_text: str
|
11 |
+
extracted_keywords: list
|
12 |
+
keyterms: list
|
13 |
+
entities: list
|
14 |
+
|
15 |
+
@property
|
16 |
+
def required_skills(self) -> List[str]:
|
17 |
+
return [kw for kw in self.extracted_keywords if 'skill' in kw.lower()]
|
18 |
+
|
19 |
+
@property
|
20 |
+
def experience_requirements(self) -> Dict:
|
21 |
+
return {
|
22 |
+
'years': self._extract_years(),
|
23 |
+
'technologies': self._extract_tech()
|
24 |
+
}
|
25 |
+
|
26 |
+
def _extract_years(self) -> int:
|
27 |
+
# Extract years requirement using regex
|
28 |
+
pass
|
29 |
+
|
30 |
+
def _extract_tech(self) -> List[str]:
|
31 |
+
# Extract required technologies
|
32 |
+
pass
|
data_models/resume.py
ADDED
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from dataclasses import dataclass
|
2 |
+
from typing import Dict, List
|
3 |
+
|
4 |
+
@dataclass
|
5 |
+
class Resume:
|
6 |
+
# raw_text: str
|
7 |
+
# formatting: Dict
|
8 |
+
# metadata: Dict
|
9 |
+
# scores: Dict = None
|
10 |
+
raw_text: str
|
11 |
+
extracted_keywords: list
|
12 |
+
keyterms: list
|
13 |
+
entities: list
|
14 |
+
|
15 |
+
@classmethod
|
16 |
+
def from_docx(cls, file_path: str):
|
17 |
+
"""Parse while preserving formatting"""
|
18 |
+
text, formatting = parse_docx_with_formatting(file_path)
|
19 |
+
return cls(text, formatting, extract_metadata(text))
|
20 |
+
|
21 |
+
def to_docx(self, output_path: str):
|
22 |
+
"""Export with original formatting"""
|
23 |
+
apply_formatting(self.raw_text, self.formatting, output_path)
|
requirement.txt
ADDED
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Core
|
2 |
+
python-docx==0.8.11
|
3 |
+
pdfminer.six==20221105
|
4 |
+
#python-docx-template==0.16.0
|
5 |
+
docxtpl==0.16.0
|
6 |
+
pdf2docx==0.5.6
|
7 |
+
qdrant-client==1.6.4
|
8 |
+
sentence-transformers==2.2.2
|
9 |
+
|
10 |
+
# API & Services
|
11 |
+
google-api-python-client==2.104.0
|
12 |
+
gspread==5.11.3
|
13 |
+
requests==2.31.0
|
14 |
+
oauth2client==4.1.3
|
15 |
+
|
16 |
+
# UI
|
17 |
+
streamlit==1.29.0
|
18 |
+
plotly==5.18.0
|
19 |
+
|
20 |
+
# NLP
|
21 |
+
spacy==3.7.2
|
22 |
+
en-core-web-md @ https://github.com/explosion/spacy-models/releases/download/en_core_web_md-3.7.0/en_core_web_md-3.7.0-py3-none-any.whl
|
setup.py
ADDED
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# setup.py
|
2 |
+
from setuptools import setup, find_packages
|
3 |
+
|
4 |
+
setup(
|
5 |
+
name="ats_optimizer",
|
6 |
+
version="0.1",
|
7 |
+
packages=find_packages(),
|
8 |
+
install_requires=[
|
9 |
+
'python-docx>=0.8.11',
|
10 |
+
'pdfminer.six>=20221105',
|
11 |
+
'pyyaml>=6.0',
|
12 |
+
'streamlit>=1.28.0',
|
13 |
+
'spacy>=3.7.2',
|
14 |
+
],
|
15 |
+
python_requires='>=3.9',
|
16 |
+
)
|
templates/Ramen_DXC.docx
ADDED
Binary file (39.9 kB). View file
|
|
templates/professional.docx
ADDED
File without changes
|
utils/__init__.py
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from .file_handlers import FileHandler
|
2 |
+
from .logger import setup_logger
|
3 |
+
from .config_manager import Config
|
4 |
+
|
5 |
+
__all__ = ['FileHandler', 'setup_logger', 'Config']
|
utils/config_manager.py
ADDED
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import yaml
|
2 |
+
|
3 |
+
class Config:
|
4 |
+
def __init__(self, config_path: str):
|
5 |
+
with open(config_path, 'r') as f:
|
6 |
+
self.config = yaml.safe_load(f)
|
7 |
+
|
8 |
+
def __getattr__(self, name):
|
9 |
+
return self.config.get(name)
|
utils/file_handlers.py
ADDED
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
from docx import Document
|
3 |
+
from pdfminer.high_level import extract_text
|
4 |
+
from pathlib import Path
|
5 |
+
from tempfile import NamedTemporaryFile
|
6 |
+
import uuid
|
7 |
+
|
8 |
+
class FileHandler:
|
9 |
+
@staticmethod
|
10 |
+
def read_file(file_path: str) -> str:
|
11 |
+
if file_path.endswith('.docx'):
|
12 |
+
return FileHandler._read_docx(file_path)
|
13 |
+
elif file_path.endswith('.pdf'):
|
14 |
+
return extract_text(file_path)
|
15 |
+
else:
|
16 |
+
with open(file_path, 'r') as f:
|
17 |
+
return f.read()
|
18 |
+
#--
|
19 |
+
|
20 |
+
@staticmethod
|
21 |
+
def save_uploaded_file(uploaded_file, directory="temp_uploads"):
|
22 |
+
"""Save Streamlit uploaded file to a temporary directory"""
|
23 |
+
try:
|
24 |
+
# Create directory if it doesn't exist
|
25 |
+
Path(directory).mkdir(exist_ok=True)
|
26 |
+
|
27 |
+
# Generate unique filename
|
28 |
+
file_ext = Path(uploaded_file.name).suffix
|
29 |
+
unique_id = uuid.uuid4().hex
|
30 |
+
temp_file = Path(directory) / f"{unique_id}{file_ext}"
|
31 |
+
|
32 |
+
# Save file
|
33 |
+
with open(temp_file, "wb") as f:
|
34 |
+
f.write(uploaded_file.getbuffer())
|
35 |
+
|
36 |
+
return str(temp_file)
|
37 |
+
except Exception as e:
|
38 |
+
print(f"Error saving file: {e}")
|
39 |
+
return None
|
40 |
+
|
41 |
+
@staticmethod
|
42 |
+
def cleanup_temp_files(directory="temp_uploads"):
|
43 |
+
"""Remove temporary files"""
|
44 |
+
try:
|
45 |
+
for file in Path(directory).glob("*"):
|
46 |
+
file.unlink()
|
47 |
+
except Exception as e:
|
48 |
+
print(f"Error cleaning files: {e}")
|
49 |
+
#--
|
50 |
+
|
51 |
+
@staticmethod
|
52 |
+
def _read_docx(file_path: str) -> str:
|
53 |
+
doc = Document(file_path)
|
54 |
+
return '\n'.join([para.text for para in doc.paragraphs])
|
55 |
+
|
56 |
+
@staticmethod
|
57 |
+
def save_resume(resume_data: dict, output_path: str):
|
58 |
+
if output_path.endswith('.docx'):
|
59 |
+
FileHandler._save_as_docx(resume_data, output_path)
|
60 |
+
else:
|
61 |
+
with open(output_path, 'w') as f:
|
62 |
+
f.write(resume_data['content'])
|
63 |
+
|
64 |
+
@staticmethod
|
65 |
+
def _save_as_docx(resume_data: dict, output_path: str):
|
66 |
+
doc = Document()
|
67 |
+
# Add formatting preservation logic here
|
68 |
+
doc.save(output_path)
|
69 |
+
|
utils/file_handlers1.py
ADDED
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
from pathlib import Path
|
3 |
+
import uuid
|
4 |
+
from docx import Document
|
5 |
+
from pdfminer.high_level import extract_text
|
6 |
+
|
7 |
+
class FileHandler:
|
8 |
+
@staticmethod
|
9 |
+
def save_uploaded_file(uploaded_file, directory="temp_uploads"):
|
10 |
+
"""Save Streamlit uploaded file to temporary directory"""
|
11 |
+
try:
|
12 |
+
# Create directory if needed
|
13 |
+
Path(directory).mkdir(exist_ok=True)
|
14 |
+
|
15 |
+
# Generate unique filename
|
16 |
+
file_ext = Path(uploaded_file.name).suffix
|
17 |
+
unique_id = uuid.uuid4().hex
|
18 |
+
temp_file = Path(directory) / f"{unique_id}{file_ext}"
|
19 |
+
|
20 |
+
# Save file
|
21 |
+
with open(temp_file, "wb") as f:
|
22 |
+
f.write(uploaded_file.getbuffer())
|
23 |
+
|
24 |
+
return str(temp_file)
|
25 |
+
except Exception as e:
|
26 |
+
print(f"Error saving file: {e}")
|
27 |
+
return None
|
28 |
+
|
29 |
+
@staticmethod
|
30 |
+
def save_resume(resume_data, output_path):
|
31 |
+
"""Save resume content to file"""
|
32 |
+
try:
|
33 |
+
if output_path.endswith('.docx'):
|
34 |
+
doc = Document()
|
35 |
+
for paragraph in resume_data.split('\n'):
|
36 |
+
doc.add_paragraph(paragraph)
|
37 |
+
doc.save(output_path)
|
38 |
+
else:
|
39 |
+
with open(output_path, 'w') as f:
|
40 |
+
f.write(resume_data)
|
41 |
+
return True
|
42 |
+
except Exception as e:
|
43 |
+
print(f"Error saving resume: {e}")
|
44 |
+
return False
|
45 |
+
|
46 |
+
@staticmethod
|
47 |
+
def cleanup_temp_files(directory="temp_uploads"):
|
48 |
+
"""Remove temporary files"""
|
49 |
+
try:
|
50 |
+
for file in Path(directory).glob("*"):
|
51 |
+
file.unlink()
|
52 |
+
return True
|
53 |
+
except Exception as e:
|
54 |
+
print(f"Error cleaning files: {e}")
|
55 |
+
return False
|
utils/logger.py
ADDED
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import logging
|
2 |
+
|
3 |
+
def setup_logger(name: str = 'ats_optimizer'):
|
4 |
+
logger = logging.getLogger(name)
|
5 |
+
logger.setLevel(logging.INFO)
|
6 |
+
|
7 |
+
handler = logging.StreamHandler()
|
8 |
+
formatter = logging.Formatter(
|
9 |
+
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
10 |
+
)
|
11 |
+
handler.setFormatter(formatter)
|
12 |
+
logger.addHandler(handler)
|
13 |
+
|
14 |
+
return logger
|
15 |
+
|
16 |
+
|
17 |
+
# Create a module-level logger instance
|
18 |
+
logger = setup_logger()
|