WIP: LiveKit voice call integration with Julia AI agent

NOT TESTED ON REAL DEVICE - simulator only verification

Components:
- LiveKit Cloud agent deployment (julia-agent/julia-ai/)
- React Native LiveKit client (hooks/useLiveKitRoom.ts)
- Voice call screen with audio session management
- WellNuo voice_ask API integration in Python agent

Tech stack:
- LiveKit Cloud for agent hosting
- @livekit/react-native SDK
- Deepgram STT/TTS (via LiveKit Cloud)
- Silero VAD for voice activity detection

Known issues:
- Microphone permissions may need manual testing on real device
- LiveKit audio playback not verified on physical hardware
- Agent greeting audio not confirmed working end-to-end

Next steps:
- Test on physical iOS device
- Verify microphone capture works
- Confirm TTS audio playback
- Test full conversation loop
This commit is contained in:
Sergei 2026-01-18 20:16:25 -08:00
parent 7525bdc0f8
commit 059bc29b6b
16 changed files with 1296 additions and 1272 deletions

View File

@ -2,7 +2,7 @@
"expo": { "expo": {
"name": "WellNuo", "name": "WellNuo",
"slug": "WellNuo", "slug": "WellNuo",
"version": "1.0.2", "version": "1.0.3",
"orientation": "portrait", "orientation": "portrait",
"icon": "./assets/images/icon.png", "icon": "./assets/images/icon.png",
"scheme": "wellnuo", "scheme": "wellnuo",

View File

@ -53,7 +53,7 @@ export default function TabLayout() {
href: null, href: null,
}} }}
/> />
{/* Voice Debug hidden - using debug tab instead */} {/* Voice tab hidden - using Debug for testing */}
<Tabs.Screen <Tabs.Screen
name="voice" name="voice"
options={{ options={{

View File

@ -26,6 +26,9 @@ 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 Constants from 'expo-constants';
const APP_VERSION = Constants.expoConfig?.version ?? '?.?.?';
type LogEntry = { type LogEntry = {
id: string; id: string;
@ -166,14 +169,14 @@ export default function DebugScreen() {
// Step 5: Create room and setup listeners // Step 5: Create room and setup listeners
log('Step 5: Creating Room instance...', 'info'); log('Step 5: Creating Room instance...', 'info');
const room = new Room(); const newRoom = new Room();
roomRef.current = room; roomRef.current = newRoom;
log('Room instance created', 'success'); log('Room instance created', 'success');
// Setup ALL event listeners // Setup ALL event listeners
log('Step 6: Setting up event listeners...', 'info'); log('Step 6: Setting up event listeners...', 'info');
room.on(RoomEvent.ConnectionStateChanged, (state: any) => { newRoom.on(RoomEvent.ConnectionStateChanged, (state: any) => {
log(`EVENT: ConnectionStateChanged → ${state}`, 'event'); log(`EVENT: ConnectionStateChanged → ${state}`, 'event');
if (state === ConnectionState.Connected) { if (state === ConnectionState.Connected) {
setCallState('connected'); setCallState('connected');
@ -183,56 +186,56 @@ export default function DebugScreen() {
} }
}); });
room.on(RoomEvent.Connected, () => { newRoom.on(RoomEvent.Connected, () => {
log('EVENT: Connected to room', 'success'); log('EVENT: Connected to room', 'success');
}); });
room.on(RoomEvent.Disconnected, (reason?: any) => { newRoom.on(RoomEvent.Disconnected, (reason?: any) => {
log(`EVENT: Disconnected. Reason: ${reason || 'unknown'}`, 'event'); log(`EVENT: Disconnected. Reason: ${reason || 'unknown'}`, 'event');
}); });
room.on(RoomEvent.Reconnecting, () => { newRoom.on(RoomEvent.Reconnecting, () => {
log('EVENT: Reconnecting...', 'event'); log('EVENT: Reconnecting...', 'event');
}); });
room.on(RoomEvent.Reconnected, () => { newRoom.on(RoomEvent.Reconnected, () => {
log('EVENT: Reconnected', 'success'); log('EVENT: Reconnected', 'success');
}); });
room.on(RoomEvent.ParticipantConnected, (participant: any) => { newRoom.on(RoomEvent.ParticipantConnected, (participant: any) => {
log(`EVENT: Participant connected: ${participant.identity}`, 'event'); log(`EVENT: Participant connected: ${participant.identity}`, 'event');
}); });
room.on(RoomEvent.ParticipantDisconnected, (participant: any) => { newRoom.on(RoomEvent.ParticipantDisconnected, (participant: any) => {
log(`EVENT: Participant disconnected: ${participant.identity}`, 'event'); log(`EVENT: Participant disconnected: ${participant.identity}`, 'event');
}); });
room.on(RoomEvent.TrackSubscribed, (track: any, publication: any, participant: any) => { newRoom.on(RoomEvent.TrackSubscribed, (track: any, publication: any, participant: any) => {
log(`EVENT: Track subscribed: ${track.kind} from ${participant.identity}`, 'event'); log(`EVENT: Track subscribed: ${track.kind} from ${participant.identity}`, 'event');
if (track.kind === Track.Kind.Audio) { if (track.kind === Track.Kind.Audio) {
log('Audio track from Julia AI - should hear voice now', 'success'); log('Audio track from Julia AI - should hear voice now', 'success');
} }
}); });
room.on(RoomEvent.TrackUnsubscribed, (track: any, publication: any, participant: any) => { newRoom.on(RoomEvent.TrackUnsubscribed, (track: any, publication: any, participant: any) => {
log(`EVENT: Track unsubscribed: ${track.kind} from ${participant.identity}`, 'event'); log(`EVENT: Track unsubscribed: ${track.kind} from ${participant.identity}`, 'event');
}); });
room.on(RoomEvent.TrackMuted, (publication: any, participant: any) => { newRoom.on(RoomEvent.TrackMuted, (publication: any, participant: any) => {
log(`EVENT: Track muted by ${participant.identity}`, 'event'); log(`EVENT: Track muted by ${participant.identity}`, 'event');
}); });
room.on(RoomEvent.TrackUnmuted, (publication: any, participant: any) => { newRoom.on(RoomEvent.TrackUnmuted, (publication: any, participant: any) => {
log(`EVENT: Track unmuted by ${participant.identity}`, 'event'); log(`EVENT: Track unmuted by ${participant.identity}`, 'event');
}); });
room.on(RoomEvent.ActiveSpeakersChanged, (speakers: any[]) => { newRoom.on(RoomEvent.ActiveSpeakersChanged, (speakers: any[]) => {
if (speakers.length > 0) { if (speakers.length > 0) {
log(`EVENT: Active speakers: ${speakers.map(s => s.identity).join(', ')}`, 'event'); log(`EVENT: Active speakers: ${speakers.map(s => s.identity).join(', ')}`, 'event');
} }
}); });
room.on(RoomEvent.DataReceived, (payload: any, participant: any) => { newRoom.on(RoomEvent.DataReceived, (payload: any, participant: any) => {
try { try {
const data = JSON.parse(new TextDecoder().decode(payload)); const data = JSON.parse(new TextDecoder().decode(payload));
log(`EVENT: Data received: ${JSON.stringify(data).substring(0, 100)}`, 'event'); log(`EVENT: Data received: ${JSON.stringify(data).substring(0, 100)}`, 'event');
@ -241,15 +244,15 @@ export default function DebugScreen() {
} }
}); });
room.on(RoomEvent.AudioPlaybackStatusChanged, () => { newRoom.on(RoomEvent.AudioPlaybackStatusChanged, () => {
log(`EVENT: AudioPlaybackStatusChanged - canPlay: ${room.canPlaybackAudio}`, 'event'); log(`EVENT: AudioPlaybackStatusChanged - canPlay: ${newRoom.canPlaybackAudio}`, 'event');
}); });
room.on(RoomEvent.MediaDevicesError, (error: any) => { newRoom.on(RoomEvent.MediaDevicesError, (error: any) => {
log(`EVENT: MediaDevicesError: ${error?.message || error}`, 'error'); log(`EVENT: MediaDevicesError: ${error?.message || error}`, 'error');
}); });
room.on(RoomEvent.RoomMetadataChanged, (metadata: string) => { newRoom.on(RoomEvent.RoomMetadataChanged, (metadata: string) => {
log(`EVENT: RoomMetadataChanged: ${metadata}`, 'event'); log(`EVENT: RoomMetadataChanged: ${metadata}`, 'event');
}); });
@ -257,15 +260,46 @@ export default function DebugScreen() {
// 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');
await room.connect(wsUrl, token, { autoSubscribe: true }); await newRoom.connect(wsUrl, token, { autoSubscribe: true });
log('Connected to room', 'success'); log('Connected to room', 'success');
// Step 7.5: Start audio playback (required for iOS)
log('Step 7.5: Starting audio playback...', 'info');
await newRoom.startAudio();
log(`Audio playback started, canPlay: ${newRoom.canPlaybackAudio}`, 'success');
// Step 8: Enable microphone // Step 8: Enable microphone
log('Step 8: Enabling microphone...', 'info'); log('Step 8: Enabling microphone...', 'info');
await room.localParticipant.setMicrophoneEnabled(true); await newRoom.localParticipant.setMicrophoneEnabled(true);
log('Microphone enabled', 'success'); log('Microphone enabled', 'success');
log(`Local participant: ${room.localParticipant.identity}`, 'info'); // Step 9: Log local audio track info
log('Step 9: Checking local audio track...', 'info');
const localAudioTracks = newRoom.localParticipant.audioTrackPublications;
log(`Local audio publications: ${localAudioTracks.size}`, 'info');
localAudioTracks.forEach((pub: any) => {
log(`Local audio track: ${pub.trackSid}, muted: ${pub.isMuted}, source: ${pub.source}`, 'info');
if (pub.track) {
log(`Track mediaStreamTrack: ${pub.track.mediaStreamTrack ? 'exists' : 'NULL'}`, 'info');
log(`Track enabled: ${pub.track.mediaStreamTrack?.enabled}`, 'info');
}
});
// Listen for local track published
newRoom.localParticipant.on('localTrackPublished', (pub: any) => {
log(`MY TRACK PUBLISHED: ${pub.kind} sid=${pub.trackSid}`, 'success');
});
// Listen when I become an active speaker (means mic is working)
newRoom.on(RoomEvent.ActiveSpeakersChanged, (speakers: any[]) => {
const iAmSpeaking = speakers.some(s => s.identity === newRoom.localParticipant.identity);
if (iAmSpeaking) {
log(`*** I AM SPEAKING - MIC WORKS ***`, 'success');
}
});
log(`Local participant: ${newRoom.localParticipant.identity}`, 'info');
log('=== CALL ACTIVE ===', 'success'); log('=== CALL ACTIVE ===', 'success');
} catch (err: any) { } catch (err: any) {
@ -330,7 +364,10 @@ export default function DebugScreen() {
<SafeAreaView style={styles.container} edges={['top']}> <SafeAreaView style={styles.container} edges={['top']}>
{/* Header */} {/* Header */}
<View style={styles.header}> <View style={styles.header}>
<View style={styles.headerRow}>
<Text style={styles.title}>Voice Debug</Text> <Text style={styles.title}>Voice Debug</Text>
<Text style={styles.versionBadge}>v{APP_VERSION}</Text>
</View>
<Text style={styles.subtitle}>{VOICE_NAME}</Text> <Text style={styles.subtitle}>{VOICE_NAME}</Text>
</View> </View>
@ -418,11 +455,26 @@ const styles = StyleSheet.create({
borderBottomWidth: 1, borderBottomWidth: 1,
borderBottomColor: '#333', borderBottomColor: '#333',
}, },
headerRow: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
title: { title: {
fontSize: 24, fontSize: 24,
fontWeight: '700', fontWeight: '700',
color: '#fff', color: '#fff',
}, },
versionBadge: {
fontSize: 14,
fontWeight: '600',
color: '#22c55e',
backgroundColor: 'rgba(34, 197, 94, 0.15)',
paddingHorizontal: 10,
paddingVertical: 4,
borderRadius: 8,
overflow: 'hidden',
},
subtitle: { subtitle: {
fontSize: 14, fontSize: 14,
color: '#888', color: '#888',

View File

@ -1,7 +1,5 @@
// CRITICAL: Import LiveKit globals FIRST before anything else! // WebRTC globals are now registered in useLiveKitRoom hook
// This must be the very first import to set up WebRTC globals
// before any LiveKit classes are loaded. // before any LiveKit classes are loaded.
import '@/polyfills/livekit-globals';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native'; import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native';

View File

@ -1,12 +1,20 @@
/** /**
* Voice Call Screen - Fullscreen LiveKit Voice Call * Voice Call Screen - Fullscreen LiveKit Voice Call with Julia AI
* *
* Opens as a modal from chat, returns to chat when call ends. * ARCHITECTURE:
* Beautiful phone call-like UI with Julia AI. * - ALL LiveKit/WebRTC logic is in useLiveKitRoom hook
* Uses self-hosted LiveKit Server + Deepgram STT/TTS. * - This component ONLY handles UI rendering
* - No direct LiveKit imports here!
*
* Features:
* - Phone call-like UI with Julia avatar
* - Call duration timer
* - Mute/unmute
* - Debug logs panel (collapsible)
* - Proper cleanup on unmount
*/ */
import React, { useState, useCallback, useRef, useEffect } from 'react'; import React, { useEffect, useRef } from 'react';
import { import {
View, View,
Text, Text,
@ -18,381 +26,77 @@ import {
Dimensions, Dimensions,
ScrollView, ScrollView,
Alert, Alert,
AppState,
AppStateStatus,
} from 'react-native'; } from 'react-native';
import * as Clipboard from 'expo-clipboard'; import * as Clipboard from 'expo-clipboard';
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 { activateKeepAwakeAsync, deactivateKeepAwake } from 'expo-keep-awake';
// NOTE: Room and other core classes must be imported from livekit-client, not @livekit/react-native!
// @livekit/react-native only provides registerGlobals(), React hooks, and components.
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 { VOICE_NAME } from '@/services/livekitService';
import { useVoiceTranscript } from '@/contexts/VoiceTranscriptContext'; import { useVoiceTranscript } from '@/contexts/VoiceTranscriptContext';
import { debugLogger } from '@/services/DebugLogger'; import { useLiveKitRoom, ConnectionState } from '@/hooks/useLiveKitRoom';
// Polyfill Event class for React Native (livekit-client needs it)
if (typeof global.Event === 'undefined') {
(global as any).Event = class Event {
type: string;
bubbles: boolean;
cancelable: boolean;
defaultPrevented: boolean;
constructor(type: string, options?: { bubbles?: boolean; cancelable?: boolean }) {
this.type = type;
this.bubbles = options?.bubbles ?? false;
this.cancelable = options?.cancelable ?? false;
this.defaultPrevented = false;
}
preventDefault() {
this.defaultPrevented = true;
}
stopPropagation() {}
stopImmediatePropagation() {}
};
}
const { width: SCREEN_WIDTH } = Dimensions.get('window'); const { width: SCREEN_WIDTH } = Dimensions.get('window');
type CallState = 'connecting' | 'active' | 'ending';
export default function VoiceCallScreen() { export default function VoiceCallScreen() {
const router = useRouter(); const router = useRouter();
const { addTranscriptEntry, clearTranscript } = useVoiceTranscript(); const { clearTranscript, addTranscriptEntry } = useVoiceTranscript();
// Call state // Debug logs panel state
const [callState, setCallState] = useState<CallState>('connecting'); const [showLogs, setShowLogs] = React.useState(false);
const [isMuted, setIsMuted] = useState(false); const [logsMinimized, setLogsMinimized] = React.useState(false);
const [callDuration, setCallDuration] = useState(0);
const [statusText, setStatusText] = useState('Connecting...');
const callStartTimeRef = useRef<number | null>(null);
// Debug logs
const [logs, setLogs] = useState<string[]>([]);
const [showLogs, setShowLogs] = useState(false);
const [logsMinimized, setLogsMinimized] = useState(false);
const logsScrollRef = useRef<ScrollView>(null); const logsScrollRef = useRef<ScrollView>(null);
// Add log entry - both local and global // LiveKit hook - ALL logic is here
const addLog = useCallback((message: string) => { const {
const timestamp = new Date().toLocaleTimeString('en-US', { hour12: false }); state,
setLogs(prev => [...prev, `[${timestamp}] ${message}`]); error,
// Also send to global debug logger so it shows on Debug tab roomName,
debugLogger.info('VOICE', message); callDuration,
}, []); isMuted,
isAgentSpeaking,
// Copy logs to clipboard canPlayAudio,
const copyLogs = useCallback(async () => { logs,
const logsText = logs.join('\n'); participantCount,
await Clipboard.setStringAsync(logsText); connect,
Alert.alert('Copied!', `${logs.length} log entries copied to clipboard`); disconnect,
}, [logs]); toggleMute,
clearLogs,
// LiveKit room reference } = useLiveKitRoom({
const roomRef = useRef<RoomType | null>(null); userId: `user-${Date.now()}`,
const isUnmountingRef = useRef(false); onTranscript: (role, text) => {
const connectionIdRef = useRef<number>(0); addTranscriptEntry(role, text);
},
});
// Animations // Animations
const pulseAnim = useRef(new Animated.Value(1)).current; const pulseAnim = useRef(new Animated.Value(1)).current;
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;
// Background state tracking // Clear transcript and start call on mount
const appStateRef = useRef<AppStateStatus>(AppState.currentState);
// Keep screen awake during call & handle background mode
useEffect(() => { useEffect(() => {
// Prevent screen from sleeping during call
activateKeepAwakeAsync('voiceCall').catch(() => {});
// Handle app going to background/foreground
const handleAppStateChange = (nextAppState: AppStateStatus) => {
const prevState = appStateRef.current;
appStateRef.current = nextAppState;
if (prevState.match(/inactive|background/) && nextAppState === 'active') {
// App came back to foreground
addLog('App returned to foreground');
} else if (prevState === 'active' && nextAppState.match(/inactive|background/)) {
// App went to background - DON'T disconnect, keep call alive!
addLog('App went to background - call continues');
// The UIBackgroundModes: ["audio", "voip"] in app.json keeps audio alive
}
};
const subscription = AppState.addEventListener('change', handleAppStateChange);
return () => {
subscription.remove();
deactivateKeepAwake('voiceCall');
};
}, [addLog]);
// Start call on mount
useEffect(() => {
// Track current connection attempt
const currentConnectionId = ++connectionIdRef.current;
isUnmountingRef.current = false;
const startCall = async () => {
try {
// Clear previous transcript before starting new call
clearTranscript(); clearTranscript();
connect();
addLog('Starting voice call...');
// Check if unmounting
if (isUnmountingRef.current || currentConnectionId !== connectionIdRef.current) {
addLog('Aborted: screen is closing');
return;
}
// CRITICAL: Ensure WebRTC globals are registered BEFORE importing livekit-client
// This MUST happen first, otherwise Room class won't work
const { registerGlobals, AudioSession } = await import('@livekit/react-native');
// Check if globals already registered, if not - register them
if (typeof global.RTCPeerConnection === 'undefined') {
addLog('Registering WebRTC globals...');
registerGlobals();
} else {
addLog('WebRTC globals already registered');
}
// Check again if unmounting after async import
if (isUnmountingRef.current || currentConnectionId !== connectionIdRef.current) {
addLog('Aborted: screen is closing');
return;
}
// NOW it's safe to import livekit-client
addLog('Importing livekit-client...');
const {
Room,
RoomEvent,
ConnectionState,
Track,
} = await import('livekit-client');
addLog(`Room class: ${typeof Room} ${Room ? 'OK' : 'MISSING'}`);
addLog('LiveKit imported successfully');
// Check if unmounting
if (isUnmountingRef.current || currentConnectionId !== connectionIdRef.current) {
addLog('Aborted: screen is closing');
return;
}
// Configure iOS audio session
if (Platform.OS === 'ios') {
addLog('Starting iOS AudioSession...');
await AudioSession.startAudioSession();
addLog('iOS AudioSession started');
}
// Get token from our server
addLog('Requesting token from server...');
const result = await getToken(`user-${Date.now()}`);
// Check if unmounting after token request
if (isUnmountingRef.current || currentConnectionId !== connectionIdRef.current) {
addLog('Aborted: screen is closing after token request');
return;
}
if (!result.success || !result.data) {
throw new Error(result.error || 'Failed to get token');
}
const { token, wsUrl, roomName } = result.data;
addLog(`Token received. Room: ${roomName}`);
addLog(`WebSocket URL: ${wsUrl}`);
addLog(`Connecting to room: ${roomName}`);
// Create and connect to room
const room = new Room();
roomRef.current = room;
// Setup event listeners
room.on(RoomEvent.ConnectionStateChanged, (state: typeof ConnectionState[keyof typeof ConnectionState]) => {
addLog(`Connection state: ${state}`);
switch (state) {
case ConnectionState.Connecting:
setCallState('connecting');
setStatusText('Connecting...');
break;
case ConnectionState.Connected:
setCallState('active');
setStatusText('Connected');
if (!callStartTimeRef.current) {
callStartTimeRef.current = Date.now();
}
break;
case ConnectionState.Reconnecting:
setStatusText('Reconnecting...');
break;
case ConnectionState.Disconnected:
setCallState('ending');
setStatusText('Disconnected');
// Go back when disconnected
setTimeout(() => router.back(), 500);
break;
}
});
room.on(RoomEvent.TrackSubscribed, (track: any, publication: any, participant: any) => {
addLog(`Track subscribed: ${track.kind} from ${participant.identity}`);
if (track.kind === Track.Kind.Audio) {
addLog('Audio track received - Julia should be speaking');
setStatusText('Julia is speaking...');
}
});
room.on(RoomEvent.TrackUnsubscribed, (track: any, publication: any, participant: any) => {
addLog(`Track unsubscribed: ${track.kind}`);
});
room.on(RoomEvent.TrackMuted, (publication: any, participant: any) => {
addLog(`Track muted: ${publication.trackSid} by ${participant.identity}`);
});
room.on(RoomEvent.TrackUnmuted, (publication: any, participant: any) => {
addLog(`Track unmuted: ${publication.trackSid} by ${participant.identity}`);
});
room.on(RoomEvent.ParticipantConnected, (participant: any) => {
addLog(`Participant connected: ${participant.identity}`);
});
room.on(RoomEvent.ParticipantDisconnected, (participant: any) => {
addLog(`Participant disconnected: ${participant.identity}`);
});
room.on(RoomEvent.ActiveSpeakersChanged, (speakers: any[]) => {
if (speakers.length > 0) {
addLog(`Active speakers: ${speakers.map((s: any) => s.identity).join(', ')}`);
}
});
room.on(RoomEvent.DataReceived, (payload: any, participant: any) => {
try {
const data = JSON.parse(new TextDecoder().decode(payload));
addLog(`Data received: ${JSON.stringify(data).substring(0, 100)}`);
// Handle transcript data from agent
if (data.type === 'transcript') {
if (data.role === 'user' && data.text) {
addTranscriptEntry('user', data.text);
} else if (data.role === 'assistant' && data.text) {
addTranscriptEntry('assistant', data.text);
}
}
} catch (e) {
// Ignore non-JSON data
}
});
room.on(RoomEvent.AudioPlaybackStatusChanged, () => {
addLog(`Audio playback can play: ${room.canPlaybackAudio}`);
});
// Check if unmounting before connecting
if (isUnmountingRef.current || currentConnectionId !== connectionIdRef.current) {
addLog('Aborted: screen is closing before connect');
return;
}
// Connect to room
await room.connect(wsUrl, token, {
autoSubscribe: true,
});
// Check if unmounting after connect
if (isUnmountingRef.current || currentConnectionId !== connectionIdRef.current) {
addLog('Aborted: screen is closing after connect, disconnecting...');
await room.disconnect().catch(() => {});
return;
}
// Enable microphone
await room.localParticipant.setMicrophoneEnabled(true);
addLog('Connected and microphone enabled');
addLog(`Local participant: ${room.localParticipant.identity}`);
} catch (err: any) {
// Ignore errors if screen is unmounting (expected race condition)
if (isUnmountingRef.current || currentConnectionId !== connectionIdRef.current) {
console.log('[VoiceCall] Error ignored (screen closing):', err?.message);
return;
}
// Detailed error logging for debugging
console.error('[VoiceCall] Failed to start call:', err);
console.error('[VoiceCall] Error name:', err?.name);
console.error('[VoiceCall] Error message:', err?.message);
console.error('[VoiceCall] Error stack:', err?.stack);
const errorMsg = err?.message || String(err);
setStatusText(`Error: ${errorMsg.substring(0, 50)}`);
// Go back on error
setTimeout(() => router.back(), 2000);
}
};
startCall();
// Cleanup on unmount
return () => { return () => {
isUnmountingRef.current = true; // Cleanup handled by the hook
const cleanup = async () => {
if (roomRef.current) {
try {
await roomRef.current.disconnect();
} catch (e) {
// Ignore errors during cleanup
}
roomRef.current = null;
}
if (Platform.OS === 'ios') {
try {
const { AudioSession } = await import('@livekit/react-native');
await AudioSession.stopAudioSession();
} catch (e) {
// Ignore errors during cleanup
}
}
};
cleanup();
}; };
}, []); }, []);
// Call duration timer // Navigate back on disconnect or error
useEffect(() => { useEffect(() => {
if (callState !== 'active') return; if (state === 'disconnected' || state === 'error') {
const timeout = setTimeout(() => {
const interval = setInterval(() => { router.back();
if (callStartTimeRef.current) { }, state === 'error' ? 2000 : 500);
const elapsed = Math.floor((Date.now() - callStartTimeRef.current) / 1000); return () => clearTimeout(timeout);
setCallDuration(elapsed);
} }
}, 1000); }, [state, router]);
return () => clearInterval(interval);
}, [callState]);
// Pulse animation for active call // Pulse animation for active call
useEffect(() => { useEffect(() => {
if (callState === 'active') { if (state === 'connected') {
const pulse = Animated.loop( const pulse = Animated.loop(
Animated.sequence([ Animated.sequence([
Animated.timing(pulseAnim, { Animated.timing(pulseAnim, {
@ -421,11 +125,19 @@ export default function VoiceCallScreen() {
return () => pulse.stop(); return () => pulse.stop();
} }
}, [callState]); }, [state, pulseAnim, avatarScale]);
// Rotate animation for connecting // Rotate animation for connecting states
useEffect(() => { useEffect(() => {
if (callState === 'connecting') { const connectingStates: ConnectionState[] = [
'initializing',
'configuring_audio',
'requesting_token',
'connecting',
'reconnecting',
];
if (connectingStates.includes(state)) {
const rotate = Animated.loop( const rotate = Animated.loop(
Animated.timing(rotateAnim, { Animated.timing(rotateAnim, {
toValue: 1, toValue: 1,
@ -439,51 +151,69 @@ export default function VoiceCallScreen() {
} else { } else {
rotateAnim.setValue(0); rotateAnim.setValue(0);
} }
}, [callState]); }, [state, rotateAnim]);
// End call
const endCall = useCallback(async () => {
setCallState('ending');
setStatusText('Ending call...');
try {
if (roomRef.current) {
await roomRef.current.disconnect();
roomRef.current = null;
}
} catch (err) {
console.error('[VoiceCall] Error ending call:', err);
}
if (Platform.OS === 'ios') {
try {
const { AudioSession } = await import('@livekit/react-native');
await AudioSession.stopAudioSession();
await new Promise(resolve => setTimeout(resolve, 100));
} catch (err) {
console.error('[VoiceCall] Error stopping audio:', err);
}
}
// End call handler
const handleEndCall = async () => {
await disconnect();
router.back(); router.back();
}, [router]); };
// Toggle mute // Copy logs to clipboard
const toggleMute = useCallback(async () => { const copyLogs = async () => {
if (roomRef.current) { const logsText = logs.map(l => `[${l.timestamp}] ${l.message}`).join('\n');
const newMuted = !isMuted; await Clipboard.setStringAsync(logsText);
await roomRef.current.localParticipant.setMicrophoneEnabled(!newMuted); Alert.alert('Copied!', `${logs.length} log entries copied to clipboard`);
setIsMuted(newMuted); };
}
}, [isMuted]);
// Format duration // Format duration as MM:SS
const formatDuration = (seconds: number): string => { const formatDuration = (seconds: number): string => {
const mins = Math.floor(seconds / 60); const mins = Math.floor(seconds / 60);
const secs = seconds % 60; const secs = seconds % 60;
return `${mins}:${secs.toString().padStart(2, '0')}`; return `${mins}:${secs.toString().padStart(2, '0')}`;
}; };
// Get status text based on state
const getStatusText = (): string => {
switch (state) {
case 'idle':
return 'Starting...';
case 'initializing':
return 'Initializing...';
case 'configuring_audio':
return 'Configuring audio...';
case 'requesting_token':
return 'Requesting token...';
case 'connecting':
return 'Connecting...';
case 'connected':
if (isAgentSpeaking) return 'Julia is speaking...';
if (!canPlayAudio) return 'Waiting for audio...';
return 'Connected';
case 'reconnecting':
return 'Reconnecting...';
case 'disconnected':
return 'Disconnected';
case 'error':
return error || 'Error occurred';
default:
return 'Unknown state';
}
};
// Is call currently connecting?
const isConnecting = [
'idle',
'initializing',
'configuring_audio',
'requesting_token',
'connecting',
].includes(state);
// Is call active?
const isActive = state === 'connected';
// Rotation interpolation
const spin = rotateAnim.interpolate({ const spin = rotateAnim.interpolate({
inputRange: [0, 1], inputRange: [0, 1],
outputRange: ['0deg', '360deg'], outputRange: ['0deg', '360deg'],
@ -494,13 +224,16 @@ export default function VoiceCallScreen() {
{/* Background gradient effect */} {/* Background gradient effect */}
<View style={styles.backgroundGradient} /> <View style={styles.backgroundGradient} />
{/* Top bar with back button */} {/* Top bar */}
<View style={styles.topBar}> <View style={styles.topBar}>
<TouchableOpacity style={styles.backButton} onPress={endCall}> <TouchableOpacity style={styles.backButton} onPress={handleEndCall}>
<Ionicons name="chevron-down" size={28} color={AppColors.white} /> <Ionicons name="chevron-down" size={28} color={AppColors.white} />
</TouchableOpacity> </TouchableOpacity>
<View style={styles.topBarCenter}> <View style={styles.topBarCenter}>
<Text style={styles.encryptedText}>LiveKit + Deepgram</Text> <Text style={styles.encryptedText}>LiveKit + Deepgram</Text>
{roomName && (
<Text style={styles.roomNameText}>{roomName}</Text>
)}
</View> </View>
<TouchableOpacity <TouchableOpacity
style={styles.logsButton} style={styles.logsButton}
@ -522,36 +255,45 @@ export default function VoiceCallScreen() {
styles.avatarContainer, styles.avatarContainer,
{ {
transform: [ transform: [
{ scale: callState === 'active' ? pulseAnim : avatarScale }, { scale: isActive ? pulseAnim : avatarScale },
{ rotate: callState === 'connecting' ? spin : '0deg' } { rotate: isConnecting ? spin : '0deg' },
] ],
} },
]} ]}
> >
<View style={styles.avatar}> <View style={styles.avatar}>
<Text style={styles.avatarText}>J</Text> <Text style={styles.avatarText}>J</Text>
</View> </View>
{callState === 'active' && ( {isActive && <View style={styles.activeIndicator} />}
<View style={styles.activeIndicator} />
)}
</Animated.View> </Animated.View>
{/* Name and status */} {/* Name and status */}
<Text style={styles.name}>Julia AI</Text> <Text style={styles.name}>Julia AI</Text>
<Text style={styles.voiceName}>{VOICE_NAME} voice</Text> <Text style={styles.voiceName}>{VOICE_NAME} voice</Text>
{callState === 'active' ? ( {isActive ? (
<View style={styles.statusContainer}> <View style={styles.statusContainer}>
<View style={styles.activeDot} /> <View style={styles.activeDot} />
<Text style={styles.duration}>{formatDuration(callDuration)}</Text> <Text style={styles.duration}>{formatDuration(callDuration)}</Text>
</View> </View>
) : ( ) : (
<Text style={styles.status}>{statusText}</Text> <Text style={styles.status}>{getStatusText()}</Text>
)} )}
{/* Status indicator */} {/* Additional status info */}
{callState === 'active' && ( {isActive && (
<Text style={styles.listeningStatus}>{statusText}</Text> <Text style={styles.listeningStatus}>
{getStatusText()}
{participantCount > 1 && `${participantCount} participants`}
</Text>
)}
{/* Error display */}
{state === 'error' && error && (
<View style={styles.errorContainer}>
<Ionicons name="alert-circle" size={20} color={AppColors.error} />
<Text style={styles.errorText}>{error}</Text>
</View>
)} )}
</View> </View>
@ -569,11 +311,16 @@ export default function VoiceCallScreen() {
color={AppColors.white} color={AppColors.white}
/> />
</TouchableOpacity> </TouchableOpacity>
<Text style={styles.logsPanelTitle}>Logs ({logs.length})</Text> <Text style={styles.logsPanelTitle}>
Logs ({logs.length}) State: {state}
</Text>
<View style={styles.logsPanelButtons}> <View style={styles.logsPanelButtons}>
<TouchableOpacity style={styles.copyButton} onPress={copyLogs}> <TouchableOpacity style={styles.copyButton} onPress={copyLogs}>
<Ionicons name="copy-outline" size={16} color={AppColors.white} /> <Ionicons name="copy-outline" size={16} color={AppColors.white} />
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity style={styles.clearButton} onPress={clearLogs}>
<Ionicons name="trash-outline" size={16} color={AppColors.white} />
</TouchableOpacity>
<TouchableOpacity <TouchableOpacity
style={styles.closeLogsButton} style={styles.closeLogsButton}
onPress={() => setShowLogs(false)} onPress={() => setShowLogs(false)}
@ -589,7 +336,16 @@ export default function VoiceCallScreen() {
onContentSizeChange={() => logsScrollRef.current?.scrollToEnd()} onContentSizeChange={() => logsScrollRef.current?.scrollToEnd()}
> >
{logs.map((log, index) => ( {logs.map((log, index) => (
<Text key={index} style={styles.logEntry}>{log}</Text> <Text
key={index}
style={[
styles.logEntry,
log.level === 'error' && styles.logEntryError,
log.level === 'warn' && styles.logEntryWarn,
]}
>
[{log.timestamp}] {log.message}
</Text>
))} ))}
{logs.length === 0 && ( {logs.length === 0 && (
<Text style={styles.logEntryEmpty}>Waiting for events...</Text> <Text style={styles.logEntryEmpty}>Waiting for events...</Text>
@ -605,7 +361,7 @@ export default function VoiceCallScreen() {
<TouchableOpacity <TouchableOpacity
style={[styles.controlButton, isMuted && styles.controlButtonActive]} style={[styles.controlButton, isMuted && styles.controlButtonActive]}
onPress={toggleMute} onPress={toggleMute}
disabled={callState !== 'active'} disabled={!isActive}
> >
<Ionicons <Ionicons
name={isMuted ? 'mic-off' : 'mic'} name={isMuted ? 'mic-off' : 'mic'}
@ -616,15 +372,12 @@ export default function VoiceCallScreen() {
</TouchableOpacity> </TouchableOpacity>
{/* End call button */} {/* End call button */}
<TouchableOpacity <TouchableOpacity style={styles.endCallButton} onPress={handleEndCall}>
style={styles.endCallButton}
onPress={endCall}
>
<Ionicons name="call" size={32} color={AppColors.white} /> <Ionicons name="call" size={32} color={AppColors.white} />
</TouchableOpacity> </TouchableOpacity>
{/* Speaker button (placeholder) */} {/* Speaker button (placeholder for future) */}
<TouchableOpacity style={styles.controlButton}> <TouchableOpacity style={styles.controlButton} disabled>
<Ionicons name="volume-high" size={28} color={AppColors.white} /> <Ionicons name="volume-high" size={28} color={AppColors.white} />
<Text style={styles.controlLabel}>Speaker</Text> <Text style={styles.controlLabel}>Speaker</Text>
</TouchableOpacity> </TouchableOpacity>
@ -670,6 +423,11 @@ const styles = StyleSheet.create({
fontSize: FontSizes.xs, fontSize: FontSizes.xs,
color: 'rgba(255,255,255,0.5)', color: 'rgba(255,255,255,0.5)',
}, },
roomNameText: {
fontSize: 10,
color: 'rgba(255,255,255,0.3)',
marginTop: 2,
},
logsButton: { logsButton: {
width: 44, width: 44,
height: 44, height: 44,
@ -753,6 +511,18 @@ const styles = StyleSheet.create({
marginTop: Spacing.md, marginTop: Spacing.md,
fontStyle: 'italic', fontStyle: 'italic',
}, },
errorContainer: {
flexDirection: 'row',
alignItems: 'center',
marginTop: Spacing.md,
paddingHorizontal: Spacing.lg,
},
errorText: {
fontSize: FontSizes.sm,
color: AppColors.error,
marginLeft: Spacing.sm,
flex: 1,
},
controls: { controls: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-evenly', justifyContent: 'space-evenly',
@ -836,6 +606,11 @@ const styles = StyleSheet.create({
backgroundColor: 'rgba(255,255,255,0.15)', backgroundColor: 'rgba(255,255,255,0.15)',
borderRadius: BorderRadius.sm, borderRadius: BorderRadius.sm,
}, },
clearButton: {
padding: 6,
backgroundColor: 'rgba(255,255,255,0.15)',
borderRadius: BorderRadius.sm,
},
closeLogsButton: { closeLogsButton: {
padding: 6, padding: 6,
}, },
@ -849,6 +624,12 @@ const styles = StyleSheet.create({
lineHeight: 16, lineHeight: 16,
marginBottom: 2, marginBottom: 2,
}, },
logEntryError: {
color: '#f87171',
},
logEntryWarn: {
color: '#fbbf24',
},
logEntryEmpty: { logEntryEmpty: {
fontSize: FontSizes.xs, fontSize: FontSizes.xs,
color: 'rgba(255,255,255,0.5)', color: 'rgba(255,255,255,0.5)',

View File

@ -1,276 +0,0 @@
/**
* VoiceIndicator - Animated visual feedback for voice conversation
* Shows pulsing circles when listening or speaking
*/
import React, { useEffect, useRef } from 'react';
import { View, StyleSheet, Animated, Text, TouchableOpacity } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme';
interface VoiceIndicatorProps {
mode: 'listening' | 'speaking' | 'idle';
onTap?: (currentMode: 'listening' | 'speaking') => void;
}
export function VoiceIndicator({ mode, onTap }: VoiceIndicatorProps) {
// Animation values for 3 concentric circles
const ring1Scale = useRef(new Animated.Value(1)).current;
const ring2Scale = useRef(new Animated.Value(1)).current;
const ring3Scale = useRef(new Animated.Value(1)).current;
const ring1Opacity = useRef(new Animated.Value(0.8)).current;
const ring2Opacity = useRef(new Animated.Value(0.6)).current;
const ring3Opacity = useRef(new Animated.Value(0.4)).current;
// Inner circle pulse
const innerPulse = useRef(new Animated.Value(1)).current;
useEffect(() => {
if (mode === 'idle') {
// Reset all animations
ring1Scale.setValue(1);
ring2Scale.setValue(1);
ring3Scale.setValue(1);
ring1Opacity.setValue(0);
ring2Opacity.setValue(0);
ring3Opacity.setValue(0);
innerPulse.setValue(1);
return;
}
// Create pulsing animation for rings
const createRingAnimation = (
scale: Animated.Value,
opacity: Animated.Value,
delay: number
) => {
return Animated.loop(
Animated.sequence([
Animated.delay(delay),
Animated.parallel([
Animated.timing(scale, {
toValue: 2.5,
duration: 1500,
useNativeDriver: true,
}),
Animated.timing(opacity, {
toValue: 0,
duration: 1500,
useNativeDriver: true,
}),
]),
Animated.parallel([
Animated.timing(scale, {
toValue: 1,
duration: 0,
useNativeDriver: true,
}),
Animated.timing(opacity, {
toValue: mode === 'listening' ? 0.8 : 0.6,
duration: 0,
useNativeDriver: true,
}),
]),
])
);
};
// Inner pulse animation
const innerPulseAnimation = Animated.loop(
Animated.sequence([
Animated.timing(innerPulse, {
toValue: 1.15,
duration: 400,
useNativeDriver: true,
}),
Animated.timing(innerPulse, {
toValue: 1,
duration: 400,
useNativeDriver: true,
}),
])
);
// Reset opacity values
ring1Opacity.setValue(mode === 'listening' ? 0.8 : 0.6);
ring2Opacity.setValue(mode === 'listening' ? 0.6 : 0.4);
ring3Opacity.setValue(mode === 'listening' ? 0.4 : 0.3);
// Start all animations
const anim1 = createRingAnimation(ring1Scale, ring1Opacity, 0);
const anim2 = createRingAnimation(ring2Scale, ring2Opacity, 500);
const anim3 = createRingAnimation(ring3Scale, ring3Opacity, 1000);
anim1.start();
anim2.start();
anim3.start();
innerPulseAnimation.start();
return () => {
anim1.stop();
anim2.stop();
anim3.stop();
innerPulseAnimation.stop();
};
}, [mode]);
if (mode === 'idle') {
return null;
}
const isListening = mode === 'listening';
const primaryColor = isListening ? AppColors.primary : '#4CAF50';
const secondaryColor = isListening ? '#2196F3' : '#66BB6A';
// Handle tap anywhere on the indicator
const handlePress = () => {
if (mode !== 'idle') {
onTap?.(mode as 'listening' | 'speaking');
}
};
return (
<TouchableOpacity
style={styles.container}
onPress={handlePress}
activeOpacity={0.9}
>
{/* Animated rings */}
<View style={styles.ringsContainer}>
<Animated.View
style={[
styles.ring,
{
backgroundColor: primaryColor,
transform: [{ scale: ring1Scale }],
opacity: ring1Opacity,
},
]}
/>
<Animated.View
style={[
styles.ring,
{
backgroundColor: secondaryColor,
transform: [{ scale: ring2Scale }],
opacity: ring2Opacity,
},
]}
/>
<Animated.View
style={[
styles.ring,
{
backgroundColor: primaryColor,
transform: [{ scale: ring3Scale }],
opacity: ring3Opacity,
},
]}
/>
{/* Inner pulsing circle with icon */}
<Animated.View
style={[
styles.innerCircle,
{
backgroundColor: primaryColor,
transform: [{ scale: innerPulse }],
},
]}
>
<Ionicons
name={isListening ? 'mic' : 'volume-high'}
size={32}
color={AppColors.white}
/>
</Animated.View>
</View>
{/* Status text */}
<Text style={[styles.statusText, { color: primaryColor }]}>
{isListening ? 'Listening...' : 'Speaking...'}
</Text>
{/* Tap hint - shows what will happen when tapped */}
<View style={[styles.hintContainer, !isListening && styles.hintContainerSpeak]}>
<Ionicons
name={isListening ? 'close-circle' : 'mic'}
size={20}
color={isListening ? AppColors.error : AppColors.primary}
/>
<Text style={[styles.hintText, !isListening && styles.hintTextSpeak]}>
{isListening ? 'Tap to cancel' : 'Tap to interrupt & speak'}
</Text>
</View>
</TouchableOpacity>
);
}
const styles = StyleSheet.create({
container: {
alignItems: 'center',
justifyContent: 'center',
paddingVertical: Spacing.xl,
backgroundColor: 'rgba(255, 255, 255, 0.95)',
borderRadius: BorderRadius.lg,
marginHorizontal: Spacing.md,
marginBottom: Spacing.md,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 8,
elevation: 4,
},
ringsContainer: {
width: 120,
height: 120,
alignItems: 'center',
justifyContent: 'center',
},
ring: {
position: 'absolute',
width: 60,
height: 60,
borderRadius: 30,
},
innerCircle: {
width: 70,
height: 70,
borderRadius: 35,
alignItems: 'center',
justifyContent: 'center',
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3,
shadowRadius: 6,
elevation: 8,
},
statusText: {
fontSize: FontSizes.lg,
fontWeight: '600',
marginTop: Spacing.md,
},
hintContainer: {
flexDirection: 'row',
alignItems: 'center',
marginTop: Spacing.md,
paddingHorizontal: Spacing.md,
paddingVertical: Spacing.sm,
backgroundColor: 'rgba(244, 67, 54, 0.1)',
borderRadius: BorderRadius.full,
},
hintContainerSpeak: {
backgroundColor: 'rgba(33, 150, 243, 0.1)',
},
hintText: {
marginLeft: Spacing.xs,
fontSize: FontSizes.sm,
color: AppColors.error,
fontWeight: '500',
},
hintTextSpeak: {
color: AppColors.primary,
},
});
export default VoiceIndicator;

661
hooks/useLiveKitRoom.ts Normal file
View File

@ -0,0 +1,661 @@
/**
* useLiveKitRoom - Hook for LiveKit voice call with Julia AI
*
* IMPORTANT: This hook encapsulates ALL LiveKit logic.
* The UI component should only use the returned state and actions.
*
* LOGGING: Maximum transparency - every step is logged!
*/
import { useState, useCallback, useRef, useEffect } from 'react';
import { Platform, AppState, AppStateStatus, NativeModules } from 'react-native';
import type { Room as RoomType } from 'livekit-client';
// Helper to detect iOS Simulator
// Expo Go and production builds both work with this approach
const isIOSSimulator = (): boolean => {
if (Platform.OS !== 'ios') return false;
// Check via DeviceInfo module if available
const { PlatformConstants } = NativeModules;
return PlatformConstants?.interfaceIdiom === 'simulator' ||
PlatformConstants?.isSimulator === true;
};
import { getToken, VOICE_NAME } from '@/services/livekitService';
import {
configureAudioForVoiceCall,
stopAudioSession,
reconfigureAudioForPlayback,
} from '@/utils/audioSession';
// Connection states
export type ConnectionState =
| 'idle'
| 'initializing'
| 'configuring_audio'
| 'requesting_token'
| 'connecting'
| 'connected'
| 'reconnecting'
| 'disconnecting'
| 'disconnected'
| 'error';
// Log entry type
export interface LogEntry {
timestamp: string; // Formatted time string (HH:MM:SS.mmm)
level: 'info' | 'warn' | 'error' | 'success';
message: string;
}
// Hook options
export interface UseLiveKitRoomOptions {
userId: string;
onTranscript?: (role: 'user' | 'assistant', text: string) => void;
autoConnect?: boolean;
}
// Hook return type
export interface UseLiveKitRoomReturn {
// Connection state
state: ConnectionState;
error: string | null;
// Call info
roomName: string | null;
callDuration: number;
// Audio state
isMuted: boolean;
isAgentSpeaking: boolean;
canPlayAudio: boolean;
// Debug info
logs: LogEntry[];
participantCount: number;
// Actions
connect: () => Promise<void>;
disconnect: () => Promise<void>;
toggleMute: () => Promise<void>;
clearLogs: () => void;
}
/**
* Main hook for LiveKit voice calls
*/
export function useLiveKitRoom(options: UseLiveKitRoomOptions): UseLiveKitRoomReturn {
const { userId, onTranscript, autoConnect = false } = options;
// State
const [state, setState] = useState<ConnectionState>('idle');
const [error, setError] = useState<string | null>(null);
const [roomName, setRoomName] = useState<string | null>(null);
const [callDuration, setCallDuration] = useState(0);
const [isMuted, setIsMuted] = useState(false);
const [isAgentSpeaking, setIsAgentSpeaking] = useState(false);
const [canPlayAudio, setCanPlayAudio] = useState(false);
const [logs, setLogs] = useState<LogEntry[]>([]);
const [participantCount, setParticipantCount] = useState(0);
// Refs
const roomRef = useRef<RoomType | null>(null);
const callStartTimeRef = useRef<number | null>(null);
const connectionIdRef = useRef(0);
const isUnmountingRef = useRef(false);
const appStateRef = useRef<AppStateStatus>(AppState.currentState);
// ===================
// LOGGING FUNCTIONS
// ===================
const log = useCallback((level: LogEntry['level'], message: string) => {
const now = new Date();
const timestamp = now.toLocaleTimeString('en-US', {
hour12: false,
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
}) + '.' + now.getMilliseconds().toString().padStart(3, '0');
const entry: LogEntry = {
timestamp,
level,
message,
};
setLogs((prev) => [...prev, entry]);
// Also log to console with color
const prefix = `[LiveKit ${timestamp}]`;
switch (level) {
case 'error':
console.error(`${prefix} ERROR: ${message}`);
break;
case 'warn':
console.warn(`${prefix} WARN: ${message}`);
break;
case 'success':
console.log(`${prefix} SUCCESS: ${message}`);
break;
default:
console.log(`${prefix} INFO: ${message}`);
}
}, []);
const logInfo = useCallback((msg: string) => log('info', msg), [log]);
const logWarn = useCallback((msg: string) => log('warn', msg), [log]);
const logError = useCallback((msg: string) => log('error', msg), [log]);
const logSuccess = useCallback((msg: string) => log('success', msg), [log]);
const clearLogs = useCallback(() => {
setLogs([]);
}, []);
// ===================
// CONNECT FUNCTION
// ===================
const connect = useCallback(async () => {
// Prevent multiple concurrent connection attempts
const currentConnectionId = ++connectionIdRef.current;
logInfo('========== STARTING VOICE CALL ==========');
logInfo(`User ID: ${userId}`);
logInfo(`Platform: ${Platform.OS}`);
logInfo(`Connection ID: ${currentConnectionId}`);
// Check if already connected
if (roomRef.current) {
logWarn('Already connected to a room, disconnecting first...');
await roomRef.current.disconnect();
roomRef.current = null;
}
try {
// ========== STEP 1: Initialize ==========
setState('initializing');
logInfo('STEP 1/6: Initializing...');
// Detect simulator vs real device
const isSimulator = isIOSSimulator();
logInfo(`Device type: ${isSimulator ? 'SIMULATOR' : 'REAL DEVICE'}`);
logInfo(`Device model: ${Platform.OS} ${Platform.Version}`);
if (isSimulator) {
logWarn('⚠️ SIMULATOR DETECTED - Microphone will NOT work!');
logWarn('Simulator can only test: connection, token, agent presence, TTS playback');
logWarn('For full STT test, use a real iPhone device');
}
// Check if connection was cancelled
if (isUnmountingRef.current || currentConnectionId !== connectionIdRef.current) {
logWarn('Connection cancelled (component unmounting or new connection started)');
return;
}
// ========== STEP 2: Register WebRTC Globals ==========
logInfo('STEP 2/6: Registering WebRTC globals...');
const { registerGlobals } = await import('@livekit/react-native');
if (typeof global.RTCPeerConnection === 'undefined') {
logInfo('RTCPeerConnection not found, calling registerGlobals()...');
registerGlobals();
logSuccess('WebRTC globals registered!');
} else {
logInfo('WebRTC globals already registered');
}
// Check again
if (isUnmountingRef.current || currentConnectionId !== connectionIdRef.current) {
logWarn('Connection cancelled after registerGlobals');
return;
}
// ========== STEP 3: Configure iOS Audio ==========
setState('configuring_audio');
logInfo('STEP 3/6: Configuring iOS AudioSession...');
await configureAudioForVoiceCall();
logSuccess('iOS AudioSession configured!');
// Check again
if (isUnmountingRef.current || currentConnectionId !== connectionIdRef.current) {
logWarn('Connection cancelled after audio config');
await stopAudioSession();
return;
}
// ========== STEP 4: Get Token ==========
setState('requesting_token');
logInfo('STEP 4/6: Requesting token from server...');
const tokenResult = await getToken(userId);
if (!tokenResult.success || !tokenResult.data) {
const errorMsg = tokenResult.error || 'Failed to get token';
logError(`Token request failed: ${errorMsg}`);
setError(errorMsg);
setState('error');
return;
}
const { token, wsUrl, roomName: room } = tokenResult.data;
setRoomName(room);
logSuccess(`Token received!`);
logInfo(` Room: ${room}`);
logInfo(` WebSocket URL: ${wsUrl}`);
logInfo(` Token length: ${token.length} chars`);
// Check again
if (isUnmountingRef.current || currentConnectionId !== connectionIdRef.current) {
logWarn('Connection cancelled after token');
await stopAudioSession();
return;
}
// ========== STEP 5: Import LiveKit and Create Room ==========
logInfo('STEP 5/6: Creating LiveKit Room...');
const { Room, RoomEvent, ConnectionState: LKConnectionState, Track } = await import(
'livekit-client'
);
logInfo(` Room class available: ${typeof Room === 'function'}`);
logInfo(` RoomEvent available: ${typeof RoomEvent === 'object'}`);
const lkRoom = new Room();
roomRef.current = lkRoom;
logSuccess('Room instance created!');
// ========== Setup Event Listeners ==========
logInfo('Setting up event listeners...');
// Connection state changes
lkRoom.on(RoomEvent.ConnectionStateChanged, (newState) => {
logInfo(`EVENT: ConnectionStateChanged -> ${newState}`);
switch (newState) {
case LKConnectionState.Connecting:
setState('connecting');
break;
case LKConnectionState.Connected:
setState('connected');
logSuccess('Connected to room!');
if (!callStartTimeRef.current) {
callStartTimeRef.current = Date.now();
logInfo('Call timer started');
}
break;
case LKConnectionState.Reconnecting:
setState('reconnecting');
logWarn('Reconnecting...');
break;
case LKConnectionState.Disconnected:
setState('disconnected');
logInfo('Disconnected from room');
break;
}
});
// Track subscribed (audio from agent)
lkRoom.on(RoomEvent.TrackSubscribed, async (track, publication, participant) => {
logInfo(`EVENT: TrackSubscribed`);
logInfo(` Track kind: ${track.kind}`);
logInfo(` Track source: ${track.source}`);
logInfo(` Participant: ${participant.identity}`);
logInfo(` Publication SID: ${publication.trackSid}`);
if (track.kind === Track.Kind.Audio) {
logSuccess(`Audio track from ${participant.identity} - should hear voice now!`);
setIsAgentSpeaking(true);
// Reconfigure audio for playback
logInfo('Reconfiguring audio for playback...');
await reconfigureAudioForPlayback();
}
});
// Track unsubscribed
lkRoom.on(RoomEvent.TrackUnsubscribed, (track, publication, participant) => {
logInfo(`EVENT: TrackUnsubscribed`);
logInfo(` Track kind: ${track.kind}`);
logInfo(` Participant: ${participant.identity}`);
if (track.kind === Track.Kind.Audio) {
setIsAgentSpeaking(false);
}
});
// Track muted/unmuted
lkRoom.on(RoomEvent.TrackMuted, (publication, participant) => {
logInfo(`EVENT: TrackMuted - ${publication.trackSid} by ${participant.identity}`);
});
lkRoom.on(RoomEvent.TrackUnmuted, (publication, participant) => {
logInfo(`EVENT: TrackUnmuted - ${publication.trackSid} by ${participant.identity}`);
});
// Participants
lkRoom.on(RoomEvent.ParticipantConnected, (participant) => {
logSuccess(`EVENT: ParticipantConnected - ${participant.identity}`);
setParticipantCount((c) => c + 1);
});
lkRoom.on(RoomEvent.ParticipantDisconnected, (participant) => {
logInfo(`EVENT: ParticipantDisconnected - ${participant.identity}`);
setParticipantCount((c) => Math.max(0, c - 1));
});
// Active speakers (voice activity)
lkRoom.on(RoomEvent.ActiveSpeakersChanged, (speakers) => {
if (speakers.length > 0) {
const speakerNames = speakers.map((s: any) => s.identity).join(', ');
logInfo(`EVENT: ActiveSpeakersChanged - ${speakerNames}`);
// Check if agent is speaking
const agentSpeaking = speakers.some((s: any) => s.identity.startsWith('agent'));
setIsAgentSpeaking(agentSpeaking);
}
});
// Local track published (our mic)
lkRoom.on(RoomEvent.LocalTrackPublished, (publication, participant) => {
logSuccess(`EVENT: LocalTrackPublished`);
logInfo(` Track: ${publication.trackSid}`);
logInfo(` Kind: ${publication.kind}`);
logInfo(` Source: ${publication.source}`);
});
// Audio playback status
lkRoom.on(RoomEvent.AudioPlaybackStatusChanged, () => {
const canPlay = lkRoom.canPlaybackAudio;
logInfo(`EVENT: AudioPlaybackStatusChanged - canPlaybackAudio: ${canPlay}`);
setCanPlayAudio(canPlay);
});
// Data received (transcripts)
lkRoom.on(RoomEvent.DataReceived, (payload, participant) => {
try {
const data = JSON.parse(new TextDecoder().decode(payload));
logInfo(`EVENT: DataReceived from ${participant?.identity || 'unknown'}`);
logInfo(` Type: ${data.type}`);
if (data.type === 'transcript' && onTranscript) {
logInfo(` Role: ${data.role}, Text: ${data.text?.substring(0, 50)}...`);
onTranscript(data.role, data.text);
}
} catch (e) {
// Non-JSON data, ignore
}
});
// Errors
lkRoom.on(RoomEvent.Disconnected, (reason) => {
logWarn(`EVENT: Disconnected - Reason: ${reason}`);
});
// Check again before connect
if (isUnmountingRef.current || currentConnectionId !== connectionIdRef.current) {
logWarn('Connection cancelled before room.connect()');
await stopAudioSession();
return;
}
// ========== STEP 6: Connect to Room ==========
setState('connecting');
logInfo('STEP 6/6: Connecting to LiveKit room...');
logInfo(` URL: ${wsUrl}`);
logInfo(` Room: ${room}`);
await lkRoom.connect(wsUrl, token, {
autoSubscribe: true,
});
logSuccess('Connected to room!');
// Check if connection was cancelled after connect
if (isUnmountingRef.current || currentConnectionId !== connectionIdRef.current) {
logWarn('Connection cancelled after room.connect()');
await lkRoom.disconnect();
await stopAudioSession();
return;
}
// ========== Enable Microphone ==========
logInfo('Enabling microphone...');
try {
await lkRoom.localParticipant.setMicrophoneEnabled(true);
logSuccess('Microphone enabled!');
logInfo(` Local participant: ${lkRoom.localParticipant.identity}`);
// Log track info - CRITICAL for debugging!
const audioTracks = lkRoom.localParticipant.getTrackPublications();
logInfo(` Published tracks: ${audioTracks.length}`);
let micTrackFound = false;
audioTracks.forEach((pub) => {
logInfo(` - ${pub.kind}: ${pub.trackSid} (${pub.source})`);
logInfo(` isMuted: ${pub.isMuted}, isSubscribed: ${pub.isSubscribed}`);
if (pub.kind === 'audio' && pub.source === 'microphone') {
micTrackFound = true;
const track = pub.track;
if (track) {
logInfo(` Track mediaStreamTrack: ${track.mediaStreamTrack?.readyState || 'N/A'}`);
logInfo(` Track enabled: ${track.mediaStreamTrack?.enabled || 'N/A'}`);
} else {
logWarn(` WARNING: No track object on publication!`);
}
}
});
if (!micTrackFound) {
// Check if simulator
const isSimulator = isIOSSimulator();
if (isSimulator) {
logWarn('No microphone track - EXPECTED on simulator');
logInfo('Simulator test: check if Agent joined and TTS works');
} else {
logError('CRITICAL: No microphone track published! STT will NOT work!');
logError('Possible causes: permissions denied, AudioSession not configured, hardware issue');
}
} else {
logSuccess('Microphone track found and published - STT should work');
}
} catch (micError: any) {
logError(`Failed to enable microphone: ${micError.message}`);
logError(`Stack: ${micError.stack || 'N/A'}`);
// This is CRITICAL - user must know!
setError(`Microphone error: ${micError.message}`);
}
// Set initial participant count
setParticipantCount(lkRoom.remoteParticipants.size);
logInfo(`Remote participants: ${lkRoom.remoteParticipants.size}`);
logSuccess('========== VOICE CALL STARTED ==========');
} catch (err: any) {
// Ignore errors if unmounting
if (isUnmountingRef.current || currentConnectionId !== connectionIdRef.current) {
logWarn('Error ignored (component unmounting)');
return;
}
const errorMsg = err?.message || String(err);
logError(`Connection failed: ${errorMsg}`);
logError(`Stack: ${err?.stack || 'N/A'}`);
setError(errorMsg);
setState('error');
// Cleanup
await stopAudioSession();
}
}, [userId, onTranscript, logInfo, logWarn, logError, logSuccess]);
// ===================
// DISCONNECT FUNCTION
// ===================
const disconnect = useCallback(async () => {
logInfo('========== DISCONNECTING ==========');
setState('disconnecting');
try {
if (roomRef.current) {
logInfo('Disconnecting from room...');
await roomRef.current.disconnect();
roomRef.current = null;
logSuccess('Disconnected from room');
} else {
logInfo('No room to disconnect from');
}
} catch (err: any) {
logError(`Disconnect error: ${err.message}`);
}
logInfo('Stopping audio session...');
await stopAudioSession();
// Reset state
setState('disconnected');
setRoomName(null);
setIsMuted(false);
setIsAgentSpeaking(false);
setParticipantCount(0);
callStartTimeRef.current = null;
logSuccess('========== DISCONNECTED ==========');
}, [logInfo, logError, logSuccess]);
// ===================
// TOGGLE MUTE
// ===================
const toggleMute = useCallback(async () => {
if (!roomRef.current) {
logWarn('Cannot toggle mute - not connected');
return;
}
const newMuted = !isMuted;
logInfo(`Toggling mute: ${isMuted} -> ${newMuted}`);
try {
await roomRef.current.localParticipant.setMicrophoneEnabled(!newMuted);
setIsMuted(newMuted);
logSuccess(`Microphone ${newMuted ? 'muted' : 'unmuted'}`);
} catch (err: any) {
logError(`Failed to toggle mute: ${err.message}`);
}
}, [isMuted, logInfo, logWarn, logError, logSuccess]);
// ===================
// CALL DURATION TIMER
// ===================
useEffect(() => {
if (state !== 'connected') return;
const interval = setInterval(() => {
if (callStartTimeRef.current) {
const elapsed = Math.floor((Date.now() - callStartTimeRef.current) / 1000);
setCallDuration(elapsed);
}
}, 1000);
return () => clearInterval(interval);
}, [state]);
// ===================
// APP STATE HANDLING
// ===================
useEffect(() => {
const handleAppStateChange = (nextAppState: AppStateStatus) => {
const prevState = appStateRef.current;
appStateRef.current = nextAppState;
if (prevState.match(/inactive|background/) && nextAppState === 'active') {
logInfo('App returned to foreground');
} else if (prevState === 'active' && nextAppState.match(/inactive|background/)) {
logInfo('App went to background - call continues in background');
}
};
const subscription = AppState.addEventListener('change', handleAppStateChange);
return () => subscription.remove();
}, [logInfo]);
// ===================
// CLEANUP ON UNMOUNT
// ===================
useEffect(() => {
isUnmountingRef.current = false;
return () => {
isUnmountingRef.current = true;
// Cleanup
const cleanup = async () => {
if (roomRef.current) {
try {
await roomRef.current.disconnect();
} catch (e) {
// Ignore
}
roomRef.current = null;
}
await stopAudioSession();
};
cleanup();
};
}, []);
// ===================
// AUTO CONNECT
// ===================
useEffect(() => {
if (autoConnect && state === 'idle') {
connect();
}
}, [autoConnect, state, connect]);
// ===================
// RETURN
// ===================
return {
// Connection state
state,
error,
// Call info
roomName,
callDuration,
// Audio state
isMuted,
isAgentSpeaking,
canPlayAudio,
// Debug
logs,
participantCount,
// Actions
connect,
disconnect,
toggleMute,
clearLogs,
};
}
export { VOICE_NAME };

View File

@ -1,10 +1,8 @@
[agent]
id = "CA_zKZCpX36ZT5C"
name = "julia-ai"
subdomain = "live-kit-demo-70txlh6a"
[project] [project]
subdomain = "live-kit-demo-70txlh6a" subdomain = "live-kit-demo-70txlh6a"
[agent]
id = "CA_Yd3qcuYEVKKE"
[build] [build]
dockerfile = "Dockerfile" dockerfile = "Dockerfile"

View File

@ -9,11 +9,12 @@ description = "WellNuo Julia AI voice assistant for elderly care"
requires-python = ">=3.10, <3.14" requires-python = ">=3.10, <3.14"
dependencies = [ dependencies = [
"livekit-agents[silero,turn-detector]~=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", "livekit-plugins-openai~=1.0",
"python-dotenv", "python-dotenv",
"aiohttp",
] ]
[dependency-groups] [dependency-groups]

View File

@ -1,157 +1,158 @@
""" """
WellNuo Voice Agent - Julia AI WellNuo Voice Agent - Julia AI
LiveKit Agents Cloud deployment LiveKit Agents Cloud deployment
Uses Deepgram STT/TTS + OpenAI GPT-4o LLM Uses WellNuo voice_ask API for LLM responses, Deepgram for STT/TTS
""" """
import logging import logging
import os import os
import json import random
from datetime import datetime import aiohttp
from dotenv import load_dotenv
from livekit import rtc
from livekit.agents import ( from livekit.agents import (
Agent, Agent,
AgentServer,
AgentSession, AgentSession,
JobContext, JobContext,
JobProcess, JobProcess,
RoomInputOptions,
WorkerOptions,
cli, cli,
room_io, llm,
) )
from livekit.plugins import deepgram, openai, silero, noise_cancellation from livekit.plugins import deepgram, silero, noise_cancellation
from livekit.plugins.turn_detector.multilingual import MultilingualModel
logger = logging.getLogger("julia-ai") logger = logging.getLogger("julia-ai")
load_dotenv(".env.local") # WellNuo API Configuration
WELLNUO_API_URL = "https://eluxnetworks.net/function/well-api/api"
WELLNUO_USER = os.getenv("WELLNUO_USER", "anandk")
WELLNUO_PASSWORD = os.getenv("WELLNUO_PASSWORD", "anandk_8")
# Hardcoded Ferdinand's deployment_id for testing
DEPLOYMENT_ID = os.getenv("DEPLOYMENT_ID", "21")
# Demo data for Ferdinand Zmrzli - WellNuo elderly care beneficiary # Julia's personality for voice synthesis
FERDINAND_DATA = { JULIA_GREETING = "Hello! I'm Julia, your AI care assistant. How can I help you today?"
"client": {
"name": "Ferdinand Zmrzli",
"address": "661 Encore Way" class WellNuoLLM(llm.LLM):
}, """Custom LLM that uses WellNuo voice_ask API."""
"today_alerts": [
{ def __init__(self):
"type": "fall_detected", super().__init__()
"time": "06:32", self._token = None
"severity": "critical", self._session = None
"location": "bathroom"
}, async def _ensure_token(self):
{ """Get authentication token from WellNuo API."""
"type": "short_sleep", if self._token:
"time": "06:30", return self._token
"severity": "high",
"note": "Only 5 hours sleep (normal: 7-8)" async with aiohttp.ClientSession() as session:
}, # Generate random nonce for request
{ nonce = str(random.randint(0, 999999))
"type": "missed_medication", data = {
"time": "08:30", "function": "credentials",
"severity": "high", "clientId": "001",
"note": "Morning medication not taken" "user_name": WELLNUO_USER,
"ps": WELLNUO_PASSWORD, # API expects 'ps' not 'password'
"nonce": nonce,
} }
], async with session.post(WELLNUO_API_URL, data=data) as resp:
"vitals": { result = await resp.json()
"heart_rate": {"value": 72, "unit": "bpm", "status": "normal"}, if result.get("status") == "200 OK":
"blood_pressure": {"systolic": 135, "diastolic": 82, "status": "slightly_elevated"}, self._token = result.get("access_token")
"sleep_quality": {"hours": 5, "deep_sleep_percent": 18, "status": "poor"}, logger.info("WellNuo token obtained successfully")
"activity_level": {"steps": 1240, "goal": 4000, "status": "low"} return self._token
}, else:
"weekly_summary": { logger.error(f"Failed to get WellNuo token: {result}")
"total_alerts": 12, raise Exception("Failed to authenticate with WellNuo API")
"critical_alerts": 3,
"average_sleep": 5.5, async def chat(
"medication_adherence": "67%", self,
"activity_trend": "declining" *,
}, chat_ctx: llm.ChatContext,
"recommendations": [ tools: list[llm.FunctionTool] | None = None,
"Follow up on fall incident - consider bathroom grab bars", tool_choice: llm.ToolChoice | None = None,
"Discuss sleep quality with physician", parallel_tool_calls: bool | None = None,
"Review medication reminder settings" extra_body: dict | None = None,
] ) -> llm.LLMStream:
} """Send user question to WellNuo voice_ask API."""
# Get the last user message
user_message = ""
for msg in reversed(chat_ctx.items):
if hasattr(msg, 'role') and msg.role == "user":
if hasattr(msg, 'content'):
user_message = msg.content
break
if not user_message:
# Return a default response if no user message
return WellNuoLLMStream("I'm here to help. What would you like to know?")
logger.info(f"User question: {user_message}")
# Get response from WellNuo API
try:
token = await self._ensure_token()
async with aiohttp.ClientSession() as session:
data = {
"function": "voice_ask",
"clientId": "001",
"user_name": WELLNUO_USER,
"token": token,
"question": user_message,
"deployment_id": DEPLOYMENT_ID,
}
async with session.post(WELLNUO_API_URL, data=data) as resp:
result = await resp.json()
if result.get("ok"):
response_body = result.get("response", {}).get("body", "")
logger.info(f"WellNuo response: {response_body}")
return WellNuoLLMStream(response_body)
else:
logger.error(f"WellNuo API error: {result}")
return WellNuoLLMStream("I'm sorry, I couldn't get that information right now.")
except Exception as e:
logger.error(f"Error calling WellNuo API: {e}")
return WellNuoLLMStream("I'm having trouble connecting. Please try again.")
def build_system_prompt() -> str: class WellNuoLLMStream(llm.LLMStream):
"""Build Julia AI system prompt with Ferdinand's context data.""" """Stream wrapper for WellNuo API response."""
today = datetime.now().strftime("%B %d, %Y")
alerts_text = "" def __init__(self, response_text: str):
for alert in FERDINAND_DATA["today_alerts"]: super().__init__(
alerts_text += f"- {alert['type'].replace('_', ' ').title()} at {alert['time']}" llm=None,
if 'location' in alert: chat_ctx=llm.ChatContext(),
alerts_text += f" (in {alert['location']})" tools=[],
if 'note' in alert: tool_choice=None,
alerts_text += f" - {alert['note']}" parallel_tool_calls=None,
alerts_text += f" [Severity: {alert['severity']}]\n" extra_body=None,
)
self._response_text = response_text
self._sent = False
vitals = FERDINAND_DATA["vitals"] async def _run(self):
vitals_text = f"""- Heart Rate: {vitals['heart_rate']['value']} {vitals['heart_rate']['unit']} ({vitals['heart_rate']['status']}) """Yield the response as a single chunk."""
- Blood Pressure: {vitals['blood_pressure']['systolic']}/{vitals['blood_pressure']['diastolic']} ({vitals['blood_pressure']['status']}) pass
- Sleep: {vitals['sleep_quality']['hours']} hours, {vitals['sleep_quality']['deep_sleep_percent']}% deep sleep ({vitals['sleep_quality']['status']})
- Activity: {vitals['activity_level']['steps']} steps of {vitals['activity_level']['goal']} goal ({vitals['activity_level']['status']})"""
weekly = FERDINAND_DATA["weekly_summary"] async def __anext__(self) -> llm.ChatChunk:
weekly_text = f"""- Total alerts this week: {weekly['total_alerts']} ({weekly['critical_alerts']} critical) if self._sent:
- Average sleep: {weekly['average_sleep']} hours raise StopAsyncIteration
- Medication adherence: {weekly['medication_adherence']}
- Activity trend: {weekly['activity_trend']}"""
recs = "\n".join(f"- {r}" for r in FERDINAND_DATA["recommendations"]) self._sent = True
return llm.ChatChunk(
id="wellnuo-response",
delta=llm.ChoiceDelta(
role="assistant",
content=self._response_text,
),
)
return f"""You are Julia, a compassionate and knowledgeable AI care assistant for WellNuo, a platform that helps families care for their elderly loved ones. def __aiter__(self):
return self
Today's Date: {today}
Current Client: {FERDINAND_DATA["client"]["name"]}
Address: {FERDINAND_DATA["client"]["address"]}
TODAY'S ALERTS:
{alerts_text}
CURRENT VITALS:
{vitals_text}
WEEKLY SUMMARY:
{weekly_text}
RECOMMENDATIONS:
{recs}
PERSONALITY AND COMMUNICATION STYLE:
- Be warm, empathetic, and supportive - families are often worried about their loved ones
- Speak naturally and conversationally - this is a voice interaction
- Be concise - users are listening, not reading
- Use simple language, avoid medical jargon unless necessary
- When discussing alerts, prioritize by severity but don't alarm unnecessarily
- Offer actionable advice and next steps
- Show understanding of the emotional aspects of caregiving
CAPABILITIES:
- Summarize today's health status and alerts
- Explain what specific alerts mean and suggest actions
- Provide context on vital signs and trends
- Answer questions about the care recipient's wellbeing
- Offer recommendations for improving care
IMPORTANT GUIDELINES:
- Always refer to Ferdinand by name
- If asked about something not in the data, say you'll need to check and suggest they contact their care coordinator
- Keep responses focused and relevant
- Do not invent data not provided above
- Express appropriate concern for critical alerts while remaining calm and helpful
- Do not use any special formatting, markdown, emojis, or punctuation marks that would not sound natural when spoken aloud"""
class JuliaAssistant(Agent):
"""Julia AI voice assistant for WellNuo elderly care platform."""
def __init__(self) -> None:
super().__init__(instructions=build_system_prompt())
server = AgentServer()
def prewarm(proc: JobProcess): def prewarm(proc: JobProcess):
@ -159,60 +160,43 @@ def prewarm(proc: JobProcess):
proc.userdata["vad"] = silero.VAD.load() proc.userdata["vad"] = silero.VAD.load()
server.setup_fnc = prewarm async def entrypoint(ctx: JobContext):
@server.rtc_session()
async def julia_session(ctx: JobContext):
"""Main Julia AI voice session handler.""" """Main Julia AI voice session handler."""
ctx.log_context_fields = {
"room": ctx.room.name,
"agent": "julia-ai",
}
logger.info(f"Starting Julia AI session in room {ctx.room.name}") logger.info(f"Starting Julia AI session in room {ctx.room.name}")
logger.info(f"Using WellNuo voice_ask API with deployment_id: {DEPLOYMENT_ID}")
# Set up voice AI pipeline with Deepgram STT/TTS and OpenAI LLM
session = AgentSession( session = AgentSession(
# Deepgram Nova-2 for accurate speech-to-text # Deepgram Nova-2 for accurate speech-to-text
stt=deepgram.STT(model="nova-2"), stt=deepgram.STT(model="nova-2"),
# OpenAI GPT-4o for intelligent responses # WellNuo voice_ask API for LLM
llm=openai.LLM( llm=WellNuoLLM(),
model="gpt-4o",
api_key=os.getenv("OPENAI_API_KEY"),
),
# 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"),
# LiveKit turn detector for multilingual support
turn_detection=MultilingualModel(),
# Silero VAD for voice activity detection # Silero VAD for voice activity detection
vad=ctx.proc.userdata["vad"], vad=ctx.proc.userdata["vad"],
# Allow preemptive generation for faster responses
preemptive_generation=True,
) )
# Start the session with Julia assistant # Start the session with Julia assistant
await session.start( await session.start(
agent=JuliaAssistant(), agent=Agent(instructions="You are Julia, a helpful AI care assistant."),
room=ctx.room, room=ctx.room,
room_options=room_io.RoomOptions( room_input_options=RoomInputOptions(
audio_input=room_io.AudioInputOptions( # Enable noise cancellation
noise_cancellation=lambda params: noise_cancellation.BVCTelephony() noise_cancellation=noise_cancellation.BVC(),
if params.participant.kind == rtc.ParticipantKind.PARTICIPANT_KIND_SIP
else noise_cancellation.BVC(),
),
), ),
) )
# Connect to the room
await ctx.connect()
# Generate initial greeting # Generate initial greeting
await session.generate_reply( await session.generate_reply(
instructions="Greet the user warmly as Julia. Briefly introduce yourself as their AI care assistant and mention you're here to help them stay updated on Ferdinand's wellbeing. If there are critical alerts today, mention you have some important updates to share." instructions="Greet the user warmly as Julia. Briefly introduce yourself as their AI care assistant and ask how you can help them today."
) )
if __name__ == "__main__": if __name__ == "__main__":
cli.run_app(server) cli.run_app(
WorkerOptions(
entrypoint_fnc=entrypoint,
prewarm_fnc=prewarm,
)
)

View File

@ -424,22 +424,13 @@ name = "exceptiongroup"
version = "1.3.1" version = "1.3.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "typing-extensions", marker = "python_full_version < '3.13'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
] ]
[[package]]
name = "filelock"
version = "3.20.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1d/65/ce7f1b70157833bf3cb851b556a37d4547ceafc158aa9b34b36782f23696/filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1", size = 19485, upload-time = "2026-01-09T17:55:05.421Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1", size = 16701, upload-time = "2026-01-09T17:55:04.334Z" },
]
[[package]] [[package]]
name = "flatbuffers" name = "flatbuffers"
version = "25.12.19" version = "25.12.19"
@ -537,15 +528,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" },
] ]
[[package]]
name = "fsspec"
version = "2026.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d5/7d/5df2650c57d47c57232af5ef4b4fdbff182070421e405e0d62c6cdbfaa87/fsspec-2026.1.0.tar.gz", hash = "sha256:e987cb0496a0d81bba3a9d1cee62922fb395e7d4c3b575e57f547953334fe07b", size = 310496, upload-time = "2026-01-09T15:21:35.562Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/01/c9/97cc5aae1648dcb851958a3ddf73ccd7dbe5650d95203ecb4d7720b4cdbf/fsspec-2026.1.0-py3-none-any.whl", hash = "sha256:cb76aa913c2285a3b49bdd5fc55b1d7c708d7208126b60f2eb8194fe1b4cbdcc", size = 201838, upload-time = "2026-01-09T15:21:34.041Z" },
]
[[package]] [[package]]
name = "googleapis-common-protos" name = "googleapis-common-protos"
version = "1.72.0" version = "1.72.0"
@ -618,28 +600,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
] ]
[[package]]
name = "hf-xet"
version = "1.2.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/5e/6e/0f11bacf08a67f7fb5ee09740f2ca54163863b07b70d579356e9222ce5d8/hf_xet-1.2.0.tar.gz", hash = "sha256:a8c27070ca547293b6890c4bf389f713f80e8c478631432962bb7f4bc0bd7d7f", size = 506020, upload-time = "2025-10-24T19:04:32.129Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9e/a5/85ef910a0aa034a2abcfadc360ab5ac6f6bc4e9112349bd40ca97551cff0/hf_xet-1.2.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:ceeefcd1b7aed4956ae8499e2199607765fbd1c60510752003b6cc0b8413b649", size = 2861870, upload-time = "2025-10-24T19:04:11.422Z" },
{ url = "https://files.pythonhosted.org/packages/ea/40/e2e0a7eb9a51fe8828ba2d47fe22a7e74914ea8a0db68a18c3aa7449c767/hf_xet-1.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b70218dd548e9840224df5638fdc94bd033552963cfa97f9170829381179c813", size = 2717584, upload-time = "2025-10-24T19:04:09.586Z" },
{ url = "https://files.pythonhosted.org/packages/a5/7d/daf7f8bc4594fdd59a8a596f9e3886133fdc68e675292218a5e4c1b7e834/hf_xet-1.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d40b18769bb9a8bc82a9ede575ce1a44c75eb80e7375a01d76259089529b5dc", size = 3315004, upload-time = "2025-10-24T19:04:00.314Z" },
{ url = "https://files.pythonhosted.org/packages/b1/ba/45ea2f605fbf6d81c8b21e4d970b168b18a53515923010c312c06cd83164/hf_xet-1.2.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:cd3a6027d59cfb60177c12d6424e31f4b5ff13d8e3a1247b3a584bf8977e6df5", size = 3222636, upload-time = "2025-10-24T19:03:58.111Z" },
{ url = "https://files.pythonhosted.org/packages/4a/1d/04513e3cab8f29ab8c109d309ddd21a2705afab9d52f2ba1151e0c14f086/hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6de1fc44f58f6dd937956c8d304d8c2dea264c80680bcfa61ca4a15e7b76780f", size = 3408448, upload-time = "2025-10-24T19:04:20.951Z" },
{ url = "https://files.pythonhosted.org/packages/f0/7c/60a2756d7feec7387db3a1176c632357632fbe7849fce576c5559d4520c7/hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f182f264ed2acd566c514e45da9f2119110e48a87a327ca271027904c70c5832", size = 3503401, upload-time = "2025-10-24T19:04:22.549Z" },
{ url = "https://files.pythonhosted.org/packages/4e/64/48fffbd67fb418ab07451e4ce641a70de1c40c10a13e25325e24858ebe5a/hf_xet-1.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:293a7a3787e5c95d7be1857358a9130694a9c6021de3f27fa233f37267174382", size = 2900866, upload-time = "2025-10-24T19:04:33.461Z" },
{ url = "https://files.pythonhosted.org/packages/96/2d/22338486473df5923a9ab7107d375dbef9173c338ebef5098ef593d2b560/hf_xet-1.2.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:46740d4ac024a7ca9b22bebf77460ff43332868b661186a8e46c227fdae01848", size = 2866099, upload-time = "2025-10-24T19:04:15.366Z" },
{ url = "https://files.pythonhosted.org/packages/7f/8c/c5becfa53234299bc2210ba314eaaae36c2875e0045809b82e40a9544f0c/hf_xet-1.2.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:27df617a076420d8845bea087f59303da8be17ed7ec0cd7ee3b9b9f579dff0e4", size = 2722178, upload-time = "2025-10-24T19:04:13.695Z" },
{ url = "https://files.pythonhosted.org/packages/9a/92/cf3ab0b652b082e66876d08da57fcc6fa2f0e6c70dfbbafbd470bb73eb47/hf_xet-1.2.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3651fd5bfe0281951b988c0facbe726aa5e347b103a675f49a3fa8144c7968fd", size = 3320214, upload-time = "2025-10-24T19:04:03.596Z" },
{ url = "https://files.pythonhosted.org/packages/46/92/3f7ec4a1b6a65bf45b059b6d4a5d38988f63e193056de2f420137e3c3244/hf_xet-1.2.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d06fa97c8562fb3ee7a378dd9b51e343bc5bc8190254202c9771029152f5e08c", size = 3229054, upload-time = "2025-10-24T19:04:01.949Z" },
{ url = "https://files.pythonhosted.org/packages/0b/dd/7ac658d54b9fb7999a0ccb07ad863b413cbaf5cf172f48ebcd9497ec7263/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4c1428c9ae73ec0939410ec73023c4f842927f39db09b063b9482dac5a3bb737", size = 3413812, upload-time = "2025-10-24T19:04:24.585Z" },
{ url = "https://files.pythonhosted.org/packages/92/68/89ac4e5b12a9ff6286a12174c8538a5930e2ed662091dd2572bbe0a18c8a/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a55558084c16b09b5ed32ab9ed38421e2d87cf3f1f89815764d1177081b99865", size = 3508920, upload-time = "2025-10-24T19:04:26.927Z" },
{ url = "https://files.pythonhosted.org/packages/cb/44/870d44b30e1dcfb6a65932e3e1506c103a8a5aea9103c337e7a53180322c/hf_xet-1.2.0-cp37-abi3-win_amd64.whl", hash = "sha256:e6584a52253f72c9f52f9e549d5895ca7a471608495c4ecaa6cc73dba2b24d69", size = 2905735, upload-time = "2025-10-24T19:04:35.928Z" },
]
[[package]] [[package]]
name = "httpcore" name = "httpcore"
version = "1.0.9" version = "1.0.9"
@ -668,25 +628,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
] ]
[[package]]
name = "huggingface-hub"
version = "0.36.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "filelock" },
{ name = "fsspec" },
{ name = "hf-xet", marker = "platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" },
{ name = "packaging" },
{ name = "pyyaml" },
{ name = "requests" },
{ name = "tqdm" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/98/63/4910c5fa9128fdadf6a9c5ac138e8b1b6cee4ca44bf7915bbfbce4e355ee/huggingface_hub-0.36.0.tar.gz", hash = "sha256:47b3f0e2539c39bf5cde015d63b72ec49baff67b6931c3d97f3f84532e2b8d25", size = 463358, upload-time = "2025-10-23T12:12:01.413Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/bd/1a875e0d592d447cbc02805fd3fe0f497714d6a2583f59d14fa9ebad96eb/huggingface_hub-0.36.0-py3-none-any.whl", hash = "sha256:7bcc9ad17d5b3f07b57c78e79d527102d08313caa278a641993acddcb894548d", size = 566094, upload-time = "2025-10-23T12:11:59.557Z" },
]
[[package]] [[package]]
name = "humanfriendly" name = "humanfriendly"
version = "10.0" version = "10.0"
@ -729,18 +670,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
] ]
[[package]]
name = "jinja2"
version = "3.1.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markupsafe" },
]
sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
]
[[package]] [[package]]
name = "jiter" name = "jiter"
version = "0.12.0" version = "0.12.0"
@ -818,7 +747,8 @@ name = "julia-ai-agent"
version = "1.0.0" version = "1.0.0"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "livekit-agents", extra = ["silero", "turn-detector"] }, { name = "aiohttp" },
{ 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 = "livekit-plugins-openai" },
@ -834,7 +764,8 @@ dev = [
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "livekit-agents", extras = ["silero", "turn-detector"], specifier = "~=1.3" }, { name = "aiohttp" },
{ 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 = "livekit-plugins-openai", specifier = "~=1.0" },
@ -919,9 +850,6 @@ images = [
silero = [ silero = [
{ name = "livekit-plugins-silero" }, { name = "livekit-plugins-silero" },
] ]
turn-detector = [
{ name = "livekit-plugins-turn-detector" },
]
[[package]] [[package]]
name = "livekit-api" name = "livekit-api"
@ -1023,23 +951,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/79/98/1fcd4baac07e4bbcc361f2b8da58438eee289309f58e86e10ce481b2bfe2/livekit_plugins_silero-1.3.11-py3-none-any.whl", hash = "sha256:8af33e3c6ede9dfdbe865595876765db0e3554dd05aa0ef3266b68523518a421", size = 3903694, upload-time = "2026-01-14T18:45:53.829Z" }, { url = "https://files.pythonhosted.org/packages/79/98/1fcd4baac07e4bbcc361f2b8da58438eee289309f58e86e10ce481b2bfe2/livekit_plugins_silero-1.3.11-py3-none-any.whl", hash = "sha256:8af33e3c6ede9dfdbe865595876765db0e3554dd05aa0ef3266b68523518a421", size = 3903694, upload-time = "2026-01-14T18:45:53.829Z" },
] ]
[[package]]
name = "livekit-plugins-turn-detector"
version = "1.3.11"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "jinja2" },
{ name = "livekit-agents" },
{ 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 = "onnxruntime" },
{ name = "transformers" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f0/6a/4029b3aadad86bffefceba43f46ba8eb0267aee65dbf5518a928a4928d9d/livekit_plugins_turn_detector-1.3.11.tar.gz", hash = "sha256:fc1c07d4e209e7e980f31a8441d3452cf330268fdff1a840843ce7d538f79f0e", size = 8666, upload-time = "2026-01-14T18:46:08.351Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/27/70/59293b81a83dea062a2f14726d2f59ce7dfea7bd22fa1ed096990cf2e2b5/livekit_plugins_turn_detector-1.3.11-py3-none-any.whl", hash = "sha256:810ab55c41031730cb9686148959e00fefb1b8b48ed896e1754516fede99c1ef", size = 10314, upload-time = "2026-01-14T18:46:07.54Z" },
]
[[package]] [[package]]
name = "livekit-protocol" name = "livekit-protocol"
version = "1.1.1" version = "1.1.1"
@ -1065,69 +976,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" },
] ]
[[package]]
name = "markupsafe"
version = "3.0.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" },
{ url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" },
{ url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" },
{ url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" },
{ url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" },
{ url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" },
{ url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" },
{ url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" },
{ url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" },
{ url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" },
{ url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" },
{ url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" },
{ url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" },
{ url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" },
{ url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" },
{ url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" },
{ url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" },
{ url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" },
{ url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" },
{ url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" },
{ url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" },
{ url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" },
{ url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" },
{ url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" },
{ url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" },
{ url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" },
{ url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" },
{ url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" },
{ url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" },
{ url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" },
{ url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" },
{ url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" },
{ url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" },
{ url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" },
{ url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" },
{ url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" },
{ url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" },
{ url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" },
{ url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" },
{ url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" },
{ url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" },
{ url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" },
{ url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" },
{ url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" },
{ url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" },
{ url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" },
{ url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" },
{ url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" },
{ url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" },
{ url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" },
{ url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" },
{ url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" },
{ url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" },
{ url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" },
{ url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" },
]
[[package]] [[package]]
name = "mdurl" name = "mdurl"
version = "0.1.2" version = "0.1.2"
@ -1962,141 +1810,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" },
] ]
[[package]]
name = "pyyaml"
version = "6.0.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" },
{ url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" },
{ url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" },
{ url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" },
{ url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" },
{ url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" },
{ url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" },
{ url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" },
{ url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" },
{ url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" },
{ url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" },
{ url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" },
{ url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" },
{ url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" },
{ url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" },
{ url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" },
{ url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" },
{ url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" },
{ url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" },
{ url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" },
{ url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" },
{ url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" },
{ url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" },
{ url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" },
{ url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" },
{ url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" },
{ url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" },
{ url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" },
{ url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
{ url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
{ url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
{ url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
{ url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
{ url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
{ url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
{ url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
{ url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
{ url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
]
[[package]]
name = "regex"
version = "2026.1.15"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/0b/86/07d5056945f9ec4590b518171c4254a5925832eb727b56d3c38a7476f316/regex-2026.1.15.tar.gz", hash = "sha256:164759aa25575cbc0651bef59a0b18353e54300d79ace8084c818ad8ac72b7d5", size = 414811, upload-time = "2026-01-14T23:18:02.775Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ea/d2/e6ee96b7dff201a83f650241c52db8e5bd080967cb93211f57aa448dc9d6/regex-2026.1.15-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4e3dd93c8f9abe8aa4b6c652016da9a3afa190df5ad822907efe6b206c09896e", size = 488166, upload-time = "2026-01-14T23:13:46.408Z" },
{ url = "https://files.pythonhosted.org/packages/23/8a/819e9ce14c9f87af026d0690901b3931f3101160833e5d4c8061fa3a1b67/regex-2026.1.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:97499ff7862e868b1977107873dd1a06e151467129159a6ffd07b66706ba3a9f", size = 290632, upload-time = "2026-01-14T23:13:48.688Z" },
{ url = "https://files.pythonhosted.org/packages/d5/c3/23dfe15af25d1d45b07dfd4caa6003ad710dcdcb4c4b279909bdfe7a2de8/regex-2026.1.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0bda75ebcac38d884240914c6c43d8ab5fb82e74cde6da94b43b17c411aa4c2b", size = 288500, upload-time = "2026-01-14T23:13:50.503Z" },
{ url = "https://files.pythonhosted.org/packages/c6/31/1adc33e2f717df30d2f4d973f8776d2ba6ecf939301efab29fca57505c95/regex-2026.1.15-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7dcc02368585334f5bc81fc73a2a6a0bbade60e7d83da21cead622faf408f32c", size = 781670, upload-time = "2026-01-14T23:13:52.453Z" },
{ url = "https://files.pythonhosted.org/packages/23/ce/21a8a22d13bc4adcb927c27b840c948f15fc973e21ed2346c1bd0eae22dc/regex-2026.1.15-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:693b465171707bbe882a7a05de5e866f33c76aa449750bee94a8d90463533cc9", size = 850820, upload-time = "2026-01-14T23:13:54.894Z" },
{ url = "https://files.pythonhosted.org/packages/6c/4f/3eeacdf587a4705a44484cd0b30e9230a0e602811fb3e2cc32268c70d509/regex-2026.1.15-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b0d190e6f013ea938623a58706d1469a62103fb2a241ce2873a9906e0386582c", size = 898777, upload-time = "2026-01-14T23:13:56.908Z" },
{ url = "https://files.pythonhosted.org/packages/79/a9/1898a077e2965c35fc22796488141a22676eed2d73701e37c73ad7c0b459/regex-2026.1.15-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ff818702440a5878a81886f127b80127f5d50563753a28211482867f8318106", size = 791750, upload-time = "2026-01-14T23:13:58.527Z" },
{ url = "https://files.pythonhosted.org/packages/4c/84/e31f9d149a178889b3817212827f5e0e8c827a049ff31b4b381e76b26e2d/regex-2026.1.15-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f052d1be37ef35a54e394de66136e30fa1191fab64f71fc06ac7bc98c9a84618", size = 782674, upload-time = "2026-01-14T23:13:59.874Z" },
{ url = "https://files.pythonhosted.org/packages/d2/ff/adf60063db24532add6a1676943754a5654dcac8237af024ede38244fd12/regex-2026.1.15-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6bfc31a37fd1592f0c4fc4bfc674b5c42e52efe45b4b7a6a14f334cca4bcebe4", size = 767906, upload-time = "2026-01-14T23:14:01.298Z" },
{ url = "https://files.pythonhosted.org/packages/af/3e/e6a216cee1e2780fec11afe7fc47b6f3925d7264e8149c607ac389fd9b1a/regex-2026.1.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3d6ce5ae80066b319ae3bc62fd55a557c9491baa5efd0d355f0de08c4ba54e79", size = 774798, upload-time = "2026-01-14T23:14:02.715Z" },
{ url = "https://files.pythonhosted.org/packages/0f/98/23a4a8378a9208514ed3efc7e7850c27fa01e00ed8557c958df0335edc4a/regex-2026.1.15-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:1704d204bd42b6bb80167df0e4554f35c255b579ba99616def38f69e14a5ccb9", size = 845861, upload-time = "2026-01-14T23:14:04.824Z" },
{ url = "https://files.pythonhosted.org/packages/f8/57/d7605a9d53bd07421a8785d349cd29677fe660e13674fa4c6cbd624ae354/regex-2026.1.15-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:e3174a5ed4171570dc8318afada56373aa9289eb6dc0d96cceb48e7358b0e220", size = 755648, upload-time = "2026-01-14T23:14:06.371Z" },
{ url = "https://files.pythonhosted.org/packages/6f/76/6f2e24aa192da1e299cc1101674a60579d3912391867ce0b946ba83e2194/regex-2026.1.15-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:87adf5bd6d72e3e17c9cb59ac4096b1faaf84b7eb3037a5ffa61c4b4370f0f13", size = 836250, upload-time = "2026-01-14T23:14:08.343Z" },
{ url = "https://files.pythonhosted.org/packages/11/3a/1f2a1d29453299a7858eab7759045fc3d9d1b429b088dec2dc85b6fa16a2/regex-2026.1.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e85dc94595f4d766bd7d872a9de5ede1ca8d3063f3bdf1e2c725f5eb411159e3", size = 779919, upload-time = "2026-01-14T23:14:09.954Z" },
{ url = "https://files.pythonhosted.org/packages/c0/67/eab9bc955c9dcc58e9b222c801e39cff7ca0b04261792a2149166ce7e792/regex-2026.1.15-cp310-cp310-win32.whl", hash = "sha256:21ca32c28c30d5d65fc9886ff576fc9b59bbca08933e844fa2363e530f4c8218", size = 265888, upload-time = "2026-01-14T23:14:11.35Z" },
{ url = "https://files.pythonhosted.org/packages/1d/62/31d16ae24e1f8803bddb0885508acecaec997fcdcde9c243787103119ae4/regex-2026.1.15-cp310-cp310-win_amd64.whl", hash = "sha256:3038a62fc7d6e5547b8915a3d927a0fbeef84cdbe0b1deb8c99bbd4a8961b52a", size = 277830, upload-time = "2026-01-14T23:14:12.908Z" },
{ url = "https://files.pythonhosted.org/packages/e5/36/5d9972bccd6417ecd5a8be319cebfd80b296875e7f116c37fb2a2deecebf/regex-2026.1.15-cp310-cp310-win_arm64.whl", hash = "sha256:505831646c945e3e63552cc1b1b9b514f0e93232972a2d5bedbcc32f15bc82e3", size = 270376, upload-time = "2026-01-14T23:14:14.782Z" },
{ url = "https://files.pythonhosted.org/packages/d0/c9/0c80c96eab96948363d270143138d671d5731c3a692b417629bf3492a9d6/regex-2026.1.15-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ae6020fb311f68d753b7efa9d4b9a5d47a5d6466ea0d5e3b5a471a960ea6e4a", size = 488168, upload-time = "2026-01-14T23:14:16.129Z" },
{ url = "https://files.pythonhosted.org/packages/17/f0/271c92f5389a552494c429e5cc38d76d1322eb142fb5db3c8ccc47751468/regex-2026.1.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:eddf73f41225942c1f994914742afa53dc0d01a6e20fe14b878a1b1edc74151f", size = 290636, upload-time = "2026-01-14T23:14:17.715Z" },
{ url = "https://files.pythonhosted.org/packages/a0/f9/5f1fd077d106ca5655a0f9ff8f25a1ab55b92128b5713a91ed7134ff688e/regex-2026.1.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e8cd52557603f5c66a548f69421310886b28b7066853089e1a71ee710e1cdc1", size = 288496, upload-time = "2026-01-14T23:14:19.326Z" },
{ url = "https://files.pythonhosted.org/packages/b5/e1/8f43b03a4968c748858ec77f746c286d81f896c2e437ccf050ebc5d3128c/regex-2026.1.15-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5170907244b14303edc5978f522f16c974f32d3aa92109fabc2af52411c9433b", size = 793503, upload-time = "2026-01-14T23:14:20.922Z" },
{ url = "https://files.pythonhosted.org/packages/8d/4e/a39a5e8edc5377a46a7c875c2f9a626ed3338cb3bb06931be461c3e1a34a/regex-2026.1.15-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2748c1ec0663580b4510bd89941a31560b4b439a0b428b49472a3d9944d11cd8", size = 860535, upload-time = "2026-01-14T23:14:22.405Z" },
{ url = "https://files.pythonhosted.org/packages/dc/1c/9dce667a32a9477f7a2869c1c767dc00727284a9fa3ff5c09a5c6c03575e/regex-2026.1.15-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2f2775843ca49360508d080eaa87f94fa248e2c946bbcd963bb3aae14f333413", size = 907225, upload-time = "2026-01-14T23:14:23.897Z" },
{ url = "https://files.pythonhosted.org/packages/a4/3c/87ca0a02736d16b6262921425e84b48984e77d8e4e572c9072ce96e66c30/regex-2026.1.15-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9ea2604370efc9a174c1b5dcc81784fb040044232150f7f33756049edfc9026", size = 800526, upload-time = "2026-01-14T23:14:26.039Z" },
{ url = "https://files.pythonhosted.org/packages/4b/ff/647d5715aeea7c87bdcbd2f578f47b415f55c24e361e639fe8c0cc88878f/regex-2026.1.15-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0dcd31594264029b57bf16f37fd7248a70b3b764ed9e0839a8f271b2d22c0785", size = 773446, upload-time = "2026-01-14T23:14:28.109Z" },
{ url = "https://files.pythonhosted.org/packages/af/89/bf22cac25cb4ba0fe6bff52ebedbb65b77a179052a9d6037136ae93f42f4/regex-2026.1.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c08c1f3e34338256732bd6938747daa3c0d5b251e04b6e43b5813e94d503076e", size = 783051, upload-time = "2026-01-14T23:14:29.929Z" },
{ url = "https://files.pythonhosted.org/packages/1e/f4/6ed03e71dca6348a5188363a34f5e26ffd5db1404780288ff0d79513bce4/regex-2026.1.15-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e43a55f378df1e7a4fa3547c88d9a5a9b7113f653a66821bcea4718fe6c58763", size = 854485, upload-time = "2026-01-14T23:14:31.366Z" },
{ url = "https://files.pythonhosted.org/packages/d9/9a/8e8560bd78caded8eb137e3e47612430a05b9a772caf60876435192d670a/regex-2026.1.15-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:f82110ab962a541737bd0ce87978d4c658f06e7591ba899192e2712a517badbb", size = 762195, upload-time = "2026-01-14T23:14:32.802Z" },
{ url = "https://files.pythonhosted.org/packages/38/6b/61fc710f9aa8dfcd764fe27d37edfaa023b1a23305a0d84fccd5adb346ea/regex-2026.1.15-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:27618391db7bdaf87ac6c92b31e8f0dfb83a9de0075855152b720140bda177a2", size = 845986, upload-time = "2026-01-14T23:14:34.898Z" },
{ url = "https://files.pythonhosted.org/packages/fd/2e/fbee4cb93f9d686901a7ca8d94285b80405e8c34fe4107f63ffcbfb56379/regex-2026.1.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bfb0d6be01fbae8d6655c8ca21b3b72458606c4aec9bbc932db758d47aba6db1", size = 788992, upload-time = "2026-01-14T23:14:37.116Z" },
{ url = "https://files.pythonhosted.org/packages/ed/14/3076348f3f586de64b1ab75a3fbabdaab7684af7f308ad43be7ef1849e55/regex-2026.1.15-cp311-cp311-win32.whl", hash = "sha256:b10e42a6de0e32559a92f2f8dc908478cc0fa02838d7dbe764c44dca3fa13569", size = 265893, upload-time = "2026-01-14T23:14:38.426Z" },
{ url = "https://files.pythonhosted.org/packages/0f/19/772cf8b5fc803f5c89ba85d8b1870a1ca580dc482aa030383a9289c82e44/regex-2026.1.15-cp311-cp311-win_amd64.whl", hash = "sha256:e9bf3f0bbdb56633c07d7116ae60a576f846efdd86a8848f8d62b749e1209ca7", size = 277840, upload-time = "2026-01-14T23:14:39.785Z" },
{ url = "https://files.pythonhosted.org/packages/78/84/d05f61142709474da3c0853222d91086d3e1372bcdab516c6fd8d80f3297/regex-2026.1.15-cp311-cp311-win_arm64.whl", hash = "sha256:41aef6f953283291c4e4e6850607bd71502be67779586a61472beacb315c97ec", size = 270374, upload-time = "2026-01-14T23:14:41.592Z" },
{ url = "https://files.pythonhosted.org/packages/92/81/10d8cf43c807d0326efe874c1b79f22bfb0fb226027b0b19ebc26d301408/regex-2026.1.15-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4c8fcc5793dde01641a35905d6731ee1548f02b956815f8f1cab89e515a5bdf1", size = 489398, upload-time = "2026-01-14T23:14:43.741Z" },
{ url = "https://files.pythonhosted.org/packages/90/b0/7c2a74e74ef2a7c32de724658a69a862880e3e4155cba992ba04d1c70400/regex-2026.1.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bfd876041a956e6a90ad7cdb3f6a630c07d491280bfeed4544053cd434901681", size = 291339, upload-time = "2026-01-14T23:14:45.183Z" },
{ url = "https://files.pythonhosted.org/packages/19/4d/16d0773d0c818417f4cc20aa0da90064b966d22cd62a8c46765b5bd2d643/regex-2026.1.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9250d087bc92b7d4899ccd5539a1b2334e44eee85d848c4c1aef8e221d3f8c8f", size = 289003, upload-time = "2026-01-14T23:14:47.25Z" },
{ url = "https://files.pythonhosted.org/packages/c6/e4/1fc4599450c9f0863d9406e944592d968b8d6dfd0d552a7d569e43bceada/regex-2026.1.15-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8a154cf6537ebbc110e24dabe53095e714245c272da9c1be05734bdad4a61aa", size = 798656, upload-time = "2026-01-14T23:14:48.77Z" },
{ url = "https://files.pythonhosted.org/packages/b2/e6/59650d73a73fa8a60b3a590545bfcf1172b4384a7df2e7fe7b9aab4e2da9/regex-2026.1.15-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8050ba2e3ea1d8731a549e83c18d2f0999fbc99a5f6bd06b4c91449f55291804", size = 864252, upload-time = "2026-01-14T23:14:50.528Z" },
{ url = "https://files.pythonhosted.org/packages/6e/ab/1d0f4d50a1638849a97d731364c9a80fa304fec46325e48330c170ee8e80/regex-2026.1.15-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf065240704cb8951cc04972cf107063917022511273e0969bdb34fc173456c", size = 912268, upload-time = "2026-01-14T23:14:52.952Z" },
{ url = "https://files.pythonhosted.org/packages/dd/df/0d722c030c82faa1d331d1921ee268a4e8fb55ca8b9042c9341c352f17fa/regex-2026.1.15-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c32bef3e7aeee75746748643667668ef941d28b003bfc89994ecf09a10f7a1b5", size = 803589, upload-time = "2026-01-14T23:14:55.182Z" },
{ url = "https://files.pythonhosted.org/packages/66/23/33289beba7ccb8b805c6610a8913d0131f834928afc555b241caabd422a9/regex-2026.1.15-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d5eaa4a4c5b1906bd0d2508d68927f15b81821f85092e06f1a34a4254b0e1af3", size = 775700, upload-time = "2026-01-14T23:14:56.707Z" },
{ url = "https://files.pythonhosted.org/packages/e7/65/bf3a42fa6897a0d3afa81acb25c42f4b71c274f698ceabd75523259f6688/regex-2026.1.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:86c1077a3cc60d453d4084d5b9649065f3bf1184e22992bd322e1f081d3117fb", size = 787928, upload-time = "2026-01-14T23:14:58.312Z" },
{ url = "https://files.pythonhosted.org/packages/f4/f5/13bf65864fc314f68cdd6d8ca94adcab064d4d39dbd0b10fef29a9da48fc/regex-2026.1.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:2b091aefc05c78d286657cd4db95f2e6313375ff65dcf085e42e4c04d9c8d410", size = 858607, upload-time = "2026-01-14T23:15:00.657Z" },
{ url = "https://files.pythonhosted.org/packages/a3/31/040e589834d7a439ee43fb0e1e902bc81bd58a5ba81acffe586bb3321d35/regex-2026.1.15-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:57e7d17f59f9ebfa9667e6e5a1c0127b96b87cb9cede8335482451ed00788ba4", size = 763729, upload-time = "2026-01-14T23:15:02.248Z" },
{ url = "https://files.pythonhosted.org/packages/9b/84/6921e8129687a427edf25a34a5594b588b6d88f491320b9de5b6339a4fcb/regex-2026.1.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:c6c4dcdfff2c08509faa15d36ba7e5ef5fcfab25f1e8f85a0c8f45bc3a30725d", size = 850697, upload-time = "2026-01-14T23:15:03.878Z" },
{ url = "https://files.pythonhosted.org/packages/8a/87/3d06143d4b128f4229158f2de5de6c8f2485170c7221e61bf381313314b2/regex-2026.1.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cf8ff04c642716a7f2048713ddc6278c5fd41faa3b9cab12607c7abecd012c22", size = 789849, upload-time = "2026-01-14T23:15:06.102Z" },
{ url = "https://files.pythonhosted.org/packages/77/69/c50a63842b6bd48850ebc7ab22d46e7a2a32d824ad6c605b218441814639/regex-2026.1.15-cp312-cp312-win32.whl", hash = "sha256:82345326b1d8d56afbe41d881fdf62f1926d7264b2fc1537f99ae5da9aad7913", size = 266279, upload-time = "2026-01-14T23:15:07.678Z" },
{ url = "https://files.pythonhosted.org/packages/f2/36/39d0b29d087e2b11fd8191e15e81cce1b635fcc845297c67f11d0d19274d/regex-2026.1.15-cp312-cp312-win_amd64.whl", hash = "sha256:4def140aa6156bc64ee9912383d4038f3fdd18fee03a6f222abd4de6357ce42a", size = 277166, upload-time = "2026-01-14T23:15:09.257Z" },
{ url = "https://files.pythonhosted.org/packages/28/32/5b8e476a12262748851fa8ab1b0be540360692325975b094e594dfebbb52/regex-2026.1.15-cp312-cp312-win_arm64.whl", hash = "sha256:c6c565d9a6e1a8d783c1948937ffc377dd5771e83bd56de8317c450a954d2056", size = 270415, upload-time = "2026-01-14T23:15:10.743Z" },
{ url = "https://files.pythonhosted.org/packages/f8/2e/6870bb16e982669b674cce3ee9ff2d1d46ab80528ee6bcc20fb2292efb60/regex-2026.1.15-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e69d0deeb977ffe7ed3d2e4439360089f9c3f217ada608f0f88ebd67afb6385e", size = 489164, upload-time = "2026-01-14T23:15:13.962Z" },
{ url = "https://files.pythonhosted.org/packages/dc/67/9774542e203849b0286badf67199970a44ebdb0cc5fb739f06e47ada72f8/regex-2026.1.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3601ffb5375de85a16f407854d11cca8fe3f5febbe3ac78fb2866bb220c74d10", size = 291218, upload-time = "2026-01-14T23:15:15.647Z" },
{ url = "https://files.pythonhosted.org/packages/b2/87/b0cda79f22b8dee05f774922a214da109f9a4c0eca5da2c9d72d77ea062c/regex-2026.1.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4c5ef43b5c2d4114eb8ea424bb8c9cec01d5d17f242af88b2448f5ee81caadbc", size = 288895, upload-time = "2026-01-14T23:15:17.788Z" },
{ url = "https://files.pythonhosted.org/packages/3b/6a/0041f0a2170d32be01ab981d6346c83a8934277d82c780d60b127331f264/regex-2026.1.15-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:968c14d4f03e10b2fd960f1d5168c1f0ac969381d3c1fcc973bc45fb06346599", size = 798680, upload-time = "2026-01-14T23:15:19.342Z" },
{ url = "https://files.pythonhosted.org/packages/58/de/30e1cfcdbe3e891324aa7568b7c968771f82190df5524fabc1138cb2d45a/regex-2026.1.15-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56a5595d0f892f214609c9f76b41b7428bed439d98dc961efafdd1354d42baae", size = 864210, upload-time = "2026-01-14T23:15:22.005Z" },
{ url = "https://files.pythonhosted.org/packages/64/44/4db2f5c5ca0ccd40ff052ae7b1e9731352fcdad946c2b812285a7505ca75/regex-2026.1.15-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf650f26087363434c4e560011f8e4e738f6f3e029b85d4904c50135b86cfa5", size = 912358, upload-time = "2026-01-14T23:15:24.569Z" },
{ url = "https://files.pythonhosted.org/packages/79/b6/e6a5665d43a7c42467138c8a2549be432bad22cbd206f5ec87162de74bd7/regex-2026.1.15-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18388a62989c72ac24de75f1449d0fb0b04dfccd0a1a7c1c43af5eb503d890f6", size = 803583, upload-time = "2026-01-14T23:15:26.526Z" },
{ url = "https://files.pythonhosted.org/packages/e7/53/7cd478222169d85d74d7437e74750005e993f52f335f7c04ff7adfda3310/regex-2026.1.15-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6d220a2517f5893f55daac983bfa9fe998a7dbcaee4f5d27a88500f8b7873788", size = 775782, upload-time = "2026-01-14T23:15:29.352Z" },
{ url = "https://files.pythonhosted.org/packages/ca/b5/75f9a9ee4b03a7c009fe60500fe550b45df94f0955ca29af16333ef557c5/regex-2026.1.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9c08c2fbc6120e70abff5d7f28ffb4d969e14294fb2143b4b5c7d20e46d1714", size = 787978, upload-time = "2026-01-14T23:15:31.295Z" },
{ url = "https://files.pythonhosted.org/packages/72/b3/79821c826245bbe9ccbb54f6eadb7879c722fd3e0248c17bfc90bf54e123/regex-2026.1.15-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7ef7d5d4bd49ec7364315167a4134a015f61e8266c6d446fc116a9ac4456e10d", size = 858550, upload-time = "2026-01-14T23:15:33.558Z" },
{ url = "https://files.pythonhosted.org/packages/4a/85/2ab5f77a1c465745bfbfcb3ad63178a58337ae8d5274315e2cc623a822fa/regex-2026.1.15-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6e42844ad64194fa08d5ccb75fe6a459b9b08e6d7296bd704460168d58a388f3", size = 763747, upload-time = "2026-01-14T23:15:35.206Z" },
{ url = "https://files.pythonhosted.org/packages/6d/84/c27df502d4bfe2873a3e3a7cf1bdb2b9cc10284d1a44797cf38bed790470/regex-2026.1.15-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:cfecdaa4b19f9ca534746eb3b55a5195d5c95b88cac32a205e981ec0a22b7d31", size = 850615, upload-time = "2026-01-14T23:15:37.523Z" },
{ url = "https://files.pythonhosted.org/packages/7d/b7/658a9782fb253680aa8ecb5ccbb51f69e088ed48142c46d9f0c99b46c575/regex-2026.1.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:08df9722d9b87834a3d701f3fca570b2be115654dbfd30179f30ab2f39d606d3", size = 789951, upload-time = "2026-01-14T23:15:39.582Z" },
{ url = "https://files.pythonhosted.org/packages/fc/2a/5928af114441e059f15b2f63e188bd00c6529b3051c974ade7444b85fcda/regex-2026.1.15-cp313-cp313-win32.whl", hash = "sha256:d426616dae0967ca225ab12c22274eb816558f2f99ccb4a1d52ca92e8baf180f", size = 266275, upload-time = "2026-01-14T23:15:42.108Z" },
{ url = "https://files.pythonhosted.org/packages/4f/16/5bfbb89e435897bff28cf0352a992ca719d9e55ebf8b629203c96b6ce4f7/regex-2026.1.15-cp313-cp313-win_amd64.whl", hash = "sha256:febd38857b09867d3ed3f4f1af7d241c5c50362e25ef43034995b77a50df494e", size = 277145, upload-time = "2026-01-14T23:15:44.244Z" },
{ url = "https://files.pythonhosted.org/packages/56/c1/a09ff7392ef4233296e821aec5f78c51be5e91ffde0d163059e50fd75835/regex-2026.1.15-cp313-cp313-win_arm64.whl", hash = "sha256:8e32f7896f83774f91499d239e24cebfadbc07639c1494bb7213983842348337", size = 270411, upload-time = "2026-01-14T23:15:45.858Z" },
{ url = "https://files.pythonhosted.org/packages/3c/38/0cfd5a78e5c6db00e6782fdae70458f89850ce95baa5e8694ab91d89744f/regex-2026.1.15-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ec94c04149b6a7b8120f9f44565722c7ae31b7a6d2275569d2eefa76b83da3be", size = 492068, upload-time = "2026-01-14T23:15:47.616Z" },
{ url = "https://files.pythonhosted.org/packages/50/72/6c86acff16cb7c959c4355826bbf06aad670682d07c8f3998d9ef4fee7cd/regex-2026.1.15-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40c86d8046915bb9aeb15d3f3f15b6fd500b8ea4485b30e1bbc799dab3fe29f8", size = 292756, upload-time = "2026-01-14T23:15:49.307Z" },
{ url = "https://files.pythonhosted.org/packages/4e/58/df7fb69eadfe76526ddfce28abdc0af09ffe65f20c2c90932e89d705153f/regex-2026.1.15-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:726ea4e727aba21643205edad8f2187ec682d3305d790f73b7a51c7587b64bdd", size = 291114, upload-time = "2026-01-14T23:15:51.484Z" },
{ url = "https://files.pythonhosted.org/packages/ed/6c/a4011cd1cf96b90d2cdc7e156f91efbd26531e822a7fbb82a43c1016678e/regex-2026.1.15-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1cb740d044aff31898804e7bf1181cc72c03d11dfd19932b9911ffc19a79070a", size = 807524, upload-time = "2026-01-14T23:15:53.102Z" },
{ url = "https://files.pythonhosted.org/packages/1d/25/a53ffb73183f69c3e9f4355c4922b76d2840aee160af6af5fac229b6201d/regex-2026.1.15-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05d75a668e9ea16f832390d22131fe1e8acc8389a694c8febc3e340b0f810b93", size = 873455, upload-time = "2026-01-14T23:15:54.956Z" },
{ url = "https://files.pythonhosted.org/packages/66/0b/8b47fc2e8f97d9b4a851736f3890a5f786443aa8901061c55f24c955f45b/regex-2026.1.15-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d991483606f3dbec93287b9f35596f41aa2e92b7c2ebbb935b63f409e243c9af", size = 915007, upload-time = "2026-01-14T23:15:57.041Z" },
{ url = "https://files.pythonhosted.org/packages/c2/fa/97de0d681e6d26fabe71968dbee06dd52819e9a22fdce5dac7256c31ed84/regex-2026.1.15-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:194312a14819d3e44628a44ed6fea6898fdbecb0550089d84c403475138d0a09", size = 812794, upload-time = "2026-01-14T23:15:58.916Z" },
{ url = "https://files.pythonhosted.org/packages/22/38/e752f94e860d429654aa2b1c51880bff8dfe8f084268258adf9151cf1f53/regex-2026.1.15-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe2fda4110a3d0bc163c2e0664be44657431440722c5c5315c65155cab92f9e5", size = 781159, upload-time = "2026-01-14T23:16:00.817Z" },
{ url = "https://files.pythonhosted.org/packages/e9/a7/d739ffaef33c378fc888302a018d7f81080393d96c476b058b8c64fd2b0d/regex-2026.1.15-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:124dc36c85d34ef2d9164da41a53c1c8c122cfb1f6e1ec377a1f27ee81deb794", size = 795558, upload-time = "2026-01-14T23:16:03.267Z" },
{ url = "https://files.pythonhosted.org/packages/3e/c4/542876f9a0ac576100fc73e9c75b779f5c31e3527576cfc9cb3009dcc58a/regex-2026.1.15-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1774cd1981cd212506a23a14dba7fdeaee259f5deba2df6229966d9911e767a", size = 868427, upload-time = "2026-01-14T23:16:05.646Z" },
{ url = "https://files.pythonhosted.org/packages/fc/0f/d5655bea5b22069e32ae85a947aa564912f23758e112cdb74212848a1a1b/regex-2026.1.15-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:b5f7d8d2867152cdb625e72a530d2ccb48a3d199159144cbdd63870882fb6f80", size = 769939, upload-time = "2026-01-14T23:16:07.542Z" },
{ url = "https://files.pythonhosted.org/packages/20/06/7e18a4fa9d326daeda46d471a44ef94201c46eaa26dbbb780b5d92cbfdda/regex-2026.1.15-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:492534a0ab925d1db998defc3c302dae3616a2fc3fe2e08db1472348f096ddf2", size = 854753, upload-time = "2026-01-14T23:16:10.395Z" },
{ url = "https://files.pythonhosted.org/packages/3b/67/dc8946ef3965e166f558ef3b47f492bc364e96a265eb4a2bb3ca765c8e46/regex-2026.1.15-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c661fc820cfb33e166bf2450d3dadbda47c8d8981898adb9b6fe24e5e582ba60", size = 799559, upload-time = "2026-01-14T23:16:12.347Z" },
{ url = "https://files.pythonhosted.org/packages/a5/61/1bba81ff6d50c86c65d9fd84ce9699dd106438ee4cdb105bf60374ee8412/regex-2026.1.15-cp313-cp313t-win32.whl", hash = "sha256:99ad739c3686085e614bf77a508e26954ff1b8f14da0e3765ff7abbf7799f952", size = 268879, upload-time = "2026-01-14T23:16:14.049Z" },
{ url = "https://files.pythonhosted.org/packages/e9/5e/cef7d4c5fb0ea3ac5c775fd37db5747f7378b29526cc83f572198924ff47/regex-2026.1.15-cp313-cp313t-win_amd64.whl", hash = "sha256:32655d17905e7ff8ba5c764c43cb124e34a9245e45b83c22e81041e1071aee10", size = 280317, upload-time = "2026-01-14T23:16:15.718Z" },
{ url = "https://files.pythonhosted.org/packages/b4/52/4317f7a5988544e34ab57b4bde0f04944c4786128c933fb09825924d3e82/regex-2026.1.15-cp313-cp313t-win_arm64.whl", hash = "sha256:b2a13dd6a95e95a489ca242319d18fc02e07ceb28fa9ad146385194d95b3c829", size = 271551, upload-time = "2026-01-14T23:16:17.533Z" },
]
[[package]] [[package]]
name = "requests" name = "requests"
version = "2.32.5" version = "2.32.5"
@ -2151,32 +1864,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/4d/e1/7348090988095e4e39560cfc2f7555b1b2a7357deba19167b600fdf5215d/ruff-0.14.13-py3-none-win_arm64.whl", hash = "sha256:7ab819e14f1ad9fe39f246cfcc435880ef7a9390d81a2b6ac7e01039083dd247", size = 13080224, upload-time = "2026-01-15T20:14:45.853Z" }, { url = "https://files.pythonhosted.org/packages/4d/e1/7348090988095e4e39560cfc2f7555b1b2a7357deba19167b600fdf5215d/ruff-0.14.13-py3-none-win_arm64.whl", hash = "sha256:7ab819e14f1ad9fe39f246cfcc435880ef7a9390d81a2b6ac7e01039083dd247", size = 13080224, upload-time = "2026-01-15T20:14:45.853Z" },
] ]
[[package]]
name = "safetensors"
version = "0.7.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/29/9c/6e74567782559a63bd040a236edca26fd71bc7ba88de2ef35d75df3bca5e/safetensors-0.7.0.tar.gz", hash = "sha256:07663963b67e8bd9f0b8ad15bb9163606cd27cc5a1b96235a50d8369803b96b0", size = 200878, upload-time = "2025-11-19T15:18:43.199Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fa/47/aef6c06649039accf914afef490268e1067ed82be62bcfa5b7e886ad15e8/safetensors-0.7.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c82f4d474cf725255d9e6acf17252991c3c8aac038d6ef363a4bf8be2f6db517", size = 467781, upload-time = "2025-11-19T15:18:35.84Z" },
{ url = "https://files.pythonhosted.org/packages/e8/00/374c0c068e30cd31f1e1b46b4b5738168ec79e7689ca82ee93ddfea05109/safetensors-0.7.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:94fd4858284736bb67a897a41608b5b0c2496c9bdb3bf2af1fa3409127f20d57", size = 447058, upload-time = "2025-11-19T15:18:34.416Z" },
{ url = "https://files.pythonhosted.org/packages/f1/06/578ffed52c2296f93d7fd2d844cabfa92be51a587c38c8afbb8ae449ca89/safetensors-0.7.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e07d91d0c92a31200f25351f4acb2bc6aff7f48094e13ebb1d0fb995b54b6542", size = 491748, upload-time = "2025-11-19T15:18:09.79Z" },
{ url = "https://files.pythonhosted.org/packages/ae/33/1debbbb70e4791dde185edb9413d1fe01619255abb64b300157d7f15dddd/safetensors-0.7.0-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8469155f4cb518bafb4acf4865e8bb9d6804110d2d9bdcaa78564b9fd841e104", size = 503881, upload-time = "2025-11-19T15:18:16.145Z" },
{ url = "https://files.pythonhosted.org/packages/8e/1c/40c2ca924d60792c3be509833df711b553c60effbd91da6f5284a83f7122/safetensors-0.7.0-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:54bef08bf00a2bff599982f6b08e8770e09cc012d7bba00783fc7ea38f1fb37d", size = 623463, upload-time = "2025-11-19T15:18:21.11Z" },
{ url = "https://files.pythonhosted.org/packages/9b/3a/13784a9364bd43b0d61eef4bea2845039bc2030458b16594a1bd787ae26e/safetensors-0.7.0-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42cb091236206bb2016d245c377ed383aa7f78691748f3bb6ee1bfa51ae2ce6a", size = 532855, upload-time = "2025-11-19T15:18:25.719Z" },
{ url = "https://files.pythonhosted.org/packages/a0/60/429e9b1cb3fc651937727befe258ea24122d9663e4d5709a48c9cbfceecb/safetensors-0.7.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac7252938f0696ddea46f5e855dd3138444e82236e3be475f54929f0c510d48", size = 507152, upload-time = "2025-11-19T15:18:33.023Z" },
{ url = "https://files.pythonhosted.org/packages/3c/a8/4b45e4e059270d17af60359713ffd83f97900d45a6afa73aaa0d737d48b6/safetensors-0.7.0-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1d060c70284127fa805085d8f10fbd0962792aed71879d00864acda69dbab981", size = 541856, upload-time = "2025-11-19T15:18:31.075Z" },
{ url = "https://files.pythonhosted.org/packages/06/87/d26d8407c44175d8ae164a95b5a62707fcc445f3c0c56108e37d98070a3d/safetensors-0.7.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cdab83a366799fa730f90a4ebb563e494f28e9e92c4819e556152ad55e43591b", size = 674060, upload-time = "2025-11-19T15:18:37.211Z" },
{ url = "https://files.pythonhosted.org/packages/11/f5/57644a2ff08dc6325816ba7217e5095f17269dada2554b658442c66aed51/safetensors-0.7.0-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:672132907fcad9f2aedcb705b2d7b3b93354a2aec1b2f706c4db852abe338f85", size = 771715, upload-time = "2025-11-19T15:18:38.689Z" },
{ url = "https://files.pythonhosted.org/packages/86/31/17883e13a814bd278ae6e266b13282a01049b0c81341da7fd0e3e71a80a3/safetensors-0.7.0-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:5d72abdb8a4d56d4020713724ba81dac065fedb7f3667151c4a637f1d3fb26c0", size = 714377, upload-time = "2025-11-19T15:18:40.162Z" },
{ url = "https://files.pythonhosted.org/packages/4a/d8/0c8a7dc9b41dcac53c4cbf9df2b9c83e0e0097203de8b37a712b345c0be5/safetensors-0.7.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b0f6d66c1c538d5a94a73aa9ddca8ccc4227e6c9ff555322ea40bdd142391dd4", size = 677368, upload-time = "2025-11-19T15:18:41.627Z" },
{ url = "https://files.pythonhosted.org/packages/05/e5/cb4b713c8a93469e3c5be7c3f8d77d307e65fe89673e731f5c2bfd0a9237/safetensors-0.7.0-cp38-abi3-win32.whl", hash = "sha256:c74af94bf3ac15ac4d0f2a7c7b4663a15f8c2ab15ed0fc7531ca61d0835eccba", size = 326423, upload-time = "2025-11-19T15:18:45.74Z" },
{ url = "https://files.pythonhosted.org/packages/5d/e6/ec8471c8072382cb91233ba7267fd931219753bb43814cbc71757bfd4dab/safetensors-0.7.0-cp38-abi3-win_amd64.whl", hash = "sha256:d1239932053f56f3456f32eb9625590cc7582e905021f94636202a864d470755", size = 341380, upload-time = "2025-11-19T15:18:44.427Z" },
{ url = "https://files.pythonhosted.org/packages/a7/6a/4d08d89a6fcbe905c5ae68b8b34f0791850882fc19782d0d02c65abbdf3b/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4729811a6640d019a4b7ba8638ee2fd21fa5ca8c7e7bdf0fed62068fcaac737", size = 492430, upload-time = "2025-11-19T15:18:11.884Z" },
{ url = "https://files.pythonhosted.org/packages/dd/29/59ed8152b30f72c42d00d241e58eaca558ae9dbfa5695206e2e0f54c7063/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12f49080303fa6bb424b362149a12949dfbbf1e06811a88f2307276b0c131afd", size = 503977, upload-time = "2025-11-19T15:18:17.523Z" },
{ url = "https://files.pythonhosted.org/packages/d3/0b/4811bfec67fa260e791369b16dab105e4bae82686120554cc484064e22b4/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0071bffba4150c2f46cae1432d31995d77acfd9f8db598b5d1a2ce67e8440ad2", size = 623890, upload-time = "2025-11-19T15:18:22.666Z" },
{ url = "https://files.pythonhosted.org/packages/58/5b/632a58724221ef03d78ab65062e82a1010e1bef8e8e0b9d7c6d7b8044841/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:473b32699f4200e69801bf5abf93f1a4ecd432a70984df164fc22ccf39c4a6f3", size = 531885, upload-time = "2025-11-19T15:18:27.146Z" },
]
[[package]] [[package]]
name = "shellingham" name = "shellingham"
version = "1.5.4" version = "1.5.4"
@ -2222,36 +1909,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" },
] ]
[[package]]
name = "tokenizers"
version = "0.22.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "huggingface-hub" },
]
sdist = { url = "https://files.pythonhosted.org/packages/73/6f/f80cfef4a312e1fb34baf7d85c72d4411afde10978d4657f8cdd811d3ccc/tokenizers-0.22.2.tar.gz", hash = "sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917", size = 372115, upload-time = "2026-01-05T10:45:15.988Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/92/97/5dbfabf04c7e348e655e907ed27913e03db0923abb5dfdd120d7b25630e1/tokenizers-0.22.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:544dd704ae7238755d790de45ba8da072e9af3eea688f698b137915ae959281c", size = 3100275, upload-time = "2026-01-05T10:41:02.158Z" },
{ url = "https://files.pythonhosted.org/packages/2e/47/174dca0502ef88b28f1c9e06b73ce33500eedfac7a7692108aec220464e7/tokenizers-0.22.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001", size = 2981472, upload-time = "2026-01-05T10:41:00.276Z" },
{ url = "https://files.pythonhosted.org/packages/d6/84/7990e799f1309a8b87af6b948f31edaa12a3ed22d11b352eaf4f4b2e5753/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7", size = 3290736, upload-time = "2026-01-05T10:40:32.165Z" },
{ url = "https://files.pythonhosted.org/packages/78/59/09d0d9ba94dcd5f4f1368d4858d24546b4bdc0231c2354aa31d6199f0399/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd", size = 3168835, upload-time = "2026-01-05T10:40:38.847Z" },
{ url = "https://files.pythonhosted.org/packages/47/50/b3ebb4243e7160bda8d34b731e54dd8ab8b133e50775872e7a434e524c28/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfb88f22a209ff7b40a576d5324bf8286b519d7358663db21d6246fb17eea2d5", size = 3521673, upload-time = "2026-01-05T10:40:56.614Z" },
{ url = "https://files.pythonhosted.org/packages/e0/fa/89f4cb9e08df770b57adb96f8cbb7e22695a4cb6c2bd5f0c4f0ebcf33b66/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e", size = 3724818, upload-time = "2026-01-05T10:40:44.507Z" },
{ url = "https://files.pythonhosted.org/packages/64/04/ca2363f0bfbe3b3d36e95bf67e56a4c88c8e3362b658e616d1ac185d47f2/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b", size = 3379195, upload-time = "2026-01-05T10:40:51.139Z" },
{ url = "https://files.pythonhosted.org/packages/2e/76/932be4b50ef6ccedf9d3c6639b056a967a86258c6d9200643f01269211ca/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67", size = 3274982, upload-time = "2026-01-05T10:40:58.331Z" },
{ url = "https://files.pythonhosted.org/packages/1d/28/5f9f5a4cc211b69e89420980e483831bcc29dade307955cc9dc858a40f01/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4", size = 9478245, upload-time = "2026-01-05T10:41:04.053Z" },
{ url = "https://files.pythonhosted.org/packages/6c/fb/66e2da4704d6aadebf8cb39f1d6d1957df667ab24cff2326b77cda0dcb85/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a", size = 9560069, upload-time = "2026-01-05T10:45:10.673Z" },
{ url = "https://files.pythonhosted.org/packages/16/04/fed398b05caa87ce9b1a1bb5166645e38196081b225059a6edaff6440fac/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:791135ee325f2336f498590eb2f11dc5c295232f288e75c99a36c5dbce63088a", size = 9899263, upload-time = "2026-01-05T10:45:12.559Z" },
{ url = "https://files.pythonhosted.org/packages/05/a1/d62dfe7376beaaf1394917e0f8e93ee5f67fea8fcf4107501db35996586b/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5", size = 10033429, upload-time = "2026-01-05T10:45:14.333Z" },
{ url = "https://files.pythonhosted.org/packages/fd/18/a545c4ea42af3df6effd7d13d250ba77a0a86fb20393143bbb9a92e434d4/tokenizers-0.22.2-cp39-abi3-win32.whl", hash = "sha256:a6bf3f88c554a2b653af81f3204491c818ae2ac6fbc09e76ef4773351292bc92", size = 2502363, upload-time = "2026-01-05T10:45:20.593Z" },
{ url = "https://files.pythonhosted.org/packages/65/71/0670843133a43d43070abeb1949abfdef12a86d490bea9cd9e18e37c5ff7/tokenizers-0.22.2-cp39-abi3-win_amd64.whl", hash = "sha256:c9ea31edff2968b44a88f97d784c2f16dc0729b8b143ed004699ebca91f05c48", size = 2747786, upload-time = "2026-01-05T10:45:18.411Z" },
{ url = "https://files.pythonhosted.org/packages/72/f4/0de46cfa12cdcbcd464cc59fde36912af405696f687e53a091fb432f694c/tokenizers-0.22.2-cp39-abi3-win_arm64.whl", hash = "sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc", size = 2612133, upload-time = "2026-01-05T10:45:17.232Z" },
{ url = "https://files.pythonhosted.org/packages/84/04/655b79dbcc9b3ac5f1479f18e931a344af67e5b7d3b251d2dcdcd7558592/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:753d47ebd4542742ef9261d9da92cd545b2cacbb48349a1225466745bb866ec4", size = 3282301, upload-time = "2026-01-05T10:40:34.858Z" },
{ url = "https://files.pythonhosted.org/packages/46/cd/e4851401f3d8f6f45d8480262ab6a5c8cb9c4302a790a35aa14eeed6d2fd/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e10bf9113d209be7cd046d40fbabbaf3278ff6d18eb4da4c500443185dc1896c", size = 3161308, upload-time = "2026-01-05T10:40:40.737Z" },
{ url = "https://files.pythonhosted.org/packages/6f/6e/55553992a89982cd12d4a66dddb5e02126c58677ea3931efcbe601d419db/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:64d94e84f6660764e64e7e0b22baa72f6cd942279fdbb21d46abd70d179f0195", size = 3718964, upload-time = "2026-01-05T10:40:46.56Z" },
{ url = "https://files.pythonhosted.org/packages/59/8c/b1c87148aa15e099243ec9f0cf9d0e970cc2234c3257d558c25a2c5304e6/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f01a9c019878532f98927d2bacb79bbb404b43d3437455522a00a30718cdedb5", size = 3373542, upload-time = "2026-01-05T10:40:52.803Z" },
]
[[package]] [[package]]
name = "tomli" name = "tomli"
version = "2.4.0" version = "2.4.0"
@ -2300,28 +1957,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" },
] ]
[[package]]
name = "transformers"
version = "4.57.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "filelock" },
{ name = "huggingface-hub" },
{ 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 = "packaging" },
{ name = "pyyaml" },
{ name = "regex" },
{ name = "requests" },
{ name = "safetensors" },
{ name = "tokenizers" },
{ name = "tqdm" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d6/68/a39307bcc4116a30b2106f2e689130a48de8bd8a1e635b5e1030e46fcd9e/transformers-4.57.1.tar.gz", hash = "sha256:f06c837959196c75039809636cd964b959f6604b75b8eeec6fdfc0440b89cc55", size = 10142511, upload-time = "2025-10-14T15:39:26.18Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/71/d3/c16c3b3cf7655a67db1144da94b021c200ac1303f82428f2beef6c2e72bb/transformers-4.57.1-py3-none-any.whl", hash = "sha256:b10d05da8fa67dc41644dbbf9bc45a44cb86ae33da6f9295f5fbf5b7890bd267", size = 11990925, upload-time = "2025-10-14T15:39:23.085Z" },
]
[[package]] [[package]]
name = "typer" name = "typer"
version = "0.21.1" version = "0.21.1"

39
package-lock.json generated
View File

@ -19,6 +19,7 @@
"expo": "~54.0.29", "expo": "~54.0.29",
"expo-clipboard": "~8.0.8", "expo-clipboard": "~8.0.8",
"expo-constants": "~18.0.12", "expo-constants": "~18.0.12",
"expo-device": "~8.0.10",
"expo-file-system": "~19.0.21", "expo-file-system": "~19.0.21",
"expo-font": "~14.0.10", "expo-font": "~14.0.10",
"expo-haptics": "~15.0.8", "expo-haptics": "~15.0.8",
@ -6963,6 +6964,44 @@
"react-native": "*" "react-native": "*"
} }
}, },
"node_modules/expo-device": {
"version": "8.0.10",
"resolved": "https://registry.npmjs.org/expo-device/-/expo-device-8.0.10.tgz",
"integrity": "sha512-jd5BxjaF7382JkDMaC+P04aXXknB2UhWaVx5WiQKA05ugm/8GH5uaz9P9ckWdMKZGQVVEOC8MHaUADoT26KmFA==",
"license": "MIT",
"dependencies": {
"ua-parser-js": "^0.7.33"
},
"peerDependencies": {
"expo": "*"
}
},
"node_modules/expo-device/node_modules/ua-parser-js": {
"version": "0.7.41",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.41.tgz",
"integrity": "sha512-O3oYyCMPYgNNHuO7Jjk3uacJWZF8loBgwrfd/5LE/HyZ3lUIOdniQ7DNXJcIgZbwioZxk0fLfI4EVnetdiX5jg==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/ua-parser-js"
},
{
"type": "paypal",
"url": "https://paypal.me/faisalman"
},
{
"type": "github",
"url": "https://github.com/sponsors/faisalman"
}
],
"license": "MIT",
"bin": {
"ua-parser-js": "script/cli.js"
},
"engines": {
"node": "*"
}
},
"node_modules/expo-file-system": { "node_modules/expo-file-system": {
"version": "19.0.21", "version": "19.0.21",
"resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-19.0.21.tgz", "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-19.0.21.tgz",

View File

@ -22,6 +22,7 @@
"expo": "~54.0.29", "expo": "~54.0.29",
"expo-clipboard": "~8.0.8", "expo-clipboard": "~8.0.8",
"expo-constants": "~18.0.12", "expo-constants": "~18.0.12",
"expo-device": "~8.0.10",
"expo-file-system": "~19.0.21", "expo-file-system": "~19.0.21",
"expo-font": "~14.0.10", "expo-font": "~14.0.10",
"expo-haptics": "~15.0.8", "expo-haptics": "~15.0.8",

View File

@ -1,15 +0,0 @@
/**
* LiveKit WebRTC Globals Setup
*
* CRITICAL: This file MUST be imported BEFORE any LiveKit components!
* It sets up WebRTC globals that Room class depends on.
*
* Import this as the first line in your app entry point.
*/
import { registerGlobals } from '@livekit/react-native';
// Register WebRTC globals immediately on module load
registerGlobals();
console.log('[LiveKit] WebRTC globals registered');

View File

@ -54,6 +54,13 @@ export const ULTRAVOX_TOOLS: UltravoxTool[] = [
client: {}, client: {},
}, },
}, },
{
temporaryTool: {
modelToolName: 'navigateToChat',
description: 'Navigate back to the Julia AI chat screen. Use when user asks to go to chat, messages, Julia, or wants to return to the conversation screen.',
client: {},
},
},
]; ];
// Build context from Ferdinand data // Build context from Ferdinand data
@ -140,26 +147,37 @@ TODAY'S ACTIVITY PATTERN:
export function getSystemPrompt(): string { export function getSystemPrompt(): string {
const context = buildFerdinandContext(); const context = buildFerdinandContext();
return `You are Julia, a compassionate and knowledgeable AI wellness assistant for WellNuo app. // Check if there are critical/high alerts
Your role is to help caregivers monitor and understand the wellbeing of their loved ones. const todayData = ferdinandData.days.find(d => d.date === 'today');
const hasCriticalAlerts = todayData?.alerts?.some(a => a.severity === 'critical' || a.severity === 'high');
return `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 ${ferdinandData.client.name} (the beneficiary), NOT about yourself!
${context} ${context}
IMPORTANT GUIDELINES: CONVERSATION RULES:
- Be warm, empathetic, and supportive in your responses 1. When user asks "how are you?" or "how's it going?" - ALWAYS respond about ${ferdinandData.client.name}'s status, NOT about yourself as AI
- You have FULL access to ${ferdinandData.client.name}'s wellness data from the last 7 days - NEVER say "I'm doing well as an AI" - the user wants to know about their loved one!
- When asked about status, alerts, or concerns - refer to the actual data above
- Prioritize critical and high severity alerts when discussing concerns
- The FALL DETECTED today at 06:32 is the most urgent concern - acknowledge it if user asks about current status
- You can navigate the app using available tools when the user requests it
- If user asks to "show dashboard", "open dashboard", "see the overview" - use the navigateToDashboard tool
- Keep responses conversational and natural for voice interaction
- Speak in a calm, reassuring tone
- Be specific with times and details from the data
- If asked about something not in the data, say you don't have that information
Remember: You're speaking with a caregiver who wants the best for their loved one. 2. When user asks "what's happening?" or "any updates?" - report ${ferdinandData.client.name}'s current status and alerts
Be supportive and helpful while maintaining appropriate boundaries about medical advice.`;
3. ALWAYS assume questions are about ${ferdinandData.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! ${hasCriticalAlerts ? `I have some important updates about ${ferdinandData.client.name}. Would you like to hear them?` : `${ferdinandData.client.name} is doing well today. Anything specific you'd 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 ${ferdinandData.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 ${ferdinandData.client.name}.`;
} }
// API Response types // API Response types

147
utils/audioSession.ts Normal file
View File

@ -0,0 +1,147 @@
/**
* iOS AudioSession Configuration Helpers
*
* CRITICAL: This must be configured BEFORE connecting to LiveKit room!
* Without proper AudioSession setup, microphone won't work on iOS.
*/
import { Platform } from 'react-native';
// AudioSession module - use 'any' to avoid complex typing issues with @livekit/react-native
// The actual AudioSession from LiveKit has specific enum types that are hard to match statically
let audioSessionModule: any = null;
/**
* Import AudioSession module lazily
* This is needed because @livekit/react-native must be imported after registerGlobals()
*/
async function getAudioSession(): Promise<any | null> {
if (Platform.OS !== 'ios') return null;
if (!audioSessionModule) {
const livekit = await import('@livekit/react-native');
audioSessionModule = livekit.AudioSession;
}
return audioSessionModule;
}
/**
* Configure iOS AudioSession for bidirectional voice call
*
* MUST be called BEFORE connecting to LiveKit room!
*
* Configuration:
* - Category: playAndRecord (both speaker and mic)
* - Mode: voiceChat (optimized for voice calls)
* - Options: Bluetooth, speaker, mix with others
*/
export async function configureAudioForVoiceCall(): Promise<void> {
if (Platform.OS !== 'ios') {
console.log('[AudioSession] Skipping on non-iOS platform');
return;
}
console.log('[AudioSession] Configuring for voice call...');
try {
const AudioSession = await getAudioSession();
if (!AudioSession) {
console.error('[AudioSession] Failed to get AudioSession module');
return;
}
// Step 1: Set Apple-specific audio configuration
console.log('[AudioSession] Step 1: Setting Apple audio config...');
await AudioSession.setAppleAudioConfiguration({
audioCategory: 'playAndRecord',
audioCategoryOptions: [
'allowBluetooth',
'allowBluetoothA2DP',
'defaultToSpeaker',
'mixWithOthers',
],
audioMode: 'voiceChat',
});
// Step 2: Configure default output to speaker
console.log('[AudioSession] Step 2: Setting default output...');
await AudioSession.configureAudio({
ios: {
defaultOutput: 'speaker',
},
});
// Step 3: Start the audio session
console.log('[AudioSession] Step 3: Starting audio session...');
await AudioSession.startAudioSession();
console.log('[AudioSession] Configuration complete!');
} catch (error) {
console.error('[AudioSession] Configuration error:', error);
throw error;
}
}
/**
* Stop iOS AudioSession
*
* Should be called when disconnecting from voice call
*/
export async function stopAudioSession(): Promise<void> {
if (Platform.OS !== 'ios') {
return;
}
console.log('[AudioSession] Stopping audio session...');
try {
const AudioSession = await getAudioSession();
if (!AudioSession) {
return;
}
await AudioSession.stopAudioSession();
console.log('[AudioSession] Stopped');
} catch (error) {
console.error('[AudioSession] Error stopping:', error);
// Don't throw - cleanup errors are not critical
}
}
/**
* Reconfigure audio session after remote track arrives
*
* Sometimes iOS needs a kick to properly route audio after remote participant joins
*/
export async function reconfigureAudioForPlayback(): Promise<void> {
if (Platform.OS !== 'ios') {
return;
}
console.log('[AudioSession] Reconfiguring for playback...');
try {
const AudioSession = await getAudioSession();
if (!AudioSession) {
return;
}
// Just reconfigure the same settings - this "refreshes" the audio routing
await AudioSession.setAppleAudioConfiguration({
audioCategory: 'playAndRecord',
audioCategoryOptions: [
'allowBluetooth',
'allowBluetoothA2DP',
'defaultToSpeaker',
'mixWithOthers',
],
audioMode: 'voiceChat',
});
console.log('[AudioSession] Reconfigured successfully');
} catch (error) {
console.error('[AudioSession] Reconfigure error:', error);
// Don't throw - this is a best-effort operation
}
}