talking-head-studio 0.3.2 → 0.3.3

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.
@@ -54,6 +54,7 @@ export interface TalkingHeadProps {
54
54
  onLoadingChange?: (state: TalkingHeadLoadingState) => void;
55
55
  onReady?: () => void;
56
56
  onError?: (message: string) => void;
57
+ onAvatarState?: (state: string) => void;
57
58
  style?: StyleProp<ViewStyle>;
58
59
  /** Base URL for vendored assets. When set, replaces all cdn.jsdelivr.net references. */
59
60
  vendorBaseUrl?: string | null;
@@ -92,5 +93,7 @@ export interface TalkingHeadRef {
92
93
  setSkinColor: (color: string) => void;
93
94
  setEyeColor: (color: string) => void;
94
95
  setAccessories: (accessories: TalkingHeadAccessory[]) => void;
96
+ /** Dispatch a named motion/gesture to the avatar (e.g. 'wave_right', 'dance_idle'). */
97
+ dispatchMotion: (name: string) => void;
95
98
  }
96
99
  export declare const TalkingHead: React.ForwardRefExoticComponent<TalkingHeadProps & React.RefAttributes<TalkingHeadRef>>;
@@ -6,7 +6,7 @@ const react_1 = require("react");
6
6
  const react_native_1 = require("react-native");
7
7
  const react_native_webview_1 = require("react-native-webview");
8
8
  const html_1 = require("./html");
9
- exports.TalkingHead = (0, react_1.forwardRef)(({ avatarUrl, authToken, mood = 'neutral', cameraView = 'upper', cameraDistance = -0.5, hairColor, skinColor, eyeColor, accessories, onLoadingChange, onReady, onError, style, vendorBaseUrl, }, ref) => {
9
+ exports.TalkingHead = (0, react_1.forwardRef)(({ avatarUrl, authToken, mood = 'neutral', cameraView = 'upper', cameraDistance = -0.5, hairColor, skinColor, eyeColor, accessories, onLoadingChange, onReady, onError, onAvatarState, style, vendorBaseUrl, }, ref) => {
10
10
  const webViewRef = (0, react_1.useRef)(null);
11
11
  const readyRef = (0, react_1.useRef)(false);
12
12
  const firstMessageSeenRef = (0, react_1.useRef)(false);
@@ -23,6 +23,8 @@ exports.TalkingHead = (0, react_1.forwardRef)(({ avatarUrl, authToken, mood = 'n
23
23
  onReadyRef.current = onReady;
24
24
  const onErrorRef = (0, react_1.useRef)(onError);
25
25
  onErrorRef.current = onError;
26
+ const onAvatarStateRef = (0, react_1.useRef)(onAvatarState);
27
+ onAvatarStateRef.current = onAvatarState;
26
28
  // The WebView HTML is built once from stable initial values.
27
29
  // avatarUrl + authToken changing causes a controlled key-based remount.
28
30
  // All other prop changes (mood, colors, accessories) go via postMessage.
@@ -124,6 +126,7 @@ exports.TalkingHead = (0, react_1.forwardRef)(({ avatarUrl, authToken, mood = 'n
124
126
  post({ type: 'set_accessories', accessories: newAccessories });
125
127
  }
126
128
  },
129
+ dispatchMotion: (name) => post({ type: 'motion', name }),
127
130
  }), [post]);
128
131
  // Sync mood via postMessage only — never causes a WebView reload
129
132
  (0, react_1.useEffect)(() => {
@@ -220,6 +223,9 @@ exports.TalkingHead = (0, react_1.forwardRef)(({ avatarUrl, authToken, mood = 'n
220
223
  else if (msg.type === 'error') {
221
224
  onErrorRef.current?.(msg.message);
222
225
  }
226
+ else if (msg.type === 'avatarState') {
227
+ onAvatarStateRef.current?.(msg.state);
228
+ }
223
229
  else if (msg.type === 'log') {
224
230
  console.log('[TalkingHead]', msg.message);
225
231
  }
@@ -23,6 +23,7 @@ export interface TalkingHeadProps {
23
23
  onLoadingChange?: (state: TalkingHeadLoadingState) => void;
24
24
  onReady?: () => void;
25
25
  onError?: (message: string) => void;
26
+ onAvatarState?: (state: string) => void;
26
27
  style?: React.CSSProperties;
27
28
  }
28
29
  export interface TalkingHeadRef {
@@ -34,5 +35,6 @@ export interface TalkingHeadRef {
34
35
  setSkinColor: (color: string) => void;
35
36
  setEyeColor: (color: string) => void;
36
37
  setAccessories: (accessories: TalkingHeadAccessory[]) => void;
38
+ dispatchMotion: (name: string) => void;
37
39
  }
38
40
  export declare const TalkingHead: React.ForwardRefExoticComponent<TalkingHeadProps & React.RefAttributes<TalkingHeadRef>>;
@@ -48,7 +48,7 @@ const iframeStyle = {
48
48
  border: 'none',
49
49
  backgroundColor: 'transparent',
50
50
  };
51
- exports.TalkingHead = (0, react_1.forwardRef)(({ avatarUrl, authToken, mood = 'neutral', cameraView = 'upper', cameraDistance = -0.5, hairColor, skinColor, eyeColor, accessories, onLoadingChange, onReady, onError, style, }, ref) => {
51
+ exports.TalkingHead = (0, react_1.forwardRef)(({ avatarUrl, authToken, mood = 'neutral', cameraView = 'upper', cameraDistance = -0.5, hairColor, skinColor, eyeColor, accessories, onLoadingChange, onReady, onError, onAvatarState, style, }, ref) => {
52
52
  const iframeRef = (0, react_1.useRef)(null);
53
53
  const readyRef = (0, react_1.useRef)(false);
54
54
  const pendingMoodRef = (0, react_1.useRef)(mood);
@@ -97,6 +97,7 @@ exports.TalkingHead = (0, react_1.forwardRef)(({ avatarUrl, authToken, mood = 'n
97
97
  post({ type: 'set_accessories', accessories: newAccessories });
98
98
  }
99
99
  },
100
+ dispatchMotion: (name) => post({ type: 'motion', name }),
100
101
  }), [post]);
101
102
  (0, react_1.useEffect)(() => {
102
103
  pendingMoodRef.current = mood;
@@ -188,6 +189,9 @@ exports.TalkingHead = (0, react_1.forwardRef)(({ avatarUrl, authToken, mood = 'n
188
189
  else if (msg.type === 'error') {
189
190
  onError?.(msg.message);
190
191
  }
192
+ else if (msg.type === 'avatarState') {
193
+ onAvatarState?.(msg.state);
194
+ }
191
195
  else if (msg.type === 'log') {
192
196
  console.log('[TalkingHead]', msg.message);
193
197
  }
package/dist/html.js CHANGED
@@ -856,6 +856,12 @@ function onIncomingMessage(event) {
856
856
  EYE_COLOR = msg.value; applyColorOverrides();
857
857
  } else if (msg.type === 'set_accessories') {
858
858
  applyAccessories(msg.accessories || []);
859
+ } else if (msg.type === 'motion' && typeof msg.name === 'string') {
860
+ if ((window as any).motionEngine) {
861
+ (window as any).motionEngine.play(msg.name).catch(() => {});
862
+ }
863
+ window.ReactNativeWebView?.postMessage(JSON.stringify({ type: 'avatarState', state: 'motion:' + msg.name }));
864
+ log('motion dispatched: ' + msg.name);
859
865
  }
860
866
  } catch (err) {
861
867
  log('Message parse error: ' + err);
package/dist/index.d.ts CHANGED
@@ -10,3 +10,4 @@ export { TalkingHeadVisualization } from './TalkingHeadVisualization';
10
10
  export type { TalkingHeadVisualizationRef } from './TalkingHeadVisualization';
11
11
  export { useDirectVisemeStream } from './tts/useDirectVisemeStream';
12
12
  export type { VisemeStreamPayload } from './tts/useDirectVisemeStream';
13
+ export { useMotionMarkers } from './tts/useMotionMarkers';
package/dist/index.js CHANGED
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.useDirectVisemeStream = exports.TalkingHeadVisualization = exports.normalizeAppearance = exports.pickTargetForMaterialName = exports.applyAppearanceToObject3D = exports.TalkingHead = void 0;
17
+ exports.useMotionMarkers = exports.useDirectVisemeStream = exports.TalkingHeadVisualization = exports.normalizeAppearance = exports.pickTargetForMaterialName = exports.applyAppearanceToObject3D = exports.TalkingHead = void 0;
18
18
  var TalkingHead_1 = require("./TalkingHead");
19
19
  Object.defineProperty(exports, "TalkingHead", { enumerable: true, get: function () { return TalkingHead_1.TalkingHead; } });
20
20
  // Export appearance utilities, but exclude AvatarAppearance — the canonical
@@ -31,3 +31,5 @@ var TalkingHeadVisualization_1 = require("./TalkingHeadVisualization");
31
31
  Object.defineProperty(exports, "TalkingHeadVisualization", { enumerable: true, get: function () { return TalkingHeadVisualization_1.TalkingHeadVisualization; } });
32
32
  var useDirectVisemeStream_1 = require("./tts/useDirectVisemeStream");
33
33
  Object.defineProperty(exports, "useDirectVisemeStream", { enumerable: true, get: function () { return useDirectVisemeStream_1.useDirectVisemeStream; } });
34
+ var useMotionMarkers_1 = require("./tts/useMotionMarkers");
35
+ Object.defineProperty(exports, "useMotionMarkers", { enumerable: true, get: function () { return useMotionMarkers_1.useMotionMarkers; } });
@@ -0,0 +1,2 @@
1
+ import type { TalkingHeadRef } from '../TalkingHead';
2
+ export declare function useMotionMarkers(ref: React.RefObject<TalkingHeadRef | null>): (text: string) => string;
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useMotionMarkers = useMotionMarkers;
4
+ const react_1 = require("react");
5
+ /**
6
+ * Parses ::marker_name:: tokens out of LLM transcript text, fires the
7
+ * corresponding motion on the avatar ref, and returns the cleaned text
8
+ * (markers stripped) ready to pass to TTS.
9
+ *
10
+ * Supports optional arguments: ::wave_right(slow):: — the arg string is
11
+ * forwarded as the second parameter of dispatchMotion for future use.
12
+ *
13
+ * @example
14
+ * const parseMarkers = useMotionMarkers(avatarRef);
15
+ * const cleanText = parseMarkers(rawLlmText); // fires motions, strips markers
16
+ * tts.speak(cleanText);
17
+ */
18
+ const MARKER_RE = /::([a-z][a-z0-9_]*)(?:\(([^)]*)\))?::/g;
19
+ function useMotionMarkers(ref) {
20
+ return (0, react_1.useCallback)((text) => text.replace(MARKER_RE, (_, name) => {
21
+ ref.current?.dispatchMotion(name);
22
+ return '';
23
+ }), [ref]);
24
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "talking-head-studio",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
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.web.js",
6
6
  "browser": "dist/index.web.js",