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.
- package/CHANGELOG.md +85 -0
- package/README.md +31 -0
- package/THIRD_PARTY_NOTICES.md +37 -1
- package/bin/cozyclay.mjs +51 -2
- package/dist/ai-camera-control/index.html +406 -0
- package/dist/app/index.html +5 -5
- package/dist/assets/app-B3U5aut1.js +4811 -0
- package/dist/assets/app-BrRF0wso.css +1 -0
- package/dist/assets/vision_bundle-jFkh-fIS.js +41 -0
- package/dist/fonts/InstrumentSerif-OFL.txt +93 -0
- package/dist/fonts/Inter-OFL.txt +92 -0
- package/dist/fonts/README.md +15 -0
- package/dist/index.html +60 -16
- package/dist/sitemap.xml +7 -1
- package/mcp/LIVE-PROTOCOL.md +63 -0
- package/mcp/README.md +142 -0
- package/mcp/ardy-prompts.mjs +170 -0
- package/mcp/live-hub.mjs +105 -0
- package/mcp/package.json +24 -0
- package/mcp/server.mjs +1394 -0
- package/package.json +122 -90
- package/src/App.jsx +2957 -514
- package/src/ardy/cskel27.js +7 -2
- package/src/ardy/ik.js +25 -15
- package/src/ardy/npz.js +64 -3
- package/src/ardy/playback.js +31 -1
- package/src/ardy/prompt-clips.js +7 -2
- package/src/ardy/retime.js +211 -0
- package/src/ardy/timeline-coordinates.js +13 -0
- package/src/ardy/timeline.jsx +124 -5
- package/src/ardy/to-cskel27.js +34 -12
- package/src/ardy/trim.js +33 -0
- package/src/asset-pane.jsx +36 -0
- package/src/dualview.jsx +14 -8
- package/src/hierarchy-model.js +95 -13
- package/src/hierarchy-panel.jsx +139 -6
- package/src/live-control.js +122 -0
- package/src/matte-editor.js +543 -0
- package/src/matte.js +503 -0
- package/src/multimodel-ingest.js +344 -0
- package/src/object-gizmo.jsx +43 -15
- package/src/planview.jsx +43 -32
- package/src/pose-extract/detector.js +75 -0
- package/src/pose-extract/index.js +3 -0
- package/src/pose-extract/take.js +87 -0
- package/src/pose-extract/video-frames.js +91 -0
- package/src/pose-thumbs.js +152 -0
- package/src/posestudio.jsx +361 -11
- package/src/project-browser.jsx +135 -0
- package/src/project.js +289 -0
- package/src/props.jsx +69 -3
- package/src/room.jsx +14 -35
- package/src/scene-asset-cache.js +125 -0
- package/src/scene-assets.js +288 -0
- package/src/scene-objects.js +245 -7
- package/src/scenes.js +207 -26
- package/src/shot-authoring.js +55 -13
- package/src/styles.css +1296 -129
- package/tools/ardy/BRIDGE.md +3 -2
- package/tools/ardy/README.md +9 -5
- package/tools/ardy/bridge.mjs +57 -1
- package/tools/ardy/bvh-cskel27.mjs +1209 -0
- package/tools/ardy/cclay_constrained_generate.py +123 -11
- package/tools/ardy/cclay_sequence_generate.py +49 -0
- package/tools/ardy/extract.mjs +367 -0
- package/tools/ardy/footage.mjs +462 -0
- package/tools/ardy/npz.mjs +74 -9
- package/tools/ardy/run-on-box.sh +25 -0
- package/tools/ardy/run-sequence-on-box.sh +15 -0
- package/tools/ardy/runners/remote.mjs +8 -2
- package/dist/assets/app-Cgpk2hwX.js +0 -4803
- 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
|
+
}
|
package/mcp/live-hub.mjs
ADDED
|
@@ -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
|
+
}
|
package/mcp/package.json
ADDED
|
@@ -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
|
+
}
|