talking-head-studio 0.3.0 → 0.3.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.
@@ -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,25 @@
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
+ * Retries on transient failures (network blip, 503) with exponential backoff
19
+ * up to STREAM_RETRY_BUDGET_MS. Aborts cleanly when a new requestId arrives
20
+ * or the component unmounts.
21
+ */
22
+ export declare function useDirectVisemeStream(onVisemes: (payload: VisemeStreamPayload) => void): {
23
+ openStream: ({ requestId, ttsBaseUrl }: OpenStreamOptions) => void;
24
+ };
25
+ export {};
@@ -0,0 +1,185 @@
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
+ // How long to keep retrying a stream before giving up (ms).
7
+ const STREAM_RETRY_BUDGET_MS = 3000;
8
+ // Initial retry delay; doubles each attempt up to MAX_RETRY_DELAY_MS.
9
+ const INITIAL_RETRY_DELAY_MS = 150;
10
+ const MAX_RETRY_DELAY_MS = 1000;
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
+ * Retries on transient failures (network blip, 503) with exponential backoff
19
+ * up to STREAM_RETRY_BUDGET_MS. Aborts cleanly when a new requestId arrives
20
+ * or the component unmounts.
21
+ */
22
+ function useDirectVisemeStream(onVisemes) {
23
+ const abortControllerRef = (0, react_1.useRef)(null);
24
+ const onVisemesRef = (0, react_1.useRef)(onVisemes);
25
+ // Keep callback ref up to date without requiring it in openStream's dep array
26
+ (0, react_1.useEffect)(() => {
27
+ onVisemesRef.current = onVisemes;
28
+ });
29
+ const openStream = (0, react_1.useCallback)(({ requestId, ttsBaseUrl }) => {
30
+ // Abort any existing stream for a previous request
31
+ if (abortControllerRef.current) {
32
+ abortControllerRef.current.abort();
33
+ abortControllerRef.current = null;
34
+ }
35
+ const controller = new AbortController();
36
+ abortControllerRef.current = controller;
37
+ const { signal } = controller;
38
+ // Strip trailing /v1 if present so we don't double it
39
+ const base = ttsBaseUrl.replace(/\/v1\/?$/, "");
40
+ const url = `${base}/v1/audio/visemes/${encodeURIComponent(requestId)}/stream`;
41
+ (async () => {
42
+ const startedAt = Date.now();
43
+ let retryDelay = INITIAL_RETRY_DELAY_MS;
44
+ while (!signal.aborted) {
45
+ try {
46
+ const response = await (0, fetch_1.fetch)(url, {
47
+ headers: { Accept: "text/event-stream" },
48
+ signal,
49
+ });
50
+ if (!response.ok) {
51
+ const retryable = response.status === 503 || response.status === 502 || response.status === 429;
52
+ if (!retryable || Date.now() - startedAt >= STREAM_RETRY_BUDGET_MS) {
53
+ console.warn("[VisemeSSE] Non-OK response, giving up", { requestId, status: response.status });
54
+ return;
55
+ }
56
+ console.warn("[VisemeSSE] Retryable error, backing off", { requestId, status: response.status, retryDelay });
57
+ await sleep(retryDelay, signal);
58
+ retryDelay = Math.min(retryDelay * 2, MAX_RETRY_DELAY_MS);
59
+ continue;
60
+ }
61
+ const reader = response.body?.getReader();
62
+ if (!reader) {
63
+ console.warn("[VisemeSSE] No response body reader", { requestId });
64
+ return;
65
+ }
66
+ await readSseStream(reader, signal, (payload) => {
67
+ onVisemesRef.current(payload);
68
+ });
69
+ // Stream ended cleanly — done.
70
+ console.log("[VisemeSSE] stream ended", { requestId });
71
+ return;
72
+ }
73
+ catch (err) {
74
+ if (err?.name === "AbortError" || signal.aborted)
75
+ return;
76
+ const elapsed = Date.now() - startedAt;
77
+ if (elapsed >= STREAM_RETRY_BUDGET_MS) {
78
+ console.warn("[VisemeSSE] stream error, retry budget exhausted", { requestId, err });
79
+ return;
80
+ }
81
+ console.warn("[VisemeSSE] stream error, retrying", { requestId, retryDelay, err });
82
+ await sleep(retryDelay, signal);
83
+ retryDelay = Math.min(retryDelay * 2, MAX_RETRY_DELAY_MS);
84
+ }
85
+ }
86
+ })().finally(() => {
87
+ if (abortControllerRef.current === controller) {
88
+ abortControllerRef.current = null;
89
+ }
90
+ });
91
+ }, []);
92
+ // Clean up on unmount
93
+ (0, react_1.useEffect)(() => {
94
+ return () => {
95
+ if (abortControllerRef.current) {
96
+ abortControllerRef.current.abort();
97
+ abortControllerRef.current = null;
98
+ }
99
+ };
100
+ }, []);
101
+ return { openStream };
102
+ }
103
+ // ─── SSE parser ──────────────────────────────────────────────────────────────
104
+ /**
105
+ * Reads an SSE stream to completion, dispatching `event: visemes` messages.
106
+ *
107
+ * Follows the SSE spec: fields accumulate per-message block; an empty line
108
+ * dispatches the block. Handles streams that end without a trailing newline
109
+ * by flushing the remaining buffer on EOF.
110
+ */
111
+ async function readSseStream(reader, signal, onVisemes) {
112
+ const decoder = new TextDecoder();
113
+ let buffer = "";
114
+ // Per-message accumulator (reset on empty-line dispatch)
115
+ let eventType = null;
116
+ let dataLines = [];
117
+ const dispatchBlock = () => {
118
+ if (eventType === "visemes" && dataLines.length > 0) {
119
+ const jsonText = dataLines.join("\n");
120
+ try {
121
+ const payload = JSON.parse(jsonText);
122
+ console.log("[VisemeSSE] received", {
123
+ requestId: payload.requestId,
124
+ cues: Array.isArray(payload.cues) ? payload.cues.length : 0,
125
+ durationMs: payload.durationMs ?? null,
126
+ });
127
+ onVisemes(payload);
128
+ }
129
+ catch (parseErr) {
130
+ console.warn("[VisemeSSE] JSON parse error", parseErr);
131
+ }
132
+ }
133
+ eventType = null;
134
+ dataLines = [];
135
+ };
136
+ const processLines = (chunk) => {
137
+ const lines = chunk.split("\n");
138
+ for (const rawLine of lines) {
139
+ const line = rawLine.trimEnd();
140
+ if (line === "") {
141
+ // Empty line = end of SSE message block → dispatch
142
+ dispatchBlock();
143
+ }
144
+ else if (line.startsWith("event:")) {
145
+ eventType = line.slice("event:".length).trim();
146
+ }
147
+ else if (line.startsWith("data:")) {
148
+ dataLines.push(line.slice("data:".length).trimStart());
149
+ }
150
+ // Ignore comment lines (":"), id:, retry: fields
151
+ }
152
+ };
153
+ while (!signal.aborted) {
154
+ const { done, value } = await reader.read();
155
+ if (done) {
156
+ // Flush any remaining buffered content without a trailing newline
157
+ if (buffer) {
158
+ processLines(buffer + "\n");
159
+ buffer = "";
160
+ }
161
+ break;
162
+ }
163
+ const chunk = decoder.decode(value, { stream: true });
164
+ // Combine with any previous incomplete line, then split on newlines.
165
+ // Keep the last (potentially incomplete) segment in the buffer.
166
+ const combined = buffer + chunk;
167
+ const lastNewline = combined.lastIndexOf("\n");
168
+ if (lastNewline === -1) {
169
+ buffer = combined;
170
+ continue;
171
+ }
172
+ buffer = combined.slice(lastNewline + 1);
173
+ processLines(combined.slice(0, lastNewline + 1));
174
+ }
175
+ }
176
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
177
+ function sleep(ms, signal) {
178
+ return new Promise((resolve, reject) => {
179
+ const id = setTimeout(resolve, ms);
180
+ signal.addEventListener("abort", () => {
181
+ clearTimeout(id);
182
+ reject(Object.assign(new Error("AbortError"), { name: "AbortError" }));
183
+ }, { once: true });
184
+ });
185
+ }
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.2",
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
  },