| import io |
| import json |
| from typing import Optional |
|
|
| import gradio as gr |
| import PyPDF2 |
|
|
| from resume_ai import score, improve |
|
|
| def extract_text_from_pdf(file_obj: io.IOBase) -> str: |
| """Extract text from a PDF file-like object.""" |
| try: |
| reader = PyPDF2.PdfReader(file_obj) |
| text_chunks = [] |
| for page in reader.pages: |
| page_text = page.extract_text() or "" |
| text_chunks.append(page_text) |
| text = "\n".join(text_chunks).strip() |
| if not text: |
| raise ValueError("No extractable text found in PDF.") |
| return text |
| except Exception as e: |
| raise ValueError(f"Error reading PDF: {e}") |
|
|
| def read_resume_to_text(resume_file_path) -> str: |
| """ |
| Accepts a file path and returns text content. |
| Supports PDF and plain text files. |
| """ |
| if resume_file_path is None: |
| raise ValueError("Please upload a resume file.") |
|
|
| filename = str(resume_file_path).lower() |
| if filename.endswith(".pdf"): |
| with open(resume_file_path, "rb") as f: |
| return extract_text_from_pdf(f) |
| else: |
| with open(resume_file_path, "rb") as f: |
| data = f.read() |
| if not data: |
| raise ValueError("Uploaded file is empty.") |
| try: |
| return data.decode("utf-8").strip() |
| except UnicodeDecodeError: |
| return data.decode("latin-1").strip() |
|
|
| def score_fn(resume_file_path, job_desc: str) -> str: |
| try: |
| if not job_desc or not job_desc.strip(): |
| raise ValueError("Please paste a job description.") |
| resume_text = read_resume_to_text(resume_file_path) |
| result = score(resume_text, job_desc) |
| return json.dumps(result, indent=2, ensure_ascii=False) |
| except Exception as e: |
| return f"Error: {e}" |
|
|
| def improve_fn(resume_file_path, job_desc: Optional[str]) -> str: |
| try: |
| resume_text = read_resume_to_text(resume_file_path) |
| jd_text = job_desc if job_desc and job_desc.strip() else None |
| suggestions = improve(resume_text, jd_text) |
| if isinstance(suggestions, (list, tuple)): |
| bullets = "\n".join(f"- {s}" for s in suggestions) |
| return f"### Suggestions\n{bullets}" |
| elif isinstance(suggestions, dict): |
| return "```json\n" + json.dumps(suggestions, indent=2, ensure_ascii=False) + "\n```" |
| else: |
| return str(suggestions) |
| except Exception as e: |
| return f"Error: {e}" |
|
|
| def format_score_display(result_json) -> str: |
| """ |
| Takes the result JSON (as dict or str), parses it, and returns a Markdown string for display. |
| """ |
| if isinstance(result_json, str): |
| try: |
| result = json.loads(result_json) |
| except Exception: |
| return f"```\n{result_json}\n```" |
| else: |
| result = result_json |
|
|
| md = f"## ๐ ATS Compatibility Score: **{result.get('overall_score', 0)}%**\n\n" |
| md += "### Category Scores\n" |
| md += "| Skills | Experience | Education |\n" |
| md += "|--------|------------|-----------|\n" |
| cs = result.get("category_scores", {}) |
| md += f"| {cs.get('skills',0)}% | {cs.get('experience',0)}% | {cs.get('education',0)}% |\n\n" |
|
|
| gaps = result.get("top_skill_gaps", []) |
| if gaps: |
| md += "### ๐ฉ Top Skill Gaps\n" |
| for gap in gaps: |
| md += f"- {gap}\n" |
| return md |
|
|
| |
|
|
| with gr.Blocks(title="Resume AI (Score & Improve)") as demo: |
| gr.Markdown( |
| """ |
| # ๐ Resume AI โ Score & Improve |
| Upload your resume (PDF or TXT), paste a Job Description, and get: |
| - **Score**: A formatted breakdown of your resume's ATS compatibility |
| - **Improve**: A healthy set of suggestions to enhance your resume |
| """ |
| ) |
|
|
| with gr.Row(): |
| resume = gr.File(label="Upload Resume (PDF or TXT)", file_types=[".pdf", ".txt", "application/pdf", "text/plain", "application/octet-stream"], type="filepath") |
| jd = gr.Textbox(label="Job Description (paste here)", lines=10, placeholder="Paste JD text...") |
|
|
| with gr.Row(): |
| score_btn = gr.Button("โ๏ธ Score Resume", variant="primary") |
| improve_btn = gr.Button("โจ Improve Resume") |
|
|
| score_out = gr.Markdown(label="Score (Formatted)") |
| improve_out = gr.Markdown(label="Improvement Suggestions") |
|
|
| def score_fn_display(resume_file_path, job_desc: str) -> str: |
| try: |
| if not job_desc or not job_desc.strip(): |
| raise ValueError("Please paste a job description.") |
| resume_text = read_resume_to_text(resume_file_path) |
| result = score(resume_text, job_desc) |
| return format_score_display(result) |
| except Exception as e: |
| return f"Error: {e}" |
|
|
| score_btn.click(fn=score_fn_display, inputs=[resume, jd], outputs=score_out) |
| improve_btn.click(fn=improve_fn, inputs=[resume, jd], outputs=improve_out) |
|
|
|
|
| if __name__ == "__main__": |
| demo.launch(server_name="0.0.0.0",server_port=7860, pwa=True, mcp_server=True) |