report-master.pptx(日报)、weekly-master.pptx(周报)、monthly-master.pptx(月报)assets/ 目录,按 report_type 硬编码映射.pptx 模板,skill 按该模板的样式(配色、字体、布局、背景)生成报告┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ 用户上传模板 │ ──→ │ TemplateParser │ ──→ │ TemplateProfile │
│ (任意.pptx) │ │ 解析结构+样式 │ │ 结构化模板描述 │
└─────────────────┘ └──────────────────┘ └─────────────────┘
│
┌───────────────────────────┘
▼
┌─────────────────────┐
│ ppt_builder.py │
│ 按 TemplateProfile │
│ 动态选择母版页复制 │
│ 动态应用配色/字体 │
└─────────────────────┘
关键原则:
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 个内置模板也走同一套解析流程,输出 TemplateProfilescripts/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] # 如 "未检测到目录页母版,将自动生成"
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_TEMPLATE 或 config.use_template_theme == True 时,调用 extract_theme_from_template()scripts/page_layouts.py核心问题:当前布局常量(SLIDE_WIDTH = 16256000 等)是硬编码的 16:9 尺寸,若用户模板是 4:3 或其他尺寸会错位。
修改方案:
slide_width / slide_height 可选参数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,传入各布局函数。
scripts/ppt_builder.py(核心构建器)在 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))
现状:固定索引复制
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])
现状:硬编码颜色常量 + 主题转换
C_PRIMARY = RGBColor(0x1E, 0x3A, 0x5F) # 硬编码
colors = theme_to_rgb_colors(config.theme) # 用户主题
新设计:三层优先级
config.theme(最高优先级)detected_theme(当 use_template_theme=True)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"
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 参数
...
用户模板中的占位符命名可能与内置模板不同。建立占位符别名映射:
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))
scripts/quality_inspector.py适配点:
SLIDE_WIDTH/SLIDE_HEIGHT,改为读取实际 slide.slide_width / slide.slide_heightLayoutContext 或从模板 profile 传入的 content_top# 修改前(硬编码)
sw = int(slide.slide_width) if hasattr(slide, 'slide_width') else SLIDE_WIDTH
# 修改后(实际读取,已有逻辑,确认生效即可)
# 补充:支持从 config 读取 template_profile 中的安全边距
scripts/agent_analyzer.py(用户确认流程)第 5 项确认(页面结构与模板方案)增强:
当用户提供了自定义模板时,展示解析结果供确认:
【模板解析结果】
- 检测到母版页:封面页(✓) 内容页(✓) 目录页(✗) 尾页(✓)
- 检测到配色:主色 #1E3A5F,强调色 #10B981
- 检测到字体:标题=微软雅黑,正文=微软雅黑
- 内容区域起始:距顶部 2.1cm
- ⚠️ 未检测到目录页母版,目录页将使用内容页母版替代
是否应用模板提取的配色和字体? [是/否]
| 文件 | 修改类型 | 修改内容 |
|---|---|---|
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 |
修改 | 更新文档:自定义模板使用说明、模板制作规范 |
用户: 我要用我自己的模板生成报告
(上传 my-template.pptx)
Agent: 收到模板,正在解析...
[调用 template_parser.parse_template()]
Agent: 【模板解析完成】
- 尺寸: 16:9 宽屏
- 母版页: 封面✓ 内容✓ 尾页✓
- 检测到配色: 主色 #2B579A,强调色 #FF6B35
- 检测到字体: 标题=思源黑体,正文=微软雅黑
- ⚠️ 未找到 {page_title} 占位符,将自动添加页面标题
Agent: 是否应用模板提取的配色?[是/否]
用户: 是
Agent: (继续原有6项确认流程,第5项已包含模板信息)
...
Agent: (调用 build_report(),内部使用 TemplateProfile)
→ 生成按用户模板样式的 PPT
为获得最佳效果,建议用户在模板中遵循以下规范:
建议模板包含 4 个母版幻灯片(至少包含封面页和内容页):
| 母版页 | 建议包含的占位符 | 用途 |
|--------|-----------------|------|
| 封面页 | {report_title}, {date}, {department} | 报告封面 |
| 目录页 | {chapter1_title}, {chapter1_desc}, ... | 目录/导航页 |
| 内容页 | {page_title}, {source}, {period} | 正文页(图表、洞察) |
| 尾页 | {report_title} | 结束页 |
{} 包裹,如 {report_title}| 风险场景 | 兜底策略 |
|---|---|
| 用户模板只有1页 | 该页同时作为封面/内容/尾页的复制源;缺失页类型跳过 |
| 无法识别母版页类型 | 默认第1页=cover,最后1页=end,其余=content |
| 无法提取主题色 | 回退到 ThemePreset.BUSINESS_CLASSIC |
| 用户模板尺寸非标准 | LayoutContext 读取实际尺寸,布局函数自适应计算 |
| 占位符命名完全自定义 | 通过语义相似度匹配(如文本框位置、内容特征) |
| 模板有复杂动画/媒体 | python-pptx 复制元素时会保留 XML,通常可保留;视频等不支持元素会自动跳过 |
assets/ 内置模板,行为与现在完全一致template_path 但无 template_profile:自动调用 template_parser 解析,兼容旧配置theme 且 use_template_theme=False:完全使用用户指定主题,忽略模板颜色build_daily_report() / build_report() / quality_assured_build() 签名不变,内部自动适配按以下顺序实施,每步可独立测试:
P0 - 模板解析器(template_parser.py)
TemplateProfile 与现有硬编码逻辑一致P0 - 动态布局上下文(page_layouts.py + ppt_builder.py 参数传递)
LayoutContextP1 - 动态母版选择(ppt_builder.py)
TemplateProfile 选择母版页复制P1 - 主题色/字体提取与应用(theme_manager.py + ppt_builder.py)
P2 - 占位符别名与增强匹配
P2 - 质量检查适配(quality_inspector.py)
P2 - 用户确认流程展示(agent_analyzer.py + SKILL.md)