cozyclay 2.1.1 → 3.0.1

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