Spaces:
Sleeping
Sleeping
| """ | |
| 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] | |
| 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() | |