TheCoderScientist commited on
Commit
651fd71
·
verified ·
1 Parent(s): 36d70d7

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +122 -386
app.py CHANGED
@@ -1,7 +1,5 @@
1
  # ==============================================================================
2
- # 🦅 GarudaCoder-27B — Indonesian Coding Intelligence
3
- # Precision Engineered Interface (Hugging Face ZeroGPU Edition)
4
- # Fully Compatible with Gradio 6.x & ZeroGPU A100
5
  # ==============================================================================
6
 
7
  import os
@@ -28,6 +26,7 @@ from transformers import (
28
  TextIteratorStreamer,
29
  )
30
  from peft import PeftModel
 
31
 
32
  try:
33
  from transformers import AutoModelForMultimodalLM
@@ -37,9 +36,6 @@ except ImportError:
37
  except ImportError:
38
  from transformers import AutoModelForVision2Seq as AutoModelForMultimodalLM
39
 
40
- # ==============================================================================
41
- # 1. KONFIGURASI MODEL & PROMPT
42
- # ==============================================================================
43
  BASE_ID = "unsloth/Qwen3.8-27B-unsloth-bnb-4bit"
44
  ADAPTER_ID = "TheCoderScientist/GarudaCoder-27B-ID-lora"
45
  EOS_IDS = [248046, 248044]
@@ -53,15 +49,26 @@ Prioritas:
53
  - Jangan mengarang fakta, API, versi, angka, dokumentasi, atau hasil eksekusi.
54
  - Bila informasi tidak cukup, katakan apa yang kurang.
55
  - Gunakan konteks gambar, video, file, atau hasil web search yang diberikan.
56
- - Bedakan fakta dari inferensi.
57
- - Untuk informasi yang berubah atau bersifat lokal, gunakan sumber yang dapat diverifikasi.
58
- - Jangan menampilkan chain-of-thought privat.
59
- - Jangan mengklaim telah menjalankan kode jika memang belum dijalankan.
60
  - Identitas kamu adalah GarudaCoder.
61
  - Jika ditanya basis model, jelaskan secara ringkas bahwa GarudaCoder menggunakan Qwen3.8-27B sebagai base model dengan adapter GarudaCoder.
62
  """
63
 
64
- print("[Init] Menyiapkan Processor & Tokenizer...")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  processor = AutoProcessor.from_pretrained(ADAPTER_ID, trust_remote_code=True)
66
  tokenizer = getattr(processor, "tokenizer", processor)
67
 
@@ -71,10 +78,10 @@ if getattr(tokenizer, "pad_token", None) is None:
71
  _model = None
72
 
73
  def get_model():
74
- """ZeroGPU Lazy Loader: Muat model hanya saat inferensi diminta."""
75
  global _model
76
  if _model is None:
77
- print("[ZeroGPU] Memuat bobot 4-bit ke NVIDIA A100...")
78
  bnb_config = BitsAndBytesConfig(
79
  load_in_4bit=True,
80
  bnb_4bit_quant_type="nf4",
@@ -87,7 +94,7 @@ def get_model():
87
  attn_implementation="sdpa",
88
  trust_remote_code=True,
89
  )
90
- print("[ZeroGPU] Mengaktifkan adapter LoRA GarudaCoder...")
91
  _model = PeftModel.from_pretrained(
92
  base_model,
93
  ADAPTER_ID,
@@ -98,7 +105,7 @@ def get_model():
98
  return _model
99
 
100
  # ==============================================================================
101
- # 2. FILE & MULTIMODAL PARSERS
102
  # ==============================================================================
103
  IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".webp", ".bmp"}
104
  VIDEO_EXTENSIONS = {".mp4", ".mov", ".mkv", ".webm"}
@@ -117,92 +124,54 @@ def normalize_path(file_obj):
117
  return file_obj.get("path") or file_obj.get("name")
118
  return getattr(file_obj, "path", None) or getattr(file_obj, "name", None)
119
 
120
- def file_uri(path):
121
- return Path(os.path.abspath(path)).as_uri()
122
-
123
- def extract_text_file(path, max_chars=40000):
124
- try:
125
- with open(path, "r", encoding="utf-8", errors="ignore") as f:
126
- return f.read(max_chars)
127
- except Exception as e:
128
- return f"[Gagal membaca file teks: {e}]"
129
-
130
- def extract_pdf(path, max_chars=40000):
131
  try:
132
- from pypdf import PdfReader
133
- reader = PdfReader(path)
134
- chunks = []
135
- for page in reader.pages:
136
- txt = page.extract_text() or ""
137
- chunks.append(txt)
138
- if sum(len(x) for x in chunks) >= max_chars:
139
- break
140
- return "\n\n".join(chunks)[:max_chars]
141
- except Exception as e:
142
- return f"[Gagal membaca PDF: {e}]"
143
-
144
- def extract_docx(path, max_chars=40000):
145
- try:
146
- from docx import Document
147
- doc = Document(path)
148
- parts = [p.text for p in doc.paragraphs if p.text.strip()]
149
- return "\n".join(parts)[:max_chars]
150
- except Exception as e:
151
- return f"[Gagal membaca DOCX: {e}]"
152
-
153
- def extract_xlsx(path, max_chars=40000):
154
- try:
155
- from openpyxl import load_workbook
156
- wb = load_workbook(path, read_only=True, data_only=True)
157
- parts = []
158
- for ws in wb.worksheets:
159
- parts.append(f"## Sheet: {ws.title}")
160
- for row in ws.iter_rows(values_only=True):
161
- vals = ["" if v is None else str(v) for v in row]
162
- parts.append(" | ".join(vals))
163
- if sum(len(x) for x in parts) >= max_chars:
164
- break
165
- return "\n".join(parts)[:max_chars]
166
  except Exception as e:
167
- return f"[Gagal membaca XLSX: {e}]"
 
168
 
169
- def inspect_uploaded_files(files):
170
  vision_blocks = []
171
- text_parts = []
172
- summary = []
173
-
174
  for raw in files or []:
175
  path = normalize_path(raw)
176
  if not path or not os.path.exists(path):
177
  continue
178
-
179
  name = os.path.basename(path)
180
  ext = Path(path).suffix.lower()
 
181
 
182
  if ext in IMAGE_EXTENSIONS:
183
- vision_blocks.append({"type": "image", "image": file_uri(path)})
184
- summary.append(name)
185
  elif ext in VIDEO_EXTENSIONS:
186
- vision_blocks.append({"type": "video", "video": file_uri(path), "fps": 1.0})
187
- summary.append(name)
188
- elif ext in TEXT_EXTENSIONS:
189
- text_parts.append(f"### [FILE: {name}]\n```text\n{extract_text_file(path)}\n```")
190
- summary.append(name)
191
- elif ext == ".pdf":
192
- text_parts.append(f"### [PDF: {name}]\n{extract_pdf(path)}")
193
- summary.append(name)
194
- elif ext == ".docx":
195
- text_parts.append(f"### [DOCX: {name}]\n{extract_docx(path)}")
196
- summary.append(name)
197
- elif ext in {".xlsx", ".xlsm"}:
198
- text_parts.append(f"### [SPREADSHEET: {name}]\n{extract_xlsx(path)}")
199
- summary.append(name)
200
 
201
- return vision_blocks, "\n\n".join(text_parts), summary
202
-
203
- # ==============================================================================
204
- # 3. FAST WEB SEARCH (DUCKDUCKGO)
205
- # ==============================================================================
206
  def web_search(query, max_results=4):
207
  query = (query or "").strip()
208
  if not query:
@@ -213,41 +182,16 @@ def web_search(query, max_results=4):
213
  with DDGS() as ddgs:
214
  for r in ddgs.text(query, max_results=max_results, region="wt-wt", safesearch="moderate"):
215
  results.append({
216
- "title": r.get("title") or "Dokumentasi",
217
  "url": r.get("href") or "",
218
  "snippet": r.get("body") or "",
219
  })
220
  return results
221
- except Exception as e:
222
- print(f"[Web Search Warn] {e}")
223
  return []
224
 
225
- def format_search_context(results):
226
- if not results:
227
- return ""
228
- chunks = ["## HASIL DOKUMENTASI/WEB TERBARU:"]
229
- for i, r in enumerate(results, 1):
230
- chunks.append(f"[{i}] {r['title']} ({r['url']})\nSnippet: {r['snippet']}")
231
- return "\n\n".join(chunks)
232
-
233
- def sources_html(results):
234
- if not results:
235
- return '<div class="sources-empty">Pencarian web nonaktif atau belum ada kueri.</div>'
236
- chips = []
237
- for i, r in enumerate(results, 1):
238
- title = gr.utils.sanitize_html(r["title"][:75])
239
- url = r["url"]
240
- domain = re.sub(r"^https?://(www\.)?", "", url).split("/")[0]
241
- chips.append(
242
- f'<a class="source-chip" href="{url}" target="_blank" title="{title}">'
243
- f'<span class="source-domain">{domain}</span>'
244
- f'<span class="source-title">{title}</span>'
245
- f'</a>'
246
- )
247
- return f'<div class="sources-grid">{"".join(chips)}</div>'
248
-
249
  # ==============================================================================
250
- # 4. CHAT GENERATION ENGINE (ZEROGPU A100)
251
  # ==============================================================================
252
  @spaces.GPU(duration=120)
253
  def generate_chat(
@@ -265,13 +209,20 @@ def generate_chat(
265
  user_text = (message or {}).get("text", "").strip()
266
  files = (message or {}).get("files", [])
267
 
268
- vision_blocks, file_context, file_names = inspect_uploaded_files(files)
269
 
270
  search_results = []
271
  if enable_web_search and user_text:
272
  search_results = web_search(user_text, max_results=4)
273
- search_context = format_search_context(search_results)
274
 
 
 
 
 
 
 
 
 
275
  sys_content = system_prompt.strip() if system_prompt else SYSTEM_PROMPT
276
  messages = [{"role": "system", "content": sys_content}]
277
 
@@ -281,24 +232,24 @@ def generate_chat(
281
  if role in {"user", "assistant"} and isinstance(content, str):
282
  messages.append({"role": role, "content": content})
283
 
284
- current_turn_content = []
285
- current_turn_content.extend(vision_blocks)
286
 
287
- context_sections = []
288
  if file_context:
289
- context_sections.append(file_context)
290
  if search_context:
291
- context_sections.append(search_context)
292
 
293
- if context_sections:
294
- full_text = "\n\n".join(context_sections) + (f"\n\n## INSTRUKSI USER:\n{user_text}" if user_text else "")
295
- current_turn_content.append({"type": "text", "text": full_text})
296
  elif user_text:
297
- current_turn_content.append({"type": "text", "text": user_text})
298
  elif not vision_blocks:
299
- current_turn_content.append({"type": "text", "text": "Halo!"})
300
 
301
- messages.append({"role": "user", "content": current_turn_content})
302
 
303
  try:
304
  model_inputs = processor.apply_chat_template(
@@ -350,312 +301,97 @@ def generate_chat(
350
  thread.start()
351
 
352
  badges = []
353
- if file_names:
354
- badges.append(f"Attachment: {', '.join(file_names[:2])}{'...' if len(file_names) > 2 else ''}")
355
  if search_results:
356
- badges.append(f"Web Search: {len(search_results)} rujukan")
 
357
 
358
- meta_tag = f'<div class="context-tag">{" · ".join(badges)}</div>\n\n' if badges else ""
359
  output = ""
360
-
361
  for chunk in streamer:
362
  output += chunk
363
- yield meta_tag + output, search_results
364
 
365
  thread.join()
366
-
367
  if errors:
368
- yield output + f"\n\n> ⚠️ **Runtime Issue:** `{errors[0]}`", search_results
369
 
370
  # ==============================================================================
371
- # 5. MODERN UI SYSTEM (LINEAR / CURSOR AESTHETICS)
372
  # ==============================================================================
373
- WELCOME_MD = """### Siap membantu audit kode dan arsitektur sistem.
 
374
 
375
- Pilih topik di bawah atau lampirkan file dokumen/gambar langsung:
376
  """
377
 
378
- CSS = r"""
379
- @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
380
-
381
- :root {
382
- --bg-base: #0a0c10;
383
- --bg-surface: #11141b;
384
- --bg-subtle: #181d26;
385
- --border-subtle: rgba(255, 255, 255, 0.07);
386
- --border-active: rgba(255, 255, 255, 0.15);
387
- --text-primary: #f0f3f6;
388
- --text-secondary: #8c96a5;
389
- --text-muted: #566171;
390
- --accent: #10b981;
391
- --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
392
- --font-mono: 'JetBrains Mono', monospace;
393
- }
394
-
395
- body, .gradio-container {
396
- background-color: var(--bg-base) !important;
397
- color: var(--text-primary) !important;
398
- font-family: var(--font-sans) !important;
399
- max-width: 1380px !important;
400
- margin: 0 auto !important;
401
- }
402
-
403
- /* Header Navbar */
404
- .app-nav {
405
- display: flex;
406
- justify-content: space-between;
407
- align-items: center;
408
- padding: 18px 0;
409
- border-bottom: 1px solid var(--border-subtle);
410
- margin-bottom: 16px;
411
- }
412
- .brand-group {
413
- display: flex;
414
- align-items: center;
415
- gap: 12px;
416
- }
417
- .brand-badge {
418
- background: var(--bg-subtle);
419
- border: 1px solid var(--border-subtle);
420
- border-radius: 8px;
421
- padding: 6px 9px;
422
- font-size: 14px;
423
- font-weight: 700;
424
- }
425
- .brand-name {
426
- font-size: 16px;
427
- font-weight: 600;
428
- letter-spacing: -0.02em;
429
- color: var(--text-primary);
430
- }
431
- .brand-desc {
432
- font-size: 12px;
433
- color: var(--text-secondary);
434
- }
435
- .status-pill {
436
- display: inline-flex;
437
- align-items: center;
438
- gap: 6px;
439
- font-size: 11px;
440
- font-family: var(--font-mono);
441
- color: var(--text-secondary);
442
- background: var(--bg-surface);
443
- border: 1px solid var(--border-subtle);
444
- padding: 4px 10px;
445
- border-radius: 9999px;
446
- }
447
- .status-dot {
448
- width: 6px;
449
- height: 6px;
450
- border-radius: 50%;
451
- background-color: var(--accent);
452
- }
453
-
454
- /* Chatbox Area */
455
- .chat-viewport {
456
- background: var(--bg-surface) !important;
457
- border: 1px solid var(--border-subtle) !important;
458
- border-radius: 12px !important;
459
- }
460
-
461
- /* Source Reference Chips (Perplexity Style) */
462
- .sources-grid {
463
- display: flex;
464
- flex-wrap: wrap;
465
- gap: 8px;
466
- padding: 4px 0;
467
- }
468
- .source-chip {
469
- display: inline-flex;
470
- flex-direction: column;
471
- background: var(--bg-subtle);
472
- border: 1px solid var(--border-subtle);
473
- border-radius: 8px;
474
- padding: 6px 10px;
475
- text-decoration: none !important;
476
- transition: border-color 0.15s ease, background 0.15s ease;
477
- max-width: 100%;
478
- }
479
- .source-chip:hover {
480
- border-color: var(--border-active);
481
- background: #1e2430;
482
- }
483
- .source-domain {
484
- font-size: 10px;
485
- font-family: var(--font-mono);
486
- color: var(--accent);
487
- text-transform: uppercase;
488
- letter-spacing: 0.04em;
489
- }
490
- .source-title {
491
- font-size: 11px;
492
- font-weight: 500;
493
- color: var(--text-primary) !important;
494
- white-space: nowrap;
495
- overflow: hidden;
496
- text-overflow: ellipsis;
497
- max-width: 240px;
498
- }
499
- .sources-empty {
500
- font-size: 12px;
501
- color: var(--text-muted);
502
- font-style: italic;
503
- }
504
-
505
- /* Context Badge in Response */
506
- .context-tag {
507
- display: inline-block;
508
- font-family: var(--font-mono);
509
- font-size: 10px;
510
- color: var(--text-secondary);
511
- background: var(--bg-subtle);
512
- border: 1px solid var(--border-subtle);
513
- border-radius: 4px;
514
- padding: 3px 6px;
515
- margin-bottom: 6px;
516
- }
517
-
518
- /* Prompt Suggestion Chips */
519
- .quick-prompt-row {
520
- display: flex;
521
- gap: 8px;
522
- margin-bottom: 12px;
523
- flex-wrap: wrap;
524
- }
525
- .quick-prompt-btn {
526
- background: var(--bg-surface) !important;
527
- border: 1px solid var(--border-subtle) !important;
528
- color: var(--text-secondary) !important;
529
- font-size: 11px !important;
530
- border-radius: 20px !important;
531
- padding: 4px 12px !important;
532
- cursor: pointer !important;
533
- font-weight: 400 !important;
534
- transition: all 0.15s ease !important;
535
- }
536
- .quick-prompt-btn:hover {
537
- color: var(--text-primary) !important;
538
- border-color: var(--border-active) !important;
539
- background: var(--bg-subtle) !important;
540
- }
541
-
542
- /* Sidebar Box Minimalist */
543
- .sidebar-panel {
544
- background: var(--bg-surface) !important;
545
- border: 1px solid var(--border-subtle) !important;
546
- border-radius: 12px !important;
547
- padding: 16px !important;
548
- }
549
- """
550
-
551
- # ==============================================================================
552
- # 6. GRADIO 6.x COMPATIBLE BLOCKS & LAUNCH
553
- # ==============================================================================
554
  with gr.Blocks(title="GarudaCoder-27B") as demo:
555
- # ------------------ TOP NAV BAR ------------------
556
- gr.HTML("""
557
- <div class="app-nav">
558
- <div class="brand-group">
559
- <div class="brand-badge">🦅 GC-27B</div>
560
- <div>
561
- <div class="brand-name">GarudaCoder Workspace</div>
562
- <div class="brand-desc">Indonesian Specialized Coding Intelligence · Qwen3.8 Architecture</div>
563
- </div>
564
- </div>
565
- <div class="status-pill">
566
- <span class="status-dot"></span>
567
- <span>ZeroGPU A100 · 4-Bit Native</span>
568
- </div>
569
- </div>
570
- """)
571
-
572
  with gr.Row():
573
- # ------------------ SETTINGS / SIDEBAR ------------------
574
- with gr.Column(scale=1, min_width=270):
575
- with gr.Group(elem_classes="sidebar-panel"):
576
- gr.Markdown("#### Runtime Controls")
577
- enable_thinking = gr.Checkbox(value=False, label="Deep Reasoning (CoT)")
578
- enable_web_search = gr.Checkbox(value=False, label="Live Web Context (DDG)")
579
-
580
- with gr.Accordion("Sampling & Temperature", open=False):
581
- temperature = gr.Slider(0.0, 1.5, value=0.6, step=0.05, label="Temperature")
582
- top_p = gr.Slider(0.1, 1.0, value=0.85, step=0.05, label="Top-P")
583
- max_new_tokens = gr.Slider(128, 4096, value=1024, step=64, label="Max Tokens")
584
-
585
- with gr.Accordion("System Instructions", open=False):
586
- system_prompt = gr.Textbox(value=SYSTEM_PROMPT, label="Prompt", lines=6, show_label=False)
587
-
588
- with gr.Group(elem_classes="sidebar-panel", visible=True):
589
- gr.Markdown("#### Web References")
590
- sources_box = gr.HTML('<div class="sources-empty">Pencarian web nonaktif.</div>')
591
-
592
- # ------------------ CHAT WORKSPACE ------------------
593
  with gr.Column(scale=3):
594
- # Pada Gradio 6, type="messages" dihapus karena sudah jadi format standar bawaan
595
  chatbot = gr.Chatbot(
596
  value=[{"role": "assistant", "content": WELCOME_MD}],
597
- height=620,
598
  show_label=False,
599
- elem_classes="chat-viewport"
600
  )
601
 
602
- # Quick Prompt Suggestions (Interactive Pills)
603
- with gr.Row(elem_classes="quick-prompt-row"):
604
- p1 = gr.Button("Memory leak pada Python generator?", elem_classes="quick-prompt-btn")
605
- p2 = gr.Button("Arsitektur Microservices vs Modular Monolith", elem_classes="quick-prompt-btn")
606
- p3 = gr.Button("Optimasi SQL Query (N+1 Problem)", elem_classes="quick-prompt-btn")
607
 
608
  message = gr.MultimodalTextbox(
609
- placeholder="Tanyakan analisis kode, upload dokumen teknis (PDF/Docx/Sheet), atau screenshot error...",
610
  file_types=["image", "video", "file"],
611
  file_count="multiple",
612
- submit_btn="Send",
613
- stop_btn="Stop",
 
614
  )
615
 
616
- # ------------------ INTERAKSI & EVENTS ------------------
617
- def set_prompt_p1():
618
- return {"text": "Bagaimana cara mendeteksi dan mencegah memory leak pada penggunaan generator & closure di Python?", "files": []}
619
- def set_prompt_p2():
620
- return {"text": "Kapan sebaiknya beralih dari Modular Monolith ke Microservices? Berikan trade-off engineeringnya.", "files": []}
621
- def set_prompt_p3():
622
- return {"text": "Jelaskan apa itu masalah N+1 Query pada ORM dan 3 strategi konkret untuk mengatasinya.", "files": []}
623
-
624
- p1.click(set_prompt_p1, outputs=[message])
625
- p2.click(set_prompt_p2, outputs=[message])
626
- p3.click(set_prompt_p3, outputs=[message])
627
 
628
  def stream_handler(msg, hist, sp, temp, tp, mnt, eth, ews):
629
  user_text = (msg or {}).get("text", "")
630
  files = (msg or {}).get("files", [])
631
 
632
- user_content_display = []
633
  for f in files:
634
  path = normalize_path(f)
635
  if path:
636
- user_content_display.append({"path": path})
637
  if user_text:
638
- user_content_display.append(user_text)
639
 
640
  new_history = list(hist or [])
641
- new_history.append({"role": "user", "content": user_content_display if user_content_display else user_text})
642
  new_history.append({"role": "assistant", "content": ""})
643
 
644
- last_search = []
645
- for partial_text, search_res in generate_chat(msg, hist, sp, temp, tp, mnt, eth, ews):
646
  new_history[-1]["content"] = partial_text
647
- last_search = search_res
648
- yield new_history, sources_html(last_search)
649
 
650
  message.submit(
651
  stream_handler,
652
  inputs=[message, chatbot, system_prompt, temperature, top_p, max_new_tokens, enable_thinking, enable_web_search],
653
- outputs=[chatbot, sources_box]
654
  )
655
 
656
- # launch() di Gradio 6 menerima parameter theme dan css langsung di sini
657
  if __name__ == "__main__":
658
  demo.queue(max_size=16, default_concurrency_limit=1).launch(
659
- theme=gr.themes.Base(),
660
- css=CSS
661
  )
 
1
  # ==============================================================================
2
+ # 🦅 GarudaCoder-27B — Workspace (Native Gradio 6 + ZeroGPU A100)
 
 
3
  # ==============================================================================
4
 
5
  import os
 
26
  TextIteratorStreamer,
27
  )
28
  from peft import PeftModel
29
+ from huggingface_hub import snapshot_download
30
 
31
  try:
32
  from transformers import AutoModelForMultimodalLM
 
36
  except ImportError:
37
  from transformers import AutoModelForVision2Seq as AutoModelForMultimodalLM
38
 
 
 
 
39
  BASE_ID = "unsloth/Qwen3.8-27B-unsloth-bnb-4bit"
40
  ADAPTER_ID = "TheCoderScientist/GarudaCoder-27B-ID-lora"
41
  EOS_IDS = [248046, 248044]
 
49
  - Jangan mengarang fakta, API, versi, angka, dokumentasi, atau hasil eksekusi.
50
  - Bila informasi tidak cukup, katakan apa yang kurang.
51
  - Gunakan konteks gambar, video, file, atau hasil web search yang diberikan.
 
 
 
 
52
  - Identitas kamu adalah GarudaCoder.
53
  - Jika ditanya basis model, jelaskan secara ringkas bahwa GarudaCoder menggunakan Qwen3.8-27B sebagai base model dengan adapter GarudaCoder.
54
  """
55
 
56
+ # ==============================================================================
57
+ # 1. PRE-DOWNLOAD BOBOT MODEL KE DISK (DI STARTUP, BUKAN PAS USER CHAT)
58
+ # ==============================================================================
59
+ print("\n" + "=" * 80)
60
+ print("[Startup] Memastikan bobot model 22.3 GB terunduh di disk...")
61
+ print("=" * 80)
62
+
63
+ # Unduh Base Model dan Adapter langsung saat container boot
64
+ snapshot_download(
65
+ repo_id=BASE_ID,
66
+ allow_patterns=["*.json", "*.safetensors", "*.model", "*.jinja"],
67
+ )
68
+ snapshot_download(repo_id=ADAPTER_ID)
69
+ print("[Startup] Semua bobot model siap di disk lokal.\n")
70
+
71
+ print("[Startup] Memuat Processor & Tokenizer...")
72
  processor = AutoProcessor.from_pretrained(ADAPTER_ID, trust_remote_code=True)
73
  tokenizer = getattr(processor, "tokenizer", processor)
74
 
 
78
  _model = None
79
 
80
  def get_model():
81
+ """Memindahkan bobot dari disk lokal ke NVIDIA A100 ZeroGPU."""
82
  global _model
83
  if _model is None:
84
+ print("[ZeroGPU] Memuat bobot 4-bit ke VRAM A100...")
85
  bnb_config = BitsAndBytesConfig(
86
  load_in_4bit=True,
87
  bnb_4bit_quant_type="nf4",
 
94
  attn_implementation="sdpa",
95
  trust_remote_code=True,
96
  )
97
+ print("[ZeroGPU] Menempelkan adapter LoRA...")
98
  _model = PeftModel.from_pretrained(
99
  base_model,
100
  ADAPTER_ID,
 
105
  return _model
106
 
107
  # ==============================================================================
108
+ # 2. FILE EXTRACTOR & WEB SEARCH
109
  # ==============================================================================
110
  IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".webp", ".bmp"}
111
  VIDEO_EXTENSIONS = {".mp4", ".mov", ".mkv", ".webm"}
 
124
  return file_obj.get("path") or file_obj.get("name")
125
  return getattr(file_obj, "path", None) or getattr(file_obj, "name", None)
126
 
127
+ def extract_text(path, max_chars=40000):
 
 
 
 
 
 
 
 
 
 
128
  try:
129
+ ext = Path(path).suffix.lower()
130
+ if ext in TEXT_EXTENSIONS:
131
+ with open(path, "r", encoding="utf-8", errors="ignore") as f:
132
+ return f.read(max_chars)
133
+ elif ext == ".pdf":
134
+ from pypdf import PdfReader
135
+ reader = PdfReader(path)
136
+ return "\n\n".join([page.extract_text() or "" for page in reader.pages])[:max_chars]
137
+ elif ext == ".docx":
138
+ from docx import Document
139
+ doc = Document(path)
140
+ return "\n".join([p.text for p in doc.paragraphs if p.text.strip()])[:max_chars]
141
+ elif ext in {".xlsx", ".xlsm"}:
142
+ from openpyxl import load_workbook
143
+ wb = load_workbook(path, read_only=True, data_only=True)
144
+ rows = []
145
+ for ws in wb.worksheets:
146
+ for r in ws.iter_rows(values_only=True):
147
+ rows.append(" | ".join(["" if v is None else str(v) for v in r]))
148
+ return "\n".join(rows)[:max_chars]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
  except Exception as e:
150
+ return f"[Gagal membaca lampiran: {e}]"
151
+ return ""
152
 
153
+ def inspect_files(files):
154
  vision_blocks = []
155
+ text_blocks = []
156
+ filenames = []
 
157
  for raw in files or []:
158
  path = normalize_path(raw)
159
  if not path or not os.path.exists(path):
160
  continue
 
161
  name = os.path.basename(path)
162
  ext = Path(path).suffix.lower()
163
+ filenames.append(name)
164
 
165
  if ext in IMAGE_EXTENSIONS:
166
+ vision_blocks.append({"type": "image", "image": Path(os.path.abspath(path)).as_uri()})
 
167
  elif ext in VIDEO_EXTENSIONS:
168
+ vision_blocks.append({"type": "video", "video": Path(os.path.abspath(path)).as_uri(), "fps": 1.0})
169
+ else:
170
+ t = extract_text(path)
171
+ if t:
172
+ text_blocks.append(f"### [FILE: {name}]\n```\n{t}\n```")
173
+ return vision_blocks, "\n\n".join(text_blocks), filenames
 
 
 
 
 
 
 
 
174
 
 
 
 
 
 
175
  def web_search(query, max_results=4):
176
  query = (query or "").strip()
177
  if not query:
 
182
  with DDGS() as ddgs:
183
  for r in ddgs.text(query, max_results=max_results, region="wt-wt", safesearch="moderate"):
184
  results.append({
185
+ "title": r.get("title") or "Rujukan",
186
  "url": r.get("href") or "",
187
  "snippet": r.get("body") or "",
188
  })
189
  return results
190
+ except Exception:
 
191
  return []
192
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
193
  # ==============================================================================
194
+ # 3. CHAT GENERATION (ZEROGPU)
195
  # ==============================================================================
196
  @spaces.GPU(duration=120)
197
  def generate_chat(
 
209
  user_text = (message or {}).get("text", "").strip()
210
  files = (message or {}).get("files", [])
211
 
212
+ vision_blocks, file_context, filenames = inspect_files(files)
213
 
214
  search_results = []
215
  if enable_web_search and user_text:
216
  search_results = web_search(user_text, max_results=4)
 
217
 
218
+ search_context = ""
219
+ if search_results:
220
+ chunks = ["## HASIL PENCARIAN TERBARU:"]
221
+ for i, r in enumerate(search_results, 1):
222
+ chunks.append(f"[{i}] {r['title']} ({r['url']})\nSnippet: {r['snippet']}")
223
+ search_context = "\n\n".join(chunks)
224
+
225
+ # Siapkan pesan
226
  sys_content = system_prompt.strip() if system_prompt else SYSTEM_PROMPT
227
  messages = [{"role": "system", "content": sys_content}]
228
 
 
232
  if role in {"user", "assistant"} and isinstance(content, str):
233
  messages.append({"role": role, "content": content})
234
 
235
+ current_turn = []
236
+ current_turn.extend(vision_blocks)
237
 
238
+ contexts = []
239
  if file_context:
240
+ contexts.append(file_context)
241
  if search_context:
242
+ contexts.append(search_context)
243
 
244
+ if contexts:
245
+ combined = "\n\n".join(contexts) + (f"\n\n## INSTRUKSI:\n{user_text}" if user_text else "")
246
+ current_turn.append({"type": "text", "text": combined})
247
  elif user_text:
248
+ current_turn.append({"type": "text", "text": user_text})
249
  elif not vision_blocks:
250
+ current_turn.append({"type": "text", "text": "Halo!"})
251
 
252
+ messages.append({"role": "user", "content": current_turn})
253
 
254
  try:
255
  model_inputs = processor.apply_chat_template(
 
301
  thread.start()
302
 
303
  badges = []
304
+ if filenames:
305
+ badges.append(f"📁 {len(filenames)} file")
306
  if search_results:
307
+ badges.append(f"🌐 {len(search_results)} sumber")
308
+ prefix = f"`{' · '.join(badges)}`\n\n" if badges else ""
309
 
 
310
  output = ""
 
311
  for chunk in streamer:
312
  output += chunk
313
+ yield prefix + output
314
 
315
  thread.join()
 
316
  if errors:
317
+ yield output + f"\n\n> ⚠️ **Error:** `{errors[0]}`"
318
 
319
  # ==============================================================================
320
+ # 4. TAMPILAN BERSIH GRADIO 6 (TANPA CSS GLITCH)
321
  # ==============================================================================
322
+ WELCOME_MD = """### 🦅 GarudaCoder-27B
323
+ Asisten coding berbahasa Indonesia berbasis **Qwen3.8-27B** di atas akselerator **NVIDIA A100**.
324
 
325
+ Mendukung analisis kode, diagram arsitektur, ekstraksi file dokumen/spreadsheet, dan pencarian web.
326
  """
327
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
328
  with gr.Blocks(title="GarudaCoder-27B") as demo:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
329
  with gr.Row():
330
+ with gr.Column(scale=1, min_width=260):
331
+ gr.Markdown("### ⚙️ Pengaturan")
332
+ enable_thinking = gr.Checkbox(value=False, label="🧠 Thinking Mode (CoT)")
333
+ enable_web_search = gr.Checkbox(value=False, label="🌐 Live Web Search")
334
+
335
+ with gr.Accordion("Parameter Generasi", open=False):
336
+ temperature = gr.Slider(0.0, 1.5, value=0.6, step=0.05, label="Temperature")
337
+ top_p = gr.Slider(0.1, 1.0, value=0.85, step=0.05, label="Top-P")
338
+ max_new_tokens = gr.Slider(128, 4096, value=1024, step=64, label="Max Tokens")
339
+
340
+ with gr.Accordion("Instruksi Sistem", open=False):
341
+ system_prompt = gr.Textbox(value=SYSTEM_PROMPT, lines=6, label="System Prompt")
342
+
 
 
 
 
 
 
 
343
  with gr.Column(scale=3):
 
344
  chatbot = gr.Chatbot(
345
  value=[{"role": "assistant", "content": WELCOME_MD}],
346
+ height=600,
347
  show_label=False,
 
348
  )
349
 
350
+ with gr.Row():
351
+ p1 = gr.Button("Memory leak pada Python?", size="sm")
352
+ p2 = gr.Button("Microservices vs Modular Monolith", size="sm")
353
+ p3 = gr.Button("Optimasi N+1 Query SQL", size="sm")
 
354
 
355
  message = gr.MultimodalTextbox(
356
+ placeholder="Ketik pertanyaan atau lampirkan gambar, video, dan file dokumen...",
357
  file_types=["image", "video", "file"],
358
  file_count="multiple",
359
+ show_label=False,
360
+ submit_btn="Kirim",
361
+ stop_btn="Batal",
362
  )
363
 
364
+ p1.click(lambda: {"text": "Bagaimana cara mendeteksi dan mencegah memory leak pada Python?", "files": []}, outputs=[message])
365
+ p2.click(lambda: {"text": "Kapan sebaiknya beralih dari Modular Monolith ke Microservices?", "files": []}, outputs=[message])
366
+ p3.click(lambda: {"text": "Jelaskan apa itu N+1 Query Problem dan cara mengatasinya.", "files": []}, outputs=[message])
 
 
 
 
 
 
 
 
367
 
368
  def stream_handler(msg, hist, sp, temp, tp, mnt, eth, ews):
369
  user_text = (msg or {}).get("text", "")
370
  files = (msg or {}).get("files", [])
371
 
372
+ user_content = []
373
  for f in files:
374
  path = normalize_path(f)
375
  if path:
376
+ user_content.append({"path": path})
377
  if user_text:
378
+ user_content.append(user_text)
379
 
380
  new_history = list(hist or [])
381
+ new_history.append({"role": "user", "content": user_content if user_content else user_text})
382
  new_history.append({"role": "assistant", "content": ""})
383
 
384
+ for partial_text in generate_chat(msg, hist, sp, temp, tp, mnt, eth, ews):
 
385
  new_history[-1]["content"] = partial_text
386
+ yield new_history
 
387
 
388
  message.submit(
389
  stream_handler,
390
  inputs=[message, chatbot, system_prompt, temperature, top_p, max_new_tokens, enable_thinking, enable_web_search],
391
+ outputs=[chatbot]
392
  )
393
 
 
394
  if __name__ == "__main__":
395
  demo.queue(max_size=16, default_concurrency_limit=1).launch(
396
+ theme=gr.themes.Soft(primary_hue="emerald", neutral_hue="slate")
 
397
  )