# -*- coding: utf-8 -*- """Generate diagram assets for Smart Hospital PPT (medical tech blue).""" from __future__ import annotations import math from pathlib import Path from PIL import Image, ImageDraw, ImageFont, ImageFilter OUT = Path(__file__).resolve().parent / "assets" OUT.mkdir(parents=True, exist_ok=True) # Palette NAVY = (11, 58, 92) TEAL = (26, 122, 156) ACCENT = (43, 187, 173) LIGHT = (244, 248, 251) WHITE = (255, 255, 255) DARK = (30, 41, 59) MUTED = (100, 116, 139) SOFT = (226, 236, 244) ORANGE = (245, 158, 11) GREEN = (34, 197, 94) RED = (239, 68, 68) PURPLE = (99, 102, 241) def font(size: int, bold: bool = False) -> ImageFont.FreeTypeFont: candidates = [ 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\simsun.ttc", r"C:\Windows\Fonts\arial.ttf", ] for p in candidates: try: return ImageFont.truetype(p, size=size) except OSError: continue return ImageFont.load_default() def round_rect(draw, xy, r, fill, outline=None, width=1): draw.rounded_rectangle(xy, radius=r, fill=fill, outline=outline, width=width) def text_center(draw, xy, text, fnt, fill=WHITE): bbox = draw.textbbox((0, 0), text, font=fnt) tw, th = bbox[2] - bbox[0], bbox[3] - bbox[1] x = xy[0] - tw // 2 y = xy[1] - th // 2 draw.text((x, y), text, font=fnt, fill=fill) def text_left(draw, xy, text, fnt, fill=DARK): draw.text(xy, text, font=fnt, fill=fill) def gradient_bg(w, h, c1=NAVY, c2=TEAL): img = Image.new("RGB", (w, h), c1) px = img.load() for y in range(h): t = y / max(h - 1, 1) r = int(c1[0] * (1 - t) + c2[0] * t) g = int(c1[1] * (1 - t) + c2[1] * t) b = int(c1[2] * (1 - t) + c2[2] * t) for x in range(w): # subtle radial-ish variation edge = abs(x - w / 2) / (w / 2) k = 0.12 * edge px[x, y] = ( max(0, min(255, int(r * (1 - k)))), max(0, min(255, int(g * (1 - k)))), max(0, min(255, int(b * (1 - k)))), ) return img def draw_grid_dots(draw, w, h, step=40, color=(255, 255, 255, 40)): for x in range(0, w, step): for y in range(0, h, step): draw.ellipse((x, y, x + 2, y + 2), fill=color[:3]) def cover_bg(): w, h = 1920, 1080 img = gradient_bg(w, h, NAVY, (8, 90, 110)) overlay = Image.new("RGBA", (w, h), (0, 0, 0, 0)) d = ImageDraw.Draw(overlay) # abstract circles / network for i, (cx, cy, r) in enumerate( [(300, 200, 180), (1600, 250, 220), (1400, 850, 260), (200, 900, 150), (960, 540, 320)] ): alpha = 28 + (i % 3) * 10 d.ellipse((cx - r, cy - r, cx + r, cy + r), outline=(*ACCENT, alpha), width=3) # connecting lines pts = [(320, 260), (700, 400), (1100, 320), (1500, 500), (1200, 700), (600, 720)] for i in range(len(pts) - 1): d.line([pts[i], pts[i + 1]], fill=(*ACCENT, 70), width=2) for p in pts: d.ellipse((p[0] - 8, p[1] - 8, p[0] + 8, p[1] + 8), fill=(*ACCENT, 160)) # soft panel d.rounded_rectangle((120, 180, 1800, 900), radius=28, fill=(11, 40, 70, 120), outline=(*ACCENT, 90), width=2) # medical cross abstract cx, cy = 1600, 780 d.rounded_rectangle((cx - 18, cy - 55, cx + 18, cy + 55), radius=6, fill=(*ACCENT, 180)) d.rounded_rectangle((cx - 55, cy - 18, cx + 55, cy + 18), radius=6, fill=(*ACCENT, 180)) img = Image.alpha_composite(img.convert("RGBA"), overlay).convert("RGB") img.save(OUT / "cover_bg.png", quality=95) print("cover_bg.png") def dual_chain(): w, h = 1400, 720 img = Image.new("RGB", (w, h), LIGHT) d = ImageDraw.Draw(img) f_title = font(28, True) f_node = font(20, True) f_sub = font(16) # title text_left(d, (40, 24), "双核心业务链路", f_title, NAVY) def chain(y, title, color, nodes): round_rect(d, (40, y, w - 40, y + 280), 18, WHITE, SOFT, 2) d.rectangle((40, y, 52, y + 280), fill=color) text_left(d, (70, y + 16), title, f_title, color) n = len(nodes) box_w, box_h = 220, 100 gap = (w - 120 - n * box_w) // max(n - 1, 1) xs = [] for i, (t1, t2) in enumerate(nodes): x = 70 + i * (box_w + gap) xs.append(x + box_w // 2) round_rect(d, (x, y + 90, x + box_w, y + 90 + box_h), 14, color, None) text_center(d, (x + box_w // 2, y + 90 + 38), t1, f_node, WHITE) text_center(d, (x + box_w // 2, y + 90 + 68), t2, f_sub, (230, 250, 248)) if i < n - 1: x1 = x + box_w + 8 x2 = x + box_w + gap - 8 midy = y + 90 + box_h // 2 d.line([(x1, midy), (x2, midy)], fill=color, width=3) d.polygon([(x2, midy), (x2 - 12, midy - 7), (x2 - 12, midy + 7)], fill=color) chain( 70, "链路 A · 医学影像 AI 读片", TEAL, [("影像检查", "上传 X光/CT/MRI/超声"), ("YOLO 检测", "标注框 + 置信度"), ("结构化报告", "所见/印象/建议")], ) chain( 390, "链路 B · 电子病历辅助决策", ACCENT, [("病历录入", "主诉→诊断→用药"), ("RAG + LLM", "知识检索与生成"), ("决策建议", "治疗/护理/随访")], ) img.save(OUT / "dual_chain.png", quality=95) print("dual_chain.png") def feature_panorama(): w, h = 1500, 820 img = Image.new("RGB", (w, h), LIGHT) d = ImageDraw.Draw(img) f_h = font(30, True) f_c = font(22, True) f_i = font(18) text_left(d, (40, 24), "智慧医院功能全景", f_h, NAVY) cols = [ ("身份与权限", NAVY, ["登录 / JWT", "修改密码", "角色菜单", "头像上传"]), ("业务主数据", TEAL, ["患者管理", "用户管理", "知识库文档"]), ("诊疗业务", ACCENT, ["影像检查", "电子病历", "预约挂号", "仪表盘统计"]), ("智能能力", PURPLE, ["YOLO 影像检测", "AI 诊断报告", "EMR 决策 + RAG", "AI 对话助手", "LLM / YOLO 管理"]), ] cw = 340 gap = 20 x0 = 40 for i, (title, color, items) in enumerate(cols): x = x0 + i * (cw + gap) round_rect(d, (x, 90, x + cw, h - 40), 16, WHITE, SOFT, 2) round_rect(d, (x, 90, x + cw, 160), 16, color) # fix bottom of header d.rectangle((x, 140, x + cw, 160), fill=color) text_center(d, (x + cw // 2, 125), title, f_c, WHITE) yy = 190 for it in items: round_rect(d, (x + 20, yy, x + cw - 20, yy + 48), 10, SOFT) d.ellipse((x + 36, yy + 16, x + 52, yy + 32), fill=color) text_left(d, (x + 66, yy + 12), it, f_i, DARK) yy += 58 img.save(OUT / "feature_panorama.png", quality=95) print("feature_panorama.png") def architecture(): w, h = 1600, 900 img = Image.new("RGB", (w, h), LIGHT) d = ImageDraw.Draw(img) f_h = font(28, True) f_b = font(20, True) f_s = font(16) text_left(d, (40, 20), "系统逻辑架构", f_h, NAVY) def box(x, y, bw, bh, title, lines, color): round_rect(d, (x, y, x + bw, y + bh), 14, WHITE, color, 3) round_rect(d, (x, y, x + bw, y + 44), 14, color) d.rectangle((x, y + 28, x + bw, y + 44), fill=color) text_center(d, (x + bw // 2, y + 22), title, f_b, WHITE) yy = y + 58 for ln in lines: text_center(d, (x + bw // 2, yy), ln, f_s, DARK) yy += 26 # top browser box(560, 70, 480, 120, "浏览器 Vue SPA", ["localhost:5173", "Element Plus · Pinia · ECharts"], TEAL) # arrow down d.line([(800, 190), (800, 240)], fill=NAVY, width=3) d.polygon([(800, 250), (790, 235), (810, 235)], fill=NAVY) text_left(d, (820, 205), "/api Vite 代理", f_s, MUTED) box(480, 260, 640, 140, "Spring Boot 业务端", ["localhost:8080 · JWT / JPA / 文件存储", "鉴权 · 落库 · 任务状态 · 降级"], NAVY) # split arrows d.line([(640, 400), (640, 470)], fill=TEAL, width=3) d.line([(1000, 400), (1000, 470)], fill=ACCENT, width=3) d.line([(640, 470), (1000, 470)], fill=MUTED, width=2) d.polygon([(640, 485), (630, 470), (650, 470)], fill=TEAL) d.polygon([(1000, 485), (990, 470), (1010, 470)], fill=ACCENT) box(280, 500, 420, 160, "H2 / MySQL 业务库", ["默认 H2 内存 · 可选 MySQL", "DataInitializer 种子数据"], TEAL) box(900, 500, 480, 180, "FastAPI AI 服务", ["localhost:8001", "YOLO · RAG · LLM 报告/决策", "失败可降级到模板规则"], ACCENT) # bottom leaves box(820, 720, 200, 100, "本地权重", ["best.pt 等"], PURPLE) box(1040, 720, 200, 100, "知识 Markdown", ["高血压/肺炎…"], ORANGE) box(1260, 720, 220, 100, "外部 LLM API", ["DeepSeek 兼容"], GREEN) d.line([(1140, 680), (920, 720)], fill=MUTED, width=2) d.line([(1140, 680), (1140, 720)], fill=MUTED, width=2) d.line([(1140, 680), (1370, 720)], fill=MUTED, width=2) # principles strip round_rect(d, (40, 720, 760, 860), 12, WHITE, SOFT, 2) text_left(d, (60, 735), "调用原则", f_b, NAVY) principles = [ "1. 浏览器只访问业务后端(经代理)", "2. AI 能力集中在 FastAPI", "3. 配置单向同步:管理端 → AI 运行时", "4. 失败可降级:超时/宕机仍可演示", ] yy = 770 for p in principles: text_left(d, (60, yy), p, f_s, DARK) yy += 22 img.save(OUT / "architecture.png", quality=95) print("architecture.png") def imaging_flow(): w, h = 1600, 900 img = Image.new("RGB", (w, h), LIGHT) d = ImageDraw.Draw(img) f_h = font(28, True) f_n = font(18, True) f_s = font(15) text_left(d, (40, 20), "核心流程 · AI 影像诊断", f_h, NAVY) steps = [ (80, 100, "新建影像记录\n上传图片", TEAL), (80, 250, "状态 PENDING", MUTED), (80, 400, "点击「AI 诊断」", ORANGE), (80, 550, "状态 ANALYZING", ORANGE), (420, 550, "Spring\nAIDiagnosisService\n@Async", NAVY), (760, 550, "FastAPI\n/imaging/analyze", ACCENT), (1100, 450, "YOLO 检测\n绘制标注图", PURPLE), (1100, 620, "报告生成\nLLM 或模板", TEAL), (1400, 520, "写结果\n更新记录", GREEN), (1400, 250, "COMPLETED\n/ ERROR", GREEN), (1100, 120, "前端轮询\n报告弹窗", TEAL), ] # draw boxes positions = {} for i, (x, y, text, color) in enumerate(steps): bw, bh = 220, 90 if "\n" in text and text.count("\n") >= 2: bh = 110 round_rect(d, (x, y, x + bw, y + bh), 12, color) lines = text.split("\n") for j, ln in enumerate(lines): text_center(d, (x + bw // 2, y + 22 + j * 24), ln, f_n if j == 0 else f_s, WHITE) positions[i] = (x + bw // 2, y + bh // 2, x, y, bw, bh) def arrow(a, b): x1, y1 = positions[a][0], positions[a][1] x2, y2 = positions[b][0], positions[b][1] # adjust to box edges roughly d.line([(x1, y1), (x2, y2)], fill=NAVY, width=2) ang = math.atan2(y2 - y1, x2 - x1) ax, ay = x2 - 18 * math.cos(ang), y2 - 18 * math.sin(ang) d.polygon( [ (ax, ay), (ax - 10 * math.cos(ang - 0.4), ay - 10 * math.sin(ang - 0.4)), (ax - 10 * math.cos(ang + 0.4), ay - 10 * math.sin(ang + 0.4)), ], fill=NAVY, ) for a, b in [(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 6), (5, 7), (6, 8), (7, 8), (8, 9), (9, 10)]: arrow(a, b) # note round_rect(d, (420, 100, 1000, 220), 12, WHITE, SOFT, 2) text_left(d, (440, 115), "技术要点", f_n, NAVY) notes = [ "· 异步诊断:AsyncConfig 线程池,避免阻塞 HTTP", "· 前端轮询有上限,组件卸载时清理定时器", "· AI 不可用时 Spring 本地规则降级,演示不断链", "· 报告分段:所见 / 印象 / 建议 / 声明", ] yy = 150 for n in notes: text_left(d, (440, yy), n, f_s, DARK) yy += 22 img.save(OUT / "imaging_flow.png", quality=95) print("imaging_flow.png") def state_machine(): w, h = 1100, 320 img = Image.new("RGB", (w, h), LIGHT) d = ImageDraw.Draw(img) f_n = font(20, True) f_s = font(14) states = [ ("PENDING", "已登记,待诊断", MUTED), ("ANALYZING", "诊断进行中", ORANGE), ("COMPLETED", "成功,可看报告", GREEN), ("ERROR", "失败", RED), ] for i, (name, desc, color) in enumerate(states): x = 40 + i * 270 round_rect(d, (x, 80, x + 200, 200), 16, color) text_center(d, (x + 100, 130), name, f_n, WHITE) text_center(d, (x + 100, 170), desc, f_s, WHITE) if i < 3: d.line([(x + 210, 140), (x + 255, 140)], fill=NAVY, width=3) d.polygon([(x + 260, 140), (x + 248, 132), (x + 248, 148)], fill=NAVY) # branch note from analyzing text_left(d, (40, 30), "影像诊断状态机", font(24, True), NAVY) text_left(d, (580, 250), "ANALYZING 后分支到 COMPLETED 或 ERROR", f_s, MUTED) img.save(OUT / "state_machine.png", quality=95) print("state_machine.png") def er_diagram(): w, h = 1200, 700 img = Image.new("RGB", (w, h), LIGHT) d = ImageDraw.Draw(img) f_h = font(26, True) f_n = font(18, True) f_s = font(14) text_left(d, (30, 20), "主要实体关系", f_h, NAVY) def ent(x, y, title, fields, color): bw = 240 bh = 40 + 22 * len(fields) + 16 round_rect(d, (x, y, x + bw, y + bh), 12, WHITE, color, 2) round_rect(d, (x, y, x + bw, y + 40), 12, color) d.rectangle((x, y + 26, x + bw, y + 40), fill=color) text_center(d, (x + bw // 2, y + 20), title, f_n, WHITE) yy = y + 52 for f in fields: text_left(d, (x + 16, yy), f, f_s, DARK) yy += 22 return x + bw // 2, y + bh // 2, x, y, bw, bh u = ent(480, 60, "User", ["ADMIN/DOCTOR/RADIOLOGIST", "BCrypt 密码 · 科室"], NAVY) p = ent(40, 280, "Patient", ["姓名/性别/年龄", "证件/联系/既往史"], TEAL) im = ent(360, 280, "ImagingRecord", ["类型/部位/图像", "状态机 PENDING…"], ACCENT) ai = ent(360, 520, "AIDiagnosisResult", ["置信度/所见/建议", "检测JSON/标注图"], PURPLE) em = ent(700, 280, "ElectronicMedicalRecord", ["主诉→随访全字段", "关联患者/医生"], TEAL) ap = ent(960, 280, "Appointment", ["预约日/科室", "状态流转"], ORANGE) def link(a, b, label=""): d.line([(a[0], a[1]), (b[0], b[1])], fill=MUTED, width=2) if label: mx, my = (a[0] + b[0]) // 2, (a[1] + b[1]) // 2 text_left(d, (mx + 4, my - 10), label, f_s, MUTED) link(u, im) link(u, em) link(u, ap) link(p, im) link(p, em) link(p, ap) link(im, ai, "1:1") img.save(OUT / "er_diagram.png", quality=95) print("er_diagram.png") def llm_sync(): w, h = 1200, 280 img = Image.new("RGB", (w, h), LIGHT) d = ImageDraw.Draw(img) f_n = font(18, True) f_s = font(14) text_left(d, (30, 16), "LLM 配置同步链路", font(22, True), NAVY) nodes = [ ("管理员\nAI 配置保存", NAVY), ("./data/ai/\nsettings.json", TEAL), ("AI 服务\n/llm-config", ACCENT), ("FastAPI 运行时\n启用大模型", GREEN), ] for i, (t, c) in enumerate(nodes): x = 40 + i * 290 round_rect(d, (x, 70, x + 230, 180), 14, c) lines = t.split("\n") for j, ln in enumerate(lines): text_center(d, (x + 115, 110 + j * 28), ln, f_n, WHITE) if i < 3: d.line([(x + 240, 125), (x + 275, 125)], fill=NAVY, width=3) d.polygon([(x + 280, 125), (x + 268, 117), (x + 268, 133)], fill=NAVY) text_left(d, (40, 220), "报告与决策从模板切换到大模型增强", f_s, MUTED) img.save(OUT / "llm_sync.png", quality=95) print("llm_sync.png") def decision_flow(): w, h = 1300, 420 img = Image.new("RGB", (w, h), LIGHT) d = ImageDraw.Draw(img) f_n = font(17, True) f_s = font(14) text_left(d, (30, 16), "电子病历辅助决策流程", font(22, True), NAVY) nodes = [ ("保存病历", TEAL), ("DecisionSupport\nService", NAVY), ("FastAPI\n/report/decision", ACCENT), ("RAG + LLM\n或模板回退", PURPLE), ("七类建议\nDialog 展示", GREEN), ] for i, (t, c) in enumerate(nodes): x = 30 + i * 255 round_rect(d, (x, 90, x + 220, 200), 14, c) for j, ln in enumerate(t.split("\n")): text_center(d, (x + 110, 140 + j * 28), ln, f_n, WHITE) if i < 4: d.line([(x + 230, 155), (x + 245, 155)], fill=NAVY, width=3) d.polygon([(x + 250, 155), (x + 238, 147), (x + 238, 163)], fill=NAVY) outs = ["治疗", "用药", "护理", "随访", "风险", "冲突", "RAG来源"] for i, o in enumerate(outs): x = 40 + i * 175 round_rect(d, (x, 320, x + 155, 380), 10, SOFT, ACCENT, 2) text_center(d, (x + 77, 350), o, f_s, NAVY) img.save(OUT / "decision_flow.png", quality=95) print("decision_flow.png") def transform_arrow(): w, h = 900, 360 img = Image.new("RGB", (w, h), LIGHT) d = ImageDraw.Draw(img) f_n = font(18, True) f_s = font(14) text_left(d, (30, 16), "架构演进:从单体到三端分离", font(22, True), NAVY) round_rect(d, (40, 80, 360, 300), 16, WHITE, MUTED, 2) round_rect(d, (40, 80, 360, 130), 16, MUTED) d.rectangle((40, 115, 360, 130), fill=MUTED) text_center(d, (200, 105), "早期原型", f_n, WHITE) for i, t in enumerate(["Thymeleaf 服务端渲染", "HttpSession 登录", "规则/随机 AI 模拟", "强依赖 MySQL"]): text_left(d, (60, 150 + i * 32), "· " + t, f_s, DARK) # arrow d.polygon([(400, 180), (480, 160), (480, 175), (560, 175), (560, 185), (480, 185), (480, 200)], fill=ACCENT) round_rect(d, (580, 80, 860, 300), 16, WHITE, ACCENT, 2) round_rect(d, (580, 80, 860, 130), 16, ACCENT) d.rectangle((580, 115, 860, 130), fill=ACCENT) text_center(d, (720, 105), "当前实现", f_n, WHITE) for i, t in enumerate(["Vue3 + Element Plus", "Spring Security + JWT", "YOLO 实检 + RAG/LLM", "默认 H2 可离线演示"]): text_left(d, (600, 150 + i * 32), "· " + t, f_s, DARK) img.save(OUT / "transform.png", quality=95) print("transform.png") def tech_layers(): w, h = 1400, 520 img = Image.new("RGB", (w, h), LIGHT) d = ImageDraw.Draw(img) f_h = font(24, True) f_n = font(18, True) f_s = font(15) text_left(d, (30, 16), "三层技术栈", f_h, NAVY) layers = [ ("前端 SPA", TEAL, "Vue 3.5.10 · Vite 5.4.8 · Element Plus 2.8.4 · Pinia · Router · Axios · ECharts · marked"), ("业务后端", NAVY, "Spring Boot 3.3.4 · Java 17 · Security + jjwt 0.12.6 · JPA · H2/MySQL · Lombok · Maven"), ("AI 微服务", ACCENT, "FastAPI ≥0.110 · Ultralytics YOLO · OpenCV · LangChain ≥0.2 · DeepSeek 等 OpenAI 兼容"), ] for i, (title, color, desc) in enumerate(layers): y = 70 + i * 140 round_rect(d, (40, y, w - 40, y + 120), 16, WHITE, color, 3) round_rect(d, (40, y, 220, y + 120), 16, color) d.rectangle((180, y, 220, y + 120), fill=color) text_center(d, (130, y + 60), title, f_n, WHITE) # wrap desc roughly text_left(d, (250, y + 45), desc, f_s, DARK) img.save(OUT / "tech_layers.png", quality=95) print("tech_layers.png") def scene_cards(): """Four scene cards as one image for roles page.""" w, h = 1400, 360 img = Image.new("RGB", (w, h), LIGHT) d = ImageDraw.Draw(img) f_n = font(20, True) f_s = font(15) cards = [ ("影像科", "上传胸片 → 一键 AI 诊断\n查看标注框、置信度与分段报告", TEAL), ("临床医生", "书写病历关键词 → 获取\n治疗/护理/随访建议与知识来源", ACCENT), ("管理员", "配置 LLM、管理知识库\n切换/上传 YOLO 权重与用户", NAVY), ("挂号窗口", "预约登记与状态流转\n预约→确认→完成/取消/未到诊", ORANGE), ] for i, (t, desc, c) in enumerate(cards): x = 20 + i * 345 round_rect(d, (x, 30, x + 320, 320), 16, WHITE, c, 3) round_rect(d, (x, 30, x + 320, 90), 16, c) d.rectangle((x, 70, x + 320, 90), fill=c) text_center(d, (x + 160, 60), t, f_n, WHITE) # icon circle d.ellipse((x + 130, 110, x + 190, 170), fill=SOFT, outline=c, width=3) text_center(d, (x + 160, 140), str(i + 1), f_n, c) for j, ln in enumerate(desc.split("\n")): text_center(d, (x + 160, 200 + j * 28), ln, f_s, DARK) img.save(OUT / "scene_cards.png", quality=95) print("scene_cards.png") def yolo_concept(): """Simple chest-xray style + detection boxes concept.""" w, h = 900, 560 img = Image.new("RGB", (w, h), (20, 28, 40)) d = ImageDraw.Draw(img) f_n = font(18, True) f_s = font(14) # left original round_rect(d, (40, 60, 420, 500), 12, (35, 45, 60)) text_center(d, (230, 40), "原始影像(示意)", f_n, ACCENT) # fake lung-ish shapes d.ellipse((90, 140, 220, 380), outline=(180, 200, 210), width=2) d.ellipse((240, 140, 370, 380), outline=(180, 200, 210), width=2) d.line([(230, 120), (230, 420)], fill=(120, 140, 150), width=2) d.rectangle((150, 200, 190, 250), outline=(100, 120, 130), width=1) # right annotated round_rect(d, (480, 60, 860, 500), 12, (35, 45, 60)) text_center(d, (670, 40), "YOLO 标注结果(示意)", f_n, ACCENT) d.ellipse((530, 140, 660, 380), outline=(180, 200, 210), width=2) d.ellipse((680, 140, 810, 380), outline=(180, 200, 210), width=2) d.line([(670, 120), (670, 420)], fill=(120, 140, 150), width=2) # detection boxes d.rectangle((560, 200, 640, 270), outline=ACCENT, width=3) text_left(d, (560, 175), "findings 0.87", f_s, ACCENT) d.rectangle((720, 250, 790, 320), outline=ORANGE, width=3) text_left(d, (700, 225), "nodule 0.76", f_s, ORANGE) text_left(d, (40, 520), "仅教学演示概念图,非真实患者数据与临床结论", f_s, MUTED) img.save(OUT / "yolo_concept.png", quality=95) print("yolo_concept.png") def module_tree(): w, h = 1200, 700 img = Image.new("RGB", (w, h), LIGHT) d = ImageDraw.Draw(img) f_h = font(24, True) f_n = font(16, True) f_s = font(14) text_left(d, (30, 20), "仓库三工程结构", f_h, NAVY) roots = [ (40, "frontend/", TEAL, ["src/api HTTP 封装", "src/views 业务页面", "src/router 守卫", "src/stores Pinia"]), (420, "smart-hospital/", NAVY, ["controller/service", "security JWT", "uploads/ 影像头像", "data/ai/ 配置与聊天"]), (800, "ai-service/", ACCENT, ["api/ imaging·rag", "services/ yolo·llm", "knowledge/ MD", "data/weights/ pt"]), ] for x, title, color, items in roots: round_rect(d, (x, 80, x + 340, 640), 16, WHITE, color, 3) round_rect(d, (x, 80, x + 340, 150), 16, color) d.rectangle((x, 130, x + 340, 150), fill=color) text_center(d, (x + 170, 115), title, f_n, WHITE) yy = 180 for it in items: round_rect(d, (x + 24, yy, x + 316, yy + 70), 10, SOFT) text_left(d, (x + 44, yy + 22), it, f_s, DARK) yy += 90 img.save(OUT / "module_tree.png", quality=95) print("module_tree.png") def closing_bg(): w, h = 1920, 1080 img = gradient_bg(w, h, NAVY, (12, 80, 100)) overlay = Image.new("RGBA", (w, h), (0, 0, 0, 0)) d = ImageDraw.Draw(overlay) d.ellipse((760, 280, 1160, 680), outline=(*ACCENT, 100), width=4) d.ellipse((820, 340, 1100, 620), outline=(*TEAL, 80), width=3) # cross cx, cy = 960, 480 d.rounded_rectangle((cx - 16, cy - 50, cx + 16, cy + 50), radius=6, fill=(*ACCENT, 200)) d.rounded_rectangle((cx - 50, cy - 16, cx + 50, cy + 16), radius=6, fill=(*ACCENT, 200)) img = Image.alpha_composite(img.convert("RGBA"), overlay).convert("RGB") img.save(OUT / "closing_bg.png", quality=95) print("closing_bg.png") def demo_timeline(): w, h = 1500, 280 img = Image.new("RGB", (w, h), LIGHT) d = ImageDraw.Draw(img) f_n = font(14, True) f_s = font(12) text_left(d, (20, 12), "推荐演示剧本(7 步)", font(20, True), NAVY) steps = [ "doctor1\n登录仪表盘", "患者\n360°档案", "影像\nAI诊断", "病历\n辅助决策", "预约\n状态流转", "admin\nAI/YOLO", "关闭AI\n验证降级", ] n = len(steps) for i, t in enumerate(steps): x = 40 + i * 210 d.ellipse((x + 60, 60, x + 110, 110), fill=TEAL if i < 5 else NAVY) text_center(d, (x + 85, 85), str(i + 1), font(18, True), WHITE) for j, ln in enumerate(t.split("\n")): text_center(d, (x + 85, 130 + j * 22), ln, f_s, DARK) if i < n - 1: d.line([(x + 120, 85), (x + 200, 85)], fill=ACCENT, width=3) img.save(OUT / "demo_timeline.png", quality=95) print("demo_timeline.png") def priority_board(): w, h = 1500, 780 img = Image.new("RGB", (w, h), LIGHT) d = ImageDraw.Draw(img) f_h = font(26, True) f_n = font(18, True) f_s = font(14) text_left(d, (30, 16), "功能需求优先级看板", f_h, NAVY) cols = [ ( "P0 必须具备", RED, [ "F-01 登录/登出/改密 · JWT+BCrypt", "F-02 患者 CRUD + 360°档案", "F-03 影像 CRUD + 上传", "F-04 AI 影像诊断状态机", "F-05 病历 + 辅助决策", "F-06 角色权限双端控制", ], ), ( "P1 重要增强", ORANGE, [ "F-07 预约挂号全流程", "F-08 仪表盘可视化", "F-09 AI 对话助手", "F-10 知识库管理 RAG", "F-11 AI/LLM 配置", "F-12 YOLO 权重管理", ], ), ( "P2 体验与健壮", GREEN, [ "F-13 AI 不可用业务降级", "F-14 弹窗 append-to-body", "F-15 报告分段可读", "F-16 路由过渡与仪表盘体验", ], ), ] for i, (title, color, items) in enumerate(cols): x = 30 + i * 490 round_rect(d, (x, 70, x + 460, h - 40), 16, WHITE, color, 3) round_rect(d, (x, 70, x + 460, 130), 16, color) d.rectangle((x, 110, x + 460, 130), fill=color) text_center(d, (x + 230, 100), title, f_n, WHITE) yy = 160 for it in items: round_rect(d, (x + 20, yy, x + 440, yy + 70), 10, SOFT) text_left(d, (x + 36, yy + 22), it, f_s, DARK) yy += 85 img.save(OUT / "priority_board.png", quality=95) print("priority_board.png") if __name__ == "__main__": cover_bg() dual_chain() feature_panorama() architecture() imaging_flow() state_machine() er_diagram() llm_sync() decision_flow() transform_arrow() tech_layers() scene_cards() yolo_concept() module_tree() closing_bg() demo_timeline() priority_board() print("ALL ASSETS DONE ->", OUT)