koyelog commited on
Commit
5ed40c4
·
verified ·
1 Parent(s): 47e4768

Upload 3 files

Browse files
Files changed (3) hide show
  1. analyzer.py +195 -0
  2. main.py +76 -0
  3. requirements.txt +10 -0
analyzer.py ADDED
@@ -0,0 +1,195 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import spacy
2
+ import re
3
+
4
+ try:
5
+ nlp = spacy.load("en_core_web_sm")
6
+ except OSError:
7
+ import subprocess
8
+ subprocess.run(["python", "-m", "spacy", "download", "en_core_web_sm"])
9
+ nlp = spacy.load("en_core_web_sm")
10
+
11
+ _ner_pipeline = None
12
+
13
+ def get_ner_pipeline():
14
+ global _ner_pipeline
15
+ if _ner_pipeline is None:
16
+ try:
17
+ from transformers import pipeline
18
+ _ner_pipeline = pipeline("ner", model="dslim/bert-base-NER", grouped_entities=True)
19
+ except Exception:
20
+ _ner_pipeline = False
21
+ return _ner_pipeline if _ner_pipeline else None
22
+
23
+
24
+ SKILLS_DB = [
25
+ "Python", "JavaScript", "TypeScript", "React", "Next.js", "Vue.js", "Angular",
26
+ "Node.js", "Express.js", "FastAPI", "Flask", "Django", "Spring Boot",
27
+ "Machine Learning", "Deep Learning", "TensorFlow", "PyTorch", "Scikit-learn",
28
+ "Keras", "XGBoost", "LightGBM", "CatBoost",
29
+ "SQL", "MySQL", "PostgreSQL", "MongoDB", "Redis", "Cassandra", "SQLite",
30
+ "Docker", "Kubernetes", "AWS", "GCP", "Azure", "Terraform", "CI/CD", "Jenkins",
31
+ "Git", "GitHub", "GitLab", "Bitbucket",
32
+ "Linux", "REST API", "GraphQL", "gRPC", "WebSockets",
33
+ "HTML", "CSS", "Tailwind CSS", "Bootstrap", "SASS",
34
+ "Java", "C++", "C", "Go", "Rust", "R", "Scala", "Kotlin",
35
+ "Pandas", "NumPy", "Matplotlib", "Seaborn", "Plotly",
36
+ "NLP", "Computer Vision", "LLM", "Hugging Face", "OpenCV", "NLTK", "spaCy",
37
+ "Tableau", "Power BI", "Excel", "Spark", "Hadoop", "Kafka",
38
+ "Selenium", "Pytest", "Jest", "JUnit", "Postman",
39
+ "Figma", "Jira", "Agile", "Scrum"
40
+ ]
41
+
42
+ ROLE_RULES = [
43
+ (["Machine Learning", "Deep Learning", "TensorFlow", "PyTorch", "Scikit-learn"], "ML/AI Engineer"),
44
+ (["NLP", "NLTK", "spaCy", "Hugging Face", "LLM"], "NLP Engineer"),
45
+ (["Computer Vision", "OpenCV"], "Computer Vision Engineer"),
46
+ (["React", "Next.js", "Vue.js", "Angular", "HTML", "CSS", "Tailwind CSS"], "Frontend Developer"),
47
+ (["Node.js", "FastAPI", "Flask", "Django", "Express.js", "Spring Boot"], "Backend Developer"),
48
+ (["Docker", "Kubernetes", "CI/CD", "Terraform", "Jenkins", "AWS", "GCP", "Azure"], "DevOps/Cloud Engineer"),
49
+ (["SQL", "PostgreSQL", "MySQL", "MongoDB", "Spark", "Hadoop", "Kafka", "Tableau", "Power BI"], "Data Engineer/Analyst"),
50
+ (["React", "Node.js", "MongoDB", "FastAPI", "Flask", "PostgreSQL"], "Full Stack Developer"),
51
+ ]
52
+
53
+ SKILL_CATEGORIES = {
54
+ "Languages": ["Python", "JavaScript", "TypeScript", "Java", "C++", "C", "Go", "Rust", "R", "Scala", "Kotlin"],
55
+ "Frontend": ["React", "Next.js", "Vue.js", "Angular", "HTML", "CSS", "Tailwind CSS", "Bootstrap", "SASS"],
56
+ "Backend": ["Node.js", "Express.js", "FastAPI", "Flask", "Django", "Spring Boot", "REST API", "GraphQL", "gRPC"],
57
+ "ML/AI": ["Machine Learning", "Deep Learning", "TensorFlow", "PyTorch", "Scikit-learn", "Keras", "XGBoost", "NLP", "Computer Vision", "Hugging Face", "LLM", "OpenCV", "NLTK", "spaCy"],
58
+ "Databases": ["SQL", "MySQL", "PostgreSQL", "MongoDB", "Redis", "Cassandra", "SQLite"],
59
+ "DevOps/Cloud": ["Docker", "Kubernetes", "AWS", "GCP", "Azure", "Terraform", "CI/CD", "Jenkins", "Linux"],
60
+ "Data Tools": ["Pandas", "NumPy", "Matplotlib", "Seaborn", "Plotly", "Tableau", "Power BI", "Spark", "Hadoop", "Kafka"],
61
+ "Tools": ["Git", "GitHub", "GitLab", "Jira", "Postman", "Figma", "Selenium", "Pytest", "Jest"],
62
+ }
63
+
64
+
65
+ def extract_skills(text):
66
+ return list({skill for skill in SKILLS_DB if re.search(r'\b' + re.escape(skill) + r'\b', text, re.IGNORECASE)})
67
+
68
+ def categorize_skills(skills):
69
+ return {cat: [s for s in skills if s in cat_skills] for cat, cat_skills in SKILL_CATEGORIES.items() if any(s in cat_skills for s in skills)}
70
+
71
+ def extract_experience_years(text):
72
+ matches = re.findall(r'(\d+)[\+]?\s*(?:years?|yrs?)\s*(?:of)?\s*(?:experience|exp)', text, re.IGNORECASE)
73
+ matches += re.findall(r'experience\s*(?:of)?\s*(\d+)[\+]?\s*(?:years?|yrs?)', text, re.IGNORECASE)
74
+ return max(int(m) for m in matches) if matches else 0
75
+
76
+ def extract_education(text):
77
+ degrees = ["B.Tech", "B.E", "B.Sc", "M.Tech", "M.Sc", "MCA", "BCA", "MBA", "Ph.D", "Bachelor", "Master", "Doctorate"]
78
+ return list({d for d in degrees if re.search(r'\b' + re.escape(d) + r'\b', text, re.IGNORECASE)})
79
+
80
+ def extract_email(text):
81
+ m = re.search(r'[\w.+-]+@[\w-]+\.[\w.]+', text)
82
+ return m.group(0) if m else None
83
+
84
+ def extract_phone(text):
85
+ m = re.search(r'(?:\+91[\s-]?)?[6-9]\d{9}|(?:\+\d{1,3}[\s-]?)?\(?\d{3}\)?[\s-]?\d{3}[\s-]?\d{4}', text)
86
+ return m.group(0) if m else None
87
+
88
+ def extract_github(text):
89
+ m = re.search(r'github\.com/([\w-]+)', text, re.IGNORECASE)
90
+ return f"github.com/{m.group(1)}" if m else None
91
+
92
+ def extract_linkedin(text):
93
+ m = re.search(r'linkedin\.com/in/([\w-]+)', text, re.IGNORECASE)
94
+ return f"linkedin.com/in/{m.group(1)}" if m else None
95
+
96
+ def extract_name(text):
97
+ ner = get_ner_pipeline()
98
+ if ner:
99
+ try:
100
+ for ent in ner(text[:512]):
101
+ if ent["entity_group"] == "PER":
102
+ return ent["word"]
103
+ except Exception:
104
+ pass
105
+ doc = nlp(text[:500])
106
+ for ent in doc.ents:
107
+ if ent.label_ == "PERSON":
108
+ return ent.text
109
+ for line in text.strip().split('\n'):
110
+ line = line.strip()
111
+ if line and 2 < len(line) < 60 and not re.search(r'[@http]|resume|cv|summary|objective', line, re.IGNORECASE):
112
+ return line
113
+ return "Unknown"
114
+
115
+ def predict_role(skills):
116
+ best_role, best_score = "Software Developer", 0
117
+ for rule_skills, role in ROLE_RULES:
118
+ score = sum(1 for s in skills if s in rule_skills)
119
+ if score > best_score:
120
+ best_score, best_role = score, role
121
+ return best_role
122
+
123
+ def compute_score(skills, experience, education):
124
+ score = min(len(skills) * 3, 40) + min(experience * 5, 30)
125
+ score += 10 if education else 0
126
+ score += 5 if len(skills) > 10 else 0
127
+ score += 5 if experience > 2 else 0
128
+ score += min(len(categorize_skills(skills)) * 2, 10)
129
+ return min(score, 100)
130
+
131
+ def get_ats_score(text, skills):
132
+ score = 0
133
+ wc = len(text.split())
134
+ if wc > 200: score += 20
135
+ if wc > 400: score += 10
136
+ score += min(len(skills) * 2, 30)
137
+ for kw in ['experience|work|employment', 'education|degree|university', 'project|portfolio|github', 'achievement|award|certification']:
138
+ if re.search(kw, text, re.IGNORECASE): score += 10
139
+ return min(score, 100)
140
+
141
+ def match_job_description(resume_skills, jd_text):
142
+ jd_skills = extract_skills(jd_text)
143
+ if not jd_skills:
144
+ return {"match_score": 0, "matched_skills": [], "missing_skills": [], "jd_skills_total": 0}
145
+ matched = [s for s in jd_skills if s in resume_skills]
146
+ missing = [s for s in jd_skills if s not in resume_skills]
147
+ return {"match_score": int(len(matched)/len(jd_skills)*100), "matched_skills": matched, "missing_skills": missing, "jd_skills_total": len(jd_skills)}
148
+
149
+ def get_section_checklist(text):
150
+ return {
151
+ "Contact Info": bool(re.search(r'email|phone|linkedin|github|@', text, re.IGNORECASE)),
152
+ "Summary/Objective": bool(re.search(r'summary|objective|profile|about', text, re.IGNORECASE)),
153
+ "Skills": bool(re.search(r'skill|technologies|tech stack|tools', text, re.IGNORECASE)),
154
+ "Experience": bool(re.search(r'experience|work|employment|internship', text, re.IGNORECASE)),
155
+ "Education": bool(re.search(r'education|degree|university|college|b\.tech|m\.tech|bca|mca', text, re.IGNORECASE)),
156
+ "Projects": bool(re.search(r'project|built|developed|created|implemented', text, re.IGNORECASE)),
157
+ "Certifications": bool(re.search(r'certification|certified|certificate|course', text, re.IGNORECASE)),
158
+ "Achievements": bool(re.search(r'achievement|award|honor|winner|rank|prize', text, re.IGNORECASE)),
159
+ }
160
+
161
+ def suggest_improvements(skills, experience, education, text):
162
+ suggestions = []
163
+ skill_set = set(s.lower() for s in skills)
164
+ if "docker" not in skill_set: suggestions.append({"type": "skill", "msg": "Add Docker for containerization knowledge"})
165
+ if "git" not in skill_set: suggestions.append({"type": "skill", "msg": "Mention Git version control experience"})
166
+ if not any(c in skill_set for c in ["aws", "gcp", "azure"]): suggestions.append({"type": "skill", "msg": "Add cloud platform experience (AWS/GCP/Azure)"})
167
+ if not any(db in skill_set for db in ["sql", "mysql", "postgresql", "mongodb"]): suggestions.append({"type": "skill", "msg": "Include database skills (SQL, MongoDB, etc.)"})
168
+ if len(skills) < 6: suggestions.append({"type": "content", "msg": "List more technical skills to improve ATS visibility"})
169
+ if experience == 0: suggestions.append({"type": "content", "msg": "Explicitly mention years of experience or internship duration"})
170
+ if not education: suggestions.append({"type": "content", "msg": "Add your educational qualifications clearly"})
171
+ if not re.search(r'github\.com', text, re.IGNORECASE): suggestions.append({"type": "link", "msg": "Add your GitHub profile link to showcase projects"})
172
+ if not re.search(r'linkedin\.com', text, re.IGNORECASE): suggestions.append({"type": "link", "msg": "Add your LinkedIn profile for professional presence"})
173
+ if not re.search(r'project', text, re.IGNORECASE): suggestions.append({"type": "content", "msg": "Include a Projects section with tech stack and impact"})
174
+ if not re.search(r'certification|certified', text, re.IGNORECASE): suggestions.append({"type": "content", "msg": "Add certifications (Coursera, Google, AWS, etc.) to stand out"})
175
+ return suggestions
176
+
177
+ def analyze_resume(text: str, jd_text: str = None) -> dict:
178
+ skills = extract_skills(text)
179
+ experience = extract_experience_years(text)
180
+ education = extract_education(text)
181
+ return {
182
+ "name": extract_name(text),
183
+ "contact": {"email": extract_email(text), "phone": extract_phone(text), "github": extract_github(text), "linkedin": extract_linkedin(text)},
184
+ "skills": skills,
185
+ "skills_by_category": categorize_skills(skills),
186
+ "experience_years": experience,
187
+ "education": education,
188
+ "predicted_role": predict_role(skills),
189
+ "resume_score": compute_score(skills, experience, education),
190
+ "ats_score": get_ats_score(text, skills),
191
+ "suggestions": suggest_improvements(skills, experience, education, text),
192
+ "section_checklist": get_section_checklist(text),
193
+ "jd_match": match_job_description(skills, jd_text) if jd_text else None,
194
+ "word_count": len(text.split()),
195
+ }
main.py ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, UploadFile, File, HTTPException, Form
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ from pydantic import BaseModel
4
+ from typing import Optional
5
+ import pdfplumber
6
+ import docx
7
+ import io
8
+ from analyzer import analyze_resume
9
+
10
+ app = FastAPI(
11
+ title="Resume Analyzer API",
12
+ description="Analyze resumes using NLP and pretrained models",
13
+ version="3.0.0"
14
+ )
15
+
16
+ app.add_middleware(
17
+ CORSMiddleware,
18
+ allow_origins=["*"],
19
+ allow_credentials=True,
20
+ allow_methods=["*"],
21
+ allow_headers=["*"],
22
+ )
23
+
24
+
25
+ class TextRequest(BaseModel):
26
+ text: str
27
+ jd_text: Optional[str] = None
28
+
29
+
30
+ def extract_text_from_pdf(file_bytes: bytes) -> str:
31
+ text = ""
32
+ with pdfplumber.open(io.BytesIO(file_bytes)) as pdf:
33
+ for page in pdf.pages:
34
+ page_text = page.extract_text()
35
+ if page_text:
36
+ text += page_text + "\n"
37
+ return text.strip()
38
+
39
+
40
+ def extract_text_from_docx(file_bytes: bytes) -> str:
41
+ doc = docx.Document(io.BytesIO(file_bytes))
42
+ return "\n".join([para.text for para in doc.paragraphs if para.text.strip()])
43
+
44
+
45
+ @app.get("/")
46
+ def root():
47
+ return {"message": "Resume Analyzer API v3.0 is running ✅", "version": "3.0.0"}
48
+
49
+
50
+ @app.post("/analyze/text")
51
+ def analyze_text(request: TextRequest):
52
+ if not request.text or len(request.text.strip()) < 50:
53
+ raise HTTPException(status_code=400, detail="Resume text too short.")
54
+ return analyze_resume(request.text, request.jd_text)
55
+
56
+
57
+ @app.post("/analyze/file")
58
+ async def analyze_file(
59
+ file: UploadFile = File(...),
60
+ jd_text: Optional[str] = Form(None)
61
+ ):
62
+ content = await file.read()
63
+
64
+ if file.filename.endswith(".pdf"):
65
+ text = extract_text_from_pdf(content)
66
+ elif file.filename.endswith(".docx"):
67
+ text = extract_text_from_docx(content)
68
+ elif file.filename.endswith(".txt"):
69
+ text = content.decode("utf-8", errors="ignore")
70
+ else:
71
+ raise HTTPException(status_code=400, detail="Unsupported file. Upload PDF, DOCX, or TXT.")
72
+
73
+ if not text or len(text.strip()) < 50:
74
+ raise HTTPException(status_code=400, detail="Could not extract text from file.")
75
+
76
+ return analyze_resume(text, jd_text)
requirements.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi==0.115.0
2
+ uvicorn==0.30.1
3
+ transformers==4.41.2
4
+ torch==2.3.1
5
+ pdfplumber==0.11.0
6
+ python-docx==1.1.2
7
+ pydantic==2.7.3
8
+ httpx==0.27.0
9
+ python-multipart==0.0.9
10
+ spacy==3.8.3