""" 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 "

No matching jobs found. Try again later.

" cards_html = "" for job in results: cards_html += f"""
{job['title']} {job['score']}% match
{job['company']}
📍 {job['location']}
""" 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("
🤖 AI Job Finder
") gr.HTML( "
Upload your resume and let AI recommend the best matching jobs for you
" ) 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( "

Your matched jobs will appear here after you submit your resume.

" ) submit_btn.click(fn=find_jobs, inputs=resume_input, outputs=output_html) clear_btn.click( fn=lambda: (None, "

Your matched jobs will appear here after you submit your resume.

"), inputs=None, outputs=[resume_input, output_html], ) if __name__ == "__main__": demo.launch()