Update app.py
Browse files
app.py
CHANGED
|
@@ -9,6 +9,8 @@ import sys
|
|
| 9 |
import os
|
| 10 |
import torch
|
| 11 |
|
|
|
|
|
|
|
| 12 |
# Initialize Models once at startup
|
| 13 |
print("π Initializing SETHU AI Engine...")
|
| 14 |
try:
|
|
@@ -19,9 +21,16 @@ except:
|
|
| 19 |
nlp = spacy.load("en_core_web_sm")
|
| 20 |
|
| 21 |
# Use a high-quality Hugging Face model for embeddings
|
| 22 |
-
# 'all-MiniLM-L6-v2' is fast and efficient for local use
|
| 23 |
model = SentenceTransformer('all-MiniLM-L6-v2')
|
| 24 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
TECH_SKILLS = [
|
| 26 |
"python", "javascript", "react", "fastapi", "aws", "docker", "kubernetes", "sql",
|
| 27 |
"git", "ml", "nlp", "tensorflow", "pytorch", "java", "golang", "postgresql",
|
|
@@ -99,8 +108,6 @@ def create_score_gauges(match_score, content_score, search_score, ats_score):
|
|
| 99 |
|
| 100 |
fig = go.Figure()
|
| 101 |
fig.add_trace(make_gauge(match_score, "Match Score", "#00dfd8"))
|
| 102 |
-
# In a real dashboard we'd have 4 separate plots, but for simplicity we'll show the main one
|
| 103 |
-
# and use different layout for others or just focus on the primary one.
|
| 104 |
|
| 105 |
fig.update_layout(
|
| 106 |
paper_bgcolor='rgba(0,0,0,0)',
|
|
@@ -147,7 +154,7 @@ def estimate_salary(score, skills):
|
|
| 147 |
return f"${low}k - ${high}k"
|
| 148 |
|
| 149 |
def main_process(resume_file, jd_text, progress=gr.Progress()):
|
| 150 |
-
print("--- New Analysis Request ---")
|
| 151 |
if not resume_file or not jd_text.strip():
|
| 152 |
return [
|
| 153 |
"β οΈ Please upload resume/JD.", "", None, None,
|
|
@@ -168,30 +175,31 @@ def main_process(resume_file, jd_text, progress=gr.Progress()):
|
|
| 168 |
match_skills = sorted(list(r_skills.intersection(j_skills)))
|
| 169 |
gap_skills = sorted(list(j_skills - r_skills))
|
| 170 |
|
| 171 |
-
progress(0.
|
| 172 |
emb1 = model.encode(resume_text, convert_to_tensor=True)
|
| 173 |
emb2 = model.encode(jd_text, convert_to_tensor=True)
|
| 174 |
score = round(util.pytorch_cos_sim(emb1, emb2).item() * 100, 1)
|
| 175 |
|
| 176 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
content_score = min(100, len(resume_text.split()) / 5)
|
| 178 |
search_score = min(100, len(r_skills) * 10)
|
| 179 |
ats_score = round(score * 0.9, 1)
|
| 180 |
|
| 181 |
-
progress(0.
|
| 182 |
gauge_plot = create_score_gauges(score, content_score, search_score, ats_score)
|
| 183 |
radar_plot = create_radar_chart(len(match_skills)*10, 75, 80, score, search_score)
|
| 184 |
|
| 185 |
salary_range = estimate_salary(score, match_skills)
|
| 186 |
|
| 187 |
-
ai_analysis = f"Based on our AI alignment, your profile shows {score}% match for this role. "
|
| 188 |
-
if score > 80:
|
| 189 |
-
ai_analysis += "Excellent match! You are a top-tier candidate."
|
| 190 |
-
elif score > 50:
|
| 191 |
-
ai_analysis += "Strong foundation, but some gaps in technical stacks were detected."
|
| 192 |
-
else:
|
| 193 |
-
ai_analysis += "Needs improvement. Focus on the learning roadmap to bridge gaps."
|
| 194 |
-
|
| 195 |
present_str = ", ".join([s.upper() for s in match_skills]) if match_skills else "No direct matches."
|
| 196 |
gap_str = ", ".join([s.upper() for s in gap_skills]) if gap_skills else "No gaps detected!"
|
| 197 |
|
|
@@ -206,13 +214,13 @@ def generate_roadmap(gap_skills):
|
|
| 206 |
return """
|
| 207 |
<div style='text-align: center; padding: 40px; background: rgba(0, 223, 216, 0.1); border-radius: 15px;'>
|
| 208 |
<h2 style='color: #00dfd8;'>π Profile is 100% Ready!</h2>
|
| 209 |
-
<p>Your skills perfectly align with this role. Focus on interview storytelling
|
| 210 |
</div>
|
| 211 |
"""
|
| 212 |
|
| 213 |
cards_html = "<div style='display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 20px; margin-top: 20px;'>"
|
| 214 |
for s in gap_skills:
|
| 215 |
-
res = ROADMAP_DB.get(s.lower(), f"Master {s.upper()} via hands-on projects
|
| 216 |
cards_html += f"""
|
| 217 |
<div style='background: rgba(255, 255, 255, 0.05); border: 1px solid rgba(0, 223, 216, 0.3); border-radius: 12px; padding: 20px; border-left: 5px solid #00dfd8;'>
|
| 218 |
<div style='display: flex; justify-content: space-between; align-items: start;'>
|
|
@@ -233,6 +241,19 @@ def generate_roadmap(gap_skills):
|
|
| 233 |
cards_html += "</div>"
|
| 234 |
return f"### π Learning Pathfinder\n{cards_html}"
|
| 235 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 236 |
# Premium CSS for Glassmorphism
|
| 237 |
STYLE = """
|
| 238 |
.gradio-container {
|
|
|
|
| 9 |
import os
|
| 10 |
import torch
|
| 11 |
|
| 12 |
+
from transformers import pipeline
|
| 13 |
+
|
| 14 |
# Initialize Models once at startup
|
| 15 |
print("π Initializing SETHU AI Engine...")
|
| 16 |
try:
|
|
|
|
| 21 |
nlp = spacy.load("en_core_web_sm")
|
| 22 |
|
| 23 |
# Use a high-quality Hugging Face model for embeddings
|
|
|
|
| 24 |
model = SentenceTransformer('all-MiniLM-L6-v2')
|
| 25 |
|
| 26 |
+
# Initialize LLM for Reasoning (using a lightweight model for local speed)
|
| 27 |
+
print("π€ Loading Reasoning LLM (FLAN-T5)...")
|
| 28 |
+
try:
|
| 29 |
+
llm_reasoner = pipeline("text2text-generation", model="google/flan-t5-small", device_map="auto")
|
| 30 |
+
except Exception as e:
|
| 31 |
+
print(f"β οΈ LLM Load failed: {e}. Falling back to heuristic reasoning.")
|
| 32 |
+
llm_reasoner = None
|
| 33 |
+
|
| 34 |
TECH_SKILLS = [
|
| 35 |
"python", "javascript", "react", "fastapi", "aws", "docker", "kubernetes", "sql",
|
| 36 |
"git", "ml", "nlp", "tensorflow", "pytorch", "java", "golang", "postgresql",
|
|
|
|
| 108 |
|
| 109 |
fig = go.Figure()
|
| 110 |
fig.add_trace(make_gauge(match_score, "Match Score", "#00dfd8"))
|
|
|
|
|
|
|
| 111 |
|
| 112 |
fig.update_layout(
|
| 113 |
paper_bgcolor='rgba(0,0,0,0)',
|
|
|
|
| 154 |
return f"${low}k - ${high}k"
|
| 155 |
|
| 156 |
def main_process(resume_file, jd_text, progress=gr.Progress()):
|
| 157 |
+
print("--- New Analysis Request (LLM Enabled) ---")
|
| 158 |
if not resume_file or not jd_text.strip():
|
| 159 |
return [
|
| 160 |
"β οΈ Please upload resume/JD.", "", None, None,
|
|
|
|
| 175 |
match_skills = sorted(list(r_skills.intersection(j_skills)))
|
| 176 |
gap_skills = sorted(list(j_skills - r_skills))
|
| 177 |
|
| 178 |
+
progress(0.5, desc="Calculating Match Intelligence...")
|
| 179 |
emb1 = model.encode(resume_text, convert_to_tensor=True)
|
| 180 |
emb2 = model.encode(jd_text, convert_to_tensor=True)
|
| 181 |
score = round(util.pytorch_cos_sim(emb1, emb2).item() * 100, 1)
|
| 182 |
|
| 183 |
+
progress(0.7, desc="Generating AI Analysis with LLM...")
|
| 184 |
+
if llm_reasoner:
|
| 185 |
+
prompt = f"Analyze resume match for Job Description. Match Score: {score}%. Gaps: {', '.join(gap_skills)}. Summarize fit in 2 sentences."
|
| 186 |
+
ai_response = llm_reasoner(prompt, max_length=100)[0]['generated_text']
|
| 187 |
+
ai_analysis = f"**AI Verdict**: {ai_response}"
|
| 188 |
+
else:
|
| 189 |
+
ai_analysis = f"Based on our AI alignment, your profile shows {score}% match. "
|
| 190 |
+
ai_analysis += "Strong foundation!" if score > 50 else "Focus on the gaps."
|
| 191 |
+
|
| 192 |
+
# Heuristic metrics
|
| 193 |
content_score = min(100, len(resume_text.split()) / 5)
|
| 194 |
search_score = min(100, len(r_skills) * 10)
|
| 195 |
ats_score = round(score * 0.9, 1)
|
| 196 |
|
| 197 |
+
progress(0.9, desc="Finalizing Dashboard...")
|
| 198 |
gauge_plot = create_score_gauges(score, content_score, search_score, ats_score)
|
| 199 |
radar_plot = create_radar_chart(len(match_skills)*10, 75, 80, score, search_score)
|
| 200 |
|
| 201 |
salary_range = estimate_salary(score, match_skills)
|
| 202 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 203 |
present_str = ", ".join([s.upper() for s in match_skills]) if match_skills else "No direct matches."
|
| 204 |
gap_str = ", ".join([s.upper() for s in gap_skills]) if gap_skills else "No gaps detected!"
|
| 205 |
|
|
|
|
| 214 |
return """
|
| 215 |
<div style='text-align: center; padding: 40px; background: rgba(0, 223, 216, 0.1); border-radius: 15px;'>
|
| 216 |
<h2 style='color: #00dfd8;'>π Profile is 100% Ready!</h2>
|
| 217 |
+
<p>Your skills perfectly align with this role. Focus on interview storytelling.</p>
|
| 218 |
</div>
|
| 219 |
"""
|
| 220 |
|
| 221 |
cards_html = "<div style='display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 20px; margin-top: 20px;'>"
|
| 222 |
for s in gap_skills:
|
| 223 |
+
res = ROADMAP_DB.get(s.lower(), f"Master {s.upper()} via hands-on projects.")
|
| 224 |
cards_html += f"""
|
| 225 |
<div style='background: rgba(255, 255, 255, 0.05); border: 1px solid rgba(0, 223, 216, 0.3); border-radius: 12px; padding: 20px; border-left: 5px solid #00dfd8;'>
|
| 226 |
<div style='display: flex; justify-content: space-between; align-items: start;'>
|
|
|
|
| 241 |
cards_html += "</div>"
|
| 242 |
return f"### π Learning Pathfinder\n{cards_html}"
|
| 243 |
|
| 244 |
+
def generate_interview_questions(gaps):
|
| 245 |
+
if not llm_reasoner:
|
| 246 |
+
return "1. Can you walk us through your most successful project?\n2. How do you stay updated with tech trends?"
|
| 247 |
+
|
| 248 |
+
print("π€ Generating questions with LLM...")
|
| 249 |
+
context = ", ".join(gaps[:3]) if gaps else "general tech"
|
| 250 |
+
prompt = f"Generate 3 technical interview questions for a candidate missing: {context}. Be specific."
|
| 251 |
+
try:
|
| 252 |
+
q_gen = llm_reasoner(prompt, max_length=150)[0]['generated_text']
|
| 253 |
+
return f"### π€ AI-Generated Questions\n{q_gen}\n\n*Tip: Leverage your adjacent skills to bridge these gaps.*"
|
| 254 |
+
except:
|
| 255 |
+
return "1. Describe a complex technical problem you solved.\n2. How do you handle deadlines?"
|
| 256 |
+
|
| 257 |
# Premium CSS for Glassmorphism
|
| 258 |
STYLE = """
|
| 259 |
.gradio-container {
|