Skip to content
Merged
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
6 changes: 6 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,9 @@
- Stage only the intended files, commit the scoped change, push the branch, and open a draft pull request.
- Run the most relevant available checks before opening the pull request, and mention any checks that could not be run.
- Do not overwrite, reset, or include unrelated local/user changes from other worktrees.

## Package Boundary Discipline

- Do not patch consumer apps with Latticework runtime shims, copied services, or compatibility stand-ins for runtime APIs.
- If Latticework fails in a consuming bundler or framework, fix the source/build/export surface in this package and then update the consumer dependency pin.
- For temporary local verification, link or install a built package artifact only; do not commit consumer-local copies of package runtime behavior.
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