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

657 lines
25 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""
智慧医院项目答辩 PPT v2
结构:项目主题 → 项目需求 → 技术栈 → 实现功能 → 六人分工
要求:图文表并茂、色彩丰富、字体统一、≤20 页、布局防重叠
"""
from __future__ import annotations
from pathlib import Path
from PIL import Image as PILImage
from pptx import Presentation
from pptx.dml.color import RGBColor
from pptx.enum.shapes import MSO_SHAPE
from pptx.enum.text import MSO_ANCHOR, PP_ALIGN
from pptx.oxml import parse_xml
from pptx.oxml.ns import qn
from pptx.util import Inches, Pt
ROOT = Path(__file__).resolve().parent
ASSETS = ROOT / "assets_v2"
OUT = ROOT / "智慧医院_项目答辩PPT.pptx"
OUT_ALT = ROOT / "智慧医院_项目答辩PPT_v2.pptx"
SLIDE_W = Inches(13.333)
SLIDE_H = Inches(7.5)
HEADER_H = 0.88
FOOTER_Y = 7.12
TOTAL = 18
# —— 统一字体 ——
FONT = "微软雅黑"
SIZE_H1 = 22
SIZE_H2 = 16
SIZE_BODY = 12
SIZE_SMALL = 11
SIZE_TABLE = 11
# —— 多彩配色 ——
NAVY = RGBColor(0x0F, 0x20, 0x5A)
BLUE = RGBColor(0x25, 0x63, 0xEB)
CYAN = RGBColor(0x06, 0xB6, 0xD4)
TEAL = RGBColor(0x14, 0xB8, 0xA6)
PURPLE = RGBColor(0xA8, 0x55, 0xF7)
VIOLET = RGBColor(0x8B, 0x5C, 0xF6)
ORANGE = RGBColor(0xF9, 0x73, 0x16)
CORAL = RGBColor(0xFB, 0x71, 0x85)
PINK = RGBColor(0xEC, 0x48, 0x99)
GREEN = RGBColor(0x22, 0xC5, 0x5E)
INDIGO = RGBColor(0x63, 0x66, 0xF1)
YELLOW = RGBColor(0xEA, 0xB3, 0x08)
DARK = RGBColor(0x0F, 0x17, 0x2A)
MUTED = RGBColor(0x64, 0x74, 0x8B)
LIGHT = RGBColor(0xF8, 0xFA, 0xFC)
WHITE = RGBColor(0xFF, 0xFF, 0xFF)
SOFT = RGBColor(0xF1, 0xF5, 0xF9)
PANEL = RGBColor(0x0C, 0x1A, 0x40)
SECTION_COLORS = [BLUE, CORAL, PURPLE, TEAL, ORANGE] # 五大篇章色
def set_font(run, size=SIZE_BODY, bold=False, color=DARK):
run.font.size = Pt(size)
run.font.bold = bold
run.font.color.rgb = color
run.font.name = FONT
rPr = run._r.get_or_add_rPr()
ea = rPr.find(qn("a:ea"))
if ea is None:
ea = parse_xml(
f'<a:ea xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" typeface="{FONT}"/>'
)
rPr.append(ea)
else:
ea.set("typeface", FONT)
def blank(prs):
return prs.slides.add_slide(prs.slide_layouts[6])
def bg(slide, color=LIGHT):
f = slide.background.fill
f.solid()
f.fore_color.rgb = color
def shape(slide, l, t, w, h, fill, line=None):
s = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE, Inches(l), Inches(t), Inches(w), Inches(h))
s.fill.solid()
s.fill.fore_color.rgb = fill
if line is None:
s.line.fill.background()
else:
s.line.color.rgb = line
s.line.width = Pt(1.25)
try:
s.adjustments[0] = 0.08
except Exception:
pass
return s
def shape_text(s, lines, size=SIZE_BODY, bold=False, color=WHITE, align=PP_ALIGN.CENTER):
tf = s.text_frame
tf.clear()
tf.word_wrap = True
if isinstance(lines, str):
lines = lines.split("\n")
for i, line in enumerate(lines):
p = tf.paragraphs[0] if i == 0 else tf.add_paragraph()
p.alignment = align
p.space_before = Pt(0)
p.space_after = Pt(2)
run = p.add_run()
run.text = line
set_font(run, size=size if i == 0 else max(size - 1, 10), bold=(bold and i == 0), color=color)
def textbox(slide, l, t, w, h, text, size=SIZE_BODY, bold=False, color=DARK, align=PP_ALIGN.LEFT):
box = slide.shapes.add_textbox(Inches(l), Inches(t), Inches(w), Inches(h))
tf = box.text_frame
tf.word_wrap = True
p = tf.paragraphs[0]
p.alignment = align
p.space_before = Pt(0)
p.space_after = Pt(0)
# multi-line support
first = True
for line in str(text).split("\n"):
p = tf.paragraphs[0] if first else tf.add_paragraph()
first = False
p.alignment = align
p.space_before = Pt(0)
p.space_after = Pt(2)
run = p.add_run()
run.text = line
set_font(run, size=size, bold=bold, color=color)
return box
def paras(slide, l, t, w, h, lines, size=SIZE_BODY, color=DARK, spacing=4):
box = slide.shapes.add_textbox(Inches(l), Inches(t), Inches(w), Inches(h))
tf = box.text_frame
tf.word_wrap = True
for i, line in enumerate(lines):
p = tf.paragraphs[0] if i == 0 else tf.add_paragraph()
p.alignment = PP_ALIGN.LEFT
p.space_before = Pt(0)
p.space_after = Pt(spacing)
run = p.add_run()
run.text = line
set_font(run, size=size, color=color)
return box
def table(slide, l, t, w, h, data, widths=None, fsize=SIZE_TABLE, header_color=NAVY):
rows, cols = len(data), len(data[0])
ts = slide.shapes.add_table(rows, cols, Inches(l), Inches(t), Inches(w), Inches(h))
tb = ts.table
if widths:
for i, wi in enumerate(widths):
tb.columns[i].width = Inches(wi)
for r in range(rows):
for c in range(cols):
cell = tb.cell(r, c)
cell.text = ""
p = cell.text_frame.paragraphs[0]
p.alignment = PP_ALIGN.LEFT
p.space_before = Pt(0)
p.space_after = Pt(0)
run = p.add_run()
run.text = str(data[r][c])
is_h = r == 0
set_font(run, size=fsize + (1 if is_h else 0), bold=is_h, color=WHITE if is_h else DARK)
cell.fill.solid()
# colorful header / zebra
if is_h:
cell.fill.fore_color.rgb = header_color
else:
cell.fill.fore_color.rgb = SOFT if r % 2 == 0 else WHITE
return ts
def header(slide, title, subtitle, section_idx=0):
color = SECTION_COLORS[section_idx % len(SECTION_COLORS)]
shape(slide, 0, 0, 13.333, HEADER_H, NAVY)
# colorful accent strip
shape(slide, 0, HEADER_H, 13.333, 0.06, color)
# left color badge
badge = shape(slide, 0.25, 0.18, 0.12, 0.52, color)
box = slide.shapes.add_textbox(Inches(0.55), Inches(0.1), Inches(12.3), Inches(0.7))
tf = box.text_frame
tf.word_wrap = True
p = tf.paragraphs[0]
run = p.add_run()
run.text = title
set_font(run, size=SIZE_H1, bold=True, color=WHITE)
if subtitle:
p2 = tf.add_paragraph()
run2 = p2.add_run()
run2.text = subtitle
set_font(run2, size=SIZE_SMALL, color=RGBColor(0xB8, 0xD4, 0xF0))
def footer(slide, page):
shape(slide, 0, FOOTER_Y, 13.333, 7.5 - FOOTER_Y, NAVY)
# rainbow dots
colors = [CORAL, ORANGE, YELLOW, TEAL, BLUE, PURPLE]
for i, c in enumerate(colors):
shape(slide, 0.25 + i * 0.22, FOOTER_Y + 0.1, 0.14, 0.14, c)
textbox(slide, 1.7, FOOTER_Y + 0.05, 9.5, 0.28,
"智慧医院 AI 影像诊断与电子病历辅助决策系统 · 项目答辩 · 仅供教学演示",
size=10, color=RGBColor(0xC5, 0xD8, 0xE8))
textbox(slide, 11.4, FOOTER_Y + 0.05, 1.6, 0.28, f"{page} / {TOTAL}",
size=10, bold=True, color=WHITE, align=PP_ALIGN.RIGHT)
def fit_img(slide, path, l, t, max_w, max_h):
path = Path(path)
if not path.exists():
return None
with PILImage.open(path) as im:
iw, ih = im.size
aspect = iw / float(ih)
w, h = max_w, max_w / aspect
if h > max_h:
h = max_h
w = h * aspect
return slide.shapes.add_picture(str(path), Inches(l), Inches(t), width=Inches(w), height=Inches(h))
def card(slide, l, t, w, h, title, lines, color=BLUE, body_size=SIZE_BODY):
shape(slide, l, t, w, h, WHITE, SOFT)
band = shape(slide, l, t, w, 0.46, color)
shape_text(band, [title], size=14, bold=True, color=WHITE)
if lines:
paras(slide, l + 0.15, t + 0.55, w - 0.3, h - 0.7, lines, size=body_size, color=DARK, spacing=4)
# ===================== SLIDES =====================
def s01_cover(prs):
slide = blank(prs)
fit_img(slide, ASSETS / "cover.png", 0, 0, 13.333, 7.5)
shape(slide, 1.0, 1.4, 11.3, 4.5, PANEL)
textbox(slide, 1.3, 1.6, 10.7, 0.35, "PROJECT DEFENSE · SMART HOSPITAL",
size=13, color=CYAN, align=PP_ALIGN.CENTER)
textbox(slide, 1.3, 2.15, 10.7, 1.15, "智慧医院 AI 影像诊断与\n电子病历辅助决策系统",
size=32, bold=True, color=WHITE, align=PP_ALIGN.CENTER)
textbox(slide, 1.3, 3.45, 10.7, 0.4, "实训教学演示级 Web 应用 · 业务系统 + AI 微服务",
size=15, color=RGBColor(0xB8, 0xD4, 0xF0), align=PP_ALIGN.CENTER)
chips = [
("主题", "智慧医院", CORAL),
("架构", "三端分离", ORANGE),
("AI", "YOLO+RAG", TEAL),
("前端", "Vue3", BLUE),
("后端", "Boot3", PURPLE),
("团队", "6 人协作", PINK),
]
for i, (k, v, c) in enumerate(chips):
x = 1.35 + i * 1.8
box = shape(slide, x, 4.15, 1.65, 0.95, c)
shape_text(box, [k, v], size=12, bold=True, color=WHITE)
textbox(slide, 1.2, 5.5, 10.9, 0.35, "文档 v1.0 · 2026-07-27 · 严格依据《项目详细文档》",
size=12, color=RGBColor(0xC5, 0xD8, 0xE8), align=PP_ALIGN.CENTER)
textbox(slide, 1.2, 6.3, 10.9, 0.55,
"声明:输出仅供教学实训与辅助决策演示,不能替代执业医师正式诊断与医疗文书。",
size=11, color=RGBColor(0xE2, 0xEC, 0xF4), align=PP_ALIGN.CENTER)
def s02_toc(prs):
slide = blank(prs)
bg(slide)
header(slide, "目录 · 答辩叙事五篇章", "主题 → 需求 → 技术栈 → 功能 → 分工", 0)
sections = [
("01", "项目主题", "P3 – P4", "定位 · 双链路 · 目标 · 场景", BLUE, "主题"),
("02", "项目需求", "P5 – P7", "痛点 · 角色 · 优先级 · 非功能", CORAL, "需求"),
("03", "技术栈", "P8 – P10", "三层技术 · 架构 · 工程结构", PURPLE, "技术"),
("04", "实现功能", "P11 – P14", "全景 · 影像 · 决策 · 管理安全", TEAL, "功能"),
("05", "六人分工", "P15 – P16", "协作总览 · 职责 · 交付物", ORANGE, "分工"),
("06", "演示总结", "P17 – P18", "演示路径 · 验证 · 致谢", PINK, "收束"),
]
for i, (num, title, pages, desc, color, tag) in enumerate(sections):
col = i % 3
row = i // 3
x = 0.45 + col * 4.25
y = 1.2 + row * 2.7
shape(slide, x, y, 4.05, 2.45, WHITE, color)
band = shape(slide, x, y, 4.05, 0.7, color)
shape_text(band, [f"{num} {title}"], size=18, bold=True, color=WHITE)
textbox(slide, x + 0.2, y + 0.9, 3.6, 0.35, pages, size=14, bold=True, color=color)
textbox(slide, x + 0.2, y + 1.35, 3.6, 0.8, desc, size=13, color=DARK)
footer(slide, 2)
def s03_theme(prs):
slide = blank(prs)
bg(slide)
header(slide, "一、项目主题 · 定位与双链路", "智慧医院 = 影像 AI 读片 + 病历 AI 决策", 0)
fit_img(slide, ASSETS / "dual_chain.png", 0.3, 1.1, 12.7, 5.8)
footer(slide, 3)
def s04_goals_scenes(prs):
slide = blank(prs)
bg(slide)
header(slide, "一、项目主题 · 建设目标与应用场景", "五维目标 + 四类演示场景", 0)
fit_img(slide, ASSETS / "goals.png", 0.3, 1.1, 12.7, 2.5)
fit_img(slide, ASSETS / "scenes.png", 0.3, 3.7, 12.7, 3.15)
footer(slide, 4)
def s05_req_pain(prs):
slide = blank(prs)
bg(slide)
header(slide, "二、项目需求 · 背景痛点与改造", "解决传统教学 HIS 的五类不足", 1)
fit_img(slide, ASSETS / "pain.png", 0.25, 1.1, 8.0, 4.0)
data = [
["问题", "传统表现", "本项目对策"],
["业务割裂", "缺统一视图", "患者 360° 档案"],
["AI 假数据", "随机/关键字", "YOLO + LLM/RAG"],
["技术陈旧", "Thymeleaf 耦合", "Vue3 前后端分离"],
["权限薄弱", "仅 Session", "JWT + 角色双端"],
["不可降级", "依赖失败中断", "规则/模板兜底"],
]
table(slide, 8.4, 1.1, 4.55, 4.0, data, [1.2, 1.5, 1.85], fsize=10, header_color=CORAL)
nfr_note = shape(slide, 0.3, 5.3, 12.7, 1.55, WHITE, TEAL)
shape_text(
nfr_note,
[
"改造结论(文档 2.1)",
"在原始 Spring Boot + Thymeleaf 原型上完成改造 → 当前「前后端分离 + AI 微服务」版本",
"约束:实训/答辩级,非生产 HIS/PACS;影像以 JPG/PNG 为主;YOLO 为演示级需医师复核",
],
size=12,
color=DARK,
align=PP_ALIGN.LEFT,
)
footer(slide, 5)
def s06_req_roles(prs):
slide = blank(prs)
bg(slide)
header(slide, "二、项目需求 · 角色诉求与功能优先级", "三角色 + P0/P1/P2 需求看板", 1)
roles = [
["角色", "枚举", "核心诉求"],
["系统管理员", "ADMIN", "用户 · AI 配置 · YOLO 权重 · 运维"],
["临床医生", "DOCTOR", "患者病历 · 决策 · 预约 · 看报告"],
["影像医师", "RADIOLOGIST", "影像登记上传 · AI 诊断 · 审阅"],
["访客", "—", "仅登录页"],
]
table(slide, 0.3, 1.1, 12.7, 2.0, roles, [2.2, 2.3, 8.2], fsize=12, header_color=BLUE)
fit_img(slide, ASSETS / "priority.png", 0.3, 3.25, 12.7, 3.65)
footer(slide, 6)
def s07_req_nfr(prs):
slide = blank(prs)
bg(slide)
header(slide, "二、项目需求 · 非功能需求", "性能 · 可用性 · 安全 · 可维护 · 可扩展 · 兼容", 1)
fit_img(slide, ASSETS / "nfr.png", 0.3, 1.15, 12.7, 2.4)
data = [
["类别", "要求", "项目落地"],
["性能", "诊断不阻塞 HTTP", "@Async 线程池 + 前端有限轮询"],
["可用性", "无 MySQL 可演示", "默认 H2 + DataInitializer 种子"],
["安全", "鉴权/加密/CORS", "Security + JWT + BCrypt"],
["可维护", "统一响应异常", "Result<T> + GlobalExceptionHandler"],
["可扩展", "AI 与业务解耦", "FastAPI 独立 · ai.service.base-url"],
["兼容", "大模型可切换", "OpenAI 兼容(DeepSeek/Qwen)"],
]
table(slide, 0.3, 3.7, 12.7, 3.15, data, [1.6, 3.5, 7.6], fsize=11, header_color=PURPLE)
footer(slide, 7)
def s08_tech_stack(prs):
slide = blank(prs)
bg(slide)
header(slide, "三、技术栈 · 总体一览", "版本号严格对齐文档附录 A", 2)
fit_img(slide, ASSETS / "tech.png", 0.3, 1.1, 12.7, 3.5)
data = [
["层级", "关键组件", "版本"],
["前端", "Vue / Vite / Element Plus / Pinia / ECharts", "3.5.10 / 5.4.8 / 2.8.4 / 2.2.4 / 5.5.1"],
["业务端", "Spring Boot / Java / jjwt", "3.3.4 / 17 / 0.12.6"],
["数据库", "H2 默认 · MySQL 可选", "mem:smart_hospital / profile=mysql"],
["AI", "FastAPI / YOLO / LangChain / LLM", "≥0.110 / Ultralytics / ≥0.2 / DeepSeek"],
]
table(slide, 0.3, 4.75, 12.7, 2.1, data, [1.5, 6.0, 5.2], fsize=11, header_color=PURPLE)
footer(slide, 8)
def s09_tech_arch(prs):
slide = blank(prs)
bg(slide)
header(slide, "三、技术栈 · 系统逻辑架构", "Vue :5173 → Spring :8080 → H2/MySQL & FastAPI :8001", 2)
fit_img(slide, ASSETS / "architecture.png", 0.25, 1.05, 12.8, 5.9)
footer(slide, 9)
def s10_tech_modules(prs):
slide = blank(prs)
bg(slide)
header(slide, "三、技术栈 · 工程结构与数据落盘", "frontend · smart-hospital · ai-service", 2)
fit_img(slide, ASSETS / "modules.png", 0.25, 1.1, 8.5, 5.7)
card(slide, 8.95, 1.1, 4.0, 5.7, "关键配置与落盘", [
"• 前端代理 /api → :8080",
"• jwt.* 密钥/过期/前缀",
"• cors → localhost:5173",
"• ai.service.base-url:8001",
"• ./uploads/images 影像",
"• ./data/ai/settings.json",
"• chat-history.json 对话",
"• ai-service/data/weights/",
"• app/knowledge/ MD 知识",
"• multipart 最大约 500MB",
"• H2 控制台 /h2-console",
"• 启动序:AI→业务→前端",
], VIOLET, body_size=12)
footer(slide, 10)
def s11_feat_panorama(prs):
slide = blank(prs)
bg(slide)
header(slide, "四、实现功能 · 功能全景", "身份权限 · 主数据 · 诊疗业务 · 智能能力", 3)
fit_img(slide, ASSETS / "features.png", 0.2, 1.05, 9.0, 5.85)
routes = [
"/login 登录",
"/dashboard 仪表盘",
"/patients 患者",
"/imaging 影像",
"/emrs 电子病历",
"/appointments 预约",
"/ai-assistant 助手",
"/ai-knowledge 知识库",
"/ai-settings ADMIN",
"/ai-yolo ADMIN",
"/users ADMIN",
]
card(slide, 9.4, 1.1, 3.55, 5.8, "前端路由一览", ["• " + r for r in routes], TEAL, body_size=11)
footer(slide, 11)
def s12_feat_imaging(prs):
slide = blank(prs)
bg(slide)
header(slide, "四、实现功能 · 影像 AI 诊断(深度)", "状态机 · YOLO · 分段报告 · 可降级", 3)
card(slide, 0.25, 1.1, 4.0, 2.2, "检查登记能力", [
"• X_RAY / CT / MRI / ULTRASOUND",
"• 患者、部位、上传/路径",
"• 关键字/状态/类型筛选",
"• 状态 KPI 卡片快速过滤",
], BLUE, body_size=11)
fit_img(slide, ASSETS / "state.png", 4.4, 1.1, 8.55, 2.2)
fit_img(slide, ASSETS / "yolo.png", 0.25, 3.5, 5.4, 3.35)
card(slide, 5.85, 3.5, 7.1, 3.35, "结果展示清单", [
"① 诊断印象 + 置信度仪表盘",
"② 原图 vs YOLO 标注图对比",
"③ 影像所见分段 + 建议列表",
"④ 检测明细:类别/置信度/bbox",
"⑤ 完整报告:头/所见/印象/建议/声明",
"⑥ AI 不可用 → Spring 本地规则降级",
], CORAL, body_size=12)
footer(slide, 12)
def s13_feat_emr_biz(prs):
slide = blank(prs)
bg(slide)
header(slide, "四、实现功能 · 病历决策与业务模块", "辅助决策七块输出 + 仪表盘/患者/预约", 3)
fit_img(slide, ASSETS / "decision.png", 0.25, 1.1, 12.8, 2.7)
cards = [
("仪表盘", BLUE, ["KPI 多维统计", "近7日趋势 ECharts", "类型/状态分布饼图", "快捷入口与待办"]),
("患者 360°", TEAL, ["CRUD + 搜索分页", "证件/联系/既往史", "关联影像/病历/预约", "抽屉档案计数"]),
("预约挂号", ORANGE, ["新建编辑删除", "预约→确认→完成", "取消/未到诊", "按日筛选统计"]),
("关键词增强", PURPLE, ["高血压/糖尿病", "肺炎/结节", "模板+RAG 更明显", "LLM 可进一步增强"]),
]
for i, (t, c, lines) in enumerate(cards):
card(slide, 0.25 + i * 3.25, 4.0, 3.1, 2.85, t, ["• " + x for x in lines], c, body_size=11)
footer(slide, 13)
def s14_feat_admin(prs):
slide = blank(prs)
bg(slide)
header(slide, "四、实现功能 · 管理端、安全与接口", "AI 配置 · 权限双端 · 统一响应 · 实体关系", 3)
mods = [
("AI 助手", ["Markdown 渲染", "DOMPurify 消毒", "历史文件持久化"], CYAN),
("知识库", ["文档 CRUD 启停", "RAG 管线配合", "内置医学片段"], PURPLE),
("AI 配置", ["仅 ADMIN", "URL/Key/Model", "同步 FastAPI"], ORANGE),
("YOLO 管理", ["权重上传激活", "real/demo 模式", "推理统计"], CORAL),
]
for i, (t, lines, c) in enumerate(mods):
card(slide, 0.25 + i * 3.25, 1.1, 3.1, 2.15, t, ["• " + x for x in lines], c, body_size=11)
fit_img(slide, ASSETS / "er.png", 0.25, 3.4, 6.5, 3.45)
data = [
["接口/能力", "权限"],
["POST /api/auth/login", "公开"],
["患者/影像/病历/预约", "已登录"],
["AI 诊断 analyze/result", "已登录"],
["病历 ai-suggestions", "已登录"],
["用户管理 /users", "ADMIN"],
["AI 配置 / YOLO", "ADMIN"],
]
table(slide, 6.95, 3.4, 6.0, 3.45, data, [4.0, 2.0], fsize=11, header_color=INDIGO)
footer(slide, 14)
def s15_team_overview(prs):
slide = blank(prs)
bg(slide)
header(slide, "五、六人分工 · 协作总览", "按三端工程与业务模块划分(姓名可替换)", 4)
fit_img(slide, ASSETS / "team.png", 0.25, 1.05, 12.8, 5.9)
footer(slide, 15)
def s16_team_detail(prs):
"""Balanced layout: compact table on top + full-width swimlane below (no tiny corner image)."""
slide = blank(prs)
bg(slide)
header(slide, "五、六人分工 · 职责与交付物明细", "文档无具名分工 → 按模块可落地的 6 人方案", 4)
data = [
["成员", "角色定位", "主要职责", "关键交付物"],
["成员A", "前端基础/权限", "布局、登录、路由守卫、Pinia、个人中心", "BasicLayout · Login · router"],
["成员B", "前端业务", "仪表盘、患者360°、预约挂号、图表", "Dashboard · Patients · Appointments"],
["成员C", "前端智能交互", "影像报告弹窗、病历决策Dialog、AI助手/知识库页", "Imaging · EMR · AI views"],
["成员D", "后端主数据/安全", "JWT安全、用户、患者、预约、统一响应", "Security · User/Patient API"],
["成员E", "后端AI桥接", "异步诊断、AiClient、决策对接、降级逻辑", "AIDiagnosis · Decision 服务"],
["成员F", "AI微服务", "YOLO检测、报告/决策、RAG、权重与LLM配置", "ai-service 全模块"],
]
# 上半:表格约占 2.9",行高更匀称
table(slide, 0.3, 1.08, 12.7, 2.95, data, [1.3, 2.2, 5.0, 4.2], fsize=12, header_color=ORANGE)
# 下半:全宽泳道图(宽扁比例,贴满宽度,与上表约 1:1 视觉权重)
fit_img(slide, ASSETS / "team_lane.png", 0.3, 4.15, 12.7, 2.8)
footer(slide, 16)
def s17_demo(prs):
slide = blank(prs)
bg(slide)
header(slide, "演示路径 · 账号 · 验证清单", "答辩现场可按 7 步剧本走通", 5)
accounts = [
["用户名", "密码", "角色", "用途"],
["admin", "admin123", "ADMIN", "用户 / AI 配置 / YOLO"],
["doctor1", "pass123", "DOCTOR", "临床主演示"],
["radio1", "radio123", "RADIOLOGIST", "影像演示"],
]
table(slide, 0.3, 1.1, 12.7, 1.7, accounts, [2.2, 2.5, 2.8, 5.2], fsize=12, header_color=TEAL)
fit_img(slide, ASSETS / "demo.png", 0.3, 3.0, 12.7, 1.85)
checks = [
["验证项", "期望"],
["三端启动 5173 可登录", "通过"],
["admin 可见用户管理;doctor 访问 /users → 403", "通过"],
["影像 AI 至 COMPLETED,报告分段清晰", "通过"],
["病历 AI 建议含多类内容 + RAG 来源", "通过"],
["预约状态可流转;改密后需重登", "通过"],
["关闭 AI 后诊断仍返回降级结果", "通过"],
]
table(slide, 0.3, 5.0, 12.7, 1.9, checks, [8.5, 4.2], fsize=11, header_color=BLUE)
footer(slide, 17)
def s18_end(prs):
slide = blank(prs)
fit_img(slide, ASSETS / "closing.png", 0, 0, 13.333, 7.5)
textbox(slide, 1, 1.0, 11.3, 0.55, "总结与致谢", size=30, bold=True, color=WHITE, align=PP_ALIGN.CENTER)
textbox(slide, 1.2, 1.65, 10.9, 0.45,
"主题清晰 · 需求可验 · 技术可讲 · 功能可演示 · 分工可落地",
size=14, color=CYAN, align=PP_ALIGN.CENTER)
points = [
("主题", "双链路智慧医院", CORAL),
("需求", "P0–P2 + 非功能", ORANGE),
("技术", "Vue3+Boot+FastAPI", PURPLE),
("功能", "YOLO·RAG·全业务", TEAL),
("协作", "6 人三端分工", BLUE),
("边界", "教学演示非临床", PINK),
]
for i, (t, d, c) in enumerate(points):
x = 0.7 + (i % 6) * 2.1
box = shape(slide, x, 2.4, 1.95, 1.5, c)
shape_text(box, [t, d], size=12, bold=True, color=WHITE)
textbox(slide, 1, 4.2, 11.3, 0.5, "谢谢聆听 · 欢迎提问", size=26, bold=True, color=WHITE, align=PP_ALIGN.CENTER)
textbox(slide, 1.2, 5.0, 10.9, 0.9,
"声明:仅供教学实训与辅助决策演示,不能替代执业医师正式诊断。\n"
"依据:《项目详细文档.md》v1.0 · 分工表中成员名可按实际名单替换",
size=12, color=RGBColor(0xC5, 0xD8, 0xE8), align=PP_ALIGN.CENTER)
# note about team
textbox(slide, 1.2, 6.2, 10.9, 0.5,
"六人姓名未在文档中给出,PPT 使用「成员A–F」占位,答辩前请替换为真实姓名。",
size=11, color=YELLOW, align=PP_ALIGN.CENTER)
def main():
if not ASSETS.exists():
raise SystemExit("请先运行 gen_assets_v2.py 生成 assets_v2/")
prs = Presentation()
prs.slide_width = SLIDE_W
prs.slide_height = SLIDE_H
s01_cover(prs)
s02_toc(prs)
s03_theme(prs)
s04_goals_scenes(prs)
s05_req_pain(prs)
s06_req_roles(prs)
s07_req_nfr(prs)
s08_tech_stack(prs)
s09_tech_arch(prs)
s10_tech_modules(prs)
s11_feat_panorama(prs)
s12_feat_imaging(prs)
s13_feat_emr_biz(prs)
s14_feat_admin(prs)
s15_team_overview(prs)
s16_team_detail(prs)
s17_demo(prs)
s18_end(prs)
try:
prs.save(OUT)
print("Saved:", OUT)
except PermissionError:
prs.save(OUT_ALT)
print("原文件占用,另存:", OUT_ALT)
print("Slides:", len(prs.slides))
if __name__ == "__main__":
main()