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
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Writing prompts the way ARDY was trained to read them.
3
+ *
4
+ * ARDY's own examples are the specification (nv-tlabs/ardy, scripts/generate.py):
5
+ *
6
+ * python scripts/generate.py "A person walks in a circle."
7
+ * python scripts/generate.py "A person jumps." --model core --duration 8.0
8
+ * python scripts/generate.py "A person waves." --model g1 --num_samples 4
9
+ *
10
+ * Three properties do the work: an explicit subject ("A person"), exactly one
11
+ * physical action, and a full stop. The demo's preset Prompt List is built the
12
+ * same way, so this is the distribution the text encoder actually saw.
13
+ *
14
+ * The reason one action per prompt matters is architectural, not stylistic:
15
+ * the interactive demo builds its denoiser with `num_text_tokens=1`
16
+ * (scripts/interactive_demo/loading.py), so the entire prompt collapses into a
17
+ * SINGLE conditioning embedding. Two actions in one sentence do not run in
18
+ * sequence — they average into one confused pose. A sequence of actions is
19
+ * expressed as a sequence of prompts (CozyClay's Prompt Blocks), never as a
20
+ * compound sentence.
21
+ *
22
+ * What that rules out, and what this module rewrites:
23
+ * - no subject "runs forward" -> "A person runs forward."
24
+ * - compound actions "stands, then runs" -> split across phases
25
+ * - interior states "in astonishment" -> dropped; ARDY animates
26
+ * bodies, not feelings
27
+ * - camera/scene language "the rocket looms" -> dropped; the prompt
28
+ * describes the BODY only
29
+ */
30
+
31
+ /**
32
+ * Quality policy, mirrored from the studio (App.jsx PROMPT_BLOCK_MAX_FRAMES):
33
+ * one block never spans more than 4 s. ARDY's trained window is 10 s, but a
34
+ * long single block drifts — chained 4 s blocks keep each call inside the
35
+ * model's sweet spot. The studio refuses to generate a longer block, so a tool
36
+ * that produced one would be authoring something the UI would then reject.
37
+ */
38
+ export const BLOCK_MAX_SECONDS = 4;
39
+
40
+ /**
41
+ * Split a beat that runs longer than the cap into consecutive blocks that do
42
+ * not. The wording is kept for each piece: continuing the same action is
43
+ * exactly what the chained-block design is for. Returns whole seconds-ish
44
+ * spans that sum to the original duration.
45
+ */
46
+ export function splitLongBeat(seconds, max = BLOCK_MAX_SECONDS) {
47
+ if (!(seconds > max)) return [seconds];
48
+ const count = Math.ceil(seconds / max);
49
+ const even = seconds / count;
50
+ return Array.from({ length: count }, () => even);
51
+ }
52
+
53
+ /** Shown in the tool description so a caller writes good phases first time. */
54
+ export const PROMPT_GUIDE = [
55
+ "ARDY prompt rules (from nv-tlabs/ardy):",
56
+ ' - One action per phase, phrased like ARDY\'s own examples: "A person walks in a circle."',
57
+ " - Subject + single present-tense action + full stop. The prompt becomes ONE embedding",
58
+ " (num_text_tokens=1), so two actions in one phase average together instead of playing in order.",
59
+ " - Sequence = more phases, never a compound sentence.",
60
+ ` - A block holds at most ${BLOCK_MAX_SECONDS} s. Longer beats are chained into consecutive blocks,`,
61
+ " because a single long block drifts away from its prompt.",
62
+ " - Describe the BODY: no emotions, no camera, no scenery, no props the model cannot infer.",
63
+ " - WRITE THE AMPLITUDE. A diffusion model regresses toward the mean, so a neutral verb",
64
+ " generates a smaller motion than the words suggest. Pick the strong verb and state the",
65
+ " magnitude: 'strides forward quickly' over 'walks forward', 'stops abruptly' over 'slows",
66
+ " to a stop', 'leans far back and looks straight up' over 'looks up'. Simultaneous detail",
67
+ " describing ONE pose is fine; sequential actions still need separate phases.",
68
+ ' - Good: ["A person strides forward quickly.", "A person stops abruptly.", "A person leans far back and looks straight up."]',
69
+ ' - Bad: ["walks forward slowly, then stops abruptly", "staggers back in astonishment"]',
70
+ ].join("\n");
71
+
72
+ /** Interior states and cinematic language ARDY has no body channel for. */
73
+ const UNRENDERABLE = [
74
+ /\b(in|with)\s+(astonishment|awe|wonder|surprise|fear|joy|excitement|disbelief)\b/gi,
75
+ /\b(astonished|amazed|awestruck|terrified|delighted|confused|nervous|curious)ly?\b/gi,
76
+ /\b(as if|like)\s+[^,.]+/gi,
77
+ /\b(at|toward|towards)\s+(something|the)\s+(enormous|huge|massive|towering|giant)\b/gi,
78
+ /\b(the\s+)?(camera|shot|frame|rocket|spaceship|building)\b[^,.]*/gi,
79
+ ];
80
+
81
+ /** Connectives that mean "a second action is hiding in this sentence". */
82
+ const SPLIT_ON = /\s*,?\s*\b(?:and then|then|after that|before|while|as)\b\s*/i;
83
+
84
+ const SUBJECT = /^(a|the)\s+(person|man|woman|character|figure|human)\b/i;
85
+
86
+ const tidy = (s) =>
87
+ s
88
+ .replace(/\s+/g, " ")
89
+ .replace(/\s+([,.])/g, "$1")
90
+ .replace(/[,;]+$/, "")
91
+ .trim();
92
+
93
+ /**
94
+ * Rewrite one caller phrase into ARDY's sentence shape.
95
+ * Returns `{ text, notes }` — notes explain every edit, so a caller can see
96
+ * why their wording changed rather than silently getting something else.
97
+ */
98
+ export function normalizePhase(raw) {
99
+ const notes = [];
100
+ let s = tidy(String(raw ?? ""));
101
+ if (!s) return { text: "", notes: ["empty phrase"] };
102
+
103
+ for (const pattern of UNRENDERABLE) {
104
+ if (pattern.test(s)) {
105
+ s = tidy(s.replace(pattern, ""));
106
+ notes.push("dropped language ARDY cannot animate (emotion, camera or scenery)");
107
+ break;
108
+ }
109
+ }
110
+
111
+ // Keep the first action; a trailing clause belongs in its own phase.
112
+ const parts = s.split(SPLIT_ON).map(tidy).filter(Boolean);
113
+ if (parts.length > 1) {
114
+ s = parts[0];
115
+ notes.push(`kept the first action; "${parts.slice(1).join(" / ")}" belongs in its own phase`);
116
+ }
117
+
118
+ // A comma usually joins two actions too ("walks forward, stops abruptly").
119
+ const commaParts = s.split(/\s*,\s*/).map(tidy).filter(Boolean);
120
+ if (commaParts.length > 1) {
121
+ s = commaParts[0];
122
+ notes.push("kept one action per prompt");
123
+ }
124
+
125
+ s = s.replace(/^(and|then|so)\s+/i, "");
126
+ if (!SUBJECT.test(s)) {
127
+ s = s.replace(/^[A-Z]/, (c) => c.toLowerCase());
128
+ s = `A person ${s}`;
129
+ notes.push('added the subject ARDY expects ("A person ...")');
130
+ } else {
131
+ s = s.replace(/^./, (c) => c.toUpperCase());
132
+ }
133
+
134
+ if (!/[.!?]$/.test(s)) {
135
+ s = `${s}.`;
136
+ notes.push("closed the sentence");
137
+ }
138
+ return { text: tidy(s), notes };
139
+ }
140
+
141
+ /**
142
+ * Normalise a whole beat list. A phrase that carried a trailing action gets it
143
+ * back as its own phase, so "stands up then runs" becomes two real beats
144
+ * instead of one averaged embedding — capped so a caller cannot blow past the
145
+ * schema's phase limit.
146
+ */
147
+ export function normalizePhases(phases, max = 8) {
148
+ const expanded = [];
149
+ const sources = [];
150
+ for (const [index, phase] of phases.entries()) {
151
+ const pieces = String(phase ?? "")
152
+ .split(SPLIT_ON)
153
+ .map(tidy)
154
+ .filter(Boolean);
155
+ const parts = pieces.length ? pieces : [phase];
156
+ expanded.push(...parts);
157
+ // Which input each output came from, so a caller that attached a duration
158
+ // to a beat can split that duration across the pieces it became.
159
+ sources.push(...parts.map(() => index));
160
+ }
161
+ const capped = expanded.slice(0, max);
162
+ const results = capped.map((piece) => normalizePhase(piece));
163
+ return {
164
+ texts: results.map((r) => r.text),
165
+ notes: results.map((r) => r.notes),
166
+ sources: sources.slice(0, max),
167
+ expanded: expanded.length > phases.length,
168
+ dropped: expanded.length - capped.length,
169
+ };
170
+ }
@@ -0,0 +1,105 @@
1
+ import { randomUUID } from "node:crypto";
2
+
3
+ import { WebSocket, WebSocketServer } from "ws";
4
+
5
+ const CLOSE_REPLACED = 4000;
6
+ const COMMAND_TIMEOUT_MS = 5_000;
7
+
8
+ /**
9
+ * Transport-only implementation of LIVE-PROTOCOL.md. Scene semantics remain
10
+ * with the editor and the server's existing CozyClay imports.
11
+ */
12
+ export class LiveHub {
13
+ constructor(server = null) {
14
+ this.server = server;
15
+ this.editor = null;
16
+ this.pending = new Map();
17
+ }
18
+
19
+ get connected() {
20
+ return this.editor?.readyState === WebSocket.OPEN;
21
+ }
22
+
23
+ async command(name, args) {
24
+ const socket = this.editor;
25
+ if (!socket || socket.readyState !== WebSocket.OPEN) throw new Error("No live editor is connected.");
26
+
27
+ const id = randomUUID();
28
+ return new Promise((resolve, reject) => {
29
+ const timer = setTimeout(() => {
30
+ this.pending.delete(id);
31
+ reject(new Error(`Live editor timed out running ${name}.`));
32
+ }, COMMAND_TIMEOUT_MS);
33
+ this.pending.set(id, { socket, resolve, reject, timer });
34
+ try {
35
+ socket.send(JSON.stringify({ type: "cmd", id, name, args }));
36
+ } catch (error) {
37
+ clearTimeout(timer);
38
+ this.pending.delete(id);
39
+ reject(new Error(`Could not send ${name} to the live editor: ${error.message}`));
40
+ }
41
+ });
42
+ }
43
+
44
+ accept(socket) {
45
+ let greeted = false;
46
+ socket.on("message", (message, isBinary) => {
47
+ if (isBinary) return;
48
+ let frame;
49
+ try {
50
+ frame = JSON.parse(message.toString());
51
+ } catch {
52
+ return;
53
+ }
54
+ if (!greeted) {
55
+ if (frame?.type !== "hello" || frame.role !== "editor" || frame.version !== 1) {
56
+ socket.close(1002, "Expected editor hello version 1");
57
+ return;
58
+ }
59
+ greeted = true;
60
+ const displaced = this.editor;
61
+ this.editor = socket;
62
+ if (displaced && displaced !== socket) displaced.close(CLOSE_REPLACED, "Replaced by a newer editor");
63
+ return;
64
+ }
65
+ if (frame?.type !== "result" || typeof frame.id !== "string") return;
66
+ const pending = this.pending.get(frame.id);
67
+ if (!pending || pending.socket !== socket) return;
68
+ clearTimeout(pending.timer);
69
+ this.pending.delete(frame.id);
70
+ if (frame.ok === true) pending.resolve(frame.value);
71
+ else pending.reject(new Error(typeof frame.error === "string" ? frame.error : "Live editor rejected the command."));
72
+ });
73
+ socket.on("close", () => this.disconnect(socket));
74
+ socket.on("error", () => this.disconnect(socket));
75
+ }
76
+
77
+ disconnect(socket) {
78
+ if (this.editor === socket) this.editor = null;
79
+ for (const [id, pending] of this.pending) {
80
+ if (pending.socket !== socket) continue;
81
+ clearTimeout(pending.timer);
82
+ this.pending.delete(id);
83
+ pending.reject(new Error("Live editor disconnected while the command was running."));
84
+ }
85
+ }
86
+ }
87
+
88
+ /** Bind only to loopback. A taken port is an intentional memory-only mode. */
89
+ export async function startLiveHub(port) {
90
+ let server;
91
+ try {
92
+ server = new WebSocketServer({ host: "127.0.0.1", port, path: "/live" });
93
+ await new Promise((resolve, reject) => {
94
+ server.once("listening", resolve);
95
+ server.once("error", reject);
96
+ });
97
+ } catch (error) {
98
+ if (server) server.close();
99
+ if (error?.code === "EADDRINUSE") return null;
100
+ throw error;
101
+ }
102
+ const hub = new LiveHub(server);
103
+ server.on("connection", (socket) => hub.accept(socket));
104
+ return hub;
105
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@cozyclay/mcp",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "description": "MCP server for CozyClay — block a scene, place the camera, and get film vocabulary and AI prompts back.",
6
+ "type": "module",
7
+ "bin": {
8
+ "cozyclay-mcp": "server.mjs"
9
+ },
10
+ "scripts": {
11
+ "start": "node server.mjs",
12
+ "verify": "node verify.mjs",
13
+ "verify:prompts": "node verify-prompts.mjs",
14
+ "verify:live": "node verify-live.mjs"
15
+ },
16
+ "engines": {
17
+ "node": ">=22"
18
+ },
19
+ "dependencies": {
20
+ "@modelcontextprotocol/sdk": "^1.30.0",
21
+ "ws": "^8.19.0",
22
+ "zod": "^3.25.0"
23
+ }
24
+ }