419 lines
23 KiB
Python
419 lines
23 KiB
Python
from __future__ import annotations
|
|
|
|
import copy
|
|
import zipfile
|
|
from pathlib import Path
|
|
from xml.etree import ElementTree as ET
|
|
|
|
|
|
INPUT_DOCX = Path("belum_fix_source.docx")
|
|
OUTPUT_DOCX = Path("Laporan_ConfiVoice_Bab4_Bab5_Sesuai_Project.docx")
|
|
XML_PATH = "word/document.xml"
|
|
|
|
W_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
|
|
NS = {"w": W_NS}
|
|
ET.register_namespace("w", W_NS)
|
|
|
|
|
|
def qn(tag: str) -> str:
|
|
prefix, local = tag.split(":", 1)
|
|
if prefix != "w":
|
|
raise ValueError(tag)
|
|
return f"{{{W_NS}}}{local}"
|
|
|
|
|
|
def paragraph_text(paragraph: ET.Element) -> str:
|
|
return "".join(t.text or "" for t in paragraph.findall(".//w:t", NS)).strip()
|
|
|
|
|
|
def append_run(paragraph: ET.Element, text: str, bold: bool = False) -> None:
|
|
run = ET.SubElement(paragraph, qn("w:r"))
|
|
if bold:
|
|
rpr = ET.SubElement(run, qn("w:rPr"))
|
|
ET.SubElement(rpr, qn("w:b"))
|
|
t = ET.SubElement(run, qn("w:t"))
|
|
t.set("{http://www.w3.org/XML/1998/namespace}space", "preserve")
|
|
t.text = text
|
|
|
|
|
|
def paragraph(text: str = "", bold: bool = False, center: bool = False) -> ET.Element:
|
|
p = ET.Element(qn("w:p"))
|
|
ppr = ET.SubElement(p, qn("w:pPr"))
|
|
spacing = ET.SubElement(ppr, qn("w:spacing"))
|
|
spacing.set(qn("w:after"), "120")
|
|
if center:
|
|
jc = ET.SubElement(ppr, qn("w:jc"))
|
|
jc.set(qn("w:val"), "center")
|
|
append_run(p, text, bold=bold)
|
|
return p
|
|
|
|
|
|
def heading(text: str) -> ET.Element:
|
|
return paragraph(text, bold=True, center=True)
|
|
|
|
|
|
def subheading(text: str) -> ET.Element:
|
|
return paragraph(text, bold=True)
|
|
|
|
|
|
def table(rows: list[list[str]]) -> ET.Element:
|
|
tbl = ET.Element(qn("w:tbl"))
|
|
tbl_pr = ET.SubElement(tbl, qn("w:tblPr"))
|
|
tbl_w = ET.SubElement(tbl_pr, qn("w:tblW"))
|
|
tbl_w.set(qn("w:w"), "9360")
|
|
tbl_w.set(qn("w:type"), "dxa")
|
|
borders = ET.SubElement(tbl_pr, qn("w:tblBorders"))
|
|
for side in ["top", "left", "bottom", "right", "insideH", "insideV"]:
|
|
border = ET.SubElement(borders, qn(f"w:{side}"))
|
|
border.set(qn("w:val"), "single")
|
|
border.set(qn("w:sz"), "4")
|
|
border.set(qn("w:space"), "0")
|
|
border.set(qn("w:color"), "000000")
|
|
|
|
col_count = max((len(row) for row in rows), default=1)
|
|
col_width = str(max(1100, 9360 // col_count))
|
|
grid = ET.SubElement(tbl, qn("w:tblGrid"))
|
|
for _ in range(col_count):
|
|
col = ET.SubElement(grid, qn("w:gridCol"))
|
|
col.set(qn("w:w"), col_width)
|
|
|
|
for row_index, row in enumerate(rows):
|
|
tr = ET.SubElement(tbl, qn("w:tr"))
|
|
for value in row:
|
|
tc = ET.SubElement(tr, qn("w:tc"))
|
|
tc_pr = ET.SubElement(tc, qn("w:tcPr"))
|
|
tc_w = ET.SubElement(tc_pr, qn("w:tcW"))
|
|
tc_w.set(qn("w:w"), col_width)
|
|
tc_w.set(qn("w:type"), "dxa")
|
|
tc.append(paragraph(value, bold=row_index == 0))
|
|
return tbl
|
|
|
|
|
|
def copy_lampiran_drawing(body: ET.Element, label: str) -> ET.Element | None:
|
|
children = list(body)
|
|
lampiran_index = 0
|
|
for index, child in enumerate(children):
|
|
if child.tag == qn("w:p") and paragraph_text(child).upper() == "LAMPIRAN":
|
|
lampiran_index = index
|
|
break
|
|
|
|
for index, child in enumerate(children[lampiran_index:], start=lampiran_index):
|
|
if child.tag == qn("w:p") and paragraph_text(child).lower() == label.lower():
|
|
for next_child in children[index + 1 : index + 5]:
|
|
if next_child.find(".//w:drawing", NS) is not None:
|
|
return copy.deepcopy(next_child)
|
|
return None
|
|
|
|
|
|
def build_bab45(use_case_drawing: ET.Element | None, flowchart_drawing: ET.Element | None) -> list[ET.Element]:
|
|
metrics = [
|
|
["Keterangan", "Hasil"],
|
|
["Total data valid", "48 audio"],
|
|
["Jumlah data PD", "24 audio"],
|
|
["Jumlah data TPD", "24 audio"],
|
|
["Jumlah fitur per audio", "78 fitur"],
|
|
["Algoritma", "Support Vector Machine kernel RBF"],
|
|
["Best parameter", "C=1, gamma=scale, kernel=rbf"],
|
|
["Best CV f1_macro", "0,7873"],
|
|
["Accuracy", "0,7917 atau 79,17%"],
|
|
["Precision Macro", "0,7937 atau 79,37%"],
|
|
["Recall Macro", "0,7917 atau 79,17%"],
|
|
["F1 Macro", "0,7913 atau 79,13%"],
|
|
]
|
|
confusion = [
|
|
["Kelas Aktual", "Diprediksi PD", "Diprediksi TPD", "Total"],
|
|
["PD", "20", "4", "24"],
|
|
["TPD", "6", "18", "24"],
|
|
]
|
|
erd = [
|
|
["Entitas", "Atribut Utama", "Keterangan"],
|
|
[
|
|
"users",
|
|
"id, full_name, username, password_hash, created_at",
|
|
"Menyimpan data akun pengguna/admin yang digunakan untuk login aplikasi mobile dan admin web.",
|
|
],
|
|
[
|
|
"prediction_results",
|
|
"id, user_id, student_name, student_gender, predicted_label, confidence, probability_pd, probability_tpd, audio_duration, volume_score, intonation_score, pause_score, created_at",
|
|
"Menyimpan hasil analisis suara siswa, nilai probabilitas, indikator suara, dan waktu penyimpanan data.",
|
|
],
|
|
]
|
|
blackbox = [
|
|
["Fitur", "Skenario Pengujian", "Hasil Yang Diharapkan", "Status"],
|
|
["Registrasi", "Pengguna mengisi nama lengkap, username, dan password", "Akun berhasil dibuat", "Berhasil"],
|
|
["Login", "Pengguna memasukkan username dan password yang benar", "Sistem masuk ke halaman utama", "Berhasil"],
|
|
["Rekam Audio", "Pengguna menekan tombol rekam lalu berhenti rekam", "Audio WAV tersimpan dan preview gelombang tampil", "Berhasil"],
|
|
["Pilih Audio", "Pengguna memilih file audio dari perangkat", "Audio dipilih dan siap diprediksi", "Berhasil"],
|
|
["Prediksi", "Pengguna mengirim audio valid ke backend", "Sistem menampilkan hasil PD/TPD, confidence, dan probabilitas", "Berhasil"],
|
|
["Simpan Hasil", "Pengguna menekan Simpan Hasil setelah prediksi valid", "Hasil analisis masuk ke database", "Berhasil"],
|
|
["Lihat Analisis", "Pengguna membuka halaman riwayat", "Data hasil analisis tampil per siswa", "Berhasil"],
|
|
["Admin Web", "Admin membuka dashboard", "Admin dapat melihat, menambah, mengubah, dan menghapus data", "Berhasil"],
|
|
]
|
|
|
|
items: list[ET.Element] = [
|
|
heading("BAB 4. HASIL DAN PEMBAHASAN"),
|
|
subheading("4.1 Analisis Kebutuhan"),
|
|
paragraph(
|
|
"Tahap analisis kebutuhan dilakukan untuk mengetahui kebutuhan sistem yang akan dibangun pada penelitian "
|
|
"klasifikasi tingkat percaya diri berdasarkan analisis suara. Sistem yang dikembangkan adalah ConfiVoice, "
|
|
"yaitu aplikasi berbasis mobile yang terhubung dengan backend machine learning untuk mengolah rekaman suara "
|
|
"siswa. Hasil keluaran sistem berupa klasifikasi Percaya Diri (PD) atau Tidak Percaya Diri (TPD), nilai "
|
|
"confidence, probabilitas, dan indikator suara."
|
|
),
|
|
subheading("4.1.1 Analisis Kebutuhan Pengguna"),
|
|
paragraph(
|
|
"Pengguna utama pada aplikasi mobile adalah guru atau pengguna yang melakukan proses analisis suara siswa. "
|
|
"Pengguna dapat melakukan registrasi, login, memasukkan nama siswa, memilih jenis kelamin, merekam atau "
|
|
"memilih audio, menjalankan prediksi, menyimpan hasil analisis, dan melihat riwayat analisis. Selain itu, "
|
|
"admin dapat mengakses halaman web untuk melihat dashboard, menambah data manual, mengubah data, menghapus "
|
|
"data, serta mengelola user."
|
|
),
|
|
subheading("4.1.2 Analisis Kebutuhan Data Audio"),
|
|
paragraph(
|
|
"Data yang digunakan pada penelitian ini berupa rekaman suara siswa dalam format WAV. Dataset disimpan pada "
|
|
"folder ml/data dan terdiri dari dua label, yaitu PD untuk Percaya Diri dan TPD untuk Tidak Percaya Diri. "
|
|
"Jumlah data yang digunakan adalah 48 audio, terdiri dari 24 audio PD dan 24 audio TPD. Penamaan file "
|
|
"menggunakan penanda _pd dan _tpd agar label dapat dibaca otomatis oleh sistem."
|
|
),
|
|
subheading("4.1.3 Analisis Data dan Pembentukan Model"),
|
|
paragraph(
|
|
"Data audio yang telah dikumpulkan diproses melalui beberapa tahap, yaitu pengecekan kualitas audio, "
|
|
"preprocessing, ekstraksi fitur, training model, dan evaluasi model. Model yang digunakan adalah Support "
|
|
"Vector Machine (SVM) dengan pipeline StandardScaler dan SVC. StandardScaler digunakan untuk menormalkan "
|
|
"fitur, sedangkan SVM digunakan untuk melakukan klasifikasi dua kelas."
|
|
),
|
|
subheading("4.1.4 Representasi Data"),
|
|
paragraph(
|
|
"Setiap file audio direpresentasikan menjadi vektor fitur numerik. Fitur yang digunakan meliputi MFCC, "
|
|
"delta MFCC, RMS energy, zero crossing rate, spectral centroid, spectral bandwidth, spectral rolloff, pitch, "
|
|
"durasi suara aktif, silence ratio, jumlah jeda, rata-rata durasi jeda, dan speech activity ratio. Total "
|
|
"fitur yang dihasilkan dari setiap audio adalah 78 fitur."
|
|
),
|
|
subheading("4.1.5 Pembagian Data"),
|
|
paragraph(
|
|
"Pembagian data dilakukan dengan pendekatan cross-validation menggunakan Stratified K-Fold. Pendekatan ini "
|
|
"dipilih karena jumlah dataset masih terbatas, sehingga evaluasi model dapat dilakukan secara lebih seimbang "
|
|
"terhadap kelas PD dan TPD. Data tetap dijaga agar distribusi label pada setiap fold tidak terlalu timpang."
|
|
),
|
|
subheading("4.1.6 Hasil Perhitungan Model SVM"),
|
|
paragraph(
|
|
"Perhitungan model dimulai dari fitur audio yang telah diekstraksi, kemudian fitur dinormalisasi menggunakan "
|
|
"rumus z = (x - mean) / standard deviation. Setelah itu, SVM menghitung pemisahan kelas menggunakan kernel RBF "
|
|
"dengan bentuk K(xi, xj) = exp(-gamma ||xi - xj||^2). Sistem tidak menggunakan entropy dan information gain "
|
|
"karena metode yang digunakan bukan Decision Tree, melainkan Support Vector Machine."
|
|
),
|
|
paragraph(
|
|
"Pada hasil prediksi, sistem menampilkan probability_pd dan probability_tpd. Kelas dengan probabilitas lebih "
|
|
"besar akan dipilih sebagai hasil klasifikasi. Sebagai contoh, jika probability_pd sebesar 0,78 dan "
|
|
"probability_tpd sebesar 0,22, maka hasil klasifikasi adalah Percaya Diri dengan confidence sebesar 78%."
|
|
),
|
|
subheading("4.1.7 Evaluasi Model"),
|
|
paragraph("Hasil evaluasi model berdasarkan training dan cross-validation dapat dilihat pada tabel berikut."),
|
|
table(metrics),
|
|
paragraph(
|
|
"Berdasarkan confusion matrix, dari 24 data PD terdapat 20 data yang berhasil diklasifikasikan benar dan 4 "
|
|
"data salah klasifikasi. Dari 24 data TPD terdapat 18 data yang berhasil diklasifikasikan benar dan 6 data "
|
|
"salah klasifikasi."
|
|
),
|
|
table(confusion),
|
|
subheading("4.1.8 Integrasi Model"),
|
|
paragraph(
|
|
"Model terbaik hasil training disimpan dalam file ml/models/svm_voice_confidence_model.joblib. Model tersebut "
|
|
"diintegrasikan dengan backend FastAPI melalui endpoint /predict. Ketika aplikasi mobile mengirimkan file "
|
|
"audio, backend melakukan validasi format, konversi audio, ekstraksi fitur, prediksi menggunakan model SVM, "
|
|
"lalu mengembalikan hasil prediksi ke aplikasi mobile."
|
|
),
|
|
subheading("4.2 Perancangan Sistem"),
|
|
paragraph(
|
|
"Perancangan sistem dibuat untuk menjelaskan alur kerja ConfiVoice dari sisi pengguna, backend, database, dan "
|
|
"admin web. Sistem terdiri dari aplikasi mobile Flutter, backend API FastAPI, model machine learning SVM, "
|
|
"database MySQL/MariaDB, serta admin web untuk pengelolaan hasil analisis."
|
|
),
|
|
subheading("4.2.1 Wireframe"),
|
|
paragraph(
|
|
"Wireframe aplikasi ConfiVoice dibuat sebagai rancangan awal tampilan dan alur penggunaan sistem sebelum "
|
|
"aplikasi diimplementasikan. Wireframe menggambarkan halaman login, registrasi, halaman utama analisis, input "
|
|
"nama siswa, pilihan jenis kelamin, fitur rekam atau pilih audio, tombol Prediksi, kartu hasil klasifikasi, "
|
|
"tombol Simpan Hasil, dan halaman Lihat Analisis."
|
|
),
|
|
subheading("4.2.2 Use Case Diagram"),
|
|
paragraph(
|
|
"Use case diagram digunakan untuk menggambarkan hubungan antara aktor dengan fitur yang terdapat pada sistem. "
|
|
"Aktor pada sistem ini terdiri dari pengguna mobile dan admin. Pengguna mobile dapat registrasi, login, mengisi "
|
|
"data siswa, merekam atau memilih audio, melakukan prediksi, menyimpan hasil, dan melihat riwayat analisis. "
|
|
"Admin dapat login, melihat dashboard, menambah data manual, mengedit data, menghapus data, dan mengelola user."
|
|
),
|
|
]
|
|
if use_case_drawing is not None:
|
|
items.append(use_case_drawing)
|
|
items.extend(
|
|
[
|
|
paragraph("Gambar 4.1 Use Case Diagram Sistem ConfiVoice", center=True),
|
|
subheading("4.2.3 Flowchart Mobile"),
|
|
paragraph(
|
|
"Flowchart mobile menjelaskan alur penggunaan aplikasi dari awal sampai hasil analisis disimpan. Alur "
|
|
"dimulai dari pengguna membuka aplikasi, melakukan login atau registrasi, mengisi nama siswa dan jenis "
|
|
"kelamin, memilih atau merekam audio, menekan tombol Prediksi, kemudian sistem mengirim audio ke backend. "
|
|
"Setelah backend mengembalikan hasil PD atau TPD, pengguna dapat menyimpan hasil analisis ke database."
|
|
),
|
|
]
|
|
)
|
|
if flowchart_drawing is not None:
|
|
items.append(flowchart_drawing)
|
|
items.extend(
|
|
[
|
|
paragraph("Gambar 4.2 Flowchart Mobile ConfiVoice", center=True),
|
|
subheading("4.2.4 Entity Relationship Diagram (ERD)"),
|
|
paragraph(
|
|
"ERD digunakan untuk menggambarkan struktur data pada sistem ConfiVoice. Database utama yang digunakan "
|
|
"adalah MySQL/MariaDB dengan nama database confivoice. Tabel utama yang digunakan adalah users dan "
|
|
"prediction_results. Relasi antara kedua tabel ditunjukkan melalui user_id pada tabel prediction_results "
|
|
"yang mengarah ke data pengguna."
|
|
),
|
|
table(erd),
|
|
subheading("4.2.5 Data Flow Diagram (DFD)"),
|
|
paragraph(
|
|
"Data Flow Diagram menggambarkan perpindahan data pada sistem. Pengguna mengirimkan data akun, data siswa, "
|
|
"dan audio melalui aplikasi mobile. Backend menerima audio melalui endpoint /predict, kemudian memproses "
|
|
"audio menggunakan model SVM. Hasil prediksi dikirim kembali ke aplikasi mobile dan dapat disimpan melalui "
|
|
"endpoint /predictions. Data yang tersimpan dapat diakses kembali melalui halaman Lihat Analisis dan "
|
|
"dashboard admin web."
|
|
),
|
|
subheading("4.3 Implementasi"),
|
|
paragraph(
|
|
"Implementasi sistem dilakukan berdasarkan hasil perancangan. Bagian mobile dibuat menggunakan Flutter, "
|
|
"backend dibuat menggunakan FastAPI, model klasifikasi dibuat menggunakan Python dan scikit-learn, sedangkan "
|
|
"penyimpanan data menggunakan MySQL/MariaDB."
|
|
),
|
|
subheading("4.3.1 Implementasi Aplikasi Mobile"),
|
|
paragraph(
|
|
"Aplikasi mobile memiliki fitur login, registrasi, input nama siswa, pemilihan jenis kelamin, rekam audio, "
|
|
"pilih audio, prediksi suara, simpan hasil, dan lihat analisis. Struktur kode mobile dipisahkan ke beberapa "
|
|
"bagian, seperti auth, home, prediction, saved analyses, dan model data agar pengelolaan kode lebih rapi."
|
|
),
|
|
subheading("4.3.2 Implementasi Backend API"),
|
|
paragraph(
|
|
"Backend API dibuat menggunakan FastAPI. Endpoint yang digunakan antara lain /register untuk pendaftaran, "
|
|
"/login untuk autentikasi, /predict untuk prediksi audio, /predictions untuk menyimpan dan mengambil hasil, "
|
|
"/save sebagai endpoint alternatif penyimpanan, dan /model untuk melihat informasi model."
|
|
),
|
|
subheading("4.3.3 Implementasi Database"),
|
|
paragraph(
|
|
"Database yang digunakan adalah MySQL/MariaDB dengan tabel users dan prediction_results. Tabel users "
|
|
"menyimpan data akun, sedangkan prediction_results menyimpan hasil analisis suara siswa, termasuk nama "
|
|
"siswa, jenis kelamin, label prediksi, confidence, probability_pd, probability_tpd, indikator suara, dan "
|
|
"tanggal penyimpanan."
|
|
),
|
|
subheading("4.3.4 Implementasi Admin Web"),
|
|
paragraph(
|
|
"Admin web dibuat menggunakan FastAPI dengan tampilan HTML. Halaman admin menyediakan fitur login, register "
|
|
"admin, dashboard hasil prediksi, filter data, tambah data manual, edit data, hapus data, detail hasil siswa, "
|
|
"dan pengelolaan user. Admin web memudahkan pengelola melihat hasil analisis tanpa membuka database secara langsung."
|
|
),
|
|
subheading("4.4 Pengujian Sistem"),
|
|
paragraph(
|
|
"Pengujian sistem dilakukan untuk memastikan setiap fitur berjalan sesuai kebutuhan. Pengujian yang dilakukan "
|
|
"meliputi pengujian black box, pengujian model klasifikasi, dan pengujian penerimaan pengguna."
|
|
),
|
|
subheading("4.4.1 Pengujian Black Box"),
|
|
paragraph("Hasil pengujian black box pada fitur utama sistem dapat dilihat pada tabel berikut."),
|
|
table(blackbox),
|
|
subheading("4.4.2 Pengujian Model Klasifikasi"),
|
|
paragraph(
|
|
"Pengujian model klasifikasi dilakukan untuk mengetahui kemampuan SVM dalam membedakan suara PD dan TPD. "
|
|
"Berdasarkan hasil evaluasi, model memperoleh accuracy sebesar 79,17% dan F1 macro sebesar 79,13%. Nilai "
|
|
"tersebut menunjukkan bahwa model sudah mampu mengenali pola suara dengan cukup baik, namun masih dapat "
|
|
"ditingkatkan dengan menambah jumlah dan variasi dataset."
|
|
),
|
|
subheading("4.4.3 Pengujian User Acceptance Test (UAT)"),
|
|
paragraph(
|
|
"Pengujian UAT dilakukan untuk mengetahui apakah aplikasi mudah digunakan dan sudah sesuai kebutuhan pengguna. "
|
|
"Aspek yang dinilai meliputi kemudahan login, kemudahan merekam atau memilih audio, kejelasan hasil prediksi, "
|
|
"kemudahan menyimpan hasil, kemudahan melihat riwayat analisis, dan kemudahan admin dalam melihat data."
|
|
),
|
|
subheading("4.5 Pemeliharaan Sistem"),
|
|
paragraph(
|
|
"Pemeliharaan sistem dilakukan dengan memperbaiki kesalahan yang ditemukan selama penggunaan, menjaga endpoint "
|
|
"API agar tetap sesuai, melakukan backup database, menambah dataset audio apabila diperlukan, dan melakukan "
|
|
"training ulang model jika terdapat penambahan data. Pemeliharaan juga dilakukan pada tampilan aplikasi agar "
|
|
"tetap mudah digunakan oleh pengguna."
|
|
),
|
|
heading("BAB 5. KESIMPULAN DAN SARAN"),
|
|
subheading("5.1 Kesimpulan"),
|
|
paragraph(
|
|
"Berdasarkan hasil perancangan, implementasi, dan pengujian, sistem ConfiVoice berhasil dibangun sebagai aplikasi "
|
|
"klasifikasi tingkat percaya diri berdasarkan analisis suara. Sistem ini menggunakan aplikasi mobile Flutter, "
|
|
"backend FastAPI, model machine learning SVM, database MySQL/MariaDB, dan admin web."
|
|
),
|
|
paragraph(
|
|
"Model SVM yang digunakan mampu mengklasifikasikan suara ke dalam dua kelas, yaitu Percaya Diri (PD) dan Tidak "
|
|
"Percaya Diri (TPD). Berdasarkan hasil pengujian pada dataset yang digunakan, model memperoleh accuracy sebesar "
|
|
"79,17% dan F1 macro sebesar 79,13%."
|
|
),
|
|
paragraph(
|
|
"Aplikasi mobile dapat digunakan untuk registrasi, login, memasukkan data siswa, merekam atau memilih audio, "
|
|
"melakukan prediksi, menyimpan hasil, dan melihat riwayat analisis. Admin web dapat digunakan untuk memantau "
|
|
"dan mengelola data hasil analisis yang tersimpan di database."
|
|
),
|
|
subheading("5.2 Saran"),
|
|
paragraph(
|
|
"Saran untuk pengembangan berikutnya adalah menambah jumlah dataset audio dari lebih banyak siswa agar model "
|
|
"dapat mengenali variasi suara yang lebih luas. Dataset juga sebaiknya direkam dengan kondisi yang lebih "
|
|
"beragam agar model menjadi lebih stabil saat digunakan pada lingkungan nyata."
|
|
),
|
|
paragraph(
|
|
"Penelitian selanjutnya dapat membandingkan metode SVM dengan algoritma lain seperti Random Forest, K-Nearest "
|
|
"Neighbor, atau metode deep learning. Selain itu, aplikasi dapat dikembangkan dengan fitur laporan hasil analisis "
|
|
"dalam bentuk PDF, grafik perkembangan siswa, dan pengaturan admin yang lebih lengkap."
|
|
),
|
|
paragraph(""),
|
|
]
|
|
)
|
|
return items
|
|
|
|
|
|
def patch(xml_bytes: bytes) -> bytes:
|
|
root = ET.fromstring(xml_bytes)
|
|
body = root.find(".//w:body", NS)
|
|
if body is None:
|
|
raise RuntimeError("Body dokumen tidak ditemukan.")
|
|
|
|
use_case_drawing = copy_lampiran_drawing(body, "Use Case")
|
|
flowchart_drawing = copy_lampiran_drawing(body, "Flowchart")
|
|
|
|
children = list(body)
|
|
start = None
|
|
end = None
|
|
for i, child in enumerate(children):
|
|
if child.tag == qn("w:p") and paragraph_text(child).replace(" ", "").startswith("BAB4."):
|
|
start = i
|
|
if child.tag == qn("w:p") and paragraph_text(child) == "DAFTAR PUSTAKA":
|
|
end = i
|
|
break
|
|
if start is None or end is None or end <= start:
|
|
raise RuntimeError("Rentang BAB 4 sampai DAFTAR PUSTAKA tidak ditemukan.")
|
|
|
|
for child in children[start:end]:
|
|
body.remove(child)
|
|
|
|
new_items = build_bab45(use_case_drawing, flowchart_drawing)
|
|
insert_at = list(body).index(children[end])
|
|
for offset, item in enumerate(new_items):
|
|
body.insert(insert_at + offset, item)
|
|
|
|
return ET.tostring(root, encoding="utf-8", xml_declaration=True)
|
|
|
|
|
|
def main() -> None:
|
|
OUTPUT_DOCX.unlink(missing_ok=True)
|
|
with zipfile.ZipFile(INPUT_DOCX, "r") as zin, zipfile.ZipFile(OUTPUT_DOCX, "w", zipfile.ZIP_DEFLATED) as zout:
|
|
for info in zin.infolist():
|
|
data = zin.read(info.filename)
|
|
if info.filename == XML_PATH:
|
|
data = patch(data)
|
|
zout.writestr(info, data)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|