# -*- coding: utf-8 -*- """Build Smart Hospital defense PPT (19 slides) — layout-safe revision.""" 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" OUT = ROOT / "智慧医院_AI影像诊断与电子病历辅助决策系统_答辩PPT.pptx" OUT_ALT = ROOT / "智慧医院_答辩PPT_优化版.pptx" SLIDE_W = Inches(13.333) SLIDE_H = Inches(7.5) # Safe content band (below header, above footer) HEADER_H = 0.92 CONTENT_TOP = 1.05 FOOTER_TOP = 7.12 CONTENT_BOTTOM = 7.05 CONTENT_H = CONTENT_BOTTOM - CONTENT_TOP # ~6.0" NAVY = RGBColor(0x0B, 0x3A, 0x5C) TEAL = RGBColor(0x1A, 0x7A, 0x9C) ACCENT = RGBColor(0x2B, 0xBB, 0xAD) LIGHT = RGBColor(0xF4, 0xF8, 0xFB) WHITE = RGBColor(0xFF, 0xFF, 0xFF) DARK = RGBColor(0x1E, 0x29, 0x3B) MUTED = RGBColor(0x64, 0x74, 0x8B) SOFT = RGBColor(0xE2, 0xEC, 0xF4) ORANGE = RGBColor(0xF5, 0x9E, 0x0B) GREEN = RGBColor(0x22, 0xC5, 0x5E) RED = RGBColor(0xEF, 0x44, 0x44) CHIP = RGBColor(0x12, 0x4A, 0x6A) PANEL = RGBColor(0x0B, 0x2A, 0x45) FONT = "微软雅黑" TOTAL = 19 def set_run_font(run, size=14, bold=False, color=DARK, name=FONT): run.font.size = Pt(size) run.font.bold = bold run.font.color.rgb = color run.font.name = name rPr = run._r.get_or_add_rPr() ea = rPr.find(qn("a:ea")) if ea is None: ea = parse_xml( f'' ) rPr.append(ea) else: ea.set("typeface", name) def add_textbox( slide, left, top, width, height, text, size=14, bold=False, color=DARK, align=PP_ALIGN.LEFT, anchor=MSO_ANCHOR.TOP, ): box = slide.shapes.add_textbox(left, top, width, height) tf = box.text_frame tf.word_wrap = True tf.auto_size = None try: tf.paragraphs[0].space_before = Pt(0) tf.paragraphs[0].space_after = Pt(0) except Exception: pass p = tf.paragraphs[0] p.alignment = align run = p.add_run() run.text = text set_run_font(run, size=size, bold=bold, color=color) return box def add_paras(slide, left, top, width, height, lines, size=12, color=DARK, spacing=4, bold=False): box = slide.shapes.add_textbox(left, top, width, height) 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_run_font(run, size=size, bold=bold, color=color) return box def add_shape(slide, left, top, width, height, fill=NAVY, line=None): shape = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE, left, top, width, height) shape.fill.solid() shape.fill.fore_color.rgb = fill if line is None: shape.line.fill.background() else: shape.line.color.rgb = line shape.line.width = Pt(1) try: shape.adjustments[0] = 0.08 except Exception: pass return shape def fill_shape_text(shape, lines, size=13, bold=False, color=WHITE, align=PP_ALIGN.CENTER, v_anchor=MSO_ANCHOR.MIDDLE): """Put multi-line text inside a shape (avoids separate overlapping textboxes).""" tf = shape.text_frame tf.clear() tf.word_wrap = True try: tf.auto_size = None except Exception: pass 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_run_font(run, size=size, bold=bold if i == 0 else bold, color=color) return shape def add_table(slide, left, top, width, height, data, col_widths=None, font_size=11): rows, cols = len(data), len(data[0]) table_shape = slide.shapes.add_table(rows, cols, left, top, width, height) table = table_shape.table if col_widths: for i, w in enumerate(col_widths): table.columns[i].width = w for r in range(rows): for c in range(cols): cell = table.cell(r, c) cell.text = "" tf = cell.text_frame tf.word_wrap = True p = tf.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_header = r == 0 set_run_font(run, size=font_size if not is_header else font_size + 1, bold=is_header, color=WHITE if is_header else DARK) cell.fill.solid() cell.fill.fore_color.rgb = NAVY if is_header else (SOFT if r % 2 == 0 else WHITE) return table_shape def set_slide_bg(slide, color=LIGHT): fill = slide.background.fill fill.solid() fill.fore_color.rgb = color def header_bar(slide, title, subtitle=None): """Single text frame for title+subtitle to avoid stacked textbox overlap.""" add_shape(slide, Inches(0), Inches(0), SLIDE_W, Inches(HEADER_H), NAVY) add_shape(slide, Inches(0), Inches(HEADER_H), SLIDE_W, Inches(0.05), ACCENT) box = slide.shapes.add_textbox(Inches(0.4), Inches(0.12), Inches(12.5), Inches(0.72)) tf = box.text_frame tf.word_wrap = True p = tf.paragraphs[0] p.alignment = PP_ALIGN.LEFT p.space_before = Pt(0) p.space_after = Pt(2) run = p.add_run() run.text = title set_run_font(run, size=22, bold=True, color=WHITE) if subtitle: p2 = tf.add_paragraph() p2.alignment = PP_ALIGN.LEFT p2.space_before = Pt(0) p2.space_after = Pt(0) run2 = p2.add_run() run2.text = subtitle set_run_font(run2, size=11, bold=False, color=RGBColor(0xB8, 0xD4, 0xE8)) def footer(slide, page): add_shape(slide, Inches(0), Inches(FOOTER_TOP), SLIDE_W, Inches(7.5 - FOOTER_TOP), NAVY) box = slide.shapes.add_textbox(Inches(0.3), Inches(FOOTER_TOP + 0.04), Inches(10.5), Inches(0.28)) tf = box.text_frame p = tf.paragraphs[0] run = p.add_run() run.text = "智慧医院 AI 影像诊断与电子病历辅助决策系统 · 实训答辩 · 仅供教学演示" set_run_font(run, size=10, color=RGBColor(0xC5, 0xD8, 0xE8)) box2 = slide.shapes.add_textbox(Inches(11.3), Inches(FOOTER_TOP + 0.04), Inches(1.7), Inches(0.28)) p2 = box2.text_frame.paragraphs[0] p2.alignment = PP_ALIGN.RIGHT run2 = p2.add_run() run2.text = f"{page} / {TOTAL}" set_run_font(run2, size=10, bold=True, color=WHITE) def fit_image(slide, path, left, top, max_width, max_height): """Place image scaled to fit inside max box; never exceed max_height (prevents footer crash).""" path = Path(path) if not path.exists(): return None with PILImage.open(path) as im: iw, ih = im.size aspect = iw / ih # convert max to inches floats mw = max_width if isinstance(max_width, float) else max_width / 914400 mh = max_height if isinstance(max_height, float) else max_height / 914400 # left/top may be Inches def to_in(v): return v if isinstance(v, float) else v / 914400 l = to_in(left) t = to_in(top) w = mw h = w / aspect if h > mh: h = mh w = h * aspect return slide.shapes.add_picture(str(path), Inches(l), Inches(t), width=Inches(w), height=Inches(h)) def blank_slide(prs): return prs.slides.add_slide(prs.slide_layouts[6]) def card(slide, left, top, width, height, title, body_lines, color=TEAL, title_size=14, body_size=12): """Card with colored title band + body text, all non-overlapping zones.""" add_shape(slide, Inches(left), Inches(top), Inches(width), Inches(height), WHITE, SOFT) title_h = 0.48 band = add_shape(slide, Inches(left), Inches(top), Inches(width), Inches(title_h), color) fill_shape_text(band, [title], size=title_size, bold=True, color=WHITE, align=PP_ALIGN.CENTER) # body region strictly below band body_top = top + title_h + 0.12 body_h = height - title_h - 0.2 if body_lines: add_paras( slide, Inches(left + 0.15), Inches(body_top), Inches(width - 0.3), Inches(body_h), body_lines, size=body_size, color=DARK, spacing=5, ) # ---------------- slides ---------------- def slide_01_cover(prs): slide = blank_slide(prs) fit_image(slide, ASSETS / "cover_bg.png", 0, 0, 13.333, 7.5) # Center panel — leave room for disclaimer add_shape(slide, Inches(1.1), Inches(1.35), Inches(11.1), Inches(4.35), PANEL) add_textbox(slide, Inches(1.4), Inches(1.55), Inches(10.5), Inches(0.35), "SMART HOSPITAL · DEFENSE PRESENTATION", size=13, color=ACCENT, align=PP_ALIGN.CENTER) add_textbox(slide, Inches(1.4), Inches(2.05), Inches(10.5), Inches(1.1), "智慧医院 AI 影像诊断与\n电子病历辅助决策系统", size=32, bold=True, color=WHITE, align=PP_ALIGN.CENTER) add_textbox(slide, Inches(1.4), Inches(3.25), Inches(10.5), Inches(0.4), "实训教学演示级 Web 应用 · 业务系统 + AI 微服务混合架构", size=15, color=RGBColor(0xB8, 0xD4, 0xE8), align=PP_ALIGN.CENTER) chips = [("文档版本", "v1.0"), ("日期", "2026-07-27"), ("前端", "Vue3 :5173"), ("业务端", "Boot :8080"), ("AI", "FastAPI :8001")] for i, (k, v) in enumerate(chips): x = 1.5 + i * 2.05 chip = add_shape(slide, Inches(x), Inches(4.0), Inches(1.9), Inches(0.95), CHIP) fill_shape_text(chip, [k, v], size=12, bold=False, color=WHITE, align=PP_ALIGN.CENTER) # make first line accent-ish by rewriting as single centered block is OK add_textbox(slide, Inches(1.2), Inches(6.2), Inches(10.9), Inches(0.7), "声明:本系统输出仅供教学实训与辅助决策演示,不能替代执业医师的正式诊断与医疗文书。", size=12, color=RGBColor(0xE2, 0xEC, 0xF4), align=PP_ALIGN.CENTER) def slide_02_toc(prs): slide = blank_slide(prs) set_slide_bg(slide) header_bar(slide, "目录导览", "课程/实训答辩叙事结构") sections = [ ("01", "主题与需求", "P3 – P6", "主题定位 · 场景角色 · 痛点改造 · 需求优先级", TEAL), ("02", "功能与业务", "P7 – P11", "功能全景 · 核心模块 · 影像诊断 · 病历决策 · 管理端", ACCENT), ("03", "架构与技术", "P12 – P16", "技术栈 · 逻辑架构 · 工程结构 · 核心流程 · 数据与安全", NAVY), ("04", "部署与总结", "P17 – P19", "运行部署 · 演示路径 · 设计取舍 · 边界与展望", ORANGE), ] # 4 rows fit in 1.15 ~ 6.9 row_h = 1.25 gap = 0.12 start_y = 1.15 for i, (num, title, pages, desc, color) in enumerate(sections): y = start_y + i * (row_h + gap) add_shape(slide, Inches(0.55), Inches(y), Inches(12.2), Inches(row_h), WHITE, SOFT) num_box = add_shape(slide, Inches(0.55), Inches(y), Inches(1.25), Inches(row_h), color) fill_shape_text(num_box, [num], size=26, bold=True, color=WHITE) add_textbox(slide, Inches(2.05), Inches(y + 0.22), Inches(7.2), Inches(0.4), title, size=20, bold=True, color=NAVY) add_textbox(slide, Inches(2.05), Inches(y + 0.7), Inches(7.5), Inches(0.35), desc, size=12, color=MUTED) page_box = add_shape(slide, Inches(10.4), Inches(y + 0.35), Inches(1.9), Inches(0.55), color) fill_shape_text(page_box, [pages], size=13, bold=True, color=WHITE) footer(slide, 2) def slide_03_theme(prs): slide = blank_slide(prs) set_slide_bg(slide) header_bar(slide, "项目主题与建设目标", "围绕医院诊疗两条核心链路展开") # Left image max height keeps clear of footer fit_image(slide, ASSETS / "dual_chain.png", 0.3, 1.12, 8.0, 5.7) # Right goals — fixed band, no overflow gx, gy, gw, gh = 8.5, 1.12, 4.4, 5.7 add_shape(slide, Inches(gx), Inches(gy), Inches(gw), Inches(gh), WHITE, SOFT) band = add_shape(slide, Inches(gx), Inches(gy), Inches(gw), Inches(0.45), NAVY) fill_shape_text(band, ["建设目标五维"], size=14, bold=True, color=WHITE) goals = [ ("业务闭环", "登录、患者、影像、病历、预约、用户管理"), ("AI 可演示", "YOLO 实检 + DeepSeek 兼容 / 模板降级"), ("前后端分离", "Vue3 SPA + Spring Boot + FastAPI"), ("可离线实训", "默认 H2,AI 宕机业务仍可演示"), ("安全可讲", "JWT · 角色权限 · BCrypt · 双端守卫"), ] item_h = 0.95 for i, (t, d) in enumerate(goals): y = gy + 0.55 + i * item_h item = add_shape(slide, Inches(gx + 0.15), Inches(y), Inches(gw - 0.3), Inches(0.85), SOFT) fill_shape_text(item, [t, d], size=12, bold=False, color=DARK, align=PP_ALIGN.LEFT) footer(slide, 3) def slide_04_scenes(prs): slide = blank_slide(prs) set_slide_bg(slide) header_bar(slide, "应用场景与用户角色", "教学/演示四类场景 + 三角色权限模型") # Image top region fit_image(slide, ASSETS / "scene_cards.png", 0.4, 1.1, 12.5, 2.7) data = [ ["角色", "代码枚举", "核心诉求"], ["系统管理员", "ADMIN", "用户管理、AI 大模型配置、YOLO 权重、全局运维"], ["临床医生", "DOCTOR", "患者与病历、辅助决策、预约、查看影像报告"], ["影像医师", "RADIOLOGIST", "影像登记、上传、触发 AI 诊断、审阅标注与报告"], ["访客/未登录", "—", "仅可访问登录页"], ] add_table(slide, Inches(0.4), Inches(4.0), Inches(12.5), Inches(2.85), data, col_widths=[Inches(2.2), Inches(2.4), Inches(7.9)], font_size=12) footer(slide, 4) def slide_05_pain(prs): slide = blank_slide(prs) set_slide_bg(slide) header_bar(slide, "需求分析:痛点与改造", "从教学型 CRUD 到可演示的 AI 混合架构") data = [ ["问题", "传统表现", "本项目对策"], ["业务割裂", "缺统一档案视图", "患者 360° 档案关联全业务"], ["AI 仅假数据", "随机文案/关键字", "YOLO 实检 + LLM/RAG,可降级"], ["技术栈陈旧", "Thymeleaf 耦合", "Vue3 SPA + REST 分离"], ["权限薄弱", "仅 Session 登录", "JWT + 角色 + 双端守卫"], ["不可降级", "依赖失败即中断", "Spring 规则/模板兜底"], ] add_table(slide, Inches(0.35), Inches(1.15), Inches(7.0), Inches(3.55), data, col_widths=[Inches(1.5), Inches(2.5), Inches(3.0)], font_size=11) fit_image(slide, ASSETS / "transform.png", 7.55, 1.15, 5.4, 3.55) nfr = add_shape(slide, Inches(0.35), Inches(4.9), Inches(12.6), Inches(1.95), WHITE, TEAL) fill_shape_text( nfr, [ "非功能需求落地", "性能:@Async 异步诊断 + 有限轮询 | 可用性:默认 H2 + 种子数据 | 安全:JWT / BCrypt / CORS", "可维护:Result + 全局异常 | 可扩展:AI 独立进程 | 兼容:OpenAI 协议(DeepSeek / Qwen)", ], size=12, color=DARK, align=PP_ALIGN.LEFT, ) footer(slide, 5) def slide_06_priority(prs): slide = blank_slide(prs) set_slide_bg(slide) header_bar(slide, "功能需求优先级", "P0 必须 · P1 增强 · P2 体验(文档 2.3)") fit_image(slide, ASSETS / "priority_board.png", 0.35, 1.1, 12.6, 5.8) footer(slide, 6) def slide_07_panorama(prs): slide = blank_slide(prs) set_slide_bg(slide) header_bar(slide, "功能全景", "身份权限 · 主数据 · 诊疗业务 · 智能能力") fit_image(slide, ASSETS / "feature_panorama.png", 0.3, 1.1, 8.7, 5.8) routes = [ "/login 登录", "/dashboard 仪表盘", "/patients 患者", "/imaging 影像诊断", "/emrs 电子病历", "/appointments 预约", "/ai-assistant 助手", "/ai-knowledge 知识库", "/ai-settings ADMIN", "/ai-yolo ADMIN", "/users ADMIN", ] card(slide, 9.2, 1.1, 3.7, 5.8, "前端路由一览", ["• " + r for r in routes], TEAL, body_size=12) footer(slide, 7) def slide_08_modules(prs): slide = blank_slide(prs) set_slide_bg(slide) header_bar(slide, "核心业务模块", "仪表盘 · 患者 360° · 预约状态机") cards = [ ("仪表盘 Dashboard", TEAL, [ "• KPI:患者 / 影像 / 病历 / 预约及状态分布", "• 近 7 日业务趋势(ECharts)", "• 检查类型、诊断状态分布饼图", "• 快捷入口与待办,便于演示导览", ]), ("患者管理 + 360° 档案", ACCENT, [ "• 分页列表、姓名等关键字搜索", "• 证件、电话、地址、既往史", "• 抽屉关联:影像 / 病历 / 预约", "• 表单等宽两列,弹窗体验优化", ]), ("预约挂号", ORANGE, [ "• 新建 / 编辑 / 删除预约", "• 状态:预约 → 确认 → 完成", "• 亦可:取消 / 未到诊", "• 按日筛选、统计卡片、详情抽屉", ]), ] for i, (title, color, items) in enumerate(cards): card(slide, 0.35 + i * 4.3, 1.15, 4.1, 5.7, title, items, color, title_size=15, body_size=13) footer(slide, 8) def slide_09_imaging(prs): slide = blank_slide(prs) set_slide_bg(slide) header_bar(slide, "影像诊断模块(深度)", "检查登记 · 状态机 · YOLO 结果展示 · 可降级") # Row 1: capability + state machine card(slide, 0.3, 1.1, 4.0, 2.35, "检查登记能力", [ "• 类型:X_RAY / CT / MRI / ULTRASOUND", "• 选择患者、部位、上传或路径", "• 筛选:关键字 / 状态 / 类型", "• 状态 KPI 卡片可快速过滤", ], TEAL, body_size=11) fit_image(slide, ASSETS / "state_machine.png", 4.5, 1.1, 8.4, 2.35) # Row 2: yolo + results — constrained heights fit_image(slide, ASSETS / "yolo_concept.png", 0.3, 3.6, 5.3, 3.3) card(slide, 5.8, 3.6, 7.1, 3.3, "诊断结果展示清单", [ "① 诊断印象 + 置信度仪表盘", "② 原始影像 vs YOLO 标注图对比", "③ 影像所见(分段)+ 建议编号列表", "④ 检测明细:类别 / 置信度 / bbox", "⑤ 完整报告:头 / 所见 / 印象 / 建议 / 声明", "⑥ AI 不可用 → Spring 本地规则降级", ], ACCENT, body_size=12) footer(slide, 9) def slide_10_emr(prs): slide = blank_slide(prs) set_slide_bg(slide) header_bar(slide, "电子病历与 AI 辅助决策(深度)", "结构化录入 → RAG/LLM 决策 → 分节 Dialog") fit_image(slide, ASSETS / "decision_flow.png", 0.35, 1.1, 12.6, 2.5) # Bottom two cards fields = ["主诉", "现病史", "体格检查", "诊断", "治疗方案", "用药", "随访"] add_shape(slide, Inches(0.35), Inches(3.8), Inches(6.2), Inches(3.05), WHITE, SOFT) band = add_shape(slide, Inches(0.35), Inches(3.8), Inches(6.2), Inches(0.45), TEAL) fill_shape_text(band, ["病历字段链路"], size=13, bold=True, color=WHITE) for i, f in enumerate(fields): x = 0.55 + (i % 4) * 1.45 y = 4.45 + (i // 4) * 0.95 chip = add_shape(slide, Inches(x), Inches(y), Inches(1.35), Inches(0.7), TEAL if i < 4 else ACCENT) fill_shape_text(chip, [f], size=12, bold=True, color=WHITE) card(slide, 6.8, 3.8, 6.15, 3.05, "决策输出七块 + 关键词增强", [ "• 输出:治疗 / 用药 / 护理 / 随访 / 风险 / 冲突 / RAG 来源", "• 关键词:高血压 · 糖尿病 · 肺炎 · 结节", "• 配置 LLM 后由大模型增强;失败回退模板", "• 保存后可自动弹出;列表可再次查看", ], ACCENT, body_size=12) footer(slide, 10) def slide_11_admin(prs): slide = blank_slide(prs) set_slide_bg(slide) header_bar(slide, "智能运营与管理端", "AI 助手 · 知识库 · LLM 配置 · YOLO 权重 · 用户权限") mods = [ ("AI 助手", ["多轮对话 · Markdown", "marked + DOMPurify", "history → chat-history.json"], TEAL), ("知识库", ["文档 CRUD · 启停", "配合 RAG 管线", "内置医学 MD 片段"], ACCENT), ("AI 配置", ["仅 ADMIN", "Base URL / Key / Model", "settings.json 持久化"], NAVY), ("YOLO 管理", ["仅 ADMIN", "权重列表/激活/上传", "real / demo 模式"], ORANGE), ] for i, (t, lines, c) in enumerate(mods): card(slide, 0.3 + i * 3.25, 1.1, 3.1, 2.4, t, ["• " + x for x in lines], c, body_size=11) fit_image(slide, ASSETS / "llm_sync.png", 0.3, 3.7, 7.9, 3.15) card(slide, 8.4, 3.7, 4.5, 3.15, "权限双端防护", [ "• 前端:路由 meta.roles 隐藏/拦截", "• 后端:@PreAuthorize 接口鉴权", "• 用户 CRUD:角色/科室/启停", "• 禁止停用当前登录账号", "• doctor 访问 /users → 403", ], NAVY, body_size=12) footer(slide, 11) def slide_12_tech(prs): slide = blank_slide(prs) set_slide_bg(slide) header_bar(slide, "技术栈总览", "版本号与文档附录 A 对齐") # Image top, table bottom — no overlap fit_image(slide, ASSETS / "tech_layers.png", 0.4, 1.1, 12.5, 2.7) 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 / JPA", "3.3.4 / 17 / 0.12.6"], ["数据库", "H2 默认 · MySQL 可选 profile", "mem:smart_hospital / application-mysql.yml"], ["AI 服务", "FastAPI / YOLO / LangChain / LLM", "≥0.110 / Ultralytics / ≥0.2 / DeepSeek 兼容"], ["工程", "前端代理 /api→8080;AI base-url 8001", "uploads/ · data/ai/ · data/weights/"], ] add_table(slide, Inches(0.4), Inches(4.0), Inches(12.5), Inches(2.9), data, col_widths=[Inches(1.5), Inches(5.6), Inches(5.4)], font_size=11) footer(slide, 12) def slide_13_arch(prs): slide = blank_slide(prs) set_slide_bg(slide) header_bar(slide, "系统逻辑架构", "浏览器 → 业务端 → 数据库 / AI 微服务") fit_image(slide, ASSETS / "architecture.png", 0.35, 1.1, 12.6, 5.85) footer(slide, 13) def slide_14_modules_dir(prs): slide = blank_slide(prs) set_slide_bg(slide) header_bar(slide, "工程目录与模块结构", "frontend · smart-hospital · ai-service") fit_image(slide, ASSETS / "module_tree.png", 0.25, 1.1, 8.3, 5.8) card(slide, 8.7, 1.1, 4.2, 5.8, "关键落盘与配置", [ "• ./uploads/images 影像文件", "• ./uploads 头像等", "• ./data/ai/settings.json", "• ./data/ai/chat-history.json", "• ai-service/data/weights/", "• ai-service/app/knowledge/", "• jwt.* 过期与前缀", "• cors → localhost:5173", "• ai.service.base-url:8001", "• multipart 最大约 500MB", "• H2 控制台 /h2-console", ], NAVY, body_size=12) footer(slide, 14) def slide_15_flow(prs): slide = blank_slide(prs) set_slide_bg(slide) header_bar(slide, "核心流程:AI 影像诊断", "异步状态机 + FastAPI 检测报告 + 前端轮询") fit_image(slide, ASSETS / "imaging_flow.png", 0.3, 1.1, 12.7, 5.85) footer(slide, 15) def slide_16_data_api(prs): slide = blank_slide(prs) set_slide_bg(slide) header_bar(slide, "数据模型 · 接口 · 安全", "实体关系、统一响应、REST 与鉴权要点") fit_image(slide, ASSETS / "er_diagram.png", 0.25, 1.1, 6.3, 4.0) # Right column stacked, no collision card(slide, 6.7, 1.1, 6.2, 1.35, "统一响应 Result", [ '{ "code": 0, "message": "OK", "data": {} }', "code != 0 → Axios 拦截器 ElMessage 提示", ], TEAL, body_size=11) data = [ ["方法/路径", "说明", "权限"], ["POST /api/auth/login", "登录签发 JWT", "公开"], ["GET /api/stats/overview", "仪表盘统计", "登录"], ["POST .../analyze/{id}", "触发诊断", "登录"], ["GET .../ai-suggestions", "辅助决策", "登录"], ["* /api/users/**", "用户管理", "ADMIN"], ["AI /imaging/analyze 等", "检测/决策/RAG", "经业务端"], ] add_table(slide, Inches(6.7), Inches(2.6), Inches(6.2), Inches(2.5), data, col_widths=[Inches(2.7), Inches(2.0), Inches(1.5)], font_size=10) card(slide, 0.25, 5.25, 12.65, 1.6, "安全要点", [ "JWT 无状态 · BCrypt 密码 · CORS 白名单 · 角色 ADMIN / DOCTOR / RADIOLOGIST", "前端路由守卫 + 后端 @PreAuthorize · 修改密码后需重新登录", ], NAVY, body_size=12) footer(slide, 16) def slide_17_deploy(prs): slide = blank_slide(prs) set_slide_bg(slide) header_bar(slide, "部署运行与演示账号", "启动顺序 · 健康检查 · 内置账号 · 演示剧本") env = [ ["组件", "要求"], ["JDK", "17+"], ["Node.js", "18+"], ["Python", "3.10+(推荐 3.11/3.12)"], ["可选", "MySQL 8.x;GPU 非必须"], ] add_table(slide, Inches(0.3), Inches(1.1), Inches(3.9), Inches(2.15), env, col_widths=[Inches(1.2), Inches(2.7)], font_size=11) health = [ ["服务", "地址"], ["前端", "http://localhost:5173"], ["业务 API", "http://localhost:8080/api/..."], ["H2 控制台", "http://localhost:8080/h2-console"], ["AI 健康", "http://127.0.0.1:8001/health"], ["AI 文档", "http://127.0.0.1:8001/docs"], ] add_table(slide, Inches(4.4), Inches(1.1), Inches(4.5), Inches(2.15), health, col_widths=[Inches(1.4), Inches(3.1)], font_size=10) card(slide, 9.1, 1.1, 3.85, 2.15, "启动顺序", [ "1) ai-service :8001", "2) smart-hospital :8080", "3) frontend :5173", "推荐:先 AI → 业务 → 前端", ], ACCENT, body_size=11) accounts = [ ["用户名", "密码", "角色", "说明"], ["admin", "admin123", "ADMIN", "用户 / AI 配置 / YOLO"], ["doctor1", "pass123", "DOCTOR", "临床主演示"], ["radio1", "radio123", "RADIOLOGIST", "影像演示"], ] add_table(slide, Inches(0.3), Inches(3.45), Inches(12.7), Inches(1.55), accounts, col_widths=[Inches(2.2), Inches(2.5), Inches(2.8), Inches(5.2)], font_size=12) fit_image(slide, ASSETS / "demo_timeline.png", 0.3, 5.2, 12.7, 1.7) footer(slide, 17) def slide_18_design(prs): slide = blank_slide(prs) set_slide_bg(slide) header_bar(slide, "设计取舍 · 边界 · 展望", "答辩可讲清「为什么这样设计」") cols = [ ("关键设计取舍", TEAL, [ "• 默认 H2 → 降低实训门槛", "• AI 独立进程 → 隔离 Python 依赖", "• JWT 无状态 → 适配前后端分离", "• LLM 可关 → 无 Key 仍可演示", "• 报告强制分段 → 避免墙文本", "• Dialog append-to-body → 防裁切", ]), ("已知边界(非缺陷)", ORANGE, [ "• 非完整 HIS/PACS/RIS 产品", "• 无医保 / 收费 / CA / 电子签", "• YOLO 为演示级权重与类别", "• H2 重启丢失业务表数据", "• 外网 LLM 受网络/额度影响", "• DICOM 完整工作流非重点", ]), ("后续可扩展", ACCENT, [ "• DICOM 解析与序列阅片", "• 报告 PDF 与医师签收", "• 更细科室权限与审计", "• 向量库增强 RAG", "• Docker Compose 一键拉起", "• 接口自动化测试与 CI", ]), ] for i, (title, color, items) in enumerate(cols): card(slide, 0.3 + i * 4.3, 1.1, 4.15, 4.35, title, items, color, body_size=12) cmp_data = [ ["维度", "早期", "当前"], ["前端", "Thymeleaf + Bootstrap", "Vue3 + Element Plus + ECharts"], ["安全", "HttpSession", "Spring Security + JWT"], ["AI 影像", "规则/随机模拟", "YOLO 实检 + LLM/模板报告"], ] add_table(slide, Inches(0.3), Inches(5.6), Inches(12.7), Inches(1.3), cmp_data, col_widths=[Inches(2.0), Inches(5.2), Inches(5.5)], font_size=11) footer(slide, 18) def slide_19_end(prs): slide = blank_slide(prs) fit_image(slide, ASSETS / "closing_bg.png", 0, 0, 13.333, 7.5) add_textbox(slide, Inches(1), Inches(1.1), Inches(11.3), Inches(0.55), "总结与致谢", size=30, bold=True, color=WHITE, align=PP_ALIGN.CENTER) add_textbox(slide, Inches(1.2), Inches(1.7), Inches(10.9), Inches(0.45), "智慧医院 = 业务闭环 + 真 AI 链路 + 可降级演示 + 可讲清的工程与安全", size=14, color=ACCENT, align=PP_ALIGN.CENTER) values = [ ("业务闭环", "患者—影像—病历—预约统一"), ("真 AI 链路", "YOLO 检测 + RAG/LLM 决策"), ("可降级", "无 Key / AI 宕机仍可答辩"), ("工程可讲", "三端分离 · JWT · 异步状态机"), ] for i, (t, d) in enumerate(values): x = 1.15 + i * 2.85 box = add_shape(slide, Inches(x), Inches(2.4), Inches(2.65), Inches(1.45), CHIP) fill_shape_text(box, [t, d], size=12, bold=False, color=WHITE, align=PP_ALIGN.CENTER) add_textbox(slide, Inches(1.0), Inches(4.15), Inches(11.3), Inches(0.55), "验证要点:三端可登录 · 角色 403 正确 · 影像 COMPLETED 报告分段 · 病历建议多类 · 预约可流转 · 改密需重登", size=12, color=RGBColor(0xC5, 0xD8, 0xE8), align=PP_ALIGN.CENTER) add_textbox(slide, Inches(1), Inches(4.9), Inches(11.3), Inches(0.55), "谢谢聆听 · 欢迎提问", size=26, bold=True, color=WHITE, align=PP_ALIGN.CENTER) add_textbox(slide, Inches(1.2), Inches(5.7), Inches(10.9), Inches(0.9), "声明:输出内容仅供教学实训与辅助决策演示,不能替代执业医师正式诊断与医疗文书。\n" "文档依据:项目详细文档.md v1.0 · 2026-07-27", size=11, color=RGBColor(0xB8, 0xD4, 0xE8), align=PP_ALIGN.CENTER) def main(): prs = Presentation() prs.slide_width = SLIDE_W prs.slide_height = SLIDE_H slide_01_cover(prs) slide_02_toc(prs) slide_03_theme(prs) slide_04_scenes(prs) slide_05_pain(prs) slide_06_priority(prs) slide_07_panorama(prs) slide_08_modules(prs) slide_09_imaging(prs) slide_10_emr(prs) slide_11_admin(prs) slide_12_tech(prs) slide_13_arch(prs) slide_14_modules_dir(prs) slide_15_flow(prs) slide_16_data_api(prs) slide_17_deploy(prs) slide_18_design(prs) slide_19_end(prs) try: prs.save(OUT) print(f"Saved: {OUT}") except PermissionError: prs.save(OUT_ALT) print(f"原文件被占用,已另存: {OUT_ALT}") print(f"Slides: {len(prs.slides)}") if __name__ == "__main__": main()