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.
- package/CHANGELOG.md +60 -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 +3 -3
- 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-msLI71g1.js → first-shot-handoff-CC4xe4HM.js} +1 -1
- package/dist/assets/{landing-ChUvB_zL.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/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 +150 -2624
- 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 +42 -413
- 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/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 +9 -51
- 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/TopBar.jsx +5 -84
- package/src/shell/ViewportToolbar.jsx +10 -70
- 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/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 +19 -16
- package/src/studio-agent-motion.js +9 -9
- package/src/studio-agent-protocol.js +41 -71
- package/src/studio-app-binding.js +25 -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/knip-baseline.json +0 -179
- package/tools/loc-baseline.json +31 -81
- 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 +8 -124
- package/bin/agent/motion-runtime.mjs +0 -76
- package/dist/ardy/cskel27-rest.json +0 -557
- package/dist/assets/analytics-C9pMpW7s.js +0 -1
- package/dist/assets/app-B_2XB9rw.css +0 -1
- package/dist/assets/app-DAuvKPP8.js +0 -4920
- package/dist/assets/config-Bx7pYsD9.js +0 -1
- package/dist/assets/config-DeoFTYzb.css +0 -1
- package/dist/assets/demo-CvKuBgy5.js +0 -1
- package/dist/assets/scene-asset-sync-DKkpjKBV.js +0 -19
- package/dist/assets/scene-asset-sync-DjJ_cJ2R.css +0 -1
- package/dist/assets/ticket-BPk2Ni95.js +0 -1
- package/dist/assets/workflow-jJRBR94Z.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 -182
- 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,200 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* mocap-metrics.mjs — compare cskel27 motion npz takes on the three defects a
|
|
4
|
-
* bad 2D observation produces, so an estimator change (e.g. #180's palette
|
|
5
|
-
* keypoints against ViTPose) is judged by numbers instead of by eye:
|
|
6
|
-
*
|
|
7
|
-
* foot slide the planted foot's horizontal speed while it is planted.
|
|
8
|
-
* A supporting foot should read ~0 cm/s; skate is the single
|
|
9
|
-
* most visible mocap artefact (SAM's single-image lift
|
|
10
|
-
* measured 250 cm/s on locomotion). "Planted" is decided
|
|
11
|
-
* per side from the take itself: the frames whose ankle is
|
|
12
|
-
* in the lower PLANT_HEIGHT_PERCENTILE of that side's ankle
|
|
13
|
-
* heights AND whose horizontal speed is below the side's
|
|
14
|
-
* median — that is the stance phase, without needing the
|
|
15
|
-
* contact logits (a converted take has dropped them).
|
|
16
|
-
* jitter mean |second difference| of posed_joints. The raw
|
|
17
|
-
* mm/frame^2 value is retained for diagnostics and the
|
|
18
|
-
* fps-normalised mm/s^2 value is used for cross-rate gates.
|
|
19
|
-
* below floor frames whose lowest foot joint sits under Y=0. The
|
|
20
|
-
* converter grounds the clip on its 10th percentile, so a
|
|
21
|
-
* few frames are expected; many mean the legs are wrong.
|
|
22
|
-
*
|
|
23
|
-
* usage: node tools/ardy/mocap-metrics.mjs <a.npz> [<b.npz> ...]
|
|
24
|
-
*/
|
|
25
|
-
import { readNpz } from "../kimodo/read-npz.mjs";
|
|
26
|
-
import { CSKEL27_JOINTS } from "../../src/ardy/cskel27.js";
|
|
27
|
-
import { pathToFileURL } from "node:url";
|
|
28
|
-
|
|
29
|
-
const JOINT = Object.fromEntries(CSKEL27_JOINTS.map((name, index) => [name, index]));
|
|
30
|
-
const JOINTS = CSKEL27_JOINTS.length;
|
|
31
|
-
const FEET = ["RightFoot", "RightToeBase", "LeftFoot", "LeftToeBase"].map((name) => JOINT[name]);
|
|
32
|
-
const ANKLES = { right: JOINT.RightFoot, left: JOINT.LeftFoot };
|
|
33
|
-
// The stance phase of a step: the foot is both low and slow. Deriving it from
|
|
34
|
-
// the take's own distribution keeps the measure comparable across clips with
|
|
35
|
-
// different step heights, and needs no contact channel.
|
|
36
|
-
const PLANT_HEIGHT_PERCENTILE = 0.35;
|
|
37
|
-
const FLOOR_TOLERANCE_M = 0.005; // 5 mm: float32 round-trip, not penetration
|
|
38
|
-
|
|
39
|
-
function member(members, name, path) {
|
|
40
|
-
const value = members[name];
|
|
41
|
-
if (!value?.data) throw new Error(`${path}: missing or unreadable member ${name}`);
|
|
42
|
-
return value;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function percentile(values, fraction) {
|
|
46
|
-
if (!values.length) return NaN;
|
|
47
|
-
const sorted = [...values].sort((a, b) => a - b);
|
|
48
|
-
const at = (sorted.length - 1) * fraction;
|
|
49
|
-
const low = Math.floor(at);
|
|
50
|
-
return sorted[low] * (1 - (at - low)) + (sorted[Math.min(low + 1, sorted.length - 1)] ?? sorted[low]) * (at - low);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const median = (values) => percentile(values, 0.5);
|
|
54
|
-
|
|
55
|
-
function read(path) {
|
|
56
|
-
const members = readNpz(path);
|
|
57
|
-
const joints = member(members, "posed_joints", path);
|
|
58
|
-
if (joints.shape.length !== 3 || joints.shape[1] !== JOINTS || joints.shape[2] !== 3) {
|
|
59
|
-
throw new Error(`${path}: posed_joints must be (F, ${JOINTS}, 3), got (${joints.shape.join(", ")})`);
|
|
60
|
-
}
|
|
61
|
-
const fpsMember = member(members, "fps", path);
|
|
62
|
-
const fps = fpsMember.data[0];
|
|
63
|
-
if (!(fps > 0)) throw new Error(`${path}: invalid fps ${fps}`);
|
|
64
|
-
const frames = joints.shape[0];
|
|
65
|
-
const root = members.root_positions?.data ?? null;
|
|
66
|
-
return { path, frames, fps, joints: joints.data, root };
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** Build metrics from a converted motion already held in memory. */
|
|
70
|
-
export function mocapMetricsFromMotion(motion, { path = "<memory>" } = {}) {
|
|
71
|
-
if (!motion || !Number.isInteger(motion.frames) || motion.frames < 1) {
|
|
72
|
-
throw new Error(`${path}: motion.frames must be a positive integer`);
|
|
73
|
-
}
|
|
74
|
-
const fps = Number(motion.fps);
|
|
75
|
-
if (!(fps > 0)) throw new Error(`${path}: invalid fps ${motion.fps}`);
|
|
76
|
-
const joints = motion.posedJoints ?? motion.joints;
|
|
77
|
-
const expectedJoints = motion.frames * JOINTS * 3;
|
|
78
|
-
if (!joints || joints.length !== expectedJoints) {
|
|
79
|
-
throw new Error(`${path}: posedJoints must contain ${expectedJoints} values`);
|
|
80
|
-
}
|
|
81
|
-
const root = motion.rootPos ?? motion.root ?? null;
|
|
82
|
-
if (root !== null && root.length !== motion.frames * 3) {
|
|
83
|
-
throw new Error(`${path}: rootPos must contain ${motion.frames * 3} values`);
|
|
84
|
-
}
|
|
85
|
-
const take = { path, frames: motion.frames, fps, joints, root };
|
|
86
|
-
const floor = belowFloor(take);
|
|
87
|
-
const jitterMmPerFrame2 = jitter(take);
|
|
88
|
-
return {
|
|
89
|
-
path,
|
|
90
|
-
frames: take.frames,
|
|
91
|
-
fps: take.fps,
|
|
92
|
-
footSlideCmPerS: footSlide(take),
|
|
93
|
-
// Keep the raw frame-space value for backwards-compatible diagnostics,
|
|
94
|
-
// but also expose a time-normalised acceleration. A second difference
|
|
95
|
-
// scales with dt², so the raw value alone would make the same physical
|
|
96
|
-
// shake look 4x smaller when a 30fps take is sampled at 60fps.
|
|
97
|
-
jitterMmPerFrame2,
|
|
98
|
-
jitterMmPerS2: jitterMmPerFrame2 * take.fps * take.fps,
|
|
99
|
-
framesBelowFloor: floor.count,
|
|
100
|
-
deepestBelowFloorCm: floor.deepestCm,
|
|
101
|
-
rootTravelM: travel(take),
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/** Median horizontal speed (cm/s) of each ankle over its own stance frames. */
|
|
106
|
-
function footSlide({ frames, fps, joints }) {
|
|
107
|
-
const speeds = [];
|
|
108
|
-
for (const ankle of Object.values(ANKLES)) {
|
|
109
|
-
const at = (frame, axis) => joints[(frame * JOINTS + ankle) * 3 + axis];
|
|
110
|
-
const step = [];
|
|
111
|
-
for (let frame = 1; frame < frames; frame += 1) {
|
|
112
|
-
step.push({
|
|
113
|
-
speed: Math.hypot(at(frame, 0) - at(frame - 1, 0), at(frame, 2) - at(frame - 1, 2)) * fps * 100,
|
|
114
|
-
height: Math.min(at(frame, 1), at(frame - 1, 1)),
|
|
115
|
-
});
|
|
116
|
-
}
|
|
117
|
-
if (!step.length) continue;
|
|
118
|
-
const lowEnough = percentile(step.map((s) => s.height), PLANT_HEIGHT_PERCENTILE);
|
|
119
|
-
const slowEnough = median(step.map((s) => s.speed));
|
|
120
|
-
const planted = step.filter((s) => s.height <= lowEnough && s.speed <= slowEnough);
|
|
121
|
-
speeds.push(median((planted.length ? planted : step).map((s) => s.speed)));
|
|
122
|
-
}
|
|
123
|
-
return median(speeds);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/** Mean |second difference| over every joint, in mm per frame^2. */
|
|
127
|
-
function jitter({ frames, joints }) {
|
|
128
|
-
if (frames < 3) return NaN;
|
|
129
|
-
let total = 0;
|
|
130
|
-
let count = 0;
|
|
131
|
-
for (let frame = 1; frame < frames - 1; frame += 1) {
|
|
132
|
-
for (let joint = 0; joint < JOINTS; joint += 1) {
|
|
133
|
-
const base = (frame * JOINTS + joint) * 3;
|
|
134
|
-
const before = ((frame - 1) * JOINTS + joint) * 3;
|
|
135
|
-
const after = ((frame + 1) * JOINTS + joint) * 3;
|
|
136
|
-
total += Math.hypot(
|
|
137
|
-
joints[after] - 2 * joints[base] + joints[before],
|
|
138
|
-
joints[after + 1] - 2 * joints[base + 1] + joints[before + 1],
|
|
139
|
-
joints[after + 2] - 2 * joints[base + 2] + joints[before + 2],
|
|
140
|
-
) * 1000;
|
|
141
|
-
count += 1;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
return total / count;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/** Frames whose lowest foot joint is under the floor, and how deep it goes. */
|
|
148
|
-
function belowFloor({ frames, joints }) {
|
|
149
|
-
let count = 0;
|
|
150
|
-
let deepest = 0;
|
|
151
|
-
for (let frame = 0; frame < frames; frame += 1) {
|
|
152
|
-
let lowest = Infinity;
|
|
153
|
-
for (const foot of FEET) lowest = Math.min(lowest, joints[(frame * JOINTS + foot) * 3 + 1]);
|
|
154
|
-
if (lowest < -FLOOR_TOLERANCE_M) {
|
|
155
|
-
count += 1;
|
|
156
|
-
deepest = Math.min(deepest, lowest);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
return { count, deepestCm: -deepest * 100 };
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/** Straight-line XZ travel of the root, for context on the slide number. */
|
|
163
|
-
function travel({ frames, joints, root }) {
|
|
164
|
-
const source = root ?? joints;
|
|
165
|
-
const stride = root ? 3 : JOINTS * 3;
|
|
166
|
-
const last = (frames - 1) * stride;
|
|
167
|
-
return Math.hypot(source[last] - source[0], source[last + 2] - source[2]);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/** Accept an NPZ path (legacy CLI/API) or a converted motion object. */
|
|
171
|
-
export function mocapMetrics(input) {
|
|
172
|
-
if (typeof input === "string") {
|
|
173
|
-
const take = read(input);
|
|
174
|
-
return mocapMetricsFromMotion(take, { path: input });
|
|
175
|
-
}
|
|
176
|
-
return mocapMetricsFromMotion(input);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
180
|
-
const paths = process.argv.slice(2);
|
|
181
|
-
if (!paths.length) {
|
|
182
|
-
console.error("usage: node tools/ardy/mocap-metrics.mjs <a.npz> [<b.npz> ...]");
|
|
183
|
-
process.exit(2);
|
|
184
|
-
}
|
|
185
|
-
const rows = paths.map(mocapMetrics);
|
|
186
|
-
const columns = [
|
|
187
|
-
["take", (row) => row.path.replace(/^.*\//, ""), 26],
|
|
188
|
-
["frames", (row) => String(row.frames), 6],
|
|
189
|
-
["fps", (row) => String(row.fps), 3],
|
|
190
|
-
["foot slide cm/s", (row) => row.footSlideCmPerS.toFixed(2), 15],
|
|
191
|
-
["jitter mm/f^2", (row) => row.jitterMmPerFrame2.toFixed(3), 13],
|
|
192
|
-
["jitter mm/s^2", (row) => row.jitterMmPerS2.toFixed(1), 13],
|
|
193
|
-
["below floor", (row) => `${row.framesBelowFloor} (${row.deepestBelowFloorCm.toFixed(1)} cm)`, 16],
|
|
194
|
-
["root travel m", (row) => row.rootTravelM.toFixed(2), 13],
|
|
195
|
-
];
|
|
196
|
-
const line = (cells) => cells.map((cell, index) => cell.padEnd(columns[index][2])).join(" ").trimEnd();
|
|
197
|
-
console.log(line(columns.map(([title]) => title)));
|
|
198
|
-
console.log(line(columns.map(([, , width]) => "-".repeat(width))));
|
|
199
|
-
for (const row of rows) console.log(line(columns.map(([, value]) => value(row))));
|
|
200
|
-
}
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Acceptance gate for a converted CSKEL27 mocap take.
|
|
4
|
-
*
|
|
5
|
-
* The report in mocap-metrics.mjs is intentionally descriptive. This gate
|
|
6
|
-
* turns the values into an explicit, reviewable decision so a visibly shaky
|
|
7
|
-
* clip cannot be shipped by accident. Thresholds are defaults for ordinary
|
|
8
|
-
* 24–60 fps footage and can be overridden per footage class with flags.
|
|
9
|
-
*/
|
|
10
|
-
import { mocapMetrics } from "./mocap-metrics.mjs";
|
|
11
|
-
import { pathToFileURL } from "node:url";
|
|
12
|
-
|
|
13
|
-
export const DEFAULT_LIMITS = Object.freeze({
|
|
14
|
-
maxFootSlideCmPerS: 3,
|
|
15
|
-
maxJitterMmPerFrame2: 6,
|
|
16
|
-
// 6 mm/frame² at the 30fps reference rate. The gate prefers the
|
|
17
|
-
// time-normalised metric when the extractor provides it, so a 60fps clip
|
|
18
|
-
// cannot pass merely because its frame interval is shorter.
|
|
19
|
-
maxJitterMmPerS2: 5400,
|
|
20
|
-
maxBelowFloorFraction: 0.02,
|
|
21
|
-
maxDeepestBelowFloorCm: 1.5,
|
|
22
|
-
maxVerticalContactErrorCm: 2,
|
|
23
|
-
maxCameraDriftPx: 2,
|
|
24
|
-
maxBackgroundDriftP95: 8,
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
/** Return a structured decision; pure so CI and browser QA can reuse it. */
|
|
28
|
-
export function evaluateQuality(metrics, limits = DEFAULT_LIMITS) {
|
|
29
|
-
const fps = Number(metrics?.fps);
|
|
30
|
-
const jitterIsNormalised = Number.isFinite(metrics?.jitterMmPerS2);
|
|
31
|
-
const jitterValue = jitterIsNormalised ? metrics.jitterMmPerS2 : metrics?.jitterMmPerFrame2;
|
|
32
|
-
// Preserve callers that override the legacy frame-space limit. Its
|
|
33
|
-
// historical meaning was measured at 30fps, so convert it to acceleration
|
|
34
|
-
// units when a normalised metric is available.
|
|
35
|
-
const frameJitterLimit = Number(limits.maxJitterMmPerFrame2);
|
|
36
|
-
const secondJitterLimit = Number(limits.maxJitterMmPerS2);
|
|
37
|
-
const jitterLimit = jitterIsNormalised
|
|
38
|
-
? (Number.isFinite(frameJitterLimit) &&
|
|
39
|
-
(!Number.isFinite(secondJitterLimit) || secondJitterLimit === DEFAULT_LIMITS.maxJitterMmPerS2)
|
|
40
|
-
? frameJitterLimit * 30 * 30
|
|
41
|
-
: secondJitterLimit)
|
|
42
|
-
: limits.maxJitterMmPerFrame2;
|
|
43
|
-
const checks = [
|
|
44
|
-
{
|
|
45
|
-
name: "foot-slide",
|
|
46
|
-
value: metrics.footSlideCmPerS,
|
|
47
|
-
limit: limits.maxFootSlideCmPerS,
|
|
48
|
-
pass: metrics.footSlideCmPerS <= limits.maxFootSlideCmPerS,
|
|
49
|
-
unit: "cm/s",
|
|
50
|
-
},
|
|
51
|
-
{
|
|
52
|
-
name: "jitter",
|
|
53
|
-
value: jitterValue,
|
|
54
|
-
limit: jitterLimit,
|
|
55
|
-
pass: Number.isFinite(jitterValue) && jitterValue <= jitterLimit,
|
|
56
|
-
unit: jitterIsNormalised ? "mm/s²" : "mm/frame²",
|
|
57
|
-
...(jitterIsNormalised ? { rawValue: metrics.jitterMmPerFrame2, fps } : {}),
|
|
58
|
-
},
|
|
59
|
-
{
|
|
60
|
-
name: "below-floor-fraction",
|
|
61
|
-
value: metrics.framesBelowFloor / metrics.frames,
|
|
62
|
-
limit: limits.maxBelowFloorFraction,
|
|
63
|
-
pass: metrics.framesBelowFloor / metrics.frames <= limits.maxBelowFloorFraction,
|
|
64
|
-
unit: "fraction",
|
|
65
|
-
},
|
|
66
|
-
{
|
|
67
|
-
name: "deepest-below-floor",
|
|
68
|
-
value: metrics.deepestBelowFloorCm,
|
|
69
|
-
limit: limits.maxDeepestBelowFloorCm,
|
|
70
|
-
pass: metrics.deepestBelowFloorCm <= limits.maxDeepestBelowFloorCm,
|
|
71
|
-
unit: "cm",
|
|
72
|
-
},
|
|
73
|
-
];
|
|
74
|
-
// These checks need scene/video observations that an NPZ alone does not
|
|
75
|
-
// contain. Include them whenever the extractor supplies the measurements;
|
|
76
|
-
// omission is reported to callers as "unmeasured" rather than guessed.
|
|
77
|
-
for (const [name, valueKey, limitKey, unit] of [
|
|
78
|
-
["vertical-contact", "verticalContactErrorCm", "maxVerticalContactErrorCm", "cm"],
|
|
79
|
-
["camera-drift", "cameraDriftPx", "maxCameraDriftPx", "px"],
|
|
80
|
-
["background-drift", "backgroundDriftP95", "maxBackgroundDriftP95", "mean-absolute-RGB"],
|
|
81
|
-
]) {
|
|
82
|
-
if (Number.isFinite(metrics[valueKey]) && Number.isFinite(limits[limitKey])) {
|
|
83
|
-
checks.push({ name, value: metrics[valueKey], limit: limits[limitKey], pass: metrics[valueKey] <= limits[limitKey], unit });
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
return { pass: checks.every((check) => check.pass), checks, metrics };
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function usage() {
|
|
90
|
-
console.error("usage: node tools/ardy/mocap-quality-gate.mjs <take.npz> [--foot-slide cm/s] [--jitter mm/frame² @30fps] [--jitter-s2 mm/s²] [--below-floor fraction] [--depth cm]");
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
94
|
-
const [path, ...args] = process.argv.slice(2);
|
|
95
|
-
if (!path) { usage(); process.exit(2); }
|
|
96
|
-
const limits = { ...DEFAULT_LIMITS };
|
|
97
|
-
for (let i = 0; i < args.length; i += 2) {
|
|
98
|
-
const value = Number(args[i + 1]);
|
|
99
|
-
if (!Number.isFinite(value)) { usage(); process.exit(2); }
|
|
100
|
-
if (args[i] === "--foot-slide") limits.maxFootSlideCmPerS = value;
|
|
101
|
-
else if (args[i] === "--jitter") limits.maxJitterMmPerFrame2 = value;
|
|
102
|
-
else if (args[i] === "--jitter-s2") limits.maxJitterMmPerS2 = value;
|
|
103
|
-
else if (args[i] === "--below-floor") limits.maxBelowFloorFraction = value;
|
|
104
|
-
else if (args[i] === "--depth") limits.maxDeepestBelowFloorCm = value;
|
|
105
|
-
else { usage(); process.exit(2); }
|
|
106
|
-
}
|
|
107
|
-
const result = evaluateQuality(mocapMetrics(path), limits);
|
|
108
|
-
console.log(JSON.stringify(result, null, 2));
|
|
109
|
-
if (!result.pass) process.exitCode = 1;
|
|
110
|
-
}
|
|
@@ -1,348 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Small, model-agnostic cleanup pass for GVHMR retargets.
|
|
3
|
-
*
|
|
4
|
-
* GVHMR is temporally aware, but its per-frame segmentation/keypoint inputs
|
|
5
|
-
* can still produce one-frame spikes. This pass removes only high-frequency
|
|
6
|
-
* residuals: the correction is centred on the neighbouring frames and its
|
|
7
|
-
* strength falls as local speed increases. A fast step therefore remains a
|
|
8
|
-
* step while a stationary foot no longer buzzes in place.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
const JOINTS = 27;
|
|
12
|
-
|
|
13
|
-
const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
|
|
14
|
-
const distance3 = (a, b) => Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]);
|
|
15
|
-
|
|
16
|
-
function smoothPositions(input, frames, stride, fps) {
|
|
17
|
-
const out = new Float32Array(input);
|
|
18
|
-
if (frames < 3) return { values: out, corrected: 0 };
|
|
19
|
-
let corrected = 0;
|
|
20
|
-
for (let f = 1; f < frames - 1; f += 1) {
|
|
21
|
-
for (let item = 0; item < stride; item += 1) {
|
|
22
|
-
const o = (f * stride + item) * 3;
|
|
23
|
-
const p = (f - 1) * stride * 3 + item * 3;
|
|
24
|
-
const n = (f + 1) * stride * 3 + item * 3;
|
|
25
|
-
const prev = [input[p], input[p + 1], input[p + 2]];
|
|
26
|
-
const curr = [input[o], input[o + 1], input[o + 2]];
|
|
27
|
-
const next = [input[n], input[n + 1], input[n + 2]];
|
|
28
|
-
const prevVelocity = [curr[0] - prev[0], curr[1] - prev[1], curr[2] - prev[2]];
|
|
29
|
-
const nextVelocity = [next[0] - curr[0], next[1] - curr[1], next[2] - curr[2]];
|
|
30
|
-
const speed = (distance3(curr, prev) + distance3(next, curr)) * 0.5 * fps;
|
|
31
|
-
// At rest blend up to 35% towards the centred neighbour estimate;
|
|
32
|
-
// above roughly 1 m/s preserve almost all of the authored motion.
|
|
33
|
-
const reversal = prevVelocity[0] * nextVelocity[0] + prevVelocity[1] * nextVelocity[1] + prevVelocity[2] * nextVelocity[2] < 0;
|
|
34
|
-
const blend = reversal ? 0.72 : clamp(0.35 - speed * 0.12, 0.04, 0.35);
|
|
35
|
-
const correction = [(prev[0] + next[0]) * 0.5 - curr[0],
|
|
36
|
-
(prev[1] + next[1]) * 0.5 - curr[1],
|
|
37
|
-
(prev[2] + next[2]) * 0.5 - curr[2]];
|
|
38
|
-
// Isolated reversals are segmentation outliers (often 10–15 cm on a
|
|
39
|
-
// rendered limb), so permit a larger correction only in that case.
|
|
40
|
-
const amount = Math.min(reversal ? 0.12 : 0.045, Math.hypot(...correction) * blend);
|
|
41
|
-
if (amount < 1e-5) continue;
|
|
42
|
-
const scale = amount / Math.max(Math.hypot(...correction), 1e-8);
|
|
43
|
-
out[o] = curr[0] + correction[0] * scale;
|
|
44
|
-
out[o + 1] = curr[1] + correction[1] * scale;
|
|
45
|
-
out[o + 2] = curr[2] + correction[2] * scale;
|
|
46
|
-
corrected += 1;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
// A segmentation/keypoint dropout can persist for a few frames. A single
|
|
50
|
-
// three-tap pass repairs the edges of that burst but leaves its centre at the
|
|
51
|
-
// wrong location. Detect a short burst against a longer chord, using the
|
|
52
|
-
// immutable source so a correction cannot spread into clean entry/exit
|
|
53
|
-
// frames. The radius is tied to time (about 100 ms), not a fixed frame count,
|
|
54
|
-
// so 24, 30 and 60 fps clips behave alike.
|
|
55
|
-
const radius = Math.max(3, Math.min(5, Math.round(fps * 0.1)));
|
|
56
|
-
if (frames > radius * 2 + 1) {
|
|
57
|
-
const pass = new Float32Array(out);
|
|
58
|
-
// Hips (joint 0) is the authored root trajectory. A real stair or chair
|
|
59
|
-
// ascent can be a short, high-amplitude change, so never run the dropout
|
|
60
|
-
// repair over it; only limb observations are eligible.
|
|
61
|
-
const firstItem = stride === JOINTS ? 1 : stride;
|
|
62
|
-
for (let item = firstItem; item < stride; item += 1) {
|
|
63
|
-
// First identify a contiguous run from the raw input. Requiring two
|
|
64
|
-
// adjacent outliers rejects a genuine one-frame limb snap, which the
|
|
65
|
-
// preceding three-tap pass already handles with a smaller correction.
|
|
66
|
-
const candidates = new Uint8Array(frames);
|
|
67
|
-
for (let f = radius; f < frames - radius; f += 1) {
|
|
68
|
-
const o = (f * stride + item) * 3;
|
|
69
|
-
const before = ((f - radius) * stride + item) * 3;
|
|
70
|
-
const after = ((f + radius) * stride + item) * 3;
|
|
71
|
-
const rawCurrent = [input[o], input[o + 1], input[o + 2]];
|
|
72
|
-
const rawEstimate = [(input[before] + input[after]) * 0.5,
|
|
73
|
-
(input[before + 1] + input[after + 1]) * 0.5,
|
|
74
|
-
(input[before + 2] + input[after + 2]) * 0.5];
|
|
75
|
-
// 8 cm is above ordinary retarget noise and catches the short,
|
|
76
|
-
// high-amplitude plateaus seen when a detector loses a limb.
|
|
77
|
-
if (distance3(rawCurrent, rawEstimate) < 0.08) continue;
|
|
78
|
-
const spanSpeed = distance3(
|
|
79
|
-
[input[before], input[before + 1], input[before + 2]],
|
|
80
|
-
[input[after], input[after + 1], input[after + 2]],
|
|
81
|
-
) * fps / (2 * radius);
|
|
82
|
-
// Preserve running/throwing trajectories; the long chord is only a
|
|
83
|
-
// dropout signal when the surrounding motion is below 2 m/s.
|
|
84
|
-
if (spanSpeed > 2) continue;
|
|
85
|
-
candidates[f] = 1;
|
|
86
|
-
}
|
|
87
|
-
for (let f = radius; f < frames - radius;) {
|
|
88
|
-
if (!candidates[f]) { f += 1; continue; }
|
|
89
|
-
const start = f;
|
|
90
|
-
while (f < frames - radius && candidates[f]) f += 1;
|
|
91
|
-
if (f - start < 2) continue;
|
|
92
|
-
// A dropout plateau has almost no movement inside its run. Reject a
|
|
93
|
-
// run whose own samples are moving quickly; that is a real gesture,
|
|
94
|
-
// even when its longer chord happens to curve by several centimetres.
|
|
95
|
-
let coherent = true;
|
|
96
|
-
for (let g = start + 1; g < f; g += 1) {
|
|
97
|
-
const prev = ((g - 1) * stride + item) * 3;
|
|
98
|
-
const curr = (g * stride + item) * 3;
|
|
99
|
-
if (distance3([input[prev], input[prev + 1], input[prev + 2]], [input[curr], input[curr + 1], input[curr + 2]]) > 0.015) {
|
|
100
|
-
coherent = false;
|
|
101
|
-
break;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
if (!coherent) continue;
|
|
105
|
-
for (let currentFrame = start; currentFrame < f; currentFrame += 1) {
|
|
106
|
-
const o = (currentFrame * stride + item) * 3;
|
|
107
|
-
const before = ((currentFrame - radius) * stride + item) * 3;
|
|
108
|
-
const after = ((currentFrame + radius) * stride + item) * 3;
|
|
109
|
-
const estimate = [(pass[before] + pass[after]) * 0.5,
|
|
110
|
-
(pass[before + 1] + pass[after + 1]) * 0.5,
|
|
111
|
-
(pass[before + 2] + pass[after + 2]) * 0.5];
|
|
112
|
-
const current = [pass[o], pass[o + 1], pass[o + 2]];
|
|
113
|
-
const residual = distance3(current, estimate);
|
|
114
|
-
const amount = Math.min(0.1, residual * 0.9);
|
|
115
|
-
if (amount < 1e-5) continue;
|
|
116
|
-
const scale = amount / Math.max(residual, 1e-8);
|
|
117
|
-
out[o] = current[0] + (estimate[0] - current[0]) * scale;
|
|
118
|
-
out[o + 1] = current[1] + (estimate[1] - current[1]) * scale;
|
|
119
|
-
out[o + 2] = current[2] + (estimate[2] - current[2]) * scale;
|
|
120
|
-
corrected += 1;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
return { values: out, corrected };
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function matrixToQuat(m) {
|
|
129
|
-
const trace = m[0] + m[4] + m[8];
|
|
130
|
-
let x; let y; let z; let w;
|
|
131
|
-
if (trace > 0) {
|
|
132
|
-
const s = Math.sqrt(trace + 1) * 2; w = 0.25 * s; x = (m[7] - m[5]) / s; y = (m[2] - m[6]) / s; z = (m[3] - m[1]) / s;
|
|
133
|
-
} else if (m[0] > m[4] && m[0] > m[8]) {
|
|
134
|
-
const s = Math.sqrt(1 + m[0] - m[4] - m[8]) * 2; w = (m[7] - m[5]) / s; x = 0.25 * s; y = (m[1] + m[3]) / s; z = (m[2] + m[6]) / s;
|
|
135
|
-
} else if (m[4] > m[8]) {
|
|
136
|
-
const s = Math.sqrt(1 + m[4] - m[0] - m[8]) * 2; w = (m[2] - m[6]) / s; x = (m[1] + m[3]) / s; y = 0.25 * s; z = (m[5] + m[7]) / s;
|
|
137
|
-
} else {
|
|
138
|
-
const s = Math.sqrt(1 + m[8] - m[0] - m[4]) * 2; w = (m[3] - m[1]) / s; x = (m[2] + m[6]) / s; y = (m[5] + m[7]) / s; z = 0.25 * s;
|
|
139
|
-
}
|
|
140
|
-
const norm = Math.hypot(x, y, z, w) || 1;
|
|
141
|
-
return [x / norm, y / norm, z / norm, w / norm];
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function quatToMatrix(q) {
|
|
145
|
-
const [x, y, z, w] = q;
|
|
146
|
-
return [1 - 2 * (y * y + z * z), 2 * (x * y - z * w), 2 * (x * z + y * w),
|
|
147
|
-
2 * (x * y + z * w), 1 - 2 * (x * x + z * z), 2 * (y * z - x * w),
|
|
148
|
-
2 * (x * z - y * w), 2 * (y * z + x * w), 1 - 2 * (x * x + y * y)];
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function slerp(a, b, t) {
|
|
152
|
-
let d = a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + a[3] * b[3];
|
|
153
|
-
if (d < 0) { b = b.map((v) => -v); d = -d; }
|
|
154
|
-
if (d > 0.9995) {
|
|
155
|
-
const q = a.map((v, i) => v + (b[i] - v) * t); const n = Math.hypot(...q) || 1; return q.map((v) => v / n);
|
|
156
|
-
}
|
|
157
|
-
const angle = Math.acos(clamp(d, -1, 1));
|
|
158
|
-
const sin = Math.sin(angle) || 1;
|
|
159
|
-
const wa = Math.sin((1 - t) * angle) / sin; const wb = Math.sin(t * angle) / sin;
|
|
160
|
-
return a.map((v, i) => v * wa + b[i] * wb);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function smoothRotations(input, frames, joints, fps) {
|
|
164
|
-
const out = new Float32Array(input); if (frames < 3) return { values: out, corrected: 0 };
|
|
165
|
-
let corrected = 0;
|
|
166
|
-
for (let j = 0; j < joints; j += 1) {
|
|
167
|
-
const qs = new Array(frames);
|
|
168
|
-
for (let f = 0; f < frames; f += 1) qs[f] = matrixToQuat(input.subarray((f * joints + j) * 9, (f * joints + j + 1) * 9));
|
|
169
|
-
for (let f = 1; f < frames - 1; f += 1) {
|
|
170
|
-
const prev = qs[f - 1]; const curr = qs[f]; const next = qs[f + 1];
|
|
171
|
-
const neighbour = slerp(prev, next, 0.5);
|
|
172
|
-
const angular = 2 * Math.acos(clamp(Math.abs(curr[0] * prev[0] + curr[1] * prev[1] + curr[2] * prev[2] + curr[3] * prev[3]), -1, 1)) * fps;
|
|
173
|
-
const blend = clamp(0.28 - angular * 0.035, 0.025, 0.28);
|
|
174
|
-
const q = slerp(curr, neighbour, blend);
|
|
175
|
-
const o = (f * joints + j) * 9; const m = quatToMatrix(q);
|
|
176
|
-
for (let k = 0; k < 9; k += 1) out[o + k] = m[k];
|
|
177
|
-
if (blend > 0.05) corrected += 1;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
return { values: out, corrected };
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/** Return a copy of a converted motion with transient jitter reduced. */
|
|
184
|
-
function stabilizeContacts(root, posed, frames, fps, { contactHeight, groundY } = {}) {
|
|
185
|
-
const outRoot = new Float32Array(root); const outPosed = new Float32Array(posed);
|
|
186
|
-
const feet = [21, 22, 25, 26]; const window = Math.max(2, Math.round(fps * .12));
|
|
187
|
-
let corrected = 0; const targetHeight = Number.isFinite(contactHeight) ? contactHeight : (Number.isFinite(groundY) ? groundY : null);
|
|
188
|
-
// Without an explicit surface datum we cannot tell floor contact from a
|
|
189
|
-
// step, stair, or chair. Leave the source trajectory untouched; smoothing
|
|
190
|
-
// above remains safe for every scene.
|
|
191
|
-
if (targetHeight === null) return { root: outRoot, posed: outPosed, corrected };
|
|
192
|
-
for (let f = window; f < frames - window; f += 1) {
|
|
193
|
-
const samples = [];
|
|
194
|
-
for (const j of feet) {
|
|
195
|
-
const speed = distance3(
|
|
196
|
-
[posed[(f * JOINTS + j) * 3], posed[(f * JOINTS + j) * 3 + 1], posed[(f * JOINTS + j) * 3 + 2]],
|
|
197
|
-
[posed[((f - window) * JOINTS + j) * 3], posed[((f - window) * JOINTS + j) * 3 + 1], posed[((f - window) * JOINTS + j) * 3 + 2]],
|
|
198
|
-
) / (window / fps);
|
|
199
|
-
const ys = []; for (let t = f - window; t <= f + window; t += 1) ys.push(posed[(t * JOINTS + j) * 3 + 1]);
|
|
200
|
-
const spread = Math.max(...ys) - Math.min(...ys);
|
|
201
|
-
if (speed < .08 && spread < .025) samples.push(j);
|
|
202
|
-
}
|
|
203
|
-
// Bilateral contact is a conservative signal that root translation should
|
|
204
|
-
// not buzz. Single-foot IK belongs to the character-specific solver.
|
|
205
|
-
if (samples.length < 2) continue;
|
|
206
|
-
let dx = 0; let dy = 0; let dz = 0;
|
|
207
|
-
for (const j of samples) { const o = (f * JOINTS + j) * 3; dx += posed[o]; dy += posed[o + 1]; dz += posed[o + 2]; }
|
|
208
|
-
dx /= samples.length; dy /= samples.length; dz /= samples.length;
|
|
209
|
-
if (targetHeight !== null) dy -= targetHeight;
|
|
210
|
-
// Apply only a small centred correction; larger changes indicate a real
|
|
211
|
-
// step or jump and are left to the source trajectory.
|
|
212
|
-
const prevAvg = [0, 0, 0]; const nextAvg = [0, 0, 0];
|
|
213
|
-
for (const j of samples) {
|
|
214
|
-
for (let k = 0; k < 3; k += 1) { prevAvg[k] += posed[((f - 1) * JOINTS + j) * 3 + k]; nextAvg[k] += posed[((f + 1) * JOINTS + j) * 3 + k]; }
|
|
215
|
-
}
|
|
216
|
-
for (let k = 0; k < 3; k += 1) { prevAvg[k] /= samples.length; nextAvg[k] /= samples.length; }
|
|
217
|
-
const correction = [((prevAvg[0] + nextAvg[0]) * .5 - dx), targetHeight !== null ? targetHeight - dy : ((prevAvg[1] + nextAvg[1]) * .5 - dy), ((prevAvg[2] + nextAvg[2]) * .5 - dz)];
|
|
218
|
-
const amount = Math.min(.025, Math.hypot(...correction));
|
|
219
|
-
if (amount < 1e-4) continue;
|
|
220
|
-
const scale = amount / Math.max(Math.hypot(...correction), 1e-8);
|
|
221
|
-
for (let j = 0; j < JOINTS; j += 1) { const o = (f * JOINTS + j) * 3; outPosed[o] += correction[0] * scale; outPosed[o + 1] += correction[1] * scale; outPosed[o + 2] += correction[2] * scale; }
|
|
222
|
-
outRoot[f * 3] += correction[0] * scale; outRoot[f * 3 + 1] += correction[1] * scale; outRoot[f * 3 + 2] += correction[2] * scale; corrected += 1;
|
|
223
|
-
}
|
|
224
|
-
return { root: outRoot, posed: outPosed, corrected };
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* Foot-anchored root re-integration (#380).
|
|
229
|
-
*
|
|
230
|
-
* GVHMR integrates the root from per-frame velocity and, on rendered clips,
|
|
231
|
-
* under-scales the stride: measured on v13c the stance ankle moonwalked at
|
|
232
|
-
* 45 cm/s in world while the hips advanced 49 cm/s — the legs walk faster
|
|
233
|
-
* than the body moves. Its contact logits did not correlate with planted
|
|
234
|
-
* frames (r <= 0.09) so the anchor is read off the take itself: per frame the
|
|
235
|
-
* foot that is BOTH lowest and slowest is the support, and the whole body is
|
|
236
|
-
* shifted rigidly (root + every joint by one vector) so that foot holds the
|
|
237
|
-
* world XZ it had when its stance run began. A rigid shift keeps every pose
|
|
238
|
-
* exactly as authored; only the trajectory changes. Y is never touched (a
|
|
239
|
-
* step, stair or chair is a real height change).
|
|
240
|
-
*
|
|
241
|
-
* Stance is a run: the anchor is only trusted once a foot has been low+slow
|
|
242
|
-
* for MIN_STANCE frames, and it releases as soon as the foot lifts or
|
|
243
|
-
* accelerates. Low and slow are both read off the INPUT arrays (the shift is
|
|
244
|
-
* written to copies), so the correction cannot feed back into its own
|
|
245
|
-
* trigger. Between runs the accumulated offset is held, never reset, so the
|
|
246
|
-
* take stays continuous.
|
|
247
|
-
*/
|
|
248
|
-
function anchorFeetToFloor(root, posed, frames, fps, reference = posed) {
|
|
249
|
-
const outRoot = new Float32Array(root); const outPosed = new Float32Array(posed);
|
|
250
|
-
const FEET = [21, 22, 25, 26];
|
|
251
|
-
const MIN_STANCE = Math.max(2, Math.round(fps * 0.08));
|
|
252
|
-
// Stance is read off `reference` — the AUTHORED joints, before the spike
|
|
253
|
-
// filter above: that filter's reversal branch moves the last stance frame
|
|
254
|
-
// (a velocity reversal by definition) by up to 2 cm, which would shorten
|
|
255
|
-
// every run by one frame and leave that frame sliding.
|
|
256
|
-
const at = (f, j, a) => reference[(f * JOINTS + j) * 3 + a];
|
|
257
|
-
const heights = FEET.map((j) => Array.from({ length: frames }, (_, f) => at(f, j, 1)));
|
|
258
|
-
const low = FEET.map((_, i) => { const s = [...heights[i]].sort((a, b) => a - b); return s[Math.floor((s.length - 1) * 0.35)]; });
|
|
259
|
-
// World horizontal speed of the authored foot. Stance is low AND slow;
|
|
260
|
-
// "slow" is half the foot's upper-quartile speed (its swing), which
|
|
261
|
-
// separates the two phases without depending on the walk's duty cycle —
|
|
262
|
-
// a median gate sits ON the stance speed of a 50/50 gait and then admits
|
|
263
|
-
// or rejects stance frames on float noise.
|
|
264
|
-
const speed = FEET.map((j) => Array.from({ length: frames }, (_, f) => f === 0 ? 0 : Math.hypot(at(f, j, 0) - at(f - 1, j, 0), at(f, j, 2) - at(f - 1, j, 2)) * fps));
|
|
265
|
-
const quantile = (v, q) => { const s = [...v].sort((a, b) => a - b); return s[Math.floor((s.length - 1) * q)]; };
|
|
266
|
-
const slow = speed.map((v) => quantile(v, 0.75) * 0.5);
|
|
267
|
-
const planted = (i, f) => heights[i][f] <= low[i] + 0.01 && speed[i][f] <= slow[i];
|
|
268
|
-
// Pass 1: stance runs. A run is one foot planted for >= MIN_STANCE
|
|
269
|
-
// consecutive frames; the lowest such foot carries the frame. Runs of the
|
|
270
|
-
// same foot that touch are one run.
|
|
271
|
-
const support = new Int8Array(frames).fill(-1);
|
|
272
|
-
for (let f = 0; f < frames; f += 1) {
|
|
273
|
-
let best = -1;
|
|
274
|
-
for (let i = 0; i < FEET.length; i += 1) {
|
|
275
|
-
let run = 0; for (let t = f; t >= 0 && planted(i, t); t -= 1) run += 1;
|
|
276
|
-
for (let t = f + 1; t < frames && run < MIN_STANCE && planted(i, t); t += 1) run += 1;
|
|
277
|
-
if (run >= MIN_STANCE && (best < 0 || heights[i][f] < heights[best][f])) best = i;
|
|
278
|
-
}
|
|
279
|
-
support[f] = best;
|
|
280
|
-
}
|
|
281
|
-
// Pass 2: the offset that holds each run's foot at its landing position.
|
|
282
|
-
// Inside a run only the foot's DRIFT is cancelled — the straight line from
|
|
283
|
-
// where it landed to where it lifted, which is the under-scaled stride —
|
|
284
|
-
// never its frame-to-frame wobble: mirroring that wobble into the root
|
|
285
|
-
// would move the whole body by the foot's estimation noise (measured:
|
|
286
|
-
// +4.4 mm/f² of root jitter for -25 cm/s of slide). Across the gap between
|
|
287
|
-
// two runs the offset ramps linearly so the swap is spread over the swing.
|
|
288
|
-
// Every pose is shifted rigidly per frame.
|
|
289
|
-
const offsets = new Array(frames).fill(null);
|
|
290
|
-
let anchoredFrames = 0; let carry = [0, 0]; let f = 0;
|
|
291
|
-
while (f < frames) {
|
|
292
|
-
const i = support[f];
|
|
293
|
-
if (i < 0) { f += 1; continue; }
|
|
294
|
-
let g = f; while (g + 1 < frames && support[g + 1] === i) g += 1;
|
|
295
|
-
const j = FEET[i];
|
|
296
|
-
// Land where the previous run left the body: the foot's own position
|
|
297
|
-
// at the run's first frame plus the offset carried in.
|
|
298
|
-
const driftX = at(g, j, 0) - at(f, j, 0), driftZ = at(g, j, 2) - at(f, j, 2), len = Math.max(1, g - f);
|
|
299
|
-
for (let t = f; t <= g; t += 1) { const w = (t - f) / len; offsets[t] = [carry[0] - driftX * w, carry[1] - driftZ * w]; anchoredFrames += 1; }
|
|
300
|
-
carry = offsets[g];
|
|
301
|
-
f = g + 1;
|
|
302
|
-
}
|
|
303
|
-
// Fill the gaps: lead-in holds the first run's offset, gaps ramp, tail holds.
|
|
304
|
-
let prev = -1;
|
|
305
|
-
for (let t = 0; t < frames; t += 1) {
|
|
306
|
-
if (offsets[t]) { prev = t; continue; }
|
|
307
|
-
let nextAt = t; while (nextAt < frames && !offsets[nextAt]) nextAt += 1;
|
|
308
|
-
const a = prev >= 0 ? offsets[prev] : (nextAt < frames ? offsets[nextAt] : [0, 0]);
|
|
309
|
-
const b = nextAt < frames ? offsets[nextAt] : a;
|
|
310
|
-
const span = nextAt - (prev >= 0 ? prev : t - 1);
|
|
311
|
-
for (let u = t; u < nextAt; u += 1) { const w = prev >= 0 && nextAt < frames ? (u - prev) / span : 0; offsets[u] = [a[0] + (b[0] - a[0]) * w, a[1] + (b[1] - a[1]) * w]; }
|
|
312
|
-
t = nextAt - 1;
|
|
313
|
-
}
|
|
314
|
-
// The offset series is piecewise linear with a slope change at every run
|
|
315
|
-
// and gap boundary (the stride correction differs per step). Blur it over
|
|
316
|
-
// ~120 ms so the body's velocity, not just its position, is continuous:
|
|
317
|
-
// the slope changes become ramps and add no second-difference of their own.
|
|
318
|
-
// A blur this short leaves the stance foot within a few mm of still.
|
|
319
|
-
const radius = Math.max(1, Math.round(fps * 0.08));
|
|
320
|
-
const kernel = []; let norm = 0;
|
|
321
|
-
for (let k = -radius; k <= radius; k += 1) { const w = Math.exp(-0.5 * (k / (radius / 2)) ** 2); kernel.push(w); norm += w; }
|
|
322
|
-
for (let t = 0; t < frames; t += 1) {
|
|
323
|
-
let ox = 0, oz = 0;
|
|
324
|
-
for (let k = -radius; k <= radius; k += 1) { const u = Math.min(frames - 1, Math.max(0, t + k)); const w = kernel[k + radius] / norm; ox += offsets[u][0] * w; oz += offsets[u][1] * w; }
|
|
325
|
-
for (let j = 0; j < JOINTS; j += 1) { const o = (t * JOINTS + j) * 3; outPosed[o] = posed[o] + ox; outPosed[o + 2] = posed[o + 2] + oz; }
|
|
326
|
-
outRoot[t * 3] = root[t * 3] + ox; outRoot[t * 3 + 2] = root[t * 3 + 2] + oz;
|
|
327
|
-
}
|
|
328
|
-
return { root: outRoot, posed: outPosed, anchoredFrames };
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
export function stabilizeMotion(motion, { enabled = true, smoothRotations: smoothRotationSeries = true, contactHeight, groundY, anchorFeet = false } = {}) {
|
|
332
|
-
if (!enabled || !motion || motion.frames < 3) return { ...motion, stabilization: { enabled: false, correctedPositions: 0, correctedRotations: 0, anchoredFrames: 0 } };
|
|
333
|
-
const frames = motion.frames; const fps = Number(motion.fps) || 30;
|
|
334
|
-
const root = smoothPositions(motion.rootPos, frames, 1, fps);
|
|
335
|
-
const posed = smoothPositions(motion.posedJoints, frames, JOINTS, fps);
|
|
336
|
-
const rotations = smoothRotationSeries ? smoothRotations(motion.rotMats, frames, JOINTS, fps) : { values: new Float32Array(motion.rotMats), corrected: 0 };
|
|
337
|
-
// Keep the root channel and the Hips joint coherent after filtering.
|
|
338
|
-
for (let f = 0; f < frames; f += 1) {
|
|
339
|
-
root.values[f * 3] = posed.values[(f * JOINTS) * 3];
|
|
340
|
-
root.values[f * 3 + 1] = posed.values[(f * JOINTS) * 3 + 1];
|
|
341
|
-
root.values[f * 3 + 2] = posed.values[(f * JOINTS) * 3 + 2];
|
|
342
|
-
}
|
|
343
|
-
const contacts = stabilizeContacts(root.values, posed.values, frames, fps, { contactHeight, groundY });
|
|
344
|
-
const anchored = anchorFeet ? anchorFeetToFloor(contacts.root, contacts.posed, frames, fps, motion.posedJoints) : { root: contacts.root, posed: contacts.posed, anchoredFrames: 0 };
|
|
345
|
-
return { ...motion, rootPos: anchored.root, posedJoints: anchored.posed, rotMats: rotations.values,
|
|
346
|
-
stabilization: { enabled: true, correctedPositions: posed.corrected + root.corrected, correctedRotations: rotations.corrected,
|
|
347
|
-
correctedContacts: contacts.corrected, anchoredFrames: anchored.anchoredFrames, contactHeight: Number.isFinite(contactHeight) ? contactHeight : (Number.isFinite(groundY) ? groundY : null), fps } };
|
|
348
|
-
}
|