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,805 +0,0 @@
1
- /**
2
- * generate.mjs — drive the ProjFlow line-edit driver on the configured box and
3
- * bring the result back as raw hml22 joint positions.
4
- *
5
- * Shaped after tools/kimodo/generate.mjs (ssh/scp helpers, error style, one
6
- * mkdtemp per run that the `finally` always removes), with one structural
7
- * difference stated up front: Kimodo ships a CLI and we only pass it flags, so
8
- * that module has no python of its own. ProjFlow ships two samplers and NEITHER
9
- * CLI can express a CozyClay line edit — the projflow inpainting sampler
10
- * preserves source frames but has no camera, the linear-constraint sampler takes
11
- * a general operator but its demo builds no preserve rows. Contract C7 therefore
12
- * puts a driver of ours on the box, and this module SHIPS it per run
13
- * (tools/projflow/driver.py), the same way the Kimodo wrapper ships constraint
14
- * JSONs. Nothing is installed on the box; the run directory is the whole
15
- * footprint.
16
- *
17
- * THE NUMBERS THIS MODULE DOES NOT COMPUTE. Every constraint row — the affine
18
- * perspective rows for the drawn line, the identity rows that freeze the rest of
19
- * the take — is built inside driver.py, because that is where the model's
20
- * normalisation stats and column layout live. This side does exactly three
21
- * things: validate the request hard enough that a malformed line never costs a
22
- * GPU round trip, move files, and read the result back.
23
- *
24
- * SEAM: THIS MODULE RETURNS 22-JOINT POSITIONS, NOT cskel27. ACMDM's "Raw"
25
- * family generates absolute coordinates with no rotation channel at all, so the
26
- * conversion to cskel27 has to LIFT rotations (aim-based bone frames + inherited
27
- * twist) rather than re-express them. That is contract C8 and agent M2's module,
28
- * tools/projflow/hml22-to-cskel27.mjs, which is in flight in parallel with this
29
- * one. It is deliberately NOT imported here: wave-2 integration composes
30
- * `lineEditOnBox(...).positions` with `hml22ToCskel27Motion(...)`, so the two
31
- * modules can land independently and this one stays testable without a
32
- * converter. Do not add the import until that module exists.
33
- *
34
- * FRAME CLOCKS. `line.frameRange` is in GENERATION frames at 20 fps, which is
35
- * what the model samples and what driver.py indexes. The app's clip frames (24
36
- * fps) are converted upstream, in the bridge — the same split Kimodo uses, where
37
- * generate.mjs owns the generation clock and the bridge owns the app clock.
38
- * There is no conversion in this file, on purpose: two places computing the same
39
- * frame index is exactly how they drift.
40
- *
41
- * TWO ROUTES, ONE FUNCTION (contract C11). `lineEditOnBox` first offers the
42
- * request to the RESIDENT service (service.mjs): a warm `driver.py --serve`
43
- * child that already holds the checkpoint, which removes the 3.9 s model load
44
- * that gate GP3 measured as half of an 8 s round trip. If anything at all goes
45
- * wrong with that child — it is not up, it dies, it answers late, it answers
46
- * with the wrong id, it refuses the request — the request runs down the COLD
47
- * path below, unchanged, which is the contract of record. The cold path does
48
- * not know the resident exists and never will; that is what makes the fallback
49
- * trustworthy rather than a second implementation to keep in sync.
50
- */
51
-
52
- import { spawn } from "node:child_process";
53
- import { copyFile, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
54
- import { readFileSync, writeFileSync } from "node:fs";
55
- import { tmpdir } from "node:os";
56
- // `resolve` is aliased: the promise executor in `run` below binds its own
57
- // `resolve`, and a path helper shadowed by a promise callback is a trap.
58
- import { dirname, join, resolve as resolvePath } from "node:path";
59
- import { fileURLToPath, pathToFileURL } from "node:url";
60
-
61
- import { getResidentService, residentEnabled, residentLineEdit } from "./service.mjs";
62
-
63
- const HERE = dirname(fileURLToPath(import.meta.url));
64
- const DRIVER = join(HERE, "driver.py");
65
-
66
- const SSH_OPTS = [
67
- "-o", "BatchMode=yes",
68
- "-o", "ConnectTimeout=10",
69
- "-o", "ServerAliveInterval=30",
70
- "-o", "ServerAliveCountMax=240",
71
- ];
72
-
73
- /** S1 measured the model at 20 fps, (T,22,3) world metres. Not configurable:
74
- * it is a property of the checkpoint, not a preference. */
75
- export const GEN_FPS = 20;
76
- export const NUM_JOINTS = 22;
77
-
78
- /** S1: 100 ODE steps for a final, 20 for a preview (0.95 s / 0.28 s measured on
79
- * a 196-frame take with ~3800 constraint rows). */
80
- export const DEFAULT_STEPS = 100;
81
- export const PREVIEW_STEPS = 20;
82
-
83
- /** S1: ridge_lambda is the EXACTNESS knob, not steps. The repo ships 1e-3 and
84
- * lands at 2.6e-5; 1e-6 gives 2.4e-7 at half the steps. */
85
- export const DEFAULT_RIDGE = 1e-6;
86
-
87
- /**
88
- * CozyClay IK track id -> hml22 joint index. THE authoring source; driver.py
89
- * carries the same table purely as a guard and errors if the two disagree, so a
90
- * wrapper that drifts fails loudly on the box instead of editing the wrong limb.
91
- *
92
- * Composed from two documented hops: track -> cskel27 target (src/ardy/ik.js's
93
- * own IK_TRACKS/MID_TRACKS/FK_TRACKS) -> hml22 source (S2's mapping table).
94
- * Three entries look like typos and are not:
95
- * - `leftFoot` is cskel27 LeftFoot, whose hml22 source is left_ANKLE (7).
96
- * hml22's `left_foot` (10) is the TOE BASE — S2's naming trap.
97
- * - `leftShoulder` is the CLAVICLE, so left_collar (13), not left_shoulder
98
- * (16, which is the upper-arm root and is cskel27's LeftArm).
99
- * - a MID-JOINT track constrains its OWN joint (leftElbow -> 18), not its
100
- * chain's effector. The artist dragged the elbow handle and drew the
101
- * elbow's path; pinning the wrist instead would follow the line with the
102
- * wrong joint and leave the elbow wherever the model liked.
103
- */
104
- export const TRACK_TO_HML22_JOINT = Object.freeze({
105
- leftHand: 20,
106
- rightHand: 21,
107
- leftFoot: 7,
108
- rightFoot: 8,
109
- leftElbow: 18,
110
- rightElbow: 19,
111
- leftKnee: 4,
112
- rightKnee: 5,
113
- hips: 0,
114
- spine: 6,
115
- neck: 12,
116
- head: 15,
117
- leftShoulder: 13,
118
- rightShoulder: 14,
119
- });
120
-
121
- /**
122
- * The line-edit tracks with no hml22 joint behind them, with the reason.
123
- *
124
- * `chest` is cskel27 Spine2 and Spine2 is one of S2's five FILLED joints:
125
- * cskel27 has three spine links between Hips and Spine3 where hml22 has two, so
126
- * there is literally no source joint to constrain. Refused by name rather than
127
- * retargeted onto a neighbouring spine joint, which would put the drawn line on
128
- * a curve the artist did not draw.
129
- *
130
- * TRACK_TO_HML22_JOINT + UNMAPPABLE_TRACKS covers all 15 pose-studio tracks;
131
- * test/verify-projflow-runner.mjs asserts that totality against ik.js itself.
132
- */
133
- export const UNMAPPABLE_TRACKS = Object.freeze({
134
- chest: "cskel27 Spine2 has no hml22 source joint (S2: a FILLED joint)",
135
- });
136
-
137
- function run(argv, { timeoutMs = 3_600_000, onLine } = {}) {
138
- return new Promise((resolve) => {
139
- const child = spawn(argv[0], argv.slice(1), { stdio: ["ignore", "pipe", "pipe"] });
140
- let stdout = "";
141
- let stderr = "";
142
- const timer = setTimeout(() => child.kill("SIGKILL"), timeoutMs);
143
- child.stdout.on("data", (chunk) => {
144
- stdout += chunk;
145
- if (onLine) for (const line of String(chunk).split("\n")) if (line.trim()) onLine(line);
146
- });
147
- child.stderr.on("data", (chunk) => {
148
- stderr += chunk;
149
- if (onLine) for (const line of String(chunk).split("\n")) if (line.trim()) onLine(line);
150
- });
151
- child.on("close", (code) => {
152
- clearTimeout(timer);
153
- resolve({ code, stdout, stderr });
154
- });
155
- });
156
- }
157
-
158
- /** hml22 joint for an IK track id, or a named refusal. */
159
- export function trackToJoint(track) {
160
- if (typeof track !== "string" || !track) {
161
- throw new Error("lineEdit: track must be a non-empty IK track id");
162
- }
163
- if (Object.hasOwn(UNMAPPABLE_TRACKS, track)) {
164
- throw new Error(`lineEdit: track "${track}" cannot be line-edited: ${UNMAPPABLE_TRACKS[track]}`);
165
- }
166
- const joint = TRACK_TO_HML22_JOINT[track];
167
- if (joint === undefined) {
168
- throw new Error(
169
- `lineEdit: unknown track "${track}"; known line-edit tracks: ${Object.keys(TRACK_TO_HML22_JOINT).sort().join(", ")}`
170
- );
171
- }
172
- return joint;
173
- }
174
-
175
- /**
176
- * Validate a C6 line-edit request and return the exact document driver.py reads.
177
- *
178
- * Every check here exists because the alternative is discovering it after a
179
- * 6-second ssh round trip and a model load. The geometry is NOT checked (whether
180
- * the drawn line is reachable is the model's problem — that is the whole point
181
- * of a zero-shot projection sampler), only the things that would make the
182
- * operator malformed or the request meaningless.
183
- */
184
- export function buildLineRequest(line) {
185
- if (!line || typeof line !== "object" || Array.isArray(line)) {
186
- throw new Error("lineEdit: line must be an object");
187
- }
188
- const jointId = trackToJoint(line.track);
189
- if (line.jointId !== undefined && Number(line.jointId) !== jointId) {
190
- throw new Error(
191
- `lineEdit: jointId ${line.jointId} disagrees with track "${line.track}" -> joint ${jointId}`
192
- );
193
- }
194
-
195
- const range = line.frameRange;
196
- if (!range || typeof range !== "object") {
197
- throw new Error("lineEdit: frameRange {start, end} is required");
198
- }
199
- const start = Number(range.start);
200
- const end = Number(range.end);
201
- for (const [label, value] of [["start", start], ["end", end]]) {
202
- if (!Number.isInteger(value) || value < 0) {
203
- throw new Error(`lineEdit: frameRange.${label} must be a non-negative integer, got ${JSON.stringify(range[label])}`);
204
- }
205
- }
206
- // A one-frame range is two rows against one pose, which the sampler happily
207
- // solves and no artist ever means; a line needs somewhere to go.
208
- if (end <= start) {
209
- throw new Error(`lineEdit: frameRange must span at least two frames, got ${start}..${end}`);
210
- }
211
-
212
- // PINS take the whole rest of this function's job away. A pins edit carries
213
- // three identity rows per pin and no camera at all, so every check below —
214
- // the normalised-uv range, the pixel-focal-length trap, the R/t shape — is
215
- // about a field it does not have. Branching here rather than threading
216
- // `if (pins)` through each check keeps both readings of the request
217
- // self-contained, which is what makes the serialised request diffable.
218
- if (line.pins3d !== undefined) {
219
- if (line.points2d !== undefined || line.camera !== undefined) {
220
- throw new Error("lineEdit: a pins edit carries pins3d alone — no points2d, no camera");
221
- }
222
- const pins = line.pins3d;
223
- if (!Array.isArray(pins) || pins.length < 1) {
224
- throw new Error(`lineEdit: pins3d needs at least 1 pin, got ${Array.isArray(pins) ? pins.length : typeof pins}`);
225
- }
226
- let previous = -1;
227
- const pins3d = pins.map((pin, index) => {
228
- if (!pin || typeof pin !== "object" || !Number.isInteger(pin.frame)) {
229
- throw new Error(`lineEdit: pins3d[${index}] must be { frame: integer, position: [x, y, z] }`);
230
- }
231
- // GENERATION-clock frames by the time they reach here (line-edit-job
232
- // scales them), so the range they must sit inside is this request's.
233
- if (pin.frame < start || pin.frame > end) {
234
- throw new Error(`lineEdit: pins3d[${index}].frame ${pin.frame} is outside frameRange ${start}..${end}`);
235
- }
236
- if (pin.frame <= previous) {
237
- throw new Error(`lineEdit: pins3d frames must be strictly ascending, got ${pin.frame} after ${previous}`);
238
- }
239
- previous = pin.frame;
240
- if (!Array.isArray(pin.position) || pin.position.length !== 3) {
241
- throw new Error(`lineEdit: pins3d[${index}].position must be [x, y, z] world metres`);
242
- }
243
- const position = pin.position.map((value, axis) => {
244
- const number = Number(value);
245
- if (!Number.isFinite(number)) throw new Error(`lineEdit: pins3d[${index}].position[${axis}] must be finite`);
246
- return number;
247
- });
248
- return { frame: pin.frame, position };
249
- });
250
- return {
251
- track: line.track,
252
- jointId,
253
- frameRange: { start, end },
254
- pins3d,
255
- prompt: line.prompt === undefined || line.prompt === null ? "" : String(line.prompt),
256
- };
257
- }
258
-
259
- const points = line.points2d;
260
- if (!Array.isArray(points) || points.length < 2) {
261
- throw new Error(`lineEdit: points2d needs at least 2 points, got ${Array.isArray(points) ? points.length : typeof points}`);
262
- }
263
- const points2d = points.map((point, index) => {
264
- if (!Array.isArray(point) || point.length !== 2) {
265
- throw new Error(`lineEdit: points2d[${index}] must be [u, v]`);
266
- }
267
- const [u, v] = point.map(Number);
268
- if (!Number.isFinite(u) || !Number.isFinite(v)) {
269
- throw new Error(`lineEdit: points2d[${index}] must be two finite numbers, got ${JSON.stringify(point)}`);
270
- }
271
- // Viewport-normalised, per C6. Enforced rather than clamped: a point
272
- // outside the viewport is either a unit mix-up (pixels sent as
273
- // normalised) or a stroke the artist could not have seen, and both are
274
- // worth a message. The tolerance covers a stroke that grazed the edge.
275
- if (u < -0.01 || u > 1.01 || v < -0.01 || v > 1.01) {
276
- throw new Error(
277
- `lineEdit: points2d[${index}] = [${u}, ${v}] is outside the 0..1 normalised viewport; ` +
278
- "points2d and the camera intrinsics must both be in normalised units"
279
- );
280
- }
281
- return [u, v];
282
- });
283
-
284
- const camera = line.camera;
285
- if (!camera || typeof camera !== "object") {
286
- throw new Error("lineEdit: camera {fx, fy, cx, cy, R, t} is required");
287
- }
288
- const intrinsics = {};
289
- for (const key of ["fx", "fy", "cx", "cy"]) {
290
- const value = Number(camera[key]);
291
- if (!Number.isFinite(value)) {
292
- throw new Error(`lineEdit: camera.${key} must be a finite number, got ${JSON.stringify(camera[key])}`);
293
- }
294
- intrinsics[key] = value;
295
- }
296
- // A zero focal length collapses both rows of that axis to the depth row and
297
- // the normal matrix goes singular; the Cholesky failure on the box would name
298
- // LAPACK, not the camera.
299
- for (const key of ["fx", "fy"]) {
300
- if (intrinsics[key] === 0) throw new Error(`lineEdit: camera.${key} must be non-zero`);
301
- }
302
- // Intrinsics live in the SAME units as points2d (normalised 0..1). A pixel
303
- // focal length beside normalised points is the single most likely wire
304
- // mistake and it produces a plausible-looking but wrong line, so it is caught
305
- // by magnitude here rather than debugged from a bad take later.
306
- if (Math.abs(intrinsics.fx) > 50 || Math.abs(intrinsics.fy) > 50) {
307
- throw new Error(
308
- `lineEdit: camera.fx/fy look like PIXEL focal lengths (${intrinsics.fx}, ${intrinsics.fy}); ` +
309
- "they must be normalised by the viewport size, like points2d"
310
- );
311
- }
312
- if (!Array.isArray(camera.R) || camera.R.length !== 3 || camera.R.some((row) => !Array.isArray(row) || row.length !== 3)) {
313
- throw new Error("lineEdit: camera.R must be a 3x3 world-to-camera rotation");
314
- }
315
- const R = camera.R.map((row, i) => row.map((value, j) => {
316
- const number = Number(value);
317
- if (!Number.isFinite(number)) throw new Error(`lineEdit: camera.R[${i}][${j}] must be finite`);
318
- return number;
319
- }));
320
- if (!Array.isArray(camera.t) || camera.t.length !== 3) {
321
- throw new Error("lineEdit: camera.t must be a 3-vector (camera-space translation)");
322
- }
323
- const t = camera.t.map((value, i) => {
324
- const number = Number(value);
325
- if (!Number.isFinite(number)) throw new Error(`lineEdit: camera.t[${i}] must be finite`);
326
- return number;
327
- });
328
-
329
- const prompt = line.prompt === undefined || line.prompt === null ? "" : String(line.prompt);
330
-
331
- // Field order is fixed so two identical requests serialise byte-identically
332
- // and a run directory can be diffed.
333
- return {
334
- track: line.track,
335
- jointId,
336
- frameRange: { start, end },
337
- points2d,
338
- camera: { ...intrinsics, R, t },
339
- prompt,
340
- };
341
- }
342
-
343
- /**
344
- * Minimal reader for a numpy .npy v1/v2 array of little-endian float32 in C
345
- * order — which is what `np.save` writes for the driver's (T,22,3) output.
346
- *
347
- * tools/kimodo/read-npz.mjs has a reader for the same payload format, but it is
348
- * bound to the ZIP container `np.savez` produces and returns members by name.
349
- * ProjFlow writes a bare .npy, so the container layer does not apply; the header
350
- * grammar below is the same one, restated for the one dtype this path can
351
- * produce. Widening or Fortran order are refused rather than guessed: a silently
352
- * transposed motion is indistinguishable from a bad take.
353
- */
354
- export function readNpyFloat32(path) {
355
- const buffer = readFileSync(path);
356
- if (buffer.toString("latin1", 0, 6) !== "\x93NUMPY") {
357
- throw new Error(`readNpyFloat32: ${path} is not a .npy file`);
358
- }
359
- const major = buffer.readUInt8(6);
360
- const headerLength = major === 1 ? buffer.readUInt16LE(8) : buffer.readUInt32LE(8);
361
- const headerStart = major === 1 ? 10 : 12;
362
- const header = buffer.toString("latin1", headerStart, headerStart + headerLength);
363
-
364
- const descr = /'descr'\s*:\s*'([^']+)'/.exec(header)?.[1];
365
- const fortran = /'fortran_order'\s*:\s*(True|False)/.exec(header)?.[1];
366
- const shapeText = /'shape'\s*:\s*\(([^)]*)\)/.exec(header)?.[1];
367
- if (!descr || !fortran || shapeText === undefined) {
368
- throw new Error(`readNpyFloat32: ${path} has an unreadable .npy header`);
369
- }
370
- if (descr !== "<f4") {
371
- throw new Error(`readNpyFloat32: ${path} has dtype ${descr}; the driver writes float32`);
372
- }
373
- if (fortran === "True") {
374
- throw new Error(`readNpyFloat32: ${path} is Fortran-ordered; only C order is supported`);
375
- }
376
- const shape = shapeText
377
- .split(",")
378
- .map((part) => part.trim())
379
- .filter((part) => part.length > 0)
380
- .map((part) => {
381
- const value = Number(part);
382
- if (!Number.isInteger(value) || value < 0) {
383
- throw new Error(`readNpyFloat32: ${path} has a bad shape entry ${part}`);
384
- }
385
- return value;
386
- });
387
-
388
- const count = shape.reduce((total, dim) => total * dim, 1);
389
- const body = buffer.subarray(headerStart + headerLength);
390
- if (body.length < count * 4) {
391
- throw new Error(`readNpyFloat32: ${path} is truncated (need ${count * 4} bytes, have ${body.length})`);
392
- }
393
- // A copy, not a view: `buffer` is a pooled Node allocation whose byteOffset
394
- // need not be 4-aligned, which a Float32Array view over it would reject.
395
- const data = new Float32Array(count);
396
- for (let index = 0; index < count; index += 1) data[index] = body.readFloatLE(index * 4);
397
- return { shape, data };
398
- }
399
-
400
- /**
401
- * Write an hml22 (T,22,3) float32 .npy — the inverse of readNpyFloat32, and the
402
- * ONLY way to produce this module's own `sourceMotionNpy` input.
403
- *
404
- * It lives here rather than in the wave-2 composition layer because the header
405
- * grammar above is the contract with driver.py: a writer that padded or spelled
406
- * the dict differently would be a second, drifting definition of the same file
407
- * format, and the round trip readNpyFloat32(writeNpyFloat32(x)) is the cheapest
408
- * possible test of both. (tools/ardy/npz.mjs has the same 10-byte prefix inside
409
- * its ZIP builder, but it only ever emits ARCHIVES; a bare .npy is not one of
410
- * its outputs.)
411
- *
412
- * @param {string} path
413
- * @param {Float32Array} data C-order payload, shape.reduce(*) long
414
- * @param {number[]} shape
415
- */
416
- export function writeNpyFloat32(path, data, shape) {
417
- if (!(data instanceof Float32Array)) {
418
- throw new Error("writeNpyFloat32: data must be a Float32Array");
419
- }
420
- if (!Array.isArray(shape) || shape.length === 0 || shape.some((dim) => !Number.isInteger(dim) || dim < 0)) {
421
- throw new Error(`writeNpyFloat32: shape must be non-negative integers, got ${JSON.stringify(shape)}`);
422
- }
423
- const count = shape.reduce((total, dim) => total * dim, 1);
424
- if (count !== data.length) {
425
- throw new Error(`writeNpyFloat32: shape (${shape.join(",")}) needs ${count} values, got ${data.length}`);
426
- }
427
- // A NaN reaching the box surfaces as a LAPACK failure inside the Cholesky,
428
- // which names neither the frame nor the joint; refuse it here instead.
429
- for (let index = 0; index < data.length; index += 1) {
430
- if (!Number.isFinite(data[index])) {
431
- throw new Error(`writeNpyFloat32: data[${index}] is ${data[index]}`);
432
- }
433
- }
434
- const tuple = `(${shape.join(", ")}${shape.length === 1 ? "," : ""})`;
435
- const dict = `{'descr': '<f4', 'fortran_order': False, 'shape': ${tuple}, }`;
436
- // numpy pads so that magic(6) + version(2) + length(2) + header is a multiple
437
- // of 64; the same convention tools/ardy/npz.mjs uses.
438
- const header = `${dict}${" ".repeat((53 - (dict.length % 64) + 64) % 64)}\n`;
439
- const prefix = Buffer.alloc(10);
440
- prefix.writeUInt8(0x93, 0);
441
- prefix.write("NUMPY", 1, "ascii");
442
- prefix.writeUInt8(1, 6);
443
- prefix.writeUInt8(0, 7);
444
- prefix.writeUInt16LE(header.length, 8);
445
- writeFileSync(
446
- path,
447
- Buffer.concat([prefix, Buffer.from(header, "ascii"), Buffer.from(data.buffer, data.byteOffset, data.byteLength)])
448
- );
449
- }
450
-
451
- /**
452
- * The warm route: hand the request to the resident child and shape its answer
453
- * like a cold run's.
454
- *
455
- * WHY IT MIRRORS THE COLD ARGV RATHER THAN IMPROVING ON IT. The cold path
456
- * resolves `preview` into a step count and passes `--steps 20`, never
457
- * `--preview`, so a cold preview's metadata says `preview: false`. This sends
458
- * exactly the same thing, because two routes that produce different metadata
459
- * for the same request would make every warm-vs-cold comparison (and gate GS1's
460
- * fallback proof) an argument about which one is right.
461
- *
462
- * `nativeOut` is written HERE rather than scp'd back, since the motion arrived
463
- * in memory. Same two files, same names, same contract: the raw hml22 .npy and
464
- * its .meta.json sidecar, which is what a LATER edit sources from.
465
- */
466
- async function warmLineEdit({
467
- sourceMotionNpy,
468
- request,
469
- steps,
470
- ridge,
471
- preserveStride,
472
- preserveMargin,
473
- seamPin,
474
- seed,
475
- cfg,
476
- host,
477
- python,
478
- repo,
479
- boxHome,
480
- nativeOut,
481
- onLine,
482
- service,
483
- }) {
484
- const { shape, data } = readNpyFloat32(sourceMotionNpy);
485
- if (shape.length !== 3 || shape[1] !== NUM_JOINTS || shape[2] !== 3) {
486
- throw new Error(`lineEditOnBox: sourceMotionNpy must be (T,22,3), got (${shape.join(",")})`);
487
- }
488
- const resident = service || getResidentService({ host, python, repo, boxHome });
489
- const warm = await residentLineEdit({
490
- service: resident,
491
- source: data,
492
- sourceShape: shape,
493
- line: request,
494
- steps,
495
- preview: false,
496
- ridge,
497
- preserveStride,
498
- preserveMargin,
499
- seamPin,
500
- seed,
501
- cfg,
502
- onLine,
503
- });
504
- if (warm.shape.length !== 3 || warm.shape[1] !== NUM_JOINTS || warm.shape[2] !== 3) {
505
- throw new Error(`lineEditOnBox: the resident returned a (${warm.shape.join(",")}) result, expected (T,22,3)`);
506
- }
507
- // The ONE key that is not in a cold run's metadata. It is worth the
508
- // asymmetry: without it a report cannot say whether an edit was warm, and
509
- // "how many edits fell back" is the number this contract is judged on.
510
- const meta = { ...warm.meta, resident: true };
511
- if (nativeOut) {
512
- writeNpyFloat32(nativeOut, warm.positions, warm.shape);
513
- writeFileSync(`${String(nativeOut).replace(/\.npy$/i, "")}.meta.json`, `${JSON.stringify(meta, null, 2)}\n`);
514
- }
515
- return {
516
- positions: warm.positions,
517
- frames: warm.shape[0],
518
- joints: warm.shape[1],
519
- fps: GEN_FPS,
520
- meta,
521
- line: request,
522
- nativeNpy: nativeOut || null,
523
- };
524
- }
525
-
526
- /**
527
- * Run one line edit on the box.
528
- *
529
- * @param {object} options
530
- * @param {string} options.sourceMotionNpy local path to the source take as an
531
- * hml22 (T,22,3) float32 .npy — the SAME format the driver writes, so the
532
- * output of one edit is the input of the next.
533
- * @param {object} options.line a C6 line-edit request (see buildLineRequest).
534
- * @returns {Promise<{positions: Float32Array, frames: number, joints: number,
535
- * fps: number, meta: object, line: object}>} `positions` is (frames*22*3) in
536
- * C order, world metres, hml22 joint order. Converting to cskel27 is M2's
537
- * module — see the seam note in the file header.
538
- */
539
- export async function lineEditOnBox({
540
- sourceMotionNpy,
541
- line,
542
- // 100 final / 20 preview (contract C7). `preview` wins over `steps` because
543
- // it is the interactive path's whole point.
544
- steps = Number(process.env.CCLAY_PROJFLOW_STEPS || DEFAULT_STEPS),
545
- preview = false,
546
- ridge = Number(process.env.CCLAY_PROJFLOW_RIDGE || DEFAULT_RIDGE),
547
- // Preserve-row subsampling. The policy and its reasoning live in driver.py's
548
- // build_preserve_rows; these are the knobs, not the decision.
549
- preserveStride = Number(process.env.CCLAY_PROJFLOW_PRESERVE_STRIDE || 2),
550
- preserveMargin = Number(process.env.CCLAY_PROJFLOW_PRESERVE_MARGIN || 20),
551
- // How many of the edit range's OWN outermost frames are pinned whole-body to
552
- // the source. Licensed by the app's guarantee that the curve's first and last
553
- // points already sit on the source trail; see build_preserve_rows rule 4 for
554
- // the measurement. 0 restores the pre-seam-pin behaviour.
555
- seamPin = Number(process.env.CCLAY_PROJFLOW_SEAM_PIN ?? 2),
556
- seed,
557
- cfg,
558
- // CCLAY_PROJFLOW_HOST falls back to the Kimodo host: one GPU box serves both
559
- // engines today, and making the operator set two variables to the same value
560
- // is how one of them ends up stale.
561
- host = process.env.CCLAY_PROJFLOW_HOST || process.env.CCLAY_KIMODO_HOST || "",
562
- repo = process.env.CCLAY_PROJFLOW_REPO || "/home/yun/projflow-scout/repo",
563
- python = process.env.CCLAY_PROJFLOW_PYTHON || "/home/yun/projflow-scout/venv/bin/python",
564
- // S1 pinned HOME inside the scout directory so CLIP's 338 MB ViT-B/32
565
- // download never lands in the box user's real cache. The model loads CLIP on
566
- // every run, so this is not optional — with the default HOME the first run
567
- // re-downloads it somewhere we do not clean up.
568
- boxHome = process.env.CCLAY_PROJFLOW_HOME || "/home/yun/projflow-scout/home",
569
- // Where to keep the RAW hml22 result (and its .meta.json) after the run
570
- // directory is removed. Same role as Kimodo's CCLAY_KIMODO_NATIVE_OUT: the
571
- // take a LATER edit sources from must be in the engine's own format, and the
572
- // cskel27 file downstream serves is a lossy conversion of it.
573
- nativeOut = process.env.CCLAY_PROJFLOW_NATIVE_OUT || "",
574
- onLine,
575
- } = {}) {
576
- if (!host) {
577
- throw new Error("lineEditOnBox: CCLAY_PROJFLOW_HOST (or CCLAY_KIMODO_HOST) is required");
578
- }
579
- if (typeof sourceMotionNpy !== "string" || !sourceMotionNpy) {
580
- throw new Error("lineEditOnBox: sourceMotionNpy is required (an hml22 (T,22,3) .npy)");
581
- }
582
- // Validated BEFORE anything touches the network: a malformed request must
583
- // cost milliseconds, not an ssh round trip plus a 3.9 s model load.
584
- const request = buildLineRequest(line);
585
- const effectiveSteps = preview ? PREVIEW_STEPS : Number(steps);
586
- if (!Number.isInteger(effectiveSteps) || effectiveSteps < 1) {
587
- throw new Error(`lineEditOnBox: steps must be a positive integer, got ${JSON.stringify(steps)}`);
588
- }
589
- if (!Number.isFinite(ridge) || ridge < 0) {
590
- throw new Error(`lineEditOnBox: ridge must be a non-negative number, got ${JSON.stringify(ridge)}`);
591
- }
592
-
593
- // --- the warm route (contract C11) --------------------------------------
594
- // Tried first, never retried, never allowed to fail an edit. Everything the
595
- // cold argv below carries travels in the request, so the two routes ask the
596
- // same driver for the same thing and the meta comes back identical.
597
- if (residentEnabled()) {
598
- try {
599
- return await warmLineEdit({
600
- sourceMotionNpy,
601
- request,
602
- steps: effectiveSteps,
603
- ridge,
604
- preserveStride: Number(preserveStride),
605
- preserveMargin: Number(preserveMargin),
606
- seamPin: Number(seamPin),
607
- seed,
608
- cfg,
609
- host,
610
- python,
611
- repo,
612
- boxHome,
613
- nativeOut,
614
- onLine,
615
- });
616
- } catch (error) {
617
- // Loud on the status stream, because a session that silently pays the
618
- // cold price every edit is the failure mode this contract exists to
619
- // prevent — the kill criterion is stated in edits-per-fallback.
620
- onLine?.(`run-projflow-line-edit: resident unavailable (${error.message}); running the cold path`);
621
- }
622
- }
623
-
624
- const remoteDir = `/tmp/cclay-projflow-${Date.now()}-${Math.floor(Math.random() * 1e6)}`;
625
- const localDir = await mkdtemp(join(tmpdir(), "cclay-projflow-"));
626
- try {
627
- const made = await run(["ssh", ...SSH_OPTS, host, `mkdir -p ${remoteDir}`]);
628
- if (made.code !== 0) {
629
- throw new Error(`could not create ${remoteDir} on ${host}: ${made.stderr.trim()}`);
630
- }
631
- // Local paths travel as scp ARGV entries, never inside a shell string;
632
- // every remote path is built from remoteDir, which this process generated.
633
- const push = async (localPath, remotePath, label) => {
634
- const pushed = await run(["scp", ...SSH_OPTS, localPath, `${host}:${remotePath}`]);
635
- if (pushed.code !== 0) {
636
- throw new Error(`could not copy ${label} to ${host} (exit ${pushed.code}): ${pushed.stderr.trim()}`);
637
- }
638
- };
639
-
640
- const localLine = join(localDir, "line.json");
641
- await writeFile(localLine, `${JSON.stringify(request, null, 2)}\n`);
642
- // The driver ships per run rather than being installed: the box keeps no
643
- // CozyClay state, so a driver change takes effect on the next request with
644
- // no deploy step and no chance of a stale copy on the box.
645
- await push(DRIVER, `${remoteDir}/driver.py`, "the line-edit driver");
646
- await push(localLine, `${remoteDir}/line.json`, "the line request");
647
- await push(sourceMotionNpy, `${remoteDir}/source.npy`, "the source motion");
648
-
649
- // One invocation. HOME is an env assignment on the same command word
650
- // list; `cd` would be a separate command and is not needed because the
651
- // driver chdirs to the repo itself (its checkpoint paths are repo-relative).
652
- const words = [
653
- `HOME=${boxHome}`,
654
- python,
655
- `${remoteDir}/driver.py`,
656
- `--source ${remoteDir}/source.npy`,
657
- `--line ${remoteDir}/line.json`,
658
- `--out ${remoteDir}/edited.npy`,
659
- `--repo ${JSON.stringify(repo)}`,
660
- `--steps ${effectiveSteps}`,
661
- `--ridge ${ridge}`,
662
- `--preserve-stride ${Number(preserveStride)}`,
663
- `--preserve-margin ${Number(preserveMargin)}`,
664
- `--seam-pin ${Number(seamPin)}`,
665
- seed === undefined ? "" : `--seed ${Number(seed)}`,
666
- cfg === undefined ? "" : `--cfg ${Number(cfg)}`,
667
- ].filter(Boolean).join(" ");
668
-
669
- const edited = await run(["ssh", ...SSH_OPTS, host, words], { onLine });
670
- if (edited.code !== 0) {
671
- throw new Error(
672
- `the ProjFlow line-edit driver on ${host} failed (exit ${edited.code}):\n` +
673
- edited.stderr.split("\n").slice(-25).join("\n")
674
- );
675
- }
676
-
677
- const localNpy = join(localDir, "edited.npy");
678
- const localMeta = join(localDir, "edited.meta.json");
679
- for (const [remoteName, localPath, label] of [
680
- ["edited.npy", localNpy, "the edited motion"],
681
- ["edited.meta.json", localMeta, "the run metadata"],
682
- ]) {
683
- const copied = await run(["scp", ...SSH_OPTS, `${host}:${remoteDir}/${remoteName}`, localPath]);
684
- if (copied.code !== 0) {
685
- throw new Error(`scp of ${label} failed (exit ${copied.code}): ${copied.stderr.trim()}`);
686
- }
687
- }
688
-
689
- const { shape, data } = readNpyFloat32(localNpy);
690
- if (shape.length !== 3 || shape[1] !== NUM_JOINTS || shape[2] !== 3) {
691
- throw new Error(`lineEditOnBox: expected a (T,22,3) result, got (${shape.join(",")})`);
692
- }
693
- // `resident: false` is the counterpart of the warm route's `true`: every
694
- // result says which route produced it, so a session that quietly stopped
695
- // being warm is visible in the report instead of only in the clock.
696
- const meta = { ...JSON.parse(await readFile(localMeta, "utf8")), resident: false };
697
- if (nativeOut) {
698
- // NOT best-effort, unlike Kimodo's equivalent: there the native npz is
699
- // a bonus beside a take the caller already holds in memory, here it is
700
- // the wrapper's ONLY deliverable to the bridge, and a silently missing
701
- // file would be reported as a successful run.
702
- await copyFile(localNpy, nativeOut);
703
- await copyFile(localMeta, `${String(nativeOut).replace(/\.npy$/i, "")}.meta.json`);
704
- }
705
- // The driver measures its own exactness (it is the only side that has both
706
- // the source and the result in one place) and the caller is entitled to
707
- // the numbers rather than a boolean: contract GP1 gates on the
708
- // reprojection error and GP2 on the preserved frames.
709
- return {
710
- positions: data,
711
- frames: shape[0],
712
- joints: shape[1],
713
- fps: GEN_FPS,
714
- meta,
715
- line: request,
716
- nativeNpy: nativeOut || null,
717
- };
718
- } finally {
719
- await rm(localDir, { recursive: true, force: true });
720
- await run(["ssh", ...SSH_OPTS, host, `rm -rf ${remoteDir}`]);
721
- }
722
- }
723
-
724
- /**
725
- * CLI entry point, so tools/projflow/runner.mjs has something spawnable.
726
- *
727
- * WHY THE WRAPPER LIVES IN THIS FILE. Kimodo puts its box wrappers in separate
728
- * scripts (run-sequence-on-box.mjs, run-edit-on-box.mjs) because two run modes
729
- * share one argv contract with the ARDY wrappers. ProjFlow v1 has exactly one
730
- * run mode and no legacy argv to match, so a second file would only be a place
731
- * for the two to drift.
732
- *
733
- * WHAT IT WRITES, AND WHAT IT DOES NOT. The output is the RAW hml22 (T,22,3)
734
- * .npy plus its .meta.json — NOT a cclay npz. Turning 22-joint positions into a
735
- * cskel27 take requires lifting rotations (contract C8, agent M2's
736
- * hml22-to-cskel27.mjs) and splicing the result back into the source take
737
- * (agent M5), and neither exists yet. Wave-2 integration composes those on top
738
- * of this file; until then the done line names the raw motion, which is exactly
739
- * what a later line edit sources from anyway.
740
- */
741
- async function main(argv) {
742
- const args = { steps: undefined, preview: false, seed: undefined };
743
- for (let index = 0; index < argv.length; index += 1) {
744
- const flag = argv[index];
745
- const next = () => {
746
- const value = argv[index + 1];
747
- if (value === undefined) throw new Error(`${flag} needs a value`);
748
- index += 1;
749
- return value;
750
- };
751
- if (flag === "--source") args.source = next();
752
- else if (flag === "--line") args.line = next();
753
- else if (flag === "--output") args.output = next();
754
- else if (flag === "--steps") args.steps = Number(next());
755
- else if (flag === "--preview") args.preview = true;
756
- else if (flag === "--ridge") args.ridge = Number(next());
757
- else if (flag === "--preserve-stride") args.preserveStride = Number(next());
758
- else if (flag === "--preserve-margin") args.preserveMargin = Number(next());
759
- else if (flag === "--seam-pin") args.seamPin = Number(next());
760
- else if (flag === "--seed") args.seed = Number(next());
761
- else throw new Error(`unknown flag ${flag}`);
762
- }
763
- for (const required of ["source", "line", "output"]) {
764
- if (!args[required]) throw new Error(`--${required} is required`);
765
- }
766
- // THE CLI IS A ONE-SHOT PROCESS, so a resident it would have to start, use
767
- // once and kill is pure overhead — the point of contract C11 is amortising a
768
- // model load across a SESSION, and this process is not one. The bridge does
769
- // not come through here (it imports line-edit-job.mjs in-process, where the
770
- // child survives between edits). An operator who wants to exercise the warm
771
- // route from the shell sets the variable explicitly.
772
- if (process.env.CCLAY_PROJFLOW_RESIDENT === undefined) process.env.CCLAY_PROJFLOW_RESIDENT = "0";
773
- const line = JSON.parse(await readFile(resolvePath(args.line), "utf8"));
774
- const result = await lineEditOnBox({
775
- sourceMotionNpy: resolvePath(args.source),
776
- line,
777
- nativeOut: resolvePath(args.output),
778
- ...(args.steps === undefined ? {} : { steps: args.steps }),
779
- ...(args.ridge === undefined ? {} : { ridge: args.ridge }),
780
- ...(args.preserveStride === undefined ? {} : { preserveStride: args.preserveStride }),
781
- ...(args.preserveMargin === undefined ? {} : { preserveMargin: args.preserveMargin }),
782
- ...(args.seamPin === undefined ? {} : { seamPin: args.seamPin }),
783
- ...(args.seed === undefined ? {} : { seed: args.seed }),
784
- preview: args.preview,
785
- onLine: (text) => console.error(text),
786
- });
787
- const { m, steps, sampling_seconds: samplingSeconds, checks } = result.meta;
788
- // The exactness numbers on the status stream, not buried in the sidecar:
789
- // gates GP1/GP2 read exactly these and an operator watching a run should see
790
- // a regression without opening a file.
791
- console.log(
792
- `run-projflow-line-edit: rows=${m} steps=${steps} sample=${samplingSeconds}s ` +
793
- `preserved=${checks.preservedMaxAbsDiffM} reproj=${checks.lineMaxReprojErr} ` +
794
- `resident=${result.meta.resident === true}`
795
- );
796
- const { size } = await stat(resolvePath(args.output));
797
- console.log(`run-projflow-line-edit: done - ${resolvePath(args.output)} (${size} bytes)`);
798
- }
799
-
800
- if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
801
- main(process.argv.slice(2)).catch((error) => {
802
- console.error(`run-projflow-line-edit: ${error.message}`);
803
- process.exit(1);
804
- });
805
- }