Spaces:
Sleeping
Sleeping
Upload 6 files
Browse files- .env.example +5 -2
- .gitignore +5 -1
- README.md +154 -20
- app.py +402 -469
- packages.txt +1 -0
- requirements.txt +4 -3
.env.example
CHANGED
|
@@ -1,3 +1,6 @@
|
|
| 1 |
-
|
| 2 |
-
|
|
|
|
|
|
|
|
|
|
| 3 |
NVIDIA_BASE_URL=https://integrate.api.nvidia.com/v1
|
|
|
|
| 1 |
+
# Jangan upload file .env asli ke Hugging Face publik.
|
| 2 |
+
# Di Hugging Face, masukkan NVIDIA_API_KEY sebagai Secret.
|
| 3 |
+
|
| 4 |
+
NVIDIA_API_KEY=isi_api_key_nvidia_kamu
|
| 5 |
+
NVIDIA_MODEL=qwen/qwen2.5-coder-32b-instruct
|
| 6 |
NVIDIA_BASE_URL=https://integrate.api.nvidia.com/v1
|
.gitignore
CHANGED
|
@@ -1,5 +1,9 @@
|
|
| 1 |
.env
|
| 2 |
__pycache__/
|
| 3 |
*.pyc
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
*.docx
|
| 5 |
-
*.
|
|
|
|
| 1 |
.env
|
| 2 |
__pycache__/
|
| 3 |
*.pyc
|
| 4 |
+
*.pyo
|
| 5 |
+
*.pyd
|
| 6 |
+
.ipynb_checkpoints/
|
| 7 |
+
.DS_Store
|
| 8 |
*.docx
|
| 9 |
+
*.zip
|
README.md
CHANGED
|
@@ -7,52 +7,186 @@ sdk: gradio
|
|
| 7 |
sdk_version: 4.44.1
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
|
|
|
| 10 |
---
|
| 11 |
|
| 12 |
# AI Assistant Mahasiswa NVIDIA
|
| 13 |
|
| 14 |
-
Aplikasi Hugging Face Spaces berbasis Gradio untuk:
|
| 15 |
|
| 16 |
-
1. Chat umum
|
| 17 |
-
2.
|
| 18 |
-
3.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
|
| 20 |
## Cara Upload ke Hugging Face Spaces
|
| 21 |
|
| 22 |
1. Buat Space baru di Hugging Face.
|
| 23 |
2. Pilih SDK: **Gradio**.
|
| 24 |
-
3.
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
|
| 31 |
```txt
|
| 32 |
NVIDIA_API_KEY=isi_api_key_nvidia_kamu
|
| 33 |
```
|
| 34 |
|
| 35 |
-
|
|
|
|
|
|
|
| 36 |
|
| 37 |
```txt
|
| 38 |
-
NVIDIA_MODEL=
|
| 39 |
NVIDIA_BASE_URL=https://integrate.api.nvidia.com/v1
|
| 40 |
```
|
| 41 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
## Cara Pakai
|
| 43 |
|
| 44 |
### Chat Umum
|
| 45 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
|
| 47 |
### Buat Laporan DOCX dari IPYNB
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
|
| 54 |
## Catatan Penting
|
| 55 |
|
| 56 |
-
- API key
|
| 57 |
-
- Simpan API key di Hugging Face Secret
|
|
|
|
|
|
|
| 58 |
- AI diarahkan agar tidak mengarang angka, akurasi, dataset, atau hasil evaluasi yang tidak ada di notebook.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
sdk_version: 4.44.1
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
+
python_version: 3.10
|
| 11 |
---
|
| 12 |
|
| 13 |
# AI Assistant Mahasiswa NVIDIA
|
| 14 |
|
| 15 |
+
Aplikasi Hugging Face Spaces berbasis Gradio untuk membantu mahasiswa dalam:
|
| 16 |
|
| 17 |
+
1. Chat umum dan tanya jawab materi kuliah.
|
| 18 |
+
2. Membantu tugas, coding, error Python, dan laporan akademik.
|
| 19 |
+
3. Membuat laporan Deep Learning otomatis dari file `.ipynb`.
|
| 20 |
+
4. Menghasilkan output laporan langsung dalam format Microsoft Word `.docx`.
|
| 21 |
+
|
| 22 |
+
## Fitur Utama
|
| 23 |
+
|
| 24 |
+
### 1. Chat Umum Mahasiswa
|
| 25 |
+
|
| 26 |
+
Mahasiswa dapat bertanya tentang:
|
| 27 |
+
|
| 28 |
+
- materi kuliah
|
| 29 |
+
- tugas
|
| 30 |
+
- coding dasar
|
| 31 |
+
- error Python
|
| 32 |
+
- Deep Learning
|
| 33 |
+
- Machine Learning
|
| 34 |
+
- laporan akademik
|
| 35 |
+
- pertanyaan umum lainnya
|
| 36 |
+
|
| 37 |
+
### 2. Generator Laporan Deep Learning dari IPYNB
|
| 38 |
+
|
| 39 |
+
Fitur ini memungkinkan mahasiswa untuk upload file notebook `.ipynb`, lalu sistem akan membaca isi kode dan output notebook untuk dibuatkan laporan otomatis.
|
| 40 |
+
|
| 41 |
+
Output laporan berupa file:
|
| 42 |
+
|
| 43 |
+
```txt
|
| 44 |
+
.docx
|
| 45 |
+
```
|
| 46 |
+
|
| 47 |
+
Jadi hasilnya bisa langsung dibuka dan diedit di Microsoft Word.
|
| 48 |
+
|
| 49 |
+
### 3. Format Laporan
|
| 50 |
+
|
| 51 |
+
Laporan dibuat dengan gaya laporan praktikum mahasiswa, yaitu:
|
| 52 |
+
|
| 53 |
+
- cover laporan
|
| 54 |
+
- judul laporan
|
| 55 |
+
- nama
|
| 56 |
+
- NIM
|
| 57 |
+
- dosen
|
| 58 |
+
- kelas
|
| 59 |
+
- nama anggota/partner opsional
|
| 60 |
+
- program studi
|
| 61 |
+
- kampus
|
| 62 |
+
- tahun
|
| 63 |
+
- bagian-bagian pembahasan
|
| 64 |
+
- kode notebook
|
| 65 |
+
- output notebook jika tersedia
|
| 66 |
+
- gambar/grafik output notebook jika tersimpan
|
| 67 |
+
- penjelasan setiap kode dengan format `Penjelasannya:`
|
| 68 |
+
|
| 69 |
+
### 4. Cek API
|
| 70 |
+
|
| 71 |
+
Tersedia tab **Cek API** untuk memastikan API key NVIDIA dan nama model sudah benar.
|
| 72 |
|
| 73 |
## Cara Upload ke Hugging Face Spaces
|
| 74 |
|
| 75 |
1. Buat Space baru di Hugging Face.
|
| 76 |
2. Pilih SDK: **Gradio**.
|
| 77 |
+
3. Pilih Hardware: **CPU Basic**.
|
| 78 |
+
4. Upload file berikut ke Space:
|
| 79 |
+
|
| 80 |
+
```txt
|
| 81 |
+
app.py
|
| 82 |
+
requirements.txt
|
| 83 |
+
README.md
|
| 84 |
+
```
|
| 85 |
+
|
| 86 |
+
5. Masuk ke menu:
|
| 87 |
+
|
| 88 |
+
```txt
|
| 89 |
+
Settings > Variables and secrets
|
| 90 |
+
```
|
| 91 |
+
|
| 92 |
+
6. Tambahkan Secret dan Variable sesuai bagian di bawah.
|
| 93 |
+
|
| 94 |
+
## Secret yang Wajib Dibuat
|
| 95 |
+
|
| 96 |
+
Tambahkan sebagai **Secret**, bukan Variable biasa:
|
| 97 |
|
| 98 |
```txt
|
| 99 |
NVIDIA_API_KEY=isi_api_key_nvidia_kamu
|
| 100 |
```
|
| 101 |
|
| 102 |
+
## Variable yang Disarankan
|
| 103 |
+
|
| 104 |
+
Tambahkan sebagai **Variable**:
|
| 105 |
|
| 106 |
```txt
|
| 107 |
+
NVIDIA_MODEL=qwen/qwen2.5-coder-32b-instruct
|
| 108 |
NVIDIA_BASE_URL=https://integrate.api.nvidia.com/v1
|
| 109 |
```
|
| 110 |
|
| 111 |
+
Model `qwen/qwen2.5-coder-32b-instruct` disarankan karena aplikasi ini banyak membaca kode Python dan file notebook `.ipynb`.
|
| 112 |
+
|
| 113 |
+
## Requirements
|
| 114 |
+
|
| 115 |
+
Pastikan file `requirements.txt` berisi:
|
| 116 |
+
|
| 117 |
+
```txt
|
| 118 |
+
gradio==4.44.1
|
| 119 |
+
openai>=1.30.0
|
| 120 |
+
nbformat>=5.10.4
|
| 121 |
+
python-docx>=1.1.2
|
| 122 |
+
pillow>=10.0.0
|
| 123 |
+
audioop-lts>=0.2.1
|
| 124 |
+
```
|
| 125 |
+
|
| 126 |
## Cara Pakai
|
| 127 |
|
| 128 |
### Chat Umum
|
| 129 |
+
|
| 130 |
+
1. Buka tab **Chat Umum**.
|
| 131 |
+
2. Ketik pertanyaan.
|
| 132 |
+
3. AI akan menjawab seperti asisten mahasiswa.
|
| 133 |
+
|
| 134 |
+
Contoh pertanyaan:
|
| 135 |
+
|
| 136 |
+
```txt
|
| 137 |
+
Jelaskan apa itu convolutional neural network.
|
| 138 |
+
```
|
| 139 |
+
|
| 140 |
+
```txt
|
| 141 |
+
Bantu saya buat rumusan masalah untuk laporan Deep Learning.
|
| 142 |
+
```
|
| 143 |
+
|
| 144 |
+
```txt
|
| 145 |
+
Kenapa kode Python saya error?
|
| 146 |
+
```
|
| 147 |
|
| 148 |
### Buat Laporan DOCX dari IPYNB
|
| 149 |
+
|
| 150 |
+
1. Jalankan notebook `.ipynb` terlebih dahulu agar output, grafik, dan hasil training tersimpan.
|
| 151 |
+
2. Buka tab **Buat Laporan DOCX dari IPYNB**.
|
| 152 |
+
3. Upload file `.ipynb`.
|
| 153 |
+
4. Isi data cover laporan:
|
| 154 |
+
- judul laporan
|
| 155 |
+
- nama
|
| 156 |
+
- NIM
|
| 157 |
+
- dosen
|
| 158 |
+
- kelas
|
| 159 |
+
- nama anggota/partner jika ada
|
| 160 |
+
- program studi
|
| 161 |
+
- kampus
|
| 162 |
+
- tahun
|
| 163 |
+
5. Klik tombol **Buat Laporan DOCX**.
|
| 164 |
+
6. Download file `.docx` yang muncul.
|
| 165 |
+
|
| 166 |
+
### Cek API
|
| 167 |
+
|
| 168 |
+
1. Buka tab **Cek API**.
|
| 169 |
+
2. Klik **Cek Koneksi NVIDIA API**.
|
| 170 |
+
3. Jika berhasil, akan muncul status bahwa API aktif.
|
| 171 |
|
| 172 |
## Catatan Penting
|
| 173 |
|
| 174 |
+
- Jangan menulis API key langsung di `app.py`.
|
| 175 |
+
- Simpan API key di Hugging Face Secret dengan nama `NVIDIA_API_KEY`.
|
| 176 |
+
- File `.ipynb` sebaiknya sudah dijalankan terlebih dahulu sebelum di-upload.
|
| 177 |
+
- Jika notebook belum dijalankan, output seperti akurasi, loss, grafik, dan hasil evaluasi mungkin tidak terbaca.
|
| 178 |
- AI diarahkan agar tidak mengarang angka, akurasi, dataset, atau hasil evaluasi yang tidak ada di notebook.
|
| 179 |
+
- Jika Hugging Face error karena Python 3.13, pastikan bagian atas README memiliki:
|
| 180 |
+
|
| 181 |
+
```yaml
|
| 182 |
+
python_version: 3.10
|
| 183 |
+
```
|
| 184 |
+
|
| 185 |
+
## Setelah Upload
|
| 186 |
+
|
| 187 |
+
Jika sudah upload semua file dan set Secret/Variable:
|
| 188 |
+
|
| 189 |
+
1. Klik **Restart Space**.
|
| 190 |
+
2. Jika masih error, klik **Factory Rebuild**.
|
| 191 |
+
3. Tunggu proses build selesai.
|
| 192 |
+
4. Aplikasi siap digunakan.
|
app.py
CHANGED
|
@@ -1,82 +1,106 @@
|
|
| 1 |
import base64
|
|
|
|
| 2 |
import json
|
| 3 |
import os
|
| 4 |
import re
|
| 5 |
import tempfile
|
| 6 |
-
import textwrap
|
| 7 |
-
import uuid
|
| 8 |
from pathlib import Path
|
|
|
|
| 9 |
|
| 10 |
import gradio as gr
|
| 11 |
import nbformat
|
| 12 |
-
from openai import OpenAI
|
| 13 |
from docx import Document
|
| 14 |
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
| 15 |
-
from docx.
|
| 16 |
-
from
|
| 17 |
-
from
|
| 18 |
-
|
| 19 |
|
| 20 |
-
# =========================
|
| 21 |
# KONFIGURASI NVIDIA API
|
| 22 |
-
# =========================
|
| 23 |
-
NVIDIA_API_KEY = os.getenv("NVIDIA_API_KEY", "")
|
| 24 |
-
|
| 25 |
-
|
| 26 |
|
| 27 |
client = OpenAI(
|
| 28 |
-
api_key=NVIDIA_API_KEY
|
| 29 |
base_url=NVIDIA_BASE_URL,
|
| 30 |
)
|
| 31 |
|
| 32 |
-
|
| 33 |
SYSTEM_PROMPT = """
|
| 34 |
Kamu adalah AI Assistant Mahasiswa.
|
| 35 |
|
| 36 |
-
Tugas utamamu
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
-
|
| 41 |
-
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
|
| 49 |
REPORT_STYLE_PROMPT = """
|
| 50 |
-
|
| 51 |
|
| 52 |
-
Format jawaban
|
| 53 |
{
|
| 54 |
-
"judul": "Judul
|
| 55 |
-
"penjelasan": "Penjelasannya:
|
| 56 |
}
|
| 57 |
|
| 58 |
-
|
| 59 |
-
-
|
| 60 |
-
-
|
| 61 |
-
-
|
| 62 |
-
- Jelaskan fungsi kode, tujuan tahap, dan arti output jika
|
| 63 |
-
- Jangan mengarang angka,
|
| 64 |
-
- Jika
|
| 65 |
-
|
|
|
|
| 66 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
|
| 68 |
-
# =========================
|
| 69 |
-
# UTILITAS UMUM
|
| 70 |
-
# =========================
|
| 71 |
-
def ensure_api_key():
|
| 72 |
-
if not NVIDIA_API_KEY:
|
| 73 |
-
raise gr.Error(
|
| 74 |
-
"NVIDIA_API_KEY belum diset. Buka Hugging Face Space > Settings > Variables and secrets > tambahkan secret NVIDIA_API_KEY."
|
| 75 |
-
)
|
| 76 |
|
|
|
|
|
|
|
|
|
|
| 77 |
|
| 78 |
-
def ask_nvidia(messages, temperature=0.35, max_tokens=900):
|
| 79 |
-
ensure_api_key()
|
| 80 |
response = client.chat.completions.create(
|
| 81 |
model=NVIDIA_MODEL,
|
| 82 |
messages=messages,
|
|
@@ -86,218 +110,210 @@ def ask_nvidia(messages, temperature=0.35, max_tokens=900):
|
|
| 86 |
return response.choices[0].message.content or ""
|
| 87 |
|
| 88 |
|
| 89 |
-
def
|
| 90 |
-
|
| 91 |
-
return ""
|
| 92 |
-
if isinstance(text, list):
|
| 93 |
-
text = "\n".join(str(x) for x in text)
|
| 94 |
-
text = str(text)
|
| 95 |
-
# hapus ANSI color code dari output notebook
|
| 96 |
-
text = re.sub(r"\x1b\[[0-9;]*m", "", text)
|
| 97 |
-
text = text.replace("\r\n", "\n").replace("\r", "\n")
|
| 98 |
-
return text.strip()
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
def limit_text(text, max_chars):
|
| 102 |
-
text = clean_text(text)
|
| 103 |
-
if len(text) <= max_chars:
|
| 104 |
-
return text
|
| 105 |
-
return text[:max_chars] + "\n... [dipotong agar tidak terlalu panjang]"
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
def sanitize_filename(name):
|
| 109 |
-
name = clean_text(name) or "laporan_deep_learning"
|
| 110 |
-
name = re.sub(r"[^a-zA-Z0-9_\- ]+", "", name)
|
| 111 |
-
name = re.sub(r"\s+", "_", name).strip("_")
|
| 112 |
-
return name[:80] or "laporan_deep_learning"
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
def parse_json_from_model(text):
|
| 116 |
-
text = clean_text(text)
|
| 117 |
try:
|
| 118 |
return json.loads(text)
|
| 119 |
except Exception:
|
| 120 |
pass
|
| 121 |
|
| 122 |
match = re.search(r"\{.*\}", text, flags=re.DOTALL)
|
| 123 |
-
if match:
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
return {
|
| 130 |
-
"judul": "Bagian Notebook",
|
| 131 |
-
"penjelasan": text if text.lower().startswith("penjelasannya") else f"Penjelasannya: {text}",
|
| 132 |
-
}
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
def guess_title_from_code(code, markdown_context=""):
|
| 136 |
-
joined = f"{markdown_context}\n{code}".lower()
|
| 137 |
-
if "import " in joined and ("tensorflow" in joined or "keras" in joined or "pandas" in joined):
|
| 138 |
-
return "Import Library"
|
| 139 |
-
if "read_csv" in joined or "os.listdir" in joined or "dataframe" in joined or "dataset" in joined:
|
| 140 |
-
return "Pembacaan Dataset"
|
| 141 |
-
if "train_test_split" in joined:
|
| 142 |
-
return "Split Dataset"
|
| 143 |
-
if "class_weight" in joined or "compute_class_weight" in joined:
|
| 144 |
-
return "Cek Distribusi & Class Weights"
|
| 145 |
-
if "imagedatagenerator" in joined or "augmentation" in joined:
|
| 146 |
-
return "Generator Inputan Data Augmentation"
|
| 147 |
-
if "flow_from_dataframe" in joined:
|
| 148 |
-
return "Flow Generator"
|
| 149 |
-
if "sequential" in joined or "conv2d" in joined or "mobilenet" in joined or "efficientnet" in joined or "resnet" in joined:
|
| 150 |
-
return "Arsitektur Model"
|
| 151 |
-
if "compile" in joined and "optimizer" in joined:
|
| 152 |
-
return "Kompilasi Model"
|
| 153 |
-
if "earlystopping" in joined or "reducelronplateau" in joined or "callback" in joined:
|
| 154 |
-
return "Callback Model"
|
| 155 |
-
if ".fit" in joined or "fit(" in joined or "training" in joined:
|
| 156 |
-
return "Training Model"
|
| 157 |
-
if "classification_report" in joined:
|
| 158 |
-
return "Classification Report"
|
| 159 |
-
if "confusion_matrix" in joined or "heatmap" in joined:
|
| 160 |
-
return "Confusion Matrix"
|
| 161 |
-
if "evaluate" in joined:
|
| 162 |
-
return "Evaluasi Model"
|
| 163 |
-
if "history" in joined and ("accuracy" in joined or "loss" in joined):
|
| 164 |
-
return "Visualisasi Loss & Accuracy"
|
| 165 |
-
if ".save" in joined or "save_model" in joined:
|
| 166 |
-
return "Simpan Model"
|
| 167 |
-
if "predict" in joined or "prediction" in joined:
|
| 168 |
-
return "Visualisasi Prediksi"
|
| 169 |
-
if "gradio" in joined or "interface.launch" in joined or "gr.interface" in joined:
|
| 170 |
-
return "Deployment Gradio"
|
| 171 |
-
return "Bagian Notebook"
|
| 172 |
|
| 173 |
|
| 174 |
-
# =========================
|
| 175 |
# CHAT UMUM
|
| 176 |
-
# =========================
|
| 177 |
-
def normal_chat(message, history):
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
messages = [{"role": "system", "content": SYSTEM_PROMPT}]
|
| 181 |
-
|
| 182 |
-
# Gradio ChatInterface umumnya mengirim history berbentuk list tuple: [(user, bot), ...]
|
| 183 |
-
for item in history or []:
|
| 184 |
-
if isinstance(item, (list, tuple)) and len(item) >= 2:
|
| 185 |
-
user_msg, assistant_msg = item[0], item[1]
|
| 186 |
-
if user_msg:
|
| 187 |
-
messages.append({"role": "user", "content": str(user_msg)})
|
| 188 |
-
if assistant_msg:
|
| 189 |
-
messages.append({"role": "assistant", "content": str(assistant_msg)})
|
| 190 |
-
elif isinstance(item, dict):
|
| 191 |
-
role = item.get("role")
|
| 192 |
-
content = item.get("content")
|
| 193 |
-
if role and content:
|
| 194 |
-
messages.append({"role": role, "content": str(content)})
|
| 195 |
-
|
| 196 |
-
messages.append({"role": "user", "content": message})
|
| 197 |
-
return ask_nvidia(messages, temperature=0.5, max_tokens=1200)
|
| 198 |
-
except Exception as e:
|
| 199 |
-
return f"Terjadi error: {e}"
|
| 200 |
-
|
| 201 |
|
| 202 |
-
|
| 203 |
-
# BACA NOTEBOOK IPYNB
|
| 204 |
-
# =========================
|
| 205 |
-
def extract_outputs(cell, temp_dir):
|
| 206 |
-
output_texts = []
|
| 207 |
-
image_paths = []
|
| 208 |
|
| 209 |
-
|
| 210 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 211 |
|
| 212 |
-
|
| 213 |
-
output_texts.append(clean_text(output.get("text", "")))
|
| 214 |
|
| 215 |
-
|
| 216 |
-
|
|
|
|
|
|
|
| 217 |
|
| 218 |
-
if "text/plain" in data:
|
| 219 |
-
output_texts.append(clean_text(data.get("text/plain", "")))
|
| 220 |
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
|
|
|
|
|
|
| 234 |
|
| 235 |
-
elif output_type == "error":
|
| 236 |
-
ename = output.get("ename", "Error")
|
| 237 |
-
evalue = output.get("evalue", "")
|
| 238 |
-
traceback = clean_text(output.get("traceback", []))
|
| 239 |
-
output_texts.append(clean_text(f"{ename}: {evalue}\n{traceback}"))
|
| 240 |
|
| 241 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 242 |
|
| 243 |
|
| 244 |
-
def read_ipynb(ipynb_path
|
|
|
|
| 245 |
notebook = nbformat.read(ipynb_path, as_version=4)
|
| 246 |
-
sections = []
|
| 247 |
last_markdown = ""
|
| 248 |
|
| 249 |
for cell in notebook.cells:
|
| 250 |
if cell.cell_type == "markdown":
|
| 251 |
-
|
| 252 |
-
if
|
| 253 |
-
last_markdown =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 254 |
continue
|
| 255 |
|
| 256 |
-
if cell.cell_type =
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 271 |
|
| 272 |
return sections
|
| 273 |
|
| 274 |
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 280 |
|
|
|
|
| 281 |
prompt = f"""
|
| 282 |
{REPORT_STYLE_PROMPT}
|
| 283 |
|
| 284 |
-
Konteks markdown
|
| 285 |
-
```
|
| 286 |
-
{
|
| 287 |
```
|
| 288 |
|
| 289 |
Kode notebook:
|
| 290 |
```python
|
| 291 |
-
{
|
| 292 |
```
|
| 293 |
|
| 294 |
Output notebook:
|
| 295 |
-
```
|
| 296 |
-
{
|
| 297 |
```
|
| 298 |
-
|
| 299 |
-
Jika judul bagian bisa ditebak, gunakan judul yang paling sesuai. Kandidat judul awal: {fallback_title}
|
| 300 |
-
""".strip()
|
| 301 |
|
| 302 |
try:
|
| 303 |
result = ask_nvidia(
|
|
@@ -306,339 +322,250 @@ Jika judul bagian bisa ditebak, gunakan judul yang paling sesuai. Kandidat judul
|
|
| 306 |
{"role": "user", "content": prompt},
|
| 307 |
],
|
| 308 |
temperature=0.25,
|
| 309 |
-
max_tokens=
|
| 310 |
)
|
| 311 |
data = parse_json_from_model(result)
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
return
|
| 319 |
except Exception:
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
def set_run_font(run, size=12, bold=False, italic=False, name="Times New Roman"):
|
| 327 |
-
run.font.name = name
|
| 328 |
-
run._element.rPr.rFonts.set(qn("w:eastAsia"), name)
|
| 329 |
-
run.font.size = Pt(size)
|
| 330 |
-
run.bold = bold
|
| 331 |
-
run.italic = italic
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
def set_cell_shading(cell, fill="F2F2F2"):
|
| 335 |
-
tc_pr = cell._tc.get_or_add_tcPr()
|
| 336 |
-
tc_pr.append(parse_xml(r'<w:shd {} w:fill="{}"/>'.format(nsdecls("w"), fill)))
|
| 337 |
|
| 338 |
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
tag.set(qn("w:sz"), "4")
|
| 347 |
-
tag.set(qn("w:space"), "0")
|
| 348 |
-
tag.set(qn("w:color"), "D9D9D9")
|
| 349 |
-
borders.append(tag)
|
| 350 |
-
tc_pr.append(borders)
|
| 351 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 352 |
|
| 353 |
-
def add_footer(section, kampus):
|
| 354 |
-
footer = section.footer
|
| 355 |
-
p = footer.paragraphs[0]
|
| 356 |
-
p.alignment = WD_ALIGN_PARAGRAPH.RIGHT
|
| 357 |
-
r = p.add_run(kampus if kampus else "AI Assistant Mahasiswa")
|
| 358 |
-
set_run_font(r, size=9, italic=True)
|
| 359 |
-
r.font.color.rgb = RGBColor(0, 102, 153)
|
| 360 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 361 |
|
| 362 |
-
def add_code_block(doc, text, max_chars=5000):
|
| 363 |
-
text = limit_text(text, max_chars)
|
| 364 |
-
if not text:
|
| 365 |
-
return
|
| 366 |
|
|
|
|
|
|
|
| 367 |
table = doc.add_table(rows=1, cols=1)
|
| 368 |
-
table.
|
| 369 |
cell = table.cell(0, 0)
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
p.paragraph_format.space_after = Pt(0)
|
| 375 |
-
p.paragraph_format.space_before = Pt(0)
|
| 376 |
|
| 377 |
-
# bagi supaya Word tidak berat ketika code panjang
|
| 378 |
-
for i, chunk in enumerate(textwrap.wrap(text, width=110, replace_whitespace=False, drop_whitespace=False)):
|
| 379 |
-
if i > 0:
|
| 380 |
-
p.add_run("\n")
|
| 381 |
-
run = p.add_run(chunk)
|
| 382 |
-
set_run_font(run, size=8, name="Courier New")
|
| 383 |
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
p.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
|
| 388 |
-
p.paragraph_format.space_after = Pt(6)
|
| 389 |
-
p.paragraph_format.line_spacing = 1.15
|
| 390 |
-
|
| 391 |
-
text = clean_text(text)
|
| 392 |
-
if bold_prefix and text.startswith(bold_prefix):
|
| 393 |
-
r1 = p.add_run(bold_prefix)
|
| 394 |
-
set_run_font(r1, bold=True)
|
| 395 |
-
r2 = p.add_run(text[len(bold_prefix):])
|
| 396 |
-
set_run_font(r2)
|
| 397 |
-
else:
|
| 398 |
-
r = p.add_run(text)
|
| 399 |
-
set_run_font(r)
|
| 400 |
-
return p
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
def add_heading_numbered(doc, number, title):
|
| 404 |
p = doc.add_paragraph()
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
set_run_font(run, size=13, bold=True)
|
| 409 |
-
return p
|
| 410 |
|
| 411 |
|
| 412 |
-
def
|
| 413 |
try:
|
| 414 |
p = doc.add_paragraph()
|
| 415 |
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
| 416 |
run = p.add_run()
|
| 417 |
-
run.add_picture(image_path, width=Inches(5.
|
| 418 |
except Exception:
|
| 419 |
-
#
|
| 420 |
pass
|
| 421 |
|
| 422 |
|
| 423 |
-
def
|
| 424 |
-
|
| 425 |
-
judul_laporan,
|
| 426 |
-
nama,
|
| 427 |
-
nim,
|
| 428 |
-
dosen,
|
| 429 |
-
kelas,
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
include_output=True,
|
| 435 |
-
progress=None,
|
| 436 |
):
|
| 437 |
-
doc =
|
| 438 |
-
|
| 439 |
-
section = doc.sections[0]
|
| 440 |
-
section.top_margin = Inches(0.9)
|
| 441 |
-
section.bottom_margin = Inches(0.8)
|
| 442 |
-
section.left_margin = Inches(1.1)
|
| 443 |
-
section.right_margin = Inches(1.0)
|
| 444 |
-
add_footer(section, kampus)
|
| 445 |
|
| 446 |
-
|
| 447 |
-
normal_style.font.name = "Times New Roman"
|
| 448 |
-
normal_style._element.rPr.rFonts.set(qn("w:eastAsia"), "Times New Roman")
|
| 449 |
-
normal_style.font.size = Pt(12)
|
| 450 |
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
| 454 |
-
r = p.add_run("Laporan Deep Learning")
|
| 455 |
-
set_run_font(r, size=16, bold=True)
|
| 456 |
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
nama,
|
| 466 |
-
f"NIM. {nim}" if nim else "NIM.",
|
| 467 |
-
"",
|
| 468 |
-
"Dosen Mata kuliah",
|
| 469 |
-
dosen,
|
| 470 |
-
"",
|
| 471 |
-
kelas,
|
| 472 |
-
]
|
| 473 |
-
for line in cover_lines:
|
| 474 |
-
p = doc.add_paragraph()
|
| 475 |
-
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
| 476 |
-
r = p.add_run(line or "")
|
| 477 |
-
set_run_font(r, size=12, bold=True)
|
| 478 |
|
| 479 |
doc.add_paragraph("\n\n")
|
| 480 |
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
r = p.add_run((line or "").upper())
|
| 485 |
-
set_run_font(r, size=12, bold=True)
|
| 486 |
|
| 487 |
doc.add_page_break()
|
| 488 |
|
| 489 |
-
# ISI LAPORAN
|
| 490 |
-
total = max(len(sections), 1)
|
| 491 |
-
for idx, item in enumerate(sections, start=1):
|
| 492 |
-
if progress:
|
| 493 |
-
progress((idx - 1) / total, desc=f"Membuat penjelasan bagian {idx}/{total}...")
|
| 494 |
-
|
| 495 |
-
title, explanation = summarize_code_section(
|
| 496 |
-
item["code"],
|
| 497 |
-
item.get("output_text", ""),
|
| 498 |
-
item.get("markdown_context", ""),
|
| 499 |
-
)
|
| 500 |
-
|
| 501 |
-
add_heading_numbered(doc, idx, title)
|
| 502 |
|
| 503 |
-
|
| 504 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 505 |
|
| 506 |
-
for image_path in item.get("image_paths", []):
|
| 507 |
-
add_image(doc, image_path)
|
| 508 |
|
| 509 |
-
if include_output and item.get("output_text"):
|
| 510 |
-
out_title = doc.add_paragraph()
|
| 511 |
-
r = out_title.add_run("Output:")
|
| 512 |
-
set_run_font(r, bold=True)
|
| 513 |
-
add_code_block(doc, item["output_text"], max_chars=2500)
|
| 514 |
-
|
| 515 |
-
add_normal_paragraph(doc, explanation, bold_prefix="Penjelasannya:")
|
| 516 |
-
|
| 517 |
-
if progress:
|
| 518 |
-
progress(1.0, desc="Menyimpan dokumen DOCX...")
|
| 519 |
-
|
| 520 |
-
filename = sanitize_filename(judul_laporan or "laporan_deep_learning") + ".docx"
|
| 521 |
-
output_path = str(Path(tempfile.gettempdir()) / filename)
|
| 522 |
-
doc.save(output_path)
|
| 523 |
-
return output_path
|
| 524 |
-
|
| 525 |
-
|
| 526 |
-
# =========================
|
| 527 |
-
# GRADIO FUNCTION: BUAT DOCX
|
| 528 |
-
# =========================
|
| 529 |
def create_report_docx(
|
| 530 |
-
ipynb_file,
|
| 531 |
-
judul_laporan,
|
| 532 |
-
nama,
|
| 533 |
-
nim,
|
| 534 |
-
dosen,
|
| 535 |
-
kelas,
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
|
| 539 |
-
|
| 540 |
-
include_output,
|
| 541 |
-
progress=gr.Progress(track_tqdm=False),
|
| 542 |
):
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
if ipynb_file is None:
|
| 546 |
-
raise gr.Error("Upload file .ipynb terlebih dahulu.")
|
| 547 |
|
| 548 |
-
ipynb_path =
|
| 549 |
if not ipynb_path.endswith(".ipynb"):
|
| 550 |
raise gr.Error("File harus berformat .ipynb")
|
| 551 |
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
|
|
|
|
|
|
|
| 555 |
|
| 556 |
-
|
| 557 |
-
|
|
|
|
| 558 |
|
| 559 |
-
|
| 560 |
-
|
| 561 |
judul_laporan=judul_laporan,
|
| 562 |
nama=nama,
|
| 563 |
nim=nim,
|
| 564 |
dosen=dosen,
|
| 565 |
kelas=kelas,
|
|
|
|
| 566 |
prodi=prodi,
|
| 567 |
kampus=kampus,
|
| 568 |
tahun=tahun,
|
| 569 |
-
include_code=include_code,
|
| 570 |
-
include_output=include_output,
|
| 571 |
-
progress=progress,
|
| 572 |
)
|
| 573 |
|
|
|
|
|
|
|
| 574 |
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
|
| 579 |
-
.
|
| 580 |
-
|
| 581 |
-
|
| 582 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 583 |
|
| 584 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 585 |
gr.Markdown(
|
| 586 |
-
""
|
| 587 |
-
# AI Assistant Mahasiswa
|
| 588 |
-
Chatbot untuk membantu laporan, tugas kuliah, materi belajar, coding dasar, dan pembuatan laporan Deep Learning dari file `.ipynb`.
|
| 589 |
-
"""
|
| 590 |
)
|
| 591 |
|
| 592 |
-
if not NVIDIA_API_KEY:
|
| 593 |
-
gr.Warning("NVIDIA_API_KEY belum diset. Tambahkan di Settings > Variables and secrets pada Hugging Face Space.")
|
| 594 |
-
|
| 595 |
with gr.Tab("Chat Umum"):
|
|
|
|
| 596 |
gr.ChatInterface(
|
| 597 |
fn=normal_chat,
|
| 598 |
-
title="Chat AI",
|
| 599 |
-
description="Tanya apa saja
|
| 600 |
-
examples=[
|
| 601 |
-
"Bantu buatkan kerangka laporan praktikum Deep Learning.",
|
| 602 |
-
"Jelaskan CNN dengan bahasa sederhana.",
|
| 603 |
-
"Perbaiki kalimat ini agar lebih formal untuk laporan.",
|
| 604 |
-
],
|
| 605 |
)
|
| 606 |
|
| 607 |
with gr.Tab("Buat Laporan DOCX dari IPYNB"):
|
| 608 |
gr.Markdown(
|
| 609 |
-
""
|
| 610 |
-
Upload file `.ipynb`, isi data cover, lalu klik **Buat Laporan DOCX**.
|
| 611 |
-
Output yang keluar langsung berupa file Word `.docx`.
|
| 612 |
-
"""
|
| 613 |
)
|
| 614 |
|
| 615 |
with gr.Row():
|
| 616 |
-
with gr.Column(
|
| 617 |
ipynb_file = gr.File(label="Upload File IPYNB", file_types=[".ipynb"])
|
| 618 |
-
|
| 619 |
judul_laporan = gr.Textbox(label="Judul Laporan", value="Klasifikasi Penyakit Mata")
|
| 620 |
nama = gr.Textbox(label="Nama", value="Hadid Zarid Nawfal")
|
| 621 |
nim = gr.Textbox(label="NIM", value="2355301079")
|
| 622 |
dosen = gr.Textbox(label="Dosen Mata Kuliah", value="Dr. Juni Nurma Sari, S.Kom., M.MT.")
|
| 623 |
kelas = gr.Textbox(label="Kelas", value="ILB")
|
|
|
|
| 624 |
prodi = gr.Textbox(label="Program Studi", value="Program Studi Teknik Informatika")
|
| 625 |
kampus = gr.Textbox(label="Kampus", value="Politeknik Caltex Riau")
|
| 626 |
tahun = gr.Textbox(label="Tahun", value="2025")
|
| 627 |
-
|
| 628 |
-
include_code = gr.Checkbox(label="Masukkan kode notebook ke laporan", value=True)
|
| 629 |
-
include_output = gr.Checkbox(label="Masukkan output teks/gambar notebook ke laporan", value=True)
|
| 630 |
-
|
| 631 |
btn = gr.Button("Buat Laporan DOCX", variant="primary")
|
| 632 |
|
| 633 |
-
with gr.Column(
|
| 634 |
output_file = gr.File(label="Download Laporan DOCX", file_types=[".docx"])
|
| 635 |
gr.Markdown(
|
| 636 |
-
""
|
| 637 |
-
### Catatan
|
| 638 |
-
- Jangan lupa jalankan notebook dulu sebelum di-upload agar output/grafik ikut tersimpan di file `.ipynb`.
|
| 639 |
-
- AI tidak akan mengarang angka atau hasil evaluasi yang tidak ada di notebook.
|
| 640 |
-
- Untuk API key, gunakan Secret bernama `NVIDIA_API_KEY`.
|
| 641 |
-
"""
|
| 642 |
)
|
| 643 |
|
| 644 |
btn.click(
|
|
@@ -650,15 +577,21 @@ Output yang keluar langsung berupa file Word `.docx`.
|
|
| 650 |
nim,
|
| 651 |
dosen,
|
| 652 |
kelas,
|
|
|
|
| 653 |
prodi,
|
| 654 |
kampus,
|
| 655 |
tahun,
|
| 656 |
-
include_code,
|
| 657 |
-
include_output,
|
| 658 |
],
|
| 659 |
outputs=output_file,
|
| 660 |
)
|
| 661 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 662 |
|
| 663 |
if __name__ == "__main__":
|
| 664 |
-
demo.launch()
|
|
|
|
| 1 |
import base64
|
| 2 |
+
import io
|
| 3 |
import json
|
| 4 |
import os
|
| 5 |
import re
|
| 6 |
import tempfile
|
|
|
|
|
|
|
| 7 |
from pathlib import Path
|
| 8 |
+
from typing import Any, Dict, List, Optional, Tuple
|
| 9 |
|
| 10 |
import gradio as gr
|
| 11 |
import nbformat
|
|
|
|
| 12 |
from docx import Document
|
| 13 |
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
| 14 |
+
from docx.shared import Inches, Pt
|
| 15 |
+
from openai import OpenAI
|
| 16 |
+
from PIL import Image
|
|
|
|
| 17 |
|
| 18 |
+
# ==========================================================
|
| 19 |
# KONFIGURASI NVIDIA API
|
| 20 |
+
# ==========================================================
|
| 21 |
+
NVIDIA_API_KEY = os.getenv("NVIDIA_API_KEY", "")
|
| 22 |
+
NVIDIA_MODEL = os.getenv("NVIDIA_MODEL", "qwen/qwen2.5-coder-32b-instruct")
|
| 23 |
+
NVIDIA_BASE_URL = os.getenv("NVIDIA_BASE_URL", "https://integrate.api.nvidia.com/v1")
|
| 24 |
|
| 25 |
client = OpenAI(
|
| 26 |
+
api_key=NVIDIA_API_KEY,
|
| 27 |
base_url=NVIDIA_BASE_URL,
|
| 28 |
)
|
| 29 |
|
|
|
|
| 30 |
SYSTEM_PROMPT = """
|
| 31 |
Kamu adalah AI Assistant Mahasiswa.
|
| 32 |
|
| 33 |
+
Tugas utamamu membantu mahasiswa dalam kegiatan akademik, terutama:
|
| 34 |
+
- menjelaskan materi kuliah,
|
| 35 |
+
- membantu coding dasar,
|
| 36 |
+
- membantu memahami error,
|
| 37 |
+
- membantu membuat laporan akademik,
|
| 38 |
+
- membantu membuat laporan Deep Learning dari kode notebook.
|
| 39 |
+
|
| 40 |
+
Gunakan bahasa Indonesia yang jelas, sopan, dan mudah dipahami.
|
| 41 |
+
Untuk kebutuhan laporan, gunakan bahasa formal seperti laporan mahasiswa.
|
| 42 |
+
|
| 43 |
+
Aturan penting:
|
| 44 |
+
- Jangan mengarang data, akurasi, loss, jumlah dataset, nama kelas, hasil evaluasi, atau referensi.
|
| 45 |
+
- Jika angka atau hasil tidak ada di notebook, jangan dibuat-buat.
|
| 46 |
+
- Jika memberi contoh, beri label sebagai contoh.
|
| 47 |
+
- Bantu mahasiswa memahami isi, bukan sekadar memberi jawaban untuk disalin mentah-mentah.
|
| 48 |
+
"""
|
| 49 |
|
| 50 |
REPORT_STYLE_PROMPT = """
|
| 51 |
+
Kamu membuat penjelasan laporan praktikum Deep Learning dari satu bagian kode notebook.
|
| 52 |
|
| 53 |
+
Format jawaban WAJIB JSON valid:
|
| 54 |
{
|
| 55 |
+
"judul": "Judul Bagian Singkat",
|
| 56 |
+
"penjelasan": "Penjelasannya: ..."
|
| 57 |
}
|
| 58 |
|
| 59 |
+
Gaya laporan:
|
| 60 |
+
- Bahasa Indonesia formal, seperti laporan mahasiswa.
|
| 61 |
+
- Judul bagian singkat dan relevan, misalnya Import Library, Pembacaan Dataset, Split Dataset, Arsitektur Model, Training Model, Evaluasi Model, Confusion Matrix, Deployment Gradio.
|
| 62 |
+
- Penjelasan diawali persis dengan: Penjelasannya:
|
| 63 |
+
- Jelaskan fungsi kode, tujuan tahap, dan arti output jika output tersedia.
|
| 64 |
+
- Jangan mengarang angka, akurasi, loss, dataset, nama kelas, atau hasil evaluasi yang tidak ada pada kode/output.
|
| 65 |
+
- Jika output kosong, cukup jelaskan fungsi kode dan tujuan tahap tersebut.
|
| 66 |
+
- Jangan membuat markdown, jangan membuat bullet panjang, dan jangan menambahkan teks di luar JSON.
|
| 67 |
+
"""
|
| 68 |
|
| 69 |
+
# ==========================================================
|
| 70 |
+
# HELPER UMUM
|
| 71 |
+
# ==========================================================
|
| 72 |
+
def get_uploaded_path(file_obj: Any) -> str:
|
| 73 |
+
"""Mengambil path file dari komponen gr.File."""
|
| 74 |
+
if file_obj is None:
|
| 75 |
+
raise gr.Error("File belum di-upload.")
|
| 76 |
+
if isinstance(file_obj, str):
|
| 77 |
+
return file_obj
|
| 78 |
+
if hasattr(file_obj, "name"):
|
| 79 |
+
return file_obj.name
|
| 80 |
+
if isinstance(file_obj, dict) and "path" in file_obj:
|
| 81 |
+
return file_obj["path"]
|
| 82 |
+
raise gr.Error("Format file upload tidak dikenali.")
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def safe_filename(text: str) -> str:
|
| 86 |
+
text = text.strip().lower()
|
| 87 |
+
text = re.sub(r"[^a-z0-9A-Z_-]+", "_", text)
|
| 88 |
+
text = re.sub(r"_+", "_", text).strip("_")
|
| 89 |
+
return text or "laporan_deep_learning"
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def truncate_text(text: str, limit: int) -> str:
|
| 93 |
+
if not text:
|
| 94 |
+
return ""
|
| 95 |
+
if len(text) <= limit:
|
| 96 |
+
return text
|
| 97 |
+
return text[:limit] + "\n... [dipotong karena terlalu panjang]"
|
| 98 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
|
| 100 |
+
def ask_nvidia(messages: List[Dict[str, str]], temperature: float = 0.4, max_tokens: int = 1200) -> str:
|
| 101 |
+
if not NVIDIA_API_KEY:
|
| 102 |
+
raise gr.Error("NVIDIA_API_KEY belum diset di Hugging Face Secrets.")
|
| 103 |
|
|
|
|
|
|
|
| 104 |
response = client.chat.completions.create(
|
| 105 |
model=NVIDIA_MODEL,
|
| 106 |
messages=messages,
|
|
|
|
| 110 |
return response.choices[0].message.content or ""
|
| 111 |
|
| 112 |
|
| 113 |
+
def parse_json_from_model(text: str) -> Optional[Dict[str, Any]]:
|
| 114 |
+
"""Mencoba mengambil JSON dari jawaban model."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
try:
|
| 116 |
return json.loads(text)
|
| 117 |
except Exception:
|
| 118 |
pass
|
| 119 |
|
| 120 |
match = re.search(r"\{.*\}", text, flags=re.DOTALL)
|
| 121 |
+
if not match:
|
| 122 |
+
return None
|
| 123 |
+
try:
|
| 124 |
+
return json.loads(match.group(0))
|
| 125 |
+
except Exception:
|
| 126 |
+
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 127 |
|
| 128 |
|
| 129 |
+
# ==========================================================
|
| 130 |
# CHAT UMUM
|
| 131 |
+
# ==========================================================
|
| 132 |
+
def normal_chat(message: str, history: Optional[List[Any]]):
|
| 133 |
+
if not NVIDIA_API_KEY:
|
| 134 |
+
return "Error: NVIDIA_API_KEY belum diset di Hugging Face Secrets."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
|
| 136 |
+
messages = [{"role": "system", "content": SYSTEM_PROMPT}]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
|
| 138 |
+
# Gradio 4 biasanya memberi history list tuple: [(user, assistant), ...]
|
| 139 |
+
for item in history or []:
|
| 140 |
+
if isinstance(item, (list, tuple)) and len(item) >= 2:
|
| 141 |
+
user_msg, assistant_msg = item[0], item[1]
|
| 142 |
+
if user_msg:
|
| 143 |
+
messages.append({"role": "user", "content": str(user_msg)})
|
| 144 |
+
if assistant_msg:
|
| 145 |
+
messages.append({"role": "assistant", "content": str(assistant_msg)})
|
| 146 |
|
| 147 |
+
messages.append({"role": "user", "content": message})
|
|
|
|
| 148 |
|
| 149 |
+
try:
|
| 150 |
+
return ask_nvidia(messages, temperature=0.5, max_tokens=1500)
|
| 151 |
+
except Exception as e:
|
| 152 |
+
return f"Terjadi error saat menghubungi NVIDIA API: {str(e)}"
|
| 153 |
|
|
|
|
|
|
|
| 154 |
|
| 155 |
+
def test_api_connection():
|
| 156 |
+
if not NVIDIA_API_KEY:
|
| 157 |
+
return "❌ NVIDIA_API_KEY belum diset di Hugging Face Secrets."
|
| 158 |
+
try:
|
| 159 |
+
result = ask_nvidia(
|
| 160 |
+
[
|
| 161 |
+
{"role": "system", "content": "Jawab singkat."},
|
| 162 |
+
{"role": "user", "content": "Balas hanya dengan kata: OK"},
|
| 163 |
+
],
|
| 164 |
+
temperature=0,
|
| 165 |
+
max_tokens=20,
|
| 166 |
+
)
|
| 167 |
+
return f"✅ API aktif. Model: {NVIDIA_MODEL}. Respons: {result.strip()}"
|
| 168 |
+
except Exception as e:
|
| 169 |
+
return f"❌ API error: {str(e)}"
|
| 170 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 171 |
|
| 172 |
+
# ==========================================================
|
| 173 |
+
# PEMBACA IPYNB
|
| 174 |
+
# ==========================================================
|
| 175 |
+
def save_notebook_image(data_b64: str, suffix: str = ".png") -> Optional[str]:
|
| 176 |
+
"""Menyimpan image base64 dari output notebook menjadi file sementara."""
|
| 177 |
+
try:
|
| 178 |
+
if isinstance(data_b64, list):
|
| 179 |
+
data_b64 = "".join(data_b64)
|
| 180 |
+
image_bytes = base64.b64decode(data_b64)
|
| 181 |
+
image = Image.open(io.BytesIO(image_bytes))
|
| 182 |
+
tmp = tempfile.NamedTemporaryFile(delete=False, suffix=suffix)
|
| 183 |
+
image.save(tmp.name)
|
| 184 |
+
return tmp.name
|
| 185 |
+
except Exception:
|
| 186 |
+
return None
|
| 187 |
|
| 188 |
|
| 189 |
+
def read_ipynb(ipynb_path: str) -> List[Dict[str, Any]]:
|
| 190 |
+
"""Membaca cell notebook, output teks, dan gambar output."""
|
| 191 |
notebook = nbformat.read(ipynb_path, as_version=4)
|
| 192 |
+
sections: List[Dict[str, Any]] = []
|
| 193 |
last_markdown = ""
|
| 194 |
|
| 195 |
for cell in notebook.cells:
|
| 196 |
if cell.cell_type == "markdown":
|
| 197 |
+
text = (cell.source or "").strip()
|
| 198 |
+
if text:
|
| 199 |
+
last_markdown = text
|
| 200 |
+
sections.append(
|
| 201 |
+
{
|
| 202 |
+
"type": "markdown",
|
| 203 |
+
"source": text,
|
| 204 |
+
"context": text,
|
| 205 |
+
"output_text": "",
|
| 206 |
+
"images": [],
|
| 207 |
+
}
|
| 208 |
+
)
|
| 209 |
continue
|
| 210 |
|
| 211 |
+
if cell.cell_type != "code":
|
| 212 |
+
continue
|
| 213 |
+
|
| 214 |
+
code = (cell.source or "").strip()
|
| 215 |
+
if not code:
|
| 216 |
+
continue
|
| 217 |
+
|
| 218 |
+
output_texts: List[str] = []
|
| 219 |
+
image_paths: List[str] = []
|
| 220 |
+
|
| 221 |
+
for output in cell.get("outputs", []):
|
| 222 |
+
output_type = output.get("output_type")
|
| 223 |
+
|
| 224 |
+
if output_type == "stream":
|
| 225 |
+
output_texts.append(str(output.get("text", "")))
|
| 226 |
+
|
| 227 |
+
elif output_type in ["execute_result", "display_data"]:
|
| 228 |
+
data = output.get("data", {})
|
| 229 |
+
|
| 230 |
+
if "text/plain" in data:
|
| 231 |
+
text_plain = data["text/plain"]
|
| 232 |
+
if isinstance(text_plain, list):
|
| 233 |
+
text_plain = "".join(text_plain)
|
| 234 |
+
output_texts.append(str(text_plain))
|
| 235 |
+
|
| 236 |
+
if "image/png" in data:
|
| 237 |
+
path = save_notebook_image(data["image/png"], ".png")
|
| 238 |
+
if path:
|
| 239 |
+
image_paths.append(path)
|
| 240 |
+
|
| 241 |
+
if "image/jpeg" in data:
|
| 242 |
+
path = save_notebook_image(data["image/jpeg"], ".jpg")
|
| 243 |
+
if path:
|
| 244 |
+
image_paths.append(path)
|
| 245 |
+
|
| 246 |
+
elif output_type == "error":
|
| 247 |
+
ename = output.get("ename", "Error")
|
| 248 |
+
evalue = output.get("evalue", "")
|
| 249 |
+
traceback = output.get("traceback", [])
|
| 250 |
+
output_texts.append(f"{ename}: {evalue}\n" + "\n".join(traceback))
|
| 251 |
+
|
| 252 |
+
sections.append(
|
| 253 |
+
{
|
| 254 |
+
"type": "code",
|
| 255 |
+
"source": code,
|
| 256 |
+
"context": last_markdown,
|
| 257 |
+
"output_text": "\n".join(output_texts).strip(),
|
| 258 |
+
"images": image_paths,
|
| 259 |
+
}
|
| 260 |
+
)
|
| 261 |
|
| 262 |
return sections
|
| 263 |
|
| 264 |
|
| 265 |
+
def fallback_title_from_code(code: str, context: str = "") -> str:
|
| 266 |
+
text = f"{context}\n{code}".lower()
|
| 267 |
+
|
| 268 |
+
rules = [
|
| 269 |
+
("import ", "Import Library"),
|
| 270 |
+
("mount", "Mount Google Drive"),
|
| 271 |
+
("zipfile", "Ekstrak Dataset"),
|
| 272 |
+
("os.listdir", "Pembacaan Dataset"),
|
| 273 |
+
("dataframe", "Memuat Data ke DataFrame"),
|
| 274 |
+
("df.info", "Cek Informasi Data"),
|
| 275 |
+
("train_test_split", "Split Dataset"),
|
| 276 |
+
("compute_class_weight", "Cek Distribusi & Class Weights"),
|
| 277 |
+
("imagedatagenerator", "Generator Inputan Data Augmentation"),
|
| 278 |
+
("flow_from_dataframe", "Flow Generator"),
|
| 279 |
+
("mobilenet", "Arsitektur Model MobileNet"),
|
| 280 |
+
("sequential", "Arsitektur Model CNN"),
|
| 281 |
+
("model.compile", "Kompilasi Model"),
|
| 282 |
+
("earlystopping", "Kompilasi & Callback Model"),
|
| 283 |
+
("model.fit", "Training Model"),
|
| 284 |
+
("classification_report", "Classification Report"),
|
| 285 |
+
("confusion_matrix", "Confusion Matrix"),
|
| 286 |
+
("model.evaluate", "Evaluasi Model"),
|
| 287 |
+
("plt.plot", "Visualisasi Loss & Accuracy"),
|
| 288 |
+
("model.save", "Simpan Model"),
|
| 289 |
+
("gradio", "Deployment dengan Gradio"),
|
| 290 |
+
]
|
| 291 |
+
|
| 292 |
+
for keyword, title in rules:
|
| 293 |
+
if keyword in text:
|
| 294 |
+
return title
|
| 295 |
+
return "Bagian Notebook"
|
| 296 |
+
|
| 297 |
|
| 298 |
+
def summarize_code_section(code: str, output_text: str, context: str = "") -> Tuple[str, str]:
|
| 299 |
prompt = f"""
|
| 300 |
{REPORT_STYLE_PROMPT}
|
| 301 |
|
| 302 |
+
Konteks markdown sebelumnya:
|
| 303 |
+
```txt
|
| 304 |
+
{truncate_text(context, 1500)}
|
| 305 |
```
|
| 306 |
|
| 307 |
Kode notebook:
|
| 308 |
```python
|
| 309 |
+
{truncate_text(code, 6000)}
|
| 310 |
```
|
| 311 |
|
| 312 |
Output notebook:
|
| 313 |
+
```txt
|
| 314 |
+
{truncate_text(output_text, 3500)}
|
| 315 |
```
|
| 316 |
+
"""
|
|
|
|
|
|
|
| 317 |
|
| 318 |
try:
|
| 319 |
result = ask_nvidia(
|
|
|
|
| 322 |
{"role": "user", "content": prompt},
|
| 323 |
],
|
| 324 |
temperature=0.25,
|
| 325 |
+
max_tokens=900,
|
| 326 |
)
|
| 327 |
data = parse_json_from_model(result)
|
| 328 |
+
if data:
|
| 329 |
+
judul = str(data.get("judul") or fallback_title_from_code(code, context)).strip()
|
| 330 |
+
penjelasan = str(data.get("penjelasan") or "").strip()
|
| 331 |
+
if not penjelasan.lower().startswith("penjelasannya"):
|
| 332 |
+
penjelasan = "Penjelasannya: " + penjelasan
|
| 333 |
+
return judul, penjelasan
|
| 334 |
+
return fallback_title_from_code(code, context), result.strip()
|
| 335 |
except Exception:
|
| 336 |
+
judul = fallback_title_from_code(code, context)
|
| 337 |
+
penjelasan = (
|
| 338 |
+
"Penjelasannya: Bagian kode ini digunakan dalam proses pembuatan model Deep Learning. "
|
| 339 |
+
"Kode perlu dibaca bersama output notebook untuk mengetahui hasil yang diperoleh secara lebih detail."
|
| 340 |
+
)
|
| 341 |
+
return judul, penjelasan
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 342 |
|
| 343 |
|
| 344 |
+
# ==========================================================
|
| 345 |
+
# PEMBUAT DOCX
|
| 346 |
+
# ==========================================================
|
| 347 |
+
def set_document_style(doc: Document):
|
| 348 |
+
style = doc.styles["Normal"]
|
| 349 |
+
style.font.name = "Times New Roman"
|
| 350 |
+
style.font.size = Pt(12)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 351 |
|
| 352 |
+
for section in doc.sections:
|
| 353 |
+
section.top_margin = Inches(1)
|
| 354 |
+
section.bottom_margin = Inches(0.8)
|
| 355 |
+
section.left_margin = Inches(1.2)
|
| 356 |
+
section.right_margin = Inches(1)
|
| 357 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 358 |
|
| 359 |
+
def add_center_paragraph(doc: Document, text: str, bold: bool = True, size: int = 12):
|
| 360 |
+
p = doc.add_paragraph()
|
| 361 |
+
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
| 362 |
+
run = p.add_run(text)
|
| 363 |
+
run.bold = bold
|
| 364 |
+
run.font.name = "Times New Roman"
|
| 365 |
+
run.font.size = Pt(size)
|
| 366 |
+
return p
|
| 367 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 368 |
|
| 369 |
+
def add_code_block(doc: Document, code: str):
|
| 370 |
+
code = truncate_text(code, 4500)
|
| 371 |
table = doc.add_table(rows=1, cols=1)
|
| 372 |
+
table.style = "Table Grid"
|
| 373 |
cell = table.cell(0, 0)
|
| 374 |
+
paragraph = cell.paragraphs[0]
|
| 375 |
+
run = paragraph.add_run(code)
|
| 376 |
+
run.font.name = "Courier New"
|
| 377 |
+
run.font.size = Pt(8)
|
|
|
|
|
|
|
| 378 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 379 |
|
| 380 |
+
def add_output_block(doc: Document, output_text: str):
|
| 381 |
+
if not output_text.strip():
|
| 382 |
+
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 383 |
p = doc.add_paragraph()
|
| 384 |
+
r = p.add_run("Output:")
|
| 385 |
+
r.bold = True
|
| 386 |
+
add_code_block(doc, output_text)
|
|
|
|
|
|
|
| 387 |
|
| 388 |
|
| 389 |
+
def add_image_to_doc(doc: Document, image_path: str):
|
| 390 |
try:
|
| 391 |
p = doc.add_paragraph()
|
| 392 |
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
| 393 |
run = p.add_run()
|
| 394 |
+
run.add_picture(image_path, width=Inches(5.6))
|
| 395 |
except Exception:
|
| 396 |
+
# Jika gambar gagal dimasukkan, lewati agar pembuatan laporan tetap jalan.
|
| 397 |
pass
|
| 398 |
|
| 399 |
|
| 400 |
+
def create_cover(
|
| 401 |
+
doc: Document,
|
| 402 |
+
judul_laporan: str,
|
| 403 |
+
nama: str,
|
| 404 |
+
nim: str,
|
| 405 |
+
dosen: str,
|
| 406 |
+
kelas: str,
|
| 407 |
+
anggota: str,
|
| 408 |
+
prodi: str,
|
| 409 |
+
kampus: str,
|
| 410 |
+
tahun: str,
|
|
|
|
|
|
|
| 411 |
):
|
| 412 |
+
add_center_paragraph(doc, "Laporan Deep Learning", bold=True, size=16)
|
| 413 |
+
add_center_paragraph(doc, judul_laporan.upper(), bold=True, size=14)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 414 |
|
| 415 |
+
doc.add_paragraph("\n\n\n")
|
|
|
|
|
|
|
|
|
|
| 416 |
|
| 417 |
+
add_center_paragraph(doc, nama, bold=True)
|
| 418 |
+
add_center_paragraph(doc, f"NIM. {nim}", bold=True)
|
|
|
|
|
|
|
|
|
|
| 419 |
|
| 420 |
+
doc.add_paragraph("")
|
| 421 |
+
add_center_paragraph(doc, "Dosen Mata kuliah", bold=True)
|
| 422 |
+
add_center_paragraph(doc, dosen, bold=True)
|
| 423 |
+
add_center_paragraph(doc, kelas, bold=True)
|
| 424 |
|
| 425 |
+
if anggota.strip():
|
| 426 |
+
doc.add_paragraph("")
|
| 427 |
+
add_center_paragraph(doc, anggota.upper(), bold=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 428 |
|
| 429 |
doc.add_paragraph("\n\n")
|
| 430 |
|
| 431 |
+
add_center_paragraph(doc, prodi.upper(), bold=True)
|
| 432 |
+
add_center_paragraph(doc, kampus.upper(), bold=True)
|
| 433 |
+
add_center_paragraph(doc, tahun, bold=True)
|
|
|
|
|
|
|
| 434 |
|
| 435 |
doc.add_page_break()
|
| 436 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 437 |
|
| 438 |
+
def add_footer(doc: Document, kampus: str):
|
| 439 |
+
for section in doc.sections:
|
| 440 |
+
footer = section.footer
|
| 441 |
+
p = footer.paragraphs[0]
|
| 442 |
+
p.alignment = WD_ALIGN_PARAGRAPH.RIGHT
|
| 443 |
+
run = p.add_run(kampus)
|
| 444 |
+
run.italic = True
|
| 445 |
+
run.font.size = Pt(9)
|
| 446 |
|
|
|
|
|
|
|
| 447 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 448 |
def create_report_docx(
|
| 449 |
+
ipynb_file: Any,
|
| 450 |
+
judul_laporan: str,
|
| 451 |
+
nama: str,
|
| 452 |
+
nim: str,
|
| 453 |
+
dosen: str,
|
| 454 |
+
kelas: str,
|
| 455 |
+
anggota: str,
|
| 456 |
+
prodi: str,
|
| 457 |
+
kampus: str,
|
| 458 |
+
tahun: str,
|
|
|
|
|
|
|
| 459 |
):
|
| 460 |
+
if not NVIDIA_API_KEY:
|
| 461 |
+
raise gr.Error("NVIDIA_API_KEY belum diset di Hugging Face Secrets.")
|
|
|
|
|
|
|
| 462 |
|
| 463 |
+
ipynb_path = get_uploaded_path(ipynb_file)
|
| 464 |
if not ipynb_path.endswith(".ipynb"):
|
| 465 |
raise gr.Error("File harus berformat .ipynb")
|
| 466 |
|
| 467 |
+
sections = read_ipynb(ipynb_path)
|
| 468 |
+
code_sections = [item for item in sections if item.get("type") == "code"]
|
| 469 |
+
|
| 470 |
+
if not code_sections:
|
| 471 |
+
raise gr.Error("Notebook tidak memiliki cell kode yang bisa dibuat menjadi laporan.")
|
| 472 |
|
| 473 |
+
doc = Document()
|
| 474 |
+
set_document_style(doc)
|
| 475 |
+
add_footer(doc, kampus)
|
| 476 |
|
| 477 |
+
create_cover(
|
| 478 |
+
doc=doc,
|
| 479 |
judul_laporan=judul_laporan,
|
| 480 |
nama=nama,
|
| 481 |
nim=nim,
|
| 482 |
dosen=dosen,
|
| 483 |
kelas=kelas,
|
| 484 |
+
anggota=anggota,
|
| 485 |
prodi=prodi,
|
| 486 |
kampus=kampus,
|
| 487 |
tahun=tahun,
|
|
|
|
|
|
|
|
|
|
| 488 |
)
|
| 489 |
|
| 490 |
+
number = 1
|
| 491 |
+
total = len(code_sections)
|
| 492 |
|
| 493 |
+
for item in code_sections:
|
| 494 |
+
code = item.get("source", "")
|
| 495 |
+
output_text = item.get("output_text", "")
|
| 496 |
+
context = item.get("context", "")
|
| 497 |
+
images = item.get("images", [])
|
| 498 |
+
|
| 499 |
+
judul, penjelasan = summarize_code_section(code, output_text, context)
|
| 500 |
+
|
| 501 |
+
heading = doc.add_paragraph()
|
| 502 |
+
heading_run = heading.add_run(f"{number}. {judul}")
|
| 503 |
+
heading_run.bold = True
|
| 504 |
+
heading_run.font.name = "Times New Roman"
|
| 505 |
+
heading_run.font.size = Pt(13)
|
| 506 |
|
| 507 |
+
add_code_block(doc, code)
|
| 508 |
+
add_output_block(doc, output_text)
|
| 509 |
+
|
| 510 |
+
for image_path in images[:4]:
|
| 511 |
+
add_image_to_doc(doc, image_path)
|
| 512 |
+
|
| 513 |
+
p = doc.add_paragraph()
|
| 514 |
+
p.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
|
| 515 |
+
r = p.add_run(penjelasan)
|
| 516 |
+
r.font.name = "Times New Roman"
|
| 517 |
+
r.font.size = Pt(12)
|
| 518 |
+
|
| 519 |
+
if number < total:
|
| 520 |
+
doc.add_paragraph("")
|
| 521 |
+
number += 1
|
| 522 |
+
|
| 523 |
+
output_name = safe_filename(judul_laporan)
|
| 524 |
+
output_path = str(Path(tempfile.gettempdir()) / f"{output_name}.docx")
|
| 525 |
+
doc.save(output_path)
|
| 526 |
+
return output_path
|
| 527 |
+
|
| 528 |
+
|
| 529 |
+
# ==========================================================
|
| 530 |
+
# UI GRADIO
|
| 531 |
+
# ==========================================================
|
| 532 |
+
with gr.Blocks(title="AI Assistant Mahasiswa NVIDIA") as demo:
|
| 533 |
+
gr.Markdown("# 🎓 AI Assistant Mahasiswa NVIDIA")
|
| 534 |
gr.Markdown(
|
| 535 |
+
"Asisten AI untuk chat umum mahasiswa dan pembuatan laporan Deep Learning otomatis dari file `.ipynb` ke `.docx`."
|
|
|
|
|
|
|
|
|
|
| 536 |
)
|
| 537 |
|
|
|
|
|
|
|
|
|
|
| 538 |
with gr.Tab("Chat Umum"):
|
| 539 |
+
gr.Markdown("Gunakan tab ini untuk bertanya tentang materi, tugas, coding, laporan, atau pertanyaan umum.")
|
| 540 |
gr.ChatInterface(
|
| 541 |
fn=normal_chat,
|
| 542 |
+
title="Chat AI Mahasiswa",
|
| 543 |
+
description="Tanya apa saja seputar kuliah, coding, laporan, Deep Learning, atau pertanyaan umum.",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 544 |
)
|
| 545 |
|
| 546 |
with gr.Tab("Buat Laporan DOCX dari IPYNB"):
|
| 547 |
gr.Markdown(
|
| 548 |
+
"Upload file `.ipynb` yang sudah dijalankan, isi data cover, lalu klik tombol untuk membuat laporan Word `.docx`."
|
|
|
|
|
|
|
|
|
|
| 549 |
)
|
| 550 |
|
| 551 |
with gr.Row():
|
| 552 |
+
with gr.Column():
|
| 553 |
ipynb_file = gr.File(label="Upload File IPYNB", file_types=[".ipynb"])
|
|
|
|
| 554 |
judul_laporan = gr.Textbox(label="Judul Laporan", value="Klasifikasi Penyakit Mata")
|
| 555 |
nama = gr.Textbox(label="Nama", value="Hadid Zarid Nawfal")
|
| 556 |
nim = gr.Textbox(label="NIM", value="2355301079")
|
| 557 |
dosen = gr.Textbox(label="Dosen Mata Kuliah", value="Dr. Juni Nurma Sari, S.Kom., M.MT.")
|
| 558 |
kelas = gr.Textbox(label="Kelas", value="ILB")
|
| 559 |
+
anggota = gr.Textbox(label="Nama Anggota / Partner Opsional", value="")
|
| 560 |
prodi = gr.Textbox(label="Program Studi", value="Program Studi Teknik Informatika")
|
| 561 |
kampus = gr.Textbox(label="Kampus", value="Politeknik Caltex Riau")
|
| 562 |
tahun = gr.Textbox(label="Tahun", value="2025")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 563 |
btn = gr.Button("Buat Laporan DOCX", variant="primary")
|
| 564 |
|
| 565 |
+
with gr.Column():
|
| 566 |
output_file = gr.File(label="Download Laporan DOCX", file_types=[".docx"])
|
| 567 |
gr.Markdown(
|
| 568 |
+
"**Catatan:** Notebook sebaiknya sudah dijalankan terlebih dahulu agar output, grafik, akurasi, loss, dan hasil evaluasi tersimpan di file `.ipynb`."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 569 |
)
|
| 570 |
|
| 571 |
btn.click(
|
|
|
|
| 577 |
nim,
|
| 578 |
dosen,
|
| 579 |
kelas,
|
| 580 |
+
anggota,
|
| 581 |
prodi,
|
| 582 |
kampus,
|
| 583 |
tahun,
|
|
|
|
|
|
|
| 584 |
],
|
| 585 |
outputs=output_file,
|
| 586 |
)
|
| 587 |
|
| 588 |
+
with gr.Tab("Cek API"):
|
| 589 |
+
gr.Markdown("Gunakan tab ini untuk mengecek apakah NVIDIA API key dan model sudah benar.")
|
| 590 |
+
gr.Markdown(f"**Model saat ini:** `{NVIDIA_MODEL}`")
|
| 591 |
+
gr.Markdown(f"**Base URL:** `{NVIDIA_BASE_URL}`")
|
| 592 |
+
check_btn = gr.Button("Cek Koneksi NVIDIA API")
|
| 593 |
+
check_output = gr.Textbox(label="Status", lines=4)
|
| 594 |
+
check_btn.click(fn=test_api_connection, inputs=None, outputs=check_output)
|
| 595 |
|
| 596 |
if __name__ == "__main__":
|
| 597 |
+
demo.queue(max_size=20).launch()
|
packages.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Tidak wajib. Dibiarkan kosong karena aplikasi ini hanya butuh package Python.
|
requirements.txt
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
-
gradio
|
| 2 |
-
openai>=1.
|
| 3 |
-
nbformat>=5.10.
|
| 4 |
python-docx>=1.1.2
|
| 5 |
pillow>=10.0.0
|
|
|
|
|
|
| 1 |
+
gradio==4.44.1
|
| 2 |
+
openai>=1.30.0
|
| 3 |
+
nbformat>=5.10.4
|
| 4 |
python-docx>=1.1.2
|
| 5 |
pillow>=10.0.0
|
| 6 |
+
audioop-lts>=0.2.1
|