cozyclay 2.1.0 → 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 (583) hide show
  1. package/CHANGELOG.md +65 -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 +22 -6
  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-BlT33CGf.js → first-shot-handoff-CC4xe4HM.js} +1 -1
  33. package/dist/assets/{landing-BSesrCpL.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/llms.txt +14 -0
  40. package/dist/sw.js +3 -6
  41. package/dist/workflow/index.html +5 -4
  42. package/mcp/LIVE-PROTOCOL.md +7 -47
  43. package/mcp/README.md +16 -26
  44. package/mcp/live-hub.mjs +6 -10
  45. package/mcp/package.json +1 -3
  46. package/mcp/tool-handlers.mjs +4 -168
  47. package/package.json +3 -22
  48. package/src/App.jsx +174 -2639
  49. package/src/agent-progress.js +79 -0
  50. package/src/agent-turn-mark.js +30 -0
  51. package/src/analytics.js +1 -19
  52. package/src/app-context.js +5 -0
  53. package/src/app-stage.jsx +2 -34
  54. package/src/asset-pane.jsx +6 -5
  55. package/src/camera-block.js +21 -0
  56. package/src/camera-direction.js +120 -0
  57. package/src/character-kind.js +3 -9
  58. package/src/command-bus.js +8 -6
  59. package/src/commands/cast.js +5 -34
  60. package/src/commands/elements/character.js +1 -1
  61. package/src/commands/elements/motion.js +1 -6
  62. package/src/commands/elements/object.js +6 -2
  63. package/src/commands/elements/shot.js +4 -1
  64. package/src/commands/elements.js +38 -14
  65. package/src/commands/index.js +3 -1
  66. package/src/commands/kits.js +37 -0
  67. package/src/commands/motion-timing.js +57 -0
  68. package/src/commands/motion.js +9 -152
  69. package/src/commands/motion3-compile.js +65 -0
  70. package/src/commands/motion3.js +207 -0
  71. package/src/commands/objects.js +6 -4
  72. package/src/commands/shot.js +49 -11
  73. package/src/commands/stage.js +14 -1
  74. package/src/commands/view.js +4 -1
  75. package/src/document-mirrors.js +42 -0
  76. package/src/document-store.js +4 -0
  77. package/src/domains/cast.js +19 -194
  78. package/src/domains/motion/motion-range-pin-rules.js +1 -1
  79. package/src/domains/motion/take-source.js +51 -0
  80. package/src/domains/motion.js +60 -1838
  81. package/src/domains/scenes.js +12 -55
  82. package/src/domains/shots.js +2 -2
  83. package/src/domains/stage.js +4 -2
  84. package/src/draft-ghost.js +46 -0
  85. package/src/draft-ghost.jsx +129 -0
  86. package/src/dualview.jsx +4 -3
  87. package/src/effects/EffectsLayer.jsx +112 -0
  88. package/src/effects/effect-meshes.js +232 -0
  89. package/src/effects/effect-record.js +103 -0
  90. package/src/effects/emitter.js +59 -0
  91. package/src/effects/simulate.js +261 -0
  92. package/src/effects/state.js +59 -0
  93. package/src/execution-telemetry.js +2 -3
  94. package/src/first-success-guide.jsx +5 -5
  95. package/src/ghost-set.js +257 -0
  96. package/src/held-object-edit.jsx +113 -0
  97. package/src/held-object-pose.js +175 -0
  98. package/src/hierarchy-model.js +1 -1
  99. package/src/hierarchy-panel.jsx +1 -1
  100. package/src/imported-mesh.jsx +103 -0
  101. package/src/kits/held.js +84 -0
  102. package/src/kits/index.js +70 -0
  103. package/src/kits/smoking.js +57 -0
  104. package/src/light-blockers.js +93 -0
  105. package/src/motion/beat-board-model.js +84 -0
  106. package/src/motion/beat-board.css +124 -0
  107. package/src/motion/beat-board.jsx +59 -0
  108. package/src/motion/center-of-mass.js +66 -0
  109. package/src/motion/fix-collisions.js +850 -0
  110. package/src/motion/physics-review.js +118 -0
  111. package/src/{ardy → motion}/physics-support.js +1 -76
  112. package/src/motion/rig-snapshot.js +22 -0
  113. package/src/motion/take-segments.jsx +140 -0
  114. package/src/{ardy → motion}/timeline-coordinates.js +0 -21
  115. package/src/{ardy → motion}/timeline.css +1 -2
  116. package/src/{ardy → motion}/timeline.jsx +48 -419
  117. package/src/motion/waypoints.js +76 -0
  118. package/src/motion3/beats.js +83 -0
  119. package/src/motion3/body.js +158 -0
  120. package/src/motion3/bones.js +47 -0
  121. package/src/motion3/carried.js +47 -0
  122. package/src/motion3/compile-job.js +75 -0
  123. package/src/motion3/compile-worker.js +10 -0
  124. package/src/motion3/compiler/anchors.js +105 -0
  125. package/src/motion3/compiler/arm-orient.js +128 -0
  126. package/src/motion3/compiler/channels.js +125 -0
  127. package/src/motion3/compiler/compile.js +205 -0
  128. package/src/motion3/compiler/gait-rhythm.js +97 -0
  129. package/src/motion3/compiler/grip.js +171 -0
  130. package/src/motion3/compiler/head-steady.js +48 -0
  131. package/src/motion3/compiler/hold.js +134 -0
  132. package/src/motion3/compiler/idle.js +113 -0
  133. package/src/motion3/compiler/limbs.js +170 -0
  134. package/src/motion3/compiler/math.js +72 -0
  135. package/src/motion3/compiler/pose.js +162 -0
  136. package/src/motion3/compiler/quat.js +49 -0
  137. package/src/motion3/compiler/skeleton.js +74 -0
  138. package/src/motion3/compiler/validate-common.js +32 -0
  139. package/src/motion3/compiler/validate-goals.js +118 -0
  140. package/src/motion3/compiler/validate-objects.js +143 -0
  141. package/src/motion3/compiler/validate-parts.js +116 -0
  142. package/src/motion3/compiler/validate.js +122 -0
  143. package/src/motion3/compiler/vocab.js +73 -0
  144. package/src/motion3/compiler/walk-pelvis.js +275 -0
  145. package/src/motion3/compiler/walk.js +207 -0
  146. package/src/motion3/compiler/wrist-clamp.js +32 -0
  147. package/src/motion3/delta.js +47 -0
  148. package/src/motion3/describe.js +174 -0
  149. package/src/motion3/docs.js +27 -0
  150. package/src/motion3/edit-carry.js +21 -0
  151. package/src/motion3/fix/ease.js +16 -0
  152. package/src/motion3/fix/ground.js +37 -0
  153. package/src/motion3/fix/index.js +52 -0
  154. package/src/motion3/fix/push.js +71 -0
  155. package/src/motion3/fix/reach.js +46 -0
  156. package/src/motion3/fix/retime.js +79 -0
  157. package/src/motion3/fix/self.js +211 -0
  158. package/src/motion3/fix/slide.js +70 -0
  159. package/src/motion3/fix/turn.js +51 -0
  160. package/src/motion3/follow.js +39 -0
  161. package/src/motion3/free-motion.js +173 -0
  162. package/src/motion3/grip-choice.js +130 -0
  163. package/src/motion3/hold-plan.js +157 -0
  164. package/src/motion3/json-patch.js +77 -0
  165. package/src/motion3/key-aim.js +105 -0
  166. package/src/motion3/key-edit.js +82 -0
  167. package/src/motion3/lenient.js +127 -0
  168. package/src/motion3/object-keys.js +236 -0
  169. package/src/motion3/object-pose.js +182 -0
  170. package/src/motion3/object-tracks.js +12 -0
  171. package/src/motion3/pipeline.js +102 -0
  172. package/src/motion3/playback.js +51 -0
  173. package/src/motion3/probes.js +91 -0
  174. package/src/motion3/qa/body-checks.js +34 -0
  175. package/src/motion3/qa/check-names.js +14 -0
  176. package/src/motion3/qa/checks.js +121 -0
  177. package/src/motion3/qa/contact-checks.js +51 -0
  178. package/src/motion3/qa/explain.js +80 -0
  179. package/src/motion3/qa/fmt.js +5 -0
  180. package/src/motion3/qa/foot-checks.js +72 -0
  181. package/src/motion3/qa/geometry.js +77 -0
  182. package/src/motion3/qa/goals.js +191 -0
  183. package/src/motion3/qa/ground.js +60 -0
  184. package/src/motion3/qa/hints-contact.js +179 -0
  185. package/src/motion3/qa/hints-goal.js +160 -0
  186. package/src/motion3/qa/hints-knee.js +24 -0
  187. package/src/motion3/qa/hints-motion.js +209 -0
  188. package/src/motion3/qa/hints-object.js +91 -0
  189. package/src/motion3/qa/index.js +42 -0
  190. package/src/motion3/qa/key-checks.js +82 -0
  191. package/src/motion3/qa/near.js +96 -0
  192. package/src/motion3/qa/object-checks.js +135 -0
  193. package/src/motion3/qa/patch.js +161 -0
  194. package/src/motion3/qa/severity.js +38 -0
  195. package/src/motion3/qa/spec-checks.js +65 -0
  196. package/src/motion3/qa/style.js +218 -0
  197. package/src/motion3/qa/wrist-checks.js +69 -0
  198. package/src/motion3/qa/wrist-obs.js +60 -0
  199. package/src/motion3/refs.js +120 -0
  200. package/src/motion3/repair-tune.js +121 -0
  201. package/src/motion3/repair.js +83 -0
  202. package/src/motion3/report.js +67 -0
  203. package/src/motion3/rigs/index.js +14 -0
  204. package/src/motion3/rigs/x-bot-tpose-body.json +1 -0
  205. package/src/motion3/rigs/x-bot-tpose.json +1 -0
  206. package/src/motion3/rigs/y-bot-tpose-body.json +1 -0
  207. package/src/motion3/rigs/y-bot-tpose.json +1 -0
  208. package/src/motion3/scene.js +60 -0
  209. package/src/motion3/schema/brief.js +89 -0
  210. package/src/motion3/schema/fields.js +44 -0
  211. package/src/motion3/schema/spec-examples.js +46 -0
  212. package/src/motion3/schema/topics.js +111 -0
  213. package/src/motion3/schema/types.js +147 -0
  214. package/src/motion3/search.js +117 -0
  215. package/src/motion3/second-hand.js +112 -0
  216. package/src/motion3/skills/basic.js +82 -0
  217. package/src/motion3/skills/book.js +76 -0
  218. package/src/motion3/skills/car.js +145 -0
  219. package/src/motion3/skills/frames-auto.js +29 -0
  220. package/src/motion3/skills/index.js +83 -0
  221. package/src/motion3/skills/info.js +65 -0
  222. package/src/motion3/skills/smoke-kit.js +109 -0
  223. package/src/motion3/skills/smoke.js +160 -0
  224. package/src/motion3/spec-store.js +59 -0
  225. package/src/motion3/take-edit.js +155 -0
  226. package/src/motion3/take.js +63 -0
  227. package/src/motion3/timeline.js +127 -0
  228. package/src/motion3/views.js +121 -0
  229. package/src/motion3/walk-plan.js +119 -0
  230. package/src/object-catalog.jsx +7 -1
  231. package/src/object-colors.js +98 -0
  232. package/src/object-gizmo-parts.js +115 -0
  233. package/src/object-gizmo.jsx +37 -122
  234. package/src/object-glow.js +139 -0
  235. package/src/object-hidden.js +33 -0
  236. package/src/object-look.js +196 -0
  237. package/src/object-size.js +10 -0
  238. package/src/object-travel.js +71 -6
  239. package/src/panels/CameraPanel.jsx +7 -7
  240. package/src/panels/HdrEnvironmentControls.jsx +59 -0
  241. package/src/panels/LightPanel.jsx +25 -4
  242. package/src/panels/ObjectLookPanel.jsx +88 -0
  243. package/src/panels/PoseImagePanel.jsx +1 -1
  244. package/src/panels/PosePanel.jsx +2 -17
  245. package/src/panels/RigControlPanel.jsx +5 -53
  246. package/src/panels/SubjectBox.jsx +4 -1
  247. package/src/panels/details.css +14 -0
  248. package/src/posestudio.jsx +4 -27
  249. package/src/project-browser.jsx +3 -4
  250. package/src/project-resources.js +18 -31
  251. package/src/project.js +13 -104
  252. package/src/props.jsx +25 -280
  253. package/src/rail-dot-drag.js +23 -0
  254. package/src/render-passes.js +10 -0
  255. package/src/resource-status.jsx +1 -0
  256. package/src/result-modal.jsx +4 -19
  257. package/src/room.jsx +20 -12
  258. package/src/scene-assets.js +8 -6
  259. package/src/scene-light-place.js +72 -0
  260. package/src/scene-lights.jsx +148 -0
  261. package/src/scene-objects.js +31 -139
  262. package/src/scenes.js +15 -105
  263. package/src/semantic-edit.js +2 -5
  264. package/src/set-pieces.jsx +183 -0
  265. package/src/settings-menu.jsx +3 -11
  266. package/src/shell/BottomDock.jsx +11 -53
  267. package/src/shell/CompositionGuideButton.jsx +20 -0
  268. package/src/shell/DetailsSlot.jsx +16 -200
  269. package/src/shell/GhostsMenuGroup.jsx +74 -0
  270. package/src/shell/LibrarySlot.jsx +1 -3
  271. package/src/shell/MenuBar.jsx +3 -3
  272. package/src/shell/PreferencesDialog.jsx +15 -264
  273. package/src/shell/PreferencesSlot.jsx +2 -8
  274. package/src/shell/ShotPreviewHeader.jsx +17 -0
  275. package/src/shell/TopBar.jsx +5 -83
  276. package/src/shell/ViewportToolbar.jsx +23 -93
  277. package/src/shell/agent-glass.css +19 -6
  278. package/src/shell/details-selection.js +42 -0
  279. package/src/shell/ghosts-menu.css +22 -0
  280. package/src/shell/glass-regions.css +29 -3
  281. package/src/shell/preferences.css +6 -1
  282. package/src/shell/viewport.css +12 -0
  283. package/src/shot-authoring.js +4 -7
  284. package/src/soft-shadows.js +37 -0
  285. package/src/stage-attach-frames.js +12 -12
  286. package/src/stage-canvas-rigs.jsx +3 -3
  287. package/src/stage-capture.jsx +42 -8
  288. package/src/stage-characters.jsx +22 -216
  289. package/src/stage-editor-gizmos.jsx +6 -6
  290. package/src/stage-environment.js +93 -0
  291. package/src/stage-hdr-builtins.js +107 -0
  292. package/src/stage-hdr-image.js +116 -0
  293. package/src/stage-hdr-library.js +70 -0
  294. package/src/stage-hdr-map.js +94 -0
  295. package/src/stage-hdr.js +130 -0
  296. package/src/stage-lighting.js +149 -0
  297. package/src/stage-occlusion.js +210 -0
  298. package/src/stage-sky.jsx +159 -0
  299. package/src/stage-startup-defaults.js +5 -9
  300. package/src/studio-actions.js +12 -14
  301. package/src/studio-agent-commands.js +46 -16
  302. package/src/studio-agent-motion.js +9 -9
  303. package/src/studio-agent-protocol.js +46 -73
  304. package/src/studio-app-binding.js +29 -45
  305. package/src/studio-asset-list.js +17 -0
  306. package/src/studio-contact-sheet.js +37 -5
  307. package/src/studio-elements.js +27 -7
  308. package/src/studio-entity-bounds.js +18 -0
  309. package/src/studio-error-codes.js +8 -0
  310. package/src/studio-export-framing.js +152 -0
  311. package/src/studio-lighting-check.js +103 -0
  312. package/src/studio-motion-visual.js +150 -0
  313. package/src/studio-schema-errors.js +80 -0
  314. package/src/studio-support-gap.js +74 -0
  315. package/src/styles.css +1 -19
  316. package/src/timeline-defaults.js +5 -0
  317. package/src/timeline-extent.js +8 -20
  318. package/src/tutorial-walk.js +17 -0
  319. package/src/ui-scrub.js +1 -1
  320. package/src/use-shot-camera-tracks.js +12 -0
  321. package/src/view-ghosts.js +97 -0
  322. package/src/view-ghosts.jsx +49 -0
  323. package/src/workflow/AgentPanel.jsx +47 -166
  324. package/src/workflow/AgentProgressCard.jsx +107 -0
  325. package/src/workflow/AgentReplyText.jsx +37 -0
  326. package/src/workflow/AgentTurnSteps.jsx +84 -0
  327. package/src/workflow/LoginPanel.jsx +140 -0
  328. package/src/workflow/WorkflowBuilder.jsx +7 -23
  329. package/src/workflow/agent-client.js +54 -51
  330. package/src/workflow/agent-panel.css +71 -1
  331. package/src/workflow/agent-progress-card.css +167 -0
  332. package/src/workflow/agent-progress-view.js +47 -0
  333. package/src/workflow/agent-turn-steps.css +113 -0
  334. package/src/workflow/agent-turn.js +191 -0
  335. package/src/workflow/login-client.js +34 -0
  336. package/src/workflow/motion-input.js +18 -31
  337. package/src/workflow/node-schema.js +1 -1
  338. package/src/workflow/reply-markdown.js +69 -0
  339. package/src/workflow/scene-asset-sync.js +8 -8
  340. package/tools/agent-session-stats.mjs +61 -0
  341. package/tools/dev-full.mjs +5 -56
  342. package/tools/eslint-baseline.json +12 -13
  343. package/tools/headless-studio.mjs +85 -0
  344. package/tools/knip-baseline.json +0 -179
  345. package/tools/loc-baseline.json +33 -83
  346. package/tools/motion3/extract-body.mjs +103 -0
  347. package/tools/motion3/extract-rig.mjs +85 -0
  348. package/tools/process-supervisor.mjs +0 -88
  349. package/tools/run-tests.mjs +9 -124
  350. package/bin/agent/motion-runtime.mjs +0 -76
  351. package/dist/ardy/cskel27-rest.json +0 -557
  352. package/dist/assets/analytics-B41FmPYI.js +0 -1
  353. package/dist/assets/app-Do-Og5Pu.css +0 -1
  354. package/dist/assets/app-DwP-tl2x.js +0 -4920
  355. package/dist/assets/config-Bx7pYsD9.js +0 -1
  356. package/dist/assets/config-DeoFTYzb.css +0 -1
  357. package/dist/assets/demo-Dt0qZfS7.js +0 -1
  358. package/dist/assets/scene-asset-sync-BCWDJB1d.js +0 -19
  359. package/dist/assets/scene-asset-sync-DjJ_cJ2R.css +0 -1
  360. package/dist/assets/ticket-jqMhqL88.js +0 -1
  361. package/dist/assets/workflow-Br-K7Srs.js +0 -179
  362. package/dist/demo/walk-then-stop.npz +0 -0
  363. package/mcp/ardy-prompts.mjs +0 -218
  364. package/src/ardy/auto-fix-panel.css +0 -108
  365. package/src/ardy/auto-physics.js +0 -1194
  366. package/src/ardy/client.js +0 -153
  367. package/src/ardy/convert.js +0 -313
  368. package/src/ardy/cskel27-neutral.js +0 -39
  369. package/src/ardy/cskel27.js +0 -77
  370. package/src/ardy/export.js +0 -165
  371. package/src/ardy/fix-collisions.js +0 -2257
  372. package/src/ardy/foot-lock.js +0 -230
  373. package/src/ardy/motion-calibration.js +0 -118
  374. package/src/ardy/motion-edit.js +0 -213
  375. package/src/ardy/motion-url.js +0 -55
  376. package/src/ardy/mp4-duration.js +0 -102
  377. package/src/ardy/npz.js +0 -595
  378. package/src/ardy/physics-panel.css +0 -21
  379. package/src/ardy/physics-panel.jsx +0 -89
  380. package/src/ardy/physics-refine.js +0 -111
  381. package/src/ardy/physics-review.js +0 -567
  382. package/src/ardy/physics-temporal.js +0 -54
  383. package/src/ardy/platform-fit-panel.css +0 -64
  384. package/src/ardy/platform-fit-panel.jsx +0 -41
  385. package/src/ardy/platform-fit.js +0 -326
  386. package/src/ardy/playback.js +0 -711
  387. package/src/ardy/pose-pin.js +0 -97
  388. package/src/ardy/prompt-clips.js +0 -30
  389. package/src/ardy/retime.js +0 -211
  390. package/src/ardy/root-drop.js +0 -369
  391. package/src/ardy/timeline-resize.js +0 -13
  392. package/src/ardy/to-cskel27.js +0 -225
  393. package/src/ardy/trim.js +0 -33
  394. package/src/ardy/waypoints.js +0 -369
  395. package/src/ardy-bridge-contract.js +0 -281
  396. package/src/domains/motion/motion-video-capture.js +0 -664
  397. package/src/i2v-motion-client.js +0 -77
  398. package/src/i2v-motion-studio.jsx +0 -180
  399. package/src/line-edit.js +0 -1348
  400. package/src/motion/generation.js +0 -105
  401. package/src/motion-readiness-ui.jsx +0 -67
  402. package/src/motion-readiness.js +0 -40
  403. package/src/motion-resources.js +0 -177
  404. package/src/motion-store.js +0 -65
  405. package/src/motion-trail.js +0 -601
  406. package/src/multimodel-ingest.js +0 -394
  407. package/src/panels/PromptBlocksPanel.jsx +0 -444
  408. package/src/panels/VideoCapturePanel.jsx +0 -181
  409. package/src/pose-extract/browser.js +0 -42
  410. package/src/pose-extract/detector.js +0 -100
  411. package/src/pose-extract/fit.js +0 -227
  412. package/src/pose-extract/image-frame.js +0 -51
  413. package/src/pose-extract/index.js +0 -58
  414. package/src/pose-extract/keyframes.js +0 -74
  415. package/src/pose-extract/landmarks.js +0 -162
  416. package/src/pose-extract/mirror.js +0 -153
  417. package/src/pose-extract/take.js +0 -131
  418. package/src/pose-extract/video-frames.js +0 -91
  419. package/src/take-recipe.js +0 -165
  420. package/src/trail-key-conflicts.js +0 -39
  421. package/src/trail-pick.js +0 -59
  422. package/src/use-i2v-capture-modal.js +0 -15
  423. package/tools/ardy/BRIDGE.md +0 -37
  424. package/tools/ardy/GVHMR-NPZ.md +0 -68
  425. package/tools/ardy/README.md +0 -124
  426. package/tools/ardy/__pycache__/cclay_gvhmr_worker.cpython-313.pyc +0 -0
  427. package/tools/ardy/artifacts.mjs +0 -34
  428. package/tools/ardy/bridge.mjs +0 -1732
  429. package/tools/ardy/bvh-cskel27.mjs +0 -1265
  430. package/tools/ardy/cclay_constrained_generate.py +0 -561
  431. package/tools/ardy/cclay_gvhmr_worker.py +0 -264
  432. package/tools/ardy/cclay_motion_edit.py +0 -510
  433. package/tools/ardy/cclay_sequence_generate.py +0 -705
  434. package/tools/ardy/cclay_worker.py +0 -304
  435. package/tools/ardy/dump-npz.py +0 -205
  436. package/tools/ardy/extract-rest.mjs +0 -299
  437. package/tools/ardy/extract.mjs +0 -507
  438. package/tools/ardy/footage.mjs +0 -467
  439. package/tools/ardy/gvhmr-floor.mjs +0 -102
  440. package/tools/ardy/gvhmr-to-cskel27.mjs +0 -21
  441. package/tools/ardy/gvhmr_fastpath.py +0 -168
  442. package/tools/ardy/gvhmr_trajectory.py +0 -240
  443. package/tools/ardy/merge-text-encoder.py +0 -160
  444. package/tools/ardy/mocap-metrics.mjs +0 -200
  445. package/tools/ardy/mocap-quality-gate.mjs +0 -110
  446. package/tools/ardy/motion-stabilize.mjs +0 -348
  447. package/tools/ardy/npz.mjs +0 -414
  448. package/tools/ardy/pose-to-npz.mjs +0 -106
  449. package/tools/ardy/prompt-limits.mjs +0 -5
  450. package/tools/ardy/run-edit-on-box.sh +0 -73
  451. package/tools/ardy/run-local.mjs +0 -233
  452. package/tools/ardy/run-on-box.sh +0 -593
  453. package/tools/ardy/run-sequence-on-box.sh +0 -177
  454. package/tools/ardy/runners/gvhmr-worker.mjs +0 -206
  455. package/tools/ardy/runners/index.mjs +0 -18
  456. package/tools/ardy/runners/local.mjs +0 -455
  457. package/tools/ardy/runners/proc.mjs +0 -125
  458. package/tools/ardy/runners/remote.mjs +0 -252
  459. package/tools/ardy/setup-local.mjs +0 -189
  460. package/tools/ardy/setup-text-encoder-on-box.sh +0 -93
  461. package/tools/ardy/setup-text-encoder.py +0 -306
  462. package/tools/ardy/smpl-cskel27.mjs +0 -178
  463. package/tools/ardy/visual-qa.mjs +0 -196
  464. package/tools/ardy/vq-car.mjs +0 -52
  465. package/tools/bench/EXP3.md +0 -61
  466. package/tools/bench/cclay_bench_extract_incam.py +0 -125
  467. package/tools/bench/cclay_bench_extract_obs.py +0 -204
  468. package/tools/bench/cclay_bench_runner.py +0 -42
  469. package/tools/bench/cube-contact.mjs +0 -162
  470. package/tools/bench/exp3.mjs +0 -152
  471. package/tools/bench/extract-bench-lib.mjs +0 -216
  472. package/tools/bench/extract-bench.mjs +0 -302
  473. package/tools/bench/fal-generate.mjs +0 -88
  474. package/tools/bench/fit/README.md +0 -189
  475. package/tools/bench/fit/camera.mjs +0 -59
  476. package/tools/bench/fit/contact.mjs +0 -419
  477. package/tools/bench/fit/footlock.mjs +0 -234
  478. package/tools/bench/fit/motion.mjs +0 -85
  479. package/tools/bench/fit/pin.mjs +0 -27
  480. package/tools/bench/fit/remote.mjs +0 -105
  481. package/tools/bench/fit-bench.mjs +0 -126
  482. package/tools/bench/fit-sanity.mjs +0 -45
  483. package/tools/bench/metrics.mjs +0 -313
  484. package/tools/bench/obs/depth.mjs +0 -258
  485. package/tools/bench/obs/extrinsics.mjs +0 -175
  486. package/tools/bench/obs/fit_mannequin_betas.py +0 -340
  487. package/tools/bench/obs/ground.mjs +0 -294
  488. package/tools/bench/obs/heading.mjs +0 -151
  489. package/tools/bench/obs/ladder.mjs +0 -555
  490. package/tools/bench/obs/mannequin-betas.json +0 -124
  491. package/tools/bench/obs/remote.mjs +0 -83
  492. package/tools/bench/obs/rest_joints.py +0 -52
  493. package/tools/bench/obs/ybot-targets.mjs +0 -49
  494. package/tools/bench/obs-bench.mjs +0 -520
  495. package/tools/bench/score.mjs +0 -343
  496. package/tools/bench/summarize.mjs +0 -86
  497. package/tools/demo-worker/README.md +0 -137
  498. package/tools/demo-worker/api-client.mjs +0 -233
  499. package/tools/demo-worker/generate.mjs +0 -301
  500. package/tools/demo-worker/index.mjs +0 -466
  501. package/tools/demo-worker/sign-core.mjs +0 -112
  502. package/tools/demo-worker/sign.mjs +0 -161
  503. package/tools/gt-render/browser.mjs +0 -159
  504. package/tools/gt-render/camera-math.mjs +0 -276
  505. package/tools/gt-render/page.mjs +0 -267
  506. package/tools/gt-render/render.mjs +0 -514
  507. package/tools/gt-render/scene-box.mjs +0 -31
  508. package/tools/gt-render/take-transform.mjs +0 -87
  509. package/tools/kimodo/compare-seam.mjs +0 -108
  510. package/tools/kimodo/constraints.mjs +0 -244
  511. package/tools/kimodo/edit.mjs +0 -259
  512. package/tools/kimodo/effector-constraints.mjs +0 -200
  513. package/tools/kimodo/generate.mjs +0 -646
  514. package/tools/kimodo/local-output.mjs +0 -265
  515. package/tools/kimodo/measure-pose.mjs +0 -65
  516. package/tools/kimodo/measure-preserve.mjs +0 -899
  517. package/tools/kimodo/measure-waypoints.mjs +0 -55
  518. package/tools/kimodo/mlx-generate.py +0 -111
  519. package/tools/kimodo/pose-constraints.mjs +0 -248
  520. package/tools/kimodo/preserve-mask.mjs +0 -543
  521. package/tools/kimodo/read-npz.mjs +0 -189
  522. package/tools/kimodo/run-edit-on-box.mjs +0 -155
  523. package/tools/kimodo/run-sequence-on-box.mjs +0 -178
  524. package/tools/kimodo/runner.mjs +0 -284
  525. package/tools/kimodo/seam-batch.mjs +0 -77
  526. package/tools/kimodo/setup-local.mjs +0 -30
  527. package/tools/kimodo/setup-on-box.sh +0 -339
  528. package/tools/kimodo/soma77-to-cskel27.mjs +0 -305
  529. package/tools/morphgs/assets/gen_truth.py +0 -39
  530. package/tools/morphgs/assets/mesh_ori_rig.txt +0 -27
  531. package/tools/morphgs/assets/playback-check.mjs +0 -30
  532. package/tools/morphgs/demo-gate.sh +0 -26
  533. package/tools/morphgs/fbx2morphgs.mjs +0 -68
  534. package/tools/morphgs/morphgs-to-cskel27.mjs +0 -105
  535. package/tools/morphgs/patches/preprocess_src-none-mode.patch +0 -76
  536. package/tools/morphgs/setup-on-cluster.sh +0 -126
  537. package/tools/projflow/driver.py +0 -1069
  538. package/tools/projflow/generate.mjs +0 -805
  539. package/tools/projflow/hml22-to-cskel27.mjs +0 -735
  540. package/tools/projflow/line-edit-job.mjs +0 -547
  541. package/tools/projflow/replay.mjs +0 -452
  542. package/tools/projflow/runner.mjs +0 -269
  543. package/tools/projflow/service.mjs +0 -742
  544. package/tools/track/DESIGN.md +0 -513
  545. package/tools/track/backfill-provenance.mjs +0 -116
  546. package/tools/track/budget.mjs +0 -8
  547. package/tools/track/check-rig.mjs +0 -209
  548. package/tools/track/diagnostics.schema.json +0 -60
  549. package/tools/track/export-rig.mjs +0 -256
  550. package/tools/track/fallback.mjs +0 -7
  551. package/tools/track/fk-parity-fixture.mjs +0 -161
  552. package/tools/track/gate.mjs +0 -349
  553. package/tools/track/masks.mjs +0 -318
  554. package/tools/track/metrics.mjs +0 -201
  555. package/tools/track/publish-obs.mjs +0 -108
  556. package/tools/track/py/check_env.py +0 -61
  557. package/tools/track/py/eval_lr.py +0 -229
  558. package/tools/track/py/lr_viterbi.py +0 -294
  559. package/tools/track/py/masks.py +0 -405
  560. package/tools/track/py/objective.py +0 -382
  561. package/tools/track/py/rig.py +0 -508
  562. package/tools/track/py/scene.py +0 -326
  563. package/tools/track/py/test_joint_indices.py +0 -123
  564. package/tools/track/py/test_lr_viterbi.py +0 -195
  565. package/tools/track/py/test_masks.py +0 -229
  566. package/tools/track/py/test_rig.py +0 -288
  567. package/tools/track/py/test_scene.py +0 -354
  568. package/tools/track/py/test_track.py +0 -328
  569. package/tools/track/py/track.py +0 -411
  570. package/tools/track/remote.mjs +0 -140
  571. package/tools/track/rig-dump.mjs +0 -227
  572. package/tools/track/run-box-tests.mjs +0 -14
  573. package/tools/track/run-box.mjs +0 -168
  574. package/tools/track/setup-box.sh +0 -59
  575. package/tools/track/study-2d.mjs +0 -728
  576. /package/src/{ardy → motion}/collision-blockers.js +0 -0
  577. /package/src/{ardy → motion}/ground.js +0 -0
  578. /package/src/{ardy → motion}/ik-drag.js +0 -0
  579. /package/src/{ardy → motion}/ik-key-json.js +0 -0
  580. /package/src/{ardy → motion}/ik.js +0 -0
  581. /package/src/{ardy → motion}/physics-surface.js +0 -0
  582. /package/src/{ardy → motion}/playback-clock.js +0 -0
  583. /package/src/{ardy → motion}/range-pin.js +0 -0
@@ -1,899 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * measure-preserve.mjs — how much of a base take survived a regeneration?
4
- *
5
- * This is the measuring stick for the scheduled-inpainting acceptance gates
6
- * (.omo/plans/scheduled-inpainting.md, G1–G4): it compares two cclay motion
7
- * npz files — the take the user already had, and the take that came back out
8
- * of a preserve-enabled generation — and reports how far apart they are.
9
- *
10
- * GPU-free and dependency-free on purpose: the gates are run over artefacts
11
- * pulled back from the box, so this has to work from a laptop with nothing but
12
- * node and the two files.
13
- *
14
- * WHY ROOT-RELATIVE POSITIONS ARE THE HEADLINE NUMBER. Kimodo generates in a
15
- * canonicalised space and the take is re-anchored afterwards, so two takes that
16
- * hold the SAME pose can sit metres apart in world coordinates. A raw
17
- * posed_joints difference is then dominated by root drift and says nothing about
18
- * whether the body was preserved, which is the thing the gates are about. So the
19
- * headline L2P subtracts each frame's Hips position from every joint of that
20
- * frame before differencing, and the undrifted figure is still reported
21
- * separately as globalL2P because a take that preserves the pose but walks off
22
- * in a different direction is its own kind of failure and must stay visible.
23
- *
24
- * WHY ROTATION IS MEASURED GEODESICALLY. Per-element matrix differences are not
25
- * a metric on SO(3) — the same 10° error scores differently depending on where
26
- * on the sphere it sits. The geodesic angle acos((tr(Rᵀ_a R_b) - 1)/2) is the
27
- * actual rotation you would have to apply to get from one to the other, in
28
- * radians, and is directly comparable between joints and between clips.
29
- *
30
- * WHY FOOT SLIDING IS MEASURED PER CLIP, NOT AS A DIFFERENCE. G4 asks whether
31
- * preservation introduced skating, and skating is a property of ONE clip (a
32
- * planted foot that translates), not of the pair. So each clip is measured on
33
- * its own timeline with its own floor, and only then are the two numbers
34
- * subtracted.
35
- *
36
- * WHY THERE IS A PER-JOINT SPLIT (round 2). The grouped-mask gates are claims
37
- * about WHICH PART of the body moved: "free the left arm over this range and the
38
- * arm moves while the legs do not" (GA), "the edited hand lands on target and the
39
- * other limbs stay put" (GC). A single whole-body mean cannot distinguish "the
40
- * arm moved 10 cm" from "every joint drifted 1 cm", and those are a pass and a
41
- * failure of the same gate. So the same root-relative L2P is also accumulated
42
- * per cskel27 joint and aggregated into the seven C1v2 mask groups.
43
- *
44
- * usage:
45
- * measure-preserve.mjs <base.npz> <candidate.npz> [--ranges 40:80,120:150]
46
- * [--json] [--per-joint] [--groups]
47
- * [--max-l2p M] [--max-l2r RAD] [--max-foot-delta M]
48
- *
49
- * `--ranges` are APP frames, half-open [start,end), in the BASE clip's frame
50
- * space — the same space C3's `editRanges` are authored in. Frames inside any
51
- * range are the ones the edit was allowed to change; everything else is what
52
- * G2 says must stay pinned to the base.
53
- *
54
- * Any --max-* flag turns this into a gate: exceeding it exits 1. With no
55
- * threshold flag the tool only reports and exits 0.
56
- */
57
-
58
- import { readNpz } from "./read-npz.mjs";
59
- import { CSKEL27_JOINTS } from "../../src/ardy/cskel27.js";
60
-
61
- const JOINTS = 27;
62
-
63
- /** Joint 0 is the root in cskel27; asserted rather than assumed because every
64
- * root-relative number below silently changes meaning if the order ever moves. */
65
- const HIPS = 0;
66
- if (CSKEL27_JOINTS[HIPS] !== "Hips") {
67
- throw new Error(`measure-preserve: cskel27 joint 0 is ${CSKEL27_JOINTS[HIPS]}, expected Hips`);
68
- }
69
-
70
- /** The joints whose contact with the floor defines sliding. Toes are included
71
- * because a heel-planted foot pivoting on the toe is not skating, but a toe
72
- * that translates while down is. */
73
- const FOOT_JOINTS = ["LeftFoot", "LeftToeBase", "RightFoot", "RightToeBase"];
74
- const FOOT_INDICES = FOOT_JOINTS.map((name) => {
75
- const index = CSKEL27_JOINTS.indexOf(name);
76
- if (index < 0) throw new Error(`measure-preserve: cskel27 has no joint ${name}`);
77
- return index;
78
- });
79
-
80
- /**
81
- * Contact heuristic, deliberately crude and deliberately per-joint-per-clip.
82
- *
83
- * A joint counts as planted on a frame when its height is within 5 cm of THAT
84
- * JOINT's own minimum height across THAT clip. Per-joint because the toe sits
85
- * lower than the ankle and a shared threshold would call the ankle airborne for
86
- * the whole clip; per-clip because soma77ToCskel27Motion floor-shifts each take
87
- * by its own lowest sample, so "y == 0" is not a shared floor between two files.
88
- *
89
- * The honest caveat: for a clip with no ground contact at all (a jump loop, a
90
- * lying-down take) the minimum is not the floor and this will report the slide
91
- * of whatever was lowest. That is acceptable here because G4 compares the SAME
92
- * motion before and after preservation, so the bias is identical on both sides
93
- * and cancels in the delta. Do not lift this number into an absolute
94
- * "does this clip skate" claim without checking the clip has real contacts.
95
- */
96
- const CONTACT_BAND_M = 0.05;
97
-
98
- /* ------------------------------------------------------- cskel27 -> C1v2 groups */
99
-
100
- /**
101
- * The seven C1v2 mask groups expressed in cskel27 JOINT names.
102
- *
103
- * THIS IS A DIFFERENT NAMESPACE FROM THE MASK'S TRACK IDS and the two must not
104
- * be confused. tools/kimodo/preserve-mask.mjs maps IK TRACK ids — the handles
105
- * the user drags: `leftHand`, `leftElbow`, `chest`, `hips` — to these same seven
106
- * group names. This table maps the 27 MEASURED joints to them. The names differ
107
- * on both sides of the arrow: the `leftElbow` track is the joint cskel27 calls
108
- * LeftForeArm, the `chest` track spans Spine1/Spine2, and joints the user has no
109
- * handle for at all (LeftHandEnd, RightToeBase, Spine3) still have to land in a
110
- * group here because they are measured. So this is a mirror of TRACK_GROUPS'
111
- * SEMANTICS, deliberately re-derived rather than imported — importing would
112
- * suggest a name correspondence that does not exist.
113
- *
114
- * Two consequences worth stating before someone reads a number wrong:
115
- *
116
- * - `root` is Hips alone, and the root-relative L2P of Hips is IDENTICALLY
117
- * ZERO by construction (every joint is expressed against its own frame's
118
- * hips, so the hips are always the origin). The root group's meaningful
119
- * number is therefore its GLOBAL L2P, which is reported alongside. A root
120
- * l2p_m of 0.000000 is arithmetic, not a passing gate.
121
- * - Hips appears in BOTH `root` and `torso`, mirroring the mask's `hips` track
122
- * which frees both. The groups therefore overlap and their joint counts sum
123
- * to 28, not 27.
124
- */
125
- export const CSKEL27_GROUPS = Object.freeze({
126
- root: Object.freeze(["Hips"]),
127
- // the shoulders are torso, not arm: a clavicle swings the whole shoulder
128
- // mass, exactly as TRACK_GROUPS puts leftShoulder/rightShoulder in torso.
129
- torso: Object.freeze(["Hips", "Spine", "Spine1", "Spine2", "Spine3", "LeftShoulder", "RightShoulder"]),
130
- head: Object.freeze(["Neck", "Head"]),
131
- leftArm: Object.freeze(["LeftArm", "LeftForeArm", "LeftHand", "LeftHandEnd", "LeftHandThumb1"]),
132
- rightArm: Object.freeze(["RightArm", "RightForeArm", "RightHand", "RightHandEnd", "RightHandThumb1"]),
133
- leftLeg: Object.freeze(["LeftUpLeg", "LeftLeg", "LeftFoot", "LeftToeBase"]),
134
- rightLeg: Object.freeze(["RightUpLeg", "RightLeg", "RightFoot", "RightToeBase"]),
135
- });
136
-
137
- /** Group name -> joint indices, resolved once and checked for total coverage. */
138
- const GROUP_INDICES = Object.fromEntries(
139
- Object.entries(CSKEL27_GROUPS).map(([group, joints]) => [
140
- group,
141
- joints.map((name) => {
142
- const index = CSKEL27_JOINTS.indexOf(name);
143
- if (index < 0) throw new Error(`measure-preserve: group ${group} names unknown cskel27 joint ${name}`);
144
- return index;
145
- }),
146
- ])
147
- );
148
-
149
- /** Joint index -> the groups it belongs to. A joint in NO group would be
150
- * silently missing from every grouped number, so the partition is checked at
151
- * load rather than trusted. */
152
- const JOINT_GROUPS = CSKEL27_JOINTS.map((_, index) =>
153
- Object.keys(GROUP_INDICES).filter((group) => GROUP_INDICES[group].includes(index))
154
- );
155
- for (let joint = 0; joint < JOINTS; joint += 1) {
156
- if (JOINT_GROUPS[joint].length === 0) {
157
- throw new Error(`measure-preserve: cskel27 joint ${CSKEL27_JOINTS[joint]} belongs to no C1v2 group`);
158
- }
159
- }
160
-
161
- /* ------------------------------------------------------------------ loading */
162
-
163
- function requireMember(members, name, path) {
164
- const member = members[name];
165
- if (!member) {
166
- throw new Error(
167
- `measure-preserve: ${path} is missing the '${name}' member — is this a cclay motion npz? ` +
168
- `(found: ${Object.keys(members).join(", ") || "nothing"})`
169
- );
170
- }
171
- if (member.unsupported || !member.data) {
172
- throw new Error(
173
- `measure-preserve: ${path} stores '${name}' as unsupported dtype ${member.dtype}`
174
- );
175
- }
176
- return member;
177
- }
178
-
179
- /** Count non-finite entries so a NaN in the source is reported by name instead
180
- * of quietly turning every aggregate into NaN. */
181
- function countNonFinite(data) {
182
- let count = 0;
183
- for (let index = 0; index < data.length; index += 1) {
184
- if (!Number.isFinite(data[index])) count += 1;
185
- }
186
- return count;
187
- }
188
-
189
- /**
190
- * Load a cclay motion npz down to the arrays this tool compares.
191
- * @returns {{path:string, frames:number, fps:number, fpsAssumed:boolean,
192
- * rot:Float32Array, posed:Float32Array, nonFinite:object}}
193
- */
194
- export function loadMotion(path, { defaultFps = 20 } = {}) {
195
- const members = readNpz(path);
196
- const rot = requireMember(members, "local_rot_mats", path);
197
- const posed = requireMember(members, "posed_joints", path);
198
-
199
- if (rot.shape.length !== 4 || rot.shape[1] !== JOINTS || rot.shape[2] !== 3 || rot.shape[3] !== 3) {
200
- throw new Error(
201
- `measure-preserve: ${path} local_rot_mats must be [T,${JOINTS},3,3], got [${rot.shape}]`
202
- );
203
- }
204
- if (posed.shape.length !== 3 || posed.shape[1] !== JOINTS || posed.shape[2] !== 3) {
205
- throw new Error(
206
- `measure-preserve: ${path} posed_joints must be [T,${JOINTS},3], got [${posed.shape}]`
207
- );
208
- }
209
- const frames = rot.shape[0];
210
- if (posed.shape[0] !== frames) {
211
- throw new Error(
212
- `measure-preserve: ${path} local_rot_mats has ${frames} frames but posed_joints has ${posed.shape[0]}`
213
- );
214
- }
215
- if (frames < 1) throw new Error(`measure-preserve: ${path} has no frames`);
216
-
217
- // fps is optional in the wild: ARDY-generated takes write it as <i4 and
218
- // ARDY-Core demos as <i8, but a hand-built or trimmed file may omit it. A
219
- // missing fps is assumed, never guessed silently — it changes the resampling.
220
- let fps = null;
221
- let fpsAssumed = false;
222
- if (members.fps && members.fps.data && !members.fps.unsupported) {
223
- fps = Math.round(members.fps.data[0]);
224
- if (!Number.isInteger(fps) || fps < 1) {
225
- throw new Error(`measure-preserve: ${path} has a non-positive fps member (${fps})`);
226
- }
227
- } else {
228
- fps = defaultFps;
229
- fpsAssumed = true;
230
- }
231
-
232
- return {
233
- path,
234
- frames,
235
- fps,
236
- fpsAssumed,
237
- rot: rot.data,
238
- posed: posed.data,
239
- nonFinite: {
240
- local_rot_mats: countNonFinite(rot.data),
241
- posed_joints: countNonFinite(posed.data),
242
- },
243
- };
244
- }
245
-
246
- /* -------------------------------------------------------------- comparison */
247
-
248
- /**
249
- * Map every base frame onto a candidate frame by NEAREST sample in time.
250
- *
251
- * Nearest, not interpolated, because interpolating rotation matrices
252
- * element-wise leaves SO(3) and would make L2R measure the interpolation error
253
- * instead of the generation's. Nearest is exact when the clocks match (the
254
- * normal case) and is an honest, reportable approximation when they do not.
255
- *
256
- * Base frames whose time falls past the end of the candidate are dropped rather
257
- * than clamped: clamping would compare the whole tail of the base against one
258
- * repeated candidate frame and invent an error that is really just a length
259
- * mismatch. The count of dropped frames is reported as a truncation warning.
260
- */
261
- export function buildFrameMap(base, candidate) {
262
- const map = new Int32Array(base.frames);
263
- let compared = 0;
264
- for (let frame = 0; frame < base.frames; frame += 1) {
265
- const index = Math.round((frame * candidate.fps) / base.fps);
266
- if (index > candidate.frames - 1) break;
267
- map[frame] = index;
268
- compared += 1;
269
- }
270
- if (compared === 0) {
271
- throw new Error(
272
- `measure-preserve: no overlapping frames between ${base.path} ` +
273
- `(${base.frames}@${base.fps}) and ${candidate.path} (${candidate.frames}@${candidate.fps})`
274
- );
275
- }
276
- return { map, compared };
277
- }
278
-
279
- /** Accumulator for a mean over per-sample values, NaN-skipping and counting. */
280
- function accumulator() {
281
- return { sum: 0, count: 0, skipped: 0, max: 0 };
282
- }
283
-
284
- function add(acc, value) {
285
- if (!Number.isFinite(value)) {
286
- acc.skipped += 1;
287
- return;
288
- }
289
- acc.sum += value;
290
- acc.count += 1;
291
- if (value > acc.max) acc.max = value;
292
- }
293
-
294
- function mean(acc) {
295
- return acc.count > 0 ? acc.sum / acc.count : null;
296
- }
297
-
298
- function summarize(acc) {
299
- return { mean: mean(acc), max: acc.count > 0 ? acc.max : null, samples: acc.count, skipped: acc.skipped };
300
- }
301
-
302
- /**
303
- * Merge accumulators into one, so a group's mean is the mean over ALL its
304
- * joints' samples rather than the mean of their per-joint means. The two differ
305
- * whenever a joint skipped a NaN sample, and the sample-weighted one is the
306
- * honest reading of "how far apart are these joints".
307
- */
308
- function combine(accs) {
309
- const merged = accumulator();
310
- for (const acc of accs) {
311
- merged.sum += acc.sum;
312
- merged.count += acc.count;
313
- merged.skipped += acc.skipped;
314
- if (acc.count > 0 && acc.max > merged.max) merged.max = acc.max;
315
- }
316
- return merged;
317
- }
318
-
319
- /**
320
- * Geodesic angle between two rotation matrices, in radians.
321
- *
322
- * The textbook form is acos((tr(Rᵀ_a R_b) - 1) / 2), and it is a TRAP at the
323
- * small angles this tool exists to measure. acos has infinite slope at 1, so it
324
- * amplifies error like sqrt: the npz stores float32, whose matrices are only
325
- * orthonormal to ~1e-7, and acos turns that 1e-7 into ~3e-4 rad of phantom
326
- * rotation error. Measured, not theorised — the first cut of this file scored a
327
- * fixture against ITSELF at 1.04e-4 rad mean / 7.6e-4 max, which is the same
328
- * order as a real preservation difference and would have made G1 unreadable.
329
- *
330
- * So the angle comes from atan2(sin, cos) instead, where sin is read off the
331
- * skew-symmetric part of Rᵀ_a R_b. Near zero, sin IS the angle to first order
332
- * and is built from DIFFERENCES that vanish when the matrices agree, so the
333
- * float32 noise stays 1e-7 instead of being square-rooted up.
334
- *
335
- * The bitwise fast path on top is not an optimisation dodge: two identical
336
- * float32 blocks are the same rotation, and the geodesic distance from a
337
- * rotation to itself is exactly 0. Any nonzero result there is arithmetic
338
- * noise, and reporting it would be wrong, not precise.
339
- *
340
- * Neither the transpose nor the product is materialised — the six entries and
341
- * the trace are read straight out of the two flat blocks.
342
- */
343
- function geodesicAngle(rot, aBase, bBase) {
344
- const a = rot.a;
345
- const b = rot.b;
346
- let identical = true;
347
- for (let element = 0; element < 9; element += 1) {
348
- if (a[aBase + element] !== b[bBase + element]) {
349
- identical = false;
350
- break;
351
- }
352
- }
353
- if (identical) return 0;
354
-
355
- // R = Aᵀ B, so R[i][j] = sum_k A[k][i] * B[k][j]; A[r][c] is flat[base+r*3+c].
356
- let trace = 0;
357
- for (let element = 0; element < 9; element += 1) trace += a[aBase + element] * b[bBase + element];
358
-
359
- let r21 = 0, r12 = 0, r02 = 0, r20 = 0, r10 = 0, r01 = 0;
360
- for (let k = 0; k < 3; k += 1) {
361
- const ak = aBase + k * 3;
362
- const bk = bBase + k * 3;
363
- r21 += a[ak + 2] * b[bk + 1];
364
- r12 += a[ak + 1] * b[bk + 2];
365
- r02 += a[ak + 0] * b[bk + 2];
366
- r20 += a[ak + 2] * b[bk + 0];
367
- r10 += a[ak + 1] * b[bk + 0];
368
- r01 += a[ak + 0] * b[bk + 1];
369
- }
370
- const sin = 0.5 * Math.hypot(r21 - r12, r02 - r20, r10 - r01);
371
- const cos = (trace - 1) / 2;
372
- // atan2 needs no clamping: it is defined for any (sin, cos) pair, including
373
- // the slightly-off-unit-circle pairs a non-exactly-orthonormal input gives.
374
- return Math.atan2(sin, cos);
375
- }
376
-
377
- /**
378
- * Parse `--ranges 40:80,120:150` into half-open [start,end) app-frame ranges.
379
- */
380
- export function parseRanges(text) {
381
- const ranges = [];
382
- for (const part of text.split(",")) {
383
- const trimmed = part.trim();
384
- if (!trimmed) continue;
385
- const bits = trimmed.split(":");
386
- if (bits.length !== 2) {
387
- throw new Error(`measure-preserve: bad range '${trimmed}', expected start:end`);
388
- }
389
- const start = Number(bits[0]);
390
- const end = Number(bits[1]);
391
- if (!Number.isInteger(start) || !Number.isInteger(end) || start < 0 || end <= start) {
392
- throw new Error(
393
- `measure-preserve: bad range '${trimmed}' — start and end must be integers with 0 <= start < end`
394
- );
395
- }
396
- ranges.push({ startFrame: start, endFrame: end });
397
- }
398
- return ranges;
399
- }
400
-
401
- /** Per-frame membership of the union of the edited ranges, in base frame space. */
402
- function rangeMembership(ranges, frames) {
403
- const inside = new Uint8Array(frames);
404
- for (const range of ranges) {
405
- const from = Math.max(0, range.startFrame);
406
- const to = Math.min(frames, range.endFrame);
407
- for (let frame = from; frame < to; frame += 1) inside[frame] = 1;
408
- }
409
- return inside;
410
- }
411
-
412
- /**
413
- * Foot sliding for ONE clip, in metres of XZ travel per frame.
414
- *
415
- * Headline number is the TOTAL slide accumulated across all four foot joints
416
- * divided by the clip's frame transitions, so a clip where both feet skate
417
- * scores twice a clip where one does. Per-joint figures are reported alongside
418
- * because a single skating toe is a different bug from a whole body gliding.
419
- */
420
- export function footSliding(motion) {
421
- const { frames, posed } = motion;
422
- const transitions = Math.max(1, frames - 1);
423
- const perJoint = [];
424
- let total = 0;
425
- let contactFrames = 0;
426
-
427
- for (let slot = 0; slot < FOOT_INDICES.length; slot += 1) {
428
- const joint = FOOT_INDICES[slot];
429
- let minY = Infinity;
430
- for (let frame = 0; frame < frames; frame += 1) {
431
- const y = posed[(frame * JOINTS + joint) * 3 + 1];
432
- if (Number.isFinite(y) && y < minY) minY = y;
433
- }
434
- let slide = 0;
435
- let contacts = 0;
436
- if (Number.isFinite(minY)) {
437
- const ceiling = minY + CONTACT_BAND_M;
438
- for (let frame = 0; frame < frames - 1; frame += 1) {
439
- const here = (frame * JOINTS + joint) * 3;
440
- const next = ((frame + 1) * JOINTS + joint) * 3;
441
- if (!(posed[here + 1] <= ceiling)) continue; // airborne, or NaN
442
- const dx = posed[next] - posed[here];
443
- const dz = posed[next + 2] - posed[here + 2];
444
- const step = Math.hypot(dx, dz);
445
- if (!Number.isFinite(step)) continue;
446
- slide += step;
447
- contacts += 1;
448
- }
449
- }
450
- total += slide;
451
- contactFrames += contacts;
452
- perJoint.push({
453
- joint: CSKEL27_JOINTS[joint],
454
- minHeight_m: Number.isFinite(minY) ? minY : null,
455
- contactFrames: contacts,
456
- slide_m: slide,
457
- slide_m_per_frame: slide / transitions,
458
- });
459
- }
460
-
461
- return {
462
- contactBand_m: CONTACT_BAND_M,
463
- frames,
464
- contactFrames,
465
- total_slide_m: total,
466
- slide_m_per_frame: total / transitions,
467
- perJoint,
468
- };
469
- }
470
-
471
- /**
472
- * Compare a base take against a candidate.
473
- * @param {object} base from loadMotion
474
- * @param {object} candidate from loadMotion
475
- * @param {Array<{startFrame:number,endFrame:number}>} ranges edited app-frame ranges
476
- */
477
- export function measurePreserve(base, candidate, ranges = []) {
478
- const warnings = [];
479
- for (const motion of [base, candidate]) {
480
- for (const [member, count] of Object.entries(motion.nonFinite)) {
481
- if (count > 0) warnings.push(`${motion.path}: ${count} non-finite values in ${member}`);
482
- }
483
- if (motion.fpsAssumed) warnings.push(`${motion.path}: no fps member, assumed ${motion.fps}`);
484
- }
485
-
486
- // Resampling and truncation are reported separately because they are
487
- // different accusations: a clock mismatch means the numbers below carry a
488
- // nearest-frame approximation, whereas an equal-clock length mismatch means
489
- // the candidate is simply shorter and nothing was approximated.
490
- const resampled = base.fps !== candidate.fps;
491
- const { map, compared } = buildFrameMap(base, candidate);
492
- if (resampled) {
493
- warnings.push(
494
- `candidate resampled onto the base timeline by nearest frame ` +
495
- `(base ${base.frames}f@${base.fps} vs candidate ${candidate.frames}f@${candidate.fps})`
496
- );
497
- }
498
- const truncated = compared < base.frames;
499
- if (truncated) {
500
- warnings.push(
501
- `candidate is short: only ${compared} of ${base.frames} base frames have a candidate sample; ` +
502
- `the remaining ${base.frames - compared} were dropped, not clamped`
503
- );
504
- }
505
- for (const range of ranges) {
506
- // The classic mix-up is handing this tool GENERATION-space frames (30 fps)
507
- // for an app-space (20 fps) clip, which silently lands the range past the
508
- // end and reports a flawless out-of-range score over the whole take.
509
- if (range.startFrame >= compared) {
510
- warnings.push(
511
- `range [${range.startFrame},${range.endFrame}) starts past the last compared frame ` +
512
- `(${compared}) — are these app frames in the BASE clip's fps space?`
513
- );
514
- } else if (range.endFrame > compared) {
515
- warnings.push(
516
- `range [${range.startFrame},${range.endFrame}) is clipped to the compared span (${compared} frames)`
517
- );
518
- }
519
- }
520
-
521
- const inside = rangeMembership(ranges, base.frames);
522
- const rot = { a: base.rot, b: candidate.rot };
523
-
524
- // Three buckets per metric: everything, inside the edited ranges, outside.
525
- const buckets = {
526
- l2p: { all: accumulator(), inRange: accumulator(), outOfRange: accumulator() },
527
- globalL2p: { all: accumulator(), inRange: accumulator(), outOfRange: accumulator() },
528
- l2r: { all: accumulator(), inRange: accumulator(), outOfRange: accumulator() },
529
- };
530
- const perFrame = { l2p: new Float64Array(compared), l2r: new Float64Array(compared) };
531
-
532
- // Per-joint buckets, the round-2 addition. Three per joint for the same
533
- // reason the whole-body metric has three: GA and GC are claims about a joint
534
- // INSIDE the edited range versus the same joint outside it.
535
- const perJointBuckets = [];
536
- for (let joint = 0; joint < JOINTS; joint += 1) {
537
- perJointBuckets.push({
538
- l2p: { all: accumulator(), inRange: accumulator(), outOfRange: accumulator() },
539
- globalL2p: accumulator(),
540
- l2r: accumulator(),
541
- });
542
- }
543
-
544
- for (let frame = 0; frame < compared; frame += 1) {
545
- const other = map[frame];
546
- const baseFrameBase = frame * JOINTS * 3;
547
- const candFrameBase = other * JOINTS * 3;
548
- const baseHips = baseFrameBase + HIPS * 3;
549
- const candHips = candFrameBase + HIPS * 3;
550
- const bucket = inside[frame] ? "inRange" : "outOfRange";
551
-
552
- let frameL2p = 0;
553
- let frameL2r = 0;
554
- for (let joint = 0; joint < JOINTS; joint += 1) {
555
- const p = baseFrameBase + joint * 3;
556
- const q = candFrameBase + joint * 3;
557
- const jointBucket = perJointBuckets[joint];
558
-
559
- // Root-relative: each side is expressed against its OWN hips, so a
560
- // clip that is the same pose translated scores zero here.
561
- const rdx = base.posed[p] - base.posed[baseHips] - (candidate.posed[q] - candidate.posed[candHips]);
562
- const rdy = base.posed[p + 1] - base.posed[baseHips + 1] - (candidate.posed[q + 1] - candidate.posed[candHips + 1]);
563
- const rdz = base.posed[p + 2] - base.posed[baseHips + 2] - (candidate.posed[q + 2] - candidate.posed[candHips + 2]);
564
- const local = Math.hypot(rdx, rdy, rdz);
565
- add(buckets.l2p.all, local);
566
- add(buckets.l2p[bucket], local);
567
- add(jointBucket.l2p.all, local);
568
- add(jointBucket.l2p[bucket], local);
569
- frameL2p += Number.isFinite(local) ? local : 0;
570
-
571
- const global = Math.hypot(
572
- base.posed[p] - candidate.posed[q],
573
- base.posed[p + 1] - candidate.posed[q + 1],
574
- base.posed[p + 2] - candidate.posed[q + 2]
575
- );
576
- add(buckets.globalL2p.all, global);
577
- add(buckets.globalL2p[bucket], global);
578
- add(jointBucket.globalL2p, global);
579
-
580
- const angle = geodesicAngle(rot, (frame * JOINTS + joint) * 9, (other * JOINTS + joint) * 9);
581
- add(buckets.l2r.all, angle);
582
- add(buckets.l2r[bucket], angle);
583
- add(jointBucket.l2r, angle);
584
- frameL2r += Number.isFinite(angle) ? angle : 0;
585
- }
586
- perFrame.l2p[frame] = frameL2p / JOINTS;
587
- perFrame.l2r[frame] = frameL2r / JOINTS;
588
- }
589
-
590
- // Per-range breakdown: a union aggregate hides an edit that only moved one
591
- // of two ranges, which is exactly the G2 failure mode.
592
- const perRange = ranges.map((range) => {
593
- const l2p = accumulator();
594
- const l2r = accumulator();
595
- const from = Math.max(0, range.startFrame);
596
- const to = Math.min(compared, range.endFrame);
597
- for (let frame = from; frame < to; frame += 1) {
598
- add(l2p, perFrame.l2p[frame]);
599
- add(l2r, perFrame.l2r[frame]);
600
- }
601
- return {
602
- startFrame: range.startFrame,
603
- endFrame: range.endFrame,
604
- comparedFrames: Math.max(0, to - from),
605
- l2p_m: mean(l2p),
606
- l2r_rad: mean(l2r),
607
- };
608
- });
609
-
610
- // Per-joint table, in cskel27 index order so it lines up with the npz arrays
611
- // and with any other tool that indexes joints. Sorting it by error would read
612
- // better and would make two runs impossible to diff, so it stays in order.
613
- const perJoint = perJointBuckets.map((jointBucket, joint) => ({
614
- index: joint,
615
- joint: CSKEL27_JOINTS[joint],
616
- groups: JOINT_GROUPS[joint],
617
- l2p_m: mean(jointBucket.l2p.all),
618
- l2pMax_m: jointBucket.l2p.all.count > 0 ? jointBucket.l2p.all.max : null,
619
- globalL2p_m: mean(jointBucket.globalL2p),
620
- l2r_rad: mean(jointBucket.l2r),
621
- l2p_inRange_m: ranges.length ? mean(jointBucket.l2p.inRange) : null,
622
- l2p_outOfRange_m: ranges.length ? mean(jointBucket.l2p.outOfRange) : null,
623
- }));
624
-
625
- // Group aggregation. Sample-weighted across the group's joints, and reported
626
- // with globalL2p alongside because the `root` group's root-relative number is
627
- // identically zero by construction (see CSKEL27_GROUPS).
628
- const perGroup = Object.keys(CSKEL27_GROUPS).map((group) => {
629
- const indices = GROUP_INDICES[group];
630
- const l2pAll = combine(indices.map((joint) => perJointBuckets[joint].l2p.all));
631
- return {
632
- group,
633
- joints: indices.map((joint) => CSKEL27_JOINTS[joint]),
634
- jointCount: indices.length,
635
- l2p_m: mean(l2pAll),
636
- l2pMax_m: l2pAll.count > 0 ? l2pAll.max : null,
637
- globalL2p_m: mean(combine(indices.map((joint) => perJointBuckets[joint].globalL2p))),
638
- l2r_rad: mean(combine(indices.map((joint) => perJointBuckets[joint].l2r))),
639
- l2p_inRange_m: ranges.length
640
- ? mean(combine(indices.map((joint) => perJointBuckets[joint].l2p.inRange)))
641
- : null,
642
- l2p_outOfRange_m: ranges.length
643
- ? mean(combine(indices.map((joint) => perJointBuckets[joint].l2p.outOfRange)))
644
- : null,
645
- };
646
- });
647
-
648
- const baseFeet = footSliding(base);
649
- const candidateFeet = footSliding(candidate);
650
-
651
- return {
652
- base: { path: base.path, frames: base.frames, fps: base.fps },
653
- candidate: { path: candidate.path, frames: candidate.frames, fps: candidate.fps },
654
- resampled,
655
- truncated,
656
- comparedFrames: compared,
657
- ranges,
658
- jointCount: JOINTS,
659
- l2p_m: summarize(buckets.l2p.all),
660
- l2p_inRange_m: ranges.length ? summarize(buckets.l2p.inRange) : null,
661
- l2p_outOfRange_m: ranges.length ? summarize(buckets.l2p.outOfRange) : null,
662
- globalL2p_m: summarize(buckets.globalL2p.all),
663
- globalL2p_inRange_m: ranges.length ? summarize(buckets.globalL2p.inRange) : null,
664
- globalL2p_outOfRange_m: ranges.length ? summarize(buckets.globalL2p.outOfRange) : null,
665
- l2r_rad: summarize(buckets.l2r.all),
666
- l2r_inRange_rad: ranges.length ? summarize(buckets.l2r.inRange) : null,
667
- l2r_outOfRange_rad: ranges.length ? summarize(buckets.l2r.outOfRange) : null,
668
- perRange,
669
- perJoint,
670
- perGroup,
671
- footSliding: {
672
- base: baseFeet,
673
- candidate: candidateFeet,
674
- // Positive = the candidate skates more than the base. G4's budget.
675
- delta_m_per_frame: candidateFeet.slide_m_per_frame - baseFeet.slide_m_per_frame,
676
- },
677
- warnings,
678
- };
679
- }
680
-
681
- /* -------------------------------------------------------------------- CLI */
682
-
683
- function formatMetres(value) {
684
- return value === null ? " n/a " : value.toFixed(6);
685
- }
686
-
687
- function formatRadians(value) {
688
- return value === null ? " n/a " : `${value.toFixed(6)} (${((value * 180) / Math.PI).toFixed(3)}°)`;
689
- }
690
-
691
- /**
692
- * The per-joint table. Printed only on request because 27 rows would bury the
693
- * headline numbers in the common case, and the JSON carries it unconditionally
694
- * for the gate harness.
695
- */
696
- function printPerJoint(report) {
697
- const showRanges = report.ranges.length > 0;
698
- console.log(" per joint (root-relative L2P, metres — mean over the compared frames)");
699
- console.log(
700
- ` ${"joint".padEnd(16)}${"group".padEnd(10)}${"mean".padStart(10)}${"max".padStart(11)}` +
701
- `${"global".padStart(11)}` +
702
- (showRanges ? `${"inRange".padStart(11)}${"outRange".padStart(11)}` : "")
703
- );
704
- for (const row of report.perJoint) {
705
- console.log(
706
- ` ${row.joint.padEnd(16)}${row.groups.join("+").padEnd(10)}` +
707
- `${formatMetres(row.l2p_m).padStart(10)}${formatMetres(row.l2pMax_m).padStart(11)}` +
708
- `${formatMetres(row.globalL2p_m).padStart(11)}` +
709
- (showRanges
710
- ? `${formatMetres(row.l2p_inRange_m).padStart(11)}${formatMetres(row.l2p_outOfRange_m).padStart(11)}`
711
- : "")
712
- );
713
- }
714
- // The one row that is always 0.000000 and always means nothing.
715
- console.log(" (Hips root-relative L2P is 0 by construction; read its global column instead)");
716
- }
717
-
718
- /**
719
- * The seven-group aggregation — the shape the round-2 gates are phrased in
720
- * ("the arm moved, the legs did not").
721
- */
722
- function printGroups(report) {
723
- const showRanges = report.ranges.length > 0;
724
- console.log(" per C1v2 mask group (root-relative L2P, metres; groups overlap — Hips is in root AND torso)");
725
- console.log(
726
- ` ${"group".padEnd(10)}${"joints".padStart(7)}${"mean".padStart(11)}${"max".padStart(11)}` +
727
- `${"global".padStart(11)}` +
728
- (showRanges ? `${"inRange".padStart(11)}${"outRange".padStart(11)}` : "")
729
- );
730
- for (const row of report.perGroup) {
731
- console.log(
732
- ` ${row.group.padEnd(10)}${String(row.jointCount).padStart(7)}` +
733
- `${formatMetres(row.l2p_m).padStart(11)}${formatMetres(row.l2pMax_m).padStart(11)}` +
734
- `${formatMetres(row.globalL2p_m).padStart(11)}` +
735
- (showRanges
736
- ? `${formatMetres(row.l2p_inRange_m).padStart(11)}${formatMetres(row.l2p_outOfRange_m).padStart(11)}`
737
- : "")
738
- );
739
- }
740
- }
741
-
742
- function printReport(report, { perJoint = false, groups = false } = {}) {
743
- console.log(`base : ${report.base.path} ${report.base.frames} frames @ ${report.base.fps} fps`);
744
- console.log(`candidate : ${report.candidate.path} ${report.candidate.frames} frames @ ${report.candidate.fps} fps`);
745
- console.log(
746
- `compared : ${report.comparedFrames} frames x ${report.jointCount} joints` +
747
- (report.resampled ? " (candidate RESAMPLED to base timeline, nearest frame)" : "") +
748
- (report.truncated ? " (TRUNCATED to the candidate's length)" : "")
749
- );
750
- for (const warning of report.warnings) console.log(`WARN: ${warning}`);
751
-
752
- console.log("");
753
- console.log(" position error (root-relative, metres)");
754
- console.log(` all : mean ${formatMetres(report.l2p_m.mean)} max ${formatMetres(report.l2p_m.max)}`);
755
- if (report.l2p_inRange_m) {
756
- console.log(` inRange : mean ${formatMetres(report.l2p_inRange_m.mean)} max ${formatMetres(report.l2p_inRange_m.max)} (${report.l2p_inRange_m.samples} samples)`);
757
- console.log(` outOfRange : mean ${formatMetres(report.l2p_outOfRange_m.mean)} max ${formatMetres(report.l2p_outOfRange_m.max)} (${report.l2p_outOfRange_m.samples} samples)`);
758
- }
759
- console.log(" position error (global / world, metres)");
760
- console.log(` all : mean ${formatMetres(report.globalL2p_m.mean)} max ${formatMetres(report.globalL2p_m.max)}`);
761
- if (report.globalL2p_inRange_m) {
762
- console.log(` inRange : mean ${formatMetres(report.globalL2p_inRange_m.mean)}`);
763
- console.log(` outOfRange : mean ${formatMetres(report.globalL2p_outOfRange_m.mean)}`);
764
- }
765
- console.log(" rotation error (geodesic, radians)");
766
- console.log(` all : mean ${formatRadians(report.l2r_rad.mean)} max ${formatRadians(report.l2r_rad.max)}`);
767
- if (report.l2r_inRange_rad) {
768
- console.log(` inRange : mean ${formatRadians(report.l2r_inRange_rad.mean)}`);
769
- console.log(` outOfRange : mean ${formatRadians(report.l2r_outOfRange_rad.mean)}`);
770
- }
771
-
772
- if (report.perRange.length) {
773
- console.log(" per edited range");
774
- for (const range of report.perRange) {
775
- console.log(
776
- ` [${range.startFrame},${range.endFrame}) ${String(range.comparedFrames).padStart(4)} frames ` +
777
- `L2P ${formatMetres(range.l2p_m)} L2R ${formatRadians(range.l2r_rad)}`
778
- );
779
- }
780
- }
781
-
782
- if (perJoint) printPerJoint(report);
783
- if (groups) printGroups(report);
784
-
785
- const feet = report.footSliding;
786
- console.log(" foot sliding (m per frame, contact = within 5 cm of that joint's own minimum)");
787
- console.log(` base : ${feet.base.slide_m_per_frame.toFixed(6)} (${feet.base.contactFrames} contact frames)`);
788
- console.log(` candidate : ${feet.candidate.slide_m_per_frame.toFixed(6)} (${feet.candidate.contactFrames} contact frames)`);
789
- console.log(` delta : ${feet.delta_m_per_frame >= 0 ? "+" : ""}${feet.delta_m_per_frame.toFixed(6)}`);
790
-
791
- console.log("");
792
- // Flat, greppable keys, same shape as measure-waypoints.mjs's WORST_ERROR_M,
793
- // so a gate script can pull one number without parsing the whole report.
794
- console.log(`L2P_M=${report.l2p_m.mean === null ? "nan" : report.l2p_m.mean.toFixed(6)}`);
795
- console.log(`GLOBAL_L2P_M=${report.globalL2p_m.mean === null ? "nan" : report.globalL2p_m.mean.toFixed(6)}`);
796
- console.log(`L2R_RAD=${report.l2r_rad.mean === null ? "nan" : report.l2r_rad.mean.toFixed(6)}`);
797
- if (report.l2p_outOfRange_m) {
798
- console.log(`L2P_OUT_M=${report.l2p_outOfRange_m.mean === null ? "nan" : report.l2p_outOfRange_m.mean.toFixed(6)}`);
799
- console.log(`L2P_IN_M=${report.l2p_inRange_m.mean === null ? "nan" : report.l2p_inRange_m.mean.toFixed(6)}`);
800
- }
801
- if (groups) {
802
- // One flat key per group so a gate script can assert "arm moved, legs did
803
- // not" without parsing the table. The all-frames key is always emitted and
804
- // the range-scoped ones only when ranges were given, so a key never
805
- // silently changes meaning between two invocations.
806
- const number = (value) => (value === null ? "nan" : value.toFixed(6));
807
- for (const row of report.perGroup) {
808
- const name = row.group.toUpperCase();
809
- console.log(`L2P_GROUP_${name}=${number(row.l2p_m)}`);
810
- if (report.ranges.length) {
811
- console.log(`L2P_GROUP_${name}_IN=${number(row.l2p_inRange_m)}`);
812
- console.log(`L2P_GROUP_${name}_OUT=${number(row.l2p_outOfRange_m)}`);
813
- }
814
- }
815
- }
816
- console.log(`FOOT_SLIDE_DELTA_M=${feet.delta_m_per_frame.toFixed(6)}`);
817
- }
818
-
819
- function parseArgs(argv) {
820
- const positional = [];
821
- const options = {
822
- ranges: [], json: false, perJoint: false, groups: false,
823
- maxL2p: null, maxL2r: null, maxFootDelta: null,
824
- };
825
- for (let index = 0; index < argv.length; index += 1) {
826
- const arg = argv[index];
827
- if (arg === "--json") options.json = true;
828
- else if (arg === "--per-joint") options.perJoint = true;
829
- else if (arg === "--groups") options.groups = true;
830
- else if (arg === "--ranges") options.ranges = parseRanges(argv[++index] ?? "");
831
- else if (arg.startsWith("--ranges=")) options.ranges = parseRanges(arg.slice("--ranges=".length));
832
- else if (arg === "--max-l2p") options.maxL2p = Number(argv[++index]);
833
- else if (arg === "--max-l2r") options.maxL2r = Number(argv[++index]);
834
- else if (arg === "--max-foot-delta") options.maxFootDelta = Number(argv[++index]);
835
- else if (arg.startsWith("--")) throw new Error(`measure-preserve: unknown flag ${arg}`);
836
- else positional.push(arg);
837
- }
838
- for (const [flag, value] of [["--max-l2p", options.maxL2p], ["--max-l2r", options.maxL2r], ["--max-foot-delta", options.maxFootDelta]]) {
839
- if (value !== null && !Number.isFinite(value)) {
840
- throw new Error(`measure-preserve: ${flag} needs a finite number`);
841
- }
842
- }
843
- return { positional, options };
844
- }
845
-
846
- const invokedDirectly = process.argv[1] && process.argv[1].endsWith("measure-preserve.mjs");
847
- if (invokedDirectly) {
848
- let parsed;
849
- try {
850
- parsed = parseArgs(process.argv.slice(2));
851
- } catch (error) {
852
- console.error(error.message);
853
- process.exit(2);
854
- }
855
- const [basePath, candidatePath] = parsed.positional;
856
- if (!basePath || !candidatePath) {
857
- console.error(
858
- "usage: measure-preserve.mjs <base.npz> <candidate.npz> [--ranges 40:80,120:150] [--json]\n" +
859
- " [--per-joint] [--groups]\n" +
860
- " [--max-l2p M] [--max-l2r RAD] [--max-foot-delta M]"
861
- );
862
- process.exit(2);
863
- }
864
-
865
- let report;
866
- try {
867
- const base = loadMotion(basePath);
868
- // The candidate inherits the base's fps when it carries none: the two are
869
- // the same take before and after regeneration, so silently assuming a
870
- // different default would fake a resample.
871
- const candidate = loadMotion(candidatePath, { defaultFps: base.fps });
872
- report = measurePreserve(base, candidate, parsed.options.ranges);
873
- } catch (error) {
874
- console.error(error.message);
875
- process.exit(2);
876
- }
877
-
878
- // Gates are evaluated before printing so the failures can ride along in the
879
- // JSON, which is the form the acceptance harness consumes.
880
- const failures = [];
881
- const { maxL2p, maxL2r, maxFootDelta } = parsed.options;
882
- if (maxL2p !== null && !(report.l2p_m.mean <= maxL2p)) {
883
- failures.push(`L2P ${report.l2p_m.mean} > --max-l2p ${maxL2p}`);
884
- }
885
- if (maxL2r !== null && !(report.l2r_rad.mean <= maxL2r)) {
886
- failures.push(`L2R ${report.l2r_rad.mean} > --max-l2r ${maxL2r}`);
887
- }
888
- if (maxFootDelta !== null && !(report.footSliding.delta_m_per_frame <= maxFootDelta)) {
889
- failures.push(`foot slide delta ${report.footSliding.delta_m_per_frame} > --max-foot-delta ${maxFootDelta}`);
890
- }
891
-
892
- if (parsed.options.json) {
893
- console.log(JSON.stringify({ ...report, failures }, null, 2));
894
- } else {
895
- printReport(report, { perJoint: parsed.options.perJoint, groups: parsed.options.groups });
896
- for (const failure of failures) console.error(`FAIL: ${failure}`);
897
- }
898
- process.exit(failures.length ? 1 : 0);
899
- }