cozyclay 1.9.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +171 -0
- package/bin/agent/agent-routes.mjs +74 -70
- package/bin/agent/agent-runner.mjs +10 -3
- package/bin/agent/motion-runtime.mjs +32 -260
- package/bin/agent/providers.mjs +46 -3
- package/bin/agent/session-store.mjs +41 -5
- package/bin/agent/studio-prompt.mjs +1 -1
- package/bin/agent/studio-tools.mjs +48 -11
- package/bin/cozyclay.mjs +26 -8
- package/bin/live/cli.mjs +22 -3
- package/bin/mcp-runtime.mjs +35 -20
- package/bin/telemetry-state.mjs +5 -2
- package/bin/update-check.mjs +15 -0
- package/dist/app/index.html +6 -6
- package/dist/assets/analytics-MVZOEW8O.js +1 -0
- package/dist/assets/app-C1sdVbg4.css +1 -0
- package/dist/assets/app-CA55tcI3.js +4876 -0
- package/dist/assets/{demo-B-eg1CT8.js → demo-ByeYp-dY.js} +1 -1
- package/dist/assets/{first-shot-handoff-CGgnvNu8.js → first-shot-handoff-DgT28ZiV.js} +1 -1
- package/dist/assets/{landing-IbRp3FnQ.js → landing-BaXkpFip.js} +1 -1
- package/dist/assets/shot-prompt-CbqWcWYi.css +1 -0
- package/dist/assets/shot-prompt-Yrt99wZx.js +17 -0
- package/dist/assets/{ticket-BgIzOBOf.js → ticket-BcmsnHn5.js} +1 -1
- package/dist/assets/workflow-DRdwoDBT.css +1 -0
- package/dist/assets/workflow-dNPGY9xz.js +179 -0
- package/dist/cozyclay-package.json +1 -1
- package/dist/fonts/IBM-Plex-Sans-OFL.txt +92 -0
- package/dist/fonts/JetBrains-Mono-OFL.txt +93 -0
- package/dist/fonts/README.md +13 -9
- package/dist/fonts/ibm-plex-sans-400-latin.woff2 +0 -0
- package/dist/fonts/ibm-plex-sans-500-latin.woff2 +0 -0
- package/dist/fonts/ibm-plex-sans-600-latin.woff2 +0 -0
- package/dist/fonts/jetbrains-mono-400-latin.woff2 +0 -0
- package/dist/fonts/jetbrains-mono-500-latin.woff2 +0 -0
- package/dist/index.html +7 -7
- package/dist/privacy/index.html +13 -8
- package/dist/sitemap.xml +6 -6
- package/dist/workflow/index.html +5 -5
- package/mcp/LIVE-PROTOCOL.md +1 -1
- package/mcp/live-hub.mjs +35 -93
- package/mcp/mesh-file.mjs +48 -0
- package/mcp/server.mjs +6 -77
- package/mcp/tool-handlers.mjs +224 -377
- package/package.json +2 -1
- package/src/App.jsx +1273 -9075
- package/src/analytics.js +393 -9
- package/src/app-context.js +211 -0
- package/src/app-stage.jsx +25 -24
- package/src/ardy/auto-fix-panel.css +108 -0
- package/src/ardy/collision-blockers.js +9 -3
- package/src/ardy/fix-collisions.js +10 -66
- package/src/ardy/ground.js +4 -4
- package/src/ardy/ik-drag.js +101 -0
- package/src/ardy/ik-key-json.js +43 -0
- package/src/ardy/ik.js +122 -76
- package/src/ardy/physics-panel.css +2 -0
- package/src/ardy/physics-panel.jsx +17 -5
- package/src/ardy/platform-fit-panel.css +64 -0
- package/src/ardy/platform-fit-panel.jsx +40 -0
- package/src/ardy/platform-fit.js +326 -0
- package/src/ardy/playback.js +166 -20
- package/src/ardy/range-pin.js +299 -0
- package/src/ardy/timeline-coordinates.js +26 -0
- package/src/ardy/timeline.css +1199 -0
- package/src/ardy/timeline.jsx +149 -41
- package/src/ardy/waypoints.js +2 -2
- package/src/asset-pane.css +462 -0
- package/src/asset-pane.jsx +461 -232
- package/src/command-bus.js +349 -0
- package/src/commands/ai.js +31 -0
- package/src/commands/cast.js +140 -0
- package/src/commands/elements/character.js +13 -0
- package/src/commands/elements/motion.js +14 -0
- package/src/commands/elements/object.js +22 -0
- package/src/commands/elements/scene.js +16 -0
- package/src/commands/elements/shot.js +9 -0
- package/src/commands/elements/stage.js +11 -0
- package/src/commands/elements.js +155 -0
- package/src/commands/export.js +20 -0
- package/src/commands/index.js +40 -0
- package/src/commands/motion.js +231 -0
- package/src/commands/objects.js +174 -0
- package/src/commands/project.js +53 -0
- package/src/commands/scene.js +81 -0
- package/src/commands/shared.js +19 -0
- package/src/commands/shot.js +103 -0
- package/src/commands/stage.js +17 -0
- package/src/commands/view.js +42 -0
- package/src/document-store.js +144 -0
- package/src/domains/cast.js +1007 -0
- package/src/domains/motion.js +3219 -0
- package/src/domains/objects.js +932 -0
- package/src/domains/scenes.js +817 -0
- package/src/domains/shots.js +404 -0
- package/src/domains/stage.js +93 -0
- package/src/dualview.jsx +113 -57
- package/src/facing-marks.js +3 -0
- package/src/first-success-guide.jsx +14 -14
- package/src/grid-view.js +18 -5
- package/src/hierarchy-model.js +24 -16
- package/src/hierarchy-panel.css +386 -0
- package/src/hierarchy-panel.jsx +150 -21
- package/src/{fal-motion-client.js → i2v-motion-client.js} +13 -13
- package/src/i2v-motion-studio.jsx +180 -0
- package/src/ik-camera.js +3 -0
- package/src/main.jsx +6 -0
- package/src/motion/generation.js +105 -0
- package/src/motion-readiness-ui.jsx +4 -4
- package/src/motion-readiness.js +11 -0
- package/src/motion-trail.js +366 -9
- package/src/object-gizmo.jsx +7 -1
- package/src/otio.js +2 -1
- package/src/panels/CameraPanel.jsx +60 -0
- package/src/panels/CharacterTransformPanel.jsx +48 -0
- package/src/panels/EnvironmentPanel.jsx +40 -0
- package/src/panels/Foldout.jsx +32 -0
- package/src/panels/LightPanel.jsx +24 -0
- package/src/panels/ObjectTransformPanel.jsx +554 -0
- package/src/panels/PosePanel.jsx +97 -0
- package/src/panels/ProjectPanel.jsx +33 -0
- package/src/panels/PromptBlocksPanel.jsx +442 -0
- package/src/panels/PropsPanel.jsx +70 -0
- package/src/panels/ReferenceImageField.jsx +91 -0
- package/src/panels/RigControlPanel.jsx +78 -0
- package/src/panels/RigPanel.jsx +37 -0
- package/src/panels/SubjectBox.jsx +47 -0
- package/src/panels/SubjectsPanel.jsx +39 -0
- package/src/panels/VideoCapturePanel.jsx +180 -0
- package/src/panels/details.css +1171 -0
- package/src/panels/motion.css +197 -0
- package/src/panels/pose.css +39 -0
- package/src/planview.jsx +60 -31
- package/src/posestudio.jsx +59 -4
- package/src/project-browser.css +1073 -0
- package/src/project-browser.jsx +284 -123
- package/src/range-pin-object-transform.js +19 -0
- package/src/range-pin-panel.css +518 -0
- package/src/range-pin-panel.jsx +344 -0
- package/src/result-modal.jsx +5 -5
- package/src/room.jsx +79 -51
- package/src/scene-objects.js +37 -1
- package/src/scenes.js +5 -0
- package/src/semantic-edit.js +2 -0
- package/src/settings-menu.jsx +39 -105
- package/src/shell/BottomDock.jsx +450 -0
- package/src/shell/DetailsSlot.jsx +513 -0
- package/src/shell/LibrarySlot.jsx +86 -0
- package/src/shell/MenuBar.jsx +601 -0
- package/src/shell/OutlinerSlot.jsx +50 -0
- package/src/shell/PreferencesDialog.jsx +538 -0
- package/src/shell/PreferencesSlot.jsx +40 -0
- package/src/shell/StatusBar.jsx +125 -0
- package/src/shell/StudioShell.jsx +92 -0
- package/src/shell/TopBar.jsx +129 -0
- package/src/shell/ViewportToolbar.jsx +656 -0
- package/src/shell/agent-glass.css +296 -0
- package/src/shell/dock.css +93 -0
- package/src/shell/glass-regions.css +985 -0
- package/src/shell/glass.css +687 -0
- package/src/shell/log-store.js +52 -0
- package/src/shell/mode.css +9 -0
- package/src/shell/preferences.css +651 -0
- package/src/shell/shell.css +282 -0
- package/src/shell/studio-shell-context.js +19 -0
- package/src/shell/topbar.css +435 -0
- package/src/shell/viewport.css +451 -0
- package/src/store/authored-intent.js +22 -0
- package/src/store/runtime-adapters.js +92 -0
- package/src/store/scene-stage.js +21 -0
- package/src/store/use-document-store.js +14 -0
- package/src/studio-actions.js +210 -0
- package/src/studio-agent-commands.js +41 -271
- package/src/studio-agent-context.js +39 -10
- package/src/studio-agent-motion.js +122 -373
- package/src/studio-agent-protocol.js +67 -31
- package/src/studio-app-binding.js +379 -0
- package/src/studio-contact-sheet.js +73 -0
- package/src/studio-elements.js +58 -51
- package/src/styles/themes.css +200 -0
- package/src/styles/tokens.css +69 -0
- package/src/styles.css +460 -1742
- package/src/theme.js +44 -0
- package/src/timeline-extent.js +16 -0
- package/src/trail-key-conflicts.js +39 -0
- package/src/trail-pick.js +59 -0
- package/src/ui.jsx +8 -8
- package/src/use-case-question.jsx +66 -0
- package/src/workflow/AgentPanel.jsx +82 -69
- package/src/workflow/agent-client.js +12 -2
- package/src/workflow/agent-panel.css +34 -32
- package/src/workflow/cozy-scene-node.css +2 -0
- package/src/workflow/workflow.css +2 -0
- package/tools/ardy/__pycache__/cclay_gvhmr_worker.cpython-313.pyc +0 -0
- package/tools/ardy/bridge.mjs +164 -146
- package/tools/ardy/visual-qa.mjs +5 -5
- package/tools/bench/EXP3.md +61 -0
- package/tools/bench/cclay_bench_extract_incam.py +125 -0
- package/tools/bench/cclay_bench_extract_obs.py +204 -0
- package/tools/bench/cclay_bench_runner.py +42 -0
- package/tools/bench/cube-contact.mjs +162 -0
- package/tools/bench/exp3.mjs +152 -0
- package/tools/bench/extract-bench-lib.mjs +216 -0
- package/tools/bench/extract-bench.mjs +302 -0
- package/tools/bench/fal-generate.mjs +88 -0
- package/tools/bench/fit/README.md +189 -0
- package/tools/bench/fit/camera.mjs +59 -0
- package/tools/bench/fit/contact.mjs +419 -0
- package/tools/bench/fit/footlock.mjs +234 -0
- package/tools/bench/fit/motion.mjs +85 -0
- package/tools/bench/fit/pin.mjs +27 -0
- package/tools/bench/fit/remote.mjs +105 -0
- package/tools/bench/fit-bench.mjs +126 -0
- package/tools/bench/fit-sanity.mjs +45 -0
- package/tools/bench/metrics.mjs +313 -0
- package/tools/bench/obs/depth.mjs +258 -0
- package/tools/bench/obs/extrinsics.mjs +175 -0
- package/tools/bench/obs/fit_mannequin_betas.py +340 -0
- package/tools/bench/obs/ground.mjs +294 -0
- package/tools/bench/obs/heading.mjs +151 -0
- package/tools/bench/obs/ladder.mjs +555 -0
- package/tools/bench/obs/mannequin-betas.json +124 -0
- package/tools/bench/obs/remote.mjs +83 -0
- package/tools/bench/obs/rest_joints.py +52 -0
- package/tools/bench/obs/ybot-targets.mjs +49 -0
- package/tools/bench/obs-bench.mjs +520 -0
- package/tools/bench/score.mjs +343 -0
- package/tools/bench/summarize.mjs +86 -0
- package/tools/dev/pages/privacy.html +13 -8
- package/tools/gt-render/browser.mjs +159 -0
- package/tools/gt-render/camera-math.mjs +276 -0
- package/tools/gt-render/page.mjs +267 -0
- package/tools/gt-render/render.mjs +514 -0
- package/tools/gt-render/scene-box.mjs +31 -0
- package/tools/gt-render/take-transform.mjs +87 -0
- package/tools/morphgs/assets/gen_truth.py +39 -0
- package/tools/morphgs/assets/mesh_ori_rig.txt +27 -0
- package/tools/morphgs/assets/playback-check.mjs +30 -0
- package/tools/morphgs/demo-gate.sh +26 -0
- package/tools/morphgs/fbx2morphgs.mjs +68 -0
- package/tools/morphgs/morphgs-to-cskel27.mjs +105 -0
- package/tools/morphgs/patches/preprocess_src-none-mode.patch +76 -0
- package/tools/morphgs/setup-on-cluster.sh +126 -0
- package/tools/qa/css-rule-usage.mjs +283 -0
- package/tools/qa/studio-control-count.mjs +70 -32
- package/tools/run-tests.mjs +134 -12
- package/tools/track/DESIGN.md +513 -0
- package/tools/track/backfill-provenance.mjs +116 -0
- package/tools/track/budget.mjs +8 -0
- package/tools/track/check-rig.mjs +209 -0
- package/tools/track/diagnostics.schema.json +60 -0
- package/tools/track/export-rig.mjs +256 -0
- package/tools/track/fallback.mjs +7 -0
- package/tools/track/fk-parity-fixture.mjs +161 -0
- package/tools/track/gate.mjs +349 -0
- package/tools/track/masks.mjs +318 -0
- package/tools/track/metrics.mjs +201 -0
- package/tools/track/publish-obs.mjs +108 -0
- package/tools/track/py/check_env.py +61 -0
- package/tools/track/py/eval_lr.py +229 -0
- package/tools/track/py/lr_viterbi.py +294 -0
- package/tools/track/py/masks.py +405 -0
- package/tools/track/py/objective.py +382 -0
- package/tools/track/py/rig.py +508 -0
- package/tools/track/py/scene.py +326 -0
- package/tools/track/py/test_joint_indices.py +123 -0
- package/tools/track/py/test_lr_viterbi.py +195 -0
- package/tools/track/py/test_masks.py +229 -0
- package/tools/track/py/test_rig.py +288 -0
- package/tools/track/py/test_scene.py +354 -0
- package/tools/track/py/test_track.py +328 -0
- package/tools/track/py/track.py +411 -0
- package/tools/track/remote.mjs +140 -0
- package/tools/track/rig-dump.mjs +227 -0
- package/tools/track/run-box-tests.mjs +14 -0
- package/tools/track/run-box.mjs +168 -0
- package/tools/track/setup-box.sh +59 -0
- package/tools/track/study-2d.mjs +728 -0
- package/dist/assets/analytics-B1hnH66c.js +0 -1
- package/dist/assets/app-BWusbqgO.js +0 -4861
- package/dist/assets/app-qKDo4PBX.css +0 -1
- package/dist/assets/shot-prompt-CHUtw6af.js +0 -17
- package/dist/assets/shot-prompt-C_g2BHVc.css +0 -1
- package/dist/assets/workflow-CVwzMhz2.js +0 -179
- package/dist/assets/workflow-DsudxKHF.css +0 -1
- package/src/fal-motion-studio.jsx +0 -180
- package/src/scene-history.js +0 -129
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Mocap scorer (#431): compare a predicted cskel27 motion npz against a T1
|
|
4
|
+
* ground-truth render (tools/gt-render/render.mjs) by rendering the
|
|
5
|
+
* prediction through the SAME Studio rig and the SAME camera.
|
|
6
|
+
*
|
|
7
|
+
* node tools/bench/score.mjs --gt <T1 motion dir> --pred <motion.npz> --out <dir>
|
|
8
|
+
* [--box '{"min":[x,y,z],"max":[x,y,z]}'] [--pred-transform '<json>'] [--gt-npz <npz>]
|
|
9
|
+
*
|
|
10
|
+
* Ground truth is the rig as rendered (joints.json, mask/), so the prediction
|
|
11
|
+
* is read the same way: render.mjs --no-video --camera <gt camera.json>.
|
|
12
|
+
*
|
|
13
|
+
* Placements
|
|
14
|
+
* raw the prediction as the Studio places any take: frame 0 anchored on
|
|
15
|
+
* the subject, the take's own heading (plus --pred-transform, a scene
|
|
16
|
+
* calibration in the Studio's sceneCalibration semantics, if given).
|
|
17
|
+
* A constant offset baked into the npz is therefore invisible here;
|
|
18
|
+
* score.json reports it separately (npz.frame0RootOffsetM).
|
|
19
|
+
* aligned raw moved by the least-squares yaw + ground-plane translation that
|
|
20
|
+
* maps its first scored frame's joints onto the GT's, re-rendered so
|
|
21
|
+
* the mask IoU is of a real render.
|
|
22
|
+
*
|
|
23
|
+
* Timelines: the Studio plays every take at 24 fps (retimed on load). Each GT
|
|
24
|
+
* frame is paired with the nearest-in-time rendered pred frame; GT frames past
|
|
25
|
+
* the prediction's end are not scored. Both are reported in score.json.
|
|
26
|
+
*
|
|
27
|
+
* Writes <out>/score.json, <out>/frames.csv and the renders in <out>/render-*.
|
|
28
|
+
*/
|
|
29
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
30
|
+
import { execFileSync } from "node:child_process";
|
|
31
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
32
|
+
import { parseArgs } from "node:util";
|
|
33
|
+
import { decodeMotionNpz } from "../../src/ardy/npz.js";
|
|
34
|
+
import { mocapMetricsFromMotion } from "../ardy/mocap-metrics.mjs";
|
|
35
|
+
import { installSignalCleanup, spawnOwned, waitForExit } from "../process-supervisor.mjs";
|
|
36
|
+
import { startVite, terminateAll } from "../gt-render/browser.mjs";
|
|
37
|
+
import { isIdentityTransform, parseTransform, transformTake } from "../gt-render/take-transform.mjs";
|
|
38
|
+
import {
|
|
39
|
+
applyYawTranslation,
|
|
40
|
+
composeCalibration,
|
|
41
|
+
displacementXZ,
|
|
42
|
+
fitYawTranslation,
|
|
43
|
+
maskIoU,
|
|
44
|
+
mean,
|
|
45
|
+
meanJointError,
|
|
46
|
+
parseBox,
|
|
47
|
+
pathLengthXZ,
|
|
48
|
+
procrustesError,
|
|
49
|
+
ratio,
|
|
50
|
+
resampleToTimeline,
|
|
51
|
+
rootRelativeError,
|
|
52
|
+
trajectoryError,
|
|
53
|
+
} from "./metrics.mjs";
|
|
54
|
+
|
|
55
|
+
const ROOT = resolve(dirname(new URL(import.meta.url).pathname), "../..");
|
|
56
|
+
const RENDER = join(ROOT, "tools/gt-render/render.mjs");
|
|
57
|
+
const ROOT_JOINT = "Hips";
|
|
58
|
+
|
|
59
|
+
const USAGE = `usage: node tools/bench/score.mjs --gt <T1 motion dir> --pred <motion.npz> --out <dir> [options]
|
|
60
|
+
|
|
61
|
+
--gt <dir> T1 output for one motion (<dir>/mask/ + <dir>/<variant>/{camera,joints}.json),
|
|
62
|
+
or a --no-video render dir holding camera.json/joints.json/mask/
|
|
63
|
+
--pred <npz> predicted cskel27 motion npz
|
|
64
|
+
--out <dir> score.json, frames.csv, render-raw/, render-aligned/ [, render-gt/]
|
|
65
|
+
--box <json> '{"min":[x,y,z],"max":[x,y,z]}' scene box: skinned-vertex distance/penetration
|
|
66
|
+
--pred-transform <json> scene calibration for the raw placement (render.mjs --transform keys)
|
|
67
|
+
--gt-npz <npz> GT source npz (default: meta.json source.path); used for physical
|
|
68
|
+
metrics, the npz offset diagnostic and GT contact with --box
|
|
69
|
+
--url <origin> reuse a running Vite dev server of THIS checkout
|
|
70
|
+
--port <n> Vite port when starting one (default 5192)
|
|
71
|
+
--cdp-port <n> headless Chrome DevTools port for the renders (default 9232)`;
|
|
72
|
+
|
|
73
|
+
function parseOptions(argv) {
|
|
74
|
+
const { values } = parseArgs({
|
|
75
|
+
args: argv,
|
|
76
|
+
options: {
|
|
77
|
+
gt: { type: "string" },
|
|
78
|
+
pred: { type: "string" },
|
|
79
|
+
out: { type: "string" },
|
|
80
|
+
box: { type: "string" },
|
|
81
|
+
"pred-transform": { type: "string" },
|
|
82
|
+
"gt-npz": { type: "string" },
|
|
83
|
+
url: { type: "string" },
|
|
84
|
+
port: { type: "string", default: "5192" },
|
|
85
|
+
"cdp-port": { type: "string", default: "9232" },
|
|
86
|
+
help: { type: "boolean", short: "h", default: false },
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
if (values.help) {
|
|
90
|
+
console.log(USAGE);
|
|
91
|
+
process.exit(0);
|
|
92
|
+
}
|
|
93
|
+
const fail = (message) => {
|
|
94
|
+
console.error(`${message}\n\n${USAGE}`);
|
|
95
|
+
process.exit(2);
|
|
96
|
+
};
|
|
97
|
+
for (const key of ["gt", "pred", "out"]) if (!values[key]) fail(`--${key} is required`);
|
|
98
|
+
const pred = resolve(values.pred);
|
|
99
|
+
if (!existsSync(pred)) fail(`no such pred: ${pred}`);
|
|
100
|
+
let box = null;
|
|
101
|
+
let predTransform = null;
|
|
102
|
+
try {
|
|
103
|
+
if (values.box) box = parseBox(values.box);
|
|
104
|
+
predTransform = parseTransform(values["pred-transform"] ?? "{}");
|
|
105
|
+
} catch (error) {
|
|
106
|
+
fail(error.message);
|
|
107
|
+
}
|
|
108
|
+
const port = Number(values.port);
|
|
109
|
+
const cdpPort = Number(values["cdp-port"]);
|
|
110
|
+
if (!Number.isInteger(port) || !Number.isInteger(cdpPort)) fail("--port and --cdp-port must be integers");
|
|
111
|
+
return { gt: resolve(values.gt), pred, out: resolve(values.out), box, predTransform, gtNpz: values["gt-npz"] ? resolve(values["gt-npz"]) : null, url: values.url?.replace(/\/$/, "") ?? null, port, cdpPort };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** camera.json, joints.json, meta.json and mask/ of a GT motion dir. */
|
|
115
|
+
function loadGt(dir) {
|
|
116
|
+
const hasFiles = (d) => existsSync(join(d, "camera.json")) && existsSync(join(d, "joints.json"));
|
|
117
|
+
let files = hasFiles(dir) ? dir : null;
|
|
118
|
+
if (!files) {
|
|
119
|
+
const variants = readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && hasFiles(join(dir, e.name))).map((e) => e.name);
|
|
120
|
+
const pick = variants.includes("shaded") ? "shaded" : variants.sort()[0];
|
|
121
|
+
if (!pick) throw new Error(`${dir}: no camera.json/joints.json here or in a variant subdirectory`);
|
|
122
|
+
files = join(dir, pick);
|
|
123
|
+
}
|
|
124
|
+
const maskDir = [join(dir, "mask"), join(files, "mask"), join(dirname(files), "mask")].find((d) => existsSync(d));
|
|
125
|
+
if (!maskDir) throw new Error(`${dir}: no mask/ directory`);
|
|
126
|
+
const read = (name) => JSON.parse(readFileSync(join(files, name), "utf8"));
|
|
127
|
+
return { dir, files, cameraPath: join(files, "camera.json"), camera: read("camera.json"), joints: read("joints.json"), meta: existsSync(join(files, "meta.json")) ? read("meta.json") : null, sceneBox: existsSync(join(files, "scene.json")) ? read("scene.json").placement : null, maskDir };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function render({ base, cdpPort, out, camera, transform, box, sceneBox, file, children }) {
|
|
131
|
+
const args = [RENDER, "--out", out, "--no-video", "--camera", camera, "--url", base, "--cdp-port", String(cdpPort)];
|
|
132
|
+
if (transform && !isIdentityTransform(transform)) args.push("--transform", JSON.stringify(transform));
|
|
133
|
+
if (box) args.push("--box", JSON.stringify(box));
|
|
134
|
+
if (sceneBox) args.push("--scene-box", JSON.stringify(sceneBox));
|
|
135
|
+
args.push(file);
|
|
136
|
+
const child = spawnOwned(process.execPath, args, { cwd: ROOT, stdio: ["ignore", "inherit", "inherit"] });
|
|
137
|
+
children.push(child);
|
|
138
|
+
const { code, signal } = await waitForExit(child);
|
|
139
|
+
children.splice(children.indexOf(child), 1);
|
|
140
|
+
if (code !== 0) throw new Error(`render.mjs ${basename(file)} failed (code ${code}, signal ${signal})`);
|
|
141
|
+
const dir = join(out, basename(file, ".npz"));
|
|
142
|
+
const read = (name) => JSON.parse(readFileSync(join(dir, name), "utf8"));
|
|
143
|
+
return { dir, joints: read("joints.json"), meta: read("meta.json"), contact: box ? read("contact.json") : null, maskDir: join(dir, "mask") };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** All masks of a dir as one gray buffer (frames * width * height). */
|
|
147
|
+
function readMasks(dir, frames, width, height) {
|
|
148
|
+
const buffer = execFileSync("ffmpeg", ["-hide_banner", "-loglevel", "error", "-start_number", "0", "-i", join(dir, "%06d.png"), "-frames:v", String(frames), "-f", "rawvideo", "-pix_fmt", "gray", "pipe:1"], { maxBuffer: frames * width * height + 1024 });
|
|
149
|
+
if (buffer.length !== frames * width * height) throw new Error(`${dir}: decoded ${buffer.length} mask bytes, expected ${frames} x ${width} x ${height}`);
|
|
150
|
+
return (i) => buffer.subarray(i * width * height, (i + 1) * width * height);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const rootOf = (joints) => joints.joints.findIndex((joint) => joint.cskel27 === ROOT_JOINT);
|
|
154
|
+
|
|
155
|
+
async function decodeFile(path) {
|
|
156
|
+
return decodeMotionNpz(new Uint8Array(readFileSync(path)));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function physical(motion, path) {
|
|
160
|
+
const m = mocapMetricsFromMotion(motion, { path });
|
|
161
|
+
return { frames: m.frames, fps: m.fps, footSlideCmPerS: m.footSlideCmPerS, jitterMmPerS2: m.jitterMmPerS2, framesBelowFloor: m.framesBelowFloor, deepestBelowFloorCm: m.deepestBelowFloorCm, rootTravelM: m.rootTravelM };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function contactSummary(contact, indices) {
|
|
165
|
+
if (!contact) return null;
|
|
166
|
+
const frames = indices.map((i) => contact.frames[i]);
|
|
167
|
+
const minDistance = Math.min(...frames.map((f) => f.minDistanceM));
|
|
168
|
+
const maxPenetration = Math.max(...frames.map((f) => f.maxPenetrationM));
|
|
169
|
+
return { basis: contact.basis, minDistanceM: minDistance, maxPenetrationM: maxPenetration, framesPenetrating: frames.filter((f) => f.maxPenetrationM > 0).length, framesTouching: frames.filter((f) => f.signedDistanceM <= 0.01).length };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const r6 = (value) => (value === null || value === undefined || !Number.isFinite(value) ? value ?? null : Math.round(value * 1e6) / 1e6);
|
|
173
|
+
|
|
174
|
+
async function main() {
|
|
175
|
+
const options = parseOptions(process.argv.slice(2));
|
|
176
|
+
const children = [];
|
|
177
|
+
const removeSignals = installSignalCleanup(() => children, () => process.exit(130));
|
|
178
|
+
mkdirSync(options.out, { recursive: true });
|
|
179
|
+
const gt = loadGt(options.gt);
|
|
180
|
+
const { width, height } = gt.camera;
|
|
181
|
+
const gtNpz = options.gtNpz ?? (gt.meta?.source?.path && existsSync(gt.meta.source.path) ? gt.meta.source.path : null);
|
|
182
|
+
try {
|
|
183
|
+
const base = options.url ?? await startVite({ root: ROOT, port: options.port, children });
|
|
184
|
+
const common = { base, cdpPort: options.cdpPort, camera: gt.cameraPath, box: options.box, sceneBox: gt.sceneBox, children };
|
|
185
|
+
console.log(`[score] raw render of ${options.pred}`);
|
|
186
|
+
const raw = await render({ ...common, out: join(options.out, "render-raw"), transform: options.predTransform, file: options.pred });
|
|
187
|
+
|
|
188
|
+
// Timeline pairing, GT frame -> nearest pred frame.
|
|
189
|
+
const timeline = resampleToTimeline({ gtFrames: gt.joints.frames, gtFps: gt.joints.fps, predFrames: raw.joints.frames, predFps: raw.joints.fps });
|
|
190
|
+
const pairs = timeline.predIndex.map((k, i) => [i, k]).filter(([, k]) => k !== null);
|
|
191
|
+
if (!pairs.length) throw new Error("no GT frame falls inside the prediction's timeline");
|
|
192
|
+
const [gt0, pr0] = pairs[0];
|
|
193
|
+
|
|
194
|
+
// First-frame rigid alignment (yaw + ground-plane translation).
|
|
195
|
+
const alignment = fitYawTranslation(raw.joints.world[pr0], gt.joints.world[gt0]);
|
|
196
|
+
const anchor = raw.meta.placement.anchor;
|
|
197
|
+
const alignedTransform = composeCalibration(options.predTransform, anchor, alignment);
|
|
198
|
+
console.log(`[score] first-frame alignment: yaw ${alignment.yawDeg.toFixed(3)} deg, t (${alignment.tx.toFixed(4)}, ${alignment.tz.toFixed(4)}) m; aligned render`);
|
|
199
|
+
const aligned = await render({ ...common, out: join(options.out, "render-aligned"), transform: parseTransform(JSON.stringify(alignedTransform)), file: options.pred });
|
|
200
|
+
let rigidCheck = 0;
|
|
201
|
+
for (let k = 0; k < raw.joints.frames; k += 1) {
|
|
202
|
+
raw.joints.world[k].forEach((p, j) => {
|
|
203
|
+
const q = applyYawTranslation(p, alignment);
|
|
204
|
+
const a = aligned.joints.world[k][j];
|
|
205
|
+
rigidCheck = Math.max(rigidCheck, Math.hypot(q[0] - a[0], q[1] - a[1], q[2] - a[2]));
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
const gtRender = options.box && gtNpz ? await render({ ...common, out: join(options.out, "render-gt"), transform: null, file: gtNpz }) : null;
|
|
209
|
+
|
|
210
|
+
// Masks.
|
|
211
|
+
const gtMask = readMasks(gt.maskDir, gt.joints.frames, width, height);
|
|
212
|
+
const rawMask = readMasks(raw.maskDir, raw.joints.frames, width, height);
|
|
213
|
+
const alignedMask = readMasks(aligned.maskDir, aligned.joints.frames, width, height);
|
|
214
|
+
|
|
215
|
+
const root = rootOf(gt.joints);
|
|
216
|
+
if (root < 0 || rootOf(raw.joints) !== root) throw new Error("GT and pred joints.json disagree on the root joint");
|
|
217
|
+
if (gt.joints.joints.map((j) => j.bone).join() !== raw.joints.joints.map((j) => j.bone).join()) throw new Error("GT and pred joints.json list different joints");
|
|
218
|
+
|
|
219
|
+
const rows = pairs.map(([i, k]) => {
|
|
220
|
+
const g = gt.joints.world[i];
|
|
221
|
+
const pr = raw.joints.world[k];
|
|
222
|
+
const pa = aligned.joints.world[k];
|
|
223
|
+
return {
|
|
224
|
+
gtFrame: i,
|
|
225
|
+
predFrame: k,
|
|
226
|
+
timeS: i / gt.joints.fps,
|
|
227
|
+
mpjpeRawM: rootRelativeError(pr, g, root),
|
|
228
|
+
mpjpeAlignedM: rootRelativeError(pa, g, root),
|
|
229
|
+
paMpjpeM: procrustesError(pr, g),
|
|
230
|
+
jointErrRawM: meanJointError(pr, g),
|
|
231
|
+
jointErrAlignedM: meanJointError(pa, g),
|
|
232
|
+
rootErrRawM: Math.hypot(pr[root][0] - g[root][0], pr[root][1] - g[root][1], pr[root][2] - g[root][2]),
|
|
233
|
+
rootErrAlignedM: Math.hypot(pa[root][0] - g[root][0], pa[root][1] - g[root][1], pa[root][2] - g[root][2]),
|
|
234
|
+
iouRaw: maskIoU(rawMask(k), gtMask(i)).iou,
|
|
235
|
+
iouAligned: maskIoU(alignedMask(k), gtMask(i)).iou,
|
|
236
|
+
contactPredRaw: raw.contact?.frames[k] ?? null,
|
|
237
|
+
contactPredAligned: aligned.contact?.frames[k] ?? null,
|
|
238
|
+
contactGt: gtRender?.contact?.frames[i] ?? null,
|
|
239
|
+
};
|
|
240
|
+
});
|
|
241
|
+
const gtRoot = pairs.map(([i]) => gt.joints.world[i][root]);
|
|
242
|
+
const rawRoot = pairs.map(([, k]) => raw.joints.world[k][root]);
|
|
243
|
+
const alignedRoot = pairs.map(([, k]) => aligned.joints.world[k][root]);
|
|
244
|
+
const ateAligned = trajectoryError(alignedRoot, gtRoot);
|
|
245
|
+
const ateRaw = trajectoryError(rawRoot, gtRoot);
|
|
246
|
+
const last = rows[rows.length - 1];
|
|
247
|
+
|
|
248
|
+
// npz-level facts: physical metrics (tools/ardy/mocap-metrics.mjs on the
|
|
249
|
+
// take as loaded, i.e. with the scale/offsetY/yaw baked in) and the
|
|
250
|
+
// frame-0 root offset the Studio's anchoring discards.
|
|
251
|
+
const predTake = await decodeFile(options.pred);
|
|
252
|
+
const predLoaded = isIdentityTransform(options.predTransform) ? predTake : transformTake(predTake, options.predTransform).motion;
|
|
253
|
+
const gtTake = gtNpz ? await decodeFile(gtNpz) : null;
|
|
254
|
+
const npzOffset = gtTake ? Math.hypot(predTake.posedJoints[0] - gtTake.posedJoints[0], predTake.posedJoints[2] - gtTake.posedJoints[2]) : null;
|
|
255
|
+
|
|
256
|
+
const score = {
|
|
257
|
+
tool: "tools/bench/score.mjs",
|
|
258
|
+
createdAt: new Date().toISOString(),
|
|
259
|
+
gt: { dir: gt.dir, files: gt.files, camera: gt.cameraPath, npz: gtNpz, frames: gt.joints.frames, fps: gt.joints.fps },
|
|
260
|
+
pred: { npz: options.pred, transform: options.predTransform, npzFrames: predTake.frames, npzFps: predTake.fps, timelineFrames: raw.joints.frames, timelineFps: raw.joints.fps },
|
|
261
|
+
timeline: {
|
|
262
|
+
mapping: "each GT frame -> nearest-in-time rendered pred frame (Studio timeline, pred retimed to 24 fps on load); GT frames past the pred's end are not scored",
|
|
263
|
+
scoredFrames: timeline.scored,
|
|
264
|
+
droppedGtFrames: timeline.dropped,
|
|
265
|
+
exact: timeline.exact,
|
|
266
|
+
frameCountMismatch: gt.joints.frames !== raw.joints.frames ? { gt: gt.joints.frames, pred: raw.joints.frames } : null,
|
|
267
|
+
fpsMismatch: gt.joints.fps !== raw.joints.fps || (gtTake && predTake.fps !== gtTake.fps) ? { gtTimeline: gt.joints.fps, predTimeline: raw.joints.fps, gtNpz: gtTake?.fps ?? null, predNpz: predTake.fps } : null,
|
|
268
|
+
},
|
|
269
|
+
joints: { count: gt.joints.joints.length, root: ROOT_JOINT, source: "rendered Studio rig bones (joints.json), scene metres" },
|
|
270
|
+
alignment: {
|
|
271
|
+
method: "least-squares yaw about +Y + XZ translation of the first scored frame's joints (pred raw -> GT); height untouched",
|
|
272
|
+
yawDeg: r6(alignment.yawDeg),
|
|
273
|
+
translationM: [r6(alignment.tx), 0, r6(alignment.tz)],
|
|
274
|
+
renderTransform: alignedTransform,
|
|
275
|
+
anchor,
|
|
276
|
+
},
|
|
277
|
+
pose: {
|
|
278
|
+
mpjpeRootRelativeRawM: r6(mean(rows.map((r) => r.mpjpeRawM))),
|
|
279
|
+
mpjpeRootRelativeAlignedM: r6(mean(rows.map((r) => r.mpjpeAlignedM))),
|
|
280
|
+
paMpjpeM: r6(mean(rows.map((r) => r.paMpjpeM))),
|
|
281
|
+
},
|
|
282
|
+
trajectory: {
|
|
283
|
+
root: ROOT_JOINT,
|
|
284
|
+
ateAlignedM: { rmse: r6(ateAligned.rmse), mean: r6(ateAligned.mean), max: r6(ateAligned.max), final: r6(ateAligned.final) },
|
|
285
|
+
rootErrorRawM: { rmse: r6(ateRaw.rmse), mean: r6(ateRaw.mean), max: r6(ateRaw.max), final: r6(ateRaw.final) },
|
|
286
|
+
pathLengthM: { gt: r6(pathLengthXZ(gtRoot)), pred: r6(pathLengthXZ(alignedRoot)) },
|
|
287
|
+
pathLengthRatio: r6(ratio(pathLengthXZ(alignedRoot), pathLengthXZ(gtRoot))),
|
|
288
|
+
displacementM: { gt: r6(displacementXZ(gtRoot)), pred: r6(displacementXZ(alignedRoot)) },
|
|
289
|
+
displacementRatio: r6(ratio(displacementXZ(alignedRoot), displacementXZ(gtRoot))),
|
|
290
|
+
},
|
|
291
|
+
endpoints: {
|
|
292
|
+
firstFrame: { gtFrame: rows[0].gtFrame, jointErrorRawM: r6(rows[0].jointErrRawM), jointErrorAlignedM: r6(rows[0].jointErrAlignedM) },
|
|
293
|
+
lastFrame: { gtFrame: last.gtFrame, jointErrorRawM: r6(last.jointErrRawM), jointErrorAlignedM: r6(last.jointErrAlignedM) },
|
|
294
|
+
},
|
|
295
|
+
physical: {
|
|
296
|
+
source: "tools/ardy/mocap-metrics.mjs on the npz posed_joints as loaded (native fps, npz units)",
|
|
297
|
+
pred: physical(predLoaded, options.pred),
|
|
298
|
+
gt: gtTake ? physical(gtTake, gtNpz) : null,
|
|
299
|
+
},
|
|
300
|
+
overlap: {
|
|
301
|
+
maskIoURawMean: r6(mean(rows.map((r) => r.iouRaw))),
|
|
302
|
+
maskIoUAlignedMean: r6(mean(rows.map((r) => r.iouAligned))),
|
|
303
|
+
maskIoUAlignedMin: r6(Math.min(...rows.map((r) => r.iouAligned))),
|
|
304
|
+
},
|
|
305
|
+
contact: options.box ? {
|
|
306
|
+
box: options.box,
|
|
307
|
+
basis: "skinned-vertices",
|
|
308
|
+
predRaw: contactSummary(raw.contact, pairs.map(([, k]) => k)),
|
|
309
|
+
predAligned: contactSummary(aligned.contact, pairs.map(([, k]) => k)),
|
|
310
|
+
gt: gtRender ? contactSummary(gtRender.contact, pairs.map(([i]) => i)) : null,
|
|
311
|
+
gtNote: gtRender ? "GT npz re-rendered through the GT camera with --box" : "no GT npz available (pass --gt-npz)",
|
|
312
|
+
} : null,
|
|
313
|
+
npz: {
|
|
314
|
+
frame0RootOffsetM: r6(npzOffset),
|
|
315
|
+
note: "XZ distance between the pred and GT npz frame-0 roots (npz units). The Studio anchors every take's frame 0 on the subject, so this offset does not reach the renders above.",
|
|
316
|
+
},
|
|
317
|
+
checks: {
|
|
318
|
+
alignedRenderVsRigidMaxM: r6(rigidCheck),
|
|
319
|
+
},
|
|
320
|
+
files: { csv: "frames.csv", renders: ["render-raw", "render-aligned", ...(gtRender ? ["render-gt"] : [])] },
|
|
321
|
+
};
|
|
322
|
+
writeFileSync(join(options.out, "score.json"), `${JSON.stringify(score, null, "\t")}\n`);
|
|
323
|
+
const header = ["gt_frame", "pred_frame", "time_s", "mpjpe_rr_raw_m", "mpjpe_rr_aligned_m", "pa_mpjpe_m", "joint_err_raw_m", "joint_err_aligned_m", "root_err_raw_m", "root_err_aligned_m", "iou_raw", "iou_aligned"];
|
|
324
|
+
if (options.box) header.push("box_signed_pred_raw_m", "box_signed_pred_aligned_m", "box_signed_gt_m");
|
|
325
|
+
const csv = [header.join(",")];
|
|
326
|
+
for (const r of rows) {
|
|
327
|
+
const cells = [r.gtFrame, r.predFrame, r.timeS.toFixed(4), r.mpjpeRawM, r.mpjpeAlignedM, r.paMpjpeM, r.jointErrRawM, r.jointErrAlignedM, r.rootErrRawM, r.rootErrAlignedM, r.iouRaw, r.iouAligned].map((v) => (typeof v === "number" && !Number.isInteger(v) ? v.toFixed(6) : String(v)));
|
|
328
|
+
if (options.box) cells.push(r.contactPredRaw?.signedDistanceM ?? "", r.contactPredAligned?.signedDistanceM ?? "", r.contactGt?.signedDistanceM ?? "");
|
|
329
|
+
csv.push(cells.join(","));
|
|
330
|
+
}
|
|
331
|
+
writeFileSync(join(options.out, "frames.csv"), `${csv.join("\n")}\n`);
|
|
332
|
+
console.log(`[score] MPJPE rr raw ${(score.pose.mpjpeRootRelativeRawM * 1000).toFixed(1)} mm, aligned ${(score.pose.mpjpeRootRelativeAlignedM * 1000).toFixed(1)} mm, PA ${(score.pose.paMpjpeM * 1000).toFixed(1)} mm; ATE ${score.trajectory.ateAlignedM.rmse.toFixed(4)} m; IoU raw ${score.overlap.maskIoURawMean.toFixed(4)} aligned ${score.overlap.maskIoUAlignedMean.toFixed(4)}`);
|
|
333
|
+
console.log(`[score] wrote ${join(options.out, "score.json")}`);
|
|
334
|
+
} finally {
|
|
335
|
+
removeSignals();
|
|
336
|
+
await terminateAll(children);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
main().catch((error) => {
|
|
341
|
+
console.error(error?.stack || String(error));
|
|
342
|
+
process.exitCode = 1;
|
|
343
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Collect tools/bench/score.mjs results into one table (#431).
|
|
4
|
+
*
|
|
5
|
+
* node tools/bench/summarize.mjs [--md <file>] [--csv <file>] <score dir ...>
|
|
6
|
+
*
|
|
7
|
+
* Every argument is a score.mjs --out directory (holding score.json). The
|
|
8
|
+
* markdown table is printed to stdout and, with --md, written to that file;
|
|
9
|
+
* --csv writes the same columns as CSV (full precision).
|
|
10
|
+
*/
|
|
11
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
12
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
13
|
+
import { parseArgs } from "node:util";
|
|
14
|
+
|
|
15
|
+
const mm = (m) => (m === null || m === undefined ? null : m * 1000);
|
|
16
|
+
const cm = (m) => (m === null || m === undefined ? null : m * 100);
|
|
17
|
+
const resultFor = (dir) => {
|
|
18
|
+
const file = join(dirname(resolve(dir)), "result.json");
|
|
19
|
+
return existsSync(file) ? JSON.parse(readFileSync(file, "utf8")) : null;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/** [header, unit note, value(score), markdown digits] */
|
|
23
|
+
const COLUMNS = [
|
|
24
|
+
["case", "", (s, dir) => basename(dir), null],
|
|
25
|
+
["frames", "scored/GT", (s) => `${s.timeline.scoredFrames}/${s.gt.frames}`, null],
|
|
26
|
+
["MPJPE raw", "mm, root-relative", (s) => mm(s.pose.mpjpeRootRelativeRawM), 1],
|
|
27
|
+
["MPJPE aligned", "mm, root-relative", (s) => mm(s.pose.mpjpeRootRelativeAlignedM), 1],
|
|
28
|
+
["PA-MPJPE", "mm", (s) => mm(s.pose.paMpjpeM), 1],
|
|
29
|
+
["align yaw", "deg", (s) => s.alignment.yawDeg, 2],
|
|
30
|
+
["align shift", "m (XZ)", (s) => Math.hypot(s.alignment.translationM[0], s.alignment.translationM[2]), 3],
|
|
31
|
+
["root err raw", "m RMSE", (s) => s.trajectory.rootErrorRawM.rmse, 3],
|
|
32
|
+
["ATE aligned", "m RMSE", (s) => s.trajectory.ateAlignedM.rmse, 3],
|
|
33
|
+
["path ratio", "pred/GT", (s) => s.trajectory.pathLengthRatio, 3],
|
|
34
|
+
["disp ratio", "pred/GT", (s) => s.trajectory.displacementRatio, 3],
|
|
35
|
+
["first err aligned", "m", (s) => s.endpoints.firstFrame.jointErrorAlignedM, 3],
|
|
36
|
+
["last err raw", "m", (s) => s.endpoints.lastFrame.jointErrorRawM, 3],
|
|
37
|
+
["last err aligned", "m", (s) => s.endpoints.lastFrame.jointErrorAlignedM, 3],
|
|
38
|
+
["IoU raw", "mean", (s) => s.overlap.maskIoURawMean, 4],
|
|
39
|
+
["IoU aligned", "mean", (s) => s.overlap.maskIoUAlignedMean, 4],
|
|
40
|
+
["foot slide pred", "cm/s", (s) => s.physical.pred.footSlideCmPerS, 2],
|
|
41
|
+
["foot slide GT", "cm/s", (s) => s.physical.gt?.footSlideCmPerS, 2],
|
|
42
|
+
["jitter pred", "mm/s^2", (s) => s.physical.pred.jitterMmPerS2, 0],
|
|
43
|
+
["jitter GT", "mm/s^2", (s) => s.physical.gt?.jitterMmPerS2, 0],
|
|
44
|
+
["below floor pred", "frames", (s) => s.physical.pred.framesBelowFloor, null],
|
|
45
|
+
["box min dist pred", "cm, aligned", (s) => cm(s.contact?.predAligned?.minDistanceM), 1],
|
|
46
|
+
["box min dist GT", "cm", (s) => cm(s.contact?.gt?.minDistanceM), 1],
|
|
47
|
+
["box penetration pred", "cm, aligned", (s) => cm(s.contact?.predAligned?.maxPenetrationM), 1],
|
|
48
|
+
["box penetration GT", "cm", (s) => cm(s.contact?.gt?.maxPenetrationM), 1],
|
|
49
|
+
["npz frame-0 offset", "m", (s) => s.npz.frame0RootOffsetM, 3],
|
|
50
|
+
["runtimeS", "s", (s, dir) => resultFor(dir)?.runtimeS, 2],
|
|
51
|
+
["peakVramMB", "MB", (s, dir) => resultFor(dir)?.peakVramMB, 0],
|
|
52
|
+
["LR switches", "count", (s, dir) => resultFor(dir)?.lrSwitches, null],
|
|
53
|
+
["occluded frames", "frames", (s, dir) => resultFor(dir)?.occludedFrames, null],
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
function main() {
|
|
57
|
+
const { values, positionals } = parseArgs({ args: process.argv.slice(2), allowPositionals: true, options: { md: { type: "string" }, csv: { type: "string" }, help: { type: "boolean", short: "h", default: false } } });
|
|
58
|
+
if (values.help || !positionals.length) {
|
|
59
|
+
console.log("usage: node tools/bench/summarize.mjs [--md <file>] [--csv <file>] <score dir ...>");
|
|
60
|
+
process.exit(values.help ? 0 : 2);
|
|
61
|
+
}
|
|
62
|
+
const rows = positionals.map((dir) => {
|
|
63
|
+
const file = join(resolve(dir), "score.json");
|
|
64
|
+
if (!existsSync(file)) throw new Error(`${dir}: no score.json`);
|
|
65
|
+
const score = JSON.parse(readFileSync(file, "utf8"));
|
|
66
|
+
return COLUMNS.map(([, , value]) => value(score, resolve(dir)));
|
|
67
|
+
});
|
|
68
|
+
const show = (value, digits) => (value === null || value === undefined ? "-" : typeof value === "number" && digits !== null ? value.toFixed(digits) : String(value));
|
|
69
|
+
const md = [
|
|
70
|
+
`| ${COLUMNS.map(([name, unit]) => (unit ? `${name} (${unit})` : name)).join(" | ")} |`,
|
|
71
|
+
`|${COLUMNS.map((_, i) => (i === 0 ? " --- " : " ---: ")).join("|")}|`,
|
|
72
|
+
...rows.map((row) => `| ${row.map((value, i) => show(value, COLUMNS[i][3])).join(" | ")} |`),
|
|
73
|
+
].join("\n");
|
|
74
|
+
console.log(md);
|
|
75
|
+
if (values.md) writeFileSync(values.md, `${md}\n`);
|
|
76
|
+
if (values.csv) {
|
|
77
|
+
const quote = (v) => {
|
|
78
|
+
const text = v === null || v === undefined ? "" : String(v);
|
|
79
|
+
return /[",\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
|
|
80
|
+
};
|
|
81
|
+
const csv = [COLUMNS.map(([name, unit]) => quote(unit ? `${name} (${unit})` : name)).join(","), ...rows.map((row) => row.map(quote).join(","))];
|
|
82
|
+
writeFileSync(values.csv, `${csv.join("\n")}\n`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
main();
|
|
@@ -39,6 +39,8 @@
|
|
|
39
39
|
CLI restarts, ports and browser storage resets. The browser's memory-only SDK starts with this
|
|
40
40
|
anonymous ID on each launch. Independent state files receive independent random IDs, without
|
|
41
41
|
fingerprinting, account linking or cross-device matching; do not copy the state file between users.
|
|
42
|
+
A copied start command such as <code>npx cozyclay --via site</code> stores that one source label
|
|
43
|
+
with the first-launch receipt instead of asking where you heard about CozyClay.
|
|
42
44
|
Prompt text, asset names,
|
|
43
45
|
file names, project content, local paths and any user-entered text are never collected. Session
|
|
44
46
|
duration and action counts are bucketed. Source checkouts, forks, development servers, CI and tests
|
|
@@ -83,20 +85,20 @@ cclay telemetry on</code></pre>
|
|
|
83
85
|
<table>
|
|
84
86
|
<thead><tr><th>Event</th><th>Purpose</th></tr></thead>
|
|
85
87
|
<tbody>
|
|
86
|
-
<tr><td>install:first_launch, app:session_started, app:session_ended</td><td>Counting installs and sessions; duration and action counts are bucketed</td></tr>
|
|
87
|
-
<tr><td>feature:used</td><td>One signal per feature per session</td></tr>
|
|
88
|
+
<tr><td>install:first_launch, app:session_started, app:session_ended</td><td>Counting installs and sessions; duration and action counts are bucketed. install:first_launch may carry heard_from (x, hn, reddit, github, friend, other, site or playground), from the first-launch prompt or a copied <code>npx cozyclay --via</code> command. app:session_ended also carries per-session edit gesture counts in five groups, pose_edit_bucket, camera_edit_bucket, object_edit_bucket, shot_edit_bucket and prompt_edit_bucket, each bucketed (0, 1-3, 4-10 or gte11); never what was edited</td></tr>
|
|
89
|
+
<tr><td>feature:used</td><td>One signal per feature per session: a fixed feature name such as pose_edit (a real bone or pose change), pose_save (saving a pose to the library), fal_motion_open (opening the AI video A/B card), camera_fly or export_video</td></tr>
|
|
88
90
|
<tr><td>$pageview, scene:created, scene:loaded, craft:first_action, activation:completed</td><td>Funnel and drop-off analysis</td></tr>
|
|
89
91
|
<tr><td>craft:first_edit</td><td>First successful Studio authoring change per editor mount; only a fixed edit-kind label and definition version 1 (not camera navigation, playback, loading, restore, or undo/redo)</td></tr>
|
|
90
92
|
<tr><td>playground:first_edit</td><td>The same versioned first-edit definition for Playground, counted separately from Studio; no prompts or scene contents</td></tr>
|
|
91
93
|
<tr><td>project:saved, project:opened</td><td>Whether people keep their work (object counts and ages are bucketed; never names or paths)</td></tr>
|
|
92
94
|
<tr><td>motion:backend_state</td><td>Session capability baseline: backend (none, local_kimodo or hosted) and boolean host_configured, never a host address</td></tr>
|
|
93
|
-
<tr><td>motion:generate_requested</td><td>Explicit generation intent, not prompt-block editing: initiating surface (timeline, line_edit, trail or
|
|
94
|
-
<tr><td>motion:preflight_blocked</td><td>Request readiness refusal: request_id, surface and normalized reason (unconfigured, unreachable or
|
|
95
|
-
<tr><td>motion:preflight_passed</td><td>Request readiness accepted, before job start: request_id, surface and backend (none, local_kimodo or
|
|
95
|
+
<tr><td>motion:generate_requested</td><td>Explicit generation intent, not prompt-block editing: initiating surface (timeline, line_edit, trail, mcp, fal_card or agent), input_mode (prompt, pose, edit, a_to_b or still) and an ephemeral random request_id</td></tr>
|
|
96
|
+
<tr><td>motion:preflight_blocked</td><td>Request readiness refusal: request_id, surface and normalized reason (unconfigured, unreachable, unsupported_route, locked or missing_input). locked means the AI video route is not enabled for the account yet; missing_input means the A/B captures were incomplete</td></tr>
|
|
97
|
+
<tr><td>motion:preflight_passed</td><td>Request readiness accepted, before job start: request_id, surface and backend (none, local_kimodo, hosted or fal)</td></tr>
|
|
96
98
|
<tr><td>motion:job_started</td><td>Generation execution started: request_id, backend and input_mode</td></tr>
|
|
97
99
|
<tr><td>motion:job_succeeded</td><td>Generation completed before result application: request_id, backend, input_mode and bucketed duration_bucket (lt1s, 1-3s, 3-10s, 10-30s or gte30s)</td></tr>
|
|
98
|
-
<tr><td>motion:job_failed</td><td>Generation failure or real cancellation: request_id, backend, input_mode, duration_bucket and normalized error_code (aborted, unsupported_route, generation_failed or unknown), never raw error messages</td></tr>
|
|
99
|
-
<tr><td>motion:result_applied</td><td>A generated result was applied to its requesting character: request_id and backend. Job success alone does not prove application.</td></tr>
|
|
100
|
+
<tr><td>motion:job_failed</td><td>Generation failure or real cancellation: request_id, backend, input_mode, duration_bucket and normalized error_code (aborted, unsupported_route, generation_failed, quota or unknown), never raw error messages</td></tr>
|
|
101
|
+
<tr><td>motion:result_applied</td><td>A generated result was applied to its requesting character: request_id and backend. For the fal AI video route, applied means the generated clip was loaded into the Studio for extraction. Job success alone does not prove application.</td></tr>
|
|
100
102
|
<tr><td>workflow:run_requested</td><td>Workflow execution requests: surface (workflow), ephemeral run_id and node_count_bucket (0, 1-3, 4-10 or gte11). No graph contents, node labels or outputs.</td></tr>
|
|
101
103
|
<tr><td>workflow:run_succeeded, workflow:run_failed, workflow:run_cancelled</td><td>Workflow outcomes paired by run_id, with duration_bucket (lt1s, 1-3s, 3-10s, 10-30s or gte30s); failures and actual cancellations also include failure_code (aborted, capture_failed, generation_failed or unknown). Starting another run does not imply cancellation.</td></tr>
|
|
102
104
|
<tr><td>workflow:result_applied</td><td>Captured or generated image/video output was written to the Workflow canvas: run_id only, at most once per run, independently of a later failure. No image bytes or output contents.</td></tr>
|
|
@@ -111,6 +113,9 @@ cclay telemetry on</code></pre>
|
|
|
111
113
|
<tr><td>export:attempt_started, export:attempt_succeeded, export:attempt_failed, export:attempt_cancelled</td><td>Export attempts and outcomes, paired by an ephemeral random attempt_id; export_kind (video, depth_video, frame or keyframe_pack), format (mp4, png or zip) and initiating surface (studio, workflow or embed). Terminal events include a bucketed duration_bucket; failures and real cancellations include only a normalized failure_code (unsupported_codec, encode_failed, render_failed, aborted or unknown), never raw error messages, filenames, URLs, paths, prompts or scene content. Success means pipeline completion or download handoff, not confirmation that the OS saved a file.</td></tr>
|
|
112
114
|
<tr><td>export:keyframe_pack</td><td>Legacy Workflow pack handoffs, with numeric entries count and source=workflow; not an additional export attempt</td></tr>
|
|
113
115
|
<tr><td>playground:opened, playground:first_action</td><td>Whether the home-page playground gets used</td></tr>
|
|
116
|
+
<tr><td>app:error</td><td>Uncaught errors and unhandled promise rejections, at most ten distinct per session: error_kind (error or unhandled_rejection), error_type (a built-in name such as TypeError or DOMException, other or non_error), error_source (app, extension, external or unknown) and, only for the app's own scripts, error_file (the bundle file name), error_line and error_col. Never the error message, stack text, URLs, query strings, paths, prompts or scene content.</td></tr>
|
|
117
|
+
<tr><td>device:profile</td><td>Once per session, whether the studio can run smoothly: gpu_vendor (nvidia, amd, intel, apple, qualcomm, arm, software, other or unknown), gpu_class (discrete, integrated, software or unknown), webgl (webgl2, webgl1 or none), cpu_bucket (1-4, 5-8, 9-16, gte17 or unknown), memory_bucket (lt4, 4-7, gte8 or unknown) and frame_rate_bucket (lt20, 20-40, 40-55, gte55 or unknown) from one three-second sample. The GPU renderer string and exact hardware values never leave the browser.</td></tr>
|
|
118
|
+
<tr><td>survey:use_case</td><td>An optional question shown at most once after a successful export, only while telemetry is on: use_case (animation, film, game, ad_mv, personal, other or skip) and team (team, solo or skip). Closed choices only; no free text.</td></tr>
|
|
114
119
|
<tr><td>hosted:composer_viewed, hosted:login_started, hosted:ticket_created, hosted:result_opened, hosted:opened_in_studio</td><td>Hosted demo funnel</td></tr>
|
|
115
120
|
<tr><td>tutorial:started</td><td>Camera tutorial attempts on surface (studio or playground), with numeric tutorial_version 1 and start_source (query, settings or landing). Restarting starts a new attempt; no tutorial attempt identifier or user text is sent.</td></tr>
|
|
116
121
|
<tr><td>tutorial:step_entered, tutorial:dismissed</td><td>Entered steps and explicit dismissal of an unfinished tutorial; surface (studio or playground), numeric tutorial_version 1 and step_kind (fly, walk, dolly, orbit, shot, rail or play). Missing completion can indicate abandonment or missing telemetry; browser close does not promise a dismissal event.</td></tr>
|
|
@@ -120,7 +125,7 @@ cclay telemetry on</code></pre>
|
|
|
120
125
|
</table>
|
|
121
126
|
</div>
|
|
122
127
|
<p class="note">Motion request_id values are fresh random 32-character hexadecimal values, held only for one request; they are not persisted or derived from prompts, hosts, credentials, project content or other identifiers. The ambiguous motion:generate_blocked event stopped being emitted in the September 15, 2026 issue #273 contract. Historical events are not relabeled and are excluded from the new explicit-demand metric. Automatic line previews and prompt-block edits are not explicit Generate requests.</p>
|
|
123
|
-
<p class="note">Every regular app event and session-end beacon carries the same distribution (npm or hosted), app_version when available, origin_kind (local or hosted), internal_qa boolean, coarse operating-system label and, for npm sessions, install_kind (npx or global). PostHog's free plan retains events for one year.</p>
|
|
128
|
+
<p class="note">Every regular app event and session-end beacon carries the same distribution (npm or hosted), app_version when available, origin_kind (local or hosted), internal_qa boolean, coarse operating-system label and, for npm sessions, install_kind (npx or global) and update_status (latest, outdated or unknown), read from the package's own cached update check. PostHog's free plan retains events for one year.</p>
|
|
124
129
|
<p class="note">Workflow run_id, Agent turn_id and MCP request_id values are fresh random 32-character hexadecimal values held only for one execution. Execution telemetry contains only closed categories, outcomes and elapsed-time buckets; missing telemetry is left unknown, and a disconnected MCP editor is not backfilled.</p>
|
|
125
130
|
</section>
|
|
126
131
|
|