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

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.4",
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
 
@@ -89,6 +89,14 @@ const SimpleTalkingAvatar = forwardRef(({
89
89
  const manifestAnimations = await loadAnimationsFromManifest(animations.manifest);
90
90
  setLoadedAnimations(manifestAnimations);
91
91
  console.log('Loaded animations from manifest:', manifestAnimations);
92
+
93
+ // Log gender-specific groups if available
94
+ if (manifestAnimations._genderSpecific) {
95
+ console.log('Gender-specific animations detected:', {
96
+ male: Object.keys(manifestAnimations._genderSpecific.male || {}),
97
+ female: Object.keys(manifestAnimations._genderSpecific.female || {})
98
+ });
99
+ }
92
100
  } catch (error) {
93
101
  console.error('Failed to load animation manifest:', error);
94
102
  setLoadedAnimations(animations);
@@ -250,13 +258,29 @@ const SimpleTalkingAvatar = forwardRef(({
250
258
  }
251
259
  }, []);
252
260
 
253
- // Helper function to get random animation from a group
261
+ // Helper function to get random animation from a group (with gender support)
254
262
  const getRandomAnimation = useCallback((groupName) => {
255
263
  if (!loadedAnimations || !loadedAnimations[groupName]) {
256
264
  return null;
257
265
  }
258
266
 
259
- const group = loadedAnimations[groupName];
267
+ let group = loadedAnimations[groupName];
268
+
269
+ // Check if gender-specific animations are available
270
+ if (loadedAnimations._genderSpecific) {
271
+ const avatarGender = avatarBody?.toUpperCase() || 'F';
272
+ const genderKey = avatarGender === 'M' ? 'male' : 'female';
273
+ const genderGroups = loadedAnimations._genderSpecific[genderKey];
274
+
275
+ // Try gender-specific first, fallback to shared
276
+ if (genderGroups && genderGroups[groupName]) {
277
+ group = genderGroups[groupName];
278
+ console.log(`Using ${genderKey} animations for "${groupName}"`);
279
+ } else if (loadedAnimations._genderSpecific.shared && loadedAnimations._genderSpecific.shared[groupName]) {
280
+ group = loadedAnimations._genderSpecific.shared[groupName];
281
+ console.log(`Using shared animations for "${groupName}"`);
282
+ }
283
+ }
260
284
 
261
285
  // If it's an array, randomly select one
262
286
  if (Array.isArray(group) && group.length > 0) {
@@ -270,7 +294,7 @@ const SimpleTalkingAvatar = forwardRef(({
270
294
  }
271
295
 
272
296
  return null;
273
- }, [loadedAnimations]);
297
+ }, [loadedAnimations, avatarBody]);
274
298
 
275
299
  // Helper function to play random animation from a group
276
300
  const playRandomAnimation = useCallback((groupName, disablePositionLock = false) => {