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.
- package/CHANGELOG.md +65 -0
- package/README.md +12 -30
- package/bin/agent/agent-routes.mjs +40 -113
- package/bin/agent/agent-runner.mjs +9 -10
- package/bin/agent/cliproxy-provider.mjs +135 -0
- package/bin/agent/credential-store.mjs +30 -7
- package/bin/agent/pi-tools.mjs +4 -1
- package/bin/agent/provider-keys.mjs +32 -0
- package/bin/agent/provider-login.mjs +110 -0
- package/bin/agent/provider-routes.mjs +99 -0
- package/bin/agent/providers.mjs +19 -97
- package/bin/agent/script-tool.mjs +17 -4
- package/bin/agent/studio-motion-reference.mjs +23 -0
- package/bin/agent/studio-prompt.mjs +1 -1
- package/bin/agent/studio-rebase-text.mjs +32 -0
- package/bin/agent/studio-rebase.mjs +26 -39
- package/bin/agent/studio-tools.mjs +9 -28
- package/bin/agent/tool-arguments.mjs +49 -0
- package/bin/agent/turn-frames.mjs +66 -0
- package/bin/codex-auth.mjs +9 -2
- package/bin/cozyclay.mjs +10 -116
- package/bin/live/cli.mjs +22 -6
- package/bin/mcp-runtime.mjs +2 -12
- package/bin/package-signature.mjs +0 -4
- package/dist/app/index.html +7 -6
- package/dist/assets/EXRLoader-CvVIvGVE.js +1 -0
- package/dist/assets/analytics-C9sUnndd.js +1 -0
- package/dist/assets/app-CCh--1Hc.js +5087 -0
- package/dist/assets/app-f-fkV5uP.css +1 -0
- package/dist/assets/compile-worker-CkxiD6lH.js +5 -0
- package/dist/assets/fflate.module-KbGj5-H6.js +5 -0
- package/dist/assets/{first-shot-handoff-BlT33CGf.js → first-shot-handoff-CC4xe4HM.js} +1 -1
- package/dist/assets/{landing-BSesrCpL.js → landing-DjHD8u7k.js} +1 -1
- package/dist/assets/scene-asset-sync-Hx_FzdOH.js +20 -0
- package/dist/assets/scene-asset-sync-_S8NQl5B.css +1 -0
- package/dist/assets/workflow-C4yKH1p4.js +179 -0
- package/dist/cozyclay-package.json +1 -1
- package/dist/index.html +3 -3
- package/dist/llms.txt +14 -0
- package/dist/sw.js +3 -6
- package/dist/workflow/index.html +5 -4
- package/mcp/LIVE-PROTOCOL.md +7 -47
- package/mcp/README.md +16 -26
- package/mcp/live-hub.mjs +6 -10
- package/mcp/package.json +1 -3
- package/mcp/tool-handlers.mjs +4 -168
- package/package.json +3 -22
- package/src/App.jsx +174 -2639
- package/src/agent-progress.js +79 -0
- package/src/agent-turn-mark.js +30 -0
- package/src/analytics.js +1 -19
- package/src/app-context.js +5 -0
- package/src/app-stage.jsx +2 -34
- package/src/asset-pane.jsx +6 -5
- package/src/camera-block.js +21 -0
- package/src/camera-direction.js +120 -0
- package/src/character-kind.js +3 -9
- package/src/command-bus.js +8 -6
- package/src/commands/cast.js +5 -34
- package/src/commands/elements/character.js +1 -1
- package/src/commands/elements/motion.js +1 -6
- package/src/commands/elements/object.js +6 -2
- package/src/commands/elements/shot.js +4 -1
- package/src/commands/elements.js +38 -14
- package/src/commands/index.js +3 -1
- package/src/commands/kits.js +37 -0
- package/src/commands/motion-timing.js +57 -0
- package/src/commands/motion.js +9 -152
- package/src/commands/motion3-compile.js +65 -0
- package/src/commands/motion3.js +207 -0
- package/src/commands/objects.js +6 -4
- package/src/commands/shot.js +49 -11
- package/src/commands/stage.js +14 -1
- package/src/commands/view.js +4 -1
- package/src/document-mirrors.js +42 -0
- package/src/document-store.js +4 -0
- package/src/domains/cast.js +19 -194
- package/src/domains/motion/motion-range-pin-rules.js +1 -1
- package/src/domains/motion/take-source.js +51 -0
- package/src/domains/motion.js +60 -1838
- package/src/domains/scenes.js +12 -55
- package/src/domains/shots.js +2 -2
- package/src/domains/stage.js +4 -2
- package/src/draft-ghost.js +46 -0
- package/src/draft-ghost.jsx +129 -0
- package/src/dualview.jsx +4 -3
- package/src/effects/EffectsLayer.jsx +112 -0
- package/src/effects/effect-meshes.js +232 -0
- package/src/effects/effect-record.js +103 -0
- package/src/effects/emitter.js +59 -0
- package/src/effects/simulate.js +261 -0
- package/src/effects/state.js +59 -0
- package/src/execution-telemetry.js +2 -3
- package/src/first-success-guide.jsx +5 -5
- package/src/ghost-set.js +257 -0
- package/src/held-object-edit.jsx +113 -0
- package/src/held-object-pose.js +175 -0
- package/src/hierarchy-model.js +1 -1
- package/src/hierarchy-panel.jsx +1 -1
- package/src/imported-mesh.jsx +103 -0
- package/src/kits/held.js +84 -0
- package/src/kits/index.js +70 -0
- package/src/kits/smoking.js +57 -0
- package/src/light-blockers.js +93 -0
- package/src/motion/beat-board-model.js +84 -0
- package/src/motion/beat-board.css +124 -0
- package/src/motion/beat-board.jsx +59 -0
- package/src/motion/center-of-mass.js +66 -0
- package/src/motion/fix-collisions.js +850 -0
- package/src/motion/physics-review.js +118 -0
- package/src/{ardy → motion}/physics-support.js +1 -76
- package/src/motion/rig-snapshot.js +22 -0
- package/src/motion/take-segments.jsx +140 -0
- package/src/{ardy → motion}/timeline-coordinates.js +0 -21
- package/src/{ardy → motion}/timeline.css +1 -2
- package/src/{ardy → motion}/timeline.jsx +48 -419
- package/src/motion/waypoints.js +76 -0
- package/src/motion3/beats.js +83 -0
- package/src/motion3/body.js +158 -0
- package/src/motion3/bones.js +47 -0
- package/src/motion3/carried.js +47 -0
- package/src/motion3/compile-job.js +75 -0
- package/src/motion3/compile-worker.js +10 -0
- package/src/motion3/compiler/anchors.js +105 -0
- package/src/motion3/compiler/arm-orient.js +128 -0
- package/src/motion3/compiler/channels.js +125 -0
- package/src/motion3/compiler/compile.js +205 -0
- package/src/motion3/compiler/gait-rhythm.js +97 -0
- package/src/motion3/compiler/grip.js +171 -0
- package/src/motion3/compiler/head-steady.js +48 -0
- package/src/motion3/compiler/hold.js +134 -0
- package/src/motion3/compiler/idle.js +113 -0
- package/src/motion3/compiler/limbs.js +170 -0
- package/src/motion3/compiler/math.js +72 -0
- package/src/motion3/compiler/pose.js +162 -0
- package/src/motion3/compiler/quat.js +49 -0
- package/src/motion3/compiler/skeleton.js +74 -0
- package/src/motion3/compiler/validate-common.js +32 -0
- package/src/motion3/compiler/validate-goals.js +118 -0
- package/src/motion3/compiler/validate-objects.js +143 -0
- package/src/motion3/compiler/validate-parts.js +116 -0
- package/src/motion3/compiler/validate.js +122 -0
- package/src/motion3/compiler/vocab.js +73 -0
- package/src/motion3/compiler/walk-pelvis.js +275 -0
- package/src/motion3/compiler/walk.js +207 -0
- package/src/motion3/compiler/wrist-clamp.js +32 -0
- package/src/motion3/delta.js +47 -0
- package/src/motion3/describe.js +174 -0
- package/src/motion3/docs.js +27 -0
- package/src/motion3/edit-carry.js +21 -0
- package/src/motion3/fix/ease.js +16 -0
- package/src/motion3/fix/ground.js +37 -0
- package/src/motion3/fix/index.js +52 -0
- package/src/motion3/fix/push.js +71 -0
- package/src/motion3/fix/reach.js +46 -0
- package/src/motion3/fix/retime.js +79 -0
- package/src/motion3/fix/self.js +211 -0
- package/src/motion3/fix/slide.js +70 -0
- package/src/motion3/fix/turn.js +51 -0
- package/src/motion3/follow.js +39 -0
- package/src/motion3/free-motion.js +173 -0
- package/src/motion3/grip-choice.js +130 -0
- package/src/motion3/hold-plan.js +157 -0
- package/src/motion3/json-patch.js +77 -0
- package/src/motion3/key-aim.js +105 -0
- package/src/motion3/key-edit.js +82 -0
- package/src/motion3/lenient.js +127 -0
- package/src/motion3/object-keys.js +236 -0
- package/src/motion3/object-pose.js +182 -0
- package/src/motion3/object-tracks.js +12 -0
- package/src/motion3/pipeline.js +102 -0
- package/src/motion3/playback.js +51 -0
- package/src/motion3/probes.js +91 -0
- package/src/motion3/qa/body-checks.js +34 -0
- package/src/motion3/qa/check-names.js +14 -0
- package/src/motion3/qa/checks.js +121 -0
- package/src/motion3/qa/contact-checks.js +51 -0
- package/src/motion3/qa/explain.js +80 -0
- package/src/motion3/qa/fmt.js +5 -0
- package/src/motion3/qa/foot-checks.js +72 -0
- package/src/motion3/qa/geometry.js +77 -0
- package/src/motion3/qa/goals.js +191 -0
- package/src/motion3/qa/ground.js +60 -0
- package/src/motion3/qa/hints-contact.js +179 -0
- package/src/motion3/qa/hints-goal.js +160 -0
- package/src/motion3/qa/hints-knee.js +24 -0
- package/src/motion3/qa/hints-motion.js +209 -0
- package/src/motion3/qa/hints-object.js +91 -0
- package/src/motion3/qa/index.js +42 -0
- package/src/motion3/qa/key-checks.js +82 -0
- package/src/motion3/qa/near.js +96 -0
- package/src/motion3/qa/object-checks.js +135 -0
- package/src/motion3/qa/patch.js +161 -0
- package/src/motion3/qa/severity.js +38 -0
- package/src/motion3/qa/spec-checks.js +65 -0
- package/src/motion3/qa/style.js +218 -0
- package/src/motion3/qa/wrist-checks.js +69 -0
- package/src/motion3/qa/wrist-obs.js +60 -0
- package/src/motion3/refs.js +120 -0
- package/src/motion3/repair-tune.js +121 -0
- package/src/motion3/repair.js +83 -0
- package/src/motion3/report.js +67 -0
- package/src/motion3/rigs/index.js +14 -0
- package/src/motion3/rigs/x-bot-tpose-body.json +1 -0
- package/src/motion3/rigs/x-bot-tpose.json +1 -0
- package/src/motion3/rigs/y-bot-tpose-body.json +1 -0
- package/src/motion3/rigs/y-bot-tpose.json +1 -0
- package/src/motion3/scene.js +60 -0
- package/src/motion3/schema/brief.js +89 -0
- package/src/motion3/schema/fields.js +44 -0
- package/src/motion3/schema/spec-examples.js +46 -0
- package/src/motion3/schema/topics.js +111 -0
- package/src/motion3/schema/types.js +147 -0
- package/src/motion3/search.js +117 -0
- package/src/motion3/second-hand.js +112 -0
- package/src/motion3/skills/basic.js +82 -0
- package/src/motion3/skills/book.js +76 -0
- package/src/motion3/skills/car.js +145 -0
- package/src/motion3/skills/frames-auto.js +29 -0
- package/src/motion3/skills/index.js +83 -0
- package/src/motion3/skills/info.js +65 -0
- package/src/motion3/skills/smoke-kit.js +109 -0
- package/src/motion3/skills/smoke.js +160 -0
- package/src/motion3/spec-store.js +59 -0
- package/src/motion3/take-edit.js +155 -0
- package/src/motion3/take.js +63 -0
- package/src/motion3/timeline.js +127 -0
- package/src/motion3/views.js +121 -0
- package/src/motion3/walk-plan.js +119 -0
- package/src/object-catalog.jsx +7 -1
- package/src/object-colors.js +98 -0
- package/src/object-gizmo-parts.js +115 -0
- package/src/object-gizmo.jsx +37 -122
- package/src/object-glow.js +139 -0
- package/src/object-hidden.js +33 -0
- package/src/object-look.js +196 -0
- package/src/object-size.js +10 -0
- package/src/object-travel.js +71 -6
- package/src/panels/CameraPanel.jsx +7 -7
- package/src/panels/HdrEnvironmentControls.jsx +59 -0
- package/src/panels/LightPanel.jsx +25 -4
- package/src/panels/ObjectLookPanel.jsx +88 -0
- package/src/panels/PoseImagePanel.jsx +1 -1
- package/src/panels/PosePanel.jsx +2 -17
- package/src/panels/RigControlPanel.jsx +5 -53
- package/src/panels/SubjectBox.jsx +4 -1
- package/src/panels/details.css +14 -0
- package/src/posestudio.jsx +4 -27
- package/src/project-browser.jsx +3 -4
- package/src/project-resources.js +18 -31
- package/src/project.js +13 -104
- package/src/props.jsx +25 -280
- package/src/rail-dot-drag.js +23 -0
- package/src/render-passes.js +10 -0
- package/src/resource-status.jsx +1 -0
- package/src/result-modal.jsx +4 -19
- package/src/room.jsx +20 -12
- package/src/scene-assets.js +8 -6
- package/src/scene-light-place.js +72 -0
- package/src/scene-lights.jsx +148 -0
- package/src/scene-objects.js +31 -139
- package/src/scenes.js +15 -105
- package/src/semantic-edit.js +2 -5
- package/src/set-pieces.jsx +183 -0
- package/src/settings-menu.jsx +3 -11
- package/src/shell/BottomDock.jsx +11 -53
- package/src/shell/CompositionGuideButton.jsx +20 -0
- package/src/shell/DetailsSlot.jsx +16 -200
- package/src/shell/GhostsMenuGroup.jsx +74 -0
- package/src/shell/LibrarySlot.jsx +1 -3
- package/src/shell/MenuBar.jsx +3 -3
- package/src/shell/PreferencesDialog.jsx +15 -264
- package/src/shell/PreferencesSlot.jsx +2 -8
- package/src/shell/ShotPreviewHeader.jsx +17 -0
- package/src/shell/TopBar.jsx +5 -83
- package/src/shell/ViewportToolbar.jsx +23 -93
- package/src/shell/agent-glass.css +19 -6
- package/src/shell/details-selection.js +42 -0
- package/src/shell/ghosts-menu.css +22 -0
- package/src/shell/glass-regions.css +29 -3
- package/src/shell/preferences.css +6 -1
- package/src/shell/viewport.css +12 -0
- package/src/shot-authoring.js +4 -7
- package/src/soft-shadows.js +37 -0
- package/src/stage-attach-frames.js +12 -12
- package/src/stage-canvas-rigs.jsx +3 -3
- package/src/stage-capture.jsx +42 -8
- package/src/stage-characters.jsx +22 -216
- package/src/stage-editor-gizmos.jsx +6 -6
- package/src/stage-environment.js +93 -0
- package/src/stage-hdr-builtins.js +107 -0
- package/src/stage-hdr-image.js +116 -0
- package/src/stage-hdr-library.js +70 -0
- package/src/stage-hdr-map.js +94 -0
- package/src/stage-hdr.js +130 -0
- package/src/stage-lighting.js +149 -0
- package/src/stage-occlusion.js +210 -0
- package/src/stage-sky.jsx +159 -0
- package/src/stage-startup-defaults.js +5 -9
- package/src/studio-actions.js +12 -14
- package/src/studio-agent-commands.js +46 -16
- package/src/studio-agent-motion.js +9 -9
- package/src/studio-agent-protocol.js +46 -73
- package/src/studio-app-binding.js +29 -45
- package/src/studio-asset-list.js +17 -0
- package/src/studio-contact-sheet.js +37 -5
- package/src/studio-elements.js +27 -7
- package/src/studio-entity-bounds.js +18 -0
- package/src/studio-error-codes.js +8 -0
- package/src/studio-export-framing.js +152 -0
- package/src/studio-lighting-check.js +103 -0
- package/src/studio-motion-visual.js +150 -0
- package/src/studio-schema-errors.js +80 -0
- package/src/studio-support-gap.js +74 -0
- package/src/styles.css +1 -19
- package/src/timeline-defaults.js +5 -0
- package/src/timeline-extent.js +8 -20
- package/src/tutorial-walk.js +17 -0
- package/src/ui-scrub.js +1 -1
- package/src/use-shot-camera-tracks.js +12 -0
- package/src/view-ghosts.js +97 -0
- package/src/view-ghosts.jsx +49 -0
- package/src/workflow/AgentPanel.jsx +47 -166
- package/src/workflow/AgentProgressCard.jsx +107 -0
- package/src/workflow/AgentReplyText.jsx +37 -0
- package/src/workflow/AgentTurnSteps.jsx +84 -0
- package/src/workflow/LoginPanel.jsx +140 -0
- package/src/workflow/WorkflowBuilder.jsx +7 -23
- package/src/workflow/agent-client.js +54 -51
- package/src/workflow/agent-panel.css +71 -1
- package/src/workflow/agent-progress-card.css +167 -0
- package/src/workflow/agent-progress-view.js +47 -0
- package/src/workflow/agent-turn-steps.css +113 -0
- package/src/workflow/agent-turn.js +191 -0
- package/src/workflow/login-client.js +34 -0
- package/src/workflow/motion-input.js +18 -31
- package/src/workflow/node-schema.js +1 -1
- package/src/workflow/reply-markdown.js +69 -0
- package/src/workflow/scene-asset-sync.js +8 -8
- package/tools/agent-session-stats.mjs +61 -0
- package/tools/dev-full.mjs +5 -56
- package/tools/eslint-baseline.json +12 -13
- package/tools/headless-studio.mjs +85 -0
- package/tools/knip-baseline.json +0 -179
- package/tools/loc-baseline.json +33 -83
- package/tools/motion3/extract-body.mjs +103 -0
- package/tools/motion3/extract-rig.mjs +85 -0
- package/tools/process-supervisor.mjs +0 -88
- package/tools/run-tests.mjs +9 -124
- package/bin/agent/motion-runtime.mjs +0 -76
- package/dist/ardy/cskel27-rest.json +0 -557
- package/dist/assets/analytics-B41FmPYI.js +0 -1
- package/dist/assets/app-Do-Og5Pu.css +0 -1
- package/dist/assets/app-DwP-tl2x.js +0 -4920
- package/dist/assets/config-Bx7pYsD9.js +0 -1
- package/dist/assets/config-DeoFTYzb.css +0 -1
- package/dist/assets/demo-Dt0qZfS7.js +0 -1
- package/dist/assets/scene-asset-sync-BCWDJB1d.js +0 -19
- package/dist/assets/scene-asset-sync-DjJ_cJ2R.css +0 -1
- package/dist/assets/ticket-jqMhqL88.js +0 -1
- package/dist/assets/workflow-Br-K7Srs.js +0 -179
- package/dist/demo/walk-then-stop.npz +0 -0
- package/mcp/ardy-prompts.mjs +0 -218
- package/src/ardy/auto-fix-panel.css +0 -108
- package/src/ardy/auto-physics.js +0 -1194
- package/src/ardy/client.js +0 -153
- package/src/ardy/convert.js +0 -313
- package/src/ardy/cskel27-neutral.js +0 -39
- package/src/ardy/cskel27.js +0 -77
- package/src/ardy/export.js +0 -165
- package/src/ardy/fix-collisions.js +0 -2257
- package/src/ardy/foot-lock.js +0 -230
- package/src/ardy/motion-calibration.js +0 -118
- package/src/ardy/motion-edit.js +0 -213
- package/src/ardy/motion-url.js +0 -55
- package/src/ardy/mp4-duration.js +0 -102
- package/src/ardy/npz.js +0 -595
- package/src/ardy/physics-panel.css +0 -21
- package/src/ardy/physics-panel.jsx +0 -89
- package/src/ardy/physics-refine.js +0 -111
- package/src/ardy/physics-review.js +0 -567
- package/src/ardy/physics-temporal.js +0 -54
- package/src/ardy/platform-fit-panel.css +0 -64
- package/src/ardy/platform-fit-panel.jsx +0 -41
- package/src/ardy/platform-fit.js +0 -326
- package/src/ardy/playback.js +0 -711
- package/src/ardy/pose-pin.js +0 -97
- package/src/ardy/prompt-clips.js +0 -30
- package/src/ardy/retime.js +0 -211
- package/src/ardy/root-drop.js +0 -369
- package/src/ardy/timeline-resize.js +0 -13
- package/src/ardy/to-cskel27.js +0 -225
- package/src/ardy/trim.js +0 -33
- package/src/ardy/waypoints.js +0 -369
- package/src/ardy-bridge-contract.js +0 -281
- package/src/domains/motion/motion-video-capture.js +0 -664
- package/src/i2v-motion-client.js +0 -77
- package/src/i2v-motion-studio.jsx +0 -180
- package/src/line-edit.js +0 -1348
- package/src/motion/generation.js +0 -105
- package/src/motion-readiness-ui.jsx +0 -67
- package/src/motion-readiness.js +0 -40
- package/src/motion-resources.js +0 -177
- package/src/motion-store.js +0 -65
- package/src/motion-trail.js +0 -601
- package/src/multimodel-ingest.js +0 -394
- package/src/panels/PromptBlocksPanel.jsx +0 -444
- package/src/panels/VideoCapturePanel.jsx +0 -181
- package/src/pose-extract/browser.js +0 -42
- package/src/pose-extract/detector.js +0 -100
- package/src/pose-extract/fit.js +0 -227
- package/src/pose-extract/image-frame.js +0 -51
- package/src/pose-extract/index.js +0 -58
- package/src/pose-extract/keyframes.js +0 -74
- package/src/pose-extract/landmarks.js +0 -162
- package/src/pose-extract/mirror.js +0 -153
- package/src/pose-extract/take.js +0 -131
- package/src/pose-extract/video-frames.js +0 -91
- package/src/take-recipe.js +0 -165
- package/src/trail-key-conflicts.js +0 -39
- package/src/trail-pick.js +0 -59
- package/src/use-i2v-capture-modal.js +0 -15
- package/tools/ardy/BRIDGE.md +0 -37
- package/tools/ardy/GVHMR-NPZ.md +0 -68
- package/tools/ardy/README.md +0 -124
- package/tools/ardy/__pycache__/cclay_gvhmr_worker.cpython-313.pyc +0 -0
- package/tools/ardy/artifacts.mjs +0 -34
- package/tools/ardy/bridge.mjs +0 -1732
- package/tools/ardy/bvh-cskel27.mjs +0 -1265
- package/tools/ardy/cclay_constrained_generate.py +0 -561
- package/tools/ardy/cclay_gvhmr_worker.py +0 -264
- package/tools/ardy/cclay_motion_edit.py +0 -510
- package/tools/ardy/cclay_sequence_generate.py +0 -705
- package/tools/ardy/cclay_worker.py +0 -304
- package/tools/ardy/dump-npz.py +0 -205
- package/tools/ardy/extract-rest.mjs +0 -299
- package/tools/ardy/extract.mjs +0 -507
- package/tools/ardy/footage.mjs +0 -467
- package/tools/ardy/gvhmr-floor.mjs +0 -102
- package/tools/ardy/gvhmr-to-cskel27.mjs +0 -21
- package/tools/ardy/gvhmr_fastpath.py +0 -168
- package/tools/ardy/gvhmr_trajectory.py +0 -240
- package/tools/ardy/merge-text-encoder.py +0 -160
- package/tools/ardy/mocap-metrics.mjs +0 -200
- package/tools/ardy/mocap-quality-gate.mjs +0 -110
- package/tools/ardy/motion-stabilize.mjs +0 -348
- package/tools/ardy/npz.mjs +0 -414
- package/tools/ardy/pose-to-npz.mjs +0 -106
- package/tools/ardy/prompt-limits.mjs +0 -5
- package/tools/ardy/run-edit-on-box.sh +0 -73
- package/tools/ardy/run-local.mjs +0 -233
- package/tools/ardy/run-on-box.sh +0 -593
- package/tools/ardy/run-sequence-on-box.sh +0 -177
- package/tools/ardy/runners/gvhmr-worker.mjs +0 -206
- package/tools/ardy/runners/index.mjs +0 -18
- package/tools/ardy/runners/local.mjs +0 -455
- package/tools/ardy/runners/proc.mjs +0 -125
- package/tools/ardy/runners/remote.mjs +0 -252
- package/tools/ardy/setup-local.mjs +0 -189
- package/tools/ardy/setup-text-encoder-on-box.sh +0 -93
- package/tools/ardy/setup-text-encoder.py +0 -306
- package/tools/ardy/smpl-cskel27.mjs +0 -178
- package/tools/ardy/visual-qa.mjs +0 -196
- package/tools/ardy/vq-car.mjs +0 -52
- package/tools/bench/EXP3.md +0 -61
- package/tools/bench/cclay_bench_extract_incam.py +0 -125
- package/tools/bench/cclay_bench_extract_obs.py +0 -204
- package/tools/bench/cclay_bench_runner.py +0 -42
- package/tools/bench/cube-contact.mjs +0 -162
- package/tools/bench/exp3.mjs +0 -152
- package/tools/bench/extract-bench-lib.mjs +0 -216
- package/tools/bench/extract-bench.mjs +0 -302
- package/tools/bench/fal-generate.mjs +0 -88
- package/tools/bench/fit/README.md +0 -189
- package/tools/bench/fit/camera.mjs +0 -59
- package/tools/bench/fit/contact.mjs +0 -419
- package/tools/bench/fit/footlock.mjs +0 -234
- package/tools/bench/fit/motion.mjs +0 -85
- package/tools/bench/fit/pin.mjs +0 -27
- package/tools/bench/fit/remote.mjs +0 -105
- package/tools/bench/fit-bench.mjs +0 -126
- package/tools/bench/fit-sanity.mjs +0 -45
- package/tools/bench/metrics.mjs +0 -313
- package/tools/bench/obs/depth.mjs +0 -258
- package/tools/bench/obs/extrinsics.mjs +0 -175
- package/tools/bench/obs/fit_mannequin_betas.py +0 -340
- package/tools/bench/obs/ground.mjs +0 -294
- package/tools/bench/obs/heading.mjs +0 -151
- package/tools/bench/obs/ladder.mjs +0 -555
- package/tools/bench/obs/mannequin-betas.json +0 -124
- package/tools/bench/obs/remote.mjs +0 -83
- package/tools/bench/obs/rest_joints.py +0 -52
- package/tools/bench/obs/ybot-targets.mjs +0 -49
- package/tools/bench/obs-bench.mjs +0 -520
- package/tools/bench/score.mjs +0 -343
- package/tools/bench/summarize.mjs +0 -86
- package/tools/demo-worker/README.md +0 -137
- package/tools/demo-worker/api-client.mjs +0 -233
- package/tools/demo-worker/generate.mjs +0 -301
- package/tools/demo-worker/index.mjs +0 -466
- package/tools/demo-worker/sign-core.mjs +0 -112
- package/tools/demo-worker/sign.mjs +0 -161
- package/tools/gt-render/browser.mjs +0 -159
- package/tools/gt-render/camera-math.mjs +0 -276
- package/tools/gt-render/page.mjs +0 -267
- package/tools/gt-render/render.mjs +0 -514
- package/tools/gt-render/scene-box.mjs +0 -31
- package/tools/gt-render/take-transform.mjs +0 -87
- package/tools/kimodo/compare-seam.mjs +0 -108
- package/tools/kimodo/constraints.mjs +0 -244
- package/tools/kimodo/edit.mjs +0 -259
- package/tools/kimodo/effector-constraints.mjs +0 -200
- package/tools/kimodo/generate.mjs +0 -646
- package/tools/kimodo/local-output.mjs +0 -265
- package/tools/kimodo/measure-pose.mjs +0 -65
- package/tools/kimodo/measure-preserve.mjs +0 -899
- package/tools/kimodo/measure-waypoints.mjs +0 -55
- package/tools/kimodo/mlx-generate.py +0 -111
- package/tools/kimodo/pose-constraints.mjs +0 -248
- package/tools/kimodo/preserve-mask.mjs +0 -543
- package/tools/kimodo/read-npz.mjs +0 -189
- package/tools/kimodo/run-edit-on-box.mjs +0 -155
- package/tools/kimodo/run-sequence-on-box.mjs +0 -178
- package/tools/kimodo/runner.mjs +0 -284
- package/tools/kimodo/seam-batch.mjs +0 -77
- package/tools/kimodo/setup-local.mjs +0 -30
- package/tools/kimodo/setup-on-box.sh +0 -339
- package/tools/kimodo/soma77-to-cskel27.mjs +0 -305
- package/tools/morphgs/assets/gen_truth.py +0 -39
- package/tools/morphgs/assets/mesh_ori_rig.txt +0 -27
- package/tools/morphgs/assets/playback-check.mjs +0 -30
- package/tools/morphgs/demo-gate.sh +0 -26
- package/tools/morphgs/fbx2morphgs.mjs +0 -68
- package/tools/morphgs/morphgs-to-cskel27.mjs +0 -105
- package/tools/morphgs/patches/preprocess_src-none-mode.patch +0 -76
- package/tools/morphgs/setup-on-cluster.sh +0 -126
- package/tools/projflow/driver.py +0 -1069
- package/tools/projflow/generate.mjs +0 -805
- package/tools/projflow/hml22-to-cskel27.mjs +0 -735
- package/tools/projflow/line-edit-job.mjs +0 -547
- package/tools/projflow/replay.mjs +0 -452
- package/tools/projflow/runner.mjs +0 -269
- package/tools/projflow/service.mjs +0 -742
- package/tools/track/DESIGN.md +0 -513
- package/tools/track/backfill-provenance.mjs +0 -116
- package/tools/track/budget.mjs +0 -8
- package/tools/track/check-rig.mjs +0 -209
- package/tools/track/diagnostics.schema.json +0 -60
- package/tools/track/export-rig.mjs +0 -256
- package/tools/track/fallback.mjs +0 -7
- package/tools/track/fk-parity-fixture.mjs +0 -161
- package/tools/track/gate.mjs +0 -349
- package/tools/track/masks.mjs +0 -318
- package/tools/track/metrics.mjs +0 -201
- package/tools/track/publish-obs.mjs +0 -108
- package/tools/track/py/check_env.py +0 -61
- package/tools/track/py/eval_lr.py +0 -229
- package/tools/track/py/lr_viterbi.py +0 -294
- package/tools/track/py/masks.py +0 -405
- package/tools/track/py/objective.py +0 -382
- package/tools/track/py/rig.py +0 -508
- package/tools/track/py/scene.py +0 -326
- package/tools/track/py/test_joint_indices.py +0 -123
- package/tools/track/py/test_lr_viterbi.py +0 -195
- package/tools/track/py/test_masks.py +0 -229
- package/tools/track/py/test_rig.py +0 -288
- package/tools/track/py/test_scene.py +0 -354
- package/tools/track/py/test_track.py +0 -328
- package/tools/track/py/track.py +0 -411
- package/tools/track/remote.mjs +0 -140
- package/tools/track/rig-dump.mjs +0 -227
- package/tools/track/run-box-tests.mjs +0 -14
- package/tools/track/run-box.mjs +0 -168
- package/tools/track/setup-box.sh +0 -59
- package/tools/track/study-2d.mjs +0 -728
- /package/src/{ardy → motion}/collision-blockers.js +0 -0
- /package/src/{ardy → motion}/ground.js +0 -0
- /package/src/{ardy → motion}/ik-drag.js +0 -0
- /package/src/{ardy → motion}/ik-key-json.js +0 -0
- /package/src/{ardy → motion}/ik.js +0 -0
- /package/src/{ardy → motion}/physics-surface.js +0 -0
- /package/src/{ardy → motion}/playback-clock.js +0 -0
- /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
|
-
}
|