from PIL import Image, ImageDraw, ImageFont WIDTH = 2200 HEIGHT = 1500 BG = "#f6f3ed" TEXT = "#1d2430" ACCENT = "#1f5f8b" LINE = "#476072" BORDER = "#d7c8b6" LAYER_COLORS = { "presentation": "#eaf4ff", "application": "#fdf2e2", "service": "#eef8ee", "intelligence": "#f7eefb", "data": "#fff8dc", } def get_font(size: int, bold: bool = False): candidates = [] if bold: candidates.extend( [ "C:/Windows/Fonts/arialbd.ttf", "C:/Windows/Fonts/calibrib.ttf", "C:/Windows/Fonts/segoeuib.ttf", ] ) candidates.extend( [ "C:/Windows/Fonts/arial.ttf", "C:/Windows/Fonts/calibri.ttf", "C:/Windows/Fonts/segoeui.ttf", ] ) for path in candidates: try: return ImageFont.truetype(path, size) except OSError: continue return ImageFont.load_default() TITLE_FONT = get_font(42, True) SUBTITLE_FONT = get_font(24) LAYER_FONT = get_font(28, True) BOX_TITLE_FONT = get_font(23, True) BOX_TEXT_FONT = get_font(20) def wrap_text(draw, text, font, max_width): words = text.split() lines = [] current = "" for word in words: trial = word if not current else f"{current} {word}" 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) return lines def draw_box(draw, rect, title, body, fill): x1, y1, x2, y2 = rect draw.rounded_rectangle(rect, radius=22, fill=fill, outline=BORDER, width=3) title_lines = wrap_text(draw, title, BOX_TITLE_FONT, x2 - x1 - 30) body_lines = wrap_text(draw, body, BOX_TEXT_FONT, x2 - x1 - 34) lines = [(line, BOX_TITLE_FONT) for line in title_lines] + [(line, BOX_TEXT_FONT) for line in body_lines] heights = [] for line, font in lines: bbox = draw.textbbox((0, 0), line, font=font) heights.append((bbox[3] - bbox[1]) + 6) total_h = sum(heights) - 6 y = y1 + ((y2 - y1) - total_h) / 2 for (line, font), h in zip(lines, heights): bbox = draw.textbbox((0, 0), line, font=font) w = bbox[2] - bbox[0] x = x1 + ((x2 - x1) - w) / 2 draw.text((x, y), line, font=font, fill=TEXT) y += h def draw_layer(draw, rect, title, fill): x1, y1, x2, y2 = rect draw.rounded_rectangle(rect, radius=28, fill=fill, outline=BORDER, width=4) draw.text((x1 + 24, y1 + 16), title, font=LAYER_FONT, fill=ACCENT) def draw_arrow(draw, start, end, width=5): draw.line([start, end], fill=LINE, width=width) ex, ey = end sx, sy = start dx = ex - sx dy = ey - sy if abs(dx) >= abs(dy): sign = 1 if dx >= 0 else -1 pts = [(ex, ey), (ex - 16 * sign, ey - 8), (ex - 16 * sign, ey + 8)] else: sign = 1 if dy >= 0 else -1 pts = [(ex, ey), (ex - 8, ey - 16 * sign), (ex + 8, ey - 16 * sign)] draw.polygon(pts, fill=LINE) def draw_poly_arrow(draw, points, width=5): for start, end in zip(points, points[1:]): draw.line([start, end], fill=LINE, width=width) draw_arrow(draw, points[-2], points[-1], width) def main(): img = Image.new("RGB", (WIDTH, HEIGHT), BG) draw = ImageDraw.Draw(img) draw.text((70, 40), "6.3 System Architecture Diagram", font=TITLE_FONT, fill=ACCENT) draw.text((72, 95), "High-level layered architecture of the EmpowerHer prototype", font=SUBTITLE_FONT, fill=TEXT) layers = { "presentation": (120, 170, 2080, 360), "application": (120, 395, 2080, 600), "service": (120, 635, 2080, 870), "intelligence": (120, 905, 2080, 1160), "data": (120, 1190, 2080, 1420), } draw_layer(draw, layers["presentation"], "Presentation Layer", LAYER_COLORS["presentation"]) draw_layer(draw, layers["application"], "Application Layer", LAYER_COLORS["application"]) draw_layer(draw, layers["service"], "Service / Business Logic Layer", LAYER_COLORS["service"]) draw_layer(draw, layers["intelligence"], "Intelligence Layer", LAYER_COLORS["intelligence"]) draw_layer(draw, layers["data"], "Data / Knowledge Layer", LAYER_COLORS["data"]) boxes = { "user": (160, 225, 520, 325), "frontend": (720, 215, 1080, 330), "api": (540, 445, 900, 560), "static": (1220, 445, 1580, 560), "chat": (330, 700, 740, 820), "rules": (1020, 690, 1590, 830), "emotion": (180, 965, 560, 1095), "llm": (700, 965, 1080, 1095), "retriever": (1220, 955, 1640, 1105), "kb": (330, 1255, 760, 1365), "history": (1040, 1255, 1470, 1365), } draw_box(draw, boxes["user"], "End User", "Teenage girl using the chatbot through a browser", "#fffdf8") draw_box(draw, boxes["frontend"], "React Frontend", "Chat interface for sending messages and showing replies", "#fffdf8") draw_box(draw, boxes["api"], "Flask Web App / API", "Serves the system and exposes the /chat endpoint", "#fffdf8") draw_box(draw, boxes["static"], "Static Asset Serving", "Delivers the built frontend from the backend container", "#fffdf8") draw_box(draw, boxes["chat"], "ChatService", "Central orchestration of the chatbot request pipeline", "#fffdf8") draw_box(draw, boxes["rules"], "Rule-Based Logic", "Intent detection, topic detection, safety checks, follow-up handling, templates", "#fffdf8") draw_box(draw, boxes["emotion"], "Emotion Classifier", "GoEmotions RoBERTa for emotion detection", "#fffdf8") draw_box(draw, boxes["llm"], "Response Generator", "Flan-T5 model for empathetic response generation", "#fffdf8") draw_box(draw, boxes["retriever"], "KnowledgeBaseRetriever", "Searches local documents using TF-IDF or embeddings", "#fffdf8") draw_box(draw, boxes["kb"], "Local Knowledge Base", "Menstrual-health guidance stored as local text files", "#fffdf8") draw_box(draw, boxes["history"], "Conversation History", "Prior messages used for follow-up context", "#fffdf8") draw_arrow(draw, (520, 275), (720, 275)) draw_poly_arrow(draw, [(900, 330), (900, 390), (720, 390), (720, 445)]) draw_poly_arrow(draw, [(1080, 275), (1400, 275), (1400, 445)]) draw_arrow(draw, (720, 560), (720, 700)) draw_poly_arrow(draw, [(900, 505), (1020, 505), (1020, 740)]) draw_poly_arrow(draw, [(535, 820), (535, 905), (370, 905), (370, 965)]) draw_poly_arrow(draw, [(740, 760), (860, 760), (860, 965)]) draw_poly_arrow(draw, [(1380, 830), (1380, 905), (1430, 905), (1430, 955)]) draw_poly_arrow(draw, [(1300, 830), (1300, 1190), (1255, 1190), (1255, 1255)]) draw_poly_arrow(draw, [(1430, 1105), (1430, 1190), (545, 1190), (545, 1255)]) draw_poly_arrow(draw, [(370, 1095), (370, 1135), (535, 1135), (535, 820)]) draw_poly_arrow(draw, [(890, 1095), (890, 1135), (535, 1135), (535, 820)]) draw_poly_arrow(draw, [(1430, 1105), (1430, 1135), (535, 1135), (535, 820)]) draw_poly_arrow(draw, [(740, 760), (900, 760), (900, 505)]) draw_poly_arrow(draw, [(1580, 505), (1680, 505), (1680, 275), (1080, 275)]) out_path = "d:/FYP IMPLEMENTATION/EmpowerHer_Chatbot/docs/system_architecture_diagram.png" img.save(out_path, format="PNG") print(out_path) if __name__ == "__main__": main()