talking-head-studio 0.3.0 → 0.3.1

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.
@@ -3,3 +3,4 @@ export type { FilamentAvatarRef } from './FilamentAvatar';
3
3
  export { useAuthedFilamentUri } from './useAuthedFilamentUri';
4
4
  export type { AuthedFileResult } from './useAuthedFilamentUri';
5
5
  export * from './morphTables';
6
+ export { FACE_SQUEEZE_LOCAL_MODULE } from './faceSqueezeAssets';
@@ -14,9 +14,11 @@ 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.useAuthedFilamentUri = exports.FilamentAvatar = void 0;
17
+ exports.FACE_SQUEEZE_LOCAL_MODULE = exports.useAuthedFilamentUri = exports.FilamentAvatar = void 0;
18
18
  var FilamentAvatar_1 = require("./FilamentAvatar");
19
19
  Object.defineProperty(exports, "FilamentAvatar", { enumerable: true, get: function () { return FilamentAvatar_1.FilamentAvatar; } });
20
20
  var useAuthedFilamentUri_1 = require("./useAuthedFilamentUri");
21
21
  Object.defineProperty(exports, "useAuthedFilamentUri", { enumerable: true, get: function () { return useAuthedFilamentUri_1.useAuthedFilamentUri; } });
22
22
  __exportStar(require("./morphTables"), exports);
23
+ var faceSqueezeAssets_1 = require("./faceSqueezeAssets");
24
+ Object.defineProperty(exports, "FACE_SQUEEZE_LOCAL_MODULE", { enumerable: true, get: function () { return faceSqueezeAssets_1.FACE_SQUEEZE_LOCAL_MODULE; } });
package/dist/index.d.ts CHANGED
@@ -8,3 +8,5 @@ export * from './api';
8
8
  export * from './wardrobe';
9
9
  export { TalkingHeadVisualization } from './TalkingHeadVisualization';
10
10
  export type { TalkingHeadVisualizationRef } from './TalkingHeadVisualization';
11
+ export { useDirectVisemeStream } from './tts/useDirectVisemeStream';
12
+ export type { VisemeStreamPayload } from './tts/useDirectVisemeStream';
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.TalkingHeadVisualization = exports.normalizeAppearance = exports.pickTargetForMaterialName = exports.applyAppearanceToObject3D = exports.TalkingHead = void 0;
17
+ 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
@@ -29,3 +29,5 @@ __exportStar(require("./api"), exports);
29
29
  __exportStar(require("./wardrobe"), exports);
30
30
  var TalkingHeadVisualization_1 = require("./TalkingHeadVisualization");
31
31
  Object.defineProperty(exports, "TalkingHeadVisualization", { enumerable: true, get: function () { return TalkingHeadVisualization_1.TalkingHeadVisualization; } });
32
+ var useDirectVisemeStream_1 = require("./tts/useDirectVisemeStream");
33
+ Object.defineProperty(exports, "useDirectVisemeStream", { enumerable: true, get: function () { return useDirectVisemeStream_1.useDirectVisemeStream; } });
@@ -0,0 +1,21 @@
1
+ import type { TalkingHeadVisemeSchedule } from "../TalkingHead";
2
+ export type VisemeStreamPayload = {
3
+ requestId?: string;
4
+ durationMs?: number;
5
+ cues?: TalkingHeadVisemeSchedule["cues"];
6
+ };
7
+ type OpenStreamOptions = {
8
+ requestId: string;
9
+ ttsBaseUrl: string;
10
+ };
11
+ /**
12
+ * Opens a direct SSE connection to the TTS server to receive viseme data,
13
+ * bypassing the agent data channel relay.
14
+ *
15
+ * Uses fetch() with streaming response body instead of EventSource because
16
+ * React Native does not have a reliable EventSource polyfill.
17
+ */
18
+ export declare function useDirectVisemeStream(onVisemes: (payload: VisemeStreamPayload) => void): {
19
+ openStream: ({ requestId, ttsBaseUrl }: OpenStreamOptions) => void;
20
+ };
21
+ export {};
@@ -0,0 +1,119 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useDirectVisemeStream = useDirectVisemeStream;
4
+ const react_1 = require("react");
5
+ const fetch_1 = require("expo/fetch");
6
+ /**
7
+ * Opens a direct SSE connection to the TTS server to receive viseme data,
8
+ * bypassing the agent data channel relay.
9
+ *
10
+ * Uses fetch() with streaming response body instead of EventSource because
11
+ * React Native does not have a reliable EventSource polyfill.
12
+ */
13
+ function useDirectVisemeStream(onVisemes) {
14
+ // Track the current abort controller keyed by requestId so we can detect
15
+ // when a new requestId arrives and tear down the previous stream.
16
+ const abortControllerRef = (0, react_1.useRef)(null);
17
+ const activeRequestIdRef = (0, react_1.useRef)(null);
18
+ const onVisemesRef = (0, react_1.useRef)(onVisemes);
19
+ // Keep callback ref up to date without requiring it in openStream's dep array
20
+ (0, react_1.useEffect)(() => {
21
+ onVisemesRef.current = onVisemes;
22
+ });
23
+ const openStream = (0, react_1.useCallback)(({ requestId, ttsBaseUrl }) => {
24
+ // Abort any existing stream — whether for the same or a different requestId
25
+ if (abortControllerRef.current) {
26
+ abortControllerRef.current.abort();
27
+ abortControllerRef.current = null;
28
+ }
29
+ activeRequestIdRef.current = requestId;
30
+ const controller = new AbortController();
31
+ abortControllerRef.current = controller;
32
+ const { signal } = controller;
33
+ // Strip trailing /v1 if present so we don't double it
34
+ const base = ttsBaseUrl.replace(/\/v1\/?$/, '');
35
+ const url = `${base}/v1/audio/visemes/${encodeURIComponent(requestId)}/stream`;
36
+ (async () => {
37
+ try {
38
+ const response = await (0, fetch_1.fetch)(url, {
39
+ headers: { Accept: "text/event-stream" },
40
+ signal,
41
+ });
42
+ if (!response.ok) {
43
+ console.warn("[VisemeSSE] Non-OK response", { requestId, status: response.status });
44
+ return;
45
+ }
46
+ const reader = response.body?.getReader();
47
+ if (!reader) {
48
+ console.warn("[VisemeSSE] No response body reader", { requestId });
49
+ return;
50
+ }
51
+ const decoder = new TextDecoder();
52
+ let buffer = "";
53
+ let pendingEvent = null;
54
+ while (true) {
55
+ const { done, value } = await reader.read();
56
+ if (done)
57
+ break;
58
+ buffer += decoder.decode(value, { stream: true });
59
+ // Split on newlines, keeping the remainder (incomplete line) in buffer
60
+ const lines = buffer.split("\n");
61
+ buffer = lines.pop() ?? "";
62
+ for (const rawLine of lines) {
63
+ const line = rawLine.trimEnd();
64
+ if (line.startsWith("event:")) {
65
+ pendingEvent = line.slice("event:".length).trim();
66
+ }
67
+ else if (line.startsWith("data:")) {
68
+ if (pendingEvent === "visemes") {
69
+ const jsonText = line.slice("data:".length).trim();
70
+ try {
71
+ const payload = JSON.parse(jsonText);
72
+ console.log("[VisemeSSE] received", {
73
+ requestId,
74
+ cues: Array.isArray(payload.cues) ? payload.cues.length : 0,
75
+ durationMs: payload.durationMs ?? null,
76
+ });
77
+ onVisemesRef.current(payload);
78
+ }
79
+ catch (parseErr) {
80
+ console.warn("[VisemeSSE] JSON parse error", parseErr);
81
+ }
82
+ }
83
+ // Reset pending event after consuming the data line
84
+ pendingEvent = null;
85
+ }
86
+ else if (line === "") {
87
+ // Empty line = end of SSE message block; reset pending event
88
+ pendingEvent = null;
89
+ }
90
+ }
91
+ }
92
+ console.log("[VisemeSSE] stream ended", { requestId });
93
+ }
94
+ catch (err) {
95
+ if (err?.name === "AbortError") {
96
+ // Expected — stream was intentionally cancelled
97
+ return;
98
+ }
99
+ console.warn("[VisemeSSE] stream error", { requestId, err });
100
+ }
101
+ finally {
102
+ // Only clear the ref if it still points to our controller
103
+ if (abortControllerRef.current === controller) {
104
+ abortControllerRef.current = null;
105
+ }
106
+ }
107
+ })();
108
+ }, []);
109
+ // Clean up on unmount
110
+ (0, react_1.useEffect)(() => {
111
+ return () => {
112
+ if (abortControllerRef.current) {
113
+ abortControllerRef.current.abort();
114
+ abortControllerRef.current = null;
115
+ }
116
+ };
117
+ }, []);
118
+ return { openStream };
119
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "talking-head-studio",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
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",
@@ -97,6 +97,7 @@
97
97
  "peerDependencies": {
98
98
  "@react-three/drei": ">=9",
99
99
  "@react-three/fiber": ">=8",
100
+ "expo": ">=51",
100
101
  "expo-asset": ">=10",
101
102
  "expo-file-system": ">=17",
102
103
  "react": ">=18",
@@ -115,6 +116,9 @@
115
116
  "react-native-filament": {
116
117
  "optional": true
117
118
  },
119
+ "expo": {
120
+ "optional": true
121
+ },
118
122
  "expo-asset": {
119
123
  "optional": true
120
124
  },