myreport12 commited on
Commit
8d5bf98
·
verified ·
1 Parent(s): 7101a1c

Upload 6 files

Browse files
Files changed (6) hide show
  1. .env.example +5 -2
  2. .gitignore +5 -1
  3. README.md +154 -20
  4. app.py +402 -469
  5. packages.txt +1 -0
  6. requirements.txt +4 -3
.env.example CHANGED
@@ -1,3 +1,6 @@
1
- NVIDIA_API_KEY=nvapi-isi_api_key_kamu
2
- NVIDIA_MODEL=meta/llama-3.1-8b-instruct
 
 
 
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
- *.ipynb_checkpoints/
 
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 mahasiswa.
17
- 2. Membuat laporan Deep Learning otomatis dari file `.ipynb`.
18
- 3. Output laporan langsung berupa file Word `.docx`.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
 
20
  ## Cara Upload ke Hugging Face Spaces
21
 
22
  1. Buat Space baru di Hugging Face.
23
  2. Pilih SDK: **Gradio**.
24
- 3. Upload file berikut ke Space:
25
- - `app.py`
26
- - `requirements.txt`
27
- - `README.md`
28
- 4. Masuk ke **Settings > Variables and secrets**.
29
- 5. Tambahkan secret:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
 
31
  ```txt
32
  NVIDIA_API_KEY=isi_api_key_nvidia_kamu
33
  ```
34
 
35
- Opsional, tambahkan variable:
 
 
36
 
37
  ```txt
38
- NVIDIA_MODEL=meta/llama-3.1-8b-instruct
39
  NVIDIA_BASE_URL=https://integrate.api.nvidia.com/v1
40
  ```
41
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  ## Cara Pakai
43
 
44
  ### Chat Umum
45
- Buka tab **Chat Umum**, lalu ketik pertanyaan.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
 
47
  ### Buat Laporan DOCX dari IPYNB
48
- 1. Jalankan notebook `.ipynb` terlebih dahulu agar output/grafik tersimpan.
49
- 2. Upload file `.ipynb`.
50
- 3. Isi data cover laporan.
51
- 4. Klik **Buat Laporan DOCX**.
52
- 5. Download file `.docx` yang muncul.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
 
54
  ## Catatan Penting
55
 
56
- - API key jangan ditulis langsung di kode.
57
- - Simpan API key di Hugging Face Secret bernama `NVIDIA_API_KEY`.
 
 
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.oxml import OxmlElement, parse_xml
16
- from docx.oxml.ns import nsdecls, qn
17
- from docx.shared import Inches, Pt, RGBColor
18
-
19
 
20
- # =========================
21
  # KONFIGURASI NVIDIA API
22
- # =========================
23
- NVIDIA_API_KEY = os.getenv("NVIDIA_API_KEY", "").strip()
24
- NVIDIA_BASE_URL = os.getenv("NVIDIA_BASE_URL", "https://integrate.api.nvidia.com/v1").strip()
25
- NVIDIA_MODEL = os.getenv("NVIDIA_MODEL", "meta/llama-3.1-8b-instruct").strip()
26
 
27
  client = OpenAI(
28
- api_key=NVIDIA_API_KEY if NVIDIA_API_KEY else "EMPTY",
29
  base_url=NVIDIA_BASE_URL,
30
  )
31
 
32
-
33
  SYSTEM_PROMPT = """
34
  Kamu adalah AI Assistant Mahasiswa.
35
 
36
- Tugas utamamu adalah membantu mahasiswa membuat laporan, tugas kuliah, memahami materi,
37
- merangkum teks, memperbaiki bahasa, membantu coding dasar, dan menjawab pertanyaan umum.
38
-
39
- Untuk laporan Deep Learning:
40
- - Gunakan bahasa Indonesia yang formal, jelas, dan mudah dipahami.
41
- - Jelaskan kode dengan gaya laporan praktikum mahasiswa.
42
- - Jangan mengarang hasil, angka akurasi, epoch, dataset, metrik evaluasi, atau referensi.
43
- - Gunakan informasi yang memang ada pada notebook.
44
- - Jika output tersedia, jelaskan berdasarkan output tersebut.
45
- - Jika output tidak tersedia, jelaskan tujuan kode dan fungsi tahap tersebut.
46
- - Jangan menyebut bahwa kamu adalah model bahasa.
47
- """.strip()
 
 
 
 
48
 
49
  REPORT_STYLE_PROMPT = """
50
- Buat penjelasan laporan Deep Learning dengan gaya laporan mahasiswa.
51
 
52
- Format jawaban wajib JSON valid:
53
  {
54
- "judul": "Judul bagian singkat",
55
- "penjelasan": "Penjelasannya: paragraf penjelasan"
56
  }
57
 
58
- Aturan gaya:
59
- - Judul singkat, misalnya: Import Library, Pembacaan Dataset, Split Dataset, Training Model, Evaluasi Model, Confusion Matrix, Deployment Gradio.
60
- - Penjelasan diawali dengan teks: Penjelasannya:
61
- - Bahasa Indonesia formal tetapi tetap natural seperti laporan praktikum.
62
- - Jelaskan fungsi kode, tujuan tahap, dan arti output jika ada.
63
- - Jangan mengarang angka, metrik, nama dataset, atau hasil yang tidak muncul pada notebook.
64
- - Jika ada error pada output, jelaskan bahwa kode menghasilkan error dan apa kemungkinan penyebabnya secara hati-hati.
65
- """.strip()
 
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 clean_text(text):
90
- if text is None:
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
- try:
125
- return json.loads(match.group(0))
126
- except Exception:
127
- pass
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
- try:
179
- ensure_api_key()
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
- for output in cell.get("outputs", []):
210
- output_type = output.get("output_type", "")
 
 
 
 
 
 
211
 
212
- if output_type == "stream":
213
- output_texts.append(clean_text(output.get("text", "")))
214
 
215
- elif output_type in ["execute_result", "display_data"]:
216
- data = output.get("data", {}) or {}
 
 
217
 
218
- if "text/plain" in data:
219
- output_texts.append(clean_text(data.get("text/plain", "")))
220
 
221
- for mime, ext in [("image/png", ".png"), ("image/jpeg", ".jpg")]:
222
- if mime in data:
223
- raw = data[mime]
224
- if isinstance(raw, list):
225
- raw = "".join(raw)
226
- raw = str(raw).replace("\n", "")
227
- try:
228
- img_bytes = base64.b64decode(raw)
229
- img_path = Path(temp_dir) / f"output_{uuid.uuid4().hex}{ext}"
230
- img_path.write_bytes(img_bytes)
231
- image_paths.append(str(img_path))
232
- except Exception:
233
- pass
 
 
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
- return clean_text("\n\n".join(x for x in output_texts if x)), image_paths
 
 
 
 
 
 
 
 
 
 
 
 
 
 
242
 
243
 
244
- def read_ipynb(ipynb_path, temp_dir):
 
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
- md = clean_text(cell.get("source", ""))
252
- if md:
253
- last_markdown = md
 
 
 
 
 
 
 
 
 
254
  continue
255
 
256
- if cell.cell_type == "code":
257
- code = clean_text(cell.get("source", ""))
258
- if not code:
259
- continue
260
-
261
- output_text, image_paths = extract_outputs(cell, temp_dir)
262
- sections.append(
263
- {
264
- "markdown_context": last_markdown,
265
- "code": code,
266
- "output_text": output_text,
267
- "image_paths": image_paths,
268
- }
269
- )
270
- last_markdown = ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
271
 
272
  return sections
273
 
274
 
275
- # =========================
276
- # GENERATE PENJELASAN AI
277
- # =========================
278
- def summarize_code_section(code, output_text, markdown_context=""):
279
- fallback_title = guess_title_from_code(code, markdown_context)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
280
 
 
281
  prompt = f"""
282
  {REPORT_STYLE_PROMPT}
283
 
284
- Konteks markdown sebelum kode, jika ada:
285
- ```text
286
- {limit_text(markdown_context, 1000)}
287
  ```
288
 
289
  Kode notebook:
290
  ```python
291
- {limit_text(code, 5000)}
292
  ```
293
 
294
  Output notebook:
295
- ```text
296
- {limit_text(output_text, 2500)}
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=800,
310
  )
311
  data = parse_json_from_model(result)
312
- judul = clean_text(data.get("judul", fallback_title)) or fallback_title
313
- penjelasan = clean_text(data.get("penjelasan", ""))
314
- if not penjelasan:
315
- penjelasan = "Penjelasannya: Pada tahap ini, kode digunakan untuk menjalankan salah satu proses dalam notebook Deep Learning."
316
- if not penjelasan.lower().startswith("penjelasannya"):
317
- penjelasan = f"Penjelasannya: {penjelasan}"
318
- return judul, penjelasan
319
  except Exception:
320
- return fallback_title, "Penjelasannya: Pada tahap ini, kode digunakan untuk menjalankan salah satu proses dalam notebook Deep Learning. Penjelasan dibuat berdasarkan struktur kode yang tersedia."
321
-
322
-
323
- # =========================
324
- # FORMAT DOCX
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
- def set_cell_border(cell):
340
- tc = cell._tc
341
- tc_pr = tc.get_or_add_tcPr()
342
- borders = OxmlElement("w:tcBorders")
343
- for edge in ("top", "left", "bottom", "right"):
344
- tag = OxmlElement(f"w:{edge}")
345
- tag.set(qn("w:val"), "single")
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.autofit = True
369
  cell = table.cell(0, 0)
370
- set_cell_shading(cell, "F7F7F7")
371
- set_cell_border(cell)
372
-
373
- p = cell.paragraphs[0]
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
- def add_normal_paragraph(doc, text, bold_prefix=None):
386
- p = doc.add_paragraph()
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
- p.paragraph_format.space_before = Pt(8)
406
- p.paragraph_format.space_after = Pt(4)
407
- run = p.add_run(f"{number}. {title}")
408
- set_run_font(run, size=13, bold=True)
409
- return p
410
 
411
 
412
- def add_image(doc, image_path):
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.7))
418
  except Exception:
419
- # kalau gambar terlalu besar/format bermasalah, abaikan agar dokumen tetap jadi
420
  pass
421
 
422
 
423
- def build_docx(
424
- sections,
425
- judul_laporan,
426
- nama,
427
- nim,
428
- dosen,
429
- kelas,
430
- prodi,
431
- kampus,
432
- tahun,
433
- include_code=True,
434
- include_output=True,
435
- progress=None,
436
  ):
437
- doc = Document()
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
- normal_style = doc.styles["Normal"]
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
- # COVER
452
- p = doc.add_paragraph()
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
- p = doc.add_paragraph()
458
- p.alignment = WD_ALIGN_PARAGRAPH.CENTER
459
- r = p.add_run((judul_laporan or "Laporan Deep Learning").upper())
460
- set_run_font(r, size=14, bold=True)
461
 
462
- doc.add_paragraph("\n\n\n")
463
-
464
- cover_lines = [
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
- for line in [prodi, kampus, tahun]:
482
- p = doc.add_paragraph()
483
- p.alignment = WD_ALIGN_PARAGRAPH.CENTER
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
- if include_code:
504
- add_code_block(doc, item["code"], max_chars=4500)
 
 
 
 
 
 
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
- prodi,
537
- kampus,
538
- tahun,
539
- include_code,
540
- include_output,
541
- progress=gr.Progress(track_tqdm=False),
542
  ):
543
- ensure_api_key()
544
-
545
- if ipynb_file is None:
546
- raise gr.Error("Upload file .ipynb terlebih dahulu.")
547
 
548
- ipynb_path = getattr(ipynb_file, "name", None) or str(ipynb_file)
549
  if not ipynb_path.endswith(".ipynb"):
550
  raise gr.Error("File harus berformat .ipynb")
551
 
552
- temp_dir = tempfile.mkdtemp(prefix="ipynb_outputs_")
553
- progress(0, desc="Membaca file notebook...")
554
- sections = read_ipynb(ipynb_path, temp_dir)
 
 
555
 
556
- if not sections:
557
- raise gr.Error("Notebook tidak memiliki code cell yang bisa dibuat menjadi laporan.")
 
558
 
559
- return build_docx(
560
- sections=sections,
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
- # UI HUGGING FACE SPACE
577
- # =========================
578
- custom_css = """
579
- .gradio-container {
580
- max-width: 1100px !important;
581
- }
582
- """
 
 
 
 
 
583
 
584
- with gr.Blocks(title="AI Assistant Mahasiswa", css=custom_css) as demo:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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: laporan, tugas kuliah, materi, coding, bahasa, dan pertanyaan umum.",
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(scale=1):
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(scale=1):
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>=4.44.0
2
- openai>=1.40.0
3
- nbformat>=5.10.0
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