talking-head-studio 0.2.8 → 0.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.
Files changed (38) hide show
  1. package/dist/TalkingHead.d.ts +8 -0
  2. package/dist/TalkingHead.js +104 -7
  3. package/dist/TalkingHead.web.d.ts +3 -2
  4. package/dist/TalkingHead.web.js +17 -2
  5. package/dist/TalkingHeadVisualization.d.ts +35 -0
  6. package/dist/TalkingHeadVisualization.js +277 -0
  7. package/dist/api/index.d.ts +2 -0
  8. package/dist/api/index.js +18 -0
  9. package/dist/api/studioApi.d.ts +38 -0
  10. package/dist/api/studioApi.js +235 -0
  11. package/dist/api/types.d.ts +87 -0
  12. package/dist/api/types.js +5 -0
  13. package/dist/assets/face-squeeze-local.glb +0 -0
  14. package/dist/filament/FilamentAvatar.d.ts +41 -0
  15. package/dist/filament/FilamentAvatar.js +737 -0
  16. package/dist/filament/faceSqueezeAssets.d.ts +1 -0
  17. package/dist/filament/faceSqueezeAssets.js +5 -0
  18. package/dist/filament/index.d.ts +5 -0
  19. package/dist/filament/index.js +22 -0
  20. package/dist/filament/morphTables.d.ts +5 -0
  21. package/dist/filament/morphTables.js +93 -0
  22. package/dist/filament/useAuthedFilamentUri.d.ts +11 -0
  23. package/dist/filament/useAuthedFilamentUri.js +126 -0
  24. package/dist/html.d.ts +7 -0
  25. package/dist/html.js +255 -56
  26. package/dist/index.d.ts +9 -2
  27. package/dist/index.js +13 -2
  28. package/dist/index.web.d.ts +6 -2
  29. package/dist/index.web.js +10 -2
  30. package/dist/utils/avatarUtils.d.ts +13 -0
  31. package/dist/utils/avatarUtils.js +56 -0
  32. package/dist/wardrobe/index.d.ts +2 -0
  33. package/dist/wardrobe/index.js +20 -0
  34. package/dist/wardrobe/useAvatarWardrobeHydration.d.ts +7 -0
  35. package/dist/wardrobe/useAvatarWardrobeHydration.js +34 -0
  36. package/dist/wardrobe/wardrobeStore.d.ts +30 -0
  37. package/dist/wardrobe/wardrobeStore.js +106 -0
  38. package/package.json +33 -4
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useAvatarWardrobeHydration = useAvatarWardrobeHydration;
4
+ const react_1 = require("react");
5
+ const studioApi_1 = require("../api/studioApi");
6
+ const wardrobeStore_1 = require("./wardrobeStore");
7
+ function useAvatarWardrobeHydration({ avatarId, accessories, }) {
8
+ const hydrateFromApi = (0, wardrobeStore_1.useWardrobeStore)((s) => s.hydrateFromApi);
9
+ const accessorySignature = (0, react_1.useMemo)(() => JSON.stringify(accessories ?? []), [accessories]);
10
+ (0, react_1.useEffect)(() => {
11
+ const nextAccessories = accessories ?? [];
12
+ if (!avatarId || nextAccessories.length === 0) {
13
+ hydrateFromApi([], {});
14
+ return;
15
+ }
16
+ let cancelled = false;
17
+ void (0, studioApi_1.listAssets)()
18
+ .then((allAssets) => {
19
+ if (cancelled)
20
+ return;
21
+ const lookup = {};
22
+ for (const asset of allAssets) {
23
+ lookup[asset.id] = asset;
24
+ }
25
+ hydrateFromApi(nextAccessories, lookup);
26
+ })
27
+ .catch((err) => {
28
+ console.error('[useAvatarWardrobeHydration] Failed to hydrate wardrobe:', err);
29
+ });
30
+ return () => {
31
+ cancelled = true;
32
+ };
33
+ }, [accessorySignature, avatarId, hydrateFromApi]);
34
+ }
@@ -0,0 +1,30 @@
1
+ import type { WearableAsset, EquippedAccessory } from '../api/types';
2
+ export interface AssetPlacement {
3
+ bone: string;
4
+ position: [number, number, number];
5
+ rotation: [number, number, number];
6
+ scale: number;
7
+ }
8
+ export type GizmoMode = 'translate' | 'rotate' | 'scale';
9
+ export interface WardrobeState {
10
+ /** slot -> equipped asset */
11
+ equipped: Record<string, WearableAsset>;
12
+ /** assetId -> placement transform */
13
+ placements: Record<string, AssetPlacement>;
14
+ /** asset currently being positioned (Filament mode) */
15
+ activeAssetId: string | null;
16
+ /** current gizmo operation mode */
17
+ gizmoMode: GizmoMode;
18
+ /** ID of the avatar currently synced to avoid redundant fetches */
19
+ syncedAvatarId: string | null;
20
+ equip: (slot: string, asset: WearableAsset) => void;
21
+ unequip: (slot: string) => void;
22
+ setPlacement: (assetId: string, placement: AssetPlacement) => void;
23
+ setActiveAsset: (assetId: string | null) => void;
24
+ setGizmoMode: (mode: GizmoMode) => void;
25
+ hydrateFromApi: (accessories: EquippedAccessory[], assetLookup: Record<string, WearableAsset>) => void;
26
+ syncWithAvatar: (avatarUrl: string) => Promise<void>;
27
+ serializeForApi: () => EquippedAccessory[];
28
+ reset: () => void;
29
+ }
30
+ export declare const useWardrobeStore: import("zustand").UseBoundStore<import("zustand").StoreApi<WardrobeState>>;
@@ -0,0 +1,106 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useWardrobeStore = void 0;
4
+ const zustand_1 = require("zustand");
5
+ const studioApi_1 = require("../api/studioApi");
6
+ // ---------------------------------------------------------------------------
7
+ // Inline avatar ID extraction (avoids dep on avatarUtils)
8
+ // ---------------------------------------------------------------------------
9
+ const AVATAR_PATH_RE = /\/v1\/avatars\/([^/]+)\/file/;
10
+ function extractAvatarIdFromUrl(url) {
11
+ if (!url)
12
+ return null;
13
+ const match = url.match(AVATAR_PATH_RE);
14
+ return match ? match[1] : null;
15
+ }
16
+ // ---------------------------------------------------------------------------
17
+ // Store
18
+ // ---------------------------------------------------------------------------
19
+ exports.useWardrobeStore = (0, zustand_1.create)()((set, get) => ({
20
+ equipped: {},
21
+ placements: {},
22
+ activeAssetId: null,
23
+ gizmoMode: 'translate',
24
+ syncedAvatarId: null,
25
+ equip: (slot, asset) => set((state) => ({
26
+ equipped: { ...state.equipped, [slot]: asset },
27
+ })),
28
+ unequip: (slot) => set((state) => {
29
+ const removed = state.equipped[slot];
30
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
31
+ const { [slot]: _removed, ...restEquipped } = state.equipped;
32
+ // Clean up placement data for the removed asset
33
+ const placements = { ...state.placements };
34
+ if (removed) {
35
+ delete placements[removed.id];
36
+ }
37
+ return {
38
+ equipped: restEquipped,
39
+ placements,
40
+ activeAssetId: removed && removed.id === state.activeAssetId
41
+ ? null
42
+ : state.activeAssetId,
43
+ };
44
+ }),
45
+ setPlacement: (assetId, placement) => set((state) => ({
46
+ placements: { ...state.placements, [assetId]: placement },
47
+ })),
48
+ setActiveAsset: (assetId) => set({ activeAssetId: assetId }),
49
+ setGizmoMode: (mode) => set({ gizmoMode: mode }),
50
+ hydrateFromApi: (accessories, assetLookup) => set(() => {
51
+ const equipped = {};
52
+ const placements = {};
53
+ for (const acc of accessories) {
54
+ const asset = assetLookup[acc.asset_id];
55
+ if (asset) {
56
+ equipped[asset.slot] = asset;
57
+ placements[acc.asset_id] = {
58
+ bone: acc.bone,
59
+ position: acc.position,
60
+ rotation: acc.rotation,
61
+ scale: acc.scale,
62
+ };
63
+ }
64
+ }
65
+ return { equipped, placements };
66
+ }),
67
+ syncWithAvatar: async (avatarUrl) => {
68
+ const avatarId = extractAvatarIdFromUrl(avatarUrl);
69
+ if (!avatarId)
70
+ return;
71
+ if (get().syncedAvatarId === avatarId)
72
+ return;
73
+ get().reset();
74
+ try {
75
+ const [avatar, allAssets] = await Promise.all([(0, studioApi_1.getAvatar)(avatarId), (0, studioApi_1.listAssets)()]);
76
+ const lookup = {};
77
+ for (const a of allAssets)
78
+ lookup[a.id] = a;
79
+ const accessories = avatar.appearance?.equippedAccessories ?? [];
80
+ get().hydrateFromApi(accessories, lookup);
81
+ set({ syncedAvatarId: avatarId });
82
+ }
83
+ catch (e) {
84
+ console.warn('[WardrobeStore] Failed to load avatar accessories:', e);
85
+ }
86
+ },
87
+ serializeForApi: () => {
88
+ const { equipped, placements } = get();
89
+ return Object.values(equipped).map((asset) => {
90
+ const p = placements[asset.id];
91
+ return {
92
+ asset_id: asset.id,
93
+ bone: p?.bone ?? 'Head',
94
+ position: p?.position ?? [0, 0, 0],
95
+ rotation: p?.rotation ?? [0, 0, 0],
96
+ scale: p?.scale ?? 1,
97
+ };
98
+ });
99
+ },
100
+ reset: () => set({
101
+ equipped: {},
102
+ placements: {},
103
+ activeAssetId: null,
104
+ gizmoMode: 'translate',
105
+ }),
106
+ }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "talking-head-studio",
3
- "version": "0.2.8",
3
+ "version": "0.3.0",
4
4
  "description": "Cross-platform 3D avatar component for React Native & web — lip-sync, gestures, accessories, and LLM integration. Powered by TalkingHead + Three.js.",
5
5
  "main": "dist/index.web.js",
6
6
  "browser": "dist/index.web.js",
@@ -28,6 +28,19 @@
28
28
  "./sketchfab": {
29
29
  "types": "./dist/sketchfab/index.d.ts",
30
30
  "default": "./dist/sketchfab/index.js"
31
+ },
32
+ "./api": {
33
+ "types": "./dist/api/index.d.ts",
34
+ "default": "./dist/api/index.js"
35
+ },
36
+ "./wardrobe": {
37
+ "types": "./dist/wardrobe/index.d.ts",
38
+ "default": "./dist/wardrobe/index.js"
39
+ },
40
+ "./filament": {
41
+ "react-native": "./dist/filament/index.js",
42
+ "types": "./dist/filament/index.d.ts",
43
+ "default": "./dist/filament/index.js"
31
44
  }
32
45
  },
33
46
  "files": [
@@ -35,7 +48,7 @@
35
48
  ],
36
49
  "scripts": {
37
50
  "clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
38
- "build": "npm run clean && tsc --project tsconfig.json",
51
+ "build": "npm run clean && tsc --project tsconfig.json && mkdir -p dist/assets && cp src/assets/*.glb dist/assets/",
39
52
  "typecheck": "tsc --noEmit",
40
53
  "lint": "eslint 'src/**/*.{ts,tsx}'",
41
54
  "format": "prettier --write 'src/**/*.{ts,tsx}'",
@@ -78,12 +91,18 @@
78
91
  "url": "https://github.com/sitebay/talking-head-studio/issues"
79
92
  },
80
93
  "sideEffects": false,
94
+ "dependencies": {
95
+ "zustand": "^5.0.12"
96
+ },
81
97
  "peerDependencies": {
98
+ "@react-three/drei": ">=9",
99
+ "@react-three/fiber": ">=8",
100
+ "expo-asset": ">=10",
101
+ "expo-file-system": ">=17",
82
102
  "react": ">=18",
83
103
  "react-native": ">=0.73",
104
+ "react-native-filament": ">=1",
84
105
  "react-native-webview": ">=13",
85
- "@react-three/fiber": ">=8",
86
- "@react-three/drei": ">=9",
87
106
  "three": ">=0.170"
88
107
  },
89
108
  "peerDependenciesMeta": {
@@ -93,6 +112,15 @@
93
112
  "react-native-webview": {
94
113
  "optional": true
95
114
  },
115
+ "react-native-filament": {
116
+ "optional": true
117
+ },
118
+ "expo-asset": {
119
+ "optional": true
120
+ },
121
+ "expo-file-system": {
122
+ "optional": true
123
+ },
96
124
  "@react-three/fiber": {
97
125
  "optional": true
98
126
  },
@@ -126,6 +154,7 @@
126
154
  "metro-react-native-babel-preset": "^0.77.0",
127
155
  "multer": "^2.1.0",
128
156
  "prettier": "^3.8.1",
157
+ "react-native-webview": "^13.16.0",
129
158
  "react-test-renderer": "^19.2.4",
130
159
  "ts-jest": "^29.4.6"
131
160
  }