Compare commits
10 Commits
4b97689dd3
...
6d017ea617
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d017ea617 | ||
|
|
a578ec8081 | ||
|
|
5ecb5f9683 | ||
|
|
8d98bab3cf | ||
|
|
e36b9bbf4a | ||
|
|
d9fff44fc9 | ||
|
|
42e3f389f7 | ||
|
|
ac6d458aae | ||
|
|
204cb87f05 | ||
|
|
906213e620 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -54,3 +54,4 @@ store-screenshots/
|
|||||||
|
|
||||||
# Build artifacts
|
# Build artifacts
|
||||||
WellNuoLite-Android/
|
WellNuoLite-Android/
|
||||||
|
julia-agent/julia-ai/google-credentials.json
|
||||||
|
|||||||
16
app.json
16
app.json
@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"expo": {
|
"expo": {
|
||||||
"name": "WellNuo",
|
"name": "WellNuo ROBUST",
|
||||||
"slug": "WellNuo",
|
"slug": "WellNuo",
|
||||||
"version": "1.0.4",
|
"version": "1.0.5",
|
||||||
"orientation": "portrait",
|
"orientation": "portrait",
|
||||||
"icon": "./assets/images/icon.png",
|
"icon": "./assets/images/icon.png",
|
||||||
"scheme": "wellnuo",
|
"scheme": "wellnuo",
|
||||||
@ -28,6 +28,7 @@
|
|||||||
},
|
},
|
||||||
"android": {
|
"android": {
|
||||||
"package": "com.wellnuo.app",
|
"package": "com.wellnuo.app",
|
||||||
|
"softwareKeyboardLayoutMode": "resize",
|
||||||
"adaptiveIcon": {
|
"adaptiveIcon": {
|
||||||
"backgroundColor": "#E6F4FE",
|
"backgroundColor": "#E6F4FE",
|
||||||
"foregroundImage": "./assets/images/android-icon-foreground.png",
|
"foregroundImage": "./assets/images/android-icon-foreground.png",
|
||||||
@ -68,13 +69,6 @@
|
|||||||
"backgroundColor": "#000000"
|
"backgroundColor": "#000000"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
|
||||||
[
|
|
||||||
"expo-speech-recognition",
|
|
||||||
{
|
|
||||||
"microphonePermission": "WellNuo needs access to your microphone for voice commands.",
|
|
||||||
"speechRecognitionPermission": "WellNuo uses speech recognition to convert your voice to text."
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"experiments": {
|
"experiments": {
|
||||||
@ -84,9 +78,9 @@
|
|||||||
"extra": {
|
"extra": {
|
||||||
"router": {},
|
"router": {},
|
||||||
"eas": {
|
"eas": {
|
||||||
"projectId": "a845255d-c966-4f12-aa60-c452c2d0c60d"
|
"projectId": "8618c68a-6942-47ec-94f5-787fdbe5c0b4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"owner": "serter20692"
|
"owner": "rzmrzli"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -49,12 +49,14 @@ export default function LoginScreen() {
|
|||||||
return (
|
return (
|
||||||
<KeyboardAvoidingView
|
<KeyboardAvoidingView
|
||||||
style={styles.container}
|
style={styles.container}
|
||||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
behavior="padding"
|
||||||
|
keyboardVerticalOffset={Platform.OS === 'ios' ? 0 : 20}
|
||||||
>
|
>
|
||||||
<ScrollView
|
<ScrollView
|
||||||
contentContainerStyle={styles.scrollContent}
|
contentContainerStyle={styles.scrollContent}
|
||||||
keyboardShouldPersistTaps="handled"
|
keyboardShouldPersistTaps="handled"
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
|
bounces={false}
|
||||||
>
|
>
|
||||||
{/* Logo / Header */}
|
{/* Logo / Header */}
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
@ -132,8 +134,9 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
scrollContent: {
|
scrollContent: {
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
|
justifyContent: 'center',
|
||||||
paddingHorizontal: Spacing.lg,
|
paddingHorizontal: Spacing.lg,
|
||||||
paddingTop: Spacing.xxl + Spacing.xl,
|
paddingTop: Spacing.xl,
|
||||||
paddingBottom: Spacing.xl,
|
paddingBottom: Spacing.xl,
|
||||||
},
|
},
|
||||||
header: {
|
header: {
|
||||||
|
|||||||
@ -56,7 +56,7 @@ export default function TabLayout() {
|
|||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/* Voice tab hidden - using Debug for testing */}
|
{/* Voice tab - HIDDEN (calls go through Julia tab chat screen) */}
|
||||||
<Tabs.Screen
|
<Tabs.Screen
|
||||||
name="voice"
|
name="voice"
|
||||||
options={{
|
options={{
|
||||||
@ -72,11 +72,14 @@ export default function TabLayout() {
|
|||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/* Debug tab - hidden in production */}
|
{/* Debug tab - Voice call debugging with detailed logs */}
|
||||||
<Tabs.Screen
|
<Tabs.Screen
|
||||||
name="debug"
|
name="debug"
|
||||||
options={{
|
options={{
|
||||||
href: null,
|
title: 'Debug',
|
||||||
|
tabBarIcon: ({ color, size }) => (
|
||||||
|
<Feather name="terminal" size={22} color={color} />
|
||||||
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/* Hide explore tab */}
|
{/* Hide explore tab */}
|
||||||
|
|||||||
@ -9,9 +9,9 @@ import { useBeneficiary } from '@/contexts/BeneficiaryContext';
|
|||||||
import { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme';
|
import { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme';
|
||||||
import { FullScreenError } from '@/components/ui/ErrorMessage';
|
import { FullScreenError } from '@/components/ui/ErrorMessage';
|
||||||
|
|
||||||
// Start with login page, then redirect to dashboard after auth
|
// Use dev.kresoja.net for MobileAppLogin support
|
||||||
const LOGIN_URL = 'https://react.eluxnetworks.net/login';
|
// After MobileAppLogin() is called on /login, it auto-redirects to /dashboard
|
||||||
const DASHBOARD_URL = 'https://react.eluxnetworks.net/dashboard';
|
const LOGIN_URL = 'https://dev.kresoja.net/login';
|
||||||
|
|
||||||
export default function BeneficiaryDashboardScreen() {
|
export default function BeneficiaryDashboardScreen() {
|
||||||
const { id } = useLocalSearchParams<{ id: string }>();
|
const { id } = useLocalSearchParams<{ id: string }>();
|
||||||
@ -24,7 +24,7 @@ export default function BeneficiaryDashboardScreen() {
|
|||||||
const [userName, setUserName] = useState<string | null>(null);
|
const [userName, setUserName] = useState<string | null>(null);
|
||||||
const [userId, setUserId] = useState<string | null>(null);
|
const [userId, setUserId] = useState<string | null>(null);
|
||||||
const [isTokenLoaded, setIsTokenLoaded] = useState(false);
|
const [isTokenLoaded, setIsTokenLoaded] = useState(false);
|
||||||
const [webViewUrl, setWebViewUrl] = useState(DASHBOARD_URL);
|
const [hasCalledMobileLogin, setHasCalledMobileLogin] = useState(false);
|
||||||
|
|
||||||
const beneficiaryName = currentBeneficiary?.name || 'Dashboard';
|
const beneficiaryName = currentBeneficiary?.name || 'Dashboard';
|
||||||
|
|
||||||
@ -48,31 +48,49 @@ export default function BeneficiaryDashboardScreen() {
|
|||||||
loadCredentials();
|
loadCredentials();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// JavaScript to inject token into localStorage before page loads
|
// JavaScript to call MobileAppLogin after page loads
|
||||||
// Web app uses auth2 key with JSON object: {username, token, user_id}
|
// MobileAppLogin sets is_mobile=1, saves auth2, and redirects to /dashboard
|
||||||
const injectedJavaScript = authToken
|
// This hides desktop navigation (login/logout/dashboard buttons)
|
||||||
? `
|
const getMobileLoginScript = () => {
|
||||||
|
if (!authToken) return '';
|
||||||
|
|
||||||
|
return `
|
||||||
(function() {
|
(function() {
|
||||||
try {
|
try {
|
||||||
// Web app expects auth2 as JSON object with these exact fields
|
// Wait for window.MobileAppLogin to be available
|
||||||
var authData = {
|
var checkInterval = setInterval(function() {
|
||||||
username: '${userName || ''}',
|
if (typeof window.MobileAppLogin === 'function') {
|
||||||
token: '${authToken}',
|
clearInterval(checkInterval);
|
||||||
user_id: ${userId || 'null'}
|
console.log('MobileAppLogin found, calling with auth data...');
|
||||||
};
|
|
||||||
localStorage.setItem('auth2', JSON.stringify(authData));
|
var authData = {
|
||||||
console.log('Auth data injected:', authData.username, 'user_id:', authData.user_id);
|
username: '${userName || ''}',
|
||||||
|
token: '${authToken}',
|
||||||
|
user_id: ${userId || 'null'}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.MobileAppLogin(authData);
|
||||||
|
console.log('MobileAppLogin called successfully');
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
// Timeout after 5 seconds
|
||||||
|
setTimeout(function() {
|
||||||
|
clearInterval(checkInterval);
|
||||||
|
console.log('MobileAppLogin timeout - function not found');
|
||||||
|
}, 5000);
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
console.error('Failed to inject token:', e);
|
console.error('Failed to call MobileAppLogin:', e);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
true;
|
true;
|
||||||
`
|
`;
|
||||||
: '';
|
};
|
||||||
|
|
||||||
const handleRefresh = () => {
|
const handleRefresh = () => {
|
||||||
setError(null);
|
setError(null);
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
setHasCalledMobileLogin(false); // Reset to call MobileAppLogin again
|
||||||
webViewRef.current?.reload();
|
webViewRef.current?.reload();
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -84,6 +102,21 @@ export default function BeneficiaryDashboardScreen() {
|
|||||||
|
|
||||||
const handleNavigationStateChange = (navState: any) => {
|
const handleNavigationStateChange = (navState: any) => {
|
||||||
setCanGoBack(navState.canGoBack);
|
setCanGoBack(navState.canGoBack);
|
||||||
|
|
||||||
|
// Auto-relogin when session expires (redirected to /login)
|
||||||
|
// If we're on /login and MobileAppLogin was already called, session expired
|
||||||
|
const url = navState.url || '';
|
||||||
|
const isOnLoginPage = url.includes('/login');
|
||||||
|
|
||||||
|
if (isOnLoginPage && hasCalledMobileLogin && authToken) {
|
||||||
|
console.log('[Dashboard] Session expired, re-authenticating...');
|
||||||
|
// Reset and call MobileAppLogin again
|
||||||
|
setHasCalledMobileLogin(false);
|
||||||
|
setTimeout(() => {
|
||||||
|
setHasCalledMobileLogin(true);
|
||||||
|
webViewRef.current?.injectJavaScript(getMobileLoginScript());
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleError = () => {
|
const handleError = () => {
|
||||||
@ -169,10 +202,20 @@ export default function BeneficiaryDashboardScreen() {
|
|||||||
<View style={styles.webViewContainer}>
|
<View style={styles.webViewContainer}>
|
||||||
<WebView
|
<WebView
|
||||||
ref={webViewRef}
|
ref={webViewRef}
|
||||||
source={{ uri: webViewUrl }}
|
source={{ uri: LOGIN_URL }}
|
||||||
style={styles.webView}
|
style={styles.webView}
|
||||||
onLoadStart={() => setIsLoading(true)}
|
onLoadStart={() => setIsLoading(true)}
|
||||||
onLoadEnd={() => setIsLoading(false)}
|
onLoadEnd={() => {
|
||||||
|
setIsLoading(false);
|
||||||
|
// Call MobileAppLogin only once after first load
|
||||||
|
if (!hasCalledMobileLogin && authToken) {
|
||||||
|
setHasCalledMobileLogin(true);
|
||||||
|
// Small delay to ensure React app has mounted
|
||||||
|
setTimeout(() => {
|
||||||
|
webViewRef.current?.injectJavaScript(getMobileLoginScript());
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
}}
|
||||||
onError={handleError}
|
onError={handleError}
|
||||||
onHttpError={handleError}
|
onHttpError={handleError}
|
||||||
onNavigationStateChange={handleNavigationStateChange}
|
onNavigationStateChange={handleNavigationStateChange}
|
||||||
@ -181,10 +224,6 @@ export default function BeneficiaryDashboardScreen() {
|
|||||||
startInLoadingState={true}
|
startInLoadingState={true}
|
||||||
scalesPageToFit={true}
|
scalesPageToFit={true}
|
||||||
allowsBackForwardNavigationGestures={true}
|
allowsBackForwardNavigationGestures={true}
|
||||||
// Inject token into localStorage BEFORE content loads
|
|
||||||
injectedJavaScriptBeforeContentLoaded={injectedJavaScript}
|
|
||||||
// Also inject after load in case page reads localStorage late
|
|
||||||
injectedJavaScript={injectedJavaScript}
|
|
||||||
renderLoading={() => (
|
renderLoading={() => (
|
||||||
<View style={styles.loadingContainer}>
|
<View style={styles.loadingContainer}>
|
||||||
<ActivityIndicator size="large" color={AppColors.primary} />
|
<ActivityIndicator size="large" color={AppColors.primary} />
|
||||||
|
|||||||
@ -13,12 +13,12 @@ import {
|
|||||||
FlatList,
|
FlatList,
|
||||||
TextInput,
|
TextInput,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
KeyboardAvoidingView,
|
|
||||||
Platform,
|
|
||||||
Modal,
|
Modal,
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
Keyboard,
|
Keyboard,
|
||||||
|
Platform,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
|
import { KeyboardAvoidingView } from 'react-native-keyboard-controller';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { useRouter } from 'expo-router';
|
import { useRouter } from 'expo-router';
|
||||||
@ -34,6 +34,16 @@ const API_URL = 'https://eluxnetworks.net/function/well-api/api';
|
|||||||
const WELLNUO_USER = 'anandk';
|
const WELLNUO_USER = 'anandk';
|
||||||
const WELLNUO_PASSWORD = 'anandk_8';
|
const WELLNUO_PASSWORD = 'anandk_8';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// SINGLE_DEPLOYMENT_MODE
|
||||||
|
// When true: sends only deployment_id (no beneficiary_names_dict)
|
||||||
|
// When false: sends both deployment_id AND beneficiary_names_dict
|
||||||
|
//
|
||||||
|
// Use true for WellNuo Lite (single beneficiary per user)
|
||||||
|
// Use false for full WellNuo app (multiple beneficiaries)
|
||||||
|
// ============================================================================
|
||||||
|
const SINGLE_DEPLOYMENT_MODE = true;
|
||||||
|
|
||||||
// Keywords for question normalization (same as julia-agent/julia-ai/src/agent.py)
|
// Keywords for question normalization (same as julia-agent/julia-ai/src/agent.py)
|
||||||
const STATUS_KEYWORDS = [
|
const STATUS_KEYWORDS = [
|
||||||
/\bhow\s+is\b/i,
|
/\bhow\s+is\b/i,
|
||||||
@ -260,19 +270,36 @@ export default function ChatScreen() {
|
|||||||
// (same logic as julia-agent/julia-ai/src/agent.py)
|
// (same logic as julia-agent/julia-ai/src/agent.py)
|
||||||
const normalizedQuestion = normalizeQuestion(trimmedInput);
|
const normalizedQuestion = normalizeQuestion(trimmedInput);
|
||||||
|
|
||||||
|
// Build beneficiary_names_dict from all loaded beneficiaries
|
||||||
|
// Format: {"21": "papa", "69": "David"}
|
||||||
|
const beneficiaryNamesDict: Record<string, string> = {};
|
||||||
|
beneficiaries.forEach(b => {
|
||||||
|
beneficiaryNamesDict[b.id.toString()] = b.name;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get deployment_id from current beneficiary or fallback to first one
|
||||||
|
const deploymentId = currentBeneficiary?.id?.toString() || beneficiaries[0]?.id?.toString() || '21';
|
||||||
|
|
||||||
// Call API with EXACT same params as voice agent
|
// Call API with EXACT same params as voice agent
|
||||||
// Using ask_wellnuo_ai instead of voice_ask (same params, same response format)
|
// SINGLE_DEPLOYMENT_MODE: sends only deployment_id (no beneficiary_names_dict)
|
||||||
|
const requestParams: Record<string, string> = {
|
||||||
|
function: 'ask_wellnuo_ai',
|
||||||
|
clientId: 'MA_001',
|
||||||
|
user_name: WELLNUO_USER,
|
||||||
|
token: token,
|
||||||
|
question: normalizedQuestion,
|
||||||
|
deployment_id: deploymentId,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Only add beneficiary_names_dict if NOT in single deployment mode
|
||||||
|
if (!SINGLE_DEPLOYMENT_MODE) {
|
||||||
|
requestParams.beneficiary_names_dict = JSON.stringify(beneficiaryNamesDict);
|
||||||
|
}
|
||||||
|
|
||||||
const response = await fetch(API_URL, {
|
const response = await fetch(API_URL, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
body: new URLSearchParams({
|
body: new URLSearchParams(requestParams).toString(),
|
||||||
function: 'ask_wellnuo_ai',
|
|
||||||
clientId: 'MA_001',
|
|
||||||
user_name: WELLNUO_USER,
|
|
||||||
token: token,
|
|
||||||
question: normalizedQuestion,
|
|
||||||
deployment_id: '21',
|
|
||||||
}).toString(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
@ -435,8 +462,7 @@ export default function ChatScreen() {
|
|||||||
{/* Messages */}
|
{/* Messages */}
|
||||||
<KeyboardAvoidingView
|
<KeyboardAvoidingView
|
||||||
style={styles.chatContainer}
|
style={styles.chatContainer}
|
||||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
behavior="padding"
|
||||||
keyboardVerticalOffset={Platform.OS === 'ios' ? 0 : 0}
|
|
||||||
>
|
>
|
||||||
<FlatList
|
<FlatList
|
||||||
ref={flatListRef}
|
ref={flatListRef}
|
||||||
|
|||||||
@ -20,6 +20,8 @@ import {
|
|||||||
Share,
|
Share,
|
||||||
AppState,
|
AppState,
|
||||||
AppStateStatus,
|
AppStateStatus,
|
||||||
|
TextInput,
|
||||||
|
KeyboardAvoidingView,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
@ -28,10 +30,11 @@ import { activateKeepAwakeAsync, deactivateKeepAwake } from 'expo-keep-awake';
|
|||||||
import type { Room as RoomType } from 'livekit-client';
|
import type { Room as RoomType } from 'livekit-client';
|
||||||
import { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme';
|
import { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme';
|
||||||
import { getToken, VOICE_NAME } from '@/services/livekitService';
|
import { getToken, VOICE_NAME } from '@/services/livekitService';
|
||||||
|
import { api } from '@/services/api';
|
||||||
|
import { useBeneficiary } from '@/contexts/BeneficiaryContext';
|
||||||
import {
|
import {
|
||||||
configureAudioForVoiceCall,
|
configureAudioForVoiceCall,
|
||||||
stopAudioSession,
|
stopAudioSession,
|
||||||
setAudioOutput,
|
|
||||||
} from '@/utils/audioSession';
|
} from '@/utils/audioSession';
|
||||||
import {
|
import {
|
||||||
startVoiceCallService,
|
startVoiceCallService,
|
||||||
@ -56,12 +59,64 @@ export default function DebugScreen() {
|
|||||||
const [logs, setLogs] = useState<LogEntry[]>([]);
|
const [logs, setLogs] = useState<LogEntry[]>([]);
|
||||||
const [callState, setCallState] = useState<CallState>('idle');
|
const [callState, setCallState] = useState<CallState>('idle');
|
||||||
const [callDuration, setCallDuration] = useState(0);
|
const [callDuration, setCallDuration] = useState(0);
|
||||||
const [isSpeakerOn, setIsSpeakerOn] = useState(true); // Default to speaker
|
const [agentState, setAgentState] = useState<string>('—'); // listening/thinking/speaking
|
||||||
|
const [lastUserText, setLastUserText] = useState<string>(''); // Последний распознанный текст пользователя
|
||||||
|
const [lastAgentText, setLastAgentText] = useState<string>(''); // Последний ответ агента
|
||||||
|
const [micLevel, setMicLevel] = useState<number>(0); // Уровень микрофона 0-100
|
||||||
|
const [deploymentId, setDeploymentIdState] = useState<string>(''); // Custom deployment ID
|
||||||
|
const [loadingBeneficiary, setLoadingBeneficiary] = useState(true);
|
||||||
|
const [accumulateResponses, setAccumulateResponses] = useState(true); // Накапливать chunks до полного ответа
|
||||||
const flatListRef = useRef<FlatList>(null);
|
const flatListRef = useRef<FlatList>(null);
|
||||||
|
|
||||||
|
// Refs для накопления chunks
|
||||||
|
const accumulatedUserTextRef = useRef<string>('');
|
||||||
|
const accumulatedAgentTextRef = useRef<string>('');
|
||||||
|
const lastUserSegmentIdRef = useRef<string | null>(null);
|
||||||
|
const lastAgentSegmentIdRef = useRef<string | null>(null);
|
||||||
const roomRef = useRef<RoomType | null>(null);
|
const roomRef = useRef<RoomType | null>(null);
|
||||||
const callStartTimeRef = useRef<number | null>(null);
|
const callStartTimeRef = useRef<number | null>(null);
|
||||||
const appStateRef = useRef<AppStateStatus>(AppState.currentState);
|
const appStateRef = useRef<AppStateStatus>(AppState.currentState);
|
||||||
|
|
||||||
|
const { currentBeneficiary, setDebugDeploymentId } = useBeneficiary();
|
||||||
|
|
||||||
|
// Sync deploymentId with context for voice-call.tsx to use
|
||||||
|
const setDeploymentId = useCallback((id: string) => {
|
||||||
|
setDeploymentIdState(id);
|
||||||
|
// Update context so voice-call.tsx can access it
|
||||||
|
setDebugDeploymentId(id.trim() || null);
|
||||||
|
}, [setDebugDeploymentId]);
|
||||||
|
|
||||||
|
// Load default deployment ID from first beneficiary
|
||||||
|
useEffect(() => {
|
||||||
|
const loadDefaultDeploymentId = async () => {
|
||||||
|
try {
|
||||||
|
// First check if currentBeneficiary is available
|
||||||
|
if (currentBeneficiary?.id) {
|
||||||
|
const id = currentBeneficiary.id.toString();
|
||||||
|
setDeploymentIdState(id);
|
||||||
|
setDebugDeploymentId(id); // Also set in context
|
||||||
|
setLoadingBeneficiary(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise load from API
|
||||||
|
const response = await api.getAllBeneficiaries();
|
||||||
|
if (response.ok && response.data && response.data.length > 0) {
|
||||||
|
const firstBeneficiary = response.data[0];
|
||||||
|
const id = firstBeneficiary.id.toString();
|
||||||
|
setDeploymentIdState(id);
|
||||||
|
setDebugDeploymentId(id); // Also set in context
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Debug] Failed to load beneficiary:', error);
|
||||||
|
} finally {
|
||||||
|
setLoadingBeneficiary(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadDefaultDeploymentId();
|
||||||
|
}, [currentBeneficiary, setDebugDeploymentId]);
|
||||||
|
|
||||||
// Add log entry
|
// Add log entry
|
||||||
const log = useCallback((message: string, type: LogEntry['type'] = 'info') => {
|
const log = useCallback((message: string, type: LogEntry['type'] = 'info') => {
|
||||||
const time = new Date().toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
const time = new Date().toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
||||||
@ -127,20 +182,6 @@ export default function DebugScreen() {
|
|||||||
return () => subscription.remove();
|
return () => subscription.remove();
|
||||||
}, [log]);
|
}, [log]);
|
||||||
|
|
||||||
// Toggle speaker
|
|
||||||
const toggleSpeaker = useCallback(async () => {
|
|
||||||
const newState = !isSpeakerOn;
|
|
||||||
log(`=== TOGGLING SPEAKER: ${isSpeakerOn ? 'ON' : 'OFF'} → ${newState ? 'ON' : 'OFF'} ===`, 'info');
|
|
||||||
|
|
||||||
try {
|
|
||||||
await setAudioOutput(newState);
|
|
||||||
setIsSpeakerOn(newState);
|
|
||||||
log(`Speaker toggled to ${newState ? 'ON (loud speaker)' : 'OFF (earpiece)'}`, 'success');
|
|
||||||
} catch (err: any) {
|
|
||||||
log(`Speaker toggle error: ${err?.message || err}`, 'error');
|
|
||||||
}
|
|
||||||
}, [isSpeakerOn, log]);
|
|
||||||
|
|
||||||
// Start call
|
// Start call
|
||||||
const startCall = useCallback(async () => {
|
const startCall = useCallback(async () => {
|
||||||
if (callState !== 'idle') return;
|
if (callState !== 'idle') return;
|
||||||
@ -148,7 +189,6 @@ export default function DebugScreen() {
|
|||||||
clearLogs();
|
clearLogs();
|
||||||
setCallState('connecting');
|
setCallState('connecting');
|
||||||
setCallDuration(0);
|
setCallDuration(0);
|
||||||
setIsSpeakerOn(true); // Reset speaker state
|
|
||||||
callStartTimeRef.current = null;
|
callStartTimeRef.current = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -205,7 +245,20 @@ export default function DebugScreen() {
|
|||||||
// Step 4: Get token from server
|
// Step 4: Get token from server
|
||||||
log('Step 4: Requesting token from server...', 'info');
|
log('Step 4: Requesting token from server...', 'info');
|
||||||
log(`Token server: wellnuo.smartlaunchhub.com/julia/token`, 'info');
|
log(`Token server: wellnuo.smartlaunchhub.com/julia/token`, 'info');
|
||||||
const result = await getToken(`user-${Date.now()}`);
|
|
||||||
|
// Передаём deployment ID если указан
|
||||||
|
const beneficiaryData = deploymentId.trim() ? {
|
||||||
|
deploymentId: deploymentId.trim(),
|
||||||
|
beneficiaryNamesDict: {},
|
||||||
|
} : undefined;
|
||||||
|
|
||||||
|
if (beneficiaryData) {
|
||||||
|
log(`📋 Using custom Deployment ID: ${deploymentId}`, 'success');
|
||||||
|
} else {
|
||||||
|
log(`📋 No Deployment ID specified (default mode)`, 'info');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await getToken(`user-${Date.now()}`, beneficiaryData);
|
||||||
|
|
||||||
if (!result.success || !result.data) {
|
if (!result.success || !result.data) {
|
||||||
throw new Error(result.error || 'Failed to get token');
|
throw new Error(result.error || 'Failed to get token');
|
||||||
@ -252,11 +305,54 @@ export default function DebugScreen() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
newRoom.on(RoomEvent.ParticipantConnected, (participant: any) => {
|
newRoom.on(RoomEvent.ParticipantConnected, (participant: any) => {
|
||||||
log(`EVENT: Participant connected: ${participant.identity}`, 'event');
|
log(`👋 PARTICIPANT CONNECTED: ${participant.identity}`, 'success');
|
||||||
|
|
||||||
|
// Подписаться на события этого участника (для агента Julia)
|
||||||
|
participant.on('isSpeakingChanged', (speaking: boolean) => {
|
||||||
|
if (speaking) {
|
||||||
|
log(`🔊 ${participant.identity} STARTED SPEAKING`, 'success');
|
||||||
|
setAgentState('speaking');
|
||||||
|
} else {
|
||||||
|
log(`🔇 ${participant.identity} stopped speaking`, 'info');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
participant.on('trackMuted', (pub: any) => {
|
||||||
|
log(`🔇 ${participant.identity} muted ${pub.kind}`, 'event');
|
||||||
|
});
|
||||||
|
|
||||||
|
participant.on('trackUnmuted', (pub: any) => {
|
||||||
|
log(`🔊 ${participant.identity} unmuted ${pub.kind}`, 'event');
|
||||||
|
});
|
||||||
|
|
||||||
|
participant.on('attributesChanged', (attrs: any) => {
|
||||||
|
log(`📋 ${participant.identity} ATTRIBUTES:`, 'event');
|
||||||
|
Object.entries(attrs || {}).forEach(([k, v]) => {
|
||||||
|
log(` ${k}: ${v}`, 'info');
|
||||||
|
if (k === 'lk.agent.state') {
|
||||||
|
setAgentState(String(v));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
participant.on('transcriptionReceived', (segments: any[]) => {
|
||||||
|
log(`🤖 ${participant.identity} TRANSCRIPTION:`, 'success');
|
||||||
|
segments.forEach((seg: any, i: number) => {
|
||||||
|
const text = seg.text || seg.final || '';
|
||||||
|
log(` [${i}] "${text}"`, 'info');
|
||||||
|
if (text) setLastAgentText(text);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Показать текущие атрибуты участника
|
||||||
|
const attrs = participant.attributes || {};
|
||||||
|
if (Object.keys(attrs).length > 0) {
|
||||||
|
log(` Initial attributes: ${JSON.stringify(attrs)}`, 'info');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
newRoom.on(RoomEvent.ParticipantDisconnected, (participant: any) => {
|
newRoom.on(RoomEvent.ParticipantDisconnected, (participant: any) => {
|
||||||
log(`EVENT: Participant disconnected: ${participant.identity}`, 'event');
|
log(`👋 PARTICIPANT DISCONNECTED: ${participant.identity}`, 'event');
|
||||||
});
|
});
|
||||||
|
|
||||||
newRoom.on(RoomEvent.TrackSubscribed, (track: any, publication: any, participant: any) => {
|
newRoom.on(RoomEvent.TrackSubscribed, (track: any, publication: any, participant: any) => {
|
||||||
@ -284,12 +380,45 @@ export default function DebugScreen() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
newRoom.on(RoomEvent.DataReceived, (payload: any, participant: any) => {
|
newRoom.on(RoomEvent.DataReceived, (payload: any, participant: any, kind: any, topic: any) => {
|
||||||
|
log(`📩 DATA RECEIVED from ${participant?.identity || 'unknown'}`, 'event');
|
||||||
|
log(` kind: ${kind}, topic: ${topic || 'none'}`, 'info');
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(new TextDecoder().decode(payload));
|
const text = new TextDecoder().decode(payload);
|
||||||
log(`EVENT: Data received: ${JSON.stringify(data).substring(0, 100)}`, 'event');
|
const data = JSON.parse(text);
|
||||||
|
log(` type: ${data.type || 'unknown'}`, 'info');
|
||||||
|
|
||||||
|
// Подробное логирование разных типов сообщений
|
||||||
|
if (data.type === 'transcript' || data.type === 'transcription') {
|
||||||
|
log(` 🗣️ TRANSCRIPT: role=${data.role}`, 'success');
|
||||||
|
const text = data.text || data.content || '';
|
||||||
|
log(` 📝 TEXT: "${text}"`, 'success');
|
||||||
|
// Обновить UI
|
||||||
|
if (data.role === 'user') {
|
||||||
|
setLastUserText(text);
|
||||||
|
} else if (data.role === 'assistant' || data.role === 'agent') {
|
||||||
|
setLastAgentText(text);
|
||||||
|
}
|
||||||
|
} else if (data.type === 'state' || data.type === 'agent_state') {
|
||||||
|
const stateValue = data.state || JSON.stringify(data);
|
||||||
|
log(` 🤖 AGENT STATE: ${stateValue}`, 'success');
|
||||||
|
setAgentState(stateValue);
|
||||||
|
} else if (data.type === 'function_call' || data.type === 'tool_call') {
|
||||||
|
log(` 🔧 FUNCTION CALL: ${data.name || data.function || JSON.stringify(data)}`, 'event');
|
||||||
|
} else if (data.type === 'function_result' || data.type === 'tool_result') {
|
||||||
|
log(` ✅ FUNCTION RESULT: ${JSON.stringify(data.result || data).substring(0, 200)}`, 'event');
|
||||||
|
} else {
|
||||||
|
// Показать полный JSON для неизвестных типов
|
||||||
|
log(` 📦 FULL DATA: ${JSON.stringify(data)}`, 'info');
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log(`EVENT: Data received (binary)`, 'event');
|
// Попробовать показать как текст
|
||||||
|
try {
|
||||||
|
const text = new TextDecoder().decode(payload);
|
||||||
|
log(` 📄 RAW TEXT: "${text.substring(0, 300)}"`, 'info');
|
||||||
|
} catch {
|
||||||
|
log(` 📎 BINARY DATA: ${payload.byteLength} bytes`, 'info');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -305,7 +434,267 @@ export default function DebugScreen() {
|
|||||||
log(`EVENT: RoomMetadataChanged: ${metadata}`, 'event');
|
log(`EVENT: RoomMetadataChanged: ${metadata}`, 'event');
|
||||||
});
|
});
|
||||||
|
|
||||||
log('Event listeners set up', 'success');
|
// ===========================================
|
||||||
|
// TRANSCRIPTION - распознанный текст (STT)
|
||||||
|
// ===========================================
|
||||||
|
newRoom.on(RoomEvent.TranscriptionReceived, (segments: any[], participant: any) => {
|
||||||
|
const isUser = participant?.identity === newRoom.localParticipant.identity;
|
||||||
|
const who = isUser ? '👤 USER' : '🤖 AGENT';
|
||||||
|
|
||||||
|
segments.forEach((segment: any, idx: number) => {
|
||||||
|
const text = segment.text || segment.final || '';
|
||||||
|
const segmentId = segment.id || `seg-${Date.now()}`;
|
||||||
|
const isFinalFlag = segment.final !== undefined;
|
||||||
|
|
||||||
|
if (accumulateResponses) {
|
||||||
|
// === РЕЖИМ НАКОПЛЕНИЯ: Показываем только финальные полные ответы ===
|
||||||
|
if (isUser) {
|
||||||
|
// Новый сегмент или продолжение текущего
|
||||||
|
if (lastUserSegmentIdRef.current !== segmentId) {
|
||||||
|
// Если был предыдущий финальный - логируем его
|
||||||
|
if (accumulatedUserTextRef.current && lastUserSegmentIdRef.current) {
|
||||||
|
log(`👤 USER FINAL: "${accumulatedUserTextRef.current}"`, 'success');
|
||||||
|
}
|
||||||
|
accumulatedUserTextRef.current = text;
|
||||||
|
lastUserSegmentIdRef.current = segmentId;
|
||||||
|
} else {
|
||||||
|
// Обновляем текущий сегмент
|
||||||
|
accumulatedUserTextRef.current = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если финальный - логируем сразу
|
||||||
|
if (isFinalFlag && text) {
|
||||||
|
log(`👤 USER: "${text}"`, 'success');
|
||||||
|
setLastUserText(text);
|
||||||
|
accumulatedUserTextRef.current = '';
|
||||||
|
lastUserSegmentIdRef.current = null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// AGENT
|
||||||
|
if (lastAgentSegmentIdRef.current !== segmentId) {
|
||||||
|
if (accumulatedAgentTextRef.current && lastAgentSegmentIdRef.current) {
|
||||||
|
log(`🤖 AGENT FINAL: "${accumulatedAgentTextRef.current}"`, 'success');
|
||||||
|
}
|
||||||
|
accumulatedAgentTextRef.current = text;
|
||||||
|
lastAgentSegmentIdRef.current = segmentId;
|
||||||
|
} else {
|
||||||
|
accumulatedAgentTextRef.current = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFinalFlag && text) {
|
||||||
|
log(`🤖 JULIA: "${text}"`, 'success');
|
||||||
|
setLastAgentText(text);
|
||||||
|
accumulatedAgentTextRef.current = '';
|
||||||
|
lastAgentSegmentIdRef.current = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// === РЕЖИМ ПОЛНОГО ЛОГИРОВАНИЯ: Показываем каждый chunk ===
|
||||||
|
const finalLabel = isFinalFlag ? '(FINAL)' : '(interim)';
|
||||||
|
log(`🎤 TRANSCRIPTION from ${who} (${participant?.identity || 'unknown'})`, 'success');
|
||||||
|
log(` [${idx}] ${finalLabel}: "${text}"`, 'event');
|
||||||
|
if (segment.id) log(` segment.id: ${segment.id}`, 'info');
|
||||||
|
if (segment.firstReceivedTime) log(` firstReceivedTime: ${segment.firstReceivedTime}`, 'info');
|
||||||
|
if (segment.lastReceivedTime) log(` lastReceivedTime: ${segment.lastReceivedTime}`, 'info');
|
||||||
|
|
||||||
|
// Обновить UI с последним текстом
|
||||||
|
if (text && (isFinalFlag || !segment.final)) {
|
||||||
|
if (isUser) {
|
||||||
|
setLastUserText(text);
|
||||||
|
} else {
|
||||||
|
setLastAgentText(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===========================================
|
||||||
|
// PARTICIPANT ATTRIBUTES - состояние агента
|
||||||
|
// ===========================================
|
||||||
|
newRoom.on(RoomEvent.ParticipantAttributesChanged, (changedAttributes: any, participant: any) => {
|
||||||
|
log(`👤 ATTRIBUTES CHANGED for ${participant?.identity || 'unknown'}`, 'event');
|
||||||
|
Object.entries(changedAttributes || {}).forEach(([key, value]) => {
|
||||||
|
log(` ${key}: ${value}`, 'info');
|
||||||
|
// Особенно важно: lk.agent.state показывает listening/thinking/speaking
|
||||||
|
if (key === 'lk.agent.state') {
|
||||||
|
log(` 🤖 AGENT STATE: ${value}`, 'success');
|
||||||
|
// Обновить UI
|
||||||
|
setAgentState(String(value));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Показать все текущие атрибуты
|
||||||
|
const attrs = participant?.attributes || {};
|
||||||
|
if (Object.keys(attrs).length > 0) {
|
||||||
|
log(` All attributes: ${JSON.stringify(attrs)}`, 'info');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===========================================
|
||||||
|
// SIGNAL CONNECTED/RECONNECTING
|
||||||
|
// ===========================================
|
||||||
|
newRoom.on(RoomEvent.SignalConnected, () => {
|
||||||
|
log('EVENT: SignalConnected - WebSocket подключен', 'success');
|
||||||
|
});
|
||||||
|
|
||||||
|
newRoom.on(RoomEvent.SignalReconnecting, () => {
|
||||||
|
log('EVENT: SignalReconnecting - переподключение сигнала...', 'event');
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===========================================
|
||||||
|
// LOCAL TRACK UNPUBLISHED
|
||||||
|
// ===========================================
|
||||||
|
newRoom.on(RoomEvent.LocalTrackUnpublished, (publication: any, participant: any) => {
|
||||||
|
log(`EVENT: LocalTrackUnpublished - ${publication.trackSid}`, 'event');
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===========================================
|
||||||
|
// ДОПОЛНИТЕЛЬНЫЕ СОБЫТИЯ ДЛЯ ПОЛНОГО ДЕБАГА
|
||||||
|
// ===========================================
|
||||||
|
|
||||||
|
// Качество соединения
|
||||||
|
newRoom.on(RoomEvent.ConnectionQualityChanged, (quality: any, participant: any) => {
|
||||||
|
const qualityEmoji = quality === 'excellent' ? '🟢' : quality === 'good' ? '🟡' : '🔴';
|
||||||
|
log(`${qualityEmoji} CONNECTION QUALITY: ${participant?.identity || 'local'} → ${quality}`, 'event');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Изменение устройств (микрофон/камера подключены/отключены)
|
||||||
|
newRoom.on(RoomEvent.MediaDevicesChanged, () => {
|
||||||
|
log(`🔌 MEDIA DEVICES CHANGED - устройства обновились`, 'event');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Изменение активного устройства
|
||||||
|
newRoom.on(RoomEvent.ActiveDeviceChanged, (kind: any, deviceId: any) => {
|
||||||
|
log(`🎛️ ACTIVE DEVICE CHANGED: ${kind} → ${deviceId}`, 'event');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ошибка подписки на трек
|
||||||
|
newRoom.on(RoomEvent.TrackSubscriptionFailed, (trackSid: any, participant: any, reason: any) => {
|
||||||
|
log(`❌ TRACK SUBSCRIPTION FAILED: ${trackSid} from ${participant?.identity}`, 'error');
|
||||||
|
log(` Reason: ${reason}`, 'error');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Публикация трека (когда агент начинает говорить)
|
||||||
|
newRoom.on(RoomEvent.TrackPublished, (publication: any, participant: any) => {
|
||||||
|
log(`📢 TRACK PUBLISHED by ${participant?.identity}: ${publication.kind} (${publication.source})`, 'event');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Отмена публикации трека
|
||||||
|
newRoom.on(RoomEvent.TrackUnpublished, (publication: any, participant: any) => {
|
||||||
|
log(`📤 TRACK UNPUBLISHED by ${participant?.identity}: ${publication.kind}`, 'event');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Изменение метаданных участника
|
||||||
|
newRoom.on(RoomEvent.ParticipantMetadataChanged, (metadata: any, participant: any) => {
|
||||||
|
log(`📋 PARTICIPANT METADATA: ${participant?.identity}`, 'event');
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(metadata || '{}');
|
||||||
|
log(` ${JSON.stringify(parsed)}`, 'info');
|
||||||
|
} catch {
|
||||||
|
log(` ${metadata}`, 'info');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Изменение имени участника
|
||||||
|
newRoom.on(RoomEvent.ParticipantNameChanged, (name: any, participant: any) => {
|
||||||
|
log(`👤 PARTICIPANT NAME: ${participant?.identity} → ${name}`, 'event');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Статус записи (если комната записывается)
|
||||||
|
newRoom.on(RoomEvent.RecordingStatusChanged, (recording: any) => {
|
||||||
|
log(`⏺️ RECORDING STATUS: ${recording ? 'RECORDING' : 'NOT RECORDING'}`, recording ? 'success' : 'info');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Изменение статуса потока трека
|
||||||
|
newRoom.on(RoomEvent.TrackStreamStateChanged, (publication: any, streamState: any, participant: any) => {
|
||||||
|
log(`📊 TRACK STREAM STATE: ${participant?.identity}/${publication.trackSid} → ${streamState}`, 'event');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Разрешения на подписку трека
|
||||||
|
newRoom.on(RoomEvent.TrackSubscriptionPermissionChanged, (publication: any, status: any, participant: any) => {
|
||||||
|
log(`🔐 TRACK PERMISSION: ${participant?.identity}/${publication.trackSid} → ${status}`, 'event');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Статус подписки на трек
|
||||||
|
newRoom.on(RoomEvent.TrackSubscriptionStatusChanged, (publication: any, status: any, participant: any) => {
|
||||||
|
log(`📶 TRACK SUBSCRIPTION: ${participant?.identity}/${publication.trackSid} → ${status}`, 'event');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Разрешения участника изменились
|
||||||
|
newRoom.on(RoomEvent.ParticipantPermissionsChanged, (prevPermissions: any, participant: any) => {
|
||||||
|
log(`🔑 PARTICIPANT PERMISSIONS CHANGED: ${participant?.identity}`, 'event');
|
||||||
|
log(` New permissions: ${JSON.stringify(participant?.permissions || {})}`, 'info');
|
||||||
|
});
|
||||||
|
|
||||||
|
// ChatMessage - сообщения в чате комнаты
|
||||||
|
newRoom.on(RoomEvent.ChatMessage, (message: any, participant: any) => {
|
||||||
|
log(`💬 CHAT MESSAGE from ${participant?.identity || 'system'}:`, 'success');
|
||||||
|
log(` ${message.message || JSON.stringify(message)}`, 'info');
|
||||||
|
});
|
||||||
|
|
||||||
|
// SIP DTMF - телефонные сигналы
|
||||||
|
newRoom.on(RoomEvent.SipDTMFReceived, (dtmf: any, participant: any) => {
|
||||||
|
log(`📞 SIP DTMF: ${dtmf.code} from ${participant?.identity}`, 'event');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Детекция тишины микрофона
|
||||||
|
newRoom.on(RoomEvent.LocalAudioSilenceDetected, (publication: any) => {
|
||||||
|
log(`🔇 LOCAL AUDIO SILENCE DETECTED - микрофон молчит`, 'event');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Изменения буфера DataChannel
|
||||||
|
newRoom.on(RoomEvent.DCBufferStatusChanged, (isLow: any, kind: any) => {
|
||||||
|
log(`📦 DC BUFFER: ${kind} buffer is ${isLow ? 'LOW' : 'OK'}`, isLow ? 'event' : 'info');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Метрики производительности
|
||||||
|
newRoom.on(RoomEvent.MetricsReceived, (metrics: any) => {
|
||||||
|
log(`📈 METRICS RECEIVED:`, 'info');
|
||||||
|
if (metrics.audioStats) {
|
||||||
|
log(` Audio: bitrate=${metrics.audioStats.bitrate}, packetsLost=${metrics.audioStats.packetsLost}`, 'info');
|
||||||
|
}
|
||||||
|
if (metrics.videoStats) {
|
||||||
|
log(` Video: bitrate=${metrics.videoStats.bitrate}, fps=${metrics.videoStats.fps}`, 'info');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Статус воспроизведения видео (если есть)
|
||||||
|
newRoom.on(RoomEvent.VideoPlaybackStatusChanged, () => {
|
||||||
|
log(`🎬 VIDEO PLAYBACK STATUS CHANGED`, 'event');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ошибка шифрования
|
||||||
|
newRoom.on(RoomEvent.EncryptionError, (error: any) => {
|
||||||
|
log(`🔒 ENCRYPTION ERROR: ${error?.message || error}`, 'error');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Статус шифрования участника
|
||||||
|
newRoom.on(RoomEvent.ParticipantEncryptionStatusChanged, (encrypted: any, participant: any) => {
|
||||||
|
log(`🔐 ENCRYPTION STATUS: ${participant?.identity} → ${encrypted ? 'encrypted' : 'not encrypted'}`, 'event');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Комната перемещена (редко)
|
||||||
|
newRoom.on(RoomEvent.Moved, (room: any) => {
|
||||||
|
log(`🚀 ROOM MOVED to new server`, 'event');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Участник стал активным
|
||||||
|
newRoom.on(RoomEvent.ParticipantActive, (participant: any) => {
|
||||||
|
log(`✅ PARTICIPANT ACTIVE: ${participant?.identity}`, 'success');
|
||||||
|
|
||||||
|
// Проверяем, что это агент Julia (не локальный участник)
|
||||||
|
const isAgent = participant?.identity?.startsWith('agent-') ||
|
||||||
|
(participant?.attributes?.['lk.agent_name'] === 'julia-ai');
|
||||||
|
|
||||||
|
if (isAgent) {
|
||||||
|
log(``, 'success');
|
||||||
|
log(`🟢🟢🟢 AGENT READY 🟢🟢🟢`, 'success');
|
||||||
|
log(`🔊 Julia will now speak greeting...`, 'success');
|
||||||
|
log(``, 'success');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
log('Event listeners set up (FULL DEBUG MODE)', 'success');
|
||||||
|
|
||||||
// Step 7: Connect to room
|
// Step 7: Connect to room
|
||||||
log('Step 7: Connecting to LiveKit room...', 'info');
|
log('Step 7: Connecting to LiveKit room...', 'info');
|
||||||
@ -335,21 +724,117 @@ export default function DebugScreen() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Listen for local track published
|
// ===========================================
|
||||||
|
// LOCAL PARTICIPANT EVENTS - события моего микрофона
|
||||||
|
// ===========================================
|
||||||
newRoom.localParticipant.on('localTrackPublished', (pub: any) => {
|
newRoom.localParticipant.on('localTrackPublished', (pub: any) => {
|
||||||
log(`MY TRACK PUBLISHED: ${pub.kind} sid=${pub.trackSid}`, 'success');
|
log(`🎤 MY TRACK PUBLISHED: ${pub.kind} sid=${pub.trackSid}`, 'success');
|
||||||
|
});
|
||||||
|
|
||||||
|
newRoom.localParticipant.on('localTrackUnpublished', (pub: any) => {
|
||||||
|
log(`🎤 MY TRACK UNPUBLISHED: ${pub.kind} sid=${pub.trackSid}`, 'event');
|
||||||
|
});
|
||||||
|
|
||||||
|
// IsSpeakingChanged - когда я начинаю/перестаю говорить
|
||||||
|
newRoom.localParticipant.on('isSpeakingChanged', (speaking: boolean) => {
|
||||||
|
if (speaking) {
|
||||||
|
log(`🗣️ >>> I STARTED SPEAKING <<<`, 'success');
|
||||||
|
} else {
|
||||||
|
log(`🤐 I stopped speaking`, 'info');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Мой трек замьютился/размьютился
|
||||||
|
newRoom.localParticipant.on('trackMuted', (pub: any) => {
|
||||||
|
log(`🔇 MY TRACK MUTED: ${pub.kind}`, 'event');
|
||||||
|
});
|
||||||
|
|
||||||
|
newRoom.localParticipant.on('trackUnmuted', (pub: any) => {
|
||||||
|
log(`🔊 MY TRACK UNMUTED: ${pub.kind}`, 'success');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ошибка медиа устройства на моём участнике
|
||||||
|
newRoom.localParticipant.on('mediaDevicesError', (error: any) => {
|
||||||
|
log(`❌ MY MEDIA DEVICE ERROR: ${error?.message || error}`, 'error');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Аудио поток захвачен
|
||||||
|
newRoom.localParticipant.on('audioStreamAcquired', () => {
|
||||||
|
log(`🎙️ AUDIO STREAM ACQUIRED - микрофон захвачен!`, 'success');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Транскрипция на моём треке
|
||||||
|
newRoom.localParticipant.on('transcriptionReceived', (segments: any[]) => {
|
||||||
|
log(`🎤 MY TRANSCRIPTION (${segments.length} segments):`, 'success');
|
||||||
|
segments.forEach((seg: any, i: number) => {
|
||||||
|
log(` [${i}] "${seg.text || seg.final}"`, 'info');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Listen when I become an active speaker (means mic is working)
|
// Listen when I become an active speaker (means mic is working)
|
||||||
newRoom.on(RoomEvent.ActiveSpeakersChanged, (speakers: any[]) => {
|
newRoom.on(RoomEvent.ActiveSpeakersChanged, (speakers: any[]) => {
|
||||||
const iAmSpeaking = speakers.some(s => s.identity === newRoom.localParticipant.identity);
|
const iAmSpeaking = speakers.some(s => s.identity === newRoom.localParticipant.identity);
|
||||||
if (iAmSpeaking) {
|
if (iAmSpeaking) {
|
||||||
log(`*** I AM SPEAKING - MIC WORKS ***`, 'success');
|
log(`🎙️ *** I AM SPEAKING - MIC WORKS! ***`, 'success');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
log(`Local participant: ${newRoom.localParticipant.identity}`, 'info');
|
log(`Local participant: ${newRoom.localParticipant.identity}`, 'info');
|
||||||
|
|
||||||
|
// ===========================================
|
||||||
|
// AUDIO LEVEL MONITORING - периодическая проверка уровня микрофона
|
||||||
|
// ===========================================
|
||||||
|
let audioLevelInterval: ReturnType<typeof setInterval> | null = null;
|
||||||
|
let lastLoggedLevel = -1;
|
||||||
|
|
||||||
|
const startAudioLevelMonitoring = () => {
|
||||||
|
if (audioLevelInterval) return;
|
||||||
|
|
||||||
|
audioLevelInterval = setInterval(() => {
|
||||||
|
try {
|
||||||
|
// Найти microphone track среди всех публикаций
|
||||||
|
const audioTracks = newRoom.localParticipant.audioTrackPublications;
|
||||||
|
let localAudioTrack: any = null;
|
||||||
|
audioTracks.forEach((pub: any) => {
|
||||||
|
if (pub.source === 'microphone' || pub.kind === 'audio') {
|
||||||
|
localAudioTrack = pub;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (localAudioTrack?.track) {
|
||||||
|
// Получаем audio level через LiveKit API
|
||||||
|
const audioLevel = (localAudioTrack.track as any).audioLevel;
|
||||||
|
if (audioLevel !== undefined) {
|
||||||
|
const roundedLevel = Math.round(audioLevel * 100);
|
||||||
|
// Обновить UI
|
||||||
|
setMicLevel(roundedLevel);
|
||||||
|
// Логируем только когда уровень существенно изменился
|
||||||
|
if (Math.abs(roundedLevel - lastLoggedLevel) > 5) {
|
||||||
|
lastLoggedLevel = roundedLevel;
|
||||||
|
const bars = '▓'.repeat(Math.min(20, Math.round(audioLevel * 20))) + '░'.repeat(Math.max(0, 20 - Math.round(audioLevel * 20)));
|
||||||
|
log(`🎚️ MIC LEVEL: [${bars}] ${roundedLevel}%`, audioLevel > 0.1 ? 'success' : 'info');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore errors
|
||||||
|
}
|
||||||
|
}, 200); // Проверять каждые 200мс для плавного UI
|
||||||
|
};
|
||||||
|
|
||||||
|
// Запустить мониторинг audio level после подключения
|
||||||
|
newRoom.on(RoomEvent.Connected, () => {
|
||||||
|
log('Starting audio level monitoring...', 'info');
|
||||||
|
setTimeout(startAudioLevelMonitoring, 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Остановить при отключении
|
||||||
|
newRoom.on(RoomEvent.Disconnected, () => {
|
||||||
|
if (audioLevelInterval) {
|
||||||
|
clearInterval(audioLevelInterval);
|
||||||
|
audioLevelInterval = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Android: Start foreground service to keep call alive in background
|
// Android: Start foreground service to keep call alive in background
|
||||||
if (Platform.OS === 'android') {
|
if (Platform.OS === 'android') {
|
||||||
log('Android: Starting foreground service...', 'info');
|
log('Android: Starting foreground service...', 'info');
|
||||||
@ -463,6 +948,52 @@ export default function DebugScreen() {
|
|||||||
<Text style={styles.logCount}>{logs.length} logs</Text>
|
<Text style={styles.logCount}>{logs.length} logs</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{/* Deployment ID Input */}
|
||||||
|
{callState === 'idle' && (
|
||||||
|
<View style={styles.deploymentIdContainer}>
|
||||||
|
<Text style={styles.deploymentIdLabel}>Deployment ID (optional):</Text>
|
||||||
|
<TextInput
|
||||||
|
style={styles.deploymentIdInput}
|
||||||
|
value={deploymentId}
|
||||||
|
onChangeText={setDeploymentId}
|
||||||
|
placeholder="Enter deployment ID..."
|
||||||
|
placeholderTextColor="#6b7280"
|
||||||
|
keyboardType="default"
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoCorrect={false}
|
||||||
|
/>
|
||||||
|
{deploymentId.trim() && (
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.clearDeploymentId}
|
||||||
|
onPress={() => setDeploymentId('')}
|
||||||
|
>
|
||||||
|
<Ionicons name="close-circle" size={20} color="#6b7280" />
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Log Mode Toggle */}
|
||||||
|
<View style={styles.logModeContainer}>
|
||||||
|
<Text style={styles.logModeLabel}>Log mode:</Text>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.logModeButton, accumulateResponses && styles.logModeButtonActive]}
|
||||||
|
onPress={() => setAccumulateResponses(true)}
|
||||||
|
>
|
||||||
|
<Text style={[styles.logModeButtonText, accumulateResponses && styles.logModeButtonTextActive]}>
|
||||||
|
Clean (final only)
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.logModeButton, !accumulateResponses && styles.logModeButtonActive]}
|
||||||
|
onPress={() => setAccumulateResponses(false)}
|
||||||
|
>
|
||||||
|
<Text style={[styles.logModeButtonText, !accumulateResponses && styles.logModeButtonTextActive]}>
|
||||||
|
Verbose (all chunks)
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
|
||||||
{/* Control Buttons - Row 1: Call controls */}
|
{/* Control Buttons - Row 1: Call controls */}
|
||||||
<View style={styles.controls}>
|
<View style={styles.controls}>
|
||||||
{callState === 'idle' ? (
|
{callState === 'idle' ? (
|
||||||
@ -481,19 +1012,6 @@ export default function DebugScreen() {
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Speaker Toggle Button */}
|
|
||||||
<TouchableOpacity
|
|
||||||
style={[styles.speakerButton, isSpeakerOn ? styles.speakerOn : styles.speakerOff]}
|
|
||||||
onPress={toggleSpeaker}
|
|
||||||
disabled={callState === 'idle'}
|
|
||||||
>
|
|
||||||
<Ionicons
|
|
||||||
name={isSpeakerOn ? 'volume-high' : 'ear'}
|
|
||||||
size={20}
|
|
||||||
color="#fff"
|
|
||||||
/>
|
|
||||||
<Text style={styles.smallButtonText}>{isSpeakerOn ? 'Speaker' : 'Ear'}</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Control Buttons - Row 2: Log controls */}
|
{/* Control Buttons - Row 2: Log controls */}
|
||||||
@ -518,6 +1036,54 @@ export default function DebugScreen() {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{/* ========== LIVE STATUS PANEL ========== */}
|
||||||
|
{callState === 'connected' && (
|
||||||
|
<View style={styles.liveStatusPanel}>
|
||||||
|
{/* Agent State */}
|
||||||
|
<View style={styles.liveStatusRow}>
|
||||||
|
<Text style={styles.liveStatusLabel}>🤖 Agent:</Text>
|
||||||
|
<View style={[
|
||||||
|
styles.agentStateBadge,
|
||||||
|
agentState === 'speaking' && styles.agentStateSpeaking,
|
||||||
|
agentState === 'thinking' && styles.agentStateThinking,
|
||||||
|
agentState === 'listening' && styles.agentStateListening,
|
||||||
|
]}>
|
||||||
|
<Text style={styles.agentStateText}>
|
||||||
|
{agentState === 'speaking' ? '🔊 SPEAKING' :
|
||||||
|
agentState === 'thinking' ? '🧠 THINKING' :
|
||||||
|
agentState === 'listening' ? '👂 LISTENING' :
|
||||||
|
agentState}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Mic Level */}
|
||||||
|
<View style={styles.liveStatusRow}>
|
||||||
|
<Text style={styles.liveStatusLabel}>🎙️ Mic:</Text>
|
||||||
|
<View style={styles.micLevelContainer}>
|
||||||
|
<View style={[styles.micLevelBar, { width: `${Math.min(100, micLevel)}%` }]} />
|
||||||
|
</View>
|
||||||
|
<Text style={styles.micLevelText}>{micLevel}%</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Last User Text */}
|
||||||
|
{lastUserText ? (
|
||||||
|
<View style={styles.liveStatusRow}>
|
||||||
|
<Text style={styles.liveStatusLabel}>👤 You:</Text>
|
||||||
|
<Text style={styles.transcriptText} numberOfLines={2}>{lastUserText}</Text>
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{/* Last Agent Text */}
|
||||||
|
{lastAgentText ? (
|
||||||
|
<View style={styles.liveStatusRow}>
|
||||||
|
<Text style={styles.liveStatusLabel}>🤖 Julia:</Text>
|
||||||
|
<Text style={styles.transcriptText} numberOfLines={2}>{lastAgentText}</Text>
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Logs */}
|
{/* Logs */}
|
||||||
<FlatList
|
<FlatList
|
||||||
ref={flatListRef}
|
ref={flatListRef}
|
||||||
@ -603,6 +1169,68 @@ const styles = StyleSheet.create({
|
|||||||
color: '#888',
|
color: '#888',
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
},
|
},
|
||||||
|
deploymentIdContainer: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingHorizontal: Spacing.md,
|
||||||
|
paddingVertical: Spacing.sm,
|
||||||
|
backgroundColor: '#1f1f1f',
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: '#333',
|
||||||
|
},
|
||||||
|
deploymentIdLabel: {
|
||||||
|
color: '#9ca3af',
|
||||||
|
fontSize: 12,
|
||||||
|
marginRight: 8,
|
||||||
|
},
|
||||||
|
deploymentIdInput: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: '#2a2a2a',
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: 14,
|
||||||
|
paddingVertical: 8,
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
borderRadius: 8,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: '#404040',
|
||||||
|
},
|
||||||
|
clearDeploymentId: {
|
||||||
|
marginLeft: 8,
|
||||||
|
padding: 4,
|
||||||
|
},
|
||||||
|
logModeContainer: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingHorizontal: Spacing.md,
|
||||||
|
paddingVertical: Spacing.xs,
|
||||||
|
backgroundColor: '#1a1a1a',
|
||||||
|
gap: 8,
|
||||||
|
},
|
||||||
|
logModeLabel: {
|
||||||
|
color: '#9ca3af',
|
||||||
|
fontSize: 12,
|
||||||
|
marginRight: 4,
|
||||||
|
},
|
||||||
|
logModeButton: {
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
paddingVertical: 5,
|
||||||
|
borderRadius: 6,
|
||||||
|
backgroundColor: '#333',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: '#404040',
|
||||||
|
},
|
||||||
|
logModeButtonActive: {
|
||||||
|
backgroundColor: '#3b82f6',
|
||||||
|
borderColor: '#3b82f6',
|
||||||
|
},
|
||||||
|
logModeButtonText: {
|
||||||
|
color: '#888',
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: '500',
|
||||||
|
},
|
||||||
|
logModeButtonTextActive: {
|
||||||
|
color: '#fff',
|
||||||
|
},
|
||||||
controls: {
|
controls: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
padding: Spacing.md,
|
padding: Spacing.md,
|
||||||
@ -666,19 +1294,6 @@ const styles = StyleSheet.create({
|
|||||||
paddingHorizontal: 12,
|
paddingHorizontal: 12,
|
||||||
borderRadius: 10,
|
borderRadius: 10,
|
||||||
},
|
},
|
||||||
speakerButton: {
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
paddingVertical: 10,
|
|
||||||
paddingHorizontal: 16,
|
|
||||||
borderRadius: 10,
|
|
||||||
},
|
|
||||||
speakerOn: {
|
|
||||||
backgroundColor: '#f59e0b', // Orange when speaker is ON
|
|
||||||
},
|
|
||||||
speakerOff: {
|
|
||||||
backgroundColor: '#4b5563', // Gray when earpiece
|
|
||||||
},
|
|
||||||
platformBadge: {
|
platformBadge: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
alignItems: 'flex-end',
|
alignItems: 'flex-end',
|
||||||
@ -721,4 +1336,68 @@ const styles = StyleSheet.create({
|
|||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
marginTop: 12,
|
marginTop: 12,
|
||||||
},
|
},
|
||||||
|
// ========== LIVE STATUS PANEL STYLES ==========
|
||||||
|
liveStatusPanel: {
|
||||||
|
backgroundColor: '#1a1a1a',
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: '#333',
|
||||||
|
padding: Spacing.sm,
|
||||||
|
gap: 6,
|
||||||
|
},
|
||||||
|
liveStatusRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 8,
|
||||||
|
},
|
||||||
|
liveStatusLabel: {
|
||||||
|
color: '#888',
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: '600',
|
||||||
|
width: 55,
|
||||||
|
},
|
||||||
|
agentStateBadge: {
|
||||||
|
paddingHorizontal: 8,
|
||||||
|
paddingVertical: 3,
|
||||||
|
borderRadius: 6,
|
||||||
|
backgroundColor: '#333',
|
||||||
|
},
|
||||||
|
agentStateSpeaking: {
|
||||||
|
backgroundColor: '#22c55e',
|
||||||
|
},
|
||||||
|
agentStateThinking: {
|
||||||
|
backgroundColor: '#f59e0b',
|
||||||
|
},
|
||||||
|
agentStateListening: {
|
||||||
|
backgroundColor: '#3b82f6',
|
||||||
|
},
|
||||||
|
agentStateText: {
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: '700',
|
||||||
|
},
|
||||||
|
micLevelContainer: {
|
||||||
|
flex: 1,
|
||||||
|
height: 8,
|
||||||
|
backgroundColor: '#333',
|
||||||
|
borderRadius: 4,
|
||||||
|
overflow: 'hidden',
|
||||||
|
},
|
||||||
|
micLevelBar: {
|
||||||
|
height: '100%',
|
||||||
|
backgroundColor: '#22c55e',
|
||||||
|
borderRadius: 4,
|
||||||
|
},
|
||||||
|
micLevelText: {
|
||||||
|
color: '#888',
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: '600',
|
||||||
|
width: 35,
|
||||||
|
textAlign: 'right',
|
||||||
|
},
|
||||||
|
transcriptText: {
|
||||||
|
flex: 1,
|
||||||
|
color: '#e5e5e5',
|
||||||
|
fontSize: 11,
|
||||||
|
fontStyle: 'italic',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import { Stack, router, useSegments } from 'expo-router';
|
|||||||
import { StatusBar } from 'expo-status-bar';
|
import { StatusBar } from 'expo-status-bar';
|
||||||
import * as SplashScreen from 'expo-splash-screen';
|
import * as SplashScreen from 'expo-splash-screen';
|
||||||
import 'react-native-reanimated';
|
import 'react-native-reanimated';
|
||||||
|
import { KeyboardProvider } from 'react-native-keyboard-controller';
|
||||||
|
|
||||||
import { useColorScheme } from '@/hooks/use-color-scheme';
|
import { useColorScheme } from '@/hooks/use-color-scheme';
|
||||||
import { AuthProvider, useAuth } from '@/contexts/AuthContext';
|
import { AuthProvider, useAuth } from '@/contexts/AuthContext';
|
||||||
@ -60,12 +61,14 @@ function RootLayoutNav() {
|
|||||||
|
|
||||||
export default function RootLayout() {
|
export default function RootLayout() {
|
||||||
return (
|
return (
|
||||||
<AuthProvider>
|
<KeyboardProvider>
|
||||||
<BeneficiaryProvider>
|
<AuthProvider>
|
||||||
<VoiceTranscriptProvider>
|
<BeneficiaryProvider>
|
||||||
<RootLayoutNav />
|
<VoiceTranscriptProvider>
|
||||||
</VoiceTranscriptProvider>
|
<RootLayoutNav />
|
||||||
</BeneficiaryProvider>
|
</VoiceTranscriptProvider>
|
||||||
</AuthProvider>
|
</BeneficiaryProvider>
|
||||||
|
</AuthProvider>
|
||||||
|
</KeyboardProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,24 +9,99 @@
|
|||||||
* Features:
|
* Features:
|
||||||
* - Phone call-like UI with Julia avatar
|
* - Phone call-like UI with Julia avatar
|
||||||
* - Call duration timer
|
* - Call duration timer
|
||||||
* - Mute/unmute and speaker toggle
|
* - Mute/unmute toggle
|
||||||
* - Proper cleanup on unmount
|
* - Proper cleanup on unmount
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useEffect, useRef } from 'react';
|
import React, { useEffect, useRef, useState, useMemo } from 'react';
|
||||||
import { View, Text, StyleSheet, TouchableOpacity, Animated, Easing, Dimensions } from 'react-native';
|
import { View, Text, StyleSheet, TouchableOpacity, Animated, Easing, Dimensions } from 'react-native';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { useRouter } from 'expo-router';
|
import { useRouter } from 'expo-router';
|
||||||
import { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme';
|
import { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme';
|
||||||
import { useVoiceTranscript } from '@/contexts/VoiceTranscriptContext';
|
import { useVoiceTranscript } from '@/contexts/VoiceTranscriptContext';
|
||||||
|
import { useBeneficiary } from '@/contexts/BeneficiaryContext';
|
||||||
import { useLiveKitRoom, ConnectionState } from '@/hooks/useLiveKitRoom';
|
import { useLiveKitRoom, ConnectionState } from '@/hooks/useLiveKitRoom';
|
||||||
|
import { api } from '@/services/api';
|
||||||
|
import type { Beneficiary } from '@/types';
|
||||||
|
import type { BeneficiaryData } from '@/services/livekitService';
|
||||||
|
|
||||||
const { width: SCREEN_WIDTH } = Dimensions.get('window');
|
const { width: SCREEN_WIDTH } = Dimensions.get('window');
|
||||||
|
|
||||||
export default function VoiceCallScreen() {
|
export default function VoiceCallScreen() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { clearTranscript, addTranscriptEntry } = useVoiceTranscript();
|
const { clearTranscript, addTranscriptEntry } = useVoiceTranscript();
|
||||||
|
const { currentBeneficiary, debugDeploymentId } = useBeneficiary();
|
||||||
|
|
||||||
|
// Beneficiary state for building beneficiaryData
|
||||||
|
const [beneficiaries, setBeneficiaries] = useState<Beneficiary[]>([]);
|
||||||
|
const [beneficiariesLoaded, setBeneficiariesLoaded] = useState(false);
|
||||||
|
|
||||||
|
// Load beneficiaries on mount
|
||||||
|
useEffect(() => {
|
||||||
|
const loadBeneficiaries = async () => {
|
||||||
|
try {
|
||||||
|
const response = await api.getAllBeneficiaries();
|
||||||
|
if (response.ok && response.data) {
|
||||||
|
setBeneficiaries(response.data);
|
||||||
|
console.log('[VoiceCall] Beneficiaries loaded:', response.data.length);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[VoiceCall] Failed to load beneficiaries:', error);
|
||||||
|
} finally {
|
||||||
|
setBeneficiariesLoaded(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
loadBeneficiaries();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Build beneficiaryData for voice agent
|
||||||
|
const beneficiaryData = useMemo((): BeneficiaryData | undefined => {
|
||||||
|
// PRIORITY 1: If debugDeploymentId is set (from Debug screen), use it
|
||||||
|
if (debugDeploymentId) {
|
||||||
|
console.log('[VoiceCall] Using DEBUG deployment ID:', debugDeploymentId);
|
||||||
|
return {
|
||||||
|
deploymentId: debugDeploymentId,
|
||||||
|
beneficiaryNamesDict: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// PRIORITY 2: Use beneficiaries from API
|
||||||
|
// Safety check - ensure beneficiaries is an array
|
||||||
|
if (!Array.isArray(beneficiaries) || beneficiaries.length === 0) {
|
||||||
|
console.log('[VoiceCall] No beneficiaries yet, skipping beneficiaryData');
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Build beneficiary_names_dict from all beneficiaries
|
||||||
|
// Format: {"21": "papa", "69": "David"}
|
||||||
|
const beneficiaryNamesDict: Record<string, string> = {};
|
||||||
|
beneficiaries.forEach(b => {
|
||||||
|
// Safety: check that b exists and has id and name
|
||||||
|
if (b && b.id != null && b.name) {
|
||||||
|
beneficiaryNamesDict[String(b.id)] = b.name;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get deployment_id from current beneficiary or fallback to first one
|
||||||
|
const deploymentId = currentBeneficiary?.id != null
|
||||||
|
? String(currentBeneficiary.id)
|
||||||
|
: beneficiaries[0]?.id != null
|
||||||
|
? String(beneficiaries[0].id)
|
||||||
|
: '21';
|
||||||
|
|
||||||
|
console.log('[VoiceCall] BeneficiaryData:', { deploymentId, beneficiaryNamesDict });
|
||||||
|
|
||||||
|
return {
|
||||||
|
deploymentId,
|
||||||
|
beneficiaryNamesDict,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[VoiceCall] Error building beneficiaryData:', error);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}, [beneficiaries, currentBeneficiary, debugDeploymentId]);
|
||||||
|
|
||||||
// LiveKit hook - ALL logic is here
|
// LiveKit hook - ALL logic is here
|
||||||
const {
|
const {
|
||||||
@ -42,6 +117,7 @@ export default function VoiceCallScreen() {
|
|||||||
toggleMute,
|
toggleMute,
|
||||||
} = useLiveKitRoom({
|
} = useLiveKitRoom({
|
||||||
userId: `user-${Date.now()}`,
|
userId: `user-${Date.now()}`,
|
||||||
|
beneficiaryData,
|
||||||
onTranscript: (role, text) => {
|
onTranscript: (role, text) => {
|
||||||
addTranscriptEntry(role, text);
|
addTranscriptEntry(role, text);
|
||||||
},
|
},
|
||||||
@ -52,16 +128,59 @@ export default function VoiceCallScreen() {
|
|||||||
const rotateAnim = useRef(new Animated.Value(0)).current;
|
const rotateAnim = useRef(new Animated.Value(0)).current;
|
||||||
const avatarScale = useRef(new Animated.Value(0.8)).current;
|
const avatarScale = useRef(new Animated.Value(0.8)).current;
|
||||||
|
|
||||||
// Clear transcript and start call on mount
|
// Clear transcript on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
clearTranscript();
|
clearTranscript();
|
||||||
connect();
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
// Cleanup handled by the hook
|
|
||||||
};
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Track if connect has been called to prevent duplicate calls
|
||||||
|
const connectCalledRef = useRef(false);
|
||||||
|
|
||||||
|
// Start call ONLY after beneficiaryData is ready
|
||||||
|
// IMPORTANT: We must wait for beneficiaryData to be populated!
|
||||||
|
// Without deploymentId, Julia AI agent won't know which beneficiary to talk about.
|
||||||
|
useEffect(() => {
|
||||||
|
// Prevent duplicate connect calls
|
||||||
|
if (connectCalledRef.current) return;
|
||||||
|
|
||||||
|
// If debugDeploymentId is set, connect immediately (don't wait for beneficiaries)
|
||||||
|
if (debugDeploymentId && beneficiaryData?.deploymentId) {
|
||||||
|
console.log('[VoiceCall] Starting call with DEBUG deploymentId:', debugDeploymentId);
|
||||||
|
connectCalledRef.current = true;
|
||||||
|
connect();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, only connect when beneficiaries are loaded AND beneficiaryData is ready
|
||||||
|
if (beneficiariesLoaded && beneficiaryData?.deploymentId) {
|
||||||
|
console.log('[VoiceCall] Starting call with beneficiaryData:', JSON.stringify(beneficiaryData));
|
||||||
|
connectCalledRef.current = true;
|
||||||
|
connect();
|
||||||
|
} else if (beneficiariesLoaded) {
|
||||||
|
console.log('[VoiceCall] Waiting for beneficiaryData... Current state:', {
|
||||||
|
beneficiariesLoaded,
|
||||||
|
beneficiariesCount: beneficiaries.length,
|
||||||
|
beneficiaryData: beneficiaryData ? JSON.stringify(beneficiaryData) : 'undefined'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [beneficiariesLoaded, beneficiaryData, beneficiaries.length, connect, debugDeploymentId]);
|
||||||
|
|
||||||
|
// Fallback: if beneficiaryData doesn't arrive in 5 seconds, connect anyway
|
||||||
|
// This handles edge cases where API fails or user has no beneficiaries
|
||||||
|
useEffect(() => {
|
||||||
|
if (connectCalledRef.current) return;
|
||||||
|
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
if (!connectCalledRef.current && beneficiariesLoaded) {
|
||||||
|
console.warn('[VoiceCall] Timeout: beneficiaryData not ready after 5s, connecting without it');
|
||||||
|
connectCalledRef.current = true;
|
||||||
|
connect();
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
return () => clearTimeout(timeout);
|
||||||
|
}, [beneficiariesLoaded, connect]);
|
||||||
|
|
||||||
// Navigate back on disconnect or error
|
// Navigate back on disconnect or error
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (state === 'disconnected' || state === 'error') {
|
if (state === 'disconnected' || state === 'error') {
|
||||||
@ -402,7 +521,7 @@ const styles = StyleSheet.create({
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
paddingVertical: Spacing.xl,
|
paddingVertical: Spacing.xl,
|
||||||
paddingHorizontal: Spacing.lg,
|
paddingHorizontal: Spacing.lg,
|
||||||
gap: 48, // Space between Mute and End Call buttons
|
gap: 40, // Space between 2 buttons (Mute, End Call)
|
||||||
},
|
},
|
||||||
controlButton: {
|
controlButton: {
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
|
Platform,
|
||||||
type TextInputProps,
|
type TextInputProps,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
@ -121,6 +122,11 @@ const styles = StyleSheet.create({
|
|||||||
paddingHorizontal: Spacing.md,
|
paddingHorizontal: Spacing.md,
|
||||||
fontSize: FontSizes.base,
|
fontSize: FontSizes.base,
|
||||||
color: AppColors.textPrimary,
|
color: AppColors.textPrimary,
|
||||||
|
// Fix for Android password field text visibility
|
||||||
|
...(Platform.OS === 'android' && {
|
||||||
|
fontFamily: 'Roboto',
|
||||||
|
includeFontPadding: false,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
inputWithLeftIcon: {
|
inputWithLeftIcon: {
|
||||||
paddingLeft: 0,
|
paddingLeft: 0,
|
||||||
|
|||||||
@ -7,12 +7,17 @@ interface BeneficiaryContextType {
|
|||||||
clearCurrentBeneficiary: () => void;
|
clearCurrentBeneficiary: () => void;
|
||||||
// Helper to format beneficiary context for AI
|
// Helper to format beneficiary context for AI
|
||||||
getBeneficiaryContext: () => string;
|
getBeneficiaryContext: () => string;
|
||||||
|
// Debug: Override deployment ID for testing (used by Debug screen)
|
||||||
|
debugDeploymentId: string | null;
|
||||||
|
setDebugDeploymentId: (id: string | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const BeneficiaryContext = createContext<BeneficiaryContextType | undefined>(undefined);
|
const BeneficiaryContext = createContext<BeneficiaryContextType | undefined>(undefined);
|
||||||
|
|
||||||
export function BeneficiaryProvider({ children }: { children: React.ReactNode }) {
|
export function BeneficiaryProvider({ children }: { children: React.ReactNode }) {
|
||||||
const [currentBeneficiary, setCurrentBeneficiary] = useState<Beneficiary | null>(null);
|
const [currentBeneficiary, setCurrentBeneficiary] = useState<Beneficiary | null>(null);
|
||||||
|
// Debug: Override deployment ID for testing purposes
|
||||||
|
const [debugDeploymentId, setDebugDeploymentId] = useState<string | null>(null);
|
||||||
|
|
||||||
const clearCurrentBeneficiary = useCallback(() => {
|
const clearCurrentBeneficiary = useCallback(() => {
|
||||||
setCurrentBeneficiary(null);
|
setCurrentBeneficiary(null);
|
||||||
@ -70,6 +75,8 @@ export function BeneficiaryProvider({ children }: { children: React.ReactNode })
|
|||||||
setCurrentBeneficiary,
|
setCurrentBeneficiary,
|
||||||
clearCurrentBeneficiary,
|
clearCurrentBeneficiary,
|
||||||
getBeneficiaryContext,
|
getBeneficiaryContext,
|
||||||
|
debugDeploymentId,
|
||||||
|
setDebugDeploymentId,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
2
eas.json
2
eas.json
@ -27,7 +27,7 @@
|
|||||||
"credentialsSource": "remote"
|
"credentialsSource": "remote"
|
||||||
},
|
},
|
||||||
"android": {
|
"android": {
|
||||||
"buildType": "apk"
|
"buildType": "app-bundle"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -20,7 +20,7 @@ const isIOSSimulator = (): boolean => {
|
|||||||
return PlatformConstants?.interfaceIdiom === 'simulator' ||
|
return PlatformConstants?.interfaceIdiom === 'simulator' ||
|
||||||
PlatformConstants?.isSimulator === true;
|
PlatformConstants?.isSimulator === true;
|
||||||
};
|
};
|
||||||
import { getToken, VOICE_NAME } from '@/services/livekitService';
|
import { getToken, VOICE_NAME, BeneficiaryData } from '@/services/livekitService';
|
||||||
import {
|
import {
|
||||||
configureAudioForVoiceCall,
|
configureAudioForVoiceCall,
|
||||||
stopAudioSession,
|
stopAudioSession,
|
||||||
@ -51,6 +51,7 @@ export interface LogEntry {
|
|||||||
// Hook options
|
// Hook options
|
||||||
export interface UseLiveKitRoomOptions {
|
export interface UseLiveKitRoomOptions {
|
||||||
userId: string;
|
userId: string;
|
||||||
|
beneficiaryData?: BeneficiaryData;
|
||||||
onTranscript?: (role: 'user' | 'assistant', text: string) => void;
|
onTranscript?: (role: 'user' | 'assistant', text: string) => void;
|
||||||
autoConnect?: boolean;
|
autoConnect?: boolean;
|
||||||
}
|
}
|
||||||
@ -85,7 +86,7 @@ export interface UseLiveKitRoomReturn {
|
|||||||
* Main hook for LiveKit voice calls
|
* Main hook for LiveKit voice calls
|
||||||
*/
|
*/
|
||||||
export function useLiveKitRoom(options: UseLiveKitRoomOptions): UseLiveKitRoomReturn {
|
export function useLiveKitRoom(options: UseLiveKitRoomOptions): UseLiveKitRoomReturn {
|
||||||
const { userId, onTranscript, autoConnect = false } = options;
|
const { userId, beneficiaryData, onTranscript, autoConnect = false } = options;
|
||||||
|
|
||||||
// State
|
// State
|
||||||
const [state, setState] = useState<ConnectionState>('idle');
|
const [state, setState] = useState<ConnectionState>('idle');
|
||||||
@ -248,7 +249,7 @@ export function useLiveKitRoom(options: UseLiveKitRoomOptions): UseLiveKitRoomRe
|
|||||||
setState('requesting_token');
|
setState('requesting_token');
|
||||||
logInfo('STEP 4/6: Requesting token from server...');
|
logInfo('STEP 4/6: Requesting token from server...');
|
||||||
|
|
||||||
const tokenResult = await getToken(userId);
|
const tokenResult = await getToken(userId, beneficiaryData);
|
||||||
|
|
||||||
if (!tokenResult.success || !tokenResult.data) {
|
if (!tokenResult.success || !tokenResult.data) {
|
||||||
const errorMsg = tokenResult.error || 'Failed to get token';
|
const errorMsg = tokenResult.error || 'Failed to get token';
|
||||||
@ -514,7 +515,7 @@ export function useLiveKitRoom(options: UseLiveKitRoomOptions): UseLiveKitRoomRe
|
|||||||
// Cleanup
|
// Cleanup
|
||||||
await stopAudioSession();
|
await stopAudioSession();
|
||||||
}
|
}
|
||||||
}, [userId, onTranscript, logInfo, logWarn, logError, logSuccess]);
|
}, [userId, beneficiaryData, onTranscript, logInfo, logWarn, logError, logSuccess]);
|
||||||
|
|
||||||
// ===================
|
// ===================
|
||||||
// DISCONNECT FUNCTION
|
// DISCONNECT FUNCTION
|
||||||
|
|||||||
@ -1,142 +0,0 @@
|
|||||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
||||||
import { Platform, Alert } from 'react-native';
|
|
||||||
import { debugLogger } from '@/services/DebugLogger';
|
|
||||||
|
|
||||||
// Try to import native module
|
|
||||||
let ExpoSpeechRecognitionModule: any = null;
|
|
||||||
let SPEECH_RECOGNITION_AVAILABLE = false;
|
|
||||||
try {
|
|
||||||
const speechRecognition = require('expo-speech-recognition');
|
|
||||||
ExpoSpeechRecognitionModule = speechRecognition.ExpoSpeechRecognitionModule;
|
|
||||||
if (ExpoSpeechRecognitionModule) {
|
|
||||||
SPEECH_RECOGNITION_AVAILABLE = true;
|
|
||||||
debugLogger.info('STT', 'Speech recognition module loaded successfully');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
debugLogger.warn('STT', 'Speech recognition not available', e);
|
|
||||||
console.log('[useSpeechRecognition] expo-speech-recognition not available');
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SpeechRecognitionResult {
|
|
||||||
transcript: string;
|
|
||||||
isFinal: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UseSpeechRecognitionReturn {
|
|
||||||
isListening: boolean;
|
|
||||||
recognizedText: string;
|
|
||||||
startListening: (options?: { continuous?: boolean }) => Promise<void>;
|
|
||||||
stopListening: () => void;
|
|
||||||
isAvailable: boolean;
|
|
||||||
hasPermission: boolean;
|
|
||||||
requestPermission: () => Promise<boolean>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useSpeechRecognition(): UseSpeechRecognitionReturn {
|
|
||||||
const [isListening, setIsListening] = useState(false);
|
|
||||||
const [recognizedText, setRecognizedText] = useState('');
|
|
||||||
const [hasPermission, setHasPermission] = useState(false);
|
|
||||||
|
|
||||||
// Callbacks
|
|
||||||
const onResultRef = useRef<((result: SpeechRecognitionResult) => void) | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!SPEECH_RECOGNITION_AVAILABLE || !ExpoSpeechRecognitionModule) {
|
|
||||||
debugLogger.warn('STT', 'Cannot setup listeners - module not available');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
debugLogger.info('STT', 'Setting up speech recognition event listeners');
|
|
||||||
const subscriptions: any[] = [];
|
|
||||||
|
|
||||||
if (ExpoSpeechRecognitionModule.addListener) {
|
|
||||||
subscriptions.push(
|
|
||||||
ExpoSpeechRecognitionModule.addListener('start', () => {
|
|
||||||
debugLogger.info('STT', 'Speech recognition started');
|
|
||||||
setIsListening(true);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
subscriptions.push(
|
|
||||||
ExpoSpeechRecognitionModule.addListener('end', () => {
|
|
||||||
debugLogger.info('STT', 'Speech recognition ended');
|
|
||||||
setIsListening(false);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
subscriptions.push(
|
|
||||||
ExpoSpeechRecognitionModule.addListener('result', (event: any) => {
|
|
||||||
const transcript = event.results?.[0]?.transcript || '';
|
|
||||||
const isFinal = event.results?.[0]?.isFinal || false;
|
|
||||||
debugLogger.log('STT', `Recognized: "${transcript}" (${isFinal ? 'FINAL' : 'interim'})`);
|
|
||||||
setRecognizedText(transcript);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
subscriptions.push(
|
|
||||||
ExpoSpeechRecognitionModule.addListener('error', (event: any) => {
|
|
||||||
debugLogger.error('STT', 'Speech recognition error', event);
|
|
||||||
setIsListening(false);
|
|
||||||
console.warn('[Speech] Error:', event);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
debugLogger.info('STT', 'Cleaning up speech recognition listeners');
|
|
||||||
subscriptions.forEach(sub => sub.remove?.());
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const requestPermission = async () => {
|
|
||||||
if (!SPEECH_RECOGNITION_AVAILABLE) {
|
|
||||||
debugLogger.warn('STT', 'Cannot request permission - module not available');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
debugLogger.info('STT', 'Requesting microphone permissions');
|
|
||||||
const result = await ExpoSpeechRecognitionModule.requestPermissionsAsync();
|
|
||||||
setHasPermission(result.granted);
|
|
||||||
debugLogger.log('STT', `Permission ${result.granted ? 'granted' : 'denied'}`);
|
|
||||||
return result.granted;
|
|
||||||
};
|
|
||||||
|
|
||||||
const startListening = async (options?: { continuous?: boolean }) => {
|
|
||||||
if (!SPEECH_RECOGNITION_AVAILABLE) {
|
|
||||||
debugLogger.error('STT', 'Cannot start - speech recognition not available');
|
|
||||||
Alert.alert('Not Available', 'Voice recognition is not available on this device.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Reset text
|
|
||||||
setRecognizedText('');
|
|
||||||
debugLogger.info('STT', `Starting speech recognition (continuous: ${options?.continuous ?? false})`);
|
|
||||||
|
|
||||||
await ExpoSpeechRecognitionModule.start({
|
|
||||||
lang: 'en-US',
|
|
||||||
interimResults: true,
|
|
||||||
maxAlternatives: 1,
|
|
||||||
continuous: options?.continuous ?? false,
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
debugLogger.error('STT', 'Failed to start listening', e);
|
|
||||||
console.error('Failed to start listening', e);
|
|
||||||
setIsListening(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const stopListening = () => {
|
|
||||||
debugLogger.info('STT', 'Stopping speech recognition');
|
|
||||||
if (SPEECH_RECOGNITION_AVAILABLE) {
|
|
||||||
ExpoSpeechRecognitionModule.stop();
|
|
||||||
}
|
|
||||||
setIsListening(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
isListening,
|
|
||||||
recognizedText,
|
|
||||||
startListening,
|
|
||||||
stopListening,
|
|
||||||
isAvailable: SPEECH_RECOGNITION_AVAILABLE,
|
|
||||||
hasPermission,
|
|
||||||
requestPermission
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -1,141 +0,0 @@
|
|||||||
"""
|
|
||||||
WellNuo Voice Agent - Julia AI
|
|
||||||
LiveKit Agents Cloud deployment
|
|
||||||
Uses Deepgram STT/TTS + OpenAI GPT-4o LLM
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import json
|
|
||||||
from dotenv import load_dotenv
|
|
||||||
|
|
||||||
from livekit import agents
|
|
||||||
from livekit.agents import Agent, AgentSession, RoomEventHandler
|
|
||||||
from livekit.plugins import deepgram, openai, silero
|
|
||||||
|
|
||||||
load_dotenv(".env.local")
|
|
||||||
|
|
||||||
# Ferdinand data for demo (in production, fetch from API)
|
|
||||||
FERDINAND_DATA = {
|
|
||||||
"client": {
|
|
||||||
"name": "Ferdinand Zmrzli",
|
|
||||||
"address": "661 Encore Way"
|
|
||||||
},
|
|
||||||
"today_alerts": [
|
|
||||||
{"type": "fall_detected", "time": "06:32", "severity": "critical", "location": "bathroom"},
|
|
||||||
{"type": "short_sleep", "time": "06:30", "severity": "high", "note": "Only 5 hours sleep (normal: 7-8)"},
|
|
||||||
{"type": "missed_medication", "time": "08:30", "severity": "high", "note": "Morning medication not taken"}
|
|
||||||
],
|
|
||||||
"yesterday_alerts": [
|
|
||||||
{"type": "high_bathroom_frequency", "time": "15:00", "severity": "medium", "note": "8 visits (normal: 5-6)"}
|
|
||||||
],
|
|
||||||
"summary": {
|
|
||||||
"total_alerts_7days": 12,
|
|
||||||
"critical": 2,
|
|
||||||
"high": 4,
|
|
||||||
"medium": 4,
|
|
||||||
"low": 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def build_system_prompt() -> str:
|
|
||||||
"""Build Julia AI system prompt with Ferdinand context"""
|
|
||||||
|
|
||||||
client = FERDINAND_DATA["client"]
|
|
||||||
alerts = FERDINAND_DATA["today_alerts"]
|
|
||||||
has_critical = any(a["severity"] in ["critical", "high"] for a in alerts)
|
|
||||||
|
|
||||||
alerts_text = ""
|
|
||||||
for alert in alerts:
|
|
||||||
emoji = "RED" if alert["severity"] == "critical" else "ORANGE" if alert["severity"] == "high" else "YELLOW"
|
|
||||||
alerts_text += f" [{emoji}] {alert['type'].replace('_', ' ').upper()} at {alert['time']}"
|
|
||||||
if alert.get("note"):
|
|
||||||
alerts_text += f" - {alert['note']}"
|
|
||||||
if alert.get("location"):
|
|
||||||
alerts_text += f" ({alert['location']})"
|
|
||||||
alerts_text += "\n"
|
|
||||||
|
|
||||||
return f"""You are Julia, a compassionate AI wellness assistant for WellNuo app.
|
|
||||||
You help caregivers monitor their loved ones' wellbeing.
|
|
||||||
|
|
||||||
CRITICAL: You are ALWAYS talking about {client['name']} (the beneficiary), NOT about yourself!
|
|
||||||
|
|
||||||
BENEFICIARY INFORMATION:
|
|
||||||
- Name: {client['name']}
|
|
||||||
- Address: {client['address']}
|
|
||||||
- Monitoring Period: Last 7 days
|
|
||||||
|
|
||||||
TODAY'S ALERTS:
|
|
||||||
{alerts_text}
|
|
||||||
|
|
||||||
7-DAY SUMMARY:
|
|
||||||
- Total alerts: {FERDINAND_DATA['summary']['total_alerts_7days']}
|
|
||||||
- Critical: {FERDINAND_DATA['summary']['critical']}
|
|
||||||
- High: {FERDINAND_DATA['summary']['high']}
|
|
||||||
- Medium: {FERDINAND_DATA['summary']['medium']}
|
|
||||||
- Low: {FERDINAND_DATA['summary']['low']}
|
|
||||||
|
|
||||||
CONVERSATION RULES:
|
|
||||||
1. When user asks "how are you?" or "how's it going?" - ALWAYS respond about {client['name']}'s status, NOT about yourself as AI
|
|
||||||
- NEVER say "I'm doing well as an AI" - the user wants to know about their loved one!
|
|
||||||
|
|
||||||
2. When user asks "what's happening?" or "any updates?" - report {client['name']}'s current status and alerts
|
|
||||||
|
|
||||||
3. ALWAYS assume questions are about {client['name']} unless explicitly about app features
|
|
||||||
|
|
||||||
RESPONSE STYLE - BE CONCISE, NOT PUSHY:
|
|
||||||
- DON'T overwhelm with information immediately
|
|
||||||
- First give a SHORT summary, then ASK if they want details
|
|
||||||
- Example opening: "Hi! {'I have some important updates about ' + client['name'] + '. Would you like to hear them?' if has_critical else client['name'] + ' is doing well today. Anything specific you would like to know?'}"
|
|
||||||
- Wait for user to ask before giving long explanations
|
|
||||||
- Keep initial responses to 1-2 sentences max
|
|
||||||
- Only elaborate when user asks "tell me more", "what happened?", etc.
|
|
||||||
|
|
||||||
BAD (too pushy): "Hi! Ferdinand had a fall at 6:32 AM in the bathroom, his sleep was only 5 hours, he missed his morning medication..."
|
|
||||||
GOOD (concise): "Hi! I have some concerns about {client['name']} today - there was an incident this morning. Want me to tell you more?"
|
|
||||||
|
|
||||||
You're speaking with a caregiver who cares deeply about {client['name']}."""
|
|
||||||
|
|
||||||
|
|
||||||
class JuliaAssistant(Agent):
|
|
||||||
"""Julia AI Voice Assistant for WellNuo"""
|
|
||||||
|
|
||||||
def __init__(self) -> None:
|
|
||||||
super().__init__(
|
|
||||||
instructions=build_system_prompt(),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# Create the agent server
|
|
||||||
server = agents.AgentServer()
|
|
||||||
|
|
||||||
|
|
||||||
@server.rtc_session()
|
|
||||||
async def julia_session(ctx: agents.JobContext):
|
|
||||||
"""Main voice session handler"""
|
|
||||||
|
|
||||||
# Create the agent session with STT, LLM, TTS
|
|
||||||
session = AgentSession(
|
|
||||||
stt=deepgram.STT(model="nova-2"),
|
|
||||||
llm=openai.LLM(
|
|
||||||
model="gpt-4o",
|
|
||||||
api_key=os.getenv("OPENAI_API_KEY"),
|
|
||||||
),
|
|
||||||
tts=deepgram.TTS(model="aura-asteria-en"),
|
|
||||||
vad=silero.VAD.load(),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Start the session
|
|
||||||
await session.start(
|
|
||||||
room=ctx.room,
|
|
||||||
agent=JuliaAssistant(),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Generate initial greeting
|
|
||||||
await session.generate_reply(
|
|
||||||
instructions="Greet the user warmly. If there are critical alerts, mention you have important updates. Keep it brief - 1 sentence max."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
agents.cli.run_app(server)
|
|
||||||
@ -6,3 +6,9 @@ id = "CA_Yd3qcuYEVKKE"
|
|||||||
|
|
||||||
[build]
|
[build]
|
||||||
dockerfile = "Dockerfile"
|
dockerfile = "Dockerfile"
|
||||||
|
|
||||||
|
[env]
|
||||||
|
# Deepgram for TTS
|
||||||
|
DEEPGRAM_API_KEY = "cec33b489b0ba12c4e4f1ea888e887e88fba5848"
|
||||||
|
# AssemblyAI for STT (best accuracy - correctly recognizes "dad" vs "dead")
|
||||||
|
ASSEMBLYAI_API_KEY = "42e753b65b6a4360ae4a77ac76961857"
|
||||||
|
|||||||
@ -12,7 +12,8 @@ dependencies = [
|
|||||||
"livekit-agents[silero]~=1.3",
|
"livekit-agents[silero]~=1.3",
|
||||||
"livekit-plugins-noise-cancellation~=0.2",
|
"livekit-plugins-noise-cancellation~=0.2",
|
||||||
"livekit-plugins-deepgram~=1.0",
|
"livekit-plugins-deepgram~=1.0",
|
||||||
"livekit-plugins-openai~=1.0",
|
# Removed assemblyai - was giving garbage transcriptions
|
||||||
|
# Deepgram Nova-2 is faster and more accurate
|
||||||
"python-dotenv",
|
"python-dotenv",
|
||||||
"aiohttp",
|
"aiohttp",
|
||||||
]
|
]
|
||||||
|
|||||||
@ -1,9 +1,13 @@
|
|||||||
"""
|
"""
|
||||||
WellNuo Voice Agent - Julia AI
|
WellNuo Voice Agent - Julia Robust (NO BARGE-IN)
|
||||||
LiveKit Agents Cloud deployment
|
LiveKit Agents Cloud deployment
|
||||||
Uses WellNuo ask_wellnuo_ai API for LLM responses, Deepgram for STT/TTS
|
Uses WellNuo ask_wellnuo_ai API for LLM responses, Deepgram for STT/TTS
|
||||||
|
|
||||||
|
ROBUST MODE: Barge-in is DISABLED - user cannot interrupt the agent.
|
||||||
|
This prevents hallucinations from background noise being interpreted as speech.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
@ -23,7 +27,7 @@ from livekit.agents import (
|
|||||||
from livekit.agents.types import DEFAULT_API_CONNECT_OPTIONS, APIConnectOptions
|
from livekit.agents.types import DEFAULT_API_CONNECT_OPTIONS, APIConnectOptions
|
||||||
from livekit.plugins import deepgram, noise_cancellation, silero
|
from livekit.plugins import deepgram, noise_cancellation, silero
|
||||||
|
|
||||||
logger = logging.getLogger("julia-ai")
|
logger = logging.getLogger("julia-robust")
|
||||||
|
|
||||||
# WellNuo API Configuration
|
# WellNuo API Configuration
|
||||||
WELLNUO_API_URL = "https://eluxnetworks.net/function/well-api/api"
|
WELLNUO_API_URL = "https://eluxnetworks.net/function/well-api/api"
|
||||||
@ -140,10 +144,20 @@ def normalize_question(user_message: str) -> str:
|
|||||||
class WellNuoLLM(llm.LLM):
|
class WellNuoLLM(llm.LLM):
|
||||||
"""Custom LLM that uses WellNuo ask_wellnuo_ai API."""
|
"""Custom LLM that uses WellNuo ask_wellnuo_ai API."""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(
|
||||||
|
self,
|
||||||
|
deployment_id: str | None = None,
|
||||||
|
beneficiary_names_dict: dict | None = None,
|
||||||
|
):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._token = None
|
self._token = None
|
||||||
self._model_name = "wellnuo-voice-ask"
|
self._model_name = "wellnuo-voice-ask"
|
||||||
|
# Dynamic values from participant metadata (or fallback to env/defaults)
|
||||||
|
self._deployment_id = deployment_id or DEPLOYMENT_ID
|
||||||
|
# SINGLE_DEPLOYMENT_MODE: if beneficiary_names_dict is empty or None,
|
||||||
|
# WellNuo API will automatically use the beneficiary name for this deployment_id
|
||||||
|
# This is the Lite mode - we don't need to pass the names dict
|
||||||
|
self._beneficiary_names_dict = beneficiary_names_dict if beneficiary_names_dict else None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def model(self) -> str:
|
def model(self) -> str:
|
||||||
@ -199,8 +213,22 @@ class WellNuoLLM(llm.LLM):
|
|||||||
"user_name": WELLNUO_USER,
|
"user_name": WELLNUO_USER,
|
||||||
"token": token,
|
"token": token,
|
||||||
"question": normalized_question,
|
"question": normalized_question,
|
||||||
"deployment_id": DEPLOYMENT_ID,
|
"deployment_id": self._deployment_id,
|
||||||
}
|
}
|
||||||
|
# Add beneficiary_names_dict ONLY if it's not empty
|
||||||
|
# In SINGLE_DEPLOYMENT_MODE (Lite app), we don't send names dict
|
||||||
|
# WellNuo API will use the beneficiary name for this deployment_id
|
||||||
|
if self._beneficiary_names_dict:
|
||||||
|
data["beneficiary_names_dict"] = json.dumps(
|
||||||
|
self._beneficiary_names_dict
|
||||||
|
)
|
||||||
|
logger.info(
|
||||||
|
f"Full mode: Using beneficiary_names_dict: {self._beneficiary_names_dict}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.info(
|
||||||
|
f"Single deployment mode: deployment_id={self._deployment_id}, no beneficiary_names_dict"
|
||||||
|
)
|
||||||
async with session.post(WELLNUO_API_URL, data=data) as resp:
|
async with session.post(WELLNUO_API_URL, data=data) as resp:
|
||||||
result = await resp.json()
|
result = await resp.json()
|
||||||
|
|
||||||
@ -286,8 +314,83 @@ class WellNuoLLMStream(llm.LLMStream):
|
|||||||
|
|
||||||
|
|
||||||
def prewarm(proc: JobProcess):
|
def prewarm(proc: JobProcess):
|
||||||
"""Preload VAD model for faster startup."""
|
"""Preload VAD model for faster startup.
|
||||||
proc.userdata["vad"] = silero.VAD.load()
|
|
||||||
|
ROBUST MODE: VAD is still needed for detecting when user FINISHES speaking,
|
||||||
|
but we'll disable interruptions in the AgentSession.
|
||||||
|
High thresholds to only respond to clear, deliberate speech.
|
||||||
|
"""
|
||||||
|
proc.userdata["vad"] = silero.VAD.load(
|
||||||
|
min_silence_duration=1.5, # Wait 1.5s of silence before ending speech (very patient)
|
||||||
|
min_speech_duration=0.3, # Require 0.3s of speech to start (filter short noises)
|
||||||
|
activation_threshold=0.6, # Higher threshold - only clear speech triggers (default: 0.5)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def wait_for_participant_with_metadata(
|
||||||
|
ctx: JobContext, timeout: float = 10.0
|
||||||
|
) -> tuple[str | None, dict | None]:
|
||||||
|
"""
|
||||||
|
Wait for a remote participant with metadata to join, then extract beneficiary data.
|
||||||
|
|
||||||
|
The mobile app passes this data through the LiveKit token metadata:
|
||||||
|
{
|
||||||
|
"deploymentId": "21",
|
||||||
|
"beneficiaryNamesDict": {"21": "papa", "69": "David"}
|
||||||
|
}
|
||||||
|
|
||||||
|
IMPORTANT: This function waits up to `timeout` seconds for a participant
|
||||||
|
with metadata to appear. This fixes the race condition where the agent
|
||||||
|
connects before the user's metadata is available.
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
deployment_id = None
|
||||||
|
beneficiary_names_dict = None
|
||||||
|
|
||||||
|
start_time = asyncio.get_event_loop().time()
|
||||||
|
attempt = 0
|
||||||
|
|
||||||
|
while asyncio.get_event_loop().time() - start_time < timeout:
|
||||||
|
attempt += 1
|
||||||
|
|
||||||
|
# Check all remote participants for metadata
|
||||||
|
for participant in ctx.room.remote_participants.values():
|
||||||
|
metadata = participant.metadata
|
||||||
|
if metadata:
|
||||||
|
try:
|
||||||
|
data = json.loads(metadata)
|
||||||
|
deployment_id = data.get("deploymentId")
|
||||||
|
beneficiary_names_dict = data.get("beneficiaryNamesDict")
|
||||||
|
|
||||||
|
if deployment_id:
|
||||||
|
logger.info(
|
||||||
|
f"[Attempt {attempt}] Extracted from participant "
|
||||||
|
f"{participant.identity}: deployment_id={deployment_id}, "
|
||||||
|
f"beneficiary_names_dict={beneficiary_names_dict}"
|
||||||
|
)
|
||||||
|
return deployment_id, beneficiary_names_dict
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
logger.warning(
|
||||||
|
f"Failed to parse participant metadata: {metadata}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Log waiting status every 2 seconds
|
||||||
|
if attempt % 4 == 0:
|
||||||
|
logger.info(
|
||||||
|
f"Waiting for participant with metadata... "
|
||||||
|
f"({int(asyncio.get_event_loop().time() - start_time)}s elapsed, "
|
||||||
|
f"participants: {len(ctx.room.remote_participants)})"
|
||||||
|
)
|
||||||
|
|
||||||
|
await asyncio.sleep(0.5)
|
||||||
|
|
||||||
|
# Timeout reached - log and return None
|
||||||
|
logger.warning(
|
||||||
|
f"Timeout ({timeout}s) waiting for participant metadata. "
|
||||||
|
f"Participants: {len(ctx.room.remote_participants)}"
|
||||||
|
)
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
async def entrypoint(ctx: JobContext):
|
async def entrypoint(ctx: JobContext):
|
||||||
@ -296,18 +399,48 @@ async def entrypoint(ctx: JobContext):
|
|||||||
# CRITICAL: Must connect to room first before accessing ctx.room
|
# CRITICAL: Must connect to room first before accessing ctx.room
|
||||||
await ctx.connect()
|
await ctx.connect()
|
||||||
|
|
||||||
logger.info(f"Starting Julia AI session in room {ctx.room.name}")
|
logger.info(f"Starting Julia ROBUST (no barge-in) session in room {ctx.room.name}")
|
||||||
logger.info(f"Using WellNuo ask_wellnuo_ai API with deployment_id: {DEPLOYMENT_ID}")
|
|
||||||
|
|
||||||
|
# Wait for participant with metadata - short timeout since metadata arrives immediately if present
|
||||||
|
# The mobile app sends deploymentId via token metadata
|
||||||
|
deployment_id, beneficiary_names_dict = await wait_for_participant_with_metadata(
|
||||||
|
ctx, timeout=2.0 # 2 seconds is enough - if metadata exists, it arrives within 0.5s
|
||||||
|
)
|
||||||
|
|
||||||
|
# Use deployment_id from metadata, or fall back to default
|
||||||
|
effective_deployment_id = deployment_id or DEPLOYMENT_ID
|
||||||
|
logger.info(f"Using deployment_id={effective_deployment_id} (from_metadata={deployment_id is not None})")
|
||||||
|
|
||||||
|
# ROBUST MODE: Barge-in DISABLED
|
||||||
|
# User cannot interrupt the agent while it's speaking.
|
||||||
|
# This prevents hallucinations from background noise.
|
||||||
session = AgentSession(
|
session = AgentSession(
|
||||||
# Deepgram Nova-2 for accurate speech-to-text
|
# Deepgram Nova-2 model for best STT accuracy
|
||||||
stt=deepgram.STT(model="nova-2"),
|
stt=deepgram.STT(
|
||||||
# WellNuo voice_ask API for LLM
|
model="nova-2-general",
|
||||||
llm=WellNuoLLM(),
|
language="en-US",
|
||||||
|
smart_format=True, # Better punctuation and formatting
|
||||||
|
no_delay=True, # Faster response for real-time
|
||||||
|
),
|
||||||
|
# WellNuo voice_ask API for LLM with dynamic beneficiary data
|
||||||
|
llm=WellNuoLLM(
|
||||||
|
deployment_id=effective_deployment_id,
|
||||||
|
beneficiary_names_dict=beneficiary_names_dict,
|
||||||
|
),
|
||||||
# Deepgram Aura Asteria for natural female voice
|
# Deepgram Aura Asteria for natural female voice
|
||||||
tts=deepgram.TTS(model="aura-asteria-en"),
|
tts=deepgram.TTS(model="aura-asteria-en"),
|
||||||
# Silero VAD for voice activity detection
|
# Silero VAD for voice activity detection (strict settings)
|
||||||
vad=ctx.proc.userdata["vad"],
|
vad=ctx.proc.userdata["vad"],
|
||||||
|
# DISABLE INTERRUPTIONS COMPLETELY:
|
||||||
|
# allow_interruptions=False means user cannot interrupt agent while speaking
|
||||||
|
# This prevents "hallucinations" from random noises being interpreted as speech
|
||||||
|
allow_interruptions=False,
|
||||||
|
# CRITICAL: Discard any audio captured while agent is speaking
|
||||||
|
# This ensures user speech during agent output is completely ignored
|
||||||
|
# See: https://github.com/livekit/agents/issues/4316
|
||||||
|
discard_audio_if_uninterruptible=True,
|
||||||
|
# Require longer speech before processing (filter out short noises)
|
||||||
|
min_interruption_duration=2.0,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Start the session with Julia assistant
|
# Start the session with Julia assistant
|
||||||
@ -332,6 +465,7 @@ if __name__ == "__main__":
|
|||||||
entrypoint_fnc=entrypoint,
|
entrypoint_fnc=entrypoint,
|
||||||
prewarm_fnc=prewarm,
|
prewarm_fnc=prewarm,
|
||||||
# Agent name must match what token requests (AGENT_NAME in livekit.js)
|
# Agent name must match what token requests (AGENT_NAME in livekit.js)
|
||||||
agent_name="julia-ai",
|
# ROBUST version - separate from julia-ai
|
||||||
|
agent_name="julia-robust",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
155
julia-agent/julia-ai/uv.lock
generated
155
julia-agent/julia-ai/uv.lock
generated
@ -751,7 +751,6 @@ dependencies = [
|
|||||||
{ name = "livekit-agents", extra = ["silero"] },
|
{ name = "livekit-agents", extra = ["silero"] },
|
||||||
{ name = "livekit-plugins-deepgram" },
|
{ name = "livekit-plugins-deepgram" },
|
||||||
{ name = "livekit-plugins-noise-cancellation" },
|
{ name = "livekit-plugins-noise-cancellation" },
|
||||||
{ name = "livekit-plugins-openai" },
|
|
||||||
{ name = "python-dotenv" },
|
{ name = "python-dotenv" },
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -768,7 +767,6 @@ requires-dist = [
|
|||||||
{ name = "livekit-agents", extras = ["silero"], specifier = "~=1.3" },
|
{ name = "livekit-agents", extras = ["silero"], specifier = "~=1.3" },
|
||||||
{ name = "livekit-plugins-deepgram", specifier = "~=1.0" },
|
{ name = "livekit-plugins-deepgram", specifier = "~=1.0" },
|
||||||
{ name = "livekit-plugins-noise-cancellation", specifier = "~=0.2" },
|
{ name = "livekit-plugins-noise-cancellation", specifier = "~=0.2" },
|
||||||
{ name = "livekit-plugins-openai", specifier = "~=1.0" },
|
|
||||||
{ name = "python-dotenv" },
|
{ name = "python-dotenv" },
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -844,9 +842,6 @@ codecs = [
|
|||||||
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
||||||
{ name = "numpy", version = "2.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
{ name = "numpy", version = "2.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
||||||
]
|
]
|
||||||
images = [
|
|
||||||
{ name = "pillow" },
|
|
||||||
]
|
|
||||||
silero = [
|
silero = [
|
||||||
{ name = "livekit-plugins-silero" },
|
{ name = "livekit-plugins-silero" },
|
||||||
]
|
]
|
||||||
@ -923,19 +918,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/9f/4d/37be8da861607f392d07bb0f1c6b57c635db249095084abcbfaaaab6d7b5/livekit_plugins_noise_cancellation-0.2.5-py3-none-win_amd64.whl", hash = "sha256:5879d28120a6b47a7d557832d9432683710987f79e9b514171898be36534380b", size = 65757107, upload-time = "2025-06-30T14:49:59.053Z" },
|
{ url = "https://files.pythonhosted.org/packages/9f/4d/37be8da861607f392d07bb0f1c6b57c635db249095084abcbfaaaab6d7b5/livekit_plugins_noise_cancellation-0.2.5-py3-none-win_amd64.whl", hash = "sha256:5879d28120a6b47a7d557832d9432683710987f79e9b514171898be36534380b", size = 65757107, upload-time = "2025-06-30T14:49:59.053Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "livekit-plugins-openai"
|
|
||||||
version = "1.3.11"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "livekit-agents", extra = ["codecs", "images"] },
|
|
||||||
{ name = "openai", extra = ["realtime"] },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/e3/30/44a724703548e729280f4a420ac3ec8d58b61dd4e79fff529060689f147f/livekit_plugins_openai-1.3.11.tar.gz", hash = "sha256:61d77152f96213003b9bdea48c86a44a07a70456a41c664e82a2b5ae99a7f72a", size = 48754, upload-time = "2026-01-14T18:45:46.685Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8b/68/1093471d954f24789e8a17d2f56deaa3831d86a0edbc747a63b8eb49c113/livekit_plugins_openai-1.3.11-py3-none-any.whl", hash = "sha256:c54964cd987fa8a9e341f66bb30baeb02bfeea62a87eebaf19ae52b0486e8224", size = 56582, upload-time = "2026-01-14T18:45:45.744Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "livekit-plugins-silero"
|
name = "livekit-plugins-silero"
|
||||||
version = "1.3.11"
|
version = "1.3.11"
|
||||||
@ -1289,11 +1271,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/b5/df/c306f7375d42bafb379934c2df4c2fa3964656c8c782bac75ee10c102818/openai-2.15.0-py3-none-any.whl", hash = "sha256:6ae23b932cd7230f7244e52954daa6602716d6b9bf235401a107af731baea6c3", size = 1067879, upload-time = "2026-01-09T22:10:06.446Z" },
|
{ url = "https://files.pythonhosted.org/packages/b5/df/c306f7375d42bafb379934c2df4c2fa3964656c8c782bac75ee10c102818/openai-2.15.0-py3-none-any.whl", hash = "sha256:6ae23b932cd7230f7244e52954daa6602716d6b9bf235401a107af731baea6c3", size = 1067879, upload-time = "2026-01-09T22:10:06.446Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.optional-dependencies]
|
|
||||||
realtime = [
|
|
||||||
{ name = "websockets" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "opentelemetry-api"
|
name = "opentelemetry-api"
|
||||||
version = "1.39.1"
|
version = "1.39.1"
|
||||||
@ -1416,79 +1393,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
|
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pillow"
|
|
||||||
version = "12.1.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/d0/02/d52c733a2452ef1ffcc123b68e6606d07276b0e358db70eabad7e40042b7/pillow-12.1.0.tar.gz", hash = "sha256:5c5ae0a06e9ea030ab786b0251b32c7e4ce10e58d983c0d5c56029455180b5b9", size = 46977283, upload-time = "2026-01-02T09:13:29.892Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/fe/41/f73d92b6b883a579e79600d391f2e21cb0df767b2714ecbd2952315dfeef/pillow-12.1.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:fb125d860738a09d363a88daa0f59c4533529a90e564785e20fe875b200b6dbd", size = 5304089, upload-time = "2026-01-02T09:10:24.953Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/94/55/7aca2891560188656e4a91ed9adba305e914a4496800da6b5c0a15f09edf/pillow-12.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cad302dc10fac357d3467a74a9561c90609768a6f73a1923b0fd851b6486f8b0", size = 4657815, upload-time = "2026-01-02T09:10:27.063Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e9/d2/b28221abaa7b4c40b7dba948f0f6a708bd7342c4d47ce342f0ea39643974/pillow-12.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a40905599d8079e09f25027423aed94f2823adaf2868940de991e53a449e14a8", size = 6222593, upload-time = "2026-01-02T09:10:29.115Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/71/b8/7a61fb234df6a9b0b479f69e66901209d89ff72a435b49933f9122f94cac/pillow-12.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:92a7fe4225365c5e3a8e598982269c6d6698d3e783b3b1ae979e7819f9cd55c1", size = 8027579, upload-time = "2026-01-02T09:10:31.182Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ea/51/55c751a57cc524a15a0e3db20e5cde517582359508d62305a627e77fd295/pillow-12.1.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f10c98f49227ed8383d28174ee95155a675c4ed7f85e2e573b04414f7e371bda", size = 6335760, upload-time = "2026-01-02T09:10:33.02Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/dc/7c/60e3e6f5e5891a1a06b4c910f742ac862377a6fe842f7184df4a274ce7bf/pillow-12.1.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8637e29d13f478bc4f153d8daa9ffb16455f0a6cb287da1b432fdad2bfbd66c7", size = 7027127, upload-time = "2026-01-02T09:10:35.009Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/06/37/49d47266ba50b00c27ba63a7c898f1bb41a29627ced8c09e25f19ebec0ff/pillow-12.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:21e686a21078b0f9cb8c8a961d99e6a4ddb88e0fc5ea6e130172ddddc2e5221a", size = 6449896, upload-time = "2026-01-02T09:10:36.793Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f9/e5/67fd87d2913902462cd9b79c6211c25bfe95fcf5783d06e1367d6d9a741f/pillow-12.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2415373395a831f53933c23ce051021e79c8cd7979822d8cc478547a3f4da8ef", size = 7151345, upload-time = "2026-01-02T09:10:39.064Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/bd/15/f8c7abf82af68b29f50d77c227e7a1f87ce02fdc66ded9bf603bc3b41180/pillow-12.1.0-cp310-cp310-win32.whl", hash = "sha256:e75d3dba8fc1ddfec0cd752108f93b83b4f8d6ab40e524a95d35f016b9683b09", size = 6325568, upload-time = "2026-01-02T09:10:41.035Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d4/24/7d1c0e160b6b5ac2605ef7d8be537e28753c0db5363d035948073f5513d7/pillow-12.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:64efdf00c09e31efd754448a383ea241f55a994fd079866b92d2bbff598aad91", size = 7032367, upload-time = "2026-01-02T09:10:43.09Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f4/03/41c038f0d7a06099254c60f618d0ec7be11e79620fc23b8e85e5b31d9a44/pillow-12.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:f188028b5af6b8fb2e9a76ac0f841a575bd1bd396e46ef0840d9b88a48fdbcea", size = 2452345, upload-time = "2026-01-02T09:10:44.795Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/43/c4/bf8328039de6cc22182c3ef007a2abfbbdab153661c0a9aa78af8d706391/pillow-12.1.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:a83e0850cb8f5ac975291ebfc4170ba481f41a28065277f7f735c202cd8e0af3", size = 5304057, upload-time = "2026-01-02T09:10:46.627Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/43/06/7264c0597e676104cc22ca73ee48f752767cd4b1fe084662620b17e10120/pillow-12.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b6e53e82ec2db0717eabb276aa56cf4e500c9a7cec2c2e189b55c24f65a3e8c0", size = 4657811, upload-time = "2026-01-02T09:10:49.548Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/72/64/f9189e44474610daf83da31145fa56710b627b5c4c0b9c235e34058f6b31/pillow-12.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:40a8e3b9e8773876d6e30daed22f016509e3987bab61b3b7fe309d7019a87451", size = 6232243, upload-time = "2026-01-02T09:10:51.62Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ef/30/0df458009be6a4caca4ca2c52975e6275c387d4e5c95544e34138b41dc86/pillow-12.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:800429ac32c9b72909c671aaf17ecd13110f823ddb7db4dfef412a5587c2c24e", size = 8037872, upload-time = "2026-01-02T09:10:53.446Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e4/86/95845d4eda4f4f9557e25381d70876aa213560243ac1a6d619c46caaedd9/pillow-12.1.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b022eaaf709541b391ee069f0022ee5b36c709df71986e3f7be312e46f42c84", size = 6345398, upload-time = "2026-01-02T09:10:55.426Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5c/1f/8e66ab9be3aaf1435bc03edd1ebdf58ffcd17f7349c1d970cafe87af27d9/pillow-12.1.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f345e7bc9d7f368887c712aa5054558bad44d2a301ddf9248599f4161abc7c0", size = 7034667, upload-time = "2026-01-02T09:10:57.11Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f9/f6/683b83cb9b1db1fb52b87951b1c0b99bdcfceaa75febf11406c19f82cb5e/pillow-12.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d70347c8a5b7ccd803ec0c85c8709f036e6348f1e6a5bf048ecd9c64d3550b8b", size = 6458743, upload-time = "2026-01-02T09:10:59.331Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9a/7d/de833d63622538c1d58ce5395e7c6cb7e7dce80decdd8bde4a484e095d9f/pillow-12.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1fcc52d86ce7a34fd17cb04e87cfdb164648a3662a6f20565910a99653d66c18", size = 7159342, upload-time = "2026-01-02T09:11:01.82Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8c/40/50d86571c9e5868c42b81fe7da0c76ca26373f3b95a8dd675425f4a92ec1/pillow-12.1.0-cp311-cp311-win32.whl", hash = "sha256:3ffaa2f0659e2f740473bcf03c702c39a8d4b2b7ffc629052028764324842c64", size = 6328655, upload-time = "2026-01-02T09:11:04.556Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6c/af/b1d7e301c4cd26cd45d4af884d9ee9b6fab893b0ad2450d4746d74a6968c/pillow-12.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:806f3987ffe10e867bab0ddad45df1148a2b98221798457fa097ad85d6e8bc75", size = 7031469, upload-time = "2026-01-02T09:11:06.538Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/48/36/d5716586d887fb2a810a4a61518a327a1e21c8b7134c89283af272efe84b/pillow-12.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:9f5fefaca968e700ad1a4a9de98bf0869a94e397fe3524c4c9450c1445252304", size = 2452515, upload-time = "2026-01-02T09:11:08.226Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/20/31/dc53fe21a2f2996e1b7d92bf671cdb157079385183ef7c1ae08b485db510/pillow-12.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a332ac4ccb84b6dde65dbace8431f3af08874bf9770719d32a635c4ef411b18b", size = 5262642, upload-time = "2026-01-02T09:11:10.138Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ab/c1/10e45ac9cc79419cedf5121b42dcca5a50ad2b601fa080f58c22fb27626e/pillow-12.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:907bfa8a9cb790748a9aa4513e37c88c59660da3bcfffbd24a7d9e6abf224551", size = 4657464, upload-time = "2026-01-02T09:11:12.319Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ad/26/7b82c0ab7ef40ebede7a97c72d473bda5950f609f8e0c77b04af574a0ddb/pillow-12.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:efdc140e7b63b8f739d09a99033aa430accce485ff78e6d311973a67b6bf3208", size = 6234878, upload-time = "2026-01-02T09:11:14.096Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/76/25/27abc9792615b5e886ca9411ba6637b675f1b77af3104710ac7353fe5605/pillow-12.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bef9768cab184e7ae6e559c032e95ba8d07b3023c289f79a2bd36e8bf85605a5", size = 8044868, upload-time = "2026-01-02T09:11:15.903Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/0a/ea/f200a4c36d836100e7bc738fc48cd963d3ba6372ebc8298a889e0cfc3359/pillow-12.1.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:742aea052cf5ab5034a53c3846165bc3ce88d7c38e954120db0ab867ca242661", size = 6349468, upload-time = "2026-01-02T09:11:17.631Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/11/8f/48d0b77ab2200374c66d344459b8958c86693be99526450e7aee714e03e4/pillow-12.1.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a6dfc2af5b082b635af6e08e0d1f9f1c4e04d17d4e2ca0ef96131e85eda6eb17", size = 7041518, upload-time = "2026-01-02T09:11:19.389Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1d/23/c281182eb986b5d31f0a76d2a2c8cd41722d6fb8ed07521e802f9bba52de/pillow-12.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:609e89d9f90b581c8d16358c9087df76024cf058fa693dd3e1e1620823f39670", size = 6462829, upload-time = "2026-01-02T09:11:21.28Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/25/ef/7018273e0faac099d7b00982abdcc39142ae6f3bd9ceb06de09779c4a9d6/pillow-12.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:43b4899cfd091a9693a1278c4982f3e50f7fb7cff5153b05174b4afc9593b616", size = 7166756, upload-time = "2026-01-02T09:11:23.559Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8f/c8/993d4b7ab2e341fe02ceef9576afcf5830cdec640be2ac5bee1820d693d4/pillow-12.1.0-cp312-cp312-win32.whl", hash = "sha256:aa0c9cc0b82b14766a99fbe6084409972266e82f459821cd26997a488a7261a7", size = 6328770, upload-time = "2026-01-02T09:11:25.661Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a7/87/90b358775a3f02765d87655237229ba64a997b87efa8ccaca7dd3e36e7a7/pillow-12.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:d70534cea9e7966169ad29a903b99fc507e932069a881d0965a1a84bb57f6c6d", size = 7033406, upload-time = "2026-01-02T09:11:27.474Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5d/cf/881b457eccacac9e5b2ddd97d5071fb6d668307c57cbf4e3b5278e06e536/pillow-12.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:65b80c1ee7e14a87d6a068dd3b0aea268ffcabfe0498d38661b00c5b4b22e74c", size = 2452612, upload-time = "2026-01-02T09:11:29.309Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/dd/c7/2530a4aa28248623e9d7f27316b42e27c32ec410f695929696f2e0e4a778/pillow-12.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7b5dd7cbae20285cdb597b10eb5a2c13aa9de6cde9bb64a3c1317427b1db1ae1", size = 4062543, upload-time = "2026-01-02T09:11:31.566Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8f/1f/40b8eae823dc1519b87d53c30ed9ef085506b05281d313031755c1705f73/pillow-12.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:29a4cef9cb672363926f0470afc516dbf7305a14d8c54f7abbb5c199cd8f8179", size = 4138373, upload-time = "2026-01-02T09:11:33.367Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d4/77/6fa60634cf06e52139fd0e89e5bbf055e8166c691c42fb162818b7fda31d/pillow-12.1.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:681088909d7e8fa9e31b9799aaa59ba5234c58e5e4f1951b4c4d1082a2e980e0", size = 3601241, upload-time = "2026-01-02T09:11:35.011Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4f/bf/28ab865de622e14b747f0cd7877510848252d950e43002e224fb1c9ababf/pillow-12.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:983976c2ab753166dc66d36af6e8ec15bb511e4a25856e2227e5f7e00a160587", size = 5262410, upload-time = "2026-01-02T09:11:36.682Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1c/34/583420a1b55e715937a85bd48c5c0991598247a1fd2eb5423188e765ea02/pillow-12.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:db44d5c160a90df2d24a24760bbd37607d53da0b34fb546c4c232af7192298ac", size = 4657312, upload-time = "2026-01-02T09:11:38.535Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1d/fd/f5a0896839762885b3376ff04878f86ab2b097c2f9a9cdccf4eda8ba8dc0/pillow-12.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6b7a9d1db5dad90e2991645874f708e87d9a3c370c243c2d7684d28f7e133e6b", size = 6232605, upload-time = "2026-01-02T09:11:40.602Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/98/aa/938a09d127ac1e70e6ed467bd03834350b33ef646b31edb7452d5de43792/pillow-12.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6258f3260986990ba2fa8a874f8b6e808cf5abb51a94015ca3dc3c68aa4f30ea", size = 8041617, upload-time = "2026-01-02T09:11:42.721Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/17/e8/538b24cb426ac0186e03f80f78bc8dc7246c667f58b540bdd57c71c9f79d/pillow-12.1.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e115c15e3bc727b1ca3e641a909f77f8ca72a64fff150f666fcc85e57701c26c", size = 6346509, upload-time = "2026-01-02T09:11:44.955Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/01/9a/632e58ec89a32738cabfd9ec418f0e9898a2b4719afc581f07c04a05e3c9/pillow-12.1.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6741e6f3074a35e47c77b23a4e4f2d90db3ed905cb1c5e6e0d49bff2045632bc", size = 7038117, upload-time = "2026-01-02T09:11:46.736Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c7/a2/d40308cf86eada842ca1f3ffa45d0ca0df7e4ab33c83f81e73f5eaed136d/pillow-12.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:935b9d1aed48fcfb3f838caac506f38e29621b44ccc4f8a64d575cb1b2a88644", size = 6460151, upload-time = "2026-01-02T09:11:48.625Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f1/88/f5b058ad6453a085c5266660a1417bdad590199da1b32fb4efcff9d33b05/pillow-12.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5fee4c04aad8932da9f8f710af2c1a15a83582cfb884152a9caa79d4efcdbf9c", size = 7164534, upload-time = "2026-01-02T09:11:50.445Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/19/ce/c17334caea1db789163b5d855a5735e47995b0b5dc8745e9a3605d5f24c0/pillow-12.1.0-cp313-cp313-win32.whl", hash = "sha256:a786bf667724d84aa29b5db1c61b7bfdde380202aaca12c3461afd6b71743171", size = 6332551, upload-time = "2026-01-02T09:11:52.234Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e5/07/74a9d941fa45c90a0d9465098fe1ec85de3e2afbdc15cc4766622d516056/pillow-12.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:461f9dfdafa394c59cd6d818bdfdbab4028b83b02caadaff0ffd433faf4c9a7a", size = 7040087, upload-time = "2026-01-02T09:11:54.822Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/88/09/c99950c075a0e9053d8e880595926302575bc742b1b47fe1bbcc8d388d50/pillow-12.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:9212d6b86917a2300669511ed094a9406888362e085f2431a7da985a6b124f45", size = 2452470, upload-time = "2026-01-02T09:11:56.522Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b5/ba/970b7d85ba01f348dee4d65412476321d40ee04dcb51cd3735b9dc94eb58/pillow-12.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:00162e9ca6d22b7c3ee8e61faa3c3253cd19b6a37f126cad04f2f88b306f557d", size = 5264816, upload-time = "2026-01-02T09:11:58.227Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/10/60/650f2fb55fdba7a510d836202aa52f0baac633e50ab1cf18415d332188fb/pillow-12.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7d6daa89a00b58c37cb1747ec9fb7ac3bc5ffd5949f5888657dfddde6d1312e0", size = 4660472, upload-time = "2026-01-02T09:12:00.798Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2b/c0/5273a99478956a099d533c4f46cbaa19fd69d606624f4334b85e50987a08/pillow-12.1.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e2479c7f02f9d505682dc47df8c0ea1fc5e264c4d1629a5d63fe3e2334b89554", size = 6268974, upload-time = "2026-01-02T09:12:02.572Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b4/26/0bf714bc2e73d5267887d47931d53c4ceeceea6978148ed2ab2a4e6463c4/pillow-12.1.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f188d580bd870cda1e15183790d1cc2fa78f666e76077d103edf048eed9c356e", size = 8073070, upload-time = "2026-01-02T09:12:04.75Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/43/cf/1ea826200de111a9d65724c54f927f3111dc5ae297f294b370a670c17786/pillow-12.1.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0fde7ec5538ab5095cc02df38ee99b0443ff0e1c847a045554cf5f9af1f4aa82", size = 6380176, upload-time = "2026-01-02T09:12:06.626Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/03/e0/7938dd2b2013373fd85d96e0f38d62b7a5a262af21ac274250c7ca7847c9/pillow-12.1.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ed07dca4a8464bada6139ab38f5382f83e5f111698caf3191cb8dbf27d908b4", size = 7067061, upload-time = "2026-01-02T09:12:08.624Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/86/ad/a2aa97d37272a929a98437a8c0ac37b3cf012f4f8721e1bd5154699b2518/pillow-12.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f45bd71d1fa5e5749587613037b172e0b3b23159d1c00ef2fc920da6f470e6f0", size = 6491824, upload-time = "2026-01-02T09:12:10.488Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a4/44/80e46611b288d51b115826f136fb3465653c28f491068a72d3da49b54cd4/pillow-12.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:277518bf4fe74aa91489e1b20577473b19ee70fb97c374aa50830b279f25841b", size = 7190911, upload-time = "2026-01-02T09:12:12.772Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/86/77/eacc62356b4cf81abe99ff9dbc7402750044aed02cfd6a503f7c6fc11f3e/pillow-12.1.0-cp313-cp313t-win32.whl", hash = "sha256:7315f9137087c4e0ee73a761b163fc9aa3b19f5f606a7fc08d83fd3e4379af65", size = 6336445, upload-time = "2026-01-02T09:12:14.775Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e7/3c/57d81d0b74d218706dafccb87a87ea44262c43eef98eb3b164fd000e0491/pillow-12.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:0ddedfaa8b5f0b4ffbc2fa87b556dc59f6bb4ecb14a53b33f9189713ae8053c0", size = 7045354, upload-time = "2026-01-02T09:12:16.599Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ac/82/8b9b97bba2e3576a340f93b044a3a3a09841170ab4c1eb0d5c93469fd32f/pillow-12.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:80941e6d573197a0c28f394753de529bb436b1ca990ed6e765cf42426abc39f8", size = 2454547, upload-time = "2026-01-02T09:12:18.704Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8b/bc/224b1d98cffd7164b14707c91aac83c07b047fbd8f58eba4066a3e53746a/pillow-12.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ca94b6aac0d7af2a10ba08c0f888b3d5114439b6b3ef39968378723622fed377", size = 5228605, upload-time = "2026-01-02T09:13:14.084Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/0c/ca/49ca7769c4550107de049ed85208240ba0f330b3f2e316f24534795702ce/pillow-12.1.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:351889afef0f485b84078ea40fe33727a0492b9af3904661b0abbafee0355b72", size = 4622245, upload-time = "2026-01-02T09:13:15.964Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/73/48/fac807ce82e5955bcc2718642b94b1bd22a82a6d452aea31cbb678cddf12/pillow-12.1.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb0984b30e973f7e2884362b7d23d0a348c7143ee559f38ef3eaab640144204c", size = 5247593, upload-time = "2026-01-02T09:13:17.913Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d2/95/3e0742fe358c4664aed4fd05d5f5373dcdad0b27af52aa0972568541e3f4/pillow-12.1.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:84cabc7095dd535ca934d57e9ce2a72ffd216e435a84acb06b2277b1de2689bd", size = 6989008, upload-time = "2026-01-02T09:13:20.083Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5a/74/fe2ac378e4e202e56d50540d92e1ef4ff34ed687f3c60f6a121bcf99437e/pillow-12.1.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53d8b764726d3af1a138dd353116f774e3862ec7e3794e0c8781e30db0f35dfc", size = 5313824, upload-time = "2026-01-02T09:13:22.405Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f3/77/2a60dee1adee4e2655ac328dd05c02a955c1cd683b9f1b82ec3feb44727c/pillow-12.1.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5da841d81b1a05ef940a8567da92decaa15bc4d7dedb540a8c219ad83d91808a", size = 5963278, upload-time = "2026-01-02T09:13:24.706Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2d/71/64e9b1c7f04ae0027f788a248e6297d7fcc29571371fe7d45495a78172c0/pillow-12.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:75af0b4c229ac519b155028fa1be632d812a519abba9b46b20e50c6caa184f19", size = 7029809, upload-time = "2026-01-02T09:13:26.541Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pluggy"
|
name = "pluggy"
|
||||||
version = "1.6.0"
|
version = "1.6.0"
|
||||||
@ -2091,65 +1995,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/6e/d4/ed38dd3b1767193de971e694aa544356e63353c33a85d948166b5ff58b9e/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49", size = 457546, upload-time = "2025-10-14T15:06:13.372Z" },
|
{ url = "https://files.pythonhosted.org/packages/6e/d4/ed38dd3b1767193de971e694aa544356e63353c33a85d948166b5ff58b9e/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49", size = 457546, upload-time = "2025-10-14T15:06:13.372Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "websockets"
|
|
||||||
version = "15.0.1"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1e/da/6462a9f510c0c49837bbc9345aca92d767a56c1fb2939e1579df1e1cdcf7/websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b", size = 175423, upload-time = "2025-03-05T20:01:35.363Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1c/9f/9d11c1a4eb046a9e106483b9ff69bce7ac880443f00e5ce64261b47b07e7/websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205", size = 173080, upload-time = "2025-03-05T20:01:37.304Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d5/4f/b462242432d93ea45f297b6179c7333dd0402b855a912a04e7fc61c0d71f/websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a", size = 173329, upload-time = "2025-03-05T20:01:39.668Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6e/0c/6afa1f4644d7ed50284ac59cc70ef8abd44ccf7d45850d989ea7310538d0/websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e", size = 182312, upload-time = "2025-03-05T20:01:41.815Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/dd/d4/ffc8bd1350b229ca7a4db2a3e1c482cf87cea1baccd0ef3e72bc720caeec/websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf", size = 181319, upload-time = "2025-03-05T20:01:43.967Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/97/3a/5323a6bb94917af13bbb34009fac01e55c51dfde354f63692bf2533ffbc2/websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb", size = 181631, upload-time = "2025-03-05T20:01:46.104Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a6/cc/1aeb0f7cee59ef065724041bb7ed667b6ab1eeffe5141696cccec2687b66/websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d", size = 182016, upload-time = "2025-03-05T20:01:47.603Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/79/f9/c86f8f7af208e4161a7f7e02774e9d0a81c632ae76db2ff22549e1718a51/websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9", size = 181426, upload-time = "2025-03-05T20:01:48.949Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c7/b9/828b0bc6753db905b91df6ae477c0b14a141090df64fb17f8a9d7e3516cf/websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c", size = 181360, upload-time = "2025-03-05T20:01:50.938Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/89/fb/250f5533ec468ba6327055b7d98b9df056fb1ce623b8b6aaafb30b55d02e/websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256", size = 176388, upload-time = "2025-03-05T20:01:52.213Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1c/46/aca7082012768bb98e5608f01658ff3ac8437e563eca41cf068bd5849a5e/websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41", size = 176830, upload-time = "2025-03-05T20:01:53.922Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423, upload-time = "2025-03-05T20:01:56.276Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082, upload-time = "2025-03-05T20:01:57.563Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330, upload-time = "2025-03-05T20:01:59.063Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878, upload-time = "2025-03-05T20:02:00.305Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883, upload-time = "2025-03-05T20:02:03.148Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252, upload-time = "2025-03-05T20:02:05.29Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521, upload-time = "2025-03-05T20:02:07.458Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958, upload-time = "2025-03-05T20:02:09.842Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918, upload-time = "2025-03-05T20:02:11.968Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388, upload-time = "2025-03-05T20:02:13.32Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828, upload-time = "2025-03-05T20:02:14.585Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/02/9e/d40f779fa16f74d3468357197af8d6ad07e7c5a27ea1ca74ceb38986f77a/websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3", size = 173109, upload-time = "2025-03-05T20:03:17.769Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/bc/cd/5b887b8585a593073fd92f7c23ecd3985cd2c3175025a91b0d69b0551372/websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1", size = 173343, upload-time = "2025-03-05T20:03:19.094Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/fe/ae/d34f7556890341e900a95acf4886833646306269f899d58ad62f588bf410/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475", size = 174599, upload-time = "2025-03-05T20:03:21.1Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/71/e6/5fd43993a87db364ec60fc1d608273a1a465c0caba69176dd160e197ce42/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9", size = 174207, upload-time = "2025-03-05T20:03:23.221Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2b/fb/c492d6daa5ec067c2988ac80c61359ace5c4c674c532985ac5a123436cec/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04", size = 174155, upload-time = "2025-03-05T20:03:25.321Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/68/a1/dcb68430b1d00b698ae7a7e0194433bce4f07ded185f0ee5fb21e2a2e91e/websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122", size = 176884, upload-time = "2025-03-05T20:03:27.934Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yarl"
|
name = "yarl"
|
||||||
version = "1.22.0"
|
version = "1.22.0"
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
livekit-agents[silero]~=1.3
|
livekit-agents[silero]~=1.3
|
||||||
livekit-plugins-deepgram~=1.0
|
livekit-plugins-deepgram~=1.0
|
||||||
livekit-plugins-openai~=1.0
|
|
||||||
python-dotenv~=1.0
|
python-dotenv~=1.0
|
||||||
|
aiohttp~=3.9
|
||||||
|
|||||||
@ -10,7 +10,8 @@ app.use(express.json());
|
|||||||
const LIVEKIT_API_KEY = process.env.LIVEKIT_API_KEY || 'APIEivUcPW3WSrV';
|
const LIVEKIT_API_KEY = process.env.LIVEKIT_API_KEY || 'APIEivUcPW3WSrV';
|
||||||
const LIVEKIT_API_SECRET = process.env.LIVEKIT_API_SECRET || 'A65mc5KUKE0VGdZNaMRwe6uJpA9ZQPAxS66akZTOfmL';
|
const LIVEKIT_API_SECRET = process.env.LIVEKIT_API_SECRET || 'A65mc5KUKE0VGdZNaMRwe6uJpA9ZQPAxS66akZTOfmL';
|
||||||
const LIVEKIT_URL = 'wss://live-kit-demo-70txlh6a.livekit.cloud';
|
const LIVEKIT_URL = 'wss://live-kit-demo-70txlh6a.livekit.cloud';
|
||||||
const AGENT_NAME = 'julia-ai';
|
// ROBUST MODE: Use julia-robust agent (no barge-in)
|
||||||
|
const AGENT_NAME = 'julia-robust';
|
||||||
|
|
||||||
// Health check
|
// Health check
|
||||||
app.get('/health', (req, res) => {
|
app.get('/health', (req, res) => {
|
||||||
|
|||||||
37
package-lock.json
generated
37
package-lock.json
generated
@ -29,8 +29,6 @@
|
|||||||
"expo-linking": "~8.0.10",
|
"expo-linking": "~8.0.10",
|
||||||
"expo-router": "~6.0.19",
|
"expo-router": "~6.0.19",
|
||||||
"expo-secure-store": "^15.0.8",
|
"expo-secure-store": "^15.0.8",
|
||||||
"expo-speech": "~14.0.8",
|
|
||||||
"expo-speech-recognition": "^3.0.1",
|
|
||||||
"expo-splash-screen": "~31.0.12",
|
"expo-splash-screen": "~31.0.12",
|
||||||
"expo-status-bar": "~3.0.9",
|
"expo-status-bar": "~3.0.9",
|
||||||
"expo-symbols": "~1.0.8",
|
"expo-symbols": "~1.0.8",
|
||||||
@ -41,6 +39,7 @@
|
|||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
"react-native": "0.81.5",
|
"react-native": "0.81.5",
|
||||||
"react-native-gesture-handler": "~2.28.0",
|
"react-native-gesture-handler": "~2.28.0",
|
||||||
|
"react-native-keyboard-controller": "^1.20.6",
|
||||||
"react-native-reanimated": "~4.1.1",
|
"react-native-reanimated": "~4.1.1",
|
||||||
"react-native-safe-area-context": "~5.6.0",
|
"react-native-safe-area-context": "~5.6.0",
|
||||||
"react-native-screens": "~4.16.0",
|
"react-native-screens": "~4.16.0",
|
||||||
@ -7386,26 +7385,6 @@
|
|||||||
"node": ">=20.16.0"
|
"node": ">=20.16.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/expo-speech": {
|
|
||||||
"version": "14.0.8",
|
|
||||||
"resolved": "https://registry.npmjs.org/expo-speech/-/expo-speech-14.0.8.tgz",
|
|
||||||
"integrity": "sha512-UjBFCFv58nutlLw92L7kUS0ZjbOOfaTdiEv/HbjvMrT6BfldoOLLBZbaEcEhDdZK36NY/kass0Kzxk+co6vxSQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"peerDependencies": {
|
|
||||||
"expo": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/expo-speech-recognition": {
|
|
||||||
"version": "3.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/expo-speech-recognition/-/expo-speech-recognition-3.0.1.tgz",
|
|
||||||
"integrity": "sha512-sMyOE1Vq1635B2oG0irgv6MZo0axIva3yNMmy86fwSqcFI+y9GhfOqZnYx3PFq7A0tNP2AlaNZLA9+tkgBcLhQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"peerDependencies": {
|
|
||||||
"expo": "*",
|
|
||||||
"react": "*",
|
|
||||||
"react-native": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/expo-splash-screen": {
|
"node_modules/expo-splash-screen": {
|
||||||
"version": "31.0.12",
|
"version": "31.0.12",
|
||||||
"resolved": "https://registry.npmjs.org/expo-splash-screen/-/expo-splash-screen-31.0.12.tgz",
|
"resolved": "https://registry.npmjs.org/expo-splash-screen/-/expo-splash-screen-31.0.12.tgz",
|
||||||
@ -11329,6 +11308,20 @@
|
|||||||
"react-native": "*"
|
"react-native": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-native-keyboard-controller": {
|
||||||
|
"version": "1.20.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-native-keyboard-controller/-/react-native-keyboard-controller-1.20.6.tgz",
|
||||||
|
"integrity": "sha512-RS6FjIjTFtAMQGdcXp3m6jUs1XgDa8qkpO5c4ix1S5HS0z3L2E1LUOY5rD73YUADOO3MfQN1z3JkHdBtzKucbg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"react-native-is-edge-to-edge": "^1.2.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "*",
|
||||||
|
"react-native": "*",
|
||||||
|
"react-native-reanimated": ">=3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-native-reanimated": {
|
"node_modules/react-native-reanimated": {
|
||||||
"version": "4.1.6",
|
"version": "4.1.6",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.1.6.tgz",
|
||||||
|
|||||||
@ -32,8 +32,6 @@
|
|||||||
"expo-linking": "~8.0.10",
|
"expo-linking": "~8.0.10",
|
||||||
"expo-router": "~6.0.19",
|
"expo-router": "~6.0.19",
|
||||||
"expo-secure-store": "^15.0.8",
|
"expo-secure-store": "^15.0.8",
|
||||||
"expo-speech": "~14.0.8",
|
|
||||||
"expo-speech-recognition": "^3.0.1",
|
|
||||||
"expo-splash-screen": "~31.0.12",
|
"expo-splash-screen": "~31.0.12",
|
||||||
"expo-status-bar": "~3.0.9",
|
"expo-status-bar": "~3.0.9",
|
||||||
"expo-symbols": "~1.0.8",
|
"expo-symbols": "~1.0.8",
|
||||||
@ -44,6 +42,7 @@
|
|||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
"react-native": "0.81.5",
|
"react-native": "0.81.5",
|
||||||
"react-native-gesture-handler": "~2.28.0",
|
"react-native-gesture-handler": "~2.28.0",
|
||||||
|
"react-native-keyboard-controller": "^1.20.6",
|
||||||
"react-native-reanimated": "~4.1.1",
|
"react-native-reanimated": "~4.1.1",
|
||||||
"react-native-safe-area-context": "~5.6.0",
|
"react-native-safe-area-context": "~5.6.0",
|
||||||
"react-native-screens": "~4.16.0",
|
"react-native-screens": "~4.16.0",
|
||||||
|
|||||||
@ -5,12 +5,29 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// Julia Token Server (dedicated endpoint for LiveKit tokens)
|
// Julia Token Server (dedicated endpoint for LiveKit tokens)
|
||||||
|
// Production: Use remote Julia Token Server
|
||||||
const JULIA_TOKEN_SERVER = 'https://wellnuo.smartlaunchhub.com/julia';
|
const JULIA_TOKEN_SERVER = 'https://wellnuo.smartlaunchhub.com/julia';
|
||||||
|
|
||||||
// Voice configuration
|
// Voice configuration
|
||||||
export const VOICE_ID = 'Asteria';
|
export const VOICE_ID = 'Asteria';
|
||||||
export const VOICE_NAME = 'Asteria';
|
export const VOICE_NAME = 'Asteria';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// SINGLE_DEPLOYMENT_MODE
|
||||||
|
// When true: sends only deploymentId (no beneficiaryNamesDict)
|
||||||
|
// When false: sends both deploymentId AND beneficiaryNamesDict
|
||||||
|
//
|
||||||
|
// Use true for WellNuo Lite (single beneficiary per user)
|
||||||
|
// Use false for full WellNuo app (multiple beneficiaries)
|
||||||
|
// ============================================================================
|
||||||
|
export const SINGLE_DEPLOYMENT_MODE = true;
|
||||||
|
|
||||||
|
// Beneficiary data to pass to voice agent
|
||||||
|
export interface BeneficiaryData {
|
||||||
|
deploymentId: string;
|
||||||
|
beneficiaryNamesDict: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
// API Response types
|
// API Response types
|
||||||
export interface LiveKitTokenResponse {
|
export interface LiveKitTokenResponse {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
@ -25,10 +42,37 @@ export interface LiveKitTokenResponse {
|
|||||||
/**
|
/**
|
||||||
* Get a LiveKit access token from Julia Token Server
|
* Get a LiveKit access token from Julia Token Server
|
||||||
* No authentication required - token server is dedicated for voice AI
|
* No authentication required - token server is dedicated for voice AI
|
||||||
|
* @param userId - User identifier
|
||||||
|
* @param beneficiaryData - Optional beneficiary data to pass to voice agent
|
||||||
*/
|
*/
|
||||||
export async function getToken(userId: string): Promise<LiveKitTokenResponse> {
|
export async function getToken(
|
||||||
|
userId: string,
|
||||||
|
beneficiaryData?: BeneficiaryData
|
||||||
|
): Promise<LiveKitTokenResponse> {
|
||||||
try {
|
try {
|
||||||
console.log('[LiveKit] Getting token for user:', userId);
|
console.log('[LiveKit] Getting token for user:', userId);
|
||||||
|
console.log('[LiveKit] SINGLE_DEPLOYMENT_MODE:', SINGLE_DEPLOYMENT_MODE);
|
||||||
|
|
||||||
|
// Prepare request body based on SINGLE_DEPLOYMENT_MODE
|
||||||
|
let requestBody: { userId: string; beneficiaryData?: BeneficiaryData };
|
||||||
|
|
||||||
|
if (SINGLE_DEPLOYMENT_MODE && beneficiaryData) {
|
||||||
|
// In single deployment mode: send only deploymentId, no beneficiaryNamesDict
|
||||||
|
requestBody = {
|
||||||
|
userId,
|
||||||
|
beneficiaryData: {
|
||||||
|
deploymentId: beneficiaryData.deploymentId,
|
||||||
|
beneficiaryNamesDict: {}, // Empty - no list of names
|
||||||
|
},
|
||||||
|
};
|
||||||
|
console.log('[LiveKit] Single deployment mode - sending only deploymentId:', beneficiaryData.deploymentId);
|
||||||
|
} else {
|
||||||
|
// Full mode: send everything
|
||||||
|
requestBody = { userId, beneficiaryData };
|
||||||
|
if (beneficiaryData) {
|
||||||
|
console.log('[LiveKit] Full mode - sending beneficiary data:', beneficiaryData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Request LiveKit token from Julia Token Server
|
// Request LiveKit token from Julia Token Server
|
||||||
const response = await fetch(`${JULIA_TOKEN_SERVER}/token`, {
|
const response = await fetch(`${JULIA_TOKEN_SERVER}/token`, {
|
||||||
@ -36,7 +80,7 @@ export async function getToken(userId: string): Promise<LiveKitTokenResponse> {
|
|||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ userId }),
|
body: JSON.stringify(requestBody),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
|||||||
@ -50,30 +50,76 @@ export async function configureAudioForVoiceCall(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (Platform.OS === 'ios') {
|
if (Platform.OS === 'ios') {
|
||||||
// iOS-specific configuration
|
// iOS-specific configuration with fallback strategies
|
||||||
console.log('[AudioSession] Step 1: Setting Apple audio config...');
|
// Try multiple configurations in order of preference
|
||||||
await AudioSession.setAppleAudioConfiguration({
|
|
||||||
audioCategory: 'playAndRecord',
|
|
||||||
// Note: removed 'allowBluetoothA2DP' - it's incompatible with playAndRecord
|
|
||||||
// on some iOS versions and causes "status -50" error.
|
|
||||||
// 'allowBluetooth' (HFP profile) is sufficient for voice calls.
|
|
||||||
audioCategoryOptions: [
|
|
||||||
'allowBluetooth',
|
|
||||||
'defaultToSpeaker',
|
|
||||||
'mixWithOthers',
|
|
||||||
],
|
|
||||||
audioMode: 'voiceChat',
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('[AudioSession] Step 2: Setting default output...');
|
const configs = [
|
||||||
await AudioSession.configureAudio({
|
// Strategy 1: videoChat mode (speaker by default, no problematic options)
|
||||||
ios: {
|
{
|
||||||
defaultOutput: 'speaker',
|
name: 'videoChat',
|
||||||
|
config: {
|
||||||
|
audioCategory: 'playAndRecord',
|
||||||
|
audioCategoryOptions: ['allowBluetooth', 'mixWithOthers'],
|
||||||
|
audioMode: 'videoChat',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
// Strategy 2: voiceChat mode (more compatible, but earpiece by default)
|
||||||
|
{
|
||||||
|
name: 'voiceChat',
|
||||||
|
config: {
|
||||||
|
audioCategory: 'playAndRecord',
|
||||||
|
audioCategoryOptions: ['allowBluetooth', 'mixWithOthers'],
|
||||||
|
audioMode: 'voiceChat',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Strategy 3: Minimal config (most compatible)
|
||||||
|
{
|
||||||
|
name: 'minimal',
|
||||||
|
config: {
|
||||||
|
audioCategory: 'playAndRecord',
|
||||||
|
audioCategoryOptions: [],
|
||||||
|
audioMode: 'default',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
console.log('[AudioSession] Step 3: Starting audio session...');
|
let configSuccess = false;
|
||||||
|
let lastError: any = null;
|
||||||
|
|
||||||
|
for (const { name, config } of configs) {
|
||||||
|
try {
|
||||||
|
console.log(`[AudioSession] Trying ${name} configuration...`);
|
||||||
|
await AudioSession.setAppleAudioConfiguration(config);
|
||||||
|
console.log(`[AudioSession] ${name} configuration succeeded!`);
|
||||||
|
configSuccess = true;
|
||||||
|
break;
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(`[AudioSession] ${name} config failed:`, err);
|
||||||
|
lastError = err;
|
||||||
|
// Continue to next strategy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!configSuccess) {
|
||||||
|
console.error('[AudioSession] All iOS configurations failed!');
|
||||||
|
throw lastError || new Error('All audio configurations failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[AudioSession] Starting audio session...');
|
||||||
await AudioSession.startAudioSession();
|
await AudioSession.startAudioSession();
|
||||||
|
|
||||||
|
// Try to set speaker output (non-critical, don't throw on failure)
|
||||||
|
try {
|
||||||
|
console.log('[AudioSession] Setting default output to speaker...');
|
||||||
|
await AudioSession.configureAudio({
|
||||||
|
ios: {
|
||||||
|
defaultOutput: 'speaker',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (outputErr) {
|
||||||
|
console.warn('[AudioSession] Could not set speaker output:', outputErr);
|
||||||
|
// Continue anyway - audio will work, just maybe on earpiece
|
||||||
|
}
|
||||||
} else if (Platform.OS === 'android') {
|
} else if (Platform.OS === 'android') {
|
||||||
// Android-specific configuration
|
// Android-specific configuration
|
||||||
// IMPORTANT: Using 'music' stream type to force output to speaker
|
// IMPORTANT: Using 'music' stream type to force output to speaker
|
||||||
@ -154,18 +200,15 @@ export async function reconfigureAudioForPlayback(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (Platform.OS === 'ios') {
|
if (Platform.OS === 'ios') {
|
||||||
// Just reconfigure the same settings - this "refreshes" the audio routing
|
// Reconfigure with same safe settings - this "refreshes" the audio routing
|
||||||
await AudioSession.setAppleAudioConfiguration({
|
await AudioSession.setAppleAudioConfiguration({
|
||||||
audioCategory: 'playAndRecord',
|
audioCategory: 'playAndRecord',
|
||||||
// Note: removed 'allowBluetoothA2DP' - it's incompatible with playAndRecord
|
|
||||||
// on some iOS versions and causes "status -50" error.
|
|
||||||
// 'allowBluetooth' (HFP profile) is sufficient for voice calls.
|
|
||||||
audioCategoryOptions: [
|
audioCategoryOptions: [
|
||||||
'allowBluetooth',
|
'allowBluetooth',
|
||||||
'defaultToSpeaker',
|
|
||||||
'mixWithOthers',
|
'mixWithOthers',
|
||||||
],
|
],
|
||||||
audioMode: 'voiceChat',
|
// Use 'videoChat' - defaults to speaker
|
||||||
|
audioMode: 'videoChat',
|
||||||
});
|
});
|
||||||
} else if (Platform.OS === 'android') {
|
} else if (Platform.OS === 'android') {
|
||||||
// Reconfigure Android audio to ensure speaker output
|
// Reconfigure Android audio to ensure speaker output
|
||||||
@ -209,22 +252,20 @@ export async function setAudioOutput(useSpeaker: boolean): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (Platform.OS === 'ios') {
|
if (Platform.OS === 'ios') {
|
||||||
// iOS: Configure audio output
|
// iOS: Update configuration based on desired output
|
||||||
|
// Use 'videoChat' mode for speaker, 'voiceChat' for earpiece
|
||||||
|
await AudioSession.setAppleAudioConfiguration({
|
||||||
|
audioCategory: 'playAndRecord',
|
||||||
|
audioCategoryOptions: ['allowBluetooth', 'mixWithOthers'],
|
||||||
|
audioMode: useSpeaker ? 'videoChat' : 'voiceChat',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Also set default output
|
||||||
await AudioSession.configureAudio({
|
await AudioSession.configureAudio({
|
||||||
ios: {
|
ios: {
|
||||||
defaultOutput: useSpeaker ? 'speaker' : 'earpiece',
|
defaultOutput: useSpeaker ? 'speaker' : 'earpiece',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Also update the full configuration to ensure it takes effect
|
|
||||||
// Note: removed 'allowBluetoothA2DP' - causes "status -50" error
|
|
||||||
await AudioSession.setAppleAudioConfiguration({
|
|
||||||
audioCategory: 'playAndRecord',
|
|
||||||
audioCategoryOptions: useSpeaker
|
|
||||||
? ['allowBluetooth', 'defaultToSpeaker', 'mixWithOthers']
|
|
||||||
: ['allowBluetooth', 'mixWithOthers'],
|
|
||||||
audioMode: 'voiceChat',
|
|
||||||
});
|
|
||||||
} else if (Platform.OS === 'android') {
|
} else if (Platform.OS === 'android') {
|
||||||
// Android: Switch stream type to control speaker/earpiece
|
// Android: Switch stream type to control speaker/earpiece
|
||||||
// - 'music' stream goes to speaker by default
|
// - 'music' stream goes to speaker by default
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user