mgbam commited on
Commit
073ee45
Β·
verified Β·
1 Parent(s): 7ddc3a3

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +86 -66
app.py CHANGED
@@ -9,7 +9,9 @@ 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) + fallback scraping
 
 
13
  6. Multilingual export via Deep-Translator (DeepL backend)
14
  """
15
 
@@ -26,6 +28,7 @@ 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
@@ -56,7 +59,6 @@ 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={
@@ -66,54 +68,72 @@ def get_linkedin_token():
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:
116
- return GEMINI.generate_content(prompt, generation_config={"temperature": temp}).text.strip()
 
 
117
  except Exception as e:
118
  return f"[Gemini Error] {e}"
119
 
@@ -140,7 +160,7 @@ def save_pdf(text: str) -> str:
140
  return f.name
141
 
142
  # ──────────────────────────────────────────────────────────────────────────────
143
- # Core Logic
144
  # ──────────────────────────────────────────────────────────────────────────────
145
  LANGS = {
146
  "EN": "English", "DE": "German", "FR": "French", "ES": "Spanish",
@@ -181,10 +201,10 @@ def score_resume(resume_md, jd):
181
  Evaluate this rΓ©sumΓ© against the job description. Return compact Markdown:
182
 
183
  ### Match Score
184
- <0-100>
185
 
186
  ### Suggestions
187
- - ...
188
  """
189
  return ask_gemini(prompt, temp=0.4)
190
 
@@ -214,34 +234,33 @@ Job Description:
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)")
221
 
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],
245
  )
246
 
247
  # Tab 2: Score RΓ©sumΓ©
@@ -255,10 +274,9 @@ with gr.Blocks(title="AI Resume Studio") as demo:
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")
@@ -267,18 +285,20 @@ with gr.Blocks(title="AI Resume Studio") as demo:
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)
 
9
  2. Score rΓ©sumΓ© vs. job description
10
  3. AI Section Co-Pilot (rewrite, quantify, bulletize…)
11
  4. Cover-letter generator
12
+ 5. Fetch any job description by URL:
13
+ β€’ LinkedIn via OAuth2 Jobs API
14
+ β€’ All other sites via HTML scraping
15
  6. Multilingual export via Deep-Translator (DeepL backend)
16
  """
17
 
 
28
  from reportlab.lib.pagesizes import LETTER
29
  from reportlab.pdfgen import canvas
30
  from deep_translator import DeeplTranslator
31
+ from urllib.parse import urlparse
32
 
33
  # ──────────────────────────────────────────────────────────────────────────────
34
  # Load Secrets & Configure Clients
 
59
  data = _token_cache.get("data", {})
60
  if data and data.get("expires_at", 0) > time.time():
61
  return data["access_token"]
 
62
  resp = requests.post(
63
  "https://www.linkedin.com/oauth/v2/accessToken",
64
  data={
 
68
  },
69
  timeout=10
70
  )
71
+ resp.raise_for_status()
 
 
 
72
  payload = resp.json()
73
  payload["expires_at"] = time.time() + payload.get("expires_in", 0) - 60
74
  _token_cache["data"] = payload
75
  return payload["access_token"]
76
 
77
+ # ──────────────────────────────────────────────────────────────────────────────
78
+ # Job-Description Fetcher (LinkedIn API or Generic Scraping)
79
+ # ──────────────────────────────────────────────────────────────────────────────
80
+ def fetch_job_description(url: str) -> str:
81
+ domain = urlparse(url).netloc.lower()
82
+ # 1) If it's LinkedIn, try the Jobs API first
83
+ if "linkedin.com" in domain:
84
+ m = re.search(r"(?:jobs/view/|currentJobId=)(\d+)", url)
85
+ if m:
86
+ job_id = m.group(1)
87
+ try:
88
+ token = get_linkedin_token()
89
+ api_url = (
90
+ f"https://api.linkedin.com/v2/jobPosts/{job_id}"
91
+ "?projection=(description)"
92
+ )
93
+ r = requests.get(
94
+ api_url,
95
+ headers={"Authorization": f"Bearer {token}"},
96
+ timeout=10
97
+ )
98
+ r.raise_for_status()
99
+ return r.json().get("description", "")
100
+ except Exception:
101
+ # fall through to generic scraping
102
+ pass
103
+
104
+ # 2) Generic scraping for any other site (or LinkedIn fallback)
105
  try:
106
+ page = requests.get(url, headers={"User-Agent":"Mozilla/5.0"}, timeout=10)
107
+ soup = BeautifulSoup(page.text, "html.parser")
108
+
109
+ # Common job-description selectors across platforms
110
+ selectors = [
111
+ "div.jobsearch-jobDescriptionText", # Indeed
112
+ "section.description", # generic
113
+ "div.jobs-description__content", # LinkedIn client-side
114
+ "div#job-details", # Greenhouse
115
+ "article.jobPosting", # Workday / custom
116
+ "div.jd-container", # many ATS
117
+ ]
118
+ for sel in selectors:
119
+ block = soup.select_one(sel)
120
+ if block and block.get_text(strip=True):
121
+ return block.get_text(" ", strip=True)
122
+
123
+ # Fallback: return full page text (truncated)
124
+ text = soup.get_text(" ", strip=True)
125
+ return text[:5000] + ("…" if len(text) > 5000 else "")
126
+ except Exception as e:
127
+ return f"[Error fetching job description] {e}"
 
128
 
129
  # ──────────────────────────────────────────────────────────────────────────────
130
+ # AI & File Utilities (unchanged)
131
  # ──────────────────────────────────────────────────────────────────────────────
132
  def ask_gemini(prompt: str, temp: float = 0.6) -> str:
133
  try:
134
+ return GEMINI.generate_content(
135
+ prompt, generation_config={"temperature": temp}
136
+ ).text.strip()
137
  except Exception as e:
138
  return f"[Gemini Error] {e}"
139
 
 
160
  return f.name
161
 
162
  # ──────────────────────────────────────────────────────────────────────────────
163
+ # Core AI Logic & UI Setup (unchanged)
164
  # ──────────────────────────────────────────────────────────────────────────────
165
  LANGS = {
166
  "EN": "English", "DE": "German", "FR": "French", "ES": "Spanish",
 
201
  Evaluate this rΓ©sumΓ© against the job description. Return compact Markdown:
202
 
203
  ### Match Score
204
+ <0–100>
205
 
206
  ### Suggestions
207
+ - …
208
  """
209
  return ask_gemini(prompt, temp=0.4)
210
 
 
234
  return translate_text(letter, lang)
235
 
236
  # ──────────────────────────────────────────────────────────────────────────────
237
+ # Gradio App Definition
238
  # ──────────────────────────────────────────────────────────────────────────────
239
  with gr.Blocks(title="AI Resume Studio") as demo:
240
+ gr.Markdown("## 🧠 AI Resume Studio (Gemini Γ— DeepL + Universal Job Fetcher)")
241
 
242
  # Tab 1: Generate RΓ©sumΓ©
243
  with gr.Tab("πŸ“„ Generate RΓ©sumΓ©"):
244
+ name_in, email_in, phone_in = (
245
+ gr.Textbox(label="Name"),
246
+ gr.Textbox(label="Email"),
247
+ gr.Textbox(label="Phone")
248
+ )
 
249
  sum_in = gr.Textbox(label="Professional Summary")
250
  exp_in = gr.Textbox(label="Experience")
251
  edu_in = gr.Textbox(label="Education")
252
  skills_in = gr.Textbox(label="Skills")
253
  lang_in = gr.Dropdown(list(LANGS.keys()), value="EN", label="Language")
254
 
255
+ md_out = gr.Markdown(label="RΓ©sumΓ© (Markdown)")
256
+ docx_out = gr.File(label="⬇ Download .docx")
257
+ pdf_out = gr.File(label="⬇ Download .pdf")
258
  btn_gen = gr.Button("Generate")
259
 
260
  btn_gen.click(
261
  generate_and_export,
262
  inputs=[name_in, email_in, phone_in, sum_in, exp_in, edu_in, skills_in, lang_in],
263
+ outputs=[md_out, docx_out, pdf_out],
264
  )
265
 
266
  # Tab 2: Score RΓ©sumΓ©
 
274
  # Tab 3: AI Section Co-Pilot
275
  with gr.Tab("✏️ AI Section Co-Pilot"):
276
  sec_in = gr.Textbox(label="Section Text", lines=6)
277
+ act_in = gr.Radio([
278
+ "Rewrite", "Make More Concise", "Quantify Achievements", "Convert to Bullet Points"
279
+ ], label="Action")
 
280
  lang_sec = gr.Dropdown(list(LANGS.keys()), value="EN", label="Language")
281
  sec_out = gr.Textbox(label="AI Output", lines=6)
282
  btn_sec = gr.Button("Apply")
 
285
  # Tab 4: Cover-Letter Generator
286
  with gr.Tab("πŸ“§ Cover-Letter Generator"):
287
  cv_res = gr.Textbox(label="RΓ©sumΓ© (Markdown)", lines=12)
288
+ cv_jd = gr.Textbox(label="Job Description", lines=8)
289
+ cv_tone = gr.Radio(["Professional","Friendly","Enthusiastic"], label="Tone")
290
  cv_lang = gr.Dropdown(list(LANGS.keys()), value="EN", label="Language")
291
  cv_out = gr.Markdown(label="Cover Letter")
292
  btn_cv = gr.Button("Generate")
293
+ btn_cv.click(generate_cover_letter,
294
+ inputs=[cv_res, cv_jd, cv_tone, cv_lang],
295
+ outputs=cv_out)
296
 
297
+ # Tab 5: Universal Job Description Fetcher
298
+ with gr.Tab("🌐 Fetch Job Description"):
299
+ url_in = gr.Textbox(label="Job URL")
300
  jd_out = gr.Textbox(label="Job Description", lines=12)
301
+ btn_fetch = gr.Button("Fetch Description")
302
+ btn_fetch.click(fetch_job_description, inputs=[url_in], outputs=[jd_out])
303
 
304
  demo.launch(share=False)