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