firmanaziz commited on
Commit
6d082aa
Β·
verified Β·
1 Parent(s): 86249f9

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +132 -63
app.py CHANGED
@@ -1,9 +1,26 @@
1
  import gradio as gr
 
2
  import fitz # PyMuPDF
3
  import json
 
 
4
  import urllib.parse
5
 
6
- # --- KONSTANTA BATAS TOKEN OUTPUT ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  MAX_OUTPUT_TOKENS = 8192
8
 
9
  # --- FUNGSI-FUNGSI UTAMA ---
@@ -22,91 +39,143 @@ def generate_search_links(keywords):
22
  keywords_encoded = urllib.parse.quote_plus(keywords)
23
  keywords_hyphenated = keywords.lower().replace(" ", "-").replace("(", "").replace(")", "")
24
  links = {
25
- "LinkedIn": f"https://www.linkedin.com/jobs/search/?keywords={keywords_encoded}&location=Indonesia",
26
- "JobStreet": f"https://www.jobstreet.co.id/id/job-search/{keywords_hyphenated}-jobs/",
27
- "Glints": f"https://glints.com/id/opportunities/jobs/explore?keyword={keywords_encoded}",
28
- "Indeed": f"https://id.indeed.com/jobs?q={keywords_encoded}",
29
- "Google Jobs": f"https://www.google.com/search?q={keywords_encoded}+jobs+in+Indonesia&ibp=htl;jobs"
30
  }
31
  return links
32
 
33
- def get_dummy_analysis(nama_kandidat="Kandidat"):
34
- """Mengembalikan data analisis karir dummy untuk keperluan testing."""
35
- return {
36
- "jabatan_ideal": "Software Engineer (Backend)",
37
- "alasan_kecocokan": [
38
- f"{nama_kandidat} memiliki pengalaman solid dalam pengembangan backend menggunakan Python dan Node.js.",
39
- "Portofolio menunjukkan kemampuan merancang arsitektur RESTful API yang scalable.",
40
- "Latar belakang pendidikan di bidang Ilmu Komputer mendukung pemahaman algoritma yang kuat.",
41
- "Pengalaman berkolaborasi dalam tim Agile menjadikan kandidat siap di lingkungan kerja modern."
42
- ],
43
- "deskripsi_pekerjaan": [
44
- "Merancang, membangun, dan memelihara layanan backend yang efisien dan andal.",
45
- "Berkolaborasi dengan tim frontend untuk mendefinisikan dan mengimplementasikan antarmuka API.",
46
- "Melakukan code review dan memastikan standar kualitas kode terpenuhi.",
47
- "Mengoptimalkan performa aplikasi dan query database untuk skala besar.",
48
- "Mendokumentasikan arsitektur sistem dan proses teknis secara berkala."
49
- ],
50
- "potensi_karir": [
51
- "Senior Backend Engineer dalam 2-3 tahun dengan spesialisasi di sistem terdistribusi.",
52
- "Tech Lead atau Engineering Manager setelah membangun pengalaman kepemimpinan tim.",
53
- "Solution Architect untuk kandidat yang tertarik pada desain sistem skala enterprise.",
54
- "Wirausaha teknologi atau CTO di startup bidang teknologi."
55
- ],
56
- "kisaran_gaji": {
57
- "junior": "Rp 6.000.000 - Rp 10.000.000 / bulan",
58
- "mid_level": "Rp 12.000.000 - Rp 20.000.000 / bulan",
59
- "senior": "Rp 22.000.000 - Rp 40.000.000 / bulan"
60
- },
61
- "kelebihan_tambahan": [
62
- "Kandidat memiliki kontribusi aktif di GitHub yang memperkuat kredibilitas teknis secara publik.",
63
- "Kemampuan komunikasi dalam bahasa Inggris membuka peluang karir di perusahaan multinasional atau remote global."
64
- ]
65
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
 
67
  def analyze_career_path(cv_file):
68
- """Fungsi utama pipeline (DUMMY): Ekstrak PDF -> Return JSON dummy -> Tambah Link."""
 
69
  if cv_file is None:
70
  raise gr.Error("Mohon upload file CV (PDF) Anda.")
71
 
72
  try:
73
- print("--- [DUMMY MODE] Memulai Proses Analisis Karir ---")
74
 
75
- # Tetap ekstrak teks PDF agar input pipeline tetap berjalan normal
76
  teks_cv = ekstrak_teks_dari_pdf(cv_file.name)
77
  if not teks_cv:
78
  raise gr.Error("PDF kosong atau tidak dapat dibaca.")
79
- print(f"βœ… Teks berhasil diekstrak ({len(teks_cv)} karakter). [Tidak dikirim ke API]")
80
-
81
- # Coba ambil nama dari baris pertama teks CV sebagai sentuhan personal
82
- nama_kandidat = teks_cv.strip().splitlines()[0].strip() if teks_cv.strip() else "Kandidat"
83
- print(f"πŸ‘€ Nama kandidat terdeteksi: {nama_kandidat}")
84
-
85
- print("2. [DUMMY] Melewati pemanggilan Gemini API, menggunakan data dummy...")
86
- response_json = get_dummy_analysis(nama_kandidat)
87
- print("βœ… Data dummy berhasil disiapkan.")
88
-
89
- print("3. Membuat tautan pencarian dari hasil analisis...")
90
- keywords_from_analysis = response_json.get("jabatan_ideal", "")
91
- search_links = generate_search_links(keywords_from_analysis)
92
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
  response_json["tautan_pencarian"] = search_links
94
- print("βœ… Tautan pencarian ditambahkan ke JSON.")
95
 
96
- print("--- [DUMMY MODE] Proses Selesai ---")
97
  return response_json
98
 
 
 
99
  except Exception as e:
100
  print(f"πŸ›‘ ERROR DALAM FUNGSI ANALISIS: {e}")
101
  raise gr.Error(f"Terjadi kesalahan: {e}")
102
 
103
- # --- MEMBUAT INTERFACE GRADIO ---
104
  with gr.Blocks(theme=gr.themes.Soft()) as demo:
105
  gr.Markdown("# πŸš€ API Analis Peluang Karir Personal")
106
- gr.Markdown(
107
- "> ⚠️ **MODE TESTING (DUMMY)** β€” Output menggunakan data statis, bukan hasil dari Gemini API.\n\n"
108
- "Antarmuka ini dapat digunakan untuk pengujian. Endpoint API publik tersedia di `/run/predict` untuk integrasi ke website Anda."
109
- )
110
 
111
  with gr.Row():
112
  with gr.Column(scale=1):
 
1
  import gradio as gr
2
+ import google.generativeai as genai
3
  import fitz # PyMuPDF
4
  import json
5
+ import os
6
+ import re
7
  import urllib.parse
8
 
9
+ # --- KONFIGURASI API KEY ---
10
+ API_CONFIGURED = False
11
+ try:
12
+ api_key = os.environ.get('GEMINI_API_KEY')
13
+ if api_key:
14
+ genai.configure(api_key=api_key)
15
+ model = genai.GenerativeModel('gemma-3-27b-it') # Gemma 3 1B
16
+ API_CONFIGURED = True
17
+ print("βœ… Konfigurasi API dan model berhasil.")
18
+ else:
19
+ print("πŸ›‘ Secret 'GEMINI_API_KEY' tidak ditemukan.")
20
+ except Exception as e:
21
+ print(f"πŸ›‘ Terjadi error saat inisialisasi: {e}")
22
+
23
+ # --- KONSTANTA ---
24
  MAX_OUTPUT_TOKENS = 8192
25
 
26
  # --- FUNGSI-FUNGSI UTAMA ---
 
39
  keywords_encoded = urllib.parse.quote_plus(keywords)
40
  keywords_hyphenated = keywords.lower().replace(" ", "-").replace("(", "").replace(")", "")
41
  links = {
42
+ "LinkedIn": f"https://www.linkedin.com/jobs/search/?keywords={keywords_encoded}&location=Indonesia",
43
+ "JobStreet": f"https://www.jobstreet.co.id/id/job-search/{keywords_hyphenated}-jobs/",
44
+ "Glints": f"https://glints.com/id/opportunities/jobs/explore?keyword={keywords_encoded}",
45
+ "Indeed": f"https://id.indeed.com/jobs?q={keywords_encoded}",
46
+ "Google Jobs":f"https://www.google.com/search?q={keywords_encoded}+jobs+in+Indonesia&ibp=htl;jobs"
47
  }
48
  return links
49
 
50
+ def parse_json_safe(text: str) -> dict:
51
+ """
52
+ Parse JSON dari teks bebas model.
53
+ Strategi (urutan prioritas):
54
+ 1. Cari blok ```json ... ``` atau ``` ... ```
55
+ 2. Cari objek { ... } terluar
56
+ 3. Raise error jika semua gagal
57
+ """
58
+ # Strategi 1: ambil dari blok markdown code fence
59
+ fence_match = re.search(r"```(?:json)?\s*(\{.*?\})\s*```", text, re.DOTALL)
60
+ if fence_match:
61
+ candidate = fence_match.group(1)
62
+ try:
63
+ return json.loads(candidate)
64
+ except json.JSONDecodeError:
65
+ pass # lanjut ke strategi berikutnya
66
+
67
+ # Strategi 2: ambil objek { ... } terluar
68
+ start = text.find("{")
69
+ end = text.rfind("}")
70
+ if start != -1 and end != -1 and end > start:
71
+ candidate = text[start:end + 1]
72
+ try:
73
+ return json.loads(candidate)
74
+ except json.JSONDecodeError as e:
75
+ raise ValueError(
76
+ f"Ditemukan struktur JSON tapi gagal di-parse: {e}\n"
77
+ f"Cuplikan teks: {candidate[:300]}"
78
+ )
79
+
80
+ raise ValueError(
81
+ f"Tidak ditemukan JSON valid dalam respons model.\n"
82
+ f"Cuplikan respons: {text[:300]}"
83
+ )
84
+
85
+ def log_token_usage(usage_metadata):
86
+ if usage_metadata is None:
87
+ print("⚠️ Token usage: data tidak tersedia.")
88
+ return
89
+ prompt_tokens = getattr(usage_metadata, 'prompt_token_count', 'N/A')
90
+ candidate_tokens = getattr(usage_metadata, 'candidates_token_count', 'N/A')
91
+ total_tokens = getattr(usage_metadata, 'total_token_count', 'N/A')
92
+ print("=" * 40)
93
+ print("πŸ“Š TOKEN USAGE")
94
+ print(f" πŸ”Ό Input (prompt) : {prompt_tokens}")
95
+ print(f" πŸ”½ Output (response): {candidate_tokens} [limit: {MAX_OUTPUT_TOKENS}]")
96
+ print(f" βž• Total : {total_tokens}")
97
+ print("=" * 40)
98
 
99
  def analyze_career_path(cv_file):
100
+ if not API_CONFIGURED:
101
+ raise gr.Error("API Key Gemini belum terkonfigurasi. Periksa Logs aplikasi.")
102
  if cv_file is None:
103
  raise gr.Error("Mohon upload file CV (PDF) Anda.")
104
 
105
  try:
106
+ print("--- Memulai Proses Analisis Karir ---")
107
 
 
108
  teks_cv = ekstrak_teks_dari_pdf(cv_file.name)
109
  if not teks_cv:
110
  raise gr.Error("PDF kosong atau tidak dapat dibaca.")
111
+ print("βœ… Teks berhasil diekstrak.")
112
+
113
+ print("2. Mengirim permintaan analisis karir ke model...")
114
+ prompt_analisis_karir = f"""
115
+ Anda adalah seorang "Career Analyst AI". Baca teks CV berikut dan buat laporan peluang karir.
116
+
117
+ Teks CV:
118
+ ---
119
+ {teks_cv}
120
+ ---
121
+
122
+ PENTING: Balas HANYA dengan satu blok JSON murni. Jangan tambahkan teks, penjelasan, atau komentar apapun di luar JSON.
123
+ Format output WAJIB persis seperti ini:
124
+
125
+ {{
126
+ "jabatan_ideal": "string",
127
+ "alasan_kecocokan": ["poin 1", "poin 2", "poin 3", "poin 4"],
128
+ "deskripsi_pekerjaan": ["poin 1", "poin 2", "poin 3", "poin 4", "poin 5"],
129
+ "potensi_karir": ["poin 1", "poin 2", "poin 3", "poin 4"],
130
+ "kisaran_gaji": {{
131
+ "junior": "Rp X - Rp Y / bulan",
132
+ "mid_level": "Rp X - Rp Y / bulan",
133
+ "senior": "Rp X - Rp Y / bulan"
134
+ }},
135
+ "kelebihan_tambahan": ["poin 1", "poin 2"]
136
+ }}
137
+ """
138
+
139
+ # ⚠️ Gemma 3 tidak support response_mime_type JSON β€” dihapus
140
+ generation_config = genai.types.GenerationConfig(
141
+ max_output_tokens=MAX_OUTPUT_TOKENS,
142
+ )
143
+ response = model.generate_content(prompt_analisis_karir, generation_config=generation_config)
144
+
145
+ log_token_usage(getattr(response, 'usage_metadata', None))
146
+
147
+ raw_text = response.text
148
+ print(f"πŸ“ Raw response preview: {raw_text[:200]!r}")
149
+
150
+ # Parse manual β€” tidak bergantung pada response_mime_type
151
+ try:
152
+ response_json = parse_json_safe(raw_text)
153
+ print("βœ… JSON berhasil di-parse.")
154
+ except ValueError as parse_err:
155
+ print(f"πŸ›‘ Gagal parse JSON: {parse_err}")
156
+ raise gr.Error(
157
+ f"Model tidak menghasilkan JSON yang valid.\n"
158
+ f"Detail: {parse_err}"
159
+ )
160
+
161
+ print("3. Membuat tautan pencarian...")
162
+ search_links = generate_search_links(response_json.get("jabatan_ideal", ""))
163
  response_json["tautan_pencarian"] = search_links
164
+ print("βœ… Tautan pencarian ditambahkan.")
165
 
166
+ print("--- Proses Selesai ---")
167
  return response_json
168
 
169
+ except gr.Error:
170
+ raise
171
  except Exception as e:
172
  print(f"πŸ›‘ ERROR DALAM FUNGSI ANALISIS: {e}")
173
  raise gr.Error(f"Terjadi kesalahan: {e}")
174
 
175
+ # --- INTERFACE GRADIO ---
176
  with gr.Blocks(theme=gr.themes.Soft()) as demo:
177
  gr.Markdown("# πŸš€ API Analis Peluang Karir Personal")
178
+ gr.Markdown("Antarmuka ini dapat digunakan untuk pengujian. Endpoint API publik tersedia di `/run/predict` untuk integrasi ke website Anda.")
 
 
 
179
 
180
  with gr.Row():
181
  with gr.Column(scale=1):