create-aura3d 1.3.2 → 1.3.3
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/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -2
- package/dist/index.js.map +1 -1
- package/package.json +10 -2
- package/templates/animation-channel/package.json +1 -1
- package/templates/animation-channel/tests/screenshot.spec.ts +16 -0
- package/templates/animation-studio/dist/episodes/prompt-to-scene-proof/two-robots-fixing-a-car.document.json +680 -0
- package/templates/animation-studio/dist/episodes/scene/episode-3d.webm +0 -0
- package/templates/animation-studio/dist/episodes/scene/frames/action.png +0 -0
- package/templates/animation-studio/dist/episodes/scene/frames/dialogue.png +0 -0
- package/templates/animation-studio/dist/episodes/scene/frames/final.png +0 -0
- package/templates/animation-studio/dist/episodes/scene/frames/first.png +0 -0
- package/templates/animation-studio/dist/episodes/scene/frames/mouth-closed.png +0 -0
- package/templates/animation-studio/dist/episodes/scene/frames/mouth-open.png +0 -0
- package/templates/animation-studio/dist/episodes/scene/render-live-summary.json +4829 -682
- package/templates/animation-studio/dist/episodes/scene/skeleton-overlays/friend-1.png +0 -0
- package/templates/animation-studio/dist/episodes/scene/skeleton-overlays/friend-2.png +0 -0
- package/templates/animation-studio/dist/scene/working.document.json +95 -16
- package/templates/animation-studio/index.html +12 -0
- package/templates/animation-studio/node_modules.publish-aside/.vite/deps/_metadata.json +8 -0
- package/templates/animation-studio/node_modules.publish-aside/.vite/deps/package.json +3 -0
- package/templates/animation-studio/package.json +1 -1
- package/templates/animation-studio/scripts/asset-motion-probe.ts +7 -1
- package/templates/animation-studio/scripts/render-live.ts +24 -0
- package/templates/animation-studio/src/director/prompt-to-scene.ts +73 -5
- package/templates/animation-studio/studio/dist/assets/index-C7f8tHfi.css +1 -0
- package/templates/animation-studio/studio/dist/assets/index-CLo3GXyN.js +43 -0
- package/templates/animation-studio/studio/dist/index.html +2 -2
- package/templates/animation-studio/studio/src/App.tsx +146 -78
- package/templates/animation-studio/studio/src/components/Console.tsx +8 -2
- package/templates/animation-studio/studio/src/components/Inspector.tsx +57 -1
- package/templates/animation-studio/studio/src/components/Stage.tsx +10 -5
- package/templates/animation-studio/studio/src/components/Timeline.tsx +49 -2
- package/templates/animation-studio/studio/src/state/mapDocument.ts +13 -2
- package/templates/animation-studio/studio/src/state/sceneTool.ts +20 -9
- package/templates/animation-studio/studio/src/state/types.ts +8 -0
- package/templates/animation-studio/tests/route-health.spec.ts +33 -0
- package/templates/animation-studio/tests/screenshot.spec.ts +30 -0
- package/templates/character-controller/package.json +1 -1
- package/templates/character-controller/src/main.ts +18 -1
- package/templates/cinematic-scene/package.json +1 -1
- package/templates/cinematic-scene/tests/screenshot.spec.ts +3 -1
- package/templates/episode-builder/package.json +1 -1
- package/templates/episode-builder/tests/screenshot.spec.ts +15 -0
- package/templates/fighting-game/package.json +1 -1
- package/templates/fighting-game/tests/screenshot.spec.ts +15 -0
- package/templates/mini-game/package.json +1 -1
- package/templates/product-viewer/package.json +1 -1
- package/templates/product-viewer/tests/screenshot.spec.ts +3 -1
- package/templates/prompt-animation-channel/package.json +1 -1
- package/templates/prompt-animation-channel/tests/screenshot.spec.ts +15 -0
- package/templates/three-compat-architecture-interior/index.html +16 -0
- package/templates/three-compat-architecture-interior/package.json +21 -0
- package/templates/three-compat-architecture-interior/playwright.config.ts +14 -0
- package/templates/three-compat-architecture-interior/src/main.ts +27 -0
- package/templates/three-compat-architecture-interior/tests/route-health.spec.ts +16 -0
- package/templates/three-compat-architecture-interior/tests/screenshot.spec.ts +15 -0
- package/templates/three-compat-architecture-interior/tsconfig.json +19 -0
- package/templates/three-compat-asset-inspector/index.html +16 -0
- package/templates/three-compat-asset-inspector/package.json +21 -0
- package/templates/three-compat-asset-inspector/playwright.config.ts +14 -0
- package/templates/three-compat-asset-inspector/src/main.ts +17 -0
- package/templates/three-compat-asset-inspector/tests/route-health.spec.ts +16 -0
- package/templates/three-compat-asset-inspector/tests/screenshot.spec.ts +15 -0
- package/templates/three-compat-asset-inspector/tsconfig.json +19 -0
- package/templates/three-compat-character-viewer/index.html +16 -0
- package/templates/three-compat-character-viewer/package.json +21 -0
- package/templates/three-compat-character-viewer/playwright.config.ts +14 -0
- package/templates/three-compat-character-viewer/src/main.ts +17 -0
- package/templates/three-compat-character-viewer/tests/route-health.spec.ts +16 -0
- package/templates/three-compat-character-viewer/tests/screenshot.spec.ts +15 -0
- package/templates/three-compat-character-viewer/tsconfig.json +19 -0
- package/templates/three-compat-custom-threejs-migration/index.html +16 -0
- package/templates/three-compat-custom-threejs-migration/package.json +21 -0
- package/templates/three-compat-custom-threejs-migration/playwright.config.ts +14 -0
- package/templates/three-compat-custom-threejs-migration/src/main.ts +16 -0
- package/templates/three-compat-custom-threejs-migration/tests/route-health.spec.ts +16 -0
- package/templates/three-compat-custom-threejs-migration/tests/screenshot.spec.ts +15 -0
- package/templates/three-compat-custom-threejs-migration/tsconfig.json +19 -0
- package/templates/three-compat-large-scene/index.html +16 -0
- package/templates/three-compat-large-scene/package.json +21 -0
- package/templates/three-compat-large-scene/playwright.config.ts +14 -0
- package/templates/three-compat-large-scene/src/main.ts +31 -0
- package/templates/three-compat-large-scene/tests/route-health.spec.ts +16 -0
- package/templates/three-compat-large-scene/tests/screenshot.spec.ts +15 -0
- package/templates/three-compat-large-scene/tsconfig.json +19 -0
- package/templates/three-compat-material-authoring/index.html +16 -0
- package/templates/three-compat-material-authoring/package.json +21 -0
- package/templates/three-compat-material-authoring/playwright.config.ts +14 -0
- package/templates/three-compat-material-authoring/src/main.ts +33 -0
- package/templates/three-compat-material-authoring/tests/route-health.spec.ts +16 -0
- package/templates/three-compat-material-authoring/tests/screenshot.spec.ts +15 -0
- package/templates/three-compat-material-authoring/tsconfig.json +19 -0
- package/templates/three-compat-postprocess-scene/index.html +16 -0
- package/templates/three-compat-postprocess-scene/package.json +21 -0
- package/templates/three-compat-postprocess-scene/playwright.config.ts +14 -0
- package/templates/three-compat-postprocess-scene/src/main.ts +17 -0
- package/templates/three-compat-postprocess-scene/tests/route-health.spec.ts +16 -0
- package/templates/three-compat-postprocess-scene/tests/screenshot.spec.ts +15 -0
- package/templates/three-compat-postprocess-scene/tsconfig.json +19 -0
- package/templates/three-compat-premium-product-viewer/index.html +16 -0
- package/templates/three-compat-premium-product-viewer/package.json +21 -0
- package/templates/three-compat-premium-product-viewer/playwright.config.ts +14 -0
- package/templates/three-compat-premium-product-viewer/src/main.ts +18 -0
- package/templates/three-compat-premium-product-viewer/tests/route-health.spec.ts +16 -0
- package/templates/three-compat-premium-product-viewer/tests/screenshot.spec.ts +15 -0
- package/templates/three-compat-premium-product-viewer/tsconfig.json +19 -0
- package/templates/animation-studio/studio/dist/assets/index-i8URxOOt.css +0 -1
- package/templates/animation-studio/studio/dist/assets/index-pC9hdZ-B.js +0 -11
|
@@ -75,6 +75,12 @@ export function App() {
|
|
|
75
75
|
const [continuing, setContinuing] = useState(false);
|
|
76
76
|
const consoleApi = useRef<ConsoleApi>({});
|
|
77
77
|
const raf = useRef(0);
|
|
78
|
+
// The live render-progress EventSource — kept in a ref so unmount can close it
|
|
79
|
+
// (otherwise an in-flight render leaves a perpetually reconnecting connection).
|
|
80
|
+
const progressEs = useRef<EventSource | null>(null);
|
|
81
|
+
// The active toast-dismiss timer — cleared before each new toast so an earlier
|
|
82
|
+
// timer can't dismiss a later toast early.
|
|
83
|
+
const toastTimer = useRef(0);
|
|
78
84
|
|
|
79
85
|
const DUR = data.DUR;
|
|
80
86
|
const hasShots = data.shots.length > 0;
|
|
@@ -83,8 +89,10 @@ export function App() {
|
|
|
83
89
|
: null;
|
|
84
90
|
|
|
85
91
|
// Hydrate the UI from the REAL working document (and command history). Called on mount and
|
|
86
|
-
// after each /api/scene or /api/render so the panels reflect the live document.
|
|
87
|
-
|
|
92
|
+
// after each /api/scene or /api/render so the panels reflect the live document. Returns the
|
|
93
|
+
// freshly mapped document (or null when none exists) so callers can read post-hydrate values
|
|
94
|
+
// (e.g. the NEW scene's duration) without waiting for a React state round-trip.
|
|
95
|
+
const hydrate = useCallback(async (): Promise<EpisodeDocument | null> => {
|
|
88
96
|
const [doc, hist, render] = await Promise.all([fetchDocument(), fetchHistory(), fetchExistingRender()]);
|
|
89
97
|
const exists = documentExists(doc as RuntimeDocument & { exists?: boolean });
|
|
90
98
|
setDocExists(exists);
|
|
@@ -98,7 +106,7 @@ export function App() {
|
|
|
98
106
|
if (!exists) {
|
|
99
107
|
setData(EMPTY_DOC);
|
|
100
108
|
setSel(null);
|
|
101
|
-
return;
|
|
109
|
+
return null;
|
|
102
110
|
}
|
|
103
111
|
const mapped = mapDocument(doc as RuntimeDocument);
|
|
104
112
|
setData(mapped);
|
|
@@ -106,12 +114,22 @@ export function App() {
|
|
|
106
114
|
if (prev && mapped[selKey(prev.type)].some((e: { id: string }) => e.id === prev.id)) return prev;
|
|
107
115
|
return mapped.shots[0] ? { type: "shot", id: mapped.shots[0].id } : null;
|
|
108
116
|
});
|
|
117
|
+
return mapped;
|
|
109
118
|
}, []);
|
|
110
119
|
|
|
111
120
|
useEffect(() => {
|
|
112
121
|
void hydrate();
|
|
113
122
|
}, [hydrate]);
|
|
114
123
|
|
|
124
|
+
// Close the render-progress EventSource on unmount (it is opened per-render in doRender).
|
|
125
|
+
useEffect(
|
|
126
|
+
() => () => {
|
|
127
|
+
progressEs.current?.close();
|
|
128
|
+
progressEs.current = null;
|
|
129
|
+
},
|
|
130
|
+
[]
|
|
131
|
+
);
|
|
132
|
+
|
|
115
133
|
useEffect(() => {
|
|
116
134
|
localStorage.setItem("aura.time", time.toFixed(2));
|
|
117
135
|
}, [time]);
|
|
@@ -159,7 +177,8 @@ export function App() {
|
|
|
159
177
|
|
|
160
178
|
const showToast = (msg: string, kind: Toast["kind"]) => {
|
|
161
179
|
setToast({ msg, kind });
|
|
162
|
-
window.
|
|
180
|
+
window.clearTimeout(toastTimer.current);
|
|
181
|
+
toastTimer.current = window.setTimeout(() => setToast(null), 2600);
|
|
163
182
|
};
|
|
164
183
|
|
|
165
184
|
const selectShot = (id: string) => {
|
|
@@ -190,54 +209,62 @@ export function App() {
|
|
|
190
209
|
|
|
191
210
|
// Outliner Cast "+" — run a REAL `cast add` command through the same path the Console
|
|
192
211
|
// uses (forceCommand bypasses Prompt mode), so it actually mutates the working document,
|
|
193
|
-
// appends a committed/rejected card, and re-hydrates.
|
|
194
|
-
//
|
|
212
|
+
// appends a committed/rejected card, and re-hydrates. The CLI grammar is
|
|
213
|
+
// `cast add --id <id> --query "…"`; a unique default id avoids colliding with existing
|
|
214
|
+
// cast (the user swaps the look with a follow-up `cast add --id <id> --query "…"`).
|
|
195
215
|
const addCast = () => {
|
|
196
216
|
const run = consoleApi.current.run;
|
|
197
217
|
if (!run) {
|
|
198
218
|
showToast("Console not ready — try again", "tip");
|
|
199
219
|
return;
|
|
200
220
|
}
|
|
201
|
-
const taken = new Set(data.cast.map((c) => c.
|
|
221
|
+
const taken = new Set(data.cast.map((c) => c.id.toLowerCase()));
|
|
202
222
|
let n = data.cast.length + 1;
|
|
203
|
-
let
|
|
204
|
-
while (taken.has(
|
|
205
|
-
run(`cast add ${
|
|
223
|
+
let id = `cast-${n}`;
|
|
224
|
+
while (taken.has(id)) id = `cast-${++n}`;
|
|
225
|
+
run(`cast add --id ${id} --query "friendly cartoon character"`, true);
|
|
206
226
|
};
|
|
207
227
|
|
|
208
228
|
// Generate a brand-new scene from a plain-English sentence. This is the headline action:
|
|
209
229
|
// it runs `new --prompt "…" --full` (the deterministic Director — parses cast, keyword-routes
|
|
210
|
-
// the set, lays out shots/dialogue), replacing the working document, then re-hydrates the UI
|
|
230
|
+
// the set, lays out shots/dialogue), replacing the working document, then re-hydrates the UI
|
|
231
|
+
// and auto-renders a preview so the Stage shows the new scene — not stale captions.
|
|
211
232
|
const generateScene = async () => {
|
|
212
233
|
const p = newScenePrompt.trim();
|
|
213
|
-
|
|
234
|
+
// No generate while a render is in flight: the in-flight render is of the OLD document
|
|
235
|
+
// and would put the old scene's video on the Stage (the buttons are disabled too).
|
|
236
|
+
if (!p || generating || rendering) return;
|
|
214
237
|
setGenerating(true);
|
|
215
238
|
setPlaying(false);
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
239
|
+
try {
|
|
240
|
+
// Strip quotes/backslashes so the prompt can't break the command tokenizer.
|
|
241
|
+
const safe = p.replace(/["\\]/g, "");
|
|
242
|
+
const res = await runSceneCommand(`new --prompt "${safe}" --full`);
|
|
243
|
+
if (!res.ok) {
|
|
244
|
+
showToast("Couldn't generate that scene — try rewording it", "tip");
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
await hydrate(); // panels (cast/set/props/shots/dialogue) now reflect the NEW scene
|
|
248
|
+
setNewScenePrompt("");
|
|
249
|
+
setTime(0);
|
|
250
|
+
// CRITICAL: the render on disk is the OLD scene. Drop it so the stale video can't masquerade
|
|
251
|
+
// as the new one, then auto-render the new document.
|
|
252
|
+
setRenderVideo(null);
|
|
253
|
+
setRenderPoster(null);
|
|
254
|
+
const rendered = await doRender("sequence"); // ← awaited so the spinner stays until the video is ready
|
|
255
|
+
// doRender already toasts on failure — only celebrate when the render actually succeeded.
|
|
256
|
+
if (rendered) showToast("Scene ready — click Play on the transport to watch", "ok");
|
|
257
|
+
} catch (e) {
|
|
258
|
+
showToast(`Scene generation failed — ${e instanceof Error ? e.message : "try again"}`, "tip");
|
|
259
|
+
} finally {
|
|
220
260
|
setGenerating(false);
|
|
221
|
-
showToast("Couldn't generate that scene — try rewording it", "tip");
|
|
222
|
-
return;
|
|
223
261
|
}
|
|
224
|
-
await hydrate(); // panels (cast/set/props/shots/dialogue) now reflect the NEW scene
|
|
225
|
-
setNewScenePrompt("");
|
|
226
|
-
setTime(0);
|
|
227
|
-
setGenerating(false);
|
|
228
|
-
// CRITICAL: the render on disk is the OLD scene. Drop it (so the stale video can't masquerade
|
|
229
|
-
// as the new one) and auto-render the new document, so the big preview ACTUALLY changes — not
|
|
230
|
-
// just the captions. Without this, the Stage keeps showing the previous render.
|
|
231
|
-
setRenderVideo(null);
|
|
232
|
-
setRenderPoster(null);
|
|
233
|
-
showToast("New scene built — rendering a preview so you can see it…", "ok");
|
|
234
|
-
doRender("sequence");
|
|
235
262
|
};
|
|
236
263
|
|
|
237
264
|
// "Continue scene" — add another shot to the CURRENT scene (same cast & set) so you can
|
|
238
265
|
// build a longer video without regenerating. Appends a medium shot; the user directs it next.
|
|
239
266
|
const continueScene = async () => {
|
|
240
|
-
if (continuing || generating || !docExists) return;
|
|
267
|
+
if (continuing || generating || rendering || !docExists) return;
|
|
241
268
|
setContinuing(true);
|
|
242
269
|
setPlaying(false);
|
|
243
270
|
const existing = new Set(data.shots.map((s) => s.id));
|
|
@@ -250,66 +277,105 @@ export function App() {
|
|
|
250
277
|
showToast("Couldn't add a shot — try again", "tip");
|
|
251
278
|
return;
|
|
252
279
|
}
|
|
253
|
-
|
|
280
|
+
try {
|
|
281
|
+
await hydrate();
|
|
282
|
+
} catch {
|
|
283
|
+
// The shot committed but the re-fetch failed — recover instead of an unhandled rejection.
|
|
284
|
+
showToast("Shot added, but refreshing the scene failed — reload to see it", "tip");
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
254
287
|
setSel({ type: "shot", id });
|
|
255
288
|
showToast("Shot added — same cast & set. Direct it, or tell the AI what happens next.", "ok");
|
|
256
289
|
};
|
|
257
290
|
|
|
291
|
+
// Drag-retime from the Timeline's Shots track: issues the real `shot retime` CLI
|
|
292
|
+
// command and re-hydrates so every consumer (stage, inspector, totals) updates.
|
|
293
|
+
const retimeShot = async (shotId: string, newDuration: number) => {
|
|
294
|
+
if (generating || rendering) return;
|
|
295
|
+
const duration = Math.max(1, Math.round(newDuration));
|
|
296
|
+
const res = await runSceneCommand(`shot retime --id ${shotId} --duration ${duration}`);
|
|
297
|
+
if (!res.ok) {
|
|
298
|
+
showToast("Couldn't retime the shot — try again", "tip");
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
try {
|
|
302
|
+
await hydrate();
|
|
303
|
+
} catch {
|
|
304
|
+
showToast("Shot retimed, but refreshing the scene failed — reload to see it", "tip");
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
|
|
258
308
|
// Render hits the REAL pipeline via POST /api/render (dev middleware -> render-live /
|
|
259
309
|
// warm render server). Progress easing runs while the request is in flight (renders are
|
|
260
310
|
// not streamed); on success the rendered webm/poster is loaded into the Stage and a
|
|
261
311
|
// render card is appended. Low-fi is the default for the fast studio iteration loop.
|
|
262
|
-
|
|
263
|
-
|
|
312
|
+
// Resolves true only when the render actually succeeded, so callers (e.g. generateScene)
|
|
313
|
+
// can branch their own toasts on the real outcome.
|
|
314
|
+
const doRender = async (scope: "shot" | "sequence"): Promise<boolean> => {
|
|
315
|
+
if (rendering) {
|
|
316
|
+
showToast("Render already in progress", "tip");
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
264
319
|
const isShot = scope === "shot";
|
|
265
|
-
if (isShot && !currentShot) return;
|
|
320
|
+
if (isShot && !currentShot) return false;
|
|
266
321
|
setRendering(true);
|
|
267
|
-
setRenderPct(
|
|
322
|
+
setRenderPct(-1);
|
|
268
323
|
setPlaying(false);
|
|
269
|
-
//
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
324
|
+
// Subscribe to real render-progress SSE from the render pipeline. The EventSource lives
|
|
325
|
+
// in a ref so the unmount effect can close it if the user leaves mid-render.
|
|
326
|
+
progressEs.current?.close();
|
|
327
|
+
const es = new EventSource("/api/render-progress");
|
|
328
|
+
progressEs.current = es;
|
|
329
|
+
es.onmessage = (e) => {
|
|
330
|
+
try {
|
|
331
|
+
const d = JSON.parse(e.data) as { current: number; total: number; label: string; pct: number };
|
|
332
|
+
// "waiting" = the render process hasn't written progress yet — keep the intentional
|
|
333
|
+
// indeterminate state (-1) instead of snapping the bar to a determinate 0%.
|
|
334
|
+
if (d.label === "waiting") return;
|
|
335
|
+
setRenderPct(d.pct);
|
|
336
|
+
} catch {}
|
|
278
337
|
};
|
|
279
|
-
|
|
338
|
+
es.onerror = () => { /* normal on close */ };
|
|
280
339
|
|
|
281
340
|
// Render the current shot's time-range when scope is "shot", else the whole sequence.
|
|
282
341
|
const range = isShot && currentShot ? `${Math.floor(currentShot.start)}-${Math.ceil(currentShot.start + currentShot.dur)}` : undefined;
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
342
|
+
const res = await runRender({ lowFi: true, range });
|
|
343
|
+
es.close();
|
|
344
|
+
if (progressEs.current === es) progressEs.current = null;
|
|
345
|
+
setRendering(false);
|
|
346
|
+
if (!res.ok) {
|
|
347
|
+
showToast("Render failed — see console", "tip");
|
|
348
|
+
return false;
|
|
349
|
+
}
|
|
350
|
+
// Re-sync the panels with the real document FIRST so the render card's duration reflects
|
|
351
|
+
// the document that was actually rendered (not a closure-captured pre-hydrate DUR).
|
|
352
|
+
let freshDur = DUR;
|
|
353
|
+
try {
|
|
354
|
+
const fresh = await hydrate();
|
|
355
|
+
if (fresh) freshDur = fresh.DUR;
|
|
356
|
+
} catch {
|
|
357
|
+
// Panels stay stale but the render itself succeeded — don't fail the whole render.
|
|
358
|
+
}
|
|
359
|
+
// Cache-bust so the Stage reloads the freshly rendered media.
|
|
360
|
+
const bust = "?t=" + Date.now();
|
|
361
|
+
if (res.video) setRenderVideo(res.video + bust);
|
|
362
|
+
if (res.poster) setRenderPoster(res.poster + bust);
|
|
363
|
+
setTranscript((tr) => [
|
|
364
|
+
...tr,
|
|
365
|
+
{
|
|
366
|
+
type: "render",
|
|
367
|
+
id: "r" + Date.now(),
|
|
368
|
+
frame: res.poster ? res.poster + bust : "",
|
|
369
|
+
label: "low-fi preview · " + (isShot && currentShot ? fmt(currentShot.dur) : fmt(freshDur)),
|
|
370
|
+
shot: isShot && currentShot ? currentShot.name : "Full sequence",
|
|
371
|
+
meta: res.ms ? "rendered in " + (res.ms / 1000).toFixed(1) + "s" : "just now"
|
|
290
372
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
{
|
|
298
|
-
type: "render",
|
|
299
|
-
id: "r" + Date.now(),
|
|
300
|
-
frame: res.poster ? res.poster + bust : "",
|
|
301
|
-
label: "low-fi preview · " + (isShot && currentShot ? fmt(currentShot.dur) : fmt(DUR)),
|
|
302
|
-
shot: isShot && currentShot ? currentShot.name : "Full sequence",
|
|
303
|
-
meta: res.ms ? "rendered in " + (res.ms / 1000).toFixed(1) + "s" : "just now"
|
|
304
|
-
}
|
|
305
|
-
]);
|
|
306
|
-
showToast(
|
|
307
|
-
isShot && currentShot ? "Shot render complete · " + currentShot.name : "Render complete · full sequence",
|
|
308
|
-
"ok"
|
|
309
|
-
);
|
|
310
|
-
// Re-sync the panels with the real document (shot frames may now exist).
|
|
311
|
-
void hydrate();
|
|
312
|
-
});
|
|
373
|
+
]);
|
|
374
|
+
showToast(
|
|
375
|
+
isShot && currentShot ? "Shot render complete · " + currentShot.name : "Render complete · full sequence",
|
|
376
|
+
"ok"
|
|
377
|
+
);
|
|
378
|
+
return true;
|
|
313
379
|
};
|
|
314
380
|
|
|
315
381
|
return (
|
|
@@ -330,7 +396,7 @@ export function App() {
|
|
|
330
396
|
{generating ? (
|
|
331
397
|
<span className="newscene-status">
|
|
332
398
|
<span className="newscene-spinner" />
|
|
333
|
-
Building your scene — cast, set, shots
|
|
399
|
+
Building your scene — this takes ~25 seconds (generating cast, set, shots, then rendering a preview)
|
|
334
400
|
</span>
|
|
335
401
|
) : (
|
|
336
402
|
<input
|
|
@@ -346,7 +412,9 @@ export function App() {
|
|
|
346
412
|
)}
|
|
347
413
|
<button
|
|
348
414
|
className={"newscene-btn" + (generating ? " busy" : "")}
|
|
349
|
-
disabled
|
|
415
|
+
// Also disabled while a render is in flight: generating then would race the
|
|
416
|
+
// in-flight render of the OLD document (stale video lands on the Stage).
|
|
417
|
+
disabled={generating || rendering || !newScenePrompt.trim()}
|
|
350
418
|
onClick={() => void generateScene()}
|
|
351
419
|
>
|
|
352
420
|
{generating ? "Generating…" : "Generate scene"}
|
|
@@ -354,7 +422,7 @@ export function App() {
|
|
|
354
422
|
<span className="newscene-div" />
|
|
355
423
|
<button
|
|
356
424
|
className="newscene-btn newscene-continue"
|
|
357
|
-
disabled={generating || continuing || !docExists}
|
|
425
|
+
disabled={generating || continuing || rendering || !docExists}
|
|
358
426
|
onClick={() => void continueScene()}
|
|
359
427
|
title="Add a shot to this scene — keeps the same characters & set"
|
|
360
428
|
>
|
|
@@ -390,7 +458,7 @@ export function App() {
|
|
|
390
458
|
renderPoster={renderPoster}
|
|
391
459
|
empty={!docExists}
|
|
392
460
|
/>
|
|
393
|
-
<Timeline data={data} time={time} onScrub={setTime} selShot={currentShot?.id ?? null} onSelShot={selectShot} />
|
|
461
|
+
<Timeline data={data} time={time} onScrub={setTime} selShot={currentShot?.id ?? null} onSelShot={selectShot} onRetime={(shotId, newDuration) => void retimeShot(shotId, newDuration)} />
|
|
394
462
|
</div>
|
|
395
463
|
<div className="col">
|
|
396
464
|
<Console
|
|
@@ -12,7 +12,13 @@ export interface ConsoleApi {
|
|
|
12
12
|
run?: (text: string, forceCommand?: boolean) => void;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
// Quick-insert examples — each one is the REAL CLI grammar (animation-scene.ts) and runs as-is.
|
|
16
|
+
const CHIPS = [
|
|
17
|
+
'cast add --id pip --query "robot kid"',
|
|
18
|
+
"shot retime --id shot-2 --duration 30",
|
|
19
|
+
"camera --shot shot-1 --preset close-up",
|
|
20
|
+
"gesture --character pip --shot shot-1 --clip wave"
|
|
21
|
+
];
|
|
16
22
|
|
|
17
23
|
/** The portion of a command after its (possibly two-word) verb — the card's args line. */
|
|
18
24
|
function argTail(raw: string, verb: string): string {
|
|
@@ -332,7 +338,7 @@ export function Console({ transcript, setTranscript, selShot, onRender, onSceneC
|
|
|
332
338
|
placeholder={
|
|
333
339
|
mode === "Prompt"
|
|
334
340
|
? "Tell your AI agent what to change — e.g. “make the second line angrier and cut to a close-up” (it writes this down; your agent does it)"
|
|
335
|
-
:
|
|
341
|
+
: 'Type an exact command — e.g. set garage · cast add --id pip --query "robot kid" · shot retime …'
|
|
336
342
|
}
|
|
337
343
|
onChange={(e) => {
|
|
338
344
|
setVal(e.target.value);
|
|
@@ -4,6 +4,12 @@ import { Icon } from "./Icon";
|
|
|
4
4
|
import { cap, fmt, shade } from "../state/util";
|
|
5
5
|
import type { EpisodeDocument, Selection } from "../state/types";
|
|
6
6
|
|
|
7
|
+
const STANDARD_CLIP_IDS = [
|
|
8
|
+
"idle", "talk", "gesture", "point", "nod", "wave",
|
|
9
|
+
"walk", "run", "react",
|
|
10
|
+
"sit", "shrug", "cross_arms", "salute", "shake_head"
|
|
11
|
+
];
|
|
12
|
+
|
|
7
13
|
function Field({
|
|
8
14
|
label,
|
|
9
15
|
icon,
|
|
@@ -160,8 +166,58 @@ export function Inspector({ data, sel }: InspectorProps) {
|
|
|
160
166
|
}
|
|
161
167
|
/>
|
|
162
168
|
</div>
|
|
169
|
+
<div className="prop-grid one">
|
|
170
|
+
<Field
|
|
171
|
+
label="Default clip"
|
|
172
|
+
icon="film"
|
|
173
|
+
value={
|
|
174
|
+
<select
|
|
175
|
+
disabled
|
|
176
|
+
value={c.defaultClip ?? "idle"}
|
|
177
|
+
style={{
|
|
178
|
+
width: "100%",
|
|
179
|
+
background: "transparent",
|
|
180
|
+
color: "inherit",
|
|
181
|
+
border: "none",
|
|
182
|
+
fontFamily: "var(--mono)",
|
|
183
|
+
fontSize: 12,
|
|
184
|
+
cursor: "not-allowed"
|
|
185
|
+
}}
|
|
186
|
+
>
|
|
187
|
+
{STANDARD_CLIP_IDS.map((id) => (
|
|
188
|
+
<option key={id} value={id}>{id}</option>
|
|
189
|
+
))}
|
|
190
|
+
</select>
|
|
191
|
+
}
|
|
192
|
+
/>
|
|
193
|
+
</div>
|
|
194
|
+
<div className="prop-grid">
|
|
195
|
+
<Field label="Mouth morph" icon="mic" value={c.mouthMorphIndex ?? -1} mono />
|
|
196
|
+
<Field
|
|
197
|
+
label="Foot IK"
|
|
198
|
+
icon="zap"
|
|
199
|
+
value={
|
|
200
|
+
<input
|
|
201
|
+
type="checkbox"
|
|
202
|
+
disabled
|
|
203
|
+
checked={!!c.footIk}
|
|
204
|
+
style={{ cursor: "not-allowed" }}
|
|
205
|
+
/>
|
|
206
|
+
}
|
|
207
|
+
/>
|
|
208
|
+
</div>
|
|
209
|
+
<div className="prop-grid one">
|
|
210
|
+
<Field
|
|
211
|
+
label="Grade intent"
|
|
212
|
+
value={
|
|
213
|
+
<span className="beat-chip cam" style={{ fontSize: 10 }}>
|
|
214
|
+
{c.gradeAwareIntent ?? "—"}
|
|
215
|
+
</span>
|
|
216
|
+
}
|
|
217
|
+
/>
|
|
218
|
+
</div>
|
|
163
219
|
<div className="insp-note">
|
|
164
|
-
{c.name + " is rigged and assignable to any beat. Type
|
|
220
|
+
{c.name + " is rigged and assignable to any beat. Type \"cast\" commands in the console to re-pose or re-cast."}
|
|
165
221
|
</div>
|
|
166
222
|
</Fragment>
|
|
167
223
|
);
|
|
@@ -7,6 +7,7 @@ import type { EpisodeDocument, Shot, ViewMode } from "../state/types";
|
|
|
7
7
|
function RenderOverlay({ pct }: { pct: number }) {
|
|
8
8
|
const r = 32;
|
|
9
9
|
const c = 2 * Math.PI * r;
|
|
10
|
+
const indeterminate = pct < 0;
|
|
10
11
|
return (
|
|
11
12
|
<div className="render-ov">
|
|
12
13
|
<div className="render-ring">
|
|
@@ -20,17 +21,21 @@ function RenderOverlay({ pct }: { pct: number }) {
|
|
|
20
21
|
stroke="var(--warm)"
|
|
21
22
|
strokeWidth={5}
|
|
22
23
|
strokeLinecap="round"
|
|
23
|
-
strokeDasharray={c}
|
|
24
|
-
strokeDashoffset={c * (1 - pct / 100)}
|
|
25
|
-
style={
|
|
24
|
+
strokeDasharray={indeterminate ? `${c * 0.25} ${c * 0.75}` : c}
|
|
25
|
+
strokeDashoffset={indeterminate ? undefined : c * (1 - pct / 100)}
|
|
26
|
+
style={
|
|
27
|
+
indeterminate
|
|
28
|
+
? { animation: "sp 1.2s linear infinite", transformOrigin: "37px 37px" }
|
|
29
|
+
: { transition: "stroke-dashoffset .25s" }
|
|
30
|
+
}
|
|
26
31
|
/>
|
|
27
32
|
</svg>
|
|
28
|
-
<div className="render-pct">{Math.round(pct) + "%"}</div>
|
|
33
|
+
<div className="render-pct">{indeterminate ? "···" : Math.round(pct) + "%"}</div>
|
|
29
34
|
</div>
|
|
30
35
|
<div style={{ textAlign: "center" }}>
|
|
31
36
|
<div className="ttl">Rendering preview</div>
|
|
32
37
|
<div className="sub">
|
|
33
|
-
{pct < 40 ? "aura · rigging cast…" : pct < 75 ? "aura · simulating lighting…" : "aura · compositing frames…"}
|
|
38
|
+
{indeterminate ? "aura · working…" : pct < 40 ? "aura · rigging cast…" : pct < 75 ? "aura · simulating lighting…" : "aura · compositing frames…"}
|
|
34
39
|
</div>
|
|
35
40
|
</div>
|
|
36
41
|
</div>
|
|
@@ -23,15 +23,43 @@ function Clip({
|
|
|
23
23
|
clip,
|
|
24
24
|
DUR,
|
|
25
25
|
sel,
|
|
26
|
-
onClick
|
|
26
|
+
onClick,
|
|
27
|
+
trackRef,
|
|
28
|
+
zoom,
|
|
29
|
+
onRetime
|
|
27
30
|
}: {
|
|
28
31
|
clip: ClipModel;
|
|
29
32
|
DUR: number;
|
|
30
33
|
sel: boolean;
|
|
31
34
|
onClick: (e: React.MouseEvent) => void;
|
|
35
|
+
trackRef: React.RefObject<HTMLDivElement>;
|
|
36
|
+
zoom: number;
|
|
37
|
+
onRetime?: (clipId: string, newDuration: number) => void;
|
|
32
38
|
}) {
|
|
33
39
|
const left = (clip.start / DUR) * 100;
|
|
34
40
|
const width = (clip.dur / DUR) * 100;
|
|
41
|
+
|
|
42
|
+
const handleDown = (e: React.MouseEvent) => {
|
|
43
|
+
e.stopPropagation();
|
|
44
|
+
if (!onRetime || !trackRef.current) return;
|
|
45
|
+
const initialDur = clip.dur;
|
|
46
|
+
const startX = e.clientX;
|
|
47
|
+
const trackWidth = trackRef.current.getBoundingClientRect().width;
|
|
48
|
+
const pxPerSecond = (trackWidth * zoom) / DUR;
|
|
49
|
+
|
|
50
|
+
const onMove = (ev: MouseEvent) => {
|
|
51
|
+
const delta = ev.clientX - startX;
|
|
52
|
+
const newDur = Math.max(1, initialDur + delta / pxPerSecond);
|
|
53
|
+
onRetime(clip.id, newDur);
|
|
54
|
+
};
|
|
55
|
+
const onUp = () => {
|
|
56
|
+
window.removeEventListener("mousemove", onMove);
|
|
57
|
+
window.removeEventListener("mouseup", onUp);
|
|
58
|
+
};
|
|
59
|
+
window.addEventListener("mousemove", onMove);
|
|
60
|
+
window.addEventListener("mouseup", onUp);
|
|
61
|
+
};
|
|
62
|
+
|
|
35
63
|
return (
|
|
36
64
|
<div
|
|
37
65
|
className={"clip" + (sel ? " sel" : "")}
|
|
@@ -43,6 +71,21 @@ function Clip({
|
|
|
43
71
|
}}
|
|
44
72
|
>
|
|
45
73
|
<span className="clab">{clip.label}</span>
|
|
74
|
+
{onRetime && (
|
|
75
|
+
<div
|
|
76
|
+
onMouseDown={handleDown}
|
|
77
|
+
title="Drag to retime"
|
|
78
|
+
style={{
|
|
79
|
+
position: "absolute",
|
|
80
|
+
right: 0,
|
|
81
|
+
top: 0,
|
|
82
|
+
bottom: 0,
|
|
83
|
+
width: 6,
|
|
84
|
+
cursor: "e-resize",
|
|
85
|
+
zIndex: 2
|
|
86
|
+
}}
|
|
87
|
+
/>
|
|
88
|
+
)}
|
|
46
89
|
</div>
|
|
47
90
|
);
|
|
48
91
|
}
|
|
@@ -53,9 +96,10 @@ export interface TimelineProps {
|
|
|
53
96
|
onScrub: (t: number) => void;
|
|
54
97
|
selShot: string | null;
|
|
55
98
|
onSelShot: (id: string) => void;
|
|
99
|
+
onRetime?: (clipId: string, newDuration: number) => void;
|
|
56
100
|
}
|
|
57
101
|
|
|
58
|
-
export function Timeline({ data, time, onScrub, selShot, onSelShot }: TimelineProps) {
|
|
102
|
+
export function Timeline({ data, time, onScrub, selShot, onSelShot, onRetime }: TimelineProps) {
|
|
59
103
|
const DUR = data.DUR;
|
|
60
104
|
const [zoom, setZoom] = useState(1);
|
|
61
105
|
const ref = useRef<HTMLDivElement>(null);
|
|
@@ -170,6 +214,9 @@ export function Timeline({ data, time, onScrub, selShot, onSelShot }: TimelinePr
|
|
|
170
214
|
onScrub(r.start);
|
|
171
215
|
if (t.name === "Shots") onSelShot(r.id);
|
|
172
216
|
}}
|
|
217
|
+
trackRef={ref}
|
|
218
|
+
zoom={zoom}
|
|
219
|
+
onRetime={t.name === "Shots" ? onRetime : undefined}
|
|
173
220
|
/>
|
|
174
221
|
))}
|
|
175
222
|
</div>
|
|
@@ -61,6 +61,12 @@ export interface RuntimeCharacter {
|
|
|
61
61
|
rigGrade?: "A" | "B" | "C" | "D";
|
|
62
62
|
/** M7 — dominant motion source the render played (mocap/extracted/procedural…), when recorded. */
|
|
63
63
|
motionSource?: string;
|
|
64
|
+
/** Default performance clip id. */
|
|
65
|
+
defaultClip?: string;
|
|
66
|
+
/** Mouth morph target index for lip-sync. */
|
|
67
|
+
mouthMorphIndex?: number;
|
|
68
|
+
/** Foot IK configuration (truthy when enabled). */
|
|
69
|
+
footIk?: unknown;
|
|
64
70
|
}
|
|
65
71
|
|
|
66
72
|
export interface RuntimeShotBlocking {
|
|
@@ -254,6 +260,8 @@ export function mapDocument(doc: RuntimeDocument): EpisodeDocument {
|
|
|
254
260
|
|
|
255
261
|
const cast: CastMember[] = rChars.map((c) => {
|
|
256
262
|
const { source, sourceLabel } = castProvenance(c);
|
|
263
|
+
const fidelity =
|
|
264
|
+
fidelityById.get(c.id) ?? { id: c.id, grade: "C", previz: true, reason: "previz: ungraded" };
|
|
257
265
|
return {
|
|
258
266
|
id: c.id,
|
|
259
267
|
name: cap(c.id),
|
|
@@ -263,8 +271,11 @@ export function mapDocument(doc: RuntimeDocument): EpisodeDocument {
|
|
|
263
271
|
lines: linesPerSpeaker(c.id),
|
|
264
272
|
source,
|
|
265
273
|
sourceLabel,
|
|
266
|
-
fidelity
|
|
267
|
-
|
|
274
|
+
fidelity,
|
|
275
|
+
defaultClip: c.defaultClip,
|
|
276
|
+
mouthMorphIndex: c.mouthMorphIndex,
|
|
277
|
+
footIk: !!c.footIk,
|
|
278
|
+
gradeAwareIntent: `${fidelity.grade}-grade`
|
|
268
279
|
};
|
|
269
280
|
});
|
|
270
281
|
|
|
@@ -16,16 +16,27 @@ export interface VerbSpec {
|
|
|
16
16
|
desc: string;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
// The REAL CLI grammar (animation-scene.ts) — every entry here, inserted verbatim with its
|
|
20
|
+
// placeholders filled, is accepted by the CLI's switch/flag parser. These strings are also
|
|
21
|
+
// surfaced verbatim to LLMs, so they must never advertise a syntax the validator rejects.
|
|
19
22
|
export const VERBS: VerbSpec[] = [
|
|
20
|
-
{ verb: "
|
|
21
|
-
{ verb: "
|
|
22
|
-
{ verb: "cast
|
|
23
|
-
{ verb: "
|
|
23
|
+
{ verb: "new", tail: '--prompt "<scene>" [--full]', desc: "Start a new working scene from a prompt" },
|
|
24
|
+
{ verb: "set", tail: '<studio|garage|office|kitchen|moon-garden|space-station|meadow> | --query "<setting>"', desc: "Choose the active set template" },
|
|
25
|
+
{ verb: "cast add", tail: '--id <id> --query "<look>" [--scale <s>]', desc: "Cast a character (catalog resolve)" },
|
|
26
|
+
{ verb: "prop add", tail: '--id <id> --query "<look>"', desc: "Register a prop from the catalog" },
|
|
27
|
+
{ verb: "shot add", tail: "--id <id> --preset <preset> --duration <s>", desc: "Block a new shot" },
|
|
28
|
+
{ verb: "shot remove", tail: "--id <id>", desc: "Remove a shot" },
|
|
24
29
|
{ verb: "shot retime", tail: "--id <id> --duration <s>", desc: "Change a shot's duration" },
|
|
25
|
-
{ verb: "
|
|
26
|
-
{ verb: "
|
|
27
|
-
{ verb: "
|
|
28
|
-
{ verb: "
|
|
30
|
+
{ verb: "camera", tail: "--shot <id> --preset <establishing|medium|two-shot|close-up>", desc: "Set a shot's camera" },
|
|
31
|
+
{ verb: "dialogue", tail: '--line <id> --speaker <id> --text "<line>" --start <s>', desc: "Write a dialogue line" },
|
|
32
|
+
{ verb: "block", tail: "--character <id> --shot <id> --to <x,z> [--clip <clip>]", desc: "Move a character in a shot" },
|
|
33
|
+
{ verb: "gesture", tail: "--character <id> --shot <id> --clip <clip>", desc: "Queue a gesture clip for a character" },
|
|
34
|
+
{ verb: "dress", tail: "--prop <id> --at <x,z> [--scale <s>]", desc: "Place a registered prop on the set" },
|
|
35
|
+
{ verb: "scale", tail: "--character <id> --to <s>", desc: "Rescale a character" },
|
|
36
|
+
{ verb: "show", tail: "", desc: "Print the scene summary" },
|
|
37
|
+
{ verb: "validate", tail: "", desc: "Check scene coherence" },
|
|
38
|
+
{ verb: "undo", tail: "", desc: "Undo the last committed edit" },
|
|
39
|
+
{ verb: "render", tail: "[--shot <id>]", desc: "Render a low-fi preview (shot or full sequence)" }
|
|
29
40
|
];
|
|
30
41
|
|
|
31
42
|
export interface ParsedCommand {
|
|
@@ -47,7 +58,7 @@ export function parse(raw: string): ParsedCommand {
|
|
|
47
58
|
const toks = head.split(/\s+/).filter(Boolean);
|
|
48
59
|
let verb = toks[0] || "";
|
|
49
60
|
let rest = toks.slice(1);
|
|
50
|
-
if (["cast", "shot", "
|
|
61
|
+
if (["cast", "shot", "prop"].includes(verb) && rest[0]) {
|
|
51
62
|
verb += " " + rest[0];
|
|
52
63
|
rest = rest.slice(1);
|
|
53
64
|
}
|