mgbam's picture
Update app.py
146b35d verified
"""
AI Resume Studio โ€“ Hugging Face Space
Author: Oluwafemi Idiakhoa
Updated: 2025-06-27
Features
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1. Generate rรฉsumรฉ โ†’ Word & PDF downloads
2. Score rรฉsumรฉ vs. job description
3. AI Section Co-Pilot (rewrite, quantify, bulletizeโ€ฆ)
4. Cover-letter generator
5. Fetch any job description by URL:
โ€ข LinkedIn via OAuth2 Jobs API
โ€ข All other sites via HTML scraping
6. Multilingual export via Deep-Translator (DeepL backend)
7. Auto-populate Score tab from latest Resume & JD
"""
import os
import re
import time
import tempfile
import requests
import gradio as gr
import google.generativeai as genai
from dotenv import load_dotenv
from bs4 import BeautifulSoup
from docx import Document
from reportlab.lib.pagesizes import LETTER
from reportlab.pdfgen import canvas
from deep_translator import DeeplTranslator
from urllib.parse import urlparse
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# Load Secrets & Configure Clients
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
load_dotenv()
# Gemini
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
genai.configure(api_key=GEMINI_API_KEY)
GEMINI = genai.GenerativeModel("gemini-1.5-pro-latest")
# DeepL via Deep-Translator
DEEPL_KEY = os.getenv("DEEPL_API_KEY")
def translate_text(text: str, tgt: str) -> str:
if not DEEPL_KEY or tgt.upper() == "EN":
return text
try:
return DeeplTranslator(api_key=DEEPL_KEY, target=tgt).translate(text)
except Exception as e:
return f"[Translation Error] {e}\n\n{text}"
# LinkedIn OAuth 2.0 (Client Credentials)
CLIENT_ID = os.getenv("LINKEDIN_CLIENT_ID")
CLIENT_SECRET = os.getenv("LINKEDIN_CLIENT_SECRET")
_token_cache = {}
def get_linkedin_token():
data = _token_cache.get("data", {})
if data and data.get("expires_at", 0) > time.time():
return data["access_token"]
resp = requests.post(
"https://www.linkedin.com/oauth/v2/accessToken",
data={
"grant_type": "client_credentials",
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
},
timeout=10
)
resp.raise_for_status()
payload = resp.json()
payload["expires_at"] = time.time() + payload.get("expires_in", 0) - 60
_token_cache["data"] = payload
return payload["access_token"]
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# Job-Description Fetcher (LinkedIn API or Generic Scraping)
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def fetch_job_description(url: str) -> str:
domain = urlparse(url).netloc.lower()
if "linkedin.com" in domain:
m = re.search(r"(?:jobs/view/|currentJobId=)(\d+)", url)
if m:
job_id = m.group(1)
try:
token = get_linkedin_token()
api_url = f"https://api.linkedin.com/v2/jobPosts/{job_id}?projection=(description)"
r = requests.get(api_url,
headers={"Authorization": f"Bearer {token}"},
timeout=10)
r.raise_for_status()
return r.json().get("description", "")
except Exception:
pass
try:
page = requests.get(url, headers={"User-Agent":"Mozilla/5.0"}, timeout=10)
soup = BeautifulSoup(page.text, "html.parser")
selectors = [
"div.jobsearch-jobDescriptionText",
"section.description",
"div.jobs-description__content",
"div#job-details",
"article.jobPosting",
"div.jd-container",
]
for sel in selectors:
block = soup.select_one(sel)
if block and block.get_text(strip=True):
return block.get_text(" ", strip=True)
text = soup.get_text(" ", strip=True)
return text[:5000] + ("โ€ฆ" if len(text) > 5000 else "")
except Exception as e:
return f"[Error fetching job description] {e}"
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# AI & File Utilities
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def ask_gemini(prompt: str, temp: float = 0.6) -> str:
try:
return GEMINI.generate_content(prompt, generation_config={"temperature": temp}).text.strip()
except Exception as e:
return f"[Gemini Error] {e}"
def save_docx(text: str) -> str:
f = tempfile.NamedTemporaryFile(delete=False, suffix=".docx")
doc = Document()
for line in text.splitlines():
doc.add_paragraph(line)
doc.save(f.name)
return f.name
def save_pdf(text: str) -> str:
f = tempfile.NamedTemporaryFile(delete=False, suffix=".pdf")
c = canvas.Canvas(f.name, pagesize=LETTER)
width, height = LETTER
y = height - 72
for line in text.splitlines():
c.drawString(72, y, line)
y -= 14
if y < 72:
c.showPage()
y = height - 72
c.save()
return f.name
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# Core AI Logic
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
LANGS = {
"EN": "English", "DE": "German", "FR": "French", "ES": "Spanish",
"IT": "Italian", "NL": "Dutch", "PT": "Portuguese","PL": "Polish",
"JA": "Japanese","ZH": "Chinese"
}
def generate_resume(name, email, phone, summary, exp, edu, skills, lang):
prompt = f"""
Create a professional rรฉsumรฉ in Markdown without first-person pronouns.
Output language: {LANGS[lang]}
Name: {name}
Email: {email}
Phone: {phone}
Professional Summary:
{summary}
Experience:
{exp}
Education:
{edu}
Skills:
{skills}
"""
md = ask_gemini(prompt)
return translate_text(md, lang)
def generate_and_export(name, email, phone, summary, exp, edu, skills, lang):
md = generate_resume(name, email, phone, summary, exp, edu, skills, lang)
return md, save_docx(md), save_pdf(md)
def score_resume(resume_md, jd):
prompt = f"""
Evaluate this rรฉsumรฉ against the job description. Return compact Markdown:
### Match Score
<0โ€“100>
### Suggestions
- โ€ฆ
"""
return ask_gemini(prompt, temp=0.4)
def refine_section(text, instr, lang):
prompt = f"""
Apply the following instruction to this rรฉsumรฉ section. Respond in {LANGS[lang]}.
Instruction: {instr}
Section:
{text}
"""
out = ask_gemini(prompt)
return translate_text(out, lang)
def generate_cover_letter(resume_md, jd, tone, lang):
prompt = f"""
Draft a one-page cover letter (max 300 words), in a {tone} tone, using {LANGS[lang]}.
Salutation: "Dear Hiring Manager,"
Rรฉsumรฉ:
{resume_md}
Job Description:
{jd}
"""
letter = ask_gemini(prompt)
return translate_text(letter, lang)
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# Gradio App Definition with State for Auto-Populate
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
with gr.Blocks(title="AI Resume Studio") as demo:
gr.Markdown("## ๐Ÿง  AI Resume Studio (Gemini ร— DeepL + Universal Job Fetcher)")
# State to hold last-generated rรฉsumรฉ & JD
resume_state = gr.State(value="")
jd_state = gr.State(value="")
# Tab 1: Generate Rรฉsumรฉ
with gr.Tab("๐Ÿ“„ Generate Rรฉsumรฉ"):
with gr.Row():
name_in, email_in, phone_in = (
gr.Textbox(label="Name"),
gr.Textbox(label="Email"),
gr.Textbox(label="Phone"),
)
sum_in = gr.Textbox(label="Professional Summary")
exp_in = gr.Textbox(label="Experience")
edu_in = gr.Textbox(label="Education")
skills_in = gr.Textbox(label="Skills")
lang_in = gr.Dropdown(list(LANGS.keys()), value="EN", label="Language")
md_out = gr.Markdown(label="Rรฉsumรฉ (Markdown)")
docx_out = gr.File(label="โฌ‡ Download .docx")
pdf_out = gr.File(label="โฌ‡ Download .pdf")
btn_gen = gr.Button("Generate")
btn_gen.click(
generate_and_export,
inputs=[name_in, email_in, phone_in, sum_in, exp_in, edu_in, skills_in, lang_in],
outputs=[md_out, docx_out, pdf_out, resume_state],
)
# Tab 2: Score Rรฉsumรฉ
with gr.Tab("๐Ÿงฎ Score Rรฉsumรฉ Against Job"):
res_in = gr.Textbox(value=resume_state, label="Rรฉsumรฉ (Markdown)", lines=10)
jd_in = gr.Textbox(value=jd_state, label="Job Description", lines=8)
score_out = gr.Markdown(label="Score & Suggestions")
btn_score = gr.Button("Evaluate")
btn_score.click(score_resume, inputs=[res_in, jd_in], outputs=score_out)
# Tab 3: AI Section Co-Pilot
with gr.Tab("โœ๏ธ AI Section Co-Pilot"):
sec_in = gr.Textbox(label="Section Text", lines=6)
act_in = gr.Radio(
["Rewrite", "Make More Concise", "Quantify Achievements", "Convert to Bullet Points"],
label="Action"
)
lang_sec = gr.Dropdown(list(LANGS.keys()), value="EN", label="Language")
sec_out = gr.Textbox(label="AI Output", lines=6)
btn_sec = gr.Button("Apply")
btn_sec.click(refine_section, inputs=[sec_in, act_in, lang_sec], outputs=sec_out)
# Tab 4: Cover-Letter Generator
with gr.Tab("๐Ÿ“ง Cover-Letter Generator"):
cv_res = gr.Textbox(label="Rรฉsumรฉ (Markdown)", lines=12)
cv_jd = gr.Textbox(label="Job Description", lines=8)
cv_tone = gr.Radio(["Professional", "Friendly", "Enthusiastic"], label="Tone")
cv_lang = gr.Dropdown(list(LANGS.keys()), value="EN", label="Language")
cv_out = gr.Markdown(label="Cover Letter")
btn_cv = gr.Button("Generate")
btn_cv.click(generate_cover_letter,
inputs=[cv_res, cv_jd, cv_tone, cv_lang],
outputs=cv_out)
# Tab 5: Universal Job Description Fetcher
with gr.Tab("๐ŸŒ Fetch Job Description"):
url_in = gr.Textbox(label="Job URL")
jd_out = gr.Textbox(label="Job Description", lines=12)
btn_fetch = gr.Button("Fetch Description")
btn_fetch.click(
fetch_job_description,
inputs=[url_in],
outputs=[jd_out, jd_state],
)
demo.launch(share=False)