create-aura3d 1.1.2 → 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/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/package.json +11 -9
- package/templates/{cartoon-channel → animation-channel}/README.md +11 -11
- package/templates/{cartoon-studio → animation-channel}/index.html +1 -1
- package/templates/{cartoon-channel → animation-channel}/package.json +2 -2
- package/templates/{cartoon-channel → animation-channel}/src/episode.ts +22 -22
- package/templates/{cartoon-channel → animation-channel}/src/experimental/README.md +5 -5
- package/templates/{cartoon-channel → animation-channel}/src/experimental/concept-episode-2-5d.css +3 -3
- package/templates/{cartoon-channel → animation-channel}/src/experimental/concept-episode-2-5d.ts +7 -7
- package/templates/{cartoon-channel → animation-channel}/src/experimental/image-puppet-episode.css +4 -4
- package/templates/{cartoon-channel → animation-channel}/src/experimental/image-puppet-episode.ts +6 -6
- package/templates/{cartoon-channel → animation-channel}/src/experimental/puppet-episode-2d.css +3 -3
- package/templates/{cartoon-channel → animation-channel}/src/experimental/puppet-episode-2d.ts +7 -7
- package/templates/{cartoon-channel → animation-channel}/src/main.ts +32 -32
- package/templates/{cartoon-channel → animation-channel}/src/render-plan.ts +13 -13
- package/templates/{cartoon-channel → animation-channel}/src/sample-episode-visual.css +34 -34
- package/templates/{cartoon-channel → animation-channel}/src/sample-episode-visual.ts +20 -20
- package/templates/{cartoon-channel → animation-channel}/tests/concept-2-5d-animation.spec.ts +3 -3
- package/templates/{cartoon-channel → animation-channel}/tests/concept-2-5d.spec.ts +6 -6
- package/templates/{cartoon-channel → animation-channel}/tests/image-puppet-animation.spec.ts +3 -3
- package/templates/{cartoon-channel → animation-channel}/tests/image-puppet.spec.ts +5 -5
- package/templates/{cartoon-channel → animation-channel}/tests/puppet-2d-animation.spec.ts +3 -3
- package/templates/{cartoon-channel → animation-channel}/tests/puppet-2d.spec.ts +4 -4
- package/templates/{prompt-cartoon-channel → animation-channel}/tests/route-health.spec.ts +1 -1
- package/templates/{cartoon-channel → animation-channel}/tests/sample-episode-visual.spec.ts +5 -5
- package/templates/{cartoon-channel → animation-channel}/tests/storyboard-playback.spec.ts +7 -7
- package/templates/animation-studio/AGENTS.md +71 -0
- package/templates/{cartoon-studio → animation-studio}/ASSET-LICENSES.md +7 -7
- package/templates/{cartoon-studio → animation-studio}/README.md +6 -6
- package/templates/animation-studio/aura.assets.json +669 -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 +10284 -0
- package/templates/animation-studio/dist/episodes/scene/skeleton-overlays/worker-1.png +0 -0
- package/templates/animation-studio/dist/episodes/scene/skeleton-overlays/worker-2.png +0 -0
- package/templates/animation-studio/dist/generated/second-scene.document.json +1210 -0
- package/templates/animation-studio/dist/generated/studio-edited.document.json +1196 -0
- package/templates/animation-studio/dist/scene/inshader-accept.document.json +1 -0
- package/templates/animation-studio/dist/scene/inshader-test.document.json +1 -0
- package/templates/animation-studio/dist/scene/integ.document.json +1 -0
- package/templates/animation-studio/dist/scene/working.document.json +723 -0
- package/templates/animation-studio/dist/scene/working.history.json +1 -0
- package/templates/{cartoon-studio → animation-studio}/live-route.html +1 -1
- package/templates/{cartoon-studio → animation-studio}/package-lock.json +2 -2
- package/templates/animation-studio/package.json +32 -0
- package/templates/animation-studio/public/aura-assets/cast-a.catalog.glb +0 -0
- package/templates/animation-studio/public/aura-assets/cast-b.catalog.glb +0 -0
- package/templates/animation-studio/public/aura-assets/luma.catalog.glb +0 -0
- package/templates/animation-studio/public/aura-assets/luma2.catalog.glb +0 -0
- package/templates/animation-studio/public/aura-assets/miko.catalog.glb +0 -0
- package/templates/{cartoon-studio → animation-studio}/public/aura-assets/moonGarden.gltf +1 -1
- package/templates/animation-studio/public/aura-assets/mushroom.catalog.glb +0 -0
- package/templates/animation-studio/public/aura-assets/rusty.catalog.glb +0 -0
- package/templates/animation-studio/public/cast-library/cast-library.json +71 -0
- package/templates/animation-studio/public/clip-library/gesture.json +173903 -0
- package/templates/animation-studio/public/clip-library/idle.json +23035 -0
- package/templates/animation-studio/public/clip-library/manifest.json +154 -0
- package/templates/animation-studio/public/clip-library/nod.json +173903 -0
- package/templates/animation-studio/public/clip-library/point.json +69879 -0
- package/templates/animation-studio/public/clip-library/react.json +66084 -0
- package/templates/animation-studio/public/clip-library/run.json +9648 -0
- package/templates/animation-studio/public/clip-library/talk.json +69879 -0
- package/templates/animation-studio/public/clip-library/walk.json +8283 -0
- package/templates/animation-studio/public/hifi-cast/BrainStem.glb +0 -0
- package/templates/animation-studio/public/hifi-cast/CesiumMan.glb +0 -0
- package/templates/animation-studio/public/hifi-cast/RiggedFigure.glb +0 -0
- package/templates/animation-studio/public/hifi-cast/kaykit/Barbarian.glb +0 -0
- package/templates/animation-studio/public/hifi-cast/kaykit/Knight.glb +0 -0
- package/templates/animation-studio/public/hifi-cast/kaykit/Mage.glb +0 -0
- package/templates/animation-studio/public/hifi-cast/kaykit/Rogue.glb +0 -0
- package/templates/animation-studio/public/hifi-cast/kaykit/Rogue_Hooded.glb +0 -0
- package/templates/animation-studio/public/hifi-cast/khronos-report.json +295 -0
- package/templates/animation-studio/public/hifi-cast/quaternius-report.json +449 -0
- package/templates/animation-studio/public/hifi-cast/rpm/cesiumman.glb +0 -0
- package/templates/animation-studio/public/hifi-cast/rpm/fox.glb +0 -0
- package/templates/animation-studio/public/hifi-cast/rpm/riggedfigure.glb +0 -0
- package/templates/animation-studio/public/hifi-cast/rpm-report.json +301 -0
- package/templates/animation-studio/scripts/anim.ts +103 -0
- package/templates/animation-studio/scripts/animation-scene.ts +457 -0
- package/templates/animation-studio/scripts/asset-motion-probe.ts +450 -0
- package/templates/animation-studio/scripts/asset-render-probe.ts +152 -0
- package/templates/animation-studio/scripts/author-office-scene.ts +65 -0
- package/templates/animation-studio/scripts/build-cast-library.ts +97 -0
- package/templates/animation-studio/scripts/build-characters.ts +1543 -0
- package/templates/animation-studio/scripts/build-clip-library.ts +972 -0
- package/templates/animation-studio/scripts/clip-decision-cli.ts +51 -0
- package/templates/animation-studio/scripts/determinism-check.ts +48 -0
- package/templates/animation-studio/scripts/generate-scene.ts +95 -0
- package/templates/animation-studio/scripts/hifi-cast-validate.ts +572 -0
- package/templates/animation-studio/scripts/motion-evidence-cli.ts +150 -0
- package/templates/animation-studio/scripts/motion-evidence.ts +362 -0
- package/templates/animation-studio/scripts/preview-server.ts +52 -0
- package/templates/animation-studio/scripts/regen-manifest.ts +156 -0
- package/templates/animation-studio/scripts/render-core.ts +360 -0
- package/templates/animation-studio/scripts/render-live.ts +813 -0
- package/templates/animation-studio/scripts/render-parallel.ts +70 -0
- package/templates/animation-studio/scripts/render-server.ts +119 -0
- package/templates/animation-studio/scripts/resolve-asset.ts +669 -0
- package/templates/animation-studio/scripts/skeleton-overlay-cli.ts +82 -0
- package/templates/animation-studio/scripts/skeleton-overlay.ts +293 -0
- package/templates/animation-studio/scripts/studio-tools-demo.ts +45 -0
- package/templates/animation-studio/scripts/texture-bake.ts +297 -0
- package/templates/{cartoon-studio → animation-studio}/scripts/validate-characters.ts +0 -0
- package/templates/animation-studio/scripts/verify-extracted-overlay-cli.ts +264 -0
- package/templates/animation-studio/src/animation-episode-validator.ts +97 -0
- package/templates/animation-studio/src/animation-performance.ts +556 -0
- package/templates/animation-studio/src/aura-assets.ts +71 -0
- package/templates/animation-studio/src/character.ts +40 -0
- package/templates/animation-studio/src/director/compile-episode-document.ts +45 -0
- package/templates/animation-studio/src/director/director-heuristics.ts +697 -0
- package/templates/animation-studio/src/director/prompt-to-scene.ts +369 -0
- package/templates/animation-studio/src/empty-document.ts +77 -0
- package/templates/animation-studio/src/episode-document.ts +442 -0
- package/templates/animation-studio/src/examples/moon-garden.example.ts +183 -0
- package/templates/animation-studio/src/fidelity.ts +129 -0
- package/templates/animation-studio/src/graph.ts +14 -0
- package/templates/animation-studio/src/procedural-hdri.ts +243 -0
- package/templates/animation-studio/src/profile.ts +97 -0
- package/templates/animation-studio/src/render-live-route.ts +42 -0
- package/templates/animation-studio/src/render-modes.ts +137 -0
- package/templates/animation-studio/src/scene-player.ts +867 -0
- package/templates/animation-studio/src/set-templates.ts +361 -0
- package/templates/animation-studio/src/studio/episode-document-store.ts +72 -0
- package/templates/animation-studio/src/studio/scene-tool-registry.ts +86 -0
- package/templates/animation-studio/src/studio/scene-tools.ts +169 -0
- package/templates/animation-studio/tests/reports/animation-studio/hifi-cast-validation.json +938 -0
- package/templates/animation-studio/tsconfig.json +26 -0
- package/templates/{cartoon-studio → animation-studio}/vite.config.ts +6 -1
- package/templates/character-controller/README.md +33 -0
- package/templates/character-controller/index.html +19 -0
- package/templates/character-controller/package.json +23 -0
- package/templates/character-controller/playwright.config.ts +14 -0
- package/templates/character-controller/src/controller.ts +44 -0
- package/templates/character-controller/src/main.ts +62 -0
- package/templates/character-controller/tests/route-health.spec.ts +26 -0
- package/templates/character-controller/tests/screenshot.spec.ts +9 -0
- package/templates/character-controller/tsconfig.json +15 -0
- package/templates/cinematic-scene/package.json +1 -1
- package/templates/episode-builder/README.md +4 -4
- package/templates/episode-builder/package.json +1 -1
- package/templates/episode-builder/src/aura-assets.ts +1 -1
- package/templates/episode-builder/src/builder.ts +4 -4
- package/templates/episode-builder/src/episode.ts +21 -21
- package/templates/episode-builder/src/main.ts +20 -20
- package/templates/episode-builder/src/render-plan.ts +13 -13
- package/templates/episode-builder/tests/storyboard-playback.spec.ts +7 -7
- package/templates/fighting-game/package.json +1 -1
- package/templates/mini-game/package.json +1 -1
- package/templates/product-viewer/package.json +1 -1
- package/templates/{prompt-cartoon-channel → prompt-animation-channel}/README.md +5 -5
- package/templates/{cartoon-channel → prompt-animation-channel}/index.html +1 -1
- package/templates/{prompt-cartoon-channel → prompt-animation-channel}/package.json +2 -2
- package/templates/{prompt-cartoon-channel → prompt-animation-channel}/src/aura-assets.ts +1 -1
- package/templates/{prompt-cartoon-channel → prompt-animation-channel}/src/episode.ts +22 -22
- package/templates/{prompt-cartoon-channel → prompt-animation-channel}/src/main.ts +28 -28
- package/templates/{prompt-cartoon-channel → prompt-animation-channel}/src/render-plan.ts +13 -13
- package/templates/{cartoon-channel → prompt-animation-channel}/tests/route-health.spec.ts +1 -1
- package/templates/{prompt-cartoon-channel → prompt-animation-channel}/tests/storyboard-playback.spec.ts +8 -8
- package/templates/cartoon-studio/aura.assets.json +0 -400
- package/templates/cartoon-studio/package.json +0 -33
- package/templates/cartoon-studio/public/aura-assets/luma.authored.glb +0 -0
- package/templates/cartoon-studio/public/aura-assets/miko.authored.glb +0 -0
- package/templates/cartoon-studio/scripts/build-characters.ts +0 -738
- package/templates/cartoon-studio/scripts/build-dialogue-audio.ts +0 -262
- package/templates/cartoon-studio/scripts/episode.ts +0 -323
- package/templates/cartoon-studio/scripts/render-live.ts +0 -873
- package/templates/cartoon-studio/src/aura-assets.ts +0 -64
- package/templates/cartoon-studio/src/characters.ts +0 -65
- package/templates/cartoon-studio/src/episode-renderer.ts +0 -605
- package/templates/cartoon-studio/src/episode.ts +0 -306
- package/templates/cartoon-studio/src/main.ts +0 -400
- package/templates/cartoon-studio/src/render-live-route.ts +0 -913
- package/templates/cartoon-studio/src/render-plan.ts +0 -567
- package/templates/cartoon-studio/src/review.ts +0 -99
- package/templates/cartoon-studio/src/studio.ts +0 -101
- package/templates/cartoon-studio/test-results/.last-run.json +0 -4
- package/templates/cartoon-studio/tests/episode-render.spec.ts +0 -44
- package/templates/cartoon-studio/tests/motion-quality.spec.ts +0 -45
- package/templates/cartoon-studio/tests/route-health.spec.ts +0 -7
- package/templates/cartoon-studio/tests/storyboard-playback.spec.ts +0 -99
- package/templates/cartoon-studio/tests/visual-review.spec.ts +0 -21
- package/templates/cartoon-studio/tsconfig.json +0 -14
- package/templates/prompt-cartoon-channel/index.html +0 -13
- package/templates/prompt-cartoon-channel/src/sets.ts +0 -14
- /package/templates/{cartoon-channel → animation-channel}/aura.assets.json +0 -0
- /package/templates/{cartoon-channel → animation-channel}/playwright.config.ts +0 -0
- /package/templates/{cartoon-channel → animation-channel}/public/aura-assets/luma.047f5e5f.glb +0 -0
- /package/templates/{cartoon-channel → animation-channel}/public/aura-assets/luma.thumb.svg +0 -0
- /package/templates/{cartoon-channel → animation-channel}/public/aura-assets/miko.047f5e5f.glb +0 -0
- /package/templates/{cartoon-channel → animation-channel}/public/aura-assets/miko.thumb.svg +0 -0
- /package/templates/{cartoon-channel → animation-channel}/public/aura-assets/moon-garden-feature-frame.png +0 -0
- /package/templates/{cartoon-channel → animation-channel}/src/aura-assets.ts +0 -0
- /package/templates/{cartoon-channel → animation-channel}/src/characters.ts +0 -0
- /package/templates/{cartoon-channel → animation-channel}/src/contract.ts +0 -0
- /package/templates/{cartoon-channel → animation-channel}/src/sets.ts +0 -0
- /package/templates/{cartoon-channel → animation-channel}/test-results/.last-run.json +0 -0
- /package/templates/{cartoon-channel → animation-channel}/tests/reports/prompt-animation/cartoon-sample-episode.png +0 -0
- /package/templates/{cartoon-channel → animation-channel}/tsconfig.json +0 -0
- /package/templates/{cartoon-studio → animation-studio}/playwright.config.ts +0 -0
- /package/templates/{cartoon-studio → animation-studio}/public/aura-assets/luma.thumb.svg +0 -0
- /package/templates/{cartoon-studio → animation-studio}/public/aura-assets/miko.thumb.svg +0 -0
- /package/templates/{cartoon-studio → animation-studio}/src/contract.ts +0 -0
- /package/templates/{prompt-cartoon-channel → prompt-animation-channel}/playwright.config.ts +0 -0
- /package/templates/{prompt-cartoon-channel → prompt-animation-channel}/src/characters.ts +0 -0
- /package/templates/{prompt-cartoon-channel → prompt-animation-channel}/src/contract.ts +0 -0
- /package/templates/{cartoon-studio → prompt-animation-channel}/src/sets.ts +0 -0
- /package/templates/{prompt-cartoon-channel → prompt-animation-channel}/tsconfig.json +0 -0
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* animation-scene.ts — the AGENT-NATIVE Scene-Tool CLI.
|
|
3
|
+
*
|
|
4
|
+
* The "AI prompt studio" does NOT need its own LLM. The creator is already in a signed-in
|
|
5
|
+
* coding agent (Claude Code, Codex, …) — THAT agent is the director. It reads the creator's
|
|
6
|
+
* natural language and drives the scene by running these commands. No separate model, no API
|
|
7
|
+
* key. Every command edits a persisted EpisodeDocument through the validated scene tools, so
|
|
8
|
+
* the agent has scene-authoring powers, not arbitrary-code powers.
|
|
9
|
+
*
|
|
10
|
+
* animation-scene new --prompt "<scene>" [--characters a,b] start a working scene from a prompt (EMPTY-cast skeleton)
|
|
11
|
+
* animation-scene new --prompt "<scene>" --full generate a COMPLETE scene (cast+dialogue+actions; F2)
|
|
12
|
+
* animation-scene new --from <doc.json> clone an existing working scene
|
|
13
|
+
* animation-scene show print the document summary
|
|
14
|
+
* animation-scene block --character <id> --shot <id> --to x,z [--yaw r] [--clip name]
|
|
15
|
+
* animation-scene camera --shot <id> [--preset establishing|two-shot|close-up] [--subject x,y,z]
|
|
16
|
+
* animation-scene gesture --character <id> --shot <id> --clip <name>
|
|
17
|
+
* animation-scene dress --prop <id> --at x,z [--scale s] [--feet f]
|
|
18
|
+
* animation-scene clear-props [--prop <id>]
|
|
19
|
+
* animation-scene undo
|
|
20
|
+
* animation-scene validate
|
|
21
|
+
* animation-scene render [--range a-b]
|
|
22
|
+
*
|
|
23
|
+
* Run via: pnpm exec tsx --tsconfig tsconfig.base.json packages/create-aura3d/templates/animation-studio/scripts/animation-scene.ts <cmd> ...
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { spawnSync } from "node:child_process";
|
|
27
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
28
|
+
import { dirname, resolve } from "node:path";
|
|
29
|
+
import { fileURLToPath } from "node:url";
|
|
30
|
+
import { compilePromptEpisodePlan, type CameraPresetId } from "@aura3d/engine";
|
|
31
|
+
import { summarizeDocument, type EpisodeDocument, type Vec3 } from "../src/episode-document.js";
|
|
32
|
+
import { validateEpisodeDocument } from "../src/animation-episode-validator.js";
|
|
33
|
+
import { addShot, clearProps, placeProp, removeDialogueLine, removeShot, retimeDialogue, retimeShot, setBlocking, setCamera, setCharacterScale, setDialogueLine, setGesture } from "../src/studio/scene-tools.js";
|
|
34
|
+
import { compileEpisodeDocument } from "../src/director/compile-episode-document.js";
|
|
35
|
+
import type { DirectorSceneInput } from "../src/director/director-heuristics.js";
|
|
36
|
+
import { generateSceneFromPrompt } from "../src/director/prompt-to-scene.js";
|
|
37
|
+
import { SET_TEMPLATES, getSetTemplate, pickSetForPrompt } from "../src/set-templates.js";
|
|
38
|
+
import { resolveAsset, resolveLocalGlb, type ResolvedAsset } from "./resolve-asset.js";
|
|
39
|
+
|
|
40
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
41
|
+
const REPO = resolve(__dirname, "..", "..", "..", "..", "..");
|
|
42
|
+
const STATE_DIR = resolve(__dirname, "..", "dist", "scene");
|
|
43
|
+
const DOC_PATH = resolve(STATE_DIR, "working.document.json");
|
|
44
|
+
const HISTORY_PATH = resolve(STATE_DIR, "working.history.json");
|
|
45
|
+
|
|
46
|
+
const argv = process.argv.slice(2);
|
|
47
|
+
const cmd = argv[0];
|
|
48
|
+
function opt(name: string): string | undefined {
|
|
49
|
+
const i = argv.indexOf(`--${name}`);
|
|
50
|
+
return i >= 0 ? argv[i + 1] : undefined;
|
|
51
|
+
}
|
|
52
|
+
function vec(s: string | undefined, fallbackY = 0): Vec3 | undefined {
|
|
53
|
+
if (!s) return undefined;
|
|
54
|
+
const n = s.split(",").map(Number);
|
|
55
|
+
if (n.length === 2) return [n[0]!, fallbackY, n[1]!];
|
|
56
|
+
if (n.length === 3) return [n[0]!, n[1]!, n[2]!];
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
function loadDoc(): EpisodeDocument {
|
|
60
|
+
if (!existsSync(DOC_PATH)) {
|
|
61
|
+
console.error("no working scene. Run: animation-scene new");
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
return JSON.parse(readFileSync(DOC_PATH, "utf8")) as EpisodeDocument;
|
|
65
|
+
}
|
|
66
|
+
function loadHistory(): EpisodeDocument[] {
|
|
67
|
+
return existsSync(HISTORY_PATH) ? (JSON.parse(readFileSync(HISTORY_PATH, "utf8")) as EpisodeDocument[]) : [];
|
|
68
|
+
}
|
|
69
|
+
function persist(doc: EpisodeDocument, history: EpisodeDocument[]): void {
|
|
70
|
+
mkdirSync(STATE_DIR, { recursive: true });
|
|
71
|
+
writeFileSync(DOC_PATH, `${JSON.stringify(doc, null, 2)}\n`);
|
|
72
|
+
writeFileSync(HISTORY_PATH, `${JSON.stringify(history.slice(-50), null, 2)}\n`);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Build the per-character clip lists the validator needs so its clip-existence guardrail can
|
|
76
|
+
* fire. Each CharacterAsset carries a resolved clip list (`availableClips`/`clips`) and always a
|
|
77
|
+
* `defaultClip`; we expose every name we know so scheduled clips can be fuzzy-matched against it.
|
|
78
|
+
*/
|
|
79
|
+
function availableClipsByCharacter(doc: EpisodeDocument): Record<string, readonly string[]> {
|
|
80
|
+
const map: Record<string, readonly string[]> = {};
|
|
81
|
+
for (const c of doc.assets.characters) {
|
|
82
|
+
const resolved = (c as { availableClips?: readonly string[]; clips?: readonly string[] });
|
|
83
|
+
const clips = resolved.availableClips ?? resolved.clips ?? (c.defaultClip ? [c.defaultClip] : []);
|
|
84
|
+
if (clips.length > 0) map[c.id] = clips;
|
|
85
|
+
}
|
|
86
|
+
return map;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Apply a validated edit: validate (with the clip-existence guardrail) → commit or reject. */
|
|
90
|
+
function applyEdit(transform: (d: EpisodeDocument) => EpisodeDocument): void {
|
|
91
|
+
const current = loadDoc();
|
|
92
|
+
const next = transform(current);
|
|
93
|
+
const v = validateEpisodeDocument(next, { availableClipsByCharacter: availableClipsByCharacter(next) });
|
|
94
|
+
if (!v.ok) {
|
|
95
|
+
console.error(`REJECTED — edit would break the scene:\n - ${v.errors.join("\n - ")}`);
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
persist(next, [...loadHistory(), current]);
|
|
99
|
+
console.log(`ok${v.warnings.length ? ` (warnings: ${v.warnings.length})` : ""}`);
|
|
100
|
+
for (const w of v.warnings) console.log(` warn: ${w}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Suggest 1–3 cast ids from the prompt so the directing agent has concrete `cast add` targets.
|
|
105
|
+
* Pulls capitalized names first (e.g. "Miko and Luma"); otherwise derives generic ids from the
|
|
106
|
+
* acting nouns (robots → robot-1/robot-2). Never invents a final cast — these are suggestions.
|
|
107
|
+
*/
|
|
108
|
+
function suggestCastIds(prompt: string): string[] {
|
|
109
|
+
const slug = (s: string): string => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
110
|
+
const stop = new Set(["the", "a", "an", "and", "on", "in", "at", "of", "to", "with", "two", "three", "their", "his", "her"]);
|
|
111
|
+
// 1. Proper names ("Miko and Luma argue …").
|
|
112
|
+
const names = (prompt.match(/\b[A-Z][a-z]{2,}\b/g) ?? []).map(slug).filter((n) => n && !stop.has(n));
|
|
113
|
+
if (names.length > 0) return [...new Set(names)].slice(0, 3);
|
|
114
|
+
// 2. A plural acting noun ("two robots argue") → robot-1, robot-2.
|
|
115
|
+
const plural = prompt.toLowerCase().match(/\b([a-z]{4,})s\b/);
|
|
116
|
+
if (plural) {
|
|
117
|
+
const base = slug(plural[1]!);
|
|
118
|
+
if (base && !stop.has(base)) return [`${base}-1`, `${base}-2`];
|
|
119
|
+
}
|
|
120
|
+
// 3. A singular acting noun fallback.
|
|
121
|
+
const word = prompt.toLowerCase().split(/[^a-z]+/).find((w) => w.length >= 4 && !stop.has(w));
|
|
122
|
+
return [word ? slug(word) : "hero"];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Build a fresh SKELETON document from a PROMPT: compilePromptEpisodePlan turns the prompt into
|
|
127
|
+
* a storyboard + shots; the Director fills camera + the set template's ground dressing. The cast
|
|
128
|
+
* starts EMPTY — the directing agent resolves real characters with `cast add` before blocking and
|
|
129
|
+
* dialogue are authored. The SET is chosen from the prompt (not always the Moon Garden).
|
|
130
|
+
*/
|
|
131
|
+
function buildFromPrompt(prompt: string): EpisodeDocument {
|
|
132
|
+
const duration = 60;
|
|
133
|
+
// DYNAMIC SET: pick the environment from the prompt — NOT always the Moon Garden.
|
|
134
|
+
const template = pickSetForPrompt(prompt);
|
|
135
|
+
// Generate a simple storyboard (beats) from the prompt; the agent refines the lines later.
|
|
136
|
+
const beatCount = 3;
|
|
137
|
+
const beatDur = duration / beatCount;
|
|
138
|
+
const beats = Array.from({ length: beatCount }, (_, i) => ({
|
|
139
|
+
id: `beat-${i + 1}`,
|
|
140
|
+
sceneId: "storyboard",
|
|
141
|
+
shotId: `shot-${i + 1}`,
|
|
142
|
+
locationId: "scene-location",
|
|
143
|
+
summary: `Beat ${i + 1} of: ${prompt}`,
|
|
144
|
+
visualIntent: i === 0 ? "Wide establishing shot." : i === beatCount - 1 ? "Close-up resolution." : "Medium two-shot.",
|
|
145
|
+
duration: beatDur,
|
|
146
|
+
// The cast is unresolved — beats describe the staging, not who plays it yet.
|
|
147
|
+
characters: [] as string[],
|
|
148
|
+
props: [],
|
|
149
|
+
gestureByCharacterId: {},
|
|
150
|
+
blockingByCharacterId: {},
|
|
151
|
+
// NO fabricated dialogue. The DIRECTING AGENT writes the real lines via `dialogue` after
|
|
152
|
+
// resolving the cast. A skeleton must never ship placeholder captions.
|
|
153
|
+
dialogue: [] as { speakerId: string; text: string; emotion: string; delivery: string }[]
|
|
154
|
+
}));
|
|
155
|
+
const plan = compilePromptEpisodePlan({
|
|
156
|
+
episodeId: `scene-${prompt.toLowerCase().replace(/[^a-z0-9]+/g, "-").slice(0, 24)}`,
|
|
157
|
+
title: prompt.slice(0, 48),
|
|
158
|
+
prompt,
|
|
159
|
+
language: "en",
|
|
160
|
+
runtime: { duration, frameRate: 30, resolution: { width: 1280, height: 720 }, aspectRatio: "16:9", reducedMotion: true, highContrast: true, maxTimingDriftFrames: 1 },
|
|
161
|
+
route: "/",
|
|
162
|
+
// No characters declared — the skeleton has an empty cast.
|
|
163
|
+
characters: [],
|
|
164
|
+
locations: [{ id: "scene-location", name: template.id, description: prompt, mood: "soft" }],
|
|
165
|
+
props: [],
|
|
166
|
+
safety: { childSafe: true, captionRequired: true, reducedMotionDefault: true, highContrastDefault: true, flashing: "reduced" },
|
|
167
|
+
beats
|
|
168
|
+
});
|
|
169
|
+
const scene: DirectorSceneInput = {
|
|
170
|
+
duration,
|
|
171
|
+
// EMPTY cast — no blocking is generated for characters that don't exist yet.
|
|
172
|
+
characters: [],
|
|
173
|
+
shots: plan.shotTimeline.shots.map((s) => ({ shotId: s.shotId, startTime: s.startTime, endTime: s.endTime })),
|
|
174
|
+
// No dialogue until the cast is resolved and the agent authors the real lines.
|
|
175
|
+
dialogue: [],
|
|
176
|
+
walkableBounds: template.walkableBounds,
|
|
177
|
+
// Ground dressing comes from the chosen SET TEMPLATE (not a hard-coded mushroom×6); a
|
|
178
|
+
// template with no groundProp scatters nothing.
|
|
179
|
+
props: template.groundProp
|
|
180
|
+
? [{ propId: template.groundProp.propId, count: 6, scaleRange: [...template.groundProp.scaleRange] as [number, number], feetOffset: template.groundProp.feetOffset }]
|
|
181
|
+
: []
|
|
182
|
+
};
|
|
183
|
+
// EMPTY cast: the document is a set + shots + timeline skeleton. The directing agent resolves
|
|
184
|
+
// the suggested cast ids with `cast add` (catalog resolve) before authoring performance.
|
|
185
|
+
// The only registered asset is the SET TEMPLATE's ground prop (so its scatter is valid); the
|
|
186
|
+
// agent swaps it for a real GLB with `prop add --id <propId>` when refining.
|
|
187
|
+
const props = template.groundProp
|
|
188
|
+
? [{ id: template.groundProp.propId, url: `/aura-assets/${template.groundProp.propId}.catalog.glb`, attribution: `placeholder: ${template.groundProp.query}` }]
|
|
189
|
+
: [];
|
|
190
|
+
const result = compileEpisodeDocument({ id: plan.episodeId ?? "scene", duration, assets: { characters: [], props }, set: template.set, scene });
|
|
191
|
+
return {
|
|
192
|
+
...result.document,
|
|
193
|
+
walkableBounds: template.walkableBounds,
|
|
194
|
+
// An empty dialogue track is correct for a skeleton — authored after cast + blocking.
|
|
195
|
+
dialogue: { language: plan.dialogueTrack.language ?? "en", lines: [] }
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Resolve a character and register it under `id` (swap or add). Source is either the hosted
|
|
201
|
+
* CATALOG (`--query`) or a USER-UPLOADED local GLB (`--file`); both are graded + render-probed by
|
|
202
|
+
* the resolver, so an uploaded character goes through the same fidelity gate as a catalog one.
|
|
203
|
+
*/
|
|
204
|
+
async function castAdd(
|
|
205
|
+
id: string,
|
|
206
|
+
source: { query: string } | { file: string },
|
|
207
|
+
scaleOverride?: number
|
|
208
|
+
): Promise<void> {
|
|
209
|
+
const isUpload = "file" in source;
|
|
210
|
+
const a: ResolvedAsset = isUpload ? await resolveLocalGlb(source.file, id) : await resolveAsset(source.query, id, "character");
|
|
211
|
+
const current = loadDoc();
|
|
212
|
+
const autoScale = a.bounds[1] > 0.1 && a.bounds[1] < 3 ? Math.min(4, Math.max(0.3, 1.6 / a.bounds[1])) : 1.6;
|
|
213
|
+
const scale = scaleOverride ?? autoScale;
|
|
214
|
+
// Persist the GLB's real clip list on the asset so the clip-existence guardrail can check
|
|
215
|
+
// scheduled clips against it on every later edit (not just here).
|
|
216
|
+
// E3 provenance: a `cast add --query` member is CATALOG-RESOLVED (the UI labels it as such and
|
|
217
|
+
// never as authored-fallback). An uploaded GLB (`cast add --file`) is "user-uploaded".
|
|
218
|
+
const fidelity = a.fidelity ?? a.rigGrade;
|
|
219
|
+
const newChar = { id, url: a.url, scale, defaultClip: a.clips[0] ?? "idle", availableClips: a.clips, mouthMorphIndex: -1, source: (isUpload ? "user-uploaded" : "catalog-resolved") as "user-uploaded" | "catalog-resolved", attribution: a.attribution, license: a.license, sourceUrl: a.sourceUrl, hash: a.hash, ...(fidelity ? { fidelityGrade: fidelity } : {}) };
|
|
220
|
+
const characters = current.assets.characters.some((c) => c.id === id)
|
|
221
|
+
? current.assets.characters.map((c) => (c.id === id ? newChar : c))
|
|
222
|
+
: [...current.assets.characters, newChar];
|
|
223
|
+
const next = { ...current, assets: { ...current.assets, characters } };
|
|
224
|
+
const v = validateEpisodeDocument(next, { availableClipsByCharacter: availableClipsByCharacter(next) });
|
|
225
|
+
if (!v.ok) { console.error(`REJECTED:\n - ${v.errors.join("\n - ")}`); process.exit(1); }
|
|
226
|
+
persist(next, [...loadHistory(), current]);
|
|
227
|
+
console.log(`cast ${id} ← ${a.attribution} [fidelity ${fidelity ?? "?"}] (clips: ${a.clips.slice(0, 3).join(",")}, scale ${scale.toFixed(2)})`);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Resolve a prop from the catalog and register it under `id` (swap or add). */
|
|
231
|
+
async function propAdd(id: string, query: string): Promise<void> {
|
|
232
|
+
const a = await resolveAsset(query, id, "prop");
|
|
233
|
+
const current = loadDoc();
|
|
234
|
+
const newProp = { id, url: a.url, attribution: a.attribution, license: a.license, sourceUrl: a.sourceUrl, hash: a.hash };
|
|
235
|
+
const props = current.assets.props.some((p) => p.id === id)
|
|
236
|
+
? current.assets.props.map((p) => (p.id === id ? newProp : p))
|
|
237
|
+
: [...current.assets.props, newProp];
|
|
238
|
+
const next = { ...current, assets: { ...current.assets, props } };
|
|
239
|
+
const v = validateEpisodeDocument(next, { availableClipsByCharacter: availableClipsByCharacter(next) });
|
|
240
|
+
if (!v.ok) { console.error(`REJECTED:\n - ${v.errors.join("\n - ")}`); process.exit(1); }
|
|
241
|
+
persist(next, [...loadHistory(), current]);
|
|
242
|
+
console.log(`prop ${id} ← ${a.attribution}`);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
switch (cmd) {
|
|
246
|
+
case "new": {
|
|
247
|
+
const from = opt("from");
|
|
248
|
+
const prompt = opt("prompt");
|
|
249
|
+
// PROMPT is the ONLY new-scene path (or cloning an explicit --from document). There is NO
|
|
250
|
+
// default content fixture — `animation-scene new` with no prompt/from is an error, so a
|
|
251
|
+
// content scene (e.g. Moon Garden) can never leak in as the default.
|
|
252
|
+
if (!from && !prompt) {
|
|
253
|
+
console.error(
|
|
254
|
+
'animation-scene new requires a prompt: animation-scene new --prompt "<your scene>"\n' +
|
|
255
|
+
" (or clone an existing document with --from <doc.json>). There is no default scene."
|
|
256
|
+
);
|
|
257
|
+
process.exit(1);
|
|
258
|
+
}
|
|
259
|
+
// `--full` (F2): generate a COMPLETE, no-fallback scene from the prompt — cast, dialogue,
|
|
260
|
+
// camera and per-beat actions all derived from the prompt (procedural placeholder rigs the
|
|
261
|
+
// agent can swap for catalog rigs with `cast add`). Default `new` stays the EMPTY skeleton.
|
|
262
|
+
const full = argv.includes("--full");
|
|
263
|
+
const base: EpisodeDocument = from
|
|
264
|
+
? (JSON.parse(readFileSync(resolve(from), "utf8")) as EpisodeDocument)
|
|
265
|
+
: full
|
|
266
|
+
? generateSceneFromPrompt(prompt!).document
|
|
267
|
+
: buildFromPrompt(prompt!);
|
|
268
|
+
persist(base, []);
|
|
269
|
+
console.log(`new working scene${prompt ? ` from prompt "${prompt}"` : ""}${full ? " (full generation)" : ""} → ${DOC_PATH}`);
|
|
270
|
+
console.log(summarizeDocument(base));
|
|
271
|
+
if (prompt && full) {
|
|
272
|
+
console.log(
|
|
273
|
+
`\nFULL scene generated: ${base.assets.characters.length} cast, ${base.dialogue?.lines.length ?? 0} dialogue lines, ` +
|
|
274
|
+
`${base.shots.length} shots with directed actions. Swap placeholder rigs for real ones with:\n` +
|
|
275
|
+
base.assets.characters
|
|
276
|
+
.map((c) => ` animation-scene cast add --id ${c.id} --query "<look for ${c.id.replace(/-/g, " ")}>"`)
|
|
277
|
+
.join("\n") +
|
|
278
|
+
`\nThen render → animation-scene render`
|
|
279
|
+
);
|
|
280
|
+
} else if (prompt) {
|
|
281
|
+
// Suggest concrete cast ids from the prompt (or honor an explicit --characters list) so
|
|
282
|
+
// the directing agent has ready-to-run `cast add` commands. The cast is EMPTY until run.
|
|
283
|
+
const explicit = opt("characters")?.split(",").map((s) => s.trim()).filter(Boolean);
|
|
284
|
+
const suggested = explicit && explicit.length > 0 ? explicit : suggestCastIds(prompt);
|
|
285
|
+
const castAddLines = suggested
|
|
286
|
+
.map((id) => ` animation-scene cast add --id ${id} --query "<look for ${id.replace(/-/g, " ")}>"`)
|
|
287
|
+
.join("\n");
|
|
288
|
+
console.log(
|
|
289
|
+
`\nThis is a SKELETON (set + shots + timeline only; cast is EMPTY). YOU, the directing agent, now author the story:\n` +
|
|
290
|
+
` 1. cast: resolve real characters from the prompt (suggested ids: ${suggested.join(", ")}):\n` +
|
|
291
|
+
`${castAddLines}\n` +
|
|
292
|
+
` 2. dialogue: write the real lines → animation-scene dialogue --line <id> --speaker <id> --text "..." --start <s> --end <s>\n` +
|
|
293
|
+
` 3. perform: distinct motion per beat → animation-scene block/gesture --character <id> --shot <id> ...\n` +
|
|
294
|
+
` 4. render → animation-scene render\n` +
|
|
295
|
+
`No placeholder captions and no default cast are written — resolve real characters before authoring performance.`
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
300
|
+
case "show":
|
|
301
|
+
console.log(summarizeDocument(loadDoc()));
|
|
302
|
+
break;
|
|
303
|
+
case "block": {
|
|
304
|
+
const characterId = opt("character")!;
|
|
305
|
+
const shotId = opt("shot")!;
|
|
306
|
+
const to = vec(opt("to"));
|
|
307
|
+
const yaw = Number(opt("yaw") ?? "0");
|
|
308
|
+
const clip = opt("clip");
|
|
309
|
+
if (!characterId || !shotId || !to) {
|
|
310
|
+
console.error("usage: block --character <id> --shot <id> --to x,z [--yaw r] [--clip name]");
|
|
311
|
+
process.exit(1);
|
|
312
|
+
}
|
|
313
|
+
applyEdit((d) => setBlocking(d, characterId, shotId, [{ time: 0, position: to, yaw }], clip));
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
case "camera": {
|
|
317
|
+
const shotId = opt("shot")!;
|
|
318
|
+
if (!shotId) { console.error("usage: camera --shot <id> [--preset p] [--subject x,y,z]"); process.exit(1); }
|
|
319
|
+
applyEdit((d) => setCamera(d, shotId, { preset: opt("preset") as CameraPresetId | undefined, subject: vec(opt("subject"), 0.75) }));
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
322
|
+
case "gesture": {
|
|
323
|
+
const characterId = opt("character")!;
|
|
324
|
+
const shotId = opt("shot")!;
|
|
325
|
+
const clip = opt("clip")!;
|
|
326
|
+
if (!characterId || !shotId || !clip) { console.error("usage: gesture --character <id> --shot <id> --clip <name>"); process.exit(1); }
|
|
327
|
+
applyEdit((d) => setGesture(d, characterId, shotId, clip));
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
case "dress": {
|
|
331
|
+
const propId = opt("prop")!;
|
|
332
|
+
const at = vec(opt("at"));
|
|
333
|
+
if (!propId || !at) { console.error("usage: dress --prop <id> --at x,z [--scale s] [--feet f]"); process.exit(1); }
|
|
334
|
+
applyEdit((d) => placeProp(d, { propId, position: at, scale: Number(opt("scale") ?? "0.12"), feetOffset: Number(opt("feet") ?? "2.8") }));
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
337
|
+
case "clear-props":
|
|
338
|
+
applyEdit((d) => clearProps(d, opt("prop")));
|
|
339
|
+
break;
|
|
340
|
+
case "set": {
|
|
341
|
+
const id = argv[1] && !argv[1].startsWith("--") ? argv[1] : undefined;
|
|
342
|
+
const query = opt("query");
|
|
343
|
+
const template = id ? getSetTemplate(id) : query ? pickSetForPrompt(query) : undefined;
|
|
344
|
+
if (!template) {
|
|
345
|
+
console.error(`usage: set <${SET_TEMPLATES.map((t) => t.id).join("|")}> | set --query "..."`);
|
|
346
|
+
process.exit(1);
|
|
347
|
+
}
|
|
348
|
+
applyEdit((d) => ({ ...d, set: template.set, walkableBounds: template.walkableBounds }));
|
|
349
|
+
console.log(`set → ${template.id}`);
|
|
350
|
+
break;
|
|
351
|
+
}
|
|
352
|
+
case "cast": {
|
|
353
|
+
const file = opt("file");
|
|
354
|
+
const query = opt("query");
|
|
355
|
+
// EITHER resolve from the catalog (`--query`) OR upload a local rigged GLB (`--file`).
|
|
356
|
+
if (argv[1] !== "add" || !opt("id") || (!query && !file)) {
|
|
357
|
+
console.error('usage: cast add --id <id> (--query "..." | --file <path.glb>) [--scale s]');
|
|
358
|
+
process.exit(1);
|
|
359
|
+
}
|
|
360
|
+
const src = file ? { file } : { query: query! };
|
|
361
|
+
void castAdd(opt("id")!, src, opt("scale") ? Number(opt("scale")) : undefined).catch((e) => { console.error(e instanceof Error ? e.message : e); process.exitCode = 1; });
|
|
362
|
+
break;
|
|
363
|
+
}
|
|
364
|
+
case "scale": {
|
|
365
|
+
const id = opt("character");
|
|
366
|
+
const to = Number(opt("to"));
|
|
367
|
+
if (!id || Number.isNaN(to)) { console.error("usage: scale --character <id> --to <s>"); process.exit(1); }
|
|
368
|
+
applyEdit((d) => setCharacterScale(d, id, to));
|
|
369
|
+
break;
|
|
370
|
+
}
|
|
371
|
+
case "shot": {
|
|
372
|
+
const sub = argv[1];
|
|
373
|
+
if (sub === "add") {
|
|
374
|
+
const shotId = opt("id");
|
|
375
|
+
if (!shotId) { console.error("usage: shot add --id <id> [--preset p] [--duration s] [--subject x,y,z]"); process.exit(1); }
|
|
376
|
+
applyEdit((d) => addShot(d, shotId, (opt("preset") ?? "two-shot") as CameraPresetId, Number(opt("duration") ?? "12"), vec(opt("subject"), 0.75) ?? [0, 0.75, 0]));
|
|
377
|
+
} else if (sub === "remove") {
|
|
378
|
+
const shotId = opt("id");
|
|
379
|
+
if (!shotId) { console.error("usage: shot remove --id <id>"); process.exit(1); }
|
|
380
|
+
applyEdit((d) => removeShot(d, shotId));
|
|
381
|
+
} else if (sub === "retime") {
|
|
382
|
+
const shotId = opt("id");
|
|
383
|
+
const duration = Number(opt("duration"));
|
|
384
|
+
if (!shotId || Number.isNaN(duration)) { console.error("usage: shot retime --id <id> --duration s"); process.exit(1); }
|
|
385
|
+
applyEdit((d) => retimeShot(d, shotId, duration));
|
|
386
|
+
} else {
|
|
387
|
+
console.error("usage: shot add|remove|retime ...");
|
|
388
|
+
process.exit(1);
|
|
389
|
+
}
|
|
390
|
+
break;
|
|
391
|
+
}
|
|
392
|
+
case "prop": {
|
|
393
|
+
if (argv[1] !== "add" || !opt("id") || !opt("query")) { console.error('usage: prop add --id <id> --query "..."'); process.exit(1); }
|
|
394
|
+
void propAdd(opt("id")!, opt("query")!).catch((e) => { console.error(e instanceof Error ? e.message : e); process.exitCode = 1; });
|
|
395
|
+
break;
|
|
396
|
+
}
|
|
397
|
+
case "dialogue": {
|
|
398
|
+
if (argv.includes("--remove")) {
|
|
399
|
+
const lineId = opt("line");
|
|
400
|
+
if (!lineId) { console.error("usage: dialogue --remove --line <id>"); process.exit(1); }
|
|
401
|
+
applyEdit((d) => removeDialogueLine(d, lineId));
|
|
402
|
+
break;
|
|
403
|
+
}
|
|
404
|
+
const lineId = opt("line")!;
|
|
405
|
+
const speakerId = opt("speaker")!;
|
|
406
|
+
const text = opt("text")!;
|
|
407
|
+
if (!lineId || !speakerId || !text) { console.error('usage: dialogue --line <id> --speaker <id> --text "..." --start s [--end s]'); process.exit(1); }
|
|
408
|
+
// --end is OPTIONAL: when omitted, setDialogueLine auto-computes it from the speech
|
|
409
|
+
// duration of the text (captions show only while the line would be spoken).
|
|
410
|
+
const endRaw = opt("end");
|
|
411
|
+
const endTime = endRaw !== undefined ? Number(endRaw) : undefined;
|
|
412
|
+
applyEdit((d) => setDialogueLine(d, { lineId, speakerId, text, startTime: Number(opt("start") ?? "0"), endTime }));
|
|
413
|
+
break;
|
|
414
|
+
}
|
|
415
|
+
case "retime": {
|
|
416
|
+
// Re-sequence ALL dialogue lines back-to-back from t=0 using the speech-duration model.
|
|
417
|
+
applyEdit((d) => retimeDialogue(d));
|
|
418
|
+
break;
|
|
419
|
+
}
|
|
420
|
+
case "undo": {
|
|
421
|
+
const history = loadHistory();
|
|
422
|
+
const prev = history.pop();
|
|
423
|
+
if (!prev) { console.error("nothing to undo"); process.exit(1); }
|
|
424
|
+
persist(prev, history);
|
|
425
|
+
console.log("undone");
|
|
426
|
+
break;
|
|
427
|
+
}
|
|
428
|
+
case "validate": {
|
|
429
|
+
const v = validateEpisodeDocument(loadDoc());
|
|
430
|
+
console.log(`coherence: ${v.ok ? "PASS" : "FAIL"}`);
|
|
431
|
+
for (const e of v.errors) console.log(` ERROR: ${e}`);
|
|
432
|
+
for (const w of v.warnings) console.log(` warn: ${w}`);
|
|
433
|
+
process.exitCode = v.ok ? 0 : 1;
|
|
434
|
+
break;
|
|
435
|
+
}
|
|
436
|
+
case "render": {
|
|
437
|
+
if (!existsSync(DOC_PATH)) { console.error("no working scene. Run: animation-scene new"); process.exit(1); }
|
|
438
|
+
const range = opt("range");
|
|
439
|
+
const r = spawnSync(
|
|
440
|
+
"pnpm",
|
|
441
|
+
["exec", "tsx", "--tsconfig", "tsconfig.base.json", "packages/create-aura3d/templates/animation-studio/scripts/render-live.ts"],
|
|
442
|
+
{ cwd: REPO, stdio: "inherit", env: { ...process.env, AURA_DOCUMENT: DOC_PATH, AURA_OUTPUT_DIR: "dist/episodes/scene", ...(range ? { AURA_PREVIEW_RANGE: range } : {}) } }
|
|
443
|
+
);
|
|
444
|
+
process.exitCode = r.status ?? 0;
|
|
445
|
+
break;
|
|
446
|
+
}
|
|
447
|
+
default:
|
|
448
|
+
console.log("commands:");
|
|
449
|
+
console.log(" new [--prompt \"...\"] [--full] [--characters a,b] [--from doc.json] | show | validate | render [--range a-b]");
|
|
450
|
+
console.log(" set <template|--query \"...\"> | cast add --id <id> --query \"...\" | prop add --id <id> --query \"...\"");
|
|
451
|
+
console.log(" block --character <id> --shot <id> --to x,z [--yaw r] [--clip name] | scale --character <id> --to s");
|
|
452
|
+
console.log(" camera --shot <id> [--preset p] [--subject x,y,z] | gesture --character <id> --shot <id> --clip name");
|
|
453
|
+
console.log(" dress --prop <id> --at x,z [--scale s] [--feet f] | clear-props [--prop <id>]");
|
|
454
|
+
console.log(" dialogue --line <id> --speaker <id> --text \"...\" --start s [--end s] | dialogue --remove --line <id> | retime");
|
|
455
|
+
console.log(" shot add|remove|retime --id <id> [--preset p] [--duration s] [--subject x,y,z] | undo");
|
|
456
|
+
console.log("\nthe agent you're already using (Claude Code / Codex / …) drives these — no separate LLM.");
|
|
457
|
+
}
|