cozyclay 2.1.1 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +88 -0
- package/README.md +12 -30
- package/bin/agent/agent-routes.mjs +43 -116
- package/bin/agent/agent-runner.mjs +14 -15
- 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/session-store.mjs +2 -2
- package/bin/agent/studio-context-budget.mjs +93 -0
- 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-BWCqdgWA.js +1 -0
- package/dist/assets/app-DfTCxaKk.js +5087 -0
- package/dist/assets/app-f-fkV5uP.css +1 -0
- package/dist/assets/compile-worker-BxKXh07s.js +5 -0
- package/dist/assets/fflate.module-KbGj5-H6.js +5 -0
- package/dist/assets/{first-shot-handoff-msLI71g1.js → first-shot-handoff-DNO8kv_N.js} +1 -1
- package/dist/assets/{landing-ChUvB_zL.js → landing-yfZaJkWc.js} +1 -1
- package/dist/assets/scene-asset-sync-U3kS2rvK.js +20 -0
- package/dist/assets/scene-asset-sync-_S8NQl5B.css +1 -0
- package/dist/assets/workflow-D78eiKTa.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 +151 -2625
- package/src/agent-progress.js +80 -0
- package/src/agent-turn-mark.js +30 -0
- package/src/analytics.js +1 -19
- package/src/app-context.js +12 -2
- 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 +10 -8
- 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 +214 -0
- package/src/commands/objects.js +6 -4
- package/src/commands/shot.js +55 -13
- 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 +53 -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 +47 -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 +60 -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 +79 -0
- package/src/motion3/compile-worker.js +10 -0
- package/src/motion3/compiler/anchors.js +113 -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 +197 -0
- package/src/motion3/compiler/math.js +72 -0
- package/src/motion3/compiler/pose.js +177 -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 +144 -0
- package/src/motion3/compiler/validate-parts.js +118 -0
- package/src/motion3/compiler/validate.js +122 -0
- package/src/motion3/compiler/vocab.js +74 -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 +177 -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 +204 -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/people.js +82 -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 +125 -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 +195 -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 +44 -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/people-checks.js +37 -0
- package/src/motion3/qa/severity.js +38 -0
- package/src/motion3/qa/spec-checks.js +65 -0
- package/src/motion3/qa/style.js +228 -0
- package/src/motion3/qa/wrist-checks.js +69 -0
- package/src/motion3/qa/wrist-obs.js +60 -0
- package/src/motion3/refs.js +129 -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 +113 -0
- package/src/motion3/schema/types.js +149 -0
- package/src/motion3/search.js +117 -0
- package/src/motion3/second-hand.js +112 -0
- package/src/motion3/settle.js +136 -0
- package/src/motion3/skills/basic.js +83 -0
- package/src/motion3/skills/book.js +76 -0
- package/src/motion3/skills/car.js +145 -0
- package/src/motion3/skills/flip.js +200 -0
- package/src/motion3/skills/frames-auto.js +29 -0
- package/src/motion3/skills/impact.js +109 -0
- package/src/motion3/skills/index.js +85 -0
- package/src/motion3/skills/info.js +80 -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/qa-label.js +25 -0
- 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 +16 -14
- package/src/studio-agent-commands.js +19 -16
- package/src/studio-agent-motion.js +9 -9
- package/src/studio-agent-protocol.js +47 -77
- package/src/studio-app-binding.js +27 -47
- 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 +15 -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 +109 -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 +9 -125
- 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,899 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* measure-preserve.mjs — how much of a base take survived a regeneration?
|
|
4
|
-
*
|
|
5
|
-
* This is the measuring stick for the scheduled-inpainting acceptance gates
|
|
6
|
-
* (.omo/plans/scheduled-inpainting.md, G1–G4): it compares two cclay motion
|
|
7
|
-
* npz files — the take the user already had, and the take that came back out
|
|
8
|
-
* of a preserve-enabled generation — and reports how far apart they are.
|
|
9
|
-
*
|
|
10
|
-
* GPU-free and dependency-free on purpose: the gates are run over artefacts
|
|
11
|
-
* pulled back from the box, so this has to work from a laptop with nothing but
|
|
12
|
-
* node and the two files.
|
|
13
|
-
*
|
|
14
|
-
* WHY ROOT-RELATIVE POSITIONS ARE THE HEADLINE NUMBER. Kimodo generates in a
|
|
15
|
-
* canonicalised space and the take is re-anchored afterwards, so two takes that
|
|
16
|
-
* hold the SAME pose can sit metres apart in world coordinates. A raw
|
|
17
|
-
* posed_joints difference is then dominated by root drift and says nothing about
|
|
18
|
-
* whether the body was preserved, which is the thing the gates are about. So the
|
|
19
|
-
* headline L2P subtracts each frame's Hips position from every joint of that
|
|
20
|
-
* frame before differencing, and the undrifted figure is still reported
|
|
21
|
-
* separately as globalL2P because a take that preserves the pose but walks off
|
|
22
|
-
* in a different direction is its own kind of failure and must stay visible.
|
|
23
|
-
*
|
|
24
|
-
* WHY ROTATION IS MEASURED GEODESICALLY. Per-element matrix differences are not
|
|
25
|
-
* a metric on SO(3) — the same 10° error scores differently depending on where
|
|
26
|
-
* on the sphere it sits. The geodesic angle acos((tr(Rᵀ_a R_b) - 1)/2) is the
|
|
27
|
-
* actual rotation you would have to apply to get from one to the other, in
|
|
28
|
-
* radians, and is directly comparable between joints and between clips.
|
|
29
|
-
*
|
|
30
|
-
* WHY FOOT SLIDING IS MEASURED PER CLIP, NOT AS A DIFFERENCE. G4 asks whether
|
|
31
|
-
* preservation introduced skating, and skating is a property of ONE clip (a
|
|
32
|
-
* planted foot that translates), not of the pair. So each clip is measured on
|
|
33
|
-
* its own timeline with its own floor, and only then are the two numbers
|
|
34
|
-
* subtracted.
|
|
35
|
-
*
|
|
36
|
-
* WHY THERE IS A PER-JOINT SPLIT (round 2). The grouped-mask gates are claims
|
|
37
|
-
* about WHICH PART of the body moved: "free the left arm over this range and the
|
|
38
|
-
* arm moves while the legs do not" (GA), "the edited hand lands on target and the
|
|
39
|
-
* other limbs stay put" (GC). A single whole-body mean cannot distinguish "the
|
|
40
|
-
* arm moved 10 cm" from "every joint drifted 1 cm", and those are a pass and a
|
|
41
|
-
* failure of the same gate. So the same root-relative L2P is also accumulated
|
|
42
|
-
* per cskel27 joint and aggregated into the seven C1v2 mask groups.
|
|
43
|
-
*
|
|
44
|
-
* usage:
|
|
45
|
-
* measure-preserve.mjs <base.npz> <candidate.npz> [--ranges 40:80,120:150]
|
|
46
|
-
* [--json] [--per-joint] [--groups]
|
|
47
|
-
* [--max-l2p M] [--max-l2r RAD] [--max-foot-delta M]
|
|
48
|
-
*
|
|
49
|
-
* `--ranges` are APP frames, half-open [start,end), in the BASE clip's frame
|
|
50
|
-
* space — the same space C3's `editRanges` are authored in. Frames inside any
|
|
51
|
-
* range are the ones the edit was allowed to change; everything else is what
|
|
52
|
-
* G2 says must stay pinned to the base.
|
|
53
|
-
*
|
|
54
|
-
* Any --max-* flag turns this into a gate: exceeding it exits 1. With no
|
|
55
|
-
* threshold flag the tool only reports and exits 0.
|
|
56
|
-
*/
|
|
57
|
-
|
|
58
|
-
import { readNpz } from "./read-npz.mjs";
|
|
59
|
-
import { CSKEL27_JOINTS } from "../../src/ardy/cskel27.js";
|
|
60
|
-
|
|
61
|
-
const JOINTS = 27;
|
|
62
|
-
|
|
63
|
-
/** Joint 0 is the root in cskel27; asserted rather than assumed because every
|
|
64
|
-
* root-relative number below silently changes meaning if the order ever moves. */
|
|
65
|
-
const HIPS = 0;
|
|
66
|
-
if (CSKEL27_JOINTS[HIPS] !== "Hips") {
|
|
67
|
-
throw new Error(`measure-preserve: cskel27 joint 0 is ${CSKEL27_JOINTS[HIPS]}, expected Hips`);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/** The joints whose contact with the floor defines sliding. Toes are included
|
|
71
|
-
* because a heel-planted foot pivoting on the toe is not skating, but a toe
|
|
72
|
-
* that translates while down is. */
|
|
73
|
-
const FOOT_JOINTS = ["LeftFoot", "LeftToeBase", "RightFoot", "RightToeBase"];
|
|
74
|
-
const FOOT_INDICES = FOOT_JOINTS.map((name) => {
|
|
75
|
-
const index = CSKEL27_JOINTS.indexOf(name);
|
|
76
|
-
if (index < 0) throw new Error(`measure-preserve: cskel27 has no joint ${name}`);
|
|
77
|
-
return index;
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Contact heuristic, deliberately crude and deliberately per-joint-per-clip.
|
|
82
|
-
*
|
|
83
|
-
* A joint counts as planted on a frame when its height is within 5 cm of THAT
|
|
84
|
-
* JOINT's own minimum height across THAT clip. Per-joint because the toe sits
|
|
85
|
-
* lower than the ankle and a shared threshold would call the ankle airborne for
|
|
86
|
-
* the whole clip; per-clip because soma77ToCskel27Motion floor-shifts each take
|
|
87
|
-
* by its own lowest sample, so "y == 0" is not a shared floor between two files.
|
|
88
|
-
*
|
|
89
|
-
* The honest caveat: for a clip with no ground contact at all (a jump loop, a
|
|
90
|
-
* lying-down take) the minimum is not the floor and this will report the slide
|
|
91
|
-
* of whatever was lowest. That is acceptable here because G4 compares the SAME
|
|
92
|
-
* motion before and after preservation, so the bias is identical on both sides
|
|
93
|
-
* and cancels in the delta. Do not lift this number into an absolute
|
|
94
|
-
* "does this clip skate" claim without checking the clip has real contacts.
|
|
95
|
-
*/
|
|
96
|
-
const CONTACT_BAND_M = 0.05;
|
|
97
|
-
|
|
98
|
-
/* ------------------------------------------------------- cskel27 -> C1v2 groups */
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* The seven C1v2 mask groups expressed in cskel27 JOINT names.
|
|
102
|
-
*
|
|
103
|
-
* THIS IS A DIFFERENT NAMESPACE FROM THE MASK'S TRACK IDS and the two must not
|
|
104
|
-
* be confused. tools/kimodo/preserve-mask.mjs maps IK TRACK ids — the handles
|
|
105
|
-
* the user drags: `leftHand`, `leftElbow`, `chest`, `hips` — to these same seven
|
|
106
|
-
* group names. This table maps the 27 MEASURED joints to them. The names differ
|
|
107
|
-
* on both sides of the arrow: the `leftElbow` track is the joint cskel27 calls
|
|
108
|
-
* LeftForeArm, the `chest` track spans Spine1/Spine2, and joints the user has no
|
|
109
|
-
* handle for at all (LeftHandEnd, RightToeBase, Spine3) still have to land in a
|
|
110
|
-
* group here because they are measured. So this is a mirror of TRACK_GROUPS'
|
|
111
|
-
* SEMANTICS, deliberately re-derived rather than imported — importing would
|
|
112
|
-
* suggest a name correspondence that does not exist.
|
|
113
|
-
*
|
|
114
|
-
* Two consequences worth stating before someone reads a number wrong:
|
|
115
|
-
*
|
|
116
|
-
* - `root` is Hips alone, and the root-relative L2P of Hips is IDENTICALLY
|
|
117
|
-
* ZERO by construction (every joint is expressed against its own frame's
|
|
118
|
-
* hips, so the hips are always the origin). The root group's meaningful
|
|
119
|
-
* number is therefore its GLOBAL L2P, which is reported alongside. A root
|
|
120
|
-
* l2p_m of 0.000000 is arithmetic, not a passing gate.
|
|
121
|
-
* - Hips appears in BOTH `root` and `torso`, mirroring the mask's `hips` track
|
|
122
|
-
* which frees both. The groups therefore overlap and their joint counts sum
|
|
123
|
-
* to 28, not 27.
|
|
124
|
-
*/
|
|
125
|
-
export const CSKEL27_GROUPS = Object.freeze({
|
|
126
|
-
root: Object.freeze(["Hips"]),
|
|
127
|
-
// the shoulders are torso, not arm: a clavicle swings the whole shoulder
|
|
128
|
-
// mass, exactly as TRACK_GROUPS puts leftShoulder/rightShoulder in torso.
|
|
129
|
-
torso: Object.freeze(["Hips", "Spine", "Spine1", "Spine2", "Spine3", "LeftShoulder", "RightShoulder"]),
|
|
130
|
-
head: Object.freeze(["Neck", "Head"]),
|
|
131
|
-
leftArm: Object.freeze(["LeftArm", "LeftForeArm", "LeftHand", "LeftHandEnd", "LeftHandThumb1"]),
|
|
132
|
-
rightArm: Object.freeze(["RightArm", "RightForeArm", "RightHand", "RightHandEnd", "RightHandThumb1"]),
|
|
133
|
-
leftLeg: Object.freeze(["LeftUpLeg", "LeftLeg", "LeftFoot", "LeftToeBase"]),
|
|
134
|
-
rightLeg: Object.freeze(["RightUpLeg", "RightLeg", "RightFoot", "RightToeBase"]),
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
/** Group name -> joint indices, resolved once and checked for total coverage. */
|
|
138
|
-
const GROUP_INDICES = Object.fromEntries(
|
|
139
|
-
Object.entries(CSKEL27_GROUPS).map(([group, joints]) => [
|
|
140
|
-
group,
|
|
141
|
-
joints.map((name) => {
|
|
142
|
-
const index = CSKEL27_JOINTS.indexOf(name);
|
|
143
|
-
if (index < 0) throw new Error(`measure-preserve: group ${group} names unknown cskel27 joint ${name}`);
|
|
144
|
-
return index;
|
|
145
|
-
}),
|
|
146
|
-
])
|
|
147
|
-
);
|
|
148
|
-
|
|
149
|
-
/** Joint index -> the groups it belongs to. A joint in NO group would be
|
|
150
|
-
* silently missing from every grouped number, so the partition is checked at
|
|
151
|
-
* load rather than trusted. */
|
|
152
|
-
const JOINT_GROUPS = CSKEL27_JOINTS.map((_, index) =>
|
|
153
|
-
Object.keys(GROUP_INDICES).filter((group) => GROUP_INDICES[group].includes(index))
|
|
154
|
-
);
|
|
155
|
-
for (let joint = 0; joint < JOINTS; joint += 1) {
|
|
156
|
-
if (JOINT_GROUPS[joint].length === 0) {
|
|
157
|
-
throw new Error(`measure-preserve: cskel27 joint ${CSKEL27_JOINTS[joint]} belongs to no C1v2 group`);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/* ------------------------------------------------------------------ loading */
|
|
162
|
-
|
|
163
|
-
function requireMember(members, name, path) {
|
|
164
|
-
const member = members[name];
|
|
165
|
-
if (!member) {
|
|
166
|
-
throw new Error(
|
|
167
|
-
`measure-preserve: ${path} is missing the '${name}' member — is this a cclay motion npz? ` +
|
|
168
|
-
`(found: ${Object.keys(members).join(", ") || "nothing"})`
|
|
169
|
-
);
|
|
170
|
-
}
|
|
171
|
-
if (member.unsupported || !member.data) {
|
|
172
|
-
throw new Error(
|
|
173
|
-
`measure-preserve: ${path} stores '${name}' as unsupported dtype ${member.dtype}`
|
|
174
|
-
);
|
|
175
|
-
}
|
|
176
|
-
return member;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/** Count non-finite entries so a NaN in the source is reported by name instead
|
|
180
|
-
* of quietly turning every aggregate into NaN. */
|
|
181
|
-
function countNonFinite(data) {
|
|
182
|
-
let count = 0;
|
|
183
|
-
for (let index = 0; index < data.length; index += 1) {
|
|
184
|
-
if (!Number.isFinite(data[index])) count += 1;
|
|
185
|
-
}
|
|
186
|
-
return count;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Load a cclay motion npz down to the arrays this tool compares.
|
|
191
|
-
* @returns {{path:string, frames:number, fps:number, fpsAssumed:boolean,
|
|
192
|
-
* rot:Float32Array, posed:Float32Array, nonFinite:object}}
|
|
193
|
-
*/
|
|
194
|
-
export function loadMotion(path, { defaultFps = 20 } = {}) {
|
|
195
|
-
const members = readNpz(path);
|
|
196
|
-
const rot = requireMember(members, "local_rot_mats", path);
|
|
197
|
-
const posed = requireMember(members, "posed_joints", path);
|
|
198
|
-
|
|
199
|
-
if (rot.shape.length !== 4 || rot.shape[1] !== JOINTS || rot.shape[2] !== 3 || rot.shape[3] !== 3) {
|
|
200
|
-
throw new Error(
|
|
201
|
-
`measure-preserve: ${path} local_rot_mats must be [T,${JOINTS},3,3], got [${rot.shape}]`
|
|
202
|
-
);
|
|
203
|
-
}
|
|
204
|
-
if (posed.shape.length !== 3 || posed.shape[1] !== JOINTS || posed.shape[2] !== 3) {
|
|
205
|
-
throw new Error(
|
|
206
|
-
`measure-preserve: ${path} posed_joints must be [T,${JOINTS},3], got [${posed.shape}]`
|
|
207
|
-
);
|
|
208
|
-
}
|
|
209
|
-
const frames = rot.shape[0];
|
|
210
|
-
if (posed.shape[0] !== frames) {
|
|
211
|
-
throw new Error(
|
|
212
|
-
`measure-preserve: ${path} local_rot_mats has ${frames} frames but posed_joints has ${posed.shape[0]}`
|
|
213
|
-
);
|
|
214
|
-
}
|
|
215
|
-
if (frames < 1) throw new Error(`measure-preserve: ${path} has no frames`);
|
|
216
|
-
|
|
217
|
-
// fps is optional in the wild: ARDY-generated takes write it as <i4 and
|
|
218
|
-
// ARDY-Core demos as <i8, but a hand-built or trimmed file may omit it. A
|
|
219
|
-
// missing fps is assumed, never guessed silently — it changes the resampling.
|
|
220
|
-
let fps = null;
|
|
221
|
-
let fpsAssumed = false;
|
|
222
|
-
if (members.fps && members.fps.data && !members.fps.unsupported) {
|
|
223
|
-
fps = Math.round(members.fps.data[0]);
|
|
224
|
-
if (!Number.isInteger(fps) || fps < 1) {
|
|
225
|
-
throw new Error(`measure-preserve: ${path} has a non-positive fps member (${fps})`);
|
|
226
|
-
}
|
|
227
|
-
} else {
|
|
228
|
-
fps = defaultFps;
|
|
229
|
-
fpsAssumed = true;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
return {
|
|
233
|
-
path,
|
|
234
|
-
frames,
|
|
235
|
-
fps,
|
|
236
|
-
fpsAssumed,
|
|
237
|
-
rot: rot.data,
|
|
238
|
-
posed: posed.data,
|
|
239
|
-
nonFinite: {
|
|
240
|
-
local_rot_mats: countNonFinite(rot.data),
|
|
241
|
-
posed_joints: countNonFinite(posed.data),
|
|
242
|
-
},
|
|
243
|
-
};
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
/* -------------------------------------------------------------- comparison */
|
|
247
|
-
|
|
248
|
-
/**
|
|
249
|
-
* Map every base frame onto a candidate frame by NEAREST sample in time.
|
|
250
|
-
*
|
|
251
|
-
* Nearest, not interpolated, because interpolating rotation matrices
|
|
252
|
-
* element-wise leaves SO(3) and would make L2R measure the interpolation error
|
|
253
|
-
* instead of the generation's. Nearest is exact when the clocks match (the
|
|
254
|
-
* normal case) and is an honest, reportable approximation when they do not.
|
|
255
|
-
*
|
|
256
|
-
* Base frames whose time falls past the end of the candidate are dropped rather
|
|
257
|
-
* than clamped: clamping would compare the whole tail of the base against one
|
|
258
|
-
* repeated candidate frame and invent an error that is really just a length
|
|
259
|
-
* mismatch. The count of dropped frames is reported as a truncation warning.
|
|
260
|
-
*/
|
|
261
|
-
export function buildFrameMap(base, candidate) {
|
|
262
|
-
const map = new Int32Array(base.frames);
|
|
263
|
-
let compared = 0;
|
|
264
|
-
for (let frame = 0; frame < base.frames; frame += 1) {
|
|
265
|
-
const index = Math.round((frame * candidate.fps) / base.fps);
|
|
266
|
-
if (index > candidate.frames - 1) break;
|
|
267
|
-
map[frame] = index;
|
|
268
|
-
compared += 1;
|
|
269
|
-
}
|
|
270
|
-
if (compared === 0) {
|
|
271
|
-
throw new Error(
|
|
272
|
-
`measure-preserve: no overlapping frames between ${base.path} ` +
|
|
273
|
-
`(${base.frames}@${base.fps}) and ${candidate.path} (${candidate.frames}@${candidate.fps})`
|
|
274
|
-
);
|
|
275
|
-
}
|
|
276
|
-
return { map, compared };
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
/** Accumulator for a mean over per-sample values, NaN-skipping and counting. */
|
|
280
|
-
function accumulator() {
|
|
281
|
-
return { sum: 0, count: 0, skipped: 0, max: 0 };
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
function add(acc, value) {
|
|
285
|
-
if (!Number.isFinite(value)) {
|
|
286
|
-
acc.skipped += 1;
|
|
287
|
-
return;
|
|
288
|
-
}
|
|
289
|
-
acc.sum += value;
|
|
290
|
-
acc.count += 1;
|
|
291
|
-
if (value > acc.max) acc.max = value;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
function mean(acc) {
|
|
295
|
-
return acc.count > 0 ? acc.sum / acc.count : null;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
function summarize(acc) {
|
|
299
|
-
return { mean: mean(acc), max: acc.count > 0 ? acc.max : null, samples: acc.count, skipped: acc.skipped };
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
/**
|
|
303
|
-
* Merge accumulators into one, so a group's mean is the mean over ALL its
|
|
304
|
-
* joints' samples rather than the mean of their per-joint means. The two differ
|
|
305
|
-
* whenever a joint skipped a NaN sample, and the sample-weighted one is the
|
|
306
|
-
* honest reading of "how far apart are these joints".
|
|
307
|
-
*/
|
|
308
|
-
function combine(accs) {
|
|
309
|
-
const merged = accumulator();
|
|
310
|
-
for (const acc of accs) {
|
|
311
|
-
merged.sum += acc.sum;
|
|
312
|
-
merged.count += acc.count;
|
|
313
|
-
merged.skipped += acc.skipped;
|
|
314
|
-
if (acc.count > 0 && acc.max > merged.max) merged.max = acc.max;
|
|
315
|
-
}
|
|
316
|
-
return merged;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
/**
|
|
320
|
-
* Geodesic angle between two rotation matrices, in radians.
|
|
321
|
-
*
|
|
322
|
-
* The textbook form is acos((tr(Rᵀ_a R_b) - 1) / 2), and it is a TRAP at the
|
|
323
|
-
* small angles this tool exists to measure. acos has infinite slope at 1, so it
|
|
324
|
-
* amplifies error like sqrt: the npz stores float32, whose matrices are only
|
|
325
|
-
* orthonormal to ~1e-7, and acos turns that 1e-7 into ~3e-4 rad of phantom
|
|
326
|
-
* rotation error. Measured, not theorised — the first cut of this file scored a
|
|
327
|
-
* fixture against ITSELF at 1.04e-4 rad mean / 7.6e-4 max, which is the same
|
|
328
|
-
* order as a real preservation difference and would have made G1 unreadable.
|
|
329
|
-
*
|
|
330
|
-
* So the angle comes from atan2(sin, cos) instead, where sin is read off the
|
|
331
|
-
* skew-symmetric part of Rᵀ_a R_b. Near zero, sin IS the angle to first order
|
|
332
|
-
* and is built from DIFFERENCES that vanish when the matrices agree, so the
|
|
333
|
-
* float32 noise stays 1e-7 instead of being square-rooted up.
|
|
334
|
-
*
|
|
335
|
-
* The bitwise fast path on top is not an optimisation dodge: two identical
|
|
336
|
-
* float32 blocks are the same rotation, and the geodesic distance from a
|
|
337
|
-
* rotation to itself is exactly 0. Any nonzero result there is arithmetic
|
|
338
|
-
* noise, and reporting it would be wrong, not precise.
|
|
339
|
-
*
|
|
340
|
-
* Neither the transpose nor the product is materialised — the six entries and
|
|
341
|
-
* the trace are read straight out of the two flat blocks.
|
|
342
|
-
*/
|
|
343
|
-
function geodesicAngle(rot, aBase, bBase) {
|
|
344
|
-
const a = rot.a;
|
|
345
|
-
const b = rot.b;
|
|
346
|
-
let identical = true;
|
|
347
|
-
for (let element = 0; element < 9; element += 1) {
|
|
348
|
-
if (a[aBase + element] !== b[bBase + element]) {
|
|
349
|
-
identical = false;
|
|
350
|
-
break;
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
if (identical) return 0;
|
|
354
|
-
|
|
355
|
-
// R = Aᵀ B, so R[i][j] = sum_k A[k][i] * B[k][j]; A[r][c] is flat[base+r*3+c].
|
|
356
|
-
let trace = 0;
|
|
357
|
-
for (let element = 0; element < 9; element += 1) trace += a[aBase + element] * b[bBase + element];
|
|
358
|
-
|
|
359
|
-
let r21 = 0, r12 = 0, r02 = 0, r20 = 0, r10 = 0, r01 = 0;
|
|
360
|
-
for (let k = 0; k < 3; k += 1) {
|
|
361
|
-
const ak = aBase + k * 3;
|
|
362
|
-
const bk = bBase + k * 3;
|
|
363
|
-
r21 += a[ak + 2] * b[bk + 1];
|
|
364
|
-
r12 += a[ak + 1] * b[bk + 2];
|
|
365
|
-
r02 += a[ak + 0] * b[bk + 2];
|
|
366
|
-
r20 += a[ak + 2] * b[bk + 0];
|
|
367
|
-
r10 += a[ak + 1] * b[bk + 0];
|
|
368
|
-
r01 += a[ak + 0] * b[bk + 1];
|
|
369
|
-
}
|
|
370
|
-
const sin = 0.5 * Math.hypot(r21 - r12, r02 - r20, r10 - r01);
|
|
371
|
-
const cos = (trace - 1) / 2;
|
|
372
|
-
// atan2 needs no clamping: it is defined for any (sin, cos) pair, including
|
|
373
|
-
// the slightly-off-unit-circle pairs a non-exactly-orthonormal input gives.
|
|
374
|
-
return Math.atan2(sin, cos);
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
/**
|
|
378
|
-
* Parse `--ranges 40:80,120:150` into half-open [start,end) app-frame ranges.
|
|
379
|
-
*/
|
|
380
|
-
export function parseRanges(text) {
|
|
381
|
-
const ranges = [];
|
|
382
|
-
for (const part of text.split(",")) {
|
|
383
|
-
const trimmed = part.trim();
|
|
384
|
-
if (!trimmed) continue;
|
|
385
|
-
const bits = trimmed.split(":");
|
|
386
|
-
if (bits.length !== 2) {
|
|
387
|
-
throw new Error(`measure-preserve: bad range '${trimmed}', expected start:end`);
|
|
388
|
-
}
|
|
389
|
-
const start = Number(bits[0]);
|
|
390
|
-
const end = Number(bits[1]);
|
|
391
|
-
if (!Number.isInteger(start) || !Number.isInteger(end) || start < 0 || end <= start) {
|
|
392
|
-
throw new Error(
|
|
393
|
-
`measure-preserve: bad range '${trimmed}' — start and end must be integers with 0 <= start < end`
|
|
394
|
-
);
|
|
395
|
-
}
|
|
396
|
-
ranges.push({ startFrame: start, endFrame: end });
|
|
397
|
-
}
|
|
398
|
-
return ranges;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
/** Per-frame membership of the union of the edited ranges, in base frame space. */
|
|
402
|
-
function rangeMembership(ranges, frames) {
|
|
403
|
-
const inside = new Uint8Array(frames);
|
|
404
|
-
for (const range of ranges) {
|
|
405
|
-
const from = Math.max(0, range.startFrame);
|
|
406
|
-
const to = Math.min(frames, range.endFrame);
|
|
407
|
-
for (let frame = from; frame < to; frame += 1) inside[frame] = 1;
|
|
408
|
-
}
|
|
409
|
-
return inside;
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
/**
|
|
413
|
-
* Foot sliding for ONE clip, in metres of XZ travel per frame.
|
|
414
|
-
*
|
|
415
|
-
* Headline number is the TOTAL slide accumulated across all four foot joints
|
|
416
|
-
* divided by the clip's frame transitions, so a clip where both feet skate
|
|
417
|
-
* scores twice a clip where one does. Per-joint figures are reported alongside
|
|
418
|
-
* because a single skating toe is a different bug from a whole body gliding.
|
|
419
|
-
*/
|
|
420
|
-
export function footSliding(motion) {
|
|
421
|
-
const { frames, posed } = motion;
|
|
422
|
-
const transitions = Math.max(1, frames - 1);
|
|
423
|
-
const perJoint = [];
|
|
424
|
-
let total = 0;
|
|
425
|
-
let contactFrames = 0;
|
|
426
|
-
|
|
427
|
-
for (let slot = 0; slot < FOOT_INDICES.length; slot += 1) {
|
|
428
|
-
const joint = FOOT_INDICES[slot];
|
|
429
|
-
let minY = Infinity;
|
|
430
|
-
for (let frame = 0; frame < frames; frame += 1) {
|
|
431
|
-
const y = posed[(frame * JOINTS + joint) * 3 + 1];
|
|
432
|
-
if (Number.isFinite(y) && y < minY) minY = y;
|
|
433
|
-
}
|
|
434
|
-
let slide = 0;
|
|
435
|
-
let contacts = 0;
|
|
436
|
-
if (Number.isFinite(minY)) {
|
|
437
|
-
const ceiling = minY + CONTACT_BAND_M;
|
|
438
|
-
for (let frame = 0; frame < frames - 1; frame += 1) {
|
|
439
|
-
const here = (frame * JOINTS + joint) * 3;
|
|
440
|
-
const next = ((frame + 1) * JOINTS + joint) * 3;
|
|
441
|
-
if (!(posed[here + 1] <= ceiling)) continue; // airborne, or NaN
|
|
442
|
-
const dx = posed[next] - posed[here];
|
|
443
|
-
const dz = posed[next + 2] - posed[here + 2];
|
|
444
|
-
const step = Math.hypot(dx, dz);
|
|
445
|
-
if (!Number.isFinite(step)) continue;
|
|
446
|
-
slide += step;
|
|
447
|
-
contacts += 1;
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
total += slide;
|
|
451
|
-
contactFrames += contacts;
|
|
452
|
-
perJoint.push({
|
|
453
|
-
joint: CSKEL27_JOINTS[joint],
|
|
454
|
-
minHeight_m: Number.isFinite(minY) ? minY : null,
|
|
455
|
-
contactFrames: contacts,
|
|
456
|
-
slide_m: slide,
|
|
457
|
-
slide_m_per_frame: slide / transitions,
|
|
458
|
-
});
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
return {
|
|
462
|
-
contactBand_m: CONTACT_BAND_M,
|
|
463
|
-
frames,
|
|
464
|
-
contactFrames,
|
|
465
|
-
total_slide_m: total,
|
|
466
|
-
slide_m_per_frame: total / transitions,
|
|
467
|
-
perJoint,
|
|
468
|
-
};
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
/**
|
|
472
|
-
* Compare a base take against a candidate.
|
|
473
|
-
* @param {object} base from loadMotion
|
|
474
|
-
* @param {object} candidate from loadMotion
|
|
475
|
-
* @param {Array<{startFrame:number,endFrame:number}>} ranges edited app-frame ranges
|
|
476
|
-
*/
|
|
477
|
-
export function measurePreserve(base, candidate, ranges = []) {
|
|
478
|
-
const warnings = [];
|
|
479
|
-
for (const motion of [base, candidate]) {
|
|
480
|
-
for (const [member, count] of Object.entries(motion.nonFinite)) {
|
|
481
|
-
if (count > 0) warnings.push(`${motion.path}: ${count} non-finite values in ${member}`);
|
|
482
|
-
}
|
|
483
|
-
if (motion.fpsAssumed) warnings.push(`${motion.path}: no fps member, assumed ${motion.fps}`);
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
// Resampling and truncation are reported separately because they are
|
|
487
|
-
// different accusations: a clock mismatch means the numbers below carry a
|
|
488
|
-
// nearest-frame approximation, whereas an equal-clock length mismatch means
|
|
489
|
-
// the candidate is simply shorter and nothing was approximated.
|
|
490
|
-
const resampled = base.fps !== candidate.fps;
|
|
491
|
-
const { map, compared } = buildFrameMap(base, candidate);
|
|
492
|
-
if (resampled) {
|
|
493
|
-
warnings.push(
|
|
494
|
-
`candidate resampled onto the base timeline by nearest frame ` +
|
|
495
|
-
`(base ${base.frames}f@${base.fps} vs candidate ${candidate.frames}f@${candidate.fps})`
|
|
496
|
-
);
|
|
497
|
-
}
|
|
498
|
-
const truncated = compared < base.frames;
|
|
499
|
-
if (truncated) {
|
|
500
|
-
warnings.push(
|
|
501
|
-
`candidate is short: only ${compared} of ${base.frames} base frames have a candidate sample; ` +
|
|
502
|
-
`the remaining ${base.frames - compared} were dropped, not clamped`
|
|
503
|
-
);
|
|
504
|
-
}
|
|
505
|
-
for (const range of ranges) {
|
|
506
|
-
// The classic mix-up is handing this tool GENERATION-space frames (30 fps)
|
|
507
|
-
// for an app-space (20 fps) clip, which silently lands the range past the
|
|
508
|
-
// end and reports a flawless out-of-range score over the whole take.
|
|
509
|
-
if (range.startFrame >= compared) {
|
|
510
|
-
warnings.push(
|
|
511
|
-
`range [${range.startFrame},${range.endFrame}) starts past the last compared frame ` +
|
|
512
|
-
`(${compared}) — are these app frames in the BASE clip's fps space?`
|
|
513
|
-
);
|
|
514
|
-
} else if (range.endFrame > compared) {
|
|
515
|
-
warnings.push(
|
|
516
|
-
`range [${range.startFrame},${range.endFrame}) is clipped to the compared span (${compared} frames)`
|
|
517
|
-
);
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
const inside = rangeMembership(ranges, base.frames);
|
|
522
|
-
const rot = { a: base.rot, b: candidate.rot };
|
|
523
|
-
|
|
524
|
-
// Three buckets per metric: everything, inside the edited ranges, outside.
|
|
525
|
-
const buckets = {
|
|
526
|
-
l2p: { all: accumulator(), inRange: accumulator(), outOfRange: accumulator() },
|
|
527
|
-
globalL2p: { all: accumulator(), inRange: accumulator(), outOfRange: accumulator() },
|
|
528
|
-
l2r: { all: accumulator(), inRange: accumulator(), outOfRange: accumulator() },
|
|
529
|
-
};
|
|
530
|
-
const perFrame = { l2p: new Float64Array(compared), l2r: new Float64Array(compared) };
|
|
531
|
-
|
|
532
|
-
// Per-joint buckets, the round-2 addition. Three per joint for the same
|
|
533
|
-
// reason the whole-body metric has three: GA and GC are claims about a joint
|
|
534
|
-
// INSIDE the edited range versus the same joint outside it.
|
|
535
|
-
const perJointBuckets = [];
|
|
536
|
-
for (let joint = 0; joint < JOINTS; joint += 1) {
|
|
537
|
-
perJointBuckets.push({
|
|
538
|
-
l2p: { all: accumulator(), inRange: accumulator(), outOfRange: accumulator() },
|
|
539
|
-
globalL2p: accumulator(),
|
|
540
|
-
l2r: accumulator(),
|
|
541
|
-
});
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
for (let frame = 0; frame < compared; frame += 1) {
|
|
545
|
-
const other = map[frame];
|
|
546
|
-
const baseFrameBase = frame * JOINTS * 3;
|
|
547
|
-
const candFrameBase = other * JOINTS * 3;
|
|
548
|
-
const baseHips = baseFrameBase + HIPS * 3;
|
|
549
|
-
const candHips = candFrameBase + HIPS * 3;
|
|
550
|
-
const bucket = inside[frame] ? "inRange" : "outOfRange";
|
|
551
|
-
|
|
552
|
-
let frameL2p = 0;
|
|
553
|
-
let frameL2r = 0;
|
|
554
|
-
for (let joint = 0; joint < JOINTS; joint += 1) {
|
|
555
|
-
const p = baseFrameBase + joint * 3;
|
|
556
|
-
const q = candFrameBase + joint * 3;
|
|
557
|
-
const jointBucket = perJointBuckets[joint];
|
|
558
|
-
|
|
559
|
-
// Root-relative: each side is expressed against its OWN hips, so a
|
|
560
|
-
// clip that is the same pose translated scores zero here.
|
|
561
|
-
const rdx = base.posed[p] - base.posed[baseHips] - (candidate.posed[q] - candidate.posed[candHips]);
|
|
562
|
-
const rdy = base.posed[p + 1] - base.posed[baseHips + 1] - (candidate.posed[q + 1] - candidate.posed[candHips + 1]);
|
|
563
|
-
const rdz = base.posed[p + 2] - base.posed[baseHips + 2] - (candidate.posed[q + 2] - candidate.posed[candHips + 2]);
|
|
564
|
-
const local = Math.hypot(rdx, rdy, rdz);
|
|
565
|
-
add(buckets.l2p.all, local);
|
|
566
|
-
add(buckets.l2p[bucket], local);
|
|
567
|
-
add(jointBucket.l2p.all, local);
|
|
568
|
-
add(jointBucket.l2p[bucket], local);
|
|
569
|
-
frameL2p += Number.isFinite(local) ? local : 0;
|
|
570
|
-
|
|
571
|
-
const global = Math.hypot(
|
|
572
|
-
base.posed[p] - candidate.posed[q],
|
|
573
|
-
base.posed[p + 1] - candidate.posed[q + 1],
|
|
574
|
-
base.posed[p + 2] - candidate.posed[q + 2]
|
|
575
|
-
);
|
|
576
|
-
add(buckets.globalL2p.all, global);
|
|
577
|
-
add(buckets.globalL2p[bucket], global);
|
|
578
|
-
add(jointBucket.globalL2p, global);
|
|
579
|
-
|
|
580
|
-
const angle = geodesicAngle(rot, (frame * JOINTS + joint) * 9, (other * JOINTS + joint) * 9);
|
|
581
|
-
add(buckets.l2r.all, angle);
|
|
582
|
-
add(buckets.l2r[bucket], angle);
|
|
583
|
-
add(jointBucket.l2r, angle);
|
|
584
|
-
frameL2r += Number.isFinite(angle) ? angle : 0;
|
|
585
|
-
}
|
|
586
|
-
perFrame.l2p[frame] = frameL2p / JOINTS;
|
|
587
|
-
perFrame.l2r[frame] = frameL2r / JOINTS;
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
// Per-range breakdown: a union aggregate hides an edit that only moved one
|
|
591
|
-
// of two ranges, which is exactly the G2 failure mode.
|
|
592
|
-
const perRange = ranges.map((range) => {
|
|
593
|
-
const l2p = accumulator();
|
|
594
|
-
const l2r = accumulator();
|
|
595
|
-
const from = Math.max(0, range.startFrame);
|
|
596
|
-
const to = Math.min(compared, range.endFrame);
|
|
597
|
-
for (let frame = from; frame < to; frame += 1) {
|
|
598
|
-
add(l2p, perFrame.l2p[frame]);
|
|
599
|
-
add(l2r, perFrame.l2r[frame]);
|
|
600
|
-
}
|
|
601
|
-
return {
|
|
602
|
-
startFrame: range.startFrame,
|
|
603
|
-
endFrame: range.endFrame,
|
|
604
|
-
comparedFrames: Math.max(0, to - from),
|
|
605
|
-
l2p_m: mean(l2p),
|
|
606
|
-
l2r_rad: mean(l2r),
|
|
607
|
-
};
|
|
608
|
-
});
|
|
609
|
-
|
|
610
|
-
// Per-joint table, in cskel27 index order so it lines up with the npz arrays
|
|
611
|
-
// and with any other tool that indexes joints. Sorting it by error would read
|
|
612
|
-
// better and would make two runs impossible to diff, so it stays in order.
|
|
613
|
-
const perJoint = perJointBuckets.map((jointBucket, joint) => ({
|
|
614
|
-
index: joint,
|
|
615
|
-
joint: CSKEL27_JOINTS[joint],
|
|
616
|
-
groups: JOINT_GROUPS[joint],
|
|
617
|
-
l2p_m: mean(jointBucket.l2p.all),
|
|
618
|
-
l2pMax_m: jointBucket.l2p.all.count > 0 ? jointBucket.l2p.all.max : null,
|
|
619
|
-
globalL2p_m: mean(jointBucket.globalL2p),
|
|
620
|
-
l2r_rad: mean(jointBucket.l2r),
|
|
621
|
-
l2p_inRange_m: ranges.length ? mean(jointBucket.l2p.inRange) : null,
|
|
622
|
-
l2p_outOfRange_m: ranges.length ? mean(jointBucket.l2p.outOfRange) : null,
|
|
623
|
-
}));
|
|
624
|
-
|
|
625
|
-
// Group aggregation. Sample-weighted across the group's joints, and reported
|
|
626
|
-
// with globalL2p alongside because the `root` group's root-relative number is
|
|
627
|
-
// identically zero by construction (see CSKEL27_GROUPS).
|
|
628
|
-
const perGroup = Object.keys(CSKEL27_GROUPS).map((group) => {
|
|
629
|
-
const indices = GROUP_INDICES[group];
|
|
630
|
-
const l2pAll = combine(indices.map((joint) => perJointBuckets[joint].l2p.all));
|
|
631
|
-
return {
|
|
632
|
-
group,
|
|
633
|
-
joints: indices.map((joint) => CSKEL27_JOINTS[joint]),
|
|
634
|
-
jointCount: indices.length,
|
|
635
|
-
l2p_m: mean(l2pAll),
|
|
636
|
-
l2pMax_m: l2pAll.count > 0 ? l2pAll.max : null,
|
|
637
|
-
globalL2p_m: mean(combine(indices.map((joint) => perJointBuckets[joint].globalL2p))),
|
|
638
|
-
l2r_rad: mean(combine(indices.map((joint) => perJointBuckets[joint].l2r))),
|
|
639
|
-
l2p_inRange_m: ranges.length
|
|
640
|
-
? mean(combine(indices.map((joint) => perJointBuckets[joint].l2p.inRange)))
|
|
641
|
-
: null,
|
|
642
|
-
l2p_outOfRange_m: ranges.length
|
|
643
|
-
? mean(combine(indices.map((joint) => perJointBuckets[joint].l2p.outOfRange)))
|
|
644
|
-
: null,
|
|
645
|
-
};
|
|
646
|
-
});
|
|
647
|
-
|
|
648
|
-
const baseFeet = footSliding(base);
|
|
649
|
-
const candidateFeet = footSliding(candidate);
|
|
650
|
-
|
|
651
|
-
return {
|
|
652
|
-
base: { path: base.path, frames: base.frames, fps: base.fps },
|
|
653
|
-
candidate: { path: candidate.path, frames: candidate.frames, fps: candidate.fps },
|
|
654
|
-
resampled,
|
|
655
|
-
truncated,
|
|
656
|
-
comparedFrames: compared,
|
|
657
|
-
ranges,
|
|
658
|
-
jointCount: JOINTS,
|
|
659
|
-
l2p_m: summarize(buckets.l2p.all),
|
|
660
|
-
l2p_inRange_m: ranges.length ? summarize(buckets.l2p.inRange) : null,
|
|
661
|
-
l2p_outOfRange_m: ranges.length ? summarize(buckets.l2p.outOfRange) : null,
|
|
662
|
-
globalL2p_m: summarize(buckets.globalL2p.all),
|
|
663
|
-
globalL2p_inRange_m: ranges.length ? summarize(buckets.globalL2p.inRange) : null,
|
|
664
|
-
globalL2p_outOfRange_m: ranges.length ? summarize(buckets.globalL2p.outOfRange) : null,
|
|
665
|
-
l2r_rad: summarize(buckets.l2r.all),
|
|
666
|
-
l2r_inRange_rad: ranges.length ? summarize(buckets.l2r.inRange) : null,
|
|
667
|
-
l2r_outOfRange_rad: ranges.length ? summarize(buckets.l2r.outOfRange) : null,
|
|
668
|
-
perRange,
|
|
669
|
-
perJoint,
|
|
670
|
-
perGroup,
|
|
671
|
-
footSliding: {
|
|
672
|
-
base: baseFeet,
|
|
673
|
-
candidate: candidateFeet,
|
|
674
|
-
// Positive = the candidate skates more than the base. G4's budget.
|
|
675
|
-
delta_m_per_frame: candidateFeet.slide_m_per_frame - baseFeet.slide_m_per_frame,
|
|
676
|
-
},
|
|
677
|
-
warnings,
|
|
678
|
-
};
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
/* -------------------------------------------------------------------- CLI */
|
|
682
|
-
|
|
683
|
-
function formatMetres(value) {
|
|
684
|
-
return value === null ? " n/a " : value.toFixed(6);
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
function formatRadians(value) {
|
|
688
|
-
return value === null ? " n/a " : `${value.toFixed(6)} (${((value * 180) / Math.PI).toFixed(3)}°)`;
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
/**
|
|
692
|
-
* The per-joint table. Printed only on request because 27 rows would bury the
|
|
693
|
-
* headline numbers in the common case, and the JSON carries it unconditionally
|
|
694
|
-
* for the gate harness.
|
|
695
|
-
*/
|
|
696
|
-
function printPerJoint(report) {
|
|
697
|
-
const showRanges = report.ranges.length > 0;
|
|
698
|
-
console.log(" per joint (root-relative L2P, metres — mean over the compared frames)");
|
|
699
|
-
console.log(
|
|
700
|
-
` ${"joint".padEnd(16)}${"group".padEnd(10)}${"mean".padStart(10)}${"max".padStart(11)}` +
|
|
701
|
-
`${"global".padStart(11)}` +
|
|
702
|
-
(showRanges ? `${"inRange".padStart(11)}${"outRange".padStart(11)}` : "")
|
|
703
|
-
);
|
|
704
|
-
for (const row of report.perJoint) {
|
|
705
|
-
console.log(
|
|
706
|
-
` ${row.joint.padEnd(16)}${row.groups.join("+").padEnd(10)}` +
|
|
707
|
-
`${formatMetres(row.l2p_m).padStart(10)}${formatMetres(row.l2pMax_m).padStart(11)}` +
|
|
708
|
-
`${formatMetres(row.globalL2p_m).padStart(11)}` +
|
|
709
|
-
(showRanges
|
|
710
|
-
? `${formatMetres(row.l2p_inRange_m).padStart(11)}${formatMetres(row.l2p_outOfRange_m).padStart(11)}`
|
|
711
|
-
: "")
|
|
712
|
-
);
|
|
713
|
-
}
|
|
714
|
-
// The one row that is always 0.000000 and always means nothing.
|
|
715
|
-
console.log(" (Hips root-relative L2P is 0 by construction; read its global column instead)");
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
/**
|
|
719
|
-
* The seven-group aggregation — the shape the round-2 gates are phrased in
|
|
720
|
-
* ("the arm moved, the legs did not").
|
|
721
|
-
*/
|
|
722
|
-
function printGroups(report) {
|
|
723
|
-
const showRanges = report.ranges.length > 0;
|
|
724
|
-
console.log(" per C1v2 mask group (root-relative L2P, metres; groups overlap — Hips is in root AND torso)");
|
|
725
|
-
console.log(
|
|
726
|
-
` ${"group".padEnd(10)}${"joints".padStart(7)}${"mean".padStart(11)}${"max".padStart(11)}` +
|
|
727
|
-
`${"global".padStart(11)}` +
|
|
728
|
-
(showRanges ? `${"inRange".padStart(11)}${"outRange".padStart(11)}` : "")
|
|
729
|
-
);
|
|
730
|
-
for (const row of report.perGroup) {
|
|
731
|
-
console.log(
|
|
732
|
-
` ${row.group.padEnd(10)}${String(row.jointCount).padStart(7)}` +
|
|
733
|
-
`${formatMetres(row.l2p_m).padStart(11)}${formatMetres(row.l2pMax_m).padStart(11)}` +
|
|
734
|
-
`${formatMetres(row.globalL2p_m).padStart(11)}` +
|
|
735
|
-
(showRanges
|
|
736
|
-
? `${formatMetres(row.l2p_inRange_m).padStart(11)}${formatMetres(row.l2p_outOfRange_m).padStart(11)}`
|
|
737
|
-
: "")
|
|
738
|
-
);
|
|
739
|
-
}
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
function printReport(report, { perJoint = false, groups = false } = {}) {
|
|
743
|
-
console.log(`base : ${report.base.path} ${report.base.frames} frames @ ${report.base.fps} fps`);
|
|
744
|
-
console.log(`candidate : ${report.candidate.path} ${report.candidate.frames} frames @ ${report.candidate.fps} fps`);
|
|
745
|
-
console.log(
|
|
746
|
-
`compared : ${report.comparedFrames} frames x ${report.jointCount} joints` +
|
|
747
|
-
(report.resampled ? " (candidate RESAMPLED to base timeline, nearest frame)" : "") +
|
|
748
|
-
(report.truncated ? " (TRUNCATED to the candidate's length)" : "")
|
|
749
|
-
);
|
|
750
|
-
for (const warning of report.warnings) console.log(`WARN: ${warning}`);
|
|
751
|
-
|
|
752
|
-
console.log("");
|
|
753
|
-
console.log(" position error (root-relative, metres)");
|
|
754
|
-
console.log(` all : mean ${formatMetres(report.l2p_m.mean)} max ${formatMetres(report.l2p_m.max)}`);
|
|
755
|
-
if (report.l2p_inRange_m) {
|
|
756
|
-
console.log(` inRange : mean ${formatMetres(report.l2p_inRange_m.mean)} max ${formatMetres(report.l2p_inRange_m.max)} (${report.l2p_inRange_m.samples} samples)`);
|
|
757
|
-
console.log(` outOfRange : mean ${formatMetres(report.l2p_outOfRange_m.mean)} max ${formatMetres(report.l2p_outOfRange_m.max)} (${report.l2p_outOfRange_m.samples} samples)`);
|
|
758
|
-
}
|
|
759
|
-
console.log(" position error (global / world, metres)");
|
|
760
|
-
console.log(` all : mean ${formatMetres(report.globalL2p_m.mean)} max ${formatMetres(report.globalL2p_m.max)}`);
|
|
761
|
-
if (report.globalL2p_inRange_m) {
|
|
762
|
-
console.log(` inRange : mean ${formatMetres(report.globalL2p_inRange_m.mean)}`);
|
|
763
|
-
console.log(` outOfRange : mean ${formatMetres(report.globalL2p_outOfRange_m.mean)}`);
|
|
764
|
-
}
|
|
765
|
-
console.log(" rotation error (geodesic, radians)");
|
|
766
|
-
console.log(` all : mean ${formatRadians(report.l2r_rad.mean)} max ${formatRadians(report.l2r_rad.max)}`);
|
|
767
|
-
if (report.l2r_inRange_rad) {
|
|
768
|
-
console.log(` inRange : mean ${formatRadians(report.l2r_inRange_rad.mean)}`);
|
|
769
|
-
console.log(` outOfRange : mean ${formatRadians(report.l2r_outOfRange_rad.mean)}`);
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
if (report.perRange.length) {
|
|
773
|
-
console.log(" per edited range");
|
|
774
|
-
for (const range of report.perRange) {
|
|
775
|
-
console.log(
|
|
776
|
-
` [${range.startFrame},${range.endFrame}) ${String(range.comparedFrames).padStart(4)} frames ` +
|
|
777
|
-
`L2P ${formatMetres(range.l2p_m)} L2R ${formatRadians(range.l2r_rad)}`
|
|
778
|
-
);
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
if (perJoint) printPerJoint(report);
|
|
783
|
-
if (groups) printGroups(report);
|
|
784
|
-
|
|
785
|
-
const feet = report.footSliding;
|
|
786
|
-
console.log(" foot sliding (m per frame, contact = within 5 cm of that joint's own minimum)");
|
|
787
|
-
console.log(` base : ${feet.base.slide_m_per_frame.toFixed(6)} (${feet.base.contactFrames} contact frames)`);
|
|
788
|
-
console.log(` candidate : ${feet.candidate.slide_m_per_frame.toFixed(6)} (${feet.candidate.contactFrames} contact frames)`);
|
|
789
|
-
console.log(` delta : ${feet.delta_m_per_frame >= 0 ? "+" : ""}${feet.delta_m_per_frame.toFixed(6)}`);
|
|
790
|
-
|
|
791
|
-
console.log("");
|
|
792
|
-
// Flat, greppable keys, same shape as measure-waypoints.mjs's WORST_ERROR_M,
|
|
793
|
-
// so a gate script can pull one number without parsing the whole report.
|
|
794
|
-
console.log(`L2P_M=${report.l2p_m.mean === null ? "nan" : report.l2p_m.mean.toFixed(6)}`);
|
|
795
|
-
console.log(`GLOBAL_L2P_M=${report.globalL2p_m.mean === null ? "nan" : report.globalL2p_m.mean.toFixed(6)}`);
|
|
796
|
-
console.log(`L2R_RAD=${report.l2r_rad.mean === null ? "nan" : report.l2r_rad.mean.toFixed(6)}`);
|
|
797
|
-
if (report.l2p_outOfRange_m) {
|
|
798
|
-
console.log(`L2P_OUT_M=${report.l2p_outOfRange_m.mean === null ? "nan" : report.l2p_outOfRange_m.mean.toFixed(6)}`);
|
|
799
|
-
console.log(`L2P_IN_M=${report.l2p_inRange_m.mean === null ? "nan" : report.l2p_inRange_m.mean.toFixed(6)}`);
|
|
800
|
-
}
|
|
801
|
-
if (groups) {
|
|
802
|
-
// One flat key per group so a gate script can assert "arm moved, legs did
|
|
803
|
-
// not" without parsing the table. The all-frames key is always emitted and
|
|
804
|
-
// the range-scoped ones only when ranges were given, so a key never
|
|
805
|
-
// silently changes meaning between two invocations.
|
|
806
|
-
const number = (value) => (value === null ? "nan" : value.toFixed(6));
|
|
807
|
-
for (const row of report.perGroup) {
|
|
808
|
-
const name = row.group.toUpperCase();
|
|
809
|
-
console.log(`L2P_GROUP_${name}=${number(row.l2p_m)}`);
|
|
810
|
-
if (report.ranges.length) {
|
|
811
|
-
console.log(`L2P_GROUP_${name}_IN=${number(row.l2p_inRange_m)}`);
|
|
812
|
-
console.log(`L2P_GROUP_${name}_OUT=${number(row.l2p_outOfRange_m)}`);
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
console.log(`FOOT_SLIDE_DELTA_M=${feet.delta_m_per_frame.toFixed(6)}`);
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
function parseArgs(argv) {
|
|
820
|
-
const positional = [];
|
|
821
|
-
const options = {
|
|
822
|
-
ranges: [], json: false, perJoint: false, groups: false,
|
|
823
|
-
maxL2p: null, maxL2r: null, maxFootDelta: null,
|
|
824
|
-
};
|
|
825
|
-
for (let index = 0; index < argv.length; index += 1) {
|
|
826
|
-
const arg = argv[index];
|
|
827
|
-
if (arg === "--json") options.json = true;
|
|
828
|
-
else if (arg === "--per-joint") options.perJoint = true;
|
|
829
|
-
else if (arg === "--groups") options.groups = true;
|
|
830
|
-
else if (arg === "--ranges") options.ranges = parseRanges(argv[++index] ?? "");
|
|
831
|
-
else if (arg.startsWith("--ranges=")) options.ranges = parseRanges(arg.slice("--ranges=".length));
|
|
832
|
-
else if (arg === "--max-l2p") options.maxL2p = Number(argv[++index]);
|
|
833
|
-
else if (arg === "--max-l2r") options.maxL2r = Number(argv[++index]);
|
|
834
|
-
else if (arg === "--max-foot-delta") options.maxFootDelta = Number(argv[++index]);
|
|
835
|
-
else if (arg.startsWith("--")) throw new Error(`measure-preserve: unknown flag ${arg}`);
|
|
836
|
-
else positional.push(arg);
|
|
837
|
-
}
|
|
838
|
-
for (const [flag, value] of [["--max-l2p", options.maxL2p], ["--max-l2r", options.maxL2r], ["--max-foot-delta", options.maxFootDelta]]) {
|
|
839
|
-
if (value !== null && !Number.isFinite(value)) {
|
|
840
|
-
throw new Error(`measure-preserve: ${flag} needs a finite number`);
|
|
841
|
-
}
|
|
842
|
-
}
|
|
843
|
-
return { positional, options };
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
const invokedDirectly = process.argv[1] && process.argv[1].endsWith("measure-preserve.mjs");
|
|
847
|
-
if (invokedDirectly) {
|
|
848
|
-
let parsed;
|
|
849
|
-
try {
|
|
850
|
-
parsed = parseArgs(process.argv.slice(2));
|
|
851
|
-
} catch (error) {
|
|
852
|
-
console.error(error.message);
|
|
853
|
-
process.exit(2);
|
|
854
|
-
}
|
|
855
|
-
const [basePath, candidatePath] = parsed.positional;
|
|
856
|
-
if (!basePath || !candidatePath) {
|
|
857
|
-
console.error(
|
|
858
|
-
"usage: measure-preserve.mjs <base.npz> <candidate.npz> [--ranges 40:80,120:150] [--json]\n" +
|
|
859
|
-
" [--per-joint] [--groups]\n" +
|
|
860
|
-
" [--max-l2p M] [--max-l2r RAD] [--max-foot-delta M]"
|
|
861
|
-
);
|
|
862
|
-
process.exit(2);
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
let report;
|
|
866
|
-
try {
|
|
867
|
-
const base = loadMotion(basePath);
|
|
868
|
-
// The candidate inherits the base's fps when it carries none: the two are
|
|
869
|
-
// the same take before and after regeneration, so silently assuming a
|
|
870
|
-
// different default would fake a resample.
|
|
871
|
-
const candidate = loadMotion(candidatePath, { defaultFps: base.fps });
|
|
872
|
-
report = measurePreserve(base, candidate, parsed.options.ranges);
|
|
873
|
-
} catch (error) {
|
|
874
|
-
console.error(error.message);
|
|
875
|
-
process.exit(2);
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
// Gates are evaluated before printing so the failures can ride along in the
|
|
879
|
-
// JSON, which is the form the acceptance harness consumes.
|
|
880
|
-
const failures = [];
|
|
881
|
-
const { maxL2p, maxL2r, maxFootDelta } = parsed.options;
|
|
882
|
-
if (maxL2p !== null && !(report.l2p_m.mean <= maxL2p)) {
|
|
883
|
-
failures.push(`L2P ${report.l2p_m.mean} > --max-l2p ${maxL2p}`);
|
|
884
|
-
}
|
|
885
|
-
if (maxL2r !== null && !(report.l2r_rad.mean <= maxL2r)) {
|
|
886
|
-
failures.push(`L2R ${report.l2r_rad.mean} > --max-l2r ${maxL2r}`);
|
|
887
|
-
}
|
|
888
|
-
if (maxFootDelta !== null && !(report.footSliding.delta_m_per_frame <= maxFootDelta)) {
|
|
889
|
-
failures.push(`foot slide delta ${report.footSliding.delta_m_per_frame} > --max-foot-delta ${maxFootDelta}`);
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
if (parsed.options.json) {
|
|
893
|
-
console.log(JSON.stringify({ ...report, failures }, null, 2));
|
|
894
|
-
} else {
|
|
895
|
-
printReport(report, { perJoint: parsed.options.perJoint, groups: parsed.options.groups });
|
|
896
|
-
for (const failure of failures) console.error(`FAIL: ${failure}`);
|
|
897
|
-
}
|
|
898
|
-
process.exit(failures.length ? 1 : 0);
|
|
899
|
-
}
|