Pradyumn Tendulkar commited on
Commit
f641225
·
1 Parent(s): 60a3c49

separated app.py into local_model.py and app.py.

Browse files
Files changed (2) hide show
  1. app.py +154 -2
  2. local_model.py +270 -0
app.py CHANGED
@@ -1,4 +1,4 @@
1
- import io
2
  import os
3
  import re
4
  import tempfile
@@ -390,4 +390,156 @@ def build_ui():
390
  if __name__ == "__main__":
391
  demo = build_ui()
392
  demo.launch()
393
- #demo.launch(server_name="0.0.0.0")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ '''import io
2
  import os
3
  import re
4
  import tempfile
 
390
  if __name__ == "__main__":
391
  demo = build_ui()
392
  demo.launch()
393
+ #demo.launch(server_name="0.0.0.0")'''
394
+
395
+ # app.py
396
+ import gradio as gr
397
+
398
+ from local_model import (
399
+ extract_text_from_fileobj,
400
+ preprocess_text,
401
+ calculate_similarity,
402
+ analyze_resume_keywords,
403
+ format_missing_keywords,
404
+ suggest_jobs,
405
+ extract_projects_section,
406
+ analyze_projects_fit,
407
+ extract_top_keywords,
408
+ )
409
+
410
+ # --------------------------
411
+ # Main Gradio app logic
412
+ # --------------------------
413
+ def analyze_resumes(files, job_description: str, mode: str):
414
+ if not files or not job_description.strip():
415
+ return 0.0, "Please upload resumes and paste a job description.", "", "", "", "", "", "", "", ""
416
+
417
+ results = []
418
+ for file in files:
419
+ try:
420
+ resume_text, fname = extract_text_from_fileobj(file)
421
+ if resume_text.strip().startswith("[Error"):
422
+ continue # Skip errored files
423
+ cleaned_resume = preprocess_text(resume_text)
424
+ cleaned_job = preprocess_text(job_description)
425
+ sim_pct = calculate_similarity(cleaned_resume, cleaned_job, mode=mode)
426
+ results.append((sim_pct, resume_text, fname))
427
+ except Exception:
428
+ continue # Skip if any error
429
+
430
+ if not results:
431
+ return 0.0, "No valid resumes were provided.", "", "", "", "", "", "", "", ""
432
+
433
+ # Select the best matching resume
434
+ best = max(results, key=lambda x: x[0]) # highest similarity
435
+ sim_pct, resume_text, fname = best
436
+
437
+ missing_dict, suggestions_text = analyze_resume_keywords(resume_text, job_description)
438
+ missing_formatted = format_missing_keywords(missing_dict)
439
+ job_suggestions = suggest_jobs(resume_text)
440
+ projects_section = extract_projects_section(resume_text)
441
+ project_fit_verdict = analyze_projects_fit(projects_section, job_description, mode)
442
+ resume_keywords_text = extract_top_keywords(preprocess_text(resume_text))
443
+ jd_keywords_text = extract_top_keywords(preprocess_text(job_description))
444
+
445
+ verdict = (
446
+ f"<h3 style='color:green;'>✅ Best Match: {fname} ({sim_pct:.2f}%)</h3>" if sim_pct >= 80 else
447
+ f"<h3 style='color:limegreen;'>👍 Best Match: {fname} ({sim_pct:.2f}%)</h3>" if sim_pct >= 60 else
448
+ f"<h3 style='color:orange;'>⚠️ Best Match: {fname} ({sim_pct:.2f}%)</h3>" if sim_pct >= 40 else
449
+ f"<h3 style='color:red;'>❌ Low Match: {fname} ({sim_pct:.2f}%)</h3>"
450
+ )
451
+
452
+ return (
453
+ float(sim_pct), verdict, missing_formatted, suggestions_text,
454
+ job_suggestions, projects_section, project_fit_verdict, resume_keywords_text, jd_keywords_text, fname
455
+ )
456
+
457
+ # --------------------------
458
+ # Clear Button Logic
459
+ # --------------------------
460
+ def clear_inputs():
461
+ return None, "", "sbert", 0.0, "", "", "", "", "", "", ""
462
+
463
+ # --------------------------
464
+ # Build Gradio UI
465
+ # --------------------------
466
+ def build_ui():
467
+ with gr.Blocks(theme=gr.themes.Default(), title="Resume ↔ Job Matcher") as demo:
468
+ gr.Markdown("# 📄 Resume & Job Description Analyzer 🎯")
469
+ gr.Markdown(
470
+ "Upload a resume, paste a job description, and get an instant analysis, keyword suggestions, and potential job matches."
471
+ )
472
+
473
+ with gr.Row():
474
+ with gr.Column(scale=2):
475
+ file_in = gr.File(
476
+ label="Upload resumes (PDF or DOCX)",
477
+ file_count="multiple",
478
+ file_types=[".pdf", ".docx"]
479
+ )
480
+ job_desc = gr.Textbox(
481
+ lines=10,
482
+ label="Job Description",
483
+ placeholder="Paste the full job description here..."
484
+ )
485
+ mode = gr.Radio(
486
+ choices=["sbert", "bert"],
487
+ value="sbert",
488
+ label="Analysis Mode",
489
+ info="SBERT is faster, BERT is more detailed."
490
+ )
491
+ with gr.Row():
492
+ clear_btn = gr.Button("Clear")
493
+ run_btn = gr.Button("Analyze Resume", variant="primary")
494
+
495
+ with gr.Column(scale=3):
496
+ with gr.Tabs():
497
+ with gr.TabItem("📊 Analysis & Suggestions"):
498
+ score_slider = gr.Slider(
499
+ value=0, minimum=0, maximum=100, step=0.01, interactive=False,
500
+ label="Similarity Score"
501
+ )
502
+ score_text = gr.Markdown()
503
+ suggestions_out = gr.Textbox(
504
+ label="Suggestions to Improve Your Resume", interactive=False, lines=5
505
+ )
506
+ missing_out = gr.Markdown(label="Keywords Check")
507
+
508
+ with gr.TabItem("🛠️ Project Analysis"):
509
+ project_fit_out = gr.Markdown(label="Project Fit Verdict")
510
+ projects_out = gr.Textbox(label="Extracted Projects Section", interactive=False, lines=12)
511
+
512
+ with gr.TabItem("🚀 Job Suggestions"):
513
+ job_suggestions_out = gr.Markdown(label="Potential Job Roles")
514
+
515
+ with gr.TabItem("🔑 Top Keywords"):
516
+ resume_keywords_out = gr.Textbox(label="Top Resume Keywords")
517
+ jd_keywords_out = gr.Textbox(label="Top Job Description Keywords")
518
+
519
+ best_fname_out = gr.Textbox(label="Best Match Filename", interactive=False)
520
+
521
+ run_btn.click(
522
+ analyze_resumes,
523
+ inputs=[file_in, job_desc, mode],
524
+ outputs=[
525
+ score_slider, score_text, missing_out, suggestions_out, job_suggestions_out, projects_out,
526
+ project_fit_out, resume_keywords_out, jd_keywords_out, best_fname_out
527
+ ],
528
+ show_progress='full'
529
+ )
530
+
531
+ clear_btn.click(
532
+ clear_inputs,
533
+ inputs=[],
534
+ outputs=[
535
+ file_in, job_desc, mode, score_slider, score_text, missing_out, suggestions_out,
536
+ job_suggestions_out, projects_out, project_fit_out, resume_keywords_out, jd_keywords_out, best_fname_out
537
+ ]
538
+ )
539
+
540
+ return demo
541
+
542
+ if __name__ == "__main__":
543
+ demo = build_ui()
544
+ demo.launch()
545
+ # demo.launch(server_name="0.0.0.0")
local_model.py ADDED
@@ -0,0 +1,270 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # local_model.py
2
+ import io
3
+ import os
4
+ import re
5
+ import traceback
6
+ from typing import Tuple, Dict
7
+
8
+ import fitz # PyMuPDF
9
+ import docx # python-docx
10
+
11
+ import numpy as np
12
+ from sklearn.metrics.pairwise import cosine_similarity
13
+ from sklearn.feature_extraction.text import TfidfVectorizer
14
+
15
+ # --------------------------
16
+ # Pre-load all heavy libraries and models at startup.
17
+ # --------------------------
18
+ print("Starting up: Loading transformer models...")
19
+ from sentence_transformers import SentenceTransformer
20
+ from transformers import BertTokenizer, BertModel
21
+ import torch
22
+
23
+ # Load models into memory once when the module is imported
24
+ _sentence_transformer = SentenceTransformer("all-MiniLM-L6-v2")
25
+ _bert_tokenizer = BertTokenizer.from_pretrained("bert-base-uncased")
26
+ _bert_model = BertModel.from_pretrained("bert-base-uncased")
27
+ _bert_model.eval()
28
+ print("Transformer models loaded successfully.")
29
+
30
+ # --------------------------
31
+ # Built-in stopwords
32
+ # --------------------------
33
+ EN_STOPWORDS = {
34
+ "a", "about", "above", "after", "again", "against", "all", "am", "an", "and", "any", "are", "as",
35
+ "at", "be", "because", "been", "before", "being", "below", "between", "both", "but", "by",
36
+ "could", "did", "do", "does", "doing", "down", "during", "each", "few", "for", "from", "further",
37
+ "had", "has", "have", "having", "he", "her", "here", "hers", "herself", "him", "himself", "his",
38
+ "how", "i", "if", "in", "into", "is", "it", "its", "itself", "just", "me", "more", "most", "my",
39
+ "myself", "no", "nor", "not", "now", "of", "off", "on", "once", "only", "or", "other", "ought", "our",
40
+ "ours", "ourselves", "out", "over", "own", "same", "she", "should", "so", "some", "such", "than",
41
+ "that", "the", "their", "theirs", "them", "themselves", "then", "there", "these", "they", "this",
42
+ "those", "through", "to", "too", "under", "until", "up", "very", "was", "we", "were", "what", "when",
43
+ "where", "which", "while", "who", "whom", "why", "with", "would", "you", "your", "yours", "yourself",
44
+ "yourselves", "resume", "job", "description", "work", "experience", "skill", "skills", "applicant", "application"
45
+ }
46
+
47
+ # --------------------------
48
+ # Job Suggestions Database
49
+ # --------------------------
50
+ JOB_SUGGESTIONS_DB = {
51
+ "Data Scientist": {"python", "sql", "machine", "learning", "tensorflow", "pytorch", "analysis"},
52
+ "Data Analyst": {"sql", "python", "excel", "tableau", "analysis", "statistics"},
53
+ "Backend Developer": {"python", "java", "sql", "docker", "aws", "api", "git"},
54
+ "Frontend Developer": {"react", "javascript", "html", "css", "git", "ui", "ux"},
55
+ "Full-Stack Developer": {"python", "javascript", "react", "sql", "docker", "git"},
56
+ "Machine Learning Engineer": {"python", "tensorflow", "pytorch", "machine", "learning", "docker", "cloud"},
57
+ "Project Manager": {"agile", "scrum", "project", "management", "jira"}
58
+ }
59
+
60
+ # --------------------------
61
+ # Utilities: text extraction
62
+ # --------------------------
63
+ def extract_text_from_pdf_bytes(pdf_bytes: bytes) -> str:
64
+ try:
65
+ doc = fitz.open(stream=pdf_bytes, filetype="pdf")
66
+ pages = [p.get_text("text") for p in doc]
67
+ doc.close()
68
+ return "\n".join(p for p in pages if p)
69
+ except Exception as e:
70
+ return f"[Error reading PDF: {e}]"
71
+
72
+
73
+ def extract_text_from_docx_bytes(docx_bytes: bytes) -> str:
74
+ try:
75
+ docx_io = io.BytesIO(docx_bytes)
76
+ doc = docx.Document(docx_io)
77
+ paragraphs = [p.text for p in doc.paragraphs if p.text]
78
+ return "\n".join(paragraphs)
79
+ except Exception as e:
80
+ return f"[Error reading DOCX: {e}]"
81
+
82
+
83
+ def extract_text_from_fileobj(file_obj) -> Tuple[str, str]:
84
+ fname = "uploaded_file"
85
+ try:
86
+ fname = os.path.basename(file_obj.name)
87
+ with open(file_obj.name, "rb") as f:
88
+ raw_bytes = f.read()
89
+ ext = fname.split('.')[-1].lower()
90
+ if ext == "pdf":
91
+ return (extract_text_from_pdf_bytes(raw_bytes), fname)
92
+ elif ext == "docx":
93
+ return (extract_text_from_docx_bytes(raw_bytes), fname)
94
+ else:
95
+ return (raw_bytes.decode("utf-8", errors="ignore"), fname)
96
+ except Exception as exc:
97
+ return (f"[Error reading uploaded file: {exc}\n{traceback.format_exc()}]", fname)
98
+
99
+ # --------------------------
100
+ # Text preprocessing
101
+ # --------------------------
102
+ def preprocess_text(text: str, remove_stopwords: bool = True) -> str:
103
+ if not text:
104
+ return ""
105
+ t = text.lower()
106
+ t = re.sub(r"\s+", " ", t)
107
+ t = re.sub(r"[^a-z0-9\s]", " ", t)
108
+ words = t.split()
109
+ if remove_stopwords:
110
+ words = [w for w in words if w not in EN_STOPWORDS]
111
+ return " ".join(words)
112
+
113
+ # --------------------------
114
+ # Embedding helpers
115
+ # --------------------------
116
+ def get_sentence_embedding(text: str, mode: str = "sbert") -> np.ndarray:
117
+ if mode == "sbert":
118
+ return _sentence_transformer.encode([text], show_progress_bar=False)
119
+ elif mode == "bert":
120
+ tokens = _bert_tokenizer(text, return_tensors="pt", truncation=True, padding=True, max_length=512)
121
+ with torch.no_grad():
122
+ out = _bert_model(**tokens)
123
+ cls = out.last_hidden_state[:, 0, :].numpy()
124
+ return cls
125
+ else:
126
+ raise ValueError("Unsupported mode")
127
+
128
+ def calculate_similarity(resume_text: str, job_text: str, mode: str = "sbert") -> float:
129
+ r_emb = get_sentence_embedding(resume_text, mode=mode)
130
+ j_emb = get_sentence_embedding(job_text, mode=mode)
131
+ sim = cosine_similarity(r_emb, j_emb)[0][0]
132
+ return float(np.round(sim * 100, 2))
133
+
134
+ # --------------------------
135
+ # Keyword analysis
136
+ # --------------------------
137
+ DEFAULT_KEYWORDS = {
138
+ "skills": {"python", "nlp", "java", "sql", "tensorflow", "pytorch", "docker", "git", "react", "cloud", "aws",
139
+ "azure"},
140
+ "concepts": {"machine", "learning", "data", "analysis", "nlp", "vision", "agile", "scrum"},
141
+ "roles": {"software", "engineer", "developer", "manager", "scientist", "analyst", "architect"},
142
+ }
143
+
144
+ def analyze_resume_keywords(resume_text: str, job_description: str):
145
+ clean_resume = preprocess_text(resume_text)
146
+ clean_job = preprocess_text(job_description)
147
+ resume_words = set(clean_resume.split())
148
+ job_words = set(clean_job.split())
149
+ missing = {}
150
+ for cat, kws in DEFAULT_KEYWORDS.items():
151
+ missing_from_cat = [kw for kw in kws if kw in job_words and kw not in resume_words]
152
+ if missing_from_cat:
153
+ missing[cat] = sorted(missing_from_cat)
154
+ low_resume = (resume_text or "").lower()
155
+ sections_present = {
156
+ "skills": "skills" in low_resume,
157
+ "experience": "experience" in low_resume or "employment" in low_resume,
158
+ "summary": "summary" in low_resume or "objective" in low_resume,
159
+ }
160
+ suggestions = []
161
+ if any(missing.values()):
162
+ for cat, kws in missing.items():
163
+ for kw in kws:
164
+ if cat == "skills":
165
+ suggestions.append(
166
+ f"Add keyword '{kw}' to your Skills section." if sections_present["skills"]
167
+ else f"Consider creating a Skills section to include '{kw}'."
168
+ )
169
+ elif cat == "concepts":
170
+ suggestions.append(
171
+ f"Try to demonstrate your knowledge of '{kw}' in your Experience or Projects section."
172
+ )
173
+ elif cat == "roles":
174
+ suggestions.append(f"Align your Summary/Objective to mention the title '{kw}'.")
175
+ else:
176
+ suggestions.append("Great job! Your resume contains many of the keywords found in the job description.")
177
+ return missing, "\n".join(f"- {s}" for s in suggestions)
178
+
179
+ # --------------------------
180
+ # Project Section Analysis
181
+ # --------------------------
182
+ def extract_projects_section(resume_text: str) -> str:
183
+ project_headings = ["projects", "personal projects", "academic projects", "portfolio"]
184
+ end_headings = [
185
+ "skills", "technical skills", "experience", "work experience",
186
+ "education", "awards", "certifications", "languages", "references"
187
+ ]
188
+ lines = resume_text.split('\n')
189
+ start_index = -1
190
+ end_index = len(lines)
191
+
192
+ # Find start
193
+ for i, line in enumerate(lines):
194
+ cleaned_line = line.strip().lower()
195
+ if cleaned_line in project_headings:
196
+ start_index = i
197
+ break
198
+ if start_index == -1:
199
+ return "Could not automatically identify a 'Projects' section in this resume."
200
+
201
+ # Find end (FIX: use lines[i], not stale 'line')
202
+ for i in range(start_index + 1, len(lines)):
203
+ cleaned_line = lines[i].strip().lower()
204
+ if len(cleaned_line.split()) < 4 and cleaned_line in end_headings:
205
+ end_index = i
206
+ break
207
+
208
+ project_section_lines = lines[start_index:end_index]
209
+ return "\n".join(project_section_lines)
210
+
211
+ def analyze_projects_fit(projects_text: str, job_description_text: str, mode: str) -> str:
212
+ if projects_text.startswith("Could not"):
213
+ return "Cannot analyze project fit as no projects section was found."
214
+
215
+ cleaned_projects = preprocess_text(projects_text)
216
+ cleaned_job = preprocess_text(job_description_text)
217
+
218
+ if not cleaned_projects:
219
+ return "Projects section is empty or contains no relevant text to analyze."
220
+
221
+ project_fit_score = calculate_similarity(cleaned_projects, cleaned_job, mode=mode)
222
+
223
+ if project_fit_score >= 75:
224
+ verdict = f"<p style='color:green;'>✅ <b>Highly Relevant ({project_fit_score:.2f}%)</b>: The projects listed are an excellent match for this job's requirements.</p>"
225
+ elif project_fit_score >= 50:
226
+ verdict = f"<p style='color:limegreen;'>👍 <b>Relevant ({project_fit_score:.2f}%)</b>: The projects show relevant skills and experience for this role.</p>"
227
+ else:
228
+ verdict = f"<p style='color:orange;'>⚠️ <b>Moderately Relevant ({project_fit_score:.2f}%)</b>: The projects may not directly align with the key requirements. Consider highlighting different aspects of your work.</p>"
229
+
230
+ return verdict
231
+
232
+ # --------------------------
233
+ # Formatting and Suggestion Functions
234
+ # --------------------------
235
+ def format_missing_keywords(missing: Dict) -> str:
236
+ if not any(missing.values()):
237
+ return "✅ No critical keywords seem to be missing. Great job!"
238
+ output = "### 🔑 Keywords Missing From Your Resume\n"
239
+ for category, keywords in missing.items():
240
+ if keywords:
241
+ output += f"**Missing {category.capitalize()}:** {', '.join(keywords)}\n"
242
+ return output
243
+
244
+ def suggest_jobs(resume_text: str) -> str:
245
+ resume_words = set(preprocess_text(resume_text).split())
246
+ suggestions = []
247
+ for job_title, required_skills in JOB_SUGGESTIONS_DB.items():
248
+ matched_skills = resume_words.intersection(required_skills)
249
+ if len(matched_skills) >= 3:
250
+ suggestions.append(job_title)
251
+ if not suggestions:
252
+ return "Could not determine strong job matches from the resume. Try adding more specific skills and technologies."
253
+ output = "### 🚀 Job Titles You May Be a Good Fit For\n"
254
+ for job in suggestions:
255
+ output += f"- {job}\n"
256
+ return output
257
+
258
+ def extract_top_keywords(text: str, top_n: int = 15) -> str:
259
+ if not text.strip():
260
+ return "Not enough text provided."
261
+ try:
262
+ vectorizer = TfidfVectorizer(stop_words=list(EN_STOPWORDS))
263
+ tfidf_matrix = vectorizer.fit_transform([text])
264
+ feature_names = np.array(vectorizer.get_feature_names_out())
265
+ scores = tfidf_matrix.toarray().flatten()
266
+ top_indices = scores.argsort()[-top_n:][::-1]
267
+ top_keywords = feature_names[top_indices]
268
+ return ", ".join(top_keywords)
269
+ except ValueError:
270
+ return "Could not extract keywords (text may be too short)."