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