deankk commited on
Commit
8a18fea
·
verified ·
1 Parent(s): 54f5198

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +439 -0
app.py ADDED
@@ -0,0 +1,439 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ✍️ CapGen AI — Caption Generator Indonesia
3
+ ===========================================
4
+ SETUP (HANYA 2 LANGKAH):
5
+ ------------------------
6
+ 1. Buat token gratis di: https://huggingface.co/settings/tokens
7
+ - Klik "New token"
8
+ - Type pilih "Read" (cukup ini saja)
9
+ - Copy token-nya
10
+
11
+ 2. Di Space kamu, buka: Settings → Repository Secrets
12
+ - Name: HF_TOKEN
13
+ - Value: paste token tadi
14
+ - Klik Save
15
+
16
+ SELESAI! Restart Space dan langsung bisa dipakai.
17
+ """
18
+
19
+ import gradio as gr
20
+ import requests
21
+ import os
22
+
23
+ # ══════════════════════════════════════════════════════
24
+ # KONFIGURASI MODEL
25
+ # ══════════════════════════════════════════════════════
26
+
27
+ # Pilih salah satu (hapus tanda # di depan yang mau dipakai):
28
+ MODEL_NAME = "Qwen/Qwen2.5-3B-Instruct" # ← RECOMMENDED, bagus buat Indo
29
+ # MODEL_NAME = "Qwen/Qwen2.5-1.5B-Instruct" # ← Lebih ringan, cukup bagus
30
+ # MODEL_NAME = "meta-llama/Llama-3.2-1B-Instruct" # ← Paling ringan, kualitas lower
31
+ # MODEL_NAME = "google/gemma-2-2b-it" # ← Alternatif bagus
32
+
33
+ HF_TOKEN = os.environ.get("HF_TOKEN", "")
34
+ CHAT_API = f"https://api-inference.huggingface.co/models/{MODEL_NAME}/v1/chat/completions"
35
+ TEXT_API = f"https://api-inference.huggingface.co/models/{MODEL_NAME}"
36
+
37
+ # ══════════════════════════════════════════════════════
38
+ # DATA PLATFORM, GAYA, BAHASA
39
+ # ══════════════════════════════════════════════════════
40
+
41
+ PLATFORMS = {
42
+ "Instagram": "Maks 2200 karakter. Boleh panjang, emoji bebas, hashtag penting.",
43
+ "TikTok": "Maks 150 karakter. SANGAT pendek, catchy, hook di awal.",
44
+ "Twitter/X": "Maks 280 karakter. Padat, tajam, bisa jadi thread hook.",
45
+ "LinkedIn": "Maks 3000 karakter. Profesional tapi engaging, boleh storytelling.",
46
+ "WhatsApp Status": "Maks 100 karakter. Sangat singkat, to the point.",
47
+ }
48
+
49
+ STYLES = {
50
+ "Aesthetic": "Puitis, minimalis emoji, kata-kata indah, vibes kekinian",
51
+ "Lucu": "Meme-able, punchline di akhir, bahasa slang, relatable",
52
+ "Jualan": "Ada harga/CTA, urgency, benefit jelas, profesional tapi ramah",
53
+ "Motivasi": "Inspiratif, powerful words, emoji api/bintang, hook kuat",
54
+ "Romantis": "Manis, puitis, bisa sedikit dramatis, emoji hati",
55
+ "Gaul/Jaksel": "Bahasa slang Jakarta: literally, bestie, sksksk, bangeet",
56
+ "Santai": "Casual, kayak ngobrol sama temen, ga formal",
57
+ "Formal": "Bahasa baku, sopan, cocok buat instansi/organisasi",
58
+ }
59
+
60
+ LANGUAGES = {
61
+ "Indonesia": "Bahasa Indonesia standar yang baik dan benar",
62
+ "Indonesia Gaul": "Campuran Indonesia dan slang gaul kekinian",
63
+ "Bahasa Jawa": "Bahasa Jawa (ngoko/krama sesuai konteks)",
64
+ "Bahasa Sunda": "Bahasa Sunda yang gaul dan lucu",
65
+ "English": "Bahasa Inggris yang natural",
66
+ "Bilingual": "Campuran Indonesia dan English",
67
+ }
68
+
69
+ PRESETS = {
70
+ "🍜 Makanan": "Foto makan nasi goreng spesial di warung pinggir jalan, porsi besar, harga cuma 15rb, ada telur dan kerupuk",
71
+ "🌅 Sunset": "Foto sunset di pantai Bali, langit oranye kemerahan, pegang minuman kelapa, rambut diterpa angin",
72
+ "🛒 Jualan": "Foto produk skincare baru, botol kaca bening, tekstur gel, harga normal 150rb promo jadi 89rb",
73
+ "🏢 Kantor": "Foto bareng tim kantor setelah selesai project besar, semua senang, pakai seragam kompak",
74
+ "🐱 Kucing": "Foto kucing tidur miring di bantal pink, lucu banget, bulunya lembut dan fluffy",
75
+ "🛵 Jalan-jalan": "Foto naik motor melalui jalan Malioboro malam hari, lampu warna-warni, habis belanja batik",
76
+ "🏋️ Gym": "Foto habis workout di gym, badan berkeringat, baju sport, terlihat bugar dan proud",
77
+ "📚 Belajar": "Foto meja belajar yang rapi, ada buku dan kopi, lampu belajar warm light, vibes produktif",
78
+ }
79
+
80
+ SYSTEM_PROMPT = """Kamu adalah CapGen AI, ahli pembuat caption media sosial untuk pengguna Indonesia.
81
+
82
+ ATURAN KETAT (WAJIB DIIKUTI):
83
+ 1. HANYA keluarkan caption. TANPA kata "Berikut", "Tentu", "Baik", "Oke", atau penjelasan apapun.
84
+ 2. Setiap caption dipisahkan EXACTLY dengan --- (tiga strip, tanpa spasi tambahan).
85
+ 3. Setiap caption WAJIB sertakan 3-8 hashtag relevan di AKHIR caption.
86
+ 4. Sesuaikan panjang dengan batasan platform yang dipilih.
87
+ 5. Gaya bahasa HARUS konsisten sesuai pilihan user.
88
+ 6. Jangan beri nomor urut (1., 2., 3. dst).
89
+ 7. Jangan pakai tanda kutip atau formatting markdown di caption."""
90
+
91
+
92
+ # ══════════════════════════════════════════════���═══════
93
+ # LOGIC — API CALL & GENERATE
94
+ # ══════════════════════════════════════════════════════
95
+
96
+ def call_api(messages):
97
+ """Panggil HF Inference API (chat dulu, fallback ke text)"""
98
+ headers = {
99
+ "Authorization": f"Bearer {HF_TOKEN}",
100
+ "Content-Type": "application/json"
101
+ }
102
+ params = {"max_tokens": 1000, "temperature": 0.85, "top_p": 0.9}
103
+
104
+ # ── Attempt 1: Chat Completions API ──
105
+ try:
106
+ resp = requests.post(
107
+ CHAT_API, headers=headers,
108
+ json={"model": MODEL_NAME, "messages": messages, **params},
109
+ timeout=60
110
+ )
111
+ if resp.status_code == 200:
112
+ return resp.json()["choices"][0]["message"]["content"]
113
+ if resp.status_code == 503:
114
+ return "LOADING"
115
+ if resp.status_code == 429:
116
+ return "RATE_LIMIT"
117
+ except requests.exceptions.Timeout:
118
+ return "TIMEOUT"
119
+ except Exception:
120
+ pass
121
+
122
+ # ── Attempt 2: Text Generation API (fallback) ──
123
+ try:
124
+ prompt = f"{messages[0]['content']}\n\n{messages[1]['content']}"
125
+ resp = requests.post(
126
+ TEXT_API, headers=headers,
127
+ json={
128
+ "inputs": prompt,
129
+ "parameters": {
130
+ "max_new_tokens": 1000,
131
+ "temperature": 0.85,
132
+ "top_p": 0.9,
133
+ "return_full_text": False
134
+ }
135
+ },
136
+ timeout=60
137
+ )
138
+ if resp.status_code == 200:
139
+ data = resp.json()
140
+ if isinstance(data, list) and "generated_text" in data[0]:
141
+ text = data[0]["generated_text"]
142
+ # Strip prompt kalau ikut ke-return
143
+ if prompt in text:
144
+ text = text.replace(prompt, "").strip()
145
+ return text
146
+ if resp.status_code == 503:
147
+ return "LOADING"
148
+ if resp.status_code == 429:
149
+ return "RATE_LIMIT"
150
+ except requests.exceptions.Timeout:
151
+ return "TIMEOUT"
152
+ except Exception:
153
+ pass
154
+
155
+ return "ERROR"
156
+
157
+
158
+ def clean_text(text):
159
+ """Hapus preamble yang sering muncul dari model"""
160
+ if not text:
161
+ return text
162
+ unwanted = [
163
+ "Baik, berikut", "Berikut adalah", "Berikut beberapa",
164
+ "Tentu, berikut", "Oke, berikut", "Tentu saja,",
165
+ "Berikut caption", "Berikut ini", "Ini dia",
166
+ "Sure, here", "Here are", "Of course",
167
+ ]
168
+ for uw in unwanted:
169
+ if text.strip().lower().startswith(uw.lower()):
170
+ dash_idx = text.find("---")
171
+ if dash_idx > 0:
172
+ return text[dash_idx + 3:].strip()
173
+ dot_idx = text.find(".", len(uw))
174
+ if 0 < dot_idx < 100:
175
+ text = text[dot_idx + 1:].strip()
176
+ break
177
+ return text
178
+
179
+
180
+ def parse_captions(raw_text, jumlah):
181
+ """Parse response jadi list caption individual"""
182
+ text = clean_text(raw_text)
183
+ parts = [p.strip() for p in text.split("---") if p.strip()]
184
+
185
+ captions = []
186
+ for p in parts:
187
+ p = p.strip()
188
+ # Hapus nomor di awal kalau ada
189
+ if len(p) > 2 and p[0].isdigit() and p[1] in ".)":
190
+ p = p[2:].strip()
191
+ if len(p) > 15:
192
+ captions.append(p)
193
+
194
+ if not captions and len(text) > 15:
195
+ captions = [text]
196
+
197
+ while len(captions) < jumlah:
198
+ captions.append("")
199
+ return captions[:jumlah]
200
+
201
+
202
+ def generate_caption(konteks, platform, gaya, bahasa, jumlah):
203
+ """Fungsi utama generate — return values + visibility"""
204
+ error_all_visible = tuple(
205
+ [gr.update(visible=True)] * 5
206
+ )
207
+
208
+ # Validasi kosong
209
+ if not konteks or not konteks.strip():
210
+ vals = ["⚠️ Tulis dulu deskripsi foto/konten kamu di kolom atas!"] + [""] * 4
211
+ return tuple(gr.update(value=v, visible=True) for v in vals)
212
+
213
+ # Validasi token
214
+ if not HF_TOKEN:
215
+ msg = (
216
+ "⚠️ TOKEN BELUM DISET!\n\n"
217
+ "Cara setup:\n"
218
+ "1. Buat token di huggingface.co/settings/tokens\n"
219
+ "2. Di Space ini: Settings → Repository Secrets\n"
220
+ "3. Name: HF_TOKEN Value: paste token\n"
221
+ "4. Restart Space"
222
+ )
223
+ vals = [msg] + [""] * 4
224
+ return tuple(gr.update(value=v, visible=True) for v in vals)
225
+
226
+ # Bangun user prompt
227
+ user_msg = f"""PLATFORM: {platform}
228
+ {PLATFORMS[platform]}
229
+
230
+ GAYA: {gaya}
231
+ {STYLES[gaya]}
232
+
233
+ BAHASA: {bahasa}
234
+ {LANGUAGES[bahasa]}
235
+
236
+ KONTEKS FOTO/KONTEN:
237
+ {konteks.strip()}
238
+
239
+ BUAT TEPAT {jumlah} CAPTION YANG BERBEDA."""
240
+
241
+ messages = [
242
+ {"role": "system", "content": SYSTEM_PROMPT},
243
+ {"role": "user", "content": user_msg}
244
+ ]
245
+
246
+ # Panggil API
247
+ result = call_api(messages)
248
+
249
+ error_map = {
250
+ "LOADING": "⏳ Model sedang loading... Tunggu 20-30 detik lalu coba lagi.",
251
+ "RATE_LIMIT": "⚠��� Terlalu banyak request. Tunggu 1-2 menit lalu coba lagi.",
252
+ "TIMEOUT": "⏳ Request timeout. Coba lagi.",
253
+ "ERROR": "❌ Gagal menghubungi API. Coba lagi nanti.",
254
+ }
255
+
256
+ if result in error_map:
257
+ vals = [error_map[result]] + [""] * 4
258
+ return tuple(gr.update(value=v, visible=True) for v in vals)
259
+
260
+ # Parse caption
261
+ captions = parse_captions(result, jumlah)
262
+
263
+ # Bangun output dengan value + visibility
264
+ output = []
265
+ for i in range(5):
266
+ if i < jumlah:
267
+ output.append(gr.update(value=captions[i], visible=True))
268
+ else:
269
+ output.append(gr.update(value="", visible=False))
270
+
271
+ return tuple(output)
272
+
273
+
274
+ def update_visibility(jumlah):
275
+ """Update visibility saat dropdown jumlah berubah"""
276
+ return tuple(gr.update(visible=(i < jumlah)) for i in range(5))
277
+
278
+
279
+ def fill_preset(name):
280
+ """Isi konteks dari tombol preset"""
281
+ return PRESETS.get(name, "")
282
+
283
+
284
+ # ══════════════════════════════════════════════════════
285
+ # UI — GRADIO INTERFACE
286
+ # ══════════════════════════════════════════════════════
287
+
288
+ CUSTOM_CSS = """
289
+ #btn-generate {
290
+ font-size: 18px !important;
291
+ font-weight: 600 !important;
292
+ padding: 14px 32px !important;
293
+ border-radius: 12px !important;
294
+ }
295
+ .caption-output textarea {
296
+ font-size: 15px !important;
297
+ line-height: 1.7 !important;
298
+ border-radius: 10px !important;
299
+ }
300
+ .preset-row button {
301
+ min-width: fit-content !important;
302
+ font-size: 13px !important;
303
+ }
304
+ .footer-text {
305
+ text-align: center;
306
+ color: #aaa;
307
+ font-size: 12px;
308
+ margin-top: 16px;
309
+ }
310
+ """
311
+
312
+ with gr.Blocks(
313
+ title="CapGen AI — Caption Generator Indonesia",
314
+ theme=gr.themes.Soft(
315
+ primary_hue="orange",
316
+ secondary_hue="amber",
317
+ neutral_hue="stone",
318
+ font=gr.themes.GoogleFont("Inter"),
319
+ ),
320
+ css=CUSTOM_CSS,
321
+ ) as demo:
322
+
323
+ # ── HEADER ──
324
+ gr.Markdown("""
325
+ <div style="text-align:center; padding:8px 0 4px 0;">
326
+ <h1 style="margin:0;">✍️ CapGen AI</h1>
327
+ <p style="font-size:17px; color:#555; margin:4px 0 0 0;">
328
+ Caption Generator Khusus Bahasa Indonesia
329
+ </p>
330
+ <p style="font-size:13px; color:#999; margin:2px 0 0 0;">
331
+ Gak perlu login · Gak perlu jago prompt · Langsung kepakai
332
+ </p>
333
+ </div>
334
+ """)
335
+
336
+ # ── MAIN: 2 KOLOM ──
337
+ with gr.Row(equal_height=False):
338
+
339
+ # ── KOLOM KIRI: INPUT ──
340
+ with gr.Column(scale=1):
341
+
342
+ gr.Markdown("### 📝 Ceritakan Foto/Konten Kamu")
343
+ konteks = gr.Textbox(
344
+ placeholder=(
345
+ "Contoh: Foto makan nasi goreng di warung pinggir jalan, "
346
+ "porsi besar, harga 15rb, ada telur mata sapi..."
347
+ ),
348
+ lines=4,
349
+ max_lines=8,
350
+ show_label=False,
351
+ )
352
+
353
+ gr.Markdown("### ⚡ Contoh Cepat (klik = langsung isi)")
354
+ preset_names = list(PRESETS.keys())
355
+ for row_start in range(0, len(preset_names), 4):
356
+ with gr.Row(elem_classes=["preset-row"]):
357
+ for name in preset_names[row_start:row_start + 4]:
358
+ gr.Button(name, size="sm", variant="secondary").click(
359
+ fn=lambda n=name: fill_preset(n),
360
+ outputs=konteks,
361
+ )
362
+
363
+ gr.Markdown("### ⚙️ Pengaturan")
364
+ with gr.Row():
365
+ platform = gr.Dropdown(
366
+ choices=list(PLATFORMS.keys()),
367
+ value="Instagram",
368
+ label="📱 Platform",
369
+ )
370
+ gaya = gr.Dropdown(
371
+ choices=list(STYLES.keys()),
372
+ value="Aesthetic",
373
+ label="🎨 Gaya",
374
+ )
375
+
376
+ with gr.Row():
377
+ bahasa = gr.Dropdown(
378
+ choices=list(LANGUAGES.keys()),
379
+ value="Indonesia",
380
+ label="🌐 Bahasa",
381
+ )
382
+ jumlah = gr.Dropdown(
383
+ choices=[1, 2, 3, 5],
384
+ value=3,
385
+ label="🔢 Jumlah",
386
+ )
387
+
388
+ btn_generate = gr.Button(
389
+ "✨ Generate Caption",
390
+ variant="primary",
391
+ size="lg",
392
+ elem_id="btn-generate",
393
+ )
394
+
395
+ # ── KOLOM KANAN: OUTPUT ──
396
+ with gr.Column(scale=1):
397
+ gr.Markdown("### 📋 Hasil Caption")
398
+ gr.Markdown(
399
+ '<span style="font-size:12px;color:#999;">'
400
+ 'Klik ikon 📋 di pojok kanan untuk copy</span>'
401
+ )
402
+
403
+ caption_boxes = []
404
+ for i in range(5):
405
+ box = gr.Textbox(
406
+ label=f"Caption {i + 1}",
407
+ lines=4,
408
+ max_lines=8,
409
+ show_copy_button=True,
410
+ interactive=False,
411
+ visible=(i < 3),
412
+ elem_classes=["caption-output"],
413
+ )
414
+ caption_boxes.append(box)
415
+
416
+ # Visibility update saat jumlah berubah
417
+ jumlah.change(
418
+ fn=update_visibility,
419
+ inputs=[jumlah],
420
+ outputs=caption_boxes,
421
+ )
422
+
423
+ # Generate caption
424
+ btn_generate.click(
425
+ fn=generate_caption,
426
+ inputs=[konteks, platform, gaya, bahasa, jumlah],
427
+ outputs=caption_boxes,
428
+ )
429
+
430
+ # ── FOOTER ──
431
+ gr.Markdown(
432
+ '<div class="footer-text">'
433
+ "Dibuat dengan ❤️ untuk komunitas Indonesia · "
434
+ "Powered by Hugging Face Inference API"
435
+ "</div>"
436
+ )
437
+
438
+ # ── JALANKAN ──
439
+ demo.launch()