File size: 8,761 Bytes
95f5290
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
import { execFileSync } from 'node:child_process';
import { existsSync, writeFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';

const docsDir = dirname(fileURLToPath(import.meta.url));
const imagesDir = join(docsDir, 'images');
const screenshotDir = process.env.DOC_SCREENSHOT_DIR
  || join(docsDir, '..', '..', '文件测试', '图片');
const imageWidth = 2495;
const imageHeight = 1332;

// Each mark is [number, circleX, circleY, targetX, targetY]. The arrowhead is
// always placed on the target, so every label maps to exactly one UI control
// or one clearly bounded display area.
const guides = [
  {
    input: '企业微信截图_17887710623711.png', source: '02-home.png', output: '02-home-annotated.png',
    labels: ['左侧页面菜单', '数据统计', '项目列表', '最近数据', '刷新页面'],
    marks: [
      [1, 95, 570, 95, 430], [2, 850, 285, 850, 190], [3, 620, 920, 620, 800],
      [4, 1760, 920, 1760, 800], [5, 2360, 130, 2420, 43],
    ],
  },
  {
    input: '企业微信截图_17887710942911.png', source: 'projects-list.png', output: 'projects-list-annotated.png',
    labels: ['搜索项目', '新建项目', '项目名称、状态和统计', '编辑项目', '删除项目', '展开接收批次'],
    marks: [
      [1, 2070, 82, 2180, 46], [2, 2460, 92, 2418, 46], [3, 1120, 54, 650, 122],
      [4, 2180, 165, 2288, 124], [5, 2280, 260, 2378, 214], [6, 2375, 350, 2446, 302],
    ],
  },
  {
    input: '企业微信截图_17887711082760.png', source: 'project-new.png', output: 'project-new-annotated.png',
    labels: ['项目名称', '绑定一个工作流', '项目状态', '计划接收数量(0 不限制)', '项目说明(可不填)', '保存项目'],
    marks: [
      [1, 235, 176, 330, 176], [2, 2470, 176, 2375, 176], [3, 235, 241, 330, 241],
      [4, 2470, 241, 2375, 241], [5, 235, 316, 330, 316], [6, 2470, 390, 2415, 377],
    ],
  },
  {
    input: '企业微信截图_17887711739904.png', source: 'project-workflow-select.png', output: 'project-workflow-select-annotated.png',
    labels: ['打开工作流列表', '新建空白工作流', '模板工作流', '绑定已有工作流', '编辑工作流'],
    marks: [
      [1, 2470, 145, 2432, 174], [2, 2470, 205, 1395, 210], [3, 2470, 255, 1515, 240],
      [4, 2470, 305, 1395, 296], [5, 2470, 365, 2422, 240],
    ],
  },
  {
    input: '企业微信截图_17887712673822.png', source: 'workflow-empty.png', output: 'workflow-empty-annotated.png',
    labels: ['选择已有或新建工作流', '修改工作流名称', '连接节点的画布', '模块列表(拖入画布)', '套用处理模板', '保存工作流'],
    marks: [
      [1, 430, 72, 350, 20], [2, 585, 72, 470, 20], [3, 1030, 700, 1030, 560],
      [4, 2100, 475, 2295, 285], [5, 2310, 72, 2374, 20], [6, 2435, 72, 2458, 20],
    ],
  },
  {
    input: '企业微信截图_17887712837205.png', source: 'workflow-template.png', output: 'workflow-template-annotated.png',
    labels: ['RGB-D 处理链模板', '双目 RGB 处理链模板', '取消并关闭'],
    marks: [
      [1, 1000, 650, 1210, 650], [2, 1000, 720, 1210, 720], [3, 1480, 840, 1390, 805],
    ],
  },
  {
    input: '企业微信截图_17887785935712.png', source: 'workflow-editor.png', output: 'workflow-editor-annotated.png',
    labels: ['双目 RGB 输入', '手套传感器输入', 'AI 自动标注', '人工审核', 'LeRobot 导出', '保存工作流'],
    marks: [
      [1, 500, 460, 500, 535], [2, 500, 920, 500, 760], [3, 1050, 920, 1050, 710],
      [4, 1500, 920, 1500, 710], [5, 2000, 850, 2000, 650], [6, 2415, 75, 2418, 20],
    ],
  },
  {
    input: '企业微信截图_17887784357745.png', source: 'workflow-ai-settings.png', output: 'workflow-ai-settings-annotated.png',
    labels: ['标注语言', '已保存的 API 方案', 'API 厂商', 'API 模型', 'API 密钥', 'API 地址', '测试连接', '保存设置'],
    marks: [
      [1, 800, 601, 915, 601], [2, 800, 680, 915, 680], [3, 800, 765, 915, 765],
      [4, 800, 850, 915, 850], [5, 800, 937, 915, 937], [6, 800, 1023, 915, 1023],
      [7, 930, 1130, 1095, 1100], [8, 1285, 1130, 1200, 1100],
    ],
  },
  {
    input: '企业微信截图_17887713752576.png', source: 'annotation-list.png', output: 'annotation-list-annotated.png',
    labels: ['进入标注页面', '展开项目', '选择 Episode'],
    marks: [[1, 205, 330, 72, 280], [2, 2110, 62, 2210, 52], [3, 2110, 135, 2205, 117]],
  },
  {
    input: '企业微信截图_17887720915441.png', source: 'annotation-editor.png', output: 'annotation-editor-annotated.png',
    labels: ['预览显示选项', 'RGB 与 2D 关键点', '3D 手部空间', '深度伪彩色预览', '运行 AI 标注', '选择一个标注片段', '设置开始帧和结束帧', '保存标注修改'],
    marks: [
      [1, 380, 105, 260, 65], [2, 560, 760, 560, 500], [3, 1500, 760, 1500, 545],
      [4, 1100, 1160, 1100, 1010], [5, 2110, 245, 2315, 245], [6, 2110, 395, 2315, 335],
      [7, 2110, 1120, 2315, 1170], [8, 2110, 1270, 2320, 1295],
    ],
  },
  {
    input: '企业微信截图_17887721273258.png', source: 'reviewing-list.png', output: 'reviewing-list-annotated.png',
    labels: ['待审核列表', '展开项目', '选择 Episode 并查看信息', '通过当前 Episode', '重新运行工作流'],
    marks: [
      [1, 205, 380, 75, 365], [2, 2110, 60, 2210, 52], [3, 2110, 135, 2215, 118],
      [4, 2110, 205, 2315, 203], [5, 2110, 245, 2315, 235],
    ],
  },
  {
    source: 'reviewing-player.png', output: 'reviewing-player-annotated.png',
    labels: ['预览显示选项', 'RGB 原图(可叠加关键点)', '3D 手部空间', '深度伪彩色预览', '只读标注片段', '播放和逐帧控制', '通过当前 Episode'],
    marks: [
      [1, 380, 105, 260, 65], [2, 560, 760, 560, 500], [3, 1500, 760, 1500, 545],
      [4, 1100, 1160, 1100, 1010], [5, 2110, 505, 2315, 505],
      [6, 1910, 1180, 1910, 1220], [7, 2110, 205, 2315, 203],
    ],
  },
  {
    input: '企业微信截图_17887721723042.png', source: 'approved-list.png', output: 'approved-list-annotated.png',
    labels: ['已通过列表', '当前处于多选模式', '全选当前列表', '单选或多选 Episode', '导出这一条 Episode', '取消审核'],
    marks: [
      [1, 205, 430, 75, 405], [2, 2110, 34, 2315, 18], [3, 2110, 75, 2190, 50],
      [4, 2110, 125, 2188, 120], [5, 2110, 205, 2280, 235], [6, 2470, 275, 2405, 235],
    ],
  },
  {
    input: '企业微信截图_17887722547409.png', source: 'trash.png', output: 'trash-annotated.png',
    labels: ['垃圾桶入口', '剩余保留时间', '恢复数据', '永久删除', '清空垃圾桶'],
    marks: [
      [1, 205, 1225, 75, 1225], [2, 1160, 180, 1220, 119], [3, 1430, 180, 1395, 119],
      [4, 1535, 180, 1490, 119], [5, 1700, 72, 1660, 42],
    ],
  },
];

function renderSvg(guide) {
  const arrows = guide.marks.map(([number, cx, cy, tx, ty]) => `
    <line class="arrow" x1="${cx}" y1="${cy}" x2="${tx}" y2="${ty}"/>
    <circle class="mark" cx="${cx}" cy="${cy}" r="27"/>
    <text class="num" x="${cx}" y="${cy}">${number}</text>`).join('');

  return `<svg xmlns="http://www.w3.org/2000/svg" width="${imageWidth}" height="${imageHeight}" viewBox="0 0 ${imageWidth} ${imageHeight}">
  <defs>
    <marker id="arrow" markerUnits="userSpaceOnUse" markerWidth="24" markerHeight="24" refX="22" refY="12" orient="auto">
      <path d="M0,0 L24,12 L0,24 Z" fill="#ff5a4f"/>
    </marker>
    <style>
      .arrow{stroke:#ff5a4f;stroke-width:6;fill:none;marker-end:url(#arrow)}
      .mark{fill:#2f80ed;stroke:#fff;stroke-width:3}
      .num{font:700 29px 'Noto Sans CJK SC',sans-serif;fill:#fff;text-anchor:middle;dominant-baseline:central}
    </style>
  </defs>
  ${arrows}
</svg>`;
}

for (const guide of guides) {
  const source = join(imagesDir, guide.source);
  if (guide.input) {
    const original = join(screenshotDir, guide.input);
    if (!existsSync(original)) throw new Error(`Missing source screenshot: ${original}`);
    execFileSync('ffmpeg', [
      '-hide_banner', '-loglevel', 'error', '-y', '-i', original,
      '-frames:v', '1', source,
    ]);
  }
  if (!existsSync(source)) throw new Error(`Missing local source screenshot: ${source}`);

  const overlay = join(imagesDir, guide.output.replace('-annotated.png', '-guide.svg'));
  const output = join(imagesDir, guide.output);
  writeFileSync(overlay, renderSvg(guide));
  execFileSync('ffmpeg', [
    '-hide_banner', '-loglevel', 'error', '-y',
    '-i', source, '-i', overlay,
    '-filter_complex', '[0:v][1:v]overlay=0:0:format=auto',
    '-frames:v', '1', output,
  ]);
}

console.log(`Rendered ${guides.length} documentation images from ${screenshotDir}.`);