cozyclay 2.1.1 → 3.0.1

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 (585) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/README.md +12 -30
  3. package/bin/agent/agent-routes.mjs +43 -116
  4. package/bin/agent/agent-runner.mjs +14 -15
  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/session-store.mjs +2 -2
  14. package/bin/agent/studio-context-budget.mjs +93 -0
  15. package/bin/agent/studio-motion-reference.mjs +23 -0
  16. package/bin/agent/studio-prompt.mjs +1 -1
  17. package/bin/agent/studio-rebase-text.mjs +32 -0
  18. package/bin/agent/studio-rebase.mjs +26 -39
  19. package/bin/agent/studio-tools.mjs +9 -28
  20. package/bin/agent/tool-arguments.mjs +49 -0
  21. package/bin/agent/turn-frames.mjs +66 -0
  22. package/bin/codex-auth.mjs +9 -2
  23. package/bin/cozyclay.mjs +10 -116
  24. package/bin/live/cli.mjs +3 -3
  25. package/bin/mcp-runtime.mjs +2 -12
  26. package/bin/package-signature.mjs +0 -4
  27. package/dist/app/index.html +7 -6
  28. package/dist/assets/EXRLoader-CvVIvGVE.js +1 -0
  29. package/dist/assets/analytics-BWCqdgWA.js +1 -0
  30. package/dist/assets/app-DfTCxaKk.js +5087 -0
  31. package/dist/assets/app-f-fkV5uP.css +1 -0
  32. package/dist/assets/compile-worker-BxKXh07s.js +5 -0
  33. package/dist/assets/fflate.module-KbGj5-H6.js +5 -0
  34. package/dist/assets/{first-shot-handoff-msLI71g1.js → first-shot-handoff-DNO8kv_N.js} +1 -1
  35. package/dist/assets/{landing-ChUvB_zL.js → landing-yfZaJkWc.js} +1 -1
  36. package/dist/assets/scene-asset-sync-U3kS2rvK.js +20 -0
  37. package/dist/assets/scene-asset-sync-_S8NQl5B.css +1 -0
  38. package/dist/assets/workflow-D78eiKTa.js +179 -0
  39. package/dist/cozyclay-package.json +1 -1
  40. package/dist/index.html +3 -3
  41. package/dist/sw.js +3 -6
  42. package/dist/workflow/index.html +5 -4
  43. package/mcp/LIVE-PROTOCOL.md +7 -47
  44. package/mcp/README.md +16 -26
  45. package/mcp/live-hub.mjs +6 -10
  46. package/mcp/package.json +1 -3
  47. package/mcp/tool-handlers.mjs +4 -168
  48. package/package.json +3 -22
  49. package/src/App.jsx +151 -2625
  50. package/src/agent-progress.js +80 -0
  51. package/src/agent-turn-mark.js +30 -0
  52. package/src/analytics.js +1 -19
  53. package/src/app-context.js +12 -2
  54. package/src/app-stage.jsx +2 -34
  55. package/src/asset-pane.jsx +6 -5
  56. package/src/camera-block.js +21 -0
  57. package/src/camera-direction.js +120 -0
  58. package/src/character-kind.js +3 -9
  59. package/src/command-bus.js +10 -8
  60. package/src/commands/cast.js +5 -34
  61. package/src/commands/elements/character.js +1 -1
  62. package/src/commands/elements/motion.js +1 -6
  63. package/src/commands/elements/object.js +6 -2
  64. package/src/commands/elements/shot.js +4 -1
  65. package/src/commands/elements.js +38 -14
  66. package/src/commands/index.js +3 -1
  67. package/src/commands/kits.js +37 -0
  68. package/src/commands/motion-timing.js +57 -0
  69. package/src/commands/motion.js +9 -152
  70. package/src/commands/motion3-compile.js +65 -0
  71. package/src/commands/motion3.js +214 -0
  72. package/src/commands/objects.js +6 -4
  73. package/src/commands/shot.js +55 -13
  74. package/src/commands/stage.js +14 -1
  75. package/src/commands/view.js +4 -1
  76. package/src/document-mirrors.js +42 -0
  77. package/src/document-store.js +4 -0
  78. package/src/domains/cast.js +19 -194
  79. package/src/domains/motion/motion-range-pin-rules.js +1 -1
  80. package/src/domains/motion/take-source.js +53 -0
  81. package/src/domains/motion.js +60 -1838
  82. package/src/domains/scenes.js +12 -55
  83. package/src/domains/shots.js +2 -2
  84. package/src/domains/stage.js +4 -2
  85. package/src/draft-ghost.js +47 -0
  86. package/src/draft-ghost.jsx +129 -0
  87. package/src/dualview.jsx +4 -3
  88. package/src/effects/EffectsLayer.jsx +112 -0
  89. package/src/effects/effect-meshes.js +232 -0
  90. package/src/effects/effect-record.js +103 -0
  91. package/src/effects/emitter.js +59 -0
  92. package/src/effects/simulate.js +261 -0
  93. package/src/effects/state.js +59 -0
  94. package/src/execution-telemetry.js +2 -3
  95. package/src/first-success-guide.jsx +5 -5
  96. package/src/ghost-set.js +257 -0
  97. package/src/held-object-edit.jsx +113 -0
  98. package/src/held-object-pose.js +175 -0
  99. package/src/hierarchy-model.js +1 -1
  100. package/src/hierarchy-panel.jsx +1 -1
  101. package/src/imported-mesh.jsx +103 -0
  102. package/src/kits/held.js +84 -0
  103. package/src/kits/index.js +70 -0
  104. package/src/kits/smoking.js +57 -0
  105. package/src/light-blockers.js +93 -0
  106. package/src/motion/beat-board-model.js +84 -0
  107. package/src/motion/beat-board.css +124 -0
  108. package/src/motion/beat-board.jsx +60 -0
  109. package/src/motion/center-of-mass.js +66 -0
  110. package/src/motion/fix-collisions.js +850 -0
  111. package/src/motion/physics-review.js +118 -0
  112. package/src/{ardy → motion}/physics-support.js +1 -76
  113. package/src/motion/rig-snapshot.js +22 -0
  114. package/src/motion/take-segments.jsx +140 -0
  115. package/src/{ardy → motion}/timeline-coordinates.js +0 -21
  116. package/src/{ardy → motion}/timeline.css +1 -2
  117. package/src/{ardy → motion}/timeline.jsx +42 -413
  118. package/src/motion/waypoints.js +76 -0
  119. package/src/motion3/beats.js +83 -0
  120. package/src/motion3/body.js +158 -0
  121. package/src/motion3/bones.js +47 -0
  122. package/src/motion3/carried.js +47 -0
  123. package/src/motion3/compile-job.js +79 -0
  124. package/src/motion3/compile-worker.js +10 -0
  125. package/src/motion3/compiler/anchors.js +113 -0
  126. package/src/motion3/compiler/arm-orient.js +128 -0
  127. package/src/motion3/compiler/channels.js +125 -0
  128. package/src/motion3/compiler/compile.js +205 -0
  129. package/src/motion3/compiler/gait-rhythm.js +97 -0
  130. package/src/motion3/compiler/grip.js +171 -0
  131. package/src/motion3/compiler/head-steady.js +48 -0
  132. package/src/motion3/compiler/hold.js +134 -0
  133. package/src/motion3/compiler/idle.js +113 -0
  134. package/src/motion3/compiler/limbs.js +197 -0
  135. package/src/motion3/compiler/math.js +72 -0
  136. package/src/motion3/compiler/pose.js +177 -0
  137. package/src/motion3/compiler/quat.js +49 -0
  138. package/src/motion3/compiler/skeleton.js +74 -0
  139. package/src/motion3/compiler/validate-common.js +32 -0
  140. package/src/motion3/compiler/validate-goals.js +118 -0
  141. package/src/motion3/compiler/validate-objects.js +144 -0
  142. package/src/motion3/compiler/validate-parts.js +118 -0
  143. package/src/motion3/compiler/validate.js +122 -0
  144. package/src/motion3/compiler/vocab.js +74 -0
  145. package/src/motion3/compiler/walk-pelvis.js +275 -0
  146. package/src/motion3/compiler/walk.js +207 -0
  147. package/src/motion3/compiler/wrist-clamp.js +32 -0
  148. package/src/motion3/delta.js +47 -0
  149. package/src/motion3/describe.js +177 -0
  150. package/src/motion3/docs.js +27 -0
  151. package/src/motion3/edit-carry.js +21 -0
  152. package/src/motion3/fix/ease.js +16 -0
  153. package/src/motion3/fix/ground.js +37 -0
  154. package/src/motion3/fix/index.js +52 -0
  155. package/src/motion3/fix/push.js +71 -0
  156. package/src/motion3/fix/reach.js +46 -0
  157. package/src/motion3/fix/retime.js +79 -0
  158. package/src/motion3/fix/self.js +211 -0
  159. package/src/motion3/fix/slide.js +70 -0
  160. package/src/motion3/fix/turn.js +51 -0
  161. package/src/motion3/follow.js +39 -0
  162. package/src/motion3/free-motion.js +204 -0
  163. package/src/motion3/grip-choice.js +130 -0
  164. package/src/motion3/hold-plan.js +157 -0
  165. package/src/motion3/json-patch.js +77 -0
  166. package/src/motion3/key-aim.js +105 -0
  167. package/src/motion3/key-edit.js +82 -0
  168. package/src/motion3/lenient.js +127 -0
  169. package/src/motion3/object-keys.js +236 -0
  170. package/src/motion3/object-pose.js +182 -0
  171. package/src/motion3/object-tracks.js +12 -0
  172. package/src/motion3/people.js +82 -0
  173. package/src/motion3/pipeline.js +102 -0
  174. package/src/motion3/playback.js +51 -0
  175. package/src/motion3/probes.js +91 -0
  176. package/src/motion3/qa/body-checks.js +34 -0
  177. package/src/motion3/qa/check-names.js +14 -0
  178. package/src/motion3/qa/checks.js +125 -0
  179. package/src/motion3/qa/contact-checks.js +51 -0
  180. package/src/motion3/qa/explain.js +80 -0
  181. package/src/motion3/qa/fmt.js +5 -0
  182. package/src/motion3/qa/foot-checks.js +72 -0
  183. package/src/motion3/qa/geometry.js +77 -0
  184. package/src/motion3/qa/goals.js +191 -0
  185. package/src/motion3/qa/ground.js +60 -0
  186. package/src/motion3/qa/hints-contact.js +195 -0
  187. package/src/motion3/qa/hints-goal.js +160 -0
  188. package/src/motion3/qa/hints-knee.js +24 -0
  189. package/src/motion3/qa/hints-motion.js +209 -0
  190. package/src/motion3/qa/hints-object.js +91 -0
  191. package/src/motion3/qa/index.js +44 -0
  192. package/src/motion3/qa/key-checks.js +82 -0
  193. package/src/motion3/qa/near.js +96 -0
  194. package/src/motion3/qa/object-checks.js +135 -0
  195. package/src/motion3/qa/patch.js +161 -0
  196. package/src/motion3/qa/people-checks.js +37 -0
  197. package/src/motion3/qa/severity.js +38 -0
  198. package/src/motion3/qa/spec-checks.js +65 -0
  199. package/src/motion3/qa/style.js +228 -0
  200. package/src/motion3/qa/wrist-checks.js +69 -0
  201. package/src/motion3/qa/wrist-obs.js +60 -0
  202. package/src/motion3/refs.js +129 -0
  203. package/src/motion3/repair-tune.js +121 -0
  204. package/src/motion3/repair.js +83 -0
  205. package/src/motion3/report.js +67 -0
  206. package/src/motion3/rigs/index.js +14 -0
  207. package/src/motion3/rigs/x-bot-tpose-body.json +1 -0
  208. package/src/motion3/rigs/x-bot-tpose.json +1 -0
  209. package/src/motion3/rigs/y-bot-tpose-body.json +1 -0
  210. package/src/motion3/rigs/y-bot-tpose.json +1 -0
  211. package/src/motion3/scene.js +60 -0
  212. package/src/motion3/schema/brief.js +89 -0
  213. package/src/motion3/schema/fields.js +44 -0
  214. package/src/motion3/schema/spec-examples.js +46 -0
  215. package/src/motion3/schema/topics.js +113 -0
  216. package/src/motion3/schema/types.js +149 -0
  217. package/src/motion3/search.js +117 -0
  218. package/src/motion3/second-hand.js +112 -0
  219. package/src/motion3/settle.js +136 -0
  220. package/src/motion3/skills/basic.js +83 -0
  221. package/src/motion3/skills/book.js +76 -0
  222. package/src/motion3/skills/car.js +145 -0
  223. package/src/motion3/skills/flip.js +200 -0
  224. package/src/motion3/skills/frames-auto.js +29 -0
  225. package/src/motion3/skills/impact.js +109 -0
  226. package/src/motion3/skills/index.js +85 -0
  227. package/src/motion3/skills/info.js +80 -0
  228. package/src/motion3/skills/smoke-kit.js +109 -0
  229. package/src/motion3/skills/smoke.js +160 -0
  230. package/src/motion3/spec-store.js +59 -0
  231. package/src/motion3/take-edit.js +155 -0
  232. package/src/motion3/take.js +63 -0
  233. package/src/motion3/timeline.js +127 -0
  234. package/src/motion3/views.js +121 -0
  235. package/src/motion3/walk-plan.js +119 -0
  236. package/src/object-catalog.jsx +7 -1
  237. package/src/object-colors.js +98 -0
  238. package/src/object-gizmo-parts.js +115 -0
  239. package/src/object-gizmo.jsx +37 -122
  240. package/src/object-glow.js +139 -0
  241. package/src/object-hidden.js +33 -0
  242. package/src/object-look.js +196 -0
  243. package/src/object-size.js +10 -0
  244. package/src/object-travel.js +71 -6
  245. package/src/panels/CameraPanel.jsx +7 -7
  246. package/src/panels/HdrEnvironmentControls.jsx +59 -0
  247. package/src/panels/LightPanel.jsx +25 -4
  248. package/src/panels/ObjectLookPanel.jsx +88 -0
  249. package/src/panels/PosePanel.jsx +2 -17
  250. package/src/panels/RigControlPanel.jsx +5 -53
  251. package/src/panels/SubjectBox.jsx +4 -1
  252. package/src/panels/details.css +14 -0
  253. package/src/posestudio.jsx +4 -27
  254. package/src/project-browser.jsx +3 -4
  255. package/src/project-resources.js +18 -31
  256. package/src/project.js +13 -104
  257. package/src/props.jsx +25 -280
  258. package/src/qa-label.js +25 -0
  259. package/src/rail-dot-drag.js +23 -0
  260. package/src/render-passes.js +10 -0
  261. package/src/resource-status.jsx +1 -0
  262. package/src/result-modal.jsx +4 -19
  263. package/src/room.jsx +20 -12
  264. package/src/scene-assets.js +8 -6
  265. package/src/scene-light-place.js +72 -0
  266. package/src/scene-lights.jsx +148 -0
  267. package/src/scene-objects.js +31 -139
  268. package/src/scenes.js +15 -105
  269. package/src/semantic-edit.js +2 -5
  270. package/src/set-pieces.jsx +183 -0
  271. package/src/settings-menu.jsx +3 -11
  272. package/src/shell/BottomDock.jsx +9 -51
  273. package/src/shell/DetailsSlot.jsx +16 -200
  274. package/src/shell/GhostsMenuGroup.jsx +74 -0
  275. package/src/shell/LibrarySlot.jsx +1 -3
  276. package/src/shell/MenuBar.jsx +3 -3
  277. package/src/shell/PreferencesDialog.jsx +15 -264
  278. package/src/shell/PreferencesSlot.jsx +2 -8
  279. package/src/shell/TopBar.jsx +5 -84
  280. package/src/shell/ViewportToolbar.jsx +10 -70
  281. package/src/shell/agent-glass.css +19 -6
  282. package/src/shell/details-selection.js +42 -0
  283. package/src/shell/ghosts-menu.css +22 -0
  284. package/src/shell/glass-regions.css +29 -3
  285. package/src/shell/preferences.css +6 -1
  286. package/src/shot-authoring.js +4 -7
  287. package/src/soft-shadows.js +37 -0
  288. package/src/stage-attach-frames.js +12 -12
  289. package/src/stage-canvas-rigs.jsx +3 -3
  290. package/src/stage-capture.jsx +42 -8
  291. package/src/stage-characters.jsx +22 -216
  292. package/src/stage-editor-gizmos.jsx +6 -6
  293. package/src/stage-environment.js +93 -0
  294. package/src/stage-hdr-builtins.js +107 -0
  295. package/src/stage-hdr-image.js +116 -0
  296. package/src/stage-hdr-library.js +70 -0
  297. package/src/stage-hdr-map.js +94 -0
  298. package/src/stage-hdr.js +130 -0
  299. package/src/stage-lighting.js +149 -0
  300. package/src/stage-occlusion.js +210 -0
  301. package/src/stage-sky.jsx +159 -0
  302. package/src/stage-startup-defaults.js +5 -9
  303. package/src/studio-actions.js +16 -14
  304. package/src/studio-agent-commands.js +19 -16
  305. package/src/studio-agent-motion.js +9 -9
  306. package/src/studio-agent-protocol.js +47 -77
  307. package/src/studio-app-binding.js +27 -47
  308. package/src/studio-asset-list.js +17 -0
  309. package/src/studio-contact-sheet.js +37 -5
  310. package/src/studio-elements.js +27 -7
  311. package/src/studio-entity-bounds.js +18 -0
  312. package/src/studio-error-codes.js +15 -0
  313. package/src/studio-export-framing.js +152 -0
  314. package/src/studio-lighting-check.js +103 -0
  315. package/src/studio-motion-visual.js +150 -0
  316. package/src/studio-schema-errors.js +80 -0
  317. package/src/studio-support-gap.js +74 -0
  318. package/src/styles.css +1 -19
  319. package/src/timeline-defaults.js +5 -0
  320. package/src/timeline-extent.js +8 -20
  321. package/src/tutorial-walk.js +17 -0
  322. package/src/ui-scrub.js +1 -1
  323. package/src/use-shot-camera-tracks.js +12 -0
  324. package/src/view-ghosts.js +97 -0
  325. package/src/view-ghosts.jsx +49 -0
  326. package/src/workflow/AgentPanel.jsx +47 -166
  327. package/src/workflow/AgentProgressCard.jsx +109 -0
  328. package/src/workflow/AgentReplyText.jsx +37 -0
  329. package/src/workflow/AgentTurnSteps.jsx +84 -0
  330. package/src/workflow/LoginPanel.jsx +140 -0
  331. package/src/workflow/WorkflowBuilder.jsx +7 -23
  332. package/src/workflow/agent-client.js +54 -51
  333. package/src/workflow/agent-panel.css +71 -1
  334. package/src/workflow/agent-progress-card.css +167 -0
  335. package/src/workflow/agent-progress-view.js +47 -0
  336. package/src/workflow/agent-turn-steps.css +113 -0
  337. package/src/workflow/agent-turn.js +191 -0
  338. package/src/workflow/login-client.js +34 -0
  339. package/src/workflow/motion-input.js +18 -31
  340. package/src/workflow/node-schema.js +1 -1
  341. package/src/workflow/reply-markdown.js +69 -0
  342. package/src/workflow/scene-asset-sync.js +8 -8
  343. package/tools/agent-session-stats.mjs +61 -0
  344. package/tools/dev-full.mjs +5 -56
  345. package/tools/eslint-baseline.json +12 -13
  346. package/tools/knip-baseline.json +0 -179
  347. package/tools/loc-baseline.json +31 -81
  348. package/tools/motion3/extract-body.mjs +103 -0
  349. package/tools/motion3/extract-rig.mjs +85 -0
  350. package/tools/process-supervisor.mjs +0 -88
  351. package/tools/run-tests.mjs +9 -125
  352. package/bin/agent/motion-runtime.mjs +0 -76
  353. package/dist/ardy/cskel27-rest.json +0 -557
  354. package/dist/assets/analytics-C9pMpW7s.js +0 -1
  355. package/dist/assets/app-B_2XB9rw.css +0 -1
  356. package/dist/assets/app-DAuvKPP8.js +0 -4920
  357. package/dist/assets/config-Bx7pYsD9.js +0 -1
  358. package/dist/assets/config-DeoFTYzb.css +0 -1
  359. package/dist/assets/demo-CvKuBgy5.js +0 -1
  360. package/dist/assets/scene-asset-sync-DKkpjKBV.js +0 -19
  361. package/dist/assets/scene-asset-sync-DjJ_cJ2R.css +0 -1
  362. package/dist/assets/ticket-BPk2Ni95.js +0 -1
  363. package/dist/assets/workflow-jJRBR94Z.js +0 -179
  364. package/dist/demo/walk-then-stop.npz +0 -0
  365. package/mcp/ardy-prompts.mjs +0 -218
  366. package/src/ardy/auto-fix-panel.css +0 -108
  367. package/src/ardy/auto-physics.js +0 -1194
  368. package/src/ardy/client.js +0 -153
  369. package/src/ardy/convert.js +0 -313
  370. package/src/ardy/cskel27-neutral.js +0 -39
  371. package/src/ardy/cskel27.js +0 -77
  372. package/src/ardy/export.js +0 -165
  373. package/src/ardy/fix-collisions.js +0 -2257
  374. package/src/ardy/foot-lock.js +0 -230
  375. package/src/ardy/motion-calibration.js +0 -118
  376. package/src/ardy/motion-edit.js +0 -213
  377. package/src/ardy/motion-url.js +0 -55
  378. package/src/ardy/mp4-duration.js +0 -102
  379. package/src/ardy/npz.js +0 -595
  380. package/src/ardy/physics-panel.css +0 -21
  381. package/src/ardy/physics-panel.jsx +0 -89
  382. package/src/ardy/physics-refine.js +0 -111
  383. package/src/ardy/physics-review.js +0 -567
  384. package/src/ardy/physics-temporal.js +0 -54
  385. package/src/ardy/platform-fit-panel.css +0 -64
  386. package/src/ardy/platform-fit-panel.jsx +0 -41
  387. package/src/ardy/platform-fit.js +0 -326
  388. package/src/ardy/playback.js +0 -711
  389. package/src/ardy/pose-pin.js +0 -97
  390. package/src/ardy/prompt-clips.js +0 -30
  391. package/src/ardy/retime.js +0 -211
  392. package/src/ardy/root-drop.js +0 -369
  393. package/src/ardy/timeline-resize.js +0 -13
  394. package/src/ardy/to-cskel27.js +0 -225
  395. package/src/ardy/trim.js +0 -33
  396. package/src/ardy/waypoints.js +0 -369
  397. package/src/ardy-bridge-contract.js +0 -281
  398. package/src/domains/motion/motion-video-capture.js +0 -664
  399. package/src/i2v-motion-client.js +0 -77
  400. package/src/i2v-motion-studio.jsx +0 -180
  401. package/src/line-edit.js +0 -1348
  402. package/src/motion/generation.js +0 -105
  403. package/src/motion-readiness-ui.jsx +0 -67
  404. package/src/motion-readiness.js +0 -40
  405. package/src/motion-resources.js +0 -177
  406. package/src/motion-store.js +0 -65
  407. package/src/motion-trail.js +0 -601
  408. package/src/multimodel-ingest.js +0 -394
  409. package/src/panels/PromptBlocksPanel.jsx +0 -444
  410. package/src/panels/VideoCapturePanel.jsx +0 -181
  411. package/src/pose-extract/browser.js +0 -42
  412. package/src/pose-extract/detector.js +0 -100
  413. package/src/pose-extract/fit.js +0 -227
  414. package/src/pose-extract/image-frame.js +0 -51
  415. package/src/pose-extract/index.js +0 -58
  416. package/src/pose-extract/keyframes.js +0 -74
  417. package/src/pose-extract/landmarks.js +0 -162
  418. package/src/pose-extract/mirror.js +0 -153
  419. package/src/pose-extract/take.js +0 -131
  420. package/src/pose-extract/video-frames.js +0 -91
  421. package/src/take-recipe.js +0 -165
  422. package/src/trail-key-conflicts.js +0 -39
  423. package/src/trail-pick.js +0 -59
  424. package/src/use-i2v-capture-modal.js +0 -15
  425. package/tools/ardy/BRIDGE.md +0 -37
  426. package/tools/ardy/GVHMR-NPZ.md +0 -68
  427. package/tools/ardy/README.md +0 -124
  428. package/tools/ardy/__pycache__/cclay_gvhmr_worker.cpython-313.pyc +0 -0
  429. package/tools/ardy/artifacts.mjs +0 -34
  430. package/tools/ardy/bridge.mjs +0 -1732
  431. package/tools/ardy/bvh-cskel27.mjs +0 -1265
  432. package/tools/ardy/cclay_constrained_generate.py +0 -561
  433. package/tools/ardy/cclay_gvhmr_worker.py +0 -264
  434. package/tools/ardy/cclay_motion_edit.py +0 -510
  435. package/tools/ardy/cclay_sequence_generate.py +0 -705
  436. package/tools/ardy/cclay_worker.py +0 -304
  437. package/tools/ardy/dump-npz.py +0 -205
  438. package/tools/ardy/extract-rest.mjs +0 -299
  439. package/tools/ardy/extract.mjs +0 -507
  440. package/tools/ardy/footage.mjs +0 -467
  441. package/tools/ardy/gvhmr-floor.mjs +0 -102
  442. package/tools/ardy/gvhmr-to-cskel27.mjs +0 -21
  443. package/tools/ardy/gvhmr_fastpath.py +0 -168
  444. package/tools/ardy/gvhmr_trajectory.py +0 -240
  445. package/tools/ardy/merge-text-encoder.py +0 -160
  446. package/tools/ardy/mocap-metrics.mjs +0 -200
  447. package/tools/ardy/mocap-quality-gate.mjs +0 -110
  448. package/tools/ardy/motion-stabilize.mjs +0 -348
  449. package/tools/ardy/npz.mjs +0 -414
  450. package/tools/ardy/pose-to-npz.mjs +0 -106
  451. package/tools/ardy/prompt-limits.mjs +0 -5
  452. package/tools/ardy/run-edit-on-box.sh +0 -73
  453. package/tools/ardy/run-local.mjs +0 -233
  454. package/tools/ardy/run-on-box.sh +0 -593
  455. package/tools/ardy/run-sequence-on-box.sh +0 -177
  456. package/tools/ardy/runners/gvhmr-worker.mjs +0 -206
  457. package/tools/ardy/runners/index.mjs +0 -18
  458. package/tools/ardy/runners/local.mjs +0 -455
  459. package/tools/ardy/runners/proc.mjs +0 -125
  460. package/tools/ardy/runners/remote.mjs +0 -252
  461. package/tools/ardy/setup-local.mjs +0 -189
  462. package/tools/ardy/setup-text-encoder-on-box.sh +0 -93
  463. package/tools/ardy/setup-text-encoder.py +0 -306
  464. package/tools/ardy/smpl-cskel27.mjs +0 -178
  465. package/tools/ardy/visual-qa.mjs +0 -196
  466. package/tools/ardy/vq-car.mjs +0 -52
  467. package/tools/bench/EXP3.md +0 -61
  468. package/tools/bench/cclay_bench_extract_incam.py +0 -125
  469. package/tools/bench/cclay_bench_extract_obs.py +0 -204
  470. package/tools/bench/cclay_bench_runner.py +0 -42
  471. package/tools/bench/cube-contact.mjs +0 -162
  472. package/tools/bench/exp3.mjs +0 -152
  473. package/tools/bench/extract-bench-lib.mjs +0 -216
  474. package/tools/bench/extract-bench.mjs +0 -302
  475. package/tools/bench/fal-generate.mjs +0 -88
  476. package/tools/bench/fit/README.md +0 -189
  477. package/tools/bench/fit/camera.mjs +0 -59
  478. package/tools/bench/fit/contact.mjs +0 -419
  479. package/tools/bench/fit/footlock.mjs +0 -234
  480. package/tools/bench/fit/motion.mjs +0 -85
  481. package/tools/bench/fit/pin.mjs +0 -27
  482. package/tools/bench/fit/remote.mjs +0 -105
  483. package/tools/bench/fit-bench.mjs +0 -126
  484. package/tools/bench/fit-sanity.mjs +0 -45
  485. package/tools/bench/metrics.mjs +0 -313
  486. package/tools/bench/obs/depth.mjs +0 -258
  487. package/tools/bench/obs/extrinsics.mjs +0 -175
  488. package/tools/bench/obs/fit_mannequin_betas.py +0 -340
  489. package/tools/bench/obs/ground.mjs +0 -294
  490. package/tools/bench/obs/heading.mjs +0 -151
  491. package/tools/bench/obs/ladder.mjs +0 -555
  492. package/tools/bench/obs/mannequin-betas.json +0 -124
  493. package/tools/bench/obs/remote.mjs +0 -83
  494. package/tools/bench/obs/rest_joints.py +0 -52
  495. package/tools/bench/obs/ybot-targets.mjs +0 -49
  496. package/tools/bench/obs-bench.mjs +0 -520
  497. package/tools/bench/score.mjs +0 -343
  498. package/tools/bench/summarize.mjs +0 -86
  499. package/tools/demo-worker/README.md +0 -137
  500. package/tools/demo-worker/api-client.mjs +0 -233
  501. package/tools/demo-worker/generate.mjs +0 -301
  502. package/tools/demo-worker/index.mjs +0 -466
  503. package/tools/demo-worker/sign-core.mjs +0 -112
  504. package/tools/demo-worker/sign.mjs +0 -161
  505. package/tools/gt-render/browser.mjs +0 -182
  506. package/tools/gt-render/camera-math.mjs +0 -276
  507. package/tools/gt-render/page.mjs +0 -267
  508. package/tools/gt-render/render.mjs +0 -514
  509. package/tools/gt-render/scene-box.mjs +0 -31
  510. package/tools/gt-render/take-transform.mjs +0 -87
  511. package/tools/kimodo/compare-seam.mjs +0 -108
  512. package/tools/kimodo/constraints.mjs +0 -244
  513. package/tools/kimodo/edit.mjs +0 -259
  514. package/tools/kimodo/effector-constraints.mjs +0 -200
  515. package/tools/kimodo/generate.mjs +0 -646
  516. package/tools/kimodo/local-output.mjs +0 -265
  517. package/tools/kimodo/measure-pose.mjs +0 -65
  518. package/tools/kimodo/measure-preserve.mjs +0 -899
  519. package/tools/kimodo/measure-waypoints.mjs +0 -55
  520. package/tools/kimodo/mlx-generate.py +0 -111
  521. package/tools/kimodo/pose-constraints.mjs +0 -248
  522. package/tools/kimodo/preserve-mask.mjs +0 -543
  523. package/tools/kimodo/read-npz.mjs +0 -189
  524. package/tools/kimodo/run-edit-on-box.mjs +0 -155
  525. package/tools/kimodo/run-sequence-on-box.mjs +0 -178
  526. package/tools/kimodo/runner.mjs +0 -284
  527. package/tools/kimodo/seam-batch.mjs +0 -77
  528. package/tools/kimodo/setup-local.mjs +0 -30
  529. package/tools/kimodo/setup-on-box.sh +0 -339
  530. package/tools/kimodo/soma77-to-cskel27.mjs +0 -305
  531. package/tools/morphgs/assets/gen_truth.py +0 -39
  532. package/tools/morphgs/assets/mesh_ori_rig.txt +0 -27
  533. package/tools/morphgs/assets/playback-check.mjs +0 -30
  534. package/tools/morphgs/demo-gate.sh +0 -26
  535. package/tools/morphgs/fbx2morphgs.mjs +0 -68
  536. package/tools/morphgs/morphgs-to-cskel27.mjs +0 -105
  537. package/tools/morphgs/patches/preprocess_src-none-mode.patch +0 -76
  538. package/tools/morphgs/setup-on-cluster.sh +0 -126
  539. package/tools/projflow/driver.py +0 -1069
  540. package/tools/projflow/generate.mjs +0 -805
  541. package/tools/projflow/hml22-to-cskel27.mjs +0 -735
  542. package/tools/projflow/line-edit-job.mjs +0 -547
  543. package/tools/projflow/replay.mjs +0 -452
  544. package/tools/projflow/runner.mjs +0 -269
  545. package/tools/projflow/service.mjs +0 -742
  546. package/tools/track/DESIGN.md +0 -513
  547. package/tools/track/backfill-provenance.mjs +0 -116
  548. package/tools/track/budget.mjs +0 -8
  549. package/tools/track/check-rig.mjs +0 -209
  550. package/tools/track/diagnostics.schema.json +0 -60
  551. package/tools/track/export-rig.mjs +0 -256
  552. package/tools/track/fallback.mjs +0 -7
  553. package/tools/track/fk-parity-fixture.mjs +0 -161
  554. package/tools/track/gate.mjs +0 -349
  555. package/tools/track/masks.mjs +0 -318
  556. package/tools/track/metrics.mjs +0 -201
  557. package/tools/track/publish-obs.mjs +0 -108
  558. package/tools/track/py/check_env.py +0 -61
  559. package/tools/track/py/eval_lr.py +0 -229
  560. package/tools/track/py/lr_viterbi.py +0 -294
  561. package/tools/track/py/masks.py +0 -405
  562. package/tools/track/py/objective.py +0 -382
  563. package/tools/track/py/rig.py +0 -508
  564. package/tools/track/py/scene.py +0 -326
  565. package/tools/track/py/test_joint_indices.py +0 -123
  566. package/tools/track/py/test_lr_viterbi.py +0 -195
  567. package/tools/track/py/test_masks.py +0 -229
  568. package/tools/track/py/test_rig.py +0 -288
  569. package/tools/track/py/test_scene.py +0 -354
  570. package/tools/track/py/test_track.py +0 -328
  571. package/tools/track/py/track.py +0 -411
  572. package/tools/track/remote.mjs +0 -140
  573. package/tools/track/rig-dump.mjs +0 -227
  574. package/tools/track/run-box-tests.mjs +0 -14
  575. package/tools/track/run-box.mjs +0 -168
  576. package/tools/track/setup-box.sh +0 -59
  577. package/tools/track/study-2d.mjs +0 -728
  578. /package/src/{ardy → motion}/collision-blockers.js +0 -0
  579. /package/src/{ardy → motion}/ground.js +0 -0
  580. /package/src/{ardy → motion}/ik-drag.js +0 -0
  581. /package/src/{ardy → motion}/ik-key-json.js +0 -0
  582. /package/src/{ardy → motion}/ik.js +0 -0
  583. /package/src/{ardy → motion}/physics-surface.js +0 -0
  584. /package/src/{ardy → motion}/playback-clock.js +0 -0
  585. /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
- }