@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/dist/index.cjs +2 -2
- package/dist/index.js +528 -497
- package/package.json +1 -1
- package/scripts/generate-animation-manifest.js +90 -17
- package/src/components/SimpleTalkingAvatar.jsx +69 -9
- package/src/utils/animationLoader.js +19 -2
package/package.json
CHANGED
|
@@ -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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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 (
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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('
|
|
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
|
|
268
|
+
if (!loadedAnimations) {
|
|
269
|
+
console.warn('No animations loaded');
|
|
256
270
|
return null;
|
|
257
271
|
}
|
|
258
272
|
|
|
259
|
-
|
|
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
|
|
332
|
+
if (animationPath) {
|
|
279
333
|
try {
|
|
280
334
|
talkingHeadRef.current.playAnimation(animationPath, null, 10, 0, 0.01, disablePositionLock);
|
|
281
|
-
console.log(
|
|
335
|
+
console.log(`ā
Playing random animation from "${groupName}" group:`, animationPath);
|
|
282
336
|
return animationPath;
|
|
283
337
|
} catch (error) {
|
|
284
|
-
console.
|
|
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
|
-
|
|
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
|
}
|