Skip to content

Commit 7216b06

Browse files
authored
Merge pull request #210 from TencentCloudADP/ppt-gen
[ppt-gen] Add support for new templates
2 parents 860396b + ea6aa37 commit 7216b06

14 files changed

Lines changed: 471 additions & 678 deletions

examples/ppt_gen/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@
55
*.json
66
*.md
77
data/
8+
templates/*

examples/ppt_gen/README.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,12 @@ Run the PPT generation script.
3535
```python
3636
python main.py \
3737
--file webpage.html \
38-
--template_path templates/0.pptx \
39-
--yaml_path yaml_example.yaml \
40-
--pages 10 \
38+
--template_path templates \
39+
--yaml_path yaml_example2.yaml \
40+
--pages 8 \
4141
--disable_tooluse \
42-
--extra_prompt "Language should be English."
42+
--extra_prompt "Ensure rich content. In English" \
43+
--template_name 11
4344
```
4445

4546
The script will produce a `json` file and a `pptx` file if everything is OK. Pass `--yaml_path` to switch between different template definitions.

examples/ppt_gen/README.zh.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,12 @@ wget https://www.nobelprize.org/prizes/physics/2025/popular-information/ -O webp
3030
```python
3131
python main.py \
3232
--file webpage.html \
33-
--template_path template/0.pptx \
34-
--yaml_path yaml_example.yaml \
35-
--pages 10 \
33+
--template_path templates \
34+
--yaml_path yaml_example2.yaml \
35+
--pages 8 \
3636
--disable_tooluse \
37-
--extra_prompt "确保PPT语言是中文"
37+
--extra_prompt "确保PPT语言是中文" \
38+
--template_name 11
3839
```
3940

4041
脚本会生成同名的 `json``pptx` 文件。通过 `--yaml_path` 可以切换不同的模板配置。

examples/ppt_gen/YAML_CONFIG_GUIDE.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@
44
55
This document explains how to describe PPT templates with YAML so that the `ppt_gen` example can generate consistent slides across different topics and layouts.
66

7+
## Overview
8+
9+
There are two types of YAML configuration files:
10+
11+
1. [`yaml_example.yaml`](./yaml_example.yaml) or [`yaml_example2.yaml`](./yaml_example2.yaml) - YAML configurations shared across multiple templates.
12+
2. Individual YAML configuration files for each specific PPT project, used to define project-specific slide types, override shared YAML slide types, and modify the page order in the template (`type_map`). These YAML files are placed in the same folder as the template's pptx file.
13+
714
## YAML file layout
815

916
A template YAML file (see [`yaml_example.yaml`](./yaml_example.yaml)) is composed of two sections:
@@ -14,6 +21,8 @@ A template YAML file (see [`yaml_example.yaml`](./yaml_example.yaml)) is compose
1421
- `type`: the logical slide type (must exist in `type_map`).
1522
- One or more **field specifications** that describe the expected payload for each placeholder.
1623

24+
It is recommended to place `type_map` in the YAML configuration file for a specific PPT template. `type_map` should only list the pages supported by the template. For pages not listed in `type_map`, the `gen_schema.py` script will remove them from the final schema.
25+
1726
### Field specification keys
1827

1928
Each field under a page block can use the following attributes:

examples/ppt_gen/YAML_CONFIG_GUIDE.zh.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,25 @@
22

33
本文档说明如何使用 YAML 描述 PPT 模板。
44

5+
## YAML的合并
6+
7+
PPT模版的YAML配置主要有两种:
8+
9+
1. [`yaml_example.yaml`](./yaml_example.yaml) 或者 [`yaml_example2.yaml`](./yaml_example2.yaml) 这类多个模版间共享的YAML配置。
10+
2. 每个具体PPT项目独有的YAML配置文件,用于定义该PPT特有的页面类型、覆盖共享yaml的页面类型、修改模版中的页面顺序(`type_map`)。和模版的pptx文件放在同一个文件夹中。
11+
512
## YAML 文件结构
613

7-
YAML配置文件(见 [`yaml_example.yaml`](./yaml_example.yaml))包括两部分
14+
合并后的YAML主要包括两部分
815

916
1. **`type_map`**:按顺序将每种幻灯片的 `type` 映射到参考 PPT 模板中的幻灯片索引(从 0 开始)。渲染器根据该索引复制相应母版。
1017
2. **页面定义块**:每个 `<name>_page` 条目描述某类幻灯片的 schema,必须包含:
1118
- `description`:对页面的文字说明,会用于文档和 JSON Schema。
1219
- `type`:幻灯片类型,必须出现在 `type_map` 中。
1320
- 至少一个 **字段定义**,用于描述每个占位符希望接收的数据。
1421

22+
建议将 `type_map` 放在具体 PPT 模版的 YAML 配置文件中。`type_map`只需要列出模版支持的页面,对于没有在其中列出的页面,`gen_schema.py`脚本会把它在最终的schema中去掉。
23+
1524
### 字段定义可用的键
1625

1726
| 键名 | 说明 |

examples/ppt_gen/fill_template.py

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import json
22
import logging
3+
import re
4+
from pathlib import Path
35
from typing import Any
46

57
import yaml
68
from ppt_template_model import PageConfig
79
from pptx import Presentation
8-
from utils import delete_slide, delete_slide_range, duplicate_slide, move_slide
10+
from utils import delete_slide_range, duplicate_slide, move_slide
911

1012

1113
def fill_template_with_yaml_config(template_path, output_path, json_data, yaml_config: dict[str, Any]):
@@ -20,25 +22,30 @@ def fill_template_with_yaml_config(template_path, output_path, json_data, yaml_c
2022
for slide_data in slides_data:
2123
slide_type = slide_data.get("type")
2224
if not slide_type:
23-
logging.warning("Skipped slide without type definition: %s", slide_data)
24-
continue
25+
raise ValueError("Slide data must contain a 'type' field")
2526

2627
template_index = page_config.type_map.get(slide_type)
2728
if template_index is None or template_index >= len(prs.slides):
28-
logging.warning("No template found for slide type '%s'", slide_type)
29-
continue
29+
raise ValueError("No template found for slide type '%s'", slide_type)
3030

3131
template_slide = prs.slides[template_index]
32-
if slide_type in ("title", "acknowledgement"):
32+
if slide_type in ("title_page", "acknowledgement_page"):
3333
target_slide = template_slide
3434
else:
3535
target_slide = duplicate_slide(prs, template_slide)
3636

3737
page_config.render(target_slide, slide_data)
3838

39-
delete_slide_range(prs, range(2, 12))
40-
delete_slide(prs, 0)
41-
move_slide(prs, 1, len(prs.slides) - 1)
39+
# get title page
40+
title_pages_idx = page_config.type_map.get("title_page")
41+
acknowledgement_pages_idx = page_config.type_map.get("acknowledgement_page")
42+
max_idx = max(list(page_config.type_map.values()))
43+
# move title page to the first
44+
move_slide(prs, title_pages_idx, 0)
45+
# move acknowledgement page to the last
46+
move_slide(prs, acknowledgement_pages_idx, len(prs.slides) - 1)
47+
# remove all
48+
delete_slide_range(prs, range(1, max_idx))
4249
prs.save(output_path)
4350

4451

@@ -47,16 +54,20 @@ def extract_json(content):
4754
Extract the json data from the given content.
4855
"""
4956
# extract content within "```json" and "```"
50-
json_data = content.split("```json")[1].split("```")[0]
51-
return json_data
57+
pattern = r"```json(.*?)```"
58+
match = re.search(pattern, content, re.DOTALL)
59+
if match:
60+
return match.group(1)
61+
return content
5262

5363

5464
if __name__ == "__main__":
5565
import argparse
5666
import datetime
5767

5868
parser = argparse.ArgumentParser()
59-
parser.add_argument("-t", "--template", type=str, default="templates/0.pptx")
69+
parser.add_argument("-t", "--template", type=str, default="templates")
70+
parser.add_argument("-n", "--template_name", type=str, default="0")
6071
default_output_filename = f"output-{datetime.datetime.now().strftime('%Y-%m-%d-%H-%M-%S')}.pptx"
6172
parser.add_argument("-o", "--output", type=str, default=default_output_filename)
6273
parser.add_argument("-i", "--input", type=str, required=True)
@@ -76,8 +87,21 @@ def extract_json(content):
7687
with open(input_json) as f:
7788
content = f.read()
7889
json_data = extract_json(content)
79-
90+
if not json_data:
91+
raise ValueError("No JSON data found in input file")
8092
with open(args.yaml_config) as f:
8193
yaml_config = yaml.safe_load(f)
94+
template_yaml = Path(template) / args.template_name / f"{args.template_name}.yaml"
95+
if not template_yaml.exists():
96+
template_config = {}
97+
else:
98+
with open(template_yaml) as f:
99+
template_config = yaml.safe_load(f)
100+
# merge
101+
yaml_config.update(template_config)
102+
103+
print(json.dumps(yaml_config, indent=4))
104+
105+
template_path = Path(template) / args.template_name / f"{args.template_name}.pptx"
82106

83-
fill_template_with_yaml_config(template, output, json_data, yaml_config)
107+
fill_template_with_yaml_config(template_path, output, json_data, yaml_config)

examples/ppt_gen/gen_schema.py

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -139,13 +139,19 @@ def build_schema(yaml_root: dict[str, Any]) -> dict[str, Any]:
139139
if isinstance(item, dict):
140140
for t in item.keys():
141141
allowed_types.add(str(t))
142-
# Fallback: infer from page specs if type_map missing
143-
if not allowed_types:
144-
for key, value in yaml_root.items():
145-
if isinstance(value, dict) and key != "type_map":
146-
t = value.get("type")
147-
if t:
148-
allowed_types.add(str(t))
142+
143+
not_allowed_types = []
144+
for key, value in yaml_root.items():
145+
if key == "type_map":
146+
continue
147+
if not isinstance(value, dict):
148+
continue
149+
t = value.get("type")
150+
if t and t not in allowed_types:
151+
not_allowed_types.append(key)
152+
# remove not allowed types from yaml_root
153+
for t in not_allowed_types:
154+
del yaml_root[t]
149155

150156
one_of = []
151157
# iterate keys excluding type_map
@@ -200,7 +206,7 @@ def build_schema(yaml_root: dict[str, Any]) -> dict[str, Any]:
200206
"Paragraph": {
201207
"type": "object",
202208
"properties": {
203-
"text": {"type": "string"},
209+
"text": {"type": "string", "maxLength": 200, "minLength": 10},
204210
"bullet": {"type": "boolean", "default": False},
205211
"level": {"type": "integer", "minimum": 0},
206212
},
@@ -211,7 +217,7 @@ def build_schema(yaml_root: dict[str, Any]) -> dict[str, Any]:
211217
"type": "object",
212218
"properties": {
213219
"title": {"type": "string", "maxLength": 4},
214-
"content": {"type": "string", "maxLength": 10},
220+
"content": {"type": "string", "maxLength": 200},
215221
},
216222
"required": ["title", "content"],
217223
"additionalProperties": False,
@@ -224,7 +230,7 @@ def build_schema(yaml_root: dict[str, Any]) -> dict[str, Any]:
224230
"properties": {
225231
"paragraph": {
226232
"oneOf": [
227-
{"type": "array", "items": {"$ref": "#/$defs/Paragraph"}, "minItems": 1},
233+
{"type": "array", "items": {"$ref": "#/$defs/Paragraph"}, "minItems": 2},
228234
{"type": "string"},
229235
]
230236
}

examples/ppt_gen/main.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
import asyncio
33
import datetime
44
import json
5+
import logging
6+
from pathlib import Path
57

68
import yaml
79
from fill_template import extract_json, fill_template_with_yaml_config
@@ -29,7 +31,8 @@ async def main():
2931
parser.add_argument("--extra_prompt", type=str, default="")
3032
parser.add_argument("--pages", type=int, default=15)
3133
parser.add_argument("--url", type=str, default=None)
32-
parser.add_argument("--template_path", type=str, default="templates/0.pptx")
34+
parser.add_argument("--template_path", type=str, default="templates")
35+
parser.add_argument("--template_name", type=str, default="0")
3336
parser.add_argument("--yaml_path", type=str, default="yaml_example.yaml")
3437
parser.add_argument("--output_path", type=str, default=f"output-{current_date}.pptx")
3538
parser.add_argument("--output_json", type=str, default=f"output-{current_date}.json")
@@ -38,6 +41,17 @@ async def main():
3841

3942
with open(args.yaml_path) as f:
4043
yaml_config = yaml.safe_load(f)
44+
template_yaml_path = Path(args.template_path) / args.template_name / f"{args.template_name}.yaml"
45+
if not template_yaml_path.exists():
46+
logging.warning("Template yaml config not found")
47+
template_yaml_config = {}
48+
else:
49+
with open(template_yaml_path) as f:
50+
template_yaml_config = yaml.safe_load(f)
51+
52+
# merge yaml_config and template_yaml_config
53+
yaml_config.update(template_yaml_config)
54+
4155
schema = build_schema(yaml_config)
4256

4357
# add json schema to instructions
@@ -71,12 +85,16 @@ async def main():
7185
final_result = result.final_output
7286
print(final_result)
7387

88+
json_data = extract_json(final_result)
89+
if not json_data:
90+
raise ValueError("No JSON data found in output")
91+
7492
with open(args.output_json, "w") as f:
75-
f.write(final_result)
93+
f.write(json_data)
7694

77-
json_data = extract_json(final_result)
95+
template_pptx_path = Path(args.template_path) / args.template_name / f"{args.template_name}.pptx"
7896
fill_template_with_yaml_config(
79-
template_path=args.template_path,
97+
template_path=template_pptx_path,
8098
output_path=args.output_path,
8199
json_data=json_data,
82100
yaml_config=yaml_config,

examples/ppt_gen/ppt_template_model.py

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,8 @@ def render(self, slide, page_json: dict[str, Any]):
8787
logging.info(f"===Rendering page type: {page_type}===")
8888

8989
# Get page configuration
90-
page_config = self.pages.get(f"{page_type}_page", {})
90+
91+
page_config = self.pages.get(page_type, {})
9192

9293
# Render all fields based on their type from YAML config
9394
for field_name, field_config in page_config.items():
@@ -96,9 +97,10 @@ def render(self, slide, page_json: dict[str, Any]):
9697

9798
field_value = page_json.get(field_name)
9899
if field_value is None:
99-
continue
100+
raise ValueError(f"Field '{field_name}' not found in page JSON")
100101

101102
field_type = field_config.get("type", "str")
103+
print(f"field_name: {field_name}, field_type: {field_type}")
102104

103105
if field_type == "str":
104106
self._render_text_field(slide, field_name, field_value)
@@ -116,7 +118,7 @@ def render(self, slide, page_json: dict[str, Any]):
116118
elif field_type == "image":
117119
self._render_basic_image_field(slide, field_name, field_value)
118120
else:
119-
logging.warning(f"Unknown field type: {field_type}")
121+
raise ValueError(f"Unknown field type: {field_type}")
120122

121123
def _render_basic_image_field(self, slide, field_name: str, image_value):
122124
logging.info(f"{field_name}: {image_value}")
@@ -127,10 +129,11 @@ def _render_basic_image_field(self, slide, field_name: str, image_value):
127129

128130
def _render_text_field(self, slide, field_name: str, text_value: str):
129131
"""Render text field"""
130-
logging.info(f"{field_name}: {text_value}")
131132
shape = find_shape_with_name_except(slide.shapes, field_name)
132133
if shape:
133134
handle_pure_text(text_value, shape, slide)
135+
else:
136+
raise ValueError(f"Shape not found for {field_name}")
134137

135138
def _render_content_field(self, slide, field_name: str, content_value):
136139
"""Render content field"""
@@ -149,7 +152,7 @@ def _render_content_list_field(self, slide, field_name: str, content_values: lis
149152
if shape:
150153
handle_content(self._ensure_content_model(content_value), shape, slide)
151154
else:
152-
logging.warning(f"Shape not found for {target_name}")
155+
raise ValueError(f"Shape not found for {target_name}")
153156

154157
def _render_item_list_field(self, slide, field_name: str, items: list):
155158
"""Render item list field"""
@@ -160,9 +163,9 @@ def _render_item_list_field(self, slide, field_name: str, items: list):
160163
def _render_label_list_field(self, slide, field_name: str, labels: list):
161164
"""Render label list field"""
162165
for i, label_text in enumerate(labels):
163-
label_shape = find_shape_with_name_except(slide.shapes, f"label{i + 1}")
166+
label_shape = find_shape_with_name_except(slide.shapes, f"{field_name}{i + 1}")
164167
if label_shape:
165-
logging.info(f"label{i + 1}: {label_text}")
168+
logging.info(f"{field_name}{i + 1}: {label_text}")
166169
handle_pure_text(label_text, label_shape, slide)
167170

168171
def _ensure_basic_image_model(self, image_value: Any) -> BasicImage:
@@ -238,6 +241,7 @@ def download_image(url, base_dir="."):
238241

239242
def handle_pure_text(text: str, target_shape, slide):
240243
try:
244+
logging.info(f"handle_pure_text: {text} =fill=> {target_shape.text_frame.text}")
241245
text_frame = target_shape.text_frame
242246
has_runs = any(paragraph.runs for paragraph in text_frame.paragraphs)
243247

@@ -246,13 +250,13 @@ def handle_pure_text(text: str, target_shape, slide):
246250
paragraph = text_frame.paragraphs[0]
247251
run = paragraph.add_run()
248252
run.text = text
249-
return
250-
251-
for paragraph in text_frame.paragraphs:
252-
for run in paragraph.runs:
253-
run.text = text
254-
text = ""
253+
else:
254+
for paragraph in text_frame.paragraphs:
255+
for run in paragraph.runs:
256+
run.text = text
257+
text = ""
255258
except Exception as e:
259+
traceback.print_exc()
256260
logging.error(f"Failed to set text: {text} {e}")
257261

258262

0 commit comments

Comments
 (0)