|
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"], 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) |