feat: add Chinese office productivity plugins
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
---
|
||||
name: build-presentations
|
||||
description: 创建、读取、编辑和验证 PPTX/PowerPoint 演示文稿。用户要求制作幻灯片、改写已有 PPT、沿用模板、提取演示内容、检查文字溢出或逐页优化叙事时使用。
|
||||
---
|
||||
|
||||
# CJYSA 演示文稿
|
||||
|
||||
## 先规划再制作
|
||||
|
||||
1. 明确受众、目的、演讲时长、页数和视觉风格。
|
||||
2. 先写一行式故事线:背景 → 问题/机会 → 证据 → 方案 → 行动。
|
||||
3. 每页只承担一个主要任务;标题应表达结论,而不是只写主题名称。
|
||||
4. 有模板时以模板为唯一视觉基准;没有模板时使用统一的字体、色板、网格和留白。
|
||||
|
||||
## 实现流程
|
||||
|
||||
1. 调用工作区依赖加载器。优先使用 `@oai/artifact-tool`;不可用时使用其 Python 和 `python-pptx`。
|
||||
2. 先检查输入演示:
|
||||
|
||||
```powershell
|
||||
& "<bundled-python>" "<skill-dir>\scripts\pptx_tool.py" inspect "<input.pptx>"
|
||||
```
|
||||
|
||||
3. 创建或编辑 PPTX,保留原文件并输出新副本。
|
||||
4. 运行 `validate`,检查空白页、超出画布的对象、过小字号和疑似标题换行。
|
||||
5. 使用 artifact-tool、PowerPoint、WPS 或可靠转换器渲染全部幻灯片;逐页全尺寸检查,不把缩略图拼图当作最终验证。
|
||||
6. 修复重叠、裁切、断行、低对比度、空占位符和数据不一致后再交付。
|
||||
|
||||
## 设计底线
|
||||
|
||||
- 无模板时,标题页标题通常不小于 40pt,普通页标题不小于 28pt,正文通常不小于 16pt。
|
||||
- 内容过多时先删减或拆页,不优先缩小字体。
|
||||
- 图表必须标注单位、时间范围和来源;不要用装饰图替代证据。
|
||||
- 不复制 OpenAI 官方 Presentations 插件的模板、脚本、素材或内部文档。
|
||||
- 图片生成/搜索能力不可用时,使用用户素材、可授权本地素材或清晰的占位说明,不伪造已生成图片。
|
||||
- 交付前按 [quality-checks.md](references/quality-checks.md) 检查。
|
||||
|
||||
## 输出
|
||||
|
||||
最终只链接用户要求的 PPTX,不主动附带 builder、逐页 PNG、拼图或检查日志。
|
||||
@@ -0,0 +1,7 @@
|
||||
interface:
|
||||
display_name: "CJYSA 演示文稿"
|
||||
short_description: "创建、编辑、检查并验证专业 PowerPoint 演示文稿"
|
||||
default_prompt: "Use $build-presentations to create or edit a clear PowerPoint deck."
|
||||
|
||||
policy:
|
||||
allow_implicit_invocation: true
|
||||
@@ -0,0 +1,9 @@
|
||||
# PPTX 交付检查
|
||||
|
||||
- 故事线完整,每页有明确结论或作用。
|
||||
- 无空白页、未替换占位符、重复标题或内部制作说明。
|
||||
- 所有对象位于画布内,无非预期重叠和裁切。
|
||||
- 标题不意外换行,正文最小字号仍可阅读。
|
||||
- 图表数据、单位、时间范围和来源一致。
|
||||
- 逐页全尺寸检查;拼图只用于整体节奏检查。
|
||||
- 若无法渲染,明确说明只通过结构和边界检查。
|
||||
@@ -0,0 +1,99 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Original PPTX helper for structural inspection and common QA warnings."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from pptx import Presentation
|
||||
from pptx.enum.shapes import MSO_SHAPE_TYPE
|
||||
from pptx.util import Pt
|
||||
|
||||
|
||||
def shape_text(shape) -> str:
|
||||
if not getattr(shape, "has_text_frame", False):
|
||||
return ""
|
||||
return "\n".join(paragraph.text for paragraph in shape.text_frame.paragraphs).strip()
|
||||
|
||||
|
||||
def inspect(path: Path) -> None:
|
||||
deck = Presentation(path)
|
||||
slides = []
|
||||
for index, slide in enumerate(deck.slides, start=1):
|
||||
texts = [shape_text(shape) for shape in slide.shapes]
|
||||
nonempty_texts = [text for text in texts if text]
|
||||
slides.append(
|
||||
{
|
||||
"slide": index,
|
||||
"shapes": len(slide.shapes),
|
||||
"text_shapes": len(nonempty_texts),
|
||||
"images": sum(1 for shape in slide.shapes if shape.shape_type == MSO_SHAPE_TYPE.PICTURE),
|
||||
"title": (slide.shapes.title.text.strip() if slide.shapes.title and slide.shapes.title.text else None),
|
||||
"text_preview": " | ".join(nonempty_texts)[:300],
|
||||
}
|
||||
)
|
||||
report = {
|
||||
"file": str(path.resolve()),
|
||||
"slides": len(deck.slides),
|
||||
"width_inches": round(deck.slide_width / 914400, 3),
|
||||
"height_inches": round(deck.slide_height / 914400, 3),
|
||||
"slide_details": slides,
|
||||
}
|
||||
print(json.dumps(report, ensure_ascii=False, indent=2))
|
||||
|
||||
|
||||
def validate(path: Path, minimum_font_pt: float) -> None:
|
||||
deck = Presentation(path)
|
||||
warnings = []
|
||||
for slide_index, slide in enumerate(deck.slides, start=1):
|
||||
meaningful = 0
|
||||
for shape_index, shape in enumerate(slide.shapes, start=1):
|
||||
text = shape_text(shape)
|
||||
if text or shape.shape_type == MSO_SHAPE_TYPE.PICTURE:
|
||||
meaningful += 1
|
||||
if shape.left < 0 or shape.top < 0 or shape.left + shape.width > deck.slide_width or shape.top + shape.height > deck.slide_height:
|
||||
warnings.append({"slide": slide_index, "shape": shape_index, "kind": "outside_slide"})
|
||||
if getattr(shape, "has_text_frame", False):
|
||||
for paragraph in shape.text_frame.paragraphs:
|
||||
for run in paragraph.runs:
|
||||
if run.font.size and run.font.size < Pt(minimum_font_pt):
|
||||
warnings.append(
|
||||
{
|
||||
"slide": slide_index,
|
||||
"shape": shape_index,
|
||||
"kind": "small_font",
|
||||
"font_pt": round(run.font.size.pt, 1),
|
||||
"text": run.text[:80],
|
||||
}
|
||||
)
|
||||
if meaningful == 0:
|
||||
warnings.append({"slide": slide_index, "kind": "blank_slide"})
|
||||
title = slide.shapes.title
|
||||
if title and title.has_text_frame and "\n" in title.text.strip():
|
||||
warnings.append({"slide": slide_index, "kind": "multiline_title", "text": title.text.strip()[:160]})
|
||||
print(json.dumps({"file": str(path.resolve()), "warnings": warnings, "count": len(warnings)}, ensure_ascii=False, indent=2))
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser()
|
||||
sub = parser.add_subparsers(dest="command", required=True)
|
||||
inspect_cmd = sub.add_parser("inspect")
|
||||
inspect_cmd.add_argument("input", type=Path)
|
||||
validate_cmd = sub.add_parser("validate")
|
||||
validate_cmd.add_argument("input", type=Path)
|
||||
validate_cmd.add_argument("--minimum-font-pt", type=float, default=14.0)
|
||||
return parser
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = build_parser().parse_args()
|
||||
if args.command == "inspect":
|
||||
inspect(args.input)
|
||||
elif args.command == "validate":
|
||||
validate(args.input, args.minimum_font_pt)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user