Skill: xuyongliang-eccom/ppt-polish (version 1.0.0)

--- name: ppt-polish description: Rebuild, beautify, and optimize editable PowerPoint flowcharts, topology diagrams, architecture diagrams, and process visuals from existing PPT/PPTX files or source images. Use when the user asks to optimize a just-uploaded PPT, restyle a diagram slide, turn an image into an editable PPT diagram, improve presentation readability, unify shapes/colors/layout, or produce a cleaner client-ready diagram page. --- # PPT Polish Optimize diagram-heavy PPT pages so they become clearer, more editable, and more presentation-ready. Prefer rebuilding structure over cosmetic tweaks when the original slide is messy. The goal is not just “make it prettier”, but “make it readable, editable, and reusable”. ## Core workflow 1. Identify the source form - Existing editable PPT/PPTX slide - Screenshot/image of a diagram - Mixed case: image already placed inside a PPT 2. Classify the diagram - Flowchart / process map - System topology / architecture diagram - Org / hierarchy diagram - Dense infographic-like technical diagram 3. Preserve the information structure - Extract layers, groups, nodes, and major relationships first - Decide what is primary path vs supporting path - Do not start from colors or decoration 4. Rebuild or polish in PowerPoint - If the slide is already editable and structurally decent, adjust layout/style directly - If the slide is messy or image-based, reconstruct shapes/text/connectors instead of polishing the pasted image 5. Final quality pass - Improve readability for presentation - Ensure all important items remain editable - Reduce visual noise and accidental line crossings ## Decision rules ### When polishing an existing PPT Prefer direct editing when: - Text is already editable - Shapes are mostly separable - Layout is salvageable Prefer rebuilding on a new slide when: - The current slide is essentially a screenshot - Elements are badly misaligned or inconsistent - Connectors are too chaotic to repair efficiently - The user wants a more premium/client-facing result ### When starting from an image Use the image only as reference. Do not treat “insert image into PPT” as completion. A valid result should have editable: - titles - containers - nodes - text - connectors ## Layout heuristics ### Flowcharts - Keep a clear main direction: left→right or top→bottom - Put decision nodes at branch points - Keep similar node types same size and style - Reduce crossing lines, even if that means slightly changing spacing ### Topology / architecture diagrams - Group by layer or domain first - Put gateways / hubs / central schedulers near the center - Put external actors at the edge - Put data/storage below or on a dedicated side band - Put ops/monitoring in a side rail or bottom band ### Dense technical diagrams - Keep the density if that is part of the original style - Still enforce consistent spacing, headers, and color logic - Use container regions so the eye can parse clusters quickly ## Styling heuristics - Use 1 visual language per slide, not several mixed styles - Keep a consistent color family by layer/category - Use emphasis only for core nodes and main paths - Use lighter visual weight for supporting lines and less important modules - Prefer subtle rounded rectangles and clean borders over excessive effects - Avoid overusing gradients, shadows, and icons unless they help meaningfully ## Read bundled references when needed - Read `references/image-to-ppt-flowchart-sop.md` for image→editable-flowchart reconstruction workflow - Read `references/ppt-topology-from-image.md` for topology-specific decomposition and layer mapping ## Expected outputs Depending on the request, produce one or more of: - optimized `.pptx` file - rebuilt single-slide diagram page - alternate visual versions (e.g. dark, clean, infographic) - structure draft in Markdown / Mermaid before rebuilding ## Quality checklist Before finishing, verify: - The main information structure is preserved - Important text is editable - Shapes are editable rather than flattened into an image - Layers/groups are obvious at a glance - Alignment and spacing are consistent - Connector hierarchy is readable - The page is suitable for presentation, not just archival ## Notes When multiple output versions exist, keep filenames explicit, such as: - `*-refined.pptx` - `*-infographic.pptx` - `*-dark.pptx` - `*-polished.pptx` Prefer descriptive naming over generic names like `final-final2.pptx`.

path: references/image-to-ppt-flowchart-sop.md
content: # 图片转 PPT 流程图 SOP

目标：把一张包含流程图、架构图、步骤图的图片，转成 PowerPoint 中可编辑的流程图或结构图。

## 一、先判断图片类型

拿到图片后，先分 4 类处理：

1. 标准流程图
   - 大量方框、菱形、箭头、连接线
   - 适合直接重建

2. 组织/架构图
   - 树状、分层、分组明显
   - 适合先抽层级，再画图

3. 信息海报式流程图
   - 装饰元素多，流程只是其中一部分
   - 先保核心结构，再决定是否复刻视觉风格

4. 截图/低清图片/拍照图片
   - 先增强清晰度、裁切、矫正
   - 再做 OCR 和结构拆解

## 二、推荐工作流

### 路线 A：高保真人工复刻

适合：要交付客户、要高可编辑性、图比较复杂。

1. 观察整体布局
2. 拆节点
3. 拆连接关系
4. 拆视觉样式
5. 在 PPT 中重建
6. 收尾优化

### 路线 B：半自动重建

适合：批量图、内部使用、先快速出版本。

1. OCR 提取文字
2. 人工整理节点和连接关系
3. 输出中间结构（推荐 Markdown 列表 / Mermaid）
4. 按结构在 PPT 里快速重画

### 路线 C：自动化脚本辅助

适合：经常处理同类流程图，愿意投入一点搭建成本。

自动化通常不直接生成“完美 PPT”，而是先生成：
- 节点清单
- 流程关系表
- Mermaid / draw.io 草图
- PPT 绘制说明

## 三、实际操作 SOP

### 第 1 步：图片预处理

如果图片质量一般，先做：
- 裁掉无关边缘
- 拉正拍摄角度
- 提高清晰度和对比度
- 必要时放大 2x

### 第 2 步：提取文字

需要提取：
- 标题
- 每个节点里的主文案
- 分支条件
- 备注、图例、编号

### 第 3 步：识别结构

按“谁连到谁”整理成关系表：

```text
开始 -> 提交申请
提交申请 -> 审批
审批 -> 通过
审批 -> 驳回
```

如果有层级、泳道、分组，也要标出来。

### 第 4 步：在 PPT 中建骨架

推荐顺序：
1. 先定页面比例
2. 先放主节点
3. 再加次级节点
4. 再接连线
5. 最后补装饰元素

### 第 5 步：统一样式

统一：
- 形状大小
- 线条粗细
- 箭头样式
- 字体字号
- 圆角半径
- 填充色和边框色

### 第 6 步：检查可编辑性

检查是否满足：
- 每个框都能单独编辑
- 每段连线都能调整
- 文字不是一张图片贴上去
- 分支结构清楚

## 四、反推流程结构的办法

1. 先找主路径
2. 再找判断点
3. 再找分组
4. 最后找修饰元素

修饰元素应最后补，不要一开始被它们干扰。

## 五、常见难点

### 箭头交叉很多
- 先保主干
- 次级连接后补
- 必要时改为折线连接

### 图片很糊，OCR 不准
- 先放大、锐化、增强对比
- OCR 后人工校对
- 关键节点宁可人工录入

### 原图太花，像海报
- 先做结构复刻
- 第二版再补视觉风格

### 想一键转成 PPT
- 现实里很难一步到位
- 更合理的目标是：自动提取 60%-80% 结构 + 人工补齐


path: references/ppt-topology-from-image.md
content: # 图片生成可编辑 PPT 拓扑图拆解稿

目标：将源图片中的拓扑/结构信息，转换为 PowerPoint 中可编辑的拓扑图页面，而不是将原图直接贴入 PPT。

## 一、交付目标

交付结果应满足：

1. 可编辑
   - 节点、区域、文字、连线都能在 PPT 中单独修改
2. 结构清晰
   - 能明确表达层次、分组和连接关系
3. 适合演示
   - 页面适合汇报、讲解、二次修改

## 二、推荐页面标题

如无特殊要求，推荐使用：
- 系统拓扑架构图

## 三、页面结构建议

建议采用三层结构：

### 1. 顶层：接入 / 外部层
用于承载：
- 用户终端
- 外部系统
- 第三方平台
- Internet / 公网入口

### 2. 中层：核心业务 / 平台层
用于承载：
- 网关
- 业务平台
- 核心服务
- 管理后台
- 中间件服务

### 3. 底层：数据 / 基础设施层
用于承载：
- 数据库
- 缓存
- 文件存储
- 消息队列
- 日志 / 监控

必要时可额外拆出运维管理区。

## 四、可编辑元素拆解规范

### A. 区域容器
建议使用：
- 圆角矩形
- 浅色或深色统一填充
- 细边框
- 左上角放区域标题

常见区域：
- 外部接入区
- 核心服务区
- 数据存储区
- 运维管理区

### B. 节点元素
建议映射：

- 平台/系统：圆角矩形
- 模块/服务：矩形或圆角矩形
- 数据库：数据库图标或圆柱形
- 云资源：云朵形状
- 网络设备：图标 + 文本框
- 终端：图标 + 文本框

### C. 连线规范
- 主流程 / 主链路：深色实线箭头
- 辅链路：虚线箭头
- 双向交互：双向箭头
- 弱依赖 / 说明性关系：细灰线

## 五、建议绘制顺序

1. 先搭区域框
2. 再放核心节点
3. 再补辅助节点
4. 先连主链路
5. 再补支线和说明
6. 最后统一样式

## 六、汇报型样式建议

- 页面比例：16:9
- 背景：白色 / 极浅灰 / 深色商务风三选一，整页保持统一
- 标题居上
- 中间为主拓扑图
- 下方可加图例

### 配色建议
- 区域框：浅蓝 / 浅灰 / 深色半透明
- 核心节点：蓝色系
- 数据节点：绿色、青色或深灰
- 外部系统：橙色或中性灰
- 运维节点：紫色或灰蓝色

### 文本建议
- 区域名：16-20 pt
- 节点名：12-16 pt
- 备注：10-12 pt

### 对齐建议
- 同层节点尽量等宽等高
- 使用“对齐”和“平均分布”
- 尽量减少线条交叉

## 七、关系表模板

```text
外部用户 -> 统一入口
管理端 -> 管理后台
第三方平台 -> API 网关
统一入口 -> 业务服务A
统一入口 -> 业务服务B
业务服务A -> 数据库
业务服务A -> Redis
业务服务B -> 文件存储
运维平台 -> 业务服务A
运维平台 -> 日志系统
```

## 八、交付原则

优先保证：
1. 信息结构正确
2. 演示可读性强
3. 元素可编辑

视觉高保真是加分项，但不应牺牲可编辑性与清晰度。


path: scripts/rebuild_topology_slide.py
content: #!/usr/bin/env python3
"""Create a polished rebuilt copy of a single-slide PPT topology/diagram deck.

This is intentionally heuristic-driven rather than template-perfect.
It is useful when a user asks to optimize an already-generated PPT diagram
into a cleaner presentation slide.
"""

from __future__ import annotations

import argparse
from pathlib import Path
from zipfile import ZipFile
import re

from pptx import Presentation
from pptx.dml.color import RGBColor
from pptx.enum.shapes import MSO_AUTO_SHAPE_TYPE, MSO_CONNECTOR
from pptx.enum.text import MSO_VERTICAL_ANCHOR, PP_ALIGN
from pptx.util import Inches, Pt


TITLE_FILL = (31, 45, 78)
TITLE_LINE = (82, 112, 188)
BG = (13, 20, 36)
LAYER_STYLES = {
    "external": ((23, 31, 54), (79, 104, 168)),
    "core": ((29, 63, 84), (73, 160, 186)),
    "data": ((56, 38, 80), (160, 109, 205)),
    "ops": ((98, 63, 38), (211, 145, 91)),
}


def extract_texts(pptx_path: Path) -> list[str]:
    texts: list[str] = []
    with ZipFile(pptx_path) as zf:
        slide_names = sorted(
            n for n in zf.namelist() if n.startswith("ppt/slides/slide") and n.endswith(".xml")
        )
        if not slide_names:
            return texts
        data = zf.read(slide_names[0]).decode("utf-8", "ignore")
        texts = re.findall(r"<a:t>(.*?)</a:t>", data)
    return [t.strip() for t in texts if t.strip()]


def classify_items(texts: list[str]) -> dict[str, list[str]]:
    buckets = {"external": [], "core": [], "data": [], "ops": []}
    for t in texts:
        if t in {"外部接入层", "核心平台层", "数据资源层", "运维保障", "图例"}:
            continue
        if any(k in t for k in ["MySQL", "Redis", "文件存储", "MQ", "日志", "ES", "BI"]):
            buckets["data"].append(t)
        elif any(k in t for k in ["监控", "备份", "运维", "巡检", "告警", "报表"]):
            buckets["ops"].append(t)
        elif any(k in t for k in ["用户", "第三方", "公网", "Internet", "门户", "移动", "管理端", "外部"]):
            buckets["external"].append(t)
        else:
            buckets["core"].append(t)
    for k in buckets:
        dedup = []
        seen = set()
        for item in buckets[k]:
            if item not in seen:
                seen.add(item)
                dedup.append(item)
        buckets[k] = dedup
    return buckets


def add_box(slide, x, y, w, h, text, fill, line, font_size=12, bold=False):
    shape = slide.shapes.add_shape(MSO_AUTO_SHAPE_TYPE.ROUNDED_RECTANGLE, x, y, w, h)
    shape.fill.solid()
    shape.fill.fore_color.rgb = RGBColor(*fill)
    shape.line.color.rgb = RGBColor(*line)
    shape.line.width = Pt(1.1)
    tf = shape.text_frame
    tf.clear()
    tf.vertical_anchor = MSO_VERTICAL_ANCHOR.MIDDLE
    tf.margin_left = tf.margin_right = Inches(0.05)
    tf.margin_top = tf.margin_bottom = Inches(0.03)
    p = tf.paragraphs[0]
    p.alignment = PP_ALIGN.CENTER
    r = p.add_run()
    r.text = text
    r.font.size = Pt(font_size)
    r.font.bold = bold
    r.font.name = "Microsoft YaHei"
    r.font.color.rgb = RGBColor(240, 245, 255)
    return shape


def add_line(slide, x1, y1, x2, y2, color, width=1.5):
    c = slide.shapes.add_connector(MSO_CONNECTOR.STRAIGHT, x1, y1, x2, y2)
    c.line.color.rgb = RGBColor(*color)
    c.line.width = Pt(width)
    return c


def build_slide(texts: list[str], output: Path, title: str):
    prs = Presentation()
    prs.slide_width = Inches(13.333)
    prs.slide_height = Inches(7.5)
    slide = prs.slides.add_slide(prs.slide_layouts[6])

    bg = slide.background.fill
    bg.solid()
    bg.fore_color.rgb = RGBColor(*BG)

    add_box(slide, Inches(0.35), Inches(0.22), Inches(4.2), Inches(0.55), title, TITLE_FILL, TITLE_LINE, 24, True)
    add_box(
        slide,
        Inches(4.8),
        Inches(0.24),
        Inches(5.2),
        Inches(0.46),
        "自动优化版：层次归一 / 连线降噪 / 更适合汇报",
        (20, 29, 51),
        (58, 83, 143),
        11,
        False,
    )

    headers = [
        ("外部接入层", 0.45, 1.02, 3.0, 0.40, (38, 56, 96), (91, 122, 194)),
        ("核心平台层", 3.72, 1.02, 5.95, 0.40, (28, 70, 87), (66, 156, 179)),
        ("数据资源层", 10.0, 1.02, 1.45, 0.40, (73, 51, 97), (154, 101, 198)),
        ("运维保障", 11.65, 1.02, 1.2, 0.40, (90, 58, 36), (194, 129, 78)),
    ]
    for text, x, y, w, h, fill, line in headers:
        add_box(slide, Inches(x), Inches(y), Inches(w), Inches(h), text, fill, line, 14, True)

    buckets = classify_items(texts)

    # external
    ext_x_positions = [0.55, 1.52, 2.49, 0.55, 1.95, 0.55, 1.62]
    ext_y_positions = [1.72, 1.72, 1.72, 2.46, 2.46, 3.20, 3.20]
    ext_widths = [0.84, 0.84, 0.95, 1.28, 1.38, 0.95, 0.82]
    external = buckets["external"][:7] or ["用户终端", "管理端", "第三方平台", "Internet / 公网", "外部业务系统", "移动应用", "门户"]
    external_shapes = []
    for i, t in enumerate(external):
        s = add_box(
            slide,
            Inches(ext_x_positions[i]),
            Inches(ext_y_positions[i]),
            Inches(ext_widths[i]),
            Inches(0.56),
            t,
            *LAYER_STYLES["external"],
            font_size=11,
        )
        external_shapes.append(s)

    gateway = add_box(slide, Inches(0.98), Inches(4.18), Inches(2.22), Inches(0.72), "统一入口 / API 网关 / 负载均衡", (45, 74, 140), (121, 162, 255), 14, True)

    # core area
    frame = slide.shapes.add_shape(MSO_AUTO_SHAPE_TYPE.ROUNDED_RECTANGLE, Inches(3.78), Inches(1.60), Inches(5.8), Inches(4.45))
    frame.fill.solid()
    frame.fill.fore_color.rgb = RGBColor(17, 31, 46)
    frame.line.color.rgb = RGBColor(49, 113, 135)
    frame.line.width = Pt(1.4)

    core_items = buckets["core"][:11] or [
        "认证服务", "应用服务 A", "应用服务 B", "应用服务 C", "应用服务 D", "管理后台", "支撑服务", "开放服务",
        "调度中心 / 服务编排", "消息总线 / 中间件", "规则引擎 / 任务中心",
    ]
    top_row = core_items[:4]
    mid_row = core_items[4:8]
    bottom_row = core_items[8:11]

    top_x = [4.05, 5.3, 6.6, 7.9]
    mid_x = [4.7, 6.0, 7.3, 8.55]
    bottom_x = [4.42, 6.34, 8.10]
    top_shapes = []
    for i, t in enumerate(top_row):
        top_shapes.append(add_box(slide, Inches(top_x[i]), Inches(1.88), Inches(1.08), Inches(0.60), t, *LAYER_STYLES["core"], font_size=12, bold=True))
    mid_shapes = []
    for i, t in enumerate(mid_row):
        mid_shapes.append(add_box(slide, Inches(mid_x[i]), Inches(2.72), Inches(1.02 if i < 3 else 0.88), Inches(0.60), t, *LAYER_STYLES["core"], font_size=12))
    bottom_shapes = []
    for i, t in enumerate(bottom_row):
        bottom_shapes.append(add_box(slide, Inches(bottom_x[i]), Inches(3.72), Inches(1.68 if i == 0 else 1.54 if i == 1 else 1.2), Inches(0.62), t, (24, 88, 103), (79, 192, 198), font_size=11, bold=True))

    # data area
    data_items = buckets["data"][:7] or ["MySQL", "Redis", "文件存储", "MQ", "日志系统", "ES", "BI"]
    data_shapes = []
    data_y = [1.82, 2.56, 3.30, 4.04, 4.78, 5.52, 5.52]
    data_x = [10.12, 10.12, 10.12, 10.12, 10.12, 10.12, 10.88]
    data_w = [0.92, 0.92, 1.02, 0.82, 0.95, 0.72, 0.62]
    for i, t in enumerate(data_items):
        data_shapes.append(add_box(slide, Inches(data_x[i]), Inches(data_y[i]), Inches(data_w[i]), Inches(0.52), t, *LAYER_STYLES["data"], font_size=11))

    # ops area
    ops_items = buckets["ops"][:3] or ["监控平台", "备份 / 运维工具", "巡检 / 告警 / 报表"]
    ops_shapes = []
    ops_specs = [(11.8, 1.82, 0.85), (11.72, 2.72, 1.02), (11.68, 3.92, 1.08)]
    for i, t in enumerate(ops_items):
        x, y, w = ops_specs[i]
        ops_shapes.append(add_box(slide, Inches(x), Inches(y), Inches(w), Inches(0.62), t, *LAYER_STYLES["ops"], font_size=11))

    for s in external_shapes:
        add_line(slide, s.left + s.width / 2, s.top + s.height, gateway.left + gateway.width / 2, gateway.top, (121, 162, 255), 1.4)

    for s in top_shapes + mid_shapes:
        add_line(slide, gateway.left + gateway.width, gateway.top + gateway.height / 2, s.left, s.top + s.height / 2, (90, 172, 198), 1.2)

    for s in top_shapes + mid_shapes:
        add_line(slide, s.left + s.width / 2, s.top + s.height, s.left + s.width / 2, Inches(3.72), (88, 183, 200), 0.9)

    for s in bottom_shapes:
        add_line(slide, s.left + s.width, s.top + s.height / 2, Inches(10.0), s.top + s.height / 2, (165, 121, 222), 1.0)

    for s in ops_shapes:
        add_line(slide, Inches(9.3), Inches(4.03), s.left, s.top + s.height / 2, (212, 151, 86), 1.0)

    note = slide.shapes.add_textbox(Inches(4.20), Inches(6.45), Inches(8.6), Inches(0.4))
    p = note.text_frame.paragraphs[0]
    p.alignment = PP_ALIGN.LEFT
    r = p.add_run()
    r.text = "说明：本页由规则化重构生成，目标是提升结构清晰度、层次感和汇报可读性，同时保留原始图中的核心信息。"
    r.font.name = "Microsoft YaHei"
    r.font.size = Pt(11)
    r.font.color.rgb = RGBColor(194, 205, 229)

    output.parent.mkdir(parents=True, exist_ok=True)
    prs.save(output)


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("input", help="Source PPTX file")
    parser.add_argument("-o", "--output", required=True, help="Output PPTX file")
    parser.add_argument("--title", default="系统拓扑架构图", help="Slide title")
    args = parser.parse_args()

    input_path = Path(args.input)
    output_path = Path(args.output)
    texts = extract_texts(input_path)
    build_slide(texts, output_path, args.title)
    print(output_path)


if __name__ == "__main__":
    main()


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)