File size: 8,338 Bytes
f8387c1
bfd445f
e98268a
f8387c1
169b5ce
 
 
f8387c1
db49350
37ffd3f
 
f8387c1
c9135a2
db49350
 
c9135a2
f8387c1
 
37ffd3f
 
f8387c1
 
37ffd3f
f8387c1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
dabfbfe
f8387c1
 
 
 
 
 
 
 
 
5f61464
169b5ce
 
 
f8387c1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169b5ce
 
 
 
37ffd3f
 
 
 
 
169b5ce
 
 
 
888d73b
ef396f3
37ffd3f
 
ef396f3
db49350
 
 
169b5ce
db49350
 
46fc669
169b5ce
98e28e8
37ffd3f
169b5ce
98e28e8
888d73b
169b5ce
98e28e8
 
db49350
37ffd3f
98e28e8
f8387c1
37ffd3f
 
 
 
 
 
 
169b5ce
37ffd3f
169b5ce
 
62bcdd1
169b5ce
62bcdd1
 
169b5ce
62bcdd1
169b5ce
62bcdd1
 
169b5ce
62bcdd1
f8387c1
888d73b
67ff76d
37ffd3f
 
888d73b
 
db49350
37ffd3f
98e28e8
169b5ce
93fba37
e8f939d
169b5ce
 
 
db49350
169b5ce
93fba37
169b5ce
f8387c1
37ffd3f
f8387c1
169b5ce
f8387c1
 
 
 
 
28634dd
169b5ce
 
 
 
 
 
28634dd
 
f8387c1
c9135a2
28634dd
 
 
95242f6
c9135a2
28634dd
 
 
 
67ff76d
28634dd
169b5ce
ef396f3
67ff76d
 
 
ef396f3
169b5ce
 
 
 
 
 
 
f8387c1
d422d60
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
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
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
import os
import re
import markdown
import gradio as gr
from weasyprint import HTML
from markitdown import MarkItDown
from cerebras.cloud.sdk import Cerebras

# Pastikan file style.css tersedia sesuai path (misalnya di folder "resumes" atau di direktori yang sama)

# Dapatkan API key dari environment variables
api_key = os.environ.get("CEREBRAS_API_KEY")

# Inisialisasi MarkItDown dengan semua optional dependencies (pastikan Anda telah menginstal 'markitdown[all]')
md_converter = MarkItDown(enable_plugins=True)

def create_prompt(resume_string: str, jd_string: str) -> str:
    """
    Membuat prompt detail untuk AI agar melakukan optimasi resume
    berdasarkan job description.
    """
    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**.  
   - Include an **"Additional Suggestions"** section at the end with actionable improvement recommendations.  

---

### 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
    ### Optimized Resume
    dsb.
    """
    pattern = r'^#+.*\b(?:[Rr]esume|[Oo]ptimized)\b.*$'
    return re.sub(pattern, '', markdown_text, flags=re.MULTILINE)

def process_resume(resume, jd_string):
    """
    Memproses file resume yang di-upload dan job description, lalu
    menghasilkan resume yang telah dioptimasi + saran perbaikan.
    """
    # Gunakan file-like stream dari upload (tanpa cek ekstensi, dukung semua format yang didukung MarkItDown)
    try:
        result = md_converter.convert(resume.file)
        resume_string = result.text_content  # konten Markdown hasil konversi
    except Exception as e:
        return f"Conversion failed: {str(e)}", "", "", "", ""
    
    # Buat prompt untuk AI
    prompt = create_prompt(resume_string, jd_string)

    # Dapatkan response dari AI
    response_string = get_resume_response(prompt, api_key)

    # Pisahkan response menjadi "optimized resume" dan "additional suggestions"
    response_list = response_string.split("## Additional Suggestions")
    new_resume = response_list[0].strip()
    # Ganti tanda asterisk di awal baris menjadi tanda "-" untuk bullet list standar
    new_resume = re.sub(r'^\* ', '- ', new_resume, flags=re.MULTILINE)
    suggestions = "## Additional Suggestions\n\n" + response_list[1].strip() if len(response_list) > 1 else ""

    # ===== Hapus heading yang mengandung kata resume/optimized =====
    new_resume = new_resume.replace("# Optimized Resume", "")
    new_resume = new_resume.replace("## Optimized Resume", "")
    new_resume = new_resume.replace("Optimized Resume", "")
    new_resume = new_resume.replace("# Resume", "")
    new_resume = new_resume.replace("## Resume", "")
    new_resume = re.sub(r'^#+\s*Resume\s*', '', new_resume, flags=re.MULTILINE)
    new_resume = remove_unwanted_headings(new_resume)
    # ===============================================================

    # Simpan resume asli (Markdown) jika diperlukan
    original_resume_path = "resumes/original_resume.md"
    with open(original_resume_path, "w", encoding='utf-8') as f:
        f.write(resume_string)
    
    # Simpan resume hasil optimasi (Markdown)
    optimized_resume_path = "resumes/optimized_resume.md"
    with open(optimized_resume_path, "w", encoding='utf-8') as f:
        f.write(new_resume)

    # Kembalikan output untuk di-render di Gradio
    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.
    """
    try:
        # Konversi Markdown ke HTML dengan ekstensi tambahan agar format terjaga
        html_content = markdown.markdown(new_resume, extensions=['extra', 'nl2br'])

        # Path output PDF
        output_pdf_file = "resumes/optimized_resume.pdf"

        # Gunakan stylesheet (pastikan path style.css benar)
        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)} πŸ’”"

# Bangun aplikasi Gradio
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")

    # Saat tombol Optimize Resume diklik
    run_button.click(
        process_resume,
        inputs=[resume_input, jd_input],
        outputs=[before_md, after_md, download_before, download_after, output_suggestions]
    )

    # Saat tombol Export PDF diklik
    export_button.click(
        export_resume,
        inputs=[after_md],
        outputs=[export_result]
    )

app.launch()