sn-ppt-creative
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
Chinesesn-ppt-creative
sn-ppt-creative
Call-routing policy
调用路由策略
| Kind | Backend |
|---|---|
| LLM (text) | |
| VLM (image understanding) | |
| T2I (image generation) | |
Never mix — LLM / VLM through sn-image-base, or T2I through model_client — both violate policy.
| 类型 | 后端 |
|---|---|
| LLM(文本) | |
| VLM(图像理解) | |
| T2I(图像生成) | |
切勿混淆——禁止通过sn-image-base调用LLM/VLM,或通过model_client调用T2I,两种行为均违反策略。
Preconditions
前置条件
- exists and
<deck_dir>/task_pack.jsonppt_mode == "creative" - exists
<deck_dir>/info_pack.json - exists
<deck_dir>/pages/ - env var (OpenClaw-injected) points at the sn-image-base skill root
$SN_IMAGE_BASE - env var points at the sn-ppt-standard skill root (so we can import
$PPT_STANDARD_DIR)model_client
Any missing → stop and tell user to enter via .
/skill sn-ppt-entry- 存在且
<deck_dir>/task_pack.jsonppt_mode == "creative" - 存在
<deck_dir>/info_pack.json - 目录存在
<deck_dir>/pages/ - 环境变量(由OpenClaw注入)指向sn-image-base技能根目录
$SN_IMAGE_BASE - 环境变量指向sn-ppt-standard技能根目录(以便导入
$PPT_STANDARD_DIR)model_client
若存在任一缺失项→终止流程并告知用户通过进入。
/skill sn-ppt-entryResume
续扫流程
bash
python3 $SKILL_DIR/scripts/resume_scan.py --deck-dir <deck_dir>bash
python3 $SKILL_DIR/scripts/resume_scan.py --deck-dir <deck_dir>=> {"style_spec_done": bool, "outline_done": bool, "pptx_done": bool,
=> {"style_spec_done": bool, "outline_done": bool, "pptx_done": bool,
"pages": [{"page_no": 1, "action": "skip|render_only|full"}, ...]}
"pages": [{"page_no": 1, "action": "skip|render_only|full"}, ...]}
Dispatch:
| Manifest | Do |
|---|---|
| `style_spec_done == false` | Run Stage 2 |
| `outline_done == false` | Run Stage 3 |
| per-page `action == "full"` | Run Stage 4.1 + 4.2 |
| per-page `action == "render_only"` | Run Stage 4.2 only (prompt.txt already on disk) |
| per-page `action == "skip"` | Skip |
| `pptx_done == false` (all pages done or failed) | Run Stage 5 |
任务分发规则:
| 状态标识 | 执行操作 |
|---|---|
| `style_spec_done == false` | 执行Stage 2 |
| `outline_done == false` | 执行Stage 3 |
| 单页`action == "full"` | 执行Stage 4.1 + 4.2 |
| 单页`action == "render_only"` | 仅执行Stage 4.2(prompt.txt已存储在磁盘) |
| 单页`action == "skip"` | 跳过该页 |
| `pptx_done == false`(所有页面已完成或失败) | 执行Stage 5 |Stage 2 — style_spec.md (LLM or VLM via model_client)
Stage 2 — style_spec.md(通过model_client调用LLM或VLM)
One independent exec tool_call. Two branches based on reference images.
Branch A (no ref images, or all missing on disk) — use :
model_client.llmbash
python3 -c "
import sys, pathlib, json
sys.path.insert(0, '$PPT_STANDARD_DIR/lib')
from model_client import llm
deck = pathlib.Path('<deck_dir>')
tp = json.loads((deck / 'task_pack.json').read_text())
ip = json.loads((deck / 'info_pack.json').read_text())
sys_prompt = open('$SKILL_DIR/prompts/style_from_query.md').read()
user_prompt = json.dumps({
'params': tp['params'],
'query': ip.get('user_query'),
'digest': ip.get('document_digest'),
}, ensure_ascii=False)
md = llm(sys_prompt, user_prompt)
(deck / 'style_spec.md').write_text(md, encoding='utf-8')
print('style_spec.md ok')
"Branch B (≥1 reference image on disk) — use :
model_client.vlmbash
python3 -c "
import sys, pathlib, json
sys.path.insert(0, '$PPT_STANDARD_DIR/lib')
from model_client import vlm
deck = pathlib.Path('<deck_dir>')
ip = json.loads((deck / 'info_pack.json').read_text())
tp = json.loads((deck / 'task_pack.json').read_text())
refs = [p for p in (ip.get('user_assets') or {}).get('reference_images', []) if pathlib.Path(p).exists()]
sys_prompt = open('$SKILL_DIR/prompts/style_from_image.md').read()
user_prompt = f'PPT 主题/参数: {json.dumps(tp[\"params\"], ensure_ascii=False)}\nuser_query: {ip.get(\"user_query\") or \"\"}'
md = vlm(sys_prompt, user_prompt, images=refs)
(deck / 'style_spec.md').write_text(md, encoding='utf-8')
print(f'style_spec.md ok (from {len(refs)} ref images)')
"If is non-empty but all paths missing on disk: fall through to Branch A and prepend a line at the top of style_spec.md.
user_assets.reference_imagesreference_images_missing: <original paths>单次独立工具调用。根据参考图片分为两个分支。
分支A(无参考图片,或所有参考图片在磁盘上缺失)——使用:
model_client.llmbash
python3 -c "
import sys, pathlib, json
sys.path.insert(0, '$PPT_STANDARD_DIR/lib')
from model_client import llm
deck = pathlib.Path('<deck_dir>')
tp = json.loads((deck / 'task_pack.json').read_text())
ip = json.loads((deck / 'info_pack.json').read_text())
sys_prompt = open('$SKILL_DIR/prompts/style_from_query.md').read()
user_prompt = json.dumps({
'params': tp['params'],
'query': ip.get('user_query'),
'digest': ip.get('document_digest'),
}, ensure_ascii=False)
md = llm(sys_prompt, user_prompt)
(deck / 'style_spec.md').write_text(md, encoding='utf-8')
print('style_spec.md ok')
"分支B(磁盘上存在≥1张参考图片)——使用:
model_client.vlmbash
python3 -c "
import sys, pathlib, json
sys.path.insert(0, '$PPT_STANDARD_DIR/lib')
from model_client import vlm
deck = pathlib.Path('<deck_dir>')
ip = json.loads((deck / 'info_pack.json').read_text())
tp = json.loads((deck / 'task_pack.json').read_text())
refs = [p for p in (ip.get('user_assets') or {}).get('reference_images', []) if pathlib.Path(p).exists()]
sys_prompt = open('$SKILL_DIR/prompts/style_from_image.md').read()
user_prompt = f'PPT 主题/参数: {json.dumps(tp[\"params\"], ensure_ascii=False)}\nuser_query: {ip.get(\"user_query\") or \"\"}'
md = vlm(sys_prompt, user_prompt, images=refs)
(deck / 'style_spec.md').write_text(md, encoding='utf-8')
print(f'style_spec.md ok (from {len(refs)} ref images)')
"若非空但所有路径在磁盘上缺失:自动 fallback 到分支A,并在style_spec.md顶部添加一行。
user_assets.reference_imagesreference_images_missing: <original paths>Stage 3 — outline.json (LLM via model_client)
Stage 3 — outline.json(通过model_client调用LLM)
bash
python3 -c "
import sys, pathlib, json
sys.path.insert(0, '$PPT_STANDARD_DIR/lib')
from model_client import llm
deck = pathlib.Path('<deck_dir>')
tp = json.loads((deck / 'task_pack.json').read_text())
ip = json.loads((deck / 'info_pack.json').read_text())
style = (deck / 'style_spec.md').read_text()
sys_prompt = open('$SKILL_DIR/prompts/outline.md').read()
user_prompt = json.dumps({
'style_spec_markdown': style,
'params': tp['params'],
'query': ip.get('user_query'),
'digest': ip.get('document_digest'),
}, ensure_ascii=False)
raw = llm(sys_prompt, user_prompt).strip()
if raw.startswith('\`\`\`'):
raw = raw.split('\n', 1)[1].rsplit('\`\`\`', 1)[0]
data = json.loads(raw)
assert len(data['pages']) == tp['params']['page_count'], 'page_count mismatch'
(deck / 'outline.json').write_text(json.dumps(data, ensure_ascii=False, indent=2))
print(f'outline ok, {len(data[\"pages\"])} pages')
"On failure (non-JSON / length mismatch): abort.
bash
python3 -c "
import sys, pathlib, json
sys.path.insert(0, '$PPT_STANDARD_DIR/lib')
from model_client import llm
deck = pathlib.Path('<deck_dir>')
tp = json.loads((deck / 'task_pack.json').read_text())
ip = json.loads((deck / 'info_pack.json').read_text())
style = (deck / 'style_spec.md').read_text()
sys_prompt = open('$SKILL_DIR/prompts/outline.md').read()
user_prompt = json.dumps({
'style_spec_markdown': style,
'params': tp['params'],
'query': ip.get('user_query'),
'digest': ip.get('document_digest'),
}, ensure_ascii=False)
raw = llm(sys_prompt, user_prompt).strip()
if raw.startswith('\`\`\`'):
raw = raw.split('\n', 1)[1].rsplit('\`\`\`', 1)[0]
data = json.loads(raw)
assert len(data['pages']) == tp['params']['page_count'], 'page_count mismatch'
(deck / 'outline.json').write_text(json.dumps(data, ensure_ascii=False, indent=2))
print(f'outline ok, {len(data[\"pages\"])} pages')
"若执行失败(返回非JSON格式/页数不匹配):终止流程。
Stage 4 — per-page: one independent exec per page
Stage 4 — 单页处理:每页对应一次独立执行
4.1 Compose prompt (LLM via model_client) — skip if action == "render_only"
action == "render_only"4.1 生成提示词(通过model_client调用LLM)——若action == "render_only"
则跳过
action == "render_only"bash
python3 -c "
import sys, pathlib, json
sys.path.insert(0, '$PPT_STANDARD_DIR/lib')
from model_client import llm
deck = pathlib.Path('<deck_dir>')
N = <NNN>
style = (deck / 'style_spec.md').read_text()
outline = json.loads((deck / 'outline.json').read_text())
page = next(p for p in outline['pages'] if int(p['page_no']) == N)
sys_prompt = open('$SKILL_DIR/prompts/page_prompt.md').read()
user_prompt = json.dumps({'style_spec_markdown': style, 'page': page}, ensure_ascii=False)
txt = llm(sys_prompt, user_prompt)
(deck / 'pages' / f'page_{N:03d}.prompt.txt').write_text(txt, encoding='utf-8')
print(f'prompt page {N} ok')
"bash
python3 -c "
import sys, pathlib, json
sys.path.insert(0, '$PPT_STANDARD_DIR/lib')
from model_client import llm
deck = pathlib.Path('<deck_dir>')
N = <NNN>
style = (deck / 'style_spec.md').read_text()
outline = json.loads((deck / 'outline.json').read_text())
page = next(p for p in outline['pages'] if int(p['page_no']) == N)
sys_prompt = open('$SKILL_DIR/prompts/page_prompt.md').read()
user_prompt = json.dumps({'style_spec_markdown': style, 'page': page}, ensure_ascii=False)
txt = llm(sys_prompt, user_prompt)
(deck / 'pages' / f'page_{N:03d}.prompt.txt').write_text(txt, encoding='utf-8')
print(f'prompt page {N} ok')
"sanitize the written prompt in-place: strip hex/rgb/hsl/CSS/px/em/rem etc
原地清理生成的提示词:移除十六进制/RGB/HSL/CSS/px/em/rem等内容
to prevent T2I server-side prompt-enhance from baking them into the image.
防止T2I服务端的提示词增强功能将这些内容嵌入图像。
Silent: no chat-facing notification; removals go to stderr only.
静默执行:不向聊天发送通知;移除操作仅记录到stderr。
python3 $SKILL_DIR/scripts/sanitize_prompt.py --path <deck_dir>/pages/page_<NNN>.prompt.txt
undefinedpython3 $SKILL_DIR/scripts/sanitize_prompt.py --path <deck_dir>/pages/page_<NNN>.prompt.txt
undefined4.2 Generate image (T2I via sn-image-base)
4.2 生成图像(通过sn-image-base调用T2I)
--negative-promptbash
python $SN_IMAGE_BASE/scripts/sn_agent_runner.py sn-image-generate \
--prompt "$(cat <deck_dir>/pages/page_<NNN>.prompt.txt)" \
--negative-prompt "hex color code, #RRGGBB, rgb(), rgba(), hsl(), hsla(), css, json, yaml, code snippet, pixel values, px, em, rem, pt, color palette text, typography label, design spec, style guide, font stack, hex code, layout annotation, dimensional callout, figma-style spec sheet, wireframe annotation, swatch with numbers" \
--aspect-ratio 16:9 \
--image-size 2k \
--save-path <deck_dir>/pages/page_<NNN>.png \
--output-format json--negative-promptbash
python $SN_IMAGE_BASE/scripts/sn_agent_runner.py sn-image-generate \
--prompt "$(cat <deck_dir>/pages/page_<NNN>.prompt.txt)" \
--negative-prompt "hex color code, #RRGGBB, rgb(), rgba(), hsl(), hsla(), css, json, yaml, code snippet, pixel values, px, em, rem, pt, color palette text, typography label, design spec, style guide, font stack, hex code, layout annotation, dimensional callout, figma-style spec sheet, wireframe annotation, swatch with numbers" \
--aspect-ratio 16:9 \
--image-size 2k \
--save-path <deck_dir>/pages/page_<NNN>.png \
--output-format json4.3 Failure handling
4.3 失败处理
- 4.1 failure (model timeout / empty / malformed): record into
page_no, echo failure line, continue.failed_pages - 4.2 failure: same — record, echo, continue.
- No retries. No placeholder PNG. Don't write 1x1 transparent PNGs to fake success.
- may remain on disk for a later manual re-run of 4.2 only.
.prompt.txt
- 4.1执行失败(模型超时/返回空内容/格式错误):将记录到
page_no,输出失败信息,继续执行后续流程。failed_pages - 4.2执行失败:处理逻辑同上——记录失败、输出信息、继续执行。
- 不重试。不生成占位PNG。禁止写入1x1透明PNG来伪造成功。
- 可保留在磁盘上,以便后续手动重新执行4.2步骤。
.prompt.txt
Stage 5 — pptx 打包(一次独立 exec)
Stage 5 — PPTX打包(单次独立执行)
所有页图生成后(含部分失败的情况),把 平铺打包成 16:9 整册 PPTX,每张图满版一页。由 完成,模型只负责执行脚本。
pages/page_*.pngscripts/build_pptx.pybash
python3 $SKILL_DIR/scripts/build_pptx.py --deck-dir <deck_dir>所有页面图像生成完成后(包含部分页面失败的情况),将平铺打包为16:9比例的完整PPTX文件,每张图片占满一页。该操作由完成,模型仅负责执行脚本。
pages/page_*.pngscripts/build_pptx.pybash
python3 $SKILL_DIR/scripts/build_pptx.py --deck-dir <deck_dir>=> {"deck_id": "...", "output": "<deck_dir>/<deck_id>.pptx",
=> {"deck_id": "...", "output": "<deck_dir>/<deck_id>.pptx",
"total_slides": N, "included_pages": [...], "missing_pages": [...]}
"total_slides": N, "included_pages": [...], "missing_pages": [...]}
行为约定:
- 输出路径默认 `<deck_dir>/<deck_id>.pptx`;可用 `--output` 覆盖。
- 页序按 `outline.json` 的 `page_no` 排;缺失 `outline.json` 时按 `page_001..page_NNN` 走。
- 缺失的 PNG 会插入空白页并在 stderr 记录一行,**不中止**;这样跟 Stage 4 的"失败跳过"语义一致。
- 脚本失败(依赖缺失 / 写盘失败):echo 失败原因,**不中止整个 skill**,仍进入 Stage 6 收尾;PNG 已在磁盘上。
依赖:`python-pptx`(与 `sn-ppt-standard` 共用的打包思路;若运行环境未装,由 `sn-ppt-doctor` 的 env check 提示安装)。
行为约定:
- 默认输出路径为`<deck_dir>/<deck_id>.pptx`;可通过`--output`参数覆盖。
- 页面顺序按照`outline.json`中的`page_no`排序;若`outline.json`缺失,则按照`page_001..page_NNN`的顺序排列。
- 缺失的PNG对应的页面会插入空白页,并在stderr中记录一行信息,**不中止流程**;此逻辑与Stage4的“失败跳过”语义一致。
- 脚本执行失败(依赖缺失/写入磁盘失败):输出失败原因,**不终止整个技能流程**,仍进入Stage6收尾;PNG文件已存储在磁盘上。
依赖:`python-pptx`(与`sn-ppt-standard`采用相同的打包思路;若运行环境未安装,将由`sn-ppt-doctor`的环境检查提示安装)。Stage 6 — closing
Stage 6 — 收尾
Emit:
创意模式已完成。
📁 输出目录:<deck_dir>
📄 结果文件:
- style_spec.md
- outline.json
- pages/page_001.png ~ page_NNN.png(失败 M 页:page_..., page_...)
- <deck_id>.pptx(整册,缺失页插入空白)
⚠️ 未完成:
- page_007:生图返回超时,已跳过(pptx 中为空白页)
下一步:
- 可直接打开 <deck_id>.pptx 查看整册
- 或在 pages/ 目录查看 PNG输出以下内容:
创意模式已完成。
📁 输出目录:<deck_dir>
📄 结果文件:
- style_spec.md
- outline.json
- pages/page_001.png ~ page_NNN.png(失败 M 页:page_..., page_...)
- <deck_id>.pptx(整册,缺失页插入空白)
⚠️ 未完成:
- page_007:生图返回超时,已跳过(pptx 中为空白页)
下一步:
- 可直接打开 <deck_id>.pptx 查看整册
- 或在 pages/ 目录查看 PNGProgress echo — MANDATORY
进度反馈——强制要求
| Stage | Example |
|---|---|
| After resume_scan | |
| After Stage 2 | |
| After Stage 3 | |
| Per page-prompt (4.1) | |
| Per page-image (4.2) | |
| After Stage 5 | |
| Closing | full summary above |
- Each echo is a chat reply, not a log write.
- Per-page echo is the heartbeat for Stage 4.
- On failure, echo failure line with reason before moving on.
| 阶段 | 示例 |
|---|---|
| 续扫完成后 | |
| Stage2完成后 | |
| Stage3完成后 | |
| 单页提示词生成(4.1) | |
| 单页图像生成(4.2) | |
| Stage5完成后 | |
| 收尾阶段 | 上述完整总结内容 |
- 每条反馈均为聊天回复,而非日志写入。
- 单页反馈是Stage4的心跳信号。
- 执行失败时,需先输出包含失败原因的信息,再继续后续流程。
🚫 Hard rules
🚫 硬性规则
- Do NOT loop inside a single exec. One page = one tool_call.
- Do NOT fake images. Failed T2I → record failed, move on. No 1x1 placeholder PNGs.
- Do NOT use — T2I must go through
model_client.t2i.sn-image-basehandles only LLM / VLM.model_client - Do NOT use or
sn-text-optimizefrom sn-image-base — those must go throughsn-image-recognize/model_client.llm.model_client.vlm - Do NOT retry on first failure.
- Do NOT generate editable JSON from PNG (out of scope).
- 禁止在单次执行中循环。每页对应一次工具调用。
- 禁止伪造图像。T2I执行失败→记录失败,继续后续流程。禁止生成1x1占位PNG。
- 禁止使用——T2I必须通过
model_client.t2i调用。sn-image-base仅处理LLM/VLM调用。model_client - 禁止使用sn-image-base中的或
sn-text-optimize——这些功能必须通过sn-image-recognize/model_client.llm调用。model_client.vlm - 首次失败时禁止重试。
- 禁止从PNG生成可编辑JSON(超出当前技能范围)。