cozyclay 2.1.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (583) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +12 -30
  3. package/bin/agent/agent-routes.mjs +40 -113
  4. package/bin/agent/agent-runner.mjs +9 -10
  5. package/bin/agent/cliproxy-provider.mjs +135 -0
  6. package/bin/agent/credential-store.mjs +30 -7
  7. package/bin/agent/pi-tools.mjs +4 -1
  8. package/bin/agent/provider-keys.mjs +32 -0
  9. package/bin/agent/provider-login.mjs +110 -0
  10. package/bin/agent/provider-routes.mjs +99 -0
  11. package/bin/agent/providers.mjs +19 -97
  12. package/bin/agent/script-tool.mjs +17 -4
  13. package/bin/agent/studio-motion-reference.mjs +23 -0
  14. package/bin/agent/studio-prompt.mjs +1 -1
  15. package/bin/agent/studio-rebase-text.mjs +32 -0
  16. package/bin/agent/studio-rebase.mjs +26 -39
  17. package/bin/agent/studio-tools.mjs +9 -28
  18. package/bin/agent/tool-arguments.mjs +49 -0
  19. package/bin/agent/turn-frames.mjs +66 -0
  20. package/bin/codex-auth.mjs +9 -2
  21. package/bin/cozyclay.mjs +10 -116
  22. package/bin/live/cli.mjs +22 -6
  23. package/bin/mcp-runtime.mjs +2 -12
  24. package/bin/package-signature.mjs +0 -4
  25. package/dist/app/index.html +7 -6
  26. package/dist/assets/EXRLoader-CvVIvGVE.js +1 -0
  27. package/dist/assets/analytics-C9sUnndd.js +1 -0
  28. package/dist/assets/app-CCh--1Hc.js +5087 -0
  29. package/dist/assets/app-f-fkV5uP.css +1 -0
  30. package/dist/assets/compile-worker-CkxiD6lH.js +5 -0
  31. package/dist/assets/fflate.module-KbGj5-H6.js +5 -0
  32. package/dist/assets/{first-shot-handoff-BlT33CGf.js → first-shot-handoff-CC4xe4HM.js} +1 -1
  33. package/dist/assets/{landing-BSesrCpL.js → landing-DjHD8u7k.js} +1 -1
  34. package/dist/assets/scene-asset-sync-Hx_FzdOH.js +20 -0
  35. package/dist/assets/scene-asset-sync-_S8NQl5B.css +1 -0
  36. package/dist/assets/workflow-C4yKH1p4.js +179 -0
  37. package/dist/cozyclay-package.json +1 -1
  38. package/dist/index.html +3 -3
  39. package/dist/llms.txt +14 -0
  40. package/dist/sw.js +3 -6
  41. package/dist/workflow/index.html +5 -4
  42. package/mcp/LIVE-PROTOCOL.md +7 -47
  43. package/mcp/README.md +16 -26
  44. package/mcp/live-hub.mjs +6 -10
  45. package/mcp/package.json +1 -3
  46. package/mcp/tool-handlers.mjs +4 -168
  47. package/package.json +3 -22
  48. package/src/App.jsx +174 -2639
  49. package/src/agent-progress.js +79 -0
  50. package/src/agent-turn-mark.js +30 -0
  51. package/src/analytics.js +1 -19
  52. package/src/app-context.js +5 -0
  53. package/src/app-stage.jsx +2 -34
  54. package/src/asset-pane.jsx +6 -5
  55. package/src/camera-block.js +21 -0
  56. package/src/camera-direction.js +120 -0
  57. package/src/character-kind.js +3 -9
  58. package/src/command-bus.js +8 -6
  59. package/src/commands/cast.js +5 -34
  60. package/src/commands/elements/character.js +1 -1
  61. package/src/commands/elements/motion.js +1 -6
  62. package/src/commands/elements/object.js +6 -2
  63. package/src/commands/elements/shot.js +4 -1
  64. package/src/commands/elements.js +38 -14
  65. package/src/commands/index.js +3 -1
  66. package/src/commands/kits.js +37 -0
  67. package/src/commands/motion-timing.js +57 -0
  68. package/src/commands/motion.js +9 -152
  69. package/src/commands/motion3-compile.js +65 -0
  70. package/src/commands/motion3.js +207 -0
  71. package/src/commands/objects.js +6 -4
  72. package/src/commands/shot.js +49 -11
  73. package/src/commands/stage.js +14 -1
  74. package/src/commands/view.js +4 -1
  75. package/src/document-mirrors.js +42 -0
  76. package/src/document-store.js +4 -0
  77. package/src/domains/cast.js +19 -194
  78. package/src/domains/motion/motion-range-pin-rules.js +1 -1
  79. package/src/domains/motion/take-source.js +51 -0
  80. package/src/domains/motion.js +60 -1838
  81. package/src/domains/scenes.js +12 -55
  82. package/src/domains/shots.js +2 -2
  83. package/src/domains/stage.js +4 -2
  84. package/src/draft-ghost.js +46 -0
  85. package/src/draft-ghost.jsx +129 -0
  86. package/src/dualview.jsx +4 -3
  87. package/src/effects/EffectsLayer.jsx +112 -0
  88. package/src/effects/effect-meshes.js +232 -0
  89. package/src/effects/effect-record.js +103 -0
  90. package/src/effects/emitter.js +59 -0
  91. package/src/effects/simulate.js +261 -0
  92. package/src/effects/state.js +59 -0
  93. package/src/execution-telemetry.js +2 -3
  94. package/src/first-success-guide.jsx +5 -5
  95. package/src/ghost-set.js +257 -0
  96. package/src/held-object-edit.jsx +113 -0
  97. package/src/held-object-pose.js +175 -0
  98. package/src/hierarchy-model.js +1 -1
  99. package/src/hierarchy-panel.jsx +1 -1
  100. package/src/imported-mesh.jsx +103 -0
  101. package/src/kits/held.js +84 -0
  102. package/src/kits/index.js +70 -0
  103. package/src/kits/smoking.js +57 -0
  104. package/src/light-blockers.js +93 -0
  105. package/src/motion/beat-board-model.js +84 -0
  106. package/src/motion/beat-board.css +124 -0
  107. package/src/motion/beat-board.jsx +59 -0
  108. package/src/motion/center-of-mass.js +66 -0
  109. package/src/motion/fix-collisions.js +850 -0
  110. package/src/motion/physics-review.js +118 -0
  111. package/src/{ardy → motion}/physics-support.js +1 -76
  112. package/src/motion/rig-snapshot.js +22 -0
  113. package/src/motion/take-segments.jsx +140 -0
  114. package/src/{ardy → motion}/timeline-coordinates.js +0 -21
  115. package/src/{ardy → motion}/timeline.css +1 -2
  116. package/src/{ardy → motion}/timeline.jsx +48 -419
  117. package/src/motion/waypoints.js +76 -0
  118. package/src/motion3/beats.js +83 -0
  119. package/src/motion3/body.js +158 -0
  120. package/src/motion3/bones.js +47 -0
  121. package/src/motion3/carried.js +47 -0
  122. package/src/motion3/compile-job.js +75 -0
  123. package/src/motion3/compile-worker.js +10 -0
  124. package/src/motion3/compiler/anchors.js +105 -0
  125. package/src/motion3/compiler/arm-orient.js +128 -0
  126. package/src/motion3/compiler/channels.js +125 -0
  127. package/src/motion3/compiler/compile.js +205 -0
  128. package/src/motion3/compiler/gait-rhythm.js +97 -0
  129. package/src/motion3/compiler/grip.js +171 -0
  130. package/src/motion3/compiler/head-steady.js +48 -0
  131. package/src/motion3/compiler/hold.js +134 -0
  132. package/src/motion3/compiler/idle.js +113 -0
  133. package/src/motion3/compiler/limbs.js +170 -0
  134. package/src/motion3/compiler/math.js +72 -0
  135. package/src/motion3/compiler/pose.js +162 -0
  136. package/src/motion3/compiler/quat.js +49 -0
  137. package/src/motion3/compiler/skeleton.js +74 -0
  138. package/src/motion3/compiler/validate-common.js +32 -0
  139. package/src/motion3/compiler/validate-goals.js +118 -0
  140. package/src/motion3/compiler/validate-objects.js +143 -0
  141. package/src/motion3/compiler/validate-parts.js +116 -0
  142. package/src/motion3/compiler/validate.js +122 -0
  143. package/src/motion3/compiler/vocab.js +73 -0
  144. package/src/motion3/compiler/walk-pelvis.js +275 -0
  145. package/src/motion3/compiler/walk.js +207 -0
  146. package/src/motion3/compiler/wrist-clamp.js +32 -0
  147. package/src/motion3/delta.js +47 -0
  148. package/src/motion3/describe.js +174 -0
  149. package/src/motion3/docs.js +27 -0
  150. package/src/motion3/edit-carry.js +21 -0
  151. package/src/motion3/fix/ease.js +16 -0
  152. package/src/motion3/fix/ground.js +37 -0
  153. package/src/motion3/fix/index.js +52 -0
  154. package/src/motion3/fix/push.js +71 -0
  155. package/src/motion3/fix/reach.js +46 -0
  156. package/src/motion3/fix/retime.js +79 -0
  157. package/src/motion3/fix/self.js +211 -0
  158. package/src/motion3/fix/slide.js +70 -0
  159. package/src/motion3/fix/turn.js +51 -0
  160. package/src/motion3/follow.js +39 -0
  161. package/src/motion3/free-motion.js +173 -0
  162. package/src/motion3/grip-choice.js +130 -0
  163. package/src/motion3/hold-plan.js +157 -0
  164. package/src/motion3/json-patch.js +77 -0
  165. package/src/motion3/key-aim.js +105 -0
  166. package/src/motion3/key-edit.js +82 -0
  167. package/src/motion3/lenient.js +127 -0
  168. package/src/motion3/object-keys.js +236 -0
  169. package/src/motion3/object-pose.js +182 -0
  170. package/src/motion3/object-tracks.js +12 -0
  171. package/src/motion3/pipeline.js +102 -0
  172. package/src/motion3/playback.js +51 -0
  173. package/src/motion3/probes.js +91 -0
  174. package/src/motion3/qa/body-checks.js +34 -0
  175. package/src/motion3/qa/check-names.js +14 -0
  176. package/src/motion3/qa/checks.js +121 -0
  177. package/src/motion3/qa/contact-checks.js +51 -0
  178. package/src/motion3/qa/explain.js +80 -0
  179. package/src/motion3/qa/fmt.js +5 -0
  180. package/src/motion3/qa/foot-checks.js +72 -0
  181. package/src/motion3/qa/geometry.js +77 -0
  182. package/src/motion3/qa/goals.js +191 -0
  183. package/src/motion3/qa/ground.js +60 -0
  184. package/src/motion3/qa/hints-contact.js +179 -0
  185. package/src/motion3/qa/hints-goal.js +160 -0
  186. package/src/motion3/qa/hints-knee.js +24 -0
  187. package/src/motion3/qa/hints-motion.js +209 -0
  188. package/src/motion3/qa/hints-object.js +91 -0
  189. package/src/motion3/qa/index.js +42 -0
  190. package/src/motion3/qa/key-checks.js +82 -0
  191. package/src/motion3/qa/near.js +96 -0
  192. package/src/motion3/qa/object-checks.js +135 -0
  193. package/src/motion3/qa/patch.js +161 -0
  194. package/src/motion3/qa/severity.js +38 -0
  195. package/src/motion3/qa/spec-checks.js +65 -0
  196. package/src/motion3/qa/style.js +218 -0
  197. package/src/motion3/qa/wrist-checks.js +69 -0
  198. package/src/motion3/qa/wrist-obs.js +60 -0
  199. package/src/motion3/refs.js +120 -0
  200. package/src/motion3/repair-tune.js +121 -0
  201. package/src/motion3/repair.js +83 -0
  202. package/src/motion3/report.js +67 -0
  203. package/src/motion3/rigs/index.js +14 -0
  204. package/src/motion3/rigs/x-bot-tpose-body.json +1 -0
  205. package/src/motion3/rigs/x-bot-tpose.json +1 -0
  206. package/src/motion3/rigs/y-bot-tpose-body.json +1 -0
  207. package/src/motion3/rigs/y-bot-tpose.json +1 -0
  208. package/src/motion3/scene.js +60 -0
  209. package/src/motion3/schema/brief.js +89 -0
  210. package/src/motion3/schema/fields.js +44 -0
  211. package/src/motion3/schema/spec-examples.js +46 -0
  212. package/src/motion3/schema/topics.js +111 -0
  213. package/src/motion3/schema/types.js +147 -0
  214. package/src/motion3/search.js +117 -0
  215. package/src/motion3/second-hand.js +112 -0
  216. package/src/motion3/skills/basic.js +82 -0
  217. package/src/motion3/skills/book.js +76 -0
  218. package/src/motion3/skills/car.js +145 -0
  219. package/src/motion3/skills/frames-auto.js +29 -0
  220. package/src/motion3/skills/index.js +83 -0
  221. package/src/motion3/skills/info.js +65 -0
  222. package/src/motion3/skills/smoke-kit.js +109 -0
  223. package/src/motion3/skills/smoke.js +160 -0
  224. package/src/motion3/spec-store.js +59 -0
  225. package/src/motion3/take-edit.js +155 -0
  226. package/src/motion3/take.js +63 -0
  227. package/src/motion3/timeline.js +127 -0
  228. package/src/motion3/views.js +121 -0
  229. package/src/motion3/walk-plan.js +119 -0
  230. package/src/object-catalog.jsx +7 -1
  231. package/src/object-colors.js +98 -0
  232. package/src/object-gizmo-parts.js +115 -0
  233. package/src/object-gizmo.jsx +37 -122
  234. package/src/object-glow.js +139 -0
  235. package/src/object-hidden.js +33 -0
  236. package/src/object-look.js +196 -0
  237. package/src/object-size.js +10 -0
  238. package/src/object-travel.js +71 -6
  239. package/src/panels/CameraPanel.jsx +7 -7
  240. package/src/panels/HdrEnvironmentControls.jsx +59 -0
  241. package/src/panels/LightPanel.jsx +25 -4
  242. package/src/panels/ObjectLookPanel.jsx +88 -0
  243. package/src/panels/PoseImagePanel.jsx +1 -1
  244. package/src/panels/PosePanel.jsx +2 -17
  245. package/src/panels/RigControlPanel.jsx +5 -53
  246. package/src/panels/SubjectBox.jsx +4 -1
  247. package/src/panels/details.css +14 -0
  248. package/src/posestudio.jsx +4 -27
  249. package/src/project-browser.jsx +3 -4
  250. package/src/project-resources.js +18 -31
  251. package/src/project.js +13 -104
  252. package/src/props.jsx +25 -280
  253. package/src/rail-dot-drag.js +23 -0
  254. package/src/render-passes.js +10 -0
  255. package/src/resource-status.jsx +1 -0
  256. package/src/result-modal.jsx +4 -19
  257. package/src/room.jsx +20 -12
  258. package/src/scene-assets.js +8 -6
  259. package/src/scene-light-place.js +72 -0
  260. package/src/scene-lights.jsx +148 -0
  261. package/src/scene-objects.js +31 -139
  262. package/src/scenes.js +15 -105
  263. package/src/semantic-edit.js +2 -5
  264. package/src/set-pieces.jsx +183 -0
  265. package/src/settings-menu.jsx +3 -11
  266. package/src/shell/BottomDock.jsx +11 -53
  267. package/src/shell/CompositionGuideButton.jsx +20 -0
  268. package/src/shell/DetailsSlot.jsx +16 -200
  269. package/src/shell/GhostsMenuGroup.jsx +74 -0
  270. package/src/shell/LibrarySlot.jsx +1 -3
  271. package/src/shell/MenuBar.jsx +3 -3
  272. package/src/shell/PreferencesDialog.jsx +15 -264
  273. package/src/shell/PreferencesSlot.jsx +2 -8
  274. package/src/shell/ShotPreviewHeader.jsx +17 -0
  275. package/src/shell/TopBar.jsx +5 -83
  276. package/src/shell/ViewportToolbar.jsx +23 -93
  277. package/src/shell/agent-glass.css +19 -6
  278. package/src/shell/details-selection.js +42 -0
  279. package/src/shell/ghosts-menu.css +22 -0
  280. package/src/shell/glass-regions.css +29 -3
  281. package/src/shell/preferences.css +6 -1
  282. package/src/shell/viewport.css +12 -0
  283. package/src/shot-authoring.js +4 -7
  284. package/src/soft-shadows.js +37 -0
  285. package/src/stage-attach-frames.js +12 -12
  286. package/src/stage-canvas-rigs.jsx +3 -3
  287. package/src/stage-capture.jsx +42 -8
  288. package/src/stage-characters.jsx +22 -216
  289. package/src/stage-editor-gizmos.jsx +6 -6
  290. package/src/stage-environment.js +93 -0
  291. package/src/stage-hdr-builtins.js +107 -0
  292. package/src/stage-hdr-image.js +116 -0
  293. package/src/stage-hdr-library.js +70 -0
  294. package/src/stage-hdr-map.js +94 -0
  295. package/src/stage-hdr.js +130 -0
  296. package/src/stage-lighting.js +149 -0
  297. package/src/stage-occlusion.js +210 -0
  298. package/src/stage-sky.jsx +159 -0
  299. package/src/stage-startup-defaults.js +5 -9
  300. package/src/studio-actions.js +12 -14
  301. package/src/studio-agent-commands.js +46 -16
  302. package/src/studio-agent-motion.js +9 -9
  303. package/src/studio-agent-protocol.js +46 -73
  304. package/src/studio-app-binding.js +29 -45
  305. package/src/studio-asset-list.js +17 -0
  306. package/src/studio-contact-sheet.js +37 -5
  307. package/src/studio-elements.js +27 -7
  308. package/src/studio-entity-bounds.js +18 -0
  309. package/src/studio-error-codes.js +8 -0
  310. package/src/studio-export-framing.js +152 -0
  311. package/src/studio-lighting-check.js +103 -0
  312. package/src/studio-motion-visual.js +150 -0
  313. package/src/studio-schema-errors.js +80 -0
  314. package/src/studio-support-gap.js +74 -0
  315. package/src/styles.css +1 -19
  316. package/src/timeline-defaults.js +5 -0
  317. package/src/timeline-extent.js +8 -20
  318. package/src/tutorial-walk.js +17 -0
  319. package/src/ui-scrub.js +1 -1
  320. package/src/use-shot-camera-tracks.js +12 -0
  321. package/src/view-ghosts.js +97 -0
  322. package/src/view-ghosts.jsx +49 -0
  323. package/src/workflow/AgentPanel.jsx +47 -166
  324. package/src/workflow/AgentProgressCard.jsx +107 -0
  325. package/src/workflow/AgentReplyText.jsx +37 -0
  326. package/src/workflow/AgentTurnSteps.jsx +84 -0
  327. package/src/workflow/LoginPanel.jsx +140 -0
  328. package/src/workflow/WorkflowBuilder.jsx +7 -23
  329. package/src/workflow/agent-client.js +54 -51
  330. package/src/workflow/agent-panel.css +71 -1
  331. package/src/workflow/agent-progress-card.css +167 -0
  332. package/src/workflow/agent-progress-view.js +47 -0
  333. package/src/workflow/agent-turn-steps.css +113 -0
  334. package/src/workflow/agent-turn.js +191 -0
  335. package/src/workflow/login-client.js +34 -0
  336. package/src/workflow/motion-input.js +18 -31
  337. package/src/workflow/node-schema.js +1 -1
  338. package/src/workflow/reply-markdown.js +69 -0
  339. package/src/workflow/scene-asset-sync.js +8 -8
  340. package/tools/agent-session-stats.mjs +61 -0
  341. package/tools/dev-full.mjs +5 -56
  342. package/tools/eslint-baseline.json +12 -13
  343. package/tools/headless-studio.mjs +85 -0
  344. package/tools/knip-baseline.json +0 -179
  345. package/tools/loc-baseline.json +33 -83
  346. package/tools/motion3/extract-body.mjs +103 -0
  347. package/tools/motion3/extract-rig.mjs +85 -0
  348. package/tools/process-supervisor.mjs +0 -88
  349. package/tools/run-tests.mjs +9 -124
  350. package/bin/agent/motion-runtime.mjs +0 -76
  351. package/dist/ardy/cskel27-rest.json +0 -557
  352. package/dist/assets/analytics-B41FmPYI.js +0 -1
  353. package/dist/assets/app-Do-Og5Pu.css +0 -1
  354. package/dist/assets/app-DwP-tl2x.js +0 -4920
  355. package/dist/assets/config-Bx7pYsD9.js +0 -1
  356. package/dist/assets/config-DeoFTYzb.css +0 -1
  357. package/dist/assets/demo-Dt0qZfS7.js +0 -1
  358. package/dist/assets/scene-asset-sync-BCWDJB1d.js +0 -19
  359. package/dist/assets/scene-asset-sync-DjJ_cJ2R.css +0 -1
  360. package/dist/assets/ticket-jqMhqL88.js +0 -1
  361. package/dist/assets/workflow-Br-K7Srs.js +0 -179
  362. package/dist/demo/walk-then-stop.npz +0 -0
  363. package/mcp/ardy-prompts.mjs +0 -218
  364. package/src/ardy/auto-fix-panel.css +0 -108
  365. package/src/ardy/auto-physics.js +0 -1194
  366. package/src/ardy/client.js +0 -153
  367. package/src/ardy/convert.js +0 -313
  368. package/src/ardy/cskel27-neutral.js +0 -39
  369. package/src/ardy/cskel27.js +0 -77
  370. package/src/ardy/export.js +0 -165
  371. package/src/ardy/fix-collisions.js +0 -2257
  372. package/src/ardy/foot-lock.js +0 -230
  373. package/src/ardy/motion-calibration.js +0 -118
  374. package/src/ardy/motion-edit.js +0 -213
  375. package/src/ardy/motion-url.js +0 -55
  376. package/src/ardy/mp4-duration.js +0 -102
  377. package/src/ardy/npz.js +0 -595
  378. package/src/ardy/physics-panel.css +0 -21
  379. package/src/ardy/physics-panel.jsx +0 -89
  380. package/src/ardy/physics-refine.js +0 -111
  381. package/src/ardy/physics-review.js +0 -567
  382. package/src/ardy/physics-temporal.js +0 -54
  383. package/src/ardy/platform-fit-panel.css +0 -64
  384. package/src/ardy/platform-fit-panel.jsx +0 -41
  385. package/src/ardy/platform-fit.js +0 -326
  386. package/src/ardy/playback.js +0 -711
  387. package/src/ardy/pose-pin.js +0 -97
  388. package/src/ardy/prompt-clips.js +0 -30
  389. package/src/ardy/retime.js +0 -211
  390. package/src/ardy/root-drop.js +0 -369
  391. package/src/ardy/timeline-resize.js +0 -13
  392. package/src/ardy/to-cskel27.js +0 -225
  393. package/src/ardy/trim.js +0 -33
  394. package/src/ardy/waypoints.js +0 -369
  395. package/src/ardy-bridge-contract.js +0 -281
  396. package/src/domains/motion/motion-video-capture.js +0 -664
  397. package/src/i2v-motion-client.js +0 -77
  398. package/src/i2v-motion-studio.jsx +0 -180
  399. package/src/line-edit.js +0 -1348
  400. package/src/motion/generation.js +0 -105
  401. package/src/motion-readiness-ui.jsx +0 -67
  402. package/src/motion-readiness.js +0 -40
  403. package/src/motion-resources.js +0 -177
  404. package/src/motion-store.js +0 -65
  405. package/src/motion-trail.js +0 -601
  406. package/src/multimodel-ingest.js +0 -394
  407. package/src/panels/PromptBlocksPanel.jsx +0 -444
  408. package/src/panels/VideoCapturePanel.jsx +0 -181
  409. package/src/pose-extract/browser.js +0 -42
  410. package/src/pose-extract/detector.js +0 -100
  411. package/src/pose-extract/fit.js +0 -227
  412. package/src/pose-extract/image-frame.js +0 -51
  413. package/src/pose-extract/index.js +0 -58
  414. package/src/pose-extract/keyframes.js +0 -74
  415. package/src/pose-extract/landmarks.js +0 -162
  416. package/src/pose-extract/mirror.js +0 -153
  417. package/src/pose-extract/take.js +0 -131
  418. package/src/pose-extract/video-frames.js +0 -91
  419. package/src/take-recipe.js +0 -165
  420. package/src/trail-key-conflicts.js +0 -39
  421. package/src/trail-pick.js +0 -59
  422. package/src/use-i2v-capture-modal.js +0 -15
  423. package/tools/ardy/BRIDGE.md +0 -37
  424. package/tools/ardy/GVHMR-NPZ.md +0 -68
  425. package/tools/ardy/README.md +0 -124
  426. package/tools/ardy/__pycache__/cclay_gvhmr_worker.cpython-313.pyc +0 -0
  427. package/tools/ardy/artifacts.mjs +0 -34
  428. package/tools/ardy/bridge.mjs +0 -1732
  429. package/tools/ardy/bvh-cskel27.mjs +0 -1265
  430. package/tools/ardy/cclay_constrained_generate.py +0 -561
  431. package/tools/ardy/cclay_gvhmr_worker.py +0 -264
  432. package/tools/ardy/cclay_motion_edit.py +0 -510
  433. package/tools/ardy/cclay_sequence_generate.py +0 -705
  434. package/tools/ardy/cclay_worker.py +0 -304
  435. package/tools/ardy/dump-npz.py +0 -205
  436. package/tools/ardy/extract-rest.mjs +0 -299
  437. package/tools/ardy/extract.mjs +0 -507
  438. package/tools/ardy/footage.mjs +0 -467
  439. package/tools/ardy/gvhmr-floor.mjs +0 -102
  440. package/tools/ardy/gvhmr-to-cskel27.mjs +0 -21
  441. package/tools/ardy/gvhmr_fastpath.py +0 -168
  442. package/tools/ardy/gvhmr_trajectory.py +0 -240
  443. package/tools/ardy/merge-text-encoder.py +0 -160
  444. package/tools/ardy/mocap-metrics.mjs +0 -200
  445. package/tools/ardy/mocap-quality-gate.mjs +0 -110
  446. package/tools/ardy/motion-stabilize.mjs +0 -348
  447. package/tools/ardy/npz.mjs +0 -414
  448. package/tools/ardy/pose-to-npz.mjs +0 -106
  449. package/tools/ardy/prompt-limits.mjs +0 -5
  450. package/tools/ardy/run-edit-on-box.sh +0 -73
  451. package/tools/ardy/run-local.mjs +0 -233
  452. package/tools/ardy/run-on-box.sh +0 -593
  453. package/tools/ardy/run-sequence-on-box.sh +0 -177
  454. package/tools/ardy/runners/gvhmr-worker.mjs +0 -206
  455. package/tools/ardy/runners/index.mjs +0 -18
  456. package/tools/ardy/runners/local.mjs +0 -455
  457. package/tools/ardy/runners/proc.mjs +0 -125
  458. package/tools/ardy/runners/remote.mjs +0 -252
  459. package/tools/ardy/setup-local.mjs +0 -189
  460. package/tools/ardy/setup-text-encoder-on-box.sh +0 -93
  461. package/tools/ardy/setup-text-encoder.py +0 -306
  462. package/tools/ardy/smpl-cskel27.mjs +0 -178
  463. package/tools/ardy/visual-qa.mjs +0 -196
  464. package/tools/ardy/vq-car.mjs +0 -52
  465. package/tools/bench/EXP3.md +0 -61
  466. package/tools/bench/cclay_bench_extract_incam.py +0 -125
  467. package/tools/bench/cclay_bench_extract_obs.py +0 -204
  468. package/tools/bench/cclay_bench_runner.py +0 -42
  469. package/tools/bench/cube-contact.mjs +0 -162
  470. package/tools/bench/exp3.mjs +0 -152
  471. package/tools/bench/extract-bench-lib.mjs +0 -216
  472. package/tools/bench/extract-bench.mjs +0 -302
  473. package/tools/bench/fal-generate.mjs +0 -88
  474. package/tools/bench/fit/README.md +0 -189
  475. package/tools/bench/fit/camera.mjs +0 -59
  476. package/tools/bench/fit/contact.mjs +0 -419
  477. package/tools/bench/fit/footlock.mjs +0 -234
  478. package/tools/bench/fit/motion.mjs +0 -85
  479. package/tools/bench/fit/pin.mjs +0 -27
  480. package/tools/bench/fit/remote.mjs +0 -105
  481. package/tools/bench/fit-bench.mjs +0 -126
  482. package/tools/bench/fit-sanity.mjs +0 -45
  483. package/tools/bench/metrics.mjs +0 -313
  484. package/tools/bench/obs/depth.mjs +0 -258
  485. package/tools/bench/obs/extrinsics.mjs +0 -175
  486. package/tools/bench/obs/fit_mannequin_betas.py +0 -340
  487. package/tools/bench/obs/ground.mjs +0 -294
  488. package/tools/bench/obs/heading.mjs +0 -151
  489. package/tools/bench/obs/ladder.mjs +0 -555
  490. package/tools/bench/obs/mannequin-betas.json +0 -124
  491. package/tools/bench/obs/remote.mjs +0 -83
  492. package/tools/bench/obs/rest_joints.py +0 -52
  493. package/tools/bench/obs/ybot-targets.mjs +0 -49
  494. package/tools/bench/obs-bench.mjs +0 -520
  495. package/tools/bench/score.mjs +0 -343
  496. package/tools/bench/summarize.mjs +0 -86
  497. package/tools/demo-worker/README.md +0 -137
  498. package/tools/demo-worker/api-client.mjs +0 -233
  499. package/tools/demo-worker/generate.mjs +0 -301
  500. package/tools/demo-worker/index.mjs +0 -466
  501. package/tools/demo-worker/sign-core.mjs +0 -112
  502. package/tools/demo-worker/sign.mjs +0 -161
  503. package/tools/gt-render/browser.mjs +0 -159
  504. package/tools/gt-render/camera-math.mjs +0 -276
  505. package/tools/gt-render/page.mjs +0 -267
  506. package/tools/gt-render/render.mjs +0 -514
  507. package/tools/gt-render/scene-box.mjs +0 -31
  508. package/tools/gt-render/take-transform.mjs +0 -87
  509. package/tools/kimodo/compare-seam.mjs +0 -108
  510. package/tools/kimodo/constraints.mjs +0 -244
  511. package/tools/kimodo/edit.mjs +0 -259
  512. package/tools/kimodo/effector-constraints.mjs +0 -200
  513. package/tools/kimodo/generate.mjs +0 -646
  514. package/tools/kimodo/local-output.mjs +0 -265
  515. package/tools/kimodo/measure-pose.mjs +0 -65
  516. package/tools/kimodo/measure-preserve.mjs +0 -899
  517. package/tools/kimodo/measure-waypoints.mjs +0 -55
  518. package/tools/kimodo/mlx-generate.py +0 -111
  519. package/tools/kimodo/pose-constraints.mjs +0 -248
  520. package/tools/kimodo/preserve-mask.mjs +0 -543
  521. package/tools/kimodo/read-npz.mjs +0 -189
  522. package/tools/kimodo/run-edit-on-box.mjs +0 -155
  523. package/tools/kimodo/run-sequence-on-box.mjs +0 -178
  524. package/tools/kimodo/runner.mjs +0 -284
  525. package/tools/kimodo/seam-batch.mjs +0 -77
  526. package/tools/kimodo/setup-local.mjs +0 -30
  527. package/tools/kimodo/setup-on-box.sh +0 -339
  528. package/tools/kimodo/soma77-to-cskel27.mjs +0 -305
  529. package/tools/morphgs/assets/gen_truth.py +0 -39
  530. package/tools/morphgs/assets/mesh_ori_rig.txt +0 -27
  531. package/tools/morphgs/assets/playback-check.mjs +0 -30
  532. package/tools/morphgs/demo-gate.sh +0 -26
  533. package/tools/morphgs/fbx2morphgs.mjs +0 -68
  534. package/tools/morphgs/morphgs-to-cskel27.mjs +0 -105
  535. package/tools/morphgs/patches/preprocess_src-none-mode.patch +0 -76
  536. package/tools/morphgs/setup-on-cluster.sh +0 -126
  537. package/tools/projflow/driver.py +0 -1069
  538. package/tools/projflow/generate.mjs +0 -805
  539. package/tools/projflow/hml22-to-cskel27.mjs +0 -735
  540. package/tools/projflow/line-edit-job.mjs +0 -547
  541. package/tools/projflow/replay.mjs +0 -452
  542. package/tools/projflow/runner.mjs +0 -269
  543. package/tools/projflow/service.mjs +0 -742
  544. package/tools/track/DESIGN.md +0 -513
  545. package/tools/track/backfill-provenance.mjs +0 -116
  546. package/tools/track/budget.mjs +0 -8
  547. package/tools/track/check-rig.mjs +0 -209
  548. package/tools/track/diagnostics.schema.json +0 -60
  549. package/tools/track/export-rig.mjs +0 -256
  550. package/tools/track/fallback.mjs +0 -7
  551. package/tools/track/fk-parity-fixture.mjs +0 -161
  552. package/tools/track/gate.mjs +0 -349
  553. package/tools/track/masks.mjs +0 -318
  554. package/tools/track/metrics.mjs +0 -201
  555. package/tools/track/publish-obs.mjs +0 -108
  556. package/tools/track/py/check_env.py +0 -61
  557. package/tools/track/py/eval_lr.py +0 -229
  558. package/tools/track/py/lr_viterbi.py +0 -294
  559. package/tools/track/py/masks.py +0 -405
  560. package/tools/track/py/objective.py +0 -382
  561. package/tools/track/py/rig.py +0 -508
  562. package/tools/track/py/scene.py +0 -326
  563. package/tools/track/py/test_joint_indices.py +0 -123
  564. package/tools/track/py/test_lr_viterbi.py +0 -195
  565. package/tools/track/py/test_masks.py +0 -229
  566. package/tools/track/py/test_rig.py +0 -288
  567. package/tools/track/py/test_scene.py +0 -354
  568. package/tools/track/py/test_track.py +0 -328
  569. package/tools/track/py/track.py +0 -411
  570. package/tools/track/remote.mjs +0 -140
  571. package/tools/track/rig-dump.mjs +0 -227
  572. package/tools/track/run-box-tests.mjs +0 -14
  573. package/tools/track/run-box.mjs +0 -168
  574. package/tools/track/setup-box.sh +0 -59
  575. package/tools/track/study-2d.mjs +0 -728
  576. /package/src/{ardy → motion}/collision-blockers.js +0 -0
  577. /package/src/{ardy → motion}/ground.js +0 -0
  578. /package/src/{ardy → motion}/ik-drag.js +0 -0
  579. /package/src/{ardy → motion}/ik-key-json.js +0 -0
  580. /package/src/{ardy → motion}/ik.js +0 -0
  581. /package/src/{ardy → motion}/physics-surface.js +0 -0
  582. /package/src/{ardy → motion}/playback-clock.js +0 -0
  583. /package/src/{ardy → motion}/range-pin.js +0 -0
@@ -1,306 +0,0 @@
1
- #!/usr/bin/env python3
2
- """setup-text-encoder.py - provision the ARDY text-encoder models without a
3
- Hugging Face account, approval wait, or access token.
4
-
5
- ARDY's text encoder is LLM2Vec on top of meta-llama/Meta-Llama-3-8B-Instruct.
6
- The official Meta repository is gated (manual approval + token). This script
7
- instead downloads a byte-pinned stack from public, ungated repositories:
8
-
9
- base NousResearch/Meta-Llama-3-8B-Instruct (public mirror of the
10
- official weights; every safetensors shard is verified against a
11
- pinned SHA-256 before use)
12
- adapters McGill-NLP/LLM2Vec-Meta-Llama-3-8B-Instruct-mntp
13
- McGill-NLP/LLM2Vec-Meta-Llama-3-8B-Instruct-mntp-supervised
14
-
15
- and lays them out for ARDY's `TEXT_ENCODERS_DIR` override
16
- (ardy/model/llm2vec/llm2vec_wrapper.py), rewriting each adapter's
17
- `base_model_name_or_path` to the local base directory so nothing ever
18
- resolves back to the gated meta-llama repository at runtime.
19
-
20
- Every revision is pinned to a full commit SHA and every LFS payload to a
21
- SHA-256, so re-runs are reproducible and a tampered or changed upstream file
22
- fails loudly instead of silently drifting the embeddings (and with them the
23
- generated motion).
24
-
25
- Licenses: the base weights are Meta Llama 3 Community License (the LICENSE
26
- and USE_POLICY.md files are downloaded alongside the weights and must stay
27
- there); the McGill adapters are MIT. This script only downloads for local
28
- use - it does not redistribute anything.
29
-
30
- Usage (typically on the ARDY box; stdlib only, python >= 3.8):
31
-
32
- python3 setup-text-encoder.py # into ~/cclay-text-encoders
33
- python3 setup-text-encoder.py --dest /srv/enc # custom destination
34
- python3 setup-text-encoder.py --verify-only # re-hash an existing tree
35
-
36
- Afterwards start the encoder service with the printed environment:
37
-
38
- TEXT_ENCODERS_DIR=<dest> python scripts/run_text_encoder_server.py
39
- """
40
-
41
- import argparse
42
- import hashlib
43
- import json
44
- import os
45
- import sys
46
- import time
47
- import urllib.error
48
- import urllib.request
49
-
50
- HF = "https://huggingface.co"
51
-
52
- BASE_DIR = "NousResearch/Meta-Llama-3-8B-Instruct"
53
- MNTP_DIR = "McGill-NLP/LLM2Vec-Meta-Llama-3-8B-Instruct-mntp"
54
- # The mntp *download* lands in a -src dir: merge-text-encoder.py bakes the
55
- # base+MNTP merge into the runtime MNTP_DIR path (transformers v5 dropped
56
- # automatic adapter-dir resolution, so the runtime path must be a full model).
57
- MNTP_SRC_DIR = MNTP_DIR + "-src"
58
- SUP_DIR = "McGill-NLP/LLM2Vec-Meta-Llama-3-8B-Instruct-mntp-supervised"
59
-
60
- # (local dir, repo id, pinned full commit SHA, files)
61
- # Each file: (path, size, sha256-or-None). SHA-256 comes from the HF LFS
62
- # metadata of the pinned revision; small non-LFS files carry only a size and
63
- # are already immutable because the revision is a commit hash.
64
- # Pins match nv-tlabs/ardy issue #9's reproduction stack.
65
- MANIFEST = [
66
- (
67
- BASE_DIR,
68
- "NousResearch/Meta-Llama-3-8B-Instruct",
69
- "53346005fb0ef11d3b6a83b12c895cca40156b6c",
70
- [
71
- ("LICENSE", 7801, None),
72
- ("USE_POLICY.md", 4696, None),
73
- ("config.json", 654, None),
74
- ("generation_config.json", 187, None),
75
- ("model-00001-of-00004.safetensors", 4976698672,
76
- "d8cf9c4d0dd972e1a2131bfe656235ee98221679711a3beef6d46dadf0f20b5c"),
77
- ("model-00002-of-00004.safetensors", 4999802720,
78
- "8d4782b4a69ef03845159ce1a15e272aadaaf134dc138d68f616098e8531729c"),
79
- ("model-00003-of-00004.safetensors", 4915916176,
80
- "3acdd690e65c24f42a24581b8467af98bd3ca357444580f8012aacd2bd607921"),
81
- ("model-00004-of-00004.safetensors", 1168138808,
82
- "67e9ad31c8c32abf3a55ee7fc7217b3ecb35fd3c74d98a5bd233e0e4d6964f46"),
83
- ("model.safetensors.index.json", 23950, None),
84
- ("special_tokens_map.json", 73, None),
85
- ("tokenizer.json", 9085698, None),
86
- ("tokenizer_config.json", 50977, None),
87
- ],
88
- ),
89
- (
90
- MNTP_SRC_DIR,
91
- "McGill-NLP/LLM2Vec-Meta-Llama-3-8B-Instruct-mntp",
92
- "31474e395ada192e8ed1586db6be79fb3b70c9c0",
93
- [
94
- ("README.md", 3315, None),
95
- ("adapter_config.json", 794, None),
96
- ("adapter_model.safetensors", 167829552,
97
- "de9c8736618a13173c6a1623cdef1b75e86c69317f1073ae82cd516ac36a632d"),
98
- ("attn_mask_utils.py", 11132, None),
99
- ("config.json", 781, None),
100
- ("modeling_llama_encoder.py", 2856, None),
101
- ("special_tokens_map.json", 335, None),
102
- ("tokenizer.json", 9085671, None),
103
- ("tokenizer_config.json", 51042, None),
104
- ],
105
- ),
106
- (
107
- SUP_DIR,
108
- "McGill-NLP/LLM2Vec-Meta-Llama-3-8B-Instruct-mntp-supervised",
109
- "baa8ebf04a1c2500e61288e7dad65e8ae42601a7",
110
- [
111
- ("README.md", 62702, None),
112
- ("adapter_config.json", 794, None),
113
- ("adapter_model.safetensors", 167829552,
114
- "53f8f94ebdf396667ba99dd96e78203edae27bbcdbd1cf5f12b611e1d916b225"),
115
- ],
116
- ),
117
- ]
118
-
119
- # adapter_config.json is rewritten after download (base_model_name_or_path ->
120
- # local base dir), so its byte size legitimately changes; everything else must
121
- # keep the manifest size forever.
122
- MUTATED = {"adapter_config.json"}
123
-
124
- CHUNK = 1024 * 1024
125
-
126
-
127
- def log(msg):
128
- print("setup-text-encoder: %s" % msg, flush=True)
129
-
130
-
131
- def fail(msg):
132
- print("setup-text-encoder: ERROR: %s" % msg, file=sys.stderr, flush=True)
133
- sys.exit(1)
134
-
135
-
136
- def sha256_file(path):
137
- h = hashlib.sha256()
138
- with open(path, "rb") as f:
139
- while True:
140
- block = f.read(CHUNK)
141
- if not block:
142
- break
143
- h.update(block)
144
- return h.hexdigest()
145
-
146
-
147
- def file_ok(path, size, sha, verify_hash):
148
- """Is the on-disk file already the pinned artifact?"""
149
- if not os.path.isfile(path):
150
- return False
151
- name = os.path.basename(path)
152
- if name in MUTATED:
153
- return True # rewritten in place; checked by rewrite_adapter_config
154
- if os.path.getsize(path) != size:
155
- return False
156
- if sha is not None and verify_hash:
157
- return sha256_file(path) == sha
158
- return True
159
-
160
-
161
- def download(url, dest, size, sha):
162
- """Resumable download to dest + '.partial', verified, then atomic rename."""
163
- part = dest + ".partial"
164
- os.makedirs(os.path.dirname(dest), exist_ok=True)
165
- for attempt in range(1, 6):
166
- try:
167
- have = os.path.getsize(part) if os.path.isfile(part) else 0
168
- if have > size:
169
- os.remove(part)
170
- have = 0
171
- if have < size:
172
- req = urllib.request.Request(url)
173
- req.add_header("User-Agent", "cozyclay-setup-text-encoder/1")
174
- if have:
175
- req.add_header("Range", "bytes=%d-" % have)
176
- with urllib.request.urlopen(req, timeout=60) as resp:
177
- if have and resp.status != 206:
178
- # Server ignored the Range header; start over.
179
- have = 0
180
- mode = "wb"
181
- else:
182
- mode = "ab" if have else "wb"
183
- done = have
184
- t0 = time.time()
185
- with open(part, mode) as out:
186
- while True:
187
- block = resp.read(CHUNK)
188
- if not block:
189
- break
190
- out.write(block)
191
- done += len(block)
192
- if size > CHUNK and time.time() - t0 > 5:
193
- log(" ... %d / %d MiB" % (done // 2**20, size // 2**20))
194
- t0 = time.time()
195
- got = os.path.getsize(part)
196
- if got != size:
197
- raise IOError("size mismatch: got %d, want %d" % (got, size))
198
- if sha is not None:
199
- actual = sha256_file(part)
200
- if actual != sha:
201
- os.remove(part)
202
- raise IOError("sha256 mismatch: got %s, want %s" % (actual, sha))
203
- os.replace(part, dest)
204
- return
205
- except (urllib.error.URLError, IOError, OSError) as e:
206
- if attempt == 5:
207
- fail("giving up on %s after 5 attempts: %s" % (url, e))
208
- log(" retry %d/5 for %s (%s)" % (attempt, os.path.basename(dest), e))
209
- time.sleep(2 * attempt)
210
-
211
-
212
- def rewrite_adapter_config(path, base_path):
213
- """Point the adapter at the local base so nothing resolves to gated repos."""
214
- with open(path, "r", encoding="utf-8") as f:
215
- cfg = json.load(f)
216
- if cfg.get("base_model_name_or_path") == base_path:
217
- return False
218
- cfg["base_model_name_or_path"] = base_path
219
- tmp = path + ".tmp"
220
- with open(tmp, "w", encoding="utf-8") as f:
221
- json.dump(cfg, f, indent=2)
222
- f.write("\n")
223
- os.replace(tmp, path)
224
- return True
225
-
226
-
227
- def main():
228
- ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
229
- ap.add_argument("--dest", default=os.environ.get(
230
- "TEXT_ENCODERS_DIR", os.path.expanduser("~/cclay-text-encoders")),
231
- help="target directory (default: $TEXT_ENCODERS_DIR or ~/cclay-text-encoders)")
232
- ap.add_argument("--verify-only", action="store_true",
233
- help="verify an existing tree (sizes + SHA-256), download nothing")
234
- ap.add_argument("--print-plan", action="store_true",
235
- help="list every pinned file and exit")
236
- args = ap.parse_args()
237
-
238
- dest = os.path.abspath(os.path.expanduser(args.dest))
239
- os.makedirs(dest, exist_ok=True)
240
- dest_real = os.path.realpath(dest)
241
-
242
- def safe_join(*parts):
243
- """Join paths and verify the result stays within base."""
244
- full = os.path.realpath(os.path.join(dest_real, *parts))
245
- if full != dest_real and not full.startswith(dest_real + os.sep):
246
- fail("path traversal detected: %s" % os.path.join(*parts))
247
- return full
248
-
249
- base_path = safe_join(BASE_DIR)
250
-
251
- if args.print_plan:
252
- for local_dir, repo, rev, files in MANIFEST:
253
- print("%s @ %s" % (repo, rev))
254
- for path, size, sha in files:
255
- print(" %-40s %12d %s" % (path, size, sha or "-"))
256
- return
257
-
258
- total = sum(size for _, _, _, files in MANIFEST for _, size, _ in files)
259
- missing = []
260
- for local_dir, repo, rev, files in MANIFEST:
261
- for path, size, sha in files:
262
- target = safe_join(local_dir, path)
263
- if not file_ok(target, size, sha, verify_hash=args.verify_only):
264
- missing.append((local_dir, repo, rev, path, size, sha))
265
-
266
- if args.verify_only:
267
- if missing:
268
- for local_dir, _, _, path, _, _ in missing:
269
- print(" BAD/MISSING %s/%s" % (local_dir, path))
270
- fail("%d file(s) failed verification under %s" % (len(missing), dest))
271
- log("all %d files verified OK under %s" % (
272
- sum(len(f) for _, _, _, f in MANIFEST), dest))
273
- return
274
-
275
- if missing:
276
- todo = sum(m[4] for m in missing)
277
- log("destination: %s" % dest)
278
- log("downloading %d file(s), %.1f GiB of %.1f GiB total pinned payload"
279
- % (len(missing), todo / 2**30, total / 2**30))
280
- for local_dir, repo, rev, path, size, sha in missing:
281
- url = "%s/%s/resolve/%s/%s?download=true" % (HF, repo, rev, path)
282
- log("fetch %s/%s (%d bytes)" % (local_dir, path, size))
283
- download(url, safe_join(local_dir, path), size, sha)
284
- else:
285
- log("all pinned files already present under %s" % dest)
286
-
287
- for adapter_dir in (MNTP_SRC_DIR, SUP_DIR):
288
- cfg = safe_join(adapter_dir, "adapter_config.json")
289
- if rewrite_adapter_config(cfg, base_path):
290
- log("repointed %s -> local base" % os.path.join(adapter_dir, "adapter_config.json"))
291
-
292
- log("downloads verified. Bake the MNTP merge next (one-time, ~5 min):")
293
- log(" cd <ardy checkout> && <ardy venv python> merge-text-encoder.py --dest %s" % dest)
294
- print()
295
- print("Start ARDY's text-encoder service (and any local ARDY run) with:")
296
- print()
297
- print(" export TEXT_ENCODERS_DIR=%s" % dest)
298
- print(" python scripts/run_text_encoder_server.py")
299
- print()
300
- print("The base weights are Meta Llama 3 (Meta Llama 3 Community License;")
301
- print("see %s/LICENSE)." % base_path)
302
- print("This product is built with Meta Llama 3.")
303
-
304
-
305
- if __name__ == "__main__":
306
- main()
@@ -1,178 +0,0 @@
1
- /** Rotation-only GVHMR SMPL -> cskel27 retarget.
2
- * The argument is the member map returned by readNpz; plain arrays are also
3
- * accepted for small callers. Arrays may be nested or flat typed buffers.
4
- *
5
- * PROPORTIONS. Mocap is the product here, so the take carries the filmed
6
- * performer's body, not the canonical one: every cskel27 bone is scaled by
7
- * the ratio of the performer's SMPL rest bone length to the canonical bone
8
- * (`boneScale`, one factor per joint = the length of the bone ENDING at that
9
- * joint). posedJoints are grown with FK over those scaled offsets, so foot
10
- * placement, reach and stride are the performer's; playback applies the same
11
- * factors to the bones it drives by rotation only (the arm chain). A bone the
12
- * SMPL skeleton does not segment the same way (Spine..Spine3 are four links
13
- * against SMPL's three; hand ends; thumb) takes the nearest measured chain's
14
- * ratio so the body stays continuous. rootPos is the performer's pelvis
15
- * height as filmed: with the legs at their real length there is no height
16
- * ratio left to apply, and personScale is 1 by construction.
17
- */
18
- import { CSKEL27_JOINTS, CSKEL27_PARENTS } from "../../src/ardy/cskel27.js";
19
- import { deriveBoneOffsets, forwardKinematics, matMul, matTranspose } from "../../src/ardy/convert.js";
20
- import { canonicalCskel27Reference } from "../../src/ardy/to-cskel27.js";
21
-
22
- const SMPL_PARENTS = [-1, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 9, 9, 12, 13, 14, 16, 17, 18, 19, 20, 21];
23
- const IDENTITY = [[1, 0, 0], [0, 1, 0], [0, 0, 1]];
24
- const MAP = {
25
- Hips: 0, Spine: 3, Spine1: 6, Spine2: 9, Spine3: 9, Neck: 12, Head: 15,
26
- RightShoulder: 14, RightArm: 17, RightForeArm: 19, RightHand: 21,
27
- LeftShoulder: 13, LeftArm: 16, LeftForeArm: 18, LeftHand: 20,
28
- RightUpLeg: 2, RightLeg: 5, RightFoot: 8, RightToeBase: 11,
29
- LeftUpLeg: 1, LeftLeg: 4, LeftFoot: 7, LeftToeBase: 10,
30
- };
31
- // Limb rest directions are meaningful (SMPL's A-pose versus cskel27's
32
- // T-pose), so a constant swing aligns those chains without losing source
33
- // twist. Torso, pelvis, neck and head are deliberately absent: SMPL's joint
34
- // regressor forms an anatomical S inside the mesh and its head joint sits
35
- // forward inside the skull. Those are not visible-pose corrections and must
36
- // not deform the target character's bind shape.
37
- const CHILD = {
38
- RightShoulder: 17, RightArm: 19, RightForeArm: 21, RightHand: 23,
39
- LeftShoulder: 16, LeftArm: 18, LeftForeArm: 20, LeftHand: 22,
40
- RightUpLeg: 5, RightLeg: 8, RightFoot: 11,
41
- LeftUpLeg: 4, LeftLeg: 7, LeftFoot: 10,
42
- };
43
- const BORROW = { RightHandEnd: "RightHand", RightHandThumb1: "RightHand", LeftHandEnd: "LeftHand", LeftHandThumb1: "LeftHand" };
44
- const CORRECTION_BORROW = { RightToeBase: "RightFoot", LeftToeBase: "LeftFoot" };
45
- // Bone lengths measured on the performer, keyed by the cskel27 joint the bone
46
- // ENDS at, as [SMPL from, SMPL to] against [cskel27 from, cskel27 to]. Where
47
- // the two skeletons segment a limb differently the measurement spans the
48
- // whole segment on both sides and every link in it takes the one ratio:
49
- // - torso: SMPL's three spine joints do not sit where cskel27's four do, so
50
- // pelvis -> spine3 is one measurement over Spine..Spine3;
51
- // - shoulder girdle: SMPL's collar is a short stub (8 cm) where Mixamo's
52
- // clavicle is 16 cm, so spine3 -> shoulder covers Shoulder+Arm together
53
- // (per-link it read 0.79 / 0.57 and would have pinched the shoulders);
54
- // - neck, head, hand ends, thumb: geometry of the character, not a limb
55
- // the mocap measures. cskel27's Neck link (25 cm) already lands on the
56
- // Mixamo rig at 16 cm — the skin's neck is short by construction — so
57
- // scaling it by SMPL's 0.84 sank the neck into the chest (measured:
58
- // Spine2->Neck 16.5 -> 12.5 cm on the rig). Left at 1 like the head.
59
- const LENGTH_SOURCE = {
60
- Spine: [0, 9], Spine1: [0, 9], Spine2: [0, 9], Spine3: [0, 9],
61
- RightShoulder: [9, 17], RightArm: [9, 17], RightForeArm: [17, 19], RightHand: [19, 21],
62
- LeftShoulder: [9, 16], LeftArm: [9, 16], LeftForeArm: [16, 18], LeftHand: [18, 20],
63
- RightUpLeg: [0, 2], RightLeg: [2, 5], RightFoot: [5, 8], RightToeBase: [8, 11],
64
- LeftUpLeg: [0, 1], LeftLeg: [1, 4], LeftFoot: [4, 7], LeftToeBase: [7, 10],
65
- };
66
- const LENGTH_TARGET = {
67
- Spine: ["Hips", "Spine3"], Spine1: ["Hips", "Spine3"], Spine2: ["Hips", "Spine3"], Spine3: ["Hips", "Spine3"],
68
- RightShoulder: ["Spine3", "RightArm"], RightArm: ["Spine3", "RightArm"],
69
- LeftShoulder: ["Spine3", "LeftArm"], LeftArm: ["Spine3", "LeftArm"],
70
- };
71
-
72
- function data(value) {
73
- if (value && value.data !== undefined) return value.data;
74
- if (Array.isArray(value)) return value.flat(Infinity);
75
- return value;
76
- }
77
- function shape(value, expected, label) {
78
- const d = data(value);
79
- if (!d || d.length !== expected.reduce((a, b) => a * b, 1)) throw new Error(`${label} has the wrong size`);
80
- return d;
81
- }
82
- function vec(a) { const n = Math.hypot(...a); return n > 1e-12 ? a.map((v) => v / n) : [1, 0, 0]; }
83
- function cross(a, b) { return [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]; }
84
- function dot(a, b) { return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; }
85
- function axisAngle(axis, angle) {
86
- const [x, y, z] = axis, s = Math.sin(angle), c = Math.cos(angle), t = 1 - c;
87
- return [[t * x * x + c, t * x * y - s * z, t * x * z + s * y], [t * x * y + s * z, t * y * y + c, t * y * z - s * x], [t * x * z - s * y, t * y * z + s * x, t * z * z + c]];
88
- }
89
- function axisAngleMat(v) {
90
- const angle = Math.hypot(...v); return angle < 1e-12 ? IDENTITY.map((r) => r.slice()) : axisAngle(v.map((x) => x / angle), angle);
91
- }
92
- function swing(from, to) {
93
- const a = vec(from), b = vec(to), c = Math.max(-1, Math.min(1, dot(a, b)));
94
- if (c > 1 - 1e-10) return IDENTITY.map((r) => r.slice());
95
- let ax = cross(a, b), n = Math.hypot(...ax);
96
- if (n < 1e-10) {
97
- ax = Math.abs(a[0]) < 0.9 ? cross(a, [1, 0, 0]) : cross(a, [0, 1, 0]); n = Math.hypot(...ax);
98
- return axisAngle(ax.map((x) => x / n), Math.PI);
99
- }
100
- return axisAngle(ax.map((x) => x / n), Math.acos(c));
101
- }
102
- function readAa(flat, frame, joint) { const o = (frame * 23 + joint - 1) * 3; return [flat[o], flat[o + 1], flat[o + 2]]; }
103
- function sourceGlobals(go, pose, frames) {
104
- const worlds = new Array(frames);
105
- for (let f = 0; f < frames; f += 1) {
106
- const out = new Array(24); out[0] = axisAngleMat([go[f * 3], go[f * 3 + 1], go[f * 3 + 2]]);
107
- for (let j = 1; j < 24; j += 1) out[j] = matMul(out[SMPL_PARENTS[j]], axisAngleMat(readAa(pose, f, j)));
108
- worlds[f] = out;
109
- }
110
- return worlds;
111
- }
112
- function writeMat(out, offset, m) { out[offset] = m[0][0]; out[offset + 1] = m[0][1]; out[offset + 2] = m[0][2]; out[offset + 3] = m[1][0]; out[offset + 4] = m[1][1]; out[offset + 5] = m[1][2]; out[offset + 6] = m[2][0]; out[offset + 7] = m[2][1]; out[offset + 8] = m[2][2]; }
113
- function percentile(values, p) { const a = [...values].sort((x, y) => x - y); const x = (a.length - 1) * p, i = Math.floor(x), t = x - i; return a[i] * (1 - t) + (a[i + 1] ?? a[i]) * t; }
114
-
115
- export function smplToCskel27Motion(members, { boneScale: boneScaleOption = "derive" } = {}) {
116
- if (boneScaleOption !== "derive" && !(typeof boneScaleOption === "number" && Number.isFinite(boneScaleOption) && boneScaleOption > 0)) throw new Error('smplToCskel27Motion: boneScale must be "derive" or a positive number');
117
- const go = data(members.smpl_global_orient), poseMember = members.smpl_body_pose;
118
- const goShape = poseMember?.shape?.[0] ?? members.smpl_global_orient?.shape?.[0];
119
- const frames = Number.isInteger(goShape) ? goShape : go.length / 3;
120
- if (!Number.isInteger(frames) || frames < 1) throw new Error("smplToCskel27Motion: invalid frame count");
121
- shape(go, [frames, 3], "smpl_global_orient");
122
- const pose = shape(poseMember, [frames, 23, 3], "smpl_body_pose");
123
- const transl = shape(members.smpl_transl, [frames, 3], "smpl_transl");
124
- const rest = shape(members.smpl_rest_joints, [24, 3], "smpl_rest_joints");
125
- const fpsValue = data(members.fps); const fps = Number(fpsValue?.[0] ?? fpsValue);
126
- if (!(fps > 0)) throw new Error("smplToCskel27Motion: invalid fps");
127
- const skeleton = canonicalCskel27Reference();
128
- const cpos = skeleton.posed_joints;
129
- const restVec = (j) => [rest[j * 3], rest[j * 3 + 1], rest[j * 3 + 2]];
130
- const dist = (a, b) => Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]);
131
- const boneScale = new Float32Array(27).fill(typeof boneScaleOption === "number" ? boneScaleOption : 1);
132
- for (let j = 1; j < 27 && boneScaleOption === "derive"; j += 1) {
133
- const name = CSKEL27_JOINTS[j];
134
- if (!LENGTH_SOURCE[name]) continue;
135
- const [s0, s1] = LENGTH_SOURCE[name];
136
- const source = dist(restVec(s0), restVec(s1));
137
- const [t0, t1] = LENGTH_TARGET[name] ?? [CSKEL27_JOINTS[CSKEL27_PARENTS[j]], name];
138
- const target = dist(cpos[CSKEL27_JOINTS.indexOf(t0)], cpos[CSKEL27_JOINTS.indexOf(t1)]);
139
- if (source > 1e-6 && target > 1e-6) boneScale[j] = source / target;
140
- }
141
- const offsets = deriveBoneOffsets(cpos, skeleton.local_rot_mats).map((o, j) => [o[0] * boneScale[j], o[1] * boneScale[j], o[2] * boneScale[j]]);
142
- const corrections = {};
143
- for (const [name, sourceChild] of Object.entries(CHILD)) {
144
- const c = CSKEL27_JOINTS.indexOf(name);
145
- const targetChild = CSKEL27_PARENTS.findIndex((parent) => parent === c);
146
- if (c < 0 || targetChild < 0) continue;
147
- const source = MAP[name];
148
- const a = [cpos[targetChild][0] - cpos[c][0], cpos[targetChild][1] - cpos[c][1], cpos[targetChild][2] - cpos[c][2]];
149
- const b = [rest[sourceChild * 3] - rest[source * 3], rest[sourceChild * 3 + 1] - rest[source * 3 + 1], rest[sourceChild * 3 + 2] - rest[source * 3 + 2]];
150
- corrections[name] = swing(a, b);
151
- }
152
- for (const [name, ancestor] of Object.entries(CORRECTION_BORROW)) corrections[name] = corrections[ancestor] ?? IDENTITY;
153
- const worlds = sourceGlobals(go, pose, frames), rotMats = new Float32Array(frames * 27 * 9), rootPos = new Float32Array(frames * 3), posedJoints = new Float32Array(frames * 27 * 3);
154
- // The legs now have the performer's length, so the pelvis rides at its
155
- // filmed height: no ratio between the two skeletons is left to apply.
156
- const rootScale = 1;
157
- for (let f = 0; f < frames; f += 1) {
158
- const globals = new Array(27);
159
- for (let j = 0; j < 27; j += 1) {
160
- const name = CSKEL27_JOINTS[j], ancestor = BORROW[name];
161
- // body_pose is already a rotation FROM SMPL's zero pose. Torso/head
162
- // therefore keep cskel27's bind shape; only visible limb rest-pose
163
- // differences use the constant correction built above.
164
- globals[j] = ancestor ? globals[CSKEL27_JOINTS.indexOf(ancestor)] : matMul(worlds[f][MAP[name]], corrections[name] ?? IDENTITY);
165
- }
166
- for (let j = 0; j < 27; j += 1) {
167
- const p = CSKEL27_PARENTS[j], local = p === null ? globals[j] : matMul(matTranspose(globals[p]), globals[j]);
168
- writeMat(rotMats, (f * 27 + j) * 9, local);
169
- }
170
- rootPos[f * 3] = transl[f * 3] * rootScale; rootPos[f * 3 + 1] = transl[f * 3 + 1] * rootScale; rootPos[f * 3 + 2] = transl[f * 3 + 2] * rootScale;
171
- }
172
- const readLocals = (f) => Array.from({ length: 27 }, (_, j) => { const o = (f * 27 + j) * 9; return [[rotMats[o], rotMats[o + 1], rotMats[o + 2]], [rotMats[o + 3], rotMats[o + 4], rotMats[o + 5]], [rotMats[o + 6], rotMats[o + 7], rotMats[o + 8]]]; });
173
- const lows = new Array(frames);
174
- for (let f = 0; f < frames; f += 1) { const p = forwardKinematics(readLocals(f), offsets, [rootPos[f * 3], rootPos[f * 3 + 1], rootPos[f * 3 + 2]]); lows[f] = Math.min(...[21, 22, 25, 26].map((j) => p[j][1])); }
175
- const floor = percentile(lows, 0.1);
176
- for (let f = 0; f < frames; f += 1) { rootPos[f * 3 + 1] -= floor; const p = forwardKinematics(readLocals(f), offsets, [rootPos[f * 3], rootPos[f * 3 + 1], rootPos[f * 3 + 2]]); for (let j = 0; j < 27; j += 1) posedJoints.set(p[j], (f * 27 + j) * 3); }
177
- return { frames, fps, rotMats, rootPos, posedJoints, boneScale, personScale: 1, rawRootStart: [transl[0], transl[1], transl[2]] };
178
- }
@@ -1,196 +0,0 @@
1
- /**
2
- * Visual QA driver for CozyClay: drives headless Chrome over CDP with
3
- * Node's built-in WebSocket. Loads the studio, generates "a person walk"
4
- * with the default seed (2), then captures screenshots at several playback
5
- * frames for pose review.
6
- *
7
- * Usage: node tools/ardy/visual-qa.mjs <outDir> [url]
8
- * The default URL is the studio route (`/app/`), not the marketing landing page.
9
- */
10
- import { writeFileSync, mkdirSync } from "node:fs";
11
-
12
- const OUT = process.argv[2] || "/tmp/visual-qa";
13
- const URL = process.argv[3] || "http://127.0.0.1:5180/app/";
14
- mkdirSync(OUT, { recursive: true });
15
-
16
- const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
17
-
18
- // --- minimal CDP client -----------------------------------------------------
19
- async function newTab() {
20
- const res = await fetch("http://127.0.0.1:9222/json/new?about:blank", { method: "PUT" });
21
- const target = await res.json();
22
- const ws = new WebSocket(target.webSocketDebuggerUrl);
23
- await new Promise((resolve, reject) => {
24
- ws.onopen = resolve;
25
- ws.onerror = reject;
26
- });
27
- let id = 0;
28
- const pending = new Map();
29
- const consoleLog = [];
30
- ws.onmessage = (event) => {
31
- const msg = JSON.parse(event.data);
32
- if (msg.id && pending.has(msg.id)) {
33
- const { resolve, reject } = pending.get(msg.id);
34
- pending.delete(msg.id);
35
- if (msg.error) reject(new Error(msg.error.message));
36
- else resolve(msg.result);
37
- } else if (msg.method === "Runtime.consoleAPICalled") {
38
- const type = msg.params.type;
39
- const text = msg.params.args.map((a) => a.value ?? a.description ?? "").join(" ");
40
- if (type === "error" || type === "warning") consoleLog.push(`[${type}] ${text}`);
41
- } else if (msg.method === "Runtime.exceptionThrown") {
42
- consoleLog.push(`[exception] ${msg.params.exceptionDetails.text} ${msg.params.exceptionDetails.exception?.description ?? ""}`);
43
- }
44
- };
45
- const send = (method, params = {}) =>
46
- new Promise((resolve, reject) => {
47
- const msgId = ++id;
48
- pending.set(msgId, { resolve, reject });
49
- ws.send(JSON.stringify({ id: msgId, method, params }));
50
- });
51
- return { ws, send, consoleLog };
52
- }
53
-
54
- async function evaluate(tab, expression) {
55
- const res = await tab.send("Runtime.evaluate", {
56
- expression,
57
- awaitPromise: true,
58
- returnByValue: true,
59
- });
60
- if (res.exceptionDetails) {
61
- throw new Error(`evaluate failed: ${JSON.stringify(res.exceptionDetails.exception?.description || res.exceptionDetails.text)}`);
62
- }
63
- return res.result.value;
64
- }
65
-
66
- async function shot(tab, name) {
67
- const res = await tab.send("Page.captureScreenshot", { format: "png" });
68
- writeFileSync(`${OUT}/${name}.png`, Buffer.from(res.data, "base64"));
69
- console.log(`saved ${OUT}/${name}.png`);
70
- }
71
-
72
- // --- flow -------------------------------------------------------------------
73
- const tab = await newTab();
74
- await tab.send("Page.enable");
75
- await tab.send("Runtime.enable");
76
- await tab.send("Page.navigate", { url: URL });
77
- await sleep(6000);
78
-
79
- // Prompt Blocks replaced the old single "Generate motion" field. Select the
80
- // first character, open the Prompt Blocks foldout, then add a block before
81
- // looking for its prompt input and the top-bar Generate Motion action.
82
- await evaluate(tab, `(() => {
83
- const character = [...document.querySelectorAll('.hierarchy-row')].find((row) => /Character 1|Subject 1|인물 1/.test(row.textContent));
84
- character?.click();
85
- const foldout = [...document.querySelectorAll('.foldout-head')].find((button) => /Prompt Blocks|프롬프트 블록/.test(button.textContent));
86
- foldout?.click();
87
- const add = document.querySelector('.tl-track.prompts .tl-track-add') || document.querySelector('.tl-track-add[title*="prompt"]');
88
- add?.click();
89
- return true;
90
- })()`);
91
- await sleep(400);
92
- const state0 = await evaluate(tab, `(() => {
93
- const seed = document.querySelector('input[placeholder="empty = random"]');
94
- const promptEl = document.querySelector('input[placeholder*="motion block"], input[placeholder*="모션 블록"]');
95
- const gen = document.querySelector('[data-testid=topbar-generate]');
96
- return { seedValue: seed?.value ?? null, hasGenerate: !!gen, generateDisabled: gen?.getAttribute('aria-disabled') === 'true', hasPrompt: !!promptEl };
97
- })()`);
98
- console.log("initial:", JSON.stringify(state0));
99
-
100
- if (!state0.hasGenerate || !state0.hasPrompt) {
101
- await shot(tab, "00-no-ui");
102
- throw new Error("studio UI not ready");
103
- }
104
- if (state0.seedValue === null) throw new Error("Prompt Blocks panel did not open");
105
-
106
- // Set the prompt and generate: focus the field, then type via the Input
107
- // domain so React's controlled input sees real key events.
108
- await evaluate(tab, `(() => {
109
- const el = document.querySelector('input[placeholder*="motion block"], input[placeholder*="모션 블록"]');
110
- if (!el) return false;
111
- el.focus();
112
- el.select?.();
113
- return true;
114
- })()`);
115
- for (const key of ["a", " ", "p", "e", "r", "s", "o", "n", " ", "w", "a", "l", "k"]) {
116
- await tab.send("Input.dispatchKeyEvent", { type: "keyDown", text: key });
117
- await tab.send("Input.dispatchKeyEvent", { type: "keyUp", text: key });
118
- }
119
- await sleep(300);
120
- await shot(tab, "01-before-generate");
121
- const clicked = await evaluate(tab, `(() => {
122
- const gen = document.querySelector('[data-testid=topbar-generate]');
123
- if (!gen || gen.getAttribute('aria-disabled') === 'true') return false;
124
- gen.click();
125
- return true;
126
- })()`);
127
- if (!clicked) throw new Error("Kimodo bridge is unavailable or generation is disabled");
128
- console.log("generate clicked, waiting for the clip...");
129
-
130
- // Wait for "Motion loaded" or the PLAYBACK badge (generation ~12 s).
131
- let loaded = false;
132
- for (let i = 0; i < 40; i += 1) {
133
- await sleep(1000);
134
- const st = await evaluate(tab, `document.body.textContent.includes("Motion loaded") || document.body.textContent.includes("PLAYBACK")`);
135
- if (st) { loaded = true; break; }
136
- }
137
- console.log("motion loaded:", loaded);
138
- if (!loaded) {
139
- await shot(tab, "02-generation-timeout");
140
- throw new Error("motion did not load within 40 s");
141
- }
142
- await sleep(1500);
143
-
144
- // Scrub to specific frames by clicking the timeline's accessible ruler. The
145
- // old tick-text selector clicked labels, not the scrub surface, after the
146
- // timeline was redesigned.
147
- const frames = [0, 20, 40, 60, 79];
148
- for (const f of frames) {
149
- const point = await evaluate(tab, `(() => {
150
- const lane = document.querySelector('[role="slider"][aria-label*="Scrub timeline"], [role="slider"][aria-label*="타임라인"]');
151
- if (!lane) return null;
152
- const rect = lane.getBoundingClientRect();
153
- const max = Math.max(1, Number(lane.getAttribute('aria-valuemax')) || 1);
154
- return { x: rect.left + rect.width * Math.min(1, ${f} / max), y: rect.top + rect.height / 2 };
155
- })()`);
156
- if (!point) throw new Error("timeline scrub ruler not found");
157
- await tab.send("Input.dispatchMouseEvent", { type: "mouseMoved", x: point.x, y: point.y, button: "none" });
158
- await tab.send("Input.dispatchMouseEvent", { type: "mousePressed", x: point.x, y: point.y, button: "left", clickCount: 1 });
159
- await tab.send("Input.dispatchMouseEvent", { type: "mouseReleased", x: point.x, y: point.y, button: "left", clickCount: 1 });
160
- await sleep(700);
161
- await shot(tab, `10-frame-${String(f).padStart(2, "0")}`);
162
- }
163
-
164
- // Play a couple of seconds for a mid-stride live shot.
165
- await evaluate(tab, `(() => {
166
- const play = [...document.querySelectorAll("button")].find(b => b.textContent.trim() === "▶");
167
- play?.click();
168
- return true;
169
- })()`);
170
- await sleep(1600);
171
- await shot(tab, "20-playing-midstride");
172
- await sleep(1600);
173
- await shot(tab, "21-playing-later");
174
- await evaluate(tab, `(() => {
175
- const pause = [...document.querySelectorAll("button")].find(b => /❚❚|⏸/.test(b.textContent.trim()));
176
- pause?.click();
177
- return true;
178
- })()`);
179
-
180
- // Camera close-up on the subject for a detailed pose check.
181
- await evaluate(tab, `(() => {
182
- const chip = [...document.querySelectorAll("button")].find(b => /close-up/i.test(b.textContent));
183
- chip?.click();
184
- return true;
185
- })()`);
186
- await sleep(900);
187
- await shot(tab, "30-closeup");
188
-
189
- if (tab.consoleLog.length) {
190
- console.log("--- console errors/warnings ---");
191
- for (const line of [...new Set(tab.consoleLog)].slice(0, 15)) console.log(line);
192
- } else {
193
- console.log("no console errors/warnings captured");
194
- }
195
- tab.ws.close();
196
- console.log("visual QA capture complete");