@sage-rsc/talking-head-react 1.8.0 → 1.8.2
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 +5 -5
- package/dist/index.js +1000 -991
- package/package.json +1 -1
- package/src/components/SimpleTalkingAvatar.jsx +31 -12
- package/src/lib/talkinghead.mjs +45 -2
package/package.json
CHANGED
|
@@ -398,7 +398,8 @@ const SimpleTalkingAvatar = forwardRef(({
|
|
|
398
398
|
}
|
|
399
399
|
}, []);
|
|
400
400
|
|
|
401
|
-
// Helper function to get all animations from a group (with gender support)
|
|
401
|
+
// Helper function to get all animations from a group (with unified gender support)
|
|
402
|
+
// This ensures both male and female avatars use the same fallback logic
|
|
402
403
|
const getAllAnimationsFromGroup = useCallback((groupName) => {
|
|
403
404
|
if (!loadedAnimations) {
|
|
404
405
|
return [];
|
|
@@ -406,33 +407,47 @@ const SimpleTalkingAvatar = forwardRef(({
|
|
|
406
407
|
|
|
407
408
|
let group = null;
|
|
408
409
|
|
|
409
|
-
//
|
|
410
|
+
// Unified fallback logic for both genders:
|
|
411
|
+
// 1. Try gender-specific animations (male/talking or female/talking)
|
|
412
|
+
// 2. Try shared gender-specific animations (shared/talking)
|
|
413
|
+
// 3. Try root-level animations (talking)
|
|
414
|
+
// This ensures both genders get the same treatment
|
|
415
|
+
|
|
410
416
|
if (loadedAnimations._genderSpecific) {
|
|
411
417
|
const genderKey = getGenderKey(avatarBody);
|
|
412
418
|
|
|
419
|
+
// Try gender-specific first (e.g., male/talking or female/talking)
|
|
413
420
|
const genderGroups = loadedAnimations._genderSpecific[genderKey];
|
|
414
|
-
|
|
415
|
-
// Try gender-specific first
|
|
416
|
-
if (genderGroups && genderGroups[groupName]) {
|
|
421
|
+
if (genderGroups && genderGroups[groupName] && Array.isArray(genderGroups[groupName]) && genderGroups[groupName].length > 0) {
|
|
417
422
|
group = genderGroups[groupName];
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Fallback to shared gender-specific animations (shared/talking)
|
|
426
|
+
if (!group && loadedAnimations._genderSpecific.shared && loadedAnimations._genderSpecific.shared[groupName]) {
|
|
427
|
+
const sharedGroup = loadedAnimations._genderSpecific.shared[groupName];
|
|
428
|
+
if (Array.isArray(sharedGroup) && sharedGroup.length > 0) {
|
|
429
|
+
group = sharedGroup;
|
|
430
|
+
}
|
|
422
431
|
}
|
|
423
432
|
}
|
|
424
433
|
|
|
425
434
|
// Fallback to root-level animations if gender-specific not found
|
|
435
|
+
// This ensures both genders can use root-level animations
|
|
426
436
|
if (!group && loadedAnimations[groupName]) {
|
|
427
|
-
|
|
437
|
+
const rootGroup = loadedAnimations[groupName];
|
|
438
|
+
if (Array.isArray(rootGroup) && rootGroup.length > 0) {
|
|
439
|
+
group = rootGroup;
|
|
440
|
+
} else if (typeof rootGroup === 'string') {
|
|
441
|
+
group = [rootGroup];
|
|
442
|
+
}
|
|
428
443
|
}
|
|
429
444
|
|
|
430
|
-
if (!group) {
|
|
445
|
+
if (!group || (Array.isArray(group) && group.length === 0)) {
|
|
431
446
|
// Only log warning if animations were actually configured (not just empty object)
|
|
432
447
|
const hasAnyAnimations = Object.keys(loadedAnimations).length > 0 ||
|
|
433
448
|
(loadedAnimations._genderSpecific && Object.keys(loadedAnimations._genderSpecific).length > 0);
|
|
434
449
|
if (hasAnyAnimations) {
|
|
435
|
-
console.warn(`⚠️ No animations found for group "${groupName}". Make sure animations are configured correctly.`);
|
|
450
|
+
console.warn(`⚠️ No animations found for group "${groupName}" (gender: ${getGenderKey(avatarBody)}). Make sure animations are configured correctly.`);
|
|
436
451
|
}
|
|
437
452
|
return [];
|
|
438
453
|
}
|
|
@@ -1004,6 +1019,10 @@ const SimpleTalkingAvatar = forwardRef(({
|
|
|
1004
1019
|
// IMPORTANT: Ensure isSpeaking is false to prevent startSpeaking() from continuing
|
|
1005
1020
|
talkingHeadRef.current.isSpeaking = false;
|
|
1006
1021
|
|
|
1022
|
+
// CRITICAL: Stop animations when pausing
|
|
1023
|
+
isSpeakingRef.current = false;
|
|
1024
|
+
currentAnimationGroupRef.current = null;
|
|
1025
|
+
|
|
1007
1026
|
// If we have trimmed buffer, store speech queue to restore on resume
|
|
1008
1027
|
// This way speech won't continue automatically, only when resumed
|
|
1009
1028
|
if (pausedAudioData && pausedAudioData.audio && savedSpeechQueue) {
|
package/src/lib/talkinghead.mjs
CHANGED
|
@@ -3171,6 +3171,27 @@ class TalkingHead {
|
|
|
3171
3171
|
this.updatePoseBase(this.animClock);
|
|
3172
3172
|
if ( this.mixer ) {
|
|
3173
3173
|
this.mixer.update(dt / 1000 * this.mixer.timeScale);
|
|
3174
|
+
|
|
3175
|
+
// Manually check if current FBX animation has finished (backup to mixer 'finished' event)
|
|
3176
|
+
// This ensures the callback fires reliably even if the mixer event doesn't
|
|
3177
|
+
if (this.currentFBXActionForCallback && this.currentFBXActionForCallback.isRunning() && this.currentFBXActionClipDuration) {
|
|
3178
|
+
const actionTime = this.currentFBXActionForCallback.time;
|
|
3179
|
+
const clipDuration = this.currentFBXActionClipDuration;
|
|
3180
|
+
|
|
3181
|
+
// For LoopOnce, action.time goes from 0 to clipDuration
|
|
3182
|
+
// Check if animation has reached the end (with small tolerance for timing)
|
|
3183
|
+
// Also check if action is paused (time doesn't advance when paused)
|
|
3184
|
+
if (actionTime >= clipDuration - 0.05 || (actionTime > 0 && !this.currentFBXActionForCallback.paused && actionTime >= clipDuration - 0.1)) {
|
|
3185
|
+
// Animation finished - call callback manually
|
|
3186
|
+
if (this.currentFBXActionCallback) {
|
|
3187
|
+
this.currentFBXActionCallback();
|
|
3188
|
+
this.currentFBXActionCallback = null;
|
|
3189
|
+
}
|
|
3190
|
+
this.currentFBXActionForCallback = null;
|
|
3191
|
+
this.currentFBXActionStartTime = null;
|
|
3192
|
+
this.currentFBXActionClipDuration = null;
|
|
3193
|
+
}
|
|
3194
|
+
}
|
|
3174
3195
|
}
|
|
3175
3196
|
this.updatePoseDelta();
|
|
3176
3197
|
|
|
@@ -5556,8 +5577,6 @@ class TalkingHead {
|
|
|
5556
5577
|
this.stopAnimation();
|
|
5557
5578
|
};
|
|
5558
5579
|
|
|
5559
|
-
this.mixer.addEventListener( 'finished', finishedHandler, { once: true });
|
|
5560
|
-
|
|
5561
5580
|
// Play action with error handling
|
|
5562
5581
|
// If dur is 0 or negative, play animation once (use clip's natural duration)
|
|
5563
5582
|
const action = this.mixer.clipAction(item.clip);
|
|
@@ -5571,12 +5590,29 @@ class TalkingHead {
|
|
|
5571
5590
|
}
|
|
5572
5591
|
action.clampWhenFinished = true;
|
|
5573
5592
|
|
|
5593
|
+
// Set up callback to check when animation finishes
|
|
5594
|
+
// Use both mixer 'finished' event and manual time checking for reliability
|
|
5595
|
+
this.mixer.addEventListener( 'finished', finishedHandler, { once: true });
|
|
5596
|
+
|
|
5597
|
+
// Also track the action and check manually in animate loop for reliability
|
|
5598
|
+
// Store action reference for manual checking
|
|
5599
|
+
this.currentFBXActionForCallback = action;
|
|
5600
|
+
this.currentFBXActionCallback = finishedHandler;
|
|
5601
|
+
this.currentFBXActionClipDuration = item.clip.duration;
|
|
5602
|
+
this.currentFBXActionStartTime = null; // Will be set when action starts playing
|
|
5603
|
+
|
|
5574
5604
|
// Fade out previous action smoothly if one exists and is running
|
|
5575
5605
|
if (this.currentFBXAction && this.currentFBXAction.isRunning()) {
|
|
5576
5606
|
this.currentFBXAction.fadeOut(0.3);
|
|
5577
5607
|
// Small delay for smooth transition
|
|
5578
5608
|
setTimeout(() => {
|
|
5579
5609
|
this.currentFBXAction = action;
|
|
5610
|
+
// IMPORTANT: Update callback tracking variables inside setTimeout
|
|
5611
|
+
// This ensures manual checking works correctly for fade transitions
|
|
5612
|
+
this.currentFBXActionForCallback = action;
|
|
5613
|
+
this.currentFBXActionCallback = finishedHandler;
|
|
5614
|
+
this.currentFBXActionClipDuration = item.clip.duration;
|
|
5615
|
+
this.currentFBXActionStartTime = this.mixer.time; // Track when animation starts
|
|
5580
5616
|
try {
|
|
5581
5617
|
action.fadeIn(0.5).play();
|
|
5582
5618
|
console.log('FBX animation started successfully (with fade transition):', url);
|
|
@@ -5590,6 +5626,7 @@ class TalkingHead {
|
|
|
5590
5626
|
|
|
5591
5627
|
// Store the current FBX action for proper cleanup
|
|
5592
5628
|
this.currentFBXAction = action;
|
|
5629
|
+
this.currentFBXActionStartTime = this.mixer.time; // Track when animation starts
|
|
5593
5630
|
|
|
5594
5631
|
try {
|
|
5595
5632
|
action.fadeIn(0.5).play();
|
|
@@ -5973,6 +6010,12 @@ class TalkingHead {
|
|
|
5973
6010
|
this.currentFBXAction = null;
|
|
5974
6011
|
}
|
|
5975
6012
|
|
|
6013
|
+
// Clear callback tracking
|
|
6014
|
+
this.currentFBXActionForCallback = null;
|
|
6015
|
+
this.currentFBXActionCallback = null;
|
|
6016
|
+
this.currentFBXActionStartTime = null;
|
|
6017
|
+
this.currentFBXActionClipDuration = null;
|
|
6018
|
+
|
|
5976
6019
|
// Only destroy mixer if no other animations are running
|
|
5977
6020
|
// This allows morph target animations (lip-sync) to continue
|
|
5978
6021
|
if (this.mixer && this.mixer._actions.length === 0) {
|