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,369 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* prompt-to-scene.ts — Phase F2: a FULL, deterministic prompt → EpisodeDocument generator
|
|
3
|
+
* with NO fallback to a content fixture (no Moon Garden, no hard-coded miko/luma cast).
|
|
4
|
+
*
|
|
5
|
+
* Given a free-text prompt this produces a *complete, renderable* EpisodeDocument whose
|
|
6
|
+
* cast / set / props / dialogue / camera / actions are all DERIVED FROM THE PROMPT TERMS:
|
|
7
|
+
*
|
|
8
|
+
* - SET : picked by keyword from {@link SET_TEMPLATES} (neutral STUDIO when nothing
|
|
9
|
+
* matches — never Moon Garden unless the prompt is explicitly moon/garden/night).
|
|
10
|
+
* - CAST : 1–3 character ids parsed from the prompt (proper names first, else a plural
|
|
11
|
+
* acting noun → noun-1/noun-2, else a singular noun). Each cast member gets a
|
|
12
|
+
* procedural placeholder GLB url tagged with its prompt-derived look so the doc
|
|
13
|
+
* is self-describing without a network catalog resolve.
|
|
14
|
+
* - PROPS : the chosen set template's ground-dressing prop (scattered by the Director) PLUS
|
|
15
|
+
* any explicit object nouns mentioned in the prompt (e.g. "car", "table").
|
|
16
|
+
* - DIALOGUE: real lines synthesised from the prompt's INTENT (question / argument / teaching /
|
|
17
|
+
* greeting), alternating between cast members, timed by the speech-duration model.
|
|
18
|
+
* These exercise the F1 acting rules (questions, emphasis, disagreement, movement).
|
|
19
|
+
* - CAMERA + ACTIONS : produced by the deterministic Director ({@link directScene}) from the
|
|
20
|
+
* shots + dialogue + cast — establishing → two-shot → medium, with per-beat clip
|
|
21
|
+
* intents (talk / gesture / point / nod / walk / react), never all idle/talk.
|
|
22
|
+
*
|
|
23
|
+
* It is fully deterministic (a prompt always yields the same document) and OFFLINE (no catalog
|
|
24
|
+
* network call) so it runs in CI as the F2 regression proof. The resulting document is a valid
|
|
25
|
+
* EpisodeDocument the generic player renders directly.
|
|
26
|
+
*
|
|
27
|
+
* HONEST SCOPE: the cast GLB urls are procedural placeholders tagged with the prompt-derived
|
|
28
|
+
* look; swapping in catalog-resolved rigs is `animation-scene cast add`'s job (network path).
|
|
29
|
+
* This module proves the *generation* contract — prompt terms drive every part of the document
|
|
30
|
+
* and it never silently reuses Moon Garden — not that an arbitrary prompt is "watchable".
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import {
|
|
34
|
+
directBeats,
|
|
35
|
+
directScene,
|
|
36
|
+
validateDirectedActing,
|
|
37
|
+
type DirectorDialogueLine,
|
|
38
|
+
type DirectorSceneInput
|
|
39
|
+
} from "./director-heuristics";
|
|
40
|
+
import {
|
|
41
|
+
estimateSpeechDuration,
|
|
42
|
+
type CharacterAsset,
|
|
43
|
+
type DialogueLine,
|
|
44
|
+
type DialogueTrack,
|
|
45
|
+
type EpisodeDocument,
|
|
46
|
+
type PropAsset
|
|
47
|
+
} from "../episode-document";
|
|
48
|
+
import { pickSetForPrompt, SET_TEMPLATES, type SetTemplate } from "../set-templates";
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The curated, render-ready A-grade humanoid cast (public/cast-library/*). A generated scene binds
|
|
52
|
+
* each prompt-derived character to one of these real graded-A 21-joint GLBs (cycled), so the doc
|
|
53
|
+
* renders immediately with a properly-rigged, mouth-morph-equipped character. `animation-scene cast
|
|
54
|
+
* add --file/--query` overrides a slot with a user-uploaded or catalog-resolved rig.
|
|
55
|
+
*/
|
|
56
|
+
// NEUTRAL names on purpose: the curated cast must NOT reuse the moon-garden fixture cast
|
|
57
|
+
// (miko/luma) or a generic scene would re-introduce exactly the leakage D1 prohibits.
|
|
58
|
+
const CAST_LIBRARY: readonly { id: string; url: string; mouthMorphIndex: number }[] = [
|
|
59
|
+
{ id: "humanoid-a", url: "/aura-assets/cast-a.catalog.glb", mouthMorphIndex: 0 },
|
|
60
|
+
{ id: "humanoid-b", url: "/aura-assets/cast-b.catalog.glb", mouthMorphIndex: 0 }
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Props are only emitted when they resolve to a REAL asset on disk — a fictional placeholder GLB
|
|
65
|
+
* url would 404 and crash the render. Everything else is left to the set's procedural dressing.
|
|
66
|
+
*/
|
|
67
|
+
const REAL_PROP_GLB: Readonly<Record<string, string>> = {
|
|
68
|
+
mushroom: "/aura-assets/mushroom.catalog.glb"
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/** Words that are never a character name or object noun. */
|
|
72
|
+
const STOP = new Set([
|
|
73
|
+
"the", "a", "an", "and", "or", "but", "on", "in", "at", "of", "to", "with", "for", "from",
|
|
74
|
+
"two", "three", "four", "their", "his", "her", "its", "they", "them", "while", "as", "about",
|
|
75
|
+
"over", "under", "into", "near", "this", "that", "these", "those", "are", "is", "was", "were",
|
|
76
|
+
"who", "what", "when", "where", "why", "how", "then", "than", "some", "any", "each"
|
|
77
|
+
]);
|
|
78
|
+
|
|
79
|
+
/** The scene INTENT we infer from the prompt — drives the synthesised dialogue. */
|
|
80
|
+
export type SceneIntent = "argument" | "question" | "teaching" | "greeting" | "task";
|
|
81
|
+
|
|
82
|
+
function slug(s: string): string {
|
|
83
|
+
return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function words(prompt: string): string[] {
|
|
87
|
+
return prompt.toLowerCase().split(/[^a-z']+/).filter((w) => w.length > 0);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Location / set words — these describe WHERE the scene is, not WHO is in it, so they must never
|
|
92
|
+
* be parsed as cast members (e.g. "...in a forest" / "...in a garage"). Derived from the set-template
|
|
93
|
+
* keywords plus a few generic place words.
|
|
94
|
+
*/
|
|
95
|
+
const LOCATION_WORDS = new Set<string>([
|
|
96
|
+
...SET_TEMPLATES.flatMap((t) => t.keywords),
|
|
97
|
+
"forest", "garden", "room", "scene", "background", "place", "ground", "sky", "floor", "field", "wall", "door", "table"
|
|
98
|
+
]);
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Parse 1–3 cast ids from the prompt. Proper names win ("Miko and Luma" → miko, luma); else nouns
|
|
102
|
+
* introduced by an article or joined by "and" ("a fox and a bear" → fox, bear; "a chef teaching a
|
|
103
|
+
* child" → chef, child), excluding location words; else a plural acting noun ("two robots" →
|
|
104
|
+
* robot-1, robot-2); else a singular noun; else a single generic "hero". Never returns the
|
|
105
|
+
* Moon-Garden cast unless the prompt literally names them.
|
|
106
|
+
*/
|
|
107
|
+
export function parseCast(prompt: string): string[] {
|
|
108
|
+
const properNames = (prompt.match(/\b[A-Z][a-z]{2,}\b/g) ?? [])
|
|
109
|
+
.map(slug)
|
|
110
|
+
.filter((n) => n && !STOP.has(n));
|
|
111
|
+
if (properNames.length > 0) return [...new Set(properNames)].slice(0, 3);
|
|
112
|
+
|
|
113
|
+
// Multi-character: a noun introduced by an article (a/an/the) or "and" is a candidate actor,
|
|
114
|
+
// unless it is a location/set word. Catches "a fox and a bear", "a chef teaching a child".
|
|
115
|
+
const toks = words(prompt);
|
|
116
|
+
const candidates: string[] = [];
|
|
117
|
+
for (let i = 0; i < toks.length; i += 1) {
|
|
118
|
+
const w = toks[i]!;
|
|
119
|
+
const introducedByArticle = i > 0 && ["a", "an", "the", "and"].includes(toks[i - 1]!);
|
|
120
|
+
if (introducedByArticle && w.length >= 3 && !STOP.has(w) && !LOCATION_WORDS.has(w)) candidates.push(slug(w));
|
|
121
|
+
}
|
|
122
|
+
const uniqueCandidates = [...new Set(candidates)].filter((n) => n.length > 0);
|
|
123
|
+
if (uniqueCandidates.length >= 2) return uniqueCandidates.slice(0, 3);
|
|
124
|
+
|
|
125
|
+
const lower = prompt.toLowerCase();
|
|
126
|
+
// First plural noun that is an actor (not a location): "two robots" → robot-1, robot-2.
|
|
127
|
+
const plurals = lower.match(/\b([a-z]{3,})s\b/g) ?? [];
|
|
128
|
+
for (const p of plurals) {
|
|
129
|
+
const base = slug(p.replace(/s$/, ""));
|
|
130
|
+
if (base && !STOP.has(base) && !LOCATION_WORDS.has(base)) return [`${base}-1`, `${base}-2`];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (uniqueCandidates.length === 1) return uniqueCandidates;
|
|
134
|
+
|
|
135
|
+
const singular = words(prompt).find((w) => w.length >= 3 && !STOP.has(w) && !LOCATION_WORDS.has(w));
|
|
136
|
+
return [singular ? slug(singular) : "hero"];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Object nouns the prompt explicitly names (e.g. "car", "table") → extra registered props. */
|
|
140
|
+
export function parseObjectProps(prompt: string, castIds: readonly string[]): string[] {
|
|
141
|
+
const castWords = new Set(castIds.flatMap((id) => id.split("-")));
|
|
142
|
+
const OBJECTS = [
|
|
143
|
+
"car", "table", "chair", "desk", "door", "box", "ball", "book", "lamp", "tree",
|
|
144
|
+
"rock", "barrel", "crate", "engine", "computer", "robot", "ship", "rocket", "sign"
|
|
145
|
+
];
|
|
146
|
+
const present = new Set(words(prompt));
|
|
147
|
+
return OBJECTS.filter((o) => present.has(o) && !castWords.has(o));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Infer the scene's dramatic intent from the prompt verbs. */
|
|
151
|
+
export function inferIntent(prompt: string): SceneIntent {
|
|
152
|
+
const w = new Set(words(prompt));
|
|
153
|
+
if (["argue", "arguing", "argument", "fight", "fighting", "disagree", "clash"].some((k) => w.has(k))) return "argument";
|
|
154
|
+
if (prompt.includes("?") || ["ask", "asking", "question", "wonder", "mystery"].some((k) => w.has(k))) return "question";
|
|
155
|
+
if (["teach", "teaching", "explain", "explaining", "show", "showing", "lesson", "learn"].some((k) => w.has(k))) return "teaching";
|
|
156
|
+
if (["fix", "fixing", "repair", "build", "building", "make", "making", "work", "working"].some((k) => w.has(k))) return "task";
|
|
157
|
+
return "greeting";
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Synthesise dialogue LINES (text only) from the prompt intent + cast, alternating speakers.
|
|
162
|
+
* The lines deliberately exercise every F1 acting rule so the director produces visible acting:
|
|
163
|
+
* a question ("?"), emphasis ("!"/ALL-CAPS), a disagreement (negation), and a movement verb.
|
|
164
|
+
* The TOPIC noun (the focus object / first cast role) is woven in so the lines reference the
|
|
165
|
+
* prompt — they are not generic boilerplate.
|
|
166
|
+
*/
|
|
167
|
+
export function synthesizeDialogue(prompt: string, castIds: readonly string[], intent: SceneIntent): { speakerId: string; text: string }[] {
|
|
168
|
+
const a = castIds[0]!;
|
|
169
|
+
const b = castIds[1] ?? castIds[0]!;
|
|
170
|
+
// The topic the scene is about: the named object prop, else the first cast role word.
|
|
171
|
+
const objects = parseObjectProps(prompt, castIds);
|
|
172
|
+
const topic = objects[0] ?? a.replace(/-\d+$/, "").replace(/-/g, " ") ?? "this";
|
|
173
|
+
|
|
174
|
+
const byIntent: Record<SceneIntent, [string, string][]> = {
|
|
175
|
+
argument: [
|
|
176
|
+
[a, `Did you even look at the ${topic}?`],
|
|
177
|
+
[b, `Yes, and it is NOT my fault!`],
|
|
178
|
+
[a, `That is completely wrong.`],
|
|
179
|
+
[b, `Then come over here and prove it.`],
|
|
180
|
+
[a, `Fine. Move aside and let me check.`],
|
|
181
|
+
[b, `Right here, look at this part.`]
|
|
182
|
+
],
|
|
183
|
+
question: [
|
|
184
|
+
[a, `What happened to the ${topic}?`],
|
|
185
|
+
[b, `I am not sure, it just stopped.`],
|
|
186
|
+
[a, `Did you try anything yet?`],
|
|
187
|
+
[b, `No, I waited for you.`],
|
|
188
|
+
[a, `Alright, walk me through it slowly.`],
|
|
189
|
+
[b, `It started right about here.`]
|
|
190
|
+
],
|
|
191
|
+
teaching: [
|
|
192
|
+
[a, `Let me show you how the ${topic} works.`],
|
|
193
|
+
[b, `Okay, I am watching closely.`],
|
|
194
|
+
[a, `First, you NEVER force this part.`],
|
|
195
|
+
[b, `Oh, I always did it the wrong way!`],
|
|
196
|
+
[a, `Come closer and try it yourself.`],
|
|
197
|
+
[b, `Like this? It actually moved!`]
|
|
198
|
+
],
|
|
199
|
+
task: [
|
|
200
|
+
[a, `We have to fix this ${topic} before dark.`],
|
|
201
|
+
[b, `Agreed, hand me that tool.`],
|
|
202
|
+
[a, `Careful, do NOT drop it!`],
|
|
203
|
+
[b, `Relax, I have got it.`],
|
|
204
|
+
[a, `Now go around and hold the other side.`],
|
|
205
|
+
[b, `On it. Pulling now!`]
|
|
206
|
+
],
|
|
207
|
+
greeting: [
|
|
208
|
+
[a, `Hey, is the ${topic} ready?`],
|
|
209
|
+
[b, `Almost! Just give me a second.`],
|
|
210
|
+
[a, `That is great news.`],
|
|
211
|
+
[b, `I know, I can NOT wait to show you.`],
|
|
212
|
+
[a, `Come over and let us take a look.`],
|
|
213
|
+
[b, `Here it is, right in front of you.`]
|
|
214
|
+
]
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
return byIntent[intent].map(([speakerId, text]) => ({ speakerId, text }));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export interface PromptSceneOptions {
|
|
221
|
+
/** Scene length in seconds (default 24). Shots + dialogue are tiled across it. */
|
|
222
|
+
readonly duration?: number;
|
|
223
|
+
/** Number of shots (default 3 → establishing / two-shot / medium). */
|
|
224
|
+
readonly shotCount?: number;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export interface GeneratedScene {
|
|
228
|
+
readonly document: EpisodeDocument;
|
|
229
|
+
readonly intent: SceneIntent;
|
|
230
|
+
readonly setTemplateId: string;
|
|
231
|
+
readonly cast: readonly string[];
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Generate a complete EpisodeDocument from a prompt — the F2 entry point. NO fallback:
|
|
236
|
+
* the document's cast, set, props, dialogue, camera and actions are all derived from the
|
|
237
|
+
* prompt. A non-moon prompt never inherits any Moon-Garden asset.
|
|
238
|
+
*/
|
|
239
|
+
export function generateSceneFromPrompt(prompt: string, options: PromptSceneOptions = {}): GeneratedScene {
|
|
240
|
+
const trimmed = prompt.trim();
|
|
241
|
+
if (trimmed.length === 0) {
|
|
242
|
+
throw new Error("generateSceneFromPrompt requires a non-empty prompt — there is no default scene.");
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const duration = options.duration ?? 24;
|
|
246
|
+
const shotCount = Math.max(2, options.shotCount ?? 3);
|
|
247
|
+
const intent = inferIntent(trimmed);
|
|
248
|
+
const template: SetTemplate = pickSetForPrompt(trimmed);
|
|
249
|
+
const castIds = parseCast(trimmed);
|
|
250
|
+
|
|
251
|
+
// --- Cast assets: bind each prompt-derived character to a real graded-A humanoid (cycled) so the
|
|
252
|
+
// generated doc renders immediately; the prompt-derived id/name is preserved for the scene. ---
|
|
253
|
+
const characters: CharacterAsset[] = castIds.map((id, i) => {
|
|
254
|
+
const rig = CAST_LIBRARY[i % CAST_LIBRARY.length]!;
|
|
255
|
+
return {
|
|
256
|
+
id,
|
|
257
|
+
url: rig.url,
|
|
258
|
+
scale: 1.3,
|
|
259
|
+
defaultClip: "idle",
|
|
260
|
+
availableClips: ["idle", "talk", "gesture", "point", "nod", "walk", "run", "react"],
|
|
261
|
+
mouthMorphIndex: rig.mouthMorphIndex,
|
|
262
|
+
attribution: `"${id.replace(/-/g, " ")}" — curated A-grade humanoid (${rig.id})`,
|
|
263
|
+
license: "CC0",
|
|
264
|
+
fidelityGrade: "A"
|
|
265
|
+
} as CharacterAsset;
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// --- Props: ONLY emit a prop that resolves to a REAL asset; otherwise leave it to the set's
|
|
269
|
+
// procedural dressing (a fictional placeholder GLB would 404 and crash the render). ---
|
|
270
|
+
const objectProps = parseObjectProps(trimmed, castIds);
|
|
271
|
+
const props: PropAsset[] = [];
|
|
272
|
+
if (template.groundProp && REAL_PROP_GLB[template.groundProp.propId]) {
|
|
273
|
+
props.push({
|
|
274
|
+
id: template.groundProp.propId,
|
|
275
|
+
url: REAL_PROP_GLB[template.groundProp.propId]!,
|
|
276
|
+
attribution: `set dressing: ${template.groundProp.query}`,
|
|
277
|
+
license: "CC0"
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
for (const obj of objectProps) {
|
|
281
|
+
// Always RECORD the prompt's object noun (so the prompt provably influences the doc), but only
|
|
282
|
+
// attach a url when it resolves to a real asset; otherwise it renders as procedural dressing.
|
|
283
|
+
const url = REAL_PROP_GLB[obj];
|
|
284
|
+
props.push(url ? { id: obj, url, attribution: `prompt object: ${obj}`, license: "CC0" } : { id: obj, attribution: `prompt object: ${obj} (procedural — no resolved mesh)`, license: "CC0" });
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// --- Shots: tile the timeline contiguously from 0. ---
|
|
288
|
+
const shotDur = duration / shotCount;
|
|
289
|
+
const shots = Array.from({ length: shotCount }, (_, i) => ({
|
|
290
|
+
shotId: `shot-${i + 1}`,
|
|
291
|
+
startTime: Number((i * shotDur).toFixed(3)),
|
|
292
|
+
endTime: Number(((i + 1) * shotDur).toFixed(3))
|
|
293
|
+
}));
|
|
294
|
+
|
|
295
|
+
// --- Dialogue: synthesise prompt-derived lines, timed by the speech model, back-to-back. ---
|
|
296
|
+
const synth = synthesizeDialogue(trimmed, castIds, intent);
|
|
297
|
+
const lines: DialogueLine[] = [];
|
|
298
|
+
let cursor = 0.2;
|
|
299
|
+
synth.forEach((s, i) => {
|
|
300
|
+
const start = Number(cursor.toFixed(3));
|
|
301
|
+
const dur = estimateSpeechDuration(s.text);
|
|
302
|
+
let end = Number((start + dur).toFixed(3));
|
|
303
|
+
if (end > duration) end = duration;
|
|
304
|
+
lines.push({ lineId: `l${i}`, speakerId: s.speakerId, text: s.text, startTime: start, endTime: end });
|
|
305
|
+
cursor = end + 0.25; // small gap between lines
|
|
306
|
+
});
|
|
307
|
+
const dialogueTrack: DialogueTrack = { language: "en", lines };
|
|
308
|
+
|
|
309
|
+
// --- Director: camera + per-character actions from the shots + dialogue + cast. ---
|
|
310
|
+
const directorDialogue: DirectorDialogueLine[] = lines.map((l) => ({
|
|
311
|
+
lineId: l.lineId,
|
|
312
|
+
speakerId: l.speakerId,
|
|
313
|
+
startTime: l.startTime,
|
|
314
|
+
endTime: l.endTime,
|
|
315
|
+
text: l.text
|
|
316
|
+
}));
|
|
317
|
+
const sceneInput: DirectorSceneInput = {
|
|
318
|
+
duration,
|
|
319
|
+
characters: castIds.map((id, i) => ({ id, entersFrom: i === 0 ? "left" : i === 1 ? "right" : "none" })),
|
|
320
|
+
shots,
|
|
321
|
+
dialogue: directorDialogue,
|
|
322
|
+
walkableBounds: template.walkableBounds,
|
|
323
|
+
props: template.groundProp
|
|
324
|
+
? [{ propId: template.groundProp.propId, count: 6, scaleRange: [...template.groundProp.scaleRange] as [number, number], feetOffset: template.groundProp.feetOffset }]
|
|
325
|
+
: [],
|
|
326
|
+
durationEstimator: estimateSpeechDuration
|
|
327
|
+
};
|
|
328
|
+
const sceneId = `scene-${slug(trimmed).slice(0, 32) || "untitled"}`;
|
|
329
|
+
const directed = directScene(sceneId, sceneInput);
|
|
330
|
+
|
|
331
|
+
// F1 GATE (now wired into the REAL pipeline, not just unit-tested): the director's per-beat
|
|
332
|
+
// acting plan must pass validateDirectedActing — it must not be all idle/talk, must contain a
|
|
333
|
+
// gesture, and every character must get a non-idle intent. A genuinely degenerate scene (e.g. a
|
|
334
|
+
// static all-idle cast, or dialogue that yields no acting) is BLOCKED here at generation time
|
|
335
|
+
// rather than silently producing a dead document.
|
|
336
|
+
//
|
|
337
|
+
// The listener-reaction check (NO_REACTION) and the per-character static check (STATIC_CHARACTER)
|
|
338
|
+
// only make sense for a 2+ character scene: a SOLO cast has no second party to react, so those
|
|
339
|
+
// codes are structurally unsatisfiable and are not "degeneracy" — a one-character scene that
|
|
340
|
+
// still gestures/walks is perfectly actable. We therefore drop those two codes for a solo cast
|
|
341
|
+
// and keep the real degeneracy checks (ALL_LOW_MOTION / NO_GESTURE) for every scene.
|
|
342
|
+
const beats = directBeats(sceneInput);
|
|
343
|
+
const validation = validateDirectedActing(beats, { cast: castIds });
|
|
344
|
+
const solo = castIds.length < 2;
|
|
345
|
+
const blocking = validation.issues.filter(
|
|
346
|
+
(i) => !(solo && (i.code === "NO_REACTION" || i.code === "STATIC_CHARACTER"))
|
|
347
|
+
);
|
|
348
|
+
if (blocking.length > 0) {
|
|
349
|
+
const detail = blocking.map((i) => `[${i.code}] ${i.message}`).join("; ");
|
|
350
|
+
throw new Error(
|
|
351
|
+
`generateSceneFromPrompt produced a degenerate (un-actable) scene for prompt "${trimmed}": ${detail}`
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const document: EpisodeDocument = {
|
|
356
|
+
id: sceneId,
|
|
357
|
+
duration,
|
|
358
|
+
assets: { characters, props },
|
|
359
|
+
set: template.set,
|
|
360
|
+
walkableBounds: template.walkableBounds,
|
|
361
|
+
shots: directed.shots,
|
|
362
|
+
blocking: directed.blocking,
|
|
363
|
+
setDressing: directed.setDressing,
|
|
364
|
+
worldState: directed.worldState,
|
|
365
|
+
dialogue: dialogueTrack
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
return { document, intent, setTemplateId: template.id, cast: castIds };
|
|
369
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* empty-document.ts — the generic EMPTY/placeholder scene.
|
|
3
|
+
*
|
|
4
|
+
* This is the DEFAULT fallback when no scene has been loaded. It deliberately
|
|
5
|
+
* contains NO cast, NO props, NO scene-specific look — just a neutral studio void
|
|
6
|
+
* and a single establishing shot — so the renderer/route never silently falls back
|
|
7
|
+
* to a content fixture (e.g. the Moon Garden example). Real scenes come from
|
|
8
|
+
* `animation-scene new --prompt` (the working document) or an explicit `AURA_DOCUMENT`
|
|
9
|
+
* / injected document. When this placeholder is what's playing, callers print a clear
|
|
10
|
+
* "no scene loaded" message via `EMPTY_DOCUMENT_NOTICE`.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { EpisodeDocument, EnvironmentSpec, SetSpec } from "./episode-document";
|
|
14
|
+
|
|
15
|
+
/** Message to surface whenever the empty placeholder is the scene being rendered. */
|
|
16
|
+
export const EMPTY_DOCUMENT_NOTICE =
|
|
17
|
+
"No scene loaded — rendering the empty placeholder. Create one with `animation-scene new --prompt \"<your scene>\"`, " +
|
|
18
|
+
"or set AURA_DOCUMENT to a saved EpisodeDocument (e.g. a serialized copy of the moon-garden example).";
|
|
19
|
+
|
|
20
|
+
// A neutral, content-free "studio void": a flat grey floor + soft 3-point rig. No
|
|
21
|
+
// characters, no props, no themed set pieces — nothing tied to any particular prompt.
|
|
22
|
+
const NEUTRAL_ENVIRONMENT: EnvironmentSpec = {
|
|
23
|
+
color: [0.5, 0.52, 0.55],
|
|
24
|
+
intensity: 0.4,
|
|
25
|
+
proceduralMap: {
|
|
26
|
+
skyColor: [0.18, 0.19, 0.21],
|
|
27
|
+
horizonColor: [0.24, 0.25, 0.27],
|
|
28
|
+
groundColor: [0.08, 0.08, 0.09],
|
|
29
|
+
specularColor: [0.8, 0.82, 0.85],
|
|
30
|
+
intensity: 0.4,
|
|
31
|
+
specularIntensity: 0.5
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const NEUTRAL_SET: SetSpec = {
|
|
36
|
+
clearColor: [0.06, 0.07, 0.09, 1],
|
|
37
|
+
studioLightingScale: 0.5,
|
|
38
|
+
environment: NEUTRAL_ENVIRONMENT,
|
|
39
|
+
pieces: [
|
|
40
|
+
{
|
|
41
|
+
id: "void-floor",
|
|
42
|
+
geometry: "cube",
|
|
43
|
+
position: [0, -0.06, 0],
|
|
44
|
+
scale: [60, 0.12, 60],
|
|
45
|
+
baseColor: [0.16, 0.17, 0.19, 1],
|
|
46
|
+
metallic: 0.0,
|
|
47
|
+
roughness: 0.9,
|
|
48
|
+
emissiveColor: [0.02, 0.02, 0.03],
|
|
49
|
+
emissiveStrength: 0.05,
|
|
50
|
+
includeInAutoFrame: true
|
|
51
|
+
}
|
|
52
|
+
],
|
|
53
|
+
lights: [
|
|
54
|
+
{ id: "key", kind: "point", color: [0.9, 0.92, 0.95], position: [-2.6, 3.6, 2.6], intensity: 3.6, range: 18 },
|
|
55
|
+
{ id: "fill", kind: "point", color: [0.7, 0.74, 0.82], position: [2.4, 2.0, 1.4], intensity: 1.6, range: 13 },
|
|
56
|
+
{ id: "rim", kind: "point", color: [0.85, 0.85, 0.9], position: [0.4, 1.8, -2.0], intensity: 2.0, range: 13 }
|
|
57
|
+
]
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The placeholder EpisodeDocument. Valid for the generic player (one shot, neutral set)
|
|
62
|
+
* but intentionally empty of content. Never present this as a finished scene.
|
|
63
|
+
*/
|
|
64
|
+
export const emptyDocument: EpisodeDocument = {
|
|
65
|
+
id: "empty-placeholder",
|
|
66
|
+
duration: 6,
|
|
67
|
+
assets: { characters: [], props: [] },
|
|
68
|
+
set: NEUTRAL_SET,
|
|
69
|
+
walkableBounds: { min: [-3.6, 0, -2], max: [3.6, 0, 2] },
|
|
70
|
+
shots: [
|
|
71
|
+
{ shotId: "shot-empty", presetId: "establishing", startTime: 0, endTime: 6, cameraSubject: [0, 0.75, 0] }
|
|
72
|
+
],
|
|
73
|
+
blocking: [],
|
|
74
|
+
setDressing: [],
|
|
75
|
+
worldState: { glowSpanSeconds: 6 },
|
|
76
|
+
dialogue: { language: "en", lines: [] }
|
|
77
|
+
};
|