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,
};
|