@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 CHANGED
@@ -1,3 +1,176 @@
1
- # Temporary Holding Version
1
+ # @facepe/widget-mobile-app
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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
+ }
@@ -0,0 +1,4 @@
1
+ export * from './types';
2
+ export * from './FacePeMobileProvider';
3
+ export * from './FacePeMobileAvatar';
4
+ export * from './useFacePeAvatar';
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export * from './types';
2
+ export * from './FacePeMobileProvider';
3
+ export * from './FacePeMobileAvatar';
4
+ export * from './useFacePeAvatar';
@@ -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": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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
+ }