Download src/synthetic.js from DarthSoluke/EgoNetDatasetResearch: direct link, hf CLI and curl.
- Browser
- Download file 24.5 kB
-
https://huggingface.co/spaces/DarthSoluke/EgoNetDatasetResearch/resolve/main/src/synthetic.js
- Command line
-
hf download hf://spaces/DarthSoluke/EgoNetDatasetResearch/src/synthetic.js
-
curl -L -o synthetic.js https://huggingface.co/spaces/DarthSoluke/EgoNetDatasetResearch/resolve/main/src/synthetic.js
24.5 kB
| 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, | |
| }; | |