#!/usr/bin/env python3 """Build the defense deck from the official CICIEC PPT template.""" from __future__ import annotations import sys from io import BytesIO from pathlib import Path from zipfile import ZipFile try: from pptx import Presentation except ModuleNotFoundError: sys.path.insert(0, "/tmp/codex_pptx") from pptx import Presentation from PIL import Image from pptx.dml.color import RGBColor from pptx.enum.shapes import MSO_SHAPE from pptx.enum.text import MSO_ANCHOR, PP_ALIGN from pptx.util import Inches, Pt ROOT = Path(__file__).resolve().parents[3] HERE = Path(__file__).resolve().parent SYSTEM = ROOT / "doc/reports/system" MLKEM = ROOT / "doc/reports/mlkem_ip" TEMPLATE = HERE / "ppt_template.pptx" OUTPUT = HERE / "defense_presentation.pptx" W = 13.333 H = 7.5 # Template typefaces and palette. FONT = "HarmonyOS Sans SC" TITLE_FONT = "Alimama ShuHeiTi Bold" LATIN_FONT = "Arial" INK = "202533" MUTED = "667085" BLUE = "142DA8" ACCENT_BLUE = "4874CB" ORANGE = "EE822F" YELLOW = "F2BA02" GREEN = "75BD42" TEAL = "30C0B4" RED = "E54C5E" WHITE = "FFFFFF" PALE_BLUE = "EDF3FF" PALE_ORANGE = "FFF3E9" PALE_TEAL = "EAF9F7" PALE_GRAY = "F5F7FA" LINE = "D7DEEA" def color(value: str) -> RGBColor: return RGBColor.from_string(value) with ZipFile(TEMPLATE) as archive: MEDIA = { name.rsplit("/", 1)[-1]: archive.read(name) for name in archive.namelist() if name.startswith("ppt/media/") and not name.endswith("/") } prs = Presentation(str(TEMPLATE)) while len(prs.slides): sld_id = prs.slides._sldIdLst[0] prs.part.drop_rel(sld_id.rId) del prs.slides._sldIdLst[0] blank = prs.slide_layouts[6] def add_template_image(slide, media_name, x, y, w, h): return slide.shapes.add_picture( BytesIO(MEDIA[media_name]), Inches(x), Inches(y), Inches(w), Inches(h) ) def add_rect(slide, x, y, w, h, fill=WHITE, line=None, radius=False): shape_type = MSO_SHAPE.ROUNDED_RECTANGLE if radius else MSO_SHAPE.RECTANGLE shape = slide.shapes.add_shape(shape_type, Inches(x), Inches(y), Inches(w), Inches(h)) shape.fill.solid() shape.fill.fore_color.rgb = color(fill) if line: shape.line.color.rgb = color(line) shape.line.width = Pt(1) else: shape.line.fill.background() if radius: try: shape.adjustments[0] = 0.06 except Exception: pass return shape def add_text( slide, text, x, y, w, h, size=17, fill=INK, bold=False, font=FONT, align=PP_ALIGN.LEFT, valign=MSO_ANCHOR.TOP, margin=0.02, line_spacing=1.05, ): shape = slide.shapes.add_textbox(Inches(x), Inches(y), Inches(w), Inches(h)) frame = shape.text_frame frame.clear() frame.margin_left = frame.margin_right = Inches(margin) frame.margin_top = frame.margin_bottom = Inches(margin) frame.vertical_anchor = valign para = frame.paragraphs[0] para.text = text para.alignment = align para.space_after = Pt(0) para.line_spacing = line_spacing para.font.name = font para.font.size = Pt(size) para.font.bold = bold para.font.color.rgb = color(fill) return shape def add_rich(slide, runs, x, y, w, h, align=PP_ALIGN.LEFT): shape = slide.shapes.add_textbox(Inches(x), Inches(y), Inches(w), Inches(h)) frame = shape.text_frame frame.clear() frame.margin_left = frame.margin_right = Inches(0.02) frame.margin_top = frame.margin_bottom = Inches(0.02) para = frame.paragraphs[0] para.alignment = align for text, size, bold, fill, font in runs: run = para.add_run() run.text = text run.font.name = font run.font.size = Pt(size) run.font.bold = bold run.font.color.rgb = color(fill) return shape def add_bullets(slide, items, x, y, w, h, size=15, fill=INK, gap=5, accent=BLUE): shape = slide.shapes.add_textbox(Inches(x), Inches(y), Inches(w), Inches(h)) frame = shape.text_frame frame.clear() frame.margin_left = frame.margin_right = Inches(0.02) frame.margin_top = frame.margin_bottom = Inches(0.02) for index, item in enumerate(items): para = frame.paragraphs[0] if index == 0 else frame.add_paragraph() para.text = "• " + item para.font.name = FONT para.font.size = Pt(size) para.font.color.rgb = color(fill) para.space_after = Pt(gap) para.line_spacing = 1.06 return shape def add_arrow(slide, x, y, w, h, fill=BLUE): shape = slide.shapes.add_shape(MSO_SHAPE.RIGHT_ARROW, Inches(x), Inches(y), Inches(w), Inches(h)) shape.fill.solid() shape.fill.fore_color.rgb = color(fill) shape.line.fill.background() return shape def add_line(slide, x, y, w, h=0.02, fill=LINE): return add_rect(slide, x, y, w, h, fill) def add_image_contain(slide, path: Path, x, y, w, h, frame=True): if frame: add_rect(slide, x - 0.03, y - 0.03, w + 0.06, h + 0.06, WHITE, LINE) with Image.open(path) as image: iw, ih = image.size scale = min(w / iw, h / ih) pw, ph = iw * scale, ih * scale return slide.shapes.add_picture( str(path), Inches(x + (w - pw) / 2), Inches(y + (h - ph) / 2), Inches(pw), Inches(ph) ) def set_cell(cell, text, size=12, bold=False, fill=INK, bg=None, align=PP_ALIGN.LEFT): if bg: cell.fill.solid() cell.fill.fore_color.rgb = color(bg) frame = cell.text_frame frame.clear() frame.margin_left = frame.margin_right = Inches(0.07) frame.margin_top = frame.margin_bottom = Inches(0.03) frame.vertical_anchor = MSO_ANCHOR.MIDDLE para = frame.paragraphs[0] para.text = text para.alignment = align para.font.name = LATIN_FONT if "seed" in text else FONT para.font.size = Pt(size) para.font.bold = bold para.font.color.rgb = color(fill) def add_table(slide, rows, x, y, w, h, widths=None, size=12): shape = slide.shapes.add_table(len(rows), len(rows[0]), Inches(x), Inches(y), Inches(w), Inches(h)) table = shape.table if widths: for index, width in enumerate(widths): table.columns[index].width = Inches(width) for r, row in enumerate(rows): for c, value in enumerate(row): set_cell( table.cell(r, c), value, size=size, bold=r == 0, fill=WHITE if r == 0 else INK, bg=BLUE if r == 0 else (PALE_GRAY if r % 2 == 0 else WHITE), align=PP_ALIGN.CENTER if c else PP_ALIGN.LEFT, ) return table def add_metric(slide, value, label, x, y, w, accent=BLUE, suffix=""): add_rich( slide, [(value, 24, True, accent, TITLE_FONT), (suffix, 13, False, accent, LATIN_FONT)], x, y, w, 0.45, PP_ALIGN.CENTER, ) add_text(slide, label, x, y + 0.48, w, 0.45, 11, MUTED, align=PP_ALIGN.CENTER) def add_number_badge(slide, number, x, y, fill=BLUE): add_rect(slide, x, y, 0.47, 0.47, fill, radius=True) add_text(slide, number, x, y + 0.08, 0.47, 0.22, 12, WHITE, True, font=LATIN_FONT, align=PP_ALIGN.CENTER) def new_content_slide(title, number, subtitle=None): slide = prs.slides.add_slide(blank) add_template_image(slide, "image1.png", 0, 0, W, H) add_template_image(slide, "image2.png", 0.28, -0.02, 1.82, 1.12) add_text(slide, title, 2.4, 0.48, 8.55, 0.55, 24, BLUE, True, TITLE_FONT, PP_ALIGN.CENTER) add_rect(slide, 6.17, 1.17, 0.98, 0.07, ORANGE) if subtitle: add_text(slide, subtitle, 10.9, 0.53, 1.9, 0.28, 9.5, MUTED, align=PP_ALIGN.RIGHT) add_text(slide, str(number), 12.35, 6.98, 0.72, 0.3, 9, MUTED, font=LATIN_FONT, align=PP_ALIGN.LEFT) return slide def new_preview_slide(number, label): slide = prs.slides.add_slide(blank) add_template_image(slide, "image1.png", 0, 0, W, H) add_template_image(slide, "image2.png", 0.3, -0.02, 1.82, 1.12) add_text(slide, "作品重要亮点预览页", 3.2, 0.48, 6.95, 0.56, 25, BLUE, True, TITLE_FONT, PP_ALIGN.CENTER) add_rect(slide, 6.17, 1.17, 0.98, 0.07, ORANGE) add_text(slide, label, 10.85, 0.53, 1.95, 0.28, 10, ORANGE, True, align=PP_ALIGN.RIGHT) add_text(slide, str(number), 12.35, 6.98, 0.72, 0.3, 9, MUTED, font=LATIN_FONT, align=PP_ALIGN.LEFT) return slide # 1. Cover slide = prs.slides.add_slide(blank) add_template_image(slide, "image3.png", 0, 0, W, H) add_template_image(slide, "image4.png", 5.93, 0.65, 1.48, 1.48) add_text(slide, "第十届全国大学生集成电路创新创业大赛", 1.15, 2.2, 11.05, 0.48, 24, BLUE, True, TITLE_FONT, PP_ALIGN.CENTER) add_text(slide, "基于龙芯SoC的后量子密码与矩阵运算加速系统", 1.1, 2.82, 11.15, 0.78, 30, BLUE, True, TITLE_FONT, PP_ALIGN.CENTER) add_rect(slide, 4.2, 3.9, 4.93, 0.62, BLUE, radius=True) add_text(slide, "龙芯中科杯 · 系统设计答辩", 4.33, 4.05, 4.67, 0.26, 16, WHITE, True, align=PP_ALIGN.CENTER) add_text(slide, "作品编号:CICC1008439 · 团队:碳基生物", 4.25, 5.25, 4.85, 0.34, 13, INK, True, align=PP_ALIGN.CENTER) add_text(slide, "证据版本 d456463 · 2026-07-15", 4.4, 5.72, 4.55, 0.3, 11, MUTED, align=PP_ALIGN.CENTER) # 2. Team introduction slide = new_content_slide("团队介绍:围绕完整系统形成协同分工", 2, "团队与项目") add_text(slide, "团队名称:碳基生物", 0.88, 1.48, 4.2, 0.36, 16, BLUE, True) roles = [ (0.82, BLUE, PALE_BLUE, "系统架构与SoC集成", ["OpenLA500平台集成", "AXI、DMA与多时钟域", "SRAM控制器优化"]), (4.52, TEAL, PALE_TEAL, "密码算法与RTL实现", ["全参数ML-KEM架构", "Keccak、NTT与多项式算子", "常量时间解封装"]), (8.22, ORANGE, PALE_ORANGE, "软件验证、测试与文档", ["板级驱动与测试程序", "分层仿真和证据采集", "系统报告与答辩材料"]), ] for x, accent, pale, title, items in roles: add_rect(slide, x, 2.08, 3.35, 3.72, pale, accent, radius=True) add_text(slide, title, x + 0.18, 2.38, 2.99, 0.36, 17, accent, True, TITLE_FONT, PP_ALIGN.CENTER) add_bullets(slide, items, x + 0.32, 3.12, 2.72, 1.85, size=13, gap=10) add_rect(slide, 1.0, 6.18, 11.15, 0.55, PALE_GRAY, LINE) add_text(slide, "以统一接口、可重复测试和版本化证据贯通硬件、软件与文档。", 1.2, 6.34, 10.75, 0.24, 13, INK, True, align=PP_ALIGN.CENTER) # 3. Contents slide = prs.slides.add_slide(blank) add_template_image(slide, "image3.png", 0, 0, W, H) add_text(slide, "目录", 0.82, 0.48, 1.6, 0.55, 29, BLUE, True, TITLE_FONT) add_text(slide, "CONTENTS", 0.91, 1.15, 1.5, 0.22, 11, MUTED, font=LATIN_FONT) contents = [ ("1", "项目与系统方案", "开发过程、设计目标与系统数据流", BLUE), ("2", "自主计算IP", "Matmul流水与全参数ML-KEM", ORANGE), ("3", "验证与实现结果", "分层验证、资源时序与最终性能", TEAL), ("4", "心得与后续工作", "工程认识、设计取舍与优化路线", GREEN), ] for i, (num, title, sub, accent) in enumerate(contents): y = 1.75 + i * 1.2 add_rect(slide, 2.25, y, 0.58, 0.58, accent) add_text(slide, num, 2.25, y + 0.13, 0.58, 0.24, 15, WHITE, True, align=PP_ALIGN.CENTER) add_text(slide, title, 3.18, y - 0.02, 4.35, 0.34, 17, INK, True) add_text(slide, sub, 3.18, y + 0.42, 5.1, 0.3, 12, MUTED) # 4. Preview: architecture and contributions slide = new_preview_slide(4, "系统方案") add_rect(slide, 0.78, 1.52, 7.75, 4.8, WHITE, LINE) add_image_contain(slide, SYSTEM / "system_arch.png", 0.94, 1.7, 7.43, 4.45, frame=False) add_text(slide, "OpenLA500 + DMA数据面 + 三个自主计算IP", 0.95, 6.4, 7.4, 0.3, 13, BLUE, True, align=PP_ALIGN.CENTER) highlights = [ ("01", "硬件自治", "五千组 Matmul 任务一次启动完成", BLUE), ("02", "存储优化", "SRAM连续突发达到1 beat/cycle", ORANGE), ("03", "全参数密码", "单比特流覆盖三等级、三操作", TEAL), ("04", "异步协同", "三时钟域通过全五通道AXI FIFO连接", GREEN), ] for i, (num, title, desc, accent) in enumerate(highlights): y = 1.62 + i * 1.18 add_number_badge(slide, num, 8.85, y, accent) add_text(slide, title, 9.48, y - 0.02, 2.55, 0.3, 15, accent, True) add_text(slide, desc, 9.48, y + 0.37, 2.85, 0.48, 11.5, INK) add_rect(slide, 8.78, 6.25, 3.72, 0.58, PALE_BLUE, BLUE) add_text(slide, "全系统 0 DSP · 三时钟域零违例", 8.92, 6.42, 3.44, 0.24, 12, BLUE, True, align=PP_ALIGN.CENTER) # 5. Preview: measured results slide = new_preview_slide(5, "关键结果") metrics = [ ("4.911765", "Matmul 5000组最终时间", BLUE, " ms"), ("827.80", "FFT DMA相对软件加速", ORANGE, "×"), ("48.95–53.33", "ML-KEM DMA相对软件加速", TEAL, "×"), ("0", "DSP48E1使用量", GREEN, " DSP"), ] for i, (value, label, accent, suffix) in enumerate(metrics): x = 0.72 + i * 3.12 add_rect(slide, x, 1.62, 2.77, 1.2, WHITE, accent, radius=True) add_metric(slide, value, label, x + 0.12, 1.77, 2.53, accent, suffix) add_table( slide, [ ["验证对象", "测试规模", "结果"], ["ML-KEM主路径KAT", "3等级 × 3操作 × 1000", "9000 / 9000"], ["Decaps隐式拒绝", "3等级 × 100", "300 / 300"], ["ML-KEM板级诊断", "3等级 × 3路径 × 100", "900次零失败"], ["Matmul最终评测", "5000组 · seed a3772ec8", "4.911765 ms · 满分"], ["FFT板级验证", "CPU / DMA各3次", "误差Re=1、Im=2"], ], 0.92, 3.18, 7.2, 3.05, widths=[2.25, 2.9, 2.05], size=11.5, ) add_rect(slide, 8.5, 3.18, 3.88, 3.05, PALE_ORANGE, ORANGE, radius=True) add_text(slide, "实现质量", 8.82, 3.48, 3.2, 0.34, 18, ORANGE, True, TITLE_FONT, PP_ALIGN.CENTER) add_metric(slide, "55,066", "Slice LUT · 41.16%", 8.85, 4.04, 1.45, BLUE) add_metric(slide, "29.5", "BRAM Tile · 8.08%", 10.48, 4.04, 1.45, TEAL) add_metric(slide, "88,843", "时序端点", 8.85, 5.2, 1.45, GREEN) add_metric(slide, "+0.070", "全系统最小WNS / ns", 10.48, 5.2, 1.45, ORANGE) # 6. Development process slide = new_content_slide("开发过程:以端到端证据驱动迭代", 6, "项目与系统方案") stages = [ ("1", "基础SoC跑通", "完成处理器取指、串口及总线外设联调", BLUE), ("2", "计算IP接入", "实现Matmul、CRC32、FFT与全参数ML-KEM", TEAL), ("3", "数据通路优化", "引入DMA端点、双Bank调度和SRAM连续突发", ORANGE), ("4", "时钟与复位收敛", "完成PLL分域、AXI CDC和复位顺序验证", GREEN), ("5", "分层验证闭环", "建立叶模块、IP、SoC与板级四层证据", BLUE), ("6", "最终性能迭代", "压缩启动与稳态空闲,Matmul评测达到100分", ORANGE), ] for i, (num, title, desc, accent) in enumerate(stages): col, row = i % 3, i // 3 x, y = 0.78 + col * 4.12, 1.55 + row * 2.35 add_rect(slide, x, y, 3.68, 1.75, WHITE, accent, radius=True) add_number_badge(slide, num, x + 0.2, y + 0.25, accent) add_text(slide, title, x + 0.88, y + 0.23, 2.55, 0.3, 15, accent, True) add_text(slide, desc, x + 0.25, y + 0.82, 3.18, 0.68, 11.2, INK, align=PP_ALIGN.CENTER) if i < 2 or 3 <= i < 5: add_arrow(slide, x + 3.75, y + 0.74, 0.25, 0.17, accent) add_rect(slide, 1.0, 6.35, 11.15, 0.44, PALE_TEAL, TEAL) add_text(slide, "每轮迭代同时检查功能正确性、总线可观测性、时序与板级端到端时间。", 1.22, 6.48, 10.7, 0.2, 12.5, INK, True, align=PP_ALIGN.CENTER) # 7. Contest requirements slide = new_content_slide("赛题要求与设计响应", 7, "项目与系统方案") add_text(slide, "赛题不是孤立IP设计,而是完整SoC中的端到端实现。", 0.82, 1.48, 8.3, 0.34, 13, MUTED) add_table( slide, [ ["赛题要求 / 工程约束", "本系统的设计响应"], ["基于OpenLA500构建完整SoC", "双主八从AXI架构;CPU、系统、ML-KEM三个异步时钟域"], ["5000组4×4矩阵乘法,禁止DSP", "全LUT乘加、双Bank序列器、在线CRC32与快速通道"], ["实现FIPS 203 ML-KEM", "单比特流覆盖三等级、三操作;KAT与隐式拒绝验证"], ["提高端到端性能", "四通道DMA、端点传输、SRAM连续突发与硬件自主调度"], ], 0.85, 1.98, 11.6, 3.85, widths=[4.0, 7.6], size=13, ) add_rect(slide, 0.9, 6.15, 11.5, 0.58, PALE_TEAL, TEAL) add_text(slide, "答辩主线:赛题约束 → 系统架构 → 数据流与自主IP → 分层验证 → 实测结果", 1.1, 6.32, 11.1, 0.24, 14, INK, True, align=PP_ALIGN.CENTER) # 8. Architecture and ownership slide = new_content_slide("总体架构与自主设计边界", 8, "项目与系统方案") add_image_contain(slide, SYSTEM / "system_arch.png", 0.7, 1.48, 8.0, 5.15) add_rect(slide, 9.02, 1.5, 3.45, 3.78, WHITE, TEAL, radius=True) add_text(slide, "自主实现", 9.25, 1.78, 3.0, 0.34, 18, TEAL, True, TITLE_FONT, PP_ALIGN.CENTER) add_bullets( slide, [ "四通道DMA与加速器端点", "Matmul、批量序列器、CRC32", "1024点Radix-2² SDF FFT", "全参数ML-KEM硬件IP", "两级AXI跨时钟域桥", "SRAM控制器突发流水化", ], 9.28, 2.35, 2.95, 2.55, size=13.2, gap=5, ) add_rect(slide, 9.02, 5.5, 3.45, 1.1, PALE_GRAY, LINE, radius=True) add_text(slide, "赛事基础平台", 9.25, 5.72, 2.95, 0.28, 14, MUTED, True, align=PP_ALIGN.CENTER) add_text(slide, "OpenLA500、板级SRAM器件、UART及基础外设", 9.25, 6.08, 2.95, 0.36, 11, INK, align=PP_ALIGN.CENTER) # 9. Data movement, clocks and SRAM slide = new_content_slide("数据搬运、SRAM与三时钟域协同", 9, "项目与系统方案") clock_info = [ (0.8, BLUE, "CPU域 · 33.333 MHz", "OpenLA500 / Cache / AXI主端"), (5.0, TEAL, "系统域 · 50 MHz", "Crossbar / DMA / SRAM / Matmul / FFT"), (9.2, ORANGE, "ML-KEM域 · 76.923 MHz", "AXI前端 / Keccak / NTT / FSM"), ] for x, accent, title, desc in clock_info: add_rect(slide, x, 1.5, 3.3, 0.85, WHITE, accent, radius=True) add_text(slide, title, x + 0.1, 1.68, 3.1, 0.28, 13.5, accent, True, align=PP_ALIGN.CENTER) add_text(slide, desc, x + 0.1, 2.02, 3.1, 0.2, 9.5, MUTED, align=PP_ALIGN.CENTER) add_arrow(slide, 4.26, 1.78, 0.5, 0.25, BLUE) add_arrow(slide, 8.46, 1.78, 0.5, 0.25, TEAL) add_text(slide, "AXI双时钟FIFO", 3.84, 2.34, 1.35, 0.2, 9, MUTED, align=PP_ALIGN.CENTER) add_text(slide, "AXI双时钟FIFO", 8.04, 2.34, 1.35, 0.2, 9, MUTED, align=PP_ALIGN.CENTER) add_rect(slide, 0.82, 2.85, 5.7, 3.45, WHITE, BLUE, radius=True) add_text(slide, "硬件数据通路", 1.05, 3.08, 5.2, 0.3, 17, BLUE, True, TITLE_FONT, PP_ALIGN.CENTER) flow = [("CPU配置", BLUE), ("DMA", TEAL), ("ExtRAM", YELLOW), ("加速器", ORANGE)] x = 1.05 for index, (name, accent) in enumerate(flow): add_rect(slide, x, 3.72, 1.03, 0.58, PALE_GRAY, accent) add_text(slide, name, x, 3.88, 1.03, 0.22, 11, accent, True, align=PP_ALIGN.CENTER) if index < 3: add_arrow(slide, x + 1.08, 3.9, 0.35, 0.18, accent) x += 1.43 add_text(slide, "CPU只配置任务,DMA、SRAM与加速器完成批量搬运和计算。", 1.02, 4.65, 5.25, 0.5, 12, INK, align=PP_ALIGN.CENTER) add_text(slide, "复位顺序:sys → mlkem → cpu;释放前AXI valid保持为0。", 1.02, 5.42, 5.25, 0.38, 11, MUTED, align=PP_ALIGN.CENTER) add_rect(slide, 6.82, 2.85, 5.65, 3.45, WHITE, ORANGE, radius=True) add_text(slide, "SRAM连续突发优化", 7.05, 3.08, 5.2, 0.3, 17, ORANGE, True, TITLE_FONT, PP_ALIGN.CENTER) add_text(slide, "优化前", 7.12, 3.63, 0.75, 0.24, 10.5, MUTED, True) for i in range(4): accent = BLUE if i % 2 == 0 else "E3E7EF" add_rect(slide, 8.08 + i * 0.72, 3.58, 0.42, 0.36, accent) add_text(slide, "优化后", 7.12, 4.18, 0.75, 0.24, 10.5, ORANGE, True) for i in range(4): add_rect(slide, 8.08 + i * 0.48, 4.13, 0.42, 0.36, TEAL) add_metric(slide, "200", "理论单向带宽", 7.15, 4.82, 2.0, ORANGE, " MB/s") add_metric(slide, "1", "无反压连续读写", 9.52, 4.82, 2.0, TEAL, " beat/cycle") add_text(slide, "读数据寄存保持R通道稳定;Bank选择随事务锁存。", 7.12, 5.9, 5.0, 0.32, 11, INK, align=PP_ALIGN.CENTER) # 10. Matmul slide = new_content_slide("Matmul:一次启动完成5000组流水任务", 10, "自主计算IP") add_image_contain(slide, SYSTEM / "matmul_dataflow_gantt.png", 0.65, 1.5, 9.55, 5.3) add_rect(slide, 10.45, 1.5, 2.02, 5.3, WHITE, BLUE, radius=True) add_text(slide, "关键节拍", 10.65, 1.78, 1.62, 0.3, 17, BLUE, True, TITLE_FONT, PP_ALIGN.CENTER) for y, value, label, accent in [ (2.26, "128 B", "每组A/B读取", BLUE), (3.18, "≈22 cyc", "单Bank乘加延迟", GREEN), (4.10, "2×6 B", "每元素CRC更新", ORANGE), (5.02, "0 B", "快速模式结果写回", TEAL), ]: add_text(slide, value, 10.66, y, 1.62, 0.3, 18, accent, True, TITLE_FONT, PP_ALIGN.CENTER) add_text(slide, label, 10.66, y + 0.34, 1.62, 0.26, 10, MUTED, align=PP_ALIGN.CENTER) add_text(slide, "最后一组退休后仍等待CRC FIFO排空,再产生DONE。", 10.7, 6.03, 1.55, 0.5, 10.5, INK, align=PP_ALIGN.CENTER) # 11. ML-KEM architecture slide = new_content_slide("ML-KEM:单比特流覆盖三等级与三操作", 11, "自主计算IP") add_image_contain(slide, MLKEM / "mlkem_modules.png", 0.65, 1.48, 8.05, 4.8) add_rect(slide, 9.0, 1.5, 3.45, 3.95, WHITE, TEAL, radius=True) add_text(slide, "运行时配置", 9.25, 1.8, 2.95, 0.3, 18, TEAL, True, TITLE_FONT, PP_ALIGN.CENTER) add_metric(slide, "3", "512 / 768 / 1024", 9.27, 2.28, 1.3, TEAL) add_metric(slide, "3", "KeyGen / Encaps / Decaps", 10.78, 2.28, 1.35, ORANGE) add_bullets( slide, [ "参数在start时锁存", "存储按KMAX=4静态分配", "控制、算子、BRAM三级组织", "公钥/私钥/密文支持AXI突发窗口", "无需按安全等级重新综合", ], 9.28, 3.38, 2.95, 1.72, size=11.8, gap=4, ) add_rect(slide, 0.82, 6.36, 11.47, 0.46, PALE_TEAL, TEAL) add_text(slide, "KeyGen 生成 ek/dk", 1.45, 6.49, 2.45, 0.2, 12, BLUE, True, align=PP_ALIGN.CENTER) add_arrow(slide, 4.02, 6.5, 0.48, 0.17, BLUE) add_text(slide, "Encaps 生成 c/ss", 4.62, 6.49, 2.45, 0.2, 12, TEAL, True, align=PP_ALIGN.CENTER) add_arrow(slide, 7.18, 6.5, 0.48, 0.17, TEAL) add_text(slide, "Decaps 恢复或拒绝 ss", 7.78, 6.49, 3.05, 0.2, 12, ORANGE, True, align=PP_ALIGN.CENTER) # 12. ML-KEM key designs slide = new_content_slide("ML-KEM关键设计:复用、并行与常量时间", 12, "自主计算IP") panels = [ (0.72, BLUE, PALE_BLUE, "共享Keccak"), (4.52, TEAL, PALE_TEAL, "双蝶形NTT"), (8.32, ORANGE, PALE_ORANGE, "Decaps复用"), ] for x, accent, pale, title in panels: add_rect(slide, x, 1.55, 3.5, 4.95, pale, accent, radius=True) add_text(slide, title, x + 0.2, 1.84, 3.1, 0.34, 18, accent, True, TITLE_FONT, PP_ALIGN.CENTER) # Shared Keccak panel for i, name in enumerate(["G/H/J哈希", "SampleNTT", "CBD采样"]): add_rect(slide, 0.98, 2.55 + i * 0.62, 1.15, 0.38, WHITE, LINE) add_text(slide, name, 0.98, 2.66 + i * 0.62, 1.15, 0.16, 9.5, INK, align=PP_ALIGN.CENTER) add_arrow(slide, 2.22, 2.67 + i * 0.62, 0.36, 0.16, BLUE) add_rect(slide, 2.65, 2.83, 1.25, 1.0, WHITE, BLUE) add_text(slide, "单一\nKeccak-f[1600]", 2.72, 3.08, 1.12, 0.45, 11, BLUE, True, align=PP_ALIGN.CENTER) add_text(slide, "避免两份额外实例\n约节省10.6k LUT", 1.05, 4.85, 2.82, 0.6, 13, BLUE, True, align=PP_ALIGN.CENTER) # NTT panel add_rect(slide, 4.92, 2.6, 1.1, 1.3, WHITE, TEAL) for i in range(4): add_rect(slide, 5.08, 2.75 + i * 0.24, 0.78, 0.14, TEAL if i % 2 == 0 else ACCENT_BLUE) add_arrow(slide, 6.15, 3.08, 0.4, 0.2, TEAL) add_rect(slide, 6.65, 2.6, 1.08, 1.3, WHITE, TEAL) add_text(slide, "BU0\nBU1", 6.72, 2.99, 0.94, 0.42, 14, TEAL, True, align=PP_ALIGN.CENTER) add_text(slide, "4路BRAM无冲突供数\n读-算-写三级调度", 4.95, 4.28, 2.75, 0.58, 12, INK, align=PP_ALIGN.CENTER) add_text(slide, "4 coeff/cycle · 0 DSP", 4.98, 5.14, 2.72, 0.32, 15, TEAL, True, align=PP_ALIGN.CENTER) # Decaps panel for i, (name, accent) in enumerate([("D1–D5 解密恢复", BLUE), ("D6 复用Encaps", TEAL), ("D7 常量时间比较", ORANGE)]): add_rect(slide, 8.7, 2.5 + i * 0.82, 2.75, 0.5, WHITE, accent) add_text(slide, name, 8.82, 2.64 + i * 0.82, 2.51, 0.2, 11, accent, True, align=PP_ALIGN.CENTER) if i < 2: down = slide.shapes.add_shape(MSO_SHAPE.DOWN_ARROW, Inches(9.88), Inches(3.04 + i * 0.82), Inches(0.38), Inches(0.2)) down.fill.solid(); down.fill.fore_color.rgb = color(accent); down.line.fill.background() add_text(slide, "Accept与Reject路径\n总周期数完全相等", 8.86, 5.25, 2.44, 0.52, 13, ORANGE, True, align=PP_ALIGN.CENTER) add_text(slide, "设计取舍:以资源共享和运行时可配置性换取更低的面积与部署复杂度。", 1.0, 6.68, 11.3, 0.28, 13, INK, True, align=PP_ALIGN.CENTER) # 13. Verification slide = new_content_slide("四层验证:从算子正确到板级闭环", 13, "验证与实现结果") bands = [ (1.55, 1.35, 8.25, "板级系统", "真实FPGA、CPU软件、DMA与串口日志", ORANGE), (2.55, 1.95, 7.05, "SoC集成", "AXI解码、CDC、复位释放、SRAM取指", TEAL), (3.55, 2.55, 5.85, "IP集成", "Matmul / FFT / DMA / ML-KEM端到端", BLUE), (4.55, 3.15, 4.65, "叶模块", "Keccak、NTT、蝶形、FIFO、CRC32", GREEN), ] for y, x, width, title, desc, accent in bands: add_rect(slide, x, y, width, 0.7, WHITE, accent, radius=True) add_text(slide, title, x + 0.18, y + 0.17, 1.2, 0.24, 13, accent, True) add_text(slide, desc, x + 1.45, y + 0.16, width - 1.65, 0.26, 11.5, INK, align=PP_ALIGN.CENTER) add_rect(slide, 9.88, 1.55, 2.5, 4.75, PALE_ORANGE, ORANGE, radius=True) add_text(slide, "验证结果", 10.15, 1.82, 1.96, 0.3, 17, ORANGE, True, TITLE_FONT, PP_ALIGN.CENTER) for i, (value, label, accent) in enumerate([ ("9000", "主路径KAT", BLUE), ("300", "隐式拒绝向量", ORANGE), ("900", "ML-KEM板级运行", TEAL), ("100%", "Matmul最终得分", GREEN), ("6", "FFT板级测试", YELLOW), ]): y = 2.28 + i * 0.76 add_text(slide, value, 10.05, y, 0.95, 0.28, 16, accent, True, TITLE_FONT, PP_ALIGN.RIGHT) add_text(slide, label, 11.12, y + 0.03, 1.05, 0.26, 10, INK) add_rect(slide, 1.0, 5.75, 8.55, 0.58, PALE_TEAL, TEAL) add_text(slide, "算法逐字节比对;板级以零失败、CRC匹配和误差阈值为判据。", 1.18, 5.92, 8.2, 0.24, 12.5, INK, True, align=PP_ALIGN.CENTER) add_text(slide, "全部通过", 9.98, 6.48, 2.25, 0.36, 20, GREEN, True, TITLE_FONT, PP_ALIGN.CENTER) # 14. Resources and timing slide = new_content_slide("实现结果:资源可控,三个时钟域全部收敛", 14, "验证与实现结果") add_rect(slide, 0.78, 1.5, 5.72, 4.85, WHITE, BLUE, radius=True) add_text(slide, "FPGA资源利用", 1.05, 1.78, 5.18, 0.3, 17, BLUE, True, TITLE_FONT, PP_ALIGN.CENTER) resource_data = [ ("Slice LUT", 41.16, "55,066", BLUE), ("Registers", 11.58, "31,000", TEAL), ("BRAM Tile", 8.08, "29.5", YELLOW), ("DSP48E1", 0.0, "0", ORANGE), ] for i, (label, percent, absolute, accent) in enumerate(resource_data): y = 2.45 + i * 0.75 add_text(slide, label, 1.02, y, 1.1, 0.24, 11, INK) add_rect(slide, 2.2, y + 0.02, 2.55, 0.22, "E5E9F1") if percent: add_rect(slide, 2.2, y + 0.02, 2.55 * percent / 50, 0.22, accent) add_text(slide, f"{percent:.2f}%", 4.85, y - 0.02, 0.68, 0.26, 10.5, accent, True, align=PP_ALIGN.RIGHT) add_text(slide, absolute, 5.6, y - 0.02, 0.55, 0.26, 10, MUTED, align=PP_ALIGN.RIGHT) add_text(slide, "xc7a200tfbg676-1 · 布局布线100%完成", 1.1, 5.72, 5.08, 0.28, 11.5, MUTED, align=PP_ALIGN.CENTER) add_rect(slide, 6.82, 1.5, 5.62, 4.85, WHITE, TEAL, radius=True) add_text(slide, "布局布线后时序", 7.08, 1.78, 5.1, 0.3, 17, TEAL, True, TITLE_FONT, PP_ALIGN.CENTER) add_table( slide, [ ["时钟域", "频率", "Setup WNS", "端点"], ["cpu_clk", "33.333 MHz", "+0.737 ns", "17,197"], ["sys_clk", "50.000 MHz", "+0.125 ns", "22,307"], ["mlkem_clk", "76.923 MHz", "+0.070 ns", "31,646"], ], 7.08, 2.35, 5.1, 2.35, widths=[1.15, 1.35, 1.35, 1.25], size=10.5, ) add_metric(slide, "88,843", "时序端点", 7.15, 5.0, 1.45, TEAL) add_metric(slide, "0", "建立/保持违例", 8.95, 5.0, 1.45, GREEN) add_metric(slide, "+0.070", "全系统最小WNS / ns", 10.72, 5.0, 1.45, ORANGE) add_rect(slide, 1.0, 6.58, 11.2, 0.35, PALE_BLUE, BLUE) add_text(slide, "ML-KEM独立高速时钟域隔离了系统总线布线压力。", 1.2, 6.66, 10.8, 0.18, 11.5, BLUE, True, align=PP_ALIGN.CENTER) # 15. Performance slide = new_content_slide("最终性能:Matmul满分与端到端加速", 15, "验证与实现结果") for i, (value, label, accent, suffix) in enumerate([ ("4.911765", "Matmul 5000组最终时间", BLUE, " ms"), ("827.80", "FFT DMA相对软件加速", ORANGE, "×"), ("48.95–53.33", "ML-KEM DMA相对软件加速", TEAL, "×"), ]): x = 0.85 + i * 4.05 add_rect(slide, x, 1.5, 3.55, 1.05, WHITE, accent, radius=True) add_metric(slide, value, label, x + 0.15, 1.66, 3.25, accent, suffix) add_rect(slide, 0.78, 2.92, 7.2, 3.55, WHITE, ORANGE, radius=True) add_text(slide, "ML-KEM三路径完整往返加速比", 1.02, 3.17, 6.72, 0.3, 16, ORANGE, True, TITLE_FONT, PP_ALIGN.CENTER) levels = ["512", "768", "1024"] cpu = [13.04, 13.84, 15.05] dma = [48.95, 50.41, 53.33] for i, level in enumerate(levels): y = 3.74 + i * 0.72 add_text(slide, f"ML-KEM-{level}", 1.0, y + 0.16, 1.15, 0.22, 10.5, INK) add_rect(slide, 2.25, y, cpu[i] / 60 * 4.65, 0.23, BLUE) add_rect(slide, 2.25, y + 0.31, dma[i] / 60 * 4.65, 0.23, ORANGE) add_text(slide, f"{cpu[i]:.2f}×", 2.33 + cpu[i] / 60 * 4.65, y - 0.02, 0.62, 0.2, 8.5, BLUE, True) add_text(slide, f"{dma[i]:.2f}×", 2.33 + dma[i] / 60 * 4.65, y + 0.29, 0.62, 0.2, 8.5, ORANGE, True) add_rect(slide, 2.15, 6.04, 0.15, 0.15, BLUE) add_text(slide, "CPU寄存器路径", 2.4, 6.02, 1.25, 0.2, 9, MUTED) add_rect(slide, 3.9, 6.04, 0.15, 0.15, ORANGE) add_text(slide, "DMA窗口路径", 4.15, 6.02, 1.25, 0.2, 9, MUTED) add_rect(slide, 8.28, 2.92, 4.15, 3.55, PALE_TEAL, TEAL, radius=True) add_text(slide, "结果解释", 8.55, 3.18, 3.6, 0.3, 16, TEAL, True, TITLE_FONT, PP_ALIGN.CENTER) add_bullets( slide, [ "Matmul CRC32 0x0417d761匹配", "FFT DMA延迟296.73 μs,误差Re=1、Im=2", "ML-KEM-512 DMA往返约1.605 ms", "SRAM连续突发理论带宽200 MB/s", ], 8.55, 3.78, 3.55, 1.85, size=12, gap=6, ) add_text(slide, "随机种子0xa3772ec8 · 最终得分100.00%", 8.55, 5.92, 3.55, 0.34, 10.5, ORANGE, True, align=PP_ALIGN.CENTER) # 16. Lessons learned slide = new_content_slide("项目心得:从单点优化走向系统工程", 16, "心得与后续工作") lessons = [ ("端到端优先", "计算核延迟不是唯一指标;启动、搬运、调度和校验共同决定评测时间。", BLUE), ("先正确再加速", "先建立CRC、状态寄存器和分层测试等可观测手段,再压缩流水空闲。", ORANGE), ("取舍必须显式", "Keccak复用、双蝶形NTT和零DSP策略是在面积、吞吐与部署复杂度间的选择。", TEAL), ("证据需要版本化", "报告、软件、RTL和实测日志必须共享同一数据基线,才能保证结论可复现。", GREEN), ] for i, (title, desc, accent) in enumerate(lessons): col, row = i % 2, i // 2 x, y = 0.82 + col * 6.0, 1.55 + row * 2.45 add_rect(slide, x, y, 5.55, 1.85, WHITE, accent, radius=True) add_number_badge(slide, f"0{i+1}", x + 0.25, y + 0.28, accent) add_text(slide, title, x + 0.95, y + 0.27, 4.2, 0.32, 16, accent, True) add_text(slide, desc, x + 0.35, y + 0.9, 4.85, 0.58, 12, INK, align=PP_ALIGN.CENTER) add_rect(slide, 1.0, 6.38, 11.15, 0.42, PALE_ORANGE, ORANGE) add_text(slide, "最终满分来自软硬件协同,而不是单一模块参数的堆叠。", 1.2, 6.5, 10.75, 0.2, 13, INK, True, align=PP_ALIGN.CENTER) # 17. Future work slide = new_content_slide("后续工作:沿性能、验证与工程化继续演进", 17, "心得与后续工作") future = [ (BLUE, "ML-KEM并行度", "增加Keccak并行路径,扩展NTT蝶形单元并评估面积收益比。"), (ORANGE, "存储专项验证", "补充SRAM随机反压、跨Bank边界与持续带宽微基准。"), (TEAL, "实现与功耗证据", "完善I/O约束,使用SAIF/VCD活动率回标提高功耗估计可信度。"), (GREEN, "版本与回归体系", "统一RTL、软件、测试向量和报告指标,形成自动化回归基线。"), ] for i, (accent, title, desc) in enumerate(future): y = 1.48 + i * 1.22 add_number_badge(slide, f"0{i+1}", 0.92, y, accent) add_text(slide, title, 1.65, y + 0.07, 2.15, 0.3, 15, accent, True) add_text(slide, desc, 3.92, y + 0.06, 7.85, 0.4, 13, INK) add_line(slide, 0.95, y + 0.76, 11.15, 0.012, LINE) add_rect(slide, 1.0, 6.45, 11.15, 0.35, PALE_TEAL, TEAL) add_text(slide, "目标:在不牺牲标准完整性和可验证性的前提下继续提高系统吞吐。", 1.2, 6.55, 10.75, 0.18, 12.5, INK, True, align=PP_ALIGN.CENTER) # 18. Summary slide = new_content_slide("总结:完成从算法IP到板级系统的闭环", 18, "总结与展望") summary = [ ("01", "系统", "构建CPU、DMA、存储与三个计算加速器协同的异构SoC", BLUE), ("02", "数据流", "以SRAM连续突发、DMA端点和双Bank调度降低搬运开销", ORANGE), ("03", "密码IP", "单比特流覆盖ML-KEM三等级、三操作和常量时间拒绝", TEAL), ("04", "工程验证", "零DSP、三个时钟域收敛,并通过仿真与板级分层验证", GREEN), ] for i, (num, tag, desc, accent) in enumerate(summary): y = 1.52 + i * 1.08 add_number_badge(slide, num, 0.95, y, accent) add_text(slide, tag, 1.65, y + 0.08, 1.0, 0.28, 15, accent, True) add_text(slide, desc, 2.75, y + 0.08, 8.95, 0.36, 14, INK) add_line(slide, 0.95, y + 0.72, 11.2, 0.012, LINE) add_rect(slide, 0.92, 6.02, 11.3, 0.62, PALE_ORANGE, ORANGE) add_rich( slide, [ ("最终结果:", 12.5, True, INK, FONT), ("5000", 12.5, True, INK, LATIN_FONT), ("组 Matmul 用时4.911765 ms · CRC32匹配 · 得分100.00%", 12.5, True, INK, FONT), ], 1.12, 6.2, 10.9, 0.3, PP_ALIGN.CENTER, ) # 19. End slide = prs.slides.add_slide(blank) add_template_image(slide, "image3.png", 0, 0, W, H) add_template_image(slide, "image4.png", 5.92, 0.7, 1.48, 1.48) add_text(slide, "感谢聆听", 3.45, 2.55, 6.45, 0.75, 34, BLUE, True, TITLE_FONT, PP_ALIGN.CENTER) add_rect(slide, 4.63, 3.65, 4.08, 0.72, BLUE, radius=True) add_text(slide, "Q&A", 4.75, 3.83, 3.84, 0.3, 22, WHITE, True, LATIN_FONT, PP_ALIGN.CENTER) add_text(slide, "中国集成电路未来之星从这里起步!", 3.2, 5.2, 6.95, 0.42, 17, INK, True, align=PP_ALIGN.CENTER) # 20. Backup: address space slide = new_content_slide("备用:地址空间与软件控制流程", 20, "答疑备用") add_table( slide, [ ["AXI从端", "基地址", "功能"], ["S0", "0x1c00_0000", "BaseRAM / ExtRAM,共8 MB"], ["S5", "0x1f30_0000", "DMA与矩阵批量序列器"], ["S6", "0x1f40_0000", "FFT / ML-KEM共享窗口"], ["S7", "0x1f50_0000", "Matmul控制与数据Bank"], ], 0.8, 1.55, 6.2, 3.75, widths=[1.0, 2.0, 3.2], size=12, ) add_rect(slide, 7.4, 1.55, 4.8, 3.75, WHITE, TEAL, radius=True) add_text(slide, "软件控制流程", 7.7, 1.83, 4.2, 0.3, 17, TEAL, True, TITLE_FONT, PP_ALIGN.CENTER) for i, text in enumerate(["写配置寄存器", "配置DMA窗口", "启动硬件", "轮询done", "读取结果 / CRC"]): y = 2.35 + i * 0.56 add_rect(slide, 8.05, y, 3.5, 0.38, PALE_GRAY, TEAL if i < 3 else BLUE) add_text(slide, text, 8.15, y + 0.1, 3.3, 0.17, 10.5, INK, True, align=PP_ALIGN.CENTER) add_rect(slide, 0.92, 5.75, 11.25, 0.62, PALE_TEAL, TEAL) add_text(slide, "快速路径中CPU不参与逐组Matmul调度;ML-KEM DMA路径只进行少量批量窗口配置。", 1.15, 5.93, 10.8, 0.26, 12.5, INK, True, align=PP_ALIGN.CENTER) # 21. Backup: KeyGen timing slide = new_content_slide("备用:ML-KEM-512 KeyGen逐状态周期分解", 21, "答疑备用") states = [ ("G哈希", 27, YELLOW), ("SampleNTT矩阵A", 3213, ORANGE), ("CBD采样", 1229, TEAL), ("NTT", 4172, BLUE), ("多项式基乘", 2096, GREEN), ("密钥编码", 2081, RED), ] for i, (name, cycles, accent) in enumerate(states): y = 1.52 + i * 0.72 add_text(slide, name, 0.9, y + 0.1, 1.55, 0.22, 11, INK) add_rect(slide, 2.5, y + 0.1, 6.45, 0.25, "E5E9F1") add_rect(slide, 2.5, y + 0.1, 6.45 * cycles / 4500, 0.25, accent) add_text(slide, f"{cycles:,} cyc", 9.1, y + 0.06, 1.0, 0.25, 10, accent, True, align=PP_ALIGN.RIGHT) add_rect(slide, 10.45, 1.55, 2.0, 4.3, PALE_ORANGE, ORANGE, radius=True) add_text(slide, "主要瓶颈", 10.7, 1.88, 1.5, 0.3, 16, ORANGE, True, TITLE_FONT, PP_ALIGN.CENTER) add_text(slide, "NTT\n30.2%", 10.73, 2.5, 1.45, 0.65, 19, BLUE, True, align=PP_ALIGN.CENTER) add_text(slide, "SampleNTT\n23.3%", 10.68, 3.55, 1.55, 0.65, 16, ORANGE, True, align=PP_ALIGN.CENTER) add_text(slide, "两者合计\n53.5%", 10.72, 4.65, 1.48, 0.65, 15, INK, True, align=PP_ALIGN.CENTER) add_rect(slide, 0.92, 6.25, 11.25, 0.58, PALE_ORANGE, ORANGE) add_text(slide, "优化优先级:增加采样路径Keccak并行度,其次扩展NTT蝶形单元数量。", 1.12, 6.42, 10.85, 0.24, 12.5, INK, True, align=PP_ALIGN.CENTER) # 22. Backup: tradeoffs slide = new_content_slide("备用:设计取舍与优化路线", 22, "答疑备用") add_table( slide, [ ["维度", "当前选择", "优势", "代价"], ["安全等级", "运行时k=2/3/4", "单比特流部署", "地址与控制逻辑增加"], ["Keccak", "单核时分复用", "节省约10.6k LUT", "状态间无法重叠"], ["NTT", "双蝶形、4 coeff/cycle", "BRAM无冲突、0 DSP", "吞吐低于多BU实现"], ["控制", "30状态硬连线FSM", "行为清晰、易验证", "串行调度限制性能"], ], 0.72, 1.48, 11.85, 3.62, widths=[1.4, 3.05, 3.35, 4.05], size=11.3, ) add_text(slide, "优化路线", 0.92, 5.42, 1.45, 0.3, 17, ORANGE, True, TITLE_FONT) roadmap = [ ("1", "双Keccak或专用采样核", "预计KeyGen下降15%–20%", ORANGE), ("2", "扩展多BU NTT", "提高至8 coeff/cycle", TEAL), ("3", "完善约束与活动率回标", "提高时序与功耗结论置信度", BLUE), ] for i, (num, title, desc, accent) in enumerate(roadmap): x = 0.95 + i * 4.05 add_number_badge(slide, num, x, 5.88, accent) add_text(slide, title, x + 0.62, 5.86, 2.95, 0.28, 12, accent, True) add_text(slide, desc, x + 0.62, 6.23, 2.95, 0.4, 10.5, INK) prs.save(OUTPUT) print(f"Wrote {OUTPUT} from {TEMPLATE}")