Skip to content

Commit 425514c

Browse files
authored
Merge pull request #8 from LRriver/implement-generative-editable-pptx
Handle editable PPTX fallback failures
2 parents 2416086 + 20bdff1 commit 425514c

90 files changed

Lines changed: 51451 additions & 156 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ node_modules/
2121
output/
2222
test_output/
2323
.playwright-cli/
24+
.vite/
2425
temp_buid_note/
2526

2627
# 生成的文件

README.md

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ NotebookLM's PPT feature is closer to a one-click result generator, with limited
1919
- **Model control**: Configure separate OpenAI-compatible models for text planning, image generation, and image editing
2020
- **Local-first config**: Manage model connections through local `config.yaml` or WebUI local API configuration; saved projects and exported files do not include API keys
2121
- **Export-ready output**: Export generated decks to PDF/PPTX for presentation or further editing
22+
- **Experimental high-fidelity editable PPTX export**: Rebuild slide images into editable PowerPoint text boxes, conservative native shapes, and positioned bitmap assets through a provider-gated generative editable export path
2223

2324
## ✨ Features
2425

@@ -29,6 +30,7 @@ NotebookLM's PPT feature is closer to a one-click result generator, with limited
2930
- 🔀 **Three model roles**: Configure `prompt_model`, `image_model`, and `edit_model` separately
3031
- 🖼️ **Image result compatibility**: Accepts URLs, Markdown image links, data URLs, `b64_json`, and raw base64
3132
- 💾 **Local multi-project persistence**: Save multiple PPT projects in the browser, including source content, outline, page designs, generated images, and per-slide edit history
33+
- 📤 **Editable PPTX reconstruction**: Export a separate experimental high-fidelity editable PPTX mode after provider verification; see [Generative Editable PPTX Export](docs/generative-editable-pptx.md)
3234

3335
## 🚀 Quick Start
3436

@@ -102,6 +104,8 @@ Notes:
102104
6. **Preview & Edit**: Preview generated slides in the right panel and edit a single page when needed
103105
7. **Export**: Export to PDF or PPTX
104106

107+
The export menu keeps the existing raster PPTX option and adds a separate experimental high-fidelity editable PPTX option. The editable mode uses OCR plus image editing/image generation providers and is quality-gated by default; it fails rather than silently returning a low-fidelity deck unless an explicit fallback policy is requested. Strict live provider verification is still pending for the currently tested provider set. See [Generative Editable PPTX Export](docs/generative-editable-pptx.md).
108+
105109
The built-in demo source is `doc/L9.md`. This is a repository-relative path, so a fresh clone can use it directly in the WebUI or CLI examples.
106110

107111
## 📁 Project Structure
@@ -123,28 +127,30 @@ OpenNotebookLM-AIPPT/
123127

124128
All configurations are managed in `config.yaml`, including:
125129
- API configuration (`prompt_model`, `image_model`, `edit_model`)
130+
- Generative editable PPTX provider roles and quality gates (`ocr_model`, cleanup, asset generation, repair, validation)
126131
- PPT default settings (language, style, page count)
127132
- Timeout and retry settings
128133

129134
See `config.example.yaml` for detailed configuration examples.
130135

131136
### Using OpenAI Compatible API
132137

138+
The current protocol is OpenAI-compatible `/chat/completions`: text profiles use
139+
chat completions, and image/edit profiles use multimodal chat completions that
140+
return an image URL, data URL, or base64 payload.
141+
133142
```yaml
134143
api:
135144
models:
136145
prompt_model:
137-
adapter: "openai_chat"
138146
model: "gpt-4o"
139147
base_url: "https://api.openai.com/v1"
140148
api_key: "sk-xxx"
141149
image_model:
142-
adapter: "raw_chat_multimodal"
143150
model: "gpt-image-2"
144151
base_url: "https://api.example.com/v1"
145152
api_key: "sk-xxx"
146153
edit_model:
147-
adapter: "raw_chat_multimodal"
148154
model: "gpt-image-2"
149155
base_url: "https://api.example.com/v1"
150156
api_key: "sk-xxx"
@@ -186,14 +192,20 @@ npm run test
186192
npm run build
187193
```
188194

189-
GitHub Actions runs the same default checks on `main` and `dev` pull requests and pushes. Real model API calls are intentionally not part of the default CI because they require private keys and can be flaky.
195+
GitHub Actions runs the same default checks on `main` and `dev` pull requests and pushes. Real model API calls and desktop PowerPoint/WPS rendering are intentionally not part of the default CI because they require private keys or host applications that are not available in GitHub runners. CI still validates package-level PPTX structure, object manifests, renderer contracts, and fake-provider reconstruction paths.
190196

191197
## 📋 TODO
192198

193199
- [ ] Upgrade generated PPT images into structured, editable PPT content
194200
- [ ] Support region selection for partial slide editing
195201
- [ ] Add more provider profile templates
196202

203+
## 🙏 Acknowledgements
204+
205+
The editable PPTX reconstruction design references ideas from:
206+
- [LRriver/slide-alchemy](https://github.com/LRriver/slide-alchemy)
207+
- [ningzimu/image-to-editable-ppt-skill](https://github.com/ningzimu/image-to-editable-ppt-skill)
208+
197209
## 📄 License
198210

199211
Apache License 2.0

README_zh.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ NotebookLM 的 PPT 能力更像“一键生成结果”,中间设计过程和
1919
- **模型可控**:文本规划、生图、图片编辑可分别配置不同 OpenAI-compatible 模型
2020
- **本地可跑**:可使用本地 `config.yaml` 或 WebUI 本地 API 配置管理模型连接,项目记录和导出文件不包含 API Key
2121
- **结果可导出**:生成后可直接导出 PDF/PPTX,适合继续汇报或二次编辑
22+
- **实验性高保真可编辑 PPTX 导出**:通过 provider 门禁的生成式可编辑导出链路,把幻灯片图片重建为 PowerPoint 可编辑文本框、保守原生形状和可移动图片资产
2223

2324
## ✨ 功能特性
2425

@@ -29,6 +30,7 @@ NotebookLM 的 PPT 能力更像“一键生成结果”,中间设计过程和
2930
- 🔀 **三模型角色**:支持 `prompt_model``image_model``edit_model` 分别配置
3031
- 🖼️ **图像结果兼容**:兼容 URL、Markdown 图片链接、data URL、`b64_json` 和纯 base64
3132
- 💾 **多项目本地留存**:支持在浏览器本地保存多个 PPT 项目,恢复资料、设计大纲、逐页设计、生成图片和单页编辑历史
33+
- 📤 **可编辑 PPTX 重建**:新增独立的实验性高保真可编辑 PPTX 导出模式,需要通过 provider 验证后使用,详见 [Generative Editable PPTX Export](docs/generative-editable-pptx.md)
3234

3335
## 🚀 快速开始
3436

@@ -102,6 +104,8 @@ AIPPT 会把项目内容和图片资源保存在当前浏览器 Profile 的 Inde
102104
6. **预览编辑**:在右侧面板预览生成的幻灯片,点击可进行单页编辑
103105
7. **导出文件**:选择 PDF 或 PPTX 格式导出
104106

107+
导出菜单会保留原有栅格 PPTX 选项,并新增独立的实验性高保真可编辑 PPTX 选项。可编辑模式依赖 OCR、图像编辑和图像生成 provider,默认优先保证质量:验证失败时返回错误,不会悄悄降级为低保真 PPTX,除非请求显式允许 fallback。当前测试过的 provider 组合仍未通过严格 live 验证。详见 [Generative Editable PPTX Export](docs/generative-editable-pptx.md)
108+
105109
仓库内置演示资料为 `doc/L9.md`。该路径是仓库相对路径,clone 后可直接用于 WebUI 上传或命令行示例。
106110

107111
## 📁 项目结构
@@ -123,28 +127,30 @@ OpenNotebookLM-AIPPT/
123127

124128
所有配置统一在 `config.yaml` 中管理,包括:
125129
- API 配置(文本 prompt、生图、编辑三角色模型)
130+
- 生成式可编辑 PPTX provider 角色和质量门禁(OCR、清理、资产生成、修复、验证)
126131
- PPT 默认配置(语言、风格、页数)
127132
- 超时和重试配置
128133

129134
详细配置示例请参考 `config.example.yaml`
130135

131136
### 使用 OpenAI 兼容 API
132137

138+
当前调用协议为 OpenAI-compatible `/chat/completions`:文本模型走 chat
139+
completion,图像/编辑模型走多模态 chat completion,响应需返回图片 URL、data URL
140+
或 base64。
141+
133142
```yaml
134143
api:
135144
models:
136145
prompt_model:
137-
adapter: "openai_chat"
138146
model: "gpt-4o"
139147
base_url: "https://api.openai.com/v1"
140148
api_key: "sk-xxx"
141149
image_model:
142-
adapter: "raw_chat_multimodal"
143150
model: "gpt-image-2"
144151
base_url: "https://api.example.com/v1"
145152
api_key: "sk-xxx"
146153
edit_model:
147-
adapter: "raw_chat_multimodal"
148154
model: "gpt-image-2"
149155
base_url: "https://api.example.com/v1"
150156
api_key: "sk-xxx"

api/README.md

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ FastAPI 后端服务,为 WebUI 前端提供 PPT 生成、编辑和导出功能
77
- **文件上传**: 支持 `.md/.txt/.pdf/.docx/.pptx`,统一解析为 Markdown
88
- **PPT 生成**: 基于输入材料生成 PPT 幻灯片,支持流式返回进度
99
- **图生图编辑**: 对单页幻灯片进行修改
10-
- **导出功能**: 支持导出为 PDF PPTX 格式
10+
- **导出功能**: 支持导出为 PDF、栅格 PPTX 和生成式高保真可编辑 PPTX 格式
1111
- **模型路由**: 支持 prompt/image/edit 三角色模型 profile
1212

1313
## API 端点
@@ -44,17 +44,15 @@ Content-Type: application/json
4444
"content": "Markdown 内容",
4545
"config": {
4646
"model_profiles": {
47-
"prompt_model": {
47+
"text_model": {
4848
"model": "DeepSeek-V4-Pro",
4949
"base_url": "https://api.example.com/v1",
50-
"api_key": "your-text-key",
51-
"adapter": "openai_chat"
50+
"api_key": "your-text-key"
5251
},
5352
"image_model": {
5453
"model": "gpt-image-2",
5554
"base_url": "https://api.example.com/v1",
56-
"api_key": "your-image-key",
57-
"adapter": "raw_chat_multimodal"
55+
"api_key": "your-image-key"
5856
}
5957
},
6058
"page_count": 10,
@@ -81,9 +79,9 @@ Content-Type: application/json
8179
"instruction": "修改指令",
8280
"config": {
8381
"model_profiles": {
84-
"prompt_model": {"model": "DeepSeek-V4-Pro", "base_url": "https://api.example.com/v1", "api_key": "key", "adapter": "openai_chat"},
85-
"image_model": {"model": "gpt-image-2", "base_url": "https://api.example.com/v1", "api_key": "key", "adapter": "raw_chat_multimodal"},
86-
"edit_model": {"model": "gpt-image-2", "base_url": "https://api.example.com/v1", "api_key": "key", "adapter": "raw_chat_multimodal"}
82+
"text_model": {"model": "DeepSeek-V4-Pro", "base_url": "https://api.example.com/v1", "api_key": "key"},
83+
"image_model": {"model": "gpt-image-2", "base_url": "https://api.example.com/v1", "api_key": "key"},
84+
"edit_model": {"model": "gpt-image-2", "base_url": "https://api.example.com/v1", "api_key": "key"}
8785
},
8886
"quality": "1K",
8987
"aspect_ratio": "16:9"
@@ -115,6 +113,51 @@ Content-Type: application/json
115113
响应: 文件下载
116114
```
117115

116+
#### 生成式高保真可编辑 PPTX
117+
118+
该模式与普通 `pptx` 分开:普通 `pptx` 仍是把每页图片作为整页背景放入 PowerPoint;`generative_editable_pptx` 会通过 OCR、图片清理、前景资产重建、原生形状拟合和验证流程重建可编辑元素。
119+
120+
```
121+
POST /api/export
122+
Content-Type: application/json
123+
124+
请求体:
125+
{
126+
"format": "generative_editable_pptx",
127+
"aspect_ratio": "16:9",
128+
"slide_order": ["slide-1", "slide-2"],
129+
"editable_options": {
130+
"fallback_policy": "fail"
131+
},
132+
"slides": [
133+
{
134+
"slide_id": "slide-1",
135+
"image_base64": "base64编码的图片",
136+
"text_metadata": [
137+
{
138+
"text": "Quarterly Plan",
139+
"role": "title",
140+
"order": 1,
141+
"style_hint": {"font_size": 32, "bold": true}
142+
}
143+
]
144+
}
145+
]
146+
}
147+
```
148+
149+
`fallback_policy` 可选值:
150+
- `fail`:默认。验证失败时返回错误,不返回低保真文件。
151+
- `text_editable_background`:显式允许使用文本可编辑、背景清理后的 fallback。
152+
- `raster_pptx`:显式允许降级到现有栅格 PPTX 导出。
153+
154+
成功响应仍是 `.pptx` 文件下载。若使用 fallback,响应 header 会包含:
155+
- `X-Generative-Editable-Status`
156+
- `X-Generative-Editable-Fallback-Policy`
157+
- `X-Generative-Editable-Fallback-Used`
158+
159+
更多配置和限制见 `docs/generative-editable-pptx.md`
160+
118161
## 启动服务器
119162

120163
### 方式 1: 使用启动脚本

api/models.py

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
Pydantic 请求/响应模型定义
33
"""
44

5-
from typing import List, Optional, Literal
5+
from typing import Any, List, Optional, Literal
66
from pydantic import BaseModel, Field
77

88

@@ -51,18 +51,24 @@ class ModelProfileConfig(BaseModel):
5151
model: str = Field(..., description="模型名称")
5252
base_url: str = Field(..., description="OpenAI-compatible Base URL")
5353
api_key: str = Field(..., description="API Key")
54-
adapter: str = Field("openai_chat", description="适配器")
5554
thinking: Optional[Literal["enabled", "disabled"]] = Field(
5655
None, description="OpenAI-compatible thinking mode"
5756
)
5857

5958

6059
class ModelProfilesConfig(BaseModel):
61-
"""三角色模型配置"""
60+
"""模型 profile 配置"""
6261

63-
prompt_model: ModelProfileConfig
62+
text_model: Optional[ModelProfileConfig] = None
63+
prompt_model: Optional[ModelProfileConfig] = Field(None, description="旧字段,兼容 text_model")
6464
image_model: ModelProfileConfig
6565
edit_model: Optional[ModelProfileConfig] = None
66+
VLM: Optional[ModelProfileConfig] = Field(
67+
None, description="可选,多模态理解模型;图片转可编辑 PPTX 时使用"
68+
)
69+
ocr_model: Optional[ModelProfileConfig] = Field(
70+
None, description="可选,OCR 模型;图片转可编辑 PPTX 时使用"
71+
)
6672

6773

6874
class GenerationConfig(BaseModel):
@@ -226,6 +232,7 @@ class SlideData(BaseModel):
226232
page_number: int
227233
image_base64: str
228234
prompt: str
235+
text_metadata: List["ExportTextMetadata"] = Field(default_factory=list)
229236

230237

231238
class GenerationProgressEvent(BaseModel):
@@ -293,14 +300,33 @@ class ExportSlide(BaseModel):
293300
"""导出的幻灯片"""
294301

295302
image_base64: str
303+
slide_id: Optional[str] = None
304+
text_metadata: List["ExportTextMetadata"] = Field(default_factory=list)
305+
306+
307+
class ExportTextMetadata(BaseModel):
308+
"""导出时附带的可编辑文本语义信息"""
309+
310+
text: str
311+
role: str
312+
order: int
313+
style_hint: dict[str, Any] = Field(default_factory=dict)
314+
315+
316+
class ExportEditableOptions(BaseModel):
317+
"""可编辑导出的附加选项"""
318+
319+
fallback_policy: Literal["fail", "text_editable_background", "raster_pptx"] = "fail"
296320

297321

298322
class ExportRequest(BaseModel):
299323
"""导出请求"""
300324

301325
slides: List[ExportSlide]
302-
format: Literal["pdf", "pptx"] = Field(..., description="导出格式")
326+
format: Literal["pdf", "pptx", "generative_editable_pptx"] = Field(..., description="导出格式")
303327
aspect_ratio: Literal["16:9", "4:3"] = Field("16:9", description="导出画幅比例")
328+
slide_order: Optional[List[str]] = None
329+
editable_options: ExportEditableOptions = Field(default_factory=ExportEditableOptions)
304330

305331

306332
class ExportResponse(BaseModel):

api/profile_resolver.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,18 @@ def _has_complete_generation_profiles(config: Any) -> bool:
1515
profiles = getattr(config, "model_profiles", None)
1616
if not profiles:
1717
return False
18-
required = (profiles.prompt_model, profiles.image_model)
18+
text_profile = (
19+
getattr(profiles, "text_model", None)
20+
or getattr(profiles, "prompt_model", None)
21+
or getattr(profiles, "VLM", None)
22+
)
23+
image_profile = getattr(profiles, "image_model", None)
24+
required = (text_profile, image_profile)
1925
return all(
20-
_has_value(profile.model) and _has_value(profile.base_url) and _has_value(profile.api_key)
26+
profile
27+
and _has_value(profile.model)
28+
and _has_value(profile.base_url)
29+
and _has_value(profile.api_key)
2130
for profile in required
2231
)
2332

@@ -41,7 +50,7 @@ def profiles_from_generation_config(config: Any) -> ModelProfileSet:
4150

4251
if _has_complete_legacy_profiles(config):
4352
data: Dict[str, Any] = {
44-
"prompt_model": {
53+
"text_model": {
4554
"model": config.text.model,
4655
"base_url": config.text.base_url,
4756
"api_key": config.text.api_key,
@@ -68,7 +77,7 @@ def profiles_from_generation_config(config: Any) -> ModelProfileSet:
6877

6978
return resolve_model_profiles(
7079
{
71-
"prompt_model": {
80+
"text_model": {
7281
"model": config.get_text_model(),
7382
"base_url": config.get_text_base_url(),
7483
"api_key": config.get_text_api_key(),
@@ -98,7 +107,7 @@ def profiles_from_edit_config(config: Any) -> ModelProfileSet:
98107

99108
return resolve_model_profiles(
100109
{
101-
"prompt_model": {
110+
"text_model": {
102111
"model": config.model,
103112
"base_url": config.base_url,
104113
"api_key": config.api_key,

0 commit comments

Comments
 (0)