@sage-rsc/talking-head-react 1.3.3 → 1.3.5

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sage-rsc/talking-head-react",
3
- "version": "1.3.3",
3
+ "version": "1.3.5",
4
4
  "description": "A reusable React component for 3D talking avatars with lip-sync and text-to-speech",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.js",
@@ -20,6 +20,7 @@ const outputFile = path.join(animationsDir, 'manifest.json');
20
20
 
21
21
  function scanDirectory(dir) {
22
22
  const groups = {};
23
+ const genderGroups = { male: {}, female: {}, shared: {} };
23
24
 
24
25
  if (!fs.existsSync(dir)) {
25
26
  console.error(`Directory does not exist: ${dir}`);
@@ -28,29 +29,101 @@ function scanDirectory(dir) {
28
29
 
29
30
  const items = fs.readdirSync(dir, { withFileTypes: true });
30
31
 
31
- for (const item of items) {
32
- const fullPath = path.join(dir, item.name);
33
-
34
- if (item.isDirectory()) {
35
- // Scan subdirectory for FBX files
36
- const subFiles = fs.readdirSync(fullPath)
37
- .filter(file => file.toLowerCase().endsWith('.fbx'))
38
- .map(file => `/animations/${item.name}/${file}`);
32
+ // First pass: Check for gender-specific top-level directories
33
+ const hasGenderDirs = items.some(item => {
34
+ if (!item.isDirectory()) return false;
35
+ const name = item.name.toLowerCase();
36
+ return name === 'male' || name === 'female' || name === 'm' || name === 'f' ||
37
+ name.startsWith('male_') || name.startsWith('female_') ||
38
+ name.startsWith('m_') || name.startsWith('f_');
39
+ });
40
+
41
+ if (hasGenderDirs) {
42
+ // Structure: animations/male/talking/, animations/female/talking/
43
+ for (const item of items) {
44
+ if (!item.isDirectory()) continue;
45
+
46
+ const dirNameLower = item.name.toLowerCase();
47
+ let targetGender = null;
48
+
49
+ // Detect gender directory
50
+ if (dirNameLower === 'male' || dirNameLower === 'm' || dirNameLower.startsWith('male_') || dirNameLower.startsWith('m_')) {
51
+ targetGender = 'male';
52
+ } else if (dirNameLower === 'female' || dirNameLower === 'f' || dirNameLower.startsWith('female_') || dirNameLower.startsWith('f_')) {
53
+ targetGender = 'female';
54
+ }
39
55
 
40
- if (subFiles.length > 0) {
41
- groups[item.name] = subFiles;
42
- console.log(`Found ${subFiles.length} animations in ${item.name}/`);
56
+ if (targetGender) {
57
+ // Scan subdirectories within gender directory
58
+ const genderDir = path.join(dir, item.name);
59
+ const subItems = fs.readdirSync(genderDir, { withFileTypes: true });
60
+
61
+ for (const subItem of subItems) {
62
+ if (subItem.isDirectory()) {
63
+ const subDir = path.join(genderDir, subItem.name);
64
+ const subFiles = fs.readdirSync(subDir)
65
+ .filter(file => file.toLowerCase().endsWith('.fbx'))
66
+ .map(file => `/animations/${item.name}/${subItem.name}/${file}`);
67
+
68
+ if (subFiles.length > 0) {
69
+ const groupName = subItem.name;
70
+ if (!genderGroups[targetGender][groupName]) {
71
+ genderGroups[targetGender][groupName] = [];
72
+ }
73
+ genderGroups[targetGender][groupName].push(...subFiles);
74
+ console.log(`Found ${subFiles.length} ${targetGender} animations in ${item.name}/${subItem.name}/`);
75
+ }
76
+ }
77
+ }
78
+ } else {
79
+ // Shared directory - scan normally
80
+ const fullPath = path.join(dir, item.name);
81
+ const subFiles = fs.readdirSync(fullPath)
82
+ .filter(file => file.toLowerCase().endsWith('.fbx'))
83
+ .map(file => `/animations/${item.name}/${file}`);
84
+
85
+ if (subFiles.length > 0) {
86
+ groups[item.name] = subFiles;
87
+ console.log(`Found ${subFiles.length} shared animations in ${item.name}/`);
88
+ }
43
89
  }
44
- } else if (item.isFile() && item.name.toLowerCase().endsWith('.fbx')) {
45
- // File in root directory - add to a default group or create one
46
- const groupName = 'default';
47
- if (!groups[groupName]) {
48
- groups[groupName] = [];
90
+ }
91
+ } else {
92
+ // Structure: animations/talking/, animations/idle/ (no gender separation)
93
+ for (const item of items) {
94
+ const fullPath = path.join(dir, item.name);
95
+
96
+ if (item.isDirectory()) {
97
+ const subFiles = fs.readdirSync(fullPath)
98
+ .filter(file => file.toLowerCase().endsWith('.fbx'))
99
+ .map(file => `/animations/${item.name}/${file}`);
100
+
101
+ if (subFiles.length > 0) {
102
+ groups[item.name] = subFiles;
103
+ console.log(`Found ${subFiles.length} animations in ${item.name}/`);
104
+ }
105
+ } else if (item.isFile() && item.name.toLowerCase().endsWith('.fbx')) {
106
+ const groupName = 'default';
107
+ if (!groups[groupName]) {
108
+ groups[groupName] = [];
109
+ }
110
+ groups[groupName].push(`/animations/${item.name}`);
49
111
  }
50
- groups[groupName].push(`/animations/${item.name}`);
51
112
  }
52
113
  }
53
114
 
115
+ // Add gender-specific groups to main groups object if found
116
+ if (Object.keys(genderGroups.male).length > 0 || Object.keys(genderGroups.female).length > 0) {
117
+ groups._genderSpecific = {
118
+ male: genderGroups.male,
119
+ female: genderGroups.female,
120
+ shared: genderGroups.shared
121
+ };
122
+ console.log(`\nšŸ“Š Gender-specific groups:`);
123
+ console.log(` Male: ${Object.keys(genderGroups.male).length} groups`);
124
+ console.log(` Female: ${Object.keys(genderGroups.female).length} groups`);
125
+ }
126
+
54
127
  return groups;
55
128
  }
56
129
 
@@ -86,14 +86,27 @@ const SimpleTalkingAvatar = forwardRef(({
86
86
  const loadManifestAnimations = async () => {
87
87
  if (animations.manifest) {
88
88
  try {
89
+ console.log('šŸ”„ Loading animations from manifest:', animations.manifest);
89
90
  const manifestAnimations = await loadAnimationsFromManifest(animations.manifest);
90
91
  setLoadedAnimations(manifestAnimations);
91
- console.log('Loaded animations from manifest:', manifestAnimations);
92
+ console.log('āœ… Animations loaded and set:', manifestAnimations);
93
+
94
+ // Log gender-specific groups if available
95
+ if (manifestAnimations._genderSpecific) {
96
+ console.log('šŸ‘„ Gender-specific animations detected:', {
97
+ male: Object.keys(manifestAnimations._genderSpecific.male || {}),
98
+ female: Object.keys(manifestAnimations._genderSpecific.female || {}),
99
+ shared: Object.keys(manifestAnimations._genderSpecific.shared || {})
100
+ });
101
+ } else {
102
+ console.log('āš ļø No gender-specific animations found in manifest');
103
+ }
92
104
  } catch (error) {
93
- console.error('Failed to load animation manifest:', error);
105
+ console.error('āŒ Failed to load animation manifest:', error);
94
106
  setLoadedAnimations(animations);
95
107
  }
96
108
  } else {
109
+ console.log('šŸ“ Using animations from props (no manifest):', animations);
97
110
  setLoadedAnimations(animations);
98
111
  }
99
112
  };
@@ -250,13 +263,48 @@ const SimpleTalkingAvatar = forwardRef(({
250
263
  }
251
264
  }, []);
252
265
 
253
- // Helper function to get random animation from a group
266
+ // Helper function to get random animation from a group (with gender support)
254
267
  const getRandomAnimation = useCallback((groupName) => {
255
- if (!loadedAnimations || !loadedAnimations[groupName]) {
268
+ if (!loadedAnimations) {
269
+ console.warn('No animations loaded');
256
270
  return null;
257
271
  }
258
272
 
259
- const group = loadedAnimations[groupName];
273
+ let group = null;
274
+
275
+ // Check if gender-specific animations are available
276
+ if (loadedAnimations._genderSpecific) {
277
+ const avatarGender = avatarBody?.toUpperCase() || 'F';
278
+ const genderKey = avatarGender === 'M' ? 'male' : 'female';
279
+ const genderGroups = loadedAnimations._genderSpecific[genderKey];
280
+
281
+ // Try gender-specific first
282
+ if (genderGroups && genderGroups[groupName]) {
283
+ group = genderGroups[groupName];
284
+ console.log(`Using ${genderKey} animations for "${groupName}":`, group);
285
+ }
286
+ // Fallback to shared gender-specific animations
287
+ else if (loadedAnimations._genderSpecific.shared && loadedAnimations._genderSpecific.shared[groupName]) {
288
+ group = loadedAnimations._genderSpecific.shared[groupName];
289
+ console.log(`Using shared animations for "${groupName}":`, group);
290
+ }
291
+ }
292
+
293
+ // Fallback to root-level animations if gender-specific not found
294
+ if (!group && loadedAnimations[groupName]) {
295
+ group = loadedAnimations[groupName];
296
+ console.log(`Using root-level animations for "${groupName}":`, group);
297
+ }
298
+
299
+ if (!group) {
300
+ console.warn(`Animation group "${groupName}" not found. Available groups:`, Object.keys(loadedAnimations).filter(k => k !== '_genderSpecific'));
301
+ if (loadedAnimations._genderSpecific) {
302
+ const avatarGender = avatarBody?.toUpperCase() || 'F';
303
+ const genderKey = avatarGender === 'M' ? 'male' : 'female';
304
+ console.warn(`Gender-specific groups (${genderKey}):`, Object.keys(loadedAnimations._genderSpecific[genderKey] || {}));
305
+ }
306
+ return null;
307
+ }
260
308
 
261
309
  // If it's an array, randomly select one
262
310
  if (Array.isArray(group) && group.length > 0) {
@@ -269,21 +317,29 @@ const SimpleTalkingAvatar = forwardRef(({
269
317
  return group;
270
318
  }
271
319
 
320
+ console.warn(`Animation group "${groupName}" is not a valid format (expected array or string):`, group);
272
321
  return null;
273
- }, [loadedAnimations]);
322
+ }, [loadedAnimations, avatarBody]);
274
323
 
275
324
  // Helper function to play random animation from a group
276
325
  const playRandomAnimation = useCallback((groupName, disablePositionLock = false) => {
326
+ if (!talkingHeadRef.current) {
327
+ console.warn('TalkingHead not initialized yet');
328
+ return null;
329
+ }
330
+
277
331
  const animationPath = getRandomAnimation(groupName);
278
- if (animationPath && talkingHeadRef.current) {
332
+ if (animationPath) {
279
333
  try {
280
334
  talkingHeadRef.current.playAnimation(animationPath, null, 10, 0, 0.01, disablePositionLock);
281
- console.log(`Playing random animation from "${groupName}" group:`, animationPath);
335
+ console.log(`āœ… Playing random animation from "${groupName}" group:`, animationPath);
282
336
  return animationPath;
283
337
  } catch (error) {
284
- console.warn(`Failed to play random animation from "${groupName}" group:`, error);
338
+ console.error(`āŒ Failed to play random animation from "${groupName}" group:`, error);
285
339
  return null;
286
340
  }
341
+ } else {
342
+ console.warn(`āš ļø No animation found for group "${groupName}"`);
287
343
  }
288
344
  return null;
289
345
  }, [getRandomAnimation]);
@@ -307,7 +363,11 @@ const SimpleTalkingAvatar = forwardRef(({
307
363
  // Check both autoAnimationGroup prop and options.animationGroup
308
364
  const animationGroup = options.animationGroup || autoAnimationGroup;
309
365
  if (animationGroup && !options.skipAnimation) {
366
+ console.log(`šŸŽ¬ Attempting to play animation from group: "${animationGroup}"`);
367
+ console.log(`šŸ“Š Current avatarBody: "${avatarBody}", loadedAnimations:`, loadedAnimations);
310
368
  playRandomAnimation(animationGroup);
369
+ } else {
370
+ console.log(`ā­ļø Skipping animation (group: ${animationGroup}, skipAnimation: ${options.skipAnimation})`);
311
371
  }
312
372
 
313
373
  // Reset speech progress tracking
@@ -11,11 +11,28 @@
11
11
  */
12
12
  export async function loadAnimationsFromManifest(manifestPath) {
13
13
  try {
14
+ console.log(`šŸ“„ Loading animation manifest from: ${manifestPath}`);
14
15
  const response = await fetch(manifestPath);
16
+ if (!response.ok) {
17
+ throw new Error(`Failed to fetch manifest: ${response.status} ${response.statusText}`);
18
+ }
15
19
  const manifest = await response.json();
16
- return manifest.animations || {};
20
+ console.log('šŸ“¦ Raw manifest loaded:', manifest);
21
+
22
+ const animations = manifest.animations || {};
23
+ console.log('āœ… Processed animations object:', animations);
24
+
25
+ if (animations._genderSpecific) {
26
+ console.log('šŸ‘„ Gender-specific structure detected:', {
27
+ male: Object.keys(animations._genderSpecific.male || {}),
28
+ female: Object.keys(animations._genderSpecific.female || {}),
29
+ shared: Object.keys(animations._genderSpecific.shared || {})
30
+ });
31
+ }
32
+
33
+ return animations;
17
34
  } catch (error) {
18
- console.error('Failed to load animation manifest:', error);
35
+ console.error('āŒ Failed to load animation manifest:', error);
19
36
  return {};
20
37
  }
21
38
  }