cozyclay 2.0.2 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (262) hide show
  1. package/CHANGELOG.md +61 -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 +23 -8
  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-C9pMpW7s.js +1 -0
  26. package/dist/assets/app-B_2XB9rw.css +1 -0
  27. package/dist/assets/app-DAuvKPP8.js +4920 -0
  28. package/dist/assets/{demo-ByeYp-dY.js → demo-CvKuBgy5.js} +1 -1
  29. package/dist/assets/{first-shot-handoff-DgT28ZiV.js → first-shot-handoff-msLI71g1.js} +1 -1
  30. package/dist/assets/{landing-BaXkpFip.js → landing-ChUvB_zL.js} +1 -1
  31. package/dist/assets/scene-asset-sync-DKkpjKBV.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-BPk2Ni95.js} +1 -1
  34. package/dist/assets/workflow-jJRBR94Z.js +179 -0
  35. package/dist/cozyclay-package.json +1 -1
  36. package/dist/index.html +328 -222
  37. package/dist/llms.txt +14 -0
  38. package/dist/sitemap.xml +1 -1
  39. package/dist/workflow/index.html +4 -4
  40. package/mcp/LIVE-PROTOCOL.md +23 -2
  41. package/mcp/README.md +37 -2
  42. package/mcp/ardy-prompts.mjs +1 -1
  43. package/mcp/image-file.mjs +132 -0
  44. package/mcp/live-hub.mjs +2 -2
  45. package/mcp/package.json +1 -0
  46. package/mcp/runtime/package-lock.json +7 -0
  47. package/mcp/runtime/package.json +1 -0
  48. package/mcp/server.mjs +0 -6
  49. package/mcp/tool-handlers.mjs +289 -87
  50. package/package.json +11 -2
  51. package/src/App.jsx +813 -278
  52. package/src/agent-image-client.js +94 -0
  53. package/src/analytics.js +47 -3
  54. package/src/app-stage.jsx +129 -2745
  55. package/src/ardy/auto-physics.js +6 -16
  56. package/src/ardy/collision-blockers.js +3 -7
  57. package/src/ardy/fix-collisions.js +5 -35
  58. package/src/ardy/ik-drag.js +86 -5
  59. package/src/ardy/ik-key-json.js +3 -1
  60. package/src/ardy/ik.js +15 -13
  61. package/src/ardy/motion-calibration.js +1 -1
  62. package/src/ardy/npz.js +0 -8
  63. package/src/ardy/physics-panel.jsx +3 -2
  64. package/src/ardy/physics-review.js +3 -2
  65. package/src/ardy/physics-support.js +1 -1
  66. package/src/ardy/physics-surface.js +1 -1
  67. package/src/ardy/platform-fit-panel.jsx +2 -1
  68. package/src/ardy/playback.js +1 -5
  69. package/src/ardy/range-pin.js +2 -2
  70. package/src/ardy/root-drop.js +0 -1
  71. package/src/ardy/timeline.css +184 -6
  72. package/src/ardy/timeline.jsx +121 -36
  73. package/src/ardy/to-cskel27.js +0 -2
  74. package/src/ardy/waypoints.js +0 -3
  75. package/src/ardy-bridge-contract.js +281 -0
  76. package/src/asset-pane.jsx +19 -9
  77. package/src/auto-color.js +1 -1
  78. package/src/burn-in.js +1 -1
  79. package/src/camera-block.js +1 -1
  80. package/src/camera-follow.js +75 -32
  81. package/src/camera-move.js +0 -6
  82. package/src/camera-tutorial.jsx +6 -7
  83. package/src/character-kind.js +79 -0
  84. package/src/command-bus.js +76 -17
  85. package/src/commands/cast.js +31 -7
  86. package/src/commands/elements/object.js +1 -1
  87. package/src/commands/elements/shot.js +1 -1
  88. package/src/commands/elements.js +1 -1
  89. package/src/commands/export.js +3 -3
  90. package/src/commands/motion.js +2 -2
  91. package/src/commands/objects.js +105 -10
  92. package/src/commands/project.js +15 -2
  93. package/src/commands/sculpt-images.js +53 -0
  94. package/src/commands/shot.js +88 -10
  95. package/src/commands/view.js +10 -4
  96. package/src/coplanar-depth.js +216 -0
  97. package/src/cuts.js +7 -6
  98. package/src/domains/cast.js +60 -43
  99. package/src/domains/motion/motion-intent-codec.js +50 -0
  100. package/src/domains/motion/motion-range-pin-rules.js +23 -0
  101. package/src/domains/motion/motion-video-capture.js +664 -0
  102. package/src/domains/motion.js +253 -742
  103. package/src/domains/objects.js +106 -5
  104. package/src/domains/scenes.js +79 -28
  105. package/src/domains/shots.js +33 -7
  106. package/src/error-boundary.jsx +1 -1
  107. package/src/execution-telemetry.js +5 -5
  108. package/src/facing-marks.js +14 -1
  109. package/src/first-success-guide.jsx +35 -4
  110. package/src/gizmo-claim.js +3 -1
  111. package/src/grid-floor.jsx +1 -4
  112. package/src/hierarchy-model.js +48 -35
  113. package/src/hierarchy-panel.css +1 -0
  114. package/src/hierarchy-panel.jsx +54 -102
  115. package/src/history.js +0 -2
  116. package/src/i2v-motion-client.js +1 -4
  117. package/src/ime.js +6 -0
  118. package/src/keyframe-pack.js +15 -6
  119. package/src/line-edit.js +4 -4
  120. package/src/live-control.js +6 -6
  121. package/src/matte-editor.js +2 -8
  122. package/src/matte.js +0 -19
  123. package/src/mesh-graph-clone.js +16 -2
  124. package/src/mesh-sniff.js +0 -1
  125. package/src/monotone-cubic.js +40 -0
  126. package/src/motion-store.js +4 -4
  127. package/src/motion-trail.js +5 -11
  128. package/src/multimodel-ingest.js +1 -3
  129. package/src/object-catalog.jsx +7 -5
  130. package/src/object-gizmo.jsx +45 -31
  131. package/src/object-path.js +310 -67
  132. package/src/object-travel.js +224 -0
  133. package/src/otio.js +8 -3
  134. package/src/panels/CharacterTransformPanel.jsx +24 -6
  135. package/src/panels/ObjectTransformPanel.jsx +4 -3
  136. package/src/panels/PoseImagePanel.jsx +205 -0
  137. package/src/panels/PromptBlocksPanel.jsx +3 -1
  138. package/src/panels/ProxyFigurePanel.jsx +56 -0
  139. package/src/panels/ReferenceImageField.jsx +1 -1
  140. package/src/panels/RigControlPanel.jsx +24 -3
  141. package/src/panels/RigPanel.jsx +2 -1
  142. package/src/panels/SculptPanel.jsx +405 -0
  143. package/src/panels/SubjectBox.jsx +1 -1
  144. package/src/panels/SubjectsPanel.jsx +0 -2
  145. package/src/panels/VideoCapturePanel.jsx +2 -1
  146. package/src/panels/pose-image.css +42 -0
  147. package/src/panels/pose.css +3 -0
  148. package/src/panels/sculpt.css +162 -0
  149. package/src/plan-diagram.js +184 -0
  150. package/src/planview.jsx +127 -80
  151. package/src/playground.js +3 -5
  152. package/src/pose-fade.js +77 -0
  153. package/src/pose-gizmo.css +23 -0
  154. package/src/pose-gizmo.js +96 -0
  155. package/src/posestudio.jsx +134 -83
  156. package/src/project-browser.css +53 -0
  157. package/src/project-browser.jsx +49 -4
  158. package/src/project.js +55 -24
  159. package/src/props.jsx +177 -36
  160. package/src/proxy-figure.jsx +69 -0
  161. package/src/range-pin-panel.jsx +0 -1
  162. package/src/receipt-findings.js +20 -0
  163. package/src/render-passes.js +0 -1
  164. package/src/resource-status.jsx +1 -1
  165. package/src/root-path.js +48 -0
  166. package/src/route-owner.js +52 -0
  167. package/src/scene-asset-cache.js +1 -2
  168. package/src/scene-assets.js +12 -4
  169. package/src/scene-fbx.js +1 -1
  170. package/src/scene-mesh-cache.js +0 -1
  171. package/src/scene-objects.js +319 -19
  172. package/src/scenes.js +58 -9
  173. package/src/sculpt-blueprint.js +65 -0
  174. package/src/sculpt-compare.js +384 -0
  175. package/src/sculpt-geometry.js +141 -0
  176. package/src/sculpt-mode.js +168 -0
  177. package/src/sculpt-recipe.js +385 -0
  178. package/src/sculpt-run.js +351 -0
  179. package/src/sculpt-stage.jsx +221 -0
  180. package/src/settings-menu.jsx +1 -1
  181. package/src/shell/BoardSlot.jsx +341 -0
  182. package/src/shell/BottomDock.jsx +60 -16
  183. package/src/shell/CompositionGuideButton.jsx +20 -0
  184. package/src/shell/DetailsSlot.jsx +26 -6
  185. package/src/shell/MenuBar.jsx +78 -6
  186. package/src/shell/OutlinerSlot.jsx +3 -1
  187. package/src/shell/ShotPreviewHeader.jsx +17 -0
  188. package/src/shell/TopBar.jsx +18 -3
  189. package/src/shell/ViewportToolbar.jsx +36 -31
  190. package/src/shell/dock.css +239 -0
  191. package/src/shell/glass-regions.css +3 -2
  192. package/src/shell/log-store.js +4 -4
  193. package/src/shell/mode.css +20 -0
  194. package/src/shell/studio-shell-context.js +4 -1
  195. package/src/shell/viewport.css +12 -0
  196. package/src/shot-authoring.js +69 -6
  197. package/src/stable-items.js +1 -1
  198. package/src/stage-attach-frames.js +169 -0
  199. package/src/stage-canvas-rigs.jsx +402 -0
  200. package/src/stage-capture.jsx +216 -0
  201. package/src/stage-characters.jsx +302 -0
  202. package/src/stage-editor-gizmos.jsx +1229 -0
  203. package/src/stage-labels-ko.js +204 -0
  204. package/src/stage-startup-defaults.js +182 -0
  205. package/src/starter-scenes.js +1 -1
  206. package/src/storyboard.js +24 -5
  207. package/src/studio-actions.js +42 -9
  208. package/src/studio-agent-commands.js +213 -23
  209. package/src/studio-agent-context.js +11 -9
  210. package/src/studio-agent-motion.js +2 -1
  211. package/src/studio-agent-protocol.js +60 -24
  212. package/src/studio-app-binding.js +56 -23
  213. package/src/studio-contact-sheet.js +3 -3
  214. package/src/studio-elements.js +11 -2
  215. package/src/studio-geometry-facts.js +114 -0
  216. package/src/styles.css +92 -9
  217. package/src/ui-scrub.js +4 -4
  218. package/src/ui.jsx +6 -4
  219. package/src/use-asset-shelf.js +60 -0
  220. package/src/use-camera-tutorial.js +50 -0
  221. package/src/use-embed-bridge.js +12 -0
  222. package/src/use-first-success-prompt.js +7 -0
  223. package/src/use-i2v-capture-modal.js +15 -0
  224. package/src/use-sculpt-mode.js +95 -0
  225. package/src/workflow/AgentPanel.jsx +106 -18
  226. package/src/workflow/WorkflowBuilder.jsx +1 -1
  227. package/src/workflow/agent-client.js +113 -45
  228. package/src/workflow/agent-panel.css +35 -0
  229. package/src/workflow/attachment-image.js +1 -1
  230. package/src/workflow/canvas-commands.js +3 -3
  231. package/src/workflow/cozy-scene-node.js +3 -3
  232. package/src/workflow/local-workflow.js +2 -2
  233. package/src/workflow/motion-input.js +3 -3
  234. package/src/workflow/node-schema.js +1 -1
  235. package/src/workflow/scene-asset-sync.js +2 -9
  236. package/src/workflow/shot-prompt-node.js +1 -1
  237. package/src/workflow/video-contract.js +1 -1
  238. package/src/workflow/workflow-resources.js +0 -8
  239. package/src/workflow-send.js +87 -0
  240. package/tools/ardy/__pycache__/cclay_gvhmr_worker.cpython-313.pyc +0 -0
  241. package/tools/check-eslint.mjs +57 -0
  242. package/tools/check-knip.mjs +60 -0
  243. package/tools/check-loc.mjs +54 -0
  244. package/tools/demo-worker/api-client.mjs +7 -13
  245. package/tools/demo-worker/generate.mjs +2 -4
  246. package/tools/eslint-baseline.json +23 -0
  247. package/tools/gt-render/browser.mjs +23 -0
  248. package/tools/headless-studio.mjs +85 -0
  249. package/tools/kimodo/effector-constraints.mjs +4 -4
  250. package/tools/kimodo/generate.mjs +1 -1
  251. package/tools/knip-baseline.json +331 -0
  252. package/tools/loc-baseline.json +126 -0
  253. package/tools/process-supervisor.mjs +1 -1
  254. package/tools/projflow/replay.mjs +4 -4
  255. package/tools/projflow/runner.mjs +3 -3
  256. package/tools/qa/studio-control-count.mjs +2 -0
  257. package/tools/run-tests.mjs +95 -7
  258. package/dist/assets/analytics-MVZOEW8O.js +0 -1
  259. package/dist/assets/app-BxBLC6GT.js +0 -4876
  260. package/dist/assets/app-CJdxvnrl.css +0 -1
  261. package/dist/assets/shot-prompt-hN0j_O-p.js +0 -17
  262. package/dist/assets/workflow-B5XZwVfe.js +0 -179
@@ -0,0 +1,94 @@
1
+ // The sidecar's /agent/image request, shared by the Workflow (agent-client.js)
2
+ // and the Studio's Board panel Stylize, so the Studio can generate a picture
3
+ // without pulling the Workflow bundle in. Plain fetch, no React, no imports
4
+ // beyond the locale and the shot prompt.
5
+
6
+ import { ko } from "./locale.js";
7
+ import { buildShotPrompt } from "./shot-prompt.js";
8
+
9
+ const SIDECAR_ORIGIN = "";
10
+
11
+ export function sidecarUrl(path) {
12
+ return `${SIDECAR_ORIGIN}${path}`;
13
+ }
14
+
15
+ /** One JSON request to the loopback sidecar. A refusal throws an Error whose
16
+ * message is the sidecar's own (already sanitized) explanation, with `status`
17
+ * and the structured error fields (`code`, ...) copied onto it. */
18
+ export async function sidecarRequest(fetchImpl, path, init) {
19
+ const response = await fetchImpl(sidecarUrl(path), {
20
+ headers: { "content-type": "application/json" },
21
+ ...init,
22
+ });
23
+ if (!response.ok) {
24
+ let detail = null;
25
+ try { detail = await response.clone().json(); } catch { /* preserve the status when the server did not send JSON */ }
26
+ const message = typeof detail?.error === "string" ? detail.error : detail?.error?.message;
27
+ const error = new Error(message || `${path} responded ${response.status}`);
28
+ // Keep machine-readable verification evidence alongside the human message.
29
+ // The Workflow node can show why an H3 take was rejected without exposing
30
+ // or retaining the rejected video itself.
31
+ error.status = response.status;
32
+ if (detail?.error && typeof detail.error === "object") Object.assign(error, detail.error);
33
+ if (detail?.preservation && typeof detail.preservation === "object") error.preservation = detail.preservation;
34
+ throw error;
35
+ }
36
+ return response.json();
37
+ }
38
+
39
+ /** The /agent/image body. `references` are the scene's identity / environment
40
+ * slots (#167), passed through untouched so the sidecar decides how they are
41
+ * described to the model. */
42
+ export function agentImageBody({ prompt, imageDataUrl, referenceDataUrl, references, composition, quality = "auto", size }) {
43
+ return {
44
+ prompt,
45
+ imageDataUrl,
46
+ ...(referenceDataUrl ? { referenceDataUrl } : {}),
47
+ ...(Array.isArray(references) && references.length ? { references } : {}),
48
+ ...(composition ? { composition } : {}),
49
+ quality,
50
+ ...(size ? { size } : {}),
51
+ };
52
+ }
53
+
54
+ /** POST /agent/image; answers `{ dataUrl, width, height }`. */
55
+ export function requestAgentImage(payload, { fetchImpl = globalThis.fetch?.bind(globalThis), signal } = {}) {
56
+ return sidecarRequest(fetchImpl, "/agent/image", { method: "POST", body: JSON.stringify(agentImageBody(payload)), signal });
57
+ }
58
+
59
+ // The clay-frame preamble the sidecar puts in front of a hand-framed picture
60
+ // (bin/agent/agent-routes.mjs FRAME_COMPOSITION_GUIDANCE). The browser cannot
61
+ // import the sidecar, so the words are carried here and a test keeps the two
62
+ // copies equal.
63
+ export const CLAY_FRAME_PREAMBLE = "Use the first image (a clay blocking frame) as the layout: keep its composition and camera angle, and keep the pose but make it look natural. Render it as:";
64
+
65
+ /** The request that stylizes one storyboard panel: the clay preamble, the
66
+ * shot's labelled image prompt, then the panel's caption. */
67
+ export function panelStylizeRequest({ meta, caption, imageDataUrl }) {
68
+ const line = typeof caption === "string" ? caption.trim() : "";
69
+ const prompt = `${CLAY_FRAME_PREAMBLE}\n${buildShotPrompt(meta, { target: "image" })}${line ? `\n${line}` : ""}`;
70
+ return { prompt, imageDataUrl, quality: "auto" };
71
+ }
72
+
73
+ /** What a failed Stylize tells the author: the route's explanation, with the
74
+ * two refusals an author can act on worded for them. */
75
+ export function stylizeErrorMessage(error) {
76
+ if (error?.code === "entitlement") return ko("This account cannot generate images.", "이 계정은 이미지를 생성할 수 없어요.");
77
+ if (error?.status === 401 || error?.code === "auth") return ko("Sign in with ChatGPT in the Agent panel first.", "먼저 Agent 패널에서 ChatGPT로 로그인하세요.");
78
+ const reason = error?.message || ko("unknown error", "알 수 없는 오류");
79
+ return ko(`Stylize failed — ${reason}`, `스타일화 실패 — ${reason}`);
80
+ }
81
+
82
+ /** The generated PNG's bytes, refused past `maxBytes` with a reason. */
83
+ export function stylizedPngBytes(dataUrl, maxBytes) {
84
+ const match = typeof dataUrl === "string" ? /^data:image\/png;base64,(.*)$/s.exec(dataUrl) : null;
85
+ if (!match) throw new Error(ko("The image route did not return a PNG.", "이미지 경로가 PNG를 돌려주지 않았어요."));
86
+ const binary = atob(match[1]);
87
+ if (binary.length > maxBytes) {
88
+ const mb = Math.round(maxBytes / (1024 * 1024));
89
+ throw new Error(ko(`The stylized image is larger than ${mb} MB, so it was not added.`, `스타일화된 이미지가 ${mb} MB보다 커서 추가하지 않았어요.`));
90
+ }
91
+ const bytes = new Uint8Array(binary.length);
92
+ for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
93
+ return bytes;
94
+ }
package/src/analytics.js CHANGED
@@ -62,6 +62,14 @@ export const EXECUTION_TELEMETRY_VALUES = Object.freeze({
62
62
  outcome: new Set(["succeeded", "failed", "uncertain", "cancelled"]),
63
63
  });
64
64
 
65
+ // Closed values for the storyboard and capsule-figure events. Counts only: no
66
+ // caption, name, prompt or id ever rides along.
67
+ const PREVIS_PROPERTY_VALUES = Object.freeze({
68
+ source: new Set(["agent", "manual"]),
69
+ outcome: new Set(["ok", "error"]),
70
+ surface: new Set(["assets", "agent", "mcp"]),
71
+ });
72
+
65
73
  const OPT_OUT_KEY = "cozyclay.analyticsOptOut";
66
74
  const INTERNAL_QA_KEY = "cozyclay.internalQa";
67
75
  const ACTIVATION_KEY = "cozyclay.analyticsActivation";
@@ -79,6 +87,8 @@ export const EDIT_GROUPS = Object.freeze({
79
87
  export const EDIT_BUCKET_KEYS = Object.freeze(Object.keys(EDIT_GROUPS).map((group) => `${group}_edit_bucket`));
80
88
  export const USE_CASE_VALUES = Object.freeze(["animation", "film", "game", "ad_mv", "personal", "other", "skip"]);
81
89
  export const TEAM_VALUES = Object.freeze(["team", "solo", "skip"]);
90
+ // Mirrors PREVIS_MODES in project.js; kept local so analytics stays import-light.
91
+ export const PREVIS_MODE_VALUES = new Set(["storyboard", "animation"]);
82
92
  export const UPDATE_STATUS_VALUES = Object.freeze(["latest", "outdated", "unknown"]);
83
93
  const DEFAULT_ALLOWED_ORIGINS = Object.freeze([
84
94
  "https://cozyclay.org",
@@ -97,7 +107,10 @@ const EVENT_PROPERTIES = Object.freeze({
97
107
  "hosted:ticket_created": [],
98
108
  "hosted:result_opened": [],
99
109
  "hosted:opened_in_studio": [],
100
- "scene:created": ["scene_source"],
110
+ "scene:created": ["scene_source", "previs_mode"],
111
+ "storyboard:panel_created": ["source"],
112
+ "storyboard:panel_stylized": ["outcome"],
113
+ "cast:proxy_added": ["surface"],
101
114
  "scene:loaded": ["scene_source"],
102
115
  "project:saved": ["object_count_bucket", "shot_count_bucket"],
103
116
  "project:opened": ["age_bucket"],
@@ -128,6 +141,7 @@ const EVENT_PROPERTIES = Object.freeze({
128
141
  "tutorial:step_completed": ["surface", "tutorial_version", "step_kind", "elapsed_bucket"],
129
142
  "tutorial:completed": ["surface", "tutorial_version", "elapsed_bucket"],
130
143
  "tutorial:dismissed": ["surface", "tutorial_version", "step_kind"],
144
+ "sculpt_run": ["outcome", "rounds", "final_iou", "duration_ms", "had_target"],
131
145
  ...EXECUTION_TELEMETRY_PROPERTY_KEYS,
132
146
  });
133
147
  const FEATURE_NAMES = new Set([
@@ -156,8 +170,8 @@ const MOTION_PROPERTY_VALUES = Object.freeze({
156
170
  });
157
171
  const EXPORT_FAILURE_CODES = new Set(["unsupported_codec", "encode_failed", "render_failed", "aborted", "unknown"]);
158
172
  const EXPORT_PROPERTY_VALUES = Object.freeze({
159
- export_kind: new Set(["video", "depth_video", "frame", "keyframe_pack"]),
160
- format: new Set(["mp4", "png", "zip"]),
173
+ export_kind: new Set(["video", "depth_video", "frame", "keyframe_pack", "workflow_send", "animation_project", "contact_sheet", "panel_pack", "animatic"]),
174
+ format: new Set(["mp4", "png", "zip", "cclayproject"]),
161
175
  surface: new Set(["studio", "workflow", "embed"]),
162
176
  duration_bucket: new Set(["lt1s", "1-3s", "3-10s", "10-30s", "gte30s"]),
163
177
  failure_code: EXPORT_FAILURE_CODES,
@@ -169,6 +183,11 @@ const TUTORIAL_PROPERTY_VALUES = Object.freeze({
169
183
  step_kind: new Set(["fly", "walk", "dolly", "orbit", "shot", "rail", "play"]),
170
184
  elapsed_bucket: new Set(["lt1s", "1-3s", "3-10s", "10-30s", "gte30s"]),
171
185
  });
186
+ // Sculpt mode (#739): one sculpt_run per finished run. Numbers only: no
187
+ // reference image, recipe, part name or sculpt id ever rides along.
188
+ export const SCULPT_RUN_OUTCOMES = Object.freeze(["done", "cancelled", "failed"]);
189
+ const SCULPT_RUN_MAX_ROUNDS = 10;
190
+ const SCULPT_RUN_MAX_DURATION_MS = 86_400_000;
172
191
  const EXECUTION_FAILURE_CODES = Object.freeze({
173
192
  workflow: new Set(["aborted", "capture_failed", "generation_failed", "unknown"]),
174
193
  agent: new Set(["aborted", "auth", "rate_limited", "tool_failed", "upstream", "unknown"]),
@@ -333,6 +352,17 @@ export function sanitizeProps(event, props) {
333
352
  if (!ERROR_PROPERTY_VALUES[key]?.has(props[key])) continue;
334
353
  }
335
354
  if (event === "device:profile" && !DEVICE_PROPERTY_VALUES[key]?.has(props[key])) continue;
355
+ if (event === "sculpt_run") {
356
+ const value = props[key];
357
+ if (key === "outcome" && SCULPT_RUN_OUTCOMES.includes(value)) sanitized[key] = value;
358
+ else if (key === "rounds" && Number.isInteger(value) && value >= 0 && value <= SCULPT_RUN_MAX_ROUNDS) sanitized[key] = value;
359
+ else if (key === "final_iou" && typeof value === "number" && value >= 0 && value <= 1) sanitized[key] = Math.round(value * 100) / 100;
360
+ else if (key === "duration_ms" && typeof value === "number" && value >= 0 && value <= SCULPT_RUN_MAX_DURATION_MS) sanitized[key] = Math.round(value);
361
+ else if (key === "had_target" && typeof value === "boolean") sanitized[key] = value;
362
+ continue;
363
+ }
364
+ if (key === "previs_mode" && !PREVIS_MODE_VALUES.has(props[key])) continue;
365
+ if ((event.startsWith("storyboard:") || event.startsWith("cast:")) && !PREVIS_PROPERTY_VALUES[key]?.has(props[key])) continue;
336
366
  if (event === "survey:use_case") {
337
367
  if (key === "use_case" && !USE_CASE_VALUES.includes(props[key])) continue;
338
368
  if (key === "team" && !TEAM_VALUES.includes(props[key])) continue;
@@ -1066,6 +1096,20 @@ export function track(event, props = {}) {
1066
1096
  }
1067
1097
  }
1068
1098
 
1099
+ /** The sculpt_run payload for a sculpt-run state (src/sculpt-run.js), or null
1100
+ * while the run has not ended. `hadTarget`: the run re-sculpted an existing
1101
+ * sculpt rather than creating one. An unscored run omits final_iou. */
1102
+ export function sculptRunTelemetry(state, { hadTarget = false } = {}) {
1103
+ if (!SCULPT_RUN_OUTCOMES.includes(state?.stage)) return null;
1104
+ return {
1105
+ outcome: state.stage,
1106
+ rounds: state.round,
1107
+ ...(Number.isFinite(state.iou) ? { final_iou: Math.round(state.iou * 100) / 100 } : {}),
1108
+ duration_ms: Math.round(state.elapsedMs),
1109
+ had_target: Boolean(hadTarget),
1110
+ };
1111
+ }
1112
+
1069
1113
  export function trackFeature(name) {
1070
1114
  if (!initialized || !enabled || !posthog || !FEATURE_NAMES.has(name) || featureNamesSeen.has(name)) return false;
1071
1115
  featureNamesSeen.add(name);