Eklavya73 commited on
Commit
dff68c6
·
verified ·
1 Parent(s): e32e8fc

Upload 3 files

Browse files
Files changed (3) hide show
  1. README.md +48 -0
  2. app.py +461 -0
  3. requirements.txt +15 -0
README.md ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: CPPS — Campus Placement Prediction System
3
+ emoji: 🎓
4
+ colorFrom: blue
5
+ colorTo: purple
6
+ sdk: gradio
7
+ sdk_version: "6.13.0"
8
+ app_file: app.py
9
+ python_version: "3.10"
10
+ pinned: false
11
+ license: mit
12
+ ---
13
+
14
+ # CPPS — Campus Placement Prediction System
15
+
16
+ AI-powered resume-to-job-description matching system using shared production parsing, KeyBERT, and Sentence-BERT.
17
+
18
+ ## Features
19
+
20
+ - **Resume Upload** — Supports PDF and DOCX
21
+ - **Job Description Input** — Paste any JD text
22
+ - **Placement Score** — Overall match percentage with level badge
23
+ - **Component Breakdown** — 6-component MW-ESE scoring (Skills, Projects, Internships, Work Experience, Academics, Achievements)
24
+ - **Shared Source of Truth** — `app_logic.py` + `cpps_core.py` power both the app and the notebooks
25
+ - **Multi-Source Skill Mining** — Skills are collected from explicit skill sections, certifications, projects, internships, work experience, and full-text fallback parsing
26
+ - **Bucketed JD Skills** - JD skills are split into `required`, `preferred`, and `optional` buckets
27
+ - **Hybrid Weighted Skills Score** - `S_sk` combines bucket-weighted JD coverage, match confidence, and resume coverage with weights `0.45 / 0.35 / 0.20`
28
+ - **Degree-Aware Academics** - Degree requirements in the JD activate academics scoring, with GPA/percentage normalization and degree fallback scoring
29
+ - **Hybrid Internship Scoring** - Internships use count, semantic relevance, and parsed duration, including date ranges such as `Jan 2024 - Jun 2024`
30
+ - **Relevant Certification Credit** - Certifications are scored by semantic relevance to the target JD instead of only flat counts
31
+ - **Technical-Only Matching** - Soft skills are excluded so the matching pipeline focuses on hard technical skills
32
+ - **Expanded Skill Recall** - Matching includes synonym expansion, compound JD phrase decomposition, lower adaptive thresholds, and ontology-cluster partial matches
33
+ - **Match-Tier Debugging** - Debug output includes match types, JD bucket counts, source counts, rejected JD phrases, and diagnostic notes
34
+ - **Improvement Simulation** — What-if scenarios ranked by score impact
35
+ - **Actionable Recommendations** — Targeted suggestions to improve your profile
36
+
37
+ ## How It Works
38
+
39
+ 1. Upload your resume (PDF/DOCX)
40
+ 2. Paste the target job description
41
+ 3. Click **Analyze** to run the shared production pipeline and get the placement score plus detailed breakdown
42
+
43
+ ## Tech Stack
44
+
45
+ - **NLP Models**: Sentence-BERT (`all-MiniLM-L6-v2`), KeyBERT, spaCy
46
+ - **Skill Matching**: 5-pass (exact -> alias -> synonym -> adaptive semantic -> partial overlap/cluster)
47
+ - **Scoring**: Multi-Weight Expert Scoring Engine (MW-ESE) with bucket-weighted hybrid `S_sk`
48
+ - **UI**: Gradio
app.py ADDED
@@ -0,0 +1,461 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ CPPS Gradio App — Campus Placement Prediction System UI.
3
+
4
+ Upload a resume (PDF/DOCX) and paste a Job Description to get:
5
+ - Placement Score with match level
6
+ - Component breakdown (6 MW-ESE components)
7
+ - Improvement suggestions ranked by impact
8
+ - Actionable recommendations
9
+ """
10
+
11
+ import gradio as gr
12
+ import traceback
13
+ from app_logic import predict
14
+
15
+
16
+ # ── Theme & CSS ──────────────────────────────────────────────────────────
17
+
18
+ CUSTOM_CSS = """
19
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap');
20
+
21
+ * { font-family: 'Inter', sans-serif !important; }
22
+
23
+ .gradio-container {
24
+ max-width: 1200px !important;
25
+ margin: 0 auto !important;
26
+ background: linear-gradient(135deg, #0f0c29 0%, #1a1a3e 40%, #24243e 100%) !important;
27
+ }
28
+
29
+ /* ── Header ── */
30
+ .app-header {
31
+ text-align: center;
32
+ padding: 2rem 1rem 1rem;
33
+ background: linear-gradient(135deg, rgba(99, 102, 241, 0.15) 0%, rgba(168, 85, 247, 0.12) 100%);
34
+ border-radius: 16px;
35
+ border: 1px solid rgba(99, 102, 241, 0.2);
36
+ margin-bottom: 1.5rem;
37
+ }
38
+ .app-header h1 {
39
+ font-size: 2rem;
40
+ font-weight: 800;
41
+ background: linear-gradient(135deg, #818cf8, #c084fc, #f472b6);
42
+ -webkit-background-clip: text;
43
+ -webkit-text-fill-color: transparent;
44
+ margin: 0 0 0.3rem;
45
+ }
46
+ .app-header p {
47
+ color: #94a3b8;
48
+ font-size: 0.95rem;
49
+ margin: 0;
50
+ }
51
+
52
+ /* ── Score Card ── */
53
+ .score-card {
54
+ background: linear-gradient(135deg, rgba(99, 102, 241, 0.12) 0%, rgba(139, 92, 246, 0.08) 100%);
55
+ border: 1px solid rgba(99, 102, 241, 0.25);
56
+ border-radius: 16px;
57
+ padding: 2rem;
58
+ text-align: center;
59
+ margin-bottom: 1.5rem;
60
+ backdrop-filter: blur(12px);
61
+ }
62
+ .score-value {
63
+ font-size: 4rem;
64
+ font-weight: 900;
65
+ line-height: 1;
66
+ margin: 0.5rem 0;
67
+ }
68
+ .score-label {
69
+ font-size: 0.85rem;
70
+ text-transform: uppercase;
71
+ letter-spacing: 2px;
72
+ color: #94a3b8;
73
+ margin-bottom: 0.5rem;
74
+ }
75
+ .match-badge {
76
+ display: inline-block;
77
+ padding: 0.35rem 1.2rem;
78
+ border-radius: 20px;
79
+ font-weight: 700;
80
+ font-size: 0.8rem;
81
+ letter-spacing: 1px;
82
+ margin-top: 0.5rem;
83
+ }
84
+ .badge-excellent { background: rgba(52, 211, 153, 0.2); color: #34d399; border: 1px solid rgba(52, 211, 153, 0.3); }
85
+ .badge-good { background: rgba(96, 165, 250, 0.2); color: #60a5fa; border: 1px solid rgba(96, 165, 250, 0.3); }
86
+ .badge-moderate { background: rgba(251, 191, 36, 0.2); color: #fbbf24; border: 1px solid rgba(251, 191, 36, 0.3); }
87
+ .badge-below { background: rgba(251, 146, 60, 0.2); color: #fb923c; border: 1px solid rgba(251, 146, 60, 0.3); }
88
+ .badge-poor { background: rgba(248, 113, 113, 0.2); color: #f87171; border: 1px solid rgba(248, 113, 113, 0.3); }
89
+
90
+ /* ── Section Card ── */
91
+ .section-card {
92
+ background: rgba(30, 30, 60, 0.6);
93
+ border: 1px solid rgba(99, 102, 241, 0.15);
94
+ border-radius: 12px;
95
+ padding: 1.5rem;
96
+ margin-bottom: 1rem;
97
+ backdrop-filter: blur(8px);
98
+ }
99
+ .section-card h3 {
100
+ font-size: 1rem;
101
+ font-weight: 700;
102
+ color: #c4b5fd;
103
+ margin: 0 0 1rem;
104
+ display: flex;
105
+ align-items: center;
106
+ gap: 0.5rem;
107
+ }
108
+
109
+ /* ── Breakdown Table ── */
110
+ .breakdown-table {
111
+ width: 100%;
112
+ border-collapse: separate;
113
+ border-spacing: 0 6px;
114
+ }
115
+ .breakdown-table th {
116
+ text-align: left;
117
+ font-size: 0.75rem;
118
+ text-transform: uppercase;
119
+ letter-spacing: 1px;
120
+ color: #64748b;
121
+ padding: 0.5rem 0.75rem;
122
+ font-weight: 600;
123
+ }
124
+ .breakdown-table td {
125
+ padding: 0.65rem 0.75rem;
126
+ font-size: 0.9rem;
127
+ color: #e2e8f0;
128
+ background: rgba(99, 102, 241, 0.05);
129
+ border-top: 1px solid rgba(99, 102, 241, 0.08);
130
+ border-bottom: 1px solid rgba(99, 102, 241, 0.08);
131
+ }
132
+ .breakdown-table tr td:first-child { border-left: 1px solid rgba(99, 102, 241, 0.08); border-radius: 8px 0 0 8px; }
133
+ .breakdown-table tr td:last-child { border-right: 1px solid rgba(99, 102, 241, 0.08); border-radius: 0 8px 8px 0; }
134
+
135
+ /* ── Progress bar ── */
136
+ .progress-bar-bg {
137
+ background: rgba(99, 102, 241, 0.1);
138
+ border-radius: 6px;
139
+ height: 8px;
140
+ width: 100%;
141
+ overflow: hidden;
142
+ }
143
+ .progress-bar-fill {
144
+ height: 100%;
145
+ border-radius: 6px;
146
+ transition: width 0.6s ease;
147
+ }
148
+
149
+ /* ── Skill tags ── */
150
+ .skill-tag {
151
+ display: inline-block;
152
+ padding: 0.25rem 0.65rem;
153
+ border-radius: 6px;
154
+ font-size: 0.8rem;
155
+ font-weight: 500;
156
+ margin: 0.2rem;
157
+ }
158
+ .skill-missing { background: rgba(248, 113, 113, 0.15); color: #fca5a5; border: 1px solid rgba(248, 113, 113, 0.2); }
159
+ .skill-matched { background: rgba(52, 211, 153, 0.15); color: #6ee7b7; border: 1px solid rgba(52, 211, 153, 0.2); }
160
+
161
+ /* ── Improvement row ── */
162
+ .imp-row {
163
+ display: flex;
164
+ justify-content: space-between;
165
+ align-items: center;
166
+ padding: 0.6rem 0.75rem;
167
+ background: rgba(99, 102, 241, 0.05);
168
+ border: 1px solid rgba(99, 102, 241, 0.08);
169
+ border-radius: 8px;
170
+ margin-bottom: 6px;
171
+ font-size: 0.88rem;
172
+ color: #e2e8f0;
173
+ }
174
+ .imp-delta {
175
+ font-weight: 700;
176
+ color: #34d399;
177
+ white-space: nowrap;
178
+ }
179
+
180
+ /* ── Recommendation pill ── */
181
+ .rec-item {
182
+ display: flex;
183
+ align-items: flex-start;
184
+ gap: 0.5rem;
185
+ padding: 0.5rem 0;
186
+ color: #cbd5e1;
187
+ font-size: 0.9rem;
188
+ }
189
+ .rec-bullet {
190
+ color: #818cf8;
191
+ font-weight: 700;
192
+ flex-shrink: 0;
193
+ }
194
+
195
+ /* ── Info cards row ── */
196
+ .info-row {
197
+ display: grid;
198
+ grid-template-columns: 1fr 1fr;
199
+ gap: 1rem;
200
+ margin-bottom: 1rem;
201
+ }
202
+ .info-card {
203
+ background: rgba(30, 30, 60, 0.6);
204
+ border: 1px solid rgba(99, 102, 241, 0.15);
205
+ border-radius: 12px;
206
+ padding: 1.2rem;
207
+ text-align: center;
208
+ }
209
+ .info-card .label { font-size: 0.75rem; color: #94a3b8; text-transform: uppercase; letter-spacing: 1px; }
210
+ .info-card .value { font-size: 1.5rem; font-weight: 700; color: #c4b5fd; margin-top: 0.3rem; }
211
+
212
+ /* ── Error ── */
213
+ .error-card {
214
+ background: rgba(239, 68, 68, 0.1);
215
+ border: 1px solid rgba(239, 68, 68, 0.3);
216
+ border-radius: 12px;
217
+ padding: 1.5rem;
218
+ color: #f1f5f9;
219
+ }
220
+ .error-card h3 { color: #ffffff; margin: 0 0 0.5rem; }
221
+ .error-card pre { color: #e2e8f0; font-size: 0.8rem; white-space: pre-wrap; }
222
+
223
+ /* ── Gradio overrides ── */
224
+ footer { display: none !important; }
225
+ """
226
+
227
+ THEME = gr.themes.Base(
228
+ primary_hue=gr.themes.colors.indigo,
229
+ secondary_hue=gr.themes.colors.purple,
230
+ neutral_hue=gr.themes.colors.slate,
231
+ font=gr.themes.GoogleFont("Inter"),
232
+ )
233
+
234
+
235
+ # ── HTML Rendering ───────────────────────────────────────────────────────
236
+
237
+ def _score_color(score: float) -> str:
238
+ if score >= 85: return "#34d399"
239
+ if score >= 70: return "#60a5fa"
240
+ if score >= 50: return "#fbbf24"
241
+ if score >= 30: return "#fb923c"
242
+ return "#f87171"
243
+
244
+
245
+ def _badge_class(level: str) -> str:
246
+ level_map = {
247
+ "EXCELLENT": "badge-excellent",
248
+ "GOOD": "badge-good",
249
+ "MODERATE": "badge-moderate",
250
+ "BELOW AVERAGE": "badge-below",
251
+ "POOR": "badge-poor",
252
+ }
253
+ return level_map.get(level, "badge-poor")
254
+
255
+
256
+ def _bar_color(pct: float) -> str:
257
+ if pct >= 70: return "linear-gradient(90deg, #34d399, #6ee7b7)"
258
+ if pct >= 40: return "linear-gradient(90deg, #fbbf24, #fcd34d)"
259
+ return "linear-gradient(90deg, #f87171, #fca5a5)"
260
+
261
+
262
+ def render_results(result: dict) -> str:
263
+ """Render pipeline result dict as styled HTML."""
264
+ score = result["placement_score"]
265
+ level = result["match_level"]
266
+ domain = result.get("domain_detected", "unknown").replace("_", " ").title()
267
+ role = result.get("role_title", "Not Specified")
268
+ color = _score_color(score)
269
+
270
+ # ── Score Card ──
271
+ html = f"""
272
+ <div class="score-card">
273
+ <div class="score-label">Placement Score</div>
274
+ <div class="score-value" style="color: {color}">{score}%</div>
275
+ <div class="match-badge {_badge_class(level)}">{level}</div>
276
+ </div>
277
+ """
278
+
279
+ # ── Info Row ──
280
+ html += f"""
281
+ <div class="info-row">
282
+ <div class="info-card">
283
+ <div class="label">Domain</div>
284
+ <div class="value">{domain}</div>
285
+ </div>
286
+ <div class="info-card">
287
+ <div class="label">Role</div>
288
+ <div class="value" style="font-size: 1.1rem">{role}</div>
289
+ </div>
290
+ </div>
291
+ """
292
+
293
+ # ── Component Breakdown ──
294
+ breakdown = result.get("component_breakdown", [])
295
+ excluded_names = {e["component"] for e in result.get("excluded_components", [])}
296
+ html += '<div class="section-card"><h3>📊 Component Breakdown</h3>'
297
+ html += '<table class="breakdown-table"><tr><th>Component</th><th>Weight</th><th>Score</th><th>Progress</th></tr>'
298
+ for item in breakdown:
299
+ name = item["component"]
300
+ total = item["total_score"]
301
+ achieved = item["score_achieved"]
302
+ pct = (achieved / total * 100) if total > 0 else 0
303
+ excluded = name in excluded_names
304
+ label = f'{name} <span style="color:#64748b;font-size:0.75rem">⊘ excluded</span>' if excluded else name
305
+ weight_pct = f"{total * 100:.1f}%"
306
+ score_pct = f"{achieved * 100:.1f}%"
307
+ bar = f"""<div class="progress-bar-bg"><div class="progress-bar-fill" style="width:{min(pct,100):.0f}%;background:{_bar_color(pct)}"></div></div>"""
308
+ opacity = "opacity: 0.4;" if excluded else ""
309
+ html += f'<tr style="{opacity}"><td>{label}</td><td>{weight_pct}</td><td>{score_pct}</td><td style="min-width:120px">{bar}</td></tr>'
310
+ html += '</table></div>'
311
+
312
+ # ── Skills Analysis ──
313
+ sa = result.get("skills_analysis", {})
314
+ matched_ct = sa.get("matched_count", 0)
315
+ total_ct = sa.get("total_jd_count", 0)
316
+ missing = sa.get("missing_skills", [])
317
+ html += '<div class="section-card"><h3>🎯 Skills Analysis</h3>'
318
+ html += f'<p style="color:#94a3b8;margin:0 0 0.8rem"><strong style="color:#e2e8f0">{matched_ct}/{total_ct}</strong> JD skills matched</p>'
319
+ if missing:
320
+ html += '<p style="color:#94a3b8;font-size:0.8rem;margin:0 0 0.5rem">Missing skills:</p><div>'
321
+ for skill in missing:
322
+ html += f'<span class="skill-tag skill-missing">{skill}</span>'
323
+ html += '</div>'
324
+ else:
325
+ html += '<p style="color:#34d399">✓ All required skills matched!</p>'
326
+ html += '</div>'
327
+
328
+ # ── Improvement Suggestions ──
329
+ suggestions = result.get("improvement_suggestions", [])
330
+ if suggestions:
331
+ html += '<div class="section-card"><h3>📈 Improvement Suggestions</h3>'
332
+ for s in suggestions[:8]:
333
+ html += f"""<div class="imp-row">
334
+ <span>#{s['rank']} {s['improvement']}</span>
335
+ <span><span style="color:#94a3b8;font-size:0.8rem">{s['score_change']}</span> &nbsp;<span class="imp-delta">{s['delta']}</span></span>
336
+ </div>"""
337
+
338
+ comb = result.get("combined_improvement", {})
339
+ if comb and comb.get("combined_delta", 0) > 0:
340
+ html += f"""<div style="margin-top:0.8rem;padding:0.8rem;background:rgba(52,211,153,0.08);border:1px solid rgba(52,211,153,0.2);border-radius:8px;text-align:center">
341
+ <span style="color:#94a3b8;font-size:0.85rem">Combined potential: </span>
342
+ <strong style="color:#34d399;font-size:1.1rem">{comb['current_score']}% → {comb['combined_new_score']}%</strong>
343
+ <span style="color:#34d399;font-weight:700"> (+{comb['combined_delta']}%)</span>
344
+ </div>"""
345
+ html += '</div>'
346
+
347
+ # ── Recommendations ──
348
+ recs = result.get("recommendations", [])
349
+ if recs:
350
+ html += '<div class="section-card"><h3>💡 Recommendations</h3>'
351
+ for rec in recs:
352
+ html += f'<div class="rec-item"><span class="rec-bullet">→</span><span>{rec}</span></div>'
353
+ html += '</div>'
354
+
355
+ return html
356
+
357
+
358
+ def render_error(error_msg: str, tb: str = "") -> str:
359
+ """Render an error message as styled HTML."""
360
+ html = f'<div class="error-card"><h3>⚠ Error</h3><p>{error_msg}</p>'
361
+ if tb:
362
+ html += f'<details><summary style="cursor:pointer;color:#ffffff;margin-top:0.5rem">Technical Details</summary><pre>{tb}</pre></details>'
363
+ html += '</div>'
364
+ return html
365
+
366
+
367
+ # ── Gradio Handler ───────────────────────────────────────────────────────
368
+
369
+ def analyze(resume_file, jd_text):
370
+ try:
371
+ if resume_file is None:
372
+ return render_error("Please upload a resume file (PDF or DOCX).")
373
+ if not jd_text or not jd_text.strip():
374
+ return render_error("Please paste a job description.")
375
+
376
+ file_path = resume_file.name if hasattr(resume_file, "name") else str(resume_file)
377
+ result = predict(file_path, jd_text)
378
+ return render_results(result)
379
+
380
+ except Exception as e:
381
+ tb = traceback.format_exc()
382
+ return render_error(str(e), tb)
383
+
384
+
385
+ # ── Build Gradio App ─────────────────────────────────────────────────────
386
+ # Gradio 6: theme/css moved from gr.Blocks() constructor to app.launch()
387
+
388
+ with gr.Blocks(
389
+ title="CPPS — Campus Placement Prediction System",
390
+ ) as app:
391
+
392
+ # Header
393
+ gr.HTML("""
394
+ <div class="app-header">
395
+ <h1>🎓 CPPS</h1>
396
+ <p>Campus Placement Prediction System — AI-Powered Resume Analyzer</p>
397
+ </div>
398
+ """, padding=False)
399
+
400
+ with gr.Row(equal_height=False):
401
+ # Left Column: Inputs
402
+ with gr.Column(scale=2):
403
+ resume_input = gr.File(
404
+ label="📄 Upload Resume",
405
+ file_types=[".pdf", ".docx"],
406
+ type="filepath",
407
+ elem_id="resume-upload",
408
+ )
409
+ jd_input = gr.Textbox(
410
+ label="📋 Job Description",
411
+ placeholder="Paste the full job description here...\n\nExample:\nSoftware Developer\n\nRequired Skills:\n- Python\n- SQL\n- Flask\n- REST APIs\n\n2+ years of experience\nB.Tech in Computer Science",
412
+ lines=12,
413
+ max_lines=25,
414
+ elem_id="jd-input",
415
+ )
416
+ analyze_btn = gr.Button(
417
+ "🔍 Analyze Match",
418
+ variant="primary",
419
+ size="lg",
420
+ elem_id="analyze-btn",
421
+ )
422
+
423
+ # Right Column: Results
424
+ with gr.Column(scale=3):
425
+ output_html = gr.HTML(
426
+ value="""
427
+ <div style="text-align:center;padding:4rem 2rem;color:#64748b">
428
+ <p style="font-size:3rem;margin:0">🎓</p>
429
+ <p style="font-size:1.1rem;margin:0.5rem 0 0.3rem;color:#94a3b8">Ready to Analyze</p>
430
+ <p style="font-size:0.85rem">Upload a resume and paste a job description to get started</p>
431
+ </div>
432
+ """,
433
+ padding=False,
434
+ elem_id="results-output",
435
+ )
436
+
437
+ # Wire up
438
+ analyze_btn.click(
439
+ fn=analyze,
440
+ inputs=[resume_input, jd_input],
441
+ outputs=[output_html],
442
+ )
443
+
444
+ # Also trigger on Enter in JD box (submit event)
445
+ jd_input.submit(
446
+ fn=analyze,
447
+ inputs=[resume_input, jd_input],
448
+ outputs=[output_html],
449
+ )
450
+
451
+
452
+ # ── Launch ───────────────────────────────────────────────────────────────
453
+ # Gradio 6: theme and css are passed to launch(), not gr.Blocks()
454
+
455
+ if __name__ == "__main__":
456
+ app.launch(
457
+ server_name="0.0.0.0",
458
+ server_port=7860,
459
+ theme=THEME,
460
+ css=CUSTOM_CSS,
461
+ )
requirements.txt ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ gradio>=6.13.0
2
+
3
+ transformers>=4.41.0
4
+ torch>=2.0.0
5
+
6
+ sentence-transformers>=2.7.0
7
+ keybert>=0.8.0
8
+
9
+ spacy>=3.7.0
10
+ en-core-web-sm @ https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-3.7.1/en_core_web_sm-3.7.1-py3-none-any.whl
11
+
12
+ pymupdf
13
+ python-docx
14
+ numpy
15
+ scikit-learn