cozyclay 1.2.0 → 1.3.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 (72) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/README.md +31 -0
  3. package/THIRD_PARTY_NOTICES.md +37 -1
  4. package/bin/cozyclay.mjs +51 -2
  5. package/dist/ai-camera-control/index.html +406 -0
  6. package/dist/app/index.html +5 -5
  7. package/dist/assets/app-B3U5aut1.js +4811 -0
  8. package/dist/assets/app-BrRF0wso.css +1 -0
  9. package/dist/assets/vision_bundle-jFkh-fIS.js +41 -0
  10. package/dist/fonts/InstrumentSerif-OFL.txt +93 -0
  11. package/dist/fonts/Inter-OFL.txt +92 -0
  12. package/dist/fonts/README.md +15 -0
  13. package/dist/index.html +60 -16
  14. package/dist/sitemap.xml +7 -1
  15. package/mcp/LIVE-PROTOCOL.md +63 -0
  16. package/mcp/README.md +142 -0
  17. package/mcp/ardy-prompts.mjs +170 -0
  18. package/mcp/live-hub.mjs +105 -0
  19. package/mcp/package.json +24 -0
  20. package/mcp/server.mjs +1394 -0
  21. package/package.json +122 -90
  22. package/src/App.jsx +2957 -514
  23. package/src/ardy/cskel27.js +7 -2
  24. package/src/ardy/ik.js +25 -15
  25. package/src/ardy/npz.js +64 -3
  26. package/src/ardy/playback.js +31 -1
  27. package/src/ardy/prompt-clips.js +7 -2
  28. package/src/ardy/retime.js +211 -0
  29. package/src/ardy/timeline-coordinates.js +13 -0
  30. package/src/ardy/timeline.jsx +124 -5
  31. package/src/ardy/to-cskel27.js +34 -12
  32. package/src/ardy/trim.js +33 -0
  33. package/src/asset-pane.jsx +36 -0
  34. package/src/dualview.jsx +14 -8
  35. package/src/hierarchy-model.js +95 -13
  36. package/src/hierarchy-panel.jsx +139 -6
  37. package/src/live-control.js +122 -0
  38. package/src/matte-editor.js +543 -0
  39. package/src/matte.js +503 -0
  40. package/src/multimodel-ingest.js +344 -0
  41. package/src/object-gizmo.jsx +43 -15
  42. package/src/planview.jsx +43 -32
  43. package/src/pose-extract/detector.js +75 -0
  44. package/src/pose-extract/index.js +3 -0
  45. package/src/pose-extract/take.js +87 -0
  46. package/src/pose-extract/video-frames.js +91 -0
  47. package/src/pose-thumbs.js +152 -0
  48. package/src/posestudio.jsx +361 -11
  49. package/src/project-browser.jsx +135 -0
  50. package/src/project.js +289 -0
  51. package/src/props.jsx +69 -3
  52. package/src/room.jsx +14 -35
  53. package/src/scene-asset-cache.js +125 -0
  54. package/src/scene-assets.js +288 -0
  55. package/src/scene-objects.js +245 -7
  56. package/src/scenes.js +207 -26
  57. package/src/shot-authoring.js +55 -13
  58. package/src/styles.css +1296 -129
  59. package/tools/ardy/BRIDGE.md +3 -2
  60. package/tools/ardy/README.md +9 -5
  61. package/tools/ardy/bridge.mjs +57 -1
  62. package/tools/ardy/bvh-cskel27.mjs +1209 -0
  63. package/tools/ardy/cclay_constrained_generate.py +123 -11
  64. package/tools/ardy/cclay_sequence_generate.py +49 -0
  65. package/tools/ardy/extract.mjs +367 -0
  66. package/tools/ardy/footage.mjs +462 -0
  67. package/tools/ardy/npz.mjs +74 -9
  68. package/tools/ardy/run-on-box.sh +25 -0
  69. package/tools/ardy/run-sequence-on-box.sh +15 -0
  70. package/tools/ardy/runners/remote.mjs +8 -2
  71. package/dist/assets/app-Cgpk2hwX.js +0 -4803
  72. package/dist/assets/app-DgZvaAE1.css +0 -1
@@ -11,18 +11,115 @@ const HIERARCHY_LABELS_KO = {
11
11
  "Character 1": "캐릭터 1",
12
12
  "Character 2": "캐릭터 2",
13
13
  Rig: "리그",
14
+ Torso: "몸통",
14
15
  "Root / Hips": "루트 / 골반",
15
16
  Spine: "척추",
17
+ Chest: "가슴",
18
+ Neck: "목",
19
+ Head: "머리",
16
20
  "Left Arm": "왼팔",
21
+ "Left Shoulder": "왼쪽 어깨",
22
+ "Left Elbow": "왼쪽 팔꿈치",
23
+ "Left Hand": "왼손",
17
24
  "Right Arm": "오른팔",
25
+ "Right Shoulder": "오른쪽 어깨",
26
+ "Right Elbow": "오른쪽 팔꿈치",
27
+ "Right Hand": "오른손",
18
28
  "Left Leg": "왼다리",
29
+ "Left Knee": "왼쪽 무릎",
30
+ "Left Foot": "왼발",
19
31
  "Right Leg": "오른다리",
32
+ "Right Knee": "오른쪽 무릎",
33
+ "Right Foot": "오른발",
20
34
  Environment: "환경",
21
35
  Props: "소품",
22
36
  };
23
37
 
24
38
  const FALLBACK_SCENES = [{ id: "current-scene", name: "SCENE 01" }];
25
39
 
40
+ function HierarchyIcon({ kind, className = "" }) {
41
+ const common = {
42
+ className: `hierarchy-icon ${kind}${className ? ` ${className}` : ""}`,
43
+ viewBox: "0 0 24 24",
44
+ fill: "none",
45
+ stroke: "currentColor",
46
+ strokeWidth: 1.7,
47
+ strokeLinecap: "round",
48
+ strokeLinejoin: "round",
49
+ "aria-hidden": true,
50
+ };
51
+
52
+ switch (kind) {
53
+ case "scene":
54
+ return (
55
+ <svg {...common}>
56
+ <path d="M12 3.25 21 8l-9 4.75L3 8l9-4.75Z" fill="currentColor" fillOpacity=".16" />
57
+ <path d="m4.25 12 7.75 4.1 7.75-4.1M4.25 16 12 20.1l7.75-4.1" />
58
+ </svg>
59
+ );
60
+ case "camera":
61
+ return (
62
+ <svg {...common}>
63
+ <rect x="3" y="6.5" width="13.5" height="11" rx="2" />
64
+ <path d="m16.5 10 4.5-2.25v8.5L16.5 14" />
65
+ <circle cx="9.5" cy="12" r="2.4" />
66
+ </svg>
67
+ );
68
+ case "group":
69
+ return (
70
+ <svg {...common}>
71
+ <circle cx="9" cy="8.25" r="2.75" />
72
+ <circle cx="16.5" cy="9.25" r="2.15" />
73
+ <path d="M3.75 18c.45-3.25 2.15-5 5.25-5s4.8 1.75 5.25 5M14 14.25c3.55-.55 5.65.75 6.25 3.75" />
74
+ </svg>
75
+ );
76
+ case "character":
77
+ return (
78
+ <svg {...common}>
79
+ <circle cx="12" cy="7.25" r="3" fill="currentColor" fillOpacity=".12" />
80
+ <path d="M5.25 20c.45-5 2.7-7.5 6.75-7.5S18.3 15 18.75 20M9.25 13.25 12 16l2.75-2.75" />
81
+ </svg>
82
+ );
83
+ case "rig":
84
+ return (
85
+ <svg {...common}>
86
+ <circle cx="12" cy="4.5" r="1.75" fill="currentColor" fillOpacity=".18" />
87
+ <circle cx="6" cy="11.5" r="1.75" />
88
+ <circle cx="18" cy="11.5" r="1.75" />
89
+ <circle cx="12" cy="19.5" r="1.75" />
90
+ <path d="m10.8 5.8-3.6 4.4m6-4.4 3.6 4.4M7.75 12.65l3 5.4m5.5-5.4-3 5.4" />
91
+ </svg>
92
+ );
93
+ case "bone":
94
+ return (
95
+ <svg {...common}>
96
+ <path d="M7.4 8.25a2.5 2.5 0 1 1-3.65-3.4A2.5 2.5 0 1 1 7.2 8.5l8.3 8.3a2.5 2.5 0 1 1 3.65 3.4 2.5 2.5 0 1 1-3.4-3.65L7.4 8.25Z" />
97
+ </svg>
98
+ );
99
+ case "environment":
100
+ return (
101
+ <svg {...common}>
102
+ <circle cx="12" cy="12" r="8.5" fill="currentColor" fillOpacity=".08" />
103
+ <path d="M3.8 13h16.4M12 3.5c2.25 2.35 3.4 5.2 3.4 8.5S14.25 18.15 12 20.5C9.75 18.15 8.6 15.3 8.6 12S9.75 5.85 12 3.5Z" />
104
+ </svg>
105
+ );
106
+ case "props":
107
+ case "object":
108
+ return (
109
+ <svg {...common}>
110
+ <path d="m12 3.5 8 4.25v8.5l-8 4.25-8-4.25v-8.5L12 3.5Z" fill="currentColor" fillOpacity=".1" />
111
+ <path d="m4.25 7.9 7.75 4.2 7.75-4.2M12 12.1v8.15" />
112
+ </svg>
113
+ );
114
+ default:
115
+ return (
116
+ <svg {...common}>
117
+ <circle cx="12" cy="12" r="7.5" />
118
+ </svg>
119
+ );
120
+ }
121
+ }
122
+
26
123
  function displaySceneName(name) {
27
124
  if (!isKo) return name;
28
125
  const generatedName = /^SCENE\s+(\d+)$/i.exec(name);
@@ -108,7 +205,7 @@ function SceneSwitcher({
108
205
  }}
109
206
  onDoubleClick={() => setEditingId(scene.id)}
110
207
  >
111
- <span className="scene-document-icon" aria-hidden="true" />
208
+ <HierarchyIcon kind="scene" className="scene-document-icon" />
112
209
  <span>{displaySceneName(scene.name)}</span>
113
210
  </button>
114
211
  )}
@@ -212,8 +309,33 @@ function TreeRow({
212
309
  onRenameCommit,
213
310
  onRenameCancel,
214
311
  onRowContextMenu,
312
+ dropHandlers,
215
313
  }) {
216
314
  const branch = !!node.children?.length;
315
+ // The drop BEHAVIOUR is shared by every row that accepts a picture; the
316
+ // highlight is not. One row lit under the cursor says where the file will
317
+ // land; a whole branch lighting up says nothing.
318
+ const [dropOver, setDropOver] = useState(false);
319
+ const dropDepth = useRef(0);
320
+ const drop = dropHandlers && {
321
+ onDragEnter: (event) => {
322
+ dropHandlers.onDragEnter(event);
323
+ if (!event.dataTransfer?.types?.includes?.("Files")) return;
324
+ dropDepth.current += 1;
325
+ setDropOver(true);
326
+ },
327
+ onDragOver: dropHandlers.onDragOver,
328
+ onDragLeave: (event) => {
329
+ dropHandlers.onDragLeave(event);
330
+ dropDepth.current = Math.max(0, dropDepth.current - 1);
331
+ if (!dropDepth.current) setDropOver(false);
332
+ },
333
+ onDrop: (event) => {
334
+ dropDepth.current = 0;
335
+ setDropOver(false);
336
+ dropHandlers.onDrop(event);
337
+ },
338
+ };
217
339
  const label = displayHierarchyLabel(node);
218
340
  const rowWrapRef = useRef(null);
219
341
  const inputRef = useRef(null);
@@ -241,6 +363,8 @@ function TreeRow({
241
363
  className={"hierarchy-row-wrap" + (selectedId === node.id ? " selected" : "")}
242
364
  style={{ "--hierarchy-depth": depth }}
243
365
  data-node-id={node.id}
366
+ data-drop={drop ? (dropOver ? "over" : "target") : undefined}
367
+ {...(drop ?? {})}
244
368
  role="treeitem"
245
369
  tabIndex={-1}
246
370
  aria-selected={selectedId === node.id}
@@ -264,7 +388,7 @@ function TreeRow({
264
388
  // blur commits (docs/unity-reference.md §9.7). A div, not the
265
389
  // row button — an input inside a button is invalid HTML.
266
390
  <div className="hierarchy-row">
267
- <span className={`hierarchy-icon ${node.kind}`} aria-hidden="true" />
391
+ <HierarchyIcon kind={node.kind} />
268
392
  <input
269
393
  ref={inputRef}
270
394
  className="hierarchy-rename-input"
@@ -287,7 +411,7 @@ function TreeRow({
287
411
  </div>
288
412
  ) : (
289
413
  <button type="button" className="hierarchy-row" onClick={() => onSelect(node.id)}>
290
- <span className={`hierarchy-icon ${node.kind}`} aria-hidden="true" />
414
+ <HierarchyIcon kind={node.kind} />
291
415
  <span className="hierarchy-label">{label}</span>
292
416
  {status && <span className="hierarchy-status">{status}</span>}
293
417
  {badge !== null && badge !== undefined && badge !== 0 && <span className="hierarchy-badge">{badge}</span>}
@@ -301,6 +425,7 @@ export default function HierarchyPanel({
301
425
  selectedId,
302
426
  onSelect,
303
427
  showB,
428
+ characters = null,
304
429
  motionFrames,
305
430
  ikMode,
306
431
  sceneObjects = [],
@@ -309,6 +434,7 @@ export default function HierarchyPanel({
309
434
  onDuplicateObject,
310
435
  onDeleteObject,
311
436
  onFrameObject,
437
+ propsDrop = null,
312
438
  scenes,
313
439
  activeSceneId,
314
440
  onSceneSelect,
@@ -331,9 +457,9 @@ export default function HierarchyPanel({
331
457
  return (availableScenes.find((scene) => scene.id === activeSceneId) ?? availableScenes[0]).name;
332
458
  }, [activeSceneId, scenes]);
333
459
  const hierarchyNodes = useMemo(() => {
334
- const nodes = buildHierarchyNodes(sceneObjects);
460
+ const nodes = buildHierarchyNodes(sceneObjects, characters);
335
461
  return nodes.map((node) => node.kind === "scene" ? { ...node, label: activeSceneName } : node);
336
- }, [activeSceneName, sceneObjects]);
462
+ }, [activeSceneName, sceneObjects, characters]);
337
463
  const parents = useMemo(() => indexParents(hierarchyNodes), [hierarchyNodes]);
338
464
 
339
465
  useEffect(() => {
@@ -384,7 +510,9 @@ export default function HierarchyPanel({
384
510
  });
385
511
  };
386
512
  const badgeFor = (id) => {
387
- if (id === "characters") return showB ? 2 : 1;
513
+ if (id === "characters") {
514
+ return Array.isArray(characters) ? characters.filter((entry) => !entry.hidden).length : (showB ? 2 : 1);
515
+ }
388
516
  if (id === "props") return sceneObjects.length;
389
517
  return null;
390
518
  };
@@ -466,6 +594,11 @@ export default function HierarchyPanel({
466
594
  onRenameCommit={(name) => commitRename(node.id, name)}
467
595
  onRenameCancel={cancelRename}
468
596
  onRowContextMenu={openRowMenu}
597
+ // A picture dropped on Props — or on any prop already in it —
598
+ // becomes a cutout in the set. Dropping ON an object is the
599
+ // same gesture as dropping on the group it lives in: people
600
+ // aim at the list, not at the heading.
601
+ dropHandlers={propsDrop && (node.kind === "props" || node.kind === "object") ? propsDrop.handlers : null}
469
602
  />,
470
603
  ...(node.children && open ? renderNodes(node.children, depth + 1) : []),
471
604
  ];
@@ -0,0 +1,122 @@
1
+ // Browser-side client for the editor half of mcp/LIVE-PROTOCOL.md. This
2
+ // module deliberately has no browser imports so its frame dispatcher is
3
+ // directly testable in Node with a fake WebSocket.
4
+
5
+ export const LIVE_CONTROL_URL = "ws://127.0.0.1:5184/live";
6
+ export const LIVE_CONTROL_RECONNECT_MS = 3000;
7
+
8
+ function errorMessage(error) {
9
+ if (error instanceof Error && error.message) return error.message;
10
+ return typeof error === "string" && error ? error : "Command failed";
11
+ }
12
+
13
+ function result(id, ok, body) {
14
+ return ok
15
+ ? { type: "result", id, ok: true, value: body ?? {} }
16
+ : { type: "result", id, ok: false, error: body };
17
+ }
18
+
19
+ /**
20
+ * Parse and dispatch one incoming text frame. Non-command frames are ignored;
21
+ * a command with a valid id always receives a protocol result, including for
22
+ * malformed arguments and unknown command names.
23
+ */
24
+ export async function dispatchLiveFrame(data, handlers = {}) {
25
+ if (typeof data !== "string") return null;
26
+ let frame;
27
+ try {
28
+ frame = JSON.parse(data);
29
+ } catch {
30
+ return null;
31
+ }
32
+ if (!frame || typeof frame !== "object" || Array.isArray(frame) || frame.type !== "cmd") return null;
33
+ if (typeof frame.id !== "string") return null;
34
+ if (typeof frame.name !== "string" || !frame.name) return result(frame.id, false, "Invalid command name");
35
+ if (!frame.args || typeof frame.args !== "object" || Array.isArray(frame.args)) return result(frame.id, false, "Invalid command arguments");
36
+ const handler = handlers[frame.name];
37
+ if (typeof handler !== "function") return result(frame.id, false, `Unknown command: ${frame.name}`);
38
+ try {
39
+ return result(frame.id, true, await handler(frame.args));
40
+ } catch (error) {
41
+ return result(frame.id, false, errorMessage(error));
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Open the editor's one-way client connection. Failures are intentionally
47
+ * silent: a studio remains a fully local editor when the MCP server is absent.
48
+ */
49
+ export function createLiveControl({
50
+ handlers = {},
51
+ WebSocketImpl = globalThis.WebSocket,
52
+ url = LIVE_CONTROL_URL,
53
+ reconnectMs = LIVE_CONTROL_RECONNECT_MS,
54
+ } = {}) {
55
+ let currentHandlers = handlers;
56
+ let socket = null;
57
+ let retry = null;
58
+ let stopped = false;
59
+
60
+ const clearRetry = () => {
61
+ if (retry !== null) clearTimeout(retry);
62
+ retry = null;
63
+ };
64
+ const scheduleReconnect = () => {
65
+ if (stopped || retry !== null) return;
66
+ retry = setTimeout(() => {
67
+ retry = null;
68
+ connect();
69
+ }, reconnectMs);
70
+ };
71
+ const send = (frame) => {
72
+ if (!socket || socket.readyState !== (socket.OPEN ?? 1)) return;
73
+ try {
74
+ socket.send(JSON.stringify(frame));
75
+ } catch {
76
+ // A close between readyState and send is indistinguishable from an
77
+ // absent server to the editor, so leave it silent and retry on close.
78
+ }
79
+ };
80
+ const connect = () => {
81
+ if (stopped || !WebSocketImpl) return;
82
+ try {
83
+ socket = new WebSocketImpl(url);
84
+ } catch {
85
+ scheduleReconnect();
86
+ return;
87
+ }
88
+ const connected = socket;
89
+ connected.onopen = () => {
90
+ if (socket !== connected || stopped) return;
91
+ send({ type: "hello", role: "editor", version: 1 });
92
+ };
93
+ connected.onmessage = async (event) => {
94
+ const response = await dispatchLiveFrame(event?.data, currentHandlers);
95
+ if (response) send(response);
96
+ };
97
+ // Suppress browser error reporting for an optional local endpoint.
98
+ connected.onerror = () => {};
99
+ connected.onclose = () => {
100
+ if (socket === connected) socket = null;
101
+ scheduleReconnect();
102
+ };
103
+ };
104
+
105
+ connect();
106
+ return {
107
+ setHandlers(nextHandlers) {
108
+ currentHandlers = nextHandlers && typeof nextHandlers === "object" ? nextHandlers : {};
109
+ },
110
+ close() {
111
+ stopped = true;
112
+ clearRetry();
113
+ const current = socket;
114
+ socket = null;
115
+ try {
116
+ current?.close();
117
+ } catch {
118
+ // Optional transport cleanup must not affect the editor.
119
+ }
120
+ },
121
+ };
122
+ }