talking-head-studio 0.2.8 → 0.3.0

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 (38) hide show
  1. package/dist/TalkingHead.d.ts +8 -0
  2. package/dist/TalkingHead.js +104 -7
  3. package/dist/TalkingHead.web.d.ts +3 -2
  4. package/dist/TalkingHead.web.js +17 -2
  5. package/dist/TalkingHeadVisualization.d.ts +35 -0
  6. package/dist/TalkingHeadVisualization.js +277 -0
  7. package/dist/api/index.d.ts +2 -0
  8. package/dist/api/index.js +18 -0
  9. package/dist/api/studioApi.d.ts +38 -0
  10. package/dist/api/studioApi.js +235 -0
  11. package/dist/api/types.d.ts +87 -0
  12. package/dist/api/types.js +5 -0
  13. package/dist/assets/face-squeeze-local.glb +0 -0
  14. package/dist/filament/FilamentAvatar.d.ts +41 -0
  15. package/dist/filament/FilamentAvatar.js +737 -0
  16. package/dist/filament/faceSqueezeAssets.d.ts +1 -0
  17. package/dist/filament/faceSqueezeAssets.js +5 -0
  18. package/dist/filament/index.d.ts +5 -0
  19. package/dist/filament/index.js +22 -0
  20. package/dist/filament/morphTables.d.ts +5 -0
  21. package/dist/filament/morphTables.js +93 -0
  22. package/dist/filament/useAuthedFilamentUri.d.ts +11 -0
  23. package/dist/filament/useAuthedFilamentUri.js +126 -0
  24. package/dist/html.d.ts +7 -0
  25. package/dist/html.js +255 -56
  26. package/dist/index.d.ts +9 -2
  27. package/dist/index.js +13 -2
  28. package/dist/index.web.d.ts +6 -2
  29. package/dist/index.web.js +10 -2
  30. package/dist/utils/avatarUtils.d.ts +13 -0
  31. package/dist/utils/avatarUtils.js +56 -0
  32. package/dist/wardrobe/index.d.ts +2 -0
  33. package/dist/wardrobe/index.js +20 -0
  34. package/dist/wardrobe/useAvatarWardrobeHydration.d.ts +7 -0
  35. package/dist/wardrobe/useAvatarWardrobeHydration.js +34 -0
  36. package/dist/wardrobe/wardrobeStore.d.ts +30 -0
  37. package/dist/wardrobe/wardrobeStore.js +106 -0
  38. package/package.json +33 -4
@@ -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
+ }
@@ -0,0 +1,87 @@
1
+ export interface EquippedAccessory {
2
+ asset_id: string;
3
+ bone: string;
4
+ position: [number, number, number];
5
+ rotation: [number, number, number];
6
+ scale: number;
7
+ }
8
+ export interface AvatarAppearance {
9
+ version: 1;
10
+ hairColor?: string;
11
+ skinColor?: string;
12
+ eyeColor?: string;
13
+ equippedAccessories?: EquippedAccessory[];
14
+ }
15
+ export interface Avatar {
16
+ id: string;
17
+ name: string;
18
+ description: string | null;
19
+ url: string;
20
+ thumbnail_url: string | null;
21
+ animated_thumbnail_url: string | null;
22
+ is_public: boolean;
23
+ restricted: boolean;
24
+ file_size: number;
25
+ default_voice_profile_id: string | null;
26
+ appearance: AvatarAppearance | null;
27
+ created_at: string;
28
+ updated_at: string;
29
+ }
30
+ export interface PublicAvatar {
31
+ id: string;
32
+ name: string;
33
+ description: string | null;
34
+ thumbnail_url: string | null;
35
+ animated_thumbnail_url: string | null;
36
+ owner_user_id: string;
37
+ voice_name: string | null;
38
+ voice_language: string | null;
39
+ created_at: string;
40
+ }
41
+ export interface ProfileSample {
42
+ id: string;
43
+ profile_id: string;
44
+ audio_path: string;
45
+ reference_text: string;
46
+ }
47
+ export interface VoiceProfile {
48
+ id: string;
49
+ name: string;
50
+ description: string | null;
51
+ language: string;
52
+ avatar_path: string | null;
53
+ avatar_model_id: string | null;
54
+ created_at: string;
55
+ updated_at: string;
56
+ }
57
+ export interface AvatarUpdate {
58
+ name?: string;
59
+ description?: string;
60
+ is_public?: boolean;
61
+ appearance?: AvatarAppearance | null;
62
+ }
63
+ export interface WearableAsset {
64
+ id: string;
65
+ name: string;
66
+ category: string;
67
+ type: string;
68
+ slot: string;
69
+ storage_key: string;
70
+ file_size: number;
71
+ url: string;
72
+ attach_bone: string | null;
73
+ offset_position: number[] | null;
74
+ offset_rotation: number[] | null;
75
+ hides_body_parts: string[] | null;
76
+ triangle_count: number | null;
77
+ created_at: string;
78
+ updated_at: string;
79
+ }
80
+ export interface PlacementSuggestion {
81
+ bone: string;
82
+ position: [number, number, number];
83
+ rotation: [number, number, number];
84
+ confidence: number;
85
+ reasoning: string;
86
+ }
87
+ export type AssetType = 'skinned' | 'rigid';
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ // ---------------------------------------------------------------------------
3
+ // Avatar API types — moved from siteclaw/lib/studioApi.ts
4
+ // ---------------------------------------------------------------------------
5
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,41 @@
1
+ /**
2
+ * FilamentAvatar — native Filament-based avatar renderer.
3
+ *
4
+ * Replaces the TalkingHead WebView on iOS/Android with direct Filament
5
+ * morph target writes — no WebView bridge, no JS→native→JS roundtrip,
6
+ * visemes applied at 60fps in React Native JS.
7
+ *
8
+ * Implements the full TalkingHeadRef interface so it can be swapped in
9
+ * transparently for the WebView renderer.
10
+ */
11
+ import React from 'react';
12
+ import { ViewStyle, StyleProp } from 'react-native';
13
+ import type { TalkingHeadViseme, TalkingHeadVisemeSchedule, TalkingHeadAccessory, TalkingHeadMood } from '../TalkingHead';
14
+ export declare const CAMERA_FOCAL_FULL = 28;
15
+ export declare const CAMERA_FOCAL_PIP = 50;
16
+ export interface FilamentAvatarRef {
17
+ sendViseme: (viseme: TalkingHeadViseme, weight?: number) => void;
18
+ scheduleVisemes: (schedule: TalkingHeadVisemeSchedule) => void;
19
+ clearVisemes: () => void;
20
+ sendAmplitude: (amplitude: number) => void;
21
+ setMood: (mood: TalkingHeadMood) => void;
22
+ setHairColor: (color: string) => void;
23
+ setSkinColor: (color: string) => void;
24
+ setEyeColor: (color: string) => void;
25
+ setAccessories: (accessories: TalkingHeadAccessory[]) => void;
26
+ }
27
+ interface FilamentAvatarProps {
28
+ style?: StyleProp<ViewStyle>;
29
+ avatarUrl: string | null;
30
+ aspect?: number;
31
+ focalLength?: number;
32
+ mood?: TalkingHeadMood;
33
+ hairColor?: string;
34
+ skinColor?: string;
35
+ eyeColor?: string;
36
+ accessories?: TalkingHeadAccessory[];
37
+ onReady?: () => void;
38
+ onError?: (message: string) => void;
39
+ }
40
+ export declare const FilamentAvatar: React.ForwardRefExoticComponent<FilamentAvatarProps & React.RefAttributes<FilamentAvatarRef>>;
41
+ export {};