EgoNetDatasetResearch / src /synthetic.js
DarthSoluke's picture
Document all EgoRenderer versions and audited GRAB rendering campaigns
b0b604c verified
Raw History Blame Contribute Delete
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,
};