File size: 24,546 Bytes
831819e
b0b604c
831819e
b0b604c
831819e
 
 
 
 
 
 
aedecda
 
831819e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
aedecda
831819e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
aedecda
831819e
 
 
 
 
aedecda
831819e
 
 
271b60d
 
 
 
 
 
 
 
 
 
 
 
 
 
aedecda
 
 
 
 
 
 
 
271b60d
 
 
 
aedecda
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
831819e
 
 
 
 
 
 
 
aaa753d
aedecda
 
 
 
 
 
271b60d
 
831819e
aedecda
831819e
 
 
 
 
 
 
aaa753d
aedecda
831819e
aedecda
831819e
 
 
 
aedecda
 
 
 
831819e
 
 
 
 
 
aedecda
831819e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
aedecda
831819e
 
 
 
 
aedecda
aaa753d
aedecda
831819e
 
 
 
 
 
 
 
aedecda
 
 
 
 
831819e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b0b604c
 
831819e
 
 
 
 
 
 
 
 
aaa753d
831819e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
aedecda
 
 
 
 
 
831819e
 
aedecda
 
 
831819e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
aaa753d
831819e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b0b604c
831819e
 
 
 
 
 
 
aedecda
 
 
 
 
 
 
 
 
831819e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
aaa753d
831819e
 
 
 
 
 
 
 
 
 
 
b0b604c
831819e
 
 
 
b0b604c
831819e
 
 
b0b604c
 
831819e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
aaa753d
831819e
 
 
 
 
 
 
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
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
import { esc, number, icon, enhance, table as sharedTable } from "./ui.js";
import { versionsView, versionFromHash } from "./versions.js?v=history-20261103";
let content,
  versionHistory,
  registry = [],
  snapshot,
  root,
  timer,
  ageTimer,
  request,
  generation = 0;
let selectedCampaign = null,
  selectedVersion = "006",
  selectedJobs = "active",
  registryQuery = "",
  registryPage = 0;
const clock = (t) =>
  new Date(t * 1000).toLocaleString("zh-CN", {
    timeZone: "Asia/Kuala_Lumpur",
    month: "2-digit",
    day: "2-digit",
    hour: "2-digit",
    minute: "2-digit",
    hour12: false,
  }) + " MYT";
const table = (heads, rows) => sharedTable(heads, rows, "synth-table");
const phases = {
  WAIT_SSD_CAPACITY: "等待存储准入",
  WAIT_CAPACITY: "等待容量",
  WAIT_SHARED_GPU_FOR_CYCLES: "等待GPU",
  V5_APPEARANCE_CANONICAL_SURFACE: "表面与材质",
  V5_THREE_MOUNT_INDEPENDENT_CAMERA: "相机轨迹",
  V5_RANDOMIZED_SCENE_CANONICAL_SURFACE: "场景组装",
  CYCLES_OPTIX_16SPP_GPU_OIDN: "RGB渲染",
  VIDEO_POST_AND_DEPTH_OVERLAPPED: "视频后处理 / 深度",
  PACKAGE_NATIVE_RENDER_CHANNELS: "打包",
  VERIFIED_SEQUENTIAL_NAS_PUBLICATION: "NAS归档",
  INITIALIZING: "初始化",
  FAILED: "待定点修复",
  WAIT_UPSTREAM_ASSET: "等待上游资产",
};
const sections = [
  ["progress", "进度"],
  ["versions", "版本"],
  ["reading", "数据读取"],
  ["samples", "样例"],
];
const tab = () => location.hash.split("/")[1] || "progress";
function section(title, body) {
  return `<section class="synth-section"><h2>${esc(title)}</h2>${body}</section>`;
}
function code(title, text) {
  return `<section><h3 class="synth-code-title">${esc(title)}</h3><div class="synth-code">${icon("copy", "复制代码", "data-copy")}<pre><code>${esc(text)}</code></pre></div></section>`;
}
function icons() {
  enhance();
}
export function valid(value) {
  const c = value && value.counts;
  return (
    value &&
    value.schema === "egorenderer.public_progress.v1" &&
    Number.isFinite(value.collected_at) &&
    c &&
    ["complete", "running", "ready", "failed", "total"].every(
      (k) => Number.isInteger(c[k]) && c[k] >= 0,
    ) &&
    c.total > 0 &&
    c.complete + c.running + c.ready + c.failed === c.total
  );
}
function header() {
  return `<div class="synth-head"><div class="synth-title"><h1>EgoRenderer</h1><span class="release-label">V006</span><select id="synth-campaign" aria-label="生产数据集"><option value="OakInk2">OakInk2</option><option value="ARCTIC">ARCTIC</option></select><span class="synth-identity" id="synth-execution"></span></div><div class="synth-status" id="synth-status" aria-live="polite"><span class="synth-dot"></span><span id="synth-freshness">读取快照</span>${icon("refresh-cw", "刷新进度", 'id="synth-refresh"')}</div></div>
      <nav class="synth-nav" aria-label="合成数据">${sections.map(([id, label]) => `<a href="#synthetic/${id}" ${tab() === id ? 'aria-current="page"' : ""}>${label}</a>`).join("")}</nav>`;
}
function progressView() {
  return `<div id="synth-summary" class="synth-kpis" aria-live="polite"></div><div class="part"><div id="synth-progress" class="synth-progress" role="progressbar" aria-label="ARCTIC随机化包生成进度"></div><div id="synth-facts" class="synth-facts"></div></div>
      <div id="synth-notices"></div>${section("数据范围", '<div id="synth-cohorts"></div>')}
      <section class="synth-section"><div class="synth-toolbar"><h2>当前任务</h2><select id="synth-job-filter" aria-label="任务状态"><option value="active">运行中</option><option value="failed">失败</option><option value="inputs">待上游资产</option><option value="all">全部未完成</option></select></div><div id="synth-jobs"></div></section>
      <details class="reference-details"><summary>主机与存储</summary><div id="synth-resources" class="synth-resource"></div><div id="synth-storage" class="synth-facts"></div></details>
      ${section("最近归档", '<div id="synth-recent"></div>')}`;
}
export function progressState(value, error = false, now = Date.now() / 1000) {
  if (!value)
    return { state: "error", prefix: error ? "状态暂不可用" : "读取快照" };
  if (error) return { state: "stale", prefix: "连接中断 · " };
  if (now - value.collected_at > 180)
    return { state: "stale", prefix: "更新已过期 · " };
  const c = value.counts;
  if (
    c.complete === c.total &&
    c.running === 0 &&
    c.ready === 0 &&
    c.failed === 0
  )
    return { state: "complete", prefix: "生成队列结束 · " };
  if (
    !value.queue_alive &&
    c.waiting_assets > 0 &&
    !c.running &&
    !c.ready &&
    !c.failed
  )
    return { state: "waiting", prefix: "等待上游资产 · " };
  if (!value.queue_alive) return { state: "stopped", prefix: "队列已停 · " };
  if (value.admission_held) return { state: "stopped", prefix: "暂停新增 · " };
  return { state: "live", prefix: "" };
}
export function campaignSnapshot(value, selected = null) {
  if (!value) return null;
  const choice = selected || value.default_campaign || "ARCTIC";
  const view =
    choice === "OakInk2" && value.oakink_render
      ? value.oakink_render
      : { dataset: "ARCTIC" };
  return {
    ...value,
    ...view,
    arctic_render: {
      counts: value.counts,
      scope: value.scope,
      coverage: value.coverage,
      known_gt_missing: value.known_gt_missing,
    },
  };
}
function updateFreshness(error) {
  if (!root) return;
  const status = root.querySelector("#synth-status"),
    label = root.querySelector("#synth-freshness");
  if (!snapshot) {
    status.dataset.state = "error";
    label.textContent = error ? "状态暂不可用" : "读取快照";
    return;
  }
  const current = campaignSnapshot(snapshot, selectedCampaign);
  const selector = root.querySelector("#synth-campaign");
  selector.querySelector('option[value="OakInk2"]').disabled =
    !snapshot.oakink_render;
  selector.value = current.dataset;
  const display = progressState(current, error);
  status.dataset.state = display.state;
  label.textContent = display.prefix + clock(snapshot.collected_at);
  root.querySelector("#synth-execution").textContent =
    "执行 " + current.execution_revision;
}
function updateProgress() {
  if (!root || tab() !== "progress") return;
  if (!snapshot) {
    root.querySelector("#synth-summary").innerHTML =
      '<p class="synth-empty">状态暂不可用。不会以零值替代上次进度。</p>';
    return;
  }
  const s = campaignSnapshot(snapshot, selectedCampaign),
    c = s.counts;
  const metrics = [
    ["渲染与深度完成", `${number(c.complete)} / ${number(c.total)}`],
    ["运行中", number(c.running)],
    ["待领取", number(c.ready)],
    ["失败", number(c.failed)],
  ];
  if (c.waiting_assets) metrics.push(["待上游资产", number(c.waiting_assets)]);
  root.querySelector("#synth-summary").dataset.columns = metrics.length;
  root.querySelector("#synth-summary").innerHTML = metrics
    .map(
      ([k, v]) =>
        `<div class="synth-kpi"><strong>${v}</strong><span>${k}</span></div>`,
    )
    .join("");
  const progress = root.querySelector("#synth-progress");
  progress.setAttribute("aria-label", s.dataset + "包生成进度");
  progress.setAttribute("aria-valuemin", "0");
  progress.setAttribute("aria-valuemax", c.total);
  progress.setAttribute("aria-valuenow", c.complete);
  progress.innerHTML = [
    ["done", c.complete],
    ["running", c.running],
    ["failed", c.failed],
  ]
    .map(
      ([cl, n]) =>
        `<span class="${cl}" style="width:${(100 * n) / c.total}%"></span>`,
    )
    .join("");
  root.querySelector("#synth-facts").innerHTML =
    `<span>完成 <b>${((100 * c.complete) / c.total).toFixed(1)}%</b></span><span>已生成帧 <b>${number(s.coverage.completed_variant_frames)} / ${number(s.scope.expected_variant_frames)}</b></span><span>最近1小时 <b>${number(s.throughput.packages_last_hour)}包</b></span><span>队列预计(不含失败) <b>${s.throughput.queued_eta_hours == null ? "—" : number(s.throughput.queued_eta_hours) + "小时"}</b></span>`;
  root.querySelector("#synth-notices").innerHTML =
    `<div class="synth-callout">训练资格待确认 · 辅助拟合待交付${s.dataset === "OakInk2" ? " · 下半身为生成结果" : ""}${s.known_gt_missing.length ? ` · <b>${s.known_gt_missing.length}</b> 个历史包缺GT深度` : ""}</div>`;
  const oak = s.oakink_assets,
    hc = s.hardcases;
  root.querySelector("#synth-cohorts").innerHTML = table(
    ["数据", "规模", "当前口径"],
    [
      `<tr><td>ARCTIC · V006</td><td class="num">${number(s.arctic_render.counts.complete)} / ${number(s.arctic_render.counts.total)}包</td><td>${number(s.arctic_render.scope.source_frames)}源帧;每条动作1份${s.arctic_render.known_gt_missing.length ? ";" + s.arctic_render.known_gt_missing.length + "个历史包缺GT深度" : ""}</td></tr>`,
      `<tr><td>Hard Cases · V006</td><td class="num">${hc.complete} / ${hc.expected}条样例</td><td>${number(hc.frames)}主源帧;三类各5条,独立于主队列</td></tr>`,
      `<tr><td>OakInk2 · V006</td><td class="num">${snapshot.oakink_render ? number(snapshot.oakink_render.counts.complete) + " / " + number(snapshot.oakink_render.counts.total) + "包" : number(oak.total) + "源"}</td><td>${number(oak.counts.COMPLETE || 0)}条人体资产就绪${snapshot.oakink_render ? ";" + number(snapshot.oakink_render.counts.waiting_assets) + "条待上游;每条动作1份" : ";尚无最终合成包计数"}</td></tr>`,
      '<tr><td>GRAB · V005</td><td class="num">1,314源组 / 2,425包</td><td>历史参数附件更新完成;V006新深度尚未据此宣称完成</td></tr>',
    ],
  );
  const selected = root.querySelector("#synth-job-filter");
  selected.value = selectedJobs;
  const jobs = s.jobs.filter(
    (j) =>
      selectedJobs === "all" ||
      (selectedJobs === "failed"
        ? j.state === "FAILED"
        : selectedJobs === "inputs"
          ? j.state === "WAIT_UPSTREAM_ASSET"
          : ["RUNNING", "RECOVERING_DEPTH"].includes(j.state)),
  );
  const multi = s.scope.variants > 1;
  root.querySelector("#synth-jobs").innerHTML = jobs.length
    ? table(
        ["源动作", ...(multi ? ["变体"] : []), "帧数", "存储", "阶段"],
        jobs.map(
          (j) =>
            `<tr><td class="source">${esc(j.source)}</td>${multi ? `<td class="num">${j.variant}</td>` : ""}<td class="num">${number(j.frames)}</td><td>${esc(j.storage || "待准入")}</td><td class="status-text ${j.state === "FAILED" ? "synth-error" : ""}">${esc(phases[j.phase] || j.phase)}</td></tr>`,
        ),
      )
    : '<p class="synth-empty">当前没有此类任务。</p>';
  const gpu = s.hardware.gpus || [];
  root.querySelector("#synth-resources").innerHTML = gpu
    .map(
      (g) =>
        `<dl><dt>GPU ${g.index}</dt><dd>${g.utilization}%</dd><progress aria-label="GPU ${g.index}利用率" max="100" value="${g.utilization}"></progress><dt>显存 GiB</dt><dd>${number(g.used_mib / 1024)} / ${number(g.total_mib / 1024)}</dd></dl>`,
    )
    .join("");
  root.querySelector("#synth-storage").innerHTML =
    `<span>CPU(主机) <b>${number(s.hardware.cpu_percent)}%</b></span><span>SSD预留 <b>${number(s.storage.ssd_reserved_gib)} / ${number(s.storage.ssd_budget_gib)} GiB</b></span><span>SSD可用 <b>${number(s.storage.ssd_available_gib)} GiB</b></span><span>NAS可用 <b>${number(s.storage.nas_available_gib / 1024)} TiB</b></span><span>NAS通道 <b>${s.storage.nas_active} / ${s.storage.nas_limit}</b></span>`;
  root.querySelector("#synth-recent").innerHTML = table(
    ["源动作", ...(multi ? ["变体"] : []), "归档时间"],
    s.recent.map(
      (j) =>
        `<tr><td class="source">${esc(j.source)}</td>${multi ? `<td class="num">${j.variant}</td>` : ""}<td>${clock(j.at)}</td></tr>`,
    ),
  );
}
function versionPage() {
  return `${versionsView(content, versionHistory, selectedVersion)}
      ${section(
        "V006关键执行修订",
        table(
          ["修订", "主题", "变化"],
          content.milestones.map(
            (r) => `<tr>${r.map((x) => `<td>${esc(x)}</td>`).join("")}</tr>`,
          ),
        ),
      )}
      <section class="synth-section"><div class="synth-toolbar"><h2>执行登记簿</h2><input id="synth-registry-search" type="search" aria-label="搜索执行修订" placeholder="版本、模块或状态" value="${esc(registryQuery)}"></div><div class="subtle">登记状态不是实时任务状态;执行修订不等同数据发布版本。</div><div id="synth-registry"></div></section>`;
}
function updateRegistry() {
  const target = root.querySelector("#synth-registry");
  if (!target) return;
  const rows = registry
    .filter((r) =>
      [r.id, r.title, r.status, r.scope]
        .join(" ")
        .toLowerCase()
        .includes(registryQuery.toLowerCase()),
    )
    .slice()
    .reverse();
  registryPage = Math.max(
    0,
    Math.min(registryPage, Math.ceil(rows.length / 25) - 1),
  );
  const start = registryPage * 25;
  target.innerHTML =
    table(
      ["修订", "模块", "登记状态 / 范围"],
      rows
        .slice(start, start + 25)
        .map(
          (r) =>
            `<tr><td class="num">${esc(r.id)}</td><td>${esc(r.title)}</td><td><details><summary>${esc(r.status)}</summary><p>${esc(r.scope || "范围见对应冻结合同。")}</p><p class="mono">SHA256 ${esc(r.sha256)}</p></details></td></tr>`,
        ),
    ) +
    `<div class="synth-pager"><span>${rows.length ? start + 1 : 0}–${Math.min(start + 25, rows.length)} / ${rows.length}</span>${icon("chevron-left", "上一页", "data-registry-prev")}${icon("chevron-right", "下一页", "data-registry-next")}</div>`;
  const prev = target.querySelector("[data-registry-prev]"),
    next = target.querySelector("[data-registry-next]");
  prev.disabled = registryPage === 0;
  next.disabled = start + 25 >= rows.length;
  prev.onclick = () => {
    registryPage--;
    updateRegistry();
  };
  next.onclick = () => {
    registryPage++;
    updateRegistry();
  };
  icons();
}
function readingView() {
  const active = campaignSnapshot(snapshot, selectedCampaign);
  const isOak = active?.dataset === "OakInk2";
  const indexPath = isOak
    ? active.package_index
    : "ARCTIC/packages/final_surface279_v287_20261030/INDEX.json";
  const index = `import json, os\nfrom pathlib import Path\n\nroot = Path(os.environ["EGORENDER_ROOT"])\nindex = json.loads((root / "${indexPath}").read_text())\nfor item in index["complete"]:\n    package = Path(item["package"])\n    meta = json.loads((package / "metadata.json").read_text())\n    print(meta["sequence_index"], meta["frames"], meta["dataset_release"])\n    # complete 是渲染终态;另查深度缺口与辅助标注。`;
  const depth = `from egorender_reader import DepthReader, depth_pair\nimport numpy as np\n\ngt_path, sensor_path = depth_pair(package)\nwith DepthReader(gt_path) as gt, DepthReader(sensor_path) as sensor:\n    assert np.array_equal(gt.frames, sensor.frames)\n    z_gt, valid_gt, source_frame = gt.frame(0)\n    z_sensor, valid_sensor, other_frame = sensor.frame(0)\n    assert source_frame == other_frame\n    # z: float32 米,相机轴向Z;分别使用各自的valid。\n    # 每个DataLoader worker长期持有reader,不逐帧重新打开整个NPZ。`;
  const camera = `with np.load(package / "camera_parameters.npz", allow_pickle=False) as c:\n    source_frames = c["source_frame_index"]\n    K = c["K"]\n    world_to_camera = c["world_to_camera"]\n    camera_to_world = c["camera_to_world"]\n    print(c["camera_coordinates"], c["world_coordinates"])\n    # 先核对坐标约定和K是否逐帧,不额外套一次坐标轴翻转。\n\nfrom egorender_reader import unpack_class_frame\nwith np.load(package / "masks_visible_2bit.npz", allow_pickle=False) as m:\n    width, height = map(int, m["image_size"])\n    classes = unpack_class_frame(m["packed_classes"][0], (height, width))\n    print(m["classes"], m["bitorder"])\n    # 当前常见: 0背景 / 1人体及衣物 / 2交互物体 / 3桌面;以文件为准。`;
  const native = isOak
    ? `descriptor = json.loads((package / "native_mano_descriptor.json").read_text())\nassert descriptor["quaternion_layout"] == "wxyz"\nwith np.load(package / "mano_parameters.npz", allow_pickle=False) as h:\n    print(h.files)  # 保留原始字段,不转成ARCTIC的axis-angle表示\nsource_offsets = np.load(package / "source_offsets.npy", allow_pickle=False)\nwith np.load(package / "native_object_poses.npz", allow_pickle=False) as p:\n    object_ids = p["object_ids"]\n    object_to_oakink = p["object_to_oakink"]\nwith np.load(package / "object_membership.npz", allow_pickle=False) as m:\n    print(m.files)\n# 原始source_offsets与30fps动作索引分别保留。\n# 物体姿态在原始OakInk坐标;按descriptor.transform_chain及metadata.source_to_world变换一次。\nassert meta["lower_motion_is_gt"] is False`
    : `with np.load(package / "mano_parameters.npz", allow_pickle=False) as h:\n    print(h.files)\n    left_pose = h["left_pose"]\n    left_translation = h["left_trans"]\n    source_frames = h["source_frames"]\n    # left/right_rot, pose, trans, shape来自原始数据集参数。\n    # 参数化/手均值/坐标系遵循原始模型与包内来源,不把shape当拟合真值。\n\n# V005辅助附件不是render输入:\nfit_ref = json.loads((legacy_package / "mano_fit_current.json").read_text())\n# 按fit_ref和对应合同加载参数;左手必须搭配V005修正基底。`;
  const relocation = `from egorender_reader import DatasetLocations\n\nloc = DatasetLocations(package / "artifact_locations.json")\n# 给定包内记录的 {"path": ..., "sha256": ...} 引用:\nresolved = loc.read_bound_json(binding)\n# 先检查原JSON字节SHA,再解析历史路径及path-keyed字典。\n# 不覆盖immutable报告,也不要通过字符串替换伪造新SHA。`;
  return `${section(
    "文件与语义",
    table(
      ["通道", "文件", "表示", "读取约束"],
      content.channels.map(
        (r) => `<tr>${r.map((x) => `<td>${esc(x)}</td>`).join("")}</tr>`,
      ),
    ),
  )}
      <section class="synth-section"><div class="synth-toolbar"><h2>Python读取</h2><a href="downloads/egorender_reader.py" download>下载读取器.py</a></div><div class="synth-steps">${code("1. 按包索引定位", index)}${code("2. 两套深度与有效掩码", depth)}${code("3. 相机与可见分割", camera)}${code("4. 原始手参数与辅助拟合分开", native)}${code("5. 历史路径显式解析", relocation)}</div></section>
      ${section("训练边界", content.quality.map((q, i) => `<details ${i === 0 ? "open" : ""}><summary>${["完成与训练资格", "公开split与视角", "历史深度格式", "防止变体泄漏", "模拟传感器边界", "数据许可"][i]}</summary><p>${esc(q)}</p></details>`).join(""))}
      ${section("来源", `<div class="synth-links">${content.references.map(([name, url]) => `<a href="${esc(url)}" target="_blank" rel="noopener noreferrer">${esc(name)}</a>`).join("")}</div>`)}`;
}
function samplesView() {
  return `${section("Hard Cases · V006", `<div class="synth-facts"><span>三类各 <b>5</b> 条</span><span><b>10,838</b> 主源帧</span><span>执行修订 <b>320</b></span></div><div class="synth-samples">${content.samples.map((s) => `<figure><a href="${esc(s.image)}" target="_blank" rel="noopener"><img src="${esc(s.image)}" alt="${esc(s.title)}的实际合成场景预览" loading="lazy"></a><figcaption><b>${esc(s.title)}</b><small>${esc(s.source)}</small><p>${esc(s.note)}</p></figcaption></figure>`).join("")}</div>`)}
      <div class="synth-callout">这些是同源动作的历史参考帧,不是当前全部随机变体的逐帧验收。镜面样例在完整生产前另外加入固定种子的污渍材质。</div>
      ${section(
        "样本组织",
        table(
          ["类别", "场景来源", "读取差异"],
          [
            "<tr><td>bimanual_crossing</td><td>公开源动作中的真实双手交叉</td><td>不改变原始手部运动来制造交叉</td></tr>",
            "<tr><td>mirror_reflection</td><td>真实镜面几何与反射</td><td>镜面深度为物理首交点;反射的人不新增instance</td></tr>",
            "<tr><td>multiple_people</td><td>多个源人体及各自衣物</td><td>检查scene_instances.json的每人源时钟;主源帧数不能直接当所有演员帧总数</td></tr>",
          ],
        ),
      )}`;
}
function paint() {
  if (!root || !content) return;
  const body =
    tab() === "versions"
      ? versionPage()
      : tab() === "reading"
        ? readingView()
        : tab() === "samples"
          ? samplesView()
          : progressView();
  root.innerHTML = header() + body;
  root.querySelector("#synth-refresh").onclick = () => refresh();
  root.querySelector("#synth-campaign").onchange = (event) => {
    selectedCampaign = event.target.value;
    if (tab() === "reading") {
      paint();
      return;
    }
    updateFreshness();
    updateProgress();
  };
  const filter = root.querySelector("#synth-job-filter");
  if (filter)
    filter.onchange = () => {
      selectedJobs = filter.value;
      updateProgress();
    };
  const search = root.querySelector("#synth-registry-search");
  if (search)
    search.oninput = () => {
      registryQuery = search.value;
      registryPage = 0;
      updateRegistry();
    };
  root.querySelectorAll("[data-copy]").forEach(
    (b) =>
      (b.onclick = async () => {
        try {
          await navigator.clipboard.writeText(
            b.parentNode.querySelector("code").textContent,
          );
          b.title = "已复制";
          b.setAttribute("aria-label", "已复制");
        } catch (_) {
          b.title = "复制失败";
        }
      }),
  );
  updateFreshness();
  updateProgress();
  updateRegistry();
  icons();
}
async function refresh() {
  if (!content || !root || request) return;
  const own = generation,
    button = root.querySelector("#synth-refresh");
  button.disabled = true;
  const controller = new AbortController();
  request = controller;
  const timeout = setTimeout(() => controller.abort(), 10000);
  try {
    const response = await fetch(content.status_url + "?t=" + Date.now(), {
      cache: "no-store",
      signal: controller.signal,
    });
    if (!response.ok) throw new Error("HTTP");
    const value = await response.json();
    if (!valid(value)) throw new Error("schema");
    if (own !== generation) return;
    if (!snapshot || value.collected_at >= snapshot.collected_at)
      snapshot = value;
    updateFreshness();
    updateProgress();
  } catch (_) {
    if (own === generation) updateFreshness(true);
  } finally {
    clearTimeout(timeout);
    if (request === controller) request = null;
    if (own === generation && root)
      root.querySelector("#synth-refresh").disabled = false;
  }
}
function unmount() {
  generation++;
  clearInterval(timer);
  clearInterval(ageTimer);
  if (request) request.abort();
  request = null;
  root = null;
}
async function mount() {
  root = document.getElementById("synthetic-root");
  selectedVersion = versionFromHash(location.hash);
  const own = generation;
  try {
    if (!content) {
      const responses = await Promise.all(
        ["egorender", "egorender-registry", "egorender-progress", "egorender-versions"].map((n) =>
          fetch("data/" + n + ".json"),
        ),
      );
      if (!responses[0].ok || !responses[3].ok) throw new Error("content");
      versionHistory = await responses[3].json();
      content = await responses[0].json();
      registry = responses[1].ok ? await responses[1].json() : [];
      const cached = responses[2].ok ? await responses[2].json() : null;
      if (
        valid(cached) &&
        (!snapshot || cached.collected_at > snapshot.collected_at)
      )
        snapshot = cached;
    }
    if (own !== generation || !root) return;
    paint();
    refresh();
    timer = setInterval(refresh, 60000);
    ageTimer = setInterval(() => updateFreshness(), 15000);
  } catch (_) {
    if (root)
      root.innerHTML =
        '<p class="synth-error">合成数据文档暂不可用,请刷新重试。</p>';
  }
}
export default {
  render: () =>
    '<section class="view synth" id="synthetic-root"><div class="synth-skeleton">正在读取合成数据…</div></section>',
  mount,
  unmount,
};