Files
Shixun/ppt/gen_assets.py
T
2026-07-31 12:27:41 +08:00

723 lines
27 KiB
Python

# -*- 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)