talking-head-studio 0.4.11 → 0.4.12

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 (142) hide show
  1. package/README.md +279 -193
  2. package/dist/TalkingHead.d.ts +28 -3
  3. package/dist/TalkingHead.js +21 -2
  4. package/dist/TalkingHead.web.d.ts +31 -4
  5. package/dist/TalkingHead.web.js +11 -1
  6. package/dist/TalkingHeadVisualization.d.ts +22 -0
  7. package/dist/TalkingHeadVisualization.js +30 -10
  8. package/dist/api/studioApi.d.ts +12 -1
  9. package/dist/api/studioApi.js +16 -2
  10. package/dist/contract.d.ts +14 -0
  11. package/dist/contract.js +30 -0
  12. package/dist/core/avatar/avatarCapabilities.d.ts +60 -0
  13. package/dist/core/avatar/avatarCapabilities.js +100 -0
  14. package/dist/core/avatar/backends/gaussian.js +6 -4
  15. package/dist/core/avatar/motion.d.ts +1713 -0
  16. package/dist/core/avatar/motion.js +550 -0
  17. package/dist/core/avatar/motionRuntime.d.ts +46 -0
  18. package/dist/core/avatar/motionRuntime.js +84 -0
  19. package/dist/core/avatar/schema.d.ts +33 -5
  20. package/dist/core/avatar/visemes.d.ts +16 -1
  21. package/dist/core/avatar/visemes.js +48 -1
  22. package/dist/editor/AvatarCanvas.js +92 -1
  23. package/dist/editor/AvatarEditor.native.js +1 -0
  24. package/dist/editor/AvatarModel.js +1 -0
  25. package/dist/editor/FaceSqueezeEditor.d.ts +3 -1
  26. package/dist/editor/FaceSqueezeEditor.js +176 -112
  27. package/dist/editor/FaceSqueezeEditor.web.d.ts +3 -1
  28. package/dist/editor/FaceSqueezeEditor.web.js +30 -28
  29. package/dist/editor/RigidAccessory.js +17 -2
  30. package/dist/editor/SkinnedClothing.js +1 -0
  31. package/dist/editor/boneLockedDrag.d.ts +11 -0
  32. package/dist/editor/boneLockedDrag.js +68 -0
  33. package/dist/editor/boneSnap.web.d.ts +27 -0
  34. package/dist/editor/boneSnap.web.js +99 -0
  35. package/dist/editor/index.web.d.ts +10 -0
  36. package/dist/editor/index.web.js +26 -0
  37. package/dist/editor/sounds/haha.wav +0 -0
  38. package/dist/editor/sounds/owie.wav +0 -0
  39. package/dist/editor/sounds/stop.wav +0 -0
  40. package/dist/editor/studioTheme.d.ts +14 -14
  41. package/dist/editor/studioTheme.js +17 -14
  42. package/dist/editor/types.d.ts +1 -0
  43. package/dist/html/accessories.d.ts +7 -0
  44. package/dist/html/accessories.js +149 -0
  45. package/dist/html/motion.d.ts +1 -0
  46. package/dist/html/motion.js +189 -0
  47. package/dist/html/visemes.d.ts +7 -0
  48. package/dist/html/visemes.js +348 -0
  49. package/dist/html.d.ts +1 -1
  50. package/dist/html.js +55 -732
  51. package/dist/index.d.ts +7 -3
  52. package/dist/index.js +17 -1
  53. package/dist/index.web.d.ts +18 -1
  54. package/dist/index.web.js +36 -3
  55. package/dist/sketchfab/api.js +1 -0
  56. package/dist/sketchfab/glbInspect.d.ts +22 -0
  57. package/dist/sketchfab/glbInspect.js +58 -0
  58. package/dist/sketchfab/index.d.ts +3 -0
  59. package/dist/sketchfab/index.js +8 -1
  60. package/dist/sketchfab/inspectRemote.d.ts +13 -0
  61. package/dist/sketchfab/inspectRemote.js +77 -0
  62. package/dist/sketchfab/types.d.ts +10 -0
  63. package/dist/studio/AccessoryBrowserScreen.d.ts +6 -0
  64. package/dist/studio/AccessoryBrowserScreen.js +626 -0
  65. package/dist/studio/AccessoryPanel.d.ts +10 -0
  66. package/dist/studio/AccessoryPanel.js +396 -0
  67. package/dist/studio/AppearancePanel.d.ts +9 -0
  68. package/dist/studio/AppearancePanel.js +77 -0
  69. package/dist/studio/AvatarCreatorScreen.d.ts +5 -0
  70. package/dist/studio/AvatarCreatorScreen.js +806 -0
  71. package/dist/studio/AvatarEditorScreen.d.ts +14 -0
  72. package/dist/studio/AvatarEditorScreen.js +510 -0
  73. package/dist/studio/AvatarGrid.d.ts +23 -0
  74. package/dist/studio/AvatarGrid.js +257 -0
  75. package/dist/studio/ColorSwatch.d.ts +8 -0
  76. package/dist/studio/ColorSwatch.js +100 -0
  77. package/dist/studio/CreateVoiceProfileSheet.d.ts +8 -0
  78. package/dist/studio/CreateVoiceProfileSheet.js +242 -0
  79. package/dist/studio/DetailsPanel.d.ts +15 -0
  80. package/dist/studio/DetailsPanel.js +239 -0
  81. package/dist/studio/FilamentEditor.d.ts +2 -0
  82. package/dist/studio/FilamentEditor.js +6 -0
  83. package/dist/studio/PrecisionPanel.d.ts +2 -0
  84. package/dist/studio/PrecisionPanel.js +7 -0
  85. package/dist/studio/PublicGalleryScreen.d.ts +5 -0
  86. package/dist/studio/PublicGalleryScreen.js +358 -0
  87. package/dist/studio/SketchfabModelCard.d.ts +20 -0
  88. package/dist/studio/SketchfabModelCard.js +104 -0
  89. package/dist/studio/StudioBrowseHeader.d.ts +9 -0
  90. package/dist/studio/StudioBrowseHeader.js +28 -0
  91. package/dist/studio/StudioEmptyState.d.ts +8 -0
  92. package/dist/studio/StudioEmptyState.js +29 -0
  93. package/dist/studio/StudioFloatingAction.d.ts +13 -0
  94. package/dist/studio/StudioFloatingAction.js +42 -0
  95. package/dist/studio/StudioSectionHeader.d.ts +7 -0
  96. package/dist/studio/StudioSectionHeader.js +27 -0
  97. package/dist/studio/StudioSurfaceCard.d.ts +8 -0
  98. package/dist/studio/StudioSurfaceCard.js +20 -0
  99. package/dist/studio/VoicePanel.d.ts +15 -0
  100. package/dist/studio/VoicePanel.js +305 -0
  101. package/dist/studio/constants.d.ts +3 -0
  102. package/dist/studio/constants.js +6 -0
  103. package/dist/studio/index.d.ts +29 -0
  104. package/dist/studio/index.js +54 -0
  105. package/dist/studio/useSketchfabCapabilities.d.ts +31 -0
  106. package/dist/studio/useSketchfabCapabilities.js +82 -0
  107. package/dist/tts/useDirectVisemeStream.js +15 -10
  108. package/dist/utils/avatarUtils.js +92 -5
  109. package/dist/utils/faceLandmarkerToShapeWeights.js +2 -4
  110. package/dist/voice/useAudioPlayer.js +17 -4
  111. package/dist/voice/useVoicePreview.js +4 -2
  112. package/dist/wardrobe/index.d.ts +1 -0
  113. package/dist/wardrobe/index.js +6 -1
  114. package/dist/wardrobe/useAccessoryGestures.d.ts +20 -0
  115. package/dist/wardrobe/useAccessoryGestures.js +94 -0
  116. package/dist/wardrobe/useAvatarWardrobeHydration.js +8 -2
  117. package/dist/wardrobe/useStudioAvatar.js +11 -2
  118. package/dist/wardrobe/wardrobeStore.d.ts +2 -0
  119. package/dist/wardrobe/wardrobeStore.js +12 -2
  120. package/dist/wgpu/R3FWebGpuCanvas.d.ts +15 -0
  121. package/dist/wgpu/R3FWebGpuCanvas.js +176 -0
  122. package/dist/wgpu/WgpuAvatar.d.ts +26 -2
  123. package/dist/wgpu/WgpuAvatar.js +296 -39
  124. package/dist/wgpu/accessoryDefaults.d.ts +12 -0
  125. package/dist/wgpu/accessoryDefaults.js +19 -0
  126. package/dist/wgpu/blobShim.d.ts +2 -0
  127. package/dist/wgpu/blobShim.js +191 -0
  128. package/dist/wgpu/index.d.ts +1 -0
  129. package/dist/wgpu/index.js +4 -1
  130. package/dist/wgpu/loadGLTFFromUri.d.ts +2 -0
  131. package/dist/wgpu/loadGLTFFromUri.js +75 -0
  132. package/dist/wgpu/morphTables.js +21 -10
  133. package/dist/wgpu/motionState.d.ts +20 -0
  134. package/dist/wgpu/motionState.js +31 -0
  135. package/dist/wgpu/patchThreeForRN.d.ts +28 -0
  136. package/dist/wgpu/patchThreeForRN.js +292 -0
  137. package/dist/wgpu/scenePlacement.d.ts +5 -0
  138. package/dist/wgpu/scenePlacement.js +50 -0
  139. package/dist/wgpu/useAuthedModelUri.js +4 -2
  140. package/dist/wgpu/useNativeGLTF.d.ts +7 -0
  141. package/dist/wgpu/useNativeGLTF.js +36 -0
  142. package/package.json +97 -31
@@ -6,6 +6,7 @@ const react_1 = require("react");
6
6
  const react_native_1 = require("react-native");
7
7
  const react_native_webview_1 = require("react-native-webview");
8
8
  const html_1 = require("./html");
9
+ const shouldLogDebugMessages = __DEV__;
9
10
  exports.TalkingHead = (0, react_1.forwardRef)(({ avatarUrl, authToken, mood = 'neutral', cameraView = 'upper', cameraDistance = -0.5, hairColor, skinColor, eyeColor, accessories, onLoadingChange, onReady, onError, onAvatarState, onVoiceMood, style, vendorBaseUrl, }, ref) => {
10
11
  const webViewRef = (0, react_1.useRef)(null);
11
12
  const readyRef = (0, react_1.useRef)(false);
@@ -129,6 +130,14 @@ exports.TalkingHead = (0, react_1.forwardRef)(({ avatarUrl, authToken, mood = 'n
129
130
  }
130
131
  },
131
132
  dispatchMotion: (name) => post({ type: 'motion', name }),
133
+ stopMotion: () => post({ type: 'stop_motion' }),
134
+ playGesture: (name, opts) => post({ type: 'gesture', name, dur: opts?.dur, mirror: opts?.mirror, ms: opts?.ms }),
135
+ stopGesture: (ms) => post({ type: 'stop_gesture', ms }),
136
+ playPose: (url, dur) => post({ type: 'pose', url, dur }),
137
+ stopPose: () => post({ type: 'stop_pose' }),
138
+ playAnimation: (url, opts) => post({ type: 'animation', url, dur: opts?.dur, index: opts?.index }),
139
+ stopAnimation: () => post({ type: 'stop_animation' }),
140
+ lookAt: (x, y, ms) => post({ type: 'look_at', x, y, ms }),
132
141
  }), [post]);
133
142
  // Sync mood via postMessage only — never causes a WebView reload
134
143
  (0, react_1.useEffect)(() => {
@@ -231,7 +240,7 @@ exports.TalkingHead = (0, react_1.forwardRef)(({ avatarUrl, authToken, mood = 'n
231
240
  else if (msg.type === 'voiceMood') {
232
241
  onVoiceMoodRef.current?.(msg.mood);
233
242
  }
234
- else if (msg.type === 'log') {
243
+ else if (msg.type === 'log' && shouldLogDebugMessages) {
235
244
  console.log('[TalkingHead]', msg.message);
236
245
  }
237
246
  }
@@ -249,7 +258,17 @@ exports.TalkingHead = (0, react_1.forwardRef)(({ avatarUrl, authToken, mood = 'n
249
258
  const { statusCode, description, url } = event.nativeEvent;
250
259
  onErrorRef.current?.(`[http ${statusCode}] ${description || url || 'Avatar request failed'}`);
251
260
  }, []);
252
- return ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: [styles.container, style], children: (0, jsx_runtime_1.jsx)(react_native_webview_1.WebView, { ref: webViewRef, source: { html, baseUrl: webViewBaseUrl }, style: styles.webview, javaScriptEnabled: true, domStorageEnabled: true, allowsInlineMediaPlayback: true, mediaPlaybackRequiresUserAction: false, onMessage: onMessage, onError: handleWebViewError, onHttpError: handleWebViewHttpError, onLoadStart: () => console.log('[TalkingHead] WebView load start'), onLoadEnd: () => console.log('[TalkingHead] WebView load end'), onLoadProgress: (event) => console.log('[TalkingHead] WebView progress', event.nativeEvent.progress), originWhitelist: ['*'], allowFileAccess: true, allowFileAccessFromFileURLs: true, allowUniversalAccessFromFileURLs: true, mixedContentMode: "always" }, webViewKey) }));
261
+ return ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: [styles.container, style], children: (0, jsx_runtime_1.jsx)(react_native_webview_1.WebView, { ref: webViewRef, source: { html, baseUrl: webViewBaseUrl }, style: styles.webview, javaScriptEnabled: true, domStorageEnabled: true, allowsInlineMediaPlayback: true, mediaPlaybackRequiresUserAction: false, onMessage: onMessage, onError: handleWebViewError, onHttpError: handleWebViewHttpError, onLoadStart: () => {
262
+ if (shouldLogDebugMessages)
263
+ console.log('[TalkingHead] WebView load start');
264
+ }, onLoadEnd: () => {
265
+ if (shouldLogDebugMessages)
266
+ console.log('[TalkingHead] WebView load end');
267
+ }, onLoadProgress: (event) => {
268
+ if (shouldLogDebugMessages) {
269
+ console.log('[TalkingHead] WebView progress', event.nativeEvent.progress);
270
+ }
271
+ }, originWhitelist: ['*'], allowFileAccess: true, allowFileAccessFromFileURLs: true, allowUniversalAccessFromFileURLs: true, mixedContentMode: "always" }, webViewKey) }));
253
272
  });
254
273
  exports.TalkingHead.displayName = 'TalkingHead';
255
274
  const styles = react_native_1.StyleSheet.create({
@@ -1,7 +1,8 @@
1
1
  import React from 'react';
2
- import type { TalkingHeadLoadingState, TalkingHeadVisemeCue, TalkingHeadVisemeSchedule } from './TalkingHead';
3
- export type { TalkingHeadLoadingState, TalkingHeadVisemeCue, TalkingHeadVisemeSchedule };
4
- export type TalkingHeadMood = 'neutral' | 'happy' | 'sad' | 'angry' | 'excited' | 'thinking' | 'concerned' | 'surprised';
2
+ import type { TalkingHeadLoadingState, TalkingHeadLoadingStage, TalkingHeadViseme, TalkingHeadVisemeCue, TalkingHeadVisemeSchedule } from './TalkingHead';
3
+ import type { MotionKey, TalkingHeadGesture, TalkingHeadPose } from './core/avatar/motion';
4
+ export type { TalkingHeadLoadingStage, TalkingHeadLoadingState, TalkingHeadViseme, TalkingHeadVisemeCue, TalkingHeadVisemeSchedule, };
5
+ export type TalkingHeadMood = 'neutral' | 'happy' | 'sad' | 'angry' | 'fear' | 'disgust' | 'love' | 'sleep' | 'excited' | 'thinking' | 'concerned' | 'surprised';
5
6
  export interface TalkingHeadAccessory {
6
7
  id: string;
7
8
  url: string;
@@ -31,6 +32,7 @@ export type TalkingHeadPropsAlias = TalkingHeadProps;
31
32
  export type AvatarPlayerProps = TalkingHeadProps;
32
33
  export interface TalkingHeadRef {
33
34
  sendAmplitude: (amplitude: number) => void;
35
+ sendViseme: (viseme: TalkingHeadViseme, weight?: number) => void;
34
36
  scheduleVisemes: (schedule: TalkingHeadVisemeSchedule) => void;
35
37
  clearVisemes: () => void;
36
38
  setMood: (mood: TalkingHeadMood) => void;
@@ -38,7 +40,32 @@ export interface TalkingHeadRef {
38
40
  setSkinColor: (color: string) => void;
39
41
  setEyeColor: (color: string) => void;
40
42
  setAccessories: (accessories: TalkingHeadAccessory[]) => void;
41
- dispatchMotion: (name: string) => void;
43
+ /** Play a procedural motion (e.g. 'attack', 'defend', 'groove'). */
44
+ dispatchMotion(name: MotionKey): void;
45
+ dispatchMotion(name: string): void;
46
+ /** Stop the current procedural motion and return to rest. */
47
+ stopMotion: () => void;
48
+ /** Play an upstream TalkingHead hand gesture (e.g. 'thumbup'). */
49
+ playGesture: (name: TalkingHeadGesture | string, opts?: {
50
+ dur?: number;
51
+ mirror?: boolean;
52
+ ms?: number;
53
+ }) => void;
54
+ /** Stop the current gesture, easing out over `ms`. */
55
+ stopGesture: (ms?: number) => void;
56
+ /** Strike a pose — a built-in template name (e.g. 'oneknee') or a pose-file URL. */
57
+ playPose: (urlOrTemplate: TalkingHeadPose | string, dur?: number) => void;
58
+ /** Release the current pose and return to the default stance. */
59
+ stopPose: () => void;
60
+ /** Play a full body animation from a GLB/FBX URL (e.g. a combat move). */
61
+ playAnimation: (url: string, opts?: {
62
+ dur?: number;
63
+ index?: number;
64
+ }) => void;
65
+ /** Stop the current body animation. */
66
+ stopAnimation: () => void;
67
+ /** Turn head/eyes toward viewport coordinates (px), easing over `ms`. */
68
+ lookAt: (x: number, y: number, ms?: number) => void;
42
69
  }
43
70
  /** @deprecated Use AvatarPlayerRef */
44
71
  export type TalkingHeadRefAlias = TalkingHeadRef;
@@ -37,6 +37,7 @@ exports.TalkingHead = void 0;
37
37
  const jsx_runtime_1 = require("react/jsx-runtime");
38
38
  const react_1 = __importStar(require("react"));
39
39
  const html_1 = require("./html");
40
+ const shouldLogDebugMessages = typeof process !== 'undefined' && process.env.NODE_ENV !== 'production';
40
41
  const containerStyle = {
41
42
  overflow: 'hidden',
42
43
  borderRadius: 12,
@@ -79,6 +80,7 @@ exports.TalkingHead = (0, react_1.forwardRef)(({ avatarUrl, authToken, mood = 'n
79
80
  }, []);
80
81
  (0, react_1.useImperativeHandle)(ref, () => ({
81
82
  sendAmplitude: (amplitude) => post({ type: 'amplitude', value: amplitude }),
83
+ sendViseme: (viseme, weight = 1.0) => post({ type: 'viseme', viseme, weight }),
82
84
  scheduleVisemes: (schedule) => post({ type: 'schedule_visemes', schedule }),
83
85
  clearVisemes: () => post({ type: 'clear_visemes' }),
84
86
  setMood: (nextMood) => {
@@ -108,6 +110,14 @@ exports.TalkingHead = (0, react_1.forwardRef)(({ avatarUrl, authToken, mood = 'n
108
110
  }
109
111
  },
110
112
  dispatchMotion: (name) => post({ type: 'motion', name }),
113
+ stopMotion: () => post({ type: 'stop_motion' }),
114
+ playGesture: (name, opts) => post({ type: 'gesture', name, dur: opts?.dur, mirror: opts?.mirror, ms: opts?.ms }),
115
+ stopGesture: (ms) => post({ type: 'stop_gesture', ms }),
116
+ playPose: (url, dur) => post({ type: 'pose', url, dur }),
117
+ stopPose: () => post({ type: 'stop_pose' }),
118
+ playAnimation: (url, opts) => post({ type: 'animation', url, dur: opts?.dur, index: opts?.index }),
119
+ stopAnimation: () => post({ type: 'stop_animation' }),
120
+ lookAt: (x, y, ms) => post({ type: 'look_at', x, y, ms }),
111
121
  }), [post]);
112
122
  (0, react_1.useEffect)(() => {
113
123
  pendingMoodRef.current = mood;
@@ -202,7 +212,7 @@ exports.TalkingHead = (0, react_1.forwardRef)(({ avatarUrl, authToken, mood = 'n
202
212
  else if (msg.type === 'avatarState') {
203
213
  onAvatarStateRef.current?.(msg.state);
204
214
  }
205
- else if (msg.type === 'log') {
215
+ else if (msg.type === 'log' && shouldLogDebugMessages) {
206
216
  console.log('[TalkingHead]', msg.message);
207
217
  }
208
218
  }
@@ -1,3 +1,4 @@
1
+ import './wgpu/blobShim';
1
2
  import React from 'react';
2
3
  import { ViewStyle, StyleProp } from 'react-native';
3
4
  import { type TalkingHeadMood, type TalkingHeadAccessory, type TalkingHeadViseme, type TalkingHeadVisemeSchedule } from './TalkingHead';
@@ -27,6 +28,27 @@ export interface TalkingHeadVisualizationRef {
27
28
  clearVisemes: () => void;
28
29
  /** Trigger a named motion on the avatar (e.g. 'celebrate', 'groove', 'wave') */
29
30
  playMotion: (name: string) => void;
31
+ /** Stop the current procedural motion and return to rest. */
32
+ stopMotion: () => void;
33
+ /** Play an upstream TalkingHead gesture or native procedural fallback. */
34
+ playGesture: (name: string, opts?: {
35
+ dur?: number;
36
+ mirror?: boolean;
37
+ ms?: number;
38
+ }) => void;
39
+ /** Stop the current gesture. */
40
+ stopGesture: (ms?: number) => void;
41
+ /** Strike a pose by built-in name or pose URL. */
42
+ playPose: (name: string, dur?: number) => void;
43
+ /** Release the current pose and return to rest. */
44
+ stopPose: () => void;
45
+ /** Play a hosted animation clip on renderers that support it. */
46
+ playAnimation: (url: string, opts?: {
47
+ dur?: number;
48
+ index?: number;
49
+ }) => void;
50
+ /** Stop the current hosted animation clip. */
51
+ stopAnimation: () => void;
30
52
  }
31
53
  /**
32
54
  * TalkingHeadVisualization — optimized component for rendering the 3D avatar.
@@ -2,6 +2,9 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.TalkingHeadVisualization = void 0;
4
4
  const jsx_runtime_1 = require("react/jsx-runtime");
5
+ // IMPORTANT: blobShim must be first — installs Blob+createObjectURL patch
6
+ // before @react-three/fiber's polyfills() runs.
7
+ require("./wgpu/blobShim");
5
8
  /* eslint-disable @typescript-eslint/no-unused-expressions */
6
9
  const react_1 = require("react");
7
10
  const react_native_1 = require("react-native");
@@ -60,7 +63,7 @@ exports.TalkingHeadVisualization = (0, react_1.forwardRef)(({ style, avatarUrl,
60
63
  setFallbackUrl(_fallbackDataUri);
61
64
  return;
62
65
  }
63
- getFallbackAvatarUrl().then((u) => u && setFallbackUrl(u));
66
+ void getFallbackAvatarUrl().then((u) => u && setFallbackUrl(u));
64
67
  }, []);
65
68
  const [mood, setMood] = (0, react_1.useState)(initialMood);
66
69
  const [isAvatarReady, setIsAvatarReady] = (0, react_1.useState)(false);
@@ -98,7 +101,7 @@ exports.TalkingHeadVisualization = (0, react_1.forwardRef)(({ style, avatarUrl,
98
101
  cues: pendingSchedule.cues.length,
99
102
  ageMs,
100
103
  });
101
- activeAvatar()?.scheduleVisemes(pendingSchedule);
104
+ activeAvatar()?.scheduleVisemes?.(pendingSchedule);
102
105
  onVisemeScheduleApplied?.({
103
106
  requestId: pendingSchedule.requestId ?? null,
104
107
  appliedAtMs: Date.now(),
@@ -108,10 +111,24 @@ exports.TalkingHeadVisualization = (0, react_1.forwardRef)(({ style, avatarUrl,
108
111
  }, [onVisemeScheduleApplied, activeAvatar]);
109
112
  (0, react_1.useImperativeHandle)(ref, () => ({
110
113
  setMood: (m) => setMood(m),
111
- sendAmplitude: (a) => activeAvatar()?.sendAmplitude(a),
112
- sendViseme: (viseme, weight) => activeAvatar()?.sendViseme(viseme, weight),
113
- clearVisemes: () => activeAvatar()?.clearVisemes(),
114
- playMotion: (name) => avatarRef.current?.dispatchMotion(name),
114
+ sendAmplitude: (a) => activeAvatar()?.sendAmplitude?.(a),
115
+ sendViseme: (viseme, weight) => activeAvatar()?.sendViseme?.(viseme, weight),
116
+ clearVisemes: () => activeAvatar()?.clearVisemes?.(),
117
+ playMotion: (name) => {
118
+ const avatar = activeAvatar();
119
+ if (avatar?.playMotion) {
120
+ avatar.playMotion(name);
121
+ return;
122
+ }
123
+ avatar?.dispatchMotion?.(name);
124
+ },
125
+ stopMotion: () => activeAvatar()?.stopMotion?.(),
126
+ playGesture: (name, opts) => activeAvatar()?.playGesture?.(name, opts),
127
+ stopGesture: (ms) => activeAvatar()?.stopGesture?.(ms),
128
+ playPose: (name, dur) => activeAvatar()?.playPose?.(name, dur),
129
+ stopPose: () => activeAvatar()?.stopPose?.(),
130
+ playAnimation: (url, opts) => activeAvatar()?.playAnimation?.(url, opts),
131
+ stopAnimation: () => activeAvatar()?.stopAnimation?.(),
115
132
  scheduleVisemes: (schedule) => {
116
133
  const scheduleKey = `${schedule.requestId ?? 'anonymous'}:${schedule.startedAtMs ?? 0}`;
117
134
  if (lastScheduledVisemeKeyRef.current === scheduleKey)
@@ -119,7 +136,7 @@ exports.TalkingHeadVisualization = (0, react_1.forwardRef)(({ style, avatarUrl,
119
136
  const av = activeAvatar();
120
137
  // WgpuAvatar buffers pending schedules internally — no ready gate needed.
121
138
  // WebView (avatarRef) still needs the ready gate.
122
- if (!av || (!wgpuRef.current && !isAvatarReady)) {
139
+ if (!av?.scheduleVisemes || (!wgpuRef.current && !isAvatarReady)) {
123
140
  pendingVisemeScheduleRef.current = schedule;
124
141
  return;
125
142
  }
@@ -140,8 +157,11 @@ exports.TalkingHeadVisualization = (0, react_1.forwardRef)(({ style, avatarUrl,
140
157
  lastScheduledVisemeKeyRef.current = null;
141
158
  }, [avatarUrl]);
142
159
  const handleError = (0, react_1.useCallback)((message) => {
143
- console.warn('[TalkingHeadVisualization] Avatar load failed, switching to fallback:', message);
160
+ console.warn('[TalkingHeadVisualization] Avatar error:', message);
144
161
  setAvatarError(message);
162
+ // Only fall back to local GLB for hard load failures, not morph-target warnings
163
+ if (message.includes('morph') || message.includes('No mesh'))
164
+ return;
145
165
  setUseFallback(true);
146
166
  }, []);
147
167
  // Effective avatar URL: use remote if available, fallback GLB if error or missing
@@ -169,7 +189,7 @@ exports.TalkingHeadVisualization = (0, react_1.forwardRef)(({ style, avatarUrl,
169
189
  ready: isAvatarReady,
170
190
  });
171
191
  const av = activeAvatar();
172
- if (!av || (!wgpuRef.current && !isAvatarReady)) {
192
+ if (!av?.scheduleVisemes || (!wgpuRef.current && !isAvatarReady)) {
173
193
  pendingVisemeScheduleRef.current = visemeSchedule;
174
194
  return;
175
195
  }
@@ -193,7 +213,7 @@ exports.TalkingHeadVisualization = (0, react_1.forwardRef)(({ style, avatarUrl,
193
213
  if (!effectiveAvatarUrl) {
194
214
  return ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: [style, styles.placeholder] }));
195
215
  }
196
- return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [style, styles.container], pointerEvents: "box-none", children: [(0, jsx_runtime_1.jsx)(TalkingHead_1.TalkingHead, { ref: avatarRef, avatarUrl: effectiveAvatarUrl, authToken: authToken, cameraView: cameraView, cameraDistance: cameraDistance, accessories: accessories, mood: mood, onLoadingChange: handleLoadingChange, onReady: handleReady, onError: handleError, onVoiceMood: onVoiceMood, style: react_native_1.StyleSheet.absoluteFill, vendorBaseUrl: vendorBaseUrl }), !isAvatarReady && ((0, jsx_runtime_1.jsx)(react_native_1.View, { testID: "talking-head-loading", style: styles.loadingOverlay, pointerEvents: "none", children: (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.loadingCard, children: [(0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.loadingBadge, children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.loadingBadgeText, children: "AVATAR" }) }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.loadingTitle, children: avatarError ? 'Avatar failed to load' : getLoadingLabel(loadingState.stage) }), avatarError ? ((0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.loadingHint, children: avatarError })) : typeof loadingState.progress === 'number' ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)(react_native_1.Text, { style: styles.loadingPercent, children: [loadingState.progress, "%"] }), (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.progressTrack, children: [(0, jsx_runtime_1.jsx)(react_native_1.View, { style: { flex: Math.max(6, loadingState.progress ?? 0), height: '100%', borderRadius: 999, backgroundColor: '#5eead4' } }), (0, jsx_runtime_1.jsx)(react_native_1.View, { style: { flex: 100 - Math.max(6, loadingState.progress ?? 0) } })] })] })) : ((0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.loadingHint, children: "Preparing the avatar scene\u2026" }))] }) }))] }));
216
+ return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [style, styles.container], pointerEvents: "box-none", children: [(0, jsx_runtime_1.jsx)(TalkingHead_1.TalkingHead, { ref: avatarRef, avatarUrl: effectiveAvatarUrl, authToken: authToken, cameraView: cameraView, cameraDistance: cameraDistance, accessories: accessories, mood: mood, onLoadingChange: handleLoadingChange, onReady: handleReady, onError: handleError, onVoiceMood: onVoiceMood, style: react_native_1.StyleSheet.absoluteFillObject, vendorBaseUrl: vendorBaseUrl }), !isAvatarReady && ((0, jsx_runtime_1.jsx)(react_native_1.View, { testID: "talking-head-loading", style: styles.loadingOverlay, pointerEvents: "none", children: (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.loadingCard, children: [(0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.loadingBadge, children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.loadingBadgeText, children: "AVATAR" }) }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.loadingTitle, children: avatarError ? 'Avatar failed to load' : getLoadingLabel(loadingState.stage) }), avatarError ? ((0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.loadingHint, children: avatarError })) : typeof loadingState.progress === 'number' ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)(react_native_1.Text, { style: styles.loadingPercent, children: [loadingState.progress, "%"] }), (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.progressTrack, children: [(0, jsx_runtime_1.jsx)(react_native_1.View, { style: { flex: Math.max(6, loadingState.progress ?? 0), height: '100%', borderRadius: 999, backgroundColor: '#5eead4' } }), (0, jsx_runtime_1.jsx)(react_native_1.View, { style: { flex: 100 - Math.max(6, loadingState.progress ?? 0) } })] })] })) : ((0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.loadingHint, children: "Preparing the avatar scene\u2026" }))] }) }))] }));
197
217
  });
198
218
  exports.TalkingHeadVisualization.displayName = 'TalkingHeadVisualization';
199
219
  const styles = react_native_1.StyleSheet.create({
@@ -9,7 +9,18 @@ export declare function getMyAvatars(): Promise<Avatar[]>;
9
9
  export declare function getAvatar(id: string): Promise<Avatar>;
10
10
  export declare function updateAvatar(id: string, data: AvatarUpdate): Promise<Avatar>;
11
11
  export declare function deleteAvatar(id: string): Promise<void>;
12
- export declare function createAvatar(fileUri: string, name: string, description?: string): Promise<Avatar>;
12
+ export interface CreateAvatarOptions {
13
+ /** Sketchfab model uid this avatar was imported from. */
14
+ sketchfabUid?: string;
15
+ /** Whether the GLB has morph targets (enables real lipsync). */
16
+ sketchfabHasMorphs?: boolean;
17
+ }
18
+ export declare function createAvatar(fileUri: string, name: string, description?: string, options?: CreateAvatarOptions): Promise<Avatar>;
19
+ /**
20
+ * Sketchfab uids that previously imported with working morph targets.
21
+ * Used to pin "verified" models to the top of search results.
22
+ */
23
+ export declare function getVerifiedSketchfabUids(): Promise<string[]>;
13
24
  export declare function getPublicAvatars(): Promise<PublicAvatar[]>;
14
25
  export declare function getVoiceProfileSamples(profileId: string): Promise<ProfileSample[]>;
15
26
  export declare function getVoiceProfiles(): Promise<VoiceProfile[]>;
@@ -8,6 +8,7 @@ exports.getAvatar = getAvatar;
8
8
  exports.updateAvatar = updateAvatar;
9
9
  exports.deleteAvatar = deleteAvatar;
10
10
  exports.createAvatar = createAvatar;
11
+ exports.getVerifiedSketchfabUids = getVerifiedSketchfabUids;
11
12
  exports.getPublicAvatars = getPublicAvatars;
12
13
  exports.getVoiceProfileSamples = getVoiceProfileSamples;
13
14
  exports.getVoiceProfiles = getVoiceProfiles;
@@ -28,7 +29,9 @@ exports.uploadVoiceSample = uploadVoiceSample;
28
29
  // ---------------------------------------------------------------------------
29
30
  // Injectable configuration
30
31
  // ---------------------------------------------------------------------------
31
- let _baseUrl = process.env.EXPO_PUBLIC_BACKEND_URL ?? '';
32
+ let _baseUrl = typeof process !== 'undefined'
33
+ ? process.env.EXPO_PUBLIC_BACKEND_URL ?? ''
34
+ : '';
32
35
  let _getToken = null;
33
36
  function configureAvatarApi(opts) {
34
37
  if (opts.baseUrl !== undefined)
@@ -110,7 +113,7 @@ function updateAvatar(id, data) {
110
113
  function deleteAvatar(id) {
111
114
  return studioFetch(`/v1/avatars/${id}`, { method: 'DELETE' });
112
115
  }
113
- async function createAvatar(fileUri, name, description) {
116
+ async function createAvatar(fileUri, name, description, options) {
114
117
  const formData = new FormData();
115
118
  formData.append('file', {
116
119
  uri: fileUri,
@@ -121,8 +124,19 @@ async function createAvatar(fileUri, name, description) {
121
124
  if (description) {
122
125
  formData.append('description', description);
123
126
  }
127
+ if (options?.sketchfabUid) {
128
+ formData.append('sketchfab_uid', options.sketchfabUid);
129
+ formData.append('sketchfab_has_morphs', options.sketchfabHasMorphs ? 'true' : 'false');
130
+ }
124
131
  return studioFetchForm('/v1/avatars', 'POST', formData);
125
132
  }
133
+ /**
134
+ * Sketchfab uids that previously imported with working morph targets.
135
+ * Used to pin "verified" models to the top of search results.
136
+ */
137
+ function getVerifiedSketchfabUids() {
138
+ return studioFetch('/v1/sketchfab/verified').then((r) => r.uids);
139
+ }
126
140
  function getPublicAvatars() {
127
141
  return studioFetch('/v1/avatars/public');
128
142
  }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Stable avatar contract entrypoint for SiteBay clients.
3
+ *
4
+ * Runtime packages should import avatar control, viseme, and backend surface
5
+ * types from `talking-head-studio/contract` instead of reaching into internal
6
+ * `src/core/avatar/*` paths.
7
+ */
8
+ export type { AgentVisemePayload, ArkitBlendShape, EngineVisemeMappingArtifact, OculusViseme, OculusVisemeWeights, RhubarbViseme, VisemeCue, } from './core/avatar/visemes';
9
+ export { ARKIT_TO_OCULUS, ENGINE_VISEME_MAPPING, OCULUS_MORPH_TARGET_NAMES, OCULUS_VISEMES, RHUBARB_TO_OCULUS, RHUBARB_VISEMES, getArkitWeightsForViseme, oculusWeightsForRhubarbCue, remapArkitToOculus, } from './core/avatar/visemes';
10
+ export type { EyeGaze, ExpressionState, FaceControl, HeadPose, Viseme, } from './core/avatar/faceControls';
11
+ export { applyVisemeToExpression, createNeutralExpression, visemeToExpression, } from './core/avatar/faceControls';
12
+ export type { AvatarBackend, AvatarRenderTarget, CalibrationProfile, } from './core/avatar/backend';
13
+ export { MOTION_DEFS, MOTION_KEYS, isMotionKey, TALKINGHEAD_GESTURES, TALKINGHEAD_POSES, } from './core/avatar/motion';
14
+ export type { MotionKey, MotionDef, MotionDefWithBones, MotionBoneKey, MotionOscillator, TalkingHeadGesture, TalkingHeadPose, } from './core/avatar/motion';
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ /**
3
+ * Stable avatar contract entrypoint for SiteBay clients.
4
+ *
5
+ * Runtime packages should import avatar control, viseme, and backend surface
6
+ * types from `talking-head-studio/contract` instead of reaching into internal
7
+ * `src/core/avatar/*` paths.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.TALKINGHEAD_POSES = exports.TALKINGHEAD_GESTURES = exports.isMotionKey = exports.MOTION_KEYS = exports.MOTION_DEFS = exports.visemeToExpression = exports.createNeutralExpression = exports.applyVisemeToExpression = exports.remapArkitToOculus = exports.oculusWeightsForRhubarbCue = exports.getArkitWeightsForViseme = exports.RHUBARB_VISEMES = exports.RHUBARB_TO_OCULUS = exports.OCULUS_VISEMES = exports.OCULUS_MORPH_TARGET_NAMES = exports.ENGINE_VISEME_MAPPING = exports.ARKIT_TO_OCULUS = void 0;
11
+ var visemes_1 = require("./core/avatar/visemes");
12
+ Object.defineProperty(exports, "ARKIT_TO_OCULUS", { enumerable: true, get: function () { return visemes_1.ARKIT_TO_OCULUS; } });
13
+ Object.defineProperty(exports, "ENGINE_VISEME_MAPPING", { enumerable: true, get: function () { return visemes_1.ENGINE_VISEME_MAPPING; } });
14
+ Object.defineProperty(exports, "OCULUS_MORPH_TARGET_NAMES", { enumerable: true, get: function () { return visemes_1.OCULUS_MORPH_TARGET_NAMES; } });
15
+ Object.defineProperty(exports, "OCULUS_VISEMES", { enumerable: true, get: function () { return visemes_1.OCULUS_VISEMES; } });
16
+ Object.defineProperty(exports, "RHUBARB_TO_OCULUS", { enumerable: true, get: function () { return visemes_1.RHUBARB_TO_OCULUS; } });
17
+ Object.defineProperty(exports, "RHUBARB_VISEMES", { enumerable: true, get: function () { return visemes_1.RHUBARB_VISEMES; } });
18
+ Object.defineProperty(exports, "getArkitWeightsForViseme", { enumerable: true, get: function () { return visemes_1.getArkitWeightsForViseme; } });
19
+ Object.defineProperty(exports, "oculusWeightsForRhubarbCue", { enumerable: true, get: function () { return visemes_1.oculusWeightsForRhubarbCue; } });
20
+ Object.defineProperty(exports, "remapArkitToOculus", { enumerable: true, get: function () { return visemes_1.remapArkitToOculus; } });
21
+ var faceControls_1 = require("./core/avatar/faceControls");
22
+ Object.defineProperty(exports, "applyVisemeToExpression", { enumerable: true, get: function () { return faceControls_1.applyVisemeToExpression; } });
23
+ Object.defineProperty(exports, "createNeutralExpression", { enumerable: true, get: function () { return faceControls_1.createNeutralExpression; } });
24
+ Object.defineProperty(exports, "visemeToExpression", { enumerable: true, get: function () { return faceControls_1.visemeToExpression; } });
25
+ var motion_1 = require("./core/avatar/motion");
26
+ Object.defineProperty(exports, "MOTION_DEFS", { enumerable: true, get: function () { return motion_1.MOTION_DEFS; } });
27
+ Object.defineProperty(exports, "MOTION_KEYS", { enumerable: true, get: function () { return motion_1.MOTION_KEYS; } });
28
+ Object.defineProperty(exports, "isMotionKey", { enumerable: true, get: function () { return motion_1.isMotionKey; } });
29
+ Object.defineProperty(exports, "TALKINGHEAD_GESTURES", { enumerable: true, get: function () { return motion_1.TALKINGHEAD_GESTURES; } });
30
+ Object.defineProperty(exports, "TALKINGHEAD_POSES", { enumerable: true, get: function () { return motion_1.TALKINGHEAD_POSES; } });
@@ -0,0 +1,60 @@
1
+ import { type AvatarSchemaReport, type VisemeTier } from './schema';
2
+ import { type MotionBoneKey } from './motion';
3
+ export interface MotionCoverage {
4
+ /** Motion-engine bone keys this rig can drive (head, spine, arms, …). */
5
+ matched: MotionBoneKey[];
6
+ /** Motion-engine bone keys with no matching bone. */
7
+ missing: MotionBoneKey[];
8
+ /** Enough of the body is rigged for the procedural motion engine to read as
9
+ * "moving" (spine + head + at least one arm). */
10
+ ready: boolean;
11
+ }
12
+ export interface AvatarCapabilities {
13
+ /** Full schema report (visemes, skeleton, mesh) from walkAvatarSchema. */
14
+ schema: AvatarSchemaReport;
15
+ /** Best lip-sync strategy: oculus / arkit / minimal / none. */
16
+ lipSyncTier: VisemeTier;
17
+ /** Can this avatar lip-sync from a viseme schedule (morph-driven)? */
18
+ canLipSync: boolean;
19
+ /** Can the talking-head motion engine give this avatar body motion/gestures? */
20
+ canMove: boolean;
21
+ /** Procedural-motion bone coverage detail. */
22
+ motion: MotionCoverage;
23
+ /** Baked animation clip count (informational; the motion engine is procedural). */
24
+ animations: number;
25
+ }
26
+ type GltfJson = {
27
+ nodes?: {
28
+ name?: string;
29
+ }[];
30
+ meshes?: {
31
+ primitives?: {
32
+ indices?: number;
33
+ attributes?: Record<string, number>;
34
+ targets?: unknown[];
35
+ }[];
36
+ extras?: {
37
+ targetNames?: string[];
38
+ };
39
+ }[];
40
+ skins?: {
41
+ joints?: number[];
42
+ }[];
43
+ accessors?: {
44
+ count?: number;
45
+ }[];
46
+ animations?: unknown[];
47
+ };
48
+ /** Decode a .glb (binary) or .gltf (JSON) ArrayBuffer/Uint8Array into its glTF JSON. */
49
+ export declare function parseGlbJson(data: ArrayBuffer | Uint8Array): GltfJson;
50
+ /** Capability report from glTF JSON (the unit-testable core). */
51
+ export declare function inspectAvatarGltf(json: GltfJson): AvatarCapabilities;
52
+ /** Capability report from raw GLB/glTF bytes (browser: fetch(url).arrayBuffer()). */
53
+ export declare function inspectAvatarCapabilities(data: ArrayBuffer | Uint8Array): AvatarCapabilities;
54
+ /**
55
+ * Capability report from a GLB's raw JSON-chunk bytes — for the streaming
56
+ * import path that reads only the header + JSON chunk off disk (mirrors
57
+ * sketchfab's inspectGlbJsonChunk, but for full avatar capabilities).
58
+ */
59
+ export declare function inspectAvatarJsonChunk(jsonBytes: Uint8Array): AvatarCapabilities;
60
+ export {};
@@ -0,0 +1,100 @@
1
+ "use strict";
2
+ // src/core/avatar/glbInspect.ts
3
+ // Pre-flight avatar capability check from raw GLB/glTF bytes — no Three.js, no
4
+ // WebGL load required. Reuses walkAvatarSchema() (the single source of truth for
5
+ // viseme tier + humanoid rig) by adapting the glTF JSON into the minimal scene
6
+ // that walker expects, then layers on motion-engine bone coverage.
7
+ //
8
+ // The avatar creator runs this when a user picks/uploads/URLs an avatar, so it
9
+ // can soft-warn ("this model can't lip-sync / won't gesture") before committing.
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.parseGlbJson = parseGlbJson;
12
+ exports.inspectAvatarGltf = inspectAvatarGltf;
13
+ exports.inspectAvatarCapabilities = inspectAvatarCapabilities;
14
+ exports.inspectAvatarJsonChunk = inspectAvatarJsonChunk;
15
+ const schema_1 = require("./schema");
16
+ const motion_1 = require("./motion");
17
+ /** Decode a .glb (binary) or .gltf (JSON) ArrayBuffer/Uint8Array into its glTF JSON. */
18
+ function parseGlbJson(data) {
19
+ const bytes = data instanceof Uint8Array ? data : new Uint8Array(data);
20
+ // GLB: 'glTF' magic (0x46546C67), then 12-byte header + JSON chunk.
21
+ if (bytes.length >= 12 && bytes[0] === 0x67 && bytes[1] === 0x6c && bytes[2] === 0x54 && bytes[3] === 0x46) {
22
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
23
+ const jsonLen = view.getUint32(12, true); // first chunk length (JSON)
24
+ const jsonBytes = bytes.subarray(20, 20 + jsonLen);
25
+ return JSON.parse(new TextDecoder().decode(jsonBytes));
26
+ }
27
+ // Otherwise assume .gltf JSON text.
28
+ return JSON.parse(new TextDecoder().decode(bytes));
29
+ }
30
+ const norm = (s) => s.toLowerCase().replace(/[_\s\-.]/g, '');
31
+ /** Capability report from glTF JSON (the unit-testable core). */
32
+ function inspectAvatarGltf(json) {
33
+ const nodes = json.nodes ?? [];
34
+ const meshes = json.meshes ?? [];
35
+ const skins = json.skins ?? [];
36
+ const accessors = json.accessors ?? [];
37
+ // Adapt the glTF JSON into the minimal { traverse } scene walkAvatarSchema wants.
38
+ const objects = [];
39
+ for (const skin of skins) {
40
+ const bones = (skin.joints ?? []).map((j) => ({ name: nodes[j]?.name }));
41
+ objects.push({ isSkinnedMesh: true, skeleton: { bones } });
42
+ for (const j of skin.joints ?? []) {
43
+ const nm = nodes[j]?.name;
44
+ if (nm)
45
+ objects.push({ isBone: true, name: nm });
46
+ }
47
+ }
48
+ for (const mesh of meshes) {
49
+ const targetNames = mesh.extras?.targetNames ?? [];
50
+ const dict = targetNames.length ? Object.fromEntries(targetNames.map((n, i) => [String(n), i])) : undefined;
51
+ let idxCount = 0;
52
+ let posCount = 0;
53
+ let hasIdx = false;
54
+ for (const prim of mesh.primitives ?? []) {
55
+ if (prim.indices != null && accessors[prim.indices]) {
56
+ idxCount += accessors[prim.indices].count ?? 0;
57
+ hasIdx = true;
58
+ }
59
+ const posIdx = prim.attributes?.POSITION;
60
+ if (posIdx != null && accessors[posIdx])
61
+ posCount += accessors[posIdx].count ?? 0;
62
+ }
63
+ objects.push({
64
+ isMesh: true,
65
+ ...(dict ? { morphTargetDictionary: dict } : {}),
66
+ geometry: hasIdx ? { index: { count: idxCount } } : { attributes: { position: { count: posCount } } },
67
+ });
68
+ }
69
+ const schema = (0, schema_1.walkAvatarSchema)({ traverse: (cb) => objects.forEach((o) => cb(o)) });
70
+ // Motion-engine coverage: which logical bones the procedural engine can drive.
71
+ const bonesNorm = schema.skeleton.bones.map(norm);
72
+ const matched = [];
73
+ const missing = [];
74
+ for (const [key, kws] of Object.entries(motion_1.MOTION_BONE_SEARCH)) {
75
+ const hit = kws.some((kw) => bonesNorm.some((b) => b.includes(norm(kw))));
76
+ (hit ? matched : missing).push(key);
77
+ }
78
+ const has = (k) => matched.includes(k);
79
+ const ready = schema.skeleton.hasRig && has('spine') && has('head') && (has('leftArm') || has('rightArm'));
80
+ return {
81
+ schema,
82
+ lipSyncTier: schema.morphs.visemeTier,
83
+ canLipSync: schema.morphs.visemeTier !== 'none',
84
+ canMove: ready,
85
+ motion: { matched, missing, ready },
86
+ animations: (json.animations ?? []).length,
87
+ };
88
+ }
89
+ /** Capability report from raw GLB/glTF bytes (browser: fetch(url).arrayBuffer()). */
90
+ function inspectAvatarCapabilities(data) {
91
+ return inspectAvatarGltf(parseGlbJson(data));
92
+ }
93
+ /**
94
+ * Capability report from a GLB's raw JSON-chunk bytes — for the streaming
95
+ * import path that reads only the header + JSON chunk off disk (mirrors
96
+ * sketchfab's inspectGlbJsonChunk, but for full avatar capabilities).
97
+ */
98
+ function inspectAvatarJsonChunk(jsonBytes) {
99
+ return inspectAvatarGltf(JSON.parse(new TextDecoder().decode(jsonBytes)));
100
+ }
@@ -142,10 +142,12 @@ class GaussianBackend {
142
142
  this.boneMap.clear();
143
143
  const allBones = new Map();
144
144
  this.scene.traverse((obj) => {
145
- if (obj.isBone)
146
- allBones.set(obj.name, obj);
147
- if (obj.isSkinnedMesh && obj.skeleton?.bones) {
148
- for (const b of obj.skeleton.bones) {
145
+ const bone = obj;
146
+ if (bone.isBone)
147
+ allBones.set(bone.name, bone);
148
+ const skinned = obj;
149
+ if (skinned.isSkinnedMesh && skinned.skeleton?.bones) {
150
+ for (const b of skinned.skeleton.bones) {
149
151
  allBones.set(b.name, b);
150
152
  }
151
153
  }