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,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
- }