261 lines
11 KiB
Python
261 lines
11 KiB
Python
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 = """<svg xmlns="http://www.w3.org/2000/svg" width="1900" height="1200" viewBox="0 0 1900 1200">
|
|
<rect width="1900" height="1200" fill="#f8fafc"/>
|
|
<text x="950" y="60" text-anchor="middle" font-family="Arial" font-size="42" font-weight="700" fill="#0f172a">Data Flow Diagram Level 1 Sistem ConfiVoice</text>
|
|
<text x="950" y="105" text-anchor="middle" font-family="Arial" font-size="24" fill="#475569">Klasifikasi tingkat percaya diri berdasarkan analisis suara</text>
|
|
<text x="950" y="580" text-anchor="middle" font-family="Arial" font-size="26" fill="#334155">Versi PNG berisi diagram lengkap dengan alur data mobile, API, model SVM, database, dan admin web.</text>
|
|
</svg>
|
|
"""
|
|
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()
|