Spaces:
Running
Running
""" | |
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) | |