Spaces:
Sleeping
Sleeping
Update app.py
Browse files
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.
|
|
|
|
|
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 |
-
|
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 |
-
|
79 |
-
|
80 |
-
|
81 |
-
|
82 |
-
|
83 |
-
|
84 |
-
|
85 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
86 |
try:
|
87 |
-
|
88 |
-
|
89 |
-
|
90 |
-
|
91 |
-
|
92 |
-
|
93 |
-
|
94 |
-
|
95 |
-
|
96 |
-
|
97 |
-
|
98 |
-
|
99 |
-
|
100 |
-
|
101 |
-
|
102 |
-
|
103 |
-
|
104 |
-
|
105 |
-
|
106 |
-
|
107 |
-
|
108 |
-
|
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(
|
|
|
|
|
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
|
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
|
221 |
|
222 |
# Tab 1: Generate RΓ©sumΓ©
|
223 |
with gr.Tab("π Generate RΓ©sumΓ©"):
|
224 |
-
|
225 |
-
|
226 |
-
|
227 |
-
|
228 |
-
|
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 |
-
|
237 |
-
|
238 |
-
|
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=[
|
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 |
-
|
260 |
-
|
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",
|
271 |
-
cv_tone = gr.Radio(["Professional",
|
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,
|
|
|
|
|
276 |
|
277 |
-
# Tab 5:
|
278 |
-
with gr.Tab("π Fetch Job
|
279 |
-
url_in = gr.Textbox(label="
|
280 |
jd_out = gr.Textbox(label="Job Description", lines=12)
|
281 |
-
btn_fetch = gr.Button("Fetch
|
282 |
-
btn_fetch.click(
|
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)
|