|
import os |
|
import re |
|
import markdown |
|
import gradio as gr |
|
from weasyprint import HTML |
|
from markitdown import MarkItDown |
|
from cerebras.cloud.sdk import Cerebras |
|
|
|
|
|
api_key = os.environ.get("CEREBRAS_API_KEY") |
|
|
|
|
|
md_converter = MarkItDown() |
|
|
|
def create_prompt(resume_string: str, jd_string: str) -> str: |
|
""" |
|
Membuat prompt detail agar AI melakukan optimasi resume berdasarkan job description. |
|
Di sini kita tambahkan instruksi khusus tentang formatting, agar Work Experience |
|
berbentuk bullet list. |
|
""" |
|
return f""" |
|
You are a professional resume optimization expert specializing in tailoring resumes to specific job descriptions. |
|
Your goal is to optimize my resume and provide actionable suggestions for improvement to align with the target role. |
|
|
|
### Guidelines: |
|
1. **Relevance**: |
|
- Prioritize experiences, skills, and achievements **most relevant to the job description**. |
|
- Remove or de-emphasize irrelevant details to ensure a **concise** and **targeted** resume. |
|
- Limit work experience section to 2-3 most relevant roles |
|
- Limit bullet points under each role to 2-3 most relevant impacts |
|
|
|
2. **Action-Driven Results**: |
|
- Use **strong action verbs** and **quantifiable results** (e.g., percentages, revenue, efficiency improvements) to highlight impact. |
|
|
|
3. **Keyword Optimization**: |
|
- Integrate **keywords** and phrases from the job description naturally to optimize for ATS (Applicant Tracking Systems). |
|
|
|
4. **Additional Suggestions** *(If Gaps Exist)*: |
|
- If the resume does not fully align with the job description, suggest: |
|
1. **Additional technical or soft skills** that I could add to make my profile stronger. |
|
2. **Certifications or courses** I could pursue to bridge the gap. |
|
3. **Project ideas or experiences** that would better align with the role. |
|
|
|
5. **Formatting**: |
|
- Output the tailored resume in **clean Markdown format**. |
|
- Use "##" for main headings (WORK EXPERIENCE, EDUCATION, etc.). |
|
- Use "###" for each role in WORK EXPERIENCE or each education entry. |
|
- For each role in WORK EXPERIENCE, use bullet points ("- ") in separate lines to describe responsibilities/achievements. |
|
- Include an **"Additional Suggestions"** section at the end with actionable improvement recommendations. |
|
- Resume should not exceed one page if possible. |
|
|
|
--- |
|
|
|
### Input: |
|
- **My resume**: |
|
{resume_string} |
|
|
|
- **The job description**: |
|
{jd_string} |
|
|
|
--- |
|
|
|
### Output: |
|
1. - A resume in **Markdown format** that emphasizes relevant experience, skills, and achievements. |
|
- Incorporates job description **keywords** to optimize for ATS. |
|
- Uses strong language and is no longer than **one page**. |
|
|
|
2. **Additional Suggestions** *(if applicable)*: |
|
- List **skills** that could strengthen alignment with the role. |
|
- Recommend **certifications or courses** to pursue. |
|
- Suggest **specific projects or experiences** to develop. |
|
""" |
|
|
|
def get_resume_response(prompt: str, api_key: str, model: str = "llama-3.3-70b", temperature: float = 0.7) -> str: |
|
""" |
|
Mengirim prompt ke model Cerebras (LLM) dan mengembalikan hasil streaming response. |
|
""" |
|
client = Cerebras(api_key=api_key) |
|
stream = client.chat.completions.create( |
|
messages=[ |
|
{"role": "system", "content": "Expert resume writer"}, |
|
{"role": "user", "content": prompt} |
|
], |
|
model=model, |
|
stream=True, |
|
temperature=temperature, |
|
max_completion_tokens=1024, |
|
top_p=1 |
|
) |
|
|
|
response_string = "" |
|
for chunk in stream: |
|
response_string += chunk.choices[0].delta.content or "" |
|
return response_string |
|
|
|
def remove_unwanted_headings(markdown_text: str) -> str: |
|
""" |
|
Menghapus heading apa pun yang mengandung kata 'resume' atau 'optimized' |
|
(dalam berbagai huruf besar/kecil). |
|
Contoh heading yang akan dihapus: '# Resume', '## optimized', dsb. |
|
""" |
|
pattern = r'^#+.*\b(?:[Rr]esume|[Oo]ptimized)\b.*$' |
|
return re.sub(pattern, '', markdown_text, flags=re.MULTILINE) |
|
|
|
def fix_work_experience_bullets(text: str) -> str: |
|
""" |
|
Mencari pola ' - ' di tengah kalimat (yang menandakan bullet), |
|
lalu memecahnya ke baris baru agar menjadi bullet list Markdown yang valid. |
|
""" |
|
|
|
return re.sub(r'\s-\s', '\n- ', text) |
|
|
|
def process_resume(resume, jd_string): |
|
""" |
|
Memproses file resume (pdf, docx, dll) dan job description, |
|
lalu menghasilkan resume yang telah dioptimasi + saran perbaikan. |
|
""" |
|
|
|
supported_extensions = ('.pptx', '.docx', '.pdf', '.jpg', '.jpeg', '.png', '.xlsx') |
|
|
|
|
|
if resume.name.lower().endswith(supported_extensions): |
|
|
|
result = md_converter.convert(resume.name) |
|
resume_string = result.text_content |
|
else: |
|
return "File format not supported for conversion to Markdown.", "", "", "", "" |
|
|
|
|
|
prompt = create_prompt(resume_string, jd_string) |
|
|
|
|
|
response_string = get_resume_response(prompt, api_key) |
|
|
|
|
|
response_list = response_string.split("## Additional Suggestions") |
|
new_resume = response_list[0].strip() |
|
suggestions = "## Additional Suggestions\n\n" + response_list[1].strip() if len(response_list) > 1 else "" |
|
|
|
|
|
new_resume = remove_unwanted_headings(new_resume) |
|
|
|
|
|
new_resume = fix_work_experience_bullets(new_resume) |
|
|
|
|
|
original_resume_path = "resumes/original_resume.md" |
|
with open(original_resume_path, "w", encoding='utf-8') as f: |
|
f.write(resume_string) |
|
|
|
|
|
optimized_resume_path = "resumes/optimized_resume.md" |
|
with open(optimized_resume_path, "w", encoding='utf-8') as f: |
|
f.write(new_resume) |
|
|
|
|
|
return resume_string, new_resume, original_resume_path, optimized_resume_path, suggestions |
|
|
|
def export_resume(new_resume): |
|
""" |
|
Meng-export resume hasil optimasi (Markdown) menjadi PDF menggunakan WeasyPrint. |
|
Pastikan path 'style.css' sesuai lokasi file style.css Anda. |
|
""" |
|
try: |
|
|
|
html_content = markdown.markdown(new_resume) |
|
|
|
|
|
output_pdf_file = "resumes/optimized_resume.pdf" |
|
|
|
|
|
HTML(string=html_content).write_pdf( |
|
output_pdf_file, |
|
stylesheets=["resumes/style.css"] |
|
) |
|
|
|
return output_pdf_file |
|
except Exception as e: |
|
return f"Failed to export resume: {str(e)}" |
|
|
|
|
|
with gr.Blocks() as app: |
|
gr.Markdown("# Resume Optimizer π") |
|
gr.Markdown("Upload your resume, paste the job description, and get actionable insights!") |
|
|
|
with gr.Row(): |
|
resume_input = gr.File(label="Upload Your Resume") |
|
jd_input = gr.Textbox( |
|
label="Paste the Job Description Here", |
|
lines=9, |
|
interactive=True, |
|
placeholder="Paste job description..." |
|
) |
|
|
|
run_button = gr.Button("Optimize Resume π€") |
|
|
|
with gr.Row(): |
|
before_md = gr.Markdown(label="Original Resume (Before)") |
|
after_md = gr.Markdown(label="Optimized Resume (After)") |
|
output_suggestions = gr.Markdown(label="Suggestions") |
|
|
|
with gr.Row(): |
|
download_before = gr.File(label="Download Original Resume") |
|
download_after = gr.File(label="Download Optimized Resume") |
|
|
|
export_button = gr.Button("Export Optimized Resume as PDF π") |
|
export_result = gr.File(label="Download PDF") |
|
|
|
|
|
run_button.click( |
|
process_resume, |
|
inputs=[resume_input, jd_input], |
|
outputs=[before_md, after_md, download_before, download_after, output_suggestions] |
|
) |
|
|
|
|
|
export_button.click( |
|
export_resume, |
|
inputs=[after_md], |
|
outputs=[export_result] |
|
) |
|
|
|
app.launch() |
|
|