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.
package/dist/filament/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/filament/index.js
CHANGED
|
@@ -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.
|
|
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
|
},
|