mgbam commited on
Commit
7ddc3a3
ยท
verified ยท
1 Parent(s): 03aa58e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +114 -69
app.py CHANGED
@@ -9,76 +9,107 @@ Features
9
  2. Score rรฉsumรฉ vs. job description
10
  3. AI Section Co-Pilot (rewrite, quantify, bulletizeโ€ฆ)
11
  4. Cover-letter generator
12
- 5. Job-description via LinkedIn API (OAuth client_credentials)
13
  6. Multilingual export via Deep-Translator (DeepL backend)
14
  """
15
 
16
- import os, re, tempfile
 
 
 
17
  import requests
18
  import gradio as gr
19
  import google.generativeai as genai
20
  from dotenv import load_dotenv
 
21
  from docx import Document
22
  from reportlab.lib.pagesizes import LETTER
23
  from reportlab.pdfgen import canvas
24
  from deep_translator import DeeplTranslator
25
 
26
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
27
- # Load secrets & configure services
28
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
29
  load_dotenv()
30
 
31
- # Gemini setup
32
- genai.configure(api_key=os.getenv("GEMINI_API_KEY"))
 
33
  GEMINI = genai.GenerativeModel("gemini-1.5-pro-latest")
34
 
35
  # DeepL via Deep-Translator
36
  DEEPL_KEY = os.getenv("DEEPL_API_KEY")
37
  def translate_text(text: str, tgt: str) -> str:
38
- if not DEEPL_KEY or tgt.upper()=="EN": return text
 
39
  try:
40
  return DeeplTranslator(api_key=DEEPL_KEY, target=tgt).translate(text)
41
  except Exception as e:
42
  return f"[Translation Error] {e}\n\n{text}"
43
 
44
- # LinkedIn OAuth 2.0 Client-Credentials
45
- LINKEDIN_CLIENT_ID = os.getenv("LINKEDIN_CLIENT_ID")
46
- LINKEDIN_CLIENT_SECRET = os.getenv("LINKEDIN_CLIENT_SECRET")
47
- _TOKEN_CACHE = {}
 
48
  def get_linkedin_token():
49
- # cache until expiry
50
- token_data = _TOKEN_CACHE.get("data")
51
- if token_data and token_data["expires_at"] > time.time():
52
- return token_data["access_token"]
53
  resp = requests.post(
54
  "https://www.linkedin.com/oauth/v2/accessToken",
55
  data={
56
  "grant_type": "client_credentials",
57
- "client_id": LINKEDIN_CLIENT_ID,
58
- "client_secret": LINKEDIN_CLIENT_SECRET,
59
  },
 
60
  )
61
- resp.raise_for_status()
62
- data = resp.json()
63
- data["expires_at"] = time.time() + data.get("expires_in", 0) - 60
64
- _TOKEN_CACHE["data"] = data
65
- return data["access_token"]
66
-
67
- def fetch_job_via_api(job_url: str) -> str:
68
- job_id = (re.search(r"/jobs/view/(\d+)", job_url) or re.search(r"currentJobId=(\d+)", job_url))
69
- if not job_id:
 
 
 
 
70
  return "[Error] Unable to parse job ID from URL."
71
- token = get_linkedin_token()
72
- headers = {"Authorization": f"Bearer {token}"}
73
- # LinkedIn v2 Jobs endpoint (requires r_jobs scope)
74
- api_url = f"https://api.linkedin.com/v2/jobPosts/{job_id.group(1)}?projection=(description)"
75
- r = requests.get(api_url, headers=headers, timeout=10)
76
- if r.status_code != 200:
77
- return f"[LinkedIn API Error {r.status_code}] {r.text}"
78
- return r.json().get("description", "")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
 
80
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
81
- # AI & File utilities
82
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
83
  def ask_gemini(prompt: str, temp: float = 0.6) -> str:
84
  try:
@@ -109,7 +140,7 @@ def save_pdf(text: str) -> str:
109
  return f.name
110
 
111
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
112
- # Core application logic
113
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
114
  LANGS = {
115
  "EN": "English", "DE": "German", "FR": "French", "ES": "Spanish",
@@ -119,13 +150,14 @@ LANGS = {
119
 
120
  def generate_resume(name, email, phone, summary, exp, edu, skills, lang):
121
  prompt = f"""
122
- Create a professional rรฉsumรฉ in Markdownโ€”no first-person. Output in {LANGS[lang]}.
 
123
 
124
  Name: {name}
125
  Email: {email}
126
  Phone: {phone}
127
 
128
- Summary:
129
  {summary}
130
 
131
  Experience:
@@ -144,9 +176,9 @@ def generate_and_export(name, email, phone, summary, exp, edu, skills, lang):
144
  md = generate_resume(name, email, phone, summary, exp, edu, skills, lang)
145
  return md, save_docx(md), save_pdf(md)
146
 
147
- def score_resume(resume_md, job_desc):
148
  prompt = f"""
149
- Evaluate this rรฉsumรฉ vs. the job description. Return Markdown:
150
 
151
  ### Match Score
152
  <0-100>
@@ -156,27 +188,33 @@ Evaluate this rรฉsumรฉ vs. the job description. Return Markdown:
156
  """
157
  return ask_gemini(prompt, temp=0.4)
158
 
159
- def refine_section(section, instr, lang):
160
- prompt = f"Refine this rรฉsumรฉ section in {LANGS[lang]}.\nInstruction: {instr}\nText:\n{section}"
 
 
 
 
 
 
161
  out = ask_gemini(prompt)
162
  return translate_text(out, lang)
163
 
164
- def generate_cover_letter(resume_md, job_desc, tone, lang):
165
  prompt = f"""
166
- Draft a one-page cover letter (โ‰ค300 words), {tone} tone, in {LANGS[lang]}.
167
  Salutation: "Dear Hiring Manager,"
168
 
169
  Rรฉsumรฉ:
170
  {resume_md}
171
 
172
  Job Description:
173
- {job_desc}
174
  """
175
  letter = ask_gemini(prompt)
176
  return translate_text(letter, lang)
177
 
178
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
179
- # Gradio UI
180
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
181
  with gr.Blocks(title="AI Resume Studio") as demo:
182
  gr.Markdown("## ๐Ÿง  AI Resume Studio (Gemini ร— DeepL ร— LinkedIn)")
@@ -184,19 +222,23 @@ with gr.Blocks(title="AI Resume Studio") as demo:
184
  # Tab 1: Generate Rรฉsumรฉ
185
  with gr.Tab("๐Ÿ“„ Generate Rรฉsumรฉ"):
186
  with gr.Row():
187
- name_in, email_in, phone_in = gr.Textbox(label="Name"), gr.Textbox(label="Email"), gr.Textbox(label="Phone")
188
- sum_in = gr.Textbox(label="Summary")
 
 
 
 
189
  exp_in = gr.Textbox(label="Experience")
190
  edu_in = gr.Textbox(label="Education")
191
  skills_in = gr.Textbox(label="Skills")
192
  lang_in = gr.Dropdown(list(LANGS.keys()), value="EN", label="Language")
193
 
194
- out_md = gr.Markdown(label="Rรฉsumรฉ (Markdown)")
195
- out_docx = gr.File(label="โฌ‡ Download .docx")
196
- out_pdf = gr.File(label="โฌ‡ Download .pdf")
197
- gen_btn = gr.Button("Generate")
198
 
199
- gen_btn.click(
200
  generate_and_export,
201
  inputs=[name_in, email_in, phone_in, sum_in, exp_in, edu_in, skills_in, lang_in],
202
  outputs=[out_md, out_docx, out_pdf],
@@ -204,36 +246,39 @@ with gr.Blocks(title="AI Resume Studio") as demo:
204
 
205
  # Tab 2: Score Rรฉsumรฉ
206
  with gr.Tab("๐Ÿงฎ Score Rรฉsumรฉ Against Job"):
207
- res_in = gr.Textbox(label="Rรฉsumรฉ (Markdown)", lines=10)
208
- jd_in = gr.Textbox(label="Job Description", lines=8)
209
  score_out = gr.Markdown(label="Score & Suggestions")
210
- score_btn = gr.Button("Evaluate")
211
- score_btn.click(score_resume, inputs=[res_in, jd_in], outputs=score_out)
212
 
213
  # Tab 3: AI Section Co-Pilot
214
  with gr.Tab("โœ๏ธ AI Section Co-Pilot"):
215
  sec_in = gr.Textbox(label="Section Text", lines=6)
216
- action = gr.Radio(["Rewrite","Make More Concise","Quantify Achievements","Convert to Bullet Points"], label="Action")
217
- sec_lang = gr.Dropdown(list(LANGS.keys()), value="EN", label="Language")
 
 
 
218
  sec_out = gr.Textbox(label="AI Output", lines=6)
219
- sec_btn = gr.Button("Apply")
220
- sec_btn.click(refine_section, inputs=[sec_in, action, sec_lang], outputs=sec_out)
221
 
222
  # Tab 4: Cover-Letter Generator
223
  with gr.Tab("๐Ÿ“ง Cover-Letter Generator"):
224
- cv_res = gr.Textbox(label="Rรฉsumรฉ (Markdown)", lines=12)
225
- cv_jd = gr.Textbox(label="Job Description", lines=8)
226
- cv_tone = gr.Radio(["Professional","Friendly","Enthusiastic"], label="Tone")
227
- cv_lang = gr.Dropdown(list(LANGS.keys()), value="EN", label="Language")
228
- cv_out = gr.Markdown(label="Cover Letter")
229
- cv_btn = gr.Button("Generate")
230
- cv_btn.click(generate_cover_letter, inputs=[cv_res, cv_jd, cv_tone, cv_lang], outputs=cv_out)
231
 
232
  # Tab 5: LinkedIn Job Fetcher
233
  with gr.Tab("๐ŸŒ Fetch Job via LinkedIn API"):
234
  url_in = gr.Textbox(label="LinkedIn Job URL")
235
  jd_out = gr.Textbox(label="Job Description", lines=12)
236
- fetch_btn = gr.Button("Fetch from LinkedIn")
237
- fetch_btn.click(fetch_job_via_api, inputs=[url_in], outputs=[jd_out])
238
 
239
  demo.launch(share=False)
 
9
  2. Score rรฉsumรฉ vs. job description
10
  3. AI Section Co-Pilot (rewrite, quantify, bulletizeโ€ฆ)
11
  4. Cover-letter generator
12
+ 5. Job-description via LinkedIn API (OAuth client_credentials) + fallback scraping
13
  6. Multilingual export via Deep-Translator (DeepL backend)
14
  """
15
 
16
+ import os
17
+ import re
18
+ import time
19
+ import tempfile
20
  import requests
21
  import gradio as gr
22
  import google.generativeai as genai
23
  from dotenv import load_dotenv
24
+ from bs4 import BeautifulSoup
25
  from docx import Document
26
  from reportlab.lib.pagesizes import LETTER
27
  from reportlab.pdfgen import canvas
28
  from deep_translator import DeeplTranslator
29
 
30
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
31
+ # Load Secrets & Configure Clients
32
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
33
  load_dotenv()
34
 
35
+ # Gemini
36
+ GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
37
+ genai.configure(api_key=GEMINI_API_KEY)
38
  GEMINI = genai.GenerativeModel("gemini-1.5-pro-latest")
39
 
40
  # DeepL via Deep-Translator
41
  DEEPL_KEY = os.getenv("DEEPL_API_KEY")
42
  def translate_text(text: str, tgt: str) -> str:
43
+ if not DEEPL_KEY or tgt.upper() == "EN":
44
+ return text
45
  try:
46
  return DeeplTranslator(api_key=DEEPL_KEY, target=tgt).translate(text)
47
  except Exception as e:
48
  return f"[Translation Error] {e}\n\n{text}"
49
 
50
+ # LinkedIn OAuth 2.0 (Client Credentials)
51
+ CLIENT_ID = os.getenv("LINKEDIN_CLIENT_ID")
52
+ CLIENT_SECRET = os.getenv("LINKEDIN_CLIENT_SECRET")
53
+ _token_cache = {}
54
+
55
  def get_linkedin_token():
56
+ data = _token_cache.get("data", {})
57
+ if data and data.get("expires_at", 0) > time.time():
58
+ return data["access_token"]
59
+
60
  resp = requests.post(
61
  "https://www.linkedin.com/oauth/v2/accessToken",
62
  data={
63
  "grant_type": "client_credentials",
64
+ "client_id": CLIENT_ID,
65
+ "client_secret": CLIENT_SECRET,
66
  },
67
+ timeout=10
68
  )
69
+ if resp.status_code != 200:
70
+ # raise or let caller fallback
71
+ resp.raise_for_status()
72
+
73
+ payload = resp.json()
74
+ payload["expires_at"] = time.time() + payload.get("expires_in", 0) - 60
75
+ _token_cache["data"] = payload
76
+ return payload["access_token"]
77
+
78
+ def fetch_job_via_api(url: str) -> str:
79
+ # Extract numeric job ID
80
+ m = re.search(r"(?:jobs/view/|currentJobId=)(\d+)", url)
81
+ if not m:
82
  return "[Error] Unable to parse job ID from URL."
83
+ job_id = m.group(1)
84
+
85
+ # Try LinkedIn Jobs API
86
+ try:
87
+ token = get_linkedin_token()
88
+ api_url = f"https://api.linkedin.com/v2/jobPosts/{job_id}?projection=(description)"
89
+ r = requests.get(api_url,
90
+ headers={"Authorization": f"Bearer {token}"},
91
+ timeout=10)
92
+ r.raise_for_status()
93
+ return r.json().get("description", "")
94
+ except Exception:
95
+ # Fallback to scraping
96
+ try:
97
+ page = requests.get(url, headers={"User-Agent":"Mozilla/5.0"}, timeout=10)
98
+ soup = BeautifulSoup(page.text, "html.parser")
99
+ for sel in [
100
+ "div.jobsearch-jobDescriptionText",
101
+ "section.description",
102
+ "div.jobs-description__content"
103
+ ]:
104
+ block = soup.select_one(sel)
105
+ if block:
106
+ return block.get_text(" ", strip=True)
107
+ return "[Error] No description found via scraping."
108
+ except Exception as e:
109
+ return f"[Scrape Error] {e}"
110
 
111
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
112
+ # AI & File Utilities
113
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
114
  def ask_gemini(prompt: str, temp: float = 0.6) -> str:
115
  try:
 
140
  return f.name
141
 
142
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
143
+ # Core Logic
144
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
145
  LANGS = {
146
  "EN": "English", "DE": "German", "FR": "French", "ES": "Spanish",
 
150
 
151
  def generate_resume(name, email, phone, summary, exp, edu, skills, lang):
152
  prompt = f"""
153
+ Create a professional rรฉsumรฉ in Markdown without first-person pronouns.
154
+ Output language: {LANGS[lang]}
155
 
156
  Name: {name}
157
  Email: {email}
158
  Phone: {phone}
159
 
160
+ Professional Summary:
161
  {summary}
162
 
163
  Experience:
 
176
  md = generate_resume(name, email, phone, summary, exp, edu, skills, lang)
177
  return md, save_docx(md), save_pdf(md)
178
 
179
+ def score_resume(resume_md, jd):
180
  prompt = f"""
181
+ Evaluate this rรฉsumรฉ against the job description. Return compact Markdown:
182
 
183
  ### Match Score
184
  <0-100>
 
188
  """
189
  return ask_gemini(prompt, temp=0.4)
190
 
191
+ def refine_section(text, instr, lang):
192
+ prompt = f"""
193
+ Apply the following instruction to this rรฉsumรฉ section. Respond in {LANGS[lang]}.
194
+
195
+ Instruction: {instr}
196
+ Section:
197
+ {text}
198
+ """
199
  out = ask_gemini(prompt)
200
  return translate_text(out, lang)
201
 
202
+ def generate_cover_letter(resume_md, jd, tone, lang):
203
  prompt = f"""
204
+ Draft a one-page cover letter (max 300 words), in a {tone} tone, using {LANGS[lang]}.
205
  Salutation: "Dear Hiring Manager,"
206
 
207
  Rรฉsumรฉ:
208
  {resume_md}
209
 
210
  Job Description:
211
+ {jd}
212
  """
213
  letter = ask_gemini(prompt)
214
  return translate_text(letter, lang)
215
 
216
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
217
+ # Gradio App
218
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
219
  with gr.Blocks(title="AI Resume Studio") as demo:
220
  gr.Markdown("## ๐Ÿง  AI Resume Studio (Gemini ร— DeepL ร— LinkedIn)")
 
222
  # Tab 1: Generate Rรฉsumรฉ
223
  with gr.Tab("๐Ÿ“„ Generate Rรฉsumรฉ"):
224
  with gr.Row():
225
+ name_in, email_in, phone_in = (
226
+ gr.Textbox(label="Name"),
227
+ gr.Textbox(label="Email"),
228
+ gr.Textbox(label="Phone"),
229
+ )
230
+ sum_in = gr.Textbox(label="Professional Summary")
231
  exp_in = gr.Textbox(label="Experience")
232
  edu_in = gr.Textbox(label="Education")
233
  skills_in = gr.Textbox(label="Skills")
234
  lang_in = gr.Dropdown(list(LANGS.keys()), value="EN", label="Language")
235
 
236
+ out_md = gr.Markdown(label="Rรฉsume (Markdown)")
237
+ out_docx = gr.File(label="โฌ‡ Download .docx")
238
+ out_pdf = gr.File(label="โฌ‡ Download .pdf")
239
+ btn_gen = gr.Button("Generate")
240
 
241
+ btn_gen.click(
242
  generate_and_export,
243
  inputs=[name_in, email_in, phone_in, sum_in, exp_in, edu_in, skills_in, lang_in],
244
  outputs=[out_md, out_docx, out_pdf],
 
246
 
247
  # Tab 2: Score Rรฉsumรฉ
248
  with gr.Tab("๐Ÿงฎ Score Rรฉsumรฉ Against Job"):
249
+ res_in = gr.Textbox(label="Rรฉsumรฉ (Markdown)", lines=10)
250
+ jd_in = gr.Textbox(label="Job Description", lines=8)
251
  score_out = gr.Markdown(label="Score & Suggestions")
252
+ btn_score = gr.Button("Evaluate")
253
+ btn_score.click(score_resume, inputs=[res_in, jd_in], outputs=score_out)
254
 
255
  # Tab 3: AI Section Co-Pilot
256
  with gr.Tab("โœ๏ธ AI Section Co-Pilot"):
257
  sec_in = gr.Textbox(label="Section Text", lines=6)
258
+ act_in = gr.Radio(
259
+ ["Rewrite", "Make More Concise", "Quantify Achievements", "Convert to Bullet Points"],
260
+ label="Action"
261
+ )
262
+ lang_sec = gr.Dropdown(list(LANGS.keys()), value="EN", label="Language")
263
  sec_out = gr.Textbox(label="AI Output", lines=6)
264
+ btn_sec = gr.Button("Apply")
265
+ btn_sec.click(refine_section, inputs=[sec_in, act_in, lang_sec], outputs=sec_out)
266
 
267
  # Tab 4: Cover-Letter Generator
268
  with gr.Tab("๐Ÿ“ง Cover-Letter Generator"):
269
+ cv_res = gr.Textbox(label="Rรฉsumรฉ (Markdown)", lines=12)
270
+ cv_jd = gr.Textbox(label="Job Description", lines=8)
271
+ cv_tone = gr.Radio(["Professional", "Friendly", "Enthusiastic"], label="Tone")
272
+ cv_lang = gr.Dropdown(list(LANGS.keys()), value="EN", label="Language")
273
+ cv_out = gr.Markdown(label="Cover Letter")
274
+ btn_cv = gr.Button("Generate")
275
+ btn_cv.click(generate_cover_letter, inputs=[cv_res, cv_jd, cv_tone, cv_lang], outputs=cv_out)
276
 
277
  # Tab 5: LinkedIn Job Fetcher
278
  with gr.Tab("๐ŸŒ Fetch Job via LinkedIn API"):
279
  url_in = gr.Textbox(label="LinkedIn Job URL")
280
  jd_out = gr.Textbox(label="Job Description", lines=12)
281
+ btn_fetch = gr.Button("Fetch from LinkedIn")
282
+ btn_fetch.click(fetch_job_via_api, inputs=[url_in], outputs=[jd_out])
283
 
284
  demo.launch(share=False)