Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,15 @@ inspect the actual committed dependency spec and lockfile. A full Git SHA pin
does not move when Polyester `main` moves. A LoomLarge PR is needed to bump the
pin unless CI is explicitly resolving a linked Polyester PR for a preview.

## Runtime Boundary Discipline

When a consumer such as LoomLarge fails because Polyester's package entrypoint,
exports, or bundled runtime artifacts are incompatible with a bundler or runtime,
fix the Polyester package boundary here. Do not work around the failure by
copying, stubbing, or reimplementing Polyester/Latticework runtime services in a
consumer app. The acceptable shape is: repair this package's source/build/export
surface, verify the package artifact, then bump the consumer's pinned dependency.

## Stream And State Guidance

Do not introduce new agency-local RxJS or Most streams inside CLJS planner code.
Expand Down
106 changes: 106 additions & 0 deletions src/animation/bundledSnippetModules.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
type SnippetModule = { default?: unknown } | unknown;
export type BundledSnippetModuleLoader = () => Promise<SnippetModule>;

import emotionAngry from './snippets/emotion/angry.json';
import emotionAnxious from './snippets/emotion/anxious.json';
import emotionCalm from './snippets/emotion/calm.json';
import emotionContempt from './snippets/emotion/contempt.json';
import emotionFlirty from './snippets/emotion/flirty.json';
import emotionHopeful from './snippets/emotion/hopeful.json';
import emotionHopeless from './snippets/emotion/hopeless.json';
import emotionOverwhelmed from './snippets/emotion/overwhelmed.json';
import emotionRelieved from './snippets/emotion/relieved.json';
import emotionSad from './snippets/emotion/sad.json';
import emotionSkeptical from './snippets/emotion/skeptical.json';
import emotionSmirk from './snippets/emotion/smirk.json';
import emotionStressed from './snippets/emotion/stressed.json';
import emotionSurprise from './snippets/emotion/surprise.json';
import emotionWink from './snippets/emotion/wink.json';
import emotionWorried from './snippets/emotion/worried.json';
import speakingBrowFrownAndTilt from './snippets/speaking/browFrownAndTilt.json';
import speakingBrowRaiseAndShortHeadNod from './snippets/speaking/browRaiseAndShortHeadNod.json';
import speakingBrowRaiseLong from './snippets/speaking/browRaiseLong.json';
import speakingBrowRaiseShort from './snippets/speaking/browRaiseShort.json';
import speakingHeadNodBig from './snippets/speaking/headNodBig.json';
import speakingHeadNodSmall from './snippets/speaking/headNodSmall.json';
import visemesLipsyncAmazing from './snippets/visemes/lipsync_amazing.json';
import visemesLipsyncAnthropic from './snippets/visemes/lipsync_anthropic.json';
import visemesLipsyncBeautiful from './snippets/visemes/lipsync_beautiful.json';
import visemesLipsyncGoodMorning from './snippets/visemes/lipsync_good_morning.json';
import visemesLipsyncHello from './snippets/visemes/lipsync_hello.json';
import visemesLipsyncHelloMumbled from './snippets/visemes/lipsync_hello_mumbled.json';
import visemesLipsyncHelloPrecise from './snippets/visemes/lipsync_hello_precise.json';
import visemesLipsyncHelloRelaxed from './snippets/visemes/lipsync_hello_relaxed.json';
import visemesLipsyncHelloTheatrical from './snippets/visemes/lipsync_hello_theatrical.json';
import visemesLipsyncHelloWorld from './snippets/visemes/lipsync_hello_world.json';
import visemesLipsyncHowAreYou from './snippets/visemes/lipsync_how_are_you.json';
import visemesLipsyncSpeech from './snippets/visemes/lipsync_speech.json';
import visemesLipsyncThankYou from './snippets/visemes/lipsync_thank_you.json';
import visemesLipsyncWorld from './snippets/visemes/lipsync_world.json';
import visemesPhraseVisemeSnippet from './snippets/visemes/phrase_viseme_snippet.json';
import visemesTest1 from './snippets/visemes/test1.json';
import eyeHeadTrackingEyePitch from './snippets/eyeHeadTracking/eyePitch.json';
import eyeHeadTrackingEyeRoll from './snippets/eyeHeadTracking/eyeRoll.json';
import eyeHeadTrackingEyeRollCircular from './snippets/eyeHeadTracking/eyeRollCircular.json';
import eyeHeadTrackingEyeYaw from './snippets/eyeHeadTracking/eyeYaw.json';
import eyeHeadTrackingHeadPitch from './snippets/eyeHeadTracking/headPitch.json';
import eyeHeadTrackingHeadRoll from './snippets/eyeHeadTracking/headRoll.json';
import eyeHeadTrackingHeadRollCircular from './snippets/eyeHeadTracking/headRollCircular.json';
import eyeHeadTrackingHeadYaw from './snippets/eyeHeadTracking/headYaw.json';

export const bundledSnippetModules = {
emotionAnimationsList: {
'./snippets/emotion/angry.json': () => Promise.resolve(emotionAngry),
'./snippets/emotion/anxious.json': () => Promise.resolve(emotionAnxious),
'./snippets/emotion/calm.json': () => Promise.resolve(emotionCalm),
'./snippets/emotion/contempt.json': () => Promise.resolve(emotionContempt),
'./snippets/emotion/flirty.json': () => Promise.resolve(emotionFlirty),
'./snippets/emotion/hopeful.json': () => Promise.resolve(emotionHopeful),
'./snippets/emotion/hopeless.json': () => Promise.resolve(emotionHopeless),
'./snippets/emotion/overwhelmed.json': () => Promise.resolve(emotionOverwhelmed),
'./snippets/emotion/relieved.json': () => Promise.resolve(emotionRelieved),
'./snippets/emotion/sad.json': () => Promise.resolve(emotionSad),
'./snippets/emotion/skeptical.json': () => Promise.resolve(emotionSkeptical),
'./snippets/emotion/smirk.json': () => Promise.resolve(emotionSmirk),
'./snippets/emotion/stressed.json': () => Promise.resolve(emotionStressed),
'./snippets/emotion/surprise.json': () => Promise.resolve(emotionSurprise),
'./snippets/emotion/wink.json': () => Promise.resolve(emotionWink),
'./snippets/emotion/worried.json': () => Promise.resolve(emotionWorried),
},
speakingAnimationsList: {
'./snippets/speaking/browFrownAndTilt.json': () => Promise.resolve(speakingBrowFrownAndTilt),
'./snippets/speaking/browRaiseAndShortHeadNod.json': () => Promise.resolve(speakingBrowRaiseAndShortHeadNod),
'./snippets/speaking/browRaiseLong.json': () => Promise.resolve(speakingBrowRaiseLong),
'./snippets/speaking/browRaiseShort.json': () => Promise.resolve(speakingBrowRaiseShort),
'./snippets/speaking/headNodBig.json': () => Promise.resolve(speakingHeadNodBig),
'./snippets/speaking/headNodSmall.json': () => Promise.resolve(speakingHeadNodSmall),
},
visemeAnimationsList: {
'./snippets/visemes/lipsync_amazing.json': () => Promise.resolve(visemesLipsyncAmazing),
'./snippets/visemes/lipsync_anthropic.json': () => Promise.resolve(visemesLipsyncAnthropic),
'./snippets/visemes/lipsync_beautiful.json': () => Promise.resolve(visemesLipsyncBeautiful),
'./snippets/visemes/lipsync_good_morning.json': () => Promise.resolve(visemesLipsyncGoodMorning),
'./snippets/visemes/lipsync_hello.json': () => Promise.resolve(visemesLipsyncHello),
'./snippets/visemes/lipsync_hello_mumbled.json': () => Promise.resolve(visemesLipsyncHelloMumbled),
'./snippets/visemes/lipsync_hello_precise.json': () => Promise.resolve(visemesLipsyncHelloPrecise),
'./snippets/visemes/lipsync_hello_relaxed.json': () => Promise.resolve(visemesLipsyncHelloRelaxed),
'./snippets/visemes/lipsync_hello_theatrical.json': () => Promise.resolve(visemesLipsyncHelloTheatrical),
'./snippets/visemes/lipsync_hello_world.json': () => Promise.resolve(visemesLipsyncHelloWorld),
'./snippets/visemes/lipsync_how_are_you.json': () => Promise.resolve(visemesLipsyncHowAreYou),
'./snippets/visemes/lipsync_speech.json': () => Promise.resolve(visemesLipsyncSpeech),
'./snippets/visemes/lipsync_thank_you.json': () => Promise.resolve(visemesLipsyncThankYou),
'./snippets/visemes/lipsync_world.json': () => Promise.resolve(visemesLipsyncWorld),
'./snippets/visemes/phrase_viseme_snippet.json': () => Promise.resolve(visemesPhraseVisemeSnippet),
'./snippets/visemes/test1.json': () => Promise.resolve(visemesTest1),
},
eyeHeadTrackingAnimationsList: {
'./snippets/eyeHeadTracking/eyePitch.json': () => Promise.resolve(eyeHeadTrackingEyePitch),
'./snippets/eyeHeadTracking/eyeRoll.json': () => Promise.resolve(eyeHeadTrackingEyeRoll),
'./snippets/eyeHeadTracking/eyeRollCircular.json': () => Promise.resolve(eyeHeadTrackingEyeRollCircular),
'./snippets/eyeHeadTracking/eyeYaw.json': () => Promise.resolve(eyeHeadTrackingEyeYaw),
'./snippets/eyeHeadTracking/headPitch.json': () => Promise.resolve(eyeHeadTrackingHeadPitch),
'./snippets/eyeHeadTracking/headRoll.json': () => Promise.resolve(eyeHeadTrackingHeadRoll),
'./snippets/eyeHeadTracking/headRollCircular.json': () => Promise.resolve(eyeHeadTrackingHeadRollCircular),
'./snippets/eyeHeadTracking/headYaw.json': () => Promise.resolve(eyeHeadTrackingHeadYaw),
},
} satisfies Record<string, Record<string, BundledSnippetModuleLoader>>;
42 changes: 26 additions & 16 deletions src/animation/snippetPreloader.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
/**
* Bundled snippets stay grouped by category for UI discovery, but their JSON is
* loaded lazily only when selected. localStorage is reserved for user-authored
* snippets and legacy compatibility filtering.
* Bundled snippets stay grouped by category for UI discovery behind an async
* loader-shaped catalog. localStorage is reserved for user-authored snippets
* and legacy compatibility filtering.
*/

import {
bundledSnippetModules,
type BundledSnippetModuleLoader,
} from './bundledSnippetModules';

type SnippetModule = { default?: unknown } | unknown;
type SnippetModuleLoader = () => Promise<SnippetModule>;
type SnippetStorageLike = Pick<Storage, 'getItem' | 'removeItem'>;
type SnippetData = Record<string, unknown>;

Expand All @@ -17,12 +21,12 @@ export type SnippetCategoryKey =

interface SnippetCategory {
listKey: SnippetCategoryKey;
modules: Record<string, SnippetModuleLoader>;
modules: Record<string, BundledSnippetModuleLoader>;
}

interface BundledSnippetEntry {
name: string;
load: SnippetModuleLoader;
load: BundledSnippetModuleLoader;
}

interface BundledSnippetCategory extends SnippetCategory {
Expand All @@ -41,17 +45,23 @@ export interface ResolvedSnippetEntry {
const BUNDLED_SNIPPET_VERSION_KEY = 'bundledAnimationSnippetsVersion';
const BUNDLED_SNIPPET_MANIFEST_KEY = 'bundledAnimationSnippetsManifest';

// Keep these lazy so bundled snippet JSON is only loaded when selected.
const emotionSnippets = import.meta.glob('./snippets/emotion/*.json') as Record<string, SnippetModuleLoader>;
const speakingSnippets = import.meta.glob('./snippets/speaking/*.json') as Record<string, SnippetModuleLoader>;
const visemeSnippets = import.meta.glob('./snippets/visemes/*.json') as Record<string, SnippetModuleLoader>;
const eyeHeadTrackingSnippets = import.meta.glob('./snippets/eyeHeadTracking/*.json') as Record<string, SnippetModuleLoader>;

const CATEGORIES: SnippetCategory[] = [
{ listKey: 'emotionAnimationsList', modules: emotionSnippets },
{ listKey: 'speakingAnimationsList', modules: speakingSnippets },
{ listKey: 'visemeAnimationsList', modules: visemeSnippets },
{ listKey: 'eyeHeadTrackingAnimationsList', modules: eyeHeadTrackingSnippets },
{
listKey: 'emotionAnimationsList',
modules: bundledSnippetModules.emotionAnimationsList,
},
{
listKey: 'speakingAnimationsList',
modules: bundledSnippetModules.speakingAnimationsList,
},
{
listKey: 'visemeAnimationsList',
modules: bundledSnippetModules.visemeAnimationsList,
},
{
listKey: 'eyeHeadTrackingAnimationsList',
modules: bundledSnippetModules.eyeHeadTrackingAnimationsList,
},
];

function extractName(filePath: string): string {
Expand Down