from __future__ import annotations from pathlib import Path import textwrap from PIL import Image, ImageDraw, ImageFont OUT_DIR = Path("docs/gambar") PNG_PATH = OUT_DIR / "dfd_confivoice.png" SVG_PATH = OUT_DIR / "dfd_confivoice.svg" def load_font(size: int, bold: bool = False) -> ImageFont.FreeTypeFont: candidates = [ "/System/Library/Fonts/Supplemental/Arial Bold.ttf" if bold else "/System/Library/Fonts/Supplemental/Arial.ttf", "/System/Library/Fonts/Supplemental/Times New Roman Bold.ttf" if bold else "/System/Library/Fonts/Supplemental/Times New Roman.ttf", "/Library/Fonts/Arial Bold.ttf" if bold else "/Library/Fonts/Arial.ttf", ] for path in candidates: if path and Path(path).exists(): return ImageFont.truetype(path, size) return ImageFont.load_default() FONT_TITLE = load_font(42, bold=True) FONT_HEAD = load_font(28, bold=True) FONT_BODY = load_font(24) FONT_SMALL = load_font(20) def draw_wrapped( draw: ImageDraw.ImageDraw, text: str, box: tuple[int, int, int, int], font: ImageFont.ImageFont, fill: str = "#0f172a", align: str = "center", line_spacing: int = 6, ) -> None: x1, y1, x2, y2 = box max_width = x2 - x1 - 28 words = text.split() lines: list[str] = [] current = "" for word in words: trial = f"{current} {word}".strip() if draw.textbbox((0, 0), trial, font=font)[2] <= max_width: current = trial else: if current: lines.append(current) current = word if current: lines.append(current) line_heights = [draw.textbbox((0, 0), line, font=font)[3] for line in lines] total_h = sum(line_heights) + line_spacing * max(0, len(lines) - 1) y = y1 + ((y2 - y1) - total_h) / 2 for line, lh in zip(lines, line_heights): bbox = draw.textbbox((0, 0), line, font=font) if align == "center": x = x1 + ((x2 - x1) - (bbox[2] - bbox[0])) / 2 else: x = x1 + 14 draw.text((x, y), line, font=font, fill=fill) y += lh + line_spacing def round_rect( draw: ImageDraw.ImageDraw, box: tuple[int, int, int, int], fill: str, outline: str, width: int = 3, radius: int = 22, ) -> None: draw.rounded_rectangle(box, radius=radius, fill=fill, outline=outline, width=width) def process(draw: ImageDraw.ImageDraw, box: tuple[int, int, int, int], title: str, desc: str) -> None: round_rect(draw, box, "#e0f2fe", "#0369a1", width=4, radius=28) x1, y1, x2, y2 = box draw_wrapped(draw, title, (x1 + 10, y1 + 12, x2 - 10, y1 + 58), FONT_HEAD, "#082f49") draw.line((x1 + 20, y1 + 70, x2 - 20, y1 + 70), fill="#7dd3fc", width=2) draw_wrapped(draw, desc, (x1 + 10, y1 + 78, x2 - 10, y2 - 10), FONT_SMALL, "#0f172a") def entity(draw: ImageDraw.ImageDraw, box: tuple[int, int, int, int], title: str, desc: str) -> None: round_rect(draw, box, "#fef3c7", "#b45309", width=4, radius=16) x1, y1, x2, y2 = box draw_wrapped(draw, title, (x1 + 8, y1 + 8, x2 - 8, y1 + 50), FONT_HEAD, "#78350f") draw_wrapped(draw, desc, (x1 + 12, y1 + 56, x2 - 12, y2 - 8), FONT_SMALL, "#0f172a") def datastore(draw: ImageDraw.ImageDraw, box: tuple[int, int, int, int], title: str, desc: str) -> None: x1, y1, x2, y2 = box draw.rectangle(box, fill="#dcfce7", outline="#15803d", width=4) draw.line((x1 + 24, y1, x1 + 24, y2), fill="#15803d", width=4) draw_wrapped(draw, title, (x1 + 32, y1 + 6, x2 - 8, y1 + 44), FONT_HEAD, "#14532d") draw_wrapped(draw, desc, (x1 + 32, y1 + 46, x2 - 8, y2 - 8), FONT_SMALL, "#0f172a") def arrow(draw: ImageDraw.ImageDraw, start: tuple[int, int], end: tuple[int, int], label: str, bend: int = 0) -> None: sx, sy = start ex, ey = end if bend: mid = ((sx + ex) // 2, (sy + ey) // 2 + bend) draw.line((sx, sy, mid[0], mid[1], ex, ey), fill="#334155", width=4, joint="curve") lx, ly = mid else: draw.line((sx, sy, ex, ey), fill="#334155", width=4) lx, ly = (sx + ex) // 2, (sy + ey) // 2 # arrow head import math angle = math.atan2(ey - sy, ex - sx) size = 16 left = (ex - size * math.cos(angle - math.pi / 6), ey - size * math.sin(angle - math.pi / 6)) right = (ex - size * math.cos(angle + math.pi / 6), ey - size * math.sin(angle + math.pi / 6)) draw.polygon([end, left, right], fill="#334155") lines = textwrap.wrap(label, width=24) text = "\n".join(lines) bbox = draw.multiline_textbbox((0, 0), text, font=FONT_SMALL, spacing=3) pad = 8 label_box = (lx - (bbox[2] - bbox[0]) / 2 - pad, ly - (bbox[3] - bbox[1]) / 2 - pad, lx + (bbox[2] - bbox[0]) / 2 + pad, ly + (bbox[3] - bbox[1]) / 2 + pad) draw.rounded_rectangle(label_box, radius=8, fill="#ffffff", outline="#cbd5e1", width=2) draw.multiline_text((label_box[0] + pad, label_box[1] + pad), text, font=FONT_SMALL, fill="#0f172a", spacing=3, align="center") def numbered_arrow( draw: ImageDraw.ImageDraw, start: tuple[int, int], end: tuple[int, int], number: str, bend: int = 0, ) -> None: sx, sy = start ex, ey = end if bend: mid = ((sx + ex) // 2, (sy + ey) // 2 + bend) draw.line((sx, sy, mid[0], mid[1], ex, ey), fill="#334155", width=4, joint="curve") lx, ly = mid else: draw.line((sx, sy, ex, ey), fill="#334155", width=4) lx, ly = (sx + ex) // 2, (sy + ey) // 2 import math angle = math.atan2(ey - sy, ex - sx) size = 16 left = (ex - size * math.cos(angle - math.pi / 6), ey - size * math.sin(angle - math.pi / 6)) right = (ex - size * math.cos(angle + math.pi / 6), ey - size * math.sin(angle + math.pi / 6)) draw.polygon([end, left, right], fill="#334155") r = 18 draw.ellipse((lx - r, ly - r, lx + r, ly + r), fill="#ffffff", outline="#0f172a", width=3) draw.text((lx, ly - 1), number, font=FONT_SMALL, fill="#0f172a", anchor="mm") def create_png() -> None: width, height = 2000, 1420 img = Image.new("RGB", (width, height), "#f8fafc") draw = ImageDraw.Draw(img) draw.text((width / 2, 36), "Data Flow Diagram Level 1 Sistem ConfiVoice", font=FONT_TITLE, fill="#0f172a", anchor="ma") draw.text((width / 2, 86), "Klasifikasi tingkat percaya diri berdasarkan analisis suara", font=FONT_BODY, fill="#475569", anchor="ma") user = (70, 245, 390, 435) admin = (70, 800, 390, 980) auth = (560, 155, 930, 335) predict = (560, 405, 930, 610) history = (560, 675, 930, 875) admin_proc = (560, 940, 930, 1135) users = (1220, 155, 1675, 315) model = (1220, 390, 1675, 575) results = (1220, 650, 1725, 835) entity(draw, user, "Pengguna Mobile", "Guru/user melakukan login, input siswa, rekam/pilih audio, prediksi, simpan, dan lihat analisis") entity(draw, admin, "Admin Web", "Admin memantau dashboard, mengelola user, dan mengelola hasil analisis") process(draw, auth, "1.0 Autentikasi", "Registrasi dan login pengguna melalui endpoint /register dan /login") process(draw, predict, "2.0 Prediksi Audio", "Validasi audio, preprocessing, ekstraksi fitur, dan klasifikasi PD/TPD") process(draw, history, "3.0 Simpan & Riwayat", "Menyimpan hasil prediksi dan menampilkan riwayat analisis per siswa") process(draw, admin_proc, "4.0 Kelola Admin", "Menampilkan dashboard, tambah/edit/hapus data prediksi, dan kelola user") datastore(draw, users, "D1 users", "Akun user/admin: id, full_name, username, password_hash") datastore(draw, results, "D2 prediction_results", "Hasil analisis: siswa, gender, label, confidence, probabilitas, indikator suara") datastore(draw, model, "D3 Model SVM", "svm_voice_confidence_model.joblib + fitur suara dari librosa") numbered_arrow(draw, (390, 300), (560, 235), "1") numbered_arrow(draw, (560, 285), (390, 360), "2") numbered_arrow(draw, (930, 230), (1220, 230), "3") numbered_arrow(draw, (1220, 280), (930, 285), "4") numbered_arrow(draw, (390, 365), (560, 505), "5") numbered_arrow(draw, (930, 500), (1220, 485), "6") numbered_arrow(draw, (1220, 545), (930, 575), "7") numbered_arrow(draw, (560, 455), (390, 425), "8") numbered_arrow(draw, (390, 410), (560, 760), "9", bend=45) numbered_arrow(draw, (930, 765), (1220, 735), "10") numbered_arrow(draw, (1220, 800), (930, 835), "11") numbered_arrow(draw, (560, 835), (390, 875), "12") numbered_arrow(draw, (390, 890), (560, 1020), "13") numbered_arrow(draw, (930, 1030), (1220, 795), "14") numbered_arrow(draw, (560, 1080), (390, 930), "15") legend_x, legend_y = 70, 1165 draw.text((legend_x, legend_y - 30), "Keterangan alur data:", font=FONT_HEAD, fill="#0f172a") legend_items = [ "1 login/registrasi", "2 status akun", "3 cek/simpan akun", "4 data user", "5 data siswa dan audio", "6 fitur audio", "7 label PD/TPD + probabilitas", "8 hasil prediksi", "9 simpan/lihat riwayat", "10 simpan hasil", "11 riwayat analisis", "12 data riwayat", "13 login admin", "14 kelola hasil", "15 dashboard", ] col1 = legend_items[:8] col2 = legend_items[8:] for idx, item in enumerate(col1): draw.text((legend_x, legend_y + idx * 25), item, font=FONT_SMALL, fill="#334155") for idx, item in enumerate(col2): draw.text((legend_x + 500, legend_y + idx * 25), item, font=FONT_SMALL, fill="#334155") draw.text((1220, 1210), "PD = Percaya Diri, TPD = Tidak Percaya Diri.", font=FONT_SMALL, fill="#475569") draw.text((1220, 1240), "Database aktif: MySQL/MariaDB confivoice.", font=FONT_SMALL, fill="#475569") OUT_DIR.mkdir(parents=True, exist_ok=True) img.save(PNG_PATH) def create_svg() -> None: # The PNG is the polished deliverable. This SVG is a lightweight fallback/reference. svg = """ Data Flow Diagram Level 1 Sistem ConfiVoice Klasifikasi tingkat percaya diri berdasarkan analisis suara Versi PNG berisi diagram lengkap dengan alur data mobile, API, model SVM, database, dan admin web. """ SVG_PATH.write_text(svg, encoding="utf-8") def main() -> None: create_png() create_svg() print(PNG_PATH) print(SVG_PATH) if __name__ == "__main__": main()