Spaces:
Sleeping
Sleeping
| 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() | |