cozyclay 2.1.1 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (577) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/README.md +12 -30
  3. package/bin/agent/agent-routes.mjs +40 -113
  4. package/bin/agent/agent-runner.mjs +9 -10
  5. package/bin/agent/cliproxy-provider.mjs +135 -0
  6. package/bin/agent/credential-store.mjs +30 -7
  7. package/bin/agent/pi-tools.mjs +4 -1
  8. package/bin/agent/provider-keys.mjs +32 -0
  9. package/bin/agent/provider-login.mjs +110 -0
  10. package/bin/agent/provider-routes.mjs +99 -0
  11. package/bin/agent/providers.mjs +19 -97
  12. package/bin/agent/script-tool.mjs +17 -4
  13. package/bin/agent/studio-motion-reference.mjs +23 -0
  14. package/bin/agent/studio-prompt.mjs +1 -1
  15. package/bin/agent/studio-rebase-text.mjs +32 -0
  16. package/bin/agent/studio-rebase.mjs +26 -39
  17. package/bin/agent/studio-tools.mjs +9 -28
  18. package/bin/agent/tool-arguments.mjs +49 -0
  19. package/bin/agent/turn-frames.mjs +66 -0
  20. package/bin/codex-auth.mjs +9 -2
  21. package/bin/cozyclay.mjs +10 -116
  22. package/bin/live/cli.mjs +3 -3
  23. package/bin/mcp-runtime.mjs +2 -12
  24. package/bin/package-signature.mjs +0 -4
  25. package/dist/app/index.html +7 -6
  26. package/dist/assets/EXRLoader-CvVIvGVE.js +1 -0
  27. package/dist/assets/analytics-C9sUnndd.js +1 -0
  28. package/dist/assets/app-CCh--1Hc.js +5087 -0
  29. package/dist/assets/app-f-fkV5uP.css +1 -0
  30. package/dist/assets/compile-worker-CkxiD6lH.js +5 -0
  31. package/dist/assets/fflate.module-KbGj5-H6.js +5 -0
  32. package/dist/assets/{first-shot-handoff-msLI71g1.js → first-shot-handoff-CC4xe4HM.js} +1 -1
  33. package/dist/assets/{landing-ChUvB_zL.js → landing-DjHD8u7k.js} +1 -1
  34. package/dist/assets/scene-asset-sync-Hx_FzdOH.js +20 -0
  35. package/dist/assets/scene-asset-sync-_S8NQl5B.css +1 -0
  36. package/dist/assets/workflow-C4yKH1p4.js +179 -0
  37. package/dist/cozyclay-package.json +1 -1
  38. package/dist/index.html +3 -3
  39. package/dist/sw.js +3 -6
  40. package/dist/workflow/index.html +5 -4
  41. package/mcp/LIVE-PROTOCOL.md +7 -47
  42. package/mcp/README.md +16 -26
  43. package/mcp/live-hub.mjs +6 -10
  44. package/mcp/package.json +1 -3
  45. package/mcp/tool-handlers.mjs +4 -168
  46. package/package.json +3 -22
  47. package/src/App.jsx +150 -2624
  48. package/src/agent-progress.js +79 -0
  49. package/src/agent-turn-mark.js +30 -0
  50. package/src/analytics.js +1 -19
  51. package/src/app-context.js +5 -0
  52. package/src/app-stage.jsx +2 -34
  53. package/src/asset-pane.jsx +6 -5
  54. package/src/camera-block.js +21 -0
  55. package/src/camera-direction.js +120 -0
  56. package/src/character-kind.js +3 -9
  57. package/src/command-bus.js +8 -6
  58. package/src/commands/cast.js +5 -34
  59. package/src/commands/elements/character.js +1 -1
  60. package/src/commands/elements/motion.js +1 -6
  61. package/src/commands/elements/object.js +6 -2
  62. package/src/commands/elements/shot.js +4 -1
  63. package/src/commands/elements.js +38 -14
  64. package/src/commands/index.js +3 -1
  65. package/src/commands/kits.js +37 -0
  66. package/src/commands/motion-timing.js +57 -0
  67. package/src/commands/motion.js +9 -152
  68. package/src/commands/motion3-compile.js +65 -0
  69. package/src/commands/motion3.js +207 -0
  70. package/src/commands/objects.js +6 -4
  71. package/src/commands/shot.js +49 -11
  72. package/src/commands/stage.js +14 -1
  73. package/src/commands/view.js +4 -1
  74. package/src/document-mirrors.js +42 -0
  75. package/src/document-store.js +4 -0
  76. package/src/domains/cast.js +19 -194
  77. package/src/domains/motion/motion-range-pin-rules.js +1 -1
  78. package/src/domains/motion/take-source.js +51 -0
  79. package/src/domains/motion.js +60 -1838
  80. package/src/domains/scenes.js +12 -55
  81. package/src/domains/shots.js +2 -2
  82. package/src/domains/stage.js +4 -2
  83. package/src/draft-ghost.js +46 -0
  84. package/src/draft-ghost.jsx +129 -0
  85. package/src/dualview.jsx +4 -3
  86. package/src/effects/EffectsLayer.jsx +112 -0
  87. package/src/effects/effect-meshes.js +232 -0
  88. package/src/effects/effect-record.js +103 -0
  89. package/src/effects/emitter.js +59 -0
  90. package/src/effects/simulate.js +261 -0
  91. package/src/effects/state.js +59 -0
  92. package/src/execution-telemetry.js +2 -3
  93. package/src/first-success-guide.jsx +5 -5
  94. package/src/ghost-set.js +257 -0
  95. package/src/held-object-edit.jsx +113 -0
  96. package/src/held-object-pose.js +175 -0
  97. package/src/hierarchy-model.js +1 -1
  98. package/src/hierarchy-panel.jsx +1 -1
  99. package/src/imported-mesh.jsx +103 -0
  100. package/src/kits/held.js +84 -0
  101. package/src/kits/index.js +70 -0
  102. package/src/kits/smoking.js +57 -0
  103. package/src/light-blockers.js +93 -0
  104. package/src/motion/beat-board-model.js +84 -0
  105. package/src/motion/beat-board.css +124 -0
  106. package/src/motion/beat-board.jsx +59 -0
  107. package/src/motion/center-of-mass.js +66 -0
  108. package/src/motion/fix-collisions.js +850 -0
  109. package/src/motion/physics-review.js +118 -0
  110. package/src/{ardy → motion}/physics-support.js +1 -76
  111. package/src/motion/rig-snapshot.js +22 -0
  112. package/src/motion/take-segments.jsx +140 -0
  113. package/src/{ardy → motion}/timeline-coordinates.js +0 -21
  114. package/src/{ardy → motion}/timeline.css +1 -2
  115. package/src/{ardy → motion}/timeline.jsx +42 -413
  116. package/src/motion/waypoints.js +76 -0
  117. package/src/motion3/beats.js +83 -0
  118. package/src/motion3/body.js +158 -0
  119. package/src/motion3/bones.js +47 -0
  120. package/src/motion3/carried.js +47 -0
  121. package/src/motion3/compile-job.js +75 -0
  122. package/src/motion3/compile-worker.js +10 -0
  123. package/src/motion3/compiler/anchors.js +105 -0
  124. package/src/motion3/compiler/arm-orient.js +128 -0
  125. package/src/motion3/compiler/channels.js +125 -0
  126. package/src/motion3/compiler/compile.js +205 -0
  127. package/src/motion3/compiler/gait-rhythm.js +97 -0
  128. package/src/motion3/compiler/grip.js +171 -0
  129. package/src/motion3/compiler/head-steady.js +48 -0
  130. package/src/motion3/compiler/hold.js +134 -0
  131. package/src/motion3/compiler/idle.js +113 -0
  132. package/src/motion3/compiler/limbs.js +170 -0
  133. package/src/motion3/compiler/math.js +72 -0
  134. package/src/motion3/compiler/pose.js +162 -0
  135. package/src/motion3/compiler/quat.js +49 -0
  136. package/src/motion3/compiler/skeleton.js +74 -0
  137. package/src/motion3/compiler/validate-common.js +32 -0
  138. package/src/motion3/compiler/validate-goals.js +118 -0
  139. package/src/motion3/compiler/validate-objects.js +143 -0
  140. package/src/motion3/compiler/validate-parts.js +116 -0
  141. package/src/motion3/compiler/validate.js +122 -0
  142. package/src/motion3/compiler/vocab.js +73 -0
  143. package/src/motion3/compiler/walk-pelvis.js +275 -0
  144. package/src/motion3/compiler/walk.js +207 -0
  145. package/src/motion3/compiler/wrist-clamp.js +32 -0
  146. package/src/motion3/delta.js +47 -0
  147. package/src/motion3/describe.js +174 -0
  148. package/src/motion3/docs.js +27 -0
  149. package/src/motion3/edit-carry.js +21 -0
  150. package/src/motion3/fix/ease.js +16 -0
  151. package/src/motion3/fix/ground.js +37 -0
  152. package/src/motion3/fix/index.js +52 -0
  153. package/src/motion3/fix/push.js +71 -0
  154. package/src/motion3/fix/reach.js +46 -0
  155. package/src/motion3/fix/retime.js +79 -0
  156. package/src/motion3/fix/self.js +211 -0
  157. package/src/motion3/fix/slide.js +70 -0
  158. package/src/motion3/fix/turn.js +51 -0
  159. package/src/motion3/follow.js +39 -0
  160. package/src/motion3/free-motion.js +173 -0
  161. package/src/motion3/grip-choice.js +130 -0
  162. package/src/motion3/hold-plan.js +157 -0
  163. package/src/motion3/json-patch.js +77 -0
  164. package/src/motion3/key-aim.js +105 -0
  165. package/src/motion3/key-edit.js +82 -0
  166. package/src/motion3/lenient.js +127 -0
  167. package/src/motion3/object-keys.js +236 -0
  168. package/src/motion3/object-pose.js +182 -0
  169. package/src/motion3/object-tracks.js +12 -0
  170. package/src/motion3/pipeline.js +102 -0
  171. package/src/motion3/playback.js +51 -0
  172. package/src/motion3/probes.js +91 -0
  173. package/src/motion3/qa/body-checks.js +34 -0
  174. package/src/motion3/qa/check-names.js +14 -0
  175. package/src/motion3/qa/checks.js +121 -0
  176. package/src/motion3/qa/contact-checks.js +51 -0
  177. package/src/motion3/qa/explain.js +80 -0
  178. package/src/motion3/qa/fmt.js +5 -0
  179. package/src/motion3/qa/foot-checks.js +72 -0
  180. package/src/motion3/qa/geometry.js +77 -0
  181. package/src/motion3/qa/goals.js +191 -0
  182. package/src/motion3/qa/ground.js +60 -0
  183. package/src/motion3/qa/hints-contact.js +179 -0
  184. package/src/motion3/qa/hints-goal.js +160 -0
  185. package/src/motion3/qa/hints-knee.js +24 -0
  186. package/src/motion3/qa/hints-motion.js +209 -0
  187. package/src/motion3/qa/hints-object.js +91 -0
  188. package/src/motion3/qa/index.js +42 -0
  189. package/src/motion3/qa/key-checks.js +82 -0
  190. package/src/motion3/qa/near.js +96 -0
  191. package/src/motion3/qa/object-checks.js +135 -0
  192. package/src/motion3/qa/patch.js +161 -0
  193. package/src/motion3/qa/severity.js +38 -0
  194. package/src/motion3/qa/spec-checks.js +65 -0
  195. package/src/motion3/qa/style.js +218 -0
  196. package/src/motion3/qa/wrist-checks.js +69 -0
  197. package/src/motion3/qa/wrist-obs.js +60 -0
  198. package/src/motion3/refs.js +120 -0
  199. package/src/motion3/repair-tune.js +121 -0
  200. package/src/motion3/repair.js +83 -0
  201. package/src/motion3/report.js +67 -0
  202. package/src/motion3/rigs/index.js +14 -0
  203. package/src/motion3/rigs/x-bot-tpose-body.json +1 -0
  204. package/src/motion3/rigs/x-bot-tpose.json +1 -0
  205. package/src/motion3/rigs/y-bot-tpose-body.json +1 -0
  206. package/src/motion3/rigs/y-bot-tpose.json +1 -0
  207. package/src/motion3/scene.js +60 -0
  208. package/src/motion3/schema/brief.js +89 -0
  209. package/src/motion3/schema/fields.js +44 -0
  210. package/src/motion3/schema/spec-examples.js +46 -0
  211. package/src/motion3/schema/topics.js +111 -0
  212. package/src/motion3/schema/types.js +147 -0
  213. package/src/motion3/search.js +117 -0
  214. package/src/motion3/second-hand.js +112 -0
  215. package/src/motion3/skills/basic.js +82 -0
  216. package/src/motion3/skills/book.js +76 -0
  217. package/src/motion3/skills/car.js +145 -0
  218. package/src/motion3/skills/frames-auto.js +29 -0
  219. package/src/motion3/skills/index.js +83 -0
  220. package/src/motion3/skills/info.js +65 -0
  221. package/src/motion3/skills/smoke-kit.js +109 -0
  222. package/src/motion3/skills/smoke.js +160 -0
  223. package/src/motion3/spec-store.js +59 -0
  224. package/src/motion3/take-edit.js +155 -0
  225. package/src/motion3/take.js +63 -0
  226. package/src/motion3/timeline.js +127 -0
  227. package/src/motion3/views.js +121 -0
  228. package/src/motion3/walk-plan.js +119 -0
  229. package/src/object-catalog.jsx +7 -1
  230. package/src/object-colors.js +98 -0
  231. package/src/object-gizmo-parts.js +115 -0
  232. package/src/object-gizmo.jsx +37 -122
  233. package/src/object-glow.js +139 -0
  234. package/src/object-hidden.js +33 -0
  235. package/src/object-look.js +196 -0
  236. package/src/object-size.js +10 -0
  237. package/src/object-travel.js +71 -6
  238. package/src/panels/CameraPanel.jsx +7 -7
  239. package/src/panels/HdrEnvironmentControls.jsx +59 -0
  240. package/src/panels/LightPanel.jsx +25 -4
  241. package/src/panels/ObjectLookPanel.jsx +88 -0
  242. package/src/panels/PosePanel.jsx +2 -17
  243. package/src/panels/RigControlPanel.jsx +5 -53
  244. package/src/panels/SubjectBox.jsx +4 -1
  245. package/src/panels/details.css +14 -0
  246. package/src/posestudio.jsx +4 -27
  247. package/src/project-browser.jsx +3 -4
  248. package/src/project-resources.js +18 -31
  249. package/src/project.js +13 -104
  250. package/src/props.jsx +25 -280
  251. package/src/rail-dot-drag.js +23 -0
  252. package/src/render-passes.js +10 -0
  253. package/src/resource-status.jsx +1 -0
  254. package/src/result-modal.jsx +4 -19
  255. package/src/room.jsx +20 -12
  256. package/src/scene-assets.js +8 -6
  257. package/src/scene-light-place.js +72 -0
  258. package/src/scene-lights.jsx +148 -0
  259. package/src/scene-objects.js +31 -139
  260. package/src/scenes.js +15 -105
  261. package/src/semantic-edit.js +2 -5
  262. package/src/set-pieces.jsx +183 -0
  263. package/src/settings-menu.jsx +3 -11
  264. package/src/shell/BottomDock.jsx +9 -51
  265. package/src/shell/DetailsSlot.jsx +16 -200
  266. package/src/shell/GhostsMenuGroup.jsx +74 -0
  267. package/src/shell/LibrarySlot.jsx +1 -3
  268. package/src/shell/MenuBar.jsx +3 -3
  269. package/src/shell/PreferencesDialog.jsx +15 -264
  270. package/src/shell/PreferencesSlot.jsx +2 -8
  271. package/src/shell/TopBar.jsx +5 -84
  272. package/src/shell/ViewportToolbar.jsx +10 -70
  273. package/src/shell/agent-glass.css +19 -6
  274. package/src/shell/details-selection.js +42 -0
  275. package/src/shell/ghosts-menu.css +22 -0
  276. package/src/shell/glass-regions.css +29 -3
  277. package/src/shell/preferences.css +6 -1
  278. package/src/shot-authoring.js +4 -7
  279. package/src/soft-shadows.js +37 -0
  280. package/src/stage-attach-frames.js +12 -12
  281. package/src/stage-canvas-rigs.jsx +3 -3
  282. package/src/stage-capture.jsx +42 -8
  283. package/src/stage-characters.jsx +22 -216
  284. package/src/stage-editor-gizmos.jsx +6 -6
  285. package/src/stage-environment.js +93 -0
  286. package/src/stage-hdr-builtins.js +107 -0
  287. package/src/stage-hdr-image.js +116 -0
  288. package/src/stage-hdr-library.js +70 -0
  289. package/src/stage-hdr-map.js +94 -0
  290. package/src/stage-hdr.js +130 -0
  291. package/src/stage-lighting.js +149 -0
  292. package/src/stage-occlusion.js +210 -0
  293. package/src/stage-sky.jsx +159 -0
  294. package/src/stage-startup-defaults.js +5 -9
  295. package/src/studio-actions.js +12 -14
  296. package/src/studio-agent-commands.js +19 -16
  297. package/src/studio-agent-motion.js +9 -9
  298. package/src/studio-agent-protocol.js +41 -71
  299. package/src/studio-app-binding.js +25 -45
  300. package/src/studio-asset-list.js +17 -0
  301. package/src/studio-contact-sheet.js +37 -5
  302. package/src/studio-elements.js +27 -7
  303. package/src/studio-entity-bounds.js +18 -0
  304. package/src/studio-error-codes.js +8 -0
  305. package/src/studio-export-framing.js +152 -0
  306. package/src/studio-lighting-check.js +103 -0
  307. package/src/studio-motion-visual.js +150 -0
  308. package/src/studio-schema-errors.js +80 -0
  309. package/src/studio-support-gap.js +74 -0
  310. package/src/styles.css +1 -19
  311. package/src/timeline-defaults.js +5 -0
  312. package/src/timeline-extent.js +8 -20
  313. package/src/tutorial-walk.js +17 -0
  314. package/src/ui-scrub.js +1 -1
  315. package/src/use-shot-camera-tracks.js +12 -0
  316. package/src/view-ghosts.js +97 -0
  317. package/src/view-ghosts.jsx +49 -0
  318. package/src/workflow/AgentPanel.jsx +47 -166
  319. package/src/workflow/AgentProgressCard.jsx +107 -0
  320. package/src/workflow/AgentReplyText.jsx +37 -0
  321. package/src/workflow/AgentTurnSteps.jsx +84 -0
  322. package/src/workflow/LoginPanel.jsx +140 -0
  323. package/src/workflow/WorkflowBuilder.jsx +7 -23
  324. package/src/workflow/agent-client.js +54 -51
  325. package/src/workflow/agent-panel.css +71 -1
  326. package/src/workflow/agent-progress-card.css +167 -0
  327. package/src/workflow/agent-progress-view.js +47 -0
  328. package/src/workflow/agent-turn-steps.css +113 -0
  329. package/src/workflow/agent-turn.js +191 -0
  330. package/src/workflow/login-client.js +34 -0
  331. package/src/workflow/motion-input.js +18 -31
  332. package/src/workflow/node-schema.js +1 -1
  333. package/src/workflow/reply-markdown.js +69 -0
  334. package/src/workflow/scene-asset-sync.js +8 -8
  335. package/tools/agent-session-stats.mjs +61 -0
  336. package/tools/dev-full.mjs +5 -56
  337. package/tools/eslint-baseline.json +12 -13
  338. package/tools/knip-baseline.json +0 -179
  339. package/tools/loc-baseline.json +31 -81
  340. package/tools/motion3/extract-body.mjs +103 -0
  341. package/tools/motion3/extract-rig.mjs +85 -0
  342. package/tools/process-supervisor.mjs +0 -88
  343. package/tools/run-tests.mjs +8 -124
  344. package/bin/agent/motion-runtime.mjs +0 -76
  345. package/dist/ardy/cskel27-rest.json +0 -557
  346. package/dist/assets/analytics-C9pMpW7s.js +0 -1
  347. package/dist/assets/app-B_2XB9rw.css +0 -1
  348. package/dist/assets/app-DAuvKPP8.js +0 -4920
  349. package/dist/assets/config-Bx7pYsD9.js +0 -1
  350. package/dist/assets/config-DeoFTYzb.css +0 -1
  351. package/dist/assets/demo-CvKuBgy5.js +0 -1
  352. package/dist/assets/scene-asset-sync-DKkpjKBV.js +0 -19
  353. package/dist/assets/scene-asset-sync-DjJ_cJ2R.css +0 -1
  354. package/dist/assets/ticket-BPk2Ni95.js +0 -1
  355. package/dist/assets/workflow-jJRBR94Z.js +0 -179
  356. package/dist/demo/walk-then-stop.npz +0 -0
  357. package/mcp/ardy-prompts.mjs +0 -218
  358. package/src/ardy/auto-fix-panel.css +0 -108
  359. package/src/ardy/auto-physics.js +0 -1194
  360. package/src/ardy/client.js +0 -153
  361. package/src/ardy/convert.js +0 -313
  362. package/src/ardy/cskel27-neutral.js +0 -39
  363. package/src/ardy/cskel27.js +0 -77
  364. package/src/ardy/export.js +0 -165
  365. package/src/ardy/fix-collisions.js +0 -2257
  366. package/src/ardy/foot-lock.js +0 -230
  367. package/src/ardy/motion-calibration.js +0 -118
  368. package/src/ardy/motion-edit.js +0 -213
  369. package/src/ardy/motion-url.js +0 -55
  370. package/src/ardy/mp4-duration.js +0 -102
  371. package/src/ardy/npz.js +0 -595
  372. package/src/ardy/physics-panel.css +0 -21
  373. package/src/ardy/physics-panel.jsx +0 -89
  374. package/src/ardy/physics-refine.js +0 -111
  375. package/src/ardy/physics-review.js +0 -567
  376. package/src/ardy/physics-temporal.js +0 -54
  377. package/src/ardy/platform-fit-panel.css +0 -64
  378. package/src/ardy/platform-fit-panel.jsx +0 -41
  379. package/src/ardy/platform-fit.js +0 -326
  380. package/src/ardy/playback.js +0 -711
  381. package/src/ardy/pose-pin.js +0 -97
  382. package/src/ardy/prompt-clips.js +0 -30
  383. package/src/ardy/retime.js +0 -211
  384. package/src/ardy/root-drop.js +0 -369
  385. package/src/ardy/timeline-resize.js +0 -13
  386. package/src/ardy/to-cskel27.js +0 -225
  387. package/src/ardy/trim.js +0 -33
  388. package/src/ardy/waypoints.js +0 -369
  389. package/src/ardy-bridge-contract.js +0 -281
  390. package/src/domains/motion/motion-video-capture.js +0 -664
  391. package/src/i2v-motion-client.js +0 -77
  392. package/src/i2v-motion-studio.jsx +0 -180
  393. package/src/line-edit.js +0 -1348
  394. package/src/motion/generation.js +0 -105
  395. package/src/motion-readiness-ui.jsx +0 -67
  396. package/src/motion-readiness.js +0 -40
  397. package/src/motion-resources.js +0 -177
  398. package/src/motion-store.js +0 -65
  399. package/src/motion-trail.js +0 -601
  400. package/src/multimodel-ingest.js +0 -394
  401. package/src/panels/PromptBlocksPanel.jsx +0 -444
  402. package/src/panels/VideoCapturePanel.jsx +0 -181
  403. package/src/pose-extract/browser.js +0 -42
  404. package/src/pose-extract/detector.js +0 -100
  405. package/src/pose-extract/fit.js +0 -227
  406. package/src/pose-extract/image-frame.js +0 -51
  407. package/src/pose-extract/index.js +0 -58
  408. package/src/pose-extract/keyframes.js +0 -74
  409. package/src/pose-extract/landmarks.js +0 -162
  410. package/src/pose-extract/mirror.js +0 -153
  411. package/src/pose-extract/take.js +0 -131
  412. package/src/pose-extract/video-frames.js +0 -91
  413. package/src/take-recipe.js +0 -165
  414. package/src/trail-key-conflicts.js +0 -39
  415. package/src/trail-pick.js +0 -59
  416. package/src/use-i2v-capture-modal.js +0 -15
  417. package/tools/ardy/BRIDGE.md +0 -37
  418. package/tools/ardy/GVHMR-NPZ.md +0 -68
  419. package/tools/ardy/README.md +0 -124
  420. package/tools/ardy/__pycache__/cclay_gvhmr_worker.cpython-313.pyc +0 -0
  421. package/tools/ardy/artifacts.mjs +0 -34
  422. package/tools/ardy/bridge.mjs +0 -1732
  423. package/tools/ardy/bvh-cskel27.mjs +0 -1265
  424. package/tools/ardy/cclay_constrained_generate.py +0 -561
  425. package/tools/ardy/cclay_gvhmr_worker.py +0 -264
  426. package/tools/ardy/cclay_motion_edit.py +0 -510
  427. package/tools/ardy/cclay_sequence_generate.py +0 -705
  428. package/tools/ardy/cclay_worker.py +0 -304
  429. package/tools/ardy/dump-npz.py +0 -205
  430. package/tools/ardy/extract-rest.mjs +0 -299
  431. package/tools/ardy/extract.mjs +0 -507
  432. package/tools/ardy/footage.mjs +0 -467
  433. package/tools/ardy/gvhmr-floor.mjs +0 -102
  434. package/tools/ardy/gvhmr-to-cskel27.mjs +0 -21
  435. package/tools/ardy/gvhmr_fastpath.py +0 -168
  436. package/tools/ardy/gvhmr_trajectory.py +0 -240
  437. package/tools/ardy/merge-text-encoder.py +0 -160
  438. package/tools/ardy/mocap-metrics.mjs +0 -200
  439. package/tools/ardy/mocap-quality-gate.mjs +0 -110
  440. package/tools/ardy/motion-stabilize.mjs +0 -348
  441. package/tools/ardy/npz.mjs +0 -414
  442. package/tools/ardy/pose-to-npz.mjs +0 -106
  443. package/tools/ardy/prompt-limits.mjs +0 -5
  444. package/tools/ardy/run-edit-on-box.sh +0 -73
  445. package/tools/ardy/run-local.mjs +0 -233
  446. package/tools/ardy/run-on-box.sh +0 -593
  447. package/tools/ardy/run-sequence-on-box.sh +0 -177
  448. package/tools/ardy/runners/gvhmr-worker.mjs +0 -206
  449. package/tools/ardy/runners/index.mjs +0 -18
  450. package/tools/ardy/runners/local.mjs +0 -455
  451. package/tools/ardy/runners/proc.mjs +0 -125
  452. package/tools/ardy/runners/remote.mjs +0 -252
  453. package/tools/ardy/setup-local.mjs +0 -189
  454. package/tools/ardy/setup-text-encoder-on-box.sh +0 -93
  455. package/tools/ardy/setup-text-encoder.py +0 -306
  456. package/tools/ardy/smpl-cskel27.mjs +0 -178
  457. package/tools/ardy/visual-qa.mjs +0 -196
  458. package/tools/ardy/vq-car.mjs +0 -52
  459. package/tools/bench/EXP3.md +0 -61
  460. package/tools/bench/cclay_bench_extract_incam.py +0 -125
  461. package/tools/bench/cclay_bench_extract_obs.py +0 -204
  462. package/tools/bench/cclay_bench_runner.py +0 -42
  463. package/tools/bench/cube-contact.mjs +0 -162
  464. package/tools/bench/exp3.mjs +0 -152
  465. package/tools/bench/extract-bench-lib.mjs +0 -216
  466. package/tools/bench/extract-bench.mjs +0 -302
  467. package/tools/bench/fal-generate.mjs +0 -88
  468. package/tools/bench/fit/README.md +0 -189
  469. package/tools/bench/fit/camera.mjs +0 -59
  470. package/tools/bench/fit/contact.mjs +0 -419
  471. package/tools/bench/fit/footlock.mjs +0 -234
  472. package/tools/bench/fit/motion.mjs +0 -85
  473. package/tools/bench/fit/pin.mjs +0 -27
  474. package/tools/bench/fit/remote.mjs +0 -105
  475. package/tools/bench/fit-bench.mjs +0 -126
  476. package/tools/bench/fit-sanity.mjs +0 -45
  477. package/tools/bench/metrics.mjs +0 -313
  478. package/tools/bench/obs/depth.mjs +0 -258
  479. package/tools/bench/obs/extrinsics.mjs +0 -175
  480. package/tools/bench/obs/fit_mannequin_betas.py +0 -340
  481. package/tools/bench/obs/ground.mjs +0 -294
  482. package/tools/bench/obs/heading.mjs +0 -151
  483. package/tools/bench/obs/ladder.mjs +0 -555
  484. package/tools/bench/obs/mannequin-betas.json +0 -124
  485. package/tools/bench/obs/remote.mjs +0 -83
  486. package/tools/bench/obs/rest_joints.py +0 -52
  487. package/tools/bench/obs/ybot-targets.mjs +0 -49
  488. package/tools/bench/obs-bench.mjs +0 -520
  489. package/tools/bench/score.mjs +0 -343
  490. package/tools/bench/summarize.mjs +0 -86
  491. package/tools/demo-worker/README.md +0 -137
  492. package/tools/demo-worker/api-client.mjs +0 -233
  493. package/tools/demo-worker/generate.mjs +0 -301
  494. package/tools/demo-worker/index.mjs +0 -466
  495. package/tools/demo-worker/sign-core.mjs +0 -112
  496. package/tools/demo-worker/sign.mjs +0 -161
  497. package/tools/gt-render/browser.mjs +0 -182
  498. package/tools/gt-render/camera-math.mjs +0 -276
  499. package/tools/gt-render/page.mjs +0 -267
  500. package/tools/gt-render/render.mjs +0 -514
  501. package/tools/gt-render/scene-box.mjs +0 -31
  502. package/tools/gt-render/take-transform.mjs +0 -87
  503. package/tools/kimodo/compare-seam.mjs +0 -108
  504. package/tools/kimodo/constraints.mjs +0 -244
  505. package/tools/kimodo/edit.mjs +0 -259
  506. package/tools/kimodo/effector-constraints.mjs +0 -200
  507. package/tools/kimodo/generate.mjs +0 -646
  508. package/tools/kimodo/local-output.mjs +0 -265
  509. package/tools/kimodo/measure-pose.mjs +0 -65
  510. package/tools/kimodo/measure-preserve.mjs +0 -899
  511. package/tools/kimodo/measure-waypoints.mjs +0 -55
  512. package/tools/kimodo/mlx-generate.py +0 -111
  513. package/tools/kimodo/pose-constraints.mjs +0 -248
  514. package/tools/kimodo/preserve-mask.mjs +0 -543
  515. package/tools/kimodo/read-npz.mjs +0 -189
  516. package/tools/kimodo/run-edit-on-box.mjs +0 -155
  517. package/tools/kimodo/run-sequence-on-box.mjs +0 -178
  518. package/tools/kimodo/runner.mjs +0 -284
  519. package/tools/kimodo/seam-batch.mjs +0 -77
  520. package/tools/kimodo/setup-local.mjs +0 -30
  521. package/tools/kimodo/setup-on-box.sh +0 -339
  522. package/tools/kimodo/soma77-to-cskel27.mjs +0 -305
  523. package/tools/morphgs/assets/gen_truth.py +0 -39
  524. package/tools/morphgs/assets/mesh_ori_rig.txt +0 -27
  525. package/tools/morphgs/assets/playback-check.mjs +0 -30
  526. package/tools/morphgs/demo-gate.sh +0 -26
  527. package/tools/morphgs/fbx2morphgs.mjs +0 -68
  528. package/tools/morphgs/morphgs-to-cskel27.mjs +0 -105
  529. package/tools/morphgs/patches/preprocess_src-none-mode.patch +0 -76
  530. package/tools/morphgs/setup-on-cluster.sh +0 -126
  531. package/tools/projflow/driver.py +0 -1069
  532. package/tools/projflow/generate.mjs +0 -805
  533. package/tools/projflow/hml22-to-cskel27.mjs +0 -735
  534. package/tools/projflow/line-edit-job.mjs +0 -547
  535. package/tools/projflow/replay.mjs +0 -452
  536. package/tools/projflow/runner.mjs +0 -269
  537. package/tools/projflow/service.mjs +0 -742
  538. package/tools/track/DESIGN.md +0 -513
  539. package/tools/track/backfill-provenance.mjs +0 -116
  540. package/tools/track/budget.mjs +0 -8
  541. package/tools/track/check-rig.mjs +0 -209
  542. package/tools/track/diagnostics.schema.json +0 -60
  543. package/tools/track/export-rig.mjs +0 -256
  544. package/tools/track/fallback.mjs +0 -7
  545. package/tools/track/fk-parity-fixture.mjs +0 -161
  546. package/tools/track/gate.mjs +0 -349
  547. package/tools/track/masks.mjs +0 -318
  548. package/tools/track/metrics.mjs +0 -201
  549. package/tools/track/publish-obs.mjs +0 -108
  550. package/tools/track/py/check_env.py +0 -61
  551. package/tools/track/py/eval_lr.py +0 -229
  552. package/tools/track/py/lr_viterbi.py +0 -294
  553. package/tools/track/py/masks.py +0 -405
  554. package/tools/track/py/objective.py +0 -382
  555. package/tools/track/py/rig.py +0 -508
  556. package/tools/track/py/scene.py +0 -326
  557. package/tools/track/py/test_joint_indices.py +0 -123
  558. package/tools/track/py/test_lr_viterbi.py +0 -195
  559. package/tools/track/py/test_masks.py +0 -229
  560. package/tools/track/py/test_rig.py +0 -288
  561. package/tools/track/py/test_scene.py +0 -354
  562. package/tools/track/py/test_track.py +0 -328
  563. package/tools/track/py/track.py +0 -411
  564. package/tools/track/remote.mjs +0 -140
  565. package/tools/track/rig-dump.mjs +0 -227
  566. package/tools/track/run-box-tests.mjs +0 -14
  567. package/tools/track/run-box.mjs +0 -168
  568. package/tools/track/setup-box.sh +0 -59
  569. package/tools/track/study-2d.mjs +0 -728
  570. /package/src/{ardy → motion}/collision-blockers.js +0 -0
  571. /package/src/{ardy → motion}/ground.js +0 -0
  572. /package/src/{ardy → motion}/ik-drag.js +0 -0
  573. /package/src/{ardy → motion}/ik-key-json.js +0 -0
  574. /package/src/{ardy → motion}/ik.js +0 -0
  575. /package/src/{ardy → motion}/physics-surface.js +0 -0
  576. /package/src/{ardy → motion}/playback-clock.js +0 -0
  577. /package/src/{ardy → motion}/range-pin.js +0 -0
@@ -1,543 +0,0 @@
1
- /**
2
- * preserve-mask.mjs — user edit ranges → Kimodo's `--preserve_mask` JSON (C1 v2).
3
- *
4
- * Scheduled inpainting (Disney, "Interactive Generative Motion Editing via
5
- * Scheduled Inpainting", 2026) blends a preserved base take back into the
6
- * evolving sample inside the denoising loop:
7
- *
8
- * x_blended = alpha_t * noise(base, t) + (1 - alpha_t) * x_gen
9
- * alpha_t = alpha_time(t) * alpha_mask(frame)
10
- *
11
- * This file builds ONLY alpha_mask: one float per GENERATION frame, 1 = keep the
12
- * base take exactly, 0 = let the model generate freely. Everything about
13
- * diffusion time (sigma_s / sigma_e) lives on the Python side; the mask is a
14
- * pure function of "which frames did the user edit".
15
- *
16
- * Three things are silent-bug shaped here, so each is pinned by
17
- * test/verify-kimodo-preserve.mjs:
18
- *
19
- * 1. FRAME SPACE. Edit ranges arrive in the app's clip space, which runs at
20
- * ARDY's 20 fps (src/App.jsx ARDY_FPS). Kimodo generates at 30. An unscaled
21
- * range would free the wrong 2/3 of the clip — the user edits seconds 2..4
22
- * and the model regenerates seconds 1.33..2.67. The scaling rule is copied
23
- * verbatim from buildRoot2dConstraints in ./constraints.mjs (frame * genFps /
24
- * appFps, Math.round, clamped into the generated clip) so a waypoint and an
25
- * edit range authored on the same app frame always land on the same
26
- * generation frame. If that rule ever changes, it must change in both files.
27
- *
28
- * 2. THE FALLOFF MUST NOT BE A STEP. The paper's own ablation is explicit: a
29
- * square (hard-edged) kernel breaks motion at the seam — the preserved side
30
- * is frozen on the base pose and the free side is generated with no knowledge
31
- * of it, so the character snaps. The edit's influence therefore decays as a
32
- * Gaussian shoulder over `influenceRadius` generation frames:
33
- *
34
- * weight(f) = 1 - exp( -(d^2) / (2 * sigma^2) ), sigma = influenceRadius/2
35
- *
36
- * where d is the distance in generation frames from f to the NEAREST frame of
37
- * the edit range (so d = 0 anywhere inside it, giving weight 0 = fully free).
38
- * sigma = radius/2 is chosen so the named radius is 2 sigma: weight is ~0.86
39
- * at exactly `influenceRadius` frames out and reaches float-exact 1.0 around
40
- * 4.3 radii, i.e. "the radius is where the edit has mostly stopped mattering",
41
- * not where it is cut off. The first step away from a boundary is only
42
- * 1 - exp(-1/(2*sigma^2)) (0.0198 for the default radius 10), which is the
43
- * smoothness the tests assert.
44
- *
45
- * 3. OVERLAPS TAKE THE MINIMUM. Two edits whose shoulders meet must not add up
46
- * to MORE preservation in the gap between them than either edit asked for on
47
- * its own. Combining by min means "any edit that wants this frame free wins",
48
- * which is the only rule that keeps the mask monotone in the number of edits:
49
- * adding an edit can never preserve a frame harder than before.
50
- *
51
- * ---------------------------------------------------------------------------
52
- * ROUND 2 (C1v2) adds two orthogonal axes to that same per-frame mask. Both are
53
- * strictly additive: a call that uses neither emits a byte-identical v1 file.
54
- *
55
- * 4. PER-JOINT-GROUP MASKS. "Recompose only the arm" needs a mask that is free
56
- * for the arm and preserved for everything else on the SAME frames. The unit
57
- * is a GROUP, never a single joint: the seven groups of C1v2 (root, torso,
58
- * head, leftArm, rightArm, leftLeg, rightLeg) are the coarsest partition that
59
- * still lets a limb move without dragging the body, and they are coarse on
60
- * purpose — freeing a forearm while pinning its own hand asks the model for a
61
- * pose that does not exist, and it answers with a broken elbow.
62
- *
63
- * Callers name IK TRACK ids (the things the user actually drags in the pose
64
- * studio: `leftHand`, `leftElbow`, `hips`, ...), not group names, and
65
- * TRACK_GROUPS below is the single source of truth for that mapping —
66
- * src/ardy/ik.js owns the track ids, this file owns what they preserve.
67
- * `hips` deliberately maps to BOTH torso and root: dragging the hips moves
68
- * the pelvis bone AND translates the character, and the two live in different
69
- * halves of the motion representation, so freeing only one of them would let
70
- * the model fight the half that stayed pinned.
71
- *
72
- * A range WITHOUT tracks is a whole-body edit and lowers the TOP-LEVEL
73
- * weights, exactly as in v1. A range WITH tracks lowers only its groups'
74
- * arrays and leaves the top level at 1 there. Python reads a group's array
75
- * when one exists and falls back to the top level otherwise, so every emitted
76
- * group array must ALSO carry the whole-body edits — otherwise a mixed call
77
- * (one tracked range + one whole-body range) would quietly EXEMPT the tracked
78
- * groups from the whole-body edit, which is the opposite of what was asked.
79
- * That is why a group's array is built from the union of its own ranges and
80
- * every untracked range, not from its own ranges alone.
81
- *
82
- * 5. THE NOISE-SCHEDULED KERNEL WIDTH (paper, Appendix A). One mask width cannot
83
- * serve the whole denoising trajectory. At high noise the sample only carries
84
- * global structure, so the edit's influence has to reach FAR to move the
85
- * trajectory at all; at low noise it carries detail, and a wide mask there
86
- * would smear the preserved take. The paper's answer is two masks and a lerp:
87
- *
88
- * alpha_mask(f, t) = lerp(weights[f], wideWeights[f], w(t))
89
- *
90
- * with w(t) rising with noise (Python picks the exact schedule and documents
91
- * it). This file emits both arrays; `wideWeights` is the same Gaussian built
92
- * with `influenceRadius * wideRadiusScale`. Wider means the shoulder decays
93
- * more slowly, so wideWeights[f] <= weights[f] everywhere — the wide mask is
94
- * always the more permissive one, never the other way round.
95
- *
96
- * wideWeights is OPT-IN, not automatic: it is emitted when the call carries
97
- * tracks (grouped masks are a round-2 feature and always want the schedule)
98
- * or when the caller names `wideRadiusScale` explicitly. A plain v1-shaped
99
- * call therefore still produces a v1 file, which is what keeps round-1
100
- * callers and their fixtures valid.
101
- *
102
- * VERSION. `version` is 2 as soon as either axis is used (groups or
103
- * wideWeights) and 1 otherwise. It is a property of the emitted SHAPE, not of
104
- * the caller's intent: a reader that only understands v1 can trust that a
105
- * version-1 file has nothing it would silently ignore.
106
- */
107
-
108
- export const PRESERVE_MASK_VERSION = 1;
109
-
110
- /** The version stamped on a mask that uses groups and/or wideWeights. */
111
- export const PRESERVE_MASK_VERSION_V2 = 2;
112
-
113
- /** Default shoulder width in GENERATION frames (~0.33 s at 30 fps). */
114
- export const DEFAULT_INFLUENCE_RADIUS = 10;
115
-
116
- /**
117
- * Default multiplier for the high-noise (wide) kernel. 3 comes from the paper's
118
- * Appendix A ablation: narrower than ~2x and the wide pass barely differs from
119
- * the narrow one (no schedule at all); much wider than ~4x and the high-noise
120
- * pass frees most of the clip, which is just "generate from scratch" with extra
121
- * steps. 3 is the middle of that band.
122
- */
123
- export const DEFAULT_WIDE_RADIUS_SCALE = 3;
124
-
125
- /**
126
- * The seven mask groups of C1v2, in canonical order. Python maps these to
127
- * somaskel30 joints and then to motion-rep feature indices; a group missing from
128
- * a mask means "use the top-level weights for those features".
129
- */
130
- export const PRESERVE_GROUPS = Object.freeze([
131
- "root",
132
- "torso",
133
- "head",
134
- "leftArm",
135
- "rightArm",
136
- "leftLeg",
137
- "rightLeg",
138
- ]);
139
-
140
- /**
141
- * IK track id -> the mask groups that track's edits free. SINGLE SOURCE OF
142
- * TRUTH for the JS side (contract: "Track -> group map"); the bridge, the
143
- * effector-constraint builder and this mask builder must all read it from here.
144
- *
145
- * The ids are exactly the ones the pose studio drags, spread across three
146
- * tables in src/ardy/ik.js: IK_TRACKS (hands/feet), MID_TRACKS (elbows/knees)
147
- * and FK_TRACKS (hips/spine/chest/neck/head/shoulders). They are NOT cskel27
148
- * joint names — `leftElbow` is the joint cskel27 calls LeftForeArm, and `chest`
149
- * spans Spine1/Spine2.
150
- *
151
- * Two mappings are worth stating out loud because they look like typos:
152
- * - the SHOULDERS belong to `torso`, not to their arm. A clavicle rotation
153
- * swings the whole shoulder mass and reads as a torso motion; freeing it
154
- * with the arm would let the chest twist inside a preserved spine.
155
- * - `hips` frees `torso` AND `root` — the pelvis bone and the character's
156
- * global position/heading are different features of the representation and
157
- * a hips drag moves both.
158
- */
159
- export const TRACK_GROUPS = Object.freeze({
160
- // arms
161
- leftHand: Object.freeze(["leftArm"]),
162
- leftElbow: Object.freeze(["leftArm"]),
163
- rightHand: Object.freeze(["rightArm"]),
164
- rightElbow: Object.freeze(["rightArm"]),
165
- // legs
166
- leftFoot: Object.freeze(["leftLeg"]),
167
- leftKnee: Object.freeze(["leftLeg"]),
168
- rightFoot: Object.freeze(["rightLeg"]),
169
- rightKnee: Object.freeze(["rightLeg"]),
170
- // head
171
- head: Object.freeze(["head"]),
172
- neck: Object.freeze(["head"]),
173
- // torso
174
- spine: Object.freeze(["torso"]),
175
- chest: Object.freeze(["torso"]),
176
- leftShoulder: Object.freeze(["torso"]),
177
- rightShoulder: Object.freeze(["torso"]),
178
- // the pelvis is both a bone and the character's global transform
179
- hips: Object.freeze(["torso", "root"]),
180
- });
181
-
182
- const KNOWN_TRACK_IDS = Object.freeze(Object.keys(TRACK_GROUPS));
183
-
184
- function requireFinite(value, label, fn = "buildPreserveMask") {
185
- if (typeof value !== "number" || !Number.isFinite(value)) {
186
- throw new Error(`${fn}: ${label} must be a finite number, got ${JSON.stringify(value)}`);
187
- }
188
- return value;
189
- }
190
-
191
- function requirePositive(value, label, fn = "buildPreserveMask") {
192
- requireFinite(value, label, fn);
193
- if (value <= 0) {
194
- throw new Error(`${fn}: ${label} must be greater than 0, got ${JSON.stringify(value)}`);
195
- }
196
- return value;
197
- }
198
-
199
- /**
200
- * Resolve one range's `tracks` list to the canonical group names it frees.
201
- * Returns null when the range has no tracks (a whole-body edit).
202
- */
203
- function resolveGroups(tracks, index) {
204
- if (tracks === undefined || tracks === null) return null;
205
- if (!Array.isArray(tracks)) {
206
- throw new Error(
207
- `buildPreserveMask: editRanges[${index}].tracks must be an array of IK track ids, got ${JSON.stringify(tracks)}`
208
- );
209
- }
210
- // An EMPTY list is refused rather than read as either "whole body" or
211
- // "nothing": both readings are plausible, they differ by the entire clip, and
212
- // the caller that produced it meant one of them. Omitting the key is the
213
- // unambiguous way to say "whole body".
214
- if (tracks.length === 0) {
215
- throw new Error(
216
- `buildPreserveMask: editRanges[${index}].tracks is empty — omit the key entirely for a whole-body edit`
217
- );
218
- }
219
- const groups = new Set();
220
- for (let slot = 0; slot < tracks.length; slot += 1) {
221
- const track = tracks[slot];
222
- const mapped = typeof track === "string" ? TRACK_GROUPS[track] : undefined;
223
- if (!mapped) {
224
- throw new Error(
225
- `buildPreserveMask: editRanges[${index}].tracks[${slot}] ${JSON.stringify(track)} is not a known IK track id; ` +
226
- `valid ids are ${KNOWN_TRACK_IDS.join(", ")}`
227
- );
228
- }
229
- for (const group of mapped) groups.add(group);
230
- }
231
- // Canonical order so two callers naming the same tracks in a different order
232
- // emit byte-identical JSON.
233
- return PRESERVE_GROUPS.filter((group) => groups.has(group));
234
- }
235
-
236
- /**
237
- * The v1 Gaussian, factored out so the narrow mask, the wide mask and every
238
- * group array are provably the same function of a range list and a radius.
239
- *
240
- * `radius` is a plain number here rather than the validated integer option: the
241
- * wide kernel is `influenceRadius * wideRadiusScale` and has no reason to land
242
- * on an integer.
243
- */
244
- function gaussianWeights(ranges, frameCount, radius) {
245
- // 1.0 everywhere = preserve the whole base take. Edits only ever subtract.
246
- const weights = new Array(frameCount).fill(1);
247
- // sigma = radius/2; see the header for why the named radius is 2 sigma.
248
- const sigma = radius / 2;
249
- const twoSigmaSquared = 2 * sigma * sigma;
250
-
251
- for (const { start, end } of ranges) {
252
- for (let frame = 0; frame < frameCount; frame += 1) {
253
- // Distance to the NEAREST frame of the range. `end` is exclusive, so the
254
- // last free frame is end - 1 and the first frame past the range is one
255
- // step away from it, not zero — getting this wrong would shift the whole
256
- // trailing shoulder by a frame.
257
- let distance = 0;
258
- if (frame < start) distance = start - frame;
259
- else if (frame > end - 1) distance = frame - (end - 1);
260
-
261
- let weight;
262
- if (distance === 0) {
263
- weight = 0; // inside the edit: fully free generation
264
- } else if (radius === 0) {
265
- weight = 1; // opt-in hard edge: no shoulder at all
266
- } else {
267
- weight = 1 - Math.exp(-(distance * distance) / twoSigmaSquared);
268
- }
269
-
270
- // MIN, not sum or product: any edit that wants this frame free wins, so
271
- // adding an edit can never preserve a frame harder than before.
272
- if (weight < weights[frame]) weights[frame] = weight;
273
- }
274
- }
275
-
276
- // Clamp defensively. The Gaussian is analytically inside [0, 1), but the mask
277
- // is fed straight into a blend coefficient and a value outside [0, 1] there
278
- // would amplify rather than mix, so the invariant is enforced, not assumed.
279
- for (let frame = 0; frame < frameCount; frame += 1) {
280
- weights[frame] = Math.min(1, Math.max(0, weights[frame]));
281
- }
282
- return weights;
283
- }
284
-
285
- /**
286
- * Turn APP-space edit ranges into the dense per-frame preserve mask (C1 v1/v2).
287
- *
288
- * @param {Array<{startFrame:number,endFrame:number,tracks?:string[]}>} editRanges
289
- * Half-open [startFrame, endFrame) ranges in APP frame space (20 fps clip).
290
- * An EMPTY array is valid and means "nothing was edited": the result is all
291
- * ones, i.e. pure reconstruction of the base take. That case is the project's
292
- * G1 acceptance gate, so it must be expressible, not an error.
293
- * `tracks` (optional, v2) names IK track ids from TRACK_GROUPS; with it the
294
- * range frees only the mapped groups, without it the whole body.
295
- * @param {{appFps:number, genFps:number, genFrames:number, influenceRadius?:number,
296
- * wideRadiusScale?:number|null}} options
297
- * `wideRadiusScale` (default 3) is the multiplier for the paper's high-noise
298
- * wide kernel. Naming it — at any value — opts into `wideWeights`; passing
299
- * null suppresses `wideWeights` even for a grouped mask, which is the escape
300
- * hatch for a caller whose Python side predates the schedule.
301
- * @returns {{version:1|2, genFps:number, genFrames:number, weights:number[],
302
- * wideWeights?:number[], groups?:Object<string,{weights:number[],wideWeights?:number[]}>}}
303
- */
304
- export function buildPreserveMask(editRanges, options = {}) {
305
- const {
306
- appFps,
307
- genFps,
308
- genFrames,
309
- influenceRadius = DEFAULT_INFLUENCE_RADIUS,
310
- wideRadiusScale = DEFAULT_WIDE_RADIUS_SCALE,
311
- } = options ?? {};
312
- // Presence, not value: the default width has to exist for grouped masks, but
313
- // defaulting the EMISSION on would rewrite every round-1 caller's file.
314
- const wideRequested =
315
- options !== null && typeof options === "object" &&
316
- Object.prototype.hasOwnProperty.call(options, "wideRadiusScale");
317
-
318
- if (!Array.isArray(editRanges)) {
319
- throw new Error(`buildPreserveMask: editRanges must be an array, got ${JSON.stringify(editRanges)}`);
320
- }
321
- requirePositive(appFps, "appFps");
322
- requirePositive(genFps, "genFps");
323
- requirePositive(genFrames, "genFrames");
324
- if (genFrames < 1) throw new Error(`buildPreserveMask: genFrames must be >= 1, got ${genFrames}`);
325
- // influenceRadius 0 is EXPLICITLY ALLOWED and means a hard-edged square
326
- // kernel. The paper measured that as the broken case, so it is never the
327
- // default — but a caller that has already smoothed its own seam (or is
328
- // measuring the ablation, tools/kimodo/measure-preserve.mjs) needs the opt
329
- // out, and silently substituting a ramp would falsify their measurement.
330
- if (!Number.isInteger(influenceRadius) || influenceRadius < 0) {
331
- throw new Error(
332
- `buildPreserveMask: influenceRadius must be a non-negative integer, got ${JSON.stringify(influenceRadius)}`
333
- );
334
- }
335
- // A scale below 1 would make the "wide" kernel NARROWER than the narrow one,
336
- // inverting the paper's schedule (high noise would preserve harder than low
337
- // noise). That is not a tuning choice, it is a sign error, so it is refused.
338
- if (wideRadiusScale !== null && (!Number.isFinite(wideRadiusScale) || wideRadiusScale < 1)) {
339
- throw new Error(
340
- `buildPreserveMask: wideRadiusScale must be a finite number >= 1 (or null to suppress wideWeights), got ${JSON.stringify(wideRadiusScale)}`
341
- );
342
- }
343
-
344
- const scale = genFps / appFps;
345
- const frameCount = Math.round(genFrames);
346
- const lastFrame = frameCount - 1;
347
-
348
- // Validate and scale every range in author order first, so a bad edit list is
349
- // refused before any of it shapes the mask.
350
- const scaled = editRanges.map((range, index) => {
351
- if (!range || typeof range !== "object") {
352
- throw new Error(`buildPreserveMask: editRanges[${index}] must be an object`);
353
- }
354
- const { startFrame, endFrame, tracks } = range;
355
- const groups = resolveGroups(tracks, index);
356
- if (!Number.isInteger(startFrame) || startFrame < 0) {
357
- throw new Error(
358
- `buildPreserveMask: editRanges[${index}].startFrame must be a non-negative integer, got ${JSON.stringify(startFrame)}`
359
- );
360
- }
361
- if (!Number.isInteger(endFrame) || endFrame < 0) {
362
- throw new Error(
363
- `buildPreserveMask: editRanges[${index}].endFrame must be a non-negative integer, got ${JSON.stringify(endFrame)}`
364
- );
365
- }
366
- // Ranges are half-open, so an empty or inverted one edits nothing. It is
367
- // almost always an off-by-one in the caller rather than intent, and
368
- // accepting it would produce an all-ones mask that looks like a working
369
- // reconstruction while silently ignoring the user's edit.
370
- if (endFrame <= startFrame) {
371
- throw new Error(
372
- `buildPreserveMask: editRanges[${index}] must be a non-empty half-open range, got [${startFrame}, ${endFrame})`
373
- );
374
- }
375
-
376
- const rawStart = Math.round(startFrame * scale);
377
- const rawEnd = Math.round(endFrame * scale);
378
- // A range that misses the generated clip entirely cannot be clamped into
379
- // anything meaningful — clamping it would free a frame at one end that the
380
- // user never touched. Refuse it by name, quoting the clip length, because
381
- // the usual cause is a mask built against a different take's duration.
382
- if (rawStart >= frameCount || rawEnd <= 0) {
383
- throw new Error(
384
- `buildPreserveMask: editRanges[${index}] [${startFrame}, ${endFrame}) scales to generation frames [${rawStart}, ${rawEnd}) which lies outside the ${frameCount}-frame clip`
385
- );
386
- }
387
-
388
- // Partial overlap CLAMPS rather than throwing: an edit that runs off the
389
- // end of the clip still asks for the frames it shares with it.
390
- const start = Math.min(lastFrame, Math.max(0, rawStart));
391
- // The exclusive end clamps to frameCount, not frameCount - 1: the last
392
- // index this frees is end - 1, which is still inside [0, frameCount - 1].
393
- // Clamping the END index itself would silently preserve the clip's final
394
- // frame even when the user edited right up to it.
395
- // A downscale (e.g. 60 fps app -> 30 fps generation) can also round both
396
- // ends onto one frame; forcing at least one free frame keeps the edit
397
- // visible instead of collapsing it to nothing.
398
- const end = Math.min(frameCount, Math.max(start + 1, rawEnd));
399
- return { start, end, groups };
400
- });
401
-
402
- // Whole-body ranges shape the top level; tracked ranges shape their groups.
403
- const wholeBody = scaled.filter((range) => range.groups === null);
404
- const rangesByGroup = new Map();
405
- for (const range of scaled) {
406
- if (range.groups === null) continue;
407
- for (const group of range.groups) {
408
- if (!rangesByGroup.has(group)) rangesByGroup.set(group, []);
409
- rangesByGroup.get(group).push(range);
410
- }
411
- }
412
-
413
- const weights = gaussianWeights(wholeBody, frameCount, influenceRadius);
414
-
415
- // No edits at all is pure reconstruction (gate G1): a v1 all-ones mask, and
416
- // deliberately NO wideWeights — there is no shoulder whose width could be
417
- // scheduled, and an all-ones wide array would only invite a reader to lerp
418
- // between two identical masks and call it a feature.
419
- const hasEdits = scaled.length > 0;
420
- const hasGroups = rangesByGroup.size > 0;
421
- const emitWide = hasEdits && wideRadiusScale !== null && (hasGroups || wideRequested);
422
- const wideRadius = influenceRadius * (wideRadiusScale ?? 1);
423
-
424
- const mask = {
425
- version: hasGroups || emitWide ? PRESERVE_MASK_VERSION_V2 : PRESERVE_MASK_VERSION,
426
- genFps,
427
- genFrames: frameCount,
428
- weights,
429
- };
430
- if (emitWide) mask.wideWeights = gaussianWeights(wholeBody, frameCount, wideRadius);
431
-
432
- if (hasGroups) {
433
- const groups = {};
434
- // Canonical order, so the JSON is stable regardless of edit order.
435
- for (const name of PRESERVE_GROUPS) {
436
- const own = rangesByGroup.get(name);
437
- if (!own) continue;
438
- // The union with `wholeBody` is the load-bearing part: Python reads this
439
- // array INSTEAD of the top level for the group's features, so a group
440
- // that dropped the whole-body ranges would be exempted from them.
441
- const union = wholeBody.concat(own);
442
- const entry = { weights: gaussianWeights(union, frameCount, influenceRadius) };
443
- if (emitWide) entry.wideWeights = gaussianWeights(union, frameCount, wideRadius);
444
- groups[name] = entry;
445
- }
446
- mask.groups = groups;
447
- }
448
-
449
- return mask;
450
- }
451
-
452
- /**
453
- * The preserve + waypoints mask (paper 4.4, round-2 feature B).
454
- *
455
- * The user keeps a take's style but redraws its path. The root — global
456
- * translation and heading — must therefore come entirely from the waypoints,
457
- * while every other feature rides the preserved take. That is exactly a mask
458
- * whose `root` group is 0 for the whole clip and whose top level is 1.
459
- *
460
- * It lives here rather than in the bridge because it is a MASK SHAPE, and the
461
- * one thing worse than a one-line helper is two callers each inventing their own
462
- * spelling of the same shape. No wideWeights: a constant mask has no shoulder,
463
- * so there is no width to schedule at any noise level.
464
- *
465
- * @param {{genFps:number, genFrames:number}} options
466
- * @returns {{version:2, genFps:number, genFrames:number, weights:number[],
467
- * groups:{root:{weights:number[]}}}}
468
- */
469
- export function rootFreeMask({ genFps, genFrames } = {}) {
470
- requirePositive(genFps, "genFps", "rootFreeMask");
471
- requirePositive(genFrames, "genFrames", "rootFreeMask");
472
- const frameCount = Math.round(genFrames);
473
- return {
474
- version: PRESERVE_MASK_VERSION_V2,
475
- genFps,
476
- genFrames: frameCount,
477
- weights: new Array(frameCount).fill(1),
478
- groups: { root: { weights: new Array(frameCount).fill(0) } },
479
- };
480
- }
481
-
482
- /** Free / ramp / preserved counts for one weights array, validating as it goes. */
483
- function countWeights(weights, label) {
484
- if (!Array.isArray(weights)) {
485
- throw new Error(`preserveMaskStats: ${label} must be an array of finite weights`);
486
- }
487
- let freeFrames = 0;
488
- let preservedFrames = 0;
489
- let rampFrames = 0;
490
- for (let index = 0; index < weights.length; index += 1) {
491
- const weight = weights[index];
492
- if (typeof weight !== "number" || !Number.isFinite(weight)) {
493
- throw new Error(`preserveMaskStats: ${label}[${index}] must be a finite number, got ${JSON.stringify(weight)}`);
494
- }
495
- if (weight === 0) freeFrames += 1;
496
- else if (weight === 1) preservedFrames += 1;
497
- else rampFrames += 1;
498
- }
499
- return { freeFrames, preservedFrames, rampFrames };
500
- }
501
-
502
- /**
503
- * Summarise a mask for the UI and for run reporting: how much of the take is
504
- * being regenerated, how much is held, and how much is the blend in between.
505
- * A run whose rampFrames dwarf its freeFrames is a warning sign — the shoulders
506
- * are wider than the edit, so the "preserved" take will drift almost everywhere.
507
- *
508
- * Accepts either the mask object from buildPreserveMask or a bare weights array.
509
- *
510
- * When the mask carries groups the same three counts are reported per group
511
- * under `groups`, because the top-level numbers of a purely grouped mask are
512
- * "0 free, everything preserved" — technically true and completely misleading
513
- * as a summary of a run that is about to regenerate an arm. The key is ABSENT
514
- * for a groupless mask so a v1 summary stays deep-equal to what it always was.
515
- *
516
- * @param {{weights:number[], groups?:object}|number[]} mask
517
- * @returns {{freeFrames:number, preservedFrames:number, rampFrames:number,
518
- * groups?:Object<string,{freeFrames:number,preservedFrames:number,rampFrames:number}>}}
519
- */
520
- export function preserveMaskStats(mask) {
521
- const weights = Array.isArray(mask) ? mask : mask && mask.weights;
522
- if (!Array.isArray(weights)) {
523
- throw new Error("preserveMaskStats: mask must be a preserve mask object or a weights array");
524
- }
525
- const stats = countWeights(weights, "weights");
526
-
527
- const maskGroups = Array.isArray(mask) ? null : mask.groups;
528
- if (maskGroups !== undefined && maskGroups !== null) {
529
- if (typeof maskGroups !== "object" || Array.isArray(maskGroups)) {
530
- throw new Error(`preserveMaskStats: groups must be an object, got ${JSON.stringify(maskGroups)}`);
531
- }
532
- const names = Object.keys(maskGroups);
533
- if (names.length > 0) {
534
- const groups = {};
535
- for (const name of names) {
536
- const group = maskGroups[name];
537
- groups[name] = countWeights(group && group.weights, `groups.${name}.weights`);
538
- }
539
- stats.groups = groups;
540
- }
541
- }
542
- return stats;
543
- }