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,229 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import contextlib
4
- import os
5
-
6
- import numpy as np
7
- import pytest
8
-
9
- from masks import (
10
- NoPersonPrompt,
11
- box_from_xys,
12
- choose_anchor,
13
- main,
14
- plan_chunks,
15
- prompt_boxes,
16
- prompt_points,
17
- reprompt_frames,
18
- segment,
19
- should_reprompt,
20
- track_clip,
21
- )
22
-
23
-
24
- def test_reprompt_rule_on_synthetic_area_sequence():
25
- areas = [1000, 1100, 1200, 600, 620, 1000, 1000, 0, 0, 500, 700]
26
- # 1200->600 (-50 %), 620->1000 (+61 %), 1000->0 (-100 %); 0->500 is a recovery, 500->700 is +40 % (not > 40 %).
27
- assert reprompt_frames(areas) == [3, 5, 7]
28
- assert reprompt_frames(areas, eligible=[True] * 5 + [False] * 6) == [3]
29
- assert not should_reprompt(1000, 1400)
30
- assert should_reprompt(1000, 1401)
31
- assert not should_reprompt(0, 5000)
32
-
33
-
34
- def test_box_conversion_clips_and_rejects():
35
- assert box_from_xys([100, 50, 40], 832, 480).tolist() == [80, 30, 120, 70]
36
- assert box_from_xys([441.5, 49.0, 249.4], 832, 480)[1] == 0.0 # clipped at the top edge
37
- assert box_from_xys([10, 10, 0], 832, 480) is None
38
- assert box_from_xys([np.nan, 10, 50], 832, 480) is None
39
- assert box_from_xys([-500, 10, 50], 832, 480) is None # entirely outside the image
40
-
41
-
42
- def test_empty_bbx_raises_no_person_prompt():
43
- for bbx in (None, np.zeros((0, 3), np.float32), np.zeros((5, 3), np.float32)):
44
- with pytest.raises(NoPersonPrompt, match="no-person-prompt"):
45
- prompt_boxes(bbx, 832, 480)
46
-
47
-
48
- def test_cli_empty_bbx_exits_3_without_output(tmp_path, capsys):
49
- obs = tmp_path / "obs.npz"
50
- np.savez(obs, bbx_xys=np.zeros((0, 3), np.float32), kp2d=np.zeros((0, 17, 3), np.float32))
51
- out = tmp_path / "out"
52
- assert main(["--video", str(tmp_path / "missing.mp4"), "--obs", str(obs), "--out", str(out)]) == 3
53
- assert "no-person-prompt" in capsys.readouterr().err
54
- assert not out.exists()
55
-
56
-
57
- def test_prompt_points_use_confident_joints_inside_the_box():
58
- box = np.array([100, 100, 300, 400], np.float32)
59
- kp = np.zeros((17, 3))
60
- kp[:, :2] = 200
61
- kp[:, 2] = 0.9
62
- kp[5, :2], kp[6, :2] = (180, 150), (220, 150) # shoulders -> midpoint (200, 150)
63
- kp[0, :2], kp[0, 2] = (200, 120), 0.9 # nose
64
- kp[9, 2] = 0.1 # unconfident wrist is dropped
65
- kp[16, :2] = (500, 390) # ankle outside the box is dropped
66
- points = prompt_points("joints", box, kp)
67
- assert [tuple(p) for p in points.tolist()] == [(200, 150), (200, 200), (200, 120), (200, 200), (200, 200)]
68
- kp[15, 2] = 0.6 # an ankle needs ANKLE_TRUST (hidden feet behind props score 0.46-0.71)
69
- assert len(prompt_points("joints", box, kp)) == 4
70
- kp[9, 2] = 0.6 # a wrist at the same confidence is kept
71
- assert len(prompt_points("joints", box, kp)) == 5
72
- assert prompt_points("box", box, kp) is None
73
- assert prompt_points("centre", box, kp).tolist() == [[200, 250]]
74
- assert prompt_points("joints", box, np.zeros((17, 3))) is None # nothing confident: box only
75
- assert prompt_points("joints", None, kp) is None
76
- with pytest.raises(ValueError):
77
- prompt_points("palm", box, kp)
78
-
79
-
80
- def test_anchor_skips_untrusted_leading_frames():
81
- boxes = [np.array([0, 0, 10, 10], np.float32)] * 6
82
- assert choose_anchor(boxes, [0.02, 0.1, 0.6, 0.9, 0.9, 0.9]) == 2
83
- assert choose_anchor(boxes, [0.02, 0.3, 0.1, 0.2, 0.4, 0.1]) == 4 # nothing trusted: most confident
84
- assert choose_anchor([None, None] + boxes[:4], [0.9] * 6) == 2
85
-
86
-
87
- def test_chunk_plan_covers_every_frame_with_shared_boundaries():
88
- assert plan_chunks(124, 0, 180) == [(1, 0, 123)]
89
- assert plan_chunks(362, 0, 180) == [(1, 0, 179), (1, 179, 358), (1, 358, 361)]
90
- assert plan_chunks(20, 5, 8) == [(1, 5, 12), (1, 12, 19), (-1, 0, 5)]
91
- for frames, anchor, size in ((362, 0, 180), (20, 5, 8), (20, 19, 4), (1, 0, 2)):
92
- chunks = plan_chunks(frames, anchor, size)
93
- covered = set()
94
- for _, start, end in chunks:
95
- assert end - start + 1 <= size
96
- covered.update(range(start, end + 1))
97
- assert covered == set(range(frames))
98
-
99
-
100
- class FakePredictor:
101
- """SAM2 video-predictor stand-in: frame t tracks to TRUE area, except in the
102
- drift window where tracking alone shrinks the mask to 30 % until a box prompt
103
- is placed inside the window. Mask seeds reproduce their own area."""
104
-
105
- def __init__(self, count, drift, hw=(20, 50)):
106
- self.count, self.drift, self.hw = count, drift, hw
107
- self.true = 600
108
- self.box_prompts = []
109
- self.recomputed = []
110
-
111
- def open_chunk(self, start, end):
112
- @contextlib.contextmanager
113
- def chunk():
114
- yield {"start": start, "n": end - start + 1, "prompts": {},
115
- "output_dict_per_obj": {0: {"non_cond_frame_outputs": {}}}, "frames_tracked_per_obj": {0: {}}}
116
- return chunk()
117
-
118
- def logits(self, area):
119
- import torch
120
-
121
- flat = torch.full((self.hw[0] * self.hw[1],), -10.0)
122
- flat[:area] = 10.0
123
- return flat.reshape(1, 1, *self.hw)
124
-
125
- def add_new_points_or_box(self, state, frame_idx, obj_id, box=None, points=None, labels=None):
126
- assert frame_idx not in state["frames_tracked_per_obj"][0], "re-prompt must be a fresh conditioning frame"
127
- state["prompts"][frame_idx] = ("box", self.true)
128
- self.box_prompts.append(state["start"] + frame_idx)
129
-
130
- def add_new_mask(self, state, frame_idx, obj_id, mask):
131
- state["prompts"][frame_idx] = ("mask", int(mask.sum()))
132
-
133
- def propagate_in_video(self, state, start_frame_idx, reverse=False):
134
- order = range(start_frame_idx, -1, -1) if reverse else range(start_frame_idx, state["n"])
135
- for local in order:
136
- t = state["start"] + local
137
- if local in state["prompts"]:
138
- area = state["prompts"][local][1]
139
- elif t in self.drift and not any(p in self.drift and (p <= t if not reverse else p >= t) for p in self.box_prompts):
140
- area = int(self.true * 0.3)
141
- else:
142
- area = self.true
143
- if local in state["output_dict_per_obj"][0]["non_cond_frame_outputs"]:
144
- self.recomputed.append(t)
145
- state["output_dict_per_obj"][0]["non_cond_frame_outputs"][local] = area
146
- state["frames_tracked_per_obj"][0][local] = {"reverse": reverse}
147
- yield local, [1], self.logits(area)
148
-
149
-
150
- def run_fake(count, anchor, size, drift, trusted):
151
- fake = FakePredictor(count, set(drift))
152
- boxes = [np.array([0, 0, 10, 10], np.float32)] * count
153
- prob, area, reprompted = track_clip(fake, fake.open_chunk, count, (10, 25), boxes, np.array(trusted),
154
- anchor, plan_chunks(count, anchor, size))
155
- return fake, prob, area, reprompted
156
-
157
-
158
- def test_track_clip_reprompts_drift_once_and_resumes():
159
- fake, prob, area, reprompted = run_fake(20, 5, 8, range(14, 18), [False] * 5 + [True] * 15)
160
- assert reprompted == [14]
161
- assert fake.box_prompts == [5, 14]
162
- assert area.tolist() == [600] * 20
163
- assert prob.dtype == np.float16 and prob.shape == (20, 10, 25)
164
- assert float(prob.min()) >= 0.0 and float(prob.max()) <= 1.0
165
- assert fake.recomputed == [] # no frame is tracked twice: the restart starts at the re-prompted frame
166
-
167
-
168
- def test_track_clip_never_reprompts_untrusted_frames():
169
- fake, _, area, reprompted = run_fake(20, 5, 8, range(1, 3), [False] * 5 + [True] * 15)
170
- assert reprompted == []
171
- assert fake.box_prompts == [5]
172
- assert area[1:3].tolist() == [180, 180] and area[0] == 600
173
-
174
-
175
- def draw_person(frame, cx, cy, scale):
176
- import cv2
177
-
178
- mask = np.zeros(frame.shape[:2], np.uint8)
179
- cv2.circle(mask, (cx, cy - int(70 * scale)), int(14 * scale), 255, -1)
180
- cv2.ellipse(mask, (cx, cy - int(25 * scale)), (int(18 * scale), int(34 * scale)), 0, 0, 360, 255, -1)
181
- for dx in (-10, 10):
182
- cv2.line(mask, (cx + dx, cy), (cx + dx * 2, cy + int(60 * scale)), 255, int(10 * scale))
183
- cv2.line(mask, (cx + dx * 2, cy - int(50 * scale)), (cx + dx * 4, cy - int(10 * scale)), 255, int(8 * scale))
184
- frame[mask > 0] = (60, 90, 200)
185
- return mask > 0
186
-
187
-
188
- def test_sam2_synthetic_clip_backward_and_forward(tmp_path):
189
- cv2 = pytest.importorskip("cv2")
190
- count, width, height = 20, 832, 480
191
- rng = np.random.default_rng(3)
192
- background = np.full((height, width, 3), (200, 205, 210), np.uint8)
193
- background[height // 2:] = (170, 180, 185)
194
- for x in range(0, width, 64):
195
- cv2.line(background, (x, height // 2), (x - 200, height), (150, 150, 150), 2)
196
- background = np.clip(background.astype(int) + rng.integers(-4, 5, background.shape), 0, 255).astype(np.uint8)
197
- video = str(tmp_path / "clip.avi")
198
- writer = cv2.VideoWriter(video, cv2.VideoWriter_fourcc(*"MJPG"), 24, (width, height))
199
- truth, bbx = [], []
200
- for t in range(count):
201
- frame = background.copy()
202
- cx, cy = 250 + 12 * t, 300
203
- mask = draw_person(frame, cx, cy, 1.4)
204
- truth.append(mask)
205
- ys, xs = np.nonzero(mask)
206
- size = 1.2 * max(np.ptp(xs), np.ptp(ys))
207
- bbx.append([(xs.min() + xs.max()) / 2, (ys.min() + ys.max()) / 2, size])
208
- writer.write(frame)
209
- writer.release()
210
- bbx = np.array(bbx, np.float32)
211
- bbx[:5] = [441.5, 49.0, 249.4] # garbage leading boxes, as GVHMR emits before its first detection
212
- kp2d = np.zeros((count, 17, 3), np.float32)
213
- kp2d[:, :, 2] = 0.9
214
- kp2d[:5, :, 2] = 0.02
215
- obs = str(tmp_path / "obs.npz")
216
- np.savez(obs, bbx_xys=bbx, kp2d=kp2d)
217
-
218
- arrays, summary = segment(video, obs, chunk_frames=8)
219
- assert summary["anchor"] == 5
220
- assert summary["chunks"] == [[1, 5, 12], [1, 12, 19], [-1, 0, 5]]
221
- assert arrays["prob"].shape == (count, height // 2, width // 2) and arrays["prob"].dtype == np.float16
222
- assert summary["peakReservedMiB"] <= 5632
223
- ious = []
224
- for t in range(count):
225
- prob = cv2.resize(arrays["prob"][t].astype(np.float32), (width, height), interpolation=cv2.INTER_LINEAR)
226
- pred = prob > 0.5
227
- ious.append((pred & truth[t]).sum() / max(1, (pred | truth[t]).sum()))
228
- assert min(ious) >= 0.8, [round(v, 3) for v in ious]
229
- assert int(np.median(ious) * 100) >= 90, [round(v, 3) for v in ious]
@@ -1,288 +0,0 @@
1
- """Parity tests for rig.py (plan mocap-rearch todo 6, tests (a)-(e) + malformed input).
2
-
3
- Data (resolved in this order; a missing file FAILS, never skips):
4
- - parity fixture: <repo>/test/fixtures/track-fk-parity.json
5
- (node tools/track/fk-parity-fixture.mjs)
6
- - rig npz: $COZYFIT_RIG, <repo>/node_modules/.cache/cozyfit/rig-y-bot-tpose.npz
7
- (node tools/track/export-rig.mjs --model y-bot-tpose)
8
- - truth motion: $COZYFIT_WALK, <repo>/../../evidence/prep/gt-motions/walk.npz
9
- On the box (tools/track/run-box-tests.mjs uploads only tools/track/py/*.py) the three
10
- files are read from ~/cclay-ingest/cozyfit/testdata/ - stage them with
11
- scp test/fixtures/track-fk-parity.json node_modules/.cache/cozyfit/rig-y-bot-tpose.npz \
12
- <evidence>/prep/gt-motions/walk.npz ubuntu-baremetal:cclay-ingest/cozyfit/testdata/
13
-
14
- Measured errors are printed as one `[rig-parity] {json}` line when pytest exits, so a
15
- passing log carries the numbers, not just dots.
16
- """
17
- import atexit
18
- import json
19
- import os
20
- from pathlib import Path
21
-
22
- import numpy as np
23
- import pytest
24
- import torch
25
-
26
- import rig as rigmod
27
- from rig import RigError, State
28
-
29
- HERE = Path(__file__).resolve().parent
30
- # <repo>/tools/track/py locally; a flat /tmp/cozyfit-* upload dir on the box (no repo around it)
31
- REPO = HERE.parents[2] if len(HERE.parents) > 2 else HERE
32
- EVIDENCE = REPO.parents[1] / "evidence" if len(REPO.parents) > 1 else REPO / "evidence"
33
- BOX_DATA = Path.home() / "cclay-ingest" / "cozyfit" / "testdata"
34
- PARITY: dict = {}
35
-
36
-
37
- @atexit.register
38
- def _report():
39
- if PARITY:
40
- print("[rig-parity] " + json.dumps(PARITY, sort_keys=True))
41
-
42
-
43
- def _find(label, env, *candidates):
44
- paths = ([Path(os.environ[env])] if env and os.environ.get(env) else []) + [Path(c) for c in candidates]
45
- for p in paths:
46
- if p.is_file():
47
- return p
48
- pytest.fail(f"missing {label}; looked in {[str(p) for p in paths]} (see test_rig.py docstring for staging)")
49
-
50
-
51
- @pytest.fixture(scope="module")
52
- def fixture():
53
- path = _find("fk parity fixture", None, REPO / "test/fixtures/track-fk-parity.json", BOX_DATA / "track-fk-parity.json")
54
- return json.loads(path.read_text())
55
-
56
-
57
- @pytest.fixture(scope="module")
58
- def rig_path():
59
- return _find("rig npz", "COZYFIT_RIG", REPO / "node_modules/.cache/cozyfit/rig-y-bot-tpose.npz", BOX_DATA / "rig-y-bot-tpose.npz")
60
-
61
-
62
- @pytest.fixture(scope="module")
63
- def rig(rig_path, fixture):
64
- torch.manual_seed(0)
65
- r = rigmod.load_rig(rig_path)
66
- source = (r.meta.get("source") or {}).get("sha256")
67
- assert source == fixture["fbxSha256"], "rig npz and parity fixture come from different FBX files; regenerate one"
68
- assert r.bone_names == fixture["bones"], "rig npz bone order differs from the fixture"
69
- return r
70
-
71
-
72
- def _err(a, b):
73
- return float(torch.linalg.norm(torch.as_tensor(a, dtype=torch.float64) - torch.as_tensor(b, dtype=torch.float64), dim=-1).max())
74
-
75
-
76
- # (a) ---------------------------------------------------------------------------------
77
-
78
- def test_a_identity_reproduces_neutral(rig, fixture):
79
- canonical = torch.tensor(fixture["canonicalPosedJoints"]) # JS canonicalCskel27Reference().posed_joints
80
- state = State.identity(1, root=(0.0, -rig.min_y, 0.0))
81
- joints, globals_ = rigmod.cskel27_fk(state, None, rig)
82
- err = _err(joints[0], canonical)
83
- ones, _ = rigmod.cskel27_fk(state, np.ones(27, dtype=np.float32), rig)
84
- PARITY["a_identity_max_m"] = err
85
- assert err < 1e-5, f"identity pose vs neutral posedJoints: {err:.3e} m"
86
- assert _err(ones, joints) < 1e-7
87
- assert torch.allclose(globals_, torch.eye(3).expand_as(globals_))
88
-
89
-
90
- # (b) ---------------------------------------------------------------------------------
91
-
92
- def test_b_layer1_matches_truth_posed_joints(rig):
93
- path = _find("truth motion walk.npz", "COZYFIT_WALK", EVIDENCE / "prep/gt-motions/walk.npz", BOX_DATA / "walk.npz")
94
- with np.load(path) as z:
95
- rot, root, posed = z["local_rot_mats"], z["root_positions"], z["posed_joints"]
96
- bone_scale = z["bone_scale"] if "bone_scale" in z else None
97
- state = State.from_motion(rot, root)
98
- joints, _ = rigmod.cskel27_fk(state, bone_scale, rig)
99
- per_frame = torch.linalg.norm(joints.double() - torch.as_tensor(posed, dtype=torch.float64), dim=-1).amax(dim=1)
100
- PARITY["b_walk_frames"] = int(len(per_frame))
101
- PARITY["b_walk_max_m"] = float(per_frame.max())
102
- PARITY["b_walk_bone_scale"] = "npz" if bone_scale is not None else "none (canonical)"
103
- assert len(per_frame) == len(posed) > 100
104
- assert float(per_frame.max()) < 1e-4, f"worst frame {int(per_frame.argmax())}: {float(per_frame.max()):.3e} m"
105
-
106
-
107
- def test_b_layer1_matches_js_fk_with_bone_scale(rig, fixture):
108
- worst = 0.0
109
- for case in fixture["cases"]:
110
- rot = np.asarray(case["rotMats"], dtype=np.float32).reshape(-1, 27, 3, 3)
111
- root = np.asarray(case["rootPos"], dtype=np.float32).reshape(-1, 3)
112
- posed = np.asarray(case["posedJoints"], dtype=np.float32).reshape(-1, 27, 3)
113
- joints, _ = rigmod.cskel27_fk(State.from_motion(rot, root), case["boneScale"], rig)
114
- worst = max(worst, _err(joints, posed))
115
- PARITY["b_js_fk_max_m"] = worst
116
- assert worst < 1e-4
117
-
118
-
119
- # (c) ---------------------------------------------------------------------------------
120
-
121
- def test_c_rot6d_round_trip():
122
- g = torch.Generator().manual_seed(6)
123
- q = torch.randn(4096, 4, generator=g)
124
- R = rigmod.quat_to_matrix(q)
125
- back = rigmod.rot6d_to_matrix(rigmod.matrix_to_rot6d(R))
126
- err = float((back - R).abs().max())
127
- # arbitrary (non-orthonormal) 6D vectors land on SO(3) and are a fixed point after one pass.
128
- # The property is checked in float64; float32 Gram-Schmidt on near-parallel random column
129
- # pairs loses ~1e-5 to cancellation (measured 1.02e-5 on the box), so its bound is looser.
130
- a = torch.randn(4096, 6, generator=g, dtype=torch.float64)
131
- Ra = rigmod.rot6d_to_matrix(a)
132
- ortho = float((Ra.transpose(-1, -2) @ Ra - torch.eye(3, dtype=torch.float64)).abs().max())
133
- det = float((torch.linalg.det(Ra) - 1).abs().max())
134
- fixed = float((rigmod.rot6d_to_matrix(rigmod.matrix_to_rot6d(Ra)) - Ra).abs().max())
135
- Ra32 = rigmod.rot6d_to_matrix(a.float())
136
- ortho32 = float((Ra32.transpose(-1, -2) @ Ra32 - torch.eye(3)).abs().max())
137
- PARITY["c_rot6d_round_trip_max"] = err
138
- PARITY["c_rot6d_orthonormal_f64_max"] = ortho
139
- PARITY["c_rot6d_orthonormal_f32_max"] = ortho32
140
- assert err < 1e-5, f"6D round trip {err:.3e}"
141
- assert ortho < 1e-12 and det < 1e-12 and fixed < 1e-12, (ortho, det, fixed)
142
- assert ortho32 < 1e-4, f"float32 orthonormality {ortho32:.3e}"
143
- eye = rigmod.rot6d_to_matrix(torch.tensor([1.0, 0, 0, 0, 1, 0]))
144
- assert torch.equal(eye, torch.eye(3))
145
-
146
-
147
- # (d) ---------------------------------------------------------------------------------
148
-
149
- def test_d_gradients_finite(rig, fixture):
150
- case = fixture["cases"][0]
151
- rot = np.asarray(case["rotMats"], dtype=np.float32).reshape(-1, 27, 3, 3)
152
- root = np.asarray(case["rootPos"], dtype=np.float32).reshape(-1, 3)
153
- state = State.from_motion(rot, root)
154
- g = torch.Generator().manual_seed(7)
155
- for name in ("transl", "root6d", "local6d"):
156
- t = getattr(state, name)
157
- setattr(state, name, (t + 0.01 * torch.randn(t.shape, generator=g)).requires_grad_(True))
158
- joints, globals_ = rigmod.cskel27_fk(state, case["boneScale"], rig)
159
- _, verts = rigmod.studio_skin(globals_, joints, rig, bone_scale=case["boneScale"])
160
- loss = joints.square().sum() + verts.square().mean()
161
- loss.backward()
162
- norms = {name: float(getattr(state, name).grad.norm()) for name in ("transl", "root6d", "local6d")}
163
- PARITY["d_grad_norms"] = norms
164
- for name in norms:
165
- grad = getattr(state, name).grad
166
- assert torch.isfinite(grad).all(), name
167
- assert norms[name] > 0, name
168
- # every limb rotation reaches the skinned surface (no detached joint)
169
- driven = [j for j in range(1, 27) if rig.prep_bone[j] >= 0]
170
- assert (state.local6d.grad[:, [j - 1 for j in driven]].abs().sum(dim=(0, 2)) > 0).all()
171
-
172
-
173
- # (e) ---------------------------------------------------------------------------------
174
-
175
- def test_e_layer2_matches_studio_playback(rig, fixture):
176
- idx = fixture["vertexIndices"]
177
- bone_err, rot_err, vert_err = 0.0, 0.0, 0.0
178
- for case in fixture["cases"]:
179
- rot = torch.tensor(case["rotMats"]).reshape(-1, 27, 3, 3)
180
- posed = torch.tensor(case["posedJoints"]).reshape(-1, 27, 3)
181
- f, a = case["frame"], case["anchorFrame"]
182
- globals_ = rigmod.global_rotations(rot[f:f + 1], rig.parents)
183
- anchor = posed[a, 0, [0, 2]]
184
- W, verts = rigmod.studio_skin(globals_, posed[f:f + 1], rig, bone_scale=case["boneScale"],
185
- anchor_xz=anchor, vertex_indices=idx)
186
- ref_W = torch.tensor(case["boneWorld"]).reshape(-1, 4, 4)
187
- ref_v = torch.tensor(case["vertices"]).reshape(-1, 3)
188
- bone_err = max(bone_err, _err(W[0, :, :3, 3], ref_W[:, :3, 3]))
189
- # linear part carries the 0.01 rig root scale; compare the rotation itself
190
- rot_err = max(rot_err, float((W[0, :, :3, :3] - ref_W[:, :3, :3]).abs().max()) / rig.root_scale)
191
- vert_err = max(vert_err, _err(verts[0], ref_v))
192
- PARITY.update(e_cases=len(fixture["cases"]), e_bones=len(fixture["bones"]), e_vertices=len(idx),
193
- e_bone_pos_max_m=bone_err, e_bone_mat_max=rot_err, e_vertex_max_m=vert_err)
194
- assert bone_err < 1e-4, f"bone world position {bone_err:.3e} m"
195
- assert rot_err < 1e-4, f"bone world rotation entries {rot_err:.3e}"
196
- assert vert_err < 1e-4, f"skinned vertex {vert_err:.3e} m"
197
-
198
-
199
- def test_e_parity_detects_a_wrong_mode(rig, fixture):
200
- """Guard against a vacuous parity: dropping boneScale must move the arms by > 1 mm."""
201
- case = fixture["cases"][0]
202
- rot = torch.tensor(case["rotMats"]).reshape(-1, 27, 3, 3)
203
- posed = torch.tensor(case["posedJoints"]).reshape(-1, 27, 3)
204
- globals_ = rigmod.global_rotations(rot[1:2], rig.parents)
205
- _, verts = rigmod.studio_skin(globals_, posed[1:2], rig, bone_scale=None, anchor_xz=posed[0, 0, [0, 2]],
206
- vertex_indices=fixture["vertexIndices"])
207
- wrong = _err(verts[0], torch.tensor(case["vertices"]).reshape(-1, 3))
208
- PARITY["e_wrong_mode_vertex_max_m"] = wrong
209
- assert wrong > 1e-3
210
-
211
-
212
- # decimated tracker mesh ----------------------------------------------------------------
213
-
214
- def test_decimated_mesh(rig):
215
- faces, verts = rig.dec_faces, rig.dec_vertices
216
- w = rig.dec_skin_weight.double()
217
- PARITY.update(dec_faces=int(faces.shape[0]), dec_vertices=int(verts.shape[0]),
218
- full_vertices=int(rig.vertices.shape[0]))
219
- assert 1000 < faces.shape[0] <= rigmod.MAX_FACES
220
- assert int(faces.max()) < verts.shape[0] and int(faces.min()) >= 0
221
- assert (w >= 0).all() and float((w.sum(dim=1) - 1).abs().max()) < 1e-6
222
- assert torch.allclose(rig.dec_weights.sum(dim=1), torch.ones(len(verts)), atol=1e-6)
223
- # weights come from the nearest original vertex: that vertex is close by
224
- gap = torch.linalg.norm(verts - rig.vertices[torch.as_tensor(rig.dec_source)], dim=-1)
225
- PARITY["dec_source_gap_max_m"] = float(gap.max())
226
- assert float(gap.max()) < 0.05
227
- # rest pose: identity-ish skinning leaves the rest mesh where it is
228
- B = len(rig.bone_names)
229
- rest_worlds = torch.linalg.inv(rig.bind_inverse).unsqueeze(0)
230
- moved = rigmod.lbs(rest_worlds, rig, verts, rig.dec_weights)
231
- assert rest_worlds.shape == (1, B, 4, 4)
232
- assert float((moved[0] - verts).abs().max()) < 1e-4
233
-
234
-
235
- # malformed input -------------------------------------------------------------------------
236
-
237
- def _rewrite(rig_path, tmp_path, **changes):
238
- with np.load(rig_path) as z:
239
- members = {k: z[k] for k in z.files}
240
- for key, value in changes.items():
241
- if value is None:
242
- members.pop(key)
243
- else:
244
- members[key] = value
245
- out = tmp_path / "rig.npz"
246
- np.savez(out, **members)
247
- return out
248
-
249
-
250
- def test_bad_skin_weights_rejected(rig_path, tmp_path):
251
- with np.load(rig_path) as z:
252
- w = z["skin_weight"].copy()
253
- w[123] *= 0.9
254
- with pytest.raises(RigError) as err:
255
- rigmod.load_rig(_rewrite(rig_path, tmp_path, skin_weight=w))
256
- assert err.value.code == "bad-skin-weights" and "vertex 123" in str(err.value)
257
- w = np.load(rig_path)["skin_weight"].copy()
258
- w[7, 0], w[7, 1] = -0.25, w[7, 1] + 0.25 + w[7, 0]
259
- with pytest.raises(RigError) as err:
260
- rigmod.load_rig(_rewrite(rig_path, tmp_path, skin_weight=w))
261
- assert err.value.code == "bad-skin-weights"
262
-
263
-
264
- def test_missing_and_malformed_members_rejected(rig_path, tmp_path):
265
- with pytest.raises(RigError) as err:
266
- rigmod.load_rig(_rewrite(rig_path, tmp_path, prep_chain_rel=None))
267
- assert err.value.code == "bad-rig" and "prep_chain_rel" in str(err.value)
268
- with pytest.raises(RigError) as err:
269
- rigmod.load_rig(_rewrite(rig_path, tmp_path, prep_offsets=np.zeros((26, 3), np.float32)))
270
- assert err.value.code == "bad-rig" and "prep_offsets" in str(err.value)
271
- with pytest.raises(RigError) as err:
272
- rigmod.load_rig(tmp_path / "no-such-rig.npz")
273
- assert err.value.code == "bad-rig"
274
-
275
-
276
- def test_bad_bone_scale_and_motion_rejected(rig):
277
- state = State.identity(2)
278
- with pytest.raises(RigError) as err:
279
- rigmod.cskel27_fk(state, np.ones(26), rig)
280
- assert err.value.code == "bad-bone-scale"
281
- bad = np.ones(27)
282
- bad[3] = 0
283
- with pytest.raises(RigError) as err:
284
- rigmod.cskel27_fk(state, bad, rig)
285
- assert err.value.code == "bad-bone-scale"
286
- with pytest.raises(RigError) as err:
287
- State.from_motion(np.zeros((4, 26, 3, 3)), np.zeros((4, 3)))
288
- assert err.value.code == "bad-motion"