cozyclay 1.9.0 → 2.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 +171 -0
- package/bin/agent/agent-routes.mjs +74 -70
- package/bin/agent/agent-runner.mjs +10 -3
- package/bin/agent/motion-runtime.mjs +32 -260
- package/bin/agent/providers.mjs +46 -3
- package/bin/agent/session-store.mjs +41 -5
- package/bin/agent/studio-prompt.mjs +1 -1
- package/bin/agent/studio-tools.mjs +48 -11
- package/bin/cozyclay.mjs +26 -8
- package/bin/live/cli.mjs +22 -3
- package/bin/mcp-runtime.mjs +35 -20
- package/bin/telemetry-state.mjs +5 -2
- package/bin/update-check.mjs +15 -0
- package/dist/app/index.html +6 -6
- package/dist/assets/analytics-MVZOEW8O.js +1 -0
- package/dist/assets/app-C1sdVbg4.css +1 -0
- package/dist/assets/app-CA55tcI3.js +4876 -0
- package/dist/assets/{demo-B-eg1CT8.js → demo-ByeYp-dY.js} +1 -1
- package/dist/assets/{first-shot-handoff-CGgnvNu8.js → first-shot-handoff-DgT28ZiV.js} +1 -1
- package/dist/assets/{landing-IbRp3FnQ.js → landing-BaXkpFip.js} +1 -1
- package/dist/assets/shot-prompt-CbqWcWYi.css +1 -0
- package/dist/assets/shot-prompt-Yrt99wZx.js +17 -0
- package/dist/assets/{ticket-BgIzOBOf.js → ticket-BcmsnHn5.js} +1 -1
- package/dist/assets/workflow-DRdwoDBT.css +1 -0
- package/dist/assets/workflow-dNPGY9xz.js +179 -0
- package/dist/cozyclay-package.json +1 -1
- package/dist/fonts/IBM-Plex-Sans-OFL.txt +92 -0
- package/dist/fonts/JetBrains-Mono-OFL.txt +93 -0
- package/dist/fonts/README.md +13 -9
- package/dist/fonts/ibm-plex-sans-400-latin.woff2 +0 -0
- package/dist/fonts/ibm-plex-sans-500-latin.woff2 +0 -0
- package/dist/fonts/ibm-plex-sans-600-latin.woff2 +0 -0
- package/dist/fonts/jetbrains-mono-400-latin.woff2 +0 -0
- package/dist/fonts/jetbrains-mono-500-latin.woff2 +0 -0
- package/dist/index.html +7 -7
- package/dist/privacy/index.html +13 -8
- package/dist/sitemap.xml +6 -6
- package/dist/workflow/index.html +5 -5
- package/mcp/LIVE-PROTOCOL.md +1 -1
- package/mcp/live-hub.mjs +35 -93
- package/mcp/mesh-file.mjs +48 -0
- package/mcp/server.mjs +6 -77
- package/mcp/tool-handlers.mjs +224 -377
- package/package.json +2 -1
- package/src/App.jsx +1273 -9075
- package/src/analytics.js +393 -9
- package/src/app-context.js +211 -0
- package/src/app-stage.jsx +25 -24
- package/src/ardy/auto-fix-panel.css +108 -0
- package/src/ardy/collision-blockers.js +9 -3
- package/src/ardy/fix-collisions.js +10 -66
- package/src/ardy/ground.js +4 -4
- package/src/ardy/ik-drag.js +101 -0
- package/src/ardy/ik-key-json.js +43 -0
- package/src/ardy/ik.js +122 -76
- package/src/ardy/physics-panel.css +2 -0
- package/src/ardy/physics-panel.jsx +17 -5
- package/src/ardy/platform-fit-panel.css +64 -0
- package/src/ardy/platform-fit-panel.jsx +40 -0
- package/src/ardy/platform-fit.js +326 -0
- package/src/ardy/playback.js +166 -20
- package/src/ardy/range-pin.js +299 -0
- package/src/ardy/timeline-coordinates.js +26 -0
- package/src/ardy/timeline.css +1199 -0
- package/src/ardy/timeline.jsx +149 -41
- package/src/ardy/waypoints.js +2 -2
- package/src/asset-pane.css +462 -0
- package/src/asset-pane.jsx +461 -232
- package/src/command-bus.js +349 -0
- package/src/commands/ai.js +31 -0
- package/src/commands/cast.js +140 -0
- package/src/commands/elements/character.js +13 -0
- package/src/commands/elements/motion.js +14 -0
- package/src/commands/elements/object.js +22 -0
- package/src/commands/elements/scene.js +16 -0
- package/src/commands/elements/shot.js +9 -0
- package/src/commands/elements/stage.js +11 -0
- package/src/commands/elements.js +155 -0
- package/src/commands/export.js +20 -0
- package/src/commands/index.js +40 -0
- package/src/commands/motion.js +231 -0
- package/src/commands/objects.js +174 -0
- package/src/commands/project.js +53 -0
- package/src/commands/scene.js +81 -0
- package/src/commands/shared.js +19 -0
- package/src/commands/shot.js +103 -0
- package/src/commands/stage.js +17 -0
- package/src/commands/view.js +42 -0
- package/src/document-store.js +144 -0
- package/src/domains/cast.js +1007 -0
- package/src/domains/motion.js +3219 -0
- package/src/domains/objects.js +932 -0
- package/src/domains/scenes.js +817 -0
- package/src/domains/shots.js +404 -0
- package/src/domains/stage.js +93 -0
- package/src/dualview.jsx +113 -57
- package/src/facing-marks.js +3 -0
- package/src/first-success-guide.jsx +14 -14
- package/src/grid-view.js +18 -5
- package/src/hierarchy-model.js +24 -16
- package/src/hierarchy-panel.css +386 -0
- package/src/hierarchy-panel.jsx +150 -21
- package/src/{fal-motion-client.js → i2v-motion-client.js} +13 -13
- package/src/i2v-motion-studio.jsx +180 -0
- package/src/ik-camera.js +3 -0
- package/src/main.jsx +6 -0
- package/src/motion/generation.js +105 -0
- package/src/motion-readiness-ui.jsx +4 -4
- package/src/motion-readiness.js +11 -0
- package/src/motion-trail.js +366 -9
- package/src/object-gizmo.jsx +7 -1
- package/src/otio.js +2 -1
- package/src/panels/CameraPanel.jsx +60 -0
- package/src/panels/CharacterTransformPanel.jsx +48 -0
- package/src/panels/EnvironmentPanel.jsx +40 -0
- package/src/panels/Foldout.jsx +32 -0
- package/src/panels/LightPanel.jsx +24 -0
- package/src/panels/ObjectTransformPanel.jsx +554 -0
- package/src/panels/PosePanel.jsx +97 -0
- package/src/panels/ProjectPanel.jsx +33 -0
- package/src/panels/PromptBlocksPanel.jsx +442 -0
- package/src/panels/PropsPanel.jsx +70 -0
- package/src/panels/ReferenceImageField.jsx +91 -0
- package/src/panels/RigControlPanel.jsx +78 -0
- package/src/panels/RigPanel.jsx +37 -0
- package/src/panels/SubjectBox.jsx +47 -0
- package/src/panels/SubjectsPanel.jsx +39 -0
- package/src/panels/VideoCapturePanel.jsx +180 -0
- package/src/panels/details.css +1171 -0
- package/src/panels/motion.css +197 -0
- package/src/panels/pose.css +39 -0
- package/src/planview.jsx +60 -31
- package/src/posestudio.jsx +59 -4
- package/src/project-browser.css +1073 -0
- package/src/project-browser.jsx +284 -123
- package/src/range-pin-object-transform.js +19 -0
- package/src/range-pin-panel.css +518 -0
- package/src/range-pin-panel.jsx +344 -0
- package/src/result-modal.jsx +5 -5
- package/src/room.jsx +79 -51
- package/src/scene-objects.js +37 -1
- package/src/scenes.js +5 -0
- package/src/semantic-edit.js +2 -0
- package/src/settings-menu.jsx +39 -105
- package/src/shell/BottomDock.jsx +450 -0
- package/src/shell/DetailsSlot.jsx +513 -0
- package/src/shell/LibrarySlot.jsx +86 -0
- package/src/shell/MenuBar.jsx +601 -0
- package/src/shell/OutlinerSlot.jsx +50 -0
- package/src/shell/PreferencesDialog.jsx +538 -0
- package/src/shell/PreferencesSlot.jsx +40 -0
- package/src/shell/StatusBar.jsx +125 -0
- package/src/shell/StudioShell.jsx +92 -0
- package/src/shell/TopBar.jsx +129 -0
- package/src/shell/ViewportToolbar.jsx +656 -0
- package/src/shell/agent-glass.css +296 -0
- package/src/shell/dock.css +93 -0
- package/src/shell/glass-regions.css +985 -0
- package/src/shell/glass.css +687 -0
- package/src/shell/log-store.js +52 -0
- package/src/shell/mode.css +9 -0
- package/src/shell/preferences.css +651 -0
- package/src/shell/shell.css +282 -0
- package/src/shell/studio-shell-context.js +19 -0
- package/src/shell/topbar.css +435 -0
- package/src/shell/viewport.css +451 -0
- package/src/store/authored-intent.js +22 -0
- package/src/store/runtime-adapters.js +92 -0
- package/src/store/scene-stage.js +21 -0
- package/src/store/use-document-store.js +14 -0
- package/src/studio-actions.js +210 -0
- package/src/studio-agent-commands.js +41 -271
- package/src/studio-agent-context.js +39 -10
- package/src/studio-agent-motion.js +122 -373
- package/src/studio-agent-protocol.js +67 -31
- package/src/studio-app-binding.js +379 -0
- package/src/studio-contact-sheet.js +73 -0
- package/src/studio-elements.js +58 -51
- package/src/styles/themes.css +200 -0
- package/src/styles/tokens.css +69 -0
- package/src/styles.css +460 -1742
- package/src/theme.js +44 -0
- package/src/timeline-extent.js +16 -0
- package/src/trail-key-conflicts.js +39 -0
- package/src/trail-pick.js +59 -0
- package/src/ui.jsx +8 -8
- package/src/use-case-question.jsx +66 -0
- package/src/workflow/AgentPanel.jsx +82 -69
- package/src/workflow/agent-client.js +12 -2
- package/src/workflow/agent-panel.css +34 -32
- package/src/workflow/cozy-scene-node.css +2 -0
- package/src/workflow/workflow.css +2 -0
- package/tools/ardy/__pycache__/cclay_gvhmr_worker.cpython-313.pyc +0 -0
- package/tools/ardy/bridge.mjs +164 -146
- package/tools/ardy/visual-qa.mjs +5 -5
- package/tools/bench/EXP3.md +61 -0
- package/tools/bench/cclay_bench_extract_incam.py +125 -0
- package/tools/bench/cclay_bench_extract_obs.py +204 -0
- package/tools/bench/cclay_bench_runner.py +42 -0
- package/tools/bench/cube-contact.mjs +162 -0
- package/tools/bench/exp3.mjs +152 -0
- package/tools/bench/extract-bench-lib.mjs +216 -0
- package/tools/bench/extract-bench.mjs +302 -0
- package/tools/bench/fal-generate.mjs +88 -0
- package/tools/bench/fit/README.md +189 -0
- package/tools/bench/fit/camera.mjs +59 -0
- package/tools/bench/fit/contact.mjs +419 -0
- package/tools/bench/fit/footlock.mjs +234 -0
- package/tools/bench/fit/motion.mjs +85 -0
- package/tools/bench/fit/pin.mjs +27 -0
- package/tools/bench/fit/remote.mjs +105 -0
- package/tools/bench/fit-bench.mjs +126 -0
- package/tools/bench/fit-sanity.mjs +45 -0
- package/tools/bench/metrics.mjs +313 -0
- package/tools/bench/obs/depth.mjs +258 -0
- package/tools/bench/obs/extrinsics.mjs +175 -0
- package/tools/bench/obs/fit_mannequin_betas.py +340 -0
- package/tools/bench/obs/ground.mjs +294 -0
- package/tools/bench/obs/heading.mjs +151 -0
- package/tools/bench/obs/ladder.mjs +555 -0
- package/tools/bench/obs/mannequin-betas.json +124 -0
- package/tools/bench/obs/remote.mjs +83 -0
- package/tools/bench/obs/rest_joints.py +52 -0
- package/tools/bench/obs/ybot-targets.mjs +49 -0
- package/tools/bench/obs-bench.mjs +520 -0
- package/tools/bench/score.mjs +343 -0
- package/tools/bench/summarize.mjs +86 -0
- package/tools/dev/pages/privacy.html +13 -8
- package/tools/gt-render/browser.mjs +159 -0
- package/tools/gt-render/camera-math.mjs +276 -0
- package/tools/gt-render/page.mjs +267 -0
- package/tools/gt-render/render.mjs +514 -0
- package/tools/gt-render/scene-box.mjs +31 -0
- package/tools/gt-render/take-transform.mjs +87 -0
- package/tools/morphgs/assets/gen_truth.py +39 -0
- package/tools/morphgs/assets/mesh_ori_rig.txt +27 -0
- package/tools/morphgs/assets/playback-check.mjs +30 -0
- package/tools/morphgs/demo-gate.sh +26 -0
- package/tools/morphgs/fbx2morphgs.mjs +68 -0
- package/tools/morphgs/morphgs-to-cskel27.mjs +105 -0
- package/tools/morphgs/patches/preprocess_src-none-mode.patch +76 -0
- package/tools/morphgs/setup-on-cluster.sh +126 -0
- package/tools/qa/css-rule-usage.mjs +283 -0
- package/tools/qa/studio-control-count.mjs +70 -32
- package/tools/run-tests.mjs +134 -12
- package/tools/track/DESIGN.md +513 -0
- package/tools/track/backfill-provenance.mjs +116 -0
- package/tools/track/budget.mjs +8 -0
- package/tools/track/check-rig.mjs +209 -0
- package/tools/track/diagnostics.schema.json +60 -0
- package/tools/track/export-rig.mjs +256 -0
- package/tools/track/fallback.mjs +7 -0
- package/tools/track/fk-parity-fixture.mjs +161 -0
- package/tools/track/gate.mjs +349 -0
- package/tools/track/masks.mjs +318 -0
- package/tools/track/metrics.mjs +201 -0
- package/tools/track/publish-obs.mjs +108 -0
- package/tools/track/py/check_env.py +61 -0
- package/tools/track/py/eval_lr.py +229 -0
- package/tools/track/py/lr_viterbi.py +294 -0
- package/tools/track/py/masks.py +405 -0
- package/tools/track/py/objective.py +382 -0
- package/tools/track/py/rig.py +508 -0
- package/tools/track/py/scene.py +326 -0
- package/tools/track/py/test_joint_indices.py +123 -0
- package/tools/track/py/test_lr_viterbi.py +195 -0
- package/tools/track/py/test_masks.py +229 -0
- package/tools/track/py/test_rig.py +288 -0
- package/tools/track/py/test_scene.py +354 -0
- package/tools/track/py/test_track.py +328 -0
- package/tools/track/py/track.py +411 -0
- package/tools/track/remote.mjs +140 -0
- package/tools/track/rig-dump.mjs +227 -0
- package/tools/track/run-box-tests.mjs +14 -0
- package/tools/track/run-box.mjs +168 -0
- package/tools/track/setup-box.sh +59 -0
- package/tools/track/study-2d.mjs +728 -0
- package/dist/assets/analytics-B1hnH66c.js +0 -1
- package/dist/assets/app-BWusbqgO.js +0 -4861
- package/dist/assets/app-qKDo4PBX.css +0 -1
- package/dist/assets/shot-prompt-CHUtw6af.js +0 -17
- package/dist/assets/shot-prompt-C_g2BHVc.css +0 -1
- package/dist/assets/workflow-CVwzMhz2.js +0 -179
- package/dist/assets/workflow-DsudxKHF.css +0 -1
- package/src/fal-motion-studio.jsx +0 -180
- package/src/scene-history.js +0 -129
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
"""Box acceptance for the real track.py entry point, not a mocked optimizer.
|
|
2
|
+
|
|
3
|
+
Synthetic images are independently filled mesh triangles, not the objective's
|
|
4
|
+
point samples. A known, smoothly articulated cskel27 clip is perturbed in 3-D;
|
|
5
|
+
noisy 2-D joints contain independent 1/3/7-frame assignment errors. Acceptance
|
|
6
|
+
numbers are printed at process exit so `run-box-tests.mjs` retains them.
|
|
7
|
+
"""
|
|
8
|
+
import atexit
|
|
9
|
+
import json
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
|
|
14
|
+
import cv2
|
|
15
|
+
import numpy as np
|
|
16
|
+
import pytest
|
|
17
|
+
import torch
|
|
18
|
+
from scipy.spatial.transform import Rotation
|
|
19
|
+
|
|
20
|
+
from lr_viterbi import state_permutation
|
|
21
|
+
from objective import COCO_JOINTS, bilinear_dt, camera_tensors, project, trust_region_loss
|
|
22
|
+
from rig import State, cskel27_fk, load_rig, studio_skin
|
|
23
|
+
from track import windows
|
|
24
|
+
|
|
25
|
+
HERE = Path(__file__).resolve().parent
|
|
26
|
+
DATA = Path.home() / 'cclay-ingest/cozyfit/testdata'
|
|
27
|
+
MEASUREMENTS = {}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@atexit.register
|
|
31
|
+
def report():
|
|
32
|
+
if MEASUREMENTS:
|
|
33
|
+
print('[track-acceptance] ' + json.dumps(MEASUREMENTS, sort_keys=True))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def synthetic(directory, frames, fixed=False, seed=100):
|
|
37
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
38
|
+
torch.set_num_threads(4)
|
|
39
|
+
rig_path = DATA / 'rig-y-bot-tpose.npz'
|
|
40
|
+
rig = load_rig(rig_path)
|
|
41
|
+
fps = 30
|
|
42
|
+
t = np.arange(frames, dtype=np.float32) / fps
|
|
43
|
+
phase = t * 2 * np.pi / 3
|
|
44
|
+
aa = np.zeros((frames, 27, 3), np.float32)
|
|
45
|
+
aa[:, 0, 1] = 0.1 * np.sin(phase)
|
|
46
|
+
aa[:, 9, 2] = 0.7 + 0.18 * np.sin(phase)
|
|
47
|
+
aa[:, 14, 2] = -0.7 + 0.18 * np.cos(phase)
|
|
48
|
+
aa[:, 10, 1] = 0.4 + 0.15 * np.sin(phase)
|
|
49
|
+
aa[:, 15, 1] = -0.4 - 0.15 * np.cos(phase)
|
|
50
|
+
# Bounded periodic translation, no long-clip field-of-view difference.
|
|
51
|
+
root = np.stack((0.45 * np.sin(t * 1.4), 1.05 + 0.015 * np.sin(phase), 0.1 * np.cos(t * 1.4)), -1)
|
|
52
|
+
rotations = Rotation.from_rotvec(aa.reshape(-1, 3)).as_matrix().reshape(frames, 27, 3, 3).astype(np.float32)
|
|
53
|
+
state = State.from_motion(rotations, root)
|
|
54
|
+
with torch.no_grad():
|
|
55
|
+
joints, globals_ = cskel27_fk(state, None, rig)
|
|
56
|
+
_, vertices = studio_skin(globals_, joints, rig)
|
|
57
|
+
width, height = 832, 480
|
|
58
|
+
eye = np.array([2.8, 1.6, 6.0])
|
|
59
|
+
forward = (np.array([0, 1, 0]) - eye)
|
|
60
|
+
forward /= np.linalg.norm(forward)
|
|
61
|
+
right = np.cross(forward, [0, 1, 0]); right /= np.linalg.norm(right)
|
|
62
|
+
down = np.cross(forward, right)
|
|
63
|
+
R = np.stack((right, down, forward))
|
|
64
|
+
w = np.eye(4); w[:3, :3] = R; w[:3, 3] = -R @ eye
|
|
65
|
+
camera = dict(width=width, height=height, K=[[700, 0, width / 2], [0, 700, height / 2], [0, 0, 1]], worldToCamera=w.tolist(), tracker=dict(deltaPx=3.0, cameraFixed=fixed))
|
|
66
|
+
cam = camera_tensors(camera, 'cpu')
|
|
67
|
+
zero = torch.zeros(4)
|
|
68
|
+
uv, _ = project(joints[:, COCO_JOINTS], cam, zero)
|
|
69
|
+
pixel, _ = project(vertices, cam, zero)
|
|
70
|
+
pixel = pixel.numpy() / 2
|
|
71
|
+
faces = rig.dec_faces.numpy()
|
|
72
|
+
masks = np.zeros((frames, height // 2, width // 2), np.float16)
|
|
73
|
+
for i in range(frames):
|
|
74
|
+
mask = np.zeros((height // 2, width // 2), np.uint8)
|
|
75
|
+
# CPU triangle fill is only a fixture generator, never imported by track.
|
|
76
|
+
for triangle in np.rint(pixel[i][faces]).astype(np.int32):
|
|
77
|
+
cv2.fillConvexPoly(mask, triangle, 1)
|
|
78
|
+
masks[i] = mask
|
|
79
|
+
rng = np.random.default_rng(seed)
|
|
80
|
+
kp = np.concatenate((uv.numpy() + rng.normal(0, 0.25, uv.shape), np.ones((frames, 17, 1))), -1).astype(np.float32)
|
|
81
|
+
kp[:, :5, 2] = 0
|
|
82
|
+
expected = np.array(['identity'] * frames, dtype=object)
|
|
83
|
+
for start, size, name in [(18, 1, 'full_swap'), (36, 3, 'arms_swap'), (65, 7, 'legs_swap')]:
|
|
84
|
+
if start + size <= frames:
|
|
85
|
+
kp[start:start + size] = kp[start:start + size, state_permutation(name)]
|
|
86
|
+
expected[start:start + size] = name
|
|
87
|
+
initial_root = root + np.array([0.045, 0.025, 0.07], np.float32)
|
|
88
|
+
initial_aa = aa + rng.normal(0, 0.018, aa.shape).astype(np.float32)
|
|
89
|
+
initial_rot = Rotation.from_rotvec(initial_aa.reshape(-1, 3)).as_matrix().reshape(frames, 27, 3, 3).astype(np.float32)
|
|
90
|
+
initial_joints, _ = cskel27_fk(State.from_motion(initial_rot, initial_root), None, rig)
|
|
91
|
+
initial_mpjpe = float(torch.linalg.norm(initial_joints - joints, dim=-1).mean())
|
|
92
|
+
assert initial_mpjpe > 0.05, initial_mpjpe
|
|
93
|
+
np.savez(directory / 'init.npz', local_rot_mats=initial_rot, root_positions=initial_root, posed_joints=initial_joints.numpy(), fps=np.array(fps, np.int32))
|
|
94
|
+
np.savez(directory / 'obs.npz', kp2d=kp, K=np.array(camera['K']), fps=np.array(fps), bbx_xys=np.tile([width / 2, height / 2, height * 0.8], (frames, 1)))
|
|
95
|
+
np.savez_compressed(directory / 'masks.npz', prob=masks, reprompted=np.array([], dtype=np.int32))
|
|
96
|
+
(directory / 'camera.json').write_text(json.dumps(camera))
|
|
97
|
+
(directory / 'scene.json').write_text('{"boxes": []}')
|
|
98
|
+
# A genuine video input keeps the command boundary realistic; fitting reads
|
|
99
|
+
# only its cached observations and masks, not the encoded image samples.
|
|
100
|
+
writer = cv2.VideoWriter(str(directory / 'video.mp4'), cv2.VideoWriter_fourcc(*'mp4v'), fps, (width, height))
|
|
101
|
+
assert writer.isOpened()
|
|
102
|
+
for mask in masks:
|
|
103
|
+
frame = cv2.resize(mask.astype(np.uint8) * 180, (width, height))
|
|
104
|
+
writer.write(np.repeat(frame[..., None], 3, axis=2))
|
|
105
|
+
writer.release()
|
|
106
|
+
return dict(directory=directory, joints=joints.numpy(), root=root, expected=expected, initialMPJPE=initial_mpjpe, rig=rig_path)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def assert_schema(value, schema):
|
|
110
|
+
"""Validate the vocabulary used by the shipped dependency-free schema."""
|
|
111
|
+
kinds = dict(object=dict, array=list, integer=int, number=(int, float), string=str, boolean=bool, null=type(None))
|
|
112
|
+
names = schema.get('type', [])
|
|
113
|
+
names = names if isinstance(names, list) else [names]
|
|
114
|
+
assert not names or any(isinstance(value, kinds[name]) and (name not in ('integer', 'number') or not isinstance(value, bool)) for name in names)
|
|
115
|
+
if 'const' in schema:
|
|
116
|
+
assert value == schema['const']
|
|
117
|
+
if isinstance(value, (int, float)) and not isinstance(value, bool):
|
|
118
|
+
assert np.isfinite(value)
|
|
119
|
+
if 'minimum' in schema:
|
|
120
|
+
assert value >= schema['minimum']
|
|
121
|
+
if 'exclusiveMinimum' in schema:
|
|
122
|
+
assert value > schema['exclusiveMinimum']
|
|
123
|
+
if isinstance(value, dict):
|
|
124
|
+
assert set(schema.get('required', [])) <= value.keys()
|
|
125
|
+
properties = schema.get('properties', {})
|
|
126
|
+
extra = schema.get('additionalProperties', True)
|
|
127
|
+
for key, child in value.items():
|
|
128
|
+
assert key in properties or extra is not False
|
|
129
|
+
rule = properties.get(key, extra)
|
|
130
|
+
if isinstance(rule, dict):
|
|
131
|
+
assert_schema(child, rule)
|
|
132
|
+
if isinstance(value, list):
|
|
133
|
+
assert len(value) >= schema.get('minItems', 0)
|
|
134
|
+
assert len(value) <= schema.get('maxItems', float('inf'))
|
|
135
|
+
prefix = schema.get('prefixItems', [])
|
|
136
|
+
if schema.get('items') is False:
|
|
137
|
+
assert len(value) <= len(prefix)
|
|
138
|
+
for i, child in enumerate(value):
|
|
139
|
+
rule = prefix[i] if i < len(prefix) else schema.get('items', {})
|
|
140
|
+
if isinstance(rule, dict):
|
|
141
|
+
assert_schema(child, rule)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def invoke(case, out, *extra, timeout=220, expected_code=0):
|
|
145
|
+
d = case['directory']
|
|
146
|
+
command = [sys.executable, str(HERE / 'track.py')]
|
|
147
|
+
for key, file in [('video', 'video.mp4'), ('obs', 'obs.npz'), ('masks', 'masks.npz'), ('init', 'init.npz'), ('camera', 'camera.json'), ('scene', 'scene.json')]:
|
|
148
|
+
command.extend(['--' + key, str(d / file)])
|
|
149
|
+
command.extend(['--rig', str(case['rig']), '--out', str(out), *extra])
|
|
150
|
+
result = subprocess.run(command, text=True, capture_output=True, timeout=timeout)
|
|
151
|
+
assert result.returncode == expected_code, result.stdout + '\n' + result.stderr
|
|
152
|
+
diagnostics = json.loads((out / 'diagnostics.json').read_text())
|
|
153
|
+
# The authoritative schema is staged beside the rig for box tests.
|
|
154
|
+
assert_schema(diagnostics, json.loads((DATA / 'diagnostics.schema.json').read_text()))
|
|
155
|
+
for key in ['occluded', 'lrState', 'lrMargin', 'ambiguous']:
|
|
156
|
+
assert len(diagnostics[key]) == diagnostics['frames']
|
|
157
|
+
assert diagnostics['runtime']['peakReservedMiB'] <= 5632
|
|
158
|
+
if expected_code:
|
|
159
|
+
assert not (out / 'motion.npz').exists()
|
|
160
|
+
return diagnostics, None, result
|
|
161
|
+
assert '[track] ' in result.stdout
|
|
162
|
+
with np.load(out / 'motion.npz') as z:
|
|
163
|
+
motion = {k: z[k].copy() for k in z.files}
|
|
164
|
+
return diagnostics, motion, result
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@pytest.fixture(scope='module')
|
|
168
|
+
def small_case(tmp_path_factory):
|
|
169
|
+
return synthetic(tmp_path_factory.mktemp('synthetic124'), 124)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def test_synthetic_recovery_determinism_and_runtime(small_case, tmp_path):
|
|
173
|
+
first, motion, result = invoke(small_case, tmp_path / 'first')
|
|
174
|
+
second, repeat, _ = invoke(small_case, tmp_path / 'second')
|
|
175
|
+
mpjpe = float(np.linalg.norm(motion['posed_joints'] - small_case['joints'], axis=-1).mean())
|
|
176
|
+
root = float(np.sqrt(np.mean(np.sum((motion['root_positions'] - small_case['root']) ** 2, axis=-1))))
|
|
177
|
+
difference = float(np.max(np.abs(motion['posed_joints'] - repeat['posed_joints'])))
|
|
178
|
+
MEASUREMENTS['124'] = dict(mpjpeM=mpjpe, rootRmseM=root, scale=first['nuisance']['scale'], determinismMaxM=difference,
|
|
179
|
+
initialMPJPE=small_case['initialMPJPE'], runtime=first['runtime'], repeatRuntime=second['runtime'],
|
|
180
|
+
maskIoU=first['stageLosses']['sampleMaskIoUMean'], lrStates=first['lrState'])
|
|
181
|
+
assert first['failure'] is None
|
|
182
|
+
assert first['stageLosses']['ablation.full'] == 1.0
|
|
183
|
+
assert first['stageLosses']['input.confidenceAboveOne'] == 0
|
|
184
|
+
assert mpjpe <= 0.020 and root <= 0.020, MEASUREMENTS['124']
|
|
185
|
+
assert abs(first['nuisance']['scale'] - 1) < 0.005
|
|
186
|
+
assert difference <= 1e-4
|
|
187
|
+
assert first['runtime']['trackerSeconds'] <= 180 and second['runtime']['trackerSeconds'] <= 180
|
|
188
|
+
assert first['lrState'] == small_case['expected'].tolist()
|
|
189
|
+
for stage, steps in [('root', 150), ('pose', 300), ('refine', 200)]:
|
|
190
|
+
assert f'[track] stage {stage} {steps}/{steps}' in result.stdout
|
|
191
|
+
|
|
192
|
+
seed_results = {}
|
|
193
|
+
for seed in (101, 102):
|
|
194
|
+
seeded = synthetic(tmp_path / f'seed-{seed}', 124, seed=seed)
|
|
195
|
+
seeded_diag, seeded_motion, _ = invoke(seeded, tmp_path / f'seed-{seed}-out')
|
|
196
|
+
seeded_mpjpe = float(np.linalg.norm(seeded_motion['posed_joints'] - seeded['joints'], axis=-1).mean())
|
|
197
|
+
seeded_root = float(np.sqrt(np.mean(np.sum((seeded_motion['root_positions'] - seeded['root']) ** 2, axis=-1))))
|
|
198
|
+
seed_results[str(seed)] = dict(mpjpeM=seeded_mpjpe, rootRmseM=seeded_root,
|
|
199
|
+
scale=seeded_diag['nuisance']['scale'], runtime=seeded_diag['runtime'])
|
|
200
|
+
MEASUREMENTS['seedRecovery'] = seed_results
|
|
201
|
+
assert seeded_diag['failure'] is None
|
|
202
|
+
assert seeded_mpjpe <= 0.020 and seeded_root <= 0.020, seed_results
|
|
203
|
+
assert abs(seeded_diag['nuisance']['scale'] - 1) < 0.005
|
|
204
|
+
assert seeded_diag['runtime']['trackerSeconds'] <= 180
|
|
205
|
+
assert seeded_diag['lrState'] == seeded['expected'].tolist()
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def test_362_frame_windows_runtime(tmp_path):
|
|
209
|
+
case = synthetic(tmp_path / 'input', 362, fixed=True)
|
|
210
|
+
diag, motion, _ = invoke(case, tmp_path / 'output', timeout=570)
|
|
211
|
+
mpjpe = float(np.linalg.norm(motion['posed_joints'] - case['joints'], axis=-1).mean())
|
|
212
|
+
root = float(np.sqrt(np.mean(np.sum((motion['root_positions'] - case['root']) ** 2, axis=-1))))
|
|
213
|
+
MEASUREMENTS['362'] = dict(runtime=diag['runtime'], scale=diag['nuisance']['scale'],
|
|
214
|
+
mpjpeM=mpjpe, rootRmseM=root)
|
|
215
|
+
assert diag['failure'] is None
|
|
216
|
+
assert mpjpe <= 0.020 and root <= 0.020, MEASUREMENTS['362']
|
|
217
|
+
assert diag['runtime']['trackerSeconds'] <= 540
|
|
218
|
+
assert diag['nuisance'] == dict(scale=1.0, cameraDeltaDeg=[0.0, 0.0], fovDeltaPct=0.0)
|
|
219
|
+
assert motion['posed_joints'].shape == (362, 27, 3)
|
|
220
|
+
assert np.max(np.linalg.norm(np.diff(motion['root_positions'], axis=0), axis=-1)) < 0.1
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def test_zero_iterations_exact_init(small_case, tmp_path):
|
|
224
|
+
diag, motion, _ = invoke(small_case, tmp_path, '--iterations', '0', timeout=60)
|
|
225
|
+
with np.load(small_case['directory'] / 'init.npz') as initial:
|
|
226
|
+
assert set(motion) == set(initial.files)
|
|
227
|
+
maximum = max(float(np.max(np.abs(motion[k] - initial[k]))) for k in motion)
|
|
228
|
+
MEASUREMENTS['zeroIterationMaxDiff'] = maximum
|
|
229
|
+
assert maximum < 1e-5 and diag['failure'] is None
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def own_obs(small_case, tmp_path):
|
|
233
|
+
"""Inputs identical to small_case except for a writable obs.npz."""
|
|
234
|
+
source = small_case['directory']
|
|
235
|
+
d = tmp_path / 'inputs'; d.mkdir()
|
|
236
|
+
for name in ['init.npz', 'camera.json', 'scene.json', 'masks.npz', 'video.mp4']:
|
|
237
|
+
(d / name).symlink_to(source / name)
|
|
238
|
+
with np.load(source / 'obs.npz') as z:
|
|
239
|
+
obs = {k: z[k].copy() for k in z.files}
|
|
240
|
+
return d, obs
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def test_confidence_sanitized_and_counted(small_case, tmp_path):
|
|
244
|
+
d, obs = own_obs(small_case, tmp_path)
|
|
245
|
+
# Real ViTPose heatmap peaks reach 1.0385 on the Gate-2 items.
|
|
246
|
+
obs['kp2d'][3, 6, 2] = 1.3
|
|
247
|
+
obs['kp2d'][4, 7, 2] = np.nan
|
|
248
|
+
obs['kp2d'][5, 8, 2] = np.inf
|
|
249
|
+
obs['kp2d'][6, 9, 2] = -0.2
|
|
250
|
+
np.savez(d / 'obs.npz', **obs)
|
|
251
|
+
diag, motion, result = invoke(dict(small_case, directory=d), tmp_path / 'out', '--iterations', '0', timeout=60)
|
|
252
|
+
counts = {k: v for k, v in diag['stageLosses'].items() if k.startswith('input.')}
|
|
253
|
+
MEASUREMENTS['confidenceSanitized'] = counts
|
|
254
|
+
assert counts == {'input.confidenceNonFinite': 2, 'input.confidenceAboveOne': 1, 'input.confidenceBelowZero': 1}
|
|
255
|
+
assert diag['failure'] is None and motion is not None
|
|
256
|
+
assert '[track] sanitized keypoint confidences' in result.stdout
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def test_ablation_switch(small_case, tmp_path):
|
|
260
|
+
diag, _, _ = invoke(small_case, tmp_path / 'kp-only', '--track-ablate', 'kp-only', '--iterations', '13', timeout=120)
|
|
261
|
+
losses = diag['stageLosses']
|
|
262
|
+
MEASUREMENTS['ablationKpOnly'] = dict(failure=diag['failure'], stageLosses=losses)
|
|
263
|
+
assert losses['ablation.kp-only'] == 1.0 and 'ablation.full' not in losses
|
|
264
|
+
assert 'refine.keypoints' in losses and 'refine.acceleration' in losses
|
|
265
|
+
assert not any(k.split('.')[-1] in ('silhouette', 'skate', 'penetration') for k in losses)
|
|
266
|
+
# Without Viterbi the detector labels are used as given, despite injected swaps.
|
|
267
|
+
assert set(diag['lrState']) == {'identity'} and not any(diag['ambiguous'])
|
|
268
|
+
command = [sys.executable, str(HERE / 'track.py'), '--track-ablate', 'everything']
|
|
269
|
+
for key in ('video', 'obs', 'masks', 'init', 'camera', 'scene', 'rig', 'out'):
|
|
270
|
+
command.extend(['--' + key, str(tmp_path)])
|
|
271
|
+
assert subprocess.run(command, capture_output=True, timeout=60).returncode == 2
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def test_no_evidence_and_malformed_input(small_case, tmp_path):
|
|
275
|
+
d, obs = own_obs(small_case, tmp_path)
|
|
276
|
+
obs['kp2d'][..., 2] = 0
|
|
277
|
+
np.savez(d / 'obs.npz', **obs)
|
|
278
|
+
case = dict(small_case, directory=d)
|
|
279
|
+
out = tmp_path / 'no-evidence'; out.mkdir()
|
|
280
|
+
(out / 'motion.npz').write_bytes(b'stale output')
|
|
281
|
+
diag, _, result = invoke(case, out, expected_code=3, timeout=30)
|
|
282
|
+
assert diag['failure'] == 'no-evidence' and result.returncode == 3
|
|
283
|
+
obs['kp2d'][0, 5, 0] = np.nan
|
|
284
|
+
np.savez(d / 'obs.npz', **obs)
|
|
285
|
+
diag, _, _ = invoke(case, tmp_path / 'malformed', expected_code=2, timeout=30)
|
|
286
|
+
assert diag['failure'] == 'bad-input'
|
|
287
|
+
obs['kp2d'] = obs['kp2d'][..., :2]
|
|
288
|
+
np.savez(d / 'obs.npz', **obs)
|
|
289
|
+
diag, _, _ = invoke(case, tmp_path / 'bad-shape', expected_code=2, timeout=30)
|
|
290
|
+
assert diag['failure'] == 'bad-input'
|
|
291
|
+
MEASUREMENTS['failureCodes'] = {'no-evidence': 3, 'bad-input': 2}
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def test_trust_region_invariance_weighting_and_gradients():
|
|
295
|
+
initial = State.identity(5)
|
|
296
|
+
rotations = initial.local_rot_mats()
|
|
297
|
+
assert trust_region_loss(initial.transl, rotations, initial.transl, rotations).item() == 0
|
|
298
|
+
# Placement correction is free; a bent mid-clip trajectory is not.
|
|
299
|
+
shifted = initial.transl + torch.tensor([0.5, 0.0, -0.75])
|
|
300
|
+
assert trust_region_loss(shifted, rotations, initial.transl, rotations).item() == 0
|
|
301
|
+
shifted[2, 0] += 0.25
|
|
302
|
+
shifted.requires_grad_()
|
|
303
|
+
path = trust_region_loss(shifted, rotations, initial.transl, rotations)
|
|
304
|
+
assert 0 < path.item() < 2
|
|
305
|
+
path.backward()
|
|
306
|
+
assert torch.isfinite(shifted.grad).all() and shifted.grad[2, 0] > 0
|
|
307
|
+
turn = torch.tensor(Rotation.from_rotvec([0, 0.5, 0]).as_matrix(), dtype=torch.float32)
|
|
308
|
+
arm, leg = rotations.clone(), rotations.clone()
|
|
309
|
+
arm[:, 9], leg[:, 19] = turn, turn
|
|
310
|
+
arm.requires_grad_()
|
|
311
|
+
arm_loss = trust_region_loss(initial.transl, arm, initial.transl, rotations)
|
|
312
|
+
leg_loss = trust_region_loss(initial.transl, leg, initial.transl, rotations)
|
|
313
|
+
assert arm_loss.item() == pytest.approx(4 * leg_loss.item())
|
|
314
|
+
arm_loss.backward()
|
|
315
|
+
assert torch.isfinite(arm.grad).all() and arm.grad.abs().max() > 0
|
|
316
|
+
far = initial.transl.clone(); far[2] = 1e6
|
|
317
|
+
assert trust_region_loss(far, arm.detach(), initial.transl, rotations).item() < 4
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def test_windows_and_dt_gradients():
|
|
321
|
+
assert windows(240) == [(0, 240)]
|
|
322
|
+
assert windows(362) == [(0, 120), (104, 224), (208, 328), (312, 362)]
|
|
323
|
+
dt = torch.arange(20, dtype=torch.float32).reshape(1, 4, 5)
|
|
324
|
+
uv = torch.tensor([[[2.25, 1.75], [-2., 2.]]], requires_grad=True)
|
|
325
|
+
value = bilinear_dt(dt, uv)
|
|
326
|
+
assert value[0, 0].item() == pytest.approx(8.0)
|
|
327
|
+
value.sum().backward()
|
|
328
|
+
assert torch.isfinite(uv.grad).all() and uv.grad[0, 1, 0] < 0
|