talking-head-studio 0.2.2 → 0.2.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.
@@ -1 +1 @@
1
- {"version":3,"file":"html.d.ts","sourceRoot":"","sources":["../src/html.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,YAAY,GAAG;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,IAAI,EAAE,SAAS,GAAG,OAAO,GAAG,KAAK,GAAG,OAAO,GAAG,SAAS,GAAG,UAAU,GAAG,WAAW,GAAG,WAAW,CAAC;IACjG,UAAU,EAAE,MAAM,GAAG,OAAO,GAAG,MAAM,CAAC;IACtC,cAAc,EAAE,MAAM,CAAC;IACvB,gFAAgF;IAChF,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B,CAAC;AAEF,wBAAgB,eAAe,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,CA+iB5D"}
1
+ {"version":3,"file":"html.d.ts","sourceRoot":"","sources":["../src/html.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,YAAY,GAAG;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,IAAI,EAAE,SAAS,GAAG,OAAO,GAAG,KAAK,GAAG,OAAO,GAAG,SAAS,GAAG,UAAU,GAAG,WAAW,GAAG,WAAW,CAAC;IACjG,UAAU,EAAE,MAAM,GAAG,OAAO,GAAG,MAAM,CAAC;IACtC,cAAc,EAAE,MAAM,CAAC;IACvB,gFAAgF;IAChF,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B,CAAC;AAEF,wBAAgB,eAAe,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,CAgkB5D"}
package/dist/html.js CHANGED
@@ -126,6 +126,7 @@ async function loadStaticFallback(loadedAvatarUrl) {
126
126
  }
127
127
 
128
128
  // Collect morph targets for amplitude-driven jaw/mouth
129
+ jawMorphCache = null; // invalidate cache whenever meshes are repopulated
129
130
  staticModel.traverse((child) => {
130
131
  if (child.isMesh && child.morphTargetDictionary && child.morphTargetInfluences) {
131
132
  mouthMeshes.push(child);
@@ -228,6 +229,7 @@ async function init() {
228
229
  }
229
230
 
230
231
  if (avatarLoaded) {
232
+ jawMorphCache = null; // invalidate cache whenever meshes are repopulated
231
233
  head.armature?.traverse((child) => {
232
234
  if (child.isMesh && child.morphTargetDictionary && child.morphTargetInfluences) {
233
235
  mouthMeshes.push(child);
@@ -239,7 +241,7 @@ async function init() {
239
241
  log('Initializing HeadAudio...');
240
242
  try {
241
243
  const haModule = await import(HEAD_AUDIO_URL);
242
- if (haModule.HeadAudio && head.audioCtx) {
244
+ if (haModule.HeadAudio && head.audioCtx && head.audioCtx.audioWorklet) {
243
245
  await head.audioCtx.audioWorklet.addModule(HEAD_AUDIO_WORKLET);
244
246
  headaudio = new haModule.HeadAudio(head.audioCtx);
245
247
  await headaudio.loadModel(HEAD_AUDIO_MODEL);
@@ -312,6 +314,8 @@ function startAudioInterception() {
312
314
  }
313
315
 
314
316
  let amplitudeDecay = 0;
317
+ // Cached jaw morph-target indices — built once after model load, used every amplitude tick.
318
+ let jawMorphCache = null; // [{influences, idx}] or null = not yet built
315
319
  let THREE_REF = null;
316
320
  let gltfLoaderInstance = null;
317
321
  const currentAccessories = {}; // { [id]: { model: THREE.Group | null, url, bone, position, rotation, scale, isLoading: boolean, latestData: any } }
@@ -514,23 +518,36 @@ function onIncomingMessage(event) {
514
518
  try {
515
519
  const msg = JSON.parse(event.data);
516
520
  if (msg.type === 'amplitude' && mouthMeshes.length > 0) {
517
- const val = Math.min(1, msg.value * 2.5);
518
- amplitudeDecay = Math.max(amplitudeDecay * 0.7, val);
519
- for (const mesh of mouthMeshes) {
520
- if (!mesh.morphTargetDictionary) continue;
521
- const keys = Object.keys(mesh.morphTargetDictionary);
522
- const jawKey = keys.find((k) => {
523
- const lk = k.toLowerCase();
524
- return lk.includes('jawopen') || lk.includes('jaw_open') ||
525
- lk.includes('mouthopen') || lk.includes('mouth_open') ||
526
- lk.includes('viseme_aa') || lk === 'aa';
527
- });
528
- if (jawKey !== undefined) {
529
- mesh.morphTargetInfluences[mesh.morphTargetDictionary[jawKey]] = amplitudeDecay;
521
+ // Build jaw index cache once, reuse on every subsequent tick
522
+ if (!jawMorphCache) {
523
+ jawMorphCache = [];
524
+ for (const mesh of mouthMeshes) {
525
+ if (!mesh.morphTargetDictionary) continue;
526
+ const keys = Object.keys(mesh.morphTargetDictionary);
527
+ const jawKey = keys.find((k) => {
528
+ const lk = k.toLowerCase();
529
+ return lk.includes('jawopen') || lk.includes('jaw_open') ||
530
+ lk.includes('mouthopen') || lk.includes('mouth_open') ||
531
+ lk.includes('viseme_aa') || lk === 'aa';
532
+ });
533
+ if (jawKey !== undefined) {
534
+ jawMorphCache.push({ influences: mesh.morphTargetInfluences, idx: mesh.morphTargetDictionary[jawKey] });
535
+ }
530
536
  }
531
537
  }
538
+ const val = Math.min(1, msg.value * 1.8);
539
+ amplitudeDecay = Math.max(amplitudeDecay * 0.7, val);
540
+ for (let i = 0; i < jawMorphCache.length; i++) {
541
+ jawMorphCache[i].influences[jawMorphCache[i].idx] = amplitudeDecay;
542
+ }
532
543
  } else if (msg.type === 'mood' && head) {
533
- head.setMood(msg.value);
544
+ // Map agent moods to library moods (library accepts: neutral,happy,angry,sad,fear,disgust,love,sleep)
545
+ const moodMap = {
546
+ neutral: 'neutral', happy: 'happy', sad: 'sad', angry: 'angry',
547
+ excited: 'happy', thinking: 'neutral', concerned: 'sad', surprised: 'happy',
548
+ };
549
+ const m = moodMap[msg.value] || 'neutral';
550
+ head.setMood(m);
534
551
  } else if (msg.type === 'hair_color') {
535
552
  HAIR_COLOR = msg.value;
536
553
  applyColorOverrides();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "talking-head-studio",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "Cross-platform 3D avatar component for React Native & web — lip-sync, gestures, accessories, and LLM integration. Powered by TalkingHead + Three.js.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/src/html.ts CHANGED
@@ -138,6 +138,7 @@ async function loadStaticFallback(loadedAvatarUrl) {
138
138
  }
139
139
 
140
140
  // Collect morph targets for amplitude-driven jaw/mouth
141
+ jawMorphCache = null; // invalidate cache whenever meshes are repopulated
141
142
  staticModel.traverse((child) => {
142
143
  if (child.isMesh && child.morphTargetDictionary && child.morphTargetInfluences) {
143
144
  mouthMeshes.push(child);
@@ -240,6 +241,7 @@ async function init() {
240
241
  }
241
242
 
242
243
  if (avatarLoaded) {
244
+ jawMorphCache = null; // invalidate cache whenever meshes are repopulated
243
245
  head.armature?.traverse((child) => {
244
246
  if (child.isMesh && child.morphTargetDictionary && child.morphTargetInfluences) {
245
247
  mouthMeshes.push(child);
@@ -251,7 +253,7 @@ async function init() {
251
253
  log('Initializing HeadAudio...');
252
254
  try {
253
255
  const haModule = await import(HEAD_AUDIO_URL);
254
- if (haModule.HeadAudio && head.audioCtx) {
256
+ if (haModule.HeadAudio && head.audioCtx && head.audioCtx.audioWorklet) {
255
257
  await head.audioCtx.audioWorklet.addModule(HEAD_AUDIO_WORKLET);
256
258
  headaudio = new haModule.HeadAudio(head.audioCtx);
257
259
  await headaudio.loadModel(HEAD_AUDIO_MODEL);
@@ -324,6 +326,8 @@ function startAudioInterception() {
324
326
  }
325
327
 
326
328
  let amplitudeDecay = 0;
329
+ // Cached jaw morph-target indices — built once after model load, used every amplitude tick.
330
+ let jawMorphCache = null; // [{influences, idx}] or null = not yet built
327
331
  let THREE_REF = null;
328
332
  let gltfLoaderInstance = null;
329
333
  const currentAccessories = {}; // { [id]: { model: THREE.Group | null, url, bone, position, rotation, scale, isLoading: boolean, latestData: any } }
@@ -526,23 +530,36 @@ function onIncomingMessage(event) {
526
530
  try {
527
531
  const msg = JSON.parse(event.data);
528
532
  if (msg.type === 'amplitude' && mouthMeshes.length > 0) {
529
- const val = Math.min(1, msg.value * 2.5);
530
- amplitudeDecay = Math.max(amplitudeDecay * 0.7, val);
531
- for (const mesh of mouthMeshes) {
532
- if (!mesh.morphTargetDictionary) continue;
533
- const keys = Object.keys(mesh.morphTargetDictionary);
534
- const jawKey = keys.find((k) => {
535
- const lk = k.toLowerCase();
536
- return lk.includes('jawopen') || lk.includes('jaw_open') ||
537
- lk.includes('mouthopen') || lk.includes('mouth_open') ||
538
- lk.includes('viseme_aa') || lk === 'aa';
539
- });
540
- if (jawKey !== undefined) {
541
- mesh.morphTargetInfluences[mesh.morphTargetDictionary[jawKey]] = amplitudeDecay;
533
+ // Build jaw index cache once, reuse on every subsequent tick
534
+ if (!jawMorphCache) {
535
+ jawMorphCache = [];
536
+ for (const mesh of mouthMeshes) {
537
+ if (!mesh.morphTargetDictionary) continue;
538
+ const keys = Object.keys(mesh.morphTargetDictionary);
539
+ const jawKey = keys.find((k) => {
540
+ const lk = k.toLowerCase();
541
+ return lk.includes('jawopen') || lk.includes('jaw_open') ||
542
+ lk.includes('mouthopen') || lk.includes('mouth_open') ||
543
+ lk.includes('viseme_aa') || lk === 'aa';
544
+ });
545
+ if (jawKey !== undefined) {
546
+ jawMorphCache.push({ influences: mesh.morphTargetInfluences, idx: mesh.morphTargetDictionary[jawKey] });
547
+ }
542
548
  }
543
549
  }
550
+ const val = Math.min(1, msg.value * 1.8);
551
+ amplitudeDecay = Math.max(amplitudeDecay * 0.7, val);
552
+ for (let i = 0; i < jawMorphCache.length; i++) {
553
+ jawMorphCache[i].influences[jawMorphCache[i].idx] = amplitudeDecay;
554
+ }
544
555
  } else if (msg.type === 'mood' && head) {
545
- head.setMood(msg.value);
556
+ // Map agent moods to library moods (library accepts: neutral,happy,angry,sad,fear,disgust,love,sleep)
557
+ const moodMap = {
558
+ neutral: 'neutral', happy: 'happy', sad: 'sad', angry: 'angry',
559
+ excited: 'happy', thinking: 'neutral', concerned: 'sad', surprised: 'happy',
560
+ };
561
+ const m = moodMap[msg.value] || 'neutral';
562
+ head.setMood(m);
546
563
  } else if (msg.type === 'hair_color') {
547
564
  HAIR_COLOR = msg.value;
548
565
  applyColorOverrides();