Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
@@ -1,85 +1,79 @@
|
|
1 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2 |
import gradio as gr
|
3 |
import google.generativeai as genai
|
4 |
from dotenv import load_dotenv
|
5 |
-
|
6 |
-
|
7 |
-
from
|
8 |
-
from reportlab.lib.pagesizes import LETTER # reportlab
|
9 |
from reportlab.pdfgen import canvas
|
|
|
10 |
|
11 |
-
# โโ Environment & Model โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
12 |
load_dotenv()
|
13 |
-
API_KEY = os.getenv("GEMINI_API_KEY")
|
14 |
-
genai.configure(api_key=API_KEY)
|
15 |
-
model = genai.GenerativeModel("gemini-1.5-pro-latest")
|
16 |
-
|
17 |
-
# โโ Core AI helpers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
18 |
-
def _ask_gemini(prompt: str) -> str:
|
19 |
-
"""Utility wrapper with basic errorโhandling."""
|
20 |
-
try:
|
21 |
-
rsp = model.generate_content(prompt, generation_config={"temperature": 0.7})
|
22 |
-
return rsp.text.strip()
|
23 |
-
except Exception as exc:
|
24 |
-
return f"[Gemini Error] {exc}"
|
25 |
-
|
26 |
-
# โโ Business Logic โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
27 |
-
def generate_resume(name, email, phone, summary, experience, education, skills):
|
28 |
-
prompt = f"""
|
29 |
-
Create a professionally formatted resume. Use Markdown headings and no firstโperson pronouns.
|
30 |
-
|
31 |
-
Name: {name}
|
32 |
-
Email: {email}
|
33 |
-
Phone: {phone}
|
34 |
-
|
35 |
-
Professional Summary:
|
36 |
-
{summary}
|
37 |
-
|
38 |
-
Experience:
|
39 |
-
{experience}
|
40 |
|
41 |
-
|
42 |
-
|
|
|
43 |
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
return _ask_gemini(prompt)
|
48 |
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
"""
|
62 |
-
|
63 |
-
|
64 |
-
def refine_text(section_text, instruction):
|
65 |
-
prompt = f"Refine the following text.\nInstruction: {instruction}\nText:\n{section_text}"
|
66 |
-
return _ask_gemini(prompt)
|
67 |
-
|
68 |
-
def generate_cover_letter(resume, job_desc, tone):
|
69 |
-
prompt = f"""
|
70 |
-
Act as a career coach. Draft a oneโpage cover letter **in {tone} tone** that aligns this RESUME
|
71 |
-
to the JOB DESCRIPTION. Do not exceed 300 words.
|
72 |
|
73 |
-
|
74 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
75 |
|
76 |
-
|
77 |
-
|
78 |
-
|
79 |
-
|
|
|
|
|
|
|
|
|
80 |
|
81 |
-
|
82 |
-
def _to_docx(text: str) -> str:
|
83 |
tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".docx")
|
84 |
doc = Document()
|
85 |
for line in text.splitlines():
|
@@ -87,7 +81,7 @@ def _to_docx(text: str) -> str:
|
|
87 |
doc.save(tmp.name)
|
88 |
return tmp.name
|
89 |
|
90 |
-
def
|
91 |
tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".pdf")
|
92 |
c = canvas.Canvas(tmp.name, pagesize=LETTER)
|
93 |
width, height = LETTER
|
@@ -101,80 +95,181 @@ def _to_pdf(text: str) -> str:
|
|
101 |
c.save()
|
102 |
return tmp.name
|
103 |
|
104 |
-
|
105 |
-
|
106 |
-
|
107 |
-
|
108 |
-
|
109 |
-
|
110 |
-
|
111 |
-
|
112 |
-
|
113 |
-
|
114 |
-
|
115 |
-
|
116 |
-
|
117 |
-
|
118 |
-
|
119 |
-
|
120 |
-
|
121 |
-
|
122 |
-
|
123 |
-
|
124 |
-
|
125 |
-
|
126 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
127 |
docx_file = gr.File(label="โฌย Word (.docx)")
|
128 |
pdf_file = gr.File(label="โฌย PDF (.pdf)")
|
129 |
gen_btn = gr.Button("Generate Resume")
|
130 |
|
131 |
gen_btn.click(
|
132 |
generate_resume_and_files,
|
133 |
-
inputs=[name, email, phone, summary, experience, education, skills],
|
134 |
-
outputs=[
|
135 |
)
|
136 |
|
137 |
-
# โโ
|
138 |
-
with gr.Tab("๐งฎ
|
139 |
-
resume_in
|
140 |
-
jd_in
|
141 |
-
score_out
|
142 |
-
score_btn
|
143 |
|
144 |
-
|
145 |
-
return rsp # we leave JSON parsing to the user for now
|
146 |
|
147 |
-
|
148 |
-
|
149 |
-
|
150 |
-
|
|
|
|
|
151 |
)
|
|
|
|
|
|
|
152 |
|
153 |
-
|
154 |
-
|
155 |
-
sec_in = gr.Textbox(label="Section Text", lines=6)
|
156 |
-
instr_in = gr.Textbox(label="Instruction (e.g., make concise)")
|
157 |
-
refined = gr.Textbox(label="Refined Output", lines=6)
|
158 |
-
refine_btn = gr.Button("Refine")
|
159 |
-
|
160 |
-
refine_btn.click(
|
161 |
-
lambda text, instr: refine_text(text, instr),
|
162 |
-
inputs=[sec_in, instr_in],
|
163 |
-
outputs=refined,
|
164 |
)
|
165 |
|
166 |
-
# โโ
|
167 |
-
with gr.Tab("๐ง
|
168 |
-
cv_resume
|
169 |
-
cv_jd
|
170 |
-
cv_tone
|
171 |
-
|
172 |
-
|
|
|
173 |
|
174 |
cv_btn.click(
|
175 |
-
|
176 |
-
inputs=[cv_resume, cv_jd, cv_tone],
|
177 |
outputs=cv_out,
|
178 |
)
|
179 |
|
180 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
AI Resume Studio โ Huggingย Faceย Space
|
3 |
+
Author: Oluwafemiย Idiakhoa
|
4 |
+
Last update: 2025โ06โ27
|
5 |
+
|
6 |
+
Features
|
7 |
+
โโโโโโโโ
|
8 |
+
1. Generate resume โ Word & PDF downloads
|
9 |
+
2. Score resume against job description
|
10 |
+
3. AI Section CoโPilot (rewrite, quantify, bulletizeโฆ)
|
11 |
+
4. Coverโletter generator
|
12 |
+
5. Jobโdescription scraper by URL
|
13 |
+
6. Multilingual export via DeepL (free & pro keys supported)
|
14 |
+
"""
|
15 |
+
|
16 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
17 |
+
# Imports & setup
|
18 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
19 |
+
import os, 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 # pythonโdocx
|
26 |
+
from reportlab.lib.pagesizes import LETTER
|
|
|
27 |
from reportlab.pdfgen import canvas
|
28 |
+
import deepl # DeepL translation
|
29 |
|
|
|
30 |
load_dotenv()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
31 |
|
32 |
+
# Gemini model (make sure key has access to this version)
|
33 |
+
genai.configure(api_key=os.getenv("API_KEY"))
|
34 |
+
GEMINI_MODEL = genai.GenerativeModel("gemini-1.5-pro-latest")
|
35 |
|
36 |
+
# DeepL translator
|
37 |
+
DEEPL_KEY = os.getenv("DEEPL_API_KEY")
|
38 |
+
DEEPL_TRANSLATOR = deepl.Translator(DEEPL_KEY) if DEEPL_KEY else None
|
|
|
39 |
|
40 |
+
# Supported DeepL target languages (code โ label)
|
41 |
+
LANGS = {
|
42 |
+
"EN": "English",
|
43 |
+
"DE": "German",
|
44 |
+
"FR": "French",
|
45 |
+
"ES": "Spanish",
|
46 |
+
"IT": "Italian",
|
47 |
+
"NL": "Dutch",
|
48 |
+
"PT-PT": "Portuguese",
|
49 |
+
"PT-BR": "Portuguese (BR)",
|
50 |
+
"PL": "Polish",
|
51 |
+
"JA": "Japanese",
|
52 |
+
"ZH": "Chinese",
|
53 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
54 |
|
55 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
56 |
+
# Helpers
|
57 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
58 |
+
def ask_gemini(prompt: str, temperature: float = 0.6) -> str:
|
59 |
+
try:
|
60 |
+
rsp = GEMINI_MODEL.generate_content(
|
61 |
+
prompt, generation_config={"temperature": temperature}
|
62 |
+
)
|
63 |
+
return rsp.text.strip()
|
64 |
+
except Exception as err:
|
65 |
+
return f"[Geminiย Error] {err}"
|
66 |
|
67 |
+
def translate_if_needed(text: str, target_code: str) -> str:
|
68 |
+
if target_code == "EN" or not DEEPL_TRANSLATOR:
|
69 |
+
return text # already English or DeepL key missing
|
70 |
+
try:
|
71 |
+
res = DEEPL_TRANSLATOR.translate_text(text, target_lang=target_code)
|
72 |
+
return res.text
|
73 |
+
except Exception as err:
|
74 |
+
return f"[DeepLย Error] {err}\n\n{text}"
|
75 |
|
76 |
+
def save_docx(text: str) -> str:
|
|
|
77 |
tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".docx")
|
78 |
doc = Document()
|
79 |
for line in text.splitlines():
|
|
|
81 |
doc.save(tmp.name)
|
82 |
return tmp.name
|
83 |
|
84 |
+
def save_pdf(text: str) -> str:
|
85 |
tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".pdf")
|
86 |
c = canvas.Canvas(tmp.name, pagesize=LETTER)
|
87 |
width, height = LETTER
|
|
|
95 |
c.save()
|
96 |
return tmp.name
|
97 |
|
98 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
99 |
+
# Core AI functions
|
100 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
101 |
+
def generate_resume(name, email, phone, summary, experience, education, skills, lang):
|
102 |
+
prompt = f"""
|
103 |
+
Create a professional resume in Markdown. **Do not use firstโperson pronouns.**
|
104 |
+
|
105 |
+
Targetย language: {LANGS[lang]}
|
106 |
+
|
107 |
+
Name: {name}
|
108 |
+
Email: {email}
|
109 |
+
Phone: {phone}
|
110 |
+
|
111 |
+
Professionalย Summary:
|
112 |
+
{summary}
|
113 |
+
|
114 |
+
Experience:
|
115 |
+
{experience}
|
116 |
+
|
117 |
+
Education:
|
118 |
+
{education}
|
119 |
+
|
120 |
+
Skills:
|
121 |
+
{skills}
|
122 |
+
"""
|
123 |
+
resume_md = ask_gemini(prompt)
|
124 |
+
return translate_if_needed(resume_md, lang)
|
125 |
+
|
126 |
+
def score_resume(resume_md, job_desc):
|
127 |
+
prompt = f"""
|
128 |
+
Evaluate the RESUME against the JOBย DESCRIPTION. Return *only* compact Markdown:
|
129 |
+
### Matchย Score
|
130 |
+
<integer 0โ100>
|
131 |
+
|
132 |
+
### Suggestions
|
133 |
+
- <bulletย 1>
|
134 |
+
- <bulletย 2>
|
135 |
+
- <bulletย 3>
|
136 |
+
"""
|
137 |
+
return ask_gemini(prompt.format(resume_md=resume_md, job_desc=job_desc), temperature=0.4)
|
138 |
+
|
139 |
+
def refine_section(section_text, instruction, lang):
|
140 |
+
prompt = f"""
|
141 |
+
Perform the following instruction on the resume section. Respond in {LANGS[lang]}.
|
142 |
+
|
143 |
+
Instruction: {instruction}
|
144 |
+
Text:
|
145 |
+
{section_text}
|
146 |
+
"""
|
147 |
+
refined = ask_gemini(prompt)
|
148 |
+
return translate_if_needed(refined, lang)
|
149 |
+
|
150 |
+
def generate_cover_letter(resume_md, job_desc, tone, lang):
|
151 |
+
prompt = f"""
|
152 |
+
Draft a oneโpage cover letter (โคโฏ300โฏwords) in {tone} tone, aligning the RESUME
|
153 |
+
to the JOBย DESCRIPTION. Use {LANGS[lang]} throughout. Salutation: "Dear Hiring Manager,".
|
154 |
+
|
155 |
+
RESUME:
|
156 |
+
{resume_md}
|
157 |
+
|
158 |
+
JOBย DESCRIPTION:
|
159 |
+
{job_desc}
|
160 |
+
"""
|
161 |
+
letter = ask_gemini(prompt)
|
162 |
+
return translate_if_needed(letter, lang)
|
163 |
+
|
164 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
165 |
+
# JD scraper
|
166 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
167 |
+
def scrape_job_description(url):
|
168 |
+
try:
|
169 |
+
headers = {"User-Agent": "Mozilla/5.0"}
|
170 |
+
page = requests.get(url, headers=headers, timeout=10)
|
171 |
+
soup = BeautifulSoup(page.text, "html.parser")
|
172 |
+
|
173 |
+
# Heuristics for popular boards
|
174 |
+
selectors = [
|
175 |
+
"div.jobsearch-jobDescriptionText", # Indeed
|
176 |
+
"section.description", # generic
|
177 |
+
"div.jobs-description__content", # LinkedIn
|
178 |
+
"div#job-details" # Greenhouse
|
179 |
+
]
|
180 |
+
for sel in selectors:
|
181 |
+
block = soup.select_one(sel)
|
182 |
+
if block:
|
183 |
+
return block.get_text(" ", strip=True)
|
184 |
+
return soup.get_text(" ", strip=True)[:3000]
|
185 |
+
except Exception as e:
|
186 |
+
return f"[Error] {e}"
|
187 |
+
|
188 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
189 |
+
# Wrapper to export resume + files
|
190 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
191 |
+
def generate_resume_and_files(name, email, phone, summary,
|
192 |
+
experience, education, skills, lang):
|
193 |
+
resume_md = generate_resume(name, email, phone, summary,
|
194 |
+
experience, education, skills, lang)
|
195 |
+
return resume_md, save_docx(resume_md), save_pdf(resume_md)
|
196 |
+
|
197 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
198 |
+
# Gradio UI
|
199 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
200 |
+
with gr.Blocks(title="AI Resume Studio") as demo:
|
201 |
+
gr.Markdown("## ๐ง ย Interactiveย AIย Resumeย Studio (Geminiย รย DeepL)")
|
202 |
+
|
203 |
+
LANG_CHOICES = [(v, k) for k, v in LANGS.items()]
|
204 |
+
|
205 |
+
# โโ 1 ยท Resume generation โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
206 |
+
with gr.Tab("๐ Generate Resume"):
|
207 |
+
with gr.Row():
|
208 |
+
name = gr.Textbox(label="Name")
|
209 |
+
email = gr.Textbox(label="Email")
|
210 |
+
phone = gr.Textbox(label="Phone")
|
211 |
+
summary = gr.Textbox(label="Professional Summary")
|
212 |
+
experience = gr.Textbox(label="Experience")
|
213 |
+
education = gr.Textbox(label="Education")
|
214 |
+
skills = gr.Textbox(label="Skills")
|
215 |
+
lang_sel = gr.Dropdown(LANG_CHOICES, value="EN", label="Outputย Language")
|
216 |
+
|
217 |
+
resume_md = gr.Markdown(label="Generated Resume")
|
218 |
docx_file = gr.File(label="โฌย Word (.docx)")
|
219 |
pdf_file = gr.File(label="โฌย PDF (.pdf)")
|
220 |
gen_btn = gr.Button("Generate Resume")
|
221 |
|
222 |
gen_btn.click(
|
223 |
generate_resume_and_files,
|
224 |
+
inputs=[name, email, phone, summary, experience, education, skills, lang_sel],
|
225 |
+
outputs=[resume_md, docx_file, pdf_file],
|
226 |
)
|
227 |
|
228 |
+
# โโ 2 ยท Match score โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
229 |
+
with gr.Tab("๐งฎ Score Resume Against Job"):
|
230 |
+
resume_in = gr.Textbox(label="Resumeย (Markdown)", lines=12)
|
231 |
+
jd_in = gr.Textbox(label="Jobย Description", lines=8)
|
232 |
+
score_out = gr.Markdown(label="Scoreย &ย Suggestions")
|
233 |
+
score_btn = gr.Button("Evaluate")
|
234 |
|
235 |
+
score_btn.click(score_resume, inputs=[resume_in, jd_in], outputs=score_out)
|
|
|
236 |
|
237 |
+
# โโ 3 ยท AI Section CoโPilot โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
238 |
+
with gr.Tab("โ๏ธ AI Section CoโPilot"):
|
239 |
+
sec_text = gr.Textbox(label="Sectionย Text", lines=6)
|
240 |
+
action = gr.Radio(
|
241 |
+
["Rewrite", "Make More Concise", "Quantify Achievements", "Convert to Bullet Points"],
|
242 |
+
label="Action"
|
243 |
)
|
244 |
+
sec_lang = gr.Dropdown(LANG_CHOICES, value="EN", label="Language")
|
245 |
+
sec_out = gr.Textbox(label="AI Output", lines=6)
|
246 |
+
sec_btn = gr.Button("Apply")
|
247 |
|
248 |
+
sec_btn.click(
|
249 |
+
refine_section, inputs=[sec_text, action, sec_lang], outputs=sec_out
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
250 |
)
|
251 |
|
252 |
+
# โโ 4 ยท Coverโletter generator โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
253 |
+
with gr.Tab("๐ง CoverโLetter Generator"):
|
254 |
+
cv_resume = gr.Textbox(label="Resumeย (Markdown)", lines=12)
|
255 |
+
cv_jd = gr.Textbox(label="Jobย Description", lines=8)
|
256 |
+
cv_tone = gr.Radio(["Professional", "Friendly", "Enthusiastic"], value="Professional", label="Tone")
|
257 |
+
cv_lang = gr.Dropdown(LANG_CHOICES, value="EN", label="Language")
|
258 |
+
cv_out = gr.Markdown(label="Coverย Letter")
|
259 |
+
cv_btn = gr.Button("Generate Cover Letter")
|
260 |
|
261 |
cv_btn.click(
|
262 |
+
generate_cover_letter,
|
263 |
+
inputs=[cv_resume, cv_jd, cv_tone, cv_lang],
|
264 |
outputs=cv_out,
|
265 |
)
|
266 |
|
267 |
+
# โโ 5 ยท Jobโdescription scraper โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
268 |
+
with gr.Tab("๐ Job Description Scraper"):
|
269 |
+
url_in = gr.Textbox(label="Jobย URL")
|
270 |
+
jd_out = gr.Textbox(label="Extractedย Description", lines=12)
|
271 |
+
scrape_btn = gr.Button("Fetch Description")
|
272 |
+
|
273 |
+
scrape_btn.click(scrape_job_description, inputs=url_in, outputs=jd_out)
|
274 |
+
|
275 |
+
demo.launch(share=False) # HFย Spaces already publishes the app
|