talking-head-studio 0.2.9 → 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.
Files changed (35) hide show
  1. package/dist/TalkingHead.js +30 -2
  2. package/dist/TalkingHeadVisualization.d.ts +35 -0
  3. package/dist/TalkingHeadVisualization.js +277 -0
  4. package/dist/api/index.d.ts +2 -0
  5. package/dist/api/index.js +18 -0
  6. package/dist/api/studioApi.d.ts +38 -0
  7. package/dist/api/studioApi.js +235 -0
  8. package/dist/api/types.d.ts +87 -0
  9. package/dist/api/types.js +5 -0
  10. package/dist/assets/face-squeeze-local.glb +0 -0
  11. package/dist/filament/FilamentAvatar.d.ts +41 -0
  12. package/dist/filament/FilamentAvatar.js +737 -0
  13. package/dist/filament/faceSqueezeAssets.d.ts +1 -0
  14. package/dist/filament/faceSqueezeAssets.js +5 -0
  15. package/dist/filament/index.d.ts +6 -0
  16. package/dist/filament/index.js +24 -0
  17. package/dist/filament/morphTables.d.ts +5 -0
  18. package/dist/filament/morphTables.js +93 -0
  19. package/dist/filament/useAuthedFilamentUri.d.ts +11 -0
  20. package/dist/filament/useAuthedFilamentUri.js +126 -0
  21. package/dist/index.d.ts +10 -1
  22. package/dist/index.js +15 -2
  23. package/dist/index.web.d.ts +5 -1
  24. package/dist/index.web.js +10 -2
  25. package/dist/tts/useDirectVisemeStream.d.ts +21 -0
  26. package/dist/tts/useDirectVisemeStream.js +119 -0
  27. package/dist/utils/avatarUtils.d.ts +13 -0
  28. package/dist/utils/avatarUtils.js +56 -0
  29. package/dist/wardrobe/index.d.ts +2 -0
  30. package/dist/wardrobe/index.js +20 -0
  31. package/dist/wardrobe/useAvatarWardrobeHydration.d.ts +7 -0
  32. package/dist/wardrobe/useAvatarWardrobeHydration.js +34 -0
  33. package/dist/wardrobe/wardrobeStore.d.ts +30 -0
  34. package/dist/wardrobe/wardrobeStore.js +106 -0
  35. package/package.json +34 -2
@@ -57,14 +57,42 @@ exports.TalkingHead = (0, react_1.forwardRef)(({ avatarUrl, authToken, mood = 'n
57
57
  }
58
58
  };
59
59
  }, [avatarUrl, authToken, vendorBaseUrl]);
60
- const post = (0, react_1.useCallback)((msg) => {
60
+ const postQueue = (0, react_1.useRef)([]);
61
+ const flushTimer = (0, react_1.useRef)(null);
62
+ (0, react_1.useEffect)(() => () => {
63
+ if (flushTimer.current)
64
+ clearTimeout(flushTimer.current);
65
+ }, []);
66
+ const flushQueue = (0, react_1.useCallback)(() => {
67
+ flushTimer.current = null;
61
68
  try {
62
- webViewRef.current?.postMessage(JSON.stringify(msg));
69
+ const wv = webViewRef.current;
70
+ if (!wv)
71
+ return;
72
+ const msgs = postQueue.current.splice(0);
73
+ if (msgs.length === 0)
74
+ return;
75
+ // Batch all pending messages into a single injectJavaScript call
76
+ const js = msgs
77
+ .map((m) => {
78
+ const json = JSON.stringify(JSON.stringify(m));
79
+ return `window.dispatchEvent(new MessageEvent('message',{data:${json}}))`;
80
+ })
81
+ .join(';');
82
+ wv.injectJavaScript(js + ';true;');
63
83
  }
64
84
  catch {
65
85
  // WebView ref frozen/invalidated during unmount — ignore
86
+ postQueue.current = [];
66
87
  }
67
88
  }, []);
89
+ const post = (0, react_1.useCallback)((msg) => {
90
+ postQueue.current.push(msg);
91
+ // Coalesce into a single microtask to avoid flooding injectJavaScript
92
+ if (!flushTimer.current) {
93
+ flushTimer.current = setTimeout(flushQueue, 0);
94
+ }
95
+ }, [flushQueue]);
68
96
  (0, react_1.useImperativeHandle)(ref, () => ({
69
97
  sendAmplitude: (amplitude) => post({ type: 'amplitude', value: amplitude }),
70
98
  sendViseme: (viseme, weight = 1.0) => post({ type: 'viseme', viseme, weight }),
@@ -0,0 +1,35 @@
1
+ import React from 'react';
2
+ import { ViewStyle, StyleProp } from 'react-native';
3
+ import { type TalkingHeadMood, type TalkingHeadAccessory, type TalkingHeadViseme, type TalkingHeadVisemeSchedule } from './TalkingHead';
4
+ interface TalkingHeadVisualizationProps {
5
+ style?: StyleProp<ViewStyle>;
6
+ avatarUrl: string | null;
7
+ authToken?: string;
8
+ cameraView?: 'head' | 'upper' | 'full';
9
+ cameraDistance?: number;
10
+ accessories?: TalkingHeadAccessory[];
11
+ mood?: TalkingHeadMood;
12
+ aspect?: number;
13
+ focalLength?: number;
14
+ visemeSchedule?: TalkingHeadVisemeSchedule | null;
15
+ onVisemeScheduleApplied?: (info: {
16
+ requestId: string | null;
17
+ appliedAtMs: number;
18
+ }) => void;
19
+ vendorBaseUrl?: string | null;
20
+ }
21
+ export interface TalkingHeadVisualizationRef {
22
+ setMood: (mood: TalkingHeadMood) => void;
23
+ sendAmplitude: (amplitude: number) => void;
24
+ sendViseme: (viseme: TalkingHeadViseme, weight?: number) => void;
25
+ scheduleVisemes: (schedule: TalkingHeadVisemeSchedule) => void;
26
+ clearVisemes: () => void;
27
+ }
28
+ /**
29
+ * TalkingHeadVisualization — optimized component for rendering the 3D avatar.
30
+ *
31
+ * On native: uses FilamentAvatar (direct morph writes, no WebView bridge).
32
+ * On web: uses TalkingHead WebView renderer.
33
+ */
34
+ export declare const TalkingHeadVisualization: React.ForwardRefExoticComponent<TalkingHeadVisualizationProps & React.RefAttributes<TalkingHeadVisualizationRef>>;
35
+ export {};
@@ -0,0 +1,277 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TalkingHeadVisualization = void 0;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ /* eslint-disable @typescript-eslint/no-unused-expressions */
6
+ const react_1 = require("react");
7
+ const react_native_1 = require("react-native");
8
+ const TalkingHead_1 = require("./TalkingHead");
9
+ const FilamentAvatar_1 = require("./filament/FilamentAvatar");
10
+ const avatarUtils_1 = require("./utils/avatarUtils");
11
+ const faceSqueezeAssets_1 = require("./filament/faceSqueezeAssets");
12
+ // Cached fallback data URI — resolved once, reused across all instances
13
+ let _fallbackDataUri = null;
14
+ let _fallbackPromise = null;
15
+ function getFallbackAvatarUrl() {
16
+ if (_fallbackDataUri)
17
+ return Promise.resolve(_fallbackDataUri);
18
+ if (!_fallbackPromise) {
19
+ _fallbackPromise = (0, avatarUtils_1.resolveLocalAssetUrl)(faceSqueezeAssets_1.FACE_SQUEEZE_LOCAL_MODULE).then((uri) => {
20
+ _fallbackDataUri = uri;
21
+ return uri;
22
+ });
23
+ }
24
+ return _fallbackPromise;
25
+ }
26
+ function getLoadingLabel(stage) {
27
+ switch (stage) {
28
+ case 'fetching_model':
29
+ return 'Fetching avatar';
30
+ case 'loading_avatar':
31
+ return 'Loading avatar';
32
+ case 'loading_fallback':
33
+ return 'Loading viewer';
34
+ case 'ready':
35
+ return 'Ready';
36
+ case 'booting':
37
+ default:
38
+ return 'Starting avatar';
39
+ }
40
+ }
41
+ /**
42
+ * TalkingHeadVisualization — optimized component for rendering the 3D avatar.
43
+ *
44
+ * On native: uses FilamentAvatar (direct morph writes, no WebView bridge).
45
+ * On web: uses TalkingHead WebView renderer.
46
+ */
47
+ exports.TalkingHeadVisualization = (0, react_1.forwardRef)(({ style, avatarUrl, authToken, cameraView = 'head', cameraDistance = 0.2, accessories, mood: initialMood = 'neutral', aspect, focalLength, visemeSchedule, onVisemeScheduleApplied, vendorBaseUrl }, ref) => {
48
+ const avatarRef = (0, react_1.useRef)(null);
49
+ // On native, Filament ref is wired via callback ref — store it here so
50
+ // scheduleVisemes / sendAmplitude can route to it.
51
+ const filamentRef = (0, react_1.useRef)(null);
52
+ // Unified accessor — Filament on native, WebView on web
53
+ const activeAvatar = (0, react_1.useCallback)(() => (filamentRef.current ?? avatarRef.current), []);
54
+ // Fallback local GLB data URI — resolved once on mount
55
+ const [fallbackUrl, setFallbackUrl] = (0, react_1.useState)(_fallbackDataUri);
56
+ (0, react_1.useEffect)(() => {
57
+ if (_fallbackDataUri) {
58
+ setFallbackUrl(_fallbackDataUri);
59
+ return;
60
+ }
61
+ getFallbackAvatarUrl().then((u) => u && setFallbackUrl(u));
62
+ }, []);
63
+ const [mood, setMood] = (0, react_1.useState)(initialMood);
64
+ const [isAvatarReady, setIsAvatarReady] = (0, react_1.useState)(false);
65
+ const [avatarError, setAvatarError] = (0, react_1.useState)(null);
66
+ const [useFallback, setUseFallback] = (0, react_1.useState)(false);
67
+ const [loadingState, setLoadingState] = (0, react_1.useState)({
68
+ stage: 'booting',
69
+ progress: null,
70
+ });
71
+ const pendingVisemeScheduleRef = (0, react_1.useRef)(null);
72
+ const lastScheduledVisemeKeyRef = (0, react_1.useRef)(null);
73
+ const handleReady = (0, react_1.useCallback)(() => {
74
+ setAvatarError(null);
75
+ setIsAvatarReady(true);
76
+ setLoadingState({ stage: 'ready', progress: 100 });
77
+ const pendingSchedule = pendingVisemeScheduleRef.current;
78
+ if (!pendingSchedule)
79
+ return;
80
+ const ageMs = typeof pendingSchedule.startedAtMs === 'number'
81
+ ? Math.max(0, Date.now() - pendingSchedule.startedAtMs)
82
+ : null;
83
+ const durationMs = pendingSchedule.durationMs ?? 0;
84
+ // Drop if audio already finished playing
85
+ if (ageMs != null && durationMs > 0 && ageMs > durationMs + 500) {
86
+ __DEV__ && console.log('[TalkingHead] Dropping expired pending viseme schedule', {
87
+ requestId: pendingSchedule.requestId ?? null,
88
+ ageMs,
89
+ durationMs,
90
+ });
91
+ pendingVisemeScheduleRef.current = null;
92
+ return;
93
+ }
94
+ __DEV__ && console.log('[TalkingHead] Applying pending viseme schedule', {
95
+ requestId: pendingSchedule.requestId ?? null,
96
+ cues: pendingSchedule.cues.length,
97
+ ageMs,
98
+ });
99
+ activeAvatar()?.scheduleVisemes(pendingSchedule);
100
+ onVisemeScheduleApplied?.({
101
+ requestId: pendingSchedule.requestId ?? null,
102
+ appliedAtMs: Date.now(),
103
+ });
104
+ lastScheduledVisemeKeyRef.current = `${pendingSchedule.requestId ?? 'anonymous'}:${pendingSchedule.startedAtMs ?? 0}`;
105
+ pendingVisemeScheduleRef.current = null;
106
+ }, [onVisemeScheduleApplied, activeAvatar]);
107
+ (0, react_1.useImperativeHandle)(ref, () => ({
108
+ setMood: (m) => setMood(m),
109
+ sendAmplitude: (a) => activeAvatar()?.sendAmplitude(a),
110
+ sendViseme: (viseme, weight) => activeAvatar()?.sendViseme(viseme, weight),
111
+ clearVisemes: () => activeAvatar()?.clearVisemes(),
112
+ scheduleVisemes: (schedule) => {
113
+ const scheduleKey = `${schedule.requestId ?? 'anonymous'}:${schedule.startedAtMs ?? 0}`;
114
+ if (lastScheduledVisemeKeyRef.current === scheduleKey)
115
+ return;
116
+ const av = activeAvatar();
117
+ // Filament buffers pending schedules internally — no ready gate needed.
118
+ // WebView (avatarRef) still needs the ready gate.
119
+ if (!av || (!filamentRef.current && !isAvatarReady)) {
120
+ pendingVisemeScheduleRef.current = schedule;
121
+ return;
122
+ }
123
+ av.scheduleVisemes(schedule);
124
+ onVisemeScheduleApplied?.({ requestId: schedule.requestId ?? null, appliedAtMs: Date.now() });
125
+ lastScheduledVisemeKeyRef.current = scheduleKey;
126
+ },
127
+ }), [isAvatarReady, onVisemeScheduleApplied, activeAvatar]);
128
+ (0, react_1.useEffect)(() => {
129
+ setMood(initialMood);
130
+ }, [initialMood]);
131
+ (0, react_1.useEffect)(() => {
132
+ setIsAvatarReady(false);
133
+ setAvatarError(null);
134
+ setUseFallback(false);
135
+ setLoadingState({ stage: 'booting', progress: null });
136
+ pendingVisemeScheduleRef.current = null;
137
+ lastScheduledVisemeKeyRef.current = null;
138
+ }, [avatarUrl]);
139
+ const handleError = (0, react_1.useCallback)((message) => {
140
+ console.warn('[TalkingHeadVisualization] Avatar load failed, switching to fallback:', message);
141
+ setAvatarError(message);
142
+ setUseFallback(true);
143
+ }, []);
144
+ // Effective avatar URL: use remote if available, fallback GLB if error or missing
145
+ const effectiveAvatarUrl = useFallback || !avatarUrl ? fallbackUrl : avatarUrl;
146
+ const handleLoadingChange = (0, react_1.useCallback)((nextState) => {
147
+ setLoadingState({
148
+ stage: nextState.stage,
149
+ progress: typeof nextState.progress === 'number' && Number.isFinite(nextState.progress)
150
+ ? Math.max(0, Math.min(100, Math.round(nextState.progress)))
151
+ : null,
152
+ });
153
+ }, []);
154
+ (0, react_1.useEffect)(() => {
155
+ if (!visemeSchedule || visemeSchedule.cues.length === 0)
156
+ return;
157
+ const scheduleKey = `${visemeSchedule.requestId ?? 'anonymous'}:${visemeSchedule.startedAtMs ?? 0}`;
158
+ if (lastScheduledVisemeKeyRef.current === scheduleKey)
159
+ return;
160
+ __DEV__ && console.log('[TalkingHead] Received viseme schedule', {
161
+ requestId: visemeSchedule.requestId ?? null,
162
+ cues: visemeSchedule.cues.length,
163
+ ageMs: typeof visemeSchedule.startedAtMs === 'number'
164
+ ? Math.max(0, Date.now() - visemeSchedule.startedAtMs)
165
+ : null,
166
+ ready: isAvatarReady,
167
+ });
168
+ const av = activeAvatar();
169
+ if (!av || (!filamentRef.current && !isAvatarReady)) {
170
+ pendingVisemeScheduleRef.current = visemeSchedule;
171
+ return;
172
+ }
173
+ __DEV__ && console.log('[TalkingHead] Scheduling visemes immediately', {
174
+ requestId: visemeSchedule.requestId ?? null,
175
+ cues: visemeSchedule.cues.length,
176
+ });
177
+ av.scheduleVisemes(visemeSchedule);
178
+ onVisemeScheduleApplied?.({
179
+ requestId: visemeSchedule.requestId ?? null,
180
+ appliedAtMs: Date.now(),
181
+ });
182
+ lastScheduledVisemeKeyRef.current = scheduleKey;
183
+ }, [isAvatarReady, onVisemeScheduleApplied, visemeSchedule, activeAvatar]);
184
+ // On native use Filament — direct morph writes, no WebView bridge.
185
+ if (react_native_1.Platform.OS !== 'web') {
186
+ return ((0, jsx_runtime_1.jsx)(FilamentAvatar_1.FilamentAvatar, { focalLength: focalLength, ref: (fr) => {
187
+ filamentRef.current = fr;
188
+ if (typeof ref === 'function')
189
+ ref(fr);
190
+ else if (ref)
191
+ ref.current = fr;
192
+ if (fr && pendingVisemeScheduleRef.current) {
193
+ fr.scheduleVisemes(pendingVisemeScheduleRef.current);
194
+ pendingVisemeScheduleRef.current = null;
195
+ }
196
+ }, style: style, avatarUrl: avatarUrl ?? null, aspect: aspect, mood: mood, accessories: accessories, onReady: handleReady, onError: handleError }));
197
+ }
198
+ if (!effectiveAvatarUrl) {
199
+ return ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: [style, styles.placeholder] }));
200
+ }
201
+ return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [style, styles.container], pointerEvents: "box-none", children: [(0, jsx_runtime_1.jsx)(TalkingHead_1.TalkingHead, { ref: avatarRef, avatarUrl: effectiveAvatarUrl, authToken: authToken, cameraView: cameraView, cameraDistance: cameraDistance, accessories: accessories, mood: mood, onLoadingChange: handleLoadingChange, onReady: handleReady, onError: handleError, style: react_native_1.StyleSheet.absoluteFill, vendorBaseUrl: vendorBaseUrl }), !isAvatarReady && ((0, jsx_runtime_1.jsx)(react_native_1.View, { testID: "talking-head-loading", style: styles.loadingOverlay, pointerEvents: "none", children: (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.loadingCard, children: [(0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.loadingBadge, children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.loadingBadgeText, children: "AVATAR" }) }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.loadingTitle, children: avatarError ? 'Avatar failed to load' : getLoadingLabel(loadingState.stage) }), avatarError ? ((0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.loadingHint, children: avatarError })) : typeof loadingState.progress === 'number' ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)(react_native_1.Text, { style: styles.loadingPercent, children: [loadingState.progress, "%"] }), (0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.progressTrack, children: (0, jsx_runtime_1.jsx)(react_native_1.View, { style: [
202
+ styles.progressFill,
203
+ { width: `${Math.max(6, loadingState.progress)}%` },
204
+ ] }) })] })) : ((0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.loadingHint, children: "Preparing the avatar scene\u2026" }))] }) }))] }));
205
+ });
206
+ exports.TalkingHeadVisualization.displayName = 'TalkingHeadVisualization';
207
+ const styles = react_native_1.StyleSheet.create({
208
+ container: {
209
+ overflow: 'hidden',
210
+ },
211
+ loadingOverlay: {
212
+ ...react_native_1.StyleSheet.absoluteFillObject,
213
+ alignItems: 'center',
214
+ justifyContent: 'center',
215
+ backgroundColor: 'rgba(5, 10, 18, 0.42)',
216
+ padding: 20,
217
+ },
218
+ loadingCard: {
219
+ minWidth: 190,
220
+ maxWidth: 240,
221
+ borderRadius: 20,
222
+ paddingHorizontal: 18,
223
+ paddingVertical: 16,
224
+ backgroundColor: 'rgba(13, 20, 31, 0.88)',
225
+ borderWidth: 1,
226
+ borderColor: 'rgba(151, 163, 184, 0.18)',
227
+ alignItems: 'center',
228
+ },
229
+ loadingBadge: {
230
+ paddingHorizontal: 8,
231
+ paddingVertical: 4,
232
+ borderRadius: 999,
233
+ backgroundColor: 'rgba(83, 156, 255, 0.16)',
234
+ marginBottom: 10,
235
+ },
236
+ loadingBadgeText: {
237
+ color: '#c7dbff',
238
+ fontSize: 10,
239
+ fontWeight: '700',
240
+ letterSpacing: 1.1,
241
+ },
242
+ loadingTitle: {
243
+ color: '#f8fbff',
244
+ fontSize: 18,
245
+ fontWeight: '700',
246
+ },
247
+ loadingPercent: {
248
+ marginTop: 8,
249
+ color: '#7dd3fc',
250
+ fontSize: 28,
251
+ fontWeight: '800',
252
+ },
253
+ loadingHint: {
254
+ marginTop: 10,
255
+ color: 'rgba(226, 232, 240, 0.82)',
256
+ fontSize: 13,
257
+ textAlign: 'center',
258
+ },
259
+ progressTrack: {
260
+ width: '100%',
261
+ height: 8,
262
+ borderRadius: 999,
263
+ backgroundColor: 'rgba(148, 163, 184, 0.18)',
264
+ marginTop: 12,
265
+ overflow: 'hidden',
266
+ },
267
+ progressFill: {
268
+ height: '100%',
269
+ borderRadius: 999,
270
+ backgroundColor: '#5eead4',
271
+ },
272
+ placeholder: {
273
+ backgroundColor: 'rgba(30,34,53,0.5)',
274
+ alignItems: 'center',
275
+ justifyContent: 'center',
276
+ },
277
+ });
@@ -0,0 +1,2 @@
1
+ export * from './types';
2
+ export * from './studioApi';
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./types"), exports);
18
+ __exportStar(require("./studioApi"), exports);
@@ -0,0 +1,38 @@
1
+ import type { Avatar, AvatarUpdate, PublicAvatar, VoiceProfile, ProfileSample, WearableAsset, PlacementSuggestion } from './types';
2
+ export declare function configureAvatarApi(opts: {
3
+ baseUrl?: string;
4
+ getToken: () => Promise<string | null>;
5
+ }): void;
6
+ export declare function getToken(): Promise<string | null>;
7
+ export declare function studioFetch<T>(path: string, options?: RequestInit): Promise<T>;
8
+ export declare function getMyAvatars(): Promise<Avatar[]>;
9
+ export declare function getAvatar(id: string): Promise<Avatar>;
10
+ export declare function updateAvatar(id: string, data: AvatarUpdate): Promise<Avatar>;
11
+ export declare function deleteAvatar(id: string): Promise<void>;
12
+ export declare function createAvatar(fileUri: string, name: string, description?: string): Promise<Avatar>;
13
+ export declare function getPublicAvatars(): Promise<PublicAvatar[]>;
14
+ export declare function getVoiceProfileSamples(profileId: string): Promise<ProfileSample[]>;
15
+ export declare function getVoiceProfiles(): Promise<VoiceProfile[]>;
16
+ export declare function setDefaultVoice(avatarId: string, profileId: string): Promise<Avatar>;
17
+ export declare function removeDefaultVoice(avatarId: string): Promise<Avatar>;
18
+ export declare function avatarFileUrl(avatar: Avatar): string;
19
+ export declare function avatarThumbnailUrl(avatar: Avatar | PublicAvatar): string | null;
20
+ export declare function avatarAnimatedThumbnailUrl(avatar: Avatar | PublicAvatar): string | null;
21
+ export declare function thumbnailHeaders(): Promise<Record<string, string>>;
22
+ export declare function listAssets(category?: string): Promise<WearableAsset[]>;
23
+ export declare function getAsset(id: string): Promise<WearableAsset>;
24
+ export declare function uploadAsset(fileUri: string, meta: {
25
+ name: string;
26
+ category: string;
27
+ type: 'skinned' | 'rigid';
28
+ slot?: string;
29
+ attach_bone?: string;
30
+ offset_position?: number[];
31
+ offset_rotation?: number[];
32
+ hides_body_parts?: string[];
33
+ }): Promise<WearableAsset>;
34
+ export declare function deleteAsset(id: string): Promise<void>;
35
+ export declare function suggestPlacement(assetId: string, avatarId: string): Promise<PlacementSuggestion>;
36
+ export declare function assetFileUrl(asset: WearableAsset): string;
37
+ export declare function createVoiceProfile(name: string, language?: string): Promise<VoiceProfile>;
38
+ export declare function uploadVoiceSample(profileId: string, fileUri: string, fileName: string, referenceText: string): Promise<VoiceProfile>;
@@ -0,0 +1,235 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.configureAvatarApi = configureAvatarApi;
4
+ exports.getToken = getToken;
5
+ exports.studioFetch = studioFetch;
6
+ exports.getMyAvatars = getMyAvatars;
7
+ exports.getAvatar = getAvatar;
8
+ exports.updateAvatar = updateAvatar;
9
+ exports.deleteAvatar = deleteAvatar;
10
+ exports.createAvatar = createAvatar;
11
+ exports.getPublicAvatars = getPublicAvatars;
12
+ exports.getVoiceProfileSamples = getVoiceProfileSamples;
13
+ exports.getVoiceProfiles = getVoiceProfiles;
14
+ exports.setDefaultVoice = setDefaultVoice;
15
+ exports.removeDefaultVoice = removeDefaultVoice;
16
+ exports.avatarFileUrl = avatarFileUrl;
17
+ exports.avatarThumbnailUrl = avatarThumbnailUrl;
18
+ exports.avatarAnimatedThumbnailUrl = avatarAnimatedThumbnailUrl;
19
+ exports.thumbnailHeaders = thumbnailHeaders;
20
+ exports.listAssets = listAssets;
21
+ exports.getAsset = getAsset;
22
+ exports.uploadAsset = uploadAsset;
23
+ exports.deleteAsset = deleteAsset;
24
+ exports.suggestPlacement = suggestPlacement;
25
+ exports.assetFileUrl = assetFileUrl;
26
+ exports.createVoiceProfile = createVoiceProfile;
27
+ exports.uploadVoiceSample = uploadVoiceSample;
28
+ // ---------------------------------------------------------------------------
29
+ // Injectable configuration
30
+ // ---------------------------------------------------------------------------
31
+ let _baseUrl = process.env.EXPO_PUBLIC_BACKEND_URL ?? '';
32
+ let _getToken = null;
33
+ function configureAvatarApi(opts) {
34
+ if (opts.baseUrl !== undefined)
35
+ _baseUrl = opts.baseUrl;
36
+ _getToken = opts.getToken;
37
+ }
38
+ async function getToken() {
39
+ return _getToken ? _getToken() : null;
40
+ }
41
+ // ---------------------------------------------------------------------------
42
+ // Core fetch wrappers
43
+ // ---------------------------------------------------------------------------
44
+ async function studioFetch(path, options) {
45
+ const token = await getToken();
46
+ const headers = {
47
+ ...options?.headers,
48
+ };
49
+ if (token) {
50
+ headers['Authorization'] = `Bearer ${token}`;
51
+ }
52
+ const res = await fetch(`${_baseUrl}${path}`, {
53
+ ...options,
54
+ headers,
55
+ });
56
+ if (!res.ok) {
57
+ let detail = '';
58
+ try {
59
+ const body = await res.clone().json();
60
+ detail = body?.detail || body?.message || JSON.stringify(body);
61
+ }
62
+ catch {
63
+ detail = await res.text().catch(() => '');
64
+ }
65
+ throw new Error(`Studio API error ${res.status}: ${path}${detail ? ` — ${detail}` : ''}`);
66
+ }
67
+ // For 204 No Content (e.g. DELETE), return undefined as T
68
+ if (res.status === 204) {
69
+ return undefined;
70
+ }
71
+ return res.json();
72
+ }
73
+ /** POST/PUT with FormData, sharing auth token logic. */
74
+ async function studioFetchForm(path, method, formData) {
75
+ const token = await getToken();
76
+ const res = await fetch(`${_baseUrl}${path}`, {
77
+ method,
78
+ headers: token ? { Authorization: `Bearer ${token}` } : {},
79
+ body: formData,
80
+ });
81
+ if (!res.ok) {
82
+ let detail = '';
83
+ try {
84
+ const body = await res.clone().json();
85
+ detail = body?.detail || body?.message || JSON.stringify(body);
86
+ }
87
+ catch {
88
+ detail = await res.text().catch(() => '');
89
+ }
90
+ throw new Error(`Studio API error ${res.status}: ${path}${detail ? ` — ${detail}` : ''}`);
91
+ }
92
+ return res.json();
93
+ }
94
+ // ---------------------------------------------------------------------------
95
+ // Avatar endpoints
96
+ // ---------------------------------------------------------------------------
97
+ function getMyAvatars() {
98
+ return studioFetch('/v1/avatars');
99
+ }
100
+ function getAvatar(id) {
101
+ return studioFetch(`/v1/avatars/${id}`);
102
+ }
103
+ function updateAvatar(id, data) {
104
+ return studioFetch(`/v1/avatars/${id}`, {
105
+ method: 'PATCH',
106
+ headers: { 'Content-Type': 'application/json' },
107
+ body: JSON.stringify(data),
108
+ });
109
+ }
110
+ function deleteAvatar(id) {
111
+ return studioFetch(`/v1/avatars/${id}`, { method: 'DELETE' });
112
+ }
113
+ async function createAvatar(fileUri, name, description) {
114
+ const formData = new FormData();
115
+ formData.append('file', {
116
+ uri: fileUri,
117
+ name: `${name}.glb`,
118
+ type: 'model/gltf-binary',
119
+ });
120
+ formData.append('name', name);
121
+ if (description) {
122
+ formData.append('description', description);
123
+ }
124
+ return studioFetchForm('/v1/avatars', 'POST', formData);
125
+ }
126
+ function getPublicAvatars() {
127
+ return studioFetch('/v1/avatars/public');
128
+ }
129
+ // ---------------------------------------------------------------------------
130
+ // Voice profile endpoints
131
+ // ---------------------------------------------------------------------------
132
+ function getVoiceProfileSamples(profileId) {
133
+ return studioFetch(`/profiles/${profileId}/samples`);
134
+ }
135
+ function getVoiceProfiles() {
136
+ return studioFetch('/profiles');
137
+ }
138
+ function setDefaultVoice(avatarId, profileId) {
139
+ return studioFetch(`/v1/avatars/${avatarId}/default-voice`, {
140
+ method: 'PUT',
141
+ headers: { 'Content-Type': 'application/json' },
142
+ body: JSON.stringify({ profile_id: profileId }),
143
+ });
144
+ }
145
+ function removeDefaultVoice(avatarId) {
146
+ return studioFetch(`/v1/avatars/${avatarId}/default-voice`, {
147
+ method: 'DELETE',
148
+ });
149
+ }
150
+ // ---------------------------------------------------------------------------
151
+ // URL helpers
152
+ // ---------------------------------------------------------------------------
153
+ function avatarFileUrl(avatar) {
154
+ const base = `${_baseUrl}${avatar.url}`;
155
+ return avatar.updated_at ? `${base}?v=${encodeURIComponent(avatar.updated_at)}` : base;
156
+ }
157
+ function avatarThumbnailUrl(avatar) {
158
+ const url = avatar.thumbnail_url || avatar.animated_thumbnail_url;
159
+ if (!url)
160
+ return null;
161
+ return `${_baseUrl}${url}`;
162
+ }
163
+ function avatarAnimatedThumbnailUrl(avatar) {
164
+ if (!avatar.animated_thumbnail_url)
165
+ return null;
166
+ return `${_baseUrl}${avatar.animated_thumbnail_url}`;
167
+ }
168
+ async function thumbnailHeaders() {
169
+ const token = await getToken();
170
+ if (!token)
171
+ return {};
172
+ return { Authorization: `Bearer ${token}` };
173
+ }
174
+ // ---------------------------------------------------------------------------
175
+ // Wearable Asset endpoints
176
+ // ---------------------------------------------------------------------------
177
+ function listAssets(category) {
178
+ const query = category ? `?category=${encodeURIComponent(category)}` : '';
179
+ return studioFetch(`/v1/assets${query}`);
180
+ }
181
+ function getAsset(id) {
182
+ return studioFetch(`/v1/assets/${id}`);
183
+ }
184
+ async function uploadAsset(fileUri, meta) {
185
+ const formData = new FormData();
186
+ formData.append('file', {
187
+ uri: fileUri,
188
+ name: `${meta.name}.glb`,
189
+ type: 'model/gltf-binary',
190
+ });
191
+ formData.append('name', meta.name);
192
+ formData.append('category', meta.category);
193
+ formData.append('type', meta.type);
194
+ if (meta.slot)
195
+ formData.append('slot', meta.slot);
196
+ if (meta.attach_bone)
197
+ formData.append('attach_bone', meta.attach_bone);
198
+ if (meta.offset_position)
199
+ formData.append('offset_position', JSON.stringify(meta.offset_position));
200
+ if (meta.offset_rotation)
201
+ formData.append('offset_rotation', JSON.stringify(meta.offset_rotation));
202
+ if (meta.hides_body_parts)
203
+ formData.append('hides_body_parts', JSON.stringify(meta.hides_body_parts));
204
+ return studioFetchForm('/v1/assets', 'POST', formData);
205
+ }
206
+ function deleteAsset(id) {
207
+ return studioFetch(`/v1/assets/${id}`, { method: 'DELETE' });
208
+ }
209
+ function suggestPlacement(assetId, avatarId) {
210
+ return studioFetch(`/v1/assets/${assetId}/suggest-placement?avatar_id=${encodeURIComponent(avatarId)}`, { method: 'POST' });
211
+ }
212
+ function assetFileUrl(asset) {
213
+ const base = `${_baseUrl}${asset.url}`;
214
+ return asset.updated_at ? `${base}?v=${encodeURIComponent(asset.updated_at)}` : base;
215
+ }
216
+ async function createVoiceProfile(name, language = 'en') {
217
+ return studioFetch('/profiles', {
218
+ method: 'POST',
219
+ headers: {
220
+ 'Content-Type': 'application/json',
221
+ },
222
+ body: JSON.stringify({ name, language }),
223
+ });
224
+ }
225
+ async function uploadVoiceSample(profileId, fileUri, fileName, referenceText) {
226
+ const formData = new FormData();
227
+ formData.append('reference_text', referenceText);
228
+ // Do NOT set Content-Type header manually — fetch sets it with the correct boundary
229
+ formData.append('file', {
230
+ uri: fileUri,
231
+ name: fileName,
232
+ type: 'audio/wav',
233
+ });
234
+ return studioFetchForm(`/profiles/${profileId}/samples`, 'POST', formData);
235
+ }