heavenEyes commited on
Commit
8564de9
·
verified ·
1 Parent(s): d2b7b1d

Upload 11 files

Browse files
Files changed (11) hide show
  1. .gitattributes +1 -35
  2. .gitignore +2 -0
  3. app.py +476 -0
  4. ats_scorer.pkl +3 -0
  5. clf.pkl +3 -0
  6. encoder.pkl +3 -0
  7. get-pip.py +0 -0
  8. prototypes.pkl +3 -0
  9. tfidf.pkl +3 -0
  10. train_ats_model.py +94 -0
  11. train_model.py +60 -0
.gitattributes CHANGED
@@ -1,35 +1 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
1
+ *.pkl filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.gitignore ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ clf.pkl
2
+ *.pkl
app.py ADDED
@@ -0,0 +1,476 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import pickle
3
+ import re
4
+ import docx
5
+ import PyPDF2
6
+ from sklearn.metrics.pairwise import cosine_similarity
7
+
8
+ # 1. CONFIG
9
+ st.set_page_config(
10
+ page_title="AI Resume Screening",
11
+ layout="wide",
12
+ initial_sidebar_state="collapsed"
13
+ )
14
+
15
+ # Custom CSS for styling
16
+ st.markdown("""
17
+ <style>
18
+ /* Main background */
19
+ .stApp {
20
+ background: linear-gradient(135deg, #0f0f0f 0%, #1a1a1a 100%);
21
+ }
22
+
23
+ /* Main container styling */
24
+ .main .block-container {
25
+ padding-top: 2rem;
26
+ padding-bottom: 2rem;
27
+ max-width: 1200px;
28
+ }
29
+
30
+ /* Header styling */
31
+ h1 {
32
+ color: #ffffff !important;
33
+ font-weight: 700 !important;
34
+ text-align: center;
35
+ font-size: 3rem !important;
36
+ margin-bottom: 0.5rem !important;
37
+ text-shadow: 2px 2px 8px rgba(0,0,0,0.5);
38
+ }
39
+
40
+ .subtitle {
41
+ color: #a0a0a0;
42
+ text-align: center;
43
+ font-size: 1.1rem;
44
+ margin-bottom: 2rem;
45
+ }
46
+
47
+ /* Remove default streamlit styling */
48
+ .element-container {
49
+ background: transparent !important;
50
+ }
51
+
52
+ /* File uploader styling */
53
+ section[data-testid="stFileUploader"] {
54
+ background: rgba(30, 30, 30, 0.6);
55
+ border: 2px dashed #404040;
56
+ border-radius: 15px;
57
+ padding: 2rem;
58
+ backdrop-filter: blur(10px);
59
+ }
60
+
61
+ section[data-testid="stFileUploader"]:hover {
62
+ border-color: #606060;
63
+ background: rgba(35, 35, 35, 0.7);
64
+ }
65
+
66
+ section[data-testid="stFileUploader"] label {
67
+ color: #e0e0e0 !important;
68
+ font-size: 1.1rem !important;
69
+ font-weight: 600 !important;
70
+ }
71
+
72
+ /* Metric cards */
73
+ div[data-testid="metric-container"] {
74
+ background: linear-gradient(135deg, #2a2a2a 0%, #1f1f1f 100%);
75
+ border: 1px solid #404040;
76
+ border-radius: 15px;
77
+ padding: 1.5rem;
78
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
79
+ }
80
+
81
+ div[data-testid="metric-container"] label {
82
+ color: #b0b0b0 !important;
83
+ font-weight: 600 !important;
84
+ font-size: 0.9rem !important;
85
+ }
86
+
87
+ div[data-testid="metric-container"] div[data-testid="stMetricValue"] {
88
+ color: #ffffff !important;
89
+ font-size: 2rem !important;
90
+ font-weight: 700 !important;
91
+ }
92
+
93
+ div[data-testid="metric-container"] div[data-testid="stMetricDelta"] {
94
+ color: #808080 !important;
95
+ }
96
+
97
+ /* Success box styling */
98
+ .stSuccess {
99
+ background: rgba(40, 40, 40, 0.8);
100
+ color: #e0e0e0;
101
+ border-left: 4px solid #606060;
102
+ border-radius: 10px;
103
+ padding: 1.5rem;
104
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
105
+ }
106
+
107
+ .stSuccess h3 {
108
+ color: #ffffff !important;
109
+ }
110
+
111
+ /* Warning box styling */
112
+ .stWarning {
113
+ background: rgba(50, 40, 40, 0.8);
114
+ color: #e0e0e0;
115
+ border-left: 4px solid #707070;
116
+ border-radius: 10px;
117
+ padding: 1.5rem;
118
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
119
+ }
120
+
121
+ /* Info box styling */
122
+ .stInfo {
123
+ background: rgba(40, 40, 40, 0.8);
124
+ color: #e0e0e0;
125
+ border-left: 4px solid #606060;
126
+ border-radius: 10px;
127
+ padding: 1.5rem;
128
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
129
+ }
130
+
131
+ /* Progress bar */
132
+ .stProgress > div > div > div {
133
+ background: linear-gradient(90deg, #505050 0%, #707070 100%);
134
+ border-radius: 10px;
135
+ }
136
+
137
+ /* Expander */
138
+ .streamlit-expanderHeader {
139
+ background: rgba(40, 40, 40, 0.6);
140
+ border: 1px solid #404040;
141
+ border-radius: 10px;
142
+ color: #e0e0e0 !important;
143
+ font-weight: 600;
144
+ }
145
+
146
+ .streamlit-expanderContent {
147
+ background: rgba(30, 30, 30, 0.8);
148
+ border: 1px solid #404040;
149
+ border-radius: 0 0 10px 10px;
150
+ }
151
+
152
+ /* Text area */
153
+ textarea {
154
+ background: #1a1a1a !important;
155
+ color: #e0e0e0 !important;
156
+ border: 1px solid #404040 !important;
157
+ border-radius: 8px !important;
158
+ }
159
+
160
+ /* Custom Loading Animation */
161
+ .loading-container {
162
+ display: flex;
163
+ flex-direction: column;
164
+ align-items: center;
165
+ justify-content: center;
166
+ padding: 3rem;
167
+ margin: 2rem 0;
168
+ }
169
+
170
+ .spinner {
171
+ width: 60px;
172
+ height: 60px;
173
+ border: 4px solid #2a2a2a;
174
+ border-top: 4px solid #707070;
175
+ border-radius: 50%;
176
+ animation: spin 1s linear infinite;
177
+ }
178
+
179
+ @keyframes spin {
180
+ 0% { transform: rotate(0deg); }
181
+ 100% { transform: rotate(360deg); }
182
+ }
183
+
184
+ .loading-text {
185
+ color: #b0b0b0;
186
+ font-size: 1.1rem;
187
+ margin-top: 1.5rem;
188
+ font-weight: 600;
189
+ animation: pulse 1.5s ease-in-out infinite;
190
+ }
191
+
192
+ @keyframes pulse {
193
+ 0%, 100% { opacity: 0.6; }
194
+ 50% { opacity: 1; }
195
+ }
196
+
197
+ .loading-dots {
198
+ display: inline-block;
199
+ margin-left: 5px;
200
+ }
201
+
202
+ .loading-dots span {
203
+ animation: blink 1.4s infinite;
204
+ animation-fill-mode: both;
205
+ }
206
+
207
+ .loading-dots span:nth-child(2) {
208
+ animation-delay: 0.2s;
209
+ }
210
+
211
+ .loading-dots span:nth-child(3) {
212
+ animation-delay: 0.4s;
213
+ }
214
+
215
+ @keyframes blink {
216
+ 0%, 80%, 100% { opacity: 0; }
217
+ 40% { opacity: 1; }
218
+ }
219
+
220
+ .progress-bar-container {
221
+ width: 300px;
222
+ height: 4px;
223
+ background: #2a2a2a;
224
+ border-radius: 2px;
225
+ margin-top: 1rem;
226
+ overflow: hidden;
227
+ }
228
+
229
+ .progress-bar-fill {
230
+ height: 100%;
231
+ background: linear-gradient(90deg, #505050, #707070, #505050);
232
+ background-size: 200% 100%;
233
+ animation: loading 1.5s ease-in-out infinite;
234
+ }
235
+
236
+ @keyframes loading {
237
+ 0% { background-position: 200% 0; }
238
+ 100% { background-position: -200% 0; }
239
+ }
240
+
241
+ /* Spinner */
242
+ .stSpinner > div {
243
+ border-top-color: #707070 !important;
244
+ }
245
+
246
+ /* Hide Streamlit branding */
247
+ #MainMenu {visibility: hidden;}
248
+ footer {visibility: hidden;}
249
+
250
+ /* Score badge */
251
+ .score-badge {
252
+ display: inline-block;
253
+ padding: 0.75rem 2rem;
254
+ border-radius: 25px;
255
+ font-weight: 700;
256
+ font-size: 1.5rem;
257
+ margin: 1.5rem 0;
258
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.4);
259
+ }
260
+
261
+ .score-high {
262
+ background: linear-gradient(135deg, #3a3a3a 0%, #4a4a4a 100%);
263
+ color: #ffffff;
264
+ border: 2px solid #606060;
265
+ }
266
+
267
+ .score-medium {
268
+ background: linear-gradient(135deg, #353535 0%, #454545 100%);
269
+ color: #e0e0e0;
270
+ border: 2px solid #555555;
271
+ }
272
+
273
+ .score-low {
274
+ background: linear-gradient(135deg, #2f2f2f 0%, #3f3f3f 100%);
275
+ color: #d0d0d0;
276
+ border: 2px solid #505050;
277
+ }
278
+
279
+ /* Section headers */
280
+ h3, h4 {
281
+ color: #e0e0e0 !important;
282
+ }
283
+
284
+ /* Remove white backgrounds */
285
+ div[data-testid="stVerticalBlock"] > div {
286
+ background: transparent !important;
287
+ }
288
+
289
+ div[data-testid="column"] {
290
+ background: transparent !important;
291
+ }
292
+ </style>
293
+ """, unsafe_allow_html=True)
294
+
295
+ # 2. LOAD RESOURCES
296
+ @st.cache_resource
297
+ def load_resources():
298
+ try:
299
+ clf = pickle.load(open('clf.pkl', 'rb'))
300
+ tfidf = pickle.load(open('tfidf.pkl', 'rb'))
301
+ le = pickle.load(open('encoder.pkl', 'rb'))
302
+ ats = pickle.load(open('ats_scorer.pkl', 'rb'))
303
+ prototypes = pickle.load(open('prototypes.pkl', 'rb'))
304
+ return clf, tfidf, le, ats, prototypes
305
+ except FileNotFoundError:
306
+ return None, None, None, None, None
307
+
308
+ clf, tfidf, le, ats_model, prototypes = load_resources()
309
+
310
+ # 3. UTILS
311
+ def clean_text(txt):
312
+ txt = re.sub(r'http\S+\s', ' ', txt)
313
+ txt = re.sub(r'[^\w\s]', ' ', txt)
314
+ return txt.lower()
315
+
316
+ def extract_text(file):
317
+ try:
318
+ if file.name.endswith('.pdf'):
319
+ reader = PyPDF2.PdfReader(file)
320
+ return " ".join([page.extract_text() for page in reader.pages])
321
+ elif file.name.endswith('.docx'):
322
+ doc = docx.Document(file)
323
+ return " ".join([p.text for p in doc.paragraphs])
324
+ elif file.name.endswith('.txt'):
325
+ return file.read().decode('utf-8')
326
+ except:
327
+ return ""
328
+
329
+ def calculate_scores(text, category):
330
+ if category not in prototypes:
331
+ return 0, 0, 0
332
+
333
+ master_profile = prototypes[category]
334
+ cleaned_resume = clean_text(text)
335
+
336
+ # Cosine Similarity
337
+ vecs = tfidf.transform([cleaned_resume, master_profile])
338
+ cosine_sim = cosine_similarity(vecs[0], vecs[1])[0][0]
339
+
340
+ # Keyword Match
341
+ res_tokens = set(cleaned_resume.split())
342
+ mp_tokens = set(master_profile.split())
343
+ keyword_match = len(res_tokens.intersection(mp_tokens)) / len(mp_tokens) if mp_tokens else 0
344
+
345
+ # AI Prediction
346
+ try:
347
+ ml_score = ats_model.predict([[cosine_sim, keyword_match]])[0]
348
+ except:
349
+ ml_score = 0
350
+
351
+ # Fallback Logic
352
+ if ml_score < 10:
353
+ final_score = cosine_sim * 100
354
+ else:
355
+ final_score = ml_score
356
+ if final_score < 1:
357
+ final_score *= 100
358
+
359
+ return round(final_score, 1), round(cosine_sim*100, 1), round(keyword_match*100, 1)
360
+
361
+ # 4. MAIN APP
362
+ def main():
363
+ # Header
364
+ st.markdown("<h1>🎯 AI Resume Screening</h1>", unsafe_allow_html=True)
365
+ st.markdown("<p class='subtitle'>Powered by Machine Learning & Natural Language Processing</p>", unsafe_allow_html=True)
366
+
367
+ if not clf:
368
+ st.error("⚠️ Models missing! Run `train_model.py` then `train_ats_model.py`.")
369
+ st.stop()
370
+
371
+ # Upload section
372
+ st.markdown("<br>", unsafe_allow_html=True)
373
+ col1, col2, col3 = st.columns([1, 2, 1])
374
+ with col2:
375
+ file = st.file_uploader(
376
+ "📤 Upload Your Resume",
377
+ type=['pdf', 'docx', 'txt'],
378
+ help="Supported formats: PDF, DOCX, TXT"
379
+ )
380
+
381
+ if file:
382
+ # Custom loading animation
383
+ loading_placeholder = st.empty()
384
+
385
+ with loading_placeholder.container():
386
+ st.markdown("""
387
+ <div class='loading-container'>
388
+ <div class='spinner'></div>
389
+ <div class='loading-text'>
390
+ Analyzing your resume<span class='loading-dots'><span>.</span><span>.</span><span>.</span></span>
391
+ </div>
392
+ <div class='progress-bar-container'>
393
+ <div class='progress-bar-fill'></div>
394
+ </div>
395
+ </div>
396
+ """, unsafe_allow_html=True)
397
+
398
+ # Extract and process
399
+ text = extract_text(file)
400
+
401
+ # Clear loading animation
402
+ loading_placeholder.empty()
403
+
404
+ if len(text) > 20:
405
+ clean = clean_text(text)
406
+ vec = tfidf.transform([clean])
407
+ cat_id = clf.predict(vec)[0]
408
+ category = le.inverse_transform([cat_id])[0]
409
+
410
+ ats_score, raw_sim, key_match = calculate_scores(text, category)
411
+
412
+ st.markdown("<br>", unsafe_allow_html=True)
413
+
414
+ # Category prediction
415
+ st.success(f"### 🎯 Predicted Role: **{category}**")
416
+
417
+ # Score badge
418
+ if ats_score >= 75:
419
+ badge_class = "score-high"
420
+ emoji = "🌟"
421
+ elif ats_score >= 50:
422
+ badge_class = "score-medium"
423
+ emoji = "⚡"
424
+ else:
425
+ badge_class = "score-low"
426
+ emoji = "💡"
427
+
428
+ st.markdown(f"<div class='score-badge {badge_class}'>{emoji} ATS Score: {ats_score}%</div>", unsafe_allow_html=True)
429
+
430
+ # Metrics
431
+ st.markdown("### 📊 Detailed Analysis")
432
+ col1, col2, col3 = st.columns(3)
433
+
434
+ with col1:
435
+ st.metric(
436
+ label="🤖 AI Score",
437
+ value=f"{ats_score}%",
438
+ delta="Primary Score"
439
+ )
440
+
441
+ with col2:
442
+ st.metric(
443
+ label="📝 Content Match",
444
+ value=f"{raw_sim}%",
445
+ delta="Similarity"
446
+ )
447
+
448
+ with col3:
449
+ st.metric(
450
+ label="🔑 Keywords",
451
+ value=f"{key_match}%",
452
+ delta="Overlap"
453
+ )
454
+
455
+ # Progress bar
456
+ st.markdown("#### Match Strength")
457
+ st.progress(min(ats_score/100, 1.0))
458
+
459
+ # Feedback
460
+ if ats_score > 75:
461
+ st.balloons()
462
+ st.info("🎉 Excellent match! Your resume aligns well with this role.")
463
+ elif ats_score >= 50:
464
+ st.info("✨ Good match! Consider adding more role-specific keywords to improve.")
465
+ else:
466
+ st.warning("💡 Low match. Try adding more relevant skills and experience keywords.")
467
+
468
+ # Extracted text
469
+ with st.expander("📄 View Extracted Text"):
470
+ st.text_area("Resume Content", text, height=300)
471
+ else:
472
+ st.warning("⚠️ Could not extract text. File might be an image or scan. Please use a text-based document.")
473
+
474
+ if __name__ == "__main__":
475
+ main()
476
+
ats_scorer.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:46a86039608acdbfa77e66aa3c1f28e7f8a973a7d8afab60e29fcb1d608d50e2
3
+ size 136514
clf.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:98aef3fe524a35a33ed5c2ae0eec3a776e2723a6c9e2ce5eac13856e56c23a92
3
+ size 931343794
encoder.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:78015b3f43903a6934275ab46f36735b862d6c8876ac36028e461e879a1f05c4
3
+ size 1104
get-pip.py ADDED
The diff for this file is too large to render. See raw diff
 
prototypes.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3061636877e02cc7eef55acb57a6ba899c63ce14c310021816e258d89250cfd4
3
+ size 27298599
tfidf.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9d353d58aff8571ebd20b03567ab10aae98e5e71b350b17f69e06c7b3162af60
3
+ size 185690
train_ats_model.py ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import pickle
3
+ import numpy as np
4
+ from datasets import load_dataset
5
+ from sklearn.metrics.pairwise import cosine_similarity
6
+ from sklearn.ensemble import GradientBoostingRegressor
7
+ from sklearn.model_selection import train_test_split
8
+ import re
9
+ import time
10
+
11
+ def train_ats_scorer():
12
+ # 1. Load Dependencies
13
+ print("Loading TF-IDF Vectorizer (from Step 1)...")
14
+ try:
15
+ tfidf = pickle.load(open('tfidf.pkl', 'rb'))
16
+ except FileNotFoundError:
17
+ print("ERROR: 'tfidf.pkl' not found. Run 'train_model.py' first!")
18
+ exit()
19
+
20
+ # 2. Load ATS Dataset (0xnbk)
21
+ print("Loading 0xnbk/resume-ats-score-v1-en...")
22
+ try:
23
+ ds = load_dataset("0xnbk/resume-ats-score-v1-en")
24
+ df = pd.DataFrame(ds['train'])
25
+ print(f"Loaded {len(df)} rows.")
26
+ except Exception as e:
27
+ print(f"Error loading dataset: {e}")
28
+ exit()
29
+
30
+ # 3. Pre-Process
31
+ res_col = 'text'
32
+ score_col = 'ats_score'
33
+ cat_col = 'original_label'
34
+
35
+ df[score_col] = pd.to_numeric(df[score_col], errors='coerce')
36
+ df.dropna(subset=[score_col, res_col], inplace=True)
37
+
38
+ # 4. Generate Training Prototypes
39
+ print("Generating Training Prototypes...")
40
+ # Group resumes by label to simulate "Job Descriptions"
41
+ train_prototypes = df.groupby(cat_col)[res_col].apply(lambda x: ' '.join(x)).to_dict()
42
+
43
+ # Optimization: Pre-calculate vectors
44
+ print("Pre-calculating vectors...")
45
+ proto_vectors = {}
46
+ proto_tokens = {}
47
+
48
+ for cat, text in train_prototypes.items():
49
+ proto_vectors[cat] = tfidf.transform([text])
50
+ proto_tokens[cat] = set(re.findall(r'\w+', text.lower()))
51
+
52
+ # 5. Feature Engineering
53
+ print("Calculating features...")
54
+ cosine_sims = []
55
+ keyword_matches = []
56
+
57
+ for i, row in enumerate(df.itertuples()):
58
+ text = str(getattr(row, res_col))
59
+ cat = getattr(row, cat_col)
60
+
61
+ if cat in proto_vectors:
62
+ # Feature 1: Similarity
63
+ vec = tfidf.transform([text])
64
+ target_vec = proto_vectors[cat]
65
+ sim = cosine_similarity(vec, target_vec)[0][0]
66
+
67
+ # Feature 2: Keyword Match
68
+ tokens = set(re.findall(r'\w+', text.lower()))
69
+ target_tokens = proto_tokens[cat]
70
+ match = len(tokens.intersection(target_tokens)) / len(target_tokens) if target_tokens else 0
71
+ else:
72
+ sim = 0
73
+ match = 0
74
+
75
+ cosine_sims.append(sim)
76
+ keyword_matches.append(match)
77
+
78
+ df['cosine_sim'] = cosine_sims
79
+ df['keyword_match'] = keyword_matches
80
+
81
+ # 6. Train Regressor
82
+ print("Training ATS Regressor...")
83
+ X = df[['cosine_sim', 'keyword_match']]
84
+ y = df[score_col]
85
+
86
+ reg = GradientBoostingRegressor()
87
+ reg.fit(X, y)
88
+
89
+ # 7. Save
90
+ pickle.dump(reg, open('ats_scorer.pkl', 'wb'))
91
+ print("SUCCESS: 'ats_scorer.pkl' saved.")
92
+
93
+ if __name__ == "__main__":
94
+ train_ats_scorer()
train_model.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import pickle
3
+ import re
4
+ from datasets import load_dataset
5
+ from sklearn.feature_extraction.text import TfidfVectorizer
6
+ from sklearn.multiclass import OneVsRestClassifier
7
+ from sklearn.neighbors import KNeighborsClassifier
8
+ from sklearn.preprocessing import LabelEncoder
9
+
10
+ def train_classifier():
11
+ print("Loading AzharAli05/Resume-Screening-Dataset...")
12
+ try:
13
+ ds = load_dataset("AzharAli05/Resume-Screening-Dataset")
14
+ df = pd.DataFrame(ds['train'])
15
+ print(f"Loaded {len(df)} resumes.")
16
+ except Exception as e:
17
+ print(f"Error loading dataset: {e}")
18
+ exit()
19
+
20
+ text_col = 'Resume'
21
+ label_col = 'Role'
22
+
23
+ def clean_resume(txt):
24
+ cleanText = re.sub(r'http\S+\s', ' ', str(txt))
25
+ cleanText = re.sub(r'RT|cc', ' ', cleanText)
26
+ cleanText = re.sub(r'#\S+\s', ' ', cleanText)
27
+ cleanText = re.sub(r'@\S+', ' ', cleanText)
28
+ cleanText = re.sub(r'[!"#$%&\'()*+,-./:;<=>?@[\]^_`{|}~]', ' ', cleanText)
29
+ cleanText = re.sub(r'[^\x00-\x7f]', ' ', cleanText)
30
+ cleanText = re.sub(r'\s+', ' ', cleanText)
31
+ return cleanText
32
+
33
+ print("Cleaning data...")
34
+ df['cleaned_resume'] = df[text_col].apply(clean_resume)
35
+
36
+ print("Generating Master Profiles (Prototypes)...")
37
+
38
+ prototypes = df.groupby(label_col)['cleaned_resume'].apply(lambda x: ' '.join(x)).to_dict()
39
+ pickle.dump(prototypes, open('prototypes.pkl', 'wb'))
40
+
41
+ le = LabelEncoder()
42
+ df['Category_ID'] = le.fit_transform(df[label_col])
43
+
44
+ print("Vectorizing...")
45
+ tfidf = TfidfVectorizer(stop_words='english', max_features=5000)
46
+ tfidf.fit(df['cleaned_resume'])
47
+ requiredText = tfidf.transform(df['cleaned_resume'])
48
+
49
+ print("Training Classifier...")
50
+ clf = OneVsRestClassifier(KNeighborsClassifier())
51
+ clf.fit(requiredText, df['Category_ID'])
52
+
53
+ print("Saving models...")
54
+ pickle.dump(clf, open('clf.pkl', 'wb'))
55
+ pickle.dump(tfidf, open('tfidf.pkl', 'wb'))
56
+ pickle.dump(le, open('encoder.pkl', 'wb'))
57
+ print("SUCCESS: Classification models + Prototypes saved.")
58
+
59
+ if __name__ == "__main__":
60
+ train_classifier()