cozyclay 2.1.1 → 3.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.
Files changed (577) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/README.md +12 -30
  3. package/bin/agent/agent-routes.mjs +40 -113
  4. package/bin/agent/agent-runner.mjs +9 -10
  5. package/bin/agent/cliproxy-provider.mjs +135 -0
  6. package/bin/agent/credential-store.mjs +30 -7
  7. package/bin/agent/pi-tools.mjs +4 -1
  8. package/bin/agent/provider-keys.mjs +32 -0
  9. package/bin/agent/provider-login.mjs +110 -0
  10. package/bin/agent/provider-routes.mjs +99 -0
  11. package/bin/agent/providers.mjs +19 -97
  12. package/bin/agent/script-tool.mjs +17 -4
  13. package/bin/agent/studio-motion-reference.mjs +23 -0
  14. package/bin/agent/studio-prompt.mjs +1 -1
  15. package/bin/agent/studio-rebase-text.mjs +32 -0
  16. package/bin/agent/studio-rebase.mjs +26 -39
  17. package/bin/agent/studio-tools.mjs +9 -28
  18. package/bin/agent/tool-arguments.mjs +49 -0
  19. package/bin/agent/turn-frames.mjs +66 -0
  20. package/bin/codex-auth.mjs +9 -2
  21. package/bin/cozyclay.mjs +10 -116
  22. package/bin/live/cli.mjs +3 -3
  23. package/bin/mcp-runtime.mjs +2 -12
  24. package/bin/package-signature.mjs +0 -4
  25. package/dist/app/index.html +7 -6
  26. package/dist/assets/EXRLoader-CvVIvGVE.js +1 -0
  27. package/dist/assets/analytics-C9sUnndd.js +1 -0
  28. package/dist/assets/app-CCh--1Hc.js +5087 -0
  29. package/dist/assets/app-f-fkV5uP.css +1 -0
  30. package/dist/assets/compile-worker-CkxiD6lH.js +5 -0
  31. package/dist/assets/fflate.module-KbGj5-H6.js +5 -0
  32. package/dist/assets/{first-shot-handoff-msLI71g1.js → first-shot-handoff-CC4xe4HM.js} +1 -1
  33. package/dist/assets/{landing-ChUvB_zL.js → landing-DjHD8u7k.js} +1 -1
  34. package/dist/assets/scene-asset-sync-Hx_FzdOH.js +20 -0
  35. package/dist/assets/scene-asset-sync-_S8NQl5B.css +1 -0
  36. package/dist/assets/workflow-C4yKH1p4.js +179 -0
  37. package/dist/cozyclay-package.json +1 -1
  38. package/dist/index.html +3 -3
  39. package/dist/sw.js +3 -6
  40. package/dist/workflow/index.html +5 -4
  41. package/mcp/LIVE-PROTOCOL.md +7 -47
  42. package/mcp/README.md +16 -26
  43. package/mcp/live-hub.mjs +6 -10
  44. package/mcp/package.json +1 -3
  45. package/mcp/tool-handlers.mjs +4 -168
  46. package/package.json +3 -22
  47. package/src/App.jsx +150 -2624
  48. package/src/agent-progress.js +79 -0
  49. package/src/agent-turn-mark.js +30 -0
  50. package/src/analytics.js +1 -19
  51. package/src/app-context.js +5 -0
  52. package/src/app-stage.jsx +2 -34
  53. package/src/asset-pane.jsx +6 -5
  54. package/src/camera-block.js +21 -0
  55. package/src/camera-direction.js +120 -0
  56. package/src/character-kind.js +3 -9
  57. package/src/command-bus.js +8 -6
  58. package/src/commands/cast.js +5 -34
  59. package/src/commands/elements/character.js +1 -1
  60. package/src/commands/elements/motion.js +1 -6
  61. package/src/commands/elements/object.js +6 -2
  62. package/src/commands/elements/shot.js +4 -1
  63. package/src/commands/elements.js +38 -14
  64. package/src/commands/index.js +3 -1
  65. package/src/commands/kits.js +37 -0
  66. package/src/commands/motion-timing.js +57 -0
  67. package/src/commands/motion.js +9 -152
  68. package/src/commands/motion3-compile.js +65 -0
  69. package/src/commands/motion3.js +207 -0
  70. package/src/commands/objects.js +6 -4
  71. package/src/commands/shot.js +49 -11
  72. package/src/commands/stage.js +14 -1
  73. package/src/commands/view.js +4 -1
  74. package/src/document-mirrors.js +42 -0
  75. package/src/document-store.js +4 -0
  76. package/src/domains/cast.js +19 -194
  77. package/src/domains/motion/motion-range-pin-rules.js +1 -1
  78. package/src/domains/motion/take-source.js +51 -0
  79. package/src/domains/motion.js +60 -1838
  80. package/src/domains/scenes.js +12 -55
  81. package/src/domains/shots.js +2 -2
  82. package/src/domains/stage.js +4 -2
  83. package/src/draft-ghost.js +46 -0
  84. package/src/draft-ghost.jsx +129 -0
  85. package/src/dualview.jsx +4 -3
  86. package/src/effects/EffectsLayer.jsx +112 -0
  87. package/src/effects/effect-meshes.js +232 -0
  88. package/src/effects/effect-record.js +103 -0
  89. package/src/effects/emitter.js +59 -0
  90. package/src/effects/simulate.js +261 -0
  91. package/src/effects/state.js +59 -0
  92. package/src/execution-telemetry.js +2 -3
  93. package/src/first-success-guide.jsx +5 -5
  94. package/src/ghost-set.js +257 -0
  95. package/src/held-object-edit.jsx +113 -0
  96. package/src/held-object-pose.js +175 -0
  97. package/src/hierarchy-model.js +1 -1
  98. package/src/hierarchy-panel.jsx +1 -1
  99. package/src/imported-mesh.jsx +103 -0
  100. package/src/kits/held.js +84 -0
  101. package/src/kits/index.js +70 -0
  102. package/src/kits/smoking.js +57 -0
  103. package/src/light-blockers.js +93 -0
  104. package/src/motion/beat-board-model.js +84 -0
  105. package/src/motion/beat-board.css +124 -0
  106. package/src/motion/beat-board.jsx +59 -0
  107. package/src/motion/center-of-mass.js +66 -0
  108. package/src/motion/fix-collisions.js +850 -0
  109. package/src/motion/physics-review.js +118 -0
  110. package/src/{ardy → motion}/physics-support.js +1 -76
  111. package/src/motion/rig-snapshot.js +22 -0
  112. package/src/motion/take-segments.jsx +140 -0
  113. package/src/{ardy → motion}/timeline-coordinates.js +0 -21
  114. package/src/{ardy → motion}/timeline.css +1 -2
  115. package/src/{ardy → motion}/timeline.jsx +42 -413
  116. package/src/motion/waypoints.js +76 -0
  117. package/src/motion3/beats.js +83 -0
  118. package/src/motion3/body.js +158 -0
  119. package/src/motion3/bones.js +47 -0
  120. package/src/motion3/carried.js +47 -0
  121. package/src/motion3/compile-job.js +75 -0
  122. package/src/motion3/compile-worker.js +10 -0
  123. package/src/motion3/compiler/anchors.js +105 -0
  124. package/src/motion3/compiler/arm-orient.js +128 -0
  125. package/src/motion3/compiler/channels.js +125 -0
  126. package/src/motion3/compiler/compile.js +205 -0
  127. package/src/motion3/compiler/gait-rhythm.js +97 -0
  128. package/src/motion3/compiler/grip.js +171 -0
  129. package/src/motion3/compiler/head-steady.js +48 -0
  130. package/src/motion3/compiler/hold.js +134 -0
  131. package/src/motion3/compiler/idle.js +113 -0
  132. package/src/motion3/compiler/limbs.js +170 -0
  133. package/src/motion3/compiler/math.js +72 -0
  134. package/src/motion3/compiler/pose.js +162 -0
  135. package/src/motion3/compiler/quat.js +49 -0
  136. package/src/motion3/compiler/skeleton.js +74 -0
  137. package/src/motion3/compiler/validate-common.js +32 -0
  138. package/src/motion3/compiler/validate-goals.js +118 -0
  139. package/src/motion3/compiler/validate-objects.js +143 -0
  140. package/src/motion3/compiler/validate-parts.js +116 -0
  141. package/src/motion3/compiler/validate.js +122 -0
  142. package/src/motion3/compiler/vocab.js +73 -0
  143. package/src/motion3/compiler/walk-pelvis.js +275 -0
  144. package/src/motion3/compiler/walk.js +207 -0
  145. package/src/motion3/compiler/wrist-clamp.js +32 -0
  146. package/src/motion3/delta.js +47 -0
  147. package/src/motion3/describe.js +174 -0
  148. package/src/motion3/docs.js +27 -0
  149. package/src/motion3/edit-carry.js +21 -0
  150. package/src/motion3/fix/ease.js +16 -0
  151. package/src/motion3/fix/ground.js +37 -0
  152. package/src/motion3/fix/index.js +52 -0
  153. package/src/motion3/fix/push.js +71 -0
  154. package/src/motion3/fix/reach.js +46 -0
  155. package/src/motion3/fix/retime.js +79 -0
  156. package/src/motion3/fix/self.js +211 -0
  157. package/src/motion3/fix/slide.js +70 -0
  158. package/src/motion3/fix/turn.js +51 -0
  159. package/src/motion3/follow.js +39 -0
  160. package/src/motion3/free-motion.js +173 -0
  161. package/src/motion3/grip-choice.js +130 -0
  162. package/src/motion3/hold-plan.js +157 -0
  163. package/src/motion3/json-patch.js +77 -0
  164. package/src/motion3/key-aim.js +105 -0
  165. package/src/motion3/key-edit.js +82 -0
  166. package/src/motion3/lenient.js +127 -0
  167. package/src/motion3/object-keys.js +236 -0
  168. package/src/motion3/object-pose.js +182 -0
  169. package/src/motion3/object-tracks.js +12 -0
  170. package/src/motion3/pipeline.js +102 -0
  171. package/src/motion3/playback.js +51 -0
  172. package/src/motion3/probes.js +91 -0
  173. package/src/motion3/qa/body-checks.js +34 -0
  174. package/src/motion3/qa/check-names.js +14 -0
  175. package/src/motion3/qa/checks.js +121 -0
  176. package/src/motion3/qa/contact-checks.js +51 -0
  177. package/src/motion3/qa/explain.js +80 -0
  178. package/src/motion3/qa/fmt.js +5 -0
  179. package/src/motion3/qa/foot-checks.js +72 -0
  180. package/src/motion3/qa/geometry.js +77 -0
  181. package/src/motion3/qa/goals.js +191 -0
  182. package/src/motion3/qa/ground.js +60 -0
  183. package/src/motion3/qa/hints-contact.js +179 -0
  184. package/src/motion3/qa/hints-goal.js +160 -0
  185. package/src/motion3/qa/hints-knee.js +24 -0
  186. package/src/motion3/qa/hints-motion.js +209 -0
  187. package/src/motion3/qa/hints-object.js +91 -0
  188. package/src/motion3/qa/index.js +42 -0
  189. package/src/motion3/qa/key-checks.js +82 -0
  190. package/src/motion3/qa/near.js +96 -0
  191. package/src/motion3/qa/object-checks.js +135 -0
  192. package/src/motion3/qa/patch.js +161 -0
  193. package/src/motion3/qa/severity.js +38 -0
  194. package/src/motion3/qa/spec-checks.js +65 -0
  195. package/src/motion3/qa/style.js +218 -0
  196. package/src/motion3/qa/wrist-checks.js +69 -0
  197. package/src/motion3/qa/wrist-obs.js +60 -0
  198. package/src/motion3/refs.js +120 -0
  199. package/src/motion3/repair-tune.js +121 -0
  200. package/src/motion3/repair.js +83 -0
  201. package/src/motion3/report.js +67 -0
  202. package/src/motion3/rigs/index.js +14 -0
  203. package/src/motion3/rigs/x-bot-tpose-body.json +1 -0
  204. package/src/motion3/rigs/x-bot-tpose.json +1 -0
  205. package/src/motion3/rigs/y-bot-tpose-body.json +1 -0
  206. package/src/motion3/rigs/y-bot-tpose.json +1 -0
  207. package/src/motion3/scene.js +60 -0
  208. package/src/motion3/schema/brief.js +89 -0
  209. package/src/motion3/schema/fields.js +44 -0
  210. package/src/motion3/schema/spec-examples.js +46 -0
  211. package/src/motion3/schema/topics.js +111 -0
  212. package/src/motion3/schema/types.js +147 -0
  213. package/src/motion3/search.js +117 -0
  214. package/src/motion3/second-hand.js +112 -0
  215. package/src/motion3/skills/basic.js +82 -0
  216. package/src/motion3/skills/book.js +76 -0
  217. package/src/motion3/skills/car.js +145 -0
  218. package/src/motion3/skills/frames-auto.js +29 -0
  219. package/src/motion3/skills/index.js +83 -0
  220. package/src/motion3/skills/info.js +65 -0
  221. package/src/motion3/skills/smoke-kit.js +109 -0
  222. package/src/motion3/skills/smoke.js +160 -0
  223. package/src/motion3/spec-store.js +59 -0
  224. package/src/motion3/take-edit.js +155 -0
  225. package/src/motion3/take.js +63 -0
  226. package/src/motion3/timeline.js +127 -0
  227. package/src/motion3/views.js +121 -0
  228. package/src/motion3/walk-plan.js +119 -0
  229. package/src/object-catalog.jsx +7 -1
  230. package/src/object-colors.js +98 -0
  231. package/src/object-gizmo-parts.js +115 -0
  232. package/src/object-gizmo.jsx +37 -122
  233. package/src/object-glow.js +139 -0
  234. package/src/object-hidden.js +33 -0
  235. package/src/object-look.js +196 -0
  236. package/src/object-size.js +10 -0
  237. package/src/object-travel.js +71 -6
  238. package/src/panels/CameraPanel.jsx +7 -7
  239. package/src/panels/HdrEnvironmentControls.jsx +59 -0
  240. package/src/panels/LightPanel.jsx +25 -4
  241. package/src/panels/ObjectLookPanel.jsx +88 -0
  242. package/src/panels/PosePanel.jsx +2 -17
  243. package/src/panels/RigControlPanel.jsx +5 -53
  244. package/src/panels/SubjectBox.jsx +4 -1
  245. package/src/panels/details.css +14 -0
  246. package/src/posestudio.jsx +4 -27
  247. package/src/project-browser.jsx +3 -4
  248. package/src/project-resources.js +18 -31
  249. package/src/project.js +13 -104
  250. package/src/props.jsx +25 -280
  251. package/src/rail-dot-drag.js +23 -0
  252. package/src/render-passes.js +10 -0
  253. package/src/resource-status.jsx +1 -0
  254. package/src/result-modal.jsx +4 -19
  255. package/src/room.jsx +20 -12
  256. package/src/scene-assets.js +8 -6
  257. package/src/scene-light-place.js +72 -0
  258. package/src/scene-lights.jsx +148 -0
  259. package/src/scene-objects.js +31 -139
  260. package/src/scenes.js +15 -105
  261. package/src/semantic-edit.js +2 -5
  262. package/src/set-pieces.jsx +183 -0
  263. package/src/settings-menu.jsx +3 -11
  264. package/src/shell/BottomDock.jsx +9 -51
  265. package/src/shell/DetailsSlot.jsx +16 -200
  266. package/src/shell/GhostsMenuGroup.jsx +74 -0
  267. package/src/shell/LibrarySlot.jsx +1 -3
  268. package/src/shell/MenuBar.jsx +3 -3
  269. package/src/shell/PreferencesDialog.jsx +15 -264
  270. package/src/shell/PreferencesSlot.jsx +2 -8
  271. package/src/shell/TopBar.jsx +5 -84
  272. package/src/shell/ViewportToolbar.jsx +10 -70
  273. package/src/shell/agent-glass.css +19 -6
  274. package/src/shell/details-selection.js +42 -0
  275. package/src/shell/ghosts-menu.css +22 -0
  276. package/src/shell/glass-regions.css +29 -3
  277. package/src/shell/preferences.css +6 -1
  278. package/src/shot-authoring.js +4 -7
  279. package/src/soft-shadows.js +37 -0
  280. package/src/stage-attach-frames.js +12 -12
  281. package/src/stage-canvas-rigs.jsx +3 -3
  282. package/src/stage-capture.jsx +42 -8
  283. package/src/stage-characters.jsx +22 -216
  284. package/src/stage-editor-gizmos.jsx +6 -6
  285. package/src/stage-environment.js +93 -0
  286. package/src/stage-hdr-builtins.js +107 -0
  287. package/src/stage-hdr-image.js +116 -0
  288. package/src/stage-hdr-library.js +70 -0
  289. package/src/stage-hdr-map.js +94 -0
  290. package/src/stage-hdr.js +130 -0
  291. package/src/stage-lighting.js +149 -0
  292. package/src/stage-occlusion.js +210 -0
  293. package/src/stage-sky.jsx +159 -0
  294. package/src/stage-startup-defaults.js +5 -9
  295. package/src/studio-actions.js +12 -14
  296. package/src/studio-agent-commands.js +19 -16
  297. package/src/studio-agent-motion.js +9 -9
  298. package/src/studio-agent-protocol.js +41 -71
  299. package/src/studio-app-binding.js +25 -45
  300. package/src/studio-asset-list.js +17 -0
  301. package/src/studio-contact-sheet.js +37 -5
  302. package/src/studio-elements.js +27 -7
  303. package/src/studio-entity-bounds.js +18 -0
  304. package/src/studio-error-codes.js +8 -0
  305. package/src/studio-export-framing.js +152 -0
  306. package/src/studio-lighting-check.js +103 -0
  307. package/src/studio-motion-visual.js +150 -0
  308. package/src/studio-schema-errors.js +80 -0
  309. package/src/studio-support-gap.js +74 -0
  310. package/src/styles.css +1 -19
  311. package/src/timeline-defaults.js +5 -0
  312. package/src/timeline-extent.js +8 -20
  313. package/src/tutorial-walk.js +17 -0
  314. package/src/ui-scrub.js +1 -1
  315. package/src/use-shot-camera-tracks.js +12 -0
  316. package/src/view-ghosts.js +97 -0
  317. package/src/view-ghosts.jsx +49 -0
  318. package/src/workflow/AgentPanel.jsx +47 -166
  319. package/src/workflow/AgentProgressCard.jsx +107 -0
  320. package/src/workflow/AgentReplyText.jsx +37 -0
  321. package/src/workflow/AgentTurnSteps.jsx +84 -0
  322. package/src/workflow/LoginPanel.jsx +140 -0
  323. package/src/workflow/WorkflowBuilder.jsx +7 -23
  324. package/src/workflow/agent-client.js +54 -51
  325. package/src/workflow/agent-panel.css +71 -1
  326. package/src/workflow/agent-progress-card.css +167 -0
  327. package/src/workflow/agent-progress-view.js +47 -0
  328. package/src/workflow/agent-turn-steps.css +113 -0
  329. package/src/workflow/agent-turn.js +191 -0
  330. package/src/workflow/login-client.js +34 -0
  331. package/src/workflow/motion-input.js +18 -31
  332. package/src/workflow/node-schema.js +1 -1
  333. package/src/workflow/reply-markdown.js +69 -0
  334. package/src/workflow/scene-asset-sync.js +8 -8
  335. package/tools/agent-session-stats.mjs +61 -0
  336. package/tools/dev-full.mjs +5 -56
  337. package/tools/eslint-baseline.json +12 -13
  338. package/tools/knip-baseline.json +0 -179
  339. package/tools/loc-baseline.json +31 -81
  340. package/tools/motion3/extract-body.mjs +103 -0
  341. package/tools/motion3/extract-rig.mjs +85 -0
  342. package/tools/process-supervisor.mjs +0 -88
  343. package/tools/run-tests.mjs +8 -124
  344. package/bin/agent/motion-runtime.mjs +0 -76
  345. package/dist/ardy/cskel27-rest.json +0 -557
  346. package/dist/assets/analytics-C9pMpW7s.js +0 -1
  347. package/dist/assets/app-B_2XB9rw.css +0 -1
  348. package/dist/assets/app-DAuvKPP8.js +0 -4920
  349. package/dist/assets/config-Bx7pYsD9.js +0 -1
  350. package/dist/assets/config-DeoFTYzb.css +0 -1
  351. package/dist/assets/demo-CvKuBgy5.js +0 -1
  352. package/dist/assets/scene-asset-sync-DKkpjKBV.js +0 -19
  353. package/dist/assets/scene-asset-sync-DjJ_cJ2R.css +0 -1
  354. package/dist/assets/ticket-BPk2Ni95.js +0 -1
  355. package/dist/assets/workflow-jJRBR94Z.js +0 -179
  356. package/dist/demo/walk-then-stop.npz +0 -0
  357. package/mcp/ardy-prompts.mjs +0 -218
  358. package/src/ardy/auto-fix-panel.css +0 -108
  359. package/src/ardy/auto-physics.js +0 -1194
  360. package/src/ardy/client.js +0 -153
  361. package/src/ardy/convert.js +0 -313
  362. package/src/ardy/cskel27-neutral.js +0 -39
  363. package/src/ardy/cskel27.js +0 -77
  364. package/src/ardy/export.js +0 -165
  365. package/src/ardy/fix-collisions.js +0 -2257
  366. package/src/ardy/foot-lock.js +0 -230
  367. package/src/ardy/motion-calibration.js +0 -118
  368. package/src/ardy/motion-edit.js +0 -213
  369. package/src/ardy/motion-url.js +0 -55
  370. package/src/ardy/mp4-duration.js +0 -102
  371. package/src/ardy/npz.js +0 -595
  372. package/src/ardy/physics-panel.css +0 -21
  373. package/src/ardy/physics-panel.jsx +0 -89
  374. package/src/ardy/physics-refine.js +0 -111
  375. package/src/ardy/physics-review.js +0 -567
  376. package/src/ardy/physics-temporal.js +0 -54
  377. package/src/ardy/platform-fit-panel.css +0 -64
  378. package/src/ardy/platform-fit-panel.jsx +0 -41
  379. package/src/ardy/platform-fit.js +0 -326
  380. package/src/ardy/playback.js +0 -711
  381. package/src/ardy/pose-pin.js +0 -97
  382. package/src/ardy/prompt-clips.js +0 -30
  383. package/src/ardy/retime.js +0 -211
  384. package/src/ardy/root-drop.js +0 -369
  385. package/src/ardy/timeline-resize.js +0 -13
  386. package/src/ardy/to-cskel27.js +0 -225
  387. package/src/ardy/trim.js +0 -33
  388. package/src/ardy/waypoints.js +0 -369
  389. package/src/ardy-bridge-contract.js +0 -281
  390. package/src/domains/motion/motion-video-capture.js +0 -664
  391. package/src/i2v-motion-client.js +0 -77
  392. package/src/i2v-motion-studio.jsx +0 -180
  393. package/src/line-edit.js +0 -1348
  394. package/src/motion/generation.js +0 -105
  395. package/src/motion-readiness-ui.jsx +0 -67
  396. package/src/motion-readiness.js +0 -40
  397. package/src/motion-resources.js +0 -177
  398. package/src/motion-store.js +0 -65
  399. package/src/motion-trail.js +0 -601
  400. package/src/multimodel-ingest.js +0 -394
  401. package/src/panels/PromptBlocksPanel.jsx +0 -444
  402. package/src/panels/VideoCapturePanel.jsx +0 -181
  403. package/src/pose-extract/browser.js +0 -42
  404. package/src/pose-extract/detector.js +0 -100
  405. package/src/pose-extract/fit.js +0 -227
  406. package/src/pose-extract/image-frame.js +0 -51
  407. package/src/pose-extract/index.js +0 -58
  408. package/src/pose-extract/keyframes.js +0 -74
  409. package/src/pose-extract/landmarks.js +0 -162
  410. package/src/pose-extract/mirror.js +0 -153
  411. package/src/pose-extract/take.js +0 -131
  412. package/src/pose-extract/video-frames.js +0 -91
  413. package/src/take-recipe.js +0 -165
  414. package/src/trail-key-conflicts.js +0 -39
  415. package/src/trail-pick.js +0 -59
  416. package/src/use-i2v-capture-modal.js +0 -15
  417. package/tools/ardy/BRIDGE.md +0 -37
  418. package/tools/ardy/GVHMR-NPZ.md +0 -68
  419. package/tools/ardy/README.md +0 -124
  420. package/tools/ardy/__pycache__/cclay_gvhmr_worker.cpython-313.pyc +0 -0
  421. package/tools/ardy/artifacts.mjs +0 -34
  422. package/tools/ardy/bridge.mjs +0 -1732
  423. package/tools/ardy/bvh-cskel27.mjs +0 -1265
  424. package/tools/ardy/cclay_constrained_generate.py +0 -561
  425. package/tools/ardy/cclay_gvhmr_worker.py +0 -264
  426. package/tools/ardy/cclay_motion_edit.py +0 -510
  427. package/tools/ardy/cclay_sequence_generate.py +0 -705
  428. package/tools/ardy/cclay_worker.py +0 -304
  429. package/tools/ardy/dump-npz.py +0 -205
  430. package/tools/ardy/extract-rest.mjs +0 -299
  431. package/tools/ardy/extract.mjs +0 -507
  432. package/tools/ardy/footage.mjs +0 -467
  433. package/tools/ardy/gvhmr-floor.mjs +0 -102
  434. package/tools/ardy/gvhmr-to-cskel27.mjs +0 -21
  435. package/tools/ardy/gvhmr_fastpath.py +0 -168
  436. package/tools/ardy/gvhmr_trajectory.py +0 -240
  437. package/tools/ardy/merge-text-encoder.py +0 -160
  438. package/tools/ardy/mocap-metrics.mjs +0 -200
  439. package/tools/ardy/mocap-quality-gate.mjs +0 -110
  440. package/tools/ardy/motion-stabilize.mjs +0 -348
  441. package/tools/ardy/npz.mjs +0 -414
  442. package/tools/ardy/pose-to-npz.mjs +0 -106
  443. package/tools/ardy/prompt-limits.mjs +0 -5
  444. package/tools/ardy/run-edit-on-box.sh +0 -73
  445. package/tools/ardy/run-local.mjs +0 -233
  446. package/tools/ardy/run-on-box.sh +0 -593
  447. package/tools/ardy/run-sequence-on-box.sh +0 -177
  448. package/tools/ardy/runners/gvhmr-worker.mjs +0 -206
  449. package/tools/ardy/runners/index.mjs +0 -18
  450. package/tools/ardy/runners/local.mjs +0 -455
  451. package/tools/ardy/runners/proc.mjs +0 -125
  452. package/tools/ardy/runners/remote.mjs +0 -252
  453. package/tools/ardy/setup-local.mjs +0 -189
  454. package/tools/ardy/setup-text-encoder-on-box.sh +0 -93
  455. package/tools/ardy/setup-text-encoder.py +0 -306
  456. package/tools/ardy/smpl-cskel27.mjs +0 -178
  457. package/tools/ardy/visual-qa.mjs +0 -196
  458. package/tools/ardy/vq-car.mjs +0 -52
  459. package/tools/bench/EXP3.md +0 -61
  460. package/tools/bench/cclay_bench_extract_incam.py +0 -125
  461. package/tools/bench/cclay_bench_extract_obs.py +0 -204
  462. package/tools/bench/cclay_bench_runner.py +0 -42
  463. package/tools/bench/cube-contact.mjs +0 -162
  464. package/tools/bench/exp3.mjs +0 -152
  465. package/tools/bench/extract-bench-lib.mjs +0 -216
  466. package/tools/bench/extract-bench.mjs +0 -302
  467. package/tools/bench/fal-generate.mjs +0 -88
  468. package/tools/bench/fit/README.md +0 -189
  469. package/tools/bench/fit/camera.mjs +0 -59
  470. package/tools/bench/fit/contact.mjs +0 -419
  471. package/tools/bench/fit/footlock.mjs +0 -234
  472. package/tools/bench/fit/motion.mjs +0 -85
  473. package/tools/bench/fit/pin.mjs +0 -27
  474. package/tools/bench/fit/remote.mjs +0 -105
  475. package/tools/bench/fit-bench.mjs +0 -126
  476. package/tools/bench/fit-sanity.mjs +0 -45
  477. package/tools/bench/metrics.mjs +0 -313
  478. package/tools/bench/obs/depth.mjs +0 -258
  479. package/tools/bench/obs/extrinsics.mjs +0 -175
  480. package/tools/bench/obs/fit_mannequin_betas.py +0 -340
  481. package/tools/bench/obs/ground.mjs +0 -294
  482. package/tools/bench/obs/heading.mjs +0 -151
  483. package/tools/bench/obs/ladder.mjs +0 -555
  484. package/tools/bench/obs/mannequin-betas.json +0 -124
  485. package/tools/bench/obs/remote.mjs +0 -83
  486. package/tools/bench/obs/rest_joints.py +0 -52
  487. package/tools/bench/obs/ybot-targets.mjs +0 -49
  488. package/tools/bench/obs-bench.mjs +0 -520
  489. package/tools/bench/score.mjs +0 -343
  490. package/tools/bench/summarize.mjs +0 -86
  491. package/tools/demo-worker/README.md +0 -137
  492. package/tools/demo-worker/api-client.mjs +0 -233
  493. package/tools/demo-worker/generate.mjs +0 -301
  494. package/tools/demo-worker/index.mjs +0 -466
  495. package/tools/demo-worker/sign-core.mjs +0 -112
  496. package/tools/demo-worker/sign.mjs +0 -161
  497. package/tools/gt-render/browser.mjs +0 -182
  498. package/tools/gt-render/camera-math.mjs +0 -276
  499. package/tools/gt-render/page.mjs +0 -267
  500. package/tools/gt-render/render.mjs +0 -514
  501. package/tools/gt-render/scene-box.mjs +0 -31
  502. package/tools/gt-render/take-transform.mjs +0 -87
  503. package/tools/kimodo/compare-seam.mjs +0 -108
  504. package/tools/kimodo/constraints.mjs +0 -244
  505. package/tools/kimodo/edit.mjs +0 -259
  506. package/tools/kimodo/effector-constraints.mjs +0 -200
  507. package/tools/kimodo/generate.mjs +0 -646
  508. package/tools/kimodo/local-output.mjs +0 -265
  509. package/tools/kimodo/measure-pose.mjs +0 -65
  510. package/tools/kimodo/measure-preserve.mjs +0 -899
  511. package/tools/kimodo/measure-waypoints.mjs +0 -55
  512. package/tools/kimodo/mlx-generate.py +0 -111
  513. package/tools/kimodo/pose-constraints.mjs +0 -248
  514. package/tools/kimodo/preserve-mask.mjs +0 -543
  515. package/tools/kimodo/read-npz.mjs +0 -189
  516. package/tools/kimodo/run-edit-on-box.mjs +0 -155
  517. package/tools/kimodo/run-sequence-on-box.mjs +0 -178
  518. package/tools/kimodo/runner.mjs +0 -284
  519. package/tools/kimodo/seam-batch.mjs +0 -77
  520. package/tools/kimodo/setup-local.mjs +0 -30
  521. package/tools/kimodo/setup-on-box.sh +0 -339
  522. package/tools/kimodo/soma77-to-cskel27.mjs +0 -305
  523. package/tools/morphgs/assets/gen_truth.py +0 -39
  524. package/tools/morphgs/assets/mesh_ori_rig.txt +0 -27
  525. package/tools/morphgs/assets/playback-check.mjs +0 -30
  526. package/tools/morphgs/demo-gate.sh +0 -26
  527. package/tools/morphgs/fbx2morphgs.mjs +0 -68
  528. package/tools/morphgs/morphgs-to-cskel27.mjs +0 -105
  529. package/tools/morphgs/patches/preprocess_src-none-mode.patch +0 -76
  530. package/tools/morphgs/setup-on-cluster.sh +0 -126
  531. package/tools/projflow/driver.py +0 -1069
  532. package/tools/projflow/generate.mjs +0 -805
  533. package/tools/projflow/hml22-to-cskel27.mjs +0 -735
  534. package/tools/projflow/line-edit-job.mjs +0 -547
  535. package/tools/projflow/replay.mjs +0 -452
  536. package/tools/projflow/runner.mjs +0 -269
  537. package/tools/projflow/service.mjs +0 -742
  538. package/tools/track/DESIGN.md +0 -513
  539. package/tools/track/backfill-provenance.mjs +0 -116
  540. package/tools/track/budget.mjs +0 -8
  541. package/tools/track/check-rig.mjs +0 -209
  542. package/tools/track/diagnostics.schema.json +0 -60
  543. package/tools/track/export-rig.mjs +0 -256
  544. package/tools/track/fallback.mjs +0 -7
  545. package/tools/track/fk-parity-fixture.mjs +0 -161
  546. package/tools/track/gate.mjs +0 -349
  547. package/tools/track/masks.mjs +0 -318
  548. package/tools/track/metrics.mjs +0 -201
  549. package/tools/track/publish-obs.mjs +0 -108
  550. package/tools/track/py/check_env.py +0 -61
  551. package/tools/track/py/eval_lr.py +0 -229
  552. package/tools/track/py/lr_viterbi.py +0 -294
  553. package/tools/track/py/masks.py +0 -405
  554. package/tools/track/py/objective.py +0 -382
  555. package/tools/track/py/rig.py +0 -508
  556. package/tools/track/py/scene.py +0 -326
  557. package/tools/track/py/test_joint_indices.py +0 -123
  558. package/tools/track/py/test_lr_viterbi.py +0 -195
  559. package/tools/track/py/test_masks.py +0 -229
  560. package/tools/track/py/test_rig.py +0 -288
  561. package/tools/track/py/test_scene.py +0 -354
  562. package/tools/track/py/test_track.py +0 -328
  563. package/tools/track/py/track.py +0 -411
  564. package/tools/track/remote.mjs +0 -140
  565. package/tools/track/rig-dump.mjs +0 -227
  566. package/tools/track/run-box-tests.mjs +0 -14
  567. package/tools/track/run-box.mjs +0 -168
  568. package/tools/track/setup-box.sh +0 -59
  569. package/tools/track/study-2d.mjs +0 -728
  570. /package/src/{ardy → motion}/collision-blockers.js +0 -0
  571. /package/src/{ardy → motion}/ground.js +0 -0
  572. /package/src/{ardy → motion}/ik-drag.js +0 -0
  573. /package/src/{ardy → motion}/ik-key-json.js +0 -0
  574. /package/src/{ardy → motion}/ik.js +0 -0
  575. /package/src/{ardy → motion}/physics-surface.js +0 -0
  576. /package/src/{ardy → motion}/playback-clock.js +0 -0
  577. /package/src/{ardy → motion}/range-pin.js +0 -0
@@ -1,728 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * study-2d.mjs - Gate 0 of the known-character tracker (#500, plan todo 4).
4
- * Measures, on existing data only, what the tracker has to fix and what it
5
- * can reach:
6
- *
7
- * (a) stance slide: truth itself and run-492f Gbest, tools/track/metrics.mjs
8
- * truthStanceSlideCmPerS (+ pelvis steps > 20 deg/frame)
9
- * (b) IoU ceiling: every truth motion scored as the prediction through
10
- * tools/bench/score.mjs (the same scorer and field the gate uses)
11
- * (c) camera fixedness: per fal clip, the median background-pixel shift vs
12
- * frame 0 over textured border-band tiles (10 % of the width; SSD block
13
- * matching + Lucas-Kanade refinement), plus static truth renders as controls
14
- * (d) 2D evidence: truth joints as the video shows them (the render's
15
- * joints.json, verified to be the render of item.source) projected with
16
- * camera.json vs the obs kp2d: per-joint pixel error split by box
17
- * occlusion, L/R swap frames per bilateral group, and deltaPx
18
- * (e) grey (skin) truth set: approved-skin.json and the provenance of its
19
- * published observations
20
- *
21
- * node tools/track/study-2d.mjs [--stages 2d,stance,ceiling,camera,skin] [options]
22
- *
23
- * Each stage writes <out>/stages/<stage>.json keyed by `inputKey`, the sha256
24
- * of everything it read (item list, input file hashes, obs root / baseline,
25
- * and the analysis code). A stage named in --stages is always recomputed; any
26
- * other stage is reused only when its stored inputKey equals the current one,
27
- * recomputed when its inputs changed, and left out when it never ran.
28
- * summary.json/summary.md merge the stages. Any item that is not ok (missing
29
- * obs, failed scoring, bad provenance, ...) is listed and the exit code is 1.
30
- * Truth is read here for measurement only; nothing here feeds a tracker.
31
- */
32
- import { spawnSync } from "node:child_process";
33
- import { createHash } from "node:crypto";
34
- import { closeSync, existsSync, mkdirSync, openSync, readFileSync, writeFileSync } from "node:fs";
35
- import { dirname, join, resolve } from "node:path";
36
- import { fileURLToPath } from "node:url";
37
- import { readNpz } from "../kimodo/read-npz.mjs";
38
- import { cameraFromJson, worldToPixel } from "../bench/obs/extrinsics.mjs";
39
- import { pelvisStepsDeg, resampleMotion, truthStanceSlideCmPerS } from "./metrics.mjs";
40
-
41
- const HERE = dirname(fileURLToPath(import.meta.url));
42
- const ROOT = resolve(HERE, "../..");
43
- const STAGES = ["2d", "stance", "ceiling", "camera", "skin"];
44
-
45
- /** COCO-17 keypoint names (ViTPose order). */
46
- export const COCO_NAMES = ["nose", "leftEye", "rightEye", "leftEar", "rightEar", "leftShoulder", "rightShoulder", "leftElbow", "rightElbow", "leftWrist", "rightWrist", "leftHip", "rightHip", "leftKnee", "rightKnee", "leftAnkle", "rightAnkle"];
47
- /** COCO body keypoint -> rendered rig joint (joints.json name). Face keypoints have no rig joint and are not scored. */
48
- export const COCO_TO_RIG = { 5: "LeftArm", 6: "RightArm", 7: "LeftForeArm", 8: "RightForeArm", 9: "LeftHand", 10: "RightHand", 11: "LeftUpLeg", 12: "RightUpLeg", 13: "LeftLeg", 14: "RightLeg", 15: "LeftFoot", 16: "RightFoot" };
49
- export const BODY_KEYPOINTS = Object.keys(COCO_TO_RIG).map(Number);
50
- /** Bilateral groups as [left, right] COCO pairs (the Viterbi's arms/legs states). */
51
- export const BILATERAL = { arms: [[5, 6], [7, 8], [9, 10]], legs: [[11, 12], [13, 14], [15, 16]] };
52
- /** A group is swapped when the swapped assignment beats identity by more than this fraction of the torso height. */
53
- export const SWAP_MARGIN = 0.2;
54
- /** Keypoints below this ViTPose confidence do not vote in the swap classifier. */
55
- export const SWAP_CONF_MIN = 0.3;
56
- /** A pair vote needs this many confident L/R pairs in the group. */
57
- export const SWAP_MIN_PAIRS = 2;
58
-
59
- const sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex");
60
- const fileSha = (path) => sha256(readFileSync(path));
61
- /** sha256 of a file, or "missing" (a missing input is part of the key too). */
62
- const shaOrMissing = (path) => (path && existsSync(path) ? fileSha(path) : "missing");
63
- /** The analysis code every stage depends on, plus stage-specific repo files. */
64
- const CODE = ["tools/track/study-2d.mjs", "tools/track/metrics.mjs", "tools/bench/obs/extrinsics.mjs", "tools/kimodo/read-npz.mjs"];
65
- export const codeSha = (extra = []) => sha256([...CODE, ...extra].map((p) => `${p}:${fileSha(join(ROOT, p))}`).join("\n"));
66
- /** The cache key of a stage: sha256 of its JSON-described inputs. */
67
- export const inputKey = (inputs) => sha256(JSON.stringify(inputs));
68
- const readJson = (path) => JSON.parse(readFileSync(path, "utf8"));
69
- const putJson = (path, value) => { mkdirSync(dirname(path), { recursive: true }); writeFileSync(path, `${JSON.stringify(value, null, "\t")}\n`); };
70
- const round = (v, d = 3) => (v === null || v === undefined || !Number.isFinite(v) ? v ?? null : Math.round(v * 10 ** d) / 10 ** d);
71
- const mean = (xs) => (xs.length ? xs.reduce((s, v) => s + v, 0) / xs.length : null);
72
-
73
- /** numpy-style linear-interpolated percentile of an unsorted list (null when empty). */
74
- export function percentile(values, p) {
75
- if (!values.length) return null;
76
- const s = Float64Array.from(values).sort();
77
- const x = (p / 100) * (s.length - 1), lo = Math.floor(x), hi = Math.ceil(x);
78
- return s[lo] + (s[hi] - s[lo]) * (x - lo);
79
- }
80
- const stats = (xs) => ({ n: xs.length, medianPx: round(percentile(xs, 50), 2), p95Px: round(percentile(xs, 95), 2), meanPx: round(mean(xs), 2) });
81
-
82
- /** Frame numbers -> "a-b" runs. */
83
- export function frameRuns(frames) {
84
- const runs = [];
85
- for (const f of [...frames].sort((a, b) => a - b)) {
86
- const last = runs.at(-1);
87
- if (last && f === last[1] + 1) last[1] = f; else runs.push([f, f]);
88
- }
89
- return runs.map(([a, b]) => (a === b ? `${a}` : `${a}-${b}`));
90
- }
91
-
92
- // ---------------------------------------------------------------- the scene
93
-
94
- /** scene.json (or a bench box) -> oriented box { centre, half, yaw (rad) }. */
95
- export function normalizeBox(box) {
96
- if (Array.isArray(box?.min) && Array.isArray(box?.max) && !box.halfExtents) {
97
- return { centre: box.min.map((v, i) => (v + box.max[i]) / 2), half: box.min.map((v, i) => (box.max[i] - v) / 2), yaw: 0 };
98
- }
99
- const centre = box?.centre ?? box?.center, half = box?.halfExtents;
100
- if (!Array.isArray(centre) || !Array.isArray(half)) throw new Error("box needs {min,max} or {centre, halfExtents, yawDeg}");
101
- const yaw = Number.isFinite(box.yawDeg) ? (box.yawDeg * Math.PI) / 180 : Number(box.yaw ?? 0);
102
- return { centre, half, yaw };
103
- }
104
-
105
- /**
106
- * True when the open segment camera -> point passes through the box, i.e. the
107
- * point is hidden by it (a point inside the box counts as hidden). The box
108
- * yaw is about +Y (Three's rotation.y).
109
- */
110
- export function segmentHitsBox(origin, point, rawBox) {
111
- const { centre, half, yaw } = normalizeBox(rawBox);
112
- const c = Math.cos(yaw), s = Math.sin(yaw);
113
- const local = (p) => { const x = p[0] - centre[0], y = p[1] - centre[1], z = p[2] - centre[2]; return [c * x - s * z, y, s * x + c * z]; };
114
- const o = local(origin), q = local(point);
115
- let enter = -Infinity, exit = Infinity;
116
- for (let a = 0; a < 3; a += 1) {
117
- const d = q[a] - o[a];
118
- if (Math.abs(d) < 1e-12) {
119
- if (Math.abs(o[a]) > half[a]) return false;
120
- continue;
121
- }
122
- const t1 = (-half[a] - o[a]) / d, t2 = (half[a] - o[a]) / d;
123
- enter = Math.max(enter, Math.min(t1, t2));
124
- exit = Math.min(exit, Math.max(t1, t2));
125
- }
126
- return enter <= exit && exit > 1e-9 && enter < 1 - 1e-9;
127
- }
128
-
129
- export const jointOccluded = (cameraCentre, point, boxes) => boxes.some((box) => segmentHitsBox(cameraCentre, point, box));
130
-
131
- // ------------------------------------------------------------ the 2D study
132
-
133
- const dist = (a, b) => Math.hypot(a[0] - b[0], a[1] - b[1]);
134
-
135
- /**
136
- * L/R swap test for one bilateral group in one frame. kp: COCO-17 [x, y, conf];
137
- * truthPx: COCO index -> [u, v]. Returns { swapped: true|false|null (too few
138
- * confident pairs), identityPx, swappedPx, pairs }.
139
- */
140
- export function classifySwap(kp, truthPx, pairs, torsoPx, { confMin = SWAP_CONF_MIN, margin = SWAP_MARGIN, minPairs = SWAP_MIN_PAIRS } = {}) {
141
- let identity = 0, swapped = 0, used = 0;
142
- for (const [l, r] of pairs) {
143
- if (!(kp[l][2] >= confMin && kp[r][2] >= confMin)) continue;
144
- identity += dist(kp[l], truthPx[l]) + dist(kp[r], truthPx[r]);
145
- swapped += dist(kp[l], truthPx[r]) + dist(kp[r], truthPx[l]);
146
- used += 1;
147
- }
148
- if (used < minPairs) return { swapped: null, identityPx: null, swappedPx: null, pairs: used };
149
- identity /= 2 * used; swapped /= 2 * used;
150
- return { swapped: identity - swapped > margin * torsoPx, identityPx: identity, swappedPx: swapped, pairs: used };
151
- }
152
-
153
- /**
154
- * The pure 2D analysis of one clip. kp2d: frames x 17 x 3 (flat); truthWorld:
155
- * COCO index -> [T][3] world points; cam: cameraFromJson(camera.json); boxes:
156
- * scene boxes. Returns per-joint visible/occluded errors, swap frames and the
157
- * raw error lists (visibleBest = error under the per-frame truth-chosen L/R
158
- * assignment, the Viterbi's noise scale).
159
- */
160
- export function analyzeClip({ kp2d, frames, truthWorld, cam, boxes = [] }) {
161
- const centre = cam.t_c2w;
162
- const perJoint = Object.fromEntries(BODY_KEYPOINTS.map((k) => [COCO_NAMES[k], { visible: [], occluded: [] }]));
163
- const visibleBest = [], occludedBest = [], visibleIdentity = [], occludedIdentity = [];
164
- const swaps = { arms: [], legs: [] }, scored = { arms: 0, legs: 0 };
165
- const occludedPairs = [];
166
- for (let t = 0; t < frames; t += 1) {
167
- const kp = Array.from({ length: 17 }, (_, k) => [kp2d[(t * 17 + k) * 3], kp2d[(t * 17 + k) * 3 + 1], kp2d[(t * 17 + k) * 3 + 2]]);
168
- const px = {}, hidden = {};
169
- for (const k of BODY_KEYPOINTS) {
170
- px[k] = worldToPixel(truthWorld[k][t], cam);
171
- hidden[k] = jointOccluded(centre, truthWorld[k][t], boxes);
172
- if (hidden[k]) occludedPairs.push([t, COCO_NAMES[k]]);
173
- }
174
- const mid = (a, b) => [(px[a][0] + px[b][0]) / 2, (px[a][1] + px[b][1]) / 2];
175
- const torso = dist(mid(5, 6), mid(11, 12));
176
- const chosen = {};
177
- for (const [group, pairs] of Object.entries(BILATERAL)) {
178
- const verdict = classifySwap(kp, px, pairs, torso);
179
- if (verdict.swapped !== null) scored[group] += 1;
180
- if (verdict.swapped) swaps[group].push(t);
181
- for (const [l, r] of pairs) { chosen[l] = verdict.swapped ? r : l; chosen[r] = verdict.swapped ? l : r; }
182
- }
183
- for (const k of BODY_KEYPOINTS) {
184
- const identity = dist(kp[k], px[k]), best = dist(kp[k], px[chosen[k]]);
185
- perJoint[COCO_NAMES[k]][hidden[k] ? "occluded" : "visible"].push(identity);
186
- (hidden[k] ? occludedBest : visibleBest).push(best);
187
- (hidden[k] ? occludedIdentity : visibleIdentity).push(identity);
188
- }
189
- }
190
- const both = swaps.arms.filter((t) => swaps.legs.includes(t));
191
- const any = [...new Set([...swaps.arms, ...swaps.legs])].sort((a, b) => a - b);
192
- return {
193
- frames,
194
- perJoint: Object.fromEntries(Object.entries(perJoint).map(([name, v]) => [name, { visible: stats(v.visible), occluded: stats(v.occluded) }])),
195
- visible: stats(visibleIdentity), occluded: stats(occludedIdentity), visibleBest: stats(visibleBest), occludedBest: stats(occludedBest),
196
- swaps: {
197
- counts: { arms: swaps.arms.length, legs: swaps.legs.length, both: both.length, any: any.length },
198
- scoredFrames: scored,
199
- runs: { arms: frameRuns(swaps.arms), legs: frameRuns(swaps.legs) },
200
- frames: { arms: swaps.arms, legs: swaps.legs },
201
- },
202
- occludedJointFrames: occludedPairs.length,
203
- raw: { visibleBest, visibleIdentity },
204
- };
205
- }
206
-
207
- /** Load one truth item and its obs and run analyzeClip. Missing obs -> status "missing-obs" (never throws for it). */
208
- export function studyItem(item, obsRoot) {
209
- const label = `${item.set}/${item.name}`;
210
- const obsPath = join(obsRoot, item.set, item.name, "g5", "obs.npz");
211
- if (!existsSync(obsPath)) return { item: label, variant: item.variant, status: "missing-obs", obsPath };
212
- const files = join(item.dir, item.variant);
213
- const camera = readJson(join(files, "camera.json")), joints = readJson(join(files, "joints.json")), meta = readJson(join(files, "meta.json"));
214
- if (meta.source?.path !== item.source || meta.source?.sha256 !== fileSha(item.source)) throw new Error(`${label}: ${files} is not the render of ${item.source}`);
215
- const cam = cameraFromJson(camera);
216
- const index = Object.fromEntries(joints.joints.map((j, i) => [j.name, i]));
217
- const truthWorld = Object.fromEntries(BODY_KEYPOINTS.map((k) => {
218
- const i = index[COCO_TO_RIG[k]];
219
- if (i === undefined) throw new Error(`${label}: joints.json has no ${COCO_TO_RIG[k]}`);
220
- return [k, joints.world.map((frame) => frame[i])];
221
- }));
222
- // The projection must be the render's own (joints.json uv).
223
- let projectionCheckPx = 0;
224
- for (let t = 0; t < joints.frames; t += 1) for (const k of BODY_KEYPOINTS) projectionCheckPx = Math.max(projectionCheckPx, dist(worldToPixel(truthWorld[k][t], cam), joints.uv[t][index[COCO_TO_RIG[k]]]));
225
- if (!(projectionCheckPx < 0.01)) throw new Error(`${label}: camera.json projection disagrees with joints.json uv by ${projectionCheckPx} px`);
226
- const obs = readNpz(obsPath);
227
- const [obsFrames] = obs.kp2d.shape;
228
- const Kgap = Math.max(...camera.K.flat().map((v, i) => Math.abs(v - obs.K.data[i])));
229
- const boxes = item.scene ? [readJson(item.scene)] : [];
230
- const frames = Math.min(obsFrames, joints.frames);
231
- const result = analyzeClip({ kp2d: obs.kp2d.data, frames, truthWorld, cam, boxes });
232
- return { item: label, variant: item.variant, status: "ok", obsPath, obsSha256: fileSha(obsPath), obsFrames, truthFrames: joints.frames, obsKVsCameraMax: Kgap, projectionCheckPx, boxes: boxes.length, ...result };
233
- }
234
-
235
- // -------------------------------------------------------------- (a) stance
236
-
237
- function motionOf(path) {
238
- const npz = readNpz(path);
239
- return { frames: npz.posed_joints.shape[0], fps: npz.fps.data[0], posedJoints: npz.posed_joints.data, rotMats: npz.local_rot_mats.data };
240
- }
241
-
242
- export function stanceItem(item, baselineRun) {
243
- const label = `${item.set}/${item.name}`;
244
- const truth = motionOf(item.source);
245
- const gbestPath = join(baselineRun, item.set, item.name, "Gbest", "motion.npz");
246
- if (!existsSync(gbestPath)) return { item: label, status: "missing-baseline", gbestPath };
247
- const gbest = motionOf(gbestPath);
248
- // The truth is measured as a prediction on the same timeline as Gbest (its fps and frame count).
249
- const t = truthStanceSlideCmPerS(resampleMotion(truth, gbest.fps, gbest.frames), truth), g = truthStanceSlideCmPerS(gbest, truth);
250
- const steps = (m) => { const s = pelvisStepsDeg(m.rotMats, m.frames); return { over20: s.filter((v) => v > 20).length, maxDeg: round(Math.max(...s), 2) }; };
251
- return {
252
- item: label, status: "ok", gbestPath, gbestSha256: fileSha(gbestPath),
253
- truth: { cmPerS: round(t.meanCmPerS), stanceSamples: t.stanceSamples, fps: truth.fps, frames: truth.frames, pelvisSteps: steps(truth) },
254
- gbest: { cmPerS: round(g.meanCmPerS), stanceSamples: g.stanceSamples, fps: gbest.fps, frames: gbest.frames, pelvisSteps: steps(gbest) },
255
- rule: g.rule,
256
- };
257
- }
258
-
259
- // ------------------------------------------------------------ (b) ceiling
260
-
261
- /** Score the truth motion as the prediction with score.mjs exactly as obs-bench does for gt/cube items. */
262
- /** What a truth-as-prediction score depends on (score.json is reused only while this is unchanged). */
263
- export function ceilingInputs(item) {
264
- return {
265
- item, source: shaOrMissing(item.source), scene: shaOrMissing(item.scene),
266
- gt: ["camera.json", "joints.json"].map((f) => shaOrMissing(join(item.dir, item.variant, f))),
267
- scorer: ["tools/bench/score.mjs", "tools/gt-render/render.mjs"].map((p) => shaOrMissing(join(ROOT, p))),
268
- };
269
- }
270
-
271
- export function ceilingItem(item, outDir, { port, cdpPort, force }) {
272
- const label = `${item.set}/${item.name}`, out = join(outDir, "gt-self", item.set, item.name);
273
- const scorePath = join(out, "score.json"), keyPath = join(out, "inputs.json"), key = inputKey(ceilingInputs(item));
274
- let reused = true;
275
- if (force || !existsSync(scorePath) || !existsSync(keyPath) || readJson(keyPath).inputKey !== key) {
276
- reused = false;
277
- mkdirSync(out, { recursive: true });
278
- const args = [join(ROOT, "tools/bench/score.mjs"), "--gt", join(item.dir, item.variant), "--pred", item.source, "--out", out, "--port", String(port), "--cdp-port", String(cdpPort)];
279
- if (item.scene) { const s = readJson(item.scene); args.push("--box", JSON.stringify({ min: s.min, max: s.max }), "--gt-npz", item.source); }
280
- const fd = openSync(join(out, "score.log"), "a");
281
- try {
282
- const run = spawnSync(process.execPath, args, { cwd: ROOT, stdio: ["ignore", fd, fd], timeout: 20 * 60000 });
283
- if (run.status !== 0) return { item: label, status: "score-failed", code: run.status ?? run.signal, log: join(out, "score.log") };
284
- } finally { closeSync(fd); }
285
- putJson(keyPath, { inputKey: key, inputs: ceilingInputs(item) });
286
- }
287
- const s = readJson(scorePath);
288
- return {
289
- item: label, variant: item.variant, status: "ok", reused, score: scorePath,
290
- maskIoURawMean: s.overlap.maskIoURawMean, maskIoUAlignedMean: s.overlap.maskIoUAlignedMean,
291
- paMpjpeM: s.pose.paMpjpeM, rootErrorRawRmseM: s.trajectory.rootErrorRawM.rmse, ateAlignedRmseM: s.trajectory.ateAlignedM.rmse,
292
- };
293
- }
294
-
295
- // -------------------------------------------------------------- (c) camera
296
-
297
- const inBand = (x, y, w, h, b) => x < b || y < b || x >= w - b || y >= h - b;
298
-
299
- function halfRes(img, w, h) {
300
- const W = w >> 1, H = h >> 1, out = new Float32Array(W * H);
301
- for (let y = 0; y < H; y += 1) for (let x = 0; x < W; x += 1) out[y * W + x] = (img[2 * y * w + 2 * x] + img[2 * y * w + 2 * x + 1] + img[(2 * y + 1) * w + 2 * x] + img[(2 * y + 1) * w + 2 * x + 1]) / 4;
302
- return out;
303
- }
304
-
305
- /**
306
- * Textured tiles of the border band of the reference frame: tile x tile
307
- * blocks lying entirely within bandFrac x width of an image edge whose
308
- * structure tensor's smaller eigenvalue (per pixel, grey levels^2) is at least
309
- * minTexture, so flat sky/floor and single edges (aperture problem) do not vote.
310
- */
311
- export function bandTiles(ref, w, h, { bandFrac = 0.1, tile = 32, margin = 14, minTexture = 2 } = {}) {
312
- const band = Math.round(bandFrac * w), tiles = [];
313
- for (let y = margin; y + tile <= h - margin; y += tile) for (let x = margin; x + tile <= w - margin; x += tile) {
314
- const corners = [[x, y], [x + tile - 1, y], [x, y + tile - 1], [x + tile - 1, y + tile - 1]];
315
- if (!corners.every(([cx, cy]) => inBand(cx, cy, w, h, band))) continue;
316
- let sxx = 0, syy = 0, sxy = 0;
317
- for (let yy = y; yy < y + tile; yy += 1) for (let xx = x; xx < x + tile; xx += 1) {
318
- const gx = (ref[yy * w + xx + 1] - ref[yy * w + xx - 1]) / 2, gy = (ref[(yy + 1) * w + xx] - ref[(yy - 1) * w + xx]) / 2;
319
- sxx += gx * gx; syy += gy * gy; sxy += gx * gy;
320
- }
321
- const n = tile * tile, a = sxx / n, c = syy / n, b = sxy / n;
322
- const minEig = (a + c) / 2 - Math.sqrt(((a - c) / 2) ** 2 + b * b);
323
- if (minEig >= minTexture) tiles.push({ x, y, minEig });
324
- }
325
- return tiles;
326
- }
327
-
328
- function ssd(ref, cur, w, x, y, size, dx, dy) {
329
- let s = 0;
330
- for (let yy = y; yy < y + size; yy += 1) {
331
- const r = yy * w, c = (yy + dy) * w + dx;
332
- for (let xx = x; xx < x + size; xx += 1) { const d = ref[r + xx] - cur[c + xx]; s += d * d; }
333
- }
334
- return s;
335
- }
336
-
337
- const parabola = (m, c0, p) => { const den = m - 2 * c0 + p; return den > 0 ? (0.5 * (m - p)) / den : 0; };
338
-
339
- function bilinear(img, w, x, y) {
340
- const x0 = Math.floor(x), y0 = Math.floor(y), fx = x - x0, fy = y - y0, i = y0 * w + x0;
341
- return img[i] * (1 - fx) * (1 - fy) + img[i + 1] * fx * (1 - fy) + img[i + w] * (1 - fx) * fy + img[i + w + 1] * fx * fy;
342
- }
343
-
344
- /** Lucas-Kanade translation refinement of one tile from (dx, dy); the parabola alone is biased up to ~0.25 px. */
345
- function refineLk(ref, cur, w, h, x, y, size, dx, dy, iterations = 6) {
346
- for (let it = 0; it < iterations; it += 1) {
347
- if (x + dx < 2 || y + dy < 2 || x + dx + size > w - 3 || y + dy + size > h - 3) break;
348
- let a = 0, b = 0, c = 0, gx0 = 0, gy0 = 0;
349
- for (let yy = y; yy < y + size; yy += 1) for (let xx = x; xx < x + size; xx += 1) {
350
- const u = xx + dx, v = yy + dy;
351
- const gx = (bilinear(cur, w, u + 1, v) - bilinear(cur, w, u - 1, v)) / 2, gy = (bilinear(cur, w, u, v + 1) - bilinear(cur, w, u, v - 1)) / 2;
352
- const r = bilinear(cur, w, u, v) - ref[yy * w + xx];
353
- a += gx * gx; b += gx * gy; c += gy * gy; gx0 += gx * r; gy0 += gy * r;
354
- }
355
- const det = a * c - b * b;
356
- if (!(det > 1e-9)) break;
357
- const sx = -(c * gx0 - b * gy0) / det, sy = -(a * gy0 - b * gx0) / det;
358
- if (!(Math.abs(sx) < 1.5 && Math.abs(sy) < 1.5)) break;
359
- dx += sx; dy += sy;
360
- if (Math.hypot(sx, sy) < 1e-3) break;
361
- }
362
- return [dx, dy];
363
- }
364
-
365
- /**
366
- * Translation of every tile between ref and cur (SSD block matching: +-radius
367
- * px on the half-resolution frames, then +-2 px at full resolution with a
368
- * parabolic sub-pixel fit). Returns [{ x, y, dx, dy, shift, clipped }] where
369
- * clipped = the best match sits on the search bound (a shift >= radius).
370
- */
371
- export function tileShifts(ref, cur, w, h, tiles, { tile = 32, radius = 12, refHalf = null, curHalf = null } = {}) {
372
- const rh = refHalf ?? halfRes(ref, w, h), ch = curHalf ?? halfRes(cur, w, h), hw = w >> 1, hh = h >> 1, R = Math.ceil(radius / 2), ht = tile >> 1;
373
- return tiles.map(({ x, y }) => {
374
- const hx = x >> 1, hy = y >> 1;
375
- let best = Infinity, bx = 0, by = 0;
376
- for (let dy = -R; dy <= R; dy += 1) for (let dx = -R; dx <= R; dx += 1) {
377
- if (hx + dx < 0 || hy + dy < 0 || hx + dx + ht > hw || hy + dy + ht > hh) continue;
378
- const s = ssd(rh, ch, hw, hx, hy, ht, dx, dy);
379
- if (s < best) { best = s; bx = dx; by = dy; }
380
- }
381
- let fbest = Infinity, fx = 2 * bx, fy = 2 * by;
382
- const cands = [];
383
- for (let dy = 2 * by - 2; dy <= 2 * by + 2; dy += 1) for (let dx = 2 * bx - 2; dx <= 2 * bx + 2; dx += 1) {
384
- if (x + dx < 0 || y + dy < 0 || x + dx + tile > w || y + dy + tile > h) continue;
385
- const s = ssd(ref, cur, w, x, y, tile, dx, dy);
386
- cands.push([dx, dy, s]);
387
- if (s < fbest) { fbest = s; fx = dx; fy = dy; }
388
- }
389
- const at = (dx, dy) => cands.find((c) => c[0] === dx && c[1] === dy)?.[2];
390
- const sx = [at(fx - 1, fy), at(fx + 1, fy)], sy = [at(fx, fy - 1), at(fx, fy + 1)];
391
- const subX = sx.every(Number.isFinite) ? parabola(sx[0], fbest, sx[1]) : 0, subY = sy.every(Number.isFinite) ? parabola(sy[0], fbest, sy[1]) : 0;
392
- const [dx, dy] = refineLk(ref, cur, w, h, x, y, tile, fx + subX, fy + subY);
393
- return { x, y, dx, dy, shift: Math.hypot(dx, dy), clipped: Math.abs(bx) === R || Math.abs(by) === R };
394
- });
395
- }
396
-
397
- function decodeGrey(video, width, height) {
398
- const run = spawnSync("ffmpeg", ["-v", "error", "-i", video, "-vf", `scale=${width}:${height}`, "-f", "rawvideo", "-pix_fmt", "gray", "pipe:1"], { maxBuffer: 2 ** 31 });
399
- if (run.status !== 0) throw new Error(`ffmpeg ${video}: ${run.stderr}`);
400
- const size = width * height, frames = run.stdout.length / size;
401
- if (!Number.isInteger(frames) || frames < 2) throw new Error(`${video}: decoded ${run.stdout.length} bytes`);
402
- return Array.from({ length: frames }, (_, i) => Float32Array.from(run.stdout.subarray(i * size, (i + 1) * size)));
403
- }
404
-
405
- /** Median background-pixel shift vs frame 0 on the border band, per frame and summarised per clip. */
406
- export function cameraProbe(label, video, width, height, log = () => {}) {
407
- const frames = decodeGrey(video, width, height), tiles = bandTiles(frames[0], width, height);
408
- if (tiles.length < 8) throw new Error(`only ${tiles.length} textured band tiles in frame 0`);
409
- const refHalf = halfRes(frames[0], width, height), perFrame = [];
410
- for (let t = 1; t < frames.length; t += 1) {
411
- const shifts = tileShifts(frames[0], frames[t], width, height, tiles, { refHalf });
412
- perFrame.push({ medianPx: percentile(shifts.map((s) => s.shift), 50), movedFraction: shifts.filter((s) => s.shift > 1).length / shifts.length, clipped: shifts.filter((s) => s.clipped).length, dx: percentile(shifts.map((s) => s.dx), 50), dy: percentile(shifts.map((s) => s.dy), 50) });
413
- if (t % 40 === 0) log(`${label}: frame ${t}/${frames.length - 1}`);
414
- }
415
- const med = perFrame.map((f) => f.medianPx), last = perFrame.at(-1);
416
- return {
417
- clip: label, video, videoSha256: fileSha(video), frames: frames.length, tiles: tiles.length,
418
- medianShiftPx: round(percentile(med, 50), 3), p95ShiftPx: round(percentile(med, 95), 3), maxShiftPx: round(Math.max(...med), 3),
419
- framesOver1Px: med.filter((v) => v >= 1).length, maxMovedTileFraction: round(Math.max(...perFrame.map((f) => f.movedFraction)), 3),
420
- lastFrame: { medianPx: round(last.medianPx, 3), dx: round(last.dx, 3), dy: round(last.dy, 3), movedTileFraction: round(last.movedFraction, 3) },
421
- perFrameMedianPx: med.map((v) => round(v, 3)),
422
- };
423
- }
424
-
425
- // ---------------------------------------------------------------- (e) skin
426
-
427
- const SKIN_SET = { gt: "gt-skin", cube: "cube-skin" };
428
-
429
- /** approved.json -> approved-skin.json: the truth items as variant skin under distinct set names. Deterministic. */
430
- export function skinApprovedFrom(approved, approvedPath, approvedSha256) {
431
- return {
432
- derivedFrom: { path: approvedPath, sha256: approvedSha256 },
433
- note: "Grey (skin) truth set: the approved gt/cube items with variant skin under distinct set names so the obs cache and run dirs (keyed by set/name) never reuse the shaded observations. Written by tools/track/study-2d.mjs skinApprovedFrom().",
434
- items: approved.items.filter((i) => SKIN_SET[i.set]).map((i) => ({ set: SKIN_SET[i.set], name: i.name, variant: "skin", dir: i.dir, source: i.source, scene: i.scene, scoring: i.scoring })),
435
- };
436
- }
437
-
438
- /** Provenance of one published skin obs: manifest video sha / detector, and separation from the shaded obs. */
439
- export function skinProvenance(item, obsRoot, shadedSet) {
440
- const g5 = join(obsRoot, item.set, item.name, "g5"), manifestPath = join(g5, "manifest.json"), obsPath = join(g5, "obs.npz");
441
- const label = `${item.set}/${item.name}`;
442
- if (!existsSync(obsPath) || !existsSync(manifestPath)) return { item: label, status: "missing-obs", g5 };
443
- const manifest = readJson(manifestPath), video = join(item.dir, "skin", "video.mp4"), shadedVideo = join(item.dir, "shaded", "video.mp4");
444
- const shadedObs = join(obsRoot, shadedSet, item.name, "g5", "obs.npz");
445
- const videoSha256 = fileSha(video), obsSha256 = fileSha(obsPath);
446
- const checks = {
447
- manifestVideoSha256MatchesSkinVideo: manifest.videoSha256 === videoSha256,
448
- manifestVideoIsSkin: resolve(manifest.video) === resolve(video),
449
- detectorYolo: manifest.detector === "yolo",
450
- keypointsVitpose: manifest.keypoints === "vitpose",
451
- obsShaMatchesManifest: manifest.obsSha256 === obsSha256,
452
- skinVideoDiffersFromShaded: videoSha256 !== fileSha(shadedVideo),
453
- skinObsDiffersFromShadedObs: existsSync(shadedObs) ? obsSha256 !== fileSha(shadedObs) : null,
454
- };
455
- return { item: label, status: Object.values(checks).every((v) => v !== false) ? "ok" : "bad-provenance", g5, videoSha256, obsSha256, detector: manifest.detector, checks };
456
- }
457
-
458
- /** The no-extract handoff run: every item read its mannequin obs from the obs root and G5 scored. */
459
- export function handoffCheck(item, runDir, obsRoot) {
460
- const dir = join(runDir, item.set, item.name), log = join(dir, "bench.log"), label = `${item.set}/${item.name}`;
461
- if (!existsSync(log)) return { item: label, status: "missing-log" };
462
- const text = readFileSync(log, "utf8");
463
- const manifest = existsSync(join(dir, "obs-mannequin", "manifest.json")) ? readJson(join(dir, "obs-mannequin", "manifest.json")) : null;
464
- const result = existsSync(join(dir, "G5", "result.json")) ? readJson(join(dir, "G5", "result.json")) : null;
465
- const expected = join(obsRoot, item.set, item.name, "g5", "obs.npz");
466
- const checks = {
467
- fromObsRootLine: text.includes("obs-mannequin: from obs root"),
468
- noExtraction: !text.includes("extracting on"),
469
- manifestSourceIsPublished: resolve(manifest?.source ?? "") === resolve(expected),
470
- g5Ok: result?.ok === true,
471
- g5ScoreFileParses: result?.ok ? existsSync(join(dir, "G5", "score", "score.json")) && Number.isFinite(readJson(join(dir, "G5", "score", "score.json")).overlap?.maskIoURawMean) : false,
472
- };
473
- return { item: label, status: Object.values(checks).every(Boolean) ? "ok" : "failed", checks, g5: result?.score ? { paMpjpeM: result.score.paMpjpeM, iouRaw: result.score.iouRaw, rootErrorRawRmseM: result.score.rootErrorRawRmseM } : null };
474
- }
475
-
476
- // ---------------------------------------------------------------- summary
477
-
478
- function markdown(summary) {
479
- const L = [], f = (v, d = 2) => (v === null || v === undefined ? "-" : Number(v).toFixed(d));
480
- L.push("# Gate 0: 2D evidence, headroom ceiling, camera fixedness (#500 todo 4)", "", `Generated ${summary.createdAt} by tools/track/study-2d.mjs (commit ${summary.commit}). Numbers below are copied from summary.json.`, "");
481
- const a = summary.stance;
482
- L.push("## (a) Stance slide on truth-stance frames (truthStanceSlideCmPerS)", "");
483
- if (a) {
484
- L.push(`Rule: ${a.rule}`, "", "| item | truth cm/s | Gbest cm/s | stance samples | Gbest pelvis steps >20 deg | Gbest max step deg |", "| --- | --- | --- | --- | --- | --- |");
485
- for (const r of a.items) L.push(r.status === "ok" ? `| ${r.item} | ${f(r.truth.cmPerS)} | ${f(r.gbest.cmPerS)} | ${r.gbest.stanceSamples} | ${r.gbest.pelvisSteps.over20} | ${f(r.gbest.pelvisSteps.maxDeg, 1)} |` : `| ${r.item} | ${r.status} | | | | |`);
486
- L.push(`| **mean** | **${f(a.mean.truthCmPerS)}** | **${f(a.mean.gbestCmPerS)}** | | | |`, "");
487
- }
488
- const b = summary.iouCeiling;
489
- L.push("## (b) Truth-as-prediction IoU ceiling (overlap.maskIoURawMean)", "");
490
- if (b) {
491
- L.push("| set | items | mean IoU raw | min | IS-1 truth threshold (0.9 x) |", "| --- | --- | --- | --- | --- |");
492
- for (const [set, v] of Object.entries(b.perSet)) L.push(`| ${set} | ${v.items} | ${f(v.meanIoURaw, 4)} | ${f(v.minIoURaw, 4)} | ${f(v.is1Threshold, 4)} |`);
493
- L.push("", "| item | IoU raw | IoU aligned | PA-MPJPE m | root raw m |", "| --- | --- | --- | --- | --- |");
494
- for (const r of b.items) L.push(r.status === "ok" ? `| ${r.item} | ${f(r.maskIoURawMean, 4)} | ${f(r.maskIoUAlignedMean, 4)} | ${f(r.paMpjpeM, 4)} | ${f(r.rootErrorRawRmseM, 4)} |` : `| ${r.item} | ${r.status} | | | |`);
495
- L.push("", b.skinNote, "");
496
- }
497
- const c = summary.camera;
498
- L.push("## (c) Camera fixedness (fal clips)", "");
499
- if (c) {
500
- L.push(`Method: ${c.method}`, "", `**cameraFixed: ${c.cameraFixed}** (rule: median shift < ${c.thresholdPx} px on every fal clip; worst clip ${c.worst?.clip ?? "-"} ${f(c.worst?.medianShiftPx, 3)} px)`, "");
501
- L.push("| clip | median shift px | p95 px | max px | frames >= 1 px | textured tiles | last-frame dx, dy |", "| --- | --- | --- | --- | --- | --- | --- |");
502
- for (const r of [...c.clips, ...c.controls.map((x) => ({ ...x, clip: `control ${x.clip}` }))]) L.push(r.status !== "ok" ? `| ${r.clip ?? r.item} | ${r.status}: ${r.error} | | | | |` : `| ${r.clip} | ${f(r.medianShiftPx, 3)} | ${f(r.p95ShiftPx, 3)} | ${f(r.maxShiftPx, 3)} | ${r.framesOver1Px} | ${r.tiles} | ${f(r.lastFrame.dx)}, ${f(r.lastFrame.dy)} |`);
503
- L.push("");
504
- }
505
- const d = summary.study2d;
506
- L.push("## (d) 2D evidence: truth projected vs obs kp2d", "");
507
- if (d) {
508
- L.push(`deltaPx (median visible error under the truth-chosen L/R assignment, shaded set): **${f(summary.deltaPx)} px**. ${d.deltaNote}`, "");
509
- for (const [variant, v] of Object.entries(d.perVariant)) L.push(`- ${variant}: deltaPx ${f(v.deltaPx)} px, identity-assignment visible median ${f(v.visibleIdentityMedianPx)} px, items ok ${v.itemsOk}/${v.items}`);
510
- L.push("", "| item | variant | status | frames | arm swaps | leg swaps | swap runs (arms / legs) | visible median / p95 px (identity) | occluded median / p95 px (n) |", "| --- | --- | --- | --- | --- | --- | --- | --- | --- |");
511
- for (const r of d.items) {
512
- if (r.status !== "ok") { L.push(`| ${r.item} | ${r.variant} | ${r.status} | | | | | | |`); continue; }
513
- L.push(`| ${r.item} | ${r.variant} | ok | ${r.frames} | ${r.swaps.counts.arms} | ${r.swaps.counts.legs} | ${r.swaps.runs.arms.join(",") || "-"} / ${r.swaps.runs.legs.join(",") || "-"} | ${f(r.visible.medianPx)} / ${f(r.visible.p95Px)} | ${f(r.occluded.medianPx)} / ${f(r.occluded.p95Px)} (${r.occluded.n}) |`);
514
- }
515
- L.push("", "Per-joint median px (visible | occluded), shaded set pooled:", "", "| joint | visible median | visible p95 | occluded median | occluded p95 | occluded n |", "| --- | --- | --- | --- | --- | --- |");
516
- for (const [joint, v] of Object.entries(d.pooledPerJoint.shaded ?? {})) L.push(`| ${joint} | ${f(v.visible.medianPx)} | ${f(v.visible.p95Px)} | ${f(v.occluded.medianPx)} | ${f(v.occluded.p95Px)} | ${v.occluded.n} |`);
517
- if (d.walkFlipWindow) L.push("", `gt/walk known flip window f18-51: swap frames inside it ${d.walkFlipWindow.inside}, outside ${d.walkFlipWindow.outside} (overlap: ${d.walkFlipWindow.overlaps}).`);
518
- L.push("");
519
- }
520
- const e = summary.skin;
521
- L.push("## (e) Grey (skin) truth set", "");
522
- if (e) {
523
- L.push(`approved-skin.json: ${e.approvedSkin.items} items, sets ${e.approvedSkin.sets.join(", ")}; selectItems ok: ${e.approvedSkin.selectItemsOk}; equals skinApprovedFrom(approved.json): ${e.approvedSkin.matchesGenerator}.`, "");
524
- L.push("| item | provenance | video sha256 (skin) | detector | skin obs != shaded obs | handoff |", "| --- | --- | --- | --- | --- | --- |");
525
- for (const r of e.provenance) {
526
- const h = e.handoff?.find((x) => x.item === r.item);
527
- L.push(`| ${r.item} | ${r.status} | ${r.videoSha256?.slice(0, 12) ?? "-"} | ${r.detector ?? "-"} | ${r.checks?.skinObsDiffersFromShadedObs ?? "-"} | ${h ? h.status : "not run"} |`);
528
- }
529
- L.push("");
530
- }
531
- return `${L.join("\n")}\n`;
532
- }
533
-
534
- // -------------------------------------------------------------------- main
535
-
536
- export const USAGE = `usage: node tools/track/study-2d.mjs [options]
537
-
538
- --stages 2d,stance,ceiling,camera,skin stages to recompute (default all); other stages
539
- are reused only while their inputs are unchanged, else recomputed
540
- --approved <json> shaded truth + fal items (default evidence/obs/approved.json)
541
- --approved-skin <json> grey truth items (default evidence/obs/approved-skin.json)
542
- --write-approved-skin write --approved-skin from --approved (refuses to replace a different file)
543
- --obs-root <dir> obs cache <dir>/<set>/<name>/g5/obs.npz (default evidence/obs/cache)
544
- --baseline <run> Gbest baseline run (default evidence/obs/run-492f)
545
- --handoff <run> a no-extract obs-bench run of approved-skin.json to verify
546
- --items a,b limit to these set/name items
547
- --out <dir> default evidence/obs/study-2d
548
- --port 5504 --cdp-port 9504 scorer ports for the ceiling stage
549
- --force re-score the ceiling even when score.json matches its inputs
550
-
551
- Exit code 1 when any item is not ok (missing obs, failed scoring, bad provenance, failed handoff).`;
552
-
553
- export function parseArgs(argv) {
554
- const o = { stages: STAGES, approved: "evidence/obs/approved.json", approvedSkin: "evidence/obs/approved-skin.json", obsRoot: "evidence/obs/cache", baseline: "evidence/obs/run-492f", out: "evidence/obs/study-2d", port: 5504, cdpPort: 9504, force: false, writeApprovedSkin: false };
555
- const names = { "--stages": "stages", "--approved": "approved", "--approved-skin": "approvedSkin", "--obs-root": "obsRoot", "--baseline": "baseline", "--handoff": "handoff", "--items": "items", "--out": "out", "--port": "port", "--cdp-port": "cdpPort" };
556
- for (let i = 0; i < argv.length; i += 1) {
557
- const flag = argv[i];
558
- if (flag === "--help" || flag === "-h") { o.help = true; continue; }
559
- if (flag === "--force") { o.force = true; continue; }
560
- if (flag === "--write-approved-skin") { o.writeApprovedSkin = true; continue; }
561
- if (!names[flag]) throw new Error(`unknown option ${flag}`);
562
- const value = argv[++i];
563
- if (value === undefined || value.startsWith("--")) throw new Error(`${flag} needs a value`);
564
- o[names[flag]] = value;
565
- }
566
- if (typeof o.stages === "string") o.stages = o.stages.split(",").map((s) => s.trim()).filter(Boolean);
567
- const bad = o.stages.filter((s) => !STAGES.includes(s));
568
- if (bad.length) throw new Error(`unknown stages ${bad.join(",")}; known ${STAGES.join(",")}`);
569
- if (typeof o.items === "string") o.items = o.items.split(",").map((s) => s.trim()).filter(Boolean);
570
- for (const key of ["port", "cdpPort"]) { o[key] = Number(o[key]); if (!Number.isInteger(o[key])) throw new Error(`--${key} must be an integer`); }
571
- for (const key of ["approved", "approvedSkin", "obsRoot", "baseline", "out", "handoff"]) if (o[key]) o[key] = resolve(o[key]);
572
- return o;
573
- }
574
-
575
- function commitId() {
576
- const head = spawnSync("git", ["rev-parse", "--short", "HEAD"], { cwd: ROOT, encoding: "utf8" }).stdout.trim();
577
- const dirty = spawnSync("git", ["status", "--porcelain"], { cwd: ROOT, encoding: "utf8" }).stdout.trim();
578
- return `${head}${dirty ? "-dirty" : ""}`;
579
- }
580
-
581
- export async function main(argv = process.argv.slice(2)) {
582
- const o = parseArgs(argv);
583
- if (o.help) { console.log(USAGE); return; }
584
- const approvedBytes = readFileSync(o.approved), approved = JSON.parse(approvedBytes);
585
- if (!Array.isArray(approved?.items)) throw new Error(`${o.approved}: no items array`);
586
- const generated = skinApprovedFrom(approved, o.approved, sha256(approvedBytes));
587
- if (o.writeApprovedSkin) {
588
- const text = `${JSON.stringify(generated, null, "\t")}\n`;
589
- if (existsSync(o.approvedSkin) && readFileSync(o.approvedSkin, "utf8") !== text) throw new Error(`${o.approvedSkin} exists and differs; not replacing it`);
590
- writeFileSync(o.approvedSkin, text);
591
- console.log(`study-2d: wrote ${o.approvedSkin} (${generated.items.length} items)`);
592
- }
593
- const pick = (items) => (o.items ? items.filter((i) => o.items.includes(`${i.set}/${i.name}`)) : items);
594
- const truthItems = pick(approved.items.filter((i) => i.set === "gt" || i.set === "cube"));
595
- const falItems = pick(approved.items.filter((i) => i.set === "fal"));
596
- const skinApproved = existsSync(o.approvedSkin) ? readJson(o.approvedSkin) : null;
597
- const skinItems = skinApproved ? pick(skinApproved.items) : [];
598
- const stageDir = join(o.out, "stages"), log = (line) => console.log(`[study-2d] ${line}`);
599
- /** Run, reuse or skip one stage by its input key (see the header). */
600
- const stage = async (name, inputs, fn) => {
601
- const path = join(stageDir, `${name}.json`), key = inputKey(inputs);
602
- const cached = existsSync(path) ? readJson(path) : null;
603
- if (!o.stages.includes(name)) {
604
- if (!cached) return null;
605
- if (cached.inputKey === key) { log(`stage ${name}: reused (inputs unchanged since ${cached.createdAt})`); return cached; }
606
- log(`stage ${name}: inputs changed since ${cached.createdAt}; recomputing`);
607
- } else log(`stage ${name}`);
608
- const value = { createdAt: new Date().toISOString(), commit: commitId(), inputKey: key, ...(await fn()) };
609
- putJson(path, value);
610
- return value;
611
- };
612
- const obsPathOf = (i) => join(o.obsRoot, i.set, i.name, "g5", "obs.npz");
613
- const truthFiles = (i) => ({ item: i, source: shaOrMissing(i.source), scene: shaOrMissing(i.scene), ...Object.fromEntries(["camera.json", "joints.json", "meta.json"].map((f) => [f, shaOrMissing(join(i.dir, i.variant, f))])) });
614
- const guard = (label, fn) => { try { return fn(); } catch (error) { log(`${label}: FAILED ${error.message}`); return { item: label, status: "failed", error: error.message }; } };
615
-
616
- const study2d = await stage("2d", { code: codeSha(), obsRoot: o.obsRoot, items: [...truthItems, ...skinItems].map((i) => ({ ...truthFiles(i), obs: shaOrMissing(obsPathOf(i)) })) }, () => {
617
- const items = [...truthItems, ...skinItems].map((item) => guard(`${item.set}/${item.name}`, () => { const r = studyItem(item, o.obsRoot); log(`2d ${item.set}/${item.name} (${item.variant}): ${r.status}${r.status === "ok" ? ` visible median ${r.visible.medianPx} px, swaps arms ${r.swaps.counts.arms} legs ${r.swaps.counts.legs}` : ` ${r.obsPath}`}`); return r; }));
618
- const perVariant = {}, pooledPerJoint = {};
619
- for (const variant of ["shaded", "skin"]) {
620
- const ok = items.filter((r) => r.variant === variant && r.status === "ok");
621
- const all = items.filter((r) => r.variant === variant);
622
- perVariant[variant] = { items: all.length, itemsOk: ok.length, deltaPx: round(percentile(ok.flatMap((r) => r.raw.visibleBest), 50), 3), visibleIdentityMedianPx: round(percentile(ok.flatMap((r) => r.raw.visibleIdentity), 50), 3) };
623
- pooledPerJoint[variant] = Object.fromEntries(COCO_NAMES.filter((_, k) => BODY_KEYPOINTS.includes(k)).map((name) => {
624
- // Pooled as the median over items of each item's median / p95 (items weigh equally).
625
- const vis = ok.map((r) => r.perJoint[name].visible.medianPx).filter(Number.isFinite), occ = ok.map((r) => r.perJoint[name].occluded.medianPx).filter(Number.isFinite);
626
- const visP = ok.map((r) => r.perJoint[name].visible.p95Px).filter(Number.isFinite), occP = ok.map((r) => r.perJoint[name].occluded.p95Px).filter(Number.isFinite);
627
- return [name, { visible: { medianPx: round(percentile(vis, 50), 2), p95Px: round(percentile(visP, 50), 2), items: vis.length }, occluded: { medianPx: round(percentile(occ, 50), 2), p95Px: round(percentile(occP, 50), 2), n: ok.reduce((s, r) => s + r.perJoint[name].occluded.n, 0) } }];
628
- }));
629
- }
630
- const walk = items.find((r) => r.item === "gt/walk" && r.status === "ok");
631
- const walkFrames = walk ? [...new Set([...walk.swaps.frames.arms, ...walk.swaps.frames.legs])] : [];
632
- for (const r of items) delete r.raw;
633
- return {
634
- items, perVariant, pooledPerJoint,
635
- deltaNote: "Error of each visible body keypoint (COCO shoulders..ankles vs the rendered rig's Arm/ForeArm/Hand/UpLeg/Leg/Foot) under the per-frame, per-group L/R assignment the truth prefers; occluded = the truth joint lies behind a scene box along the camera ray. Face keypoints have no rig joint and are not scored. Per-joint pooled values are the median over items of each item's median / p95.",
636
- walkFlipWindow: walk ? { window: [18, 51], inside: walkFrames.filter((t) => t >= 18 && t <= 51).length, outside: walkFrames.filter((t) => t < 18 || t > 51).length, overlaps: walkFrames.some((t) => t >= 18 && t <= 51) } : null,
637
- };
638
- });
639
-
640
- const stance = await stage("stance", { code: codeSha(), baseline: o.baseline, items: truthItems.map((i) => ({ item: i, source: shaOrMissing(i.source), gbest: shaOrMissing(join(o.baseline, i.set, i.name, "Gbest", "motion.npz")) })) }, () => {
641
- const items = truthItems.map((item) => guard(`${item.set}/${item.name}`, () => { const r = stanceItem(item, o.baseline); log(`stance ${r.item}: ${r.status} truth ${r.truth?.cmPerS} Gbest ${r.gbest?.cmPerS} cm/s`); return r; }));
642
- const ok = items.filter((r) => r.status === "ok");
643
- return { baseline: o.baseline, rule: ok[0]?.rule ?? null, items, mean: { truthCmPerS: round(mean(ok.map((r) => r.truth.cmPerS))), gbestCmPerS: round(mean(ok.map((r) => r.gbest.cmPerS))), items: ok.length } };
644
- });
645
-
646
- const ceiling = await stage("ceiling", { code: codeSha(), out: o.out, items: truthItems.map((i) => ({ ...ceilingInputs(i), skin: ["camera.json", "joints.json"].map((f) => shaOrMissing(join(i.dir, "skin", f))) })) }, () => {
647
- const items = truthItems.map((item) => guard(`${item.set}/${item.name}`, () => { log(`ceiling ${item.set}/${item.name}: scoring truth as prediction`); const r = ceilingItem(item, o.out, o); log(`ceiling ${r.item}: ${r.status} IoU raw ${r.maskIoURawMean}`); return r; }));
648
- // The skin renders use a byte-identical camera.json/joints.json and the shared mask/, so the score of the truth is the same: verified, not assumed.
649
- const skinSame = truthItems.map((item) => ({ item: `${item.set}/${item.name}`, same: ["camera.json", "joints.json"].every((f) => fileSha(join(item.dir, "shaded", f)) === fileSha(join(item.dir, "skin", f))) }));
650
- const perSet = {};
651
- for (const set of ["gt", "cube"]) {
652
- const ok = items.filter((r) => r.status === "ok" && r.item.startsWith(`${set}/`));
653
- const ious = ok.map((r) => r.maskIoURawMean);
654
- perSet[set] = { items: ok.length, meanIoURaw: round(mean(ious), 4), minIoURaw: round(Math.min(...ious), 4), is1Threshold: round(0.9 * mean(ious), 4) };
655
- if (skinSame.filter((s) => s.item.startsWith(`${set}/`)).every((s) => s.same)) perSet[`${SKIN_SET[set]}`] = { ...perSet[set], derivedFrom: set };
656
- }
657
- const exp3 = items.filter((r) => r.item.startsWith("cube/")).map((r) => {
658
- const path = join(dirname(dirname(truthItems.find((i) => `${i.set}/${i.name}` === r.item).dir)), "gt-path", "gt-self", "shaded", r.item.split("/")[1], "score.json");
659
- return existsSync(path) ? { item: r.item, exp3GtSelf: path, maskIoURawMean: readJson(path).overlap.maskIoURawMean } : { item: r.item, exp3GtSelf: null };
660
- });
661
- return {
662
- items, perSet, skinSame, exp3CrossCheck: exp3,
663
- skinNote: `Skin ceiling: camera.json and joints.json are byte-identical between shaded and skin for ${skinSame.filter((s) => s.same).length}/${skinSame.length} items and both score against the shared mask/, so the truth-as-prediction score of the skin variant equals the shaded one (gt-skin/cube-skin rows are derived, not re-rendered).`,
664
- };
665
- });
666
-
667
- const cameraVideos = [...falItems.map((i) => [i.video, join(i.dir, i.variant, "camera.json")]), ...truthItems.map((i) => [join(i.dir, i.variant, "video.mp4"), join(i.dir, i.variant, "camera.json")])];
668
- const camera = await stage("camera", { code: codeSha(), fal: falItems.map((i) => i.name), controls: truthItems.map((i) => `${i.set}/${i.name}`), files: cameraVideos.map(([v, c]) => [v, shaOrMissing(v), shaOrMissing(c)]) }, () => {
669
- const probe = (label, video, width, height) => guard(label, () => { const t0 = Date.now(); const r = cameraProbe(label, video, width, height, log); log(`camera ${label}: median ${r.medianShiftPx} px, max ${r.maxShiftPx} px (${((Date.now() - t0) / 1000).toFixed(1)} s)`); return { status: "ok", ...r }; });
670
- const clips = falItems.map((item) => { const cam = readJson(join(item.dir, item.variant, "camera.json")); return probe(item.name, item.video, cam.width, cam.height); });
671
- const controls = truthItems.filter((i) => ["gt/walk", "cube/bump"].includes(`${i.set}/${i.name}`)).map((item) => { const cam = readJson(join(item.dir, item.variant, "camera.json")); return probe(`${item.set}/${item.name} (${item.variant} truth render, static camera)`, join(item.dir, item.variant, "video.mp4"), cam.width, cam.height); });
672
- const ok = clips.filter((c) => c.status === "ok"), thresholdPx = 1;
673
- const worst = ok.reduce((w, c) => (!w || c.medianShiftPx > w.medianShiftPx ? c : w), null);
674
- return {
675
- method: "frame 0 border band (pixels within 10 % of the width of any image edge; the person is mostly inside) split into 32 px tiles, keeping tiles whose structure-tensor min eigenvalue >= 2 grey^2 (no flat sky/floor, no single edges); per frame t each tile's translation vs frame 0 by SSD block matching (+-12 px, half-res coarse then full-res +-2 px with parabolic sub-pixel fit) refined by Lucas-Kanade; the per-frame shift is the median tile shift magnitude (the moving person is a minority of the tiles). Per clip: median / p95 / max over frames 1..T-1. Controls are truth renders with a known static camera.",
676
- thresholdPx, cameraFixed: ok.length === falItems.length && ok.length > 0 && ok.every((c) => c.medianShiftPx < thresholdPx), clipsOk: ok.length, clipsTotal: falItems.length,
677
- worst: worst ? { clip: worst.clip, medianShiftPx: worst.medianShiftPx } : null, clips, controls,
678
- };
679
- });
680
-
681
- const shadedSetOf = { "gt-skin": "gt", "cube-skin": "cube" };
682
- const skinInputs = {
683
- code: codeSha(["tools/bench/obs-bench.mjs"]), approvedSkin: shaOrMissing(o.approvedSkin), approved: sha256(approvedBytes), obsRoot: o.obsRoot,
684
- items: skinItems.map((i) => ({ item: i, ...Object.fromEntries(["obs.npz", "manifest.json"].map((f) => [f, shaOrMissing(join(o.obsRoot, i.set, i.name, "g5", f))])), skinVideo: shaOrMissing(join(i.dir, "skin", "video.mp4")), shadedVideo: shaOrMissing(join(i.dir, "shaded", "video.mp4")), shadedObs: shaOrMissing(join(o.obsRoot, shadedSetOf[i.set] ?? "-", i.name, "g5", "obs.npz")) })),
685
- handoff: o.handoff ? { dir: o.handoff, files: skinItems.map((i) => ["bench.log", "obs-mannequin/manifest.json", "G5/result.json", "G5/score/score.json"].map((f) => shaOrMissing(join(o.handoff, i.set, i.name, f)))) } : null,
686
- };
687
- // (e) imports obs-bench (three/FBXLoader) only when the stage runs.
688
- const skinSummary = await stage("skin", skinInputs, async () => {
689
- if (!skinApproved) throw new Error(`${o.approvedSkin} does not exist (use --write-approved-skin)`);
690
- const { selectItems, itemInputs } = await import("../bench/obs-bench.mjs");
691
- const selected = selectItems(skinApproved);
692
- const result = {
693
- approvedSkin: {
694
- path: o.approvedSkin, items: selected.length, sets: [...new Set(selected.map((i) => i.set))],
695
- selectItemsOk: selected.every((i) => itemInputs(i, "/unused").detector === "yolo" && itemInputs(i, "/unused").video === join(i.dir, "skin", "video.mp4")),
696
- matchesGenerator: JSON.stringify(skinApproved.items) === JSON.stringify(generated.items),
697
- },
698
- provenance: skinItems.map((item) => guard(`${item.set}/${item.name}`, () => skinProvenance(item, o.obsRoot, shadedSetOf[item.set]))),
699
- handoff: o.handoff ? skinItems.map((item) => guard(`${item.set}/${item.name}`, () => handoffCheck(item, o.handoff, o.obsRoot))) : null,
700
- handoffRun: o.handoff ?? null,
701
- };
702
- for (const r of result.provenance) log(`skin provenance ${r.item}: ${r.status}`);
703
- for (const r of result.handoff ?? []) log(`skin handoff ${r.item}: ${r.status}`);
704
- return result;
705
- });
706
-
707
- const summary = {
708
- tool: "tools/track/study-2d.mjs", createdAt: new Date().toISOString(), commit: commitId(),
709
- inputs: { approved: o.approved, approvedSkin: o.approvedSkin, obsRoot: o.obsRoot, baseline: o.baseline },
710
- deltaPx: study2d?.perVariant?.shaded?.deltaPx ?? null,
711
- cameraFixed: camera?.cameraFixed ?? null,
712
- study2d, stance, iouCeiling: ceiling, camera, skin: skinSummary,
713
- };
714
- putJson(join(o.out, "summary.json"), summary);
715
- writeFileSync(join(o.out, "summary.md"), markdown(summary));
716
- log(`wrote ${join(o.out, "summary.json")} and summary.md`);
717
- const failed = [
718
- ...[["2d", study2d?.items], ["stance", stance?.items], ["ceiling", ceiling?.items], ["camera", camera?.clips], ["camera", camera?.controls], ["skin-provenance", skinSummary?.provenance], ["skin-handoff", skinSummary?.handoff]]
719
- .flatMap(([name, rows]) => (rows ?? []).filter((r) => r.status !== "ok").map((r) => ({ stage: name, item: r.item ?? r.clip, status: r.status }))),
720
- ...(skinSummary && !(skinSummary.approvedSkin?.selectItemsOk && skinSummary.approvedSkin?.matchesGenerator) ? [{ stage: "skin", item: o.approvedSkin, status: "bad-approved-skin" }] : []),
721
- ];
722
- if (failed.length) console.error(`[study-2d] ${failed.length} item(s) not ok: ${failed.map((r) => `${r.stage}:${r.item}=${r.status}`).join(", ")}`);
723
- return { summary, failed };
724
- }
725
-
726
- if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
727
- main().then((result) => { if (result?.failed.length) process.exitCode = 1; }, (error) => { console.error(`study-2d: ${error.stack ?? error}`); process.exitCode = 1; });
728
- }