cozyclay 1.0.0 → 1.2.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/README.md +59 -24
- package/THIRD_PARTY_NOTICES.md +31 -0
- package/bin/cozyclay.mjs +105 -11
- package/dist/CNAME +1 -0
- package/dist/app/index.html +57 -0
- package/dist/assets/app-Cgpk2hwX.js +4803 -0
- package/dist/assets/app-DgZvaAE1.css +1 -0
- package/dist/favicon.ico +0 -0
- package/dist/icons/apple-touch-icon.png +0 -0
- package/dist/icons/icon-192.png +0 -0
- package/dist/icons/icon-32.png +0 -0
- package/dist/icons/icon-512.png +0 -0
- package/dist/icons/icon-maskable-512.png +0 -0
- package/dist/index.html +406 -5
- package/dist/manifest.webmanifest +42 -0
- package/dist/media/cozyclay-demo-poster.jpg +0 -0
- package/dist/media/cozyclay-logo.png +0 -0
- package/dist/robots.txt +4 -0
- package/dist/sitemap.xml +9 -0
- package/dist/sw.js +127 -0
- package/package.json +15 -3
- package/src/App.jsx +1393 -648
- package/src/ardy/client.js +5 -4
- package/src/ardy/mp4-duration.js +102 -0
- package/src/ardy/timeline-coordinates.js +11 -0
- package/src/ardy/timeline.jsx +528 -67
- package/src/ardy/waypoints.js +14 -9
- package/src/camera-block.js +60 -0
- package/src/camera-follow.js +49 -10
- package/src/camera-rail-schedule.js +176 -0
- package/src/controls.jsx +16 -2
- package/src/cuts.js +184 -0
- package/src/generation/client.js +56 -0
- package/src/generation/session.js +47 -0
- package/src/generation/shot-spec.js +40 -0
- package/src/generation/use-generation.js +55 -0
- package/src/hierarchy-model.js +9 -17
- package/src/hierarchy-panel.jsx +170 -23
- package/src/locale-toggle.jsx +18 -0
- package/src/locale.js +34 -0
- package/src/main.jsx +5 -0
- package/src/object-catalog.jsx +32 -3
- package/src/planview.jsx +174 -66
- package/src/pose-extract/browser.js +42 -0
- package/src/pose-extract/fit.js +194 -0
- package/src/pose-extract/index.js +53 -0
- package/src/pose-extract/keyframes.js +74 -0
- package/src/pose-extract/landmarks.js +162 -0
- package/src/poses.js +114 -0
- package/src/posestudio.jsx +75 -13
- package/src/pwa.js +42 -0
- package/src/result-modal.jsx +124 -0
- package/src/scenes.js +223 -0
- package/src/shot-authoring.js +204 -77
- package/src/shot.js +8 -4
- package/src/styles.css +839 -38
- package/src/ui.jsx +54 -5
- package/tools/ardy/BRIDGE.md +70 -4
- package/tools/ardy/README.md +55 -0
- package/tools/ardy/bridge.mjs +89 -389
- package/tools/ardy/cclay_constrained_generate.py +449 -0
- package/tools/ardy/cclay_motion_edit.py +74 -9
- package/tools/ardy/cclay_sequence_generate.py +68 -7
- package/tools/ardy/cclay_worker.py +304 -0
- package/tools/ardy/merge-text-encoder.py +160 -0
- package/tools/ardy/run-local.mjs +233 -0
- package/tools/ardy/runners/index.mjs +23 -0
- package/tools/ardy/runners/local.mjs +452 -0
- package/tools/ardy/runners/proc.mjs +125 -0
- package/tools/ardy/runners/remote.mjs +246 -0
- package/tools/ardy/setup-local.mjs +189 -0
- package/tools/ardy/setup-text-encoder-on-box.sh +93 -0
- package/tools/ardy/setup-text-encoder.py +296 -0
- package/tools/generation/README.md +29 -0
- package/tools/generation/bridge.mjs +149 -0
- package/tools/generation/job-store.mjs +51 -0
- package/tools/generation/providers/kling.mjs +76 -0
- package/tools/generation/providers/runway.mjs +101 -0
- package/tools/generation/providers/seedance.mjs +87 -0
- package/tools/generation/providers/shared.mjs +49 -0
- package/tools/generation/providers/veo.mjs +84 -0
- package/tools/metrics/archive-traffic.mjs +138 -0
- package/dist/assets/index-BW_S1YRy.js +0 -4395
- package/dist/assets/index-nRmmJgap.css +0 -1
- package/tools/ardy/__pycache__/cclay_sequence_generate.cpython-313.pyc +0 -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/qa-browser.mjs +0 -84
- package/tools/qa-crop3.mjs +0 -22
- package/tools/qa-playview.mjs +0 -49
- package/tools/qa-screenshot.mjs +0 -24
- package/tools/qa-visual.mjs +0 -51
package/src/cuts.js
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
// Pure optional Shot-overlay model. Shots are camera ownership cards placed
|
|
2
|
+
// on a timeline: gaps are valid free-camera time, overlaps are rejected.
|
|
3
|
+
|
|
4
|
+
import { cameraMoveAt } from "./camera-move.js";
|
|
5
|
+
import { cloneCameraBlock, createCameraBlock } from "./camera-block.js";
|
|
6
|
+
|
|
7
|
+
export const DEFAULT_SHOT_FRAMES = 40;
|
|
8
|
+
|
|
9
|
+
let nextShotId = 1;
|
|
10
|
+
const id = () => `shot-${Date.now().toString(36)}-${nextShotId++}`;
|
|
11
|
+
const frameNumber = (value, fallback = 0) => Number.isFinite(value) ? Math.round(value) : fallback;
|
|
12
|
+
|
|
13
|
+
function cloneFraming(framing) {
|
|
14
|
+
return framing ? { ...framing, pos: framing.pos ? { ...framing.pos } : framing.pos } : framing;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function uniqueKeys(keys, min = -Infinity, max = Infinity) {
|
|
18
|
+
const byFrame = new Map();
|
|
19
|
+
for (const key of Array.isArray(keys) ? keys : []) {
|
|
20
|
+
if (!key || !Number.isFinite(key.frame) || !key.framing) continue;
|
|
21
|
+
const frame = Math.max(min, Math.min(max, Math.round(key.frame)));
|
|
22
|
+
byFrame.set(frame, { frame, framing: cloneFraming(key.framing) });
|
|
23
|
+
}
|
|
24
|
+
return [...byFrame.values()].sort((a, b) => a.frame - b.frame);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function createShot(name = "Shot", startFrame = 0, endFrame = startFrame + DEFAULT_SHOT_FRAMES - 1, cameraKeys = [], camera = null) {
|
|
28
|
+
const start = Math.max(0, frameNumber(startFrame));
|
|
29
|
+
const end = Math.max(start, frameNumber(endFrame, start));
|
|
30
|
+
return {
|
|
31
|
+
id: id(),
|
|
32
|
+
name: typeof name === "string" && name.trim() ? name.trim() : "Shot",
|
|
33
|
+
startFrame: start,
|
|
34
|
+
endFrame: end,
|
|
35
|
+
cameraKeys: uniqueKeys(cameraKeys, start, end),
|
|
36
|
+
camera: createCameraBlock(camera),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** New documents begin without an implicit camera owner. */
|
|
41
|
+
export function initialShots() {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function shotIndexAtFrame(shots, frame) {
|
|
46
|
+
if (!Array.isArray(shots) || !shots.length) return -1;
|
|
47
|
+
const target = Math.max(0, frameNumber(frame));
|
|
48
|
+
let low = 0;
|
|
49
|
+
let high = shots.length - 1;
|
|
50
|
+
while (low <= high) {
|
|
51
|
+
const middle = Math.floor((low + high) / 2);
|
|
52
|
+
const shot = shots[middle];
|
|
53
|
+
if (target < shot.startFrame) high = middle - 1;
|
|
54
|
+
else if (target > shot.endFrame) low = middle + 1;
|
|
55
|
+
else return middle;
|
|
56
|
+
}
|
|
57
|
+
return -1;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function shotAtFrame(shots, frame) {
|
|
61
|
+
const index = shotIndexAtFrame(shots, frame);
|
|
62
|
+
return index < 0 ? null : shots[index];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function overlaps(shots, startFrame, endFrame, ignoredIndex = -1) {
|
|
66
|
+
return shots.some((shot, index) => index !== ignoredIndex && startFrame <= shot.endFrame && endFrame >= shot.startFrame);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Split only the Shot that actually covers the cut frame. Gap frames remain gaps. */
|
|
70
|
+
export function cutAtFrame(shots, frame, currentFraming) {
|
|
71
|
+
if (!Array.isArray(shots) || !currentFraming) return shots;
|
|
72
|
+
const cutFrame = frameNumber(frame);
|
|
73
|
+
const index = shotIndexAtFrame(shots, cutFrame);
|
|
74
|
+
if (index < 0) return shots;
|
|
75
|
+
const source = shots[index];
|
|
76
|
+
if (cutFrame <= source.startFrame || cutFrame > source.endFrame) return shots;
|
|
77
|
+
const upstream = {
|
|
78
|
+
...source,
|
|
79
|
+
endFrame: cutFrame - 1,
|
|
80
|
+
cameraKeys: uniqueKeys(source.cameraKeys, source.startFrame, cutFrame - 1),
|
|
81
|
+
camera: cloneCameraBlock(source.camera),
|
|
82
|
+
};
|
|
83
|
+
const downstream = createShot(`Shot ${shots.length + 1}`, cutFrame, source.endFrame, [
|
|
84
|
+
...source.cameraKeys.filter((key) => key.frame >= cutFrame),
|
|
85
|
+
{ frame: cutFrame, framing: currentFraming },
|
|
86
|
+
], source.camera);
|
|
87
|
+
return [...shots.slice(0, index), upstream, downstream, ...shots.slice(index + 1)];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Add a 2-second Shot without editing an existing one. If the playhead is
|
|
92
|
+
* occupied, place the new card in the next full-size free range instead. Add
|
|
93
|
+
* and Split are separate editorial actions: hanging a new card on the board
|
|
94
|
+
* must never cut the card already there.
|
|
95
|
+
*/
|
|
96
|
+
export function addShotAtFrame(shots, frame, frameCount, currentFraming, duration = DEFAULT_SHOT_FRAMES) {
|
|
97
|
+
const list = Array.isArray(shots) ? shots : [];
|
|
98
|
+
const count = Math.max(1, frameNumber(frameCount, 1));
|
|
99
|
+
const target = Math.max(0, Math.min(count - 1, frameNumber(frame)));
|
|
100
|
+
const inside = shotIndexAtFrame(list, target);
|
|
101
|
+
if (inside >= 0) {
|
|
102
|
+
const source = list[inside];
|
|
103
|
+
const length = Math.max(1, frameNumber(duration, DEFAULT_SHOT_FRAMES));
|
|
104
|
+
const startFrame = freeStart(list, length, count, source.endFrame + 1);
|
|
105
|
+
if (startFrame === undefined) return list;
|
|
106
|
+
const keys = currentFraming ? [{ frame: startFrame, framing: currentFraming }] : [];
|
|
107
|
+
return [...list, createShot(`Shot ${list.length + 1}`, startFrame, startFrame + length - 1, keys)].sort((a, b) => a.startFrame - b.startFrame);
|
|
108
|
+
}
|
|
109
|
+
const nextStart = list.find((shot) => shot.startFrame > target)?.startFrame ?? count;
|
|
110
|
+
const endFrame = Math.min(count - 1, nextStart - 1, target + Math.max(1, frameNumber(duration, DEFAULT_SHOT_FRAMES)) - 1);
|
|
111
|
+
if (endFrame < target || overlaps(list, target, endFrame)) return list;
|
|
112
|
+
const keys = currentFraming ? [{ frame: target, framing: currentFraming }] : [];
|
|
113
|
+
return [...list, createShot(`Shot ${list.length + 1}`, target, endFrame, keys)].sort((a, b) => a.startFrame - b.startFrame);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function resizeShot(shots, index, edge, rawFrame, frameCount) {
|
|
117
|
+
if (!Array.isArray(shots) || index < 0 || index >= shots.length || (edge !== "start" && edge !== "end")) return shots;
|
|
118
|
+
const count = Math.max(1, frameNumber(frameCount, 1));
|
|
119
|
+
const shot = shots[index];
|
|
120
|
+
const frame = Math.max(0, Math.min(count - 1, frameNumber(rawFrame, edge === "start" ? shot.startFrame : shot.endFrame)));
|
|
121
|
+
const startFrame = edge === "start" ? Math.min(frame, shot.endFrame) : shot.startFrame;
|
|
122
|
+
const endFrame = edge === "end" ? Math.max(frame, shot.startFrame) : shot.endFrame;
|
|
123
|
+
if ((startFrame === shot.startFrame && endFrame === shot.endFrame) || overlaps(shots, startFrame, endFrame, index)) return shots;
|
|
124
|
+
const next = shots.slice();
|
|
125
|
+
next[index] = { ...shot, startFrame, endFrame, cameraKeys: uniqueKeys(shot.cameraKeys, startFrame, endFrame) };
|
|
126
|
+
return next.sort((a, b) => a.startFrame - b.startFrame);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Compatibility name for callers that move the left edge. */
|
|
130
|
+
export function moveBoundary(shots, index, newStartFrame, frameCount) {
|
|
131
|
+
return resizeShot(shots, index, "start", newStartFrame, frameCount);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function removeShot(shots, index) {
|
|
135
|
+
if (!Array.isArray(shots) || index < 0 || index >= shots.length) return shots;
|
|
136
|
+
return shots.filter((_, shotIndex) => shotIndex !== index);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function renameShot(shots, index, name) {
|
|
140
|
+
if (!Array.isArray(shots) || index < 0 || index >= shots.length || typeof name !== "string" || !name.trim()) return shots;
|
|
141
|
+
return shots.map((shot, shotIndex) => shotIndex === index ? { ...shot, name: name.trim() } : shot);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Move a card in absolute time. Duration and local camera instruction travel together. */
|
|
145
|
+
export function reorderShot(shots, fromIndex, rawStartFrame, frameCount) {
|
|
146
|
+
if (!Array.isArray(shots) || fromIndex < 0 || fromIndex >= shots.length) return shots;
|
|
147
|
+
const shot = shots[fromIndex];
|
|
148
|
+
const count = Math.max(1, frameNumber(frameCount, 1));
|
|
149
|
+
const duration = shot.endFrame - shot.startFrame + 1;
|
|
150
|
+
const startFrame = Math.max(0, Math.min(count - duration, frameNumber(rawStartFrame, shot.startFrame)));
|
|
151
|
+
const endFrame = startFrame + duration - 1;
|
|
152
|
+
if (startFrame === shot.startFrame || overlaps(shots, startFrame, endFrame, fromIndex)) return shots;
|
|
153
|
+
const delta = startFrame - shot.startFrame;
|
|
154
|
+
const moved = {
|
|
155
|
+
...shot,
|
|
156
|
+
startFrame,
|
|
157
|
+
endFrame,
|
|
158
|
+
cameraKeys: uniqueKeys(shot.cameraKeys.map((key) => ({ ...key, frame: key.frame + delta })), startFrame, endFrame),
|
|
159
|
+
};
|
|
160
|
+
return shots.map((entry, index) => index === fromIndex ? moved : entry).sort((a, b) => a.startFrame - b.startFrame);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function freeStart(shots, duration, frameCount, preferred) {
|
|
164
|
+
const candidates = [preferred, 0, ...shots.map((shot) => shot.endFrame + 1)];
|
|
165
|
+
return candidates.find((start) => start >= 0 && start + duration <= frameCount && !overlaps(shots, start, start + duration - 1));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function duplicateShot(shots, index, frameCount) {
|
|
169
|
+
if (!Array.isArray(shots) || index < 0 || index >= shots.length) return shots;
|
|
170
|
+
const source = shots[index];
|
|
171
|
+
const duration = source.endFrame - source.startFrame + 1;
|
|
172
|
+
const startFrame = freeStart(shots, duration, Math.max(1, frameNumber(frameCount, 1)), source.endFrame + 1);
|
|
173
|
+
if (startFrame === undefined) return shots;
|
|
174
|
+
const delta = startFrame - source.startFrame;
|
|
175
|
+
const duplicate = createShot(`${source.name} copy`, startFrame, startFrame + duration - 1,
|
|
176
|
+
source.cameraKeys.map((key) => ({ ...key, frame: key.frame + delta })), source.camera);
|
|
177
|
+
return [...shots, duplicate].sort((a, b) => a.startFrame - b.startFrame);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Gaps deliberately return null; the caller interprets null as free camera. */
|
|
181
|
+
export function cameraAtFrame(shots, anchor, frame) {
|
|
182
|
+
const shot = shotAtFrame(shots, frame);
|
|
183
|
+
return shot ? cameraMoveAt(shot.cameraKeys, anchor, frame) : null;
|
|
184
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const JSON_HEADERS = { "Content-Type": "application/json" };
|
|
2
|
+
const TIMEOUT_MS = 5000;
|
|
3
|
+
const RETRYABLE = new Set([429, 500, 502, 503, 504]);
|
|
4
|
+
|
|
5
|
+
async function requestJson(path, options = {}, timeoutMs = TIMEOUT_MS, retries = 0) {
|
|
6
|
+
for (let attempt = 0; attempt <= retries; attempt += 1) {
|
|
7
|
+
const timeout = AbortSignal.timeout(timeoutMs);
|
|
8
|
+
const signal = options.signal ? AbortSignal.any([options.signal, timeout]) : timeout;
|
|
9
|
+
let response;
|
|
10
|
+
try {
|
|
11
|
+
response = await fetch(path, { ...options, signal });
|
|
12
|
+
} catch (error) {
|
|
13
|
+
if (options.signal?.aborted || attempt === retries) throw error;
|
|
14
|
+
await new Promise((resolve) => setTimeout(resolve, 250 * (2 ** attempt)));
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
const payload = await response.json().catch(() => ({}));
|
|
18
|
+
if (response.ok) return payload;
|
|
19
|
+
if (!RETRYABLE.has(response.status) || attempt === retries) {
|
|
20
|
+
throw new Error(payload.reason || payload.errors?.[0] || `generation request failed (HTTP ${response.status})`);
|
|
21
|
+
}
|
|
22
|
+
await new Promise((resolve) => setTimeout(resolve, 250 * (2 ** attempt)));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function checkGenerationBridge() {
|
|
27
|
+
try {
|
|
28
|
+
return await requestJson("/generation/health");
|
|
29
|
+
} catch (error) { return { ok: false, reason: error.message || "generation bridge unreachable" }; }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function listGenerationModels() {
|
|
33
|
+
const payload = await requestJson("/generation/models");
|
|
34
|
+
return Array.isArray(payload.models) ? payload.models : [];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function listGenerationJobs() {
|
|
38
|
+
const payload = await requestJson("/generation/jobs");
|
|
39
|
+
return Array.isArray(payload.jobs) ? payload.jobs : [];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function validateGeneration(spec, { signal } = {}) {
|
|
43
|
+
return requestJson("/generation/validate", { method: "POST", headers: JSON_HEADERS, body: JSON.stringify(spec), signal });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function submitGeneration(spec, { signal } = {}) {
|
|
47
|
+
return requestJson("/generation/jobs", { method: "POST", headers: JSON_HEADERS, body: JSON.stringify(spec), signal }, 30000, 2);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function getGenerationJob(id, { signal } = {}) {
|
|
51
|
+
return requestJson(`/generation/jobs/${encodeURIComponent(id)}`, { signal }, 30000, 3);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function cancelGenerationJob(id) {
|
|
55
|
+
return requestJson(`/generation/jobs/${encodeURIComponent(id)}`, { method: "DELETE" }, 30000, 1);
|
|
56
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { cancelGenerationJob, getGenerationJob, submitGeneration, validateGeneration } from "./client.js";
|
|
2
|
+
|
|
3
|
+
const TERMINAL = new Set(["succeeded", "failed"]);
|
|
4
|
+
|
|
5
|
+
export async function runGeneration(spec, {
|
|
6
|
+
signal,
|
|
7
|
+
timeoutMs = 10 * 60 * 1000,
|
|
8
|
+
initialDelayMs = 5000,
|
|
9
|
+
maxDelayMs = 30000,
|
|
10
|
+
validate = validateGeneration,
|
|
11
|
+
submit = submitGeneration,
|
|
12
|
+
poll = getGenerationJob,
|
|
13
|
+
cancel = cancelGenerationJob,
|
|
14
|
+
sleep = (ms, waitSignal) => new Promise((resolve, reject) => {
|
|
15
|
+
const timer = setTimeout(resolve, ms);
|
|
16
|
+
waitSignal?.addEventListener("abort", () => { clearTimeout(timer); reject(waitSignal.reason); }, { once: true });
|
|
17
|
+
}),
|
|
18
|
+
onUpdate = () => {},
|
|
19
|
+
} = {}) {
|
|
20
|
+
const timeout = AbortSignal.timeout(timeoutMs);
|
|
21
|
+
const combined = signal ? AbortSignal.any([signal, timeout]) : timeout;
|
|
22
|
+
const update = (state) => { onUpdate(state); return state; };
|
|
23
|
+
let job = null;
|
|
24
|
+
let validation = null;
|
|
25
|
+
try {
|
|
26
|
+
update({ status: "validating", job: null, error: null });
|
|
27
|
+
validation = await validate(spec, { signal: combined });
|
|
28
|
+
update({ status: "submitting", job: null, validation, error: null });
|
|
29
|
+
job = await submit(spec, { signal: combined });
|
|
30
|
+
update({ status: job.status, job, validation, error: null });
|
|
31
|
+
let delay = initialDelayMs;
|
|
32
|
+
while (!TERMINAL.has(job.status)) {
|
|
33
|
+
await sleep(delay, combined);
|
|
34
|
+
job = await poll(job.id, { signal: combined });
|
|
35
|
+
update({ status: job.status, job, validation, error: null });
|
|
36
|
+
delay = Math.min(maxDelayMs, Math.max(initialDelayMs, delay * 2));
|
|
37
|
+
}
|
|
38
|
+
if (job.status === "failed") throw new Error(job.failure || "generation failed");
|
|
39
|
+
return job;
|
|
40
|
+
} catch (error) {
|
|
41
|
+
const timedOut = timeout.aborted && !signal?.aborted;
|
|
42
|
+
const canceled = signal?.aborted;
|
|
43
|
+
if (canceled && job?.id) await cancel(job.id).catch(() => {});
|
|
44
|
+
update({ status: timedOut ? "timed_out" : canceled ? "canceled" : "failed", job, validation, error: error?.message || String(error) });
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export const SHOT_SPEC_VERSION = 1;
|
|
2
|
+
const SUPPORTED_RATIOS = new Set(["16:9", "9:16"]);
|
|
3
|
+
|
|
4
|
+
export function generationDurationForShot(shot, fps = 20, supportedDurations = [5]) {
|
|
5
|
+
const rawSeconds = shot
|
|
6
|
+
? Math.max(1, (Number(shot.endFrame) - Number(shot.startFrame) + 1) / Math.max(Number(fps) || 1, 1))
|
|
7
|
+
: 5;
|
|
8
|
+
const durations = (Array.isArray(supportedDurations) ? supportedDurations : [])
|
|
9
|
+
.map(Number)
|
|
10
|
+
.filter((duration) => Number.isInteger(duration) && duration > 0);
|
|
11
|
+
if (!durations.length) return Math.max(1, Math.min(30, Math.round(rawSeconds)));
|
|
12
|
+
return durations.reduce((closest, duration) => (
|
|
13
|
+
Math.abs(duration - rawSeconds) < Math.abs(closest - rawSeconds) ? duration : closest
|
|
14
|
+
));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function compileShotSpec({ provider = "runway", model, prompt, durationSeconds = 5, aspectRatio = "16:9", resolution = "720p", seed = null, startFrame = null, endFrame = null, referenceVideo = null, shot = null, camera = null, subjects = [] }) {
|
|
18
|
+
const duration = Math.round(Number(durationSeconds));
|
|
19
|
+
const spec = {
|
|
20
|
+
version: SHOT_SPEC_VERSION,
|
|
21
|
+
provider,
|
|
22
|
+
model,
|
|
23
|
+
prompt: String(prompt || "").trim(),
|
|
24
|
+
durationSeconds: duration,
|
|
25
|
+
aspectRatio: SUPPORTED_RATIOS.has(aspectRatio) ? aspectRatio : "16:9",
|
|
26
|
+
resolution,
|
|
27
|
+
seed: seed === "" || seed == null ? null : Number(seed),
|
|
28
|
+
shot: shot || null,
|
|
29
|
+
camera: camera || null,
|
|
30
|
+
subjects: Array.isArray(subjects) ? subjects : [],
|
|
31
|
+
conditioning: { startFrame, endFrame, referenceVideo },
|
|
32
|
+
};
|
|
33
|
+
const errors = [];
|
|
34
|
+
if (!spec.model) errors.push("model is required");
|
|
35
|
+
if (!spec.prompt) errors.push("prompt is required");
|
|
36
|
+
if (!Number.isInteger(spec.durationSeconds) || spec.durationSeconds < 1 || spec.durationSeconds > 30) errors.push("duration must be an integer between 1 and 30 seconds");
|
|
37
|
+
if (spec.seed !== null && (!Number.isInteger(spec.seed) || spec.seed < 0 || spec.seed > 4294967295)) errors.push("seed must be an integer in 0..4294967295");
|
|
38
|
+
if (errors.length) throw new Error(errors[0]);
|
|
39
|
+
return spec;
|
|
40
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
import { checkGenerationBridge, listGenerationJobs, listGenerationModels } from "./client.js";
|
|
3
|
+
import { runGeneration } from "./session.js";
|
|
4
|
+
import { compileShotSpec, generationDurationForShot } from "./shot-spec.js";
|
|
5
|
+
|
|
6
|
+
const IDLE = { status: "idle", job: null, validation: null, error: null };
|
|
7
|
+
|
|
8
|
+
export function useGeneration() {
|
|
9
|
+
const [models, setModels] = useState([]);
|
|
10
|
+
const [available, setAvailable] = useState(false);
|
|
11
|
+
const [state, setState] = useState(IDLE);
|
|
12
|
+
const [history, setHistory] = useState([]);
|
|
13
|
+
const abortRef = useRef(null);
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
let live = true;
|
|
17
|
+
Promise.all([checkGenerationBridge(), listGenerationModels(), listGenerationJobs()]).then(([health, found, jobs]) => {
|
|
18
|
+
if (!live) return;
|
|
19
|
+
setAvailable(Boolean(health.ok && found.some((model) => model.available)));
|
|
20
|
+
setModels(found.filter((model) => model.available));
|
|
21
|
+
setHistory(jobs);
|
|
22
|
+
}).catch(() => { if (live) setAvailable(false); });
|
|
23
|
+
return () => { live = false; abortRef.current?.abort(); };
|
|
24
|
+
}, []);
|
|
25
|
+
|
|
26
|
+
const start = useCallback(async (input) => {
|
|
27
|
+
abortRef.current?.abort();
|
|
28
|
+
const controller = new AbortController();
|
|
29
|
+
abortRef.current = controller;
|
|
30
|
+
const spec = compileShotSpec(input);
|
|
31
|
+
try { return await runGeneration(spec, { signal: controller.signal, onUpdate: setState }); }
|
|
32
|
+
finally {
|
|
33
|
+
listGenerationJobs().then(setHistory).catch(() => {});
|
|
34
|
+
if (abortRef.current === controller) abortRef.current = null;
|
|
35
|
+
}
|
|
36
|
+
}, []);
|
|
37
|
+
|
|
38
|
+
const startResult = useCallback((result, model) => start({
|
|
39
|
+
provider: model.provider,
|
|
40
|
+
model: model.id,
|
|
41
|
+
prompt: result.prompt,
|
|
42
|
+
durationSeconds: generationDurationForShot(result.shot, result.fps, model.durations),
|
|
43
|
+
aspectRatio: "16:9",
|
|
44
|
+
resolution: "720p",
|
|
45
|
+
startFrame: result.frame,
|
|
46
|
+
endFrame: model.capabilities?.endFrame ? result.frameB : null,
|
|
47
|
+
shot: result.shot,
|
|
48
|
+
camera: result.camera,
|
|
49
|
+
subjects: result.subjects,
|
|
50
|
+
}), [start]);
|
|
51
|
+
|
|
52
|
+
const cancel = useCallback(() => abortRef.current?.abort(new Error("generation canceled")), []);
|
|
53
|
+
const reset = useCallback(() => setState(IDLE), []);
|
|
54
|
+
return { available, models, history, state, start, startResult, cancel, reset };
|
|
55
|
+
}
|
package/src/hierarchy-model.js
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
|
+
// The Hierarchy lists what EXISTS in the 3D scene — camera, characters
|
|
2
|
+
// (with their rig groups, which have viewport handles), environment, props.
|
|
3
|
+
// Workflow surfaces (shot settings, prompt, ARDY motion, prompt blocks,
|
|
4
|
+
// root path, IK keys) are NOT scene entities: they live in the right
|
|
5
|
+
// sidebar's Shot and Motion tabs, keyed by `sidebarTab` in App.jsx.
|
|
1
6
|
export const HIERARCHY_NODES = [
|
|
2
7
|
{
|
|
8
|
+
// Keep the legacy selection id until App wiring moves to the document
|
|
9
|
+
// model; its visible role is a Scene, never an editorial Shot.
|
|
3
10
|
id: "shot",
|
|
4
|
-
label: "
|
|
5
|
-
kind: "
|
|
11
|
+
label: "SCENE 01",
|
|
12
|
+
kind: "scene",
|
|
6
13
|
children: [
|
|
7
14
|
{ id: "camera", label: "Camera", kind: "camera" },
|
|
8
15
|
{
|
|
@@ -15,17 +22,6 @@ export const HIERARCHY_NODES = [
|
|
|
15
22
|
label: "Character 1",
|
|
16
23
|
kind: "character",
|
|
17
24
|
children: [
|
|
18
|
-
{ id: "characterA.character", label: "Character", kind: "character" },
|
|
19
|
-
{
|
|
20
|
-
id: "characterA.motion",
|
|
21
|
-
label: "Motion",
|
|
22
|
-
kind: "motion",
|
|
23
|
-
children: [
|
|
24
|
-
{ id: "characterA.baseMotion", label: "Base Motion", kind: "clip" },
|
|
25
|
-
{ id: "characterA.promptBlocks", label: "Prompt Blocks", kind: "prompt" },
|
|
26
|
-
{ id: "characterA.ik", label: "IK Corrections", kind: "key" },
|
|
27
|
-
],
|
|
28
|
-
},
|
|
29
25
|
{
|
|
30
26
|
id: "characterA.rig",
|
|
31
27
|
label: "Rig",
|
|
@@ -46,13 +42,9 @@ export const HIERARCHY_NODES = [
|
|
|
46
42
|
label: "Character 2",
|
|
47
43
|
kind: "character",
|
|
48
44
|
optional: "showB",
|
|
49
|
-
children: [
|
|
50
|
-
{ id: "characterB.character", label: "Character", kind: "character" },
|
|
51
|
-
],
|
|
52
45
|
},
|
|
53
46
|
],
|
|
54
47
|
},
|
|
55
|
-
{ id: "rootPath", label: "Root Path", kind: "path" },
|
|
56
48
|
{ id: "environment", label: "Environment", kind: "environment" },
|
|
57
49
|
{ id: "props", label: "Props", kind: "props" },
|
|
58
50
|
],
|