@facepe/widget-mobile-app 0.0.0-stage → 1.0.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 +175 -2
- package/dist/FacePeMobileAvatar.d.ts +8 -0
- package/dist/FacePeMobileAvatar.js +155 -0
- package/dist/FacePeMobileProvider.d.ts +19 -0
- package/dist/FacePeMobileProvider.js +54 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/types.d.ts +67 -0
- package/dist/types.js +1 -0
- package/dist/useFacePeAvatar.d.ts +13 -0
- package/dist/useFacePeAvatar.js +23 -0
- package/package.json +51 -4
package/README.md
CHANGED
|
@@ -1,3 +1,176 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @facepe/widget-mobile-app
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The official **FacePe AI 3D Avatar 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 real-time speech recognition, natural audio dialog, and event callbacks.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 📱 Features
|
|
10
|
+
|
|
11
|
+
- **⚡ Low-Latency Live Video & Audio**: Real-time WebRTC media streaming for avatar rendering.
|
|
12
|
+
- **🗣️ Two-Way Audio Conversation**: Bidirectional voice talk with interrupt handling & VAD.
|
|
13
|
+
- **🍎 Full iOS Support**: Compatible with iOS 13.0+ (iPhone & iPad).
|
|
14
|
+
- **🤖 Full Android Support**: Compatible with Android 7.0+ (API 24+).
|
|
15
|
+
- **💬 Real-Time Transcripts**: Captions and user transcripts via `onTranscript`.
|
|
16
|
+
- **🛒 Conversational Ordering**: Extract order items and payment intents directly in your app.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 📦 Installation
|
|
21
|
+
|
|
22
|
+
In your React Native / Expo project:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install @facepe/widget-mobile-app react-native-webview
|
|
26
|
+
```
|
|
27
|
+
*(or using yarn / pnpm)*:
|
|
28
|
+
```bash
|
|
29
|
+
yarn add @facepe/widget-mobile-app react-native-webview
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## 🛠️ Platform Permissions Setup
|
|
35
|
+
|
|
36
|
+
### iOS Setup (`ios/Podfile` & `Info.plist`)
|
|
37
|
+
|
|
38
|
+
1. Open `ios/YourAppName/Info.plist` and add permission strings for microphone and camera:
|
|
39
|
+
|
|
40
|
+
```xml
|
|
41
|
+
<key>NSMicrophoneUsageDescription</key>
|
|
42
|
+
<string>We need access to your microphone to converse with the AI avatar.</string>
|
|
43
|
+
<key>NSCameraUsageDescription</key>
|
|
44
|
+
<string>We need access to your camera for interactive features.</string>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
2. Run CocoaPods:
|
|
48
|
+
```bash
|
|
49
|
+
cd ios && pod install && cd ..
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
### Android Setup (`android/app/src/main/AndroidManifest.xml`)
|
|
55
|
+
|
|
56
|
+
Open `android/app/src/main/AndroidManifest.xml` and include permissions:
|
|
57
|
+
|
|
58
|
+
```xml
|
|
59
|
+
<uses-permission android:name="android.permission.INTERNET" />
|
|
60
|
+
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
|
61
|
+
<uses-permission android:name="android.permission.CAMERA" />
|
|
62
|
+
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## 🚀 Quick Start Example
|
|
68
|
+
|
|
69
|
+
### 1. Server-side Token Endpoint
|
|
70
|
+
|
|
71
|
+
Exchange your `accessKey` and `secretKey` on your secure backend (Express / Next.js / Python / Go) so secrets are never exposed on mobile:
|
|
72
|
+
|
|
73
|
+
```javascript
|
|
74
|
+
// POST /api/facepe-token
|
|
75
|
+
app.get('/api/facepe-token', async (req, res) => {
|
|
76
|
+
const response = await fetch('https://api.facepe.ai/api/v1/sdk/token', {
|
|
77
|
+
method: 'POST',
|
|
78
|
+
headers: { 'Content-Type': 'application/json' },
|
|
79
|
+
body: JSON.stringify({
|
|
80
|
+
accessKey: process.env.FACEPE_SDK_ACCESS_KEY,
|
|
81
|
+
secretKey: process.env.FACEPE_SDK_SECRET_KEY,
|
|
82
|
+
channel: 'sdk',
|
|
83
|
+
}),
|
|
84
|
+
});
|
|
85
|
+
const data = await response.json();
|
|
86
|
+
res.send(data.data.token);
|
|
87
|
+
});
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
### 2. React Native Component
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
import React from 'react';
|
|
96
|
+
import { StyleSheet, View, SafeAreaView, Text } from 'react-native';
|
|
97
|
+
import {
|
|
98
|
+
FacePeMobileProvider,
|
|
99
|
+
FacePeMobileAvatar,
|
|
100
|
+
} from '@facepe/widget-mobile-app';
|
|
101
|
+
|
|
102
|
+
export default function App() {
|
|
103
|
+
return (
|
|
104
|
+
<SafeAreaView style={styles.container}>
|
|
105
|
+
<Text style={styles.title}>FacePe AI Digital Human</Text>
|
|
106
|
+
|
|
107
|
+
<FacePeMobileProvider
|
|
108
|
+
apiBaseUrl="https://api.facepe.ai/api/v1/sdk"
|
|
109
|
+
getToken={async () => {
|
|
110
|
+
const res = await fetch('https://your-backend.com/api/facepe-token');
|
|
111
|
+
return await res.text();
|
|
112
|
+
}}
|
|
113
|
+
>
|
|
114
|
+
<View style={styles.avatarContainer}>
|
|
115
|
+
<FacePeMobileAvatar
|
|
116
|
+
language="en"
|
|
117
|
+
onTranscript={({ payload }) => {
|
|
118
|
+
console.log(`[${payload.role}]:`, payload.text);
|
|
119
|
+
}}
|
|
120
|
+
onSpeakStart={() => console.log('Avatar speaking')}
|
|
121
|
+
onSpeakEnd={() => console.log('Avatar stopped speaking')}
|
|
122
|
+
onOrderUpdate={({ items }) => console.log('Order update:', items)}
|
|
123
|
+
onCheckout={() => console.log('Checkout requested!')}
|
|
124
|
+
/>
|
|
125
|
+
</View>
|
|
126
|
+
</FacePeMobileProvider>
|
|
127
|
+
</SafeAreaView>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const styles = StyleSheet.create({
|
|
132
|
+
container: {
|
|
133
|
+
flex: 1,
|
|
134
|
+
backgroundColor: '#0F172A',
|
|
135
|
+
},
|
|
136
|
+
title: {
|
|
137
|
+
color: '#FFFFFF',
|
|
138
|
+
fontSize: 20,
|
|
139
|
+
fontWeight: 'bold',
|
|
140
|
+
textAlign: 'center',
|
|
141
|
+
marginVertical: 16,
|
|
142
|
+
},
|
|
143
|
+
avatarContainer: {
|
|
144
|
+
flex: 1,
|
|
145
|
+
marginHorizontal: 16,
|
|
146
|
+
marginBottom: 24,
|
|
147
|
+
borderRadius: 24,
|
|
148
|
+
overflow: 'hidden',
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## ⚙️ Props & Configuration
|
|
156
|
+
|
|
157
|
+
### `<FacePeMobileAvatar />`
|
|
158
|
+
|
|
159
|
+
| Prop | Type | Description |
|
|
160
|
+
| :--- | :--- | :--- |
|
|
161
|
+
| `avatarKey` | `string` | Optional specific avatar key assigned to your company. |
|
|
162
|
+
| `language` | `string` | Spoken language (`'en'`, `'hi'`, `'es'`, `'ta'`, `'te'`, etc.). |
|
|
163
|
+
| `style` | `ViewStyle` | Style applied to container element. |
|
|
164
|
+
| `onTranscript` | `function` | Fires whenever the user or avatar utters a phrase. |
|
|
165
|
+
| `onSpeakStart` | `function` | Fires when the avatar begins vocal response. |
|
|
166
|
+
| `onSpeakEnd` | `function` | Fires when the avatar stops speaking. |
|
|
167
|
+
| `onUserSpeakStart` | `function` | Fires when the user starts speaking (voice activity). |
|
|
168
|
+
| `onUserSpeakEnd` | `function` | Fires when the user pauses / stops speaking. |
|
|
169
|
+
| `onOrderUpdate` | `function` | Fires when items are detected in conversation. |
|
|
170
|
+
| `onCheckout` | `function` | Fires when customer signals intent to pay. |
|
|
171
|
+
| `onError` | `function` | Error callback `{ code, message }`. |
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## 📄 License
|
|
176
|
+
MIT © FacePe AI
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { FacePeMobileAvatarProps } from './types';
|
|
3
|
+
export interface FacePeMobileAvatarRef {
|
|
4
|
+
start: () => Promise<void>;
|
|
5
|
+
stop: () => Promise<void>;
|
|
6
|
+
toggleMic: () => Promise<void>;
|
|
7
|
+
}
|
|
8
|
+
export declare const FacePeMobileAvatar: React.ForwardRefExoticComponent<FacePeMobileAvatarProps & React.RefAttributes<FacePeMobileAvatarRef>>;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useRef, useState, useImperativeHandle, forwardRef } from 'react';
|
|
3
|
+
import { View, StyleSheet, ActivityIndicator, Text } from 'react-native';
|
|
4
|
+
import { useFacePeMobileContext } from './FacePeMobileProvider';
|
|
5
|
+
export const FacePeMobileAvatar = forwardRef(({ avatarKey, language = 'en', style, onTranscript, onSpeakStart, onSpeakEnd, onUserSpeakStart, onUserSpeakEnd, onOrderUpdate, onCheckout, onError, }, ref) => {
|
|
6
|
+
const { token, loading, error, apiBaseUrl } = useFacePeMobileContext();
|
|
7
|
+
const [streamReady, setStreamReady] = useState(false);
|
|
8
|
+
const webViewRef = useRef(null);
|
|
9
|
+
// Build the mobile streaming URL
|
|
10
|
+
const streamUrl = (() => {
|
|
11
|
+
if (!token)
|
|
12
|
+
return '';
|
|
13
|
+
const base = apiBaseUrl.replace(/\/sdk$/, '').replace(/\/api\/v1$/, '');
|
|
14
|
+
const search = new URLSearchParams({
|
|
15
|
+
token,
|
|
16
|
+
embedded: '1',
|
|
17
|
+
mobile: '1',
|
|
18
|
+
lang: language,
|
|
19
|
+
});
|
|
20
|
+
if (avatarKey)
|
|
21
|
+
search.set('avatar', avatarKey);
|
|
22
|
+
return `${base}/?${search.toString()}`;
|
|
23
|
+
})();
|
|
24
|
+
useImperativeHandle(ref, () => ({
|
|
25
|
+
start: async () => {
|
|
26
|
+
webViewRef.current?.injectJavaScript?.('window.FacePeBridge && window.FacePeBridge.start()');
|
|
27
|
+
},
|
|
28
|
+
stop: async () => {
|
|
29
|
+
webViewRef.current?.injectJavaScript?.('window.FacePeBridge && window.FacePeBridge.stop()');
|
|
30
|
+
},
|
|
31
|
+
toggleMic: async () => {
|
|
32
|
+
webViewRef.current?.injectJavaScript?.('window.FacePeBridge && window.FacePeBridge.toggleMic()');
|
|
33
|
+
},
|
|
34
|
+
}));
|
|
35
|
+
// Handle incoming messages from bridge (transcripts, order updates, speaking states)
|
|
36
|
+
const handleBridgeMessage = (event) => {
|
|
37
|
+
try {
|
|
38
|
+
const raw = event?.nativeEvent?.data;
|
|
39
|
+
if (!raw)
|
|
40
|
+
return;
|
|
41
|
+
const msg = JSON.parse(raw);
|
|
42
|
+
switch (msg.type) {
|
|
43
|
+
case 'avatar:ready':
|
|
44
|
+
setStreamReady(true);
|
|
45
|
+
break;
|
|
46
|
+
case 'avatar:transcript':
|
|
47
|
+
onTranscript?.({ payload: msg.payload });
|
|
48
|
+
break;
|
|
49
|
+
case 'avatar:speak:start':
|
|
50
|
+
onSpeakStart?.();
|
|
51
|
+
break;
|
|
52
|
+
case 'avatar:speak:end':
|
|
53
|
+
onSpeakEnd?.();
|
|
54
|
+
break;
|
|
55
|
+
case 'user:speak:start':
|
|
56
|
+
onUserSpeakStart?.();
|
|
57
|
+
break;
|
|
58
|
+
case 'user:speak:end':
|
|
59
|
+
onUserSpeakEnd?.();
|
|
60
|
+
break;
|
|
61
|
+
case 'order:update':
|
|
62
|
+
onOrderUpdate?.({ items: msg.payload?.items || [] });
|
|
63
|
+
break;
|
|
64
|
+
case 'order:checkout':
|
|
65
|
+
onCheckout?.();
|
|
66
|
+
break;
|
|
67
|
+
case 'avatar:error':
|
|
68
|
+
onError?.({ code: msg.code || 'UNKNOWN', message: msg.message || 'Stream error' });
|
|
69
|
+
break;
|
|
70
|
+
default:
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
// ignore parse error
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
if (loading) {
|
|
79
|
+
return (_jsxs(View, { style: [styles.container, styles.center, style], children: [_jsx(ActivityIndicator, { size: "large", color: "#7c3aed" }), _jsx(Text, { style: styles.loadingText, children: "Connecting to FacePe AI Avatar\u2026" })] }));
|
|
80
|
+
}
|
|
81
|
+
if (error) {
|
|
82
|
+
return (_jsx(View, { style: [styles.container, styles.center, style], children: _jsxs(Text, { style: styles.errorText, children: ["Unable to connect to Avatar: ", error.message] }) }));
|
|
83
|
+
}
|
|
84
|
+
// Try dynamically requiring react-native-webview
|
|
85
|
+
let WebViewComponent = null;
|
|
86
|
+
try {
|
|
87
|
+
WebViewComponent = require('react-native-webview').WebView;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
// WebView not installed in project
|
|
91
|
+
}
|
|
92
|
+
if (!WebViewComponent) {
|
|
93
|
+
return (_jsxs(View, { style: [styles.container, styles.center, styles.fallbackBox, style], children: [_jsx(Text, { style: styles.noticeTitle, children: "FacePe Mobile SDK" }), _jsx(Text, { style: styles.noticeBody, children: "Please install `react-native-webview` or `@livekit/react-native` in your project to render the live stream:" }), _jsx(Text, { style: styles.codeSnippet, children: "npm install react-native-webview" })] }));
|
|
94
|
+
}
|
|
95
|
+
return (_jsx(View, { style: [styles.container, style], children: _jsx(WebViewComponent, { ref: webViewRef, source: { uri: streamUrl }, style: styles.webview, onMessage: handleBridgeMessage, allowsInlineMediaPlayback: true, mediaPlaybackRequiresUserAction: false, javaScriptEnabled: true, domStorageEnabled: true, cacheEnabled: true,
|
|
96
|
+
// iOS WebRTC permissions
|
|
97
|
+
originWhitelist: ['*'], allowsAirPlayForMediaPlayback: false, bounces: false, scrollEnabled: false }) }));
|
|
98
|
+
});
|
|
99
|
+
FacePeMobileAvatar.displayName = 'FacePeMobileAvatar';
|
|
100
|
+
const styles = StyleSheet.create({
|
|
101
|
+
container: {
|
|
102
|
+
width: '100%',
|
|
103
|
+
height: '100%',
|
|
104
|
+
backgroundColor: '#0F172A',
|
|
105
|
+
borderRadius: 16,
|
|
106
|
+
overflow: 'hidden',
|
|
107
|
+
},
|
|
108
|
+
webview: {
|
|
109
|
+
flex: 1,
|
|
110
|
+
backgroundColor: 'transparent',
|
|
111
|
+
},
|
|
112
|
+
center: {
|
|
113
|
+
justifyContent: 'center',
|
|
114
|
+
alignItems: 'center',
|
|
115
|
+
padding: 24,
|
|
116
|
+
},
|
|
117
|
+
loadingText: {
|
|
118
|
+
marginTop: 12,
|
|
119
|
+
color: '#94A3B8',
|
|
120
|
+
fontSize: 13,
|
|
121
|
+
fontWeight: '500',
|
|
122
|
+
},
|
|
123
|
+
errorText: {
|
|
124
|
+
color: '#EF4444',
|
|
125
|
+
fontSize: 13,
|
|
126
|
+
textAlign: 'center',
|
|
127
|
+
},
|
|
128
|
+
fallbackBox: {
|
|
129
|
+
backgroundColor: '#1E293B',
|
|
130
|
+
borderColor: '#334155',
|
|
131
|
+
borderWidth: 1,
|
|
132
|
+
},
|
|
133
|
+
noticeTitle: {
|
|
134
|
+
color: '#FFFFFF',
|
|
135
|
+
fontWeight: '700',
|
|
136
|
+
fontSize: 16,
|
|
137
|
+
marginBottom: 8,
|
|
138
|
+
},
|
|
139
|
+
noticeBody: {
|
|
140
|
+
color: '#94A3B8',
|
|
141
|
+
fontSize: 13,
|
|
142
|
+
textAlign: 'center',
|
|
143
|
+
lineHeight: 18,
|
|
144
|
+
marginBottom: 12,
|
|
145
|
+
},
|
|
146
|
+
codeSnippet: {
|
|
147
|
+
color: '#A78BFA',
|
|
148
|
+
backgroundColor: '#0F172A',
|
|
149
|
+
paddingHorizontal: 12,
|
|
150
|
+
paddingVertical: 6,
|
|
151
|
+
borderRadius: 8,
|
|
152
|
+
fontFamily: 'monospace',
|
|
153
|
+
fontSize: 12,
|
|
154
|
+
},
|
|
155
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import React, { type ReactNode } from 'react';
|
|
2
|
+
import type { FacePeMobileConfig, TokenProvider } from './types';
|
|
3
|
+
interface FacePeContextValue {
|
|
4
|
+
config: FacePeMobileConfig;
|
|
5
|
+
token: string | null;
|
|
6
|
+
loading: boolean;
|
|
7
|
+
error: Error | null;
|
|
8
|
+
refreshToken: (force?: boolean) => Promise<string | null>;
|
|
9
|
+
apiBaseUrl: string;
|
|
10
|
+
}
|
|
11
|
+
export interface FacePeMobileProviderProps {
|
|
12
|
+
children: ReactNode;
|
|
13
|
+
getToken: TokenProvider;
|
|
14
|
+
apiBaseUrl?: string;
|
|
15
|
+
config?: FacePeMobileConfig;
|
|
16
|
+
}
|
|
17
|
+
export declare function FacePeMobileProvider({ children, getToken, apiBaseUrl, config, }: FacePeMobileProviderProps): React.JSX.Element;
|
|
18
|
+
export declare function useFacePeMobileContext(): FacePeContextValue;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext, useState, useEffect, useRef, useMemo } from 'react';
|
|
3
|
+
const FacePeContext = createContext(null);
|
|
4
|
+
export function FacePeMobileProvider({ children, getToken, apiBaseUrl = 'https://api.facepe.ai/api/v1/sdk', config = {}, }) {
|
|
5
|
+
const [token, setToken] = useState(null);
|
|
6
|
+
const [loading, setLoading] = useState(true);
|
|
7
|
+
const [error, setError] = useState(null);
|
|
8
|
+
const tokenPromiseRef = useRef(null);
|
|
9
|
+
const refreshToken = async (force = false) => {
|
|
10
|
+
if (tokenPromiseRef.current && !force) {
|
|
11
|
+
return tokenPromiseRef.current;
|
|
12
|
+
}
|
|
13
|
+
setLoading(true);
|
|
14
|
+
setError(null);
|
|
15
|
+
const promise = (async () => {
|
|
16
|
+
try {
|
|
17
|
+
const fetched = await getToken({ forceRefresh: force });
|
|
18
|
+
setToken(fetched);
|
|
19
|
+
setLoading(false);
|
|
20
|
+
return fetched;
|
|
21
|
+
}
|
|
22
|
+
catch (err) {
|
|
23
|
+
const normalized = err instanceof Error ? err : new Error(String(err));
|
|
24
|
+
setError(normalized);
|
|
25
|
+
setLoading(false);
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
finally {
|
|
29
|
+
tokenPromiseRef.current = null;
|
|
30
|
+
}
|
|
31
|
+
})();
|
|
32
|
+
tokenPromiseRef.current = promise;
|
|
33
|
+
return promise;
|
|
34
|
+
};
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
void refreshToken();
|
|
37
|
+
}, []);
|
|
38
|
+
const value = useMemo(() => ({
|
|
39
|
+
config,
|
|
40
|
+
token,
|
|
41
|
+
loading,
|
|
42
|
+
error,
|
|
43
|
+
refreshToken,
|
|
44
|
+
apiBaseUrl: config.apiBaseUrl || apiBaseUrl,
|
|
45
|
+
}), [config, token, loading, error, apiBaseUrl]);
|
|
46
|
+
return _jsx(FacePeContext.Provider, { value: value, children: children });
|
|
47
|
+
}
|
|
48
|
+
export function useFacePeMobileContext() {
|
|
49
|
+
const ctx = useContext(FacePeContext);
|
|
50
|
+
if (!ctx) {
|
|
51
|
+
throw new Error('useFacePeMobileContext must be used within a <FacePeMobileProvider>');
|
|
52
|
+
}
|
|
53
|
+
return ctx;
|
|
54
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export type AvatarGender = 'female' | 'male';
|
|
2
|
+
export type StreamMode = 'native' | 'webview' | 'auto';
|
|
3
|
+
export interface FacePeMobileTheme {
|
|
4
|
+
primaryColor?: string;
|
|
5
|
+
backgroundColor?: string;
|
|
6
|
+
borderRadius?: number;
|
|
7
|
+
textColor?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface TranscriptPayload {
|
|
10
|
+
role: 'user' | 'assistant';
|
|
11
|
+
text: string;
|
|
12
|
+
isFinal?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export interface OrderItem {
|
|
15
|
+
name: string;
|
|
16
|
+
qty: number;
|
|
17
|
+
price?: number;
|
|
18
|
+
}
|
|
19
|
+
export interface FacePeMobileConfig {
|
|
20
|
+
/** FacePe Gateway URL (e.g. https://api.facepe.ai/api/v1/sdk) */
|
|
21
|
+
apiBaseUrl?: string;
|
|
22
|
+
/** Avatar identifier or key */
|
|
23
|
+
avatarKey?: string;
|
|
24
|
+
/** Default language code (e.g., 'en', 'es', 'hi') */
|
|
25
|
+
language?: string;
|
|
26
|
+
/** Integration mode: 'auto', 'native', or 'webview' */
|
|
27
|
+
mode?: StreamMode;
|
|
28
|
+
/** Theme styling overrides */
|
|
29
|
+
theme?: FacePeMobileTheme;
|
|
30
|
+
/** Optional audio autoplay unblocking flag */
|
|
31
|
+
autoStart?: boolean;
|
|
32
|
+
}
|
|
33
|
+
export interface TokenProviderParams {
|
|
34
|
+
forceRefresh?: boolean;
|
|
35
|
+
}
|
|
36
|
+
export type TokenProvider = (params: TokenProviderParams) => Promise<string>;
|
|
37
|
+
export interface FacePeMobileAvatarProps {
|
|
38
|
+
/** Optional avatar key override */
|
|
39
|
+
avatarKey?: string;
|
|
40
|
+
/** Language to converse in ('en', 'es', 'hi', etc.) */
|
|
41
|
+
language?: string;
|
|
42
|
+
/** Custom style object */
|
|
43
|
+
style?: any;
|
|
44
|
+
/** Called when real-time speech transcription arrives */
|
|
45
|
+
onTranscript?: (event: {
|
|
46
|
+
payload: TranscriptPayload;
|
|
47
|
+
}) => void;
|
|
48
|
+
/** Called when avatar starts speaking */
|
|
49
|
+
onSpeakStart?: () => void;
|
|
50
|
+
/** Called when avatar finishes speaking */
|
|
51
|
+
onSpeakEnd?: () => void;
|
|
52
|
+
/** Called when user starts speaking (detected by VAD) */
|
|
53
|
+
onUserSpeakStart?: () => void;
|
|
54
|
+
/** Called when user finishes speaking */
|
|
55
|
+
onUserSpeakEnd?: () => void;
|
|
56
|
+
/** Called when an order or intent snapshot is emitted */
|
|
57
|
+
onOrderUpdate?: (event: {
|
|
58
|
+
items: OrderItem[];
|
|
59
|
+
}) => void;
|
|
60
|
+
/** Called when customer says checkout / pay */
|
|
61
|
+
onCheckout?: () => void;
|
|
62
|
+
/** Called when an error occurs */
|
|
63
|
+
onError?: (event: {
|
|
64
|
+
code: string;
|
|
65
|
+
message: string;
|
|
66
|
+
}) => void;
|
|
67
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { TranscriptPayload, OrderItem } from './types';
|
|
2
|
+
export interface UseFacePeAvatarResult {
|
|
3
|
+
token: string | null;
|
|
4
|
+
loading: boolean;
|
|
5
|
+
error: Error | null;
|
|
6
|
+
transcripts: TranscriptPayload[];
|
|
7
|
+
isAvatarSpeaking: boolean;
|
|
8
|
+
isUserSpeaking: boolean;
|
|
9
|
+
orderItems: OrderItem[];
|
|
10
|
+
refreshToken: (force?: boolean) => Promise<string | null>;
|
|
11
|
+
clearTranscripts: () => void;
|
|
12
|
+
}
|
|
13
|
+
export declare function useFacePeAvatar(): UseFacePeAvatarResult;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { useState, useCallback } from 'react';
|
|
2
|
+
import { useFacePeMobileContext } from './FacePeMobileProvider';
|
|
3
|
+
export function useFacePeAvatar() {
|
|
4
|
+
const { token, loading, error, refreshToken } = useFacePeMobileContext();
|
|
5
|
+
const [transcripts, setTranscripts] = useState([]);
|
|
6
|
+
const [isAvatarSpeaking, setIsAvatarSpeaking] = useState(false);
|
|
7
|
+
const [isUserSpeaking, setIsUserSpeaking] = useState(false);
|
|
8
|
+
const [orderItems, setOrderItems] = useState([]);
|
|
9
|
+
const clearTranscripts = useCallback(() => {
|
|
10
|
+
setTranscripts([]);
|
|
11
|
+
}, []);
|
|
12
|
+
return {
|
|
13
|
+
token,
|
|
14
|
+
loading,
|
|
15
|
+
error,
|
|
16
|
+
transcripts,
|
|
17
|
+
isAvatarSpeaking,
|
|
18
|
+
isUserSpeaking,
|
|
19
|
+
orderItems,
|
|
20
|
+
refreshToken,
|
|
21
|
+
clearTranscripts,
|
|
22
|
+
};
|
|
23
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,53 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@facepe/widget-mobile-app",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "FacePe AI 3D Video Avatar 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
|
+
],
|
|
26
|
+
"author": "FacePe AI",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"scripts": {
|
|
29
|
+
"lint": "tsc --noEmit",
|
|
30
|
+
"build": "tsc"
|
|
31
|
+
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"react": ">=18.0.0",
|
|
34
|
+
"react-native": ">=0.70.0"
|
|
35
|
+
},
|
|
36
|
+
"peerDependenciesMeta": {
|
|
37
|
+
"@livekit/react-native": {
|
|
38
|
+
"optional": true
|
|
39
|
+
},
|
|
40
|
+
"@livekit/react-native-webrtc": {
|
|
41
|
+
"optional": true
|
|
42
|
+
},
|
|
43
|
+
"react-native-webview": {
|
|
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
|
+
}
|