Dev1012 commited on
Commit
c1da06e
·
1 Parent(s): d4efc9c

Initial deploy: ATS Score Analyzer API

Browse files
Files changed (2) hide show
  1. app.py +31 -21
  2. ats_core.py +127 -46
app.py CHANGED
@@ -1,9 +1,13 @@
1
  import os
2
  import time
3
- from fastapi import FastAPI, HTTPException, Request
4
  from pydantic import BaseModel
5
- from ats_core import ats_score, extract_text_from_pdf, ROLE_TEMPLATES
6
 
 
 
 
 
 
7
 
8
  PORT = int(os.environ.get("PORT", 7860))
9
 
@@ -13,14 +17,10 @@ app = FastAPI(
13
  version="1.0.0"
14
  )
15
 
 
16
  USAGE_LIMIT = 5
17
  usage_tracker = {}
18
 
19
- class ATSRequest(BaseModel):
20
- resume_text: str
21
- job_description: str
22
-
23
-
24
  def check_rate_limit(request: Request):
25
  ip = request.client.host
26
  today = time.strftime("%Y-%m-%d")
@@ -33,19 +33,17 @@ def check_rate_limit(request: Request):
33
 
34
  usage_tracker[ip][today] = usage_tracker[ip].get(today, 0) + 1
35
 
36
-
37
- from fastapi import UploadFile, File, Form
38
-
39
  @app.post("/ats-score")
40
  async def compute_ats(
 
41
  resume_file: UploadFile = File(...),
42
  job_description: str = Form(""),
43
- role: str = Form(""),
44
- request: Request = None
45
  ):
46
  check_rate_limit(request)
47
 
48
- # Read resume PDF
49
  if resume_file.content_type != "application/pdf":
50
  raise HTTPException(status_code=400, detail="Resume must be a PDF")
51
 
@@ -56,17 +54,29 @@ async def compute_ats(
56
  raise HTTPException(status_code=400, detail="Could not extract text from resume")
57
 
58
  # Decide JD source
59
- if job_description.strip():
 
 
60
  jd_text = job_description
61
- elif role.lower() in ROLE_TEMPLATES:
62
- jd_text = ROLE_TEMPLATES[role.lower()]
 
 
63
  else:
64
- raise HTTPException(
65
- status_code=400,
66
- detail="Provide job description text or select a valid role"
67
- )
 
 
 
 
 
 
 
 
68
 
69
- return ats_score(resume_text, jd_text)
70
 
71
  @app.get("/health")
72
  def health():
 
1
  import os
2
  import time
3
+ from fastapi import FastAPI, HTTPException, Request, UploadFile, File, Form
4
  from pydantic import BaseModel
 
5
 
6
+ from ats_core import (
7
+ ats_score,
8
+ extract_text_from_pdf,
9
+ ROLE_TEMPLATES
10
+ )
11
 
12
  PORT = int(os.environ.get("PORT", 7860))
13
 
 
17
  version="1.0.0"
18
  )
19
 
20
+ # ---------------- Rate Limiting ----------------
21
  USAGE_LIMIT = 5
22
  usage_tracker = {}
23
 
 
 
 
 
 
24
  def check_rate_limit(request: Request):
25
  ip = request.client.host
26
  today = time.strftime("%Y-%m-%d")
 
33
 
34
  usage_tracker[ip][today] = usage_tracker[ip].get(today, 0) + 1
35
 
36
+ # ---------------- API ----------------
 
 
37
  @app.post("/ats-score")
38
  async def compute_ats(
39
+ request: Request,
40
  resume_file: UploadFile = File(...),
41
  job_description: str = Form(""),
42
+ role: str = Form("")
 
43
  ):
44
  check_rate_limit(request)
45
 
46
+ # Validate resume
47
  if resume_file.content_type != "application/pdf":
48
  raise HTTPException(status_code=400, detail="Resume must be a PDF")
49
 
 
54
  raise HTTPException(status_code=400, detail="Could not extract text from resume")
55
 
56
  # Decide JD source
57
+ use_provided_jd = bool(job_description.strip())
58
+
59
+ if use_provided_jd:
60
  jd_text = job_description
61
+ role_context = ROLE_TEMPLATES.get(role.lower(), "")
62
+ jd_weight = 0.85
63
+ role_weight = 0.15
64
+ role_only = False
65
  else:
66
+ if role.lower() not in ROLE_TEMPLATES:
67
+ raise HTTPException(
68
+ status_code=400,
69
+ detail="Provide a job description or select a valid role"
70
+ )
71
+ jd_text = ROLE_TEMPLATES[role.lower()]
72
+ role_context = ""
73
+ jd_weight = 1.0
74
+ role_weight = 0.0
75
+ role_only = True
76
+
77
+ combined_jd = jd_text * int(jd_weight * 10) + role_context * int(role_weight * 10)
78
 
79
+ return ats_score(resume_text, combined_jd, role_only=role_only)
80
 
81
  @app.get("/health")
82
  def health():
ats_core.py CHANGED
@@ -1,40 +1,104 @@
1
- from sentence_transformers import SentenceTransformer
2
- from sklearn.metrics.pairwise import cosine_similarity
3
- import nltk
4
  import re
 
5
  import pdfplumber
 
 
6
 
7
  nltk.download("stopwords")
8
  from nltk.corpus import stopwords
9
 
 
 
 
 
 
 
 
 
 
10
  ROLE_TEMPLATES = {
11
  "backend": """
12
- Backend Engineer with experience in Python, APIs, databases,
13
- system design, REST services, Docker, and scalable backend systems.
14
- """,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  "frontend": """
16
- Frontend Developer skilled in JavaScript, React, HTML, CSS,
17
- responsive design, UI/UX, and modern frontend frameworks.
18
- """,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  "ml": """
20
- Machine Learning Engineer with experience in Python, data analysis,
21
- machine learning models, feature engineering, evaluation metrics,
22
- and deployment of ML systems.
23
- """,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  "data": """
25
- Data Analyst with experience in SQL, Python, data visualization,
26
- statistics, dashboards, and business insights.
27
- """
28
- }
29
 
30
- model = SentenceTransformer("all-MiniLM-L6-v2")
31
- STOPWORDS = set(stopwords.words("english"))
32
 
33
- GENERIC_WORDS = {
34
- "looking", "engineer", "developer", "role",
35
- "experience", "skills", "responsibilities"
 
 
 
 
 
 
 
 
 
36
  }
37
 
 
38
  def to_float(x):
39
  return float(x)
40
 
@@ -53,6 +117,11 @@ def embedding_similarity(text1, text2):
53
  emb = model.encode([text1, text2])
54
  return cosine_similarity([emb[0]], [emb[1]])[0][0]
55
 
 
 
 
 
 
56
  def formatting_score(resume_text):
57
  score = 0
58
  text = resume_text.lower()
@@ -65,47 +134,59 @@ def formatting_score(resume_text):
65
  wc = len(resume_text.split())
66
  if 300 <= wc <= 900: score += 2
67
 
68
- return score # max 10
69
-
70
- def extract_experience(text):
71
- lines = text.lower().split("\n")
72
- exp_lines = [l for l in lines if "experience" in l or "-" in l]
73
- return " ".join(exp_lines) if exp_lines else text
74
 
75
- def ats_score(resume_text, jd_text):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  skill_sim = embedding_similarity(resume_text, jd_text)
77
 
78
  jd_keywords = extract_keywords(jd_text)
79
  resume_keywords = extract_keywords(resume_text)
80
-
81
- matched = set(jd_keywords) & set(resume_keywords)
82
- keyword_score = len(matched) / max(len(jd_keywords), 1)
83
 
84
  exp_text = extract_experience(resume_text)
85
  exp_sim = embedding_similarity(exp_text, jd_text)
86
 
87
- format_score = formatting_score(resume_text) / 10
 
 
 
 
 
88
 
89
  final_score = (
90
- 0.4 * skill_sim +
91
- 0.3 * keyword_score +
92
- 0.2 * exp_sim +
93
- 0.1 * format_score
94
  ) * 100
95
 
 
 
96
  return {
97
- "ats_score": to_float(round(final_score, 2)),
 
98
  "skill_score": to_float(round(skill_sim * 40, 2)),
99
  "keyword_score": to_float(round(keyword_score * 30, 2)),
100
  "experience_score": to_float(round(exp_sim * 20, 2)),
101
  "formatting_score": to_float(round(format_score * 10, 2)),
102
  "missing_keywords": list(set(jd_keywords) - set(resume_keywords))[:10]
103
  }
104
-
105
- def extract_text_from_pdf(file_bytes):
106
- text = ""
107
- with pdfplumber.open(file_bytes) as pdf:
108
- for page in pdf.pages:
109
- if page.extract_text():
110
- text += page.extract_text() + "\n"
111
- return text.strip()
 
 
 
 
1
  import re
2
+ import nltk
3
  import pdfplumber
4
+ from sentence_transformers import SentenceTransformer
5
+ from sklearn.metrics.pairwise import cosine_similarity
6
 
7
  nltk.download("stopwords")
8
  from nltk.corpus import stopwords
9
 
10
+ STOPWORDS = set(stopwords.words("english"))
11
+ model = SentenceTransformer("all-MiniLM-L6-v2")
12
+
13
+ GENERIC_WORDS = {
14
+ "looking", "engineer", "developer", "role",
15
+ "experience", "skills", "responsibilities"
16
+ }
17
+
18
+ # ---------------- Role-based Expanded JDs ----------------
19
  ROLE_TEMPLATES = {
20
  "backend": """
21
+ Role: Backend Engineer
22
+
23
+ Responsibilities:
24
+ Design and build scalable backend systems, REST APIs, and services.
25
+ Work with databases, authentication, performance optimization, and security.
26
+
27
+ Required Skills:
28
+ Python or similar backend language, API development, backend frameworks,
29
+ authentication mechanisms, and system design concepts.
30
+
31
+ Tools & Technologies:
32
+ Python, FastAPI/Flask/Django, SQL/NoSQL databases, Docker, Git, Linux.
33
+
34
+ Experience Expectations:
35
+ Building and deploying backend services, maintaining APIs, and debugging systems.
36
+
37
+ Nice to Have:
38
+ Cloud platforms, scalability, and distributed systems experience.
39
+ """,
40
+
41
  "frontend": """
42
+ Role: Frontend Developer
43
+
44
+ Responsibilities:
45
+ Build responsive and accessible user interfaces.
46
+ Integrate frontend with backend APIs and manage application state.
47
+
48
+ Required Skills:
49
+ JavaScript, HTML, CSS, React or similar frameworks, UI/UX understanding.
50
+
51
+ Tools & Technologies:
52
+ React, Next.js, CSS frameworks, browser dev tools, Git.
53
+
54
+ Experience Expectations:
55
+ Building real-world frontend applications and handling user interactions.
56
+
57
+ Nice to Have:
58
+ Performance optimization and design systems.
59
+ """,
60
+
61
  "ml": """
62
+ Role: Machine Learning Engineer
63
+
64
+ Responsibilities:
65
+ Develop, train, evaluate, and deploy machine learning models.
66
+ Perform data preprocessing and feature engineering.
67
+
68
+ Required Skills:
69
+ Python, ML algorithms, data preprocessing, model evaluation techniques.
70
+
71
+ Tools & Technologies:
72
+ Scikit-learn, PyTorch/TensorFlow, data pipelines, APIs.
73
+
74
+ Experience Expectations:
75
+ Working with real datasets and deploying ML systems.
76
+
77
+ Nice to Have:
78
+ NLP, computer vision, and MLOps experience.
79
+ """,
80
+
81
  "data": """
82
+ Role: Data Analyst
 
 
 
83
 
84
+ Responsibilities:
85
+ Analyze datasets, create dashboards, and communicate insights.
86
 
87
+ Required Skills:
88
+ SQL, Python, statistics, data visualization.
89
+
90
+ Tools & Technologies:
91
+ Pandas, NumPy, BI tools, Excel, databases.
92
+
93
+ Experience Expectations:
94
+ Cleaning and analyzing datasets to derive insights.
95
+
96
+ Nice to Have:
97
+ A/B testing and predictive analytics.
98
+ """
99
  }
100
 
101
+ # ---------------- Utilities ----------------
102
  def to_float(x):
103
  return float(x)
104
 
 
117
  emb = model.encode([text1, text2])
118
  return cosine_similarity([emb[0]], [emb[1]])[0][0]
119
 
120
+ def extract_experience(text):
121
+ lines = text.lower().split("\n")
122
+ exp_lines = [l for l in lines if "experience" in l or "-" in l]
123
+ return " ".join(exp_lines) if exp_lines else text
124
+
125
  def formatting_score(resume_text):
126
  score = 0
127
  text = resume_text.lower()
 
134
  wc = len(resume_text.split())
135
  if 300 <= wc <= 900: score += 2
136
 
137
+ return score / 10
 
 
 
 
 
138
 
139
+ def extract_text_from_pdf(file_bytes):
140
+ text = ""
141
+ with pdfplumber.open(file_bytes) as pdf:
142
+ for page in pdf.pages:
143
+ if page.extract_text():
144
+ text += page.extract_text() + "\n"
145
+ return text.strip()
146
+
147
+ def ats_verdict(score):
148
+ if score < 40:
149
+ return "Poor Fit"
150
+ elif score < 60:
151
+ return "Average Fit"
152
+ elif score < 75:
153
+ return "Good Fit"
154
+ else:
155
+ return "Excellent Fit"
156
+
157
+ # ---------------- Core Scoring ----------------
158
+ def ats_score(resume_text, jd_text, role_only=False):
159
  skill_sim = embedding_similarity(resume_text, jd_text)
160
 
161
  jd_keywords = extract_keywords(jd_text)
162
  resume_keywords = extract_keywords(resume_text)
163
+ keyword_score = len(set(jd_keywords) & set(resume_keywords)) / max(len(jd_keywords), 1)
 
 
164
 
165
  exp_text = extract_experience(resume_text)
166
  exp_sim = embedding_similarity(exp_text, jd_text)
167
 
168
+ format_score = formatting_score(resume_text)
169
+
170
+ if role_only:
171
+ skill_w, keyword_w, exp_w, format_w = 0.45, 0.25, 0.20, 0.10
172
+ else:
173
+ skill_w, keyword_w, exp_w, format_w = 0.40, 0.30, 0.20, 0.10
174
 
175
  final_score = (
176
+ skill_w * skill_sim +
177
+ keyword_w * keyword_score +
178
+ exp_w * exp_sim +
179
+ format_w * format_score
180
  ) * 100
181
 
182
+ final = to_float(round(final_score, 2))
183
+
184
  return {
185
+ "ats_score": final,
186
+ "verdict": ats_verdict(final),
187
  "skill_score": to_float(round(skill_sim * 40, 2)),
188
  "keyword_score": to_float(round(keyword_score * 30, 2)),
189
  "experience_score": to_float(round(exp_sim * 20, 2)),
190
  "formatting_score": to_float(round(format_score * 10, 2)),
191
  "missing_keywords": list(set(jd_keywords) - set(resume_keywords))[:10]
192
  }