create-aura3d 1.1.2 → 1.3.1

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.
Files changed (215) hide show
  1. package/dist/index.d.ts +1 -1
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +6 -4
  4. package/dist/index.js.map +1 -1
  5. package/package.json +10 -9
  6. package/templates/{cartoon-channel → animation-channel}/README.md +11 -11
  7. package/templates/{cartoon-studio → animation-channel}/index.html +1 -1
  8. package/templates/{cartoon-channel → animation-channel}/package.json +2 -2
  9. package/templates/{cartoon-channel → animation-channel}/src/episode.ts +22 -22
  10. package/templates/{cartoon-channel → animation-channel}/src/experimental/README.md +5 -5
  11. package/templates/{cartoon-channel → animation-channel}/src/experimental/concept-episode-2-5d.css +3 -3
  12. package/templates/{cartoon-channel → animation-channel}/src/experimental/concept-episode-2-5d.ts +7 -7
  13. package/templates/{cartoon-channel → animation-channel}/src/experimental/image-puppet-episode.css +4 -4
  14. package/templates/{cartoon-channel → animation-channel}/src/experimental/image-puppet-episode.ts +6 -6
  15. package/templates/{cartoon-channel → animation-channel}/src/experimental/puppet-episode-2d.css +3 -3
  16. package/templates/{cartoon-channel → animation-channel}/src/experimental/puppet-episode-2d.ts +7 -7
  17. package/templates/{cartoon-channel → animation-channel}/src/main.ts +32 -32
  18. package/templates/{cartoon-channel → animation-channel}/src/render-plan.ts +13 -13
  19. package/templates/{cartoon-channel → animation-channel}/src/sample-episode-visual.css +34 -34
  20. package/templates/{cartoon-channel → animation-channel}/src/sample-episode-visual.ts +20 -20
  21. package/templates/{cartoon-channel → animation-channel}/tests/concept-2-5d-animation.spec.ts +3 -3
  22. package/templates/{cartoon-channel → animation-channel}/tests/concept-2-5d.spec.ts +6 -6
  23. package/templates/{cartoon-channel → animation-channel}/tests/image-puppet-animation.spec.ts +3 -3
  24. package/templates/{cartoon-channel → animation-channel}/tests/image-puppet.spec.ts +5 -5
  25. package/templates/{cartoon-channel → animation-channel}/tests/puppet-2d-animation.spec.ts +3 -3
  26. package/templates/{cartoon-channel → animation-channel}/tests/puppet-2d.spec.ts +4 -4
  27. package/templates/{prompt-cartoon-channel → animation-channel}/tests/route-health.spec.ts +1 -1
  28. package/templates/{cartoon-channel → animation-channel}/tests/sample-episode-visual.spec.ts +5 -5
  29. package/templates/{cartoon-channel → animation-channel}/tests/storyboard-playback.spec.ts +7 -7
  30. package/templates/animation-studio/AGENTS.md +71 -0
  31. package/templates/{cartoon-studio → animation-studio}/ASSET-LICENSES.md +7 -7
  32. package/templates/{cartoon-studio → animation-studio}/README.md +6 -6
  33. package/templates/animation-studio/aura.assets.json +669 -0
  34. package/templates/animation-studio/dist/episodes/scene/episode-3d.webm +0 -0
  35. package/templates/animation-studio/dist/episodes/scene/frames/action.png +0 -0
  36. package/templates/animation-studio/dist/episodes/scene/frames/dialogue.png +0 -0
  37. package/templates/animation-studio/dist/episodes/scene/frames/final.png +0 -0
  38. package/templates/animation-studio/dist/episodes/scene/frames/first.png +0 -0
  39. package/templates/animation-studio/dist/episodes/scene/frames/mouth-closed.png +0 -0
  40. package/templates/animation-studio/dist/episodes/scene/frames/mouth-open.png +0 -0
  41. package/templates/animation-studio/dist/episodes/scene/render-live-summary.json +10284 -0
  42. package/templates/animation-studio/dist/episodes/scene/skeleton-overlays/worker-1.png +0 -0
  43. package/templates/animation-studio/dist/episodes/scene/skeleton-overlays/worker-2.png +0 -0
  44. package/templates/animation-studio/dist/generated/second-scene.document.json +1210 -0
  45. package/templates/animation-studio/dist/generated/studio-edited.document.json +1196 -0
  46. package/templates/animation-studio/dist/scene/inshader-accept.document.json +1 -0
  47. package/templates/animation-studio/dist/scene/inshader-test.document.json +1 -0
  48. package/templates/animation-studio/dist/scene/integ.document.json +1 -0
  49. package/templates/animation-studio/dist/scene/working.document.json +723 -0
  50. package/templates/animation-studio/dist/scene/working.history.json +1 -0
  51. package/templates/{cartoon-studio → animation-studio}/live-route.html +1 -1
  52. package/templates/{cartoon-studio → animation-studio}/package-lock.json +2 -2
  53. package/templates/animation-studio/package.json +32 -0
  54. package/templates/animation-studio/public/aura-assets/cast-a.catalog.glb +0 -0
  55. package/templates/animation-studio/public/aura-assets/cast-b.catalog.glb +0 -0
  56. package/templates/animation-studio/public/aura-assets/luma.catalog.glb +0 -0
  57. package/templates/animation-studio/public/aura-assets/luma2.catalog.glb +0 -0
  58. package/templates/animation-studio/public/aura-assets/miko.catalog.glb +0 -0
  59. package/templates/{cartoon-studio → animation-studio}/public/aura-assets/moonGarden.gltf +1 -1
  60. package/templates/animation-studio/public/aura-assets/mushroom.catalog.glb +0 -0
  61. package/templates/animation-studio/public/aura-assets/rusty.catalog.glb +0 -0
  62. package/templates/animation-studio/public/cast-library/cast-library.json +71 -0
  63. package/templates/animation-studio/public/clip-library/gesture.json +173903 -0
  64. package/templates/animation-studio/public/clip-library/idle.json +23035 -0
  65. package/templates/animation-studio/public/clip-library/manifest.json +154 -0
  66. package/templates/animation-studio/public/clip-library/nod.json +173903 -0
  67. package/templates/animation-studio/public/clip-library/point.json +69879 -0
  68. package/templates/animation-studio/public/clip-library/react.json +66084 -0
  69. package/templates/animation-studio/public/clip-library/run.json +9648 -0
  70. package/templates/animation-studio/public/clip-library/talk.json +69879 -0
  71. package/templates/animation-studio/public/clip-library/walk.json +8283 -0
  72. package/templates/animation-studio/public/hifi-cast/BrainStem.glb +0 -0
  73. package/templates/animation-studio/public/hifi-cast/CesiumMan.glb +0 -0
  74. package/templates/animation-studio/public/hifi-cast/RiggedFigure.glb +0 -0
  75. package/templates/animation-studio/public/hifi-cast/kaykit/Barbarian.glb +0 -0
  76. package/templates/animation-studio/public/hifi-cast/kaykit/Knight.glb +0 -0
  77. package/templates/animation-studio/public/hifi-cast/kaykit/Mage.glb +0 -0
  78. package/templates/animation-studio/public/hifi-cast/kaykit/Rogue.glb +0 -0
  79. package/templates/animation-studio/public/hifi-cast/kaykit/Rogue_Hooded.glb +0 -0
  80. package/templates/animation-studio/public/hifi-cast/khronos-report.json +295 -0
  81. package/templates/animation-studio/public/hifi-cast/quaternius-report.json +449 -0
  82. package/templates/animation-studio/public/hifi-cast/rpm/cesiumman.glb +0 -0
  83. package/templates/animation-studio/public/hifi-cast/rpm/fox.glb +0 -0
  84. package/templates/animation-studio/public/hifi-cast/rpm/riggedfigure.glb +0 -0
  85. package/templates/animation-studio/public/hifi-cast/rpm-report.json +301 -0
  86. package/templates/animation-studio/scripts/anim.ts +103 -0
  87. package/templates/animation-studio/scripts/animation-scene.ts +457 -0
  88. package/templates/animation-studio/scripts/asset-motion-probe.ts +450 -0
  89. package/templates/animation-studio/scripts/asset-render-probe.ts +152 -0
  90. package/templates/animation-studio/scripts/author-office-scene.ts +65 -0
  91. package/templates/animation-studio/scripts/build-cast-library.ts +97 -0
  92. package/templates/animation-studio/scripts/build-characters.ts +1543 -0
  93. package/templates/animation-studio/scripts/build-clip-library.ts +972 -0
  94. package/templates/animation-studio/scripts/clip-decision-cli.ts +51 -0
  95. package/templates/animation-studio/scripts/determinism-check.ts +48 -0
  96. package/templates/animation-studio/scripts/generate-scene.ts +95 -0
  97. package/templates/animation-studio/scripts/hifi-cast-validate.ts +572 -0
  98. package/templates/animation-studio/scripts/motion-evidence-cli.ts +150 -0
  99. package/templates/animation-studio/scripts/motion-evidence.ts +362 -0
  100. package/templates/animation-studio/scripts/preview-server.ts +52 -0
  101. package/templates/animation-studio/scripts/regen-manifest.ts +156 -0
  102. package/templates/animation-studio/scripts/render-core.ts +360 -0
  103. package/templates/animation-studio/scripts/render-live.ts +813 -0
  104. package/templates/animation-studio/scripts/render-parallel.ts +70 -0
  105. package/templates/animation-studio/scripts/render-server.ts +119 -0
  106. package/templates/animation-studio/scripts/resolve-asset.ts +669 -0
  107. package/templates/animation-studio/scripts/skeleton-overlay-cli.ts +82 -0
  108. package/templates/animation-studio/scripts/skeleton-overlay.ts +293 -0
  109. package/templates/animation-studio/scripts/studio-tools-demo.ts +45 -0
  110. package/templates/animation-studio/scripts/texture-bake.ts +297 -0
  111. package/templates/{cartoon-studio → animation-studio}/scripts/validate-characters.ts +0 -0
  112. package/templates/animation-studio/scripts/verify-extracted-overlay-cli.ts +264 -0
  113. package/templates/animation-studio/src/animation-episode-validator.ts +97 -0
  114. package/templates/animation-studio/src/animation-performance.ts +556 -0
  115. package/templates/animation-studio/src/aura-assets.ts +71 -0
  116. package/templates/animation-studio/src/character.ts +40 -0
  117. package/templates/animation-studio/src/director/compile-episode-document.ts +45 -0
  118. package/templates/animation-studio/src/director/director-heuristics.ts +697 -0
  119. package/templates/animation-studio/src/director/prompt-to-scene.ts +369 -0
  120. package/templates/animation-studio/src/empty-document.ts +77 -0
  121. package/templates/animation-studio/src/episode-document.ts +442 -0
  122. package/templates/animation-studio/src/examples/moon-garden.example.ts +183 -0
  123. package/templates/animation-studio/src/fidelity.ts +129 -0
  124. package/templates/animation-studio/src/graph.ts +14 -0
  125. package/templates/animation-studio/src/procedural-hdri.ts +243 -0
  126. package/templates/animation-studio/src/profile.ts +97 -0
  127. package/templates/animation-studio/src/render-live-route.ts +42 -0
  128. package/templates/animation-studio/src/render-modes.ts +137 -0
  129. package/templates/animation-studio/src/scene-player.ts +867 -0
  130. package/templates/animation-studio/src/set-templates.ts +361 -0
  131. package/templates/animation-studio/src/studio/episode-document-store.ts +72 -0
  132. package/templates/animation-studio/src/studio/scene-tool-registry.ts +86 -0
  133. package/templates/animation-studio/src/studio/scene-tools.ts +169 -0
  134. package/templates/animation-studio/tests/reports/animation-studio/hifi-cast-validation.json +938 -0
  135. package/templates/animation-studio/tsconfig.json +26 -0
  136. package/templates/{cartoon-studio → animation-studio}/vite.config.ts +6 -1
  137. package/templates/character-controller/README.md +33 -0
  138. package/templates/character-controller/index.html +19 -0
  139. package/templates/character-controller/package.json +23 -0
  140. package/templates/character-controller/playwright.config.ts +14 -0
  141. package/templates/character-controller/src/controller.ts +44 -0
  142. package/templates/character-controller/src/main.ts +62 -0
  143. package/templates/character-controller/tests/route-health.spec.ts +26 -0
  144. package/templates/character-controller/tests/screenshot.spec.ts +9 -0
  145. package/templates/character-controller/tsconfig.json +15 -0
  146. package/templates/cinematic-scene/package.json +1 -1
  147. package/templates/episode-builder/README.md +4 -4
  148. package/templates/episode-builder/package.json +1 -1
  149. package/templates/episode-builder/src/aura-assets.ts +1 -1
  150. package/templates/episode-builder/src/builder.ts +4 -4
  151. package/templates/episode-builder/src/episode.ts +21 -21
  152. package/templates/episode-builder/src/main.ts +20 -20
  153. package/templates/episode-builder/src/render-plan.ts +13 -13
  154. package/templates/episode-builder/tests/storyboard-playback.spec.ts +7 -7
  155. package/templates/fighting-game/package.json +1 -1
  156. package/templates/mini-game/package.json +1 -1
  157. package/templates/product-viewer/package.json +1 -1
  158. package/templates/{prompt-cartoon-channel → prompt-animation-channel}/README.md +5 -5
  159. package/templates/{cartoon-channel → prompt-animation-channel}/index.html +1 -1
  160. package/templates/{prompt-cartoon-channel → prompt-animation-channel}/package.json +2 -2
  161. package/templates/{prompt-cartoon-channel → prompt-animation-channel}/src/aura-assets.ts +1 -1
  162. package/templates/{prompt-cartoon-channel → prompt-animation-channel}/src/episode.ts +22 -22
  163. package/templates/{prompt-cartoon-channel → prompt-animation-channel}/src/main.ts +28 -28
  164. package/templates/{prompt-cartoon-channel → prompt-animation-channel}/src/render-plan.ts +13 -13
  165. package/templates/{cartoon-channel → prompt-animation-channel}/tests/route-health.spec.ts +1 -1
  166. package/templates/{prompt-cartoon-channel → prompt-animation-channel}/tests/storyboard-playback.spec.ts +8 -8
  167. package/templates/cartoon-studio/aura.assets.json +0 -400
  168. package/templates/cartoon-studio/package.json +0 -33
  169. package/templates/cartoon-studio/public/aura-assets/luma.authored.glb +0 -0
  170. package/templates/cartoon-studio/public/aura-assets/miko.authored.glb +0 -0
  171. package/templates/cartoon-studio/scripts/build-characters.ts +0 -738
  172. package/templates/cartoon-studio/scripts/build-dialogue-audio.ts +0 -262
  173. package/templates/cartoon-studio/scripts/episode.ts +0 -323
  174. package/templates/cartoon-studio/scripts/render-live.ts +0 -873
  175. package/templates/cartoon-studio/src/aura-assets.ts +0 -64
  176. package/templates/cartoon-studio/src/characters.ts +0 -65
  177. package/templates/cartoon-studio/src/episode-renderer.ts +0 -605
  178. package/templates/cartoon-studio/src/episode.ts +0 -306
  179. package/templates/cartoon-studio/src/main.ts +0 -400
  180. package/templates/cartoon-studio/src/render-live-route.ts +0 -913
  181. package/templates/cartoon-studio/src/render-plan.ts +0 -567
  182. package/templates/cartoon-studio/src/review.ts +0 -99
  183. package/templates/cartoon-studio/src/studio.ts +0 -101
  184. package/templates/cartoon-studio/test-results/.last-run.json +0 -4
  185. package/templates/cartoon-studio/tests/episode-render.spec.ts +0 -44
  186. package/templates/cartoon-studio/tests/motion-quality.spec.ts +0 -45
  187. package/templates/cartoon-studio/tests/route-health.spec.ts +0 -7
  188. package/templates/cartoon-studio/tests/storyboard-playback.spec.ts +0 -99
  189. package/templates/cartoon-studio/tests/visual-review.spec.ts +0 -21
  190. package/templates/cartoon-studio/tsconfig.json +0 -14
  191. package/templates/prompt-cartoon-channel/index.html +0 -13
  192. package/templates/prompt-cartoon-channel/src/sets.ts +0 -14
  193. /package/templates/{cartoon-channel → animation-channel}/aura.assets.json +0 -0
  194. /package/templates/{cartoon-channel → animation-channel}/playwright.config.ts +0 -0
  195. /package/templates/{cartoon-channel → animation-channel}/public/aura-assets/luma.047f5e5f.glb +0 -0
  196. /package/templates/{cartoon-channel → animation-channel}/public/aura-assets/luma.thumb.svg +0 -0
  197. /package/templates/{cartoon-channel → animation-channel}/public/aura-assets/miko.047f5e5f.glb +0 -0
  198. /package/templates/{cartoon-channel → animation-channel}/public/aura-assets/miko.thumb.svg +0 -0
  199. /package/templates/{cartoon-channel → animation-channel}/public/aura-assets/moon-garden-feature-frame.png +0 -0
  200. /package/templates/{cartoon-channel → animation-channel}/src/aura-assets.ts +0 -0
  201. /package/templates/{cartoon-channel → animation-channel}/src/characters.ts +0 -0
  202. /package/templates/{cartoon-channel → animation-channel}/src/contract.ts +0 -0
  203. /package/templates/{cartoon-channel → animation-channel}/src/sets.ts +0 -0
  204. /package/templates/{cartoon-channel → animation-channel}/test-results/.last-run.json +0 -0
  205. /package/templates/{cartoon-channel → animation-channel}/tests/reports/prompt-animation/cartoon-sample-episode.png +0 -0
  206. /package/templates/{cartoon-channel → animation-channel}/tsconfig.json +0 -0
  207. /package/templates/{cartoon-studio → animation-studio}/playwright.config.ts +0 -0
  208. /package/templates/{cartoon-studio → animation-studio}/public/aura-assets/luma.thumb.svg +0 -0
  209. /package/templates/{cartoon-studio → animation-studio}/public/aura-assets/miko.thumb.svg +0 -0
  210. /package/templates/{cartoon-studio → animation-studio}/src/contract.ts +0 -0
  211. /package/templates/{prompt-cartoon-channel → prompt-animation-channel}/playwright.config.ts +0 -0
  212. /package/templates/{prompt-cartoon-channel → prompt-animation-channel}/src/characters.ts +0 -0
  213. /package/templates/{prompt-cartoon-channel → prompt-animation-channel}/src/contract.ts +0 -0
  214. /package/templates/{cartoon-studio → prompt-animation-channel}/src/sets.ts +0 -0
  215. /package/templates/{prompt-cartoon-channel → prompt-animation-channel}/tsconfig.json +0 -0
@@ -0,0 +1,556 @@
1
+ /**
2
+ * animation-performance.ts — the Phase 2.5 KEYSTONE helper for `scene-player.ts`.
3
+ *
4
+ * It turns the SHARED, rig-neutral animation library (`@aura3d/animation`'s standard humanoid
5
+ * clip registry + performance state graph) into per-frame poses that drive ANY character GLB:
6
+ *
7
+ * beat clip intent ──▶ performance state graph ──▶ sample standard library clip
8
+ * │ (rig-neutral AnimationPose)
9
+ * ▼
10
+ * retargetHumanoidPose(pose, map) (→ GLB node names)
11
+ * │
12
+ * ▼
13
+ * actor.applyRetargetedPose(pose)
14
+ *
15
+ * The library + retargeting come from the FRESHLY-BUILT monorepo `dist/animation`. The template's
16
+ * own published `node_modules/@aura3d/animation` predates Phase 2.4/2.5 (no clip library / rig
17
+ * inference / state graph), and the render route only aliases a fixed list of `@aura3d/*` specifiers
18
+ * — NOT `@aura3d/animation`. So we import the in-repo build by relative path (the same monorepo dist
19
+ * the rest of the route is validated against; vite's workspace fs.allow covers it).
20
+ */
21
+
22
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
23
+ // @ts-ignore — resolved at runtime to the freshly-built monorepo dist (has co-located .d.ts).
24
+ import {
25
+ inferHumanoidRig,
26
+ createHumanoidRetargetingMap,
27
+ retargetHumanoidPose,
28
+ createStandardHumanoidClipRegistry,
29
+ createStandardHumanoidClipDefinitions,
30
+ createPerformanceStateGraph,
31
+ AnimationClipRegistry,
32
+ AnimationTrack,
33
+ STANDARD_LIBRARY_RIG,
34
+ STANDARD_CLIP_IDS,
35
+ PERFORMANCE_GRAPH_PARAMETERS
36
+ } from "@aura3d/animation";
37
+
38
+ type StandardClipId = (typeof STANDARD_CLIP_IDS)[number];
39
+
40
+ /**
41
+ * B1/B3 motion-source provenance. `extracted` = a real catalog-mocap clip from the
42
+ * universal `public/clip-library/<intent>.json`; `procedural` = the standard-library
43
+ * synthesized clip; `embedded` = a rich GLB clip shipped by the character itself;
44
+ * `idle-fallback` = the last-resort idle clip (no usable motion for the intent).
45
+ */
46
+ export type ClipSource = "extracted" | "procedural" | "embedded" | "idle-fallback";
47
+
48
+ /** Canonical body bones a downstream gate measures (EXCLUDING mouth morph + caption + camera). */
49
+ export const BODY_BONES = [
50
+ "hips",
51
+ "spine",
52
+ "chest",
53
+ "upperChest",
54
+ "neck",
55
+ "head",
56
+ "leftShoulder",
57
+ "leftUpperArm",
58
+ "leftLowerArm",
59
+ "rightShoulder",
60
+ "rightUpperArm",
61
+ "rightLowerArm"
62
+ ] as const;
63
+ export type BodyBoneName = (typeof BODY_BONES)[number];
64
+
65
+ /**
66
+ * One character/one-beat clip-decision record (B1). It is emitted per-frame from the
67
+ * performance rig and bubbled into the route seek proof + the render summary so a
68
+ * downstream gate can prove the BODY moves (not just the mouth morph).
69
+ */
70
+ export interface ClipDecision {
71
+ /** The standard intent that was requested for this beat. */
72
+ readonly intent: StandardClipId;
73
+ /** The clip id actually sampled (may differ from intent when the graph auto-returns to idle). */
74
+ readonly clipId: StandardClipId;
75
+ /** Where the sampled motion came from (extracted catalog / procedural / embedded / idle fallback). */
76
+ readonly source: ClipSource;
77
+ /** Count of canonical body bones whose rotation amplitude this frame exceeds a tiny epsilon. */
78
+ readonly bonesTouched: number;
79
+ /** Largest per-body-bone rotation amplitude (radians) applied this frame. */
80
+ readonly maxRotAmplitudeRad: number;
81
+ /**
82
+ * Largest per-body-bone TRANSLATION amplitude (metres) applied this frame across ALL sampled bones
83
+ * (not just the hips root) — a body bone that physically displaces (weight shift, hip bob, hand
84
+ * reach) shows up here even when its rotation is small. B1 contract field.
85
+ */
86
+ readonly maxTransAmplitude: number;
87
+ /** Per-body-bone rotation amplitude (radians) keyed by canonical bone (gate input, mouth-free). */
88
+ readonly bodyBoneRotationRad: Partial<Record<BodyBoneName, number>>;
89
+ /** Root (hips) translation magnitude applied this frame (locomotion / weight shift). */
90
+ readonly rootTranslation: number;
91
+ /**
92
+ * B1 — did this decision's pose actually reach the GLB skeleton at runtime? The library pose only
93
+ * proves the LIBRARY moved; this is set true by `scene-player.ts` once the retargeted pose (or an
94
+ * embedded clip) was applied to ≥1 real skinned skeleton node (`applyRetargetedPose` /
95
+ * `playClip` reported tracks applied). `poseFor` emits it `false`; the player overwrites it.
96
+ */
97
+ readonly reachedGLBRuntime: boolean;
98
+ }
99
+
100
+ /** The 8 standard performance ids (idle/talk/gesture/point/nod/walk/run/react). */
101
+ export const STANDARD_INTENTS = STANDARD_CLIP_IDS as readonly StandardClipId[];
102
+
103
+ const INTENT_SET = new Set<string>(STANDARD_INTENTS);
104
+
105
+ /** Loop-vs-one-shot per standard clip (one-shots auto-return to idle in the graph). */
106
+ const ONE_SHOT = new Set<StandardClipId>(["gesture", "point", "nod", "react"]);
107
+
108
+ /** Plain pose transform shapes (match `@aura3d/animation`'s AnimationPose). */
109
+ interface PoseTransform {
110
+ position?: { x: number; y: number; z: number };
111
+ rotation?: { x: number; y: number; z: number; w: number };
112
+ }
113
+ interface SamplablePose {
114
+ bones: Record<string, PoseTransform>;
115
+ morphTargets?: Record<string, number>;
116
+ }
117
+
118
+ interface SamplableTrack {
119
+ readonly target: string;
120
+ readonly valueType: string;
121
+ sample(time: number): readonly number[];
122
+ }
123
+ interface LibraryClip {
124
+ readonly id: StandardClipId;
125
+ readonly duration: number;
126
+ readonly loop: boolean;
127
+ readonly tracks: readonly SamplableTrack[];
128
+ }
129
+
130
+ /**
131
+ * Map an episode beat's clip field + motion state to one of the standard performance intents.
132
+ *
133
+ * - A director-built document already emits a STANDARD intent (`talk`, `react`, `walk`, …) — use it.
134
+ * - An AUTHORED document (e.g. Moon Garden) carries GLB-clip placeholder names (`Loops`, `Run`,
135
+ * `Idle`) that are NOT standard intents — derive the intent from speaking/movement so those
136
+ * characters still perform from the shared library instead of looping one embedded clip.
137
+ * - Movement always wins (walk/run) so a translating character animates its legs.
138
+ */
139
+ export function resolveIntent(input: {
140
+ readonly clip: string;
141
+ readonly moving: boolean;
142
+ readonly running: boolean;
143
+ readonly speaking: boolean;
144
+ readonly anyDialogue: boolean;
145
+ }): StandardClipId {
146
+ // Locomotion is VELOCITY-GATED, never name-gated: legs cycle ONLY while the root is actually
147
+ // translating. `moving`/`running` come from the per-frame waypoint velocity (sampleBlocking).
148
+ if (input.running) return "run";
149
+ if (input.moving) return "walk";
150
+ // Stationary now. A shot whose authored clip is walk/run must NOT keep walking in place once the
151
+ // character has reached its mark — that is the treadmill/moonwalk bug. So when stopped, IGNORE a
152
+ // locomotion clip name and fall through to the performance/idle state; honor only a non-locomotion
153
+ // performance clip name (gesture/point/nod/react/talk/idle).
154
+ const raw = input.clip.trim().toLowerCase();
155
+ const isLocomotion = raw === "walk" || raw === "run" || /run|sprint|walk|move|loco|jog|stride/.test(raw);
156
+ if (!isLocomotion) {
157
+ if (INTENT_SET.has(raw)) return raw as StandardClipId;
158
+ // Heuristic mapping for common embedded/authored names → standard intent.
159
+ if (/wave|gesture|emote/.test(raw)) return "gesture";
160
+ if (/point/.test(raw)) return "point";
161
+ if (/nod|agree/.test(raw)) return "nod";
162
+ if (/react|surprise|recoil/.test(raw)) return "react";
163
+ if (/talk|speak|dialog/.test(raw)) return "talk";
164
+ }
165
+ // Locomotion clip but stopped, or no usable name — fall back to the dialogue state.
166
+ if (input.speaking) return "talk";
167
+ if (input.anyDialogue) return "react";
168
+ return "idle";
169
+ }
170
+
171
+ /** One character's shared-library performance driver (state graph + retarget map + clips). */
172
+ export interface PerformanceRig {
173
+ /** GLB node names this rig will write to (used to build a clean identity base each frame). */
174
+ readonly targetNodeNames: readonly string[];
175
+ /** Retarget map coverage (standard rig → this GLB rig); low coverage = sparse skeleton. */
176
+ readonly coverage: number;
177
+ /** True when the GLB's own clips are rich enough to be preferred over retargeting (fallback rule). */
178
+ readonly preferEmbedded: boolean;
179
+ /**
180
+ * Produce the GLB-node-keyed pose for `intent` at episode time `t`. Returns a pose whose `bones`
181
+ * keys are the GLB's node names — feed straight into `actor.applyRetargetedPose(pose)` — plus a
182
+ * B1 {@link ClipDecision} measuring the body-bone motion this frame (mouth/caption/camera-free).
183
+ */
184
+ poseFor(intent: StandardClipId, t: number, morph?: Record<string, number>): {
185
+ readonly pose: SamplablePose;
186
+ readonly decision: ClipDecision;
187
+ };
188
+ /** Current state-graph state name (for the proof / debugging). */
189
+ readonly state: string;
190
+ }
191
+
192
+ export interface PerformanceRigOptions {
193
+ /** Flat list of the GLB's skeleton node names (for rig inference). */
194
+ readonly nodeNames: readonly string[];
195
+ /** The GLB's own embedded clip names (used by the fallback rule). */
196
+ readonly embeddedClips: readonly string[];
197
+ /** B3 per-intent motion source (extracted catalog vs procedural) from {@link loadSharedClipLibrary}. */
198
+ readonly sources?: Readonly<Record<StandardClipId, "extracted" | "procedural">>;
199
+ }
200
+
201
+ /** Build the (procedural) shared clip registry once and reuse across characters. */
202
+ export function createSharedClipRegistry(): ReturnType<typeof createStandardHumanoidClipRegistry> {
203
+ return createStandardHumanoidClipRegistry();
204
+ }
205
+
206
+ /** A shared registry plus the resolved motion SOURCE per standard intent (B3). */
207
+ export interface SharedClipLibrary {
208
+ readonly registry: ReturnType<typeof createStandardHumanoidClipRegistry>;
209
+ /** Per-intent: did the first-choice EXTRACTED catalog clip load, or did we fall to procedural? */
210
+ readonly sources: Readonly<Record<StandardClipId, "extracted" | "procedural">>;
211
+ }
212
+
213
+ /** Shape of a `public/clip-library/<id>.json` file (mirrors build-clip-library.ts serialization). */
214
+ interface SerializedClipFile {
215
+ readonly id: StandardClipId;
216
+ readonly name: string;
217
+ readonly duration: number;
218
+ readonly frameRate: number;
219
+ readonly loop: boolean;
220
+ readonly tags?: readonly string[];
221
+ readonly source?: string;
222
+ readonly rig?: string;
223
+ readonly tracks: { target: string; valueType: string; keyframes: unknown[] }[];
224
+ readonly provenance?: { source?: string };
225
+ }
226
+
227
+ /**
228
+ * B3 — load the shared clip registry with the EXTRACTED universal catalog library as the
229
+ * DEFAULT first choice. Mirrors `loadExtractedClipLibrary` from `scripts/build-clip-library.ts`,
230
+ * but uses `fetch` (the player runs in the browser, where `node:fs` is unavailable) to read the
231
+ * SAME `public/clip-library/<intent>.json` files that script writes. We start from the procedural
232
+ * standard clips and OVERRIDE each intent with its real extracted catalog clip when one exists;
233
+ * intents with no usable extracted clip keep the procedural baseline (and are logged as such).
234
+ *
235
+ * Fallback order per intent (logged): extracted catalog clip → procedural standard clip.
236
+ * (The embedded-GLB and idle fallbacks live at the per-character decision layer in `poseFor`.)
237
+ */
238
+ export async function loadSharedClipLibrary(
239
+ baseUrl = "/clip-library"
240
+ ): Promise<SharedClipLibrary> {
241
+ const registry = new AnimationClipRegistry(createStandardHumanoidClipDefinitions());
242
+ const sources: Record<StandardClipId, "extracted" | "procedural"> = Object.create(null);
243
+ for (const id of STANDARD_INTENTS) sources[id] = "procedural";
244
+
245
+ // MOTION-SOURCE POLICY (render-verified, TWICE). The extracted catalog mocap DISTORTS the real
246
+ // 21-joint character rig: with legs it removes/collapses them; even "upper-body only" it stretches
247
+ // the arms to the floor and compresses the lower body. Both failures were only caught in a real
248
+ // render (FK overlays + numeric checks passed). So the DEFAULT is the PROCEDURAL baseline — proven
249
+ // to render clean: natural standing, normal arm length, full intact legs. Extracted is opt-in for
250
+ // future per-rig validation only.
251
+ // - default (off) → pure procedural baseline (clean, stable) ← DEFAULT
252
+ // - AURA_EXTRACTED=upper-body → extracted upper-body + stripped legs (distorts arms; validation only)
253
+ // - AURA_EXTRACTED=full → keep leg tracks too (breaks legs; validation only)
254
+ const extractedMode = (typeof process !== "undefined" && process.env?.AURA_EXTRACTED) || "off";
255
+ if (extractedMode === "off") {
256
+ console.info("[animation-performance] extracted clips OFF → procedural baseline (clean rig; mocap distorts this rig).");
257
+ return { registry, sources };
258
+ }
259
+ const STRIP_LEGS = extractedMode !== "full";
260
+ const LEG_BONE = /(leg|knee|foot|toe|ankle|thigh|shin)/i;
261
+
262
+ await Promise.all(
263
+ STANDARD_INTENTS.map(async (id) => {
264
+ try {
265
+ // Locomotion (walk/run) IS the legs — stripping their leg tracks would leave the character
266
+ // gliding with no step. Keep the PROCEDURAL walk/run (real, stable leg cycle); use extracted
267
+ // only for the upper-body-dominant intents (talk/gesture/nod/point/react/idle).
268
+ if (STRIP_LEGS && (id === "walk" || id === "run")) {
269
+ console.info(`[animation-performance] clip-library/${id}.json → procedural (locomotion keeps its leg cycle).`);
270
+ return;
271
+ }
272
+ const res = await fetch(`${baseUrl}/${id}.json`, { cache: "no-cache" });
273
+ if (!res.ok) {
274
+ console.warn(`[animation-performance] clip-library/${id}.json HTTP ${res.status} → procedural baseline.`);
275
+ return;
276
+ }
277
+ const file = (await res.json()) as SerializedClipFile;
278
+ // Only override with a REAL extracted catalog clip; procedural-fallback files are identical
279
+ // to the baseline already registered, so registering them would just be noise.
280
+ if (file.provenance?.source !== "catalog-extracted") {
281
+ console.warn(`[animation-performance] clip-library/${id}.json is "${file.provenance?.source ?? "unknown"}" (not catalog-extracted) → procedural baseline.`);
282
+ return;
283
+ }
284
+ // SANITY GATE: reject extracted clips whose root (hips) motion is in raw/un-normalized
285
+ // units or carries a collapsing static offset — those displace the character to the floor.
286
+ // Clean clips on the standard rig (meters) keep hips translation ≲1m and hips rotation
287
+ // near identity. Until the extraction pipeline normalizes units + rest pose, fall back to
288
+ // the (cranked) procedural baseline for any clip that fails this — so the default is never broken.
289
+ {
290
+ let maxTrans = 0;
291
+ let maxHipsRotOffset = 0;
292
+ // A clip with a baked axis-conversion rest offset (e.g. a constant 90° head/limb
293
+ // rotation from a Z-up source) is INVISIBLE to a range-based check — the bone never
294
+ // moves, it just sits rotated, and retargeting bakes that 90° onto the target rig so
295
+ // the character renders laid-down / contorted. Detect it by the bone that stays
296
+ // FARTHEST from rest the whole clip: for each rotation track take the MIN offset from
297
+ // identity across keyframes (the closest it ever gets to rest); genuine motion passes
298
+ // near rest, a baked offset never does. Reject if any bone's floor exceeds the limit.
299
+ const REST_FLOOR_LIMIT = 0.15; // ≈ a bone pinned ≥64° from rest for the entire clip
300
+ let worstRestFloor = 0;
301
+ let worstRestBone = "";
302
+ for (const tr of file.tracks) {
303
+ const isHipsT = /(^|[.\/])hips\.translation$/i.test(tr.target);
304
+ const isRot = /\.rotation$/i.test(tr.target);
305
+ const isHipsR = /(^|[.\/])hips\.rotation$/i.test(tr.target);
306
+ if (isHipsT) {
307
+ for (const kf of tr.keyframes as { value: number[] }[])
308
+ for (const c of kf.value ?? []) maxTrans = Math.max(maxTrans, Math.abs(c));
309
+ }
310
+ if (isRot) {
311
+ let restFloor = Infinity;
312
+ for (const kf of tr.keyframes as { value: number[] }[]) {
313
+ const v = kf.value ?? [];
314
+ if (v.length < 4) continue;
315
+ const offset = 1 - Math.abs(v[3]!); // |w| far from 1 ⇒ large rotation
316
+ restFloor = Math.min(restFloor, offset);
317
+ if (isHipsR) maxHipsRotOffset = Math.max(maxHipsRotOffset, offset);
318
+ }
319
+ if (restFloor !== Infinity && restFloor > worstRestFloor) {
320
+ worstRestFloor = restFloor;
321
+ worstRestBone = tr.target;
322
+ }
323
+ }
324
+ }
325
+ if (maxTrans > 1.5 || maxHipsRotOffset > 0.25 || worstRestFloor > REST_FLOOR_LIMIT) {
326
+ console.warn(
327
+ `[animation-performance] clip-library/${id}.json rejected (hips trans ${maxTrans.toFixed(2)} / hips rotOffset ${maxHipsRotOffset.toFixed(2)} / ${worstRestBone || "—"} rest-floor ${worstRestFloor.toFixed(2)} — un-normalized or baked rest offset) → procedural baseline.`
328
+ );
329
+ return;
330
+ }
331
+ }
332
+ // Strip leg/foot/toe tracks by default so the extracted mocap drives only the upper body;
333
+ // the legs stay at the clean rest pose (built fresh each frame), which keeps them STABLE on
334
+ // the real character rig. (Hips translation is also a leg-adjacent root signal — drop it too.)
335
+ const usableTracks = STRIP_LEGS
336
+ ? file.tracks.filter((tr) => !LEG_BONE.test(tr.target) && !/hips\.translation$/i.test(tr.target))
337
+ : file.tracks;
338
+ const tracks = usableTracks.map(
339
+ (tr) =>
340
+ new AnimationTrack({
341
+ target: tr.target,
342
+ valueType: tr.valueType as never,
343
+ keyframes: tr.keyframes as never
344
+ })
345
+ );
346
+ registry.register(
347
+ {
348
+ id: file.id,
349
+ name: file.name ?? file.id,
350
+ duration: file.duration,
351
+ frameRate: file.frameRate ?? 30,
352
+ loop: file.loop ?? true,
353
+ tags: file.tags ?? [file.id, "performance", "extracted"],
354
+ source: file.source ?? "aura3d.catalog-extracted",
355
+ tracks: tracks as never,
356
+ metadata: { rig: file.rig ?? STANDARD_LIBRARY_RIG.id, extracted: true }
357
+ } as never,
358
+ { replace: true }
359
+ );
360
+ sources[id] = "extracted";
361
+ console.info(`[animation-performance] clip-library/${id}.json → EXTRACTED catalog clip loaded (${tracks.length} tracks, ${file.duration.toFixed?.(2) ?? file.duration}s).`);
362
+ } catch (err) {
363
+ console.warn(`[animation-performance] clip-library/${id}.json fetch failed (${(err as Error).message}) → procedural baseline.`);
364
+ }
365
+ })
366
+ );
367
+
368
+ const extractedCount = STANDARD_INTENTS.filter((id) => sources[id] === "extracted").length;
369
+ console.info(`[animation-performance] shared clip library: ${extractedCount}/${STANDARD_INTENTS.length} intents from EXTRACTED universal catalog, rest procedural.`);
370
+ return { registry, sources };
371
+ }
372
+
373
+ /**
374
+ * Build a {@link PerformanceRig} for one character: infer its humanoid rig from its GLB node names,
375
+ * build a retarget map from the standard library rig onto it, and hold a performance state graph.
376
+ *
377
+ * Fallback rule (Phase 2.5): if the GLB ships rich, named, varied clips of its own (more than one
378
+ * usable motion clip beyond a single idle), we mark `preferEmbedded` so the caller plays the
379
+ * embedded clip whenever the intent matches one. Sparse rigs (one idle, or a skeleton too minimal
380
+ * to retarget well) ALWAYS get retargeted library motion.
381
+ */
382
+ export function createPerformanceRig(
383
+ registry: ReturnType<typeof createStandardHumanoidClipRegistry>,
384
+ options: PerformanceRigOptions
385
+ ): PerformanceRig {
386
+ const rig = inferHumanoidRig(options.nodeNames);
387
+ const map = createHumanoidRetargetingMap(STANDARD_LIBRARY_RIG, rig);
388
+ const targetNodeNames = Object.values(map.bindings)
389
+ .map((b: { target: { name: string } }) => b.target.name)
390
+ .filter((n): n is string => typeof n === "string" && n.length > 0);
391
+
392
+ const graph = createPerformanceStateGraph();
393
+
394
+ // Cache sampled clips by id (registry clips are immutable).
395
+ const clipCache = new Map<StandardClipId, LibraryClip>();
396
+ const getClip = (id: StandardClipId): LibraryClip | undefined => {
397
+ const cached = clipCache.get(id);
398
+ if (cached) return cached;
399
+ const clip = (registry.get?.(id) ?? registry.require?.(id)) as unknown as LibraryClip | undefined;
400
+ if (clip) clipCache.set(id, clip);
401
+ return clip;
402
+ };
403
+
404
+ // Build an identity rest transform for every target node so each frame starts clean — this
405
+ // prevents a bone touched by a previous intent (e.g. an arm raised in `talk`) from freezing when
406
+ // the next intent (e.g. `idle`) doesn't move it.
407
+ const identityBase = (): Record<string, PoseTransform> => {
408
+ const bones: Record<string, PoseTransform> = {};
409
+ for (const node of targetNodeNames) bones[node] = { rotation: { x: 0, y: 0, z: 0, w: 1 } };
410
+ return bones;
411
+ };
412
+
413
+ // Body-bone amplitude is measured on the rig-neutral (canonical) source pose BEFORE retargeting,
414
+ // so the numbers describe the LIBRARY motion (mouth-morph/caption/camera-free) regardless of how
415
+ // sparsely the target GLB maps. A quaternion's rotation magnitude is 2*acos(|w|).
416
+ const rotationAmplitude = (rot?: { x: number; y: number; z: number; w: number }): number => {
417
+ if (!rot) return 0;
418
+ const w = Math.min(1, Math.abs(rot.w));
419
+ return 2 * Math.acos(w);
420
+ };
421
+
422
+ let lastTime = 0;
423
+ let lastState = graph.currentState as string;
424
+
425
+ const poseFor = (
426
+ intent: StandardClipId,
427
+ t: number,
428
+ morph?: Record<string, number>
429
+ ): { readonly pose: SamplablePose; readonly decision: ClipDecision } => {
430
+ // Drive the state graph deterministically from the resolved intent. We set every parameter so
431
+ // the graph crossfades into the requested state (one-shots are pulsed; loops are held).
432
+ const P = PERFORMANCE_GRAPH_PARAMETERS as Record<string, string>;
433
+ graph.setParameter(P.talk, intent === "talk");
434
+ graph.setParameter(P.walk, intent === "walk" || intent === "run");
435
+ graph.setParameter(P.run, intent === "run");
436
+ for (const oneShot of ["gesture", "point", "nod", "react"] as const) {
437
+ graph.setParameter(P[oneShot], intent === oneShot);
438
+ }
439
+ const delta = Math.max(0, t - lastTime);
440
+ lastTime = t;
441
+ graph.update(delta);
442
+ lastState = graph.currentState as string;
443
+
444
+ // The graph may have auto-returned a one-shot to idle; honor the requested intent for sampling
445
+ // so a beat that asks for `gesture` actually plays gesture (the graph still governs transitions
446
+ // for proof/state). For one-shots we sample the requested clip from its local start; for loops
447
+ // we sample at the wrapped episode time so idle/talk/walk cycle continuously.
448
+ const clipId: StandardClipId = INTENT_SET.has(graph.currentState) && !ONE_SHOT.has(intent)
449
+ ? (graph.currentState as StandardClipId)
450
+ : intent;
451
+
452
+ // Sampling fallback order (logged via the decision.source): the requested clip, else idle.
453
+ let sampledId: StandardClipId = clipId;
454
+ let clip = getClip(clipId);
455
+ if (!clip) {
456
+ sampledId = "idle";
457
+ clip = getClip("idle");
458
+ }
459
+
460
+ const bones = identityBase();
461
+ const bodyBoneRotationRad: Partial<Record<BodyBoneName, number>> = {};
462
+ let rootTranslation = 0;
463
+ let maxTransAmplitude = 0;
464
+ if (clip) {
465
+ const duration = clip.duration > 0 ? clip.duration : 1;
466
+ const local = clip.loop ? t % duration : Math.min(t, duration);
467
+ // Sample the standard (rig-neutral) clip into an AnimationPose keyed by canonical bone names.
468
+ const sourceBones: Record<string, PoseTransform> = {};
469
+ for (const track of clip.tracks) {
470
+ const dot = track.target.lastIndexOf(".");
471
+ if (dot < 0) continue;
472
+ const bone = track.target.slice(0, dot);
473
+ const path = track.target.slice(dot + 1);
474
+ const v = track.sample(local);
475
+ const slot = (sourceBones[bone] ??= {});
476
+ if (path === "rotation") slot.rotation = { x: v[0]!, y: v[1]!, z: v[2]!, w: v[3]! };
477
+ else if (path === "translation") slot.position = { x: v[0]!, y: v[1]!, z: v[2]! };
478
+ }
479
+ // B1/B2: measure BODY-bone motion on the rig-neutral source pose (mouth/caption/camera-free).
480
+ for (const bone of BODY_BONES) {
481
+ const amp = rotationAmplitude(sourceBones[bone]?.rotation);
482
+ if (amp > 0) bodyBoneRotationRad[bone] = amp;
483
+ // B1 maxTransAmplitude: the largest per-body-bone translation magnitude (metres) this frame.
484
+ const pos = sourceBones[bone]?.position;
485
+ if (pos) maxTransAmplitude = Math.max(maxTransAmplitude, Math.hypot(pos.x, pos.y, pos.z));
486
+ }
487
+ const hipsPos = sourceBones.hips?.position;
488
+ if (hipsPos) rootTranslation = Math.hypot(hipsPos.x, hipsPos.y, hipsPos.z);
489
+ // Retarget rig-neutral → this GLB's node names, then overlay onto the clean identity base.
490
+ const retargeted = retargetHumanoidPose({ bones: sourceBones }, map) as SamplablePose;
491
+ for (const [node, transform] of Object.entries(retargeted.bones)) bones[node] = transform;
492
+ }
493
+
494
+ const amplitudes = Object.values(bodyBoneRotationRad);
495
+ // ~0.5° threshold: a bone is "touched" only if it actually rotated this frame (not numeric dust).
496
+ const EPS = 0.0087;
497
+ const bonesTouched = amplitudes.filter((a) => a > EPS).length;
498
+ const maxRotAmplitudeRad = amplitudes.length > 0 ? Math.max(...amplitudes) : 0;
499
+
500
+ // B3 source: which library this sampled clip's motion came from. If we had to fall back to idle
501
+ // (the requested intent had no clip at all) report `idle-fallback`; else the per-intent library
502
+ // source resolved at load time (extracted catalog vs procedural baseline).
503
+ const source: ClipSource =
504
+ sampledId !== clipId ? "idle-fallback" : (options.sources?.[sampledId] ?? "procedural");
505
+
506
+ const decision: ClipDecision = {
507
+ intent,
508
+ clipId: sampledId,
509
+ source,
510
+ bonesTouched,
511
+ maxRotAmplitudeRad: +maxRotAmplitudeRad.toFixed(5),
512
+ maxTransAmplitude: +maxTransAmplitude.toFixed(5),
513
+ bodyBoneRotationRad,
514
+ rootTranslation: +rootTranslation.toFixed(5),
515
+ // The library pose has not been applied to a GLB skeleton yet; scene-player flips this true
516
+ // once it applies the retargeted pose / embedded clip to ≥1 real skinned node.
517
+ reachedGLBRuntime: false
518
+ };
519
+
520
+ return { pose: { bones, ...(morph ? { morphTargets: morph } : {}) }, decision };
521
+ };
522
+
523
+ // Fallback rule: a GLB with >1 distinct non-idle embedded clip is considered "rich".
524
+ const nonIdle = options.embeddedClips.filter((c) => !/^idle$/i.test(c.trim()));
525
+ const preferEmbedded = nonIdle.length >= 2;
526
+
527
+ return {
528
+ targetNodeNames,
529
+ coverage: map.coverage as number,
530
+ preferEmbedded,
531
+ poseFor,
532
+ get state() {
533
+ return lastState;
534
+ }
535
+ };
536
+ }
537
+
538
+ /**
539
+ * Does the GLB ship an embedded clip that satisfies this intent? Returns the matching embedded clip
540
+ * name (so the caller can `playClip` it) or undefined to fall through to retargeting.
541
+ */
542
+ export function embeddedClipForIntent(intent: StandardClipId, embeddedClips: readonly string[]): string | undefined {
543
+ const patterns: Partial<Record<StandardClipId, RegExp>> = {
544
+ idle: /idle|breath|stand/i,
545
+ walk: /walk|move/i,
546
+ run: /run|sprint/i,
547
+ gesture: /wave|gesture|emote|hello/i,
548
+ point: /point/i,
549
+ talk: /talk|speak/i,
550
+ nod: /nod/i,
551
+ react: /react|surprise/i
552
+ };
553
+ const pat = patterns[intent];
554
+ if (!pat) return undefined;
555
+ return embeddedClips.find((c) => pat.test(c));
556
+ }
@@ -0,0 +1,71 @@
1
+ import { defineAuraAssets } from "@aura3d/engine";
2
+ import { AURAVOICE_AURA3D_PROMPT_ANIMATION_CONTRACT_ID } from "./contract";
3
+
4
+ export const assetContractId = AURAVOICE_AURA3D_PROMPT_ANIMATION_CONTRACT_ID;
5
+
6
+ export const animationStudioRequiredAssetKeys = ["miko", "luma", "moonGarden"] as const;
7
+
8
+ export const animationStudioOptionalAudioAssetKeys = [
9
+ "mikoDialogueStem",
10
+ "lumaDialogueStem",
11
+ "moonGardenMusic",
12
+ "moonGardenChimeSfx"
13
+ ] as const;
14
+
15
+ export const assets = defineAuraAssets({
16
+ miko: {
17
+ type: "model",
18
+ format: "glb",
19
+ url: "/aura-assets/miko.catalog.glb",
20
+ bounds: [1.015, 1.95, 0.369],
21
+ hash: "sha256-fcdc2b7502031e556259dfe942fdfbd7abe41963433f2046ed2522d27bce9d65",
22
+ metadata: {
23
+ contractId: assetContractId,
24
+ role: "animation-character",
25
+ license: "CC0-1.0",
26
+ attribution: "Miko — Aura3D-authored CC0 procedural humanoid",
27
+ source:
28
+ "Aura3D-authored rigged GLB generated by scripts/build-characters.ts (CC0). Full 21-joint humanoid rig (hips→spine→chest→neck→head, both arms with forearms+hands, both legs with lower-legs+feet+toes; graded-A) with embedded Idle/Wave/Walk/Talk clips and a facial mouthOpen blendshape morph target for lip-sync. Now ships a real UV-mapped 1024×1024 base-colour bitmap texture (skin tone gradients + cheek blush, woven clothing fabric, brushed accent trim, eye/iris falloff, edge ambient occlusion) instead of flat vertex colours.",
29
+ animationClips: ["Idle", "Wave", "Walk", "Talk"],
30
+ // Authored rig ships a real face morph (mouthOpen), so lip-sync drives a
31
+ // blendshape rather than the primitive mouth-card fallback.
32
+ mouthReadiness: "blendshape",
33
+ mouthMorphTargets: ["mouthOpen"]
34
+ }
35
+ },
36
+ luma: {
37
+ type: "model",
38
+ format: "glb",
39
+ url: "/aura-assets/luma2.catalog.glb",
40
+ bounds: [0.99, 1.95, 0.333],
41
+ hash: "sha256-911c60a074f6d89d0eaeefc1f1eecf4188873880b62aa11b3d02ec2c18a4e036",
42
+ metadata: {
43
+ contractId: assetContractId,
44
+ role: "animation-character",
45
+ license: "CC0-1.0",
46
+ attribution: "Luma — Aura3D-authored CC0 procedural humanoid",
47
+ source:
48
+ "Aura3D-authored rigged GLB generated by scripts/build-characters.ts (CC0). A warm-gold helper that matches Miko's style (coherent cast). Full 21-joint humanoid rig (graded-A) with embedded Idle/Wave/Walk/Talk clips and a facial mouthOpen blendshape morph target. Now ships a real UV-mapped 1024×1024 base-colour bitmap texture (skin tone gradients + cheek blush, woven clothing fabric, brushed accent trim, eye/iris falloff, edge ambient occlusion) instead of flat vertex colours.",
49
+ animationClips: ["Idle", "Wave", "Walk", "Talk"],
50
+ // Authored rig ships a real face morph (mouthOpen), so lip-sync drives a
51
+ // blendshape rather than the primitive mouth-card fallback.
52
+ mouthReadiness: "blendshape",
53
+ mouthMorphTargets: ["mouthOpen"]
54
+ }
55
+ },
56
+ moonGarden: {
57
+ type: "model",
58
+ format: "gltf",
59
+ url: "/aura-assets/moonGarden.gltf",
60
+ bounds: [3.2, 0.6, 1.2],
61
+ hash: "sha256-b7b50e37cc4319df21fa2a598aab9e055d54eeb3f5511a3b3f0d4f6c5101c456",
62
+ metadata: {
63
+ contractId: assetContractId,
64
+ role: "animation-set",
65
+ license: "MIT",
66
+ source: "Aura3D animation-studio local generated starter set anchor",
67
+ walkable: true,
68
+ framingReady: true
69
+ }
70
+ }
71
+ } as const);
@@ -0,0 +1,40 @@
1
+ // Typed character definition for Animation Studio. Pure data + types (no engine import) so the
2
+ // profile pipeline (plan/profile/package/verify) runs in plain Node.
3
+
4
+ export type LocomotionAction = "idle" | "walk" | "run";
5
+
6
+ export interface AnimationStudioIkChain {
7
+ readonly id: string;
8
+ readonly root: string;
9
+ readonly mid: string;
10
+ readonly tip: string;
11
+ }
12
+
13
+ export interface AnimationStudioCharacter {
14
+ readonly id: string;
15
+ readonly name: string;
16
+ /** Typed asset key resolved from aura.assets.json (never a raw URL). */
17
+ readonly assetKey: string;
18
+ /** Locomotion clip map: action -> embedded GLB clip name. */
19
+ readonly clipMap: Readonly<Record<LocomotionAction, string>>;
20
+ /** Speeds (units/sec) at which walk and run clips reach full weight. */
21
+ readonly walkSpeed: number;
22
+ readonly runSpeed: number;
23
+ /** Two-bone IK chains the studio can solve (feet/hands). */
24
+ readonly ikChains: readonly AnimationStudioIkChain[];
25
+ }
26
+
27
+ export const requiredLocomotionActions: readonly LocomotionAction[] = ["idle", "walk", "run"];
28
+
29
+ export const heroCharacter: AnimationStudioCharacter = {
30
+ id: "hero",
31
+ name: "Hero",
32
+ assetKey: "hero",
33
+ clipMap: { idle: "Idle", walk: "Walk", run: "Run" },
34
+ walkSpeed: 1.6,
35
+ runSpeed: 4.4,
36
+ ikChains: [
37
+ { id: "left-leg", root: "LeftUpLeg", mid: "LeftLeg", tip: "LeftFoot" },
38
+ { id: "right-leg", root: "RightUpLeg", mid: "RightLeg", tip: "RightFoot" }
39
+ ]
40
+ };