AI_Job_Finder / app.py
UMAR798's picture
Upload app.py
29ee91e verified
Raw
History Blame Contribute Delete
10.9 kB
"""
AI Job Finder — app.py
------------------------------------
Real logic (from the original notebook):
1. Extract text from the uploaded resume PDF with PyMuPDF (fitz)
2. Pull live remote job listings from the Remotive API
3. Embed the resume + job descriptions with SentenceTransformer (all-MiniLM-L6-v2)
4. Rank jobs by cosine similarity to the resume
5. Return the top 10 matches
UI: custom gradient theme, glassmorphic cards, gradient button,
results rendered as styled "job cards" instead of a plain dataframe.
Run with:
pip install -r requirements.txt
python app.py
"""
import os
import fitz # PyMuPDF
import docx # python-docx
import requests
import gradio as gr
from sentence_transformers import SentenceTransformer
from sklearn.metrics.pairwise import cosine_similarity
# Hugging Face ZeroGPU Spaces require at least one function decorated with
# @spaces.GPU. If this is running locally or on CPU-only hardware, the
# "spaces" package won't be installed / needed, so we fall back to a
# no-op decorator in that case.
try:
import spaces
GPU_DECORATOR = spaces.GPU
except ImportError:
def GPU_DECORATOR(fn):
return fn
# ----------------------------------------------------------------------
# 1. LOAD MODEL ONCE AT STARTUP (GPU if available, else CPU)
# ----------------------------------------------------------------------
import torch
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
print(f"Loading model on device: {DEVICE} ...")
model = SentenceTransformer("all-MiniLM-L6-v2", device=DEVICE)
print("Model loaded successfully.")
REMOTIVE_API_URL = "https://remotive.com/api/remote-jobs"
# ----------------------------------------------------------------------
# 2. CORE LOGIC
# ----------------------------------------------------------------------
def extract_pdf_text(pdf_path: str) -> str:
doc = fitz.open(pdf_path)
text = ""
for page in doc:
text += page.get_text()
doc.close()
return text
def extract_docx_text(docx_path: str) -> str:
document = docx.Document(docx_path)
parts = [p.text for p in document.paragraphs]
# Also pull text out of any tables (resumes sometimes use table layouts)
for table in document.tables:
for row in table.rows:
for cell in row.cells:
if cell.text:
parts.append(cell.text)
return "\n".join(parts)
def extract_resume_text(file_path: str) -> str:
ext = os.path.splitext(file_path)[1].lower()
if ext == ".pdf":
return extract_pdf_text(file_path)
elif ext in (".docx",):
return extract_docx_text(file_path)
elif ext == ".doc":
raise gr.Error(
"Legacy .doc files aren't supported — please save your resume as .docx or .pdf and try again."
)
else:
raise gr.Error("Unsupported file type. Please upload a .pdf or .docx resume.")
def fetch_remote_jobs(limit: int = 100):
response = requests.get(REMOTIVE_API_URL, timeout=15)
response.raise_for_status()
return response.json()["jobs"][:limit]
@GPU_DECORATOR
def find_jobs(resume_file):
if resume_file is None:
raise gr.Error("Please upload your resume (PDF or DOCX) before submitting.")
# --- 1. Extract resume text ---
resume_text = extract_resume_text(resume_file)
if not resume_text.strip():
raise gr.Error("Couldn't extract any text from that file. Try a different resume.")
# --- 2. Fetch live job listings ---
try:
jobs = fetch_remote_jobs(limit=100)
except Exception:
raise gr.Error("Couldn't fetch live job listings right now. Please try again shortly.")
descriptions = [job["description"] for job in jobs]
# --- 3. Embeddings + similarity (runs on GPU if available) ---
with torch.no_grad():
resume_embedding = model.encode(
resume_text,
device=DEVICE,
convert_to_numpy=True,
)
job_embeddings = model.encode(
descriptions,
device=DEVICE,
batch_size=32,
convert_to_numpy=True,
show_progress_bar=False,
)
scores = cosine_similarity([resume_embedding], job_embeddings)[0]
# --- 4. Rank + build results ---
results = []
for i, job in enumerate(jobs):
results.append({
"title": job["title"],
"company": job["company_name"],
"location": job["candidate_required_location"],
"score": round(float(scores[i]) * 100, 2),
"url": job["url"],
})
results = sorted(results, key=lambda x: x["score"], reverse=True)[:10]
# --- 5. Render as HTML job cards ---
if not results:
return "<p style='color:#9d9bc7;'>No matching jobs found. Try again later.</p>"
cards_html = ""
for job in results:
cards_html += f"""
<a href="{job['url']}" target="_blank" style="text-decoration:none;">
<div class="job-card">
<div class="job-card-header">
<span class="job-title">{job['title']}</span>
<span class="job-match">{job['score']}% match</span>
</div>
<div class="job-company">{job['company']}</div>
<div class="job-location">📍 {job['location']}</div>
</div>
</a>
"""
return cards_html
# ----------------------------------------------------------------------
# 3. CUSTOM THEME
# ----------------------------------------------------------------------
theme = gr.themes.Soft(
primary_hue=gr.themes.colors.violet,
secondary_hue=gr.themes.colors.indigo,
neutral_hue=gr.themes.colors.slate,
font=[gr.themes.GoogleFont("Poppins"), "ui-sans-serif", "sans-serif"],
font_mono=[gr.themes.GoogleFont("JetBrains Mono"), "monospace"],
).set(
body_background_fill="linear-gradient(135deg, #0f0c29 0%, #302b63 50%, #24243e 100%)",
body_background_fill_dark="linear-gradient(135deg, #0f0c29 0%, #302b63 50%, #24243e 100%)",
block_background_fill="rgba(255,255,255,0.06)",
block_background_fill_dark="rgba(255,255,255,0.06)",
block_border_width="1px",
block_border_color="rgba(255,255,255,0.12)",
block_radius="20px",
block_shadow="0 8px 32px rgba(0,0,0,0.35)",
button_primary_background_fill="linear-gradient(90deg, #7f5af0 0%, #ff6ac1 100%)",
button_primary_background_fill_hover="linear-gradient(90deg, #6b46e5 0%, #f0499f 100%)",
button_primary_text_color="#ffffff",
button_secondary_background_fill="rgba(255,255,255,0.08)",
button_secondary_background_fill_hover="rgba(255,255,255,0.16)",
button_secondary_text_color="#e5e5f5",
input_background_fill="rgba(255,255,255,0.05)",
body_text_color="#f1f0fb",
body_text_color_subdued="#b8b6d6",
)
# ----------------------------------------------------------------------
# 4. CUSTOM CSS
# ----------------------------------------------------------------------
custom_css = """
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap');
* { font-family: 'Poppins', sans-serif !important; }
#app-title {
text-align: center;
font-size: 2.4rem;
font-weight: 700;
background: linear-gradient(90deg, #a78bfa, #f472b6, #fb923c);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin-bottom: 0.2rem;
letter-spacing: -0.5px;
}
#app-subtitle {
text-align: center;
color: #c9c7e8;
font-size: 1.05rem;
margin-bottom: 1.8rem;
font-weight: 400;
}
.upload-card, .output-card {
border-radius: 22px !important;
backdrop-filter: blur(14px);
padding: 6px;
}
#submit-btn {
font-weight: 600 !important;
font-size: 1.05rem !important;
padding: 12px 0 !important;
border-radius: 14px !important;
box-shadow: 0 6px 20px rgba(127, 90, 240, 0.45);
transition: transform 0.15s ease, box-shadow 0.15s ease;
}
#submit-btn:hover {
transform: translateY(-2px);
box-shadow: 0 10px 26px rgba(244, 114, 182, 0.5);
}
#clear-btn {
border-radius: 14px !important;
font-weight: 500 !important;
transition: transform 0.15s ease;
}
#clear-btn:hover { transform: translateY(-2px); }
.job-card {
background: rgba(255,255,255,0.06);
border: 1px solid rgba(255,255,255,0.12);
border-radius: 16px;
padding: 16px 20px;
margin-bottom: 14px;
box-shadow: 0 4px 14px rgba(0,0,0,0.25);
transition: transform 0.15s ease, border-color 0.15s ease;
cursor: pointer;
}
.job-card:hover {
transform: translateY(-3px);
border-color: rgba(167,139,250,0.6);
}
.job-card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 6px;
gap: 10px;
}
.job-title {
font-size: 1.1rem;
font-weight: 600;
color: #ffffff;
}
.job-match {
background: linear-gradient(90deg, #34d399, #22c55e);
color: #062b1c;
font-size: 0.78rem;
font-weight: 700;
padding: 3px 10px;
border-radius: 999px;
white-space: nowrap;
}
.job-company {
color: #d8b4fe;
font-weight: 500;
font-size: 0.95rem;
margin-bottom: 2px;
}
.job-location {
color: #b8b6d6;
font-size: 0.85rem;
}
footer { visibility: hidden; }
"""
# ----------------------------------------------------------------------
# 5. LAYOUT
# ----------------------------------------------------------------------
with gr.Blocks(theme=theme, css=custom_css, title="AI Job Finder") as demo:
gr.HTML("<div id='app-title'>🤖 AI Job Finder</div>")
gr.HTML(
"<div id='app-subtitle'>Upload your resume and let AI recommend the best matching jobs for you</div>"
)
with gr.Row(equal_height=True):
with gr.Column(scale=1, elem_classes="upload-card"):
resume_input = gr.File(
label="📄 Upload Resume (PDF or DOCX)",
file_types=[".pdf", ".docx"],
type="filepath",
)
with gr.Row():
clear_btn = gr.Button("Clear", elem_id="clear-btn", variant="secondary")
submit_btn = gr.Button("✨ Find My Jobs", elem_id="submit-btn", variant="primary")
with gr.Column(scale=1, elem_classes="output-card"):
gr.Markdown("### 🎯 Recommended Jobs")
output_html = gr.HTML(
"<p style='color:#9d9bc7;'>Your matched jobs will appear here after you submit your resume.</p>"
)
submit_btn.click(fn=find_jobs, inputs=resume_input, outputs=output_html)
clear_btn.click(
fn=lambda: (None, "<p style='color:#9d9bc7;'>Your matched jobs will appear here after you submit your resume.</p>"),
inputs=None,
outputs=[resume_input, output_html],
)
if __name__ == "__main__":
demo.launch()