Skill: ksfrist/ppt-light-corporate (version 1.1.1)

--- name: ppt-light-corporate version: 1.1.0 description: | Generate polished corporate PPTs using a light-themed company template. Activate when: (1) User asks to create/make/generate a PPT/PowerPoint/幻灯片/演示文稿 (2) User provides content and wants it turned into slides (3) User asks to improve or redo a PPT Uses a pre-analyzed H3C-style light corporate template (white bg + China red accent). NOT for: dark theme PPTs, creative/artistic decks, or Keynote files. --- # PPT Light Corporate Skill 基于 python-pptx 的企业PPT自动生成引擎。H3C浅色企业模板，白底+中国红点缀，16:9画布。 ## 核心文件 | 文件 | 作用 | |------|------| | `assets/template-light.pptx` | 模板文件（10" × 5.625"，9种Layout，5个母版） | | `assets/icons-library.pptx` | 279个简洁矢量图标（设备相关） | | `assets/icon-index.json` | 图标语义索引（中英文标签+场景分类） | | `scripts/ppt_helpers.py` | 核心生成引擎（24种内容布局 + 工具方法） | | `scripts/ppt_qa.py` | 自动QA审校（9项检查） | | `scripts/ppt_scorer.py` | 评分系统（逐页6维 + 整体4维 + 学习循环） | | `scripts/icon_library.py` | 图标搜索/复制/变色引擎 | ## Quick Start ```python import sys sys.path.insert(0, '<skill_dir>/scripts') from ppt_helpers import TemplateBuilder tb = TemplateBuilder() s = tb.add_cover_slide() tb.text(s, 0.8, 1.5, 8.4, 0.6, '标题', sz=32, color=tb.RED2, bold=True) s = tb.add_content_slide('页面标题') tb.three_col_icon_cards(s, [ {'icon_text': '', 'title': '功能一', 'body': '说明'}, {'icon_text': '', 'title': '功能二', 'body': '说明'}, {'icon_text': '', 'title': '功能三', 'body': '说明'}, ]) # 放置白色矢量图标到红色圆圈上（自动检测圆圈位置） def find_circles(slide): circles = [] for shape in slide.shapes: if 'Oval' in shape.name: x, y = shape.left / 914400, shape.top / 914400 w, h = shape.width / 914400, shape.height / 914400 circles.append({'cx': x + w/2, 'cy': y + h/2, 'w': w}) return circles for circle, query in zip(find_circles(s), ['server', 'wifi', 'database']): sz = circle['w'] * 0.55 tb.place_icon(s, query, circle['cx'] - sz/2, circle['cy'] - sz/2, size=sz, color='FFFFFF') tb.save('output.pptx') ``` ## 设计系统 ### 色板 | 名称 | 色值 | 用途 | |------|------|------| | RED | #C00000 | 页标题、强调 | | RED2 | #EA0000 | 封面标题 | | RED3 | #D8222A | 小节标题 | | DARK | #343434 | 正文 | | MID | #4B4948 | 次要文字 | | LIGHT | #A0A0A0 | 辅助/淡化 | | BG | #F1F1F1 | 卡片背景 | | BORDER | #E7E7E7 | 边框/分隔线 | | ACCENT_GREY | #6B6B6B | 交替强调（替代蓝色） | ### 配色法则 - 60% 白色(背景) / 30% 灰黑(正文) / 10% 红色(强调) - 颜色只从色板里选，不引入新颜色 ### 字体规范 - 字体：`微软雅黑`（latin + ea 同时设置） - 页标题：20pt 粗体 RED - 小节标题：12-14pt 粗体 RED/RED3 - 正文：10-11pt DARK/MID - **最小字号：10pt**（确保投屏可读） ### 画布规则 - 画布：10" × 5.625"（16:9） - 安全边距：≥ 0.4" - 内容区起点：y = 1.0"（标题下方） - 所有 body 文本启用 `shrink=True`（自动缩字防溢出） - 多栏布局自动水平居中：`start_x = (10 - total_width) / 2` ## 24种内容布局 ### 两栏/两行（5种） | # | 方法 | 适用场景 | |---|------|----------| | 1 | `two_col_numbered_cards` | A/B对比 | | 2 | `two_row_numbered_bars` | 两要点堆叠 | | 3 | `two_row_label_tabs` | 分类标签+详情 | | 4 | `two_col_circle_cards` | 人物/角色 | | 5 | `left_text_right_stacked_cards` | 论点+论据（支持2-3张卡片自适应） | ### 三栏/三行（5种） | # | 方法 | 适用场景 | |---|------|----------| | 6 | `three_col_vertical_cards` | 三个并列特性 | | 7 | `three_row_numbered_bars` | 三步编号 | | 8 | `three_row_label_bars` | 三分类标签 | | 9 | `three_col_icon_cards` | 图标+标题+说明 | | 10 | `three_col_numbered_card_blocks` | 编号灰卡半嵌入 | ### 四栏/四行（6种） | # | 方法 | 适用场景 | |---|------|----------| | 11 | `four_col_tall_cards` | 四个高卡片（标题高度自适应） | | 12 | `four_row_badge_bars` | 四行徽章条 | | 13 | `four_col_ascending_steps` | 四步阶梯递进 | | 14 | `four_col_red_base_cards` | 红底白卡+圆环 | | 15 | `four_col_numbered_dividers` | 竖分隔线四栏 | | 16 | `four_col_parallelogram` | 倾斜平行四边形 | ### 时间轴/流程（5种） | # | 方法 | 适用场景 | |---|------|----------| | 17 | `timeline_horizontal_ascending` | 水平时间轴+箭头 | | 18 | `timeline_icon_flow` | 圆形图标流程（红/灰交替） | | 19 | `timeline_zigzag` | **上下交替时间线**（3-5步） | | 20 | `process_flow_arrows` | 流程箭头 | | 21 | `process_three_stage` | 三阶段里程碑 | ### 特殊布局（3种） | # | 方法 | 适用场景 | |---|------|----------| | 22 | `triple_overlap_circles` | 三圆交叠（倒三角） | | 23 | `diamond_quadrants` | 菱形四象限 | | 24 | `four_panel_on_platform` | 椭圆基座四面板 | ## 矢量图标系统 279个简洁矢量图标，支持搜索、复制和变色。 ### 图标调用 ```python # 搜索并放置（自动变白色） tb.place_icon(slide, 'server', x=2.0, y=1.5, size=0.5, color='FFFFFF') # 搜索关键词（中英文均可） results = tb.icons.search('数据库', limit=3) ``` ### 图标使用原则 - 红色/灰色圆圈上放白色图标（`color='FFFFFF'`） - 图标占圆圈直径的 55% 最佳 - 使用 `find_circles()` 自动检测圆圈位置，确保居中 - 每页最多4个图标 - 大小 0.4-0.8" ### 分类 | 分类 | 数量 | |------|------| | 电脑 | 50 | | 硬件 | 36 | | 网络 | 32 | | 服务器 | 25 | | 数据库 | 20 | | 设备 | 12 | | 组合 | 63 | ## 制作流程 ``` 需求理解 → 大纲(等确认) → 生成 → QA审校(修到0) → 评分 → 发送 → 迭代 ``` ### 关键规则 1. **先出大纲，等用户确认再生成** 2. 相邻页布局不能重复，同一布局全篇最多2次 3. 每页 60-220 字，填充率 40-75% 4. 红色只用于标题和重点强调，交替配色用红/灰 5. 所有 body 文本必须有 `shrink=True` 6. 多行标题自动调整高度，避免文字与装饰线重叠 7. **修改模板前必须先备份原文件** 8. 所有反馈记录到 `memory/ppt-feedback.md` ## v1.1.0 更新内容 - **新增布局**: `timeline_zigzag`（上下交替时间线） - **图标系统重建**: 279个简洁矢量图标，支持搜索/复制/变色(copy_icon color参数) - **图标居中机制**: bounding box 居中算法，配合 find_circles 自动检测 - **边距对称修复**: 多栏布局自动水平居中 - **标题高度自适应**: 多行标题自动扩大，下方元素跟随下移 - **字体统一**: text()/multi_text() 同时设置 latin + ea 为微软雅黑 - **最小字号**: 全局 10pt（从 8pt/9pt 升级）

path: assets/icon-index.json
content: [
  {
    "name": "cubes_51852",
    "type": "freeform",
    "shape_idx": 1,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "数据块",
      "模块",
      "集群"
    ],
    "tags_en": [
      "cubes",
      "cluster",
      "module"
    ],
    "category": "服务器"
  },
  {
    "name": "vintage-web-cam_76843",
    "type": "freeform",
    "shape_idx": 2,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "摄像头",
      "监控",
      "安防"
    ],
    "tags_en": [
      "camera",
      "surveillance",
      "security"
    ],
    "category": "设备"
  },
  {
    "name": "surveillance-camera_62957",
    "type": "freeform",
    "shape_idx": 3,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "摄像头",
      "监控",
      "安防"
    ],
    "tags_en": [
      "camera",
      "surveillance",
      "security"
    ],
    "category": "设备"
  },
  {
    "name": "iconfont-10459-4813181",
    "type": "freeform",
    "shape_idx": 4,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "图标",
      "设备"
    ],
    "tags_en": [
      "icon",
      "device"
    ],
    "category": "未标注"
  },
  {
    "name": "solar-panels_62953",
    "type": "freeform",
    "shape_idx": 5,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "太阳能",
      "新能源",
      "光伏"
    ],
    "tags_en": [
      "solar",
      "energy",
      "green"
    ],
    "category": "设备"
  },
  {
    "name": "server_161984",
    "type": "freeform",
    "shape_idx": 6,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "服务器",
      "数据中心",
      "云计算"
    ],
    "tags_en": [
      "server",
      "datacenter",
      "cloud"
    ],
    "category": "服务器"
  },
  {
    "name": "servers_141215",
    "type": "freeform",
    "shape_idx": 7,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "服务器",
      "数据中心",
      "云计算"
    ],
    "tags_en": [
      "server",
      "datacenter",
      "cloud"
    ],
    "category": "服务器"
  },
  {
    "name": "puzzle_208761",
    "type": "freeform",
    "shape_idx": 8,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "拼图",
      "模块",
      "集成"
    ],
    "tags_en": [
      "puzzle",
      "module",
      "integration"
    ],
    "category": "通用"
  },
  {
    "name": "graphics-card_100310",
    "type": "freeform",
    "shape_idx": 9,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "显卡",
      "GPU",
      "图形处理"
    ],
    "tags_en": [
      "gpu",
      "graphics",
      "video-card"
    ],
    "category": "硬件"
  },
  {
    "name": "computers_36035",
    "type": "freeform",
    "shape_idx": 10,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "台式电脑",
      "显示器",
      "工作站"
    ],
    "tags_en": [
      "computer",
      "desktop",
      "monitor"
    ],
    "category": "电脑"
  },
  {
    "name": "wifi_155685",
    "type": "freeform",
    "shape_idx": 11,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "无线网络",
      "WiFi",
      "信号"
    ],
    "tags_en": [
      "wifi",
      "wireless",
      "signal"
    ],
    "category": "网络"
  },
  {
    "name": "iconfont-1044-807853",
    "type": "freeform",
    "shape_idx": 12,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "图标",
      "设备"
    ],
    "tags_en": [
      "icon",
      "device"
    ],
    "category": "未标注"
  },
  {
    "name": "iconfont-11843-5650915",
    "type": "freeform",
    "shape_idx": 13,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "图标",
      "设备"
    ],
    "tags_en": [
      "icon",
      "device"
    ],
    "category": "未标注"
  },
  {
    "name": "iconfont-11464-5383814",
    "type": "freeform",
    "shape_idx": 14,
    "colors": [
      "F84D4D"
    ],
    "tags_cn": [
      "图标",
      "设备"
    ],
    "tags_en": [
      "icon",
      "device"
    ],
    "category": "未标注"
  },
  {
    "name": "mouse_122218",
    "type": "freeform",
    "shape_idx": 15,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "鼠标",
      "外设",
      "输入"
    ],
    "tags_en": [
      "mouse",
      "input",
      "peripheral"
    ],
    "category": "设备"
  },
  {
    "name": "virtual-reality_207921",
    "type": "freeform",
    "shape_idx": 16,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "VR",
      "虚拟现实",
      "头显"
    ],
    "tags_en": [
      "vr",
      "virtual-reality",
      "headset"
    ],
    "category": "设备"
  },
  {
    "name": "virtual-reality_207914",
    "type": "freeform",
    "shape_idx": 17,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "VR",
      "虚拟现实",
      "头显"
    ],
    "tags_en": [
      "vr",
      "virtual-reality",
      "headset"
    ],
    "category": "设备"
  },
  {
    "name": "iconfont-10263-5041652",
    "type": "freeform",
    "shape_idx": 18,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "图标",
      "设备"
    ],
    "tags_en": [
      "icon",
      "device"
    ],
    "category": "未标注"
  },
  {
    "name": "heavy-machinery-truck_63495",
    "type": "freeform",
    "shape_idx": 19,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "卡车",
      "机械",
      "运输"
    ],
    "tags_en": [
      "truck",
      "machinery",
      "transport"
    ],
    "category": "设备"
  },
  {
    "name": "databases-symbol-variant-for-interface_30673",
    "type": "freeform",
    "shape_idx": 20,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "数据库",
      "存储",
      "数据"
    ],
    "tags_en": [
      "database",
      "storage",
      "data"
    ],
    "category": "数据库"
  },
  {
    "name": "stethoscope_291309",
    "type": "freeform",
    "shape_idx": 21,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "医疗",
      "健康",
      "急救"
    ],
    "tags_en": [
      "medical",
      "health",
      "hospital"
    ],
    "category": "设备"
  },
  {
    "name": "first-aid-kit_230218",
    "type": "freeform",
    "shape_idx": 22,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "医疗",
      "健康",
      "急救"
    ],
    "tags_en": [
      "medical",
      "health",
      "hospital"
    ],
    "category": "设备"
  },
  {
    "name": "cupboard_87908",
    "type": "freeform",
    "shape_idx": 23,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "机柜",
      "设备柜",
      "机房"
    ],
    "tags_en": [
      "cabinet",
      "rack",
      "datacenter"
    ],
    "category": "设备"
  },
  {
    "name": "server_151098",
    "type": "freeform",
    "shape_idx": 24,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "服务器",
      "数据中心",
      "云计算"
    ],
    "tags_en": [
      "server",
      "datacenter",
      "cloud"
    ],
    "category": "服务器"
  },
  {
    "name": "server_151095",
    "type": "freeform",
    "shape_idx": 25,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "服务器",
      "数据中心",
      "云计算"
    ],
    "tags_en": [
      "server",
      "datacenter",
      "cloud"
    ],
    "category": "服务器"
  },
  {
    "name": "server_151104",
    "type": "freeform",
    "shape_idx": 26,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "服务器",
      "数据中心",
      "云计算"
    ],
    "tags_en": [
      "server",
      "datacenter",
      "cloud"
    ],
    "category": "服务器"
  },
  {
    "name": "server_151112",
    "type": "freeform",
    "shape_idx": 27,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "服务器",
      "数据中心",
      "云计算"
    ],
    "tags_en": [
      "server",
      "datacenter",
      "cloud"
    ],
    "category": "服务器"
  },
  {
    "name": "server_151106",
    "type": "freeform",
    "shape_idx": 28,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "服务器",
      "数据中心",
      "云计算"
    ],
    "tags_en": [
      "server",
      "datacenter",
      "cloud"
    ],
    "category": "服务器"
  },
  {
    "name": "server_151114",
    "type": "freeform",
    "shape_idx": 29,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "服务器",
      "数据中心",
      "云计算"
    ],
    "tags_en": [
      "server",
      "datacenter",
      "cloud"
    ],
    "category": "服务器"
  },
  {
    "name": "server_151105",
    "type": "freeform",
    "shape_idx": 30,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "服务器",
      "数据中心",
      "云计算"
    ],
    "tags_en": [
      "server",
      "datacenter",
      "cloud"
    ],
    "category": "服务器"
  },
  {
    "name": "server_151110",
    "type": "freeform",
    "shape_idx": 31,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "服务器",
      "数据中心",
      "云计算"
    ],
    "tags_en": [
      "server",
      "datacenter",
      "cloud"
    ],
    "category": "服务器"
  },
  {
    "name": "server_151111",
    "type": "freeform",
    "shape_idx": 32,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "服务器",
      "数据中心",
      "云计算"
    ],
    "tags_en": [
      "server",
      "datacenter",
      "cloud"
    ],
    "category": "服务器"
  },
  {
    "name": "server_151096",
    "type": "freeform",
    "shape_idx": 33,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "服务器",
      "数据中心",
      "云计算"
    ],
    "tags_en": [
      "server",
      "datacenter",
      "cloud"
    ],
    "category": "服务器"
  },
  {
    "name": "server_151107",
    "type": "freeform",
    "shape_idx": 34,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "服务器",
      "数据中心",
      "云计算"
    ],
    "tags_en": [
      "server",
      "datacenter",
      "cloud"
    ],
    "category": "服务器"
  },
  {
    "name": "server_151094",
    "type": "freeform",
    "shape_idx": 35,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "服务器",
      "数据中心",
      "云计算"
    ],
    "tags_en": [
      "server",
      "datacenter",
      "cloud"
    ],
    "category": "服务器"
  },
  {
    "name": "server_151099",
    "type": "freeform",
    "shape_idx": 36,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "服务器",
      "数据中心",
      "云计算"
    ],
    "tags_en": [
      "server",
      "datacenter",
      "cloud"
    ],
    "category": "服务器"
  },
  {
    "name": "server_151093",
    "type": "freeform",
    "shape_idx": 37,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "服务器",
      "数据中心",
      "云计算"
    ],
    "tags_en": [
      "server",
      "datacenter",
      "cloud"
    ],
    "category": "服务器"
  },
  {
    "name": "server_151101",
    "type": "freeform",
    "shape_idx": 38,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "服务器",
      "数据中心",
      "云计算"
    ],
    "tags_en": [
      "server",
      "datacenter",
      "cloud"
    ],
    "category": "服务器"
  },
  {
    "name": "server_151109",
    "type": "freeform",
    "shape_idx": 39,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "服务器",
      "数据中心",
      "云计算"
    ],
    "tags_en": [
      "server",
      "datacenter",
      "cloud"
    ],
    "category": "服务器"
  },
  {
    "name": "server_151108",
    "type": "freeform",
    "shape_idx": 40,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "服务器",
      "数据中心",
      "云计算"
    ],
    "tags_en": [
      "server",
      "datacenter",
      "cloud"
    ],
    "category": "服务器"
  },
  {
    "name": "server_151100",
    "type": "freeform",
    "shape_idx": 41,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "服务器",
      "数据中心",
      "云计算"
    ],
    "tags_en": [
      "server",
      "datacenter",
      "cloud"
    ],
    "category": "服务器"
  },
  {
    "name": "server_151103",
    "type": "freeform",
    "shape_idx": 42,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "服务器",
      "数据中心",
      "云计算"
    ],
    "tags_en": [
      "server",
      "datacenter",
      "cloud"
    ],
    "category": "服务器"
  },
  {
    "name": "server_151097",
    "type": "freeform",
    "shape_idx": 43,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "服务器",
      "数据中心",
      "云计算"
    ],
    "tags_en": [
      "server",
      "datacenter",
      "cloud"
    ],
    "category": "服务器"
  },
  {
    "name": "server_149744",
    "type": "freeform",
    "shape_idx": 44,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "服务器",
      "数据中心",
      "云计算"
    ],
    "tags_en": [
      "server",
      "datacenter",
      "cloud"
    ],
    "category": "服务器"
  },
  {
    "name": "laptop_159382",
    "type": "freeform",
    "shape_idx": 45,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "笔记本",
      "电脑",
      "终端"
    ],
    "tags_en": [
      "laptop",
      "computer",
      "terminal"
    ],
    "category": "电脑"
  },
  {
    "name": "laptop_159386",
    "type": "freeform",
    "shape_idx": 46,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "笔记本",
      "电脑",
      "终端"
    ],
    "tags_en": [
      "laptop",
      "computer",
      "terminal"
    ],
    "category": "电脑"
  },
  {
    "name": "laptop_159467",
    "type": "freeform",
    "shape_idx": 47,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "笔记本",
      "电脑",
      "终端"
    ],
    "tags_en": [
      "laptop",
      "computer",
      "terminal"
    ],
    "category": "电脑"
  },
  {
    "name": "laptop_159383",
    "type": "freeform",
    "shape_idx": 48,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "笔记本",
      "电脑",
      "终端"
    ],
    "tags_en": [
      "laptop",
      "computer",
      "terminal"
    ],
    "category": "电脑"
  },
  {
    "name": "laptop_159385",
    "type": "freeform",
    "shape_idx": 49,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "笔记本",
      "电脑",
      "终端"
    ],
    "tags_en": [
      "laptop",
      "computer",
      "terminal"
    ],
    "category": "电脑"
  },
  {
    "name": "laptop_159390",
    "type": "freeform",
    "shape_idx": 50,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "笔记本",
      "电脑",
      "终端"
    ],
    "tags_en": [
      "laptop",
      "computer",
      "terminal"
    ],
    "category": "电脑"
  },
  {
    "name": "laptop_159391",
    "type": "freeform",
    "shape_idx": 51,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "笔记本",
      "电脑",
      "终端"
    ],
    "tags_en": [
      "laptop",
      "computer",
      "terminal"
    ],
    "category": "电脑"
  },
  {
    "name": "laptop_159394",
    "type": "freeform",
    "shape_idx": 52,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "笔记本",
      "电脑",
      "终端"
    ],
    "tags_en": [
      "laptop",
      "computer",
      "terminal"
    ],
    "category": "电脑"
  },
  {
    "name": "laptop_159411",
    "type": "freeform",
    "shape_idx": 53,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "笔记本",
      "电脑",
      "终端"
    ],
    "tags_en": [
      "laptop",
      "computer",
      "terminal"
    ],
    "category": "电脑"
  },
  {
    "name": "laptop_159407",
    "type": "freeform",
    "shape_idx": 54,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "笔记本",
      "电脑",
      "终端"
    ],
    "tags_en": [
      "laptop",
      "computer",
      "terminal"
    ],
    "category": "电脑"
  },
  {
    "name": "laptop_159389",
    "type": "freeform",
    "shape_idx": 55,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "笔记本",
      "电脑",
      "终端"
    ],
    "tags_en": [
      "laptop",
      "computer",
      "terminal"
    ],
    "category": "电脑"
  },
  {
    "name": "laptop_159392",
    "type": "freeform",
    "shape_idx": 56,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "笔记本",
      "电脑",
      "终端"
    ],
    "tags_en": [
      "laptop",
      "computer",
      "terminal"
    ],
    "category": "电脑"
  },
  {
    "name": "laptop_159395",
    "type": "freeform",
    "shape_idx": 57,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "笔记本",
      "电脑",
      "终端"
    ],
    "tags_en": [
      "laptop",
      "computer",
      "terminal"
    ],
    "category": "电脑"
  },
  {
    "name": "laptop_159409",
    "type": "freeform",
    "shape_idx": 58,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "笔记本",
      "电脑",
      "终端"
    ],
    "tags_en": [
      "laptop",
      "computer",
      "terminal"
    ],
    "category": "电脑"
  },
  {
    "name": "laptop_159413",
    "type": "freeform",
    "shape_idx": 59,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "笔记本",
      "电脑",
      "终端"
    ],
    "tags_en": [
      "laptop",
      "computer",
      "terminal"
    ],
    "category": "电脑"
  },
  {
    "name": "laptop_159387",
    "type": "freeform",
    "shape_idx": 60,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "笔记本",
      "电脑",
      "终端"
    ],
    "tags_en": [
      "laptop",
      "computer",
      "terminal"
    ],
    "category": "电脑"
  },
  {
    "name": "laptop_159412",
    "type": "freeform",
    "shape_idx": 61,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "笔记本",
      "电脑",
      "终端"
    ],
    "tags_en": [
      "laptop",
      "computer",
      "terminal"
    ],
    "category": "电脑"
  },
  {
    "name": "laptop_159408",
    "type": "freeform",
    "shape_idx": 62,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "笔记本",
      "电脑",
      "终端"
    ],
    "tags_en": [
      "laptop",
      "computer",
      "terminal"
    ],
    "category": "电脑"
  },
  {
    "name": "laptop_159388",
    "type": "freeform",
    "shape_idx": 63,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "笔记本",
      "电脑",
      "终端"
    ],
    "tags_en": [
      "laptop",
      "computer",
      "terminal"
    ],
    "category": "电脑"
  },
  {
    "name": "database_151072",
    "type": "freeform",
    "shape_idx": 64,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "数据库",
      "存储",
      "数据"
    ],
    "tags_en": [
      "database",
      "storage",
      "data"
    ],
    "category": "数据库"
  },
  {
    "name": "database_151083",
    "type": "freeform",
    "shape_idx": 65,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "数据库",
      "存储",
      "数据"
    ],
    "tags_en": [
      "database",
      "storage",
      "data"
    ],
    "category": "数据库"
  },
  {
    "name": "database_151076",
    "type": "freeform",
    "shape_idx": 66,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "数据库",
      "存储",
      "数据"
    ],
    "tags_en": [
      "database",
      "storage",
      "data"
    ],
    "category": "数据库"
  },
  {
    "name": "database_151073",
    "type": "freeform",
    "shape_idx": 67,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "数据库",
      "存储",
      "数据"
    ],
    "tags_en": [
      "database",
      "storage",
      "data"
    ],
    "category": "数据库"
  },
  {
    "name": "database_151089",
    "type": "freeform",
    "shape_idx": 68,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "数据库",
      "存储",
      "数据"
    ],
    "tags_en": [
      "database",
      "storage",
      "data"
    ],
    "category": "数据库"
  },
  {
    "name": "database_151075",
    "type": "freeform",
    "shape_idx": 69,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "数据库",
      "存储",
      "数据"
    ],
    "tags_en": [
      "database",
      "storage",
      "data"
    ],
    "category": "数据库"
  },
  {
    "name": "database_151086",
    "type": "freeform",
    "shape_idx": 70,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "数据库",
      "存储",
      "数据"
    ],
    "tags_en": [
      "database",
      "storage",
      "data"
    ],
    "category": "数据库"
  },
  {
    "name": "database_151078",
    "type": "freeform",
    "shape_idx": 71,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "数据库",
      "存储",
      "数据"
    ],
    "tags_en": [
      "database",
      "storage",
      "data"
    ],
    "category": "数据库"
  },
  {
    "name": "database_151092",
    "type": "freeform",
    "shape_idx": 72,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "数据库",
      "存储",
      "数据"
    ],
    "tags_en": [
      "database",
      "storage",
      "data"
    ],
    "category": "数据库"
  },
  {
    "name": "database_151084",
    "type": "freeform",
    "shape_idx": 73,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "数据库",
      "存储",
      "数据"
    ],
    "tags_en": [
      "database",
      "storage",
      "data"
    ],
    "category": "数据库"
  },
  {
    "name": "database_151087",
    "type": "freeform",
    "shape_idx": 74,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "数据库",
      "存储",
      "数据"
    ],
    "tags_en": [
      "database",
      "storage",
      "data"
    ],
    "category": "数据库"
  },
  {
    "name": "database_151082",
    "type": "freeform",
    "shape_idx": 75,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "数据库",
      "存储",
      "数据"
    ],
    "tags_en": [
      "database",
      "storage",
      "data"
    ],
    "category": "数据库"
  },
  {
    "name": "database_151081",
    "type": "freeform",
    "shape_idx": 76,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "数据库",
      "存储",
      "数据"
    ],
    "tags_en": [
      "database",
      "storage",
      "data"
    ],
    "category": "数据库"
  },
  {
    "name": "database_151079",
    "type": "freeform",
    "shape_idx": 77,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "数据库",
      "存储",
      "数据"
    ],
    "tags_en": [
      "database",
      "storage",
      "data"
    ],
    "category": "数据库"
  },
  {
    "name": "database_151074",
    "type": "freeform",
    "shape_idx": 78,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "数据库",
      "存储",
      "数据"
    ],
    "tags_en": [
      "database",
      "storage",
      "data"
    ],
    "category": "数据库"
  },
  {
    "name": "database_151088",
    "type": "freeform",
    "shape_idx": 79,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "数据库",
      "存储",
      "数据"
    ],
    "tags_en": [
      "database",
      "storage",
      "data"
    ],
    "category": "数据库"
  },
  {
    "name": "database_151085",
    "type": "freeform",
    "shape_idx": 80,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "数据库",
      "存储",
      "数据"
    ],
    "tags_en": [
      "database",
      "storage",
      "data"
    ],
    "category": "数据库"
  },
  {
    "name": "database_151090",
    "type": "freeform",
    "shape_idx": 81,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "数据库",
      "存储",
      "数据"
    ],
    "tags_en": [
      "database",
      "storage",
      "data"
    ],
    "category": "数据库"
  },
  {
    "name": "database_151071",
    "type": "freeform",
    "shape_idx": 82,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "数据库",
      "存储",
      "数据"
    ],
    "tags_en": [
      "database",
      "storage",
      "data"
    ],
    "category": "数据库"
  },
  {
    "name": "组合 356",
    "type": "group",
    "shape_idx": 83,
    "colors": [
      "000000",
      "F84D4D"
    ],
    "tags_cn": [
      "组合图标",
      "设备"
    ],
    "tags_en": [
      "composite",
      "device"
    ],
    "category": "组合"
  },
  {
    "name": "组合 357",
    "type": "group",
    "shape_idx": 84,
    "colors": [
      "000000",
      "F84D4D"
    ],
    "tags_cn": [
      "组合图标",
      "设备"
    ],
    "tags_en": [
      "composite",
      "device"
    ],
    "category": "组合"
  },
  {
    "name": "laptop_159402",
    "type": "freeform",
    "shape_idx": 85,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "笔记本",
      "电脑",
      "终端"
    ],
    "tags_en": [
      "laptop",
      "computer",
      "terminal"
    ],
    "category": "电脑"
  },
  {
    "name": "laptop_159404",
    "type": "freeform",
    "shape_idx": 86,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "笔记本",
      "电脑",
      "终端"
    ],
    "tags_en": [
      "laptop",
      "computer",
      "terminal"
    ],
    "category": "电脑"
  },
  {
    "name": "laptop_159415",
    "type": "freeform",
    "shape_idx": 87,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "笔记本",
      "电脑",
      "终端"
    ],
    "tags_en": [
      "laptop",
      "computer",
      "terminal"
    ],
    "category": "电脑"
  },
  {
    "name": "laptop_159403",
    "type": "freeform",
    "shape_idx": 88,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "笔记本",
      "电脑",
      "终端"
    ],
    "tags_en": [
      "laptop",
      "computer",
      "terminal"
    ],
    "category": "电脑"
  },
  {
    "name": "laptop_159421",
    "type": "freeform",
    "shape_idx": 89,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "笔记本",
      "电脑",
      "终端"
    ],
    "tags_en": [
      "laptop",
      "computer",
      "terminal"
    ],
    "category": "电脑"
  },
  {
    "name": "laptop_159417",
    "type": "freeform",
    "shape_idx": 90,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "笔记本",
      "电脑",
      "终端"
    ],
    "tags_en": [
      "laptop",
      "computer",
      "terminal"
    ],
    "category": "电脑"
  },
  {
    "name": "laptop_159414",
    "type": "freeform",
    "shape_idx": 91,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "笔记本",
      "电脑",
      "终端"
    ],
    "tags_en": [
      "laptop",
      "computer",
      "terminal"
    ],
    "category": "电脑"
  },
  {
    "name": "laptop_159397",
    "type": "freeform",
    "shape_idx": 92,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "笔记本",
      "电脑",
      "终端"
    ],
    "tags_en": [
      "laptop",
      "computer",
      "terminal"
    ],
    "category": "电脑"
  },
  {
    "name": "laptop_159410",
    "type": "freeform",
    "shape_idx": 93,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "笔记本",
      "电脑",
      "终端"
    ],
    "tags_en": [
      "laptop",
      "computer",
      "terminal"
    ],
    "category": "电脑"
  },
  {
    "name": "组合 234",
    "type": "group",
    "shape_idx": 94,
    "colors": [
      "F84D4D"
    ],
    "tags_cn": [
      "组合图标",
      "设备"
    ],
    "tags_en": [
      "composite",
      "device"
    ],
    "category": "组合"
  },
  {
    "name": "组合 368",
    "type": "group",
    "shape_idx": 95,
    "colors": [
      "F84D4D"
    ],
    "tags_cn": [
      "组合图标",
      "设备"
    ],
    "tags_en": [
      "composite",
      "device"
    ],
    "category": "组合"
  },
  {
    "name": "组合 242",
    "type": "group",
    "shape_idx": 96,
    "colors": [
      "000000",
      "F84D4D"
    ],
    "tags_cn": [
      "组合图标",
      "设备"
    ],
    "tags_en": [
      "composite",
      "device"
    ],
    "category": "组合"
  },
  {
    "name": "computer_159340",
    "type": "freeform",
    "shape_idx": 97,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "台式电脑",
      "显示器",
      "工作站"
    ],
    "tags_en": [
      "computer",
      "desktop",
      "monitor"
    ],
    "category": "电脑"
  },
  {
    "name": "computer_159377",
    "type": "freeform",
    "shape_idx": 98,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "台式电脑",
      "显示器",
      "工作站"
    ],
    "tags_en": [
      "computer",
      "desktop",
      "monitor"
    ],
    "category": "电脑"
  },
  {
    "name": "computer_159335",
    "type": "freeform",
    "shape_idx": 99,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "台式电脑",
      "显示器",
      "工作站"
    ],
    "tags_en": [
      "computer",
      "desktop",
      "monitor"
    ],
    "category": "电脑"
  },
  {
    "name": "computer_159342",
    "type": "freeform",
    "shape_idx": 100,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "台式电脑",
      "显示器",
      "工作站"
    ],
    "tags_en": [
      "computer",
      "desktop",
      "monitor"
    ],
    "category": "电脑"
  },
  {
    "name": "computer_159354",
    "type": "freeform",
    "shape_idx": 101,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "台式电脑",
      "显示器",
      "工作站"
    ],
    "tags_en": [
      "computer",
      "desktop",
      "monitor"
    ],
    "category": "电脑"
  },
  {
    "name": "computer_159339",
    "type": "freeform",
    "shape_idx": 102,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "台式电脑",
      "显示器",
      "工作站"
    ],
    "tags_en": [
      "computer",
      "desktop",
      "monitor"
    ],
    "category": "电脑"
  },
  {
    "name": "computer_159375",
    "type": "freeform",
    "shape_idx": 103,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "台式电脑",
      "显示器",
      "工作站"
    ],
    "tags_en": [
      "computer",
      "desktop",
      "monitor"
    ],
    "category": "电脑"
  },
  {
    "name": "computer_159334",
    "type": "freeform",
    "shape_idx": 104,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "台式电脑",
      "显示器",
      "工作站"
    ],
    "tags_en": [
      "computer",
      "desktop",
      "monitor"
    ],
    "category": "电脑"
  },
  {
    "name": "computer_159367",
    "type": "freeform",
    "shape_idx": 105,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "台式电脑",
      "显示器",
      "工作站"
    ],
    "tags_en": [
      "computer",
      "desktop",
      "monitor"
    ],
    "category": "电脑"
  },
  {
    "name": "computer_159362",
    "type": "freeform",
    "shape_idx": 106,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "台式电脑",
      "显示器",
      "工作站"
    ],
    "tags_en": [
      "computer",
      "desktop",
      "monitor"
    ],
    "category": "电脑"
  },
  {
    "name": "computer_159373",
    "type": "freeform",
    "shape_idx": 107,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "台式电脑",
      "显示器",
      "工作站"
    ],
    "tags_en": [
      "computer",
      "desktop",
      "monitor"
    ],
    "category": "电脑"
  },
  {
    "name": "computer_159351",
    "type": "freeform",
    "shape_idx": 108,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "台式电脑",
      "显示器",
      "工作站"
    ],
    "tags_en": [
      "computer",
      "desktop",
      "monitor"
    ],
    "category": "电脑"
  },
  {
    "name": "computer_159349",
    "type": "freeform",
    "shape_idx": 109,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "台式电脑",
      "显示器",
      "工作站"
    ],
    "tags_en": [
      "computer",
      "desktop",
      "monitor"
    ],
    "category": "电脑"
  },
  {
    "name": "computer_159369",
    "type": "freeform",
    "shape_idx": 110,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "台式电脑",
      "显示器",
      "工作站"
    ],
    "tags_en": [
      "computer",
      "desktop",
      "monitor"
    ],
    "category": "电脑"
  },
  {
    "name": "computer_159361",
    "type": "freeform",
    "shape_idx": 111,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "台式电脑",
      "显示器",
      "工作站"
    ],
    "tags_en": [
      "computer",
      "desktop",
      "monitor"
    ],
    "category": "电脑"
  },
  {
    "name": "computer_159353",
    "type": "freeform",
    "shape_idx": 112,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "台式电脑",
      "显示器",
      "工作站"
    ],
    "tags_en": [
      "computer",
      "desktop",
      "monitor"
    ],
    "category": "电脑"
  },
  {
    "name": "computer_159344",
    "type": "freeform",
    "shape_idx": 113,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "台式电脑",
      "显示器",
      "工作站"
    ],
    "tags_en": [
      "computer",
      "desktop",
      "monitor"
    ],
    "category": "电脑"
  },
  {
    "name": "computer_159338",
    "type": "freeform",
    "shape_idx": 114,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "台式电脑",
      "显示器",
      "工作站"
    ],
    "tags_en": [
      "computer",
      "desktop",
      "monitor"
    ],
    "category": "电脑"
  },
  {
    "name": "组合 381",
    "type": "group",
    "shape_idx": 115,
    "colors": [
      "000000",
      "F84D4D"
    ],
    "tags_cn": [
      "组合图标",
      "设备"
    ],
    "tags_en": [
      "composite",
      "device"
    ],
    "category": "组合"
  },
  {
    "name": "组合 380",
    "type": "group",
    "shape_idx": 116,
    "colors": [
      "000000",
      "F84D4D"
    ],
    "tags_cn": [
      "组合图标",
      "设备"
    ],
    "tags_en": [
      "composite",
      "device"
    ],
    "category": "组合"
  },
  {
    "name": "usb-port_80881",
    "type": "freeform",
    "shape_idx": 117,
    "colors": [
      "F84D4D"
    ],
    "tags_cn": [
      "USB",
      "接口",
      "外设"
    ],
    "tags_en": [
      "usb",
      "port",
      "peripheral"
    ],
    "category": "硬件"
  },
  {
    "name": "sd-card_76752",
    "type": "freeform",
    "shape_idx": 118,
    "colors": [
      "F84D4D"
    ],
    "tags_cn": [
      "硬盘",
      "存储",
      "SSD"
    ],
    "tags_en": [
      "drive",
      "storage",
      "ssd"
    ],
    "category": "硬件"
  },
  {
    "name": "sound-card_100311",
    "type": "freeform",
    "shape_idx": 119,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "声卡",
      "音频",
      "硬件"
    ],
    "tags_en": [
      "sound-card",
      "audio",
      "hardware"
    ],
    "category": "硬件"
  },
  {
    "name": "hard-drive_22820",
    "type": "freeform",
    "shape_idx": 120,
    "colors": [
      "F84D4D"
    ],
    "tags_cn": [
      "硬盘",
      "存储",
      "SSD"
    ],
    "tags_en": [
      "drive",
      "storage",
      "ssd"
    ],
    "category": "硬件"
  },
  {
    "name": "pci-card_31859",
    "type": "freeform",
    "shape_idx": 121,
    "colors": [
      "F84D4D"
    ],
    "tags_cn": [
      "PCI卡",
      "扩展卡",
      "硬件"
    ],
    "tags_en": [
      "pci",
      "expansion",
      "card"
    ],
    "category": "硬件"
  },
  {
    "name": "random-access-memory-chip_18033",
    "type": "freeform",
    "shape_idx": 122,
    "colors": [
      "F84D4D"
    ],
    "tags_cn": [
      "芯片",
      "处理器",
      "CPU"
    ],
    "tags_en": [
      "chip",
      "processor",
      "cpu"
    ],
    "category": "硬件"
  },
  {
    "name": "video-card_141235",
    "type": "freeform",
    "shape_idx": 123,
    "colors": [
      "F84D4D"
    ],
    "tags_cn": [
      "显卡",
      "GPU",
      "图形处理"
    ],
    "tags_en": [
      "gpu",
      "graphics",
      "video-card"
    ],
    "category": "硬件"
  },
  {
    "name": "sound-card_141237",
    "type": "freeform",
    "shape_idx": 124,
    "colors": [
      "F84D4D"
    ],
    "tags_cn": [
      "声卡",
      "音频",
      "硬件"
    ],
    "tags_en": [
      "sound-card",
      "audio",
      "hardware"
    ],
    "category": "硬件"
  },
  {
    "name": "sound-card_337601",
    "type": "freeform",
    "shape_idx": 125,
    "colors": [
      "F84D4D"
    ],
    "tags_cn": [
      "声卡",
      "音频",
      "硬件"
    ],
    "tags_en": [
      "sound-card",
      "audio",
      "hardware"
    ],
    "category": "硬件"
  },
  {
    "name": "earphones_337629",
    "type": "freeform",
    "shape_idx": 126,
    "colors": [
      "F84D4D"
    ],
    "tags_cn": [
      "耳机",
      "音频",
      "外设"
    ],
    "tags_en": [
      "earphones",
      "audio",
      "peripheral"
    ],
    "category": "硬件"
  },
  {
    "name": "video-card_337599",
    "type": "freeform",
    "shape_idx": 127,
    "colors": [
      "F84D4D"
    ],
    "tags_cn": [
      "显卡",
      "GPU",
      "图形处理"
    ],
    "tags_en": [
      "gpu",
      "graphics",
      "video-card"
    ],
    "category": "硬件"
  },
  {
    "name": "fan_161875",
    "type": "freeform",
    "shape_idx": 128,
    "colors": [
      "F84D4D"
    ],
    "tags_cn": [
      "风扇",
      "散热",
      "冷却"
    ],
    "tags_en": [
      "fan",
      "cooling",
      "heat"
    ],
    "category": "硬件"
  },
  {
    "name": "square-processor_77616",
    "type": "freeform",
    "shape_idx": 129,
    "colors": [
      "F84D4D"
    ],
    "tags_cn": [
      "芯片",
      "处理器",
      "CPU"
    ],
    "tags_en": [
      "chip",
      "processor",
      "cpu"
    ],
    "category": "硬件"
  },
  {
    "name": "rectangular-processor_77615",
    "type": "freeform",
    "shape_idx": 130,
    "colors": [
      "F84D4D"
    ],
    "tags_cn": [
      "芯片",
      "处理器",
      "CPU"
    ],
    "tags_en": [
      "chip",
      "processor",
      "cpu"
    ],
    "category": "硬件"
  },
  {
    "name": "plug_159049",
    "type": "freeform",
    "shape_idx": 131,
    "colors": [
      "F84D4D"
    ],
    "tags_cn": [
      "电源",
      "插头",
      "供电"
    ],
    "tags_en": [
      "power",
      "plug",
      "supply"
    ],
    "category": "硬件"
  },
  {
    "name": "memory-card_120453",
    "type": "freeform",
    "shape_idx": 132,
    "colors": [
      "F84D4D"
    ],
    "tags_cn": [
      "内存",
      "存储",
      "RAM"
    ],
    "tags_en": [
      "ram",
      "memory",
      "storage"
    ],
    "category": "硬件"
  },
  {
    "name": "plug-in_94940",
    "type": "freeform",
    "shape_idx": 133,
    "colors": [
      "F84D4D"
    ],
    "tags_cn": [
      "电源",
      "插头",
      "供电"
    ],
    "tags_en": [
      "power",
      "plug",
      "supply"
    ],
    "category": "硬件"
  },
  {
    "name": "motherboard_141231",
    "type": "freeform",
    "shape_idx": 134,
    "colors": [
      "F84D4D"
    ],
    "tags_cn": [
      "主板",
      "电路板",
      "硬件"
    ],
    "tags_en": [
      "motherboard",
      "board",
      "hardware"
    ],
    "category": "硬件"
  },
  {
    "name": "microchip_161973",
    "type": "freeform",
    "shape_idx": 135,
    "colors": [
      "F84D4D"
    ],
    "tags_cn": [
      "芯片",
      "处理器",
      "CPU"
    ],
    "tags_en": [
      "chip",
      "processor",
      "cpu"
    ],
    "category": "硬件"
  },
  {
    "name": "chip_87166",
    "type": "freeform",
    "shape_idx": 136,
    "colors": [
      "F84D4D"
    ],
    "tags_cn": [
      "芯片",
      "处理器",
      "CPU"
    ],
    "tags_en": [
      "chip",
      "processor",
      "cpu"
    ],
    "category": "硬件"
  },
  {
    "name": "ram_99476",
    "type": "freeform",
    "shape_idx": 137,
    "colors": [
      "F84D4D"
    ],
    "tags_cn": [
      "内存",
      "存储",
      "RAM"
    ],
    "tags_en": [
      "ram",
      "memory",
      "storage"
    ],
    "category": "硬件"
  },
  {
    "name": "ram-memory_141230",
    "type": "freeform",
    "shape_idx": 138,
    "colors": [
      "F84D4D"
    ],
    "tags_cn": [
      "内存",
      "存储",
      "RAM"
    ],
    "tags_en": [
      "ram",
      "memory",
      "storage"
    ],
    "category": "硬件"
  },
  {
    "name": "gfx-card_99482",
    "type": "freeform",
    "shape_idx": 139,
    "colors": [
      "F84D4D"
    ],
    "tags_cn": [
      "显卡",
      "GPU",
      "图形处理"
    ],
    "tags_en": [
      "gpu",
      "graphics",
      "video-card"
    ],
    "category": "硬件"
  },
  {
    "name": "hard-drive_99485",
    "type": "freeform",
    "shape_idx": 140,
    "colors": [
      "F84D4D"
    ],
    "tags_cn": [
      "硬盘",
      "存储",
      "SSD"
    ],
    "tags_en": [
      "drive",
      "storage",
      "ssd"
    ],
    "category": "硬件"
  },
  {
    "name": "memory-card_107932",
    "type": "freeform",
    "shape_idx": 141,
    "colors": [
      "F84D4D"
    ],
    "tags_cn": [
      "内存",
      "存储",
      "RAM"
    ],
    "tags_en": [
      "ram",
      "memory",
      "storage"
    ],
    "category": "硬件"
  },
  {
    "name": "microchip_99466",
    "type": "freeform",
    "shape_idx": 142,
    "colors": [
      "F84D4D"
    ],
    "tags_cn": [
      "芯片",
      "处理器",
      "CPU"
    ],
    "tags_en": [
      "chip",
      "processor",
      "cpu"
    ],
    "category": "硬件"
  },
  {
    "name": "microchip_99475",
    "type": "freeform",
    "shape_idx": 143,
    "colors": [
      "F84D4D"
    ],
    "tags_cn": [
      "芯片",
      "处理器",
      "CPU"
    ],
    "tags_en": [
      "chip",
      "processor",
      "cpu"
    ],
    "category": "硬件"
  },
  {
    "name": "microchip_99465",
    "type": "freeform",
    "shape_idx": 144,
    "colors": [
      "F84D4D"
    ],
    "tags_cn": [
      "芯片",
      "处理器",
      "CPU"
    ],
    "tags_en": [
      "chip",
      "processor",
      "cpu"
    ],
    "category": "硬件"
  },
  {
    "name": "cpu_154574",
    "type": "freeform",
    "shape_idx": 145,
    "colors": [
      "F84D4D"
    ],
    "tags_cn": [
      "芯片",
      "处理器",
      "CPU"
    ],
    "tags_en": [
      "chip",
      "processor",
      "cpu"
    ],
    "category": "硬件"
  },
  {
    "name": "chip_141229",
    "type": "freeform",
    "shape_idx": 146,
    "colors": [
      "F84D4D"
    ],
    "tags_cn": [
      "芯片",
      "处理器",
      "CPU"
    ],
    "tags_en": [
      "chip",
      "processor",
      "cpu"
    ],
    "category": "硬件"
  },
  {
    "name": "usb-device-with-signal_35908",
    "type": "freeform",
    "shape_idx": 147,
    "colors": [
      "F84D4D"
    ],
    "tags_cn": [
      "信号塔",
      "天线",
      "通信"
    ],
    "tags_en": [
      "signal",
      "antenna",
      "tower"
    ],
    "category": "网络"
  },
  {
    "name": "组合 383",
    "type": "group",
    "shape_idx": 148,
    "colors": [
      "000000",
      "F84D4D"
    ],
    "tags_cn": [
      "组合图标",
      "设备"
    ],
    "tags_en": [
      "composite",
      "device"
    ],
    "category": "组合"
  },
  {
    "name": "组合 400",
    "type": "group",
    "shape_idx": 149,
    "colors": [
      "000000",
      "F84D4D"
    ],
    "tags_cn": [
      "组合图标",
      "设备"
    ],
    "tags_en": [
      "composite",
      "device"
    ],
    "category": "组合"
  },
  {
    "name": "Freeform 123",
    "type": "freeform",
    "shape_idx": 150,
    "colors": [
      "F84D4D"
    ],
    "tags_cn": [
      "组合图标",
      "设备"
    ],
    "tags_en": [
      "composite",
      "device"
    ],
    "category": "组合"
  },
  {
    "name": "组合 412",
    "type": "group",
    "shape_idx": 151,
    "colors": [
      "000000",
      "F84D4D"
    ],
    "tags_cn": [
      "组合图标",
      "设备"
    ],
    "tags_en": [
      "composite",
      "device"
    ],
    "category": "组合"
  },
  {
    "name": "Freeform 171",
    "type": "freeform",
    "shape_idx": 152,
    "colors": [
      "F84D4D"
    ],
    "tags_cn": [
      "组合图标",
      "设备"
    ],
    "tags_en": [
      "composite",
      "device"
    ],
    "category": "组合"
  },
  {
    "name": "组合 432",
    "type": "group",
    "shape_idx": 153,
    "colors": [
      "000000",
      "F84D4D"
    ],
    "tags_cn": [
      "组合图标",
      "设备"
    ],
    "tags_en": [
      "composite",
      "device"
    ],
    "category": "组合"
  },
  {
    "name": "组合 473",
    "type": "group",
    "shape_idx": 154,
    "colors": [
      "000000",
      "F84D4D"
    ],
    "tags_cn": [
      "组合图标",
      "设备"
    ],
    "tags_en": [
      "composite",
      "device"
    ],
    "category": "组合"
  },
  {
    "name": "组合 476",
    "type": "group",
    "shape_idx": 155,
    "colors": [
      "000000",
      "F84D4D"
    ],
    "tags_cn": [
      "组合图标",
      "设备"
    ],
    "tags_en": [
      "composite",
      "device"
    ],
    "category": "组合"
  },
  {
    "name": "组合 487",
    "type": "group",
    "shape_idx": 156,
    "colors": [
      "000000",
      "F84D4D"
    ],
    "tags_cn": [
      "组合图标",
      "设备"
    ],
    "tags_en": [
      "composite",
      "device"
    ],
    "category": "组合"
  },
  {
    "name": "组合 520",
    "type": "group",
    "shape_idx": 157,
    "colors": [
      "000000",
      "F84D4D"
    ],
    "tags_cn": [
      "组合图标",
      "设备"
    ],
    "tags_en": [
      "composite",
      "device"
    ],
    "category": "组合"
  },
  {
    "name": "组合 527",
    "type": "group",
    "shape_idx": 158,
    "colors": [
      "000000",
      "F84D4D"
    ],
    "tags_cn": [
      "组合图标",
      "设备"
    ],
    "tags_en": [
      "composite",
      "device"
    ],
    "category": "组合"
  },
  {
    "name": "组合 530",
    "type": "group",
    "shape_idx": 159,
    "colors": [
      "000000",
      "F84D4D"
    ],
    "tags_cn": [
      "组合图标",
      "设备"
    ],
    "tags_en": [
      "composite",
      "device"
    ],
    "category": "组合"
  },
  {
    "name": "组合 536",
    "type": "group",
    "shape_idx": 160,
    "colors": [
      "000000",
      "F84D4D"
    ],
    "tags_cn": [
      "组合图标",
      "设备"
    ],
    "tags_en": [
      "composite",
      "device"
    ],
    "category": "组合"
  },
  {
    "name": "组合 541",
    "type": "group",
    "shape_idx": 161,
    "colors": [
      "000000",
      "F84D4D"
    ],
    "tags_cn": [
      "组合图标",
      "设备"
    ],
    "tags_en": [
      "composite",
      "device"
    ],
    "category": "组合"
  },
  {
    "name": "Freeform 312",
    "type": "freeform",
    "shape_idx": 162,
    "colors": [
      "F84D4D"
    ],
    "tags_cn": [
      "组合图标",
      "设备"
    ],
    "tags_en": [
      "composite",
      "device"
    ],
    "category": "组合"
  },
  {
    "name": "组合 548",
    "type": "group",
    "shape_idx": 163,
    "colors": [
      "000000",
      "F84D4D"
    ],
    "tags_cn": [
      "组合图标",
      "设备"
    ],
    "tags_en": [
      "composite",
      "device"
    ],
    "category": "组合"
  },
  {
    "name": "Freeform 321",
    "type": "freeform",
    "shape_idx": 164,
    "colors": [
      "000000",
      "F84D4D"
    ],
    "tags_cn": [
      "组合图标",
      "设备"
    ],
    "tags_en": [
      "composite",
      "device"
    ],
    "category": "组合"
  },
  {
    "name": "组合 560",
    "type": "group",
    "shape_idx": 165,
    "colors": [
      "000000",
      "F84D4D"
    ],
    "tags_cn": [
      "组合图标",
      "设备"
    ],
    "tags_en": [
      "composite",
      "device"
    ],
    "category": "组合"
  },
  {
    "name": "组合 568",
    "type": "group",
    "shape_idx": 166,
    "colors": [
      "000000",
      "F84D4D"
    ],
    "tags_cn": [
      "组合图标",
      "设备"
    ],
    "tags_en": [
      "composite",
      "device"
    ],
    "category": "组合"
  },
  {
    "name": "dvd-disc_70982",
    "type": "freeform",
    "shape_idx": 167,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "光盘",
      "保存",
      "存储"
    ],
    "tags_en": [
      "disc",
      "save",
      "storage"
    ],
    "category": "硬件"
  },
  {
    "name": "save-icon-silhouette_69539",
    "type": "freeform",
    "shape_idx": 168,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "光盘",
      "保存",
      "存储"
    ],
    "tags_en": [
      "disc",
      "save",
      "storage"
    ],
    "category": "硬件"
  },
  {
    "name": "cable_193480",
    "type": "freeform",
    "shape_idx": 169,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "线缆",
      "连接",
      "布线"
    ],
    "tags_en": [
      "cable",
      "wire",
      "connect"
    ],
    "category": "网络"
  },
  {
    "name": "chip_141228",
    "type": "freeform",
    "shape_idx": 170,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "芯片",
      "处理器",
      "CPU"
    ],
    "tags_en": [
      "chip",
      "processor",
      "cpu"
    ],
    "category": "硬件"
  },
  {
    "name": "ethernet_337623",
    "type": "freeform",
    "shape_idx": 171,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "以太网",
      "网线",
      "有线网络"
    ],
    "tags_en": [
      "ethernet",
      "cable",
      "wired"
    ],
    "category": "网络"
  },
  {
    "name": "magsafe_337627",
    "type": "freeform",
    "shape_idx": 172,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [],
    "tags_en": [],
    "category": "未分类"
  },
  {
    "name": "iconfont-1177-866359",
    "type": "freeform",
    "shape_idx": 173,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "图标",
      "设备"
    ],
    "tags_en": [
      "icon",
      "device"
    ],
    "category": "未标注"
  },
  {
    "name": "classroom-pc_73575",
    "type": "freeform",
    "shape_idx": 174,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "教学电脑",
      "机房",
      "教育"
    ],
    "tags_en": [
      "classroom",
      "education",
      "pc"
    ],
    "category": "电脑"
  },
  {
    "name": "组合 600",
    "type": "group",
    "shape_idx": 175,
    "colors": [
      "000000",
      "F84D4D"
    ],
    "tags_cn": [
      "组合图标",
      "设备"
    ],
    "tags_en": [
      "composite",
      "device"
    ],
    "category": "组合"
  },
  {
    "name": "Freeform 188",
    "type": "freeform",
    "shape_idx": 176,
    "colors": [
      "F84D4D"
    ],
    "tags_cn": [
      "组合图标",
      "设备"
    ],
    "tags_en": [
      "composite",
      "device"
    ],
    "category": "组合"
  },
  {
    "name": "wifi_155695",
    "type": "freeform",
    "shape_idx": 177,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "无线网络",
      "WiFi",
      "信号"
    ],
    "tags_en": [
      "wifi",
      "wireless",
      "signal"
    ],
    "category": "网络"
  },
  {
    "name": "组合 717",
    "type": "group",
    "shape_idx": 178,
    "colors": [
      "000000",
      "F84D4D"
    ],
    "tags_cn": [
      "组合图标",
      "设备"
    ],
    "tags_en": [
      "composite",
      "device"
    ],
    "category": "组合"
  },
  {
    "name": "组合 736",
    "type": "group",
    "shape_idx": 179,
    "colors": [
      "000000",
      "F84D4D"
    ],
    "tags_cn": [
      "组合图标",
      "设备"
    ],
    "tags_en": [
      "composite",
      "device"
    ],
    "category": "组合"
  },
  {
    "name": "组合 729",
    "type": "group",
    "shape_idx": 180,
    "colors": [
      "000000",
      "F84D4D"
    ],
    "tags_cn": [
      "组合图标",
      "设备"
    ],
    "tags_en": [
      "composite",
      "device"
    ],
    "category": "组合"
  },
  {
    "name": "组合 728",
    "type": "group",
    "shape_idx": 181,
    "colors": [
      "000000",
      "F84D4D"
    ],
    "tags_cn": [
      "组合图标",
      "设备"
    ],
    "tags_en": [
      "composite",
      "device"
    ],
    "category": "组合"
  },
  {
    "name": "组合 727",
    "type": "group",
    "shape_idx": 182,
    "colors": [
      "000000",
      "F84D4D"
    ],
    "tags_cn": [
      "组合图标",
      "设备"
    ],
    "tags_en": [
      "composite",
      "device"
    ],
    "category": "组合"
  },
  {
    "name": "组合 726",
    "type": "group",
    "shape_idx": 183,
    "colors": [
      "000000",
      "F84D4D"
    ],
    "tags_cn": [
      "组合图标",
      "设备"
    ],
    "tags_en": [
      "composite",
      "device"
    ],
    "category": "组合"
  },
  {
    "name": "组合 725",
    "type": "group",
    "shape_idx": 184,
    "colors": [
      "000000",
      "F84D4D"
    ],
    "tags_cn": [
      "组合图标",
      "设备"
    ],
    "tags_en": [
      "composite",
      "device"
    ],
    "category": "组合"
  },
  {
    "name": "组合 732",
    "type": "group",
    "shape_idx": 185,
    "colors": [
      "000000",
      "F84D4D"
    ],
    "tags_cn": [
      "组合图标",
      "设备"
    ],
    "tags_en": [
      "composite",
      "device"
    ],
    "category": "组合"
  },
  {
    "name": "组合 733",
    "type": "group",
    "shape_idx": 186,
    "colors": [
      "000000",
      "F84D4D"
    ],
    "tags_cn": [
      "组合图标",
      "设备"
    ],
    "tags_en": [
      "composite",
      "device"
    ],
    "category": "组合"
  },
  {
    "name": "组合 734",
    "type": "group",
    "shape_idx": 187,
    "colors": [
      "000000",
      "F84D4D"
    ],
    "tags_cn": [
      "组合图标",
      "设备"
    ],
    "tags_en": [
      "composite",
      "device"
    ],
    "category": "组合"
  },
  {
    "name": "组合 731",
    "type": "group",
    "shape_idx": 188,
    "colors": [
      "000000",
      "F84D4D"
    ],
    "tags_cn": [
      "组合图标",
      "设备"
    ],
    "tags_en": [
      "composite",
      "device"
    ],
    "category": "组合"
  },
  {
    "name": "组合 735",
    "type": "group",
    "shape_idx": 189,
    "colors": [
      "000000",
      "F84D4D"
    ],
    "tags_cn": [
      "组合图标",
      "设备"
    ],
    "tags_en": [
      "composite",
      "device"
    ],
    "category": "组合"
  },
  {
    "name": "组合 718",
    "type": "group",
    "shape_idx": 190,
    "colors": [
      "000000",
      "F84D4D"
    ],
    "tags_cn": [
      "组合图标",
      "设备"
    ],
    "tags_en": [
      "composite",
      "device"
    ],
    "category": "组合"
  },
  {
    "name": "组合 719",
    "type": "group",
    "shape_idx": 191,
    "colors": [
      "000000",
      "F84D4D"
    ],
    "tags_cn": [
      "组合图标",
      "设备"
    ],
    "tags_en": [
      "composite",
      "device"
    ],
    "category": "组合"
  },
  {
    "name": "组合 720",
    "type": "group",
    "shape_idx": 192,
    "colors": [
      "000000",
      "F84D4D"
    ],
    "tags_cn": [
      "组合图标",
      "设备"
    ],
    "tags_en": [
      "composite",
      "device"
    ],
    "category": "组合"
  },
  {
    "name": "组合 721",
    "type": "group",
    "shape_idx": 193,
    "colors": [
      "000000",
      "F84D4D"
    ],
    "tags_cn": [
      "组合图标",
      "设备"
    ],
    "tags_en": [
      "composite",
      "device"
    ],
    "category": "组合"
  },
  {
    "name": "组合 722",
    "type": "group",
    "shape_idx": 194,
    "colors": [
      "000000",
      "F84D4D"
    ],
    "tags_cn": [
      "组合图标",
      "设备"
    ],
    "tags_en": [
      "composite",
      "device"
    ],
    "category": "组合"
  },
  {
    "name": "组合 723",
    "type": "group",
    "shape_idx": 195,
    "colors": [
      "000000",
      "F84D4D"
    ],
    "tags_cn": [
      "组合图标",
      "设备"
    ],
    "tags_en": [
      "composite",
      "device"
    ],
    "category": "组合"
  },
  {
    "name": "组合 724",
    "type": "group",
    "shape_idx": 196,
    "colors": [
      "000000",
      "F84D4D"
    ],
    "tags_cn": [
      "组合图标",
      "设备"
    ],
    "tags_en": [
      "composite",
      "device"
    ],
    "category": "组合"
  },
  {
    "name": "组合 730",
    "type": "group",
    "shape_idx": 197,
    "colors": [
      "000000",
      "F84D4D"
    ],
    "tags_cn": [
      "组合图标",
      "设备"
    ],
    "tags_en": [
      "composite",
      "device"
    ],
    "category": "组合"
  },
  {
    "name": "组合 737",
    "type": "group",
    "shape_idx": 198,
    "colors": [
      "000000",
      "F84D4D"
    ],
    "tags_cn": [
      "组合图标",
      "设备"
    ],
    "tags_en": [
      "composite",
      "device"
    ],
    "category": "组合"
  },
  {
    "name": "iconfont-11442-3338437",
    "type": "freeform",
    "shape_idx": 199,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "图标",
      "设备"
    ],
    "tags_en": [
      "icon",
      "device"
    ],
    "category": "未标注"
  },
  {
    "name": "iconfont-11442-3338440",
    "type": "freeform",
    "shape_idx": 200,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "图标",
      "设备"
    ],
    "tags_en": [
      "icon",
      "device"
    ],
    "category": "未标注"
  },
  {
    "name": "iconfont-11442-3338443",
    "type": "freeform",
    "shape_idx": 201,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "图标",
      "设备"
    ],
    "tags_en": [
      "icon",
      "device"
    ],
    "category": "未标注"
  },
  {
    "name": "iconfont-11442-3338444",
    "type": "freeform",
    "shape_idx": 202,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "图标",
      "设备"
    ],
    "tags_en": [
      "icon",
      "device"
    ],
    "category": "未标注"
  },
  {
    "name": "router-signal_81726",
    "type": "freeform",
    "shape_idx": 203,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "路由器",
      "网络设备",
      "交换机"
    ],
    "tags_en": [
      "router",
      "network",
      "switch"
    ],
    "category": "网络"
  },
  {
    "name": "wifi-modem_103614",
    "type": "freeform",
    "shape_idx": 204,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "无线网络",
      "WiFi",
      "信号"
    ],
    "tags_en": [
      "wifi",
      "wireless",
      "signal"
    ],
    "category": "网络"
  },
  {
    "name": "wifi-sign_81944",
    "type": "freeform",
    "shape_idx": 205,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "无线网络",
      "WiFi",
      "信号"
    ],
    "tags_en": [
      "wifi",
      "wireless",
      "signal"
    ],
    "category": "网络"
  },
  {
    "name": "wifi-storage_72715",
    "type": "freeform",
    "shape_idx": 206,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "无线网络",
      "WiFi",
      "信号"
    ],
    "tags_en": [
      "wifi",
      "wireless",
      "signal"
    ],
    "category": "网络"
  },
  {
    "name": "wifi-symbol_16062",
    "type": "freeform",
    "shape_idx": 207,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "无线网络",
      "WiFi",
      "信号"
    ],
    "tags_en": [
      "wifi",
      "wireless",
      "signal"
    ],
    "category": "网络"
  },
  {
    "name": "modem_99546",
    "type": "freeform",
    "shape_idx": 208,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "调制解调器",
      "网关",
      "接入"
    ],
    "tags_en": [
      "modem",
      "gateway",
      "access"
    ],
    "category": "网络"
  },
  {
    "name": "modem_99543",
    "type": "freeform",
    "shape_idx": 209,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "调制解调器",
      "网关",
      "接入"
    ],
    "tags_en": [
      "modem",
      "gateway",
      "access"
    ],
    "category": "网络"
  },
  {
    "name": "modem_99547",
    "type": "freeform",
    "shape_idx": 210,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "调制解调器",
      "网关",
      "接入"
    ],
    "tags_en": [
      "modem",
      "gateway",
      "access"
    ],
    "category": "网络"
  },
  {
    "name": "wifi-signal-tower_15924",
    "type": "freeform",
    "shape_idx": 211,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "无线网络",
      "WiFi",
      "信号"
    ],
    "tags_en": [
      "wifi",
      "wireless",
      "signal"
    ],
    "category": "网络"
  },
  {
    "name": "modem-with-two-antenna_81696",
    "type": "freeform",
    "shape_idx": 212,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "调制解调器",
      "网关",
      "接入"
    ],
    "tags_en": [
      "modem",
      "gateway",
      "access"
    ],
    "category": "网络"
  },
  {
    "name": "wifi_116065",
    "type": "freeform",
    "shape_idx": 213,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "无线网络",
      "WiFi",
      "信号"
    ],
    "tags_en": [
      "wifi",
      "wireless",
      "signal"
    ],
    "category": "网络"
  },
  {
    "name": "ssd-card_100312",
    "type": "freeform",
    "shape_idx": 214,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "硬盘",
      "存储",
      "SSD"
    ],
    "tags_en": [
      "drive",
      "storage",
      "ssd"
    ],
    "category": "硬件"
  },
  {
    "name": "laptop_159425",
    "type": "freeform",
    "shape_idx": 215,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "笔记本",
      "电脑",
      "终端"
    ],
    "tags_en": [
      "laptop",
      "computer",
      "terminal"
    ],
    "category": "电脑"
  },
  {
    "name": "big-power-button_81172",
    "type": "freeform",
    "shape_idx": 216,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "电源",
      "插头",
      "供电"
    ],
    "tags_en": [
      "power",
      "plug",
      "supply"
    ],
    "category": "硬件"
  },
  {
    "name": "组合 858",
    "type": "group",
    "shape_idx": 217,
    "colors": [
      "000000",
      "F84D4D"
    ],
    "tags_cn": [
      "组合图标",
      "设备"
    ],
    "tags_en": [
      "composite",
      "device"
    ],
    "category": "组合"
  },
  {
    "name": "networking-cable_3751",
    "type": "freeform",
    "shape_idx": 218,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "线缆",
      "连接",
      "布线"
    ],
    "tags_en": [
      "cable",
      "wire",
      "connect"
    ],
    "category": "网络"
  },
  {
    "name": "binoculars_1792",
    "type": "freeform",
    "shape_idx": 219,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "望远镜",
      "观察",
      "搜索"
    ],
    "tags_en": [
      "binoculars",
      "search",
      "observe"
    ],
    "category": "设备"
  },
  {
    "name": "satellite_207649",
    "type": "freeform",
    "shape_idx": 220,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "卫星",
      "通信",
      "太空"
    ],
    "tags_en": [
      "satellite",
      "space",
      "communication"
    ],
    "category": "网络"
  },
  {
    "name": "组合 870",
    "type": "group",
    "shape_idx": 221,
    "colors": [
      "000000",
      "F84D4D"
    ],
    "tags_cn": [
      "组合图标",
      "设备"
    ],
    "tags_en": [
      "composite",
      "device"
    ],
    "category": "组合"
  },
  {
    "name": "组合 879",
    "type": "group",
    "shape_idx": 222,
    "colors": [
      "000000",
      "F84D4D"
    ],
    "tags_cn": [
      "组合图标",
      "设备"
    ],
    "tags_en": [
      "composite",
      "device"
    ],
    "category": "组合"
  },
  {
    "name": "组合 882",
    "type": "group",
    "shape_idx": 223,
    "colors": [
      "000000",
      "F84D4D"
    ],
    "tags_cn": [
      "组合图标",
      "设备"
    ],
    "tags_en": [
      "composite",
      "device"
    ],
    "category": "组合"
  },
  {
    "name": "组合 886",
    "type": "group",
    "shape_idx": 224,
    "colors": [
      "000000",
      "F84D4D"
    ],
    "tags_cn": [
      "组合图标",
      "设备"
    ],
    "tags_en": [
      "composite",
      "device"
    ],
    "category": "组合"
  },
  {
    "name": "Freeform 187",
    "type": "freeform",
    "shape_idx": 225,
    "colors": [
      "F84D4D"
    ],
    "tags_cn": [
      "组合图标",
      "设备"
    ],
    "tags_en": [
      "composite",
      "device"
    ],
    "category": "组合"
  },
  {
    "name": "Freeform 252",
    "type": "freeform",
    "shape_idx": 226,
    "colors": [
      "F84D4D"
    ],
    "tags_cn": [
      "组合图标",
      "设备"
    ],
    "tags_en": [
      "composite",
      "device"
    ],
    "category": "组合"
  },
  {
    "name": "组合 975",
    "type": "group",
    "shape_idx": 227,
    "colors": [
      "000000",
      "F84D4D"
    ],
    "tags_cn": [
      "组合图标",
      "设备"
    ],
    "tags_en": [
      "composite",
      "device"
    ],
    "category": "组合"
  },
  {
    "name": "Freeform 262",
    "type": "freeform",
    "shape_idx": 228,
    "colors": [
      "F84D4D"
    ],
    "tags_cn": [
      "组合图标",
      "设备"
    ],
    "tags_en": [
      "composite",
      "device"
    ],
    "category": "组合"
  },
  {
    "name": "组合 989",
    "type": "group",
    "shape_idx": 229,
    "colors": [
      "000000",
      "F84D4D"
    ],
    "tags_cn": [
      "组合图标",
      "设备"
    ],
    "tags_en": [
      "composite",
      "device"
    ],
    "category": "组合"
  },
  {
    "name": "server-device_77474",
    "type": "freeform",
    "shape_idx": 230,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "服务器",
      "数据中心",
      "云计算"
    ],
    "tags_en": [
      "server",
      "datacenter",
      "cloud"
    ],
    "category": "服务器"
  },
  {
    "name": "组合 1006",
    "type": "group",
    "shape_idx": 232,
    "colors": [
      "000000",
      "F84D4D"
    ],
    "tags_cn": [
      "组合图标",
      "设备"
    ],
    "tags_en": [
      "composite",
      "device"
    ],
    "category": "组合"
  },
  {
    "name": "组合 1069",
    "type": "group",
    "shape_idx": 233,
    "colors": [
      "000000",
      "FFFFFF",
      "F84D4D"
    ],
    "tags_cn": [
      "组合图标",
      "设备"
    ],
    "tags_en": [
      "composite",
      "device"
    ],
    "category": "组合"
  },
  {
    "name": "Freeform 341",
    "type": "freeform",
    "shape_idx": 234,
    "colors": [
      "000000",
      "F84D4D"
    ],
    "tags_cn": [
      "组合图标",
      "设备"
    ],
    "tags_en": [
      "composite",
      "device"
    ],
    "category": "组合"
  },
  {
    "name": "组合 1100",
    "type": "group",
    "shape_idx": 235,
    "colors": [
      "000000",
      "FFFFFF",
      "F84D4D"
    ],
    "tags_cn": [
      "组合图标",
      "设备"
    ],
    "tags_en": [
      "composite",
      "device"
    ],
    "category": "组合"
  },
  {
    "name": "rss-logotypes_1057",
    "type": "freeform",
    "shape_idx": 236,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "订阅",
      "推送",
      "信息流"
    ],
    "tags_en": [
      "rss",
      "feed",
      "subscribe"
    ],
    "category": "网络"
  },
  {
    "name": "iconfont-11843-5650967",
    "type": "freeform",
    "shape_idx": 237,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "图标",
      "设备"
    ],
    "tags_en": [
      "icon",
      "device"
    ],
    "category": "未标注"
  },
  {
    "name": "iconfont-11843-5650962",
    "type": "freeform",
    "shape_idx": 238,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "图标",
      "设备"
    ],
    "tags_en": [
      "icon",
      "device"
    ],
    "category": "未标注"
  },
  {
    "name": "wifi-antenna_99538",
    "type": "freeform",
    "shape_idx": 239,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "无线网络",
      "WiFi",
      "信号"
    ],
    "tags_en": [
      "wifi",
      "wireless",
      "signal"
    ],
    "category": "网络"
  },
  {
    "name": "radio-tower_80632",
    "type": "freeform",
    "shape_idx": 240,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "信号塔",
      "天线",
      "通信"
    ],
    "tags_en": [
      "signal",
      "antenna",
      "tower"
    ],
    "category": "网络"
  },
  {
    "name": "phone-signal-connection-symbol_37585",
    "type": "freeform",
    "shape_idx": 241,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "信号塔",
      "天线",
      "通信"
    ],
    "tags_en": [
      "signal",
      "antenna",
      "tower"
    ],
    "category": "网络"
  },
  {
    "name": "satellite_243512",
    "type": "freeform",
    "shape_idx": 242,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "卫星",
      "通信",
      "太空"
    ],
    "tags_en": [
      "satellite",
      "space",
      "communication"
    ],
    "category": "网络"
  },
  {
    "name": "space-satellite_21957",
    "type": "freeform",
    "shape_idx": 243,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "卫星",
      "通信",
      "太空"
    ],
    "tags_en": [
      "satellite",
      "space",
      "communication"
    ],
    "category": "网络"
  },
  {
    "name": "router_96888",
    "type": "freeform",
    "shape_idx": 244,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "路由器",
      "网络设备",
      "交换机"
    ],
    "tags_en": [
      "router",
      "network",
      "switch"
    ],
    "category": "网络"
  },
  {
    "name": "modern-satellite_71268",
    "type": "freeform",
    "shape_idx": 245,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "卫星",
      "通信",
      "太空"
    ],
    "tags_en": [
      "satellite",
      "space",
      "communication"
    ],
    "category": "网络"
  },
  {
    "name": "dish-signal_17793",
    "type": "freeform",
    "shape_idx": 246,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "信号塔",
      "天线",
      "通信"
    ],
    "tags_en": [
      "signal",
      "antenna",
      "tower"
    ],
    "category": "网络"
  },
  {
    "name": "ethernet_161903",
    "type": "freeform",
    "shape_idx": 247,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "以太网",
      "网线",
      "有线网络"
    ],
    "tags_en": [
      "ethernet",
      "cable",
      "wired"
    ],
    "category": "网络"
  },
  {
    "name": "ethernet-plug_3753",
    "type": "freeform",
    "shape_idx": 248,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "以太网",
      "网线",
      "有线网络"
    ],
    "tags_en": [
      "ethernet",
      "cable",
      "wired"
    ],
    "category": "网络"
  },
  {
    "name": "5-pin-connector_64323",
    "type": "freeform",
    "shape_idx": 249,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "连接器",
      "接口",
      "端口"
    ],
    "tags_en": [
      "connector",
      "port",
      "interface"
    ],
    "category": "网络"
  },
  {
    "name": "cable_92943",
    "type": "freeform",
    "shape_idx": 250,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "线缆",
      "连接",
      "布线"
    ],
    "tags_en": [
      "cable",
      "wire",
      "connect"
    ],
    "category": "网络"
  },
  {
    "name": "cable_92944",
    "type": "freeform",
    "shape_idx": 251,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "线缆",
      "连接",
      "布线"
    ],
    "tags_en": [
      "cable",
      "wire",
      "connect"
    ],
    "category": "网络"
  },
  {
    "name": "building-of-big-houses-style_48771",
    "type": "freeform",
    "shape_idx": 252,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "建筑",
      "楼宇",
      "企业"
    ],
    "tags_en": [
      "building",
      "enterprise",
      "office"
    ],
    "category": "通用"
  },
  {
    "name": "iconfont-1186-600061",
    "type": "freeform",
    "shape_idx": 253,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "图标",
      "设备"
    ],
    "tags_en": [
      "icon",
      "device"
    ],
    "category": "未标注"
  },
  {
    "name": "iconfont-11837-5648589",
    "type": "freeform",
    "shape_idx": 254,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "图标",
      "设备"
    ],
    "tags_en": [
      "icon",
      "device"
    ],
    "category": "未标注"
  },
  {
    "name": "iconfont-11837-5649439",
    "type": "freeform",
    "shape_idx": 255,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "图标",
      "设备"
    ],
    "tags_en": [
      "icon",
      "device"
    ],
    "category": "未标注"
  },
  {
    "name": "iconfont-11837-5649954",
    "type": "freeform",
    "shape_idx": 256,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "图标",
      "设备"
    ],
    "tags_en": [
      "icon",
      "device"
    ],
    "category": "未标注"
  },
  {
    "name": "iconfont-11790-5634473",
    "type": "freeform",
    "shape_idx": 257,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "图标",
      "设备"
    ],
    "tags_en": [
      "icon",
      "device"
    ],
    "category": "未标注"
  },
  {
    "name": "iconfont-11790-5634475",
    "type": "freeform",
    "shape_idx": 258,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "图标",
      "设备"
    ],
    "tags_en": [
      "icon",
      "device"
    ],
    "category": "未标注"
  },
  {
    "name": "iconfont-11790-5634476",
    "type": "freeform",
    "shape_idx": 259,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "图标",
      "设备"
    ],
    "tags_en": [
      "icon",
      "device"
    ],
    "category": "未标注"
  },
  {
    "name": "iconfont-11790-5634489",
    "type": "freeform",
    "shape_idx": 260,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "图标",
      "设备"
    ],
    "tags_en": [
      "icon",
      "device"
    ],
    "category": "未标注"
  },
  {
    "name": "iconfont-11790-5634358",
    "type": "freeform",
    "shape_idx": 261,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "图标",
      "设备"
    ],
    "tags_en": [
      "icon",
      "device"
    ],
    "category": "未标注"
  },
  {
    "name": "iconfont-11790-5634361",
    "type": "freeform",
    "shape_idx": 262,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "图标",
      "设备"
    ],
    "tags_en": [
      "icon",
      "device"
    ],
    "category": "未标注"
  },
  {
    "name": "iconfont-11790-5634363",
    "type": "freeform",
    "shape_idx": 263,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "图标",
      "设备"
    ],
    "tags_en": [
      "icon",
      "device"
    ],
    "category": "未标注"
  },
  {
    "name": "simple-table_1329",
    "type": "freeform",
    "shape_idx": 264,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "表格",
      "数据",
      "报表"
    ],
    "tags_en": [
      "table",
      "data",
      "report"
    ],
    "category": "通用"
  },
  {
    "name": "hospital-building_78111",
    "type": "freeform",
    "shape_idx": 265,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "医疗",
      "健康",
      "急救"
    ],
    "tags_en": [
      "medical",
      "health",
      "hospital"
    ],
    "category": "设备"
  },
  {
    "name": "iconfont-1186-701144",
    "type": "freeform",
    "shape_idx": 266,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "图标",
      "设备"
    ],
    "tags_en": [
      "icon",
      "device"
    ],
    "category": "未标注"
  },
  {
    "name": "iconfont-1185-36809",
    "type": "freeform",
    "shape_idx": 267,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "图标",
      "设备"
    ],
    "tags_en": [
      "icon",
      "device"
    ],
    "category": "未标注"
  },
  {
    "name": "iconfont-1185-36810",
    "type": "freeform",
    "shape_idx": 268,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "图标",
      "设备"
    ],
    "tags_en": [
      "icon",
      "device"
    ],
    "category": "未标注"
  },
  {
    "name": "iconfont-11843-5651472",
    "type": "freeform",
    "shape_idx": 269,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "图标",
      "设备"
    ],
    "tags_en": [
      "icon",
      "device"
    ],
    "category": "未标注"
  },
  {
    "name": "iconfont-11843-5651535",
    "type": "freeform",
    "shape_idx": 270,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "图标",
      "设备"
    ],
    "tags_en": [
      "icon",
      "device"
    ],
    "category": "未标注"
  },
  {
    "name": "iconfont-11843-5651540",
    "type": "freeform",
    "shape_idx": 271,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "图标",
      "设备"
    ],
    "tags_en": [
      "icon",
      "device"
    ],
    "category": "未标注"
  },
  {
    "name": "iconfont-11843-5651523",
    "type": "freeform",
    "shape_idx": 272,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "图标",
      "设备"
    ],
    "tags_en": [
      "icon",
      "device"
    ],
    "category": "未标注"
  },
  {
    "name": "iconfont-11843-5651541",
    "type": "freeform",
    "shape_idx": 273,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "图标",
      "设备"
    ],
    "tags_en": [
      "icon",
      "device"
    ],
    "category": "未标注"
  },
  {
    "name": "iconfont-11843-5651542",
    "type": "freeform",
    "shape_idx": 274,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "图标",
      "设备"
    ],
    "tags_en": [
      "icon",
      "device"
    ],
    "category": "未标注"
  },
  {
    "name": "iconfont-11790-5634537",
    "type": "freeform",
    "shape_idx": 275,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "图标",
      "设备"
    ],
    "tags_en": [
      "icon",
      "device"
    ],
    "category": "未标注"
  },
  {
    "name": "tablet-smartphone-computer-checked_81287",
    "type": "freeform",
    "shape_idx": 276,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "台式电脑",
      "显示器",
      "工作站"
    ],
    "tags_en": [
      "computer",
      "desktop",
      "monitor"
    ],
    "category": "电脑"
  },
  {
    "name": "iconfont-10013-7993824",
    "type": "freeform",
    "shape_idx": 277,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "图标",
      "设备"
    ],
    "tags_en": [
      "icon",
      "device"
    ],
    "category": "未标注"
  },
  {
    "name": "iconfont-10013-7993839",
    "type": "freeform",
    "shape_idx": 278,
    "colors": [
      "scheme:accent1"
    ],
    "tags_cn": [
      "图标",
      "设备"
    ],
    "tags_en": [
      "icon",
      "device"
    ],
    "category": "未标注"
  }
]

path: references/page-types.md
content: # Page Types Catalog

Extracted from the 40-slide H3C template. Use these patterns as building blocks.

## 1. Cover Page (Layout 6: 节标题)

**When**: First slide, section dividers.

```
Title:   centered, 36-48pt bold RED2
Subtitle: centered, 14-16pt DARK
Tagline:  centered, 10pt LIGHT
Author:   centered, 9pt LIGHT, near bottom
```

Position reference: title Y≈1.2-1.5", subtitle Y≈2.2", tagline Y≈3.2"

---

## 2. Left Text + Right Card (Layout 3)

**When**: Main content with supporting visual/data on the right.

```
Left column:  x=0.5", w=4.2"
  - Section header: 13pt bold RED3
  - Body bullets: 10pt DARK, spacing 4pt
  - Can have 2 sections vertically

Right card:   x=5.1", w=4.4"
  - Rounded rect, fill=BG, border=BORDER
  - Card title: 11pt bold RED, with divider line below
  - Card content: 10-11pt, generous spacing
```

Template examples: Slides 7, 12

---

## 3. Three-Column Cards (Layout 3)

**When**: 3 parallel concepts/features to compare.

```
Card 1: x=0.5",  w=2.85"
Card 2: x=3.5",  w=2.85"
Card 3: x=6.5",  w=2.85"

Each card:
  - Rounded rect, fill=BG, border=BORDER
  - Red header bar: full width, h=0.38", RED fill, white text 12pt bold centered
  - Bullet content: 10pt, left-aligned, 0.2" indent from card edge
```

---

## 4. Data Card Wall (Layout 3)

**When**: Multiple KPI numbers or stats to showcase.

```
Grid: 3 columns × 2 rows (or 2×2, 2×3)

Each cell:
  - Number: 10pt bold #4B4948
  - Label: 8-9pt #4B4948 below number
  - Optional icon to the left of number

Grid start: y=1.5"
Cell size: ~1.6" wide × 0.8" tall
```

Template example: Slide 8 (19,000+ / 13,700+ / 443.51亿元 etc.)

---

## 5. Timeline / Milestones (Layout 3)

**When**: Development history, roadmap, phases.

```
Vertical timeline (right card):
  For each milestone:
    - Date badge: RED rect, 0.8"×0.28", white text 8-9pt bold centered
    - Title: 11pt bold DARK, right of badge
    - Description: 8pt LIGHT, below title
    - Vertical spacing: 0.75-0.85" per item

Horizontal timeline (full width):
  - Red line spanning width
  - Circle/rect nodes at intervals
  - Labels above/below alternating
```

---

## 6. Comparison Table (Layout 3)

**When**: Feature comparison, product vs product.

```
Header row: RED fill, white text 9-10pt bold centered
Data rows:  alternating WHITE / BG
Border:     BORDER on all cells
Row height: 0.34-0.38"

Column 0 (labels): left-aligned, RED text, bold
Column 1+: center-aligned, DARK text

Position: start at y=1.1-1.2", span full width (0.5" to 9.5")
```

---

## 7. Four-Quadrant Matrix (Layout 3)

**When**: 2×2 categorization, SWOT, positioning.

```
Top-left:     x=0.5",  y=1.2", w=4.2", h=1.8"
Top-right:    x=5.1",  y=1.2", w=4.4", h=1.8"
Bottom-left:  x=0.5",  y=3.2", w=4.2", h=1.8"
Bottom-right: x=5.1",  y=3.2", w=4.4", h=1.8"

Each quadrant:
  - Title: 14pt bold RED (or one RED, three DARK for emphasis)
  - Body: 10pt DARK
  - Optional BG fill on cards
```

Template example: Slide 23

---

## 8. Full-Width Content (Layout 3)

**When**: Large diagram, org chart, single focused message.

```
Content area: x=0.5", y=1.1", w=9.0", h=3.8"
Single text block or embedded chart/image.
```

Template examples: Slides 25, 29, 31 (chart slides)

---

## 9. Left Image + Right Text (Layout 3)

**When**: Product photo, screenshot, illustration with description.

```
Image area:  x=0.5",  y=1.1", w=3.8", h=3.8"
Text area:   x=4.7",  y=1.1", w=4.8", h=3.8"
  - Title: 20pt bold or 14pt bold RED
  - Body: 10-11pt, line spacing 1.2
```

Template example: Slide 7 (left photo + right text)

---

## 10. Process Flow (Layout 3)

**When**: Step-by-step workflow, funnel, pipeline.

```
Horizontal flow:
  - 3-5 rounded rects in a row
  - Arrow connectors between them
  - Each box: title + 2-3 bullet points
  - Box width: ~1.6-2.0"
  - Row y: centered vertically ≈ 2.0-3.5"

Vertical flow:
  - Steps stacked with downward arrows
  - Left-aligned, indented descriptions
```

---

## 11. Quote / Key Message Page (Layout 6 or 3)

**When**: Ending slide, key takeaway, executive summary.

```
Large quote:  24-30pt bold, centered or left-aligned
Attribution:  10pt LIGHT below
Accent:       RED horizontal line above or below the quote
```

---

## 12. Analogy Cards (Layout 3, right card)

**When**: Drawing parallels, showing precedents.

```
Inside right card:
  For each analogy:
    - Rounded rect: WHITE with BORDER (normal) or RED fill (highlight)
    - Title: 12pt bold centered
    - Description: 9pt LIGHT centered below
    - Spacing: 0.95" per card vertically
```

---

## 13. Two-Column Numbered Cards (Layout 3)

**When**: Two parallel concepts, A vs B, product vs product overview.

**Source**: Learned from 两栏模板 Slide 1.

```
Card width:  4.15"    Card height: 3.2"
Left card:   x=0.60"  Right card:  x=5.25"
Gap:         0.50"
Card top:    1.6"

Number circle: 0.65" diameter, RED fill, white 20pt bold centered
  Position: card left + 0.15", card top − 0.25" (floats above card)

Title: 16pt bold RED, y = card_top + 0.55", indent 0.25" from card edge
Divider: BORDER line at card_top + 1.0", spans card width − 0.4"
Body: 11pt DARK, y = card_top + 1.15", indent 0.25"
```

Helper: `tb.two_col_numbered_cards(slide, [{num, title, body}, ...])`

---

## 14. Stacked Numbered Bars (Layout 3)

**When**: Two key points stacked vertically, each with a number + title + description.

**Source**: Learned from 两栏模板 Slide 4.

```
Bar width:   8.50"    Bar height: 1.75"
Bar x:       0.75"    Gap: 0.30"
Bar 1 top:   1.30"    Bar 2 top: 3.35"

Number: 28pt bold RED, left side (x + 0.2", 0.8" wide)
Title:  16pt bold DARK, x + 1.2", y + 0.2"
Body:   11pt MID, x + 1.2", y + 0.65"
```

Helper: `tb.two_row_numbered_bars(slide, [{num, title, body}, ...])`

---

## 15. Label Tab Rows (Layout 3)

**When**: Two items with prominent category labels, label-then-detail style.

**Source**: Learned from 两栏模板 Slide 5.

```
Row width:   8.50"    Row height: 1.75"
Row x:       0.75"    Gap: 0.30"

Label tab: 2.6" × 0.85", RED fill, white 14pt bold centered
  Position: row_x − 0.15" (overlaps left edge), vertically centered in row

Body text: starts at row_x + tab_w + 0.1", 11pt DARK
  Width: row_w − tab_w − 0.3"
```

Helper: `tb.two_row_label_tabs(slide, [{label, body}, ...])`

---

## 16. Circle Avatar Cards (Layout 3)

**When**: Team/department showcase, role-based comparison, person profiles.

**Source**: Learned from 两栏模板 Slide 3.

```
Card width:  4.15"    Card height: 2.8"
Left card:   x=0.60"  Right card:  x=5.25"
Card top:    2.0"

Circle: 1.2" diameter, RED3 fill, centered above card
  y = card_top − 0.6" (overlaps top edge)
  Contains: 11pt bold WHITE text (role/icon label)

Title: 16pt bold RED, y = card_top + 0.75"
Body:  11pt DARK, y = card_top + 1.2"
```

Helper: `tb.two_col_circle_cards(slide, [{icon_text, title, body}, ...])`

---

## 17. Left Text + Right Stacked Cards (Layout 3)

**When**: Narrative argument on left, supporting points as mini-cards on right.

**Source**: Combined pattern (original Left Text + Right Card + stacked bars).

```
Left column:  x=0.6", w=4.0"
  Title: 14pt bold RED3, y=1.2"
  Body:  11pt DARK, y=1.65", h=3.0" (multi-paragraph OK)

Right column: x=5.0"
  Mini card: 4.6" × 1.55", BG fill + BORDER
  Card 1: y=1.2"    Card 2: y=3.05"   Gap=0.30"
  Number: 22pt bold RED, left side (0.6" wide)
  Title:  13pt bold DARK, right of number
  Body:   10pt MID, below title
```

Helper: `tb.left_text_right_stacked_cards(slide, left_title, left_body, [{num, title, body}, ...])`

---

## 18. Three-Column Vertical Cards (Layout 3)

**When**: Three parallel concepts, features, or services to compare side by side.

**Source**: Learned from 三栏模板 Slide 4.

```
Card width:  2.85"    Card height: 3.7"
Card 1: x=0.50"   Card 2: x=3.70"   Card 3: x=6.90"
Gap: 0.35"         Card top: 1.2"

Title: 14pt bold RED, y = card_top + 0.15", indent 0.15"
Divider: BORDER line at card_top + 0.8"
Body: 10pt DARK, y = card_top + 0.95", fill remaining height
```

Helper: `tb.three_col_vertical_cards(slide, [{title, body}, ...])`

---

## 19. Three-Row Numbered Bars (Layout 3)

**When**: Three key points stacked vertically with numbered emphasis.

**Source**: Learned from 三栏模板 Slide 1.

```
Bar width:   8.80"    Bar height: 1.15"
Bar x:       0.60"    Gap: 0.20"

Number circle: 0.50" diameter, RED fill, white 18pt bold centered
  Position: left side, vertically centered
Title: 14pt bold DARK, x + 0.85", y + 0.10"
Body:  10pt MID, x + 0.85", y + 0.45"
```

Helper: `tb.three_row_numbered_bars(slide, [{num, title, body}, ...])`

---

## 20. Three-Row Label Bars (Layout 3)

**When**: Three items with left-side category labels, like a mini-spec sheet.

**Source**: Learned from 三栏模板 Slide 6.

```
Label: 1.20" × 1.15", RED fill, white 14pt bold centered
Body area: 7.60" wide, BG fill + BORDER
Row x: 0.50"   Gap: 0.15" (tight stacking)
```

Helper: `tb.three_row_label_bars(slide, [{label, body}, ...])`

---

## 21. Three-Column Icon Cards (Layout 3)

**When**: Three features/services with icon badges and descriptions.

**Source**: Learned from 三栏模板 Slide 7 (adapted to H3C colors).

```
Card width:  2.85"    Card height: 3.7"
Same positions as #18

Icon circle: 0.70" diameter, RED fill, centered in card top
  Contains: 11pt bold WHITE text (short label or emoji stand-in)
Title: 13pt bold RED, centered, y = card_top + 1.1"
Body: 10pt DARK, y = card_top + 1.55"
```

Helper: `tb.three_col_icon_cards(slide, [{icon_text, title, body}, ...])`

---

## Choosing the Right Page Type

**Four-Column Layout Variants (from 四栏模板)**:

### 22. Four-Column Numbered Circles (Layout 3)

**When**: Four items arranged horizontally with numbered emphasis.

**Source**: Learned from 四栏模板 Slide 1.

```
Column width: 2.1"     Gap: 0.35"
Columns: x=0.50, 2.95, 5.40, 7.85"

Number circle: 0.6" diameter, RED fill, centered in column
Title: 12pt bold RED, centered above circle
Body: 10pt DARK, below circle
```

Helper: `tb.four_col_numbered_circles(slide, [{num, title, body}, ...])`

---

### 23. Four-Column Tall Cards (Layout 3)

**When**: Four parallel features/services with icon + title + body.

**Source**: Learned from 四栏模板 Slide 3.

```
Card: 2.1" × 3.3"    Gap: 0.35"
Icon circle: 0.6" diameter, RED fill, top center of card
Title: 12pt bold RED, centered
Divider: below title
Body: 9pt DARK
```

Helper: `tb.four_col_tall_cards(slide, [{title, body}, ...])`

---

### 24. Four-Row Badge Bars (Layout 3)

**When**: Four items with left badge/label + description (like PEST analysis).

**Source**: Learned from 四栏模板 Slide 4 & 6.

```
Badge: 1.5" × 0.5", RED fill, white 11pt bold centered
Title: 12pt bold DARK, right of badge
Body: 10pt MID, below title
Row height: 0.9"    Gap: 0.15"
```

Helper: `tb.four_row_badge_bars(slide, [{badge, title, body}, ...])`

---

### 25. Four-Column Ascending Steps (Layout 3)

**When**: Four progressive stages or levels, ascending from left to right.

**Source**: Learned from 四栏模板 Slide 5.

```
Column width: 2.1"    Gap: 0.35"
Step Y offset: 0.6" per column (rightmost is highest)
Card height: grows with step (1.2" to 2.1")
Dot: 0.12" RED circle below each card
Title: 12pt bold RED    Body: 9pt DARK
```

Helper: `tb.four_col_ascending_steps(slide, [{title, body}, ...])`

---

### 26. Timeline Horizontal Ascending (Layout 3)

**When**: Company history, product roadmap, multi-year milestones.

**Source**: Learned from 时间线流程图 Slide 1.

```
Three nodes ascending left-to-right:
  Node 1: x=1.5", y=3.5"  Node 2: x=4.2", y=2.7"  Node 3: x=6.9", y=1.9"

Year badge: 1.0" × 0.32", RED fill, white bold text
Vertical line: 0.02" wide, 1.2" tall, BORDER color
Title: 14pt bold DARK    Body: 10pt MID
Base bar: 9.0" × 0.5" at y=4.6", BG fill
```

Helper: `tb.timeline_horizontal_ascending(slide, [{year, title, body}, ...])`

---

### 27. Process Flow Arrows (Layout 3)

**When**: Three-step workflow, pipeline, sequential process.

**Source**: Learned from 时间线流程图 Slide 2.

```
Three arrow shapes at y=3.0", 2.5" wide, gap=0.5"
Number: 16pt bold WHITE centered in arrow
Vertical line up to content area
Title: 13pt bold RED    Body: 10pt DARK
```

Helper: `tb.process_flow_arrows(slide, [{num, title, body}, ...])`

---

### 28. Process Three Stage (Layout 3)

**When**: Three major phases/milestones with prominent numbering.

**Source**: Learned from 时间线流程图 Slide 3.

```
Three columns, 2.8" wide, gap=0.55"
Large number card: 2.0" × 1.3" at bottom, RED fill, 32pt bold WHITE
Dot: 0.2" RED circle at top of vertical line
Vertical line: BORDER, connects dot to number card
Title: 16pt bold RED    Body: 10pt DARK
```

Helper: `tb.process_three_stage(slide, [{num, title, body}, ...])`

---

## Choosing the Right Page Type (Final)

| Content Type | Recommended Page Type |
|---|---|
| Opening / section break | Cover Page |
| Narrative + supporting data | Left Text + Right Card |
| 3 parallel concepts | Three-Column Cards |
| KPI numbers | Data Card Wall |
| History / roadmap | Timeline |
| Product comparison | Comparison Table |
| 2×2 categorization | Four-Quadrant Matrix |
| Large visual / chart | Full-Width Content |
| Screenshot + explanation | Left Image + Right Text |
| Workflow / steps | Process Flow |
| Key takeaway | Quote / Key Message |
| Historical parallels | Analogy Cards |
| **A vs B parallel comparison** | **Two-Column Numbered Cards** |
| **Two key points, stacked** | **Stacked Numbered Bars** |
| **Category label + detail** | **Label Tab Rows** |
| **Team / role showcase** | **Circle Avatar Cards** |
| **Argument + supporting points** | **Left Text + Right Stacked Cards** |
| **3 parallel features (tall cards)** | **Three-Column Vertical Cards** |
| **3 key points, stacked** | **Three-Row Numbered Bars** |
| **3 categories with labels** | **Three-Row Label Bars** |
| **3 features with icons** | **Three-Column Icon Cards** |
| **4 items with numbers** | **Four-Column Numbered Circles** |
| **4 parallel features (tall)** | **Four-Column Tall Cards** |
| **4 rows with badges** | **Four-Row Badge Bars** |
| **4 ascending steps** | **Four-Column Ascending Steps** |
| **Timeline with years** | **Timeline Horizontal Ascending** |
| **3-step process flow** | **Process Flow Arrows** |
| **3-stage milestone** | **Process Three Stage** |


path: scripts/icon_library.py
content: #!/usr/bin/env python
# -*- coding: utf-8 -*-
"""icon_library.py — Icon search, copy, and recolor engine.

Usage:
    from icon_library import IconLibrary
    lib = IconLibrary()
    results = lib.search('服务器', limit=3)
    lib.copy_icon(slide, results[0], x=1.0, y=1.0, size=0.6, color='FFFFFF')
"""
import json, copy, re, os
from pathlib import Path
from pptx import Presentation
from pptx.util import Inches, Emu
from pptx.oxml.ns import qn
from lxml import etree


class IconLibrary:
    """Search, copy, and recolor icons from the icon library PPTX."""

    def __init__(self):
        base = Path(__file__).parent.parent / 'assets'
        self._pptx_path = base / 'icons-library.pptx'
        self._index_path = base / 'icon-index.json'
        
        with open(self._index_path, 'r', encoding='utf-8') as f:
            self._index = json.load(f)
        
        self._prs = Presentation(str(self._pptx_path))
        self._slide = self._prs.slides[0]
        self._shapes = list(self._slide.shapes)

    def search(self, query, category=None, limit=5):
        """Search icons by keyword (Chinese or English) and optional category.
        Returns list of icon entries sorted by relevance.
        """
        query = query.lower().strip()
        results = []
        
        for entry in self._index:
            score = 0
            name = entry['name'].lower()
            tags_cn = ' '.join(entry.get('tags_cn', []))
            tags_en = ' '.join(entry.get('tags_en', [])).lower()
            cat = entry.get('category', '')
            
            # Category filter
            if category and cat != category:
                continue
            
            # Exact name match
            if query in name:
                score += 10
            
            # Tag match (Chinese)
            if query in tags_cn:
                score += 8
            
            # Tag match (English)
            if query in tags_en:
                score += 8
            
            # Category match
            if query in cat:
                score += 5
            
            # Partial match
            for word in query.split():
                if word in name:
                    score += 3
                if word in tags_cn or word in tags_en:
                    score += 2
            
            if score > 0:
                results.append((score, entry))
        
        results.sort(key=lambda x: -x[0])
        return [r[1] for r in results[:limit]]

    def copy_icon(self, dst_slide, icon_entry, x, y, size=0.5, color=None):
        """Copy an icon to a destination slide, centered in a size x size box at (x, y).
        
        Args:
            dst_slide: target slide object
            icon_entry: dict from search results
            x, y: top-left of the bounding box in inches
            size: bounding box dimension in inches (icon is centered within)
            color: optional hex color string (e.g. 'FFFFFF' for white).
                   If set, all fills in the icon are replaced with this color.
        
        Returns: the new XML element
        """
        idx = icon_entry['shape_idx']
        src_shape = self._shapes[idx]
        
        # Deep copy XML
        new_el = copy.deepcopy(src_shape._element)
        
        # Scale to fit within size x size box, preserving aspect ratio
        orig_w = src_shape.width
        orig_h = src_shape.height
        target = Inches(size)
        scale = target / max(orig_w, orig_h)
        new_w = int(orig_w * scale)
        new_h = int(orig_h * scale)
        
        # Center within the bounding box
        offset_x = Inches(x) + (target - new_w) // 2
        offset_y = Inches(y) + (target - new_h) // 2
        
        # Position and resize
        is_group = src_shape.shape_type == 6
        if is_group:
            pr = new_el.find(qn('p:grpSpPr'))
        else:
            pr = new_el.find(qn('p:spPr'))
        
        if pr is not None:
            xfrm = pr.find(qn('a:xfrm'))
            if xfrm is not None:
                off = xfrm.find(qn('a:off'))
                ext = xfrm.find(qn('a:ext'))
                if off is not None:
                    off.set('x', str(offset_x))
                    off.set('y', str(offset_y))
                if ext is not None:
                    ext.set('cx', str(new_w))
                    ext.set('cy', str(new_h))
        
        # Recolor if requested
        if color:
            self._recolor(new_el, color)
        
        # Add to slide
        sp_tree = dst_slide._element.find(qn('p:cSld')).find(qn('p:spTree'))
        sp_tree.append(new_el)
        return new_el

    def _recolor(self, el, hex_color):
        """Replace all solidFill colors in an element tree."""
        for fill in el.iter(qn('a:solidFill')):
            for child in list(fill):
                fill.remove(child)
            srgb = etree.SubElement(fill, qn('a:srgbClr'))
            srgb.set('val', hex_color)

    def list_categories(self):
        """List all icon categories with counts."""
        from collections import Counter
        cats = Counter(e.get('category', '未分类') for e in self._index)
        return dict(cats.most_common())

    def __len__(self):
        return len(self._index)


path: scripts/ppt_helpers.py
content: #!/usr/bin/env python
# -*- coding: utf-8 -*-
"""ppt_helpers.py — Reusable helpers for the light corporate template.

Usage:
    from ppt_helpers import TemplateBuilder
    tb = TemplateBuilder()
    s = tb.add_content_slide("Page Title")
    tb.text(s, 0.5, 1.2, 4.2, 0.3, "Hello", sz=13, color=tb.RED3, bold=True)
    tb.save("output.pptx")
"""
import sys, os
from pathlib import Path
from pptx import Presentation
from pptx.util import Inches, Pt, Emu
from pptx.dml.color import RGBColor
from pptx.enum.text import PP_ALIGN
from pptx.enum.shapes import MSO_SHAPE
from pptx.oxml.ns import qn
from pptx.oxml import parse_xml


class TemplateBuilder:
    """Build PPTs from the light corporate template."""

    # ── Colors ───────────────────────────────────────────
    RED    = RGBColor(0xC0, 0x00, 0x00)
    RED2   = RGBColor(0xEA, 0x00, 0x00)
    RED3   = RGBColor(0xD8, 0x22, 0x2A)
    DARK   = RGBColor(0x34, 0x34, 0x34)
    MID    = RGBColor(0x4B, 0x49, 0x48)
    LIGHT  = RGBColor(0xA0, 0xA0, 0xA0)
    BG     = RGBColor(0xF1, 0xF1, 0xF1)
    BORDER = RGBColor(0xE7, 0xE7, 0xE7)
    WHITE  = RGBColor(0xFF, 0xFF, 0xFF)
    ACCENT_GREY = RGBColor(0x6B, 0x6B, 0x6B)  # warm grey for alternating accents
    CORAL  = RGBColor(0xE0, 0x6C, 0x5C)         # soft coral red
    DARK_RED = RGBColor(0x8B, 0x1A, 0x1A)        # dark red
    LIGHT_RED = RGBColor(0xF2, 0xD5, 0xD5)       # light red tint
    LIGHT_RED2 = RGBColor(0xF5, 0xE0, 0xE0)      # lighter red tint
    FONT   = "微软雅黑"

    def __init__(self, template_path=None):
        if template_path is None:
            skill_dir = Path(__file__).parent.parent
            template_path = skill_dir / "assets" / "template-light.pptx"
        self.prs = Presentation(str(template_path))
        self._clear_slides()
        self._icon_lib = None  # lazy-loaded

    @property
    def icons(self):
        """Lazy-load the icon library."""
        if self._icon_lib is None:
            from icon_library import IconLibrary
            self._icon_lib = IconLibrary()
        return self._icon_lib

    def place_icon(self, slide, query, x, y, size=0.5, color=None, category=None):
        """Search for an icon and place it on the slide.
        color: hex string e.g. 'FFFFFF' for white on dark backgrounds.
        Returns icon entry if found, None otherwise.
        """
        results = self.icons.search(query, category=category, limit=1)
        if results:
            self.icons.copy_icon(slide, results[0], x, y, size, color=color)
            return results[0]
        return None

    def _clear_slides(self):
        """Remove all demo slides from template."""
        ns = '{http://schemas.openxmlformats.org/officeDocument/2006/relationships}'
        while len(self.prs.slides._sldIdLst) > 0:
            el = self.prs.slides._sldIdLst[0]
            rId = el.get(f'{ns}id') or el.attrib.get('r:id')
            if rId:
                try: self.prs.part.drop_rel(rId)
                except: pass
            self.prs.slides._sldIdLst.remove(el)

    # ── Slide creation ───────────────────────────────────
    def add_content_slide(self, title=None):
        """Add a content slide (Layout 3: 绝密-仅标题)."""
        s = self.prs.slides.add_slide(self.prs.slide_layouts[3])
        if title:
            self._page_title(s, title)
        return s

    def add_cover_slide(self):
        """Add a cover/section slide (Layout 6: 节标题)."""
        return self.prs.slides.add_slide(self.prs.slide_layouts[6])

    def add_two_column_slide(self, title=None):
        """Add a two-column slide (Layout 1: 绝密-两栏内容)."""
        s = self.prs.slides.add_slide(self.prs.slide_layouts[1])
        if title:
            self._page_title(s, title)
        return s

    def add_comparison_slide(self, title=None):
        """Add a comparison slide (Layout 2: 绝密-比较)."""
        s = self.prs.slides.add_slide(self.prs.slide_layouts[2])
        if title:
            self._page_title(s, title)
        return s

    # ── Page title ───────────────────────────────────────
    def _page_title(self, slide, title):
        # Auto-adjust: wider box + shrink font for long titles
        title_w = 7.8  # nearly full width (was 4.2")
        title_h = 0.4
        # Estimate chars that fit: ~3.5 CJK chars per inch at 20pt
        max_chars_20pt = int(title_w * 3.5)
        if len(title) > max_chars_20pt:
            sz = 16  # shrink for very long titles
        elif len(title) > int(title_w * 2.8):
            sz = 18  # slightly shrink
        else:
            sz = 20  # default
        box = self.text(slide, 1.1, 0.4, title_w, title_h, title,
                        sz=sz, color=self.RED, bold=True)
        # Zero insets to match template
        self._zero_insets(box)
        self._set_line_spacing(box.text_frame.paragraphs[0], 120)
        return box

    # ── Shape helpers ────────────────────────────────────
    def rect(self, slide, l, t, w, h, color, border=None):
        sh = slide.shapes.add_shape(
            MSO_SHAPE.RECTANGLE, Inches(l), Inches(t), Inches(w), Inches(h))
        sh.fill.solid(); sh.fill.fore_color.rgb = color
        if border:
            sh.line.color.rgb = border; sh.line.width = Pt(0.5)
        else:
            sh.line.fill.background()
        return sh

    def rounded_rect(self, slide, l, t, w, h, color, border=None):
        sh = slide.shapes.add_shape(
            MSO_SHAPE.ROUNDED_RECTANGLE, Inches(l), Inches(t), Inches(w), Inches(h))
        sh.fill.solid(); sh.fill.fore_color.rgb = color
        if border:
            sh.line.color.rgb = border; sh.line.width = Pt(0.5)
        else:
            sh.line.fill.background()
        return sh

    def card(self, slide, l, t, w, h):
        """Standard card: BG fill + BORDER outline."""
        return self.rounded_rect(slide, l, t, w, h, self.BG, border=self.BORDER)

    def divider(self, slide, l, t, w, color=None):
        """Thin horizontal line."""
        color = color or self.BORDER
        return self.rect(slide, l, t, w, 0.005, color)

    # ── Text helpers ─────────────────────────────────────
    def text(self, slide, l, t, w, h, text, sz=11, color=None,
             bold=False, align=PP_ALIGN.LEFT, shrink=False):
        color = color or self.DARK
        box = slide.shapes.add_textbox(
            Inches(l), Inches(t), Inches(w), Inches(h))
        tf = box.text_frame; tf.word_wrap = True
        # Auto-shrink text to fit box when enabled
        if shrink:
            from pptx.enum.text import MSO_AUTO_SIZE
            tf.auto_size = MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE
        p = tf.paragraphs[0]
        p.text = text
        p.font.size = Pt(sz); p.font.color.rgb = color
        p.font.bold = bold
        # Force BOTH latin and East Asian font on ALL runs
        from lxml.etree import SubElement
        from pptx.oxml.ns import qn
        for run in p.runs:
            rPr = run._r.get_or_add_rPr()
            # Latin font
            latin = rPr.find(qn('a:latin'))
            if latin is None:
                latin = SubElement(rPr, qn('a:latin'))
            latin.set('typeface', self.FONT)
            # East Asian font
            ea = rPr.find(qn('a:ea'))
            if ea is None:
                ea = SubElement(rPr, qn('a:ea'))
            ea.set('typeface', self.FONT)
        p.alignment = align
        self._set_line_spacing(p, 120)
        return box

    def multi_text(self, slide, l, t, w, h, lines, sz=10, color=None, sp=4):
        """Multi-line text box.
        lines: list of str or (str, dict) where dict overrides: sz, color, bold, align, sa
        """
        color = color or self.DARK
        box = slide.shapes.add_textbox(
            Inches(l), Inches(t), Inches(w), Inches(h))
        tf = box.text_frame; tf.word_wrap = True
        for i, item in enumerate(lines):
            txt, ov = (item, {}) if isinstance(item, str) else (item[0], item[1])
            p = tf.paragraphs[0] if i == 0 else tf.add_paragraph()
            p.text = txt
            p.font.size = Pt(ov.get("sz", sz))
            p.font.color.rgb = ov.get("color", color)
            p.font.bold = ov.get("bold", False)
            p.font.name = ov.get("font", self.FONT)
            # Force BOTH latin and East Asian font on ALL runs
            from lxml.etree import SubElement
            from pptx.oxml.ns import qn
            for run in p.runs:
                rPr = run._r.get_or_add_rPr()
                latin = rPr.find(qn('a:latin'))
                if latin is None:
                    latin = SubElement(rPr, qn('a:latin'))
                latin.set('typeface', ov.get("font", self.FONT))
                ea = rPr.find(qn('a:ea'))
                if ea is None:
                    ea = SubElement(rPr, qn('a:ea'))
                ea.set('typeface', ov.get("font", self.FONT))
            p.alignment = ov.get("align", PP_ALIGN.LEFT)
            p.space_after = Pt(ov.get("sa", sp))
            self._set_line_spacing(p, 120)
        return box

    def section_header(self, slide, l, t, text, w=4.2):
        """Red section sub-header."""
        return self.text(slide, l, t, w, 0.3, text,
                         sz=13, color=self.RED3, bold=True)

    # ── Table helpers ────────────────────────────────────
    def table_header(self, slide, x0, y0, headers, col_widths, row_height=0.36):
        """Red header row for a comparison table."""
        x = x0
        for j, h in enumerate(headers):
            self.rect(slide, x, y0, col_widths[j], row_height, self.RED)
            self.text(slide, x, y0, col_widths[j], row_height,
                      h, sz=10, color=self.WHITE, bold=True, align=PP_ALIGN.CENTER)
            x += col_widths[j]

    def table_row(self, slide, x0, y, cells, col_widths, row_height=0.36,
                  bg=None, is_label_col=True):
        """Single data row. First column treated as label (RED, bold, left-align)."""
        bg = bg or self.WHITE
        x = x0
        for j, cell in enumerate(cells):
            self.rect(slide, x, y, col_widths[j], row_height, bg, border=self.BORDER)
            self.text(slide, x + 0.08, y, col_widths[j] - 0.16, row_height,
                      cell, sz=10,
                      color=self.RED if (j == 0 and is_label_col) else self.DARK,
                      bold=(j == 0 and is_label_col),
                      align=PP_ALIGN.CENTER if j > 0 else PP_ALIGN.LEFT)
            x += col_widths[j]

    # ── Two-Column Layout helpers (learned from 两栏模板) ──

    def two_col_numbered_cards(self, slide, items, card_top=1.6):
        """Side-by-side cards with numbered circles.
        items: list of 2 dicts: {num, title, body}
        Learned from: 两栏模板 Slide 1
        """
        card_w, card_h = 4.15, 3.2
        positions = [(0.6, items[0]), (5.25, items[1])]
        for x, item in positions:
            # Card background
            self.card(slide, x, card_top, card_w, card_h)
            # Number circle
            c = slide.shapes.add_shape(
                MSO_SHAPE.OVAL,
                Inches(x + 0.15), Inches(card_top - 0.25),
                Inches(0.65), Inches(0.65))
            c.fill.solid(); c.fill.fore_color.rgb = self.RED
            c.line.fill.background()
            self.text(slide, x + 0.15, card_top - 0.22, 0.65, 0.65,
                      item['num'], sz=20, color=self.WHITE, bold=True,
                      align=PP_ALIGN.CENTER)
            # Title
            self.text(slide, x + 0.25, card_top + 0.55, card_w - 0.5, 0.35,
                      item['title'], sz=16, color=self.RED, bold=True)
            # Divider line
            self.divider(slide, x + 0.2, card_top + 1.0, card_w - 0.4)
            # Body
            self.text(slide, x + 0.25, card_top + 1.15, card_w - 0.5, 1.8,
                      item['body'], sz=11, color=self.DARK,
                      shrink=True)

    def two_row_numbered_bars(self, slide, items, bar_top=1.3):
        """Stacked horizontal bars with left-side numbers.
        items: list of 2 dicts: {num, title, body}
        Learned from: 两栏模板 Slide 4
        """
        bar_w, bar_h = 8.5, 1.75
        bar_x = 0.75
        gap = 0.3
        for i, item in enumerate(items[:2]):
            y = bar_top + i * (bar_h + gap)
            self.card(slide, bar_x, y, bar_w, bar_h)
            self.text(slide, bar_x + 0.2, y + 0.35, 0.8, 0.7,
                      item['num'], sz=28, color=self.RED, bold=True,
                      align=PP_ALIGN.CENTER)
            self.text(slide, bar_x + 1.2, y + 0.2, bar_w - 1.6, 0.35,
                      item['title'], sz=16, color=self.DARK, bold=True)
            self.text(slide, bar_x + 1.2, y + 0.65, bar_w - 1.6, 1.0,
                      item['body'], sz=11, color=self.MID,
                      shrink=True)

    def two_row_label_tabs(self, slide, items, row_top=1.3):
        """Full-width rows with colored label tabs on the left.
        items: list of 2 dicts: {label, body}
        Learned from: 两栏模板 Slide 5
        """
        row_w, row_h = 8.5, 1.75
        row_x = 0.75
        tab_w, tab_h = 2.6, 0.85
        gap = 0.3
        for i, item in enumerate(items[:2]):
            y = row_top + i * (row_h + gap)
            self.card(slide, row_x, y, row_w, row_h)
            # Red label tab
            self.rounded_rect(slide, row_x - 0.15,
                              y + (row_h - tab_h) / 2,
                              tab_w, tab_h, self.RED)
            self.text(slide, row_x - 0.15,
                      y + (row_h - tab_h) / 2 + 0.15,
                      tab_w, tab_h - 0.3,
                      item['label'], sz=14, color=self.WHITE, bold=True,
                      align=PP_ALIGN.CENTER)
            # Body text
            self.text(slide, row_x + tab_w + 0.1, y + 0.25,
                      row_w - tab_w - 0.3, row_h - 0.5,
                      item['body'], sz=11, color=self.DARK,
                      shrink=True)

    def two_col_circle_cards(self, slide, items, card_top=2.0):
        """Left-right cards with circle avatars/icons floating above.
        items: list of 2 dicts: {icon_text, title, body}
        Learned from: 两栏模板 Slide 3
        """
        card_w, card_h = 4.15, 2.8
        positions = [(0.6, items[0]), (5.25, items[1])]
        circle_size = 1.2
        for x, item in positions:
            self.card(slide, x, card_top, card_w, card_h)
            cx = x + (card_w - circle_size) / 2
            cy = card_top - circle_size * 0.5
            c = slide.shapes.add_shape(
                MSO_SHAPE.OVAL,
                Inches(cx), Inches(cy),
                Inches(circle_size), Inches(circle_size))
            c.fill.solid(); c.fill.fore_color.rgb = self.RED3
            c.line.fill.background()
            self.text(slide, cx, cy + 0.25, circle_size, circle_size - 0.5,
                      item['icon_text'], sz=11, color=self.WHITE, bold=True,
                      align=PP_ALIGN.CENTER)
            self.text(slide, x + 0.25, card_top + 0.75, card_w - 0.5, 0.35,
                      item['title'], sz=16, color=self.RED, bold=True)
            self.text(slide, x + 0.25, card_top + 1.2, card_w - 0.5, 1.4,
                      item['body'], sz=11, color=self.DARK,
                      shrink=True)

    def left_text_right_stacked_cards(self, slide, left_title, left_body,
                                       cards, left_x=0.6, right_x=5.0):
        """Left narrative text + right stacked mini cards.
        cards: list of 2-3 dicts: {num, title, body}
        Left text uses larger font to fill the space proportionally.
        """
        n = min(len(cards), 3)
        # Left text — large title + generous body
        self.text(slide, left_x, 1.2, 4.0, 0.4,
                  left_title, sz=18, color=self.RED3, bold=True)
        self.text(slide, left_x, 1.75, 4.0, 3.0,
                  left_body, sz=13, color=self.DARK,
                  shrink=True)

        # Right stacked cards — adaptive height
        total_h = 3.6  # available height
        gap = 0.2
        mini_w = 4.6
        mini_h = (total_h - (n - 1) * gap) / n

        for i, card in enumerate(cards[:n]):
            y = 1.2 + i * (mini_h + gap)
            self.card(slide, right_x, y, mini_w, mini_h)
            # Number
            self.text(slide, right_x + 0.15, y + 0.1, 0.6, 0.45,
                      card['num'], sz=20, color=self.RED, bold=True,
                      align=PP_ALIGN.CENTER)
            # Title
            self.text(slide, right_x + 0.8, y + 0.1, mini_w - 1.0, 0.28,
                      card['title'], sz=12, color=self.DARK, bold=True)
            # Body
            body_top = y + 0.42
            body_h = mini_h - 0.52
            self.text(slide, right_x + 0.8, body_top, mini_w - 1.0, body_h,
                      card['body'], sz=10, color=self.MID,
                      shrink=True)

    def three_col_vertical_cards(self, slide, items, card_top=1.2):
        """Three tall vertical cards side by side.
        items: list of 3 dicts: {title, body}
        Learned from: 三栏模板 Slide 4
        """
        card_w, card_h = 2.85, 3.7
        gap = 0.35
        start_x = (10.0 - (3 * card_w + 2 * gap)) / 2  # centered
        for i, item in enumerate(items[:3]):
            x = start_x + i * (card_w + gap)
            self.card(slide, x, card_top, card_w, card_h)
            # Title
            self.text(slide, x + 0.15, card_top + 0.15, card_w - 0.3, 0.55,
                      item['title'], sz=14, color=self.RED, bold=True)
            # Divider
            self.divider(slide, x + 0.15, card_top + 0.8, card_w - 0.3)
            # Body
            self.text(slide, x + 0.15, card_top + 0.95, card_w - 0.3, card_h - 1.2,
                      item['body'], sz=10, color=self.DARK,
                      shrink=True)

    def three_row_numbered_bars(self, slide, items, bar_top=1.1):
        """Three stacked full-width bars with numbered circles.
        items: list of 3 dicts: {num, title, body}
        Learned from: 三栏模板 Slide 1
        """
        bar_w, bar_h = 8.8, 1.15
        bar_x = 0.6
        gap = 0.2
        for i, item in enumerate(items[:3]):
            y = bar_top + i * (bar_h + gap)
            self.card(slide, bar_x, y, bar_w, bar_h)
            # Number circle
            c = slide.shapes.add_shape(
                MSO_SHAPE.OVAL,
                Inches(bar_x + 0.15), Inches(y + (bar_h - 0.5) / 2),
                Inches(0.5), Inches(0.5))
            c.fill.solid(); c.fill.fore_color.rgb = self.RED
            c.line.fill.background()
            self.text(slide, bar_x + 0.15, y + (bar_h - 0.5) / 2 + 0.02,
                      0.5, 0.5, item['num'],
                      sz=18, color=self.WHITE, bold=True, align=PP_ALIGN.CENTER)
            # Title
            self.text(slide, bar_x + 0.85, y + 0.1, bar_w - 1.1, 0.3,
                      item['title'], sz=14, color=self.DARK, bold=True)
            # Body
            self.text(slide, bar_x + 0.85, y + 0.45, bar_w - 1.1, 0.65,
                      item['body'], sz=10, color=self.MID,
                      shrink=True)

    def three_row_label_bars(self, slide, items, row_top=1.1):
        """Three stacked rows with left colored label + right body text.
        items: list of 3 dicts: {label, body}
        Learned from: 三栏模板 Slide 6
        """
        label_w, label_h = 1.2, 1.15
        body_w = 7.6
        row_x = 0.5
        gap = 0.15
        for i, item in enumerate(items[:3]):
            y = row_top + i * (label_h + gap)
            # Label shape
            self.rounded_rect(slide, row_x, y, label_w, label_h, self.RED)
            self.text(slide, row_x, y + (label_h - 0.35) / 2, label_w, 0.35,
                      item['label'], sz=14, color=self.WHITE, bold=True,
                      align=PP_ALIGN.CENTER)
            # Body area
            self.card(slide, row_x + label_w + 0.1, y, body_w, label_h)
            self.text(slide, row_x + label_w + 0.25, y + 0.15,
                      body_w - 0.3, label_h - 0.3,
                      item['body'], sz=11, color=self.DARK,
                      shrink=True)

    def three_col_icon_cards(self, slide, items, card_top=1.2):
        """Three columns with icon circle + title + body.
        items: list of 3 dicts: {icon_text, title, body}
        Learned from: 三栏模板 Slide 7 (adapted to H3C style)
        """
        card_w, card_h = 2.85, 3.7
        gap = 0.35
        start_x = (10.0 - (3 * card_w + 2 * gap)) / 2  # centered
        for i, item in enumerate(items[:3]):
            x = start_x + i * (card_w + gap)
            self.card(slide, x, card_top, card_w, card_h)
            # Icon circle at top center
            cx = x + (card_w - 0.7) / 2
            c = slide.shapes.add_shape(
                MSO_SHAPE.OVAL,
                Inches(cx), Inches(card_top + 0.2),
                Inches(0.7), Inches(0.7))
            c.fill.solid(); c.fill.fore_color.rgb = self.RED
            c.line.fill.background()
            self.text(slide, cx, card_top + 0.3, 0.7, 0.5,
                      item['icon_text'], sz=11, color=self.WHITE, bold=True,
                      align=PP_ALIGN.CENTER)
            # Title below icon
            self.text(slide, x + 0.15, card_top + 1.1, card_w - 0.3, 0.35,
                      item['title'], sz=13, color=self.RED, bold=True,
                      align=PP_ALIGN.CENTER)
            # Body
            self.text(slide, x + 0.15, card_top + 1.55, card_w - 0.3, card_h - 1.8,
                      item['body'], sz=10, color=self.DARK,
                      shrink=True)

    # ── Four-Column Layout helpers (learned from 四栏模板) ──

    def four_col_tall_cards(self, slide, items, card_top=1.2):
        """Four tall vertical cards with icon circle + title + divider + body.
        items: list of 4 dicts: {title, body}
        Learned from: 四栏模板 Slide 3
        """
        card_w, card_h = 2.1, 3.3
        gap = 0.35
        start_x = (10.0 - (4 * card_w + 3 * gap)) / 2  # centered
        for i, item in enumerate(items[:4]):
            x = start_x + i * (card_w + gap)
            self.card(slide, x, card_top, card_w, card_h)
            # Icon circle at top center
            cx = x + (card_w - 0.6) / 2
            c = slide.shapes.add_shape(
                MSO_SHAPE.OVAL,
                Inches(cx), Inches(card_top + 0.15),
                Inches(0.6), Inches(0.6))
            c.fill.solid(); c.fill.fore_color.rgb = self.RED
            c.line.fill.background()
            # Title — auto-size height based on line count
            title_text = item['title']
            title_lines = title_text.count('\n') + 1
            title_h = 0.25 * title_lines + 0.1  # ~0.35 for 1 line, ~0.6 for 2
            title_y = card_top + 0.9
            self.text(slide, x + 0.1, title_y, card_w - 0.2, title_h,
                      title_text, sz=12, color=self.RED, bold=True,
                      align=PP_ALIGN.CENTER)
            # Divider — below title with gap
            div_y = title_y + title_h + 0.08
            self.divider(slide, x + 0.1, div_y, card_w - 0.2)
            # Body — below divider
            body_y = div_y + 0.12
            body_h = card_top + card_h - body_y - 0.15
            self.text(slide, x + 0.1, body_y, card_w - 0.2, body_h,
                      item['body'], sz=10, color=self.DARK,
                      shrink=True)

    def four_row_badge_bars(self, slide, items, row_top=1.1):
        """Four rows with left badge/label + right body text.
        items: list of 4 dicts: {badge, title, body}
        Learned from: 四栏模板 Slide 4
        """
        badge_w, badge_h = 1.5, 0.5
        body_w = 7.0
        row_x = 0.6
        row_h = 0.9
        gap = 0.15
        for i, item in enumerate(items[:4]):
            y = row_top + i * (row_h + gap)
            # Badge
            self.rounded_rect(slide, row_x, y + (row_h - badge_h) / 2,
                              badge_w, badge_h, self.RED)
            self.text(slide, row_x, y + (row_h - badge_h) / 2 + 0.05,
                      badge_w, badge_h - 0.1,
                      item['badge'], sz=11, color=self.WHITE, bold=True,
                      align=PP_ALIGN.CENTER)
            # Title + body
            self.text(slide, row_x + badge_w + 0.3, y + 0.05,
                      body_w, 0.3,
                      item['title'], sz=12, color=self.DARK, bold=True)
            self.text(slide, row_x + badge_w + 0.3, y + 0.4,
                      body_w, row_h - 0.45,
                      item['body'], sz=10, color=self.MID,
                      shrink=True)

    def four_col_ascending_steps(self, slide, items, base_top=1.2):
        """Four columns in ascending step pattern (left to right, bottom to top).
        items: list of 4 dicts: {title, body}
        Learned from: 四栏模板 Slide 5
        """
        col_w = 2.1
        start_x = 0.5
        gap = 0.35
        step_y = 0.6  # each column is higher than the previous
        for i, item in enumerate(items[:4]):
            x = start_x + i * (col_w + gap)
            y = base_top + (3 - i) * step_y  # ascending: rightmost is highest
            card_h = 1.2 + i * 0.3  # taller cards as they go up
            self.card(slide, x, y, col_w, card_h)
            self.text(slide, x + 0.1, y + 0.1, col_w - 0.2, 0.3,
                      item['title'], sz=12, color=self.RED, bold=True)
            self.text(slide, x + 0.1, y + 0.5, col_w - 0.2, card_h - 0.6,
                      item['body'], sz=10, color=self.DARK,
                      shrink=True)
            # Dot connector at bottom center
            dot = slide.shapes.add_shape(
                MSO_SHAPE.OVAL,
                Inches(x + col_w / 2 - 0.06), Inches(y + card_h + 0.05),
                Inches(0.12), Inches(0.12))
            dot.fill.solid(); dot.fill.fore_color.rgb = self.RED
            dot.line.fill.background()

    # Canvas bounds constants (10" x 5.625")
    CANVAS_W = 10.0
    CANVAS_H = 5.625
    SAFE_MARGIN = 0.4  # minimum margin from any edge

    # ── Timeline / Process Flow helpers (learned from 时间线流程图模板) ──

    def _clamp(self, val, lo, hi):
        """Clamp a value to [lo, hi] to prevent canvas overflow."""
        return max(lo, min(val, hi))

    def timeline_horizontal_ascending(self, slide, items):
        """Horizontal timeline with nodes, direction arrow, adaptive spacing.
        items: list of 2-6 dicts: {year, title, body}
        """
        n = min(len(items), 6)
        # ── Adaptive layout ──
        left_pad = 1.2          # breathing room on left
        right_pad = 0.9         # room for arrow on right
        arrow_w = 0.25
        spine_x_start = left_pad
        spine_x_end = self.CANVAS_W - right_pad
        spine_w = spine_x_end - spine_x_start

        top_limit = 1.0
        bottom_limit = self.CANVAS_H - self.SAFE_MARGIN
        spine_y = (top_limit + bottom_limit) / 2

        node_inset = 0.15
        first_cx = spine_x_start + node_inset
        last_cx = spine_x_end - node_inset - arrow_w
        node_span = last_cx - first_cx
        node_spacing = node_span / max(n - 1, 1) if n > 1 else 0
        content_w = min(node_spacing * 0.8, 1.6) if n > 1 else 1.6

        # ── Red spine ──
        self.rect(slide, spine_x_start, spine_y, spine_w, 0.025, self.RED)

        # ── Direction arrow at right end ──
        arrow = slide.shapes.add_shape(
            MSO_SHAPE.ISOSCELES_TRIANGLE,
            Inches(spine_x_end - arrow_w), Inches(spine_y - 0.12),
            Inches(arrow_w), Inches(arrow_w * 0.9))
        arrow.fill.solid(); arrow.fill.fore_color.rgb = self.RED
        arrow.line.fill.background()
        arrow.rotation = 90.0

        for i, item in enumerate(items[:n]):
            cx = first_cx + i * node_spacing
            dot_sz = 0.18
            dot = slide.shapes.add_shape(
                MSO_SHAPE.OVAL,
                Inches(cx - dot_sz / 2), Inches(spine_y - dot_sz / 2 + 0.012),
                Inches(dot_sz), Inches(dot_sz))
            dot.fill.solid(); dot.fill.fore_color.rgb = self.RED
            dot.line.fill.background()

            content_x = self._clamp(cx - content_w / 2, self.SAFE_MARGIN,
                                    self.CANVAS_W - content_w - self.SAFE_MARGIN)

            if i % 2 == 0:  # ABOVE
                stub_len = 0.2
                badge_h = 0.26; badge_w = 0.75
                stub_bottom = spine_y - dot_sz / 2
                stub_top = stub_bottom - stub_len
                badge_top = stub_top - badge_h - 0.02
                title_top = badge_top - 0.28
                body_top = title_top - 0.35
                if body_top < top_limit:
                    shift = top_limit - body_top
                    body_top += shift; title_top += shift
                    badge_top += shift; stub_top += shift
                self.rect(slide, cx - 0.007, stub_top, 0.014, stub_len, self.BORDER)
                self.rounded_rect(slide, cx - badge_w / 2, badge_top, badge_w, badge_h, self.RED)
                self.text(slide, cx - badge_w / 2, badge_top + 0.02, badge_w, badge_h - 0.04,
                          item['year'], sz=10, color=self.WHITE, bold=True,
                          align=PP_ALIGN.CENTER)
                self.text(slide, content_x, title_top, content_w, 0.24,
                          item['title'], sz=10, color=self.DARK, bold=True,
                          align=PP_ALIGN.CENTER)
                self.text(slide, content_x, body_top, content_w, 0.32,
                          item['body'], sz=10, color=self.MID,
                          align=PP_ALIGN.CENTER, shrink=True)
            else:  # BELOW
                stub_len = 0.2
                badge_h = 0.26; badge_w = 0.75
                stub_top = spine_y + dot_sz / 2
                badge_top = stub_top + stub_len + 0.02
                title_top = badge_top + badge_h + 0.05
                body_top = title_top + 0.28
                body_bottom = body_top + 0.35
                if body_bottom > bottom_limit:
                    shift = body_bottom - bottom_limit
                    body_top -= shift; title_top -= shift; badge_top -= shift
                self.rect(slide, cx - 0.007, stub_top, 0.014, stub_len, self.BORDER)
                self.rounded_rect(slide, cx - badge_w / 2, badge_top, badge_w, badge_h, self.RED)
                self.text(slide, cx - badge_w / 2, badge_top + 0.02, badge_w, badge_h - 0.04,
                          item['year'], sz=10, color=self.WHITE, bold=True,
                          align=PP_ALIGN.CENTER)
                self.text(slide, content_x, title_top, content_w, 0.24,
                          item['title'], sz=10, color=self.DARK, bold=True,
                          align=PP_ALIGN.CENTER)
                self.text(slide, content_x, body_top, content_w, 0.32,
                          item['body'], sz=10, color=self.MID,
                          align=PP_ALIGN.CENTER, shrink=True)

    def process_flow_arrows(self, slide, items):
        """Horizontal process flow with numbered step blocks.
        items: list of 3 dicts: {num, title, body}
        Learned from: 时间线流程图 Slide 2 (optimized for 10" x 5.625")
        """
        n = min(len(items), 5)
        block_w = 7.5 / n
        gap = 0.3
        actual_w = (7.5 - gap * (n - 1)) / n
        start_x = 0.7
        step_y = 2.65  # vertical center
        for i, item in enumerate(items[:n]):
            x = start_x + i * (actual_w + gap)
            # Step number block
            self.rounded_rect(slide, x, step_y, actual_w, 0.45, self.RED)
            self.text(slide, x, step_y + 0.03, actual_w, 0.4,
                      item['num'], sz=14, color=self.WHITE, bold=True,
                      align=PP_ALIGN.CENTER)
            # Arrow between blocks
            if i < n - 1:
                arrow_x = x + actual_w
                self.text(slide, arrow_x + 0.02, step_y + 0.02, gap - 0.04, 0.4,
                          '▶', sz=14, color=self.RED, align=PP_ALIGN.CENTER)
            # Title above
            self.text(slide, x, 1.15, actual_w, 0.3,
                      item['title'], sz=12, color=self.RED, bold=True)
            # Body above step block
            self.text(slide, x, 1.5, actual_w, 1.0,
                      item['body'], sz=10, color=self.DARK,
                      shrink=True)
            # Vertical connector line from content to step block
            self.rect(slide, x + actual_w / 2 - 0.008, 2.5, 0.015, 0.15, self.BORDER)
            # Small label below step block
            self.text(slide, x, step_y + 0.55, actual_w, 0.2,
                      f'STEP {item["num"]}', sz=10, color=self.LIGHT,
                      align=PP_ALIGN.CENTER)

    def process_three_stage(self, slide, items):
        """Three-stage process with large numbers and content cards.
        items: list of 3 dicts: {num, title, body}
        Learned from: 时间线流程图 Slide 3 (optimized for 10" x 5.625")
        """
        n = min(len(items), 4)
        col_w = (8.5 - 0.3 * (n - 1)) / n
        gap = 0.3
        start_x = 0.6
        for i, item in enumerate(items[:n]):
            x = start_x + i * (col_w + gap)
            # Content card (top portion)
            self.card(slide, x, 1.1, col_w, 2.5)
            # Large number at top of card
            self.text(slide, x + 0.1, 1.2, col_w - 0.2, 0.45,
                      item['num'], sz=28, color=self.RED, bold=True)
            # Thin red accent bar
            self.rect(slide, x + 0.1, 1.72, col_w * 0.35, 0.03, self.RED)
            # Title
            self.text(slide, x + 0.1, 1.85, col_w - 0.2, 0.3,
                      item['title'], sz=12, color=self.DARK, bold=True)
            # Body
            self.text(slide, x + 0.1, 2.2, col_w - 0.2, 1.3,
                      item['body'], sz=10, color=self.MID,
                      shrink=True)
            # Bottom accent strip
            self.rect(slide, x, 3.55, col_w, 0.06, self.RED)
            # Arrow to next
            if i < n - 1:
                ax = x + col_w + 0.02
                self.text(slide, ax, 2.2, gap - 0.04, 0.3,
                          '▶', sz=14, color=self.RED, align=PP_ALIGN.CENTER)

    # ── Timeline helper (original) ─────────────────────
    def timeline_item(self, slide, x, y, date, title, desc=""):
        """Single milestone in a vertical timeline."""
        self.rect(slide, x, y, 0.8, 0.28, self.RED)
        self.text(slide, x, y, 0.8, 0.28, date,
                  sz=10, color=self.WHITE, bold=True, align=PP_ALIGN.CENTER)
        self.text(slide, x + 1.0, y, 2.8, 0.22, title,
                  sz=11, color=self.DARK, bold=True)
        if desc:
            self.text(slide, x + 1.0, y + 0.25, 2.8, 0.2, desc,
                      sz=10, color=self.LIGHT)

    # ── Internal utilities ───────────────────────────────
    def _zero_insets(self, textbox):
        bp = textbox.text_frame._txBody.find(qn('a:bodyPr'))
        if bp is None: return
        for attr in ('lIns', 'tIns', 'rIns', 'bIns'):
            bp.set(attr, '0')

    def _set_line_spacing(self, paragraph, pct):
        """Set line spacing as percentage (e.g. 120 = 1.2x)."""
        pPr = paragraph._p.get_or_add_pPr()
        existing = pPr.find(qn('a:lnSpc'))
        if existing is not None:
            pPr.remove(existing)
        spc = parse_xml(
            f'<a:lnSpc xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">'
            f'<a:spcPct val="{pct * 1000}"/></a:lnSpc>')
        pPr.append(spc)

    # ── Red Base Four-Column Cards (learned from 市场需求 reference) ──

    # Accent color for alternating layouts
    BLUE = RGBColor(0x2B, 0x7A, 0xB8)
    WARM_GREY = RGBColor(0x6B, 0x6B, 0x6B)  # elegant dark grey for alternating

    def timeline_icon_flow(self, slide, items, alt_color=None):
        """Horizontal timeline with large circle icons, chevron arrows, year labels & accent bars.
        items: list of 3-5 dicts: {year, icon_text, title, body}
        alt_color: secondary color for alternating (default: WARM_GREY)
        Layout: large colored circles (red/grey alternating) linked by grey chevron arrows.
        Below each: grey bar with year, colored accent line, title + body text.
        Vertically centered in content area.
        """
        n = min(len(items), 5)
        alt = alt_color or self.WARM_GREY
        colors = [self.RED, alt, self.RED, alt, self.RED]
        margin_x = 0.5
        usable_w = self.CANVAS_W - 2 * margin_x
        col_w = usable_w / n

        # Vertical centering: content block ~3.4" tall, area 1.0"~5.2"
        content_h = 3.4
        area_top = 1.0
        area_bottom = self.CANVAS_H - self.SAFE_MARGIN
        area_h = area_bottom - area_top
        base_y = area_top + (area_h - content_h) / 2  # vertically center

        for i, item in enumerate(items[:n]):
            cx = margin_x + i * col_w + col_w / 2  # center X
            col_left = margin_x + i * col_w + 0.1
            col_inner_w = col_w - 0.2
            color = colors[i]

            # ── Large circle icon
            circ_d = 0.9
            circ_x = cx - circ_d / 2
            circ_y = base_y
            circ = slide.shapes.add_shape(
                MSO_SHAPE.OVAL,
                Inches(circ_x), Inches(circ_y),
                Inches(circ_d), Inches(circ_d))
            circ.fill.solid()
            circ.fill.fore_color.rgb = color
            circ.line.fill.background()
            # Icon text
            self.text(slide, circ_x, circ_y + 0.15, circ_d, circ_d - 0.3,
                      item['icon_text'], sz=14, color=self.WHITE, bold=True,
                      align=PP_ALIGN.CENTER)

            # ── Chevron arrow between circles
            if i < n - 1:
                arrow_x = cx + circ_d / 2 + 0.05
                arrow_w = col_w - circ_d - 0.1
                if arrow_w > 0.1:
                    self.text(slide, arrow_x, circ_y + 0.2, arrow_w, circ_d - 0.4,
                              '\u276f', sz=16, color=self.BORDER, bold=True,
                              align=PP_ALIGN.CENTER)

            # ── Grey bar with year
            bar_y = circ_y + circ_d + 0.2
            bar_h = 0.3
            self.rect(slide, col_left, bar_y, col_inner_w, bar_h, self.BG)
            self.text(slide, col_left + 0.08, bar_y + 0.02, col_inner_w - 0.16, bar_h - 0.04,
                      item['year'], sz=10, color=self.DARK, bold=True)

            # ── Colored accent line
            accent_y = bar_y + bar_h
            self.rect(slide, col_left, accent_y, col_inner_w, 0.04, color)

            # ── Title
            title_y = accent_y + 0.12
            self.text(slide, col_left, title_y, col_inner_w, 0.25,
                      item.get('title', ''), sz=10, color=self.DARK, bold=True)

            # ── Body
            body_y = title_y + 0.3
            body_h = self._clamp(self.CANVAS_H - self.SAFE_MARGIN - body_y, 0.3, 2.0)
            self.text(slide, col_left, body_y, col_inner_w, body_h,
                      item['body'], sz=10, color=self.MID,
                      shrink=True)

    def four_col_red_base_cards(self, slide, items, subtitle=None):
        """Four white cards on a red background base with ring icons & big numbers.
        items: list of 4 dicts: {num, icon_text, title, body}
        subtitle: optional subtitle text below page title
        Layout: upper half white, lower half red. White cards span both zones.
        Each card: ring icon at top, red title, body text, faded number at bottom-right.
        """
        n = min(len(items), 4)

        # Subtitle (integrated, so we can calculate spacing)
        sub_bottom = 0.95  # default: just below title
        if subtitle:
            sub_y = 0.85
            sub_h = 0.3
            self.text(slide, 1.1, sub_y, 7.8, sub_h, subtitle,
                      sz=10, color=self.MID)
            sub_bottom = sub_y + sub_h + 0.1  # 0.1" gap

        # Card top: ensure at least 0.15" below subtitle
        card_top = max(sub_bottom + 0.15, 1.35)

        # Red base rectangle (starts at 55% of card height)
        card_h = self.CANVAS_H - self.SAFE_MARGIN - card_top
        card_h = min(card_h, 3.6)  # cap height
        base_top = card_top + card_h * 0.4
        base_h = self.CANVAS_H - base_top
        self.rect(slide, 0, base_top, self.CANVAS_W, base_h, self.RED)

        # Card dimensions
        card_w = 1.95
        gap = 0.3
        total_w = n * card_w + (n - 1) * gap
        start_x = (self.CANVAS_W - total_w) / 2
        # card_top and card_h already calculated above

        for i, item in enumerate(items[:n]):
            x = start_x + i * (card_w + gap)

            # White card (no border, slight visual lift from red bg)
            self.card(slide, x, card_top, card_w, card_h)

            # Ring icon at top of card
            ring_d = 0.85
            ring_x = x + (card_w - ring_d) / 2
            ring_y = card_top + 0.2
            # Outer ring
            outer = slide.shapes.add_shape(
                MSO_SHAPE.OVAL,
                Inches(ring_x), Inches(ring_y),
                Inches(ring_d), Inches(ring_d))
            outer.fill.solid()
            outer.fill.fore_color.rgb = self.BG
            outer.line.color.rgb = self.BORDER
            outer.line.width = Pt(1)
            # Icon text inside ring
            self.text(slide, ring_x, ring_y + 0.12, ring_d, ring_d - 0.24,
                      item['icon_text'], sz=14, color=self.RED, bold=True,
                      align=PP_ALIGN.CENTER)

            # Title below ring
            title_y = ring_y + ring_d + 0.15
            self.text(slide, x + 0.1, title_y, card_w - 0.2, 0.28,
                      item['title'], sz=11, color=self.RED, bold=True,
                      align=PP_ALIGN.CENTER)

            # Body text
            body_y = title_y + 0.35
            self.text(slide, x + 0.12, body_y, card_w - 0.24, 1.4,
                      item['body'], sz=10, color=self.DARK,
                      shrink=True)

            # Big faded number at bottom-right of card
            num_y = card_top + card_h - 0.55
            self.text(slide, x + card_w - 0.75, num_y, 0.65, 0.45,
                      item['num'], sz=24, color=self.BORDER, bold=True,
                      align=PP_ALIGN.RIGHT)

    # ── Ring Badge Three-Column (learned from 推荐+社交+搜索 reference) ──

    def three_col_numbered_card_blocks(self, slide, items, alt_color=None):
        """Three light-grey cards with half-embedded numbered badges, colored titles & dividers.
        items: list of 3 dicts: {num, title, body}
        alt_color: second accent color (default: ACCENT_GREY). Pattern: RED, alt, RED.
        Layout: badge top-half outside card, left-aligned title + divider + body inside.
        Vertically centered.
        """
        n = min(len(items), 3)
        alt = alt_color or self.ACCENT_GREY
        colors = [self.RED, alt, self.RED]

        card_w = 2.6
        card_h = 2.8
        gap = 0.4
        badge_h = 0.4
        badge_w = 0.55
        embed = badge_h / 2  # half outside, half inside

        total_w = n * card_w + (n - 1) * gap
        start_x = (self.CANVAS_W - total_w) / 2

        # Vertical center (account for badge overhang)
        content_h = card_h + embed
        area_top = 1.0
        area_bottom = self.CANVAS_H - self.SAFE_MARGIN
        area_h = area_bottom - area_top
        base_y = area_top + (area_h - content_h) / 2
        card_top = base_y + embed  # card starts below badge overhang

        for i, item in enumerate(items[:n]):
            x = start_x + i * (card_w + gap)
            color = colors[i]
            pad = 0.2  # internal padding

            # ── Light grey card
            card = slide.shapes.add_shape(
                MSO_SHAPE.RECTANGLE,
                Inches(x), Inches(card_top),
                Inches(card_w), Inches(card_h))
            card.fill.solid()
            card.fill.fore_color.rgb = self.BG  # light grey
            card.line.fill.background()

            # ── Half-embedded badge (straddles card top edge)
            badge_x = x + pad
            badge_y = card_top - embed
            badge = slide.shapes.add_shape(
                MSO_SHAPE.RECTANGLE,
                Inches(badge_x), Inches(badge_y),
                Inches(badge_w), Inches(badge_h))
            badge.fill.solid()
            badge.fill.fore_color.rgb = color
            badge.line.fill.background()
            # Badge number
            self.text(slide, badge_x, badge_y + 0.02, badge_w, badge_h - 0.04,
                      item['num'], sz=14, color=self.WHITE, bold=True,
                      align=PP_ALIGN.CENTER)

            # ── Title (colored, left-aligned)
            title_y = card_top + embed + 0.15
            self.text(slide, x + pad, title_y, card_w - 2 * pad, 0.3,
                      item['title'], sz=13, color=color, bold=True)

            # ── Divider line (colored)
            div_y = title_y + 0.38
            self.rect(slide, x + pad, div_y, card_w - 2 * pad, 0.02, color)

            # ── Body text
            body_y = div_y + 0.15
            body_h = card_top + card_h - body_y - 0.15
            body_h = max(body_h, 0.3)
            self.text(slide, x + pad, body_y, card_w - 2 * pad, body_h,
                      item['body'], sz=10, color=self.DARK,
                      shrink=True)

    # ── Triple Overlapping Circles (learned from 三圆交叠 reference) ──

    def triple_overlap_circles(self, slide, items):
        """Three overlapping circles in inverted triangle with text labels outside.
        items: list of 3 dicts: {icon_text, title, body}
        Layout: circles[0]=top-left, circles[1]=top-right, circles[2]=bottom-center.
        Text: left-side right-aligned, right-side left-aligned, bottom centered.
        Colors: RED, CORAL, DARK_RED (warm red family).
        """
        import math
        colors = [self.RED, self.CORAL, self.DARK_RED]
        circ_d = 1.6
        overlap = circ_d * 0.3  # 30% overlap

        # Triangle geometry
        cx_page = self.CANVAS_W / 2
        cy_page = 2.8  # vertical center of circles group
        h_offset = (circ_d - overlap) / 2 + 0.1
        v_offset = (circ_d - overlap) / 2

        positions = [
            (cx_page - h_offset, cy_page - v_offset),  # top-left
            (cx_page + h_offset, cy_page - v_offset),  # top-right
            (cx_page, cy_page + v_offset),              # bottom-center
        ]

        # Draw circles (back to front: bottom, then top-left, then top-right)
        draw_order = [2, 0, 1]
        for di in draw_order:
            pcx, pcy = positions[di]
            color = colors[di]
            circ = slide.shapes.add_shape(
                MSO_SHAPE.OVAL,
                Inches(pcx - circ_d / 2), Inches(pcy - circ_d / 2),
                Inches(circ_d), Inches(circ_d))
            circ.fill.solid()
            circ.fill.fore_color.rgb = color
            circ.line.fill.background()
            # Icon text inside
            self.text(slide,
                      pcx - circ_d / 2, pcy - 0.2,
                      circ_d, 0.4,
                      items[di]['icon_text'],
                      sz=16, color=self.WHITE, bold=True,
                      align=PP_ALIGN.CENTER)

        # Text labels outside circles
        txt_w = 2.8
        txt_h_title = 0.25
        txt_h_body = 0.8

        # [0] Top-left: text on LEFT side, right-aligned
        lx = positions[0][0] - circ_d / 2 - txt_w - 0.15
        ly = positions[0][1] - 0.5
        self.text(slide, lx, ly, txt_w, txt_h_title,
                  items[0]['title'], sz=11, color=colors[0], bold=True,
                  align=PP_ALIGN.RIGHT)
        self.text(slide, lx, ly + 0.3, txt_w, txt_h_body,
                  items[0]['body'], sz=10, color=self.MID,
                  align=PP_ALIGN.RIGHT, shrink=True)

        # [1] Top-right: text on RIGHT side, left-aligned
        rx = positions[1][0] + circ_d / 2 + 0.15
        ry = positions[1][1] - 0.5
        self.text(slide, rx, ry, txt_w, txt_h_title,
                  items[1]['title'], sz=11, color=colors[1], bold=True)
        self.text(slide, rx, ry + 0.3, txt_w, txt_h_body,
                  items[1]['body'], sz=10, color=self.MID,
                  shrink=True)

        # [2] Bottom: text BELOW, centered
        bx = positions[2][0] - txt_w / 2
        by = positions[2][1] + circ_d / 2 + 0.1
        self.text(slide, bx, by, txt_w, txt_h_title,
                  items[2]['title'], sz=11, color=colors[2], bold=True,
                  align=PP_ALIGN.CENTER)
        self.text(slide, bx, by + 0.3, txt_w, txt_h_body,
                  items[2]['body'], sz=10, color=self.MID,
                  align=PP_ALIGN.CENTER, shrink=True)

    # ── Diamond Quadrant Layout (learned from 菱形四象限 reference) ──

    def four_col_numbered_dividers(self, slide, items, alt_color=None):
        """Four equal columns with numbered badges, colored titles, vertical dividers.
        items: list of 4 dicts: {num, title, body}
        alt_color: second accent (default: ACCENT_GREY). Pattern: RED, alt, RED, alt.
        Divider lines between columns match alternating colors.
        Vertically centered.
        """
        n = min(len(items), 4)
        alt = alt_color or self.ACCENT_GREY
        colors = [self.RED, alt, self.RED, alt]
        div_colors = [alt, self.RED, alt]  # dividers between columns

        margin_x = 0.8
        usable_w = self.CANVAS_W - 2 * margin_x
        col_w = usable_w / n

        # Vertical centering
        content_h = 3.0  # badge(0.4) + gap(0.25) + title(0.3) + gap(0.15) + body(1.5) + padding
        area_top = 1.0
        area_bottom = self.CANVAS_H - self.SAFE_MARGIN
        area_h = area_bottom - area_top
        base_y = area_top + (area_h - content_h) / 2

        badge_size = 0.4

        for i, item in enumerate(items[:n]):
            cx = margin_x + i * col_w + col_w / 2
            col_left = margin_x + i * col_w
            color = colors[i]

            # ── Number badge (small colored square)
            badge_x = cx - badge_size / 2
            badge_y = base_y
            badge = slide.shapes.add_shape(
                MSO_SHAPE.RECTANGLE,
                Inches(badge_x), Inches(badge_y),
                Inches(badge_size), Inches(badge_size))
            badge.fill.solid()
            badge.fill.fore_color.rgb = color
            badge.line.fill.background()
            self.text(slide, badge_x, badge_y + 0.02, badge_size, badge_size - 0.04,
                      item['num'], sz=14, color=self.WHITE, bold=True,
                      align=PP_ALIGN.CENTER)

            # ── Title (colored, centered)
            title_y = badge_y + badge_size + 0.2
            self.text(slide, col_left + 0.1, title_y, col_w - 0.2, 0.3,
                      item['title'], sz=11, color=color, bold=True,
                      align=PP_ALIGN.CENTER)

            # ── Body (dark, centered)
            body_y = title_y + 0.4
            body_h = content_h - (body_y - base_y) - 0.1
            body_h = max(body_h, 0.5)
            self.text(slide, col_left + 0.15, body_y, col_w - 0.3, body_h,
                      item['body'], sz=10, color=self.DARK,
                      align=PP_ALIGN.CENTER, shrink=True)

            # ── Vertical divider line (between columns, not after last)
            if i < n - 1:
                div_x = margin_x + (i + 1) * col_w
                div_top = title_y
                div_bottom = body_y + body_h
                div_color = div_colors[i] if i < len(div_colors) else alt
                self.rect(slide, div_x - 0.005, div_top, 0.01, div_bottom - div_top, div_color)

    def diamond_quadrants(self, slide, center_text, items):
        """Central diamond with 4 surrounding text quadrants.
        center_text: text inside the diamond
        items: list of 4 dicts: {title, body} for TL, TR, BL, BR
        Text alignment: TL/BL right-aligned (toward center), TR/BR left-aligned.
        """
        from pptx.enum.shapes import MSO_SHAPE

        # Page center for content area
        cx = self.CANVAS_W / 2
        cy = 3.0  # slightly below vertical center to account for title

        # ── Diamond (rotated square)
        dia_size = 1.5  # side length before rotation
        dia = slide.shapes.add_shape(
            MSO_SHAPE.DIAMOND,
            Inches(cx - dia_size / 2), Inches(cy - dia_size / 2),
            Inches(dia_size), Inches(dia_size))
        dia.fill.solid()
        dia.fill.fore_color.rgb = self.WHITE
        dia.line.color.rgb = self.RED
        dia.line.width = Pt(2.5)
        # Center text
        self.text(slide, cx - dia_size / 2 + 0.15, cy - 0.15,
                  dia_size - 0.3, 0.3,
                  center_text, sz=12, color=self.RED, bold=True,
                  align=PP_ALIGN.CENTER)

        # ── Four quadrant text blocks
        txt_w = 2.8
        txt_h = 1.2
        gap = 0.25  # gap from diamond edge
        d_half = dia_size / 2 + gap

        # Quadrant positions: (x, y, align)
        quads = [
            # TL: right-aligned, above-left of diamond
            (cx - d_half - txt_w, cy - d_half - txt_h + 0.4, PP_ALIGN.RIGHT),
            # TR: left-aligned, above-right
            (cx + d_half, cy - d_half - txt_h + 0.4, PP_ALIGN.LEFT),
            # BL: right-aligned, below-left
            (cx - d_half - txt_w, cy + d_half - 0.4, PP_ALIGN.RIGHT),
            # BR: left-aligned, below-right
            (cx + d_half, cy + d_half - 0.4, PP_ALIGN.LEFT),
        ]

        for i, item in enumerate(items[:4]):
            qx, qy, align = quads[i]
            # Light grey background block
            bg = slide.shapes.add_shape(
                MSO_SHAPE.ROUNDED_RECTANGLE,
                Inches(qx), Inches(qy),
                Inches(txt_w), Inches(txt_h))
            bg.fill.solid()
            bg.fill.fore_color.rgb = self.BG
            bg.line.fill.background()

            # Title
            self.text(slide, qx + 0.1, qy + 0.1, txt_w - 0.2, 0.25,
                      item.get('title', ''), sz=11, color=self.RED, bold=True,
                      align=align)
            # Body
            self.text(slide, qx + 0.1, qy + 0.4, txt_w - 0.2, txt_h - 0.5,
                      item['body'], sz=10, color=self.DARK,
                      align=align, shrink=True)

    # ── Parallelogram Four-Column (learned from 斜四栏 reference) ──

    LIGHT_GREY2 = RGBColor(0xEE, 0xEE, 0xEE)  # very light grey

    def four_panel_on_platform(self, slide, platform_text, items, alt_bg=None):
        """Four panels arranged above an elliptical platform base.
        platform_text: main title on the elliptical base
        items: list of 4 dicts: {title, body}
        Layout: bottom ellipse as "base", four tall cards above in slight fan.
        Colors: alternating light-red / light-grey.
        """
        n = min(len(items), 4)
        alt = alt_bg or self.LIGHT_GREY2
        bg_colors = [self.LIGHT_RED, alt, self.LIGHT_RED, alt]
        title_colors = [self.RED, self.ACCENT_GREY, self.RED, self.ACCENT_GREY]

        # ── Vertical layout: panels + platform need to be centered
        panel_h = 2.5
        plat_h = 1.0
        gap_panel_plat = 0.1  # overlap amount
        total_content_h = panel_h + plat_h - gap_panel_plat

        area_top = 1.0  # below title
        area_bottom = self.CANVAS_H - self.SAFE_MARGIN
        area_h = area_bottom - area_top
        base_y = area_top + (area_h - total_content_h) / 2

        panel_top = base_y
        panel_bottom = panel_top + panel_h

        # ── Elliptical platform
        plat_w = 6.0
        plat_x = (self.CANVAS_W - plat_w) / 2
        plat_y = panel_bottom - gap_panel_plat
        plat = slide.shapes.add_shape(
            MSO_SHAPE.OVAL,
            Inches(plat_x), Inches(plat_y),
            Inches(plat_w), Inches(plat_h))
        plat.fill.solid()
        plat.fill.fore_color.rgb = self.LIGHT_RED2
        plat.line.fill.background()
        # Platform text
        self.text(slide, plat_x + 0.5, plat_y + 0.2, plat_w - 1.0, 0.5,
                  platform_text, sz=16, color=self.RED, bold=True,
                  align=PP_ALIGN.CENTER)

        # ── Four panels above platform
        panel_w = 1.8
        gap = 0.25
        total_panels_w = n * panel_w + (n - 1) * gap
        start_x = (self.CANVAS_W - total_panels_w) / 2

        for i, item in enumerate(items[:n]):
            x = start_x + i * (panel_w + gap)
            bg_color = bg_colors[i]
            t_color = title_colors[i]

            # Panel card (rounded rectangle)
            card = slide.shapes.add_shape(
                MSO_SHAPE.ROUNDED_RECTANGLE,
                Inches(x), Inches(panel_top),
                Inches(panel_w), Inches(panel_h))
            card.fill.solid()
            card.fill.fore_color.rgb = bg_color
            card.line.fill.background()

            # Title inside panel (top area)
            self.text(slide, x + 0.12, panel_top + 0.2, panel_w - 0.24, 0.3,
                      item['title'], sz=11, color=t_color, bold=True,
                      align=PP_ALIGN.CENTER)

            # Body inside panel
            body_y = panel_top + 0.6
            body_h = panel_h - 0.8
            self.text(slide, x + 0.12, body_y, panel_w - 0.24, body_h,
                      item['body'], sz=10, color=self.DARK,
                      align=PP_ALIGN.CENTER, shrink=True)

    def four_col_parallelogram(self, slide, items, alt_bg=None):
        """Four slanted parallelogram columns with circle icons on top.
        items: list of 4 dicts: {icon_text, title, body}
        Alternating light-red / light-grey background.
        Circle icon at top in deeper color, title + body below.
        """
        n = min(len(items), 4)
        alt = alt_bg or self.LIGHT_GREY2
        bg_colors = [self.LIGHT_RED, alt, self.LIGHT_RED, alt]
        icon_colors = [self.RED, self.ACCENT_GREY, self.RED, self.ACCENT_GREY]

        margin_x = 0.6
        usable_w = self.CANVAS_W - 2 * margin_x
        gap = 0.25
        col_w = (usable_w - (n - 1) * gap) / n
        col_h = 3.2

        # Vertical centering
        area_top = 1.0
        area_bottom = self.CANVAS_H - self.SAFE_MARGIN
        area_h = area_bottom - area_top
        circ_overhang = 0.25  # circle sticks out above parallelogram
        total_h = col_h + circ_overhang
        base_y = area_top + (area_h - total_h) / 2 + circ_overhang

        for i, item in enumerate(items[:n]):
            x = margin_x + i * (col_w + gap)
            cx = x + col_w / 2
            bg_color = bg_colors[i]
            ic_color = icon_colors[i]

            # ── Parallelogram (using PARALLELOGRAM shape)
            para = slide.shapes.add_shape(
                MSO_SHAPE.PARALLELOGRAM,
                Inches(x), Inches(base_y),
                Inches(col_w), Inches(col_h))
            para.fill.solid()
            para.fill.fore_color.rgb = bg_color
            para.line.fill.background()
            # Adjust the parallelogram skew via adjustments
            if para.adjustments:
                try:
                    para.adjustments[0] = 0.15  # gentle skew
                except:
                    pass

            # ── Circle icon at top (overlapping parallelogram top edge)
            circ_d = 0.7
            circ_x = cx - circ_d / 2
            circ_y = base_y - circ_overhang
            circ = slide.shapes.add_shape(
                MSO_SHAPE.OVAL,
                Inches(circ_x), Inches(circ_y),
                Inches(circ_d), Inches(circ_d))
            circ.fill.solid()
            circ.fill.fore_color.rgb = ic_color
            circ.line.fill.background()
            self.text(slide, circ_x, circ_y + 0.1, circ_d, circ_d - 0.2,
                      item['icon_text'], sz=14, color=self.WHITE, bold=True,
                      align=PP_ALIGN.CENTER)

            # ── Title
            title_y = base_y + 0.55
            self.text(slide, x + 0.15, title_y, col_w - 0.3, 0.3,
                      item['title'], sz=11, color=ic_color, bold=True,
                      align=PP_ALIGN.CENTER)

            # ── Body
            body_y = title_y + 0.4
            body_h = col_h - (body_y - base_y) - 0.2
            body_h = max(body_h, 0.5)
            self.text(slide, x + 0.15, body_y, col_w - 0.3, body_h,
                      item['body'], sz=10, color=self.DARK,
                      align=PP_ALIGN.CENTER, shrink=True)

    def timeline_zigzag(self, slide, items):
        """Horizontal timeline with numbered circles and text blocks alternating above/below.
        items: list of 3-5 dicts: {num, title, body}
        Odd items (01/03) have text above, even items (02/04) below.
        Connected by a thin horizontal line through circle centers.
        Learned from: user-shared reference image 2026-04-14.
        """
        n = min(len(items), 5)
        margin_x = 0.6
        usable_w = self.CANVAS_W - 2 * margin_x
        col_w = usable_w / n

        # Vertical layout constants
        circ_d = 0.65
        line_y = self.CANVAS_H / 2 + 0.15  # horizontal line Y (center of circles)
        circ_top = line_y - circ_d / 2
        text_w = col_w - 0.3
        text_h_above = 1.3  # space for text block above
        text_h_below = 1.3  # space for text block below

        # ── Horizontal connecting line (behind circles)
        line_x1 = margin_x + col_w / 2
        line_x2 = margin_x + (n - 1) * col_w + col_w / 2
        line_shape = slide.shapes.add_shape(
            MSO_SHAPE.RECTANGLE,
            Inches(line_x1), Inches(line_y - 0.015),
            Inches(line_x2 - line_x1), Inches(0.03))
        line_shape.fill.solid()
        line_shape.fill.fore_color.rgb = self.BORDER
        line_shape.line.fill.background()

        for i, item in enumerate(items[:n]):
            cx = margin_x + i * col_w + col_w / 2
            col_left = cx - text_w / 2
            num = item.get('num', f'{i+1:02d}')
            is_above = (i % 2 == 0)  # 0,2,4 = above; 1,3 = below
            color = self.RED if (i % 2 == 0) else self.DARK

            # ── Numbered circle
            circ = slide.shapes.add_shape(
                MSO_SHAPE.OVAL,
                Inches(cx - circ_d / 2), Inches(circ_top),
                Inches(circ_d), Inches(circ_d))
            circ.fill.solid()
            circ.fill.fore_color.rgb = color
            circ.line.fill.background()
            self.text(slide, cx - circ_d / 2, circ_top + 0.08, circ_d, circ_d - 0.16,
                      num, sz=14, color=self.WHITE, bold=True,
                      align=PP_ALIGN.CENTER)

            # ── Text block (title + body) — directly adjacent to circle
            gap = 0.15  # small gap between circle and text
            if is_above:
                title_y = circ_top - gap - text_h_above
                body_y = title_y + 0.35
                body_h = text_h_above - 0.4
            else:
                title_y = circ_top + circ_d + gap
                body_y = title_y + 0.35
                body_h = text_h_below - 0.4

            # Title
            self.text(slide, col_left, title_y, text_w, 0.3,
                      item.get('title', ''), sz=12, color=color, bold=True,
                      align=PP_ALIGN.CENTER)
            # Body
            self.text(slide, col_left, body_y, text_w, body_h,
                      item['body'], sz=10, color=self.MID,
                      align=PP_ALIGN.CENTER, shrink=True)

    # ── Save ─────────────────────────────────────────────
    def save(self, path):
        self.prs.save(path)
        return path


path: scripts/ppt_qa.py
content: #!/usr/bin/env python
# -*- coding: utf-8 -*-
"""ppt_qa.py — Automated QA verification for generated PPTs.

Two core checks:
1. Loop Verification: boundary + text extraction + data cross-check
2. Layout Diversity: detect repeated adjacent layouts

Usage:
    from ppt_qa import PPTQualityChecker
    qc = PPTQualityChecker("output.pptx")
    report = qc.full_report(source_text="原始素材文本...")
    print(report)
"""
import re
from collections import Counter
from pptx import Presentation
from pptx.util import Emu


class PPTQualityChecker:
    """Automated QA for generated PPTs."""

    CANVAS_W = 10.0
    CANVAS_H = 5.625
    SAFE_MARGIN = 0.3  # slightly relaxed for checking (0.4 for generation)

    # Layout fingerprints: shape count + approximate positions
    # Used to detect when adjacent slides use identical layouts
    DENSITY_HIGH = 300  # chars per slide = too dense
    DENSITY_LOW = 20    # chars per slide = too empty (excl cover/ending)

    def __init__(self, pptx_path):
        self.path = pptx_path
        self.prs = Presentation(pptx_path)
        self.slides = list(self.prs.slides)
        self.n_slides = len(self.slides)

    # ── 1. Boundary Check ─────────────────────────────────

    def check_boundaries(self):
        """Check all shapes stay within canvas bounds."""
        violations = []
        for idx, slide in enumerate(self.slides):
            for shape in slide.shapes:
                l = Emu(shape.left).inches
                t = Emu(shape.top).inches
                w = Emu(shape.width).inches
                h = Emu(shape.height).inches
                r = l + w
                b = t + h
                name = shape.text_frame.text[:40] if shape.has_text_frame else shape.name
                if l < -0.01:
                    violations.append(f"S{idx+1}: \"{name}\" left={l:.2f}\" (超出左边界)")
                if t < -0.01:
                    violations.append(f"S{idx+1}: \"{name}\" top={t:.2f}\" (超出上边界)")
                if r > self.CANVAS_W + 0.01:
                    violations.append(f"S{idx+1}: \"{name}\" right={r:.2f}\" (超出右边界 {self.CANVAS_W}\")")
                if b > self.CANVAS_H + 0.01:
                    violations.append(f"S{idx+1}: \"{name}\" bottom={b:.2f}\" (超出下边界 {self.CANVAS_H}\")")
        return violations

    # ── 2. Text Extraction ────────────────────────────────

    def extract_all_text(self):
        """Extract text from every slide, return list of (slide_num, text)."""
        result = []
        for idx, slide in enumerate(self.slides):
            texts = []
            for shape in slide.shapes:
                if shape.has_text_frame:
                    t = shape.text_frame.text.strip()
                    if t:
                        texts.append(t)
            result.append((idx + 1, '\n'.join(texts)))
        return result

    # ── 3. Data Cross-Check ───────────────────────────────

    def extract_numbers_dates(self):
        """Extract all numbers and date-like strings from PPT."""
        findings = []
        for idx, slide in enumerate(self.slides):
            for shape in slide.shapes:
                if shape.has_text_frame:
                    text = shape.text_frame.text
                    # Numbers (with Chinese units)
                    nums = re.findall(r'[\d,]+\.?\d*\s*[万亿千百%+件条个项人次年月日天]+', text)
                    # Pure percentages
                    pcts = re.findall(r'\d+\.?\d*%', text)
                    # Year patterns
                    years = re.findall(r'20[0-9]{2}(?:\.[0-9]{1,2})?(?:-[0-9]{1,2})?', text)
                    # Date patterns
                    dates = re.findall(r'\d{1,2}月\d{1,2}日', text)
                    for n in set(nums + pcts + years + dates):
                        findings.append((idx + 1, n))
        # Deduplicate
        return sorted(set(findings))

    def cross_check_content(self, source_text):
        """Check if key data from source appears in PPT."""
        if not source_text:
            return []
        # Extract numbers from source
        src_nums = set(re.findall(r'[\d,]+\.?\d*\s*[万亿千百%+件条个项人次年月日天]+', source_text))
        src_nums.update(re.findall(r'\d+\.?\d*%', source_text))
        # Extract numbers from PPT
        ppt_data = self.extract_numbers_dates()
        ppt_nums = set(n for _, n in ppt_data)
        # Find missing
        missing = []
        for num in src_nums:
            # Fuzzy match: check if any PPT number contains same digits
            num_digits = re.sub(r'[^\d.]', '', num)
            found = False
            for ppt_num in ppt_nums:
                if num_digits in re.sub(r'[^\d.]', '', ppt_num):
                    found = True
                    break
            if not found and len(num_digits) >= 2:  # ignore single digits
                missing.append(num)
        return sorted(missing)

    # ── 4. Text Truncation Risk ───────────────────────────

    def check_text_overflow_risk(self):
        """Estimate if text might overflow its text box, including title checks."""
        risks = []
        for idx, slide in enumerate(self.slides):
            for shape in slide.shapes:
                if not shape.has_text_frame:
                    continue
                text = shape.text_frame.text.strip()
                if not text:
                    continue
                w_inches = Emu(shape.width).inches
                h_inches = Emu(shape.height).inches
                t_inches = Emu(shape.top).inches
                is_title = (t_inches < 0.6 and h_inches < 0.6)
                if is_title:
                    max_chars = int(w_inches * 3.5)
                    if len(text) > max_chars:
                        risks.append(f"S{idx+1}: 标题\"{text[:20]}...\" ({len(text)}字>框容{max_chars}字) — 会换行溢出")
                else:
                    chars_per_line = max(1, int(w_inches * 7))
                    lines_needed = len(text) / chars_per_line
                    height_needed = lines_needed * 0.22
                    if height_needed > h_inches * 1.5 and len(text) > 20:
                        risks.append(f"S{idx+1}: \"{text[:30]}...\" ({len(text)}字 in {w_inches:.1f}\"×{h_inches:.1f}\" box)")
        return risks

    # ── 5. Information Density ────────────────────────────

    def check_density(self):
        """Check per-slide character count for density balance."""
        issues = []
        slide_texts = self.extract_all_text()
        for slide_num, text in slide_texts:
            char_count = len(text.replace('\n', '').replace(' ', ''))
            if slide_num == 1 or slide_num == self.n_slides:
                continue  # skip cover/ending
            if char_count > self.DENSITY_HIGH:
                issues.append(f"S{slide_num}: {char_count}字 (过密，建议<{self.DENSITY_HIGH}字或拆页)")
            elif char_count < self.DENSITY_LOW:
                issues.append(f"S{slide_num}: {char_count}字 (过空，建议补充内容)")
        return issues

    # ── 5a. Element Overlap Detection ───────────────────────

    def check_element_overlap(self):
        """Check if text elements overlap with other shapes."""
        issues = []
        for idx, slide in enumerate(self.slides):
            text_shapes = []
            all_shapes = []
            for shape in slide.shapes:
                t = Emu(shape.top).inches
                b = t + Emu(shape.height).inches
                l = Emu(shape.left).inches
                r = l + Emu(shape.width).inches
                has_text = shape.has_text_frame and shape.text_frame.text.strip()
                entry = {"t": t, "b": b, "l": l, "r": r, "text": has_text, "txt": shape.text_frame.text.strip()[:20] if has_text else ""}
                all_shapes.append(entry)
                if has_text:
                    text_shapes.append(entry)
            # Check title/subtitle vs content shapes
            for ts in text_shapes:
                if ts["t"] > 1.0:  # not a title
                    continue
                for other in all_shapes:
                    if other is ts:
                        continue
                    if other["t"] < 1.0:  # both in title zone, skip
                        continue
                    # Check vertical overlap
                    v_overlap = min(ts["b"], other["b"]) - max(ts["t"], other["t"])
                    h_overlap = min(ts["r"], other["r"]) - max(ts["l"], other["l"])
                    if v_overlap > 0.05 and h_overlap > 0.1:
                        issues.append(
                            f"S{idx+1}: \"{ts['txt']}\" (y={ts['t']:.2f}~{ts['b']:.2f}) "
                            f"与内容元素(y={other['t']:.2f}~{other['b']:.2f})重叠{v_overlap:.2f}\"")
                        break  # one overlap per title is enough
        return issues

    # ── 5b. Vertical Balance ────────────────────────────────

    def check_vertical_balance(self):
        """Check if content is vertically centered in the page."""
        issues = []
        for idx, slide in enumerate(self.slides):
            if idx == 0 or idx == self.n_slides - 1:
                continue  # skip cover/ending
            tops = []
            bottoms = []
            for shape in slide.shapes:
                t = Emu(shape.top).inches
                b = t + Emu(shape.height).inches
                if t < 0.8:  # skip title area
                    continue
                tops.append(t)
                bottoms.append(b)
            if not tops:
                continue
            content_top = min(tops)
            content_bottom = max(bottoms)
            # Available area: 1.0" to 5.2" (below title, above margin)
            area_top = 1.0
            area_bottom = self.CANVAS_H - 0.4
            space_above = content_top - area_top
            space_below = area_bottom - content_bottom
            # If content uses less than 60% of area AND is heavily skewed
            content_h = content_bottom - content_top
            area_h = area_bottom - area_top
            if content_h < area_h * 0.6:
                ratio = space_below / max(space_above, 0.01) if space_above > 0 else 999
                if ratio > 3.0:
                    issues.append(
                        f"S{idx+1}: 内容偏上(上留白{space_above:.1f}\"/下留白{space_below:.1f}\")"
                        f" — 建议垂直居中")
                elif ratio < 0.33:
                    issues.append(
                        f"S{idx+1}: 内容偏下(上留白{space_above:.1f}\"/下留白{space_below:.1f}\")"
                        f" — 建议垂直居中")
        return issues

    # ── 6. Layout Diversity ───────────────────────────────

    def _slide_fingerprint(self, slide):
        """Generate a layout fingerprint for a slide.
        Based on: shape count, approximate grid positions, shape types.
        """
        shapes_info = []
        for shape in slide.shapes:
            # Quantize position to 0.5" grid
            l = round(Emu(shape.left).inches * 2) / 2
            t = round(Emu(shape.top).inches * 2) / 2
            w = round(Emu(shape.width).inches * 2) / 2
            h = round(Emu(shape.height).inches * 2) / 2
            st = str(shape.shape_type)
            shapes_info.append(f"{st}@{l},{t},{w},{h}")
        shapes_info.sort()
        return '|'.join(shapes_info)

    def check_layout_diversity(self):
        """Detect adjacent slides using identical/similar layouts."""
        issues = []
        fingerprints = []
        for slide in self.slides:
            fingerprints.append(self._slide_fingerprint(slide))

        # Check adjacent pairs
        for i in range(len(fingerprints) - 1):
            if i == 0:  # skip cover
                continue
            fp1 = fingerprints[i]
            fp2 = fingerprints[i + 1]
            if fp1 == fp2:
                issues.append(f"S{i+1} ↔ S{i+2}: 布局完全相同，建议换一种版式")
            elif self._fingerprint_similarity(fp1, fp2) > 0.8:
                issues.append(f"S{i+1} ↔ S{i+2}: 布局高度相似({self._fingerprint_similarity(fp1, fp2):.0%})，建议增加变化")

        # Check overall: any layout used 3+ times
        fp_counter = Counter(fingerprints[1:])  # exclude cover
        for fp, count in fp_counter.items():
            if count >= 3:
                # Find which slides
                slide_nums = [i+1 for i, f in enumerate(fingerprints) if f == fp]
                issues.append(f"同一布局出现{count}次 (S{','.join(map(str,slide_nums))})，建议增加版式多样性")

        return issues

    def _fingerprint_similarity(self, fp1, fp2):
        """Compute Jaccard similarity between two fingerprints."""
        set1 = set(fp1.split('|'))
        set2 = set(fp2.split('|'))
        if not set1 and not set2:
            return 1.0
        intersection = len(set1 & set2)
        union = len(set1 | set2)
        return intersection / union if union > 0 else 0

    # ── Full Report ───────────────────────────────────────

    def full_report(self, source_text=None):
        """Run all checks and return formatted report string."""
        lines = [f"📋 审校报告 ({self.path})", f"总页数: {self.n_slides}", ""]

        # 1. Boundaries
        violations = self.check_boundaries()
        if violations:
            lines.append(f"❌ 边界溢出: {len(violations)} 个问题")
            for v in violations:
                lines.append(f"   - {v}")
        else:
            lines.append("✅ 边界溢出: 0 violations")

        # 2. Text overflow risk
        risks = self.check_text_overflow_risk()
        if risks:
            lines.append(f"⚠️ 文字截断风险: {len(risks)} 个")
            for r in risks:
                lines.append(f"   - {r}")
        else:
            lines.append("✅ 文字截断: 无风险")

        # 3. Density
        density_issues = self.check_density()
        if density_issues:
            lines.append(f"⚠️ 信息密度: {len(density_issues)} 个问题")
            for d in density_issues:
                lines.append(f"   - {d}")
        else:
            lines.append("✅ 信息密度: 均衡")

        # 4. Layout diversity
        # 4a. Element overlap
        overlap_issues = self.check_element_overlap()
        if overlap_issues:
            lines.append(f"⚠️ 元素重叠: {len(overlap_issues)} 个问题")
            for o in overlap_issues:
                lines.append(f"   - {o}")
        else:
            lines.append("✅ 元素重叠: 无")

        # 4b. Vertical balance
        vb_issues = self.check_vertical_balance()
        if vb_issues:
            lines.append(f"⚠️ 垂直平衡: {len(vb_issues)} 个问题")
            for v in vb_issues:
                lines.append(f"   - {v}")
        else:
            lines.append("✅ 垂直平衡: OK")

        layout_issues = self.check_layout_diversity()
        if layout_issues:
            lines.append(f"⚠️ 布局多样性: {len(layout_issues)} 个问题")
            for l in layout_issues:
                lines.append(f"   - {l}")
        else:
            lines.append("✅ 布局多样性: OK")

        # 5. Data cross-check
        data = self.extract_numbers_dates()
        if data:
            lines.append(f"📊 数据核对 ({len(data)} 项数据，请人工确认):")
            for slide_num, num in data:
                lines.append(f"   - S{slide_num}: {num}")
        else:
            lines.append("ℹ️ 数据核对: 未发现数字/日期数据")

        # 6. Content completeness (if source provided)
        if source_text:
            missing = self.cross_check_content(source_text)
            if missing:
                lines.append(f"⚠️ 内容完整性: {len(missing)} 项素材数据未在PPT中找到")
                for m in missing:
                    lines.append(f"   - 缺失: {m}")
            else:
                lines.append("✅ 内容完整性: 素材关键数据已全部覆盖")

        return '\n'.join(lines)


# CLI entry
if __name__ == '__main__':
    import sys
    if len(sys.argv) < 2:
        print("Usage: python ppt_qa.py <pptx_path> [source_text_file]")
        sys.exit(1)
    pptx_path = sys.argv[1]
    source_text = None
    if len(sys.argv) >= 3:
        with open(sys.argv[2], 'r', encoding='utf-8') as f:
            source_text = f.read()
    qc = PPTQualityChecker(pptx_path)
    print(qc.full_report(source_text))


path: scripts/ppt_scorer.py
content: #!/usr/bin/env python
# -*- coding: utf-8 -*-
"""ppt_scorer.py — Per-page scoring & deck-level evaluation system.

Three layers:
1. Page Score: 6 dimensions, 100 points per page
2. Deck Score: 4 dimensions, 100 points total
3. Learning Loop: feedback recording + best practice capture

Usage:
    from ppt_scorer import PPTScorer
    scorer = PPTScorer("output.pptx")
    report = scorer.full_score_report()
    print(report)
"""
import re
import math
from collections import Counter
from pptx import Presentation
from pptx.util import Emu, Pt
from pptx.dml.color import RGBColor


class PPTScorer:
    """Per-page and deck-level PPT scoring system."""

    CANVAS_W = 10.0
    CANVAS_H = 5.625
    SAFE_MARGIN = 0.4
    CONTENT_TOP = 1.0  # below title
    CONTENT_BOTTOM = 5.225  # CANVAS_H - SAFE_MARGIN

    # Template colors (for color harmony check)
    TEMPLATE_COLORS = {
        (0xC0, 0x00, 0x00),  # RED
        (0xBB, 0x00, 0x00),  # RED2
        (0x33, 0x33, 0x33),  # DARK
        (0x66, 0x66, 0x66),  # MID
        (0x99, 0x99, 0x99),  # LIGHT
        (0xF5, 0xF5, 0xF5),  # BG
        (0xFF, 0xFF, 0xFF),  # WHITE
        (0xE0, 0xE0, 0xE0),  # BORDER
        (0x6B, 0x6B, 0x6B),  # ACCENT_GREY
        (0x8B, 0x1A, 0x1A),  # DARK_RED
        (0xE0, 0x6C, 0x5C),  # CORAL
        (0xFC, 0xE4, 0xE4),  # LIGHT_RED
        (0xEE, 0xEE, 0xEE),  # LIGHT_GREY2
        (0xF5, 0xD5, 0xD5),  # LIGHT_RED2
        (0x2B, 0x7A, 0xB8),  # BLUE (legacy)
        (0x90, 0x00, 0x00),  # darker red borders
        (0xA0, 0x00, 0x00),  # darker red
        (0xE8, 0x50, 0x50),  # lighter red (ring)
        (0xE0, 0xC0, 0xC0),  # light pink halo
        (0x00, 0x00, 0x00),  # pure black (rare but valid)
    }

    def __init__(self, pptx_path):
        self.path = pptx_path
        self.prs = Presentation(pptx_path)
        self.slides = list(self.prs.slides)
        self.n_slides = len(self.slides)

    # ══════════════════════════════════════════════════════
    # LAYER 1: Per-Page Scoring (6 dimensions, 100 points)
    # ══════════════════════════════════════════════════════

    def score_page(self, slide_idx):
        """Score a single page across 6 dimensions. Returns dict with scores + suggestions."""
        slide = self.slides[slide_idx]
        is_cover = (slide_idx == 0 or slide_idx == self.n_slides - 1)

        shapes_data = self._extract_shapes(slide)
        text_shapes = [s for s in shapes_data if s['text']]
        all_text = ' '.join(s['text'] for s in text_shapes)
        char_count = len(all_text.replace(' ', '').replace('\n', ''))

        scores = {}
        suggestions = []

        # ── D1: Structure (20 pts) ──
        scores['结构合理'] = self._score_structure(shapes_data, is_cover, suggestions)

        # ── D2: Whitespace (20 pts) ──
        scores['留白呼吸'] = self._score_whitespace(shapes_data, is_cover, suggestions)

        # ── D3: Information Load (20 pts) ──
        scores['信息承载'] = self._score_info_load(char_count, is_cover, suggestions)

        # ── D4: Visual Hierarchy (20 pts) ──
        scores['视觉层次'] = self._score_visual_hierarchy(text_shapes, suggestions)

        # ── D5: Color Harmony (10 pts) ──
        scores['配色协调'] = self._score_color_harmony(shapes_data, suggestions)

        # ── D6: Layout Fit (10 pts) ──
        scores['布局适配'] = self._score_layout_fit(shapes_data, char_count, suggestions)

        total = sum(scores.values())
        return {
            'slide': slide_idx + 1,
            'total': total,
            'scores': scores,
            'suggestions': suggestions,
            'is_cover': is_cover,
            'char_count': char_count,
        }

    def _extract_shapes(self, slide):
        """Extract shape metadata from a slide."""
        shapes = []
        for shape in slide.shapes:
            t = Emu(shape.top).inches
            l = Emu(shape.left).inches
            w = Emu(shape.width).inches
            h = Emu(shape.height).inches
            text = ''
            font_sizes = []
            colors = []
            if shape.has_text_frame:
                text = shape.text_frame.text.strip()
                for para in shape.text_frame.paragraphs:
                    for run in para.runs:
                        if run.font.size:
                            font_sizes.append(Emu(run.font.size).pt if hasattr(Emu(run.font.size), 'pt') else run.font.size / 12700)
                        if run.font.color and run.font.color.type is not None:
                            try:
                                c = run.font.color.rgb
                                if c:
                                    colors.append((int(str(c)[0:2], 16),
                                                  int(str(c)[2:4], 16),
                                                  int(str(c)[4:6], 16)))
                            except (AttributeError, ValueError):
                                pass
            # Shape fill color
            fill_color = None
            try:
                if shape.fill and shape.fill.fore_color and shape.fill.fore_color.rgb:
                    fc = shape.fill.fore_color.rgb
                    fill_color = (fc[0] if isinstance(fc[0], int) else int(str(fc)[0:2], 16),
                                 fc[1] if isinstance(fc[1], int) else int(str(fc)[2:4], 16),
                                 fc[2] if isinstance(fc[2], int) else int(str(fc)[4:6], 16))
            except:
                pass

            shapes.append({
                'top': t, 'left': l, 'width': w, 'height': h,
                'bottom': t + h, 'right': l + w,
                'text': text, 'font_sizes': font_sizes,
                'colors': colors, 'fill_color': fill_color,
            })
        return shapes

    # ── Scoring Functions ─────────────────────────────────

    def _score_structure(self, shapes, is_cover, suggestions):
        """D1: Structure (20 pts) — vertical centering, spacing uniformity."""
        if is_cover:
            return 18  # covers are manually placed, light check

        score = 20
        content_shapes = [s for s in shapes if s['top'] >= 0.8]
        if not content_shapes:
            return 15

        # Vertical centering check
        content_top = min(s['top'] for s in content_shapes)
        content_bottom = max(s['bottom'] for s in content_shapes)
        content_h = content_bottom - content_top
        area_h = self.CONTENT_BOTTOM - self.CONTENT_TOP

        if content_h < area_h * 0.55:
            space_above = content_top - self.CONTENT_TOP
            space_below = self.CONTENT_BOTTOM - content_bottom
            if space_above > 0:
                ratio = space_below / space_above
            else:
                ratio = 10
            if ratio > 4 or ratio < 0.25:
                score -= 8
                suggestions.append('垂直严重偏移，建议居中放置内容')
            elif ratio > 2.5 or ratio < 0.4:
                score -= 4
                suggestions.append('垂直略偏，可微调居中')
            elif ratio > 1.8 or ratio < 0.55:
                score -= 2
                suggestions.append('垂直可再优化')

        # Element spacing uniformity (check if gaps are roughly even)
        if len(content_shapes) >= 3:
            tops = sorted(set(round(s['top'], 1) for s in content_shapes))
            if len(tops) >= 3:
                gaps = [tops[i+1] - tops[i] for i in range(len(tops)-1)]
                if gaps:
                    avg_gap = sum(gaps) / len(gaps)
                    max_deviation = max(abs(g - avg_gap) for g in gaps)
                    if max_deviation > avg_gap * 0.8 and avg_gap > 0.3:
                        score -= 3
                        suggestions.append('元素间距不均匀')

        return max(score, 0)

    def _score_whitespace(self, shapes, is_cover, suggestions):
        """D2: Whitespace (20 pts) — content fills 40-75% of available area."""
        if is_cover:
            return 18

        score = 20
        content_shapes = [s for s in shapes if s['top'] >= 0.8]
        if not content_shapes:
            return 10

        # Calculate bounding box area of content
        content_top = min(s['top'] for s in content_shapes)
        content_bottom = max(s['bottom'] for s in content_shapes)
        content_left = min(s['left'] for s in content_shapes)
        content_right = max(s['right'] for s in content_shapes)
        content_area = (content_bottom - content_top) * (content_right - content_left)

        avail_area = (self.CONTENT_BOTTOM - self.CONTENT_TOP) * (self.CANVAS_W - 2 * self.SAFE_MARGIN)
        fill_ratio = content_area / avail_area if avail_area > 0 else 0

        if fill_ratio < 0.25:
            score -= 10
            suggestions.append(f'内容过少(填充{fill_ratio:.0%})，页面空旷')
        elif fill_ratio < 0.40:
            score -= 5
            suggestions.append(f'留白偏多(填充{fill_ratio:.0%})，可增加内容或放大元素')
        elif fill_ratio > 0.85:
            score -= 8
            suggestions.append(f'内容过密(填充{fill_ratio:.0%})，缺少呼吸空间')
        elif fill_ratio > 0.75:
            score -= 3
            suggestions.append(f'略显拥挤(填充{fill_ratio:.0%})，可精简或拆页')

        return max(score, 0)

    def _score_info_load(self, char_count, is_cover, suggestions):
        """D3: Information Load (20 pts) — 60-220 chars ideal for content slides."""
        if is_cover:
            return 18 if char_count < 80 else 15

        score = 20
        if char_count < 30:
            score -= 10
            suggestions.append(f'文字过少({char_count}字)，内容薄弱')
        elif char_count < 60:
            score -= 5
            suggestions.append(f'文字偏少({char_count}字)，可补充要点')
        elif char_count > 350:
            score -= 10
            suggestions.append(f'文字过多({char_count}字)，建议拆页或精简')
        elif char_count > 250:
            score -= 5
            suggestions.append(f'文字偏多({char_count}字)，可精简')
        elif char_count > 220:
            score -= 2
            suggestions.append(f'文字量偏高({char_count}字)')

        return max(score, 0)

    def _score_visual_hierarchy(self, text_shapes, suggestions):
        """D4: Visual Hierarchy (20 pts) — font size gradient, color layers."""
        score = 20

        # Collect all font sizes
        all_sizes = []
        for s in text_shapes:
            all_sizes.extend(s['font_sizes'])

        if not all_sizes:
            return 12

        unique_sizes = sorted(set(round(s) for s in all_sizes), reverse=True)

        # Font size gradient: should have at least 2 distinct sizes
        if len(unique_sizes) < 2:
            score -= 8
            suggestions.append('字号单一，缺少层次(标题/正文应有大小区分)')
        elif len(unique_sizes) < 3:
            score -= 3
            suggestions.append('字号层次可再丰富(考虑标题/副标题/正文三级)')

        # Size ratio: largest should be at least 1.5x smallest
        if len(unique_sizes) >= 2:
            ratio = unique_sizes[0] / unique_sizes[-1] if unique_sizes[-1] > 0 else 1
            if ratio < 1.3:
                score -= 4
                suggestions.append('最大字号与最小字号差异不够，层次感弱')

        # Color diversity: should have at least 2 distinct text colors
        all_colors = []
        for s in text_shapes:
            all_colors.extend(s['colors'])
        unique_colors = set(all_colors)
        if len(unique_colors) < 2:
            score -= 4
            suggestions.append('文字颜色单一，缺少视觉重点')

        return max(score, 0)

    def _score_color_harmony(self, shapes, suggestions):
        """D5: Color Harmony (10 pts) — colors within template palette."""
        score = 10

        all_fill_colors = []
        for s in shapes:
            if s['fill_color']:
                all_fill_colors.append(s['fill_color'])

        if not all_fill_colors:
            return 8  # no fills to check

        off_palette = 0
        for c in all_fill_colors:
            # Check if color is close to any template color (tolerance 30)
            min_dist = min(
                math.sqrt((c[0]-tc[0])**2 + (c[1]-tc[1])**2 + (c[2]-tc[2])**2)
                for tc in self.TEMPLATE_COLORS
            )
            if min_dist > 50:  # significantly off-palette
                off_palette += 1

        if off_palette > 3:
            score -= 6
            suggestions.append(f'{off_palette}个元素使用了模板外颜色')
        elif off_palette > 1:
            score -= 3
            suggestions.append(f'{off_palette}个元素颜色偏离模板色系')
        elif off_palette == 1:
            score -= 1

        return max(score, 0)

    def _score_layout_fit(self, shapes, char_count, suggestions):
        """D6: Layout Fit (10 pts) — layout type matches content volume."""
        score = 10
        n_shapes = len(shapes)

        # Heuristic: too many shapes for little text = over-decorated
        if n_shapes > 25 and char_count < 80:
            score -= 4
            suggestions.append('元素多但文字少，可能过度装饰')

        # Too few shapes for lots of text = wall of text
        if n_shapes < 5 and char_count > 200:
            score -= 4
            suggestions.append('文字多但元素少，缺少结构化展示')

        return max(score, 0)

    # ══════════════════════════════════════════════════════
    # LAYER 2: Deck Score (4 dimensions, 100 points)
    # ══════════════════════════════════════════════════════

    def score_deck(self, page_scores=None):
        """Score the entire deck. Returns dict."""
        if page_scores is None:
            page_scores = [self.score_page(i) for i in range(self.n_slides)]

        scores = {}
        suggestions = []

        # ── Narrative Flow (25 pts): titles tell a story ──
        titles = []
        for ps in page_scores:
            slide = self.slides[ps['slide'] - 1]
            for shape in slide.shapes:
                if shape.has_text_frame:
                    t = Emu(shape.top).inches
                    h = Emu(shape.height).inches
                    if t < 0.6 and h < 0.6:
                        titles.append(shape.text_frame.text.strip())
                        break
        score_narrative = 25
        if len(titles) < 3:
            score_narrative -= 5
        # Check for duplicate titles
        title_counter = Counter(titles)
        dupes = sum(1 for t, c in title_counter.items() if c > 1 and t)
        if dupes > 0:
            score_narrative -= 5 * dupes
            suggestions.append(f'{dupes}个重复标题，叙事线不清晰')
        scores['叙事线'] = max(score_narrative, 0)

        # ── Rhythm (25 pts): density variation ──
        densities = [ps['char_count'] for ps in page_scores if not ps['is_cover']]
        score_rhythm = 25
        if densities:
            avg_d = sum(densities) / len(densities)
            std_d = math.sqrt(sum((d - avg_d)**2 for d in densities) / len(densities)) if len(densities) > 1 else 0
            cv = std_d / avg_d if avg_d > 0 else 0
            if cv < 0.1 and len(densities) > 3:
                score_rhythm -= 8
                suggestions.append('各页信息量过于均匀，缺少节奏感(建议有疏有密)')
            elif cv < 0.15 and len(densities) > 3:
                score_rhythm -= 4
                suggestions.append('信息节奏可再丰富')
        scores['节奏感'] = max(score_rhythm, 0)

        # ── Diversity (25 pts): layout variety ──
        from ppt_qa import PPTQualityChecker
        qc = PPTQualityChecker(self.path)
        layout_issues = qc.check_layout_diversity()
        score_diversity = 25
        score_diversity -= 5 * len(layout_issues)
        if layout_issues:
            for li in layout_issues:
                suggestions.append(li)
        scores['多样性'] = max(score_diversity, 0)

        # ── Average Page Score (25 pts) ──
        content_scores = [ps['total'] for ps in page_scores if not ps['is_cover']]
        avg_score = sum(content_scores) / len(content_scores) if content_scores else 50
        scores['页均分'] = min(25, int(avg_score / 4))  # scale 100 -> 25

        total = sum(scores.values())
        return {
            'total': total,
            'scores': scores,
            'suggestions': suggestions,
        }

    # ══════════════════════════════════════════════════════
    # LAYER 3: Learning Loop (feedback + best practice)
    # ══════════════════════════════════════════════════════

    @staticmethod
    def record_feedback(memory_dir, layout_name, issue, fix):
        """Record feedback to memory/ppt-feedback.md."""
        import os
        from datetime import datetime
        feedback_path = os.path.join(memory_dir, 'ppt-feedback.md')

        # Create file if needed
        if not os.path.exists(feedback_path):
            with open(feedback_path, 'w', encoding='utf-8') as f:
                f.write('# PPT Feedback Log\n\n')
                f.write('| 日期 | 布局 | 问题 | 修复 |\n')
                f.write('|------|------|------|------|\n')

        date_str = datetime.now().strftime('%Y-%m-%d')
        line = f'| {date_str} | {layout_name} | {issue} | {fix} |\n'
        with open(feedback_path, 'a', encoding='utf-8') as f:
            f.write(line)

    @staticmethod
    def record_best_practice(memory_dir, slide_num, score, layout_name, char_count, notes=''):
        """Record high-scoring pages as best practices."""
        import os
        from datetime import datetime
        bp_path = os.path.join(memory_dir, 'ppt-best-practices.md')

        if not os.path.exists(bp_path):
            with open(bp_path, 'w', encoding='utf-8') as f:
                f.write('# PPT Best Practices\n\n')
                f.write('| 日期 | 页面 | 分数 | 布局 | 字数 | 备注 |\n')
                f.write('|------|------|------|------|------|------|\n')

        date_str = datetime.now().strftime('%Y-%m-%d')
        line = f'| {date_str} | S{slide_num} | {score}/100 | {layout_name} | {char_count} | {notes} |\n'
        with open(bp_path, 'a', encoding='utf-8') as f:
            f.write(line)

    # ══════════════════════════════════════════════════════
    # Full Score Report
    # ══════════════════════════════════════════════════════

    def full_score_report(self):
        """Generate complete scoring report: per-page + deck."""
        lines = []
        lines.append(f'🏆 PPT 评分报告 ({self.path})')
        lines.append(f'总页数: {self.n_slides}')
        lines.append('')

        # Per-page scores
        page_scores = []
        for i in range(self.n_slides):
            ps = self.score_page(i)
            page_scores.append(ps)

            slide_label = '封面' if i == 0 else ('结尾' if i == self.n_slides - 1 else f'内容')
            grade = self._grade(ps['total'])
            lines.append(f'📊 S{ps["slide"]} [{slide_label}] {ps["total"]}/100 {grade}')

            for dim, val in ps['scores'].items():
                max_val = 20 if dim in ('结构合理', '留白呼吸', '信息承载', '视觉层次') else 10
                bar = '█' * int(val / max_val * 5) + '░' * (5 - int(val / max_val * 5))
                lines.append(f'  {dim}: {val}/{max_val} {bar}')

            if ps['suggestions']:
                lines.append(f'  💡 建议:')
                for s in ps['suggestions']:
                    lines.append(f'     - {s}')
            lines.append('')

        # Deck score
        deck = self.score_deck(page_scores)
        grade = self._grade(deck['total'])
        lines.append(f'═══════════════════════════════')
        lines.append(f'🎯 整体评分: {deck["total"]}/100 {grade}')
        for dim, val in deck['scores'].items():
            bar = '█' * int(val / 25 * 5) + '░' * (5 - int(val / 25 * 5))
            lines.append(f'  {dim}: {val}/25 {bar}')
        if deck['suggestions']:
            lines.append(f'  💡 整体建议:')
            for s in deck['suggestions']:
                lines.append(f'     - {s}')

        # Summary
        lines.append('')
        avg_page = sum(ps['total'] for ps in page_scores) / len(page_scores) if page_scores else 0
        best_page = max(page_scores, key=lambda x: x['total']) if page_scores else None
        worst_page = min((ps for ps in page_scores if not ps['is_cover']), key=lambda x: x['total'], default=None)

        lines.append(f'📈 页均分: {avg_page:.0f}/100')
        if best_page:
            lines.append(f'🌟 最佳页: S{best_page["slide"]} ({best_page["total"]}/100)')
        if worst_page:
            lines.append(f'🔧 待优化: S{worst_page["slide"]} ({worst_page["total"]}/100)')

        return '\n'.join(lines)

    @staticmethod
    def _grade(score):
        """Convert score to letter grade."""
        if score >= 90:
            return '⭐ A'
        elif score >= 80:
            return '✅ B'
        elif score >= 70:
            return '🔶 C'
        elif score >= 60:
            return '⚠️ D'
        else:
            return '❌ F'


# CLI entry
if __name__ == '__main__':
    import sys
    if len(sys.argv) < 2:
        print("Usage: python ppt_scorer.py <pptx_path>")
        sys.exit(1)
    scorer = PPTScorer(sys.argv[1])
    print(scorer.full_score_report())


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