# -*- coding: utf-8 -*- """Colorful diagram assets for defense PPT v2.""" from __future__ import annotations from pathlib import Path from PIL import Image, ImageDraw, ImageFont, ImageFilter OUT = Path(__file__).resolve().parent / "assets_v2" OUT.mkdir(parents=True, exist_ok=True) # Colorful palette C = { "navy": (15, 32, 90), "blue": (37, 99, 235), "cyan": (6, 182, 212), "teal": (20, 184, 166), "green": (34, 197, 94), "lime": (132, 204, 22), "yellow": (234, 179, 8), "orange": (249, 115, 22), "coral": (251, 113, 133), "pink": (236, 72, 153), "purple": (168, 85, 247), "violet": (139, 92, 246), "indigo": (99, 102, 241), "slate": (51, 65, 85), "dark": (15, 23, 42), "muted": (100, 116, 139), "light": (248, 250, 252), "white": (255, 255, 255), "soft": (241, 245, 249), } def font(size, bold=False): cands = [ r"C:\Windows\Fonts\msyhbd.ttc" if bold else r"C:\Windows\Fonts\msyh.ttc", r"C:\Windows\Fonts\simhei.ttf", r"C:\Windows\Fonts\arial.ttf", ] for p in cands: try: return ImageFont.truetype(p, size) except OSError: continue return ImageFont.load_default() def rr(d, xy, r, fill, outline=None, w=2): d.rounded_rectangle(xy, radius=r, fill=fill, outline=outline, width=w) def tc(d, xy, text, f, fill): b = d.textbbox((0, 0), text, font=f) d.text((xy[0] - (b[2] - b[0]) // 2, xy[1] - (b[3] - b[1]) // 2), text, font=f, fill=fill) def tl(d, xy, text, f, fill): d.text(xy, text, font=f, fill=fill) def gradient(w, h, c1, c2, horizontal=False): img = Image.new("RGB", (w, h), c1) px = img.load() for y in range(h): for x in range(w): t = (x / max(w - 1, 1)) if horizontal else (y / max(h - 1, 1)) px[x, y] = tuple(int(c1[i] * (1 - t) + c2[i] * t) for i in range(3)) return img def cover(): w, h = 1920, 1080 img = gradient(w, h, (15, 23, 72), (8, 100, 140)) ov = Image.new("RGBA", (w, h), (0, 0, 0, 0)) d = ImageDraw.Draw(ov) colors = [C["cyan"], C["purple"], C["coral"], C["teal"], C["orange"], C["indigo"]] for i, (cx, cy, r) in enumerate([(200, 180, 200), (1700, 200, 240), (300, 900, 180), (1600, 880, 220), (960, 540, 380)]): col = colors[i % len(colors)] d.ellipse((cx - r, cy - r, cx + r, cy + r), outline=(*col, 70), width=4) pts = [(280, 300), (520, 220), (800, 400), (1100, 280), (1400, 450), (1650, 320), (1200, 700), (700, 750)] for i in range(len(pts) - 1): d.line([pts[i], pts[i + 1]], fill=(*C["cyan"], 90), width=3) for i, p in enumerate(pts): col = colors[i % len(colors)] d.ellipse((p[0] - 10, p[1] - 10, p[0] + 10, p[1] + 10), fill=(*col, 200)) # glass panel d.rounded_rectangle((160, 200, 1760, 880), radius=36, fill=(10, 25, 60, 150), outline=(*C["cyan"], 120), width=3) # colorful bars for i, col in enumerate([C["coral"], C["orange"], C["yellow"], C["teal"], C["blue"], C["purple"]]): d.rounded_rectangle((220 + i * 250, 780, 440 + i * 250, 820), radius=10, fill=(*col, 220)) img = Image.alpha_composite(img.convert("RGBA"), ov).convert("RGB") img.save(OUT / "cover.png", quality=95) def dual_chain(): w, h = 1500, 700 img = Image.new("RGB", (w, h), C["light"]) d = ImageDraw.Draw(img) tl(d, (40, 20), "双核心业务链路", font(28, True), C["dark"]) chains = [ (70, "链路 A · 医学影像 AI 读片", C["blue"], C["cyan"], [("影像检查", "X光/CT/MRI/超声"), ("YOLO 检测", "框选+置信度"), ("结构化报告", "所见/印象/建议")]), (390, "链路 B · 电子病历辅助决策", C["purple"], C["pink"], [("病历录入", "主诉→诊断→用药"), ("RAG + LLM", "知识检索生成"), ("决策建议", "治疗/护理/随访")]), ] for y, title, c1, c2, nodes in chains: rr(d, (30, y, w - 30, y + 280), 20, C["white"], c1, 3) d.rectangle((30, y, 48, y + 280), fill=c1) tl(d, (60, y + 18), title, font(24, True), c1) bw, bh = 360, 120 gap = 40 for i, (a, b) in enumerate(nodes): x = 70 + i * (bw + gap) col = c1 if i % 2 == 0 else c2 rr(d, (x, y + 90, x + bw, y + 90 + bh), 16, col) tc(d, (x + bw // 2, y + 125), a, font(22, True), C["white"]) tc(d, (x + bw // 2, y + 165), b, font(16), (240, 250, 255)) if i < 2: d.polygon([(x + bw + 8, y + 150), (x + bw + 32, y + 140), (x + bw + 32, y + 160)], fill=c1) img.save(OUT / "dual_chain.png", quality=95) def goals_radar(): """Five goal cards as colorful strip.""" w, h = 1500, 320 img = Image.new("RGB", (w, h), C["light"]) d = ImageDraw.Draw(img) goals = [ ("业务闭环", "登录·患者·影像\n病历·预约·用户", C["blue"]), ("AI 可演示", "YOLO 实检\nLLM/模板降级", C["purple"]), ("前后端分离", "Vue3 + Boot\n+ FastAPI", C["teal"]), ("可离线实训", "默认 H2\nAI 可降级", C["orange"]), ("安全可讲", "JWT·角色\nBCrypt·双端", C["coral"]), ] for i, (t, desc, col) in enumerate(goals): x = 25 + i * 295 rr(d, (x, 30, x + 280, 290), 18, C["white"], col, 3) rr(d, (x, 30, x + 280, 100), 18, col) d.rectangle((x, 80, x + 280, 100), fill=col) tc(d, (x + 140, 65), t, font(20, True), C["white"]) for j, ln in enumerate(desc.split("\n")): tc(d, (x + 140, 150 + j * 36), ln, font(16), C["dark"]) img.save(OUT / "goals.png", quality=95) def scenes(): w, h = 1500, 380 img = Image.new("RGB", (w, h), C["light"]) d = ImageDraw.Draw(img) cards = [ ("影像科", "上传胸片→AI诊断\n标注框·置信度·报告", C["cyan"]), ("临床医生", "病历关键词→建议\n治疗·护理·随访·RAG", C["blue"]), ("管理员", "LLM配置·知识库\nYOLO权重·用户", C["purple"]), ("挂号窗口", "预约状态流转\n预约→确认→完成", C["orange"]), ] for i, (t, desc, col) in enumerate(cards): x = 20 + i * 370 rr(d, (x, 25, x + 350, 350), 20, C["white"], col, 3) rr(d, (x, 25, x + 350, 110), 20, col) d.rectangle((x, 90, x + 350, 110), fill=col) tc(d, (x + 175, 70), t, font(24, True), C["white"]) d.ellipse((x + 145, 135, x + 205, 195), fill=col) tc(d, (x + 175, 165), str(i + 1), font(22, True), C["white"]) for j, ln in enumerate(desc.split("\n")): tc(d, (x + 175, 230 + j * 32), ln, font(15), C["dark"]) img.save(OUT / "scenes.png", quality=95) def pain_transform(): w, h = 1400, 520 img = Image.new("RGB", (w, h), C["light"]) d = ImageDraw.Draw(img) tl(d, (30, 15), "从教学原型到三端 AI 混合架构", font(24, True), C["dark"]) rr(d, (40, 70, 520, 480), 18, C["white"], C["coral"], 3) rr(d, (40, 70, 520, 130), 18, C["coral"]) d.rectangle((40, 110, 520, 130), fill=C["coral"]) tc(d, (280, 100), "早期痛点", font(22, True), C["white"]) for i, t in enumerate(["业务割裂 · 缺 360° 档案", "AI 假数据 · 无真实检测", "Thymeleaf 前后端耦合", "Session 权限薄弱", "依赖失败整链中断"]): tl(d, (70, 160 + i * 55), "● " + t, font(18), C["dark"]) # arrow d.polygon([(560, 250), (680, 220), (680, 240), (780, 240), (780, 260), (680, 260), (680, 280)], fill=C["teal"]) rr(d, (820, 70, 1360, 480), 18, C["white"], C["teal"], 3) rr(d, (820, 70, 1360, 130), 18, C["teal"]) d.rectangle((820, 110, 1360, 130), fill=C["teal"]) tc(d, (1090, 100), "当前实现", font(22, True), C["white"]) for i, t in enumerate(["患者 360° 统一视图", "YOLO 实检 + LLM/RAG", "Vue3 SPA + REST", "JWT + 角色双端守卫", "AI 宕机可规则降级"]): tl(d, (850, 160 + i * 55), "● " + t, font(18), C["dark"]) img.save(OUT / "pain.png", quality=95) def priority(): w, h = 1500, 780 img = Image.new("RGB", (w, h), C["light"]) d = ImageDraw.Draw(img) tl(d, (30, 15), "功能需求优先级看板", font(26, True), C["dark"]) cols = [ ("P0 必须具备", C["coral"], [ "F-01 登录/改密 JWT+BCrypt", "F-02 患者 CRUD + 360°", "F-03 影像 CRUD + 上传", "F-04 AI 诊断状态机", "F-05 病历 + 辅助决策", "F-06 角色权限双端", ]), ("P1 重要增强", C["orange"], [ "F-07 预约挂号全流程", "F-08 仪表盘可视化", "F-09 AI 对话助手", "F-10 知识库 RAG", "F-11 AI/LLM 配置", "F-12 YOLO 权重管理", ]), ("P2 体验健壮", C["teal"], [ "F-13 AI 不可用降级", "F-14 弹窗 append-to-body", "F-15 报告分段可读", "F-16 路由过渡体验", ]), ] for i, (title, col, items) in enumerate(cols): x = 30 + i * 490 rr(d, (x, 70, x + 460, h - 30), 18, C["white"], col, 3) rr(d, (x, 70, x + 460, 140), 18, col) d.rectangle((x, 120, x + 460, 140), fill=col) tc(d, (x + 230, 105), title, font(22, True), C["white"]) yy = 170 for it in items: rr(d, (x + 20, yy, x + 440, yy + 70), 12, C["soft"], col, 1) tl(d, (x + 40, yy + 22), it, font(15), C["dark"]) yy += 85 img.save(OUT / "priority.png", quality=95) def tech_stack(): w, h = 1500, 620 img = Image.new("RGB", (w, h), C["light"]) d = ImageDraw.Draw(img) tl(d, (30, 15), "三层技术栈(版本对齐文档)", font(26, True), C["dark"]) layers = [ ("前端 SPA :5173", C["blue"], "Vue 3.5.10 · Vite 5.4.8 · Element Plus 2.8.4 · Pinia · Router · Axios · ECharts · marked"), ("业务后端 :8080", C["purple"], "Spring Boot 3.3.4 · Java 17 · Security + jjwt 0.12.6 · JPA · H2/MySQL · Lombok · Maven"), ("AI 微服务 :8001", C["teal"], "FastAPI ≥0.110 · Ultralytics YOLO · OpenCV · LangChain ≥0.2 · DeepSeek 等 OpenAI 兼容"), ] for i, (t, col, desc) in enumerate(layers): y = 70 + i * 170 rr(d, (40, y, w - 40, y + 150), 18, C["white"], col, 4) rr(d, (40, y, 280, y + 150), 18, col) d.rectangle((220, y, 280, y + 150), fill=col) tc(d, (160, y + 75), t, font(18, True), C["white"]) tl(d, (310, y + 55), desc, font(16), C["dark"]) img.save(OUT / "tech.png", quality=95) def architecture(): w, h = 1600, 900 img = Image.new("RGB", (w, h), C["light"]) d = ImageDraw.Draw(img) tl(d, (40, 20), "系统逻辑架构", font(28, True), C["dark"]) def box(x, y, bw, bh, title, lines, col): rr(d, (x, y, x + bw, y + bh), 16, C["white"], col, 3) rr(d, (x, y, x + bw, y + 48), 16, col) d.rectangle((x, y + 30, x + bw, y + 48), fill=col) tc(d, (x + bw // 2, y + 24), title, font(18, True), C["white"]) yy = y + 65 for ln in lines: tc(d, (x + bw // 2, yy), ln, font(14), C["dark"]) yy += 24 box(560, 70, 480, 110, "浏览器 Vue SPA", ["localhost:5173 · Element Plus · ECharts"], C["blue"]) d.line([(800, 180), (800, 230)], fill=C["slate"], width=3) d.polygon([(800, 240), (790, 225), (810, 225)], fill=C["slate"]) tl(d, (820, 200), "/api Vite 代理", font(14), C["muted"]) box(480, 250, 640, 130, "Spring Boot 业务端", ["8080 · JWT / JPA / 文件 · 鉴权落库 · 降级"], C["purple"]) d.line([(640, 380), (640, 450)], fill=C["blue"], width=3) d.line([(1000, 380), (1000, 450)], fill=C["teal"], width=3) d.line([(640, 450), (1000, 450)], fill=C["muted"], width=2) d.polygon([(640, 465), (630, 450), (650, 450)], fill=C["blue"]) d.polygon([(1000, 465), (990, 450), (1010, 450)], fill=C["teal"]) box(280, 480, 420, 150, "H2 / MySQL", ["默认 H2 内存 · 可选 MySQL", "DataInitializer 种子数据"], C["orange"]) box(900, 480, 480, 170, "FastAPI AI 服务", ["8001 · YOLO · RAG · LLM", "失败可降级到模板规则"], C["teal"]) box(820, 720, 200, 100, "权重 pt", ["best.pt 等"], C["indigo"]) box(1040, 720, 200, 100, "知识 MD", ["高血压/肺炎…"], C["pink"]) box(1260, 720, 220, 100, "外部 LLM", ["DeepSeek 兼容"], C["green"]) d.line([(1140, 650), (920, 720)], fill=C["muted"], width=2) d.line([(1140, 650), (1140, 720)], fill=C["muted"], width=2) d.line([(1140, 650), (1370, 720)], fill=C["muted"], width=2) rr(d, (40, 700, 760, 860), 14, C["white"], C["violet"], 2) tl(d, (60, 720), "调用原则", font(18, True), C["violet"]) for i, p in enumerate(["1. 浏览器只访问业务后端", "2. AI 能力集中在 FastAPI", "3. 配置单向同步管理端→AI", "4. 失败可降级保证演示"]): tl(d, (60, 760 + i * 24), p, font(14), C["dark"]) img.save(OUT / "architecture.png", quality=95) def feature_panorama(): w, h = 1500, 800 img = Image.new("RGB", (w, h), C["light"]) d = ImageDraw.Draw(img) tl(d, (30, 15), "智慧医院功能全景", font(26, True), C["dark"]) cols = [ ("身份与权限", C["blue"], ["登录 / JWT", "修改密码", "角色菜单", "头像上传"]), ("业务主数据", C["purple"], ["患者管理", "用户管理", "知识库文档"]), ("诊疗业务", C["teal"], ["影像检查", "电子病历", "预约挂号", "仪表盘统计"]), ("智能能力", C["coral"], ["YOLO 影像检测", "AI 诊断报告", "EMR 决策+RAG", "AI 对话助手", "LLM/YOLO 管理"]), ] for i, (title, col, items) in enumerate(cols): x = 30 + i * 370 rr(d, (x, 70, x + 350, h - 30), 18, C["white"], col, 3) rr(d, (x, 70, x + 350, 140), 18, col) d.rectangle((x, 120, x + 350, 140), fill=col) tc(d, (x + 175, 105), title, font(20, True), C["white"]) yy = 170 for it in items: rr(d, (x + 20, yy, x + 330, yy + 70), 12, C["soft"]) d.ellipse((x + 40, yy + 22, x + 66, yy + 48), fill=col) tl(d, (x + 80, yy + 22), it, font(16), C["dark"]) yy += 85 img.save(OUT / "features.png", quality=95) def _arrow(d, x1, y1, x2, y2, fill, width=3): """Orthogonal-friendly straight segment with arrow head at end.""" d.line([(x1, y1), (x2, y2)], fill=fill, width=width) # arrow head if x2 == x1 and y2 > y1: # down d.polygon([(x2, y2), (x2 - 7, y2 - 12), (x2 + 7, y2 - 12)], fill=fill) elif x2 == x1 and y2 < y1: # up d.polygon([(x2, y2), (x2 - 7, y2 + 12), (x2 + 7, y2 + 12)], fill=fill) elif y2 == y1 and x2 > x1: # right d.polygon([(x2, y2), (x2 - 12, y2 - 7), (x2 - 12, y2 + 7)], fill=fill) elif y2 == y1 and x2 < x1: # left d.polygon([(x2, y2), (x2 + 12, y2 - 7), (x2 + 12, y2 + 7)], fill=fill) def _ortho(d, path, fill, width=3, arrow=True): """Draw polyline path [(x,y),...]; optional arrow on last segment.""" for i in range(len(path) - 1): x1, y1 = path[i] x2, y2 = path[i + 1] if i == len(path) - 2 and arrow: _arrow(d, x1, y1, x2, y2, fill, width) else: d.line([(x1, y1), (x2, y2)], fill=fill, width=width) def imaging_flow(): """Left-to-right pipeline with orthogonal connectors only.""" w, h = 1600, 880 img = Image.new("RGB", (w, h), C["light"]) d = ImageDraw.Draw(img) tl(d, (30, 15), "AI 影像诊断核心流程", font(26, True), C["dark"]) # notes card top-right (no lines through it) rr(d, (980, 50, 1560, 220), 14, C["white"], C["violet"], 2) tl(d, (1000, 65), "技术要点", font(18, True), C["violet"]) for i, t in enumerate([ "· AsyncConfig 线程池,避免阻塞 HTTP", "· 前端轮询有上限,卸载时清理", "· AI 不可用 → Spring 本地规则降级", "· 报告分段:所见 / 印象 / 建议 / 声明", ]): tl(d, (1000, 105 + i * 28), t, font(14), C["dark"]) # Compact pipeline with guaranteed gaps between boxes bh = 88 # (id, x, y, w, text, color) spec = [ (0, 40, 300, 175, "新建影像\n上传图片", C["blue"]), (1, 255, 300, 155, "PENDING", C["muted"]), (2, 450, 300, 170, "点击AI诊断", C["orange"]), (3, 660, 300, 170, "ANALYZING", C["coral"]), (4, 870, 300, 170, "Spring\n@Async", C["purple"]), (5, 1080, 300, 180, "FastAPI\nanalyze", C["teal"]), (6, 900, 500, 180, "YOLO 检测\n标注图", C["indigo"]), (7, 1160, 500, 180, "报告生成\nLLM/模板", C["cyan"]), (8, 780, 700, 180, "写结果\n更新记录", C["green"]), (9, 1020, 700, 190, "COMPLETED\n/ ERROR", C["green"]), (10, 1270, 700, 200, "前端轮询\n报告弹窗", C["blue"]), ] boxes = {} for i, x, y, wbox, text, col in spec: rr(d, (x, y, x + wbox, y + bh), 14, col) lines = text.split("\n") for j, ln in enumerate(lines): tc(d, (x + wbox // 2, y + (bh // 2 - 12) + j * 26), ln, font(14, True), C["white"]) boxes[i] = (x, y, wbox, bh) def mid_right(i): x, y, wbox, bh_ = boxes[i] return x + wbox, y + bh_ // 2 def mid_left(i): x, y, wbox, bh_ = boxes[i] return x, y + bh_ // 2 def mid_bottom(i): x, y, wbox, bh_ = boxes[i] return x + wbox // 2, y + bh_ def mid_top(i): x, y, wbox, bh_ = boxes[i] return x + wbox // 2, y # main chain for a, b in [(0, 1), (1, 2), (2, 3), (3, 4), (4, 5)]: x1, y1 = mid_right(a) x2, y2 = mid_left(b) gap = x2 - x1 if gap > 16: _arrow(d, x1 + 3, y1, x2 - 3, y2, C["slate"], 3) # FastAPI -> split bus -> YOLO / Report fx, fy = mid_bottom(5) bus_y = 450 d.line([(fx, fy), (fx, bus_y)], fill=C["slate"], width=3) yx, _ = mid_top(6) rx, _ = mid_top(7) d.line([(min(yx, fx), bus_y), (max(rx, fx), bus_y)], fill=C["slate"], width=3) _arrow(d, yx, bus_y, yx, mid_top(6)[1] - 2, C["slate"], 3) _arrow(d, rx, bus_y, rx, mid_top(7)[1] - 2, C["slate"], 3) # merge YOLO + Report -> 写结果 merge_y = 640 d.line([(mid_bottom(6)[0], mid_bottom(6)[1]), (mid_bottom(6)[0], merge_y)], fill=C["slate"], width=3) d.line([(mid_bottom(7)[0], mid_bottom(7)[1]), (mid_bottom(7)[0], merge_y)], fill=C["slate"], width=3) d.line([(mid_top(8)[0], merge_y), (mid_bottom(7)[0], merge_y)], fill=C["slate"], width=3) _arrow(d, mid_top(8)[0], merge_y, mid_top(8)[0], mid_top(8)[1] - 2, C["slate"], 3) # bottom chain for a, b in [(8, 9), (9, 10)]: x1, y1 = mid_right(a) x2, y2 = mid_left(b) if x2 - x1 > 12: _arrow(d, x1 + 3, y1, x2 - 3, y2, C["slate"], 3) img.save(OUT / "imaging_flow.png", quality=95) def decision_flow(): w, h = 1500, 500 img = Image.new("RGB", (w, h), C["light"]) d = ImageDraw.Draw(img) tl(d, (30, 15), "电子病历辅助决策流程", font(24, True), C["dark"]) nodes = [ ("保存病历", C["blue"]), ("Decision\nSupport", C["purple"]), ("FastAPI\n/report/decision", C["teal"]), ("RAG+LLM\n或模板回退", C["orange"]), ("七类建议\nDialog 展示", C["coral"]), ] boxes = [] for i, (t, col) in enumerate(nodes): x = 40 + i * 290 rr(d, (x, 70, x + 250, 200), 16, col) for j, ln in enumerate(t.split("\n")): tc(d, (x + 125, 120 + j * 30), ln, font(16, True), C["white"]) boxes.append((x, 70, 250, 130)) if i < 4: # edge-to-edge arrow in the gap only x1 = x + 250 + 6 x2 = x + 290 - 6 midy = 70 + 65 _arrow(d, x1, midy, x2, midy, C["slate"], 3) # vertical from last node down to output row bus last = boxes[-1] lx = last[0] + last[2] // 2 d.line([(lx, last[1] + last[3]), (lx, 300)], fill=C["slate"], width=3) d.line([(50, 300), (1450, 300)], fill=C["slate"], width=3) outs = [("治疗", C["blue"]), ("用药", C["purple"]), ("护理", C["teal"]), ("随访", C["green"]), ("风险", C["orange"]), ("冲突", C["coral"]), ("RAG", C["indigo"])] for i, (o, col) in enumerate(outs): x = 50 + i * 205 _arrow(d, x + 90, 300, x + 90, 330, C["slate"], 2) rr(d, (x, 335, x + 180, 450), 12, col) tc(d, (x + 90, 392), o, font(16, True), C["white"]) img.save(OUT / "decision.png", quality=95) def state_machine(): w, h = 1400, 280 img = Image.new("RGB", (w, h), C["light"]) d = ImageDraw.Draw(img) tl(d, (30, 15), "影像诊断状态机", font(22, True), C["dark"]) states = [ ("PENDING", "已登记待诊断", C["muted"]), ("ANALYZING", "诊断进行中", C["orange"]), ("COMPLETED", "成功可看报告", C["green"]), ("ERROR", "失败", C["coral"]), ] for i, (n, desc, col) in enumerate(states): x = 50 + i * 340 rr(d, (x, 70, x + 280, 220), 18, col) tc(d, (x + 140, 120), n, font(20, True), C["white"]) tc(d, (x + 140, 170), desc, font(14), C["white"]) if i < 3: d.polygon([(x + 290, 140), (x + 325, 130), (x + 325, 150)], fill=C["slate"]) img.save(OUT / "state.png", quality=95) def yolo_concept(): w, h = 1000, 560 img = Image.new("RGB", (w, h), (20, 28, 48)) d = ImageDraw.Draw(img) tc(d, (250, 30), "原始影像(示意)", font(18, True), C["cyan"]) tc(d, (750, 30), "YOLO 标注(示意)", font(18, True), C["coral"]) rr(d, (40, 60, 460, 500), 12, (30, 40, 60)) rr(d, (540, 60, 960, 500), 12, (30, 40, 60)) d.ellipse((90, 120, 220, 400), outline=(160, 190, 210), width=2) d.ellipse((250, 120, 380, 400), outline=(160, 190, 210), width=2) d.ellipse((590, 120, 720, 400), outline=(160, 190, 210), width=2) d.ellipse((750, 120, 880, 400), outline=(160, 190, 210), width=2) d.rectangle((600, 180, 700, 270), outline=C["teal"], width=3) tl(d, (600, 155), "findings 0.87", font(14), C["teal"]) d.rectangle((780, 250, 870, 330), outline=C["orange"], width=3) tl(d, (760, 225), "nodule 0.76", font(14), C["orange"]) tl(d, (40, 520), "仅教学演示概念图,非真实患者数据", font(13), C["muted"]) img.save(OUT / "yolo.png", quality=95) def team_overview(): """Six members in 2x3 grid — orthogonal tree, no diagonals through cards.""" w, h = 1500, 740 img = Image.new("RGB", (w, h), C["light"]) d = ImageDraw.Draw(img) tl(d, (30, 12), "六人协作总览(按工程模块划分)", font(26, True), C["dark"]) # hub hub = (450, 50, 1050, 115) rr(d, hub, 16, C["navy"]) tc(d, (750, 82), "Smart Hospital · 6 人三端协作", font(20, True), C["white"]) # column centers and card geometry cols_x = [40, 520, 1000] # left of cards card_w, card_h = 460, 200 top_y, bot_y = 175, 460 cxs = [x + card_w // 2 for x in cols_x] # 270, 750, 1230 # tree connectors (draw first, under cards visually by being outside card bodies) # hub bottom center down to bus bus_y = 145 d.line([(750, 115), (750, bus_y)], fill=C["slate"], width=3) d.line([(cxs[0], bus_y), (cxs[2], bus_y)], fill=C["slate"], width=3) for cx in cxs: d.line([(cx, bus_y), (cx, top_y)], fill=C["slate"], width=3) # vertical between top and bottom cards in each column for cx, x in zip(cxs, cols_x): d.line([(cx, top_y + card_h), (cx, bot_y)], fill=C["slate"], width=3) members = [ (cols_x[0], top_y, "成员A", "前端基础", "布局 · 登录 · 权限 · Pinia", C["blue"], "前端层"), (cols_x[1], top_y, "成员B", "前端业务", "仪表盘 · 患者360° · 预约", C["cyan"], "前端层"), (cols_x[2], top_y, "成员C", "前端智能", "影像 · 病历决策 · AI助手", C["purple"], "前端层"), (cols_x[0], bot_y, "成员D", "后端主数据", "JWT · 患者 · 用户 · 预约", C["orange"], "业务后端"), (cols_x[1], bot_y, "成员E", "后端 AI 桥", "异步诊断 · Client · 降级", C["coral"], "业务后端"), (cols_x[2], bot_y, "成员F", "AI 微服务", "YOLO · RAG · LLM · 权重", C["teal"], "AI 服务"), ] for x, y, name, role, desc, col, layer in members: rr(d, (x, y, x + card_w, y + card_h), 18, C["white"], col, 3) rr(d, (x, y, x + card_w, y + 58), 18, col) d.rectangle((x, y + 42, x + card_w, y + 58), fill=col) tc(d, (x + card_w // 2, y + 30), f"{name} · {role}", font(17, True), C["white"]) tc(d, (x + card_w // 2, y + 105), desc, font(15), C["dark"]) rr(d, (x + 140, y + 145, x + 320, y + 178), 10, col) tc(d, (x + card_w // 2, y + 161), layer, font(13, True), C["white"]) tl(d, (40, 690), "树状正交连线:总枢纽 → 三列 → 上下两排;连线只走卡片外空隙,不穿过文字", font(14), C["muted"]) img.save(OUT / "team.png", quality=95) def team_swimlane(): """Wide short swimlane (fits full PPT width without looking tiny).""" w, h = 1600, 360 # ~4.4:1 — scales to full slide width img = Image.new("RGB", (w, h), C["light"]) d = ImageDraw.Draw(img) tl(d, (20, 8), "六人分工泳道 · 交付物对照(全宽)", font(20, True), C["dark"]) rows = [ ("A 前端基础", C["blue"], "BasicLayout · 登录 · 路由守卫 · Pinia · 个人中心"), ("B 前端业务", C["cyan"], "Dashboard · Patients 360° · Appointments · ECharts"), ("C 前端智能", C["purple"], "Imaging 报告弹窗 · EMR 决策 Dialog · AI 助手 · 知识库页"), ("D 后端主数据", C["orange"], "Security/JWT · User · Patient · Appointment · Result"), ("E 后端 AI 桥", C["coral"], "AIDiagnosis 异步 · AiServiceClient · 决策对接 · 降级"), ("F AI 微服务", C["teal"], "YOLO · /imaging/analyze · RAG · LLM · 权重/配置"), ] row_h, gap, top = 48, 6, 42 for i, (name, col, desc) in enumerate(rows): y = top + i * (row_h + gap) rr(d, (20, y, 280, y + row_h), 10, col) tc(d, (150, y + row_h // 2), name, font(15, True), C["white"]) rr(d, (295, y, 1580, y + row_h), 10, C["white"], col, 2) tl(d, (315, y + 12), desc, font(15), C["dark"]) img.save(OUT / "team_lane.png", quality=95) def module_tree(): w, h = 1400, 620 img = Image.new("RGB", (w, h), C["light"]) d = ImageDraw.Draw(img) tl(d, (30, 15), "仓库三工程结构", font(24, True), C["dark"]) roots = [ (40, "frontend/", C["blue"], ["api/ HTTP", "views/ 页面", "router 守卫", "stores Pinia"]), (480, "smart-hospital/", C["purple"], ["controller/service", "security JWT", "uploads 文件", "data/ai 配置"]), (920, "ai-service/", C["teal"], ["api imaging/rag", "yolo · llm", "knowledge MD", "weights pt"]), ] for x, title, col, items in roots: rr(d, (x, 70, x + 400, 580), 18, C["white"], col, 3) rr(d, (x, 70, x + 400, 140), 18, col) d.rectangle((x, 120, x + 400, 140), fill=col) tc(d, (x + 200, 105), title, font(20, True), C["white"]) yy = 170 for it in items: rr(d, (x + 30, yy, x + 370, yy + 80), 12, C["soft"]) tl(d, (x + 55, yy + 25), it, font(16), C["dark"]) yy += 95 img.save(OUT / "modules.png", quality=95) def demo_path(): w, h = 1500, 300 img = Image.new("RGB", (w, h), C["light"]) d = ImageDraw.Draw(img) tl(d, (20, 10), "推荐演示剧本(7 步)", font(20, True), C["dark"]) steps = [ ("1", "doctor1\n登录", C["blue"]), ("2", "患者\n360°", C["cyan"]), ("3", "影像\nAI诊断", C["purple"]), ("4", "病历\n决策", C["teal"]), ("5", "预约\n流转", C["orange"]), ("6", "admin\n配置", C["coral"]), ("7", "降级\n验证", C["indigo"]), ] for i, (n, t, col) in enumerate(steps): x = 40 + i * 210 d.ellipse((x + 55, 55, x + 115, 115), fill=col) tc(d, (x + 85, 85), n, font(20, True), C["white"]) for j, ln in enumerate(t.split("\n")): tc(d, (x + 85, 140 + j * 28), ln, font(14), C["dark"]) if i < 6: d.line([(x + 125, 85), (x + 195, 85)], fill=C["muted"], width=3) img.save(OUT / "demo.png", quality=95) def closing(): w, h = 1920, 1080 img = gradient(w, h, (30, 20, 80), (10, 90, 120)) ov = Image.new("RGBA", (w, h), (0, 0, 0, 0)) d = ImageDraw.Draw(ov) for i, col in enumerate([C["cyan"], C["purple"], C["coral"], C["teal"], C["orange"], C["blue"]]): ang = i * 60 import math cx = 960 + int(280 * math.cos(math.radians(ang))) cy = 480 + int(200 * math.sin(math.radians(ang))) d.ellipse((cx - 40, cy - 40, cx + 40, cy + 40), fill=(*col, 160)) d.ellipse((860, 380, 1060, 580), outline=(*C["cyan"], 180), width=5) d.rounded_rectangle((940, 430, 980, 530), radius=6, fill=(*C["teal"], 220)) d.rounded_rectangle((910, 460, 1010, 500), radius=6, fill=(*C["teal"], 220)) img = Image.alpha_composite(img.convert("RGBA"), ov).convert("RGB") img.save(OUT / "closing.png", quality=95) def nfr_icons(): w, h = 1500, 280 img = Image.new("RGB", (w, h), C["light"]) d = ImageDraw.Draw(img) items = [ ("性能", "@Async 异步\n有限轮询", C["blue"]), ("可用性", "H2 开箱\n种子数据", C["teal"]), ("安全", "JWT·BCrypt\nCORS", C["purple"]), ("可维护", "Result\n全局异常", C["orange"]), ("可扩展", "AI 独立\n进程解耦", C["coral"]), ("兼容", "OpenAI\n协议可切换", C["indigo"]), ] for i, (t, desc, col) in enumerate(items): x = 20 + i * 245 rr(d, (x, 25, x + 230, 250), 16, C["white"], col, 3) rr(d, (x, 25, x + 230, 90), 16, col) d.rectangle((x, 70, x + 230, 90), fill=col) tc(d, (x + 115, 55), t, font(18, True), C["white"]) for j, ln in enumerate(desc.split("\n")): tc(d, (x + 115, 130 + j * 35), ln, font(14), C["dark"]) img.save(OUT / "nfr.png", quality=95) def er_mini(): """ Clean ER with orthogonal bus connectors — lines never cross through card text. Layout (文档 8.1 / 8.4): User ______|______ | | | Imaging EMR Appointment | | | +------+------+ | Patient Imaging | AIResult """ w, h = 1200, 640 img = Image.new("RGB", (w, h), C["light"]) d = ImageDraw.Draw(img) tl(d, (24, 12), "核心实体关系(正交连线)", font(22, True), C["dark"]) def ent(x, y, bw, title, fields, col): bh = 44 + 22 * len(fields) + 14 rr(d, (x, y, x + bw, y + bh), 12, C["white"], col, 2) rr(d, (x, y, x + bw, y + 40), 12, col) d.rectangle((x, y + 28, x + bw, y + 40), fill=col) tc(d, (x + bw // 2, y + 20), title, font(16, True), C["white"]) yy = y + 52 for f in fields: tl(d, (x + 16, yy), f, font(13), C["dark"]) yy += 22 # return box edges return {"x": x, "y": y, "w": bw, "h": bh, "cx": x + bw // 2, "cy": y + bh // 2, "top": (x + bw // 2, y), "bottom": (x + bw // 2, y + bh), "left": (x, y + bh // 2), "right": (x + bw, y + bh // 2)} BW = 220 user = ent(490, 50, BW, "User", ["ADMIN/DOCTOR/RADIO", "BCrypt 密码"], C["purple"]) imaging = ent(120, 250, BW, "Imaging", ["类型/状态", "图像 URL"], C["teal"]) emr = ent(490, 250, BW, "EMR", ["主诉→随访", "决策关联"], C["orange"]) appt = ent(860, 250, BW, "Appointment", ["状态机", "按日筛选"], C["indigo"]) patient = ent(490, 450, BW, "Patient", ["档案字段", "既往史"], C["blue"]) ai = ent(120, 450, BW, "AIResult", ["置信度/检测框", "完整报告"], C["coral"]) line_c = C["slate"] # User bottom -> horizontal bus -> drop to each mid entity top bus_y = 200 ux, uy = user["bottom"] d.line([(ux, uy), (ux, bus_y)], fill=line_c, width=3) # bus spans imaging.cx to appt.cx d.line([(imaging["cx"], bus_y), (appt["cx"], bus_y)], fill=line_c, width=3) for box in (imaging, emr, appt): _arrow(d, box["cx"], bus_y, box["cx"], box["top"][1] - 2, line_c, 3) # Mid entities bottom -> lower bus -> Patient top bus2_y = 400 for box in (imaging, emr, appt): d.line([(box["cx"], box["bottom"][1]), (box["cx"], bus2_y)], fill=line_c, width=3) d.line([(imaging["cx"], bus2_y), (appt["cx"], bus2_y)], fill=line_c, width=3) _arrow(d, patient["cx"], bus2_y, patient["cx"], patient["top"][1] - 2, line_c, 3) # Imaging → AIResult: continuous vertical (bus2 crosses same x — OK for tree) ix = imaging["cx"] d.line([(ix, imaging["bottom"][1]), (ix, ai["top"][1] - 2)], fill=line_c, width=3) _arrow(d, ix, ai["top"][1] - 14, ix, ai["top"][1] - 2, line_c, 3) # 1:1 label placed to the LEFT of the stem, clear of bus tl(d, (ix - 48, (imaging["bottom"][1] + bus2_y) // 2 - 6), "1:1", font(12, True), C["coral"]) # relationship captions on buses (right side, clear of arrows) tl(d, (740, 178), "医生创建", font(12), C["muted"]) tl(d, (740, 378), "归属患者", font(12), C["muted"]) # legend rr(d, (860, 480, 1160, 600), 12, C["white"], C["soft"], 2) tl(d, (880, 495), "关系说明", font(14, True), C["dark"]) tl(d, (880, 525), "User → 影像/病历/预约", font(12), C["muted"]) tl(d, (880, 550), "Patient ← 三类业务记录", font(12), C["muted"]) tl(d, (880, 575), "Imaging → AIResult (1:1)", font(12), C["muted"]) img.save(OUT / "er.png", quality=95) if __name__ == "__main__": cover() dual_chain() goals_radar() scenes() pain_transform() priority() tech_stack() architecture() feature_panorama() imaging_flow() decision_flow() state_machine() yolo_concept() team_overview() team_swimlane() module_tree() demo_path() closing() nfr_icons() er_mini() print("assets_v2 done ->", OUT)