cozyclay 2.1.1 → 3.0.0

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