cozyclay 1.0.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/LICENSE +674 -0
- package/README.md +109 -0
- package/THIRD_PARTY_NOTICES.md +29 -0
- package/bin/cozyclay.mjs +194 -0
- package/dist/ardy/cskel27-rest.json +557 -0
- package/dist/assets/basis_transcoder-VXdx5NbI.wasm +0 -0
- package/dist/assets/basis_transcoder-o4Hde_L7.js +19 -0
- package/dist/assets/draco_decoder-C32yEggz.wasm +0 -0
- package/dist/assets/draco_decoder-Z1_iN-Ht.wasm +0 -0
- package/dist/assets/draco_decoder-fzg4nYZr.js +34 -0
- package/dist/assets/draco_wasm_wrapper-DxJM36Ib.js +117 -0
- package/dist/assets/draco_wasm_wrapper-fZCQGLGb.js +116 -0
- package/dist/assets/index-BW_S1YRy.js +4395 -0
- package/dist/assets/index-nRmmJgap.css +1 -0
- package/dist/demo/walk-then-stop.npz +0 -0
- package/dist/fonts/instrument-serif-italic-latin.woff2 +0 -0
- package/dist/fonts/instrument-serif-latin.woff2 +0 -0
- package/dist/fonts/inter-latin.woff2 +0 -0
- package/dist/index.html +13 -0
- package/dist/models/x-bot-tpose.fbx +0 -0
- package/dist/models/y-bot-tpose.fbx +0 -0
- package/package.json +80 -0
- package/src/App.jsx +3840 -0
- package/src/ardy/client.js +140 -0
- package/src/ardy/convert.js +313 -0
- package/src/ardy/cskel27-neutral.js +39 -0
- package/src/ardy/cskel27.js +68 -0
- package/src/ardy/export.js +157 -0
- package/src/ardy/ik.js +610 -0
- package/src/ardy/npz.js +520 -0
- package/src/ardy/playback.js +414 -0
- package/src/ardy/prompt-clips.js +16 -0
- package/src/ardy/timeline-coordinates.js +17 -0
- package/src/ardy/timeline-resize.js +11 -0
- package/src/ardy/timeline.jsx +723 -0
- package/src/ardy/to-cskel27.js +205 -0
- package/src/ardy/waypoints.js +364 -0
- package/src/camera-follow.js +366 -0
- package/src/camera-move.js +286 -0
- package/src/controls.jsx +245 -0
- package/src/dualview.jsx +305 -0
- package/src/hierarchy-model.js +77 -0
- package/src/hierarchy-panel.jsx +365 -0
- package/src/history.js +90 -0
- package/src/main.jsx +10 -0
- package/src/object-catalog.jsx +90 -0
- package/src/object-gizmo.jsx +755 -0
- package/src/planview.jsx +653 -0
- package/src/poses.js +424 -0
- package/src/posestudio.jsx +811 -0
- package/src/props.jsx +302 -0
- package/src/room.jsx +64 -0
- package/src/scene-history.js +125 -0
- package/src/scene-objects.js +420 -0
- package/src/shot-authoring.js +113 -0
- package/src/shot.js +239 -0
- package/src/styles.css +5639 -0
- package/src/ui.jsx +391 -0
- package/src/use-render-activity.js +96 -0
- package/tools/ardy/BRIDGE.md +255 -0
- package/tools/ardy/README.md +136 -0
- package/tools/ardy/__pycache__/cclay_sequence_generate.cpython-313.pyc +0 -0
- package/tools/ardy/bridge.mjs +1427 -0
- package/tools/ardy/cclay_motion_edit.py +445 -0
- package/tools/ardy/cclay_sequence_generate.py +595 -0
- package/tools/ardy/dump-npz.py +205 -0
- package/tools/ardy/extract-rest.mjs +299 -0
- package/tools/ardy/npz.mjs +335 -0
- package/tools/ardy/out/gen-1786443326924-c6019e-generated.npz +0 -0
- package/tools/ardy/out/gen-1786443609325-0053db-generated.npz +0 -0
- package/tools/ardy/out/gen-1786443835628-749ed1-generated.npz +0 -0
- package/tools/ardy/out/gen-1786462605247-03cb19-generated.npz +0 -0
- package/tools/ardy/pose-to-npz.mjs +106 -0
- package/tools/ardy/run-edit-on-box.sh +73 -0
- package/tools/ardy/run-on-box.sh +568 -0
- package/tools/ardy/run-sequence-on-box.sh +162 -0
- package/tools/ardy/visual-qa.mjs +188 -0
- package/tools/ardy/vq-car.mjs +52 -0
- package/tools/dev-full.mjs +29 -0
- package/tools/process-supervisor.mjs +63 -0
- package/tools/qa-browser.mjs +84 -0
- package/tools/qa-crop3.mjs +22 -0
- package/tools/qa-playview.mjs +49 -0
- package/tools/qa-screenshot.mjs +24 -0
- package/tools/qa-visual.mjs +51 -0
package/src/shot.js
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
// Pure math + string assembly. No three.js, no React.
|
|
2
|
+
// This file is the entire "brain" of a blocking tool: it turns 3D geometry
|
|
3
|
+
// into film vocabulary, then turns film vocabulary into a prompt.
|
|
4
|
+
|
|
5
|
+
export const SUBJECT_HEIGHT_M = 1.8;
|
|
6
|
+
|
|
7
|
+
// Framing distance is measured to the subject's centre of mass, not to the
|
|
8
|
+
// midpoint of their bounding box. A camera craned to the floor is still close
|
|
9
|
+
// to the body it is pointing at, and only this pivot reports that honestly.
|
|
10
|
+
export const FRAMING_PIVOT_Y = 1.3;
|
|
11
|
+
const SENSOR_HALF_HEIGHT_MM = 12; // Blender/full-frame default 24mm sensor height
|
|
12
|
+
|
|
13
|
+
/** vertical FOV (radians) -> focal length in mm on a 24mm-tall sensor */
|
|
14
|
+
export function fovToFocalMm(fovRad) {
|
|
15
|
+
return SENSOR_HALF_HEIGHT_MM / Math.tan(fovRad / 2);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** focal length in mm -> vertical FOV in radians */
|
|
19
|
+
export function focalMmToFov(mm) {
|
|
20
|
+
return 2 * Math.atan(SENSOR_HALF_HEIGHT_MM / mm);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// A crew does not carry a 47 mm lens. Reporting the nearest prime from a real
|
|
24
|
+
// set is both more honest and more useful to an image model, which has seen far
|
|
25
|
+
// more captions saying "35mm" than "47mm".
|
|
26
|
+
export const PRIME_SET = [14, 18, 24, 28, 35, 50, 85, 100, 135];
|
|
27
|
+
|
|
28
|
+
export function nearestPrime(fovRad) {
|
|
29
|
+
const exact = fovToFocalMm(fovRad);
|
|
30
|
+
return PRIME_SET.reduce((best, mm) => (Math.abs(mm - exact) < Math.abs(best - exact) ? mm : best));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// screenFraction = subject height / visible frame height at the subject.
|
|
34
|
+
// 1.0 means the figure exactly fills the frame top to bottom.
|
|
35
|
+
const SIZE_TABLE = [
|
|
36
|
+
[2.8, "extreme close-up", "ultra-realistic skin texture with visible pores and micro-detail, tack-sharp eyes, shallow depth of field"],
|
|
37
|
+
[1.6, "close-up", "ultra-realistic skin texture and fine facial detail, soft background blur, shallow depth of field"],
|
|
38
|
+
[1.15, "medium close-up", "natural skin and fabric texture, clear facial detail, shallow depth of field"],
|
|
39
|
+
[0.8, "medium shot", "balanced detail on the subject and the surrounding space, moderate depth of field"],
|
|
40
|
+
[0.52, "medium-wide shot", "the full figure with the environment clearly visible, deep focus"],
|
|
41
|
+
[0.3, "wide shot", "full body in a richly detailed environment, deep focus"],
|
|
42
|
+
[0, "extreme wide shot", "a small figure within a vast, highly detailed environment, epic sense of scale, deep focus"],
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
// A crew names the level by where the lens physically sits, not by the angle it
|
|
46
|
+
// subtends: "put it at hip level" is an instruction, "-27 degrees" is not.
|
|
47
|
+
// Thresholds are camera height above the floor, in metres.
|
|
48
|
+
const LEVEL_TABLE = [
|
|
49
|
+
[2.5, "overhead", "a directly overhead bird's-eye view looking down at the subject"],
|
|
50
|
+
[1.8, "high angle", "a high angle looking down at the subject"],
|
|
51
|
+
[1.5, "eye level", "eye level"],
|
|
52
|
+
[1.25, "chest level", "chest level"],
|
|
53
|
+
[0.95, "hip level", "a low hip-level angle"],
|
|
54
|
+
[0.45, "knee level", "a low knee-level angle looking up at the subject"],
|
|
55
|
+
[Number.NEGATIVE_INFINITY, "ground level", "a dramatic ground-level angle looking up at the subject"],
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
const pick = (table, value) => table.find(([threshold]) => value >= threshold) ?? table[table.length - 1];
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Derive film vocabulary from raw scene geometry.
|
|
62
|
+
* @param {{x:number,y:number,z:number}} cameraPos camera world position (metres)
|
|
63
|
+
* @param {{x:number,z:number,rot:number}} subject subject ground position + facing in degrees
|
|
64
|
+
* @param {number} fovRad vertical field of view
|
|
65
|
+
* @param {number} height subject height in metres
|
|
66
|
+
*/
|
|
67
|
+
export function deriveShot(cameraPos, subject, fovRad, height = SUBJECT_HEIGHT_M) {
|
|
68
|
+
const dx = cameraPos.x - subject.x;
|
|
69
|
+
const dz = cameraPos.z - subject.z;
|
|
70
|
+
const dy = cameraPos.y - FRAMING_PIVOT_Y;
|
|
71
|
+
const horizontal = Math.hypot(dx, dz);
|
|
72
|
+
const distance = Math.max(Math.hypot(horizontal, dy), 1e-6);
|
|
73
|
+
|
|
74
|
+
// how much of the frame height the subject occupies
|
|
75
|
+
const screenFraction = height / (2 * distance * Math.tan(fovRad / 2));
|
|
76
|
+
const [, sizeLabel, sizeContext] = pick(SIZE_TABLE, screenFraction);
|
|
77
|
+
|
|
78
|
+
// Level is simply how high off the floor the lens rides.
|
|
79
|
+
const [, levelLabel, levelPhrase] = pick(LEVEL_TABLE, cameraPos.y);
|
|
80
|
+
const elevationDeg = (Math.atan2(cameraPos.y - height * 0.94, Math.max(horizontal, 1e-6)) * 180) / Math.PI;
|
|
81
|
+
|
|
82
|
+
// Which side of the subject the camera sits on. The sign of the cross product
|
|
83
|
+
// tells us camera-right from camera-left, which the phrasing needs.
|
|
84
|
+
const facingRad = (subject.rot * Math.PI) / 180;
|
|
85
|
+
const facing = { x: Math.sin(facingRad), z: Math.cos(facingRad) };
|
|
86
|
+
const toCamera = { x: dx / Math.max(horizontal, 1e-6), z: dz / Math.max(horizontal, 1e-6) };
|
|
87
|
+
const alignment = facing.x * toCamera.x + facing.z * toCamera.z;
|
|
88
|
+
const side = facing.x * toCamera.z - facing.z * toCamera.x >= 0 ? "right" : "left";
|
|
89
|
+
const initial = side === "right" ? "R" : "L";
|
|
90
|
+
|
|
91
|
+
let viewShort;
|
|
92
|
+
let viewPhrase;
|
|
93
|
+
if (alignment > 0.85) {
|
|
94
|
+
viewShort = "front";
|
|
95
|
+
viewPhrase = "seen from the front, the face toward the camera";
|
|
96
|
+
} else if (alignment < -0.85) {
|
|
97
|
+
viewShort = "back";
|
|
98
|
+
viewPhrase = "seen from directly behind, the back of the head toward the camera";
|
|
99
|
+
} else if (alignment > 0.34) {
|
|
100
|
+
viewShort = `front ¾ ${initial}`;
|
|
101
|
+
viewPhrase = `a three-quarter front view from the ${side}`;
|
|
102
|
+
} else if (alignment < -0.34) {
|
|
103
|
+
viewShort = `rear ¾ ${initial}`;
|
|
104
|
+
viewPhrase = `a three-quarter rear view from the ${side}`;
|
|
105
|
+
} else {
|
|
106
|
+
viewShort = `${side} profile`;
|
|
107
|
+
viewPhrase = `a ${side}-side profile view`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
sizeLabel,
|
|
112
|
+
sizeContext,
|
|
113
|
+
levelLabel,
|
|
114
|
+
levelPhrase,
|
|
115
|
+
viewShort,
|
|
116
|
+
viewPhrase,
|
|
117
|
+
focalMm: nearestPrime(fovRad),
|
|
118
|
+
exactFocalMm: fovToFocalMm(fovRad),
|
|
119
|
+
distance,
|
|
120
|
+
screenFraction,
|
|
121
|
+
elevationDeg,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** the burned-in slate line, e.g. "MEDIUM SHOT · FRONT · EYE LEVEL · 45MM" */
|
|
126
|
+
export function slateLine(shot) {
|
|
127
|
+
return [shot.sizeLabel, shot.viewShort, shot.levelLabel, `${shot.focalMm}mm`]
|
|
128
|
+
.map((part) => part.toUpperCase())
|
|
129
|
+
.join(" · ");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// `numericLens` models respond to an explicit aperture; `audio` models accept a
|
|
133
|
+
// sound instruction that would be dead weight in a silent image prompt.
|
|
134
|
+
export const IMAGE_MODELS = [
|
|
135
|
+
{ id: "nano_banana_pro", label: "Nano Banana Pro" },
|
|
136
|
+
{ id: "nano_banana_2", label: "Nano Banana 2" },
|
|
137
|
+
{ id: "gpt_image_2", label: "GPT Image 2" },
|
|
138
|
+
{ id: "seedream_5", label: "Seedream 5.0" },
|
|
139
|
+
{ id: "flux_2", label: "Flux 2", numericLens: true, flavor: "fine film grain, high micro-detail" },
|
|
140
|
+
];
|
|
141
|
+
|
|
142
|
+
export const VIDEO_MODELS = [
|
|
143
|
+
{ id: "seedance_2", label: "Seedance 2.0", flavor: "one continuous shot" },
|
|
144
|
+
{ id: "kling_3", label: "Kling 3.0", flavor: "one continuous shot" },
|
|
145
|
+
{ id: "veo_3_1", label: "Veo 3.1", audio: true },
|
|
146
|
+
{ id: "wan_2_7", label: "Wan 2.7" },
|
|
147
|
+
{ id: "hailuo", label: "Minimax Hailuo", flavor: "natural physics and subtle facial emotion" },
|
|
148
|
+
{ id: "grok_1_5", label: "Grok Imagine 1.5" },
|
|
149
|
+
];
|
|
150
|
+
|
|
151
|
+
export const CUSTOM_MOVE = "Custom…";
|
|
152
|
+
|
|
153
|
+
export const CAMERA_MOVES = [
|
|
154
|
+
"Static / locked-off",
|
|
155
|
+
"Push-in (dolly in)",
|
|
156
|
+
"Pull-out (dolly out)",
|
|
157
|
+
"Pan left",
|
|
158
|
+
"Pan right",
|
|
159
|
+
"Tilt up",
|
|
160
|
+
"Tilt down",
|
|
161
|
+
"Tracking / follow",
|
|
162
|
+
"Orbit / arc",
|
|
163
|
+
"Crane up",
|
|
164
|
+
"Crane down",
|
|
165
|
+
"Handheld",
|
|
166
|
+
"Crash zoom in",
|
|
167
|
+
"Dolly-zoom (vertigo)",
|
|
168
|
+
"Whip pan",
|
|
169
|
+
"Aerial / drone",
|
|
170
|
+
CUSTOM_MOVE,
|
|
171
|
+
];
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Assemble the final prompt.
|
|
175
|
+
*
|
|
176
|
+
* The whole trick of a blocking tool is this paragraph. The attached frame is
|
|
177
|
+
* geometrically correct but visually worthless — grey clay in a grey box — so
|
|
178
|
+
* the prompt has to claim the geometry and disown the appearance in the same
|
|
179
|
+
* breath. Anything vaguer and the model paints grey mannequins.
|
|
180
|
+
*/
|
|
181
|
+
export function composePrompt({
|
|
182
|
+
mode = "image",
|
|
183
|
+
model,
|
|
184
|
+
shot,
|
|
185
|
+
subject,
|
|
186
|
+
subject2 = null,
|
|
187
|
+
posePhrase = "",
|
|
188
|
+
pose2Phrase = "",
|
|
189
|
+
environment,
|
|
190
|
+
style,
|
|
191
|
+
cameraMove = CAMERA_MOVES[0],
|
|
192
|
+
customMove = "",
|
|
193
|
+
hasCharSheet = false,
|
|
194
|
+
hasEnvSheet = false,
|
|
195
|
+
}) {
|
|
196
|
+
// a numeric-lens model wants glass it can reason about; the rest want the word
|
|
197
|
+
const lens = model?.numericLens ? `${shot.focalMm}mm lens at f/2.2` : `${shot.focalMm}mm lens`;
|
|
198
|
+
|
|
199
|
+
const opening =
|
|
200
|
+
mode === "video"
|
|
201
|
+
? `Cinematic ${shot.sizeLabel} at ${lens}, ${shot.levelPhrase}, ${shot.viewPhrase}.`
|
|
202
|
+
: `Photorealistic cinematic film still, ${shot.sizeLabel} at ${lens}, ${shot.levelPhrase}, ${shot.viewPhrase}.`;
|
|
203
|
+
|
|
204
|
+
const withPose = (who, phrase) => (phrase ? `${who}, ${phrase}` : who);
|
|
205
|
+
|
|
206
|
+
let cast;
|
|
207
|
+
if (hasCharSheet) {
|
|
208
|
+
cast = subject2
|
|
209
|
+
? "Both subjects are the characters from the attached character sheet. Each one exactly matches their own body pose and placement shown in the blocking frame."
|
|
210
|
+
: "The subject is the character from the attached character sheet, exactly matching the body pose and placement shown in the blocking frame.";
|
|
211
|
+
} else if (subject2) {
|
|
212
|
+
cast = `Two subjects. Subject 1: ${withPose(subject, posePhrase)}. Subject 2: ${withPose(subject2, pose2Phrase)}. Each subject exactly matches their own body pose and placement shown in the blocking frame.`;
|
|
213
|
+
} else {
|
|
214
|
+
cast = `Subject: ${withPose(subject, posePhrase)}, exactly matching the body pose and placement shown in the blocking frame.`;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const place = hasEnvSheet
|
|
218
|
+
? "The setting is the location from the attached environment sheet."
|
|
219
|
+
: `Environment: ${environment}.`;
|
|
220
|
+
|
|
221
|
+
const move = cameraMove === CUSTOM_MOVE ? customMove.trim() || "static, locked-off shot" : cameraMove.toLowerCase();
|
|
222
|
+
|
|
223
|
+
const guide =
|
|
224
|
+
"Use the attached blocking frame ONLY as a camera and staging guide — it fixes the exact framing, this camera angle, the lens, and the subject placement. Replace the entire setting with the described environment and render real people; do NOT reproduce its grey mannequins, grey box set, or flat lighting.";
|
|
225
|
+
|
|
226
|
+
const parts = [
|
|
227
|
+
opening,
|
|
228
|
+
cast,
|
|
229
|
+
place,
|
|
230
|
+
`${style}, ${shot.sizeContext}.`,
|
|
231
|
+
mode === "video" ? `Camera move: ${move}.` : null,
|
|
232
|
+
guide,
|
|
233
|
+
model?.flavor ?? null,
|
|
234
|
+
mode === "video" && model?.audio ? "Include natural ambient sound." : null,
|
|
235
|
+
"16:9.",
|
|
236
|
+
];
|
|
237
|
+
|
|
238
|
+
return parts.filter(Boolean).join(" ");
|
|
239
|
+
}
|