@sage-rsc/talking-head-react 1.0.83 → 1.1.0
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 +437 -384
- package/package.json +1 -1
- package/src/lib/talkinghead.mjs +97 -20
package/package.json
CHANGED
package/src/lib/talkinghead.mjs
CHANGED
|
@@ -432,11 +432,15 @@ class TalkingHead {
|
|
|
432
432
|
anims: [
|
|
433
433
|
{ name: 'breathing', delay: 1500, dt: [ 1200,500,1000 ], vs: { chestInhale: [0.5,0.5,0] } },
|
|
434
434
|
{ name: 'pose', alt: [
|
|
435
|
-
{ p: 0.5, delay: [5000,30000], vs: { pose: ['side'] }
|
|
435
|
+
{ p: 0.5, delay: [5000,30000], vs: { pose: ['side'] },
|
|
436
|
+
'M': { delay: [5000,30000], vs: { pose: ['wide'] } }
|
|
437
|
+
},
|
|
436
438
|
{ p: 0.3, delay: [5000,30000], vs: { pose: ['hip'] },
|
|
437
439
|
'M': { delay: [5000,30000], vs: { pose: ['wide'] } }
|
|
438
440
|
},
|
|
439
|
-
{ delay: [5000,30000], vs: { pose: ['straight'] }
|
|
441
|
+
{ delay: [5000,30000], vs: { pose: ['straight'] },
|
|
442
|
+
'M': { delay: [5000,30000], vs: { pose: ['wide'] } }
|
|
443
|
+
}
|
|
440
444
|
]},
|
|
441
445
|
{ name: 'head',
|
|
442
446
|
idle: { delay: [0,1000], dt: [ [200,5000] ], vs: { bodyRotateX: [[-0.04,0.10]], bodyRotateY: [[-0.3,0.3]], bodyRotateZ: [[-0.08,0.08]] } },
|
|
@@ -456,9 +460,11 @@ class TalkingHead {
|
|
|
456
460
|
{ name: 'pose',
|
|
457
461
|
idle: {
|
|
458
462
|
alt: [
|
|
459
|
-
{ p: 0.6, delay: [5000,30000], vs: { pose: ['side'] }
|
|
463
|
+
{ p: 0.6, delay: [5000,30000], vs: { pose: ['side'] },
|
|
464
|
+
'M': { delay: [5000,30000], vs: { pose: ['wide'] } }
|
|
465
|
+
},
|
|
460
466
|
{ p: 0.2, delay: [5000,30000], vs: { pose: ['hip'] },
|
|
461
|
-
'M': { delay: [5000,30000], vs: { pose: ['
|
|
467
|
+
'M': { delay: [5000,30000], vs: { pose: ['wide'] } }
|
|
462
468
|
},
|
|
463
469
|
{ p: 0.1, delay: [5000,30000], vs: { pose: ['straight'] } },
|
|
464
470
|
{ delay: [5000,10000], vs: { pose: ['wide'] } },
|
|
@@ -467,8 +473,12 @@ class TalkingHead {
|
|
|
467
473
|
},
|
|
468
474
|
speaking: {
|
|
469
475
|
alt: [
|
|
470
|
-
{ p: 0.4, delay: [5000,30000], vs: { pose: ['side'] }
|
|
471
|
-
|
|
476
|
+
{ p: 0.4, delay: [5000,30000], vs: { pose: ['side'] },
|
|
477
|
+
'M': { delay: [5000,30000], vs: { pose: ['wide'] } }
|
|
478
|
+
},
|
|
479
|
+
{ p: 0.4, delay: [5000,30000], vs: { pose: ['straight'] },
|
|
480
|
+
'M': { delay: [5000,30000], vs: { pose: ['wide'] } }
|
|
481
|
+
},
|
|
472
482
|
{ delay: [5000,20000], vs: { pose: ['hip'] },
|
|
473
483
|
'M': { delay: [5000,30000], vs: { pose: ['wide'] } }
|
|
474
484
|
},
|
|
@@ -2742,10 +2752,10 @@ class TalkingHead {
|
|
|
2742
2752
|
} else if ( typeof x === 'function' ) {
|
|
2743
2753
|
return x;
|
|
2744
2754
|
} else if ( typeof x === 'string' || x instanceof String ) {
|
|
2745
|
-
// Intercept pose values and override 'hip' for male avatars
|
|
2746
|
-
if (mt === 'pose' && this.avatar && this.avatar.body === 'M' && x === 'hip') {
|
|
2747
|
-
console.log('Intercepting
|
|
2748
|
-
return 'wide'; //
|
|
2755
|
+
// Intercept pose values and override 'hip' and 'side' to 'wide' for male avatars
|
|
2756
|
+
if (mt === 'pose' && this.avatar && this.avatar.body === 'M' && (x === 'hip' || x === 'side')) {
|
|
2757
|
+
console.log('Intercepting pose', x, 'in animation factory, overriding to wide for male avatar');
|
|
2758
|
+
return 'wide'; // Always use 'wide' for male avatars, never 'side' or 'hip'
|
|
2749
2759
|
}
|
|
2750
2760
|
return x.slice();
|
|
2751
2761
|
} else if ( Array.isArray(x) ) {
|
|
@@ -3037,16 +3047,14 @@ class TalkingHead {
|
|
|
3037
3047
|
break;
|
|
3038
3048
|
|
|
3039
3049
|
case 'pose':
|
|
3040
|
-
// Ensure gender-appropriate pose for male avatars
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
j = 'side';
|
|
3049
|
-
console.log('Overriding hip pose to side for male avatar');
|
|
3050
|
+
// Ensure gender-appropriate pose for male avatars - always use 'wide', never 'side' or 'hip'
|
|
3051
|
+
if (this.avatar && this.avatar.body === 'M') {
|
|
3052
|
+
if (j === 'hip' || j === 'side') {
|
|
3053
|
+
// Always override 'hip' and 'side' to 'wide' for male avatars
|
|
3054
|
+
if (this.poseTemplates['wide']) {
|
|
3055
|
+
j = 'wide';
|
|
3056
|
+
console.log('Overriding pose', j === 'hip' ? 'hip' : 'side', 'to wide for male avatar');
|
|
3057
|
+
}
|
|
3050
3058
|
}
|
|
3051
3059
|
}
|
|
3052
3060
|
this.poseName = j;
|
|
@@ -5336,6 +5344,61 @@ class TalkingHead {
|
|
|
5336
5344
|
* @param {number} [ndx=0] Index of the clip
|
|
5337
5345
|
* @param {number} [scale=0.01] Position scale factor
|
|
5338
5346
|
*/
|
|
5347
|
+
/**
|
|
5348
|
+
* Get all bone names from the avatar's armature
|
|
5349
|
+
* @returns {Set<string>} Set of bone names
|
|
5350
|
+
*/
|
|
5351
|
+
getAvailableBoneNames() {
|
|
5352
|
+
const boneNames = new Set();
|
|
5353
|
+
if (!this.armature) return boneNames;
|
|
5354
|
+
|
|
5355
|
+
this.armature.traverse((child) => {
|
|
5356
|
+
if (child.isBone || child.type === 'Bone') {
|
|
5357
|
+
boneNames.add(child.name);
|
|
5358
|
+
}
|
|
5359
|
+
});
|
|
5360
|
+
|
|
5361
|
+
return boneNames;
|
|
5362
|
+
}
|
|
5363
|
+
|
|
5364
|
+
/**
|
|
5365
|
+
* Filter animation tracks to only include bones that exist in the avatar
|
|
5366
|
+
* @param {THREE.AnimationClip} clip - Animation clip to filter
|
|
5367
|
+
* @param {Set<string>} availableBones - Set of available bone names
|
|
5368
|
+
* @returns {THREE.AnimationClip} Filtered animation clip
|
|
5369
|
+
*/
|
|
5370
|
+
filterAnimationTracks(clip, availableBones) {
|
|
5371
|
+
const validTracks = [];
|
|
5372
|
+
const missingBones = new Set();
|
|
5373
|
+
|
|
5374
|
+
clip.tracks.forEach(track => {
|
|
5375
|
+
// Extract bone name from track name (e.g., "CC_Base_R_Index3.position" -> "CC_Base_R_Index3")
|
|
5376
|
+
const trackNameParts = track.name.split('.');
|
|
5377
|
+
const boneName = trackNameParts[0];
|
|
5378
|
+
|
|
5379
|
+
if (availableBones.has(boneName)) {
|
|
5380
|
+
validTracks.push(track);
|
|
5381
|
+
} else {
|
|
5382
|
+
missingBones.add(boneName);
|
|
5383
|
+
}
|
|
5384
|
+
});
|
|
5385
|
+
|
|
5386
|
+
if (missingBones.size > 0) {
|
|
5387
|
+
console.warn(`FBX animation "${clip.name}" contains tracks for ${missingBones.size} bone(s) not found in avatar skeleton:`, Array.from(missingBones).slice(0, 10).join(', '), missingBones.size > 10 ? '...' : '');
|
|
5388
|
+
console.info(`Filtered ${clip.tracks.length} tracks down to ${validTracks.length} valid tracks`);
|
|
5389
|
+
} else if (validTracks.length > 0) {
|
|
5390
|
+
console.info(`FBX animation "${clip.name}" is fully compatible: all ${validTracks.length} tracks match avatar skeleton`);
|
|
5391
|
+
}
|
|
5392
|
+
|
|
5393
|
+
// Create a new clip with only valid tracks
|
|
5394
|
+
if (validTracks.length === 0) {
|
|
5395
|
+
console.error(`No valid tracks found for animation "${clip.name}". All bones are missing from avatar skeleton.`);
|
|
5396
|
+
return null;
|
|
5397
|
+
}
|
|
5398
|
+
|
|
5399
|
+
return new THREE.AnimationClip(clip.name, clip.duration, validTracks);
|
|
5400
|
+
}
|
|
5401
|
+
|
|
5339
5402
|
async playAnimation(url, onprogress=null, dur=10, ndx=0, scale=0.01, disablePositionLock=false) {
|
|
5340
5403
|
if ( !this.armature ) return;
|
|
5341
5404
|
|
|
@@ -5483,6 +5546,20 @@ class TalkingHead {
|
|
|
5483
5546
|
if ( fbx && fbx.animations && fbx.animations[ndx] ) {
|
|
5484
5547
|
let anim = fbx.animations[ndx];
|
|
5485
5548
|
|
|
5549
|
+
// Get available bone names from avatar skeleton
|
|
5550
|
+
const availableBones = this.getAvailableBoneNames();
|
|
5551
|
+
|
|
5552
|
+
// Filter animation tracks to only include bones that exist
|
|
5553
|
+
const filteredAnim = this.filterAnimationTracks(anim, availableBones);
|
|
5554
|
+
|
|
5555
|
+
if (!filteredAnim) {
|
|
5556
|
+
console.error(`Cannot play FBX animation "${url}": No compatible bones found.`);
|
|
5557
|
+
return;
|
|
5558
|
+
}
|
|
5559
|
+
|
|
5560
|
+
// Use the filtered animation instead of the original
|
|
5561
|
+
anim = filteredAnim;
|
|
5562
|
+
|
|
5486
5563
|
// Rename and scale Mixamo tracks, create a pose
|
|
5487
5564
|
const props = {};
|
|
5488
5565
|
anim.tracks.forEach( t => {
|