cozyclay 2.0.2 → 2.1.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 (256) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/README.md +17 -2
  3. package/THIRD_PARTY_NOTICES.md +8 -0
  4. package/bin/agent/agent-routes.mjs +211 -36
  5. package/bin/agent/agent-runner.mjs +98 -4
  6. package/bin/agent/codex-client.mjs +2 -1
  7. package/bin/agent/fal-contract.mjs +0 -17
  8. package/bin/agent/h3-preservation.mjs +6 -8
  9. package/bin/agent/provider-keys.mjs +1 -1
  10. package/bin/agent/providers.mjs +124 -3
  11. package/bin/agent/script-tool.mjs +74 -0
  12. package/bin/agent/session-store.mjs +5 -5
  13. package/bin/agent/studio-prompt.mjs +2 -1
  14. package/bin/agent/studio-rebase.mjs +314 -0
  15. package/bin/agent/studio-tools.mjs +21 -14
  16. package/bin/agent/supervisor.mjs +311 -0
  17. package/bin/agent/video-adapters.mjs +0 -7
  18. package/bin/codex-auth.mjs +4 -4
  19. package/bin/live/cli.mjs +2 -3
  20. package/bin/live/client.mjs +1 -1
  21. package/bin/live/studio.mjs +5 -2
  22. package/bin/mcp-runtime.mjs +2 -2
  23. package/bin/telemetry-state.mjs +3 -3
  24. package/dist/app/index.html +6 -6
  25. package/dist/assets/analytics-B41FmPYI.js +1 -0
  26. package/dist/assets/app-Do-Og5Pu.css +1 -0
  27. package/dist/assets/app-DwP-tl2x.js +4920 -0
  28. package/dist/assets/{demo-ByeYp-dY.js → demo-Dt0qZfS7.js} +1 -1
  29. package/dist/assets/{first-shot-handoff-DgT28ZiV.js → first-shot-handoff-BlT33CGf.js} +1 -1
  30. package/dist/assets/{landing-BaXkpFip.js → landing-BSesrCpL.js} +1 -1
  31. package/dist/assets/scene-asset-sync-BCWDJB1d.js +19 -0
  32. package/dist/assets/{shot-prompt-CbqWcWYi.css → scene-asset-sync-DjJ_cJ2R.css} +1 -1
  33. package/dist/assets/{ticket-BcmsnHn5.js → ticket-jqMhqL88.js} +1 -1
  34. package/dist/assets/workflow-Br-K7Srs.js +179 -0
  35. package/dist/cozyclay-package.json +1 -1
  36. package/dist/index.html +328 -222
  37. package/dist/sitemap.xml +1 -1
  38. package/dist/workflow/index.html +4 -4
  39. package/mcp/LIVE-PROTOCOL.md +23 -2
  40. package/mcp/README.md +37 -2
  41. package/mcp/ardy-prompts.mjs +1 -1
  42. package/mcp/image-file.mjs +132 -0
  43. package/mcp/live-hub.mjs +2 -2
  44. package/mcp/package.json +1 -0
  45. package/mcp/runtime/package-lock.json +7 -0
  46. package/mcp/runtime/package.json +1 -0
  47. package/mcp/server.mjs +0 -6
  48. package/mcp/tool-handlers.mjs +289 -87
  49. package/package.json +11 -2
  50. package/src/App.jsx +789 -263
  51. package/src/agent-image-client.js +94 -0
  52. package/src/analytics.js +47 -3
  53. package/src/app-stage.jsx +129 -2745
  54. package/src/ardy/auto-physics.js +6 -16
  55. package/src/ardy/collision-blockers.js +3 -7
  56. package/src/ardy/fix-collisions.js +5 -35
  57. package/src/ardy/ik-drag.js +86 -5
  58. package/src/ardy/ik-key-json.js +3 -1
  59. package/src/ardy/ik.js +15 -13
  60. package/src/ardy/motion-calibration.js +1 -1
  61. package/src/ardy/npz.js +0 -8
  62. package/src/ardy/physics-panel.jsx +3 -2
  63. package/src/ardy/physics-review.js +3 -2
  64. package/src/ardy/physics-support.js +1 -1
  65. package/src/ardy/physics-surface.js +1 -1
  66. package/src/ardy/platform-fit-panel.jsx +2 -1
  67. package/src/ardy/playback.js +1 -5
  68. package/src/ardy/range-pin.js +2 -2
  69. package/src/ardy/root-drop.js +0 -1
  70. package/src/ardy/timeline.css +184 -6
  71. package/src/ardy/timeline.jsx +113 -28
  72. package/src/ardy/to-cskel27.js +0 -2
  73. package/src/ardy/waypoints.js +0 -3
  74. package/src/ardy-bridge-contract.js +281 -0
  75. package/src/asset-pane.jsx +19 -9
  76. package/src/auto-color.js +1 -1
  77. package/src/burn-in.js +1 -1
  78. package/src/camera-block.js +1 -1
  79. package/src/camera-follow.js +75 -32
  80. package/src/camera-move.js +0 -6
  81. package/src/camera-tutorial.jsx +6 -7
  82. package/src/character-kind.js +79 -0
  83. package/src/command-bus.js +76 -17
  84. package/src/commands/cast.js +31 -7
  85. package/src/commands/elements/object.js +1 -1
  86. package/src/commands/elements/shot.js +1 -1
  87. package/src/commands/elements.js +1 -1
  88. package/src/commands/export.js +3 -3
  89. package/src/commands/motion.js +2 -2
  90. package/src/commands/objects.js +105 -10
  91. package/src/commands/project.js +15 -2
  92. package/src/commands/sculpt-images.js +53 -0
  93. package/src/commands/shot.js +88 -10
  94. package/src/commands/view.js +10 -4
  95. package/src/coplanar-depth.js +216 -0
  96. package/src/cuts.js +7 -6
  97. package/src/domains/cast.js +60 -43
  98. package/src/domains/motion/motion-intent-codec.js +50 -0
  99. package/src/domains/motion/motion-range-pin-rules.js +23 -0
  100. package/src/domains/motion/motion-video-capture.js +664 -0
  101. package/src/domains/motion.js +253 -742
  102. package/src/domains/objects.js +106 -5
  103. package/src/domains/scenes.js +79 -28
  104. package/src/domains/shots.js +33 -7
  105. package/src/error-boundary.jsx +1 -1
  106. package/src/execution-telemetry.js +5 -5
  107. package/src/facing-marks.js +14 -1
  108. package/src/first-success-guide.jsx +35 -4
  109. package/src/gizmo-claim.js +3 -1
  110. package/src/grid-floor.jsx +1 -4
  111. package/src/hierarchy-model.js +48 -35
  112. package/src/hierarchy-panel.css +1 -0
  113. package/src/hierarchy-panel.jsx +54 -102
  114. package/src/history.js +0 -2
  115. package/src/i2v-motion-client.js +1 -4
  116. package/src/ime.js +6 -0
  117. package/src/keyframe-pack.js +15 -6
  118. package/src/line-edit.js +4 -4
  119. package/src/live-control.js +6 -6
  120. package/src/matte-editor.js +2 -8
  121. package/src/matte.js +0 -19
  122. package/src/mesh-graph-clone.js +16 -2
  123. package/src/mesh-sniff.js +0 -1
  124. package/src/monotone-cubic.js +40 -0
  125. package/src/motion-store.js +4 -4
  126. package/src/motion-trail.js +5 -11
  127. package/src/multimodel-ingest.js +1 -3
  128. package/src/object-catalog.jsx +7 -5
  129. package/src/object-gizmo.jsx +45 -31
  130. package/src/object-path.js +310 -67
  131. package/src/object-travel.js +224 -0
  132. package/src/otio.js +8 -3
  133. package/src/panels/CharacterTransformPanel.jsx +24 -6
  134. package/src/panels/ObjectTransformPanel.jsx +4 -3
  135. package/src/panels/PoseImagePanel.jsx +205 -0
  136. package/src/panels/PromptBlocksPanel.jsx +3 -1
  137. package/src/panels/ProxyFigurePanel.jsx +56 -0
  138. package/src/panels/ReferenceImageField.jsx +1 -1
  139. package/src/panels/RigControlPanel.jsx +24 -3
  140. package/src/panels/RigPanel.jsx +2 -1
  141. package/src/panels/SculptPanel.jsx +405 -0
  142. package/src/panels/SubjectBox.jsx +1 -1
  143. package/src/panels/SubjectsPanel.jsx +0 -2
  144. package/src/panels/VideoCapturePanel.jsx +2 -1
  145. package/src/panels/pose-image.css +42 -0
  146. package/src/panels/pose.css +3 -0
  147. package/src/panels/sculpt.css +162 -0
  148. package/src/plan-diagram.js +184 -0
  149. package/src/planview.jsx +127 -80
  150. package/src/playground.js +3 -5
  151. package/src/pose-fade.js +77 -0
  152. package/src/pose-gizmo.css +23 -0
  153. package/src/pose-gizmo.js +96 -0
  154. package/src/posestudio.jsx +134 -83
  155. package/src/project-browser.css +53 -0
  156. package/src/project-browser.jsx +49 -4
  157. package/src/project.js +55 -24
  158. package/src/props.jsx +177 -36
  159. package/src/proxy-figure.jsx +69 -0
  160. package/src/range-pin-panel.jsx +0 -1
  161. package/src/receipt-findings.js +20 -0
  162. package/src/render-passes.js +0 -1
  163. package/src/resource-status.jsx +1 -1
  164. package/src/root-path.js +48 -0
  165. package/src/route-owner.js +52 -0
  166. package/src/scene-asset-cache.js +1 -2
  167. package/src/scene-assets.js +12 -4
  168. package/src/scene-fbx.js +1 -1
  169. package/src/scene-mesh-cache.js +0 -1
  170. package/src/scene-objects.js +319 -19
  171. package/src/scenes.js +58 -9
  172. package/src/sculpt-blueprint.js +65 -0
  173. package/src/sculpt-compare.js +384 -0
  174. package/src/sculpt-geometry.js +141 -0
  175. package/src/sculpt-mode.js +168 -0
  176. package/src/sculpt-recipe.js +385 -0
  177. package/src/sculpt-run.js +351 -0
  178. package/src/sculpt-stage.jsx +221 -0
  179. package/src/settings-menu.jsx +1 -1
  180. package/src/shell/BoardSlot.jsx +341 -0
  181. package/src/shell/BottomDock.jsx +58 -14
  182. package/src/shell/DetailsSlot.jsx +26 -6
  183. package/src/shell/MenuBar.jsx +78 -6
  184. package/src/shell/OutlinerSlot.jsx +3 -1
  185. package/src/shell/TopBar.jsx +17 -3
  186. package/src/shell/ViewportToolbar.jsx +18 -3
  187. package/src/shell/dock.css +239 -0
  188. package/src/shell/glass-regions.css +3 -2
  189. package/src/shell/log-store.js +4 -4
  190. package/src/shell/mode.css +20 -0
  191. package/src/shell/studio-shell-context.js +4 -1
  192. package/src/shot-authoring.js +69 -6
  193. package/src/stable-items.js +1 -1
  194. package/src/stage-attach-frames.js +169 -0
  195. package/src/stage-canvas-rigs.jsx +402 -0
  196. package/src/stage-capture.jsx +216 -0
  197. package/src/stage-characters.jsx +302 -0
  198. package/src/stage-editor-gizmos.jsx +1229 -0
  199. package/src/stage-labels-ko.js +204 -0
  200. package/src/stage-startup-defaults.js +182 -0
  201. package/src/starter-scenes.js +1 -1
  202. package/src/storyboard.js +24 -5
  203. package/src/studio-actions.js +42 -9
  204. package/src/studio-agent-commands.js +186 -23
  205. package/src/studio-agent-context.js +11 -9
  206. package/src/studio-agent-motion.js +2 -1
  207. package/src/studio-agent-protocol.js +56 -23
  208. package/src/studio-app-binding.js +52 -23
  209. package/src/studio-contact-sheet.js +3 -3
  210. package/src/studio-elements.js +11 -2
  211. package/src/studio-geometry-facts.js +114 -0
  212. package/src/styles.css +92 -9
  213. package/src/ui-scrub.js +4 -4
  214. package/src/ui.jsx +6 -4
  215. package/src/use-asset-shelf.js +60 -0
  216. package/src/use-camera-tutorial.js +50 -0
  217. package/src/use-embed-bridge.js +12 -0
  218. package/src/use-first-success-prompt.js +7 -0
  219. package/src/use-i2v-capture-modal.js +15 -0
  220. package/src/use-sculpt-mode.js +95 -0
  221. package/src/workflow/AgentPanel.jsx +106 -18
  222. package/src/workflow/WorkflowBuilder.jsx +1 -1
  223. package/src/workflow/agent-client.js +113 -45
  224. package/src/workflow/agent-panel.css +35 -0
  225. package/src/workflow/attachment-image.js +1 -1
  226. package/src/workflow/canvas-commands.js +3 -3
  227. package/src/workflow/cozy-scene-node.js +3 -3
  228. package/src/workflow/local-workflow.js +2 -2
  229. package/src/workflow/motion-input.js +3 -3
  230. package/src/workflow/node-schema.js +1 -1
  231. package/src/workflow/scene-asset-sync.js +2 -9
  232. package/src/workflow/shot-prompt-node.js +1 -1
  233. package/src/workflow/video-contract.js +1 -1
  234. package/src/workflow/workflow-resources.js +0 -8
  235. package/src/workflow-send.js +87 -0
  236. package/tools/ardy/__pycache__/cclay_gvhmr_worker.cpython-313.pyc +0 -0
  237. package/tools/check-eslint.mjs +57 -0
  238. package/tools/check-knip.mjs +60 -0
  239. package/tools/check-loc.mjs +54 -0
  240. package/tools/demo-worker/api-client.mjs +7 -13
  241. package/tools/demo-worker/generate.mjs +2 -4
  242. package/tools/eslint-baseline.json +23 -0
  243. package/tools/kimodo/effector-constraints.mjs +4 -4
  244. package/tools/kimodo/generate.mjs +1 -1
  245. package/tools/knip-baseline.json +331 -0
  246. package/tools/loc-baseline.json +126 -0
  247. package/tools/process-supervisor.mjs +1 -1
  248. package/tools/projflow/replay.mjs +4 -4
  249. package/tools/projflow/runner.mjs +3 -3
  250. package/tools/qa/studio-control-count.mjs +2 -0
  251. package/tools/run-tests.mjs +94 -7
  252. package/dist/assets/analytics-MVZOEW8O.js +0 -1
  253. package/dist/assets/app-BxBLC6GT.js +0 -4876
  254. package/dist/assets/app-CJdxvnrl.css +0 -1
  255. package/dist/assets/shot-prompt-hN0j_O-p.js +0 -17
  256. package/dist/assets/workflow-B5XZwVfe.js +0 -179
package/dist/sitemap.xml CHANGED
@@ -2,7 +2,7 @@
2
2
  <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
3
3
  <url>
4
4
  <loc>https://cozyclay.org/</loc>
5
- <lastmod>2026-10-01</lastmod>
5
+ <lastmod>2026-10-08</lastmod>
6
6
  </url>
7
7
  <url>
8
8
  <loc>https://cozyclay.org/greybox-to-video/</loc>
@@ -8,11 +8,11 @@
8
8
  <meta name="theme-color" content="#101116" />
9
9
  <meta name="description" content="CozyClay visual workflow builder" />
10
10
  <title>CozyClay Workflow</title>
11
- <script type="module" crossorigin src="/assets/workflow-B5XZwVfe.js"></script>
11
+ <script type="module" crossorigin src="/assets/workflow-Br-K7Srs.js"></script>
12
12
  <link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-hePW80VL.js">
13
- <link rel="modulepreload" crossorigin href="/assets/analytics-MVZOEW8O.js">
14
- <link rel="modulepreload" crossorigin href="/assets/shot-prompt-hN0j_O-p.js">
15
- <link rel="stylesheet" crossorigin href="/assets/shot-prompt-CbqWcWYi.css">
13
+ <link rel="modulepreload" crossorigin href="/assets/analytics-B41FmPYI.js">
14
+ <link rel="modulepreload" crossorigin href="/assets/scene-asset-sync-BCWDJB1d.js">
15
+ <link rel="stylesheet" crossorigin href="/assets/scene-asset-sync-DjJ_cJ2R.css">
16
16
  <link rel="stylesheet" crossorigin href="/assets/workflow-DRdwoDBT.css">
17
17
  </head>
18
18
  <body>
@@ -144,11 +144,11 @@ scene document already uses.
144
144
  | name | args | value | notes |
145
145
  | --- | --- | --- | --- |
146
146
  | `ping` | `{}` | `{ "pong": true }` | liveness |
147
- | `describe` | `{}` | `{ document, sceneName, camera, stage, timeline, activeCharacterId, characters, objects }` | read the full live scene document plus active-scene convenience fields; the full `document` prevents one workspace's inactive scenes from leaking into another workspace's scene operations; `activeCharacterId` pins asynchronous work to the character selected when it started |
147
+ | `describe` | `{}` | `{ document, sceneName, previsMode, camera, stage, timeline, activeCharacterId, characters, objects }` | read the full live scene document plus active-scene convenience fields; the full `document` prevents one workspace's inactive scenes from leaking into another workspace's scene operations; `activeCharacterId` pins asynchronous work to the character selected when it started; `previsMode` (`"storyboard"` or `"animation"`, optional for older editors) is the project's previs mode, and a cast entry whose `model` is `proxy-figure` is a capsule figure that `describe_scene` reports as `kind: proxy` (otherwise `kind: rig`) |
148
148
  | `capture_frame` | `{}` | `{ width: 640, height: 360, mimeType: "image/png", encoding: "base64", byteSize, data, assertions: { renderable, blackFrame, nonBlackPixels, behindCameraPlane, fartherAlongCameraForward, distanceToFloor, occludedBy, visiblePixelCount, characters } }` | leaves the authored document untouched, but is classified open-world/non-idempotent because an oversized PNG creates a mode-0600 managed temporary artifact. Character visibility and occlusion are computed from mounted engine geometry with bounded ray samples. Missing camera, black frame and compressed payloads above 1 MB fail explicitly. Inline responses honor `max_inline_bytes`; managed artifacts are capped at 20 and expire after 10 minutes. |
149
149
  | `capture_framing_png` | `{}` | `{ dataUrl, width: 1920, height: 1080, frame, shotId }` | full-resolution PNG (data URL) of the CURRENT shot camera framing at the current timeline frame, rendered through the same park-and-restore shot pipeline the editor's own exports use; `width`/`height` follow the shot aspect preset (1920x1080 at 16:9) and `shotId` names the shot under the playhead. The viewport and the authored document are untouched (idempotent read), and the editor's 640x360 `capture_frame` contract is unchanged. Fails while no shot camera is mounted (e.g. a cast model is still downloading). |
150
150
  | `set_camera` | `{ x?, y?, z?, focalMm?, lookAtX?, lookAtY?, lookAtZ? }` | `{ camera }` | omitted fields keep their value; the **viewport must visibly move**. The `lookAt*` triple is additive to v1 and must arrive complete or not at all: given it, the editor aims the shot camera at that world point and records the resulting orientation as the camera's own (so the framing it commits is the one it just applied); without it the orientation is untouched, so an editor that ignores the fields degrades to the pre-aim behaviour instead of failing. `frame_shot` always sends the framing pivot, because the shot vocabulary (`deriveShot`, `captureFraming`) measures the shot as if the lens points at that pivot — a placed-but-unaimed camera reports a framing it is not holding. |
151
- | `add_character` | `{ subject, x?, z?, rot?, model? }` | `{ id }` | `model` is one of the stable character model ids |
151
+ | `add_character` | `{ subject, x?, z?, rot?, model?, posture? }` | `{ id }` | `model` is one of the stable character model ids, or `proxy-figure` for a rigless capsule figure; `posture` (`stand`, `sit`, `lie`; `stand` when omitted) applies to a capsule figure |
152
152
  | `update_character` | `{ ref, x?, y?, z?, rot?, subject?, hidden? }` | `{ id }` | `ref` = id, letter (`"A"`) or 1-based slot |
153
153
  | `remove_character` | `{ ref }` | `{ id }` | must refuse to empty the cast |
154
154
  | `place_object` | `{ kind, x?, z?, y?, rot?, name?, parent? }` | `{ id }` | `kind` from OBJECT_LIBRARY; optional `name` labels the object and optional `parent` attaches it under another object |
@@ -162,6 +162,27 @@ scene document already uses.
162
162
  | `load_motion` | `{ url, prompt?, blocks?, drop? }` | `{ loaded, url, blocks }` | install an ARDY motion on the selected character. It has the dedicated 30-second editor-processing timeout; motion-job completion carries the character id captured when generation started |
163
163
  | `load_scenes` | `{ document }` | `{ sceneName, activeSceneId, scenes: [{ id, name }] }` | replace the whole scene document (same shape `serializeSceneDocument` emits); the response attests the full scene list and active scene for `add_scene` and `switch_scene` parity |
164
164
 
165
+ ### Previs mode and character kind in the Studio context
166
+
167
+ `inspect_studio` returns the `studio-context-v1` context that every `run_action`
168
+ is admitted against. Two parts of it describe what a project and its cast can do:
169
+
170
+ | field | value | meaning |
171
+ | --- | --- | --- |
172
+ | `scene.previsMode` | `"storyboard"` \| `"animation"` | the project's previs mode; optional for older editors |
173
+ | `capabilities.storyboard` | boolean | `true` exactly when `scene.previsMode` is `storyboard` |
174
+ | `entities[].characterKind` | `"rig"` \| `"proxy"` | a rigged model, or a rigless capsule figure (`model: "proxy-figure"`) |
175
+ | `entities[].posture` | `"stand"` \| `"sit"` \| `"lie"` | a capsule figure's posture; absent for a rig |
176
+
177
+ Before an animation-only tool runs, the server reads these fields from the
178
+ context it admits the command against (`mark_camera_move`, which sends no
179
+ command, reads `previsMode` from `describe`). In a storyboard project `generate_motion`, `set_prompt_blocks`, `load_motion` and
180
+ `mark_camera_move` answer `{ ok: false, code: "NOT_IN_MODE", reason }` and send
181
+ no command; `generate_motion` and `load_motion` on a capsule figure answer
182
+ `{ ok: false, code: "TARGET_NOT_READY", reason }` with the kind's reason. The
183
+ editor's own motion, take, IK and pose commands refuse a capsule figure with
184
+ `TARGET_NOT_READY` as well, so `studio_run` reaches the same answer.
185
+
165
186
  ## Hard rules for the editor side
166
187
 
167
188
  - Every mutation MUST go through the same React state paths the UI itself
package/mcp/README.md CHANGED
@@ -35,7 +35,7 @@ Then point a client at it. For Claude Desktop, in `claude_desktop_config.json`:
35
35
  }
36
36
  ```
37
37
 
38
- Restart the client; 26 tools appear.
38
+ Restart the client; 28 tools appear.
39
39
 
40
40
  Prefer a long-lived endpoint? `node server.mjs --http 5183` serves Streamable HTTP at
41
41
  `http://127.0.0.1:5183/mcp` (one isolated session per client), with a plain status page at `/`.
@@ -68,10 +68,12 @@ generated frame matches the blocking instead of drifting off into a generic shot
68
68
  | `capture_frame` | a compressed live render with camera-plane, visibility and occlusion assertions |
69
69
  | `set_camera` | move the lens / change focal length directly |
70
70
  | `frame_shot` | frame by intent — size, view, level, side |
71
- | `add_character` / `place_character` / `remove_character` | the cast |
71
+ | `add_character` / `place_character` / `remove_character` | the cast (`add_character { model: "proxy-figure", posture: "stand" \| "sit" \| "lie" }` adds a rigless capsule figure) |
72
72
  | `focus_character` | choose who the camera frames |
73
73
  | `place_object` / `update_object` / `remove_object` | the set — `place_object` also accepts `name` and `parent`, so multi-part assets like "Building A" land as one named assembly |
74
74
  | `import_mesh` | load a local GLB, OBJ or FBX from a filesystem path as a mesh prop (live editor required); optional floor position, yaw, standing height, clay |
75
+ | `sculpt_object` | build a clay prop from a recipe of soft parts (blob, box, cylinder, torus, frame), usually from a reference image; pass `id` with `recipe` to re-sculpt or with `edits` to patch parts; `capture: true` returns the frame too. Works without an editor. Contract: `docs/sculpt-from-image.md` |
76
+ | `compare_sculpt` | score a sculpt's silhouette against its reference image (PNG/JPEG path): IoU, proportion delta, missing area by height band, parts that stick out, and a reference \| overlay sheet; `candidates` ranks up to 12 edit sets without applying any. Works without an editor |
75
77
  | `group_objects` | attach children to a parent so they move as one; pass `parent: null` to detach |
76
78
  | `apply_batch` | execute up to 100 object mutations as one user-visible undo transaction |
77
79
  | `render_prompt` | the shot as an AI image or video prompt |
@@ -131,6 +133,39 @@ result onto the active character with prompt blocks on the timeline. Beats longe
131
133
  per-block limit are split into consecutive blocks, so one phase can produce multiple blocks.
132
134
  Pass a previous `motion_url` to reload a clip without generating again.
133
135
 
136
+ ## Storyboard projects and capsule figures
137
+
138
+ The tool list is fixed when the server starts, so it is the same 28 tools in every project. A
139
+ tool that does not apply to the project or to its target answers a structured refusal instead
140
+ of acting; nothing is sent to the editor and nothing changes. The refusal is the tool's only
141
+ text content, with `isError: true`:
142
+
143
+ ```json
144
+ { "ok": false, "code": "NOT_IN_MODE", "reason": "This project is a Storyboard - generate_motion is available in Animation projects." }
145
+ ```
146
+
147
+ | code | when | tools |
148
+ | --- | --- | --- |
149
+ | `NOT_IN_MODE` | the project's `previsMode` is `storyboard` | `generate_motion`, `set_prompt_blocks`, `load_motion`, `mark_camera_move` |
150
+ | `TARGET_NOT_READY` | the target character is a capsule figure (`model: "proxy-figure"`) | `generate_motion` (the editor's active character), `load_motion` (its `character`, else the active one) |
151
+
152
+ `describe_scene` reports `previsMode: storyboard` or `previsMode: animation` (the connected
153
+ editor's project; `open_project` sets it without one) and marks each cast member
154
+ `kind: rig` or `kind: proxy`, with a capsule figure's `posture`.
155
+
156
+ A **storyboard project** is a sequence of still shots: one picture per panel, held for a
157
+ number of frames, with a caption. Block it with the camera, cast and set tools, and run the
158
+ still-shot commands (`shot.createStill`, `shot.setCaption`, `shot.setHold`, ...) through
159
+ `studio_run`. Motion, prompt blocks and named camera moves belong to Animation projects.
160
+
161
+ A **capsule figure** is a rigless stand-in for blocking: `add_character { subject, model:
162
+ "proxy-figure", posture: "sit" }`. It places, turns and frames like any cast member and holds a
163
+ posture (`stand`, `sit` or `lie`), but it has no skeleton, so motion and takes are refused with
164
+ `TARGET_NOT_READY` and the kind's reason, for example `Capsule figures have no rig - Motion
165
+ generation works on rigged characters only.` The editor refuses its own IK, pose, physics,
166
+ collision and line-edit commands on a capsule figure with the same code when they arrive
167
+ through `studio_run`. Use a rigged model for anything that moves a body.
168
+
134
169
  ## Round-trips with the studio
135
170
 
136
171
  `save_project` writes a real `.cclayproject` file, so a scene blocked here opens in the studio to
@@ -200,7 +200,7 @@ export function normalizePhases(phases, max = 8) {
200
200
  const expanded = [];
201
201
  const sources = [];
202
202
  for (const [index, phase] of phases.entries()) {
203
- expanded.push(String(phase ?? ""));
203
+ expanded.push(phase);
204
204
  // Which input each output came from, so a caller that attached a duration
205
205
  // to a beat can still line its beats up with the prompts they produced.
206
206
  sources.push(index);
@@ -0,0 +1,132 @@
1
+ // Reference images for sculpt comparison: read a PNG or JPEG from a path into
2
+ // RGBA, and write a PNG back. PNG is decoded here (zlib + the five filters);
3
+ // JPEG goes through jpeg-js. Nothing is sent to a model — the pixels become a
4
+ // silhouette mask on this machine.
5
+ import { constants as fsConstants } from "node:fs";
6
+ import { open as openFile } from "node:fs/promises";
7
+ import { deflateSync, inflateSync } from "node:zlib";
8
+ import jpeg from "jpeg-js";
9
+
10
+ import { normalizeMeshPath } from "./mesh-file.mjs";
11
+
12
+ export const REFERENCE_MAX_BYTES = 20 * 1024 * 1024;
13
+ const MAX_PIXELS = 40_000_000;
14
+ const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
15
+
16
+ function paeth(a, b, c) {
17
+ const p = a + b - c, pa = Math.abs(p - a), pb = Math.abs(p - b), pc = Math.abs(p - c);
18
+ return pa <= pb && pa <= pc ? a : pb <= pc ? b : c;
19
+ }
20
+
21
+ /** 8-bit, non-interlaced PNG (grey, grey+alpha, RGB, RGBA, palette) to RGBA. */
22
+ export function decodePng(bytes) {
23
+ const buf = Buffer.from(bytes);
24
+ if (buf.length < 8 || !buf.subarray(0, 8).equals(PNG_SIGNATURE)) throw new Error("not a PNG");
25
+ let offset = 8, width = 0, height = 0, depth = 0, type = 0, interlace = 0, palette = null, transparency = null;
26
+ const idat = [];
27
+ while (offset + 8 <= buf.length) {
28
+ const length = buf.readUInt32BE(offset), kind = buf.toString("latin1", offset + 4, offset + 8);
29
+ const data = buf.subarray(offset + 8, offset + 8 + length);
30
+ if (kind === "IHDR") {
31
+ width = data.readUInt32BE(0);
32
+ height = data.readUInt32BE(4);
33
+ depth = data[8];
34
+ type = data[9];
35
+ interlace = data[12];
36
+ } else if (kind === "PLTE") palette = data;
37
+ else if (kind === "tRNS") transparency = data;
38
+ else if (kind === "IDAT") idat.push(data);
39
+ else if (kind === "IEND") break;
40
+ offset += 12 + length;
41
+ }
42
+ if (!width || !height) throw new Error("PNG has no image header");
43
+ if (width * height > MAX_PIXELS) throw new Error(`image is too large (${width}x${height})`);
44
+ if (depth !== 8 || interlace !== 0) throw new Error("only 8-bit non-interlaced PNGs are supported; re-save it or use a JPEG");
45
+ const channels = { 0: 1, 2: 3, 3: 1, 4: 2, 6: 4 }[type];
46
+ if (!channels) throw new Error(`unsupported PNG colour type ${type}`);
47
+ const raw = inflateSync(Buffer.concat(idat));
48
+ const stride = width * channels;
49
+ const pixels = Buffer.alloc(stride * height);
50
+ for (let y = 0; y < height; y += 1) {
51
+ const filter = raw[y * (stride + 1)];
52
+ const line = raw.subarray(y * (stride + 1) + 1, (y + 1) * (stride + 1));
53
+ for (let x = 0; x < stride; x += 1) {
54
+ const a = x >= channels ? pixels[y * stride + x - channels] : 0;
55
+ const b = y > 0 ? pixels[(y - 1) * stride + x] : 0;
56
+ const c = x >= channels && y > 0 ? pixels[(y - 1) * stride + x - channels] : 0;
57
+ const value = line[x];
58
+ pixels[y * stride + x] = (filter === 0 ? value : filter === 1 ? value + a : filter === 2 ? value + b : filter === 3 ? value + ((a + b) >> 1) : value + paeth(a, b, c)) & 0xff;
59
+ }
60
+ }
61
+ const rgba = new Uint8Array(width * height * 4);
62
+ for (let i = 0; i < width * height; i += 1) {
63
+ const s = i * channels, d = i * 4;
64
+ if (type === 3) {
65
+ const index = pixels[s];
66
+ rgba[d] = palette?.[index * 3] ?? 0; rgba[d + 1] = palette?.[index * 3 + 1] ?? 0; rgba[d + 2] = palette?.[index * 3 + 2] ?? 0;
67
+ rgba[d + 3] = transparency && index < transparency.length ? transparency[index] : 255;
68
+ } else if (type === 0 || type === 4) {
69
+ rgba[d] = rgba[d + 1] = rgba[d + 2] = pixels[s];
70
+ rgba[d + 3] = type === 4 ? pixels[s + 1] : 255;
71
+ } else {
72
+ rgba[d] = pixels[s]; rgba[d + 1] = pixels[s + 1]; rgba[d + 2] = pixels[s + 2];
73
+ rgba[d + 3] = type === 6 ? pixels[s + 3] : 255;
74
+ }
75
+ }
76
+ return { width, height, data: rgba };
77
+ }
78
+
79
+ const CRC_TABLE = Array.from({ length: 256 }, (_, n) => {
80
+ let c = n;
81
+ for (let k = 0; k < 8; k += 1) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
82
+ return c >>> 0;
83
+ });
84
+ function crc32(bytes) {
85
+ let c = 0xffffffff;
86
+ for (const byte of bytes) c = CRC_TABLE[(c ^ byte) & 0xff] ^ (c >>> 8);
87
+ return (c ^ 0xffffffff) >>> 0;
88
+ }
89
+ function chunk(kind, data) {
90
+ const head = Buffer.alloc(8);
91
+ head.writeUInt32BE(data.length, 0);
92
+ head.write(kind, 4, "latin1");
93
+ const crc = Buffer.alloc(4);
94
+ crc.writeUInt32BE(crc32(Buffer.concat([head.subarray(4), data])), 0);
95
+ return Buffer.concat([head, data, crc]);
96
+ }
97
+
98
+ /** RGBA to a PNG buffer (filter 0, deflate level 6). */
99
+ export function encodePng(width, height, rgba) {
100
+ const header = Buffer.alloc(13);
101
+ header.writeUInt32BE(width, 0);
102
+ header.writeUInt32BE(height, 4);
103
+ header[8] = 8; header[9] = 6;
104
+ const raw = Buffer.alloc((width * 4 + 1) * height);
105
+ for (let y = 0; y < height; y += 1) Buffer.from(rgba.buffer, rgba.byteOffset + y * width * 4, width * 4).copy(raw, y * (width * 4 + 1) + 1);
106
+ return Buffer.concat([PNG_SIGNATURE, chunk("IHDR", header), chunk("IDAT", deflateSync(raw, { level: 6 })), chunk("IEND", Buffer.alloc(0))]);
107
+ }
108
+
109
+ /** A PNG or JPEG from a local path (file:// and ~/ accepted) as RGBA. */
110
+ export async function readImageFromPath(raw) {
111
+ const path = normalizeMeshPath(raw);
112
+ let file;
113
+ try {
114
+ file = await openFile(path, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
115
+ } catch (error) {
116
+ throw new Error(`Could not read the reference image: ${error.message}`);
117
+ }
118
+ try {
119
+ const stat = await file.stat();
120
+ if (stat.isDirectory()) throw new Error("The reference path is a directory, not an image.");
121
+ if (stat.size > REFERENCE_MAX_BYTES) throw new Error(`The reference image is larger than ${REFERENCE_MAX_BYTES / (1024 * 1024)} MB.`);
122
+ const bytes = await file.readFile();
123
+ if (bytes.subarray(0, 8).equals(PNG_SIGNATURE)) return decodePng(bytes);
124
+ if (bytes[0] === 0xff && bytes[1] === 0xd8) {
125
+ const image = jpeg.decode(bytes, { useTArray: true, formatAsRGBA: true, maxResolutionInMP: MAX_PIXELS / 1e6, maxMemoryUsageInMB: 512 });
126
+ return { width: image.width, height: image.height, data: image.data };
127
+ }
128
+ throw new Error("The reference must be a PNG or a JPEG.");
129
+ } finally {
130
+ await file.close();
131
+ }
132
+ }
package/mcp/live-hub.mjs CHANGED
@@ -18,7 +18,7 @@ export const STUDIO_COMMAND_TIMEOUT_MS = 30_000;
18
18
  const studioCommands = new Set([
19
19
  "inspect_studio", "operate_studio", "arrange_objects", "arrange_characters", "patch_elements",
20
20
  "frame_shot", "verify_result", "undo_edit", "read_studio_context", "resolve_studio_image",
21
- "capture_framing_png", "reconcile_studio_command", "run_action",
21
+ "capture_framing_png", "capture_plan_png", "reconcile_studio_command", "run_action",
22
22
  ]);
23
23
 
24
24
  const mutationCommands = new Set([
@@ -224,7 +224,7 @@ export class LiveHub {
224
224
  baseline(value) { if (mutation) mutation.before = mutationState(mutation.name, value); },
225
225
  receipt(command, value) {
226
226
  if (command === "inspect_studio") {
227
- studioContext = value?.context;
227
+ studioContext = value?.context ?? (value?.host && value?.revision ? { host: value.host, revision: value.revision } : undefined);
228
228
  } else if (command === "run_action") {
229
229
  // The bus attests application itself. No legacy describe diff or
230
230
  // post-ack read may turn an admitted refusal into uncertainty.
package/mcp/package.json CHANGED
@@ -21,6 +21,7 @@
21
21
  },
22
22
  "dependencies": {
23
23
  "@modelcontextprotocol/sdk": "^1.30.0",
24
+ "jpeg-js": "0.4.4",
24
25
  "ws": "^8.19.0",
25
26
  "zod": "^3.25.0"
26
27
  }
@@ -9,6 +9,7 @@
9
9
  "version": "0.1.0",
10
10
  "dependencies": {
11
11
  "@modelcontextprotocol/sdk": "1.30.0",
12
+ "jpeg-js": "0.4.4",
12
13
  "three": "0.185.1",
13
14
  "ws": "8.21.3",
14
15
  "zod": "3.25.76"
@@ -677,6 +678,12 @@
677
678
  "url": "https://github.com/sponsors/panva"
678
679
  }
679
680
  },
681
+ "node_modules/jpeg-js": {
682
+ "version": "0.4.4",
683
+ "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz",
684
+ "integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==",
685
+ "license": "BSD-3-Clause"
686
+ },
680
687
  "node_modules/json-schema-traverse": {
681
688
  "version": "1.0.0",
682
689
  "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
@@ -9,6 +9,7 @@
9
9
  },
10
10
  "dependencies": {
11
11
  "@modelcontextprotocol/sdk": "1.30.0",
12
+ "jpeg-js": "0.4.4",
12
13
  "three": "0.185.1",
13
14
  "ws": "8.21.3",
14
15
  "zod": "3.25.76"
package/mcp/server.mjs CHANGED
@@ -59,8 +59,6 @@ import {
59
59
  sweepCaptureArtifacts,
60
60
  } from "./tool-handlers.mjs";
61
61
 
62
- /* ------------------------------ process ---------------------------------- */
63
-
64
62
  sweepCaptureArtifacts();
65
63
  process.once("exit", cleanupCaptureArtifacts);
66
64
  for (const signal of ["SIGINT", "SIGTERM"]) process.once(signal, () => { cleanupCaptureArtifacts(); process.exit(128 + (signal === "SIGINT" ? 2 : 15)); });
@@ -70,8 +68,6 @@ const projectRootPromise = realpath(configuredProjectRoot).then((root) => {
70
68
  return root;
71
69
  });
72
70
 
73
- /* ------------------------------- server ---------------------------------- */
74
-
75
71
  /** Tool registrations are counted as they happen so the HTTP status page can
76
72
  * report the real number without reaching into SDK internals. */
77
73
  let registeredTools = 0;
@@ -149,8 +145,6 @@ const serveRegistryTool = (name, args, workspaceHandle) => {
149
145
  // is the order tools/list reports.
150
146
  for (const handler of createToolHandlers({ projectRootPromise })) registerTool(handler);
151
147
 
152
- /* -------------------------------- start ---------------------------------- */
153
-
154
148
  /** Counted as the tools are registered, so the status page cannot drift. */
155
149
  const TOOL_COUNT = registeredTools;
156
150