@facepe/widget-mobile-app 1.1.0 → 1.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.
package/README.md CHANGED
@@ -1,289 +1,344 @@
1
- # @facepe/widget-mobile-app
2
-
3
- The official **FacePe AI Digital Human Live Streaming SDK** for **React Native** (supporting both **iOS** and **Android**).
4
-
5
- Embed interactive, photorealistic, lip-synced 3D digital avatars directly inside your native mobile apps with pure WebRTC media streaming, native microphone capture, bidirectional voice dialog, and live data events — **without webviews**.
6
-
7
- ---
8
-
9
- ## 📱 Features
10
-
11
- - **⚡ Native WebRTC Streaming**: Pure native media streaming powered by `@livekit/react-native` (no WebViews or embedded iframes).
12
- - **🗣️ Natural Two-Way Voice Dialog**: Real-time microphone capture with hardware echo cancellation, noise suppression, and server VAD.
13
- - **🍎 Full iOS Support**: Hardware-accelerated video decoding on iOS 13.0+ (iPhone & iPad).
14
- - **🤖 Full Android Support**: Hardware-accelerated video decoding on Android 7.0+ (API 24+).
15
- - **💬 Real-Time Transcriptions**: Bidirectional captions for avatar speech and user speech.
16
- - **🛒 Conversational Commerce**: Automatic detection of cart items, orders, and payment intents.
17
- - **🎛️ Imperative Controls**: Ref methods to start/stop stream, mute/unmute mic, speak text, or interrupt speech.
18
-
19
- ---
20
-
21
- ## 📦 Installation
22
-
23
- In your React Native project:
24
-
25
- ```bash
26
- npm install @facepe/widget-mobile-app @livekit/react-native @livekit/react-native-webrtc
27
- ```
28
-
29
- *(or using yarn / pnpm)*:
30
-
31
- ```bash
32
- yarn add @facepe/widget-mobile-app @livekit/react-native @livekit/react-native-webrtc
33
- ```
34
-
35
- ---
36
-
37
- ## 🛠️ Platform Permissions Setup
38
-
39
- ### iOS Setup (`ios/Podfile` & `Info.plist`)
40
-
41
- 1. Open `ios/YourAppName/Info.plist` and add permission strings for microphone and audio:
42
-
43
- ```xml
44
- <key>NSMicrophoneUsageDescription</key>
45
- <string>We need access to your microphone to converse with the FacePe AI avatar.</string>
46
- <key>NSCameraUsageDescription</key>
47
- <string>Camera access for interactive features.</string>
48
- ```
49
-
50
- 2. Run CocoaPods:
51
- ```bash
52
- cd ios && pod install && cd ..
53
- ```
54
-
55
- ---
56
-
57
- ### Android Setup (`android/app/src/main/AndroidManifest.xml`)
58
-
59
- Open `android/app/src/main/AndroidManifest.xml` and include permissions:
60
-
61
- ```xml
62
- <uses-permission android:name="android.permission.INTERNET" />
63
- <uses-permission android:name="android.permission.RECORD_AUDIO" />
64
- <uses-permission android:name="android.permission.CAMERA" />
65
- <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
66
- <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
67
- ```
68
-
69
- ---
70
-
71
- ## 🚀 Quick Start Example
72
-
73
- ### 1. Server-side Token Endpoint
74
-
75
- Exchange your `accessKey` and `secretKey` on your secure backend (Node.js / Express / Next.js / Python / Go) so credentials are never exposed inside mobile apps:
76
-
77
- ```javascript
78
- // Express server route: GET /api/facepe-token
79
- app.get('/api/facepe-token', async (req, res) => {
80
- const response = await fetch('https://api.facepe.ai/api/v1/sdk/token', {
81
- method: 'POST',
82
- headers: { 'Content-Type': 'application/json' },
83
- body: JSON.stringify({
84
- accessKey: process.env.FACEPE_SDK_ACCESS_KEY,
85
- secretKey: process.env.FACEPE_SDK_SECRET_KEY,
86
- channel: 'sdk',
87
- }),
88
- });
89
- const data = await response.json();
90
- res.send(data.data.token);
91
- });
92
- ```
93
-
94
- ---
95
-
96
- ### 2. React Native Component
97
-
98
- ```tsx
99
- import React, { useRef } from 'react';
100
- import { StyleSheet, View, SafeAreaView, Text, TouchableOpacity } from 'react-native';
101
- import {
102
- FacePeMobileProvider,
103
- FacePeMobileAvatar,
104
- FacePeMobileAvatarRef,
105
- useFacePeAvatar,
106
- } from '@facepe/widget-mobile-app';
107
-
108
- export default function App() {
109
- const avatarRef = useRef<FacePeMobileAvatarRef>(null);
110
-
111
- return (
112
- <SafeAreaView style={styles.container}>
113
- <Text style={styles.title}>FacePe AI Native Digital Human</Text>
114
-
115
- <FacePeMobileProvider
116
- apiBaseUrl="https://api.facepe.ai/api/v1/sdk"
117
- getToken={async () => {
118
- const res = await fetch('https://your-backend.com/api/facepe-token');
119
- return await res.text();
120
- }}
121
- >
122
- <AvatarScreen avatarRef={avatarRef} />
123
- </FacePeMobileProvider>
124
- </SafeAreaView>
125
- );
126
- }
127
-
128
- function AvatarScreen({ avatarRef }: { avatarRef: React.RefObject<FacePeMobileAvatarRef> }) {
129
- const { transcripts, isAvatarSpeaking, avatarProps } = useFacePeAvatar();
130
-
131
- return (
132
- <View style={styles.content}>
133
- <View style={styles.avatarContainer}>
134
- <FacePeMobileAvatar
135
- ref={avatarRef}
136
- language="en"
137
- objectFit="cover"
138
- autoStart={true}
139
- enableMic={true}
140
- {...avatarProps}
141
- />
142
- </View>
143
-
144
- {/* Control bar */}
145
- <View style={styles.controls}>
146
- <TouchableOpacity
147
- style={styles.button}
148
- onPress={() => avatarRef.current?.toggleMic()}
149
- >
150
- <Text style={styles.buttonText}>Toggle Mic</Text>
151
- </TouchableOpacity>
152
-
153
- <TouchableOpacity
154
- style={styles.button}
155
- onPress={() => avatarRef.current?.interrupt()}
156
- >
157
- <Text style={styles.buttonText}>Interrupt</Text>
158
- </TouchableOpacity>
159
- </View>
160
-
161
- {/* Real-time speech transcript log */}
162
- <View style={styles.transcriptBox}>
163
- {transcripts.slice(-3).map((item, idx) => (
164
- <Text key={idx} style={styles.transcriptLine}>
165
- <Text style={item.role === 'assistant' ? styles.avatarTag : styles.userTag}>
166
- {item.role === 'assistant' ? 'Avatar: ' : 'You: '}
167
- </Text>
168
- {item.text}
169
- </Text>
170
- ))}
171
- </View>
172
- </View>
173
- );
174
- }
175
-
176
- const styles = StyleSheet.create({
177
- container: {
178
- flex: 1,
179
- backgroundColor: '#0F172A',
180
- },
181
- content: {
182
- flex: 1,
183
- padding: 16,
184
- },
185
- title: {
186
- color: '#FFFFFF',
187
- fontSize: 20,
188
- fontWeight: 'bold',
189
- textAlign: 'center',
190
- marginVertical: 12,
191
- },
192
- avatarContainer: {
193
- flex: 1,
194
- borderRadius: 24,
195
- overflow: 'hidden',
196
- backgroundColor: '#1E293B',
197
- },
198
- controls: {
199
- flexDirection: 'row',
200
- justifyContent: 'center',
201
- gap: 12,
202
- marginVertical: 14,
203
- },
204
- button: {
205
- backgroundColor: '#7C3AED',
206
- paddingHorizontal: 20,
207
- paddingVertical: 10,
208
- borderRadius: 12,
209
- },
210
- buttonText: {
211
- color: '#FFFFFF',
212
- fontWeight: '600',
213
- fontSize: 14,
214
- },
215
- transcriptBox: {
216
- backgroundColor: '#1E293B',
217
- padding: 12,
218
- borderRadius: 12,
219
- minHeight: 80,
220
- },
221
- transcriptLine: {
222
- color: '#E2E8F0',
223
- fontSize: 13,
224
- marginBottom: 4,
225
- },
226
- avatarTag: {
227
- color: '#A78BFA',
228
- fontWeight: '700',
229
- },
230
- userTag: {
231
- color: '#60A5FA',
232
- fontWeight: '700',
233
- },
234
- });
235
- ```
236
-
237
- ---
238
-
239
- ## ⚙️ Props & Configuration
240
-
241
- ### `<FacePeMobileAvatar />`
242
-
243
- | Prop | Type | Default | Description |
244
- | :--- | :--- | :--- | :--- |
245
- | `avatarKey` | `string` | *(optional)* | Specific avatar key or ID assigned to your company. |
246
- | `language` | `string` | `'en'` | Spoken language code (`'en'`, `'hi'`, `'es'`, `'ta'`, `'te'`, etc.). |
247
- | `style` | `ViewStyle` | `undefined` | Style applied to avatar container. |
248
- | `videoStyle` | `ViewStyle` | `undefined` | Style applied to native video view. |
249
- | `objectFit` | `'cover' \| 'contain'` | `'cover'` | Video aspect scaling mode. |
250
- | `mirror` | `boolean` | `false` | Mirror video orientation. |
251
- | `autoStart` | `boolean` | `true` | Automatically connect and start stream on mount. |
252
- | `enableMic` | `boolean` | `true` | Automatically enable microphone on connect. |
253
- | `onTranscript` | `function` | `undefined` | Fires when speech transcription arrives. |
254
- | `onSpeakStart` | `function` | `undefined` | Fires when avatar starts speaking. |
255
- | `onSpeakEnd` | `function` | `undefined` | Fires when avatar finishes speaking. |
256
- | `onUserSpeakStart` | `function` | `undefined` | Fires when user voice activity begins. |
257
- | `onUserSpeakEnd` | `function` | `undefined` | Fires when user voice activity ends. |
258
- | `onOrderUpdate` | `function` | `undefined` | Fires when cart items or intents are recognized. |
259
- | `onCheckout` | `function` | `undefined` | Fires when customer requests checkout / payment. |
260
- | `onStatusChange` | `function` | `undefined` | Stream lifecycle status: `'idle' \| 'connecting' \| 'connected' \| 'reconnecting' \| 'disconnected' \| 'error'`. |
261
- | `onError` | `function` | `undefined` | Stream error callback `{ code, message }`. |
262
-
263
- ---
264
-
265
- ### Imperative Ref (`FacePeMobileAvatarRef`)
266
-
267
- ```tsx
268
- const avatarRef = useRef<FacePeMobileAvatarRef>(null);
269
-
270
- // Start streaming session
271
- await avatarRef.current?.start();
272
-
273
- // Stop streaming session & finalize billing usage
274
- await avatarRef.current?.stop();
275
-
276
- // Toggle mic on/off (returns new state boolean)
277
- const isNowMuted = await avatarRef.current?.toggleMic();
278
-
279
- // Make avatar speak programmatically
280
- await avatarRef.current?.speak("Hello! How can I assist you today?");
281
-
282
- // Interrupt avatar speech immediately
283
- await avatarRef.current?.interrupt();
284
- ```
285
-
286
- ---
287
-
288
- ## 📄 License
289
- MIT © FacePe AI
1
+ # @facepe/widget-mobile-app
2
+
3
+ The official **FacePe AI Digital Human Live Streaming SDK** for **React Native** (supporting both **iOS** and **Android**).
4
+
5
+ Embed interactive, photorealistic, lip-synced 3D digital avatars directly inside your native mobile apps with pure WebRTC media streaming, native microphone capture, bidirectional voice dialog, and live data events — **without webviews**.
6
+
7
+ ---
8
+
9
+ ## 📱 Features
10
+
11
+ - **⚡ Native WebRTC Streaming**: Pure native media streaming powered by `@livekit/react-native` (no WebViews or embedded iframes).
12
+ - **🗣️ Natural Two-Way Voice Dialog**: Real-time microphone capture with hardware echo cancellation, noise suppression, and server VAD.
13
+ - **🍎 Full iOS Support**: Hardware-accelerated video decoding on iOS 13.0+ (iPhone & iPad).
14
+ - **🤖 Full Android Support**: Hardware-accelerated video decoding on Android 7.0+ (API 24+).
15
+ - **💬 Real-Time Transcriptions**: Bidirectional captions for avatar speech and user speech.
16
+ - **🛒 Conversational Commerce**: Automatic detection of cart items, orders, and payment intents.
17
+ - **🎛️ Imperative Controls**: Ref methods to start/stop stream, mute/unmute mic, speak text, or interrupt speech.
18
+
19
+ ---
20
+
21
+ ## 📦 Installation
22
+
23
+ In your React Native project:
24
+
25
+ ```bash
26
+ npm install @facepe/widget-mobile-app @livekit/react-native @livekit/react-native-webrtc livekit-client @anam-ai/js-sdk
27
+ ```
28
+
29
+ *(or using yarn / pnpm)*:
30
+
31
+ ```bash
32
+ yarn add @facepe/widget-mobile-app @livekit/react-native @livekit/react-native-webrtc livekit-client @anam-ai/js-sdk
33
+ ```
34
+
35
+ All avatars play natively (no WebView). The streaming libraries are loaded
36
+ only when a conversation starts. The app needs a development build (Expo Go
37
+ cannot load the native WebRTC module).
38
+
39
+ ---
40
+
41
+ ## 🛠️ Platform Permissions Setup
42
+
43
+ ### iOS Setup (`ios/Podfile` & `Info.plist`)
44
+
45
+ 1. Open `ios/YourAppName/Info.plist` and add permission strings for microphone and audio:
46
+
47
+ ```xml
48
+ <key>NSMicrophoneUsageDescription</key>
49
+ <string>We need access to your microphone to converse with the FacePe AI avatar.</string>
50
+ <key>NSCameraUsageDescription</key>
51
+ <string>Camera access for interactive features.</string>
52
+ ```
53
+
54
+ 2. Run CocoaPods:
55
+ ```bash
56
+ cd ios && pod install && cd ..
57
+ ```
58
+
59
+ ---
60
+
61
+ ### Android Setup (`android/app/src/main/AndroidManifest.xml`)
62
+
63
+ Open `android/app/src/main/AndroidManifest.xml` and include permissions:
64
+
65
+ ```xml
66
+ <uses-permission android:name="android.permission.INTERNET" />
67
+ <uses-permission android:name="android.permission.RECORD_AUDIO" />
68
+ <uses-permission android:name="android.permission.CAMERA" />
69
+ <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
70
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
71
+ ```
72
+
73
+ ---
74
+
75
+ ## 🚀 Quick Start Example
76
+
77
+ ### 1. Server-side Token Endpoint
78
+
79
+ Exchange your `accessKey` and `secretKey` on your secure backend (Node.js / Express / Next.js / Python / Go) so credentials are never exposed inside mobile apps:
80
+
81
+ ```javascript
82
+ // Express server route: GET /api/facepe-token
83
+ app.get('/api/facepe-token', async (req, res) => {
84
+ const response = await fetch('https://api.facepe.ai/api/v1/sdk/token', {
85
+ method: 'POST',
86
+ headers: { 'Content-Type': 'application/json' },
87
+ body: JSON.stringify({
88
+ accessKey: process.env.FACEPE_SDK_ACCESS_KEY,
89
+ secretKey: process.env.FACEPE_SDK_SECRET_KEY,
90
+ channel: 'sdk',
91
+ }),
92
+ });
93
+ const data = await response.json();
94
+ res.send(data.data.token);
95
+ });
96
+ ```
97
+
98
+ ---
99
+
100
+ ### 2. React Native Component
101
+
102
+ ```tsx
103
+ import React, { useRef } from 'react';
104
+ import { StyleSheet, View, SafeAreaView, Text, TouchableOpacity } from 'react-native';
105
+ import {
106
+ FacePeMobileProvider,
107
+ FacePeMobileAvatar,
108
+ FacePeMobileAvatarRef,
109
+ useFacePeAvatar,
110
+ } from '@facepe/widget-mobile-app';
111
+
112
+ export default function App() {
113
+ const avatarRef = useRef<FacePeMobileAvatarRef>(null);
114
+
115
+ return (
116
+ <SafeAreaView style={styles.container}>
117
+ <Text style={styles.title}>FacePe AI Native Digital Human</Text>
118
+
119
+ <FacePeMobileProvider
120
+ apiBaseUrl="https://api.facepe.ai/api/v1/sdk"
121
+ getToken={async () => {
122
+ const res = await fetch('https://your-backend.com/api/facepe-token');
123
+ return await res.text();
124
+ }}
125
+ >
126
+ <AvatarScreen avatarRef={avatarRef} />
127
+ </FacePeMobileProvider>
128
+ </SafeAreaView>
129
+ );
130
+ }
131
+
132
+ function AvatarScreen({ avatarRef }: { avatarRef: React.RefObject<FacePeMobileAvatarRef> }) {
133
+ const { transcripts, isAvatarSpeaking, avatarProps } = useFacePeAvatar();
134
+
135
+ return (
136
+ <View style={styles.content}>
137
+ <View style={styles.avatarContainer}>
138
+ <FacePeMobileAvatar
139
+ ref={avatarRef}
140
+ language="en"
141
+ objectFit="cover"
142
+ autoStart={true}
143
+ enableMic={true}
144
+ {...avatarProps}
145
+ />
146
+ </View>
147
+
148
+ {/* Control bar */}
149
+ <View style={styles.controls}>
150
+ <TouchableOpacity
151
+ style={styles.button}
152
+ onPress={() => avatarRef.current?.toggleMic()}
153
+ >
154
+ <Text style={styles.buttonText}>Toggle Mic</Text>
155
+ </TouchableOpacity>
156
+
157
+ <TouchableOpacity
158
+ style={styles.button}
159
+ onPress={() => avatarRef.current?.interrupt()}
160
+ >
161
+ <Text style={styles.buttonText}>Interrupt</Text>
162
+ </TouchableOpacity>
163
+ </View>
164
+
165
+ {/* Real-time speech transcript log */}
166
+ <View style={styles.transcriptBox}>
167
+ {transcripts.slice(-3).map((item, idx) => (
168
+ <Text key={idx} style={styles.transcriptLine}>
169
+ <Text style={item.role === 'assistant' ? styles.avatarTag : styles.userTag}>
170
+ {item.role === 'assistant' ? 'Avatar: ' : 'You: '}
171
+ </Text>
172
+ {item.text}
173
+ </Text>
174
+ ))}
175
+ </View>
176
+ </View>
177
+ );
178
+ }
179
+
180
+ const styles = StyleSheet.create({
181
+ container: {
182
+ flex: 1,
183
+ backgroundColor: '#0F172A',
184
+ },
185
+ content: {
186
+ flex: 1,
187
+ padding: 16,
188
+ },
189
+ title: {
190
+ color: '#FFFFFF',
191
+ fontSize: 20,
192
+ fontWeight: 'bold',
193
+ textAlign: 'center',
194
+ marginVertical: 12,
195
+ },
196
+ avatarContainer: {
197
+ flex: 1,
198
+ borderRadius: 24,
199
+ overflow: 'hidden',
200
+ backgroundColor: '#1E293B',
201
+ },
202
+ controls: {
203
+ flexDirection: 'row',
204
+ justifyContent: 'center',
205
+ gap: 12,
206
+ marginVertical: 14,
207
+ },
208
+ button: {
209
+ backgroundColor: '#7C3AED',
210
+ paddingHorizontal: 20,
211
+ paddingVertical: 10,
212
+ borderRadius: 12,
213
+ },
214
+ buttonText: {
215
+ color: '#FFFFFF',
216
+ fontWeight: '600',
217
+ fontSize: 14,
218
+ },
219
+ transcriptBox: {
220
+ backgroundColor: '#1E293B',
221
+ padding: 12,
222
+ borderRadius: 12,
223
+ minHeight: 80,
224
+ },
225
+ transcriptLine: {
226
+ color: '#E2E8F0',
227
+ fontSize: 13,
228
+ marginBottom: 4,
229
+ },
230
+ avatarTag: {
231
+ color: '#A78BFA',
232
+ fontWeight: '700',
233
+ },
234
+ userTag: {
235
+ color: '#60A5FA',
236
+ fontWeight: '700',
237
+ },
238
+ });
239
+ ```
240
+
241
+ ---
242
+
243
+ ## ⚙️ Props & Configuration
244
+
245
+ ### `<FacePeMobileAvatar />`
246
+
247
+ | Prop | Type | Default | Description |
248
+ | :--- | :--- | :--- | :--- |
249
+ | `avatarKey` | `string` | *(optional)* | Specific avatar key or ID assigned to your company. |
250
+ | `language` | `string` | `'en'` | Spoken language code (`'en'`, `'hi'`, `'es'`, `'ta'`, `'te'`, etc.). |
251
+ | `style` | `ViewStyle` | `undefined` | Style applied to avatar container. |
252
+ | `videoStyle` | `ViewStyle` | `undefined` | Style applied to native video view. |
253
+ | `objectFit` | `'cover' \| 'contain'` | `'cover'` | Video aspect scaling mode. |
254
+ | `mirror` | `boolean` | `false` | Mirror video orientation. |
255
+ | `autoStart` | `boolean` | `true` | Automatically connect and start stream on mount. |
256
+ | `enableMic` | `boolean` | `true` | Automatically enable microphone on connect. |
257
+ | `onTranscript` | `function` | `undefined` | Fires when speech transcription arrives. |
258
+ | `onSpeakStart` | `function` | `undefined` | Fires when avatar starts speaking. |
259
+ | `onSpeakEnd` | `function` | `undefined` | Fires when avatar finishes speaking. |
260
+ | `onUserSpeakStart` | `function` | `undefined` | Fires when user voice activity begins. |
261
+ | `onUserSpeakEnd` | `function` | `undefined` | Fires when user voice activity ends. |
262
+ | `onOrderUpdate` | `function` | `undefined` | Fires when cart items or intents are recognized. |
263
+ | `onCheckout` | `function` | `undefined` | Fires when customer requests checkout / payment. |
264
+ | `onStatusChange` | `function` | `undefined` | Stream lifecycle status: `'idle' \| 'connecting' \| 'connected' \| 'reconnecting' \| 'disconnected' \| 'error'`. |
265
+ | `onError` | `function` | `undefined` | Stream error callback `{ code, message }`. |
266
+
267
+ ---
268
+
269
+ ### Imperative Ref (`FacePeMobileAvatarRef`)
270
+
271
+ ```tsx
272
+ const avatarRef = useRef<FacePeMobileAvatarRef>(null);
273
+
274
+ // Start streaming session
275
+ await avatarRef.current?.start();
276
+
277
+ // Stop streaming session & finalize billing usage
278
+ await avatarRef.current?.stop();
279
+
280
+ // Toggle mic on/off (returns new state boolean)
281
+ const isNowMuted = await avatarRef.current?.toggleMic();
282
+
283
+ // Make avatar speak programmatically
284
+ await avatarRef.current?.speak("Hello! How can I assist you today?");
285
+
286
+ // Interrupt avatar speech immediately
287
+ await avatarRef.current?.interrupt();
288
+ ```
289
+
290
+ ---
291
+
292
+ ## 🗂️ Conversation data (`onConversationEnd`)
293
+
294
+ When a conversation ends, the SDK hands your app the **whole conversation** in
295
+ one record. It stays in your app — the SDK sends it nowhere; you decide whether
296
+ to show it, save it on your server, or ignore it.
297
+
298
+ ```tsx
299
+ <FacePeMobileAvatar
300
+ onConversationEnd={(conversation) => {
301
+ console.log(conversation.durationSec, conversation.transcript.length);
302
+ // e.g. save it on your own server:
303
+ // fetch('https://your-server.example.com/conversations', { method: 'POST', body: JSON.stringify(conversation) });
304
+ }}
305
+ />
306
+ ```
307
+
308
+ It is called once per conversation: after `stop()`, when the stream drops, or
309
+ when the component unmounts.
310
+
311
+ ```json
312
+ {
313
+ "conversationId": "cm_usage_123",
314
+ "avatar": { "id": "cm_avatar_456", "name": "Dharun" },
315
+ "language": "en",
316
+ "platform": "android",
317
+ "sdkVersion": "1.3.0",
318
+ "startedAt": "2026-10-12T11:40:02.000Z",
319
+ "endedAt": "2026-10-12T11:43:17.000Z",
320
+ "durationSec": 195,
321
+ "endReason": "ended",
322
+ "transcript": [
323
+ { "role": "assistant", "text": "Welcome! How can I help you today?", "at": "2026-10-12T11:40:05.000Z" },
324
+ { "role": "user", "text": "Two cheeseburgers please.", "at": "2026-10-12T11:40:11.000Z" }
325
+ ]
326
+ }
327
+ ```
328
+
329
+ | Field | Meaning |
330
+ |---|---|
331
+ | `conversationId` | The conversation's FacePe id (its usage record) |
332
+ | `avatar` | `{ id, name }` of the avatar |
333
+ | `language` | The language requested for the conversation |
334
+ | `platform` | `web`, `android` or `ios` |
335
+ | `sdkVersion` | The SDK version that recorded it |
336
+ | `startedAt`, `endedAt` | ISO 8601 times |
337
+ | `durationSec` | Length in seconds |
338
+ | `endReason` | `ended` (the user or your code ended it), `disconnected` (the stream dropped), `unmounted` (the component went away) |
339
+ | `transcript` | Every line in order: `{ role: "user" \| "assistant", text, at }` |
340
+
341
+ ---
342
+
343
+ ## 📄 License
344
+ MIT © FacePe AI
@@ -1,11 +1,44 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useEffect, useRef, useState, useImperativeHandle, forwardRef, useCallback, } from 'react';
3
- import { View, StyleSheet, ActivityIndicator, Text, TouchableOpacity } from 'react-native';
3
+ import { View, StyleSheet, ActivityIndicator, Text, TouchableOpacity, Platform } from 'react-native';
4
+ import { SDK_VERSION } from './version';
4
5
  import { useFacePeMobileContext } from './FacePeMobileProvider';
5
- // Resolve @livekit/react-native dynamically at runtime so bare/Expo projects can run smoothly
6
+ // Resolve LiveKit dynamically at runtime so bare/Expo projects can run smoothly.
7
+ // @livekit/react-native brings the native parts (registerGlobals, AudioSession,
8
+ // VideoView); Room, RoomEvent and Track come from livekit-client, which it
9
+ // does not re-export.
6
10
  function getLiveKitRN() {
7
11
  try {
8
- return require('@livekit/react-native');
12
+ const native = require('@livekit/react-native');
13
+ const client = require('livekit-client');
14
+ return { ...native, Room: client.Room, RoomEvent: client.RoomEvent, Track: client.Track };
15
+ }
16
+ catch {
17
+ return null;
18
+ }
19
+ }
20
+ // "stream" connections (hosted avatars) are played natively with the stream
21
+ // SDK's `stream()`, which returns plain MediaStreams — shown in the native
22
+ // RTCView, no WebView. It needs crypto.randomUUID, which React Native lacks.
23
+ function getStreamSdk() {
24
+ try {
25
+ const g = globalThis;
26
+ g.crypto = g.crypto || {};
27
+ if (typeof g.crypto.randomUUID !== 'function') {
28
+ g.crypto.randomUUID = () => 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
29
+ const r = (Math.random() * 16) | 0;
30
+ return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16);
31
+ });
32
+ }
33
+ return require('@anam-ai/js-sdk');
34
+ }
35
+ catch {
36
+ return null;
37
+ }
38
+ }
39
+ function getRTCView() {
40
+ try {
41
+ return require('@livekit/react-native-webrtc').RTCView;
9
42
  }
10
43
  catch {
11
44
  return null;
@@ -25,7 +58,7 @@ function ensureLiveKitGlobals(LiveKitRN) {
25
58
  console.warn('[FacePeMobileAvatar] registerGlobals warning:', err);
26
59
  }
27
60
  }
28
- export const FacePeMobileAvatar = forwardRef(({ avatarKey, language = 'en', style, videoStyle, objectFit = 'cover', mirror = false, autoStart = true, enableMic = true, onTranscript, onSpeakStart, onSpeakEnd, onUserSpeakStart, onUserSpeakEnd, onOrderUpdate, onCheckout, onStatusChange, onError, }, ref) => {
61
+ export const FacePeMobileAvatar = forwardRef(({ avatarKey, language = 'en', style, videoStyle, objectFit = 'cover', mirror = false, autoStart = true, enableMic = true, onTranscript, onSpeakStart, onSpeakEnd, onUserSpeakStart, onUserSpeakEnd, onOrderUpdate, onCheckout, onStatusChange, onError, onConversationEnd, }, ref) => {
29
62
  const { token, loading: tokenLoading, error: tokenError, apiBaseUrl, refreshToken } = useFacePeMobileContext();
30
63
  const [status, setStatus] = useState('idle');
31
64
  const [videoTrack, setVideoTrack] = useState(null);
@@ -36,7 +69,54 @@ export const FacePeMobileAvatar = forwardRef(({ avatarKey, language = 'en', styl
36
69
  const LiveKitRN = getLiveKitRN();
37
70
  const hasLiveKit = !!LiveKitRN;
38
71
  const roomRef = useRef(null);
72
+ // A "stream" connection: the stream SDK client, and its video for RTCView.
73
+ const streamClientRef = useRef(null);
74
+ const [streamURL, setStreamURL] = useState(null);
39
75
  const usageIdRef = useRef(null);
76
+ // The conversation as it happens, for the onConversationEnd record. Lines
77
+ // that carry an id (a line that grows while spoken) replace their earlier version.
78
+ const conversationRef = useRef(null);
79
+ const onConversationEndRef = useRef(onConversationEnd);
80
+ onConversationEndRef.current = onConversationEnd;
81
+ const onTranscriptRef = useRef(onTranscript);
82
+ onTranscriptRef.current = onTranscript;
83
+ // A transcript line: recorded for the conversation record, and passed on.
84
+ const emitLine = (role, text, id) => {
85
+ const conv = conversationRef.current;
86
+ if (conv && text) {
87
+ const same = id ? conv.lines.find((l) => l.id === id) : undefined;
88
+ if (same)
89
+ same.text = text;
90
+ else
91
+ conv.lines.push({ id, role, text, at: new Date().toISOString() });
92
+ }
93
+ onTranscriptRef.current?.({ payload: { role, text, isFinal: true } });
94
+ };
95
+ // Hands the whole conversation to the app, once.
96
+ const finishConversation = (endReason) => {
97
+ const conv = conversationRef.current;
98
+ conversationRef.current = null;
99
+ if (!conv)
100
+ return;
101
+ const endedAt = new Date();
102
+ try {
103
+ onConversationEndRef.current?.({
104
+ conversationId: conv.conversationId,
105
+ avatar: conv.avatar,
106
+ language: conv.language,
107
+ platform: Platform.OS === 'ios' ? 'ios' : Platform.OS === 'android' ? 'android' : 'web',
108
+ sdkVersion: SDK_VERSION,
109
+ startedAt: conv.startedAt.toISOString(),
110
+ endedAt: endedAt.toISOString(),
111
+ durationSec: Math.max(0, Math.round((endedAt.getTime() - conv.startedAt.getTime()) / 1000)),
112
+ endReason,
113
+ transcript: conv.lines.map(({ role, text, at }) => ({ role, text, at })),
114
+ });
115
+ }
116
+ catch (err) {
117
+ console.warn('[FacePeMobileAvatar] onConversationEnd error:', err);
118
+ }
119
+ };
40
120
  const mountedRef = useRef(true);
41
121
  const isConnectingRef = useRef(false);
42
122
  const tokenRef = useRef(token);
@@ -95,7 +175,7 @@ export const FacePeMobileAvatar = forwardRef(({ avatarKey, language = 'en', styl
95
175
  };
96
176
  // Start streaming session
97
177
  const startStream = useCallback(async () => {
98
- if (isConnectingRef.current || roomRef.current)
178
+ if (isConnectingRef.current || roomRef.current || streamClientRef.current)
99
179
  return;
100
180
  if (!hasLiveKit) {
101
181
  const msg = 'Please install `@livekit/react-native` and `@livekit/react-native-webrtc` to enable native AI avatar streaming.';
@@ -121,18 +201,18 @@ export const FacePeMobileAvatar = forwardRef(({ avatarKey, language = 'en', styl
121
201
  // Resolve target avatar ID
122
202
  let targetAvatarId = avatarKey;
123
203
  if (!targetAvatarId) {
124
- // If no avatar key passed, retrieve assigned avatars from /sdk/session
125
- const sessionRes = await fetch(`${cleanBase}/session`, {
204
+ // No avatar given: use the first one assigned to this token's user.
205
+ const listRes = await fetch(`${cleanBase}/avatars`, {
126
206
  method: 'GET',
127
207
  headers: {
128
208
  Authorization: `Bearer ${currentToken}`,
129
209
  },
130
210
  });
131
- if (sessionRes.ok) {
132
- const sessionData = await sessionRes.json();
133
- const avatars = sessionData?.data?.avatars || sessionData?.avatars || [];
211
+ if (listRes.ok) {
212
+ const listData = await listRes.json();
213
+ const avatars = listData?.data?.avatars || [];
134
214
  if (avatars.length > 0) {
135
- targetAvatarId = avatars[0].id || avatars[0].key;
215
+ targetAvatarId = avatars[0].id;
136
216
  }
137
217
  }
138
218
  }
@@ -156,14 +236,75 @@ export const FacePeMobileAvatar = forwardRef(({ avatarKey, language = 'en', styl
156
236
  const sessionPayload = await sessionResp.json();
157
237
  const sessionInfo = sessionPayload?.data || sessionPayload;
158
238
  usageIdRef.current = sessionInfo?.usageId || null;
239
+ conversationRef.current = {
240
+ conversationId: usageIdRef.current,
241
+ avatar: { id: String(sessionInfo?.avatar?.id ?? targetAvatarId), name: String(sessionInfo?.avatar?.name ?? '') },
242
+ language: language ?? null,
243
+ startedAt: new Date(),
244
+ lines: [],
245
+ };
159
246
  const connection = sessionInfo?.connection;
247
+ if (connection?.type === 'stream') {
248
+ // Hosted avatar: the stream SDK gives plain MediaStreams (video +
249
+ // the avatar's voice) and captures the microphone itself.
250
+ const StreamSdk = getStreamSdk();
251
+ if (!StreamSdk || !getRTCView() || !connection.token) {
252
+ throw new Error('This avatar needs `@anam-ai/js-sdk` in the app (npm install @anam-ai/js-sdk).');
253
+ }
254
+ const client = StreamSdk.createClient(connection.token);
255
+ streamClientRef.current = client;
256
+ const ev = StreamSdk.AnamEvent;
257
+ const seen = new Set();
258
+ client.addListener(ev.MESSAGE_HISTORY_UPDATED, (messages) => {
259
+ if (!mountedRef.current)
260
+ return;
261
+ for (const m of messages ?? []) {
262
+ const text = String(m?.content ?? '').trim();
263
+ const key = `${m?.id}:${text}`;
264
+ if (!text || seen.has(key))
265
+ continue;
266
+ seen.add(key);
267
+ emitLine(String(m.role).includes('user') ? 'user' : 'assistant', text, m?.id ? String(m.id) : undefined);
268
+ }
269
+ });
270
+ client.addListener(ev.USER_SPEECH_STARTED, () => { setIsUserSpeaking(true); onUserSpeakStart?.(); });
271
+ client.addListener(ev.USER_SPEECH_ENDED, () => { setIsUserSpeaking(false); onUserSpeakEnd?.(); });
272
+ client.addListener(ev.CONNECTION_CLOSED, () => {
273
+ if (!mountedRef.current || streamClientRef.current !== client)
274
+ return;
275
+ streamClientRef.current = null;
276
+ setStreamURL(null);
277
+ setIsUserSpeaking(false);
278
+ updateStatus('disconnected');
279
+ finishConversation('disconnected');
280
+ void endUsage();
281
+ });
282
+ const streams = await client.stream();
283
+ const video = streams.find((s) => s?.getVideoTracks?.().length > 0) ?? streams[0];
284
+ if (!video)
285
+ throw new Error('The avatar did not send a video stream.');
286
+ if (!mountedRef.current) {
287
+ await client.stopStreaming().catch(() => undefined);
288
+ return;
289
+ }
290
+ setStreamURL(video.toURL());
291
+ setIsMicEnabled(true);
292
+ updateStatus('connected');
293
+ if (!enableMic) {
294
+ client.muteInputAudio();
295
+ setIsMicEnabled(false);
296
+ }
297
+ if (connection.greeting)
298
+ client.talk(String(connection.greeting)).catch(() => undefined);
299
+ return;
300
+ }
160
301
  if (!connection || connection.type !== 'room' || !connection.url || !connection.token) {
161
- throw new Error('Avatar server did not return a valid LiveKit room connection.');
302
+ throw new Error('Avatar server did not return a valid room connection.');
162
303
  }
163
304
  // Initialize LiveKit Room natively
164
305
  const room = new LiveKitRN.Room({
165
- adaptiveStream: true,
166
- dynacast: true,
306
+ adaptiveStream: false, // Ensure full video frames arrive without adaptive downscaling
307
+ dynacast: false,
167
308
  });
168
309
  roomRef.current = room;
169
310
  // Start Audio Session on iOS/Android
@@ -247,13 +388,13 @@ export const FacePeMobileAvatar = forwardRef(({ avatarKey, language = 'en', styl
247
388
  if (type === 'avatar.transcription' || type === 'response.audio_transcript.done') {
248
389
  const str = msg.text || msg.transcript || msg.payload?.text;
249
390
  if (str)
250
- onTranscript?.({ payload: { role: 'assistant', text: String(str).trim() } });
391
+ emitLine('assistant', String(str).trim());
251
392
  }
252
393
  else if (type === 'user.transcription' ||
253
394
  type === 'conversation.item.input_audio_transcription.completed') {
254
395
  const str = msg.text || msg.transcript || msg.payload?.text;
255
396
  if (str)
256
- onTranscript?.({ payload: { role: 'user', text: String(str).trim() } });
397
+ emitLine('user', String(str).trim());
257
398
  }
258
399
  // Order & checkout intents
259
400
  if (type === 'order.update') {
@@ -270,12 +411,17 @@ export const FacePeMobileAvatar = forwardRef(({ avatarKey, language = 'en', styl
270
411
  });
271
412
  // 4. Connection state transitions
272
413
  room.on(LiveKitRN.RoomEvent.Disconnected, () => {
273
- if (!mountedRef.current)
274
- return;
414
+ if (!mountedRef.current || roomRef.current !== room)
415
+ return; // stop() handles its own end
416
+ roomRef.current = null;
275
417
  setVideoTrack(null);
276
418
  setIsAvatarSpeaking(false);
277
419
  setIsUserSpeaking(false);
278
420
  updateStatus('disconnected');
421
+ // The stream dropped: close the conversation now, so it is not
422
+ // left open (and billed) until the server's 30-minute limit.
423
+ finishConversation('disconnected');
424
+ void endUsage();
279
425
  });
280
426
  room.on(LiveKitRN.RoomEvent.Reconnecting, () => {
281
427
  updateStatus('reconnecting');
@@ -285,6 +431,25 @@ export const FacePeMobileAvatar = forwardRef(({ avatarKey, language = 'en', styl
285
431
  });
286
432
  // Connect room over WebRTC
287
433
  await room.connect(connection.url, connection.token, { autoSubscribe: true });
434
+ // Check already subscribed remote tracks in case track arrived before/during connection
435
+ try {
436
+ room.remoteParticipants?.forEach?.((participant) => {
437
+ participant.trackPublications?.forEach?.((pub) => {
438
+ if (pub?.track) {
439
+ const isVid = pub.track.kind === 'video' ||
440
+ pub.kind === 'video' ||
441
+ (LiveKitRN.Track?.Kind?.Video && pub.track.kind === LiveKitRN.Track.Kind.Video);
442
+ if (isVid && mountedRef.current) {
443
+ setVideoTrack(pub.track);
444
+ updateStatus('connected');
445
+ }
446
+ }
447
+ });
448
+ });
449
+ }
450
+ catch {
451
+ // ignore inspection error
452
+ }
288
453
  // Enable microphone capture if requested
289
454
  if (enableMic) {
290
455
  try {
@@ -310,6 +475,14 @@ export const FacePeMobileAvatar = forwardRef(({ avatarKey, language = 'en', styl
310
475
  }
311
476
  }
312
477
  catch (err) {
478
+ // A failed hosted-avatar start: close its connection too.
479
+ const failedClient = streamClientRef.current;
480
+ streamClientRef.current = null;
481
+ setStreamURL(null);
482
+ if (failedClient)
483
+ await failedClient.stopStreaming().catch(() => undefined);
484
+ // Opened on the server but never got going: still hand over the record.
485
+ finishConversation('disconnected');
313
486
  if (!mountedRef.current)
314
487
  return;
315
488
  const msg = err?.message || 'Connection failed';
@@ -339,9 +512,15 @@ export const FacePeMobileAvatar = forwardRef(({ avatarKey, language = 'en', styl
339
512
  onError,
340
513
  ]);
341
514
  // Stop streaming session
342
- const stopStream = useCallback(async () => {
515
+ const stopStream = useCallback(async (reason = 'ended') => {
516
+ finishConversation(reason);
343
517
  const room = roomRef.current;
344
518
  roomRef.current = null;
519
+ const streamClient = streamClientRef.current;
520
+ streamClientRef.current = null;
521
+ setStreamURL(null);
522
+ if (streamClient)
523
+ await streamClient.stopStreaming().catch(() => undefined);
345
524
  if (room) {
346
525
  try {
347
526
  if (room.localParticipant) {
@@ -368,6 +547,16 @@ export const FacePeMobileAvatar = forwardRef(({ avatarKey, language = 'en', styl
368
547
  await stopStream();
369
548
  },
370
549
  toggleMic: async () => {
550
+ const streamClient = streamClientRef.current;
551
+ if (streamClient) {
552
+ const next = !isMicEnabled;
553
+ if (next)
554
+ streamClient.unmuteInputAudio();
555
+ else
556
+ streamClient.muteInputAudio();
557
+ setIsMicEnabled(next);
558
+ return next;
559
+ }
371
560
  const room = roomRef.current;
372
561
  if (!room || !room.localParticipant)
373
562
  return isMicEnabled;
@@ -383,6 +572,15 @@ export const FacePeMobileAvatar = forwardRef(({ avatarKey, language = 'en', styl
383
572
  }
384
573
  },
385
574
  setMicEnabled: async (enabled) => {
575
+ const streamClient = streamClientRef.current;
576
+ if (streamClient) {
577
+ if (enabled)
578
+ streamClient.unmuteInputAudio();
579
+ else
580
+ streamClient.muteInputAudio();
581
+ setIsMicEnabled(enabled);
582
+ return;
583
+ }
386
584
  const room = roomRef.current;
387
585
  if (!room || !room.localParticipant)
388
586
  return;
@@ -395,10 +593,17 @@ export const FacePeMobileAvatar = forwardRef(({ avatarKey, language = 'en', styl
395
593
  }
396
594
  },
397
595
  speak: async (text) => {
596
+ if (streamClientRef.current) {
597
+ await streamClientRef.current.talk(text).catch(() => undefined);
598
+ return;
599
+ }
398
600
  sendData({ event_type: 'avatar.speak_text', text });
399
601
  },
400
602
  interrupt: async () => {
401
- sendData({ event_type: 'avatar.interrupt' });
603
+ if (streamClientRef.current)
604
+ streamClientRef.current.interruptPersona();
605
+ else
606
+ sendData({ event_type: 'avatar.interrupt' });
402
607
  setIsAvatarSpeaking(false);
403
608
  onSpeakEnd?.();
404
609
  },
@@ -417,7 +622,7 @@ export const FacePeMobileAvatar = forwardRef(({ avatarKey, language = 'en', styl
417
622
  }
418
623
  return () => {
419
624
  mountedRef.current = false;
420
- void stopStream();
625
+ void stopStream('unmounted');
421
626
  };
422
627
  }, [autoStart, token, tokenLoading, tokenError]);
423
628
  // Handle token loading
@@ -434,7 +639,13 @@ export const FacePeMobileAvatar = forwardRef(({ avatarKey, language = 'en', styl
434
639
  }
435
640
  // Native LiveKit VideoView component
436
641
  const VideoViewComponent = LiveKitRN.VideoView;
437
- return (_jsxs(View, { style: [styles.container, style], children: [videoTrack && VideoViewComponent ? (_jsx(VideoViewComponent, { videoTrack: videoTrack, style: [styles.video, videoStyle], objectFit: objectFit, mirror: mirror })) : (_jsx(View, { style: [styles.container, styles.center], children: status === 'connecting' || status === 'reconnecting' ? (_jsxs(_Fragment, { children: [_jsx(ActivityIndicator, { size: "large", color: "#8B5CF6" }), _jsx(Text, { style: styles.loadingText, children: status === 'connecting'
642
+ // Native WebRTC view, for a hosted avatar's MediaStream
643
+ const RTCViewComponent = streamURL ? getRTCView() : null;
644
+ return (_jsxs(View, { style: [styles.container, style], children: [streamURL && RTCViewComponent ? (_jsx(RTCViewComponent, { streamURL: streamURL, style: StyleSheet.flatten([styles.video, videoStyle]), objectFit: objectFit, mirror: mirror, zOrder: 0 })) : videoTrack && VideoViewComponent ? (_jsx(VideoViewComponent, { track: videoTrack, videoTrack: videoTrack,
645
+ // A plain object, not an array: VideoView spreads `style` into its
646
+ // container ({ ...style }), and spreading an array loses every
647
+ // size — the video got no height and stayed invisible.
648
+ style: StyleSheet.flatten([styles.video, videoStyle]), objectFit: objectFit, mirror: mirror })) : (_jsx(View, { style: [styles.container, styles.center], children: status === 'connecting' || status === 'reconnecting' ? (_jsxs(_Fragment, { children: [_jsx(ActivityIndicator, { size: "large", color: "#8B5CF6" }), _jsx(Text, { style: styles.loadingText, children: status === 'connecting'
438
649
  ? 'Connecting to FacePe AI Avatar…'
439
650
  : 'Reconnecting stream…' })] })) : status === 'error' ? (_jsxs(_Fragment, { children: [_jsx(Text, { style: styles.errorTitle, children: "Connection Failed" }), _jsx(Text, { style: styles.errorText, children: errorMessage || 'Unable to connect to stream' }), _jsx(TouchableOpacity, { style: styles.retryButton, onPress: () => void startStream(), children: _jsx(Text, { style: styles.retryText, children: "Reconnect" }) })] })) : (_jsx(TouchableOpacity, { style: styles.startButton, onPress: () => void startStream(), children: _jsx(Text, { style: styles.startText, children: "Start Avatar Session" }) })) })), status === 'connected' && (isAvatarSpeaking || isUserSpeaking) && (_jsxs(View, { style: styles.speakerPill, children: [_jsx(View, { style: [
440
651
  styles.speakerDot,
@@ -452,6 +663,7 @@ const styles = StyleSheet.create({
452
663
  position: 'relative',
453
664
  },
454
665
  video: {
666
+ flex: 1,
455
667
  width: '100%',
456
668
  height: '100%',
457
669
  },
@@ -1,4 +1,4 @@
1
- import React, { type ReactNode } from 'react';
1
+ import { type ReactNode } from 'react';
2
2
  import type { FacePeMobileConfig, TokenProvider } from './types';
3
3
  interface FacePeContextValue {
4
4
  config: FacePeMobileConfig;
@@ -14,6 +14,6 @@ export interface FacePeMobileProviderProps {
14
14
  apiBaseUrl?: string;
15
15
  config?: FacePeMobileConfig;
16
16
  }
17
- export declare function FacePeMobileProvider({ children, getToken, apiBaseUrl, config, }: FacePeMobileProviderProps): React.JSX.Element;
17
+ export declare function FacePeMobileProvider({ children, getToken, apiBaseUrl, config, }: FacePeMobileProviderProps): import("react/jsx-runtime").JSX.Element;
18
18
  export declare function useFacePeMobileContext(): FacePeContextValue;
19
19
  export {};
package/dist/index.d.ts CHANGED
@@ -2,3 +2,4 @@ export * from './types';
2
2
  export * from './FacePeMobileProvider';
3
3
  export * from './FacePeMobileAvatar';
4
4
  export * from './useFacePeAvatar';
5
+ export { SDK_VERSION } from './version';
package/dist/index.js CHANGED
@@ -2,3 +2,4 @@ export * from './types';
2
2
  export * from './FacePeMobileProvider';
3
3
  export * from './FacePeMobileAvatar';
4
4
  export * from './useFacePeAvatar';
5
+ export { SDK_VERSION } from './version';
package/dist/types.d.ts CHANGED
@@ -11,6 +11,37 @@ export interface TranscriptPayload {
11
11
  text: string;
12
12
  isFinal?: boolean;
13
13
  }
14
+ /** One line of a finished conversation, with the time it was said (ISO 8601). */
15
+ export interface FacePeConversationLine {
16
+ role: 'user' | 'assistant';
17
+ text: string;
18
+ at: string;
19
+ }
20
+ /**
21
+ * Everything about one conversation, given once when it ends
22
+ * (`onConversationEnd`). It stays in your app: the SDK sends it nowhere — you
23
+ * decide whether to show, keep or send it.
24
+ */
25
+ export interface FacePeConversation {
26
+ /** The conversation's id (its FacePe usage record), or null if none was opened. */
27
+ conversationId: string | null;
28
+ avatar: {
29
+ id: string;
30
+ name: string;
31
+ };
32
+ /** The language requested for the conversation (ISO 639-1). */
33
+ language: string | null;
34
+ platform: 'android' | 'ios' | 'web';
35
+ sdkVersion: string;
36
+ /** When it started and ended (ISO 8601). */
37
+ startedAt: string;
38
+ endedAt: string;
39
+ durationSec: number;
40
+ /** `ended`: by `stop()`; `disconnected`: the stream dropped; `unmounted`: the component went away. */
41
+ endReason: 'ended' | 'disconnected' | 'unmounted';
42
+ /** Every line, in order (a line that grew while being spoken appears once, complete). */
43
+ transcript: FacePeConversationLine[];
44
+ }
14
45
  export interface OrderItem {
15
46
  name: string;
16
47
  qty: number;
@@ -69,6 +100,8 @@ export interface FacePeMobileAvatarProps {
69
100
  onCheckout?: () => void;
70
101
  /** Called when stream connection status changes */
71
102
  onStatusChange?: (status: FacePeConnectionStatus) => void;
103
+ /** Called once when a conversation ends, with the whole conversation (stays in your app) */
104
+ onConversationEnd?: (conversation: FacePeConversation) => void;
72
105
  /** Called when an error occurs */
73
106
  onError?: (event: {
74
107
  code: string;
@@ -0,0 +1,2 @@
1
+ /** This package's version, reported in conversation records. Keep in step with package.json. */
2
+ export declare const SDK_VERSION = "1.3.0";
@@ -0,0 +1,2 @@
1
+ /** This package's version, reported in conversation records. Keep in step with package.json. */
2
+ export const SDK_VERSION = '1.3.0';
package/package.json CHANGED
@@ -1,53 +1,61 @@
1
- {
2
- "name": "@facepe/widget-mobile-app",
3
- "version": "1.1.0",
4
- "description": "FacePe AI 3D Video Avatar Native Live Streaming SDK for React Native (iOS & Android)",
5
- "main": "./dist/index.js",
6
- "module": "./dist/index.js",
7
- "types": "./dist/index.d.ts",
8
- "files": [
9
- "dist",
10
- "README.md"
11
- ],
12
- "publishConfig": {
13
- "access": "public"
14
- },
15
- "keywords": [
16
- "facepe",
17
- "avatar",
18
- "ai-avatar",
19
- "livekit",
20
- "react-native",
21
- "ios",
22
- "android",
23
- "webrtc",
24
- "streaming",
25
- "digital-human"
26
- ],
27
- "author": "FacePe AI",
28
- "license": "MIT",
29
- "scripts": {
30
- "lint": "tsc --noEmit",
31
- "build": "tsc"
32
- },
33
- "peerDependencies": {
34
- "@livekit/react-native": ">=2.0.0",
35
- "@livekit/react-native-webrtc": ">=124.0.0",
36
- "react": ">=18.0.0",
37
- "react-native": ">=0.70.0"
38
- },
39
- "peerDependenciesMeta": {
40
- "@livekit/react-native": {
41
- "optional": true
42
- },
43
- "@livekit/react-native-webrtc": {
44
- "optional": true
45
- }
46
- },
47
- "devDependencies": {
48
- "@types/react": "^18.3.3",
49
- "react": "18.3.1",
50
- "react-native": "0.74.0",
51
- "typescript": "^5.4.5"
52
- }
53
- }
1
+ {
2
+ "name": "@facepe/widget-mobile-app",
3
+ "version": "1.3.0",
4
+ "description": "FacePe AI 3D Video Avatar Native Live Streaming SDK for React Native (iOS & Android)",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "files": [
9
+ "dist",
10
+ "README.md"
11
+ ],
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
15
+ "keywords": [
16
+ "facepe",
17
+ "avatar",
18
+ "ai-avatar",
19
+ "livekit",
20
+ "react-native",
21
+ "ios",
22
+ "android",
23
+ "webrtc",
24
+ "streaming",
25
+ "digital-human"
26
+ ],
27
+ "author": "FacePe AI",
28
+ "license": "MIT",
29
+ "scripts": {
30
+ "lint": "tsc --noEmit",
31
+ "build": "tsc"
32
+ },
33
+ "peerDependencies": {
34
+ "@livekit/react-native": ">=2.0.0",
35
+ "@livekit/react-native-webrtc": ">=124.0.0",
36
+ "livekit-client": ">=2.0.0",
37
+ "react": ">=18.0.0",
38
+ "react-native": ">=0.70.0",
39
+ "@anam-ai/js-sdk": ">=4.27.0"
40
+ },
41
+ "peerDependenciesMeta": {
42
+ "@livekit/react-native": {
43
+ "optional": true
44
+ },
45
+ "@livekit/react-native-webrtc": {
46
+ "optional": true
47
+ },
48
+ "livekit-client": {
49
+ "optional": true
50
+ },
51
+ "@anam-ai/js-sdk": {
52
+ "optional": true
53
+ }
54
+ },
55
+ "devDependencies": {
56
+ "@types/react": "^18.3.3",
57
+ "react": "18.3.1",
58
+ "react-native": "0.74.0",
59
+ "typescript": "^5.4.5"
60
+ }
61
+ }