CUSTOM_TEMPLATE_DESIGN.md 18 KB

用户自定义 PPT 模板支持 — 修改方案

1. 需求概述

现状

  • Skill 内置 3 套固定模板:report-master.pptx(日报)、weekly-master.pptx(周报)、monthly-master.pptx(月报)
  • 模板存放于 assets/ 目录,按 report_type 硬编码映射
  • 构建流程:加载模板 → 复制母版幻灯片 → 替换占位符 → 插入图表/文本 → 删除原始模板页

目标

  • 用户可上传自定义 .pptx 模板,skill 按该模板的样式(配色、字体、布局、背景)生成报告
  • 工作流程完全不变:数据探查 → 六项确认 → 生成 PPT → 质量自检
  • 改动范围最小化:仅在"模板加载与样式适配"环节做修改,不触及数据分析与洞察生成逻辑

2. 核心设计思路

┌─────────────────┐     ┌──────────────────┐     ┌─────────────────┐
│  用户上传模板    │ ──→ │  TemplateParser  │ ──→ │ TemplateProfile │
│  (任意.pptx)    │     │  解析结构+样式   │     │ 结构化模板描述   │
└─────────────────┘     └──────────────────┘     └─────────────────┘
                                                          │
                              ┌───────────────────────────┘
                              ▼
                    ┌─────────────────────┐
                    │   ppt_builder.py    │
                    │  按 TemplateProfile │
                    │  动态选择母版页复制  │
                    │  动态应用配色/字体   │
                    └─────────────────────┘

关键原则

  1. 解析而非约束:自动识别模板中的母版页类型、占位符、配色、字体,不强求用户按固定规范制作模板
  2. 映射兜底:若用户模板缺少某类母版页(如目录页),自动回退到内置模板或跳过该页
  3. 样式继承优先:图表、卡片、文本的颜色/字体优先使用模板提取的样式,用户显式配置可覆盖

3. 模块级修改清单

3.1 新增模块:scripts/template_parser.py(模板解析引擎)

职责:读取任意 .pptx 模板,输出结构化的 TemplateProfile

核心数据结构

@dataclass
class MasterSlideInfo:
    slide_index: int           # 在 prs.slides 中的索引
    master_type: str           # 'cover' | 'content' | 'toc' | 'end' | 'unknown'
    placeholders: list[str]    # 检测到的占位符列表,如 ['{report_title}', '{page_title}']
    content_top: int           # 内容区域起始 Y(EMU),通过 {page_title} 底部 + gap 推算
    has_footer: bool           # 是否自带页脚
    has_background: bool       # 是否有背景图/形状

@dataclass
class TemplateProfile:
    path: str
    is_builtin: bool
    slide_width: int           # 幻灯片宽度(EMU)
    slide_height: int          # 幻灯片高度(EMU)
    master_slides: list[MasterSlideInfo]
    placeholder_map: dict      # 全局占位符 → 母版页索引 映射
    detected_theme: dict       # 提取的颜色 {primary, accent, text, ...}
    detected_fonts: dict       # 提取的字体 {title_font, body_font, number_font}
    safe_margins: dict         # {left, right, top, bottom} in EMU
    
    # 快捷访问方法
    def get_master_for(self, page_type: str) -> MasterSlideInfo: ...
    def get_content_top(self, page_type: str = 'content') -> int: ...

解析逻辑

检测项 方法 说明
母版页类型 _detect_master_type(slide) 通过文本特征匹配:{report_title}→cover,{page_title}→content,{chapter→toc,末页+感谢文字→end
占位符 _scan_placeholders(slide) 正则匹配 {\w+} 形式的文本
内容区域 _detect_content_top(slide) 已有 _detect_content_top,迁移并增强:识别标题 shape 底部 + 动态 gap
主题色 _extract_colors(slide) 从母版形状的 fill/line 颜色、主题色表(slide.slide_layout.slide_master.theme.color_scheme)提取主色、强调色
字体 _extract_fonts(slide) 统计各母版页中不同字体的使用频次,取标题区(top < 1.5M EMU)最多字体为 title_font,内容区最多为 body_font
尺寸 prs.slide_width, prs.slide_height 读取实际尺寸,支持宽屏(16:9)和标准(4:3)

内置模板适配

  • assets/ 中的 3 个内置模板也走同一套解析流程,输出 TemplateProfile
  • 这样内置/自定义模板在下游的接口完全一致

3.2 修改模块:scripts/report_config.py

新增字段

@dataclass
class ReportConfig:
    # ... 原有字段 ...
    template_path: str = ''           # 已有,保持不变
    template_profile: Optional['TemplateProfile'] = None  # NEW: 解析后的模板描述
    use_template_theme: bool = True   # NEW: 是否用模板提取的配色覆盖默认主题
    
    # 确认项扩展(第5项确认)
    # 原:"页面结构与模板方案"
    # 现增加展示:模板解析结果(可用母版页、检测到的配色、字体)

ConfirmationSpec 扩展(用户确认时展示模板信息):

@dataclass
class TemplateConfirmationDetail:
    template_name: str
    detected_master_types: list[str]  # ['cover', 'content', 'toc', 'end']
    detected_colors: dict
    detected_fonts: dict
    warnings: list[str]               # 如 "未检测到目录页母版,将自动生成"

3.3 修改模块:scripts/theme_manager.py

新增函数

def extract_theme_from_template(template_profile: 'TemplateProfile') -> ThemeConfig:
    """
    从 TemplateProfile 提取的颜色构建 ThemeConfig。
    映射规则:
      - primary → 母版中面积最大的深色填充 / 主题色表第一个颜色
      - accent → 母版中出现频次最高的亮色(绿/蓝/橙)
      - text → 母版正文文本颜色 / 默认 #333333
      - card_bg → primary 的 10% 透明度浅色变体
      - chart_series → 从母版色表提取前8色
    """

def merge_theme(template_theme: ThemeConfig, user_theme: ThemeConfig) -> ThemeConfig:
    """
    合并模板主题与用户显式配置,用户配置优先。
    """

修改 get_theme()

  • 新增预设 ThemePreset.FROM_TEMPLATE = 'from_template'
  • config.theme.preset == FROM_TEMPLATEconfig.use_template_theme == True 时,调用 extract_theme_from_template()

3.4 修改模块:scripts/page_layouts.py

核心问题:当前布局常量(SLIDE_WIDTH = 16256000 等)是硬编码的 16:9 尺寸,若用户模板是 4:3 或其他尺寸会错位。

修改方案

  1. 保留全局常量作为默认值
  2. 所有计算函数增加 slide_width / slide_height 可选参数
  3. 新增 LayoutContext 类,封装当前模板的尺寸信息
@dataclass
class LayoutContext:
    slide_width: int = SLIDE_WIDTH
    slide_height: int = SLIDE_HEIGHT
    content_top: int = int(CONTENT_TOP_BASE)
    footer_top: int = FOOTER_TOP
    margin_left: int = int(MARGIN_LEFT)
    margin_right: int = int(MARGIN_RIGHT)
    
    @classmethod
    def from_template_profile(cls, profile: 'TemplateProfile') -> 'LayoutContext': ...

修改所有布局函数签名

# 修改前
def get_kpi_grid(content_top_emu: int = None, ...) -> list[LayoutZone]:

# 修改后
def get_kpi_grid(content_top_emu: int = None, 
                 ctx: LayoutContext = None, ...) -> list[LayoutZone]:
    ctx = ctx or LayoutContext()
    ...

调用侧修改ppt_builder.py 在构建每页前创建 LayoutContext,传入各布局函数。


3.5 修改模块:scripts/ppt_builder.py(核心构建器)

3.5.1 模板解析入口

build_report()_build_without_save() 开头增加:

def _resolve_template_profile(config: ReportConfig) -> TemplateProfile:
    if config.template_profile:
        return config.template_profile
    if config.template_path:
        return parse_template(config.template_path)  # template_parser.parse_template
    # 内置模板
    return parse_template(_resolve_master_template(config))

3.5.2 母版页选择逻辑(关键修改)

现状:固定索引复制

slide = _duplicate_slide(prs, prs.slides[0])  # 封面
slide = _duplicate_slide(prs, prs.slides[1])  # 内容
slide = _duplicate_slide(prs, prs.slides[3])  # 尾页

新设计:通过 TemplateProfile 动态选择

def _duplicate_master_slide(prs, profile: TemplateProfile, page_type: str):
    master_info = profile.get_master_for(page_type)
    if master_info:
        return _duplicate_slide(prs, prs.slides[master_info.slide_index])
    # 兜底:按原硬编码索引
    fallback_index = {'cover': 0, 'content': 1, 'toc': 1, 'end': 3}.get(page_type, 1)
    return _duplicate_slide(prs, prs.slides[fallback_index])

3.5.3 配色与字体应用

现状:硬编码颜色常量 + 主题转换

C_PRIMARY = RGBColor(0x1E, 0x3A, 0x5F)  # 硬编码
colors = theme_to_rgb_colors(config.theme)  # 用户主题

新设计:三层优先级

  1. 用户显式配置的 config.theme(最高优先级)
  2. 模板提取的 detected_theme(当 use_template_theme=True
  3. 默认颜色常量(兜底)
def _resolve_colors(config: ReportConfig, profile: TemplateProfile) -> dict:
    if config.theme and not config.use_template_theme:
        return theme_to_rgb_colors(config.theme)
    template_theme = extract_theme_from_template(profile)
    return theme_to_rgb_colors(template_theme)

字体同理:模板提取的字体 → 用户配置 → 默认 "微软雅黑"/"Arial"

3.5.4 布局上下文传递

def build_report(data_file: str, config: ReportConfig, output_path: str) -> str:
    profile = _resolve_template_profile(config)
    ctx = LayoutContext.from_template_profile(profile)
    colors = _resolve_colors(config, profile)
    fonts = _resolve_fonts(config, profile)  # 新增
    
    # 后续所有 _build_xxx_page 调用增加 profile, ctx, fonts 参数
    ...

3.5.5 占位符兼容(关键)

用户模板中的占位符命名可能与内置模板不同。建立占位符别名映射

PLACEHOLDER_ALIASES = {
    '{report_title}': ['{report_title}', '{标题}', '{title}'],
    '{page_title}': ['{page_title}', '{页面标题}', '{subtitle}'],
    '{date}': ['{date}', '{日期}', '{report_date}'],
    '{department}': ['{department}', '{部门}', '{source}'],
    '{period}': ['{period}', '{周期}', '{report_period}'],
}

_replace_placeholder() 增强为支持别名匹配:

def _replace_placeholder(slide, placeholder, new_text, aliases=None):
    aliases = aliases or []
    targets = [placeholder] + aliases
    for shape in slide.shapes:
        if not shape.has_text_frame:
            continue
        for para in shape.text_frame.paragraphs:
            for target in targets:
                if target in para.text:
                    para.text = para.text.replace(target, str(new_text))

3.6 修改模块:scripts/quality_inspector.py

适配点

  1. 尺寸检查不再使用硬编码 SLIDE_WIDTH/SLIDE_HEIGHT,改为读取实际 slide.slide_width / slide.slide_height
  2. 字体一致性检查:允许模板提取的字体组合(如标题用 A 字体、正文用 B 字体),不强制单一字体
  3. 内容区域检测:使用 LayoutContext 或从模板 profile 传入的 content_top
# 修改前(硬编码)
sw = int(slide.slide_width) if hasattr(slide, 'slide_width') else SLIDE_WIDTH

# 修改后(实际读取,已有逻辑,确认生效即可)
# 补充:支持从 config 读取 template_profile 中的安全边距

3.7 修改模块:scripts/agent_analyzer.py(用户确认流程)

第 5 项确认(页面结构与模板方案)增强:

当用户提供了自定义模板时,展示解析结果供确认:

【模板解析结果】
- 检测到母版页:封面页(✓)  内容页(✓)  目录页(✗)  尾页(✓)
- 检测到配色:主色 #1E3A5F,强调色 #10B981
- 检测到字体:标题=微软雅黑,正文=微软雅黑
- 内容区域起始:距顶部 2.1cm
- ⚠️ 未检测到目录页母版,目录页将使用内容页母版替代

是否应用模板提取的配色和字体? [是/否]

4. 文件修改明细表

文件 修改类型 修改内容
scripts/template_parser.py 新增 模板解析引擎,输出 TemplateProfile
scripts/report_config.py 修改 新增 template_profile, use_template_theme 字段;新增 TemplateConfirmationDetail
scripts/theme_manager.py 修改 新增 extract_theme_from_template(), merge_theme(), ThemePreset.FROM_TEMPLATE
scripts/page_layouts.py 修改 新增 LayoutContext,所有函数支持动态尺寸
scripts/ppt_builder.py 修改 模板解析入口、动态母版选择、三层配色/字体优先级、占位符别名、LayoutContext 传递
scripts/quality_inspector.py 修改 尺寸/边距读取实际值,字体检查适配模板字体
scripts/agent_analyzer.py 修改 第5项确认展示模板解析结果
SKILL.md 修改 更新文档:自定义模板使用说明、模板制作规范

5. 用户交互流程(新增)

用户: 我要用我自己的模板生成报告
      (上传 my-template.pptx)

Agent: 收到模板,正在解析...
       [调用 template_parser.parse_template()]

Agent: 【模板解析完成】
       - 尺寸: 16:9 宽屏
       - 母版页: 封面✓ 内容✓ 尾页✓
       - 检测到配色: 主色 #2B579A,强调色 #FF6B35
       - 检测到字体: 标题=思源黑体,正文=微软雅黑
       - ⚠️ 未找到 {page_title} 占位符,将自动添加页面标题

Agent: 是否应用模板提取的配色?[是/否]
用户: 是

Agent: (继续原有6项确认流程,第5项已包含模板信息)
       ...

Agent: (调用 build_report(),内部使用 TemplateProfile)
       → 生成按用户模板样式的 PPT

6. 模板制作规范(面向用户)

为获得最佳效果,建议用户在模板中遵循以下规范:

6.1 母版页结构

建议模板包含 4 个母版幻灯片(至少包含封面页和内容页): | 母版页 | 建议包含的占位符 | 用途 | |--------|-----------------|------| | 封面页 | {report_title}, {date}, {department} | 报告封面 | | 目录页 | {chapter1_title}, {chapter1_desc}, ... | 目录/导航页 | | 内容页 | {page_title}, {source}, {period} | 正文页(图表、洞察) | | 尾页 | {report_title} | 结束页 |

6.2 占位符规则

  • 占位符使用 {} 包裹,如 {report_title}
  • 不强制要求所有占位符,缺少的会自动跳过或智能补充
  • 支持自定义命名,agent 会通过语义匹配和别名映射识别

6.3 样式设计建议

  • 主题色:在母版中设置主题颜色(设计 → 变体 → 颜色),agent 会自动提取
  • 字体:在母版中分别设置标题和正文字体,agent 会识别并统一应用
  • 背景:可使用纯色、渐变或图片背景,复制时会完整保留
  • 页眉/页脚:模板中已有的页眉页脚图形会保留,agent 会自动检测避免重复添加

7. 风险与兜底策略

风险场景 兜底策略
用户模板只有1页 该页同时作为封面/内容/尾页的复制源;缺失页类型跳过
无法识别母版页类型 默认第1页=cover,最后1页=end,其余=content
无法提取主题色 回退到 ThemePreset.BUSINESS_CLASSIC
用户模板尺寸非标准 LayoutContext 读取实际尺寸,布局函数自适应计算
占位符命名完全自定义 通过语义相似度匹配(如文本框位置、内容特征)
模板有复杂动画/媒体 python-pptx 复制元素时会保留 XML,通常可保留;视频等不支持元素会自动跳过

8. 向后兼容性

  • 完全不提供模板:走现有逻辑,使用 assets/ 内置模板,行为与现在完全一致
  • 提供 template_path 但无 template_profile:自动调用 template_parser 解析,兼容旧配置
  • 显式设置 themeuse_template_theme=False:完全使用用户指定主题,忽略模板颜色
  • 原有 API build_daily_report() / build_report() / quality_assured_build() 签名不变,内部自动适配

9. 实施优先级建议

按以下顺序实施,每步可独立测试:

  1. P0 - 模板解析器template_parser.py

    • 解析内置 3 个模板,验证输出 TemplateProfile 与现有硬编码逻辑一致
    • 解析用户上传模板,验证结构识别正确
  2. P0 - 动态布局上下文page_layouts.py + ppt_builder.py 参数传递)

    • 所有布局函数支持 LayoutContext
    • 内置模板走新流程,输出应与旧版本逐页一致
  3. P1 - 动态母版选择ppt_builder.py

    • TemplateProfile 选择母版页复制
    • 支持单页模板、多页模板的母版映射
  4. P1 - 主题色/字体提取与应用theme_manager.py + ppt_builder.py

    • 从模板提取配色,应用到图表、卡片、文本
    • 用户配置覆盖机制
  5. P2 - 占位符别名与增强匹配

    • 支持更多占位符命名变体
    • 语义匹配兜底
  6. P2 - 质量检查适配quality_inspector.py

    • 读取实际幻灯片尺寸
    • 模板字体白名单
  7. P2 - 用户确认流程展示agent_analyzer.py + SKILL.md

    • 第5项确认展示模板解析结果
    • 文档更新