From 515c6100afaca25a2862f56544a0fa436d85ecc4 Mon Sep 17 00:00:00 2001 From: Siddahartha Date: Thu, 3 Apr 2025 21:59:58 -0500 Subject: [PATCH 1/2] fixed navigation code completely --- frontend/app/(tabs)/home.tsx | 1193 ++++++++++++++++------------------ 1 file changed, 562 insertions(+), 631 deletions(-) diff --git a/frontend/app/(tabs)/home.tsx b/frontend/app/(tabs)/home.tsx index 3ce8af08..cfd2fda6 100644 --- a/frontend/app/(tabs)/home.tsx +++ b/frontend/app/(tabs)/home.tsx @@ -5,111 +5,44 @@ import { ExpoSpeechRecognitionModule, useSpeechRecognitionEvent, } from "expo-speech-recognition"; -import { Text, TextInput, TouchableOpacity, Image, Modal } from 'react-native'; -import { Animated, Easing } from 'react-native'; -import * as Speech from 'expo-speech'; - -interface WebSocketEvent { - data: string; -} - -interface SpeechRecognitionErrorEvent { - error: string; - message: string; +import { Text, TextInput, TouchableOpacity, Image, Modal } from "react-native"; +import { Animated, Easing } from "react-native"; +import * as Speech from "expo-speech"; + +// Define interfaces for WebSocket messages +interface InstructionMessage { + type: string; + data: { + instruction?: string; + message?: string; + }; } -interface SpeechRecognitionResultEvent { - results: { - transcript?: string; - }[]; -} +// Define WebSocket type to address WebSocket errors +type WebSocketType = WebSocket | null; export default function HomeScreen() { - const [speechQueue, setSpeechQueue] = useState([]); - const ws = useRef(null); // WebSocket reference with proper type - const [recognizing, setRecognizing] = useState(true); + const ws = useRef(null); + const [recognizing, setRecognizing] = useState(true); // Start with true to unmute const [transcript, setTranscript] = useState(""); const [backendResponse, setBackendResponse] = useState(""); const [hasPermission, setHasPermission] = useState(false); const [loading, setLoading] = useState(false); - const [isSpeaking, setIsSpeaking] = useState(false); // Track if TTS is active - const [destination, setDestination] = useState(""); // Store the destination input + const [isSpeaking, setIsSpeaking] = useState(false); + const [destination, setDestination] = useState(""); const handleInputChange = (text: string) => { setDestination(text); // Update destination state when user types }; const handleSubmitDestination = () => { - // Handle submitting the destination (e.g., send it to backend or use it in the app) console.log("Destination entered:", destination); - }; - - const processSpeechQueue = () => { - if (speechQueue.length === 0) return; - - // Get the next item from the queue - const textToSpeak = speechQueue[0]; - - // Make sure we stop listening before speaking to prevent feedback - stopListening(); - - // Set speaking state - setIsSpeaking(true); - - // Speak the text - Speech.speak(textToSpeak, { - rate: 0.8, - pitch: 1.0, - language: "en-US", - onDone: () => { - // Update queue by removing the item we just processed - setSpeechQueue(prevQueue => prevQueue.slice(1)); - setIsSpeaking(false); - - // Add a small delay before restarting listening - // to ensure TTS has completely finished - setTimeout(() => { - if (speechQueue.length === 0) { - startListening(); - } - }, 500); - }, - onStopped: () => { - // Update queue by removing the item we just processed - setSpeechQueue(prevQueue => prevQueue.slice(1)); - setIsSpeaking(false); - - // Add a small delay before restarting listening - setTimeout(() => { - if (speechQueue.length === 0) { - startListening(); - } - }, 500); - }, - onError: (error) => { - console.error("Speech error:", error); - // Update queue by removing the item we just processed - setSpeechQueue(prevQueue => prevQueue.slice(1)); - setIsSpeaking(false); - - // Add a small delay before restarting listening - setTimeout(() => { - if (speechQueue.length === 0) { - startListening(); - } - }, 500); - } - }); - }; - - // Modified useEffect to handle the speech queue - useEffect(() => { - // Only process the queue if we're not currently speaking and there are items in the queue - if (!isSpeaking && speechQueue.length > 0) { - processSpeechQueue(); + if (destination.trim()) { + startNavigation(destination); } - }, [isSpeaking, speechQueue]); + }; + // WebSocket setup useEffect(() => { ws.current = new WebSocket("ws://localhost:8000/"); @@ -117,46 +50,30 @@ export default function HomeScreen() { console.log("WebSocket connected"); }; - ws.current.onmessage = (event: WebSocketEvent) => { - console.log("WebSocket message received:", event.data); - + ws.current.onmessage = (event: WebSocketMessageEvent) => { try { - // Parse the message data - const parsedData = JSON.parse(event.data); - console.log("Parsed WebSocket data:", parsedData); - + const parsedData = JSON.parse(event.data as string) as InstructionMessage; let textToSpeak = ""; - // Check message type and extract the appropriate text to speak - if (parsedData.type === "instruction") { - console.log("Instruction received:", parsedData.data); + if (parsedData.type === "approachingTurn") { textToSpeak = parsedData.data.instruction || ""; - } - else if (parsedData.type === "approachingTurn") { - console.log("Approaching turn:", parsedData.data); - // Extract relevant info from approaching turn data - textToSpeak = parsedData.data.instruction || ""; - } - else if (parsedData.type === "complete") { - console.log("Navigation complete:", parsedData.data); + } else if (parsedData.type === "complete") { textToSpeak = parsedData.data.message || "You have reached your destination"; } - // Update the UI with the extracted text - setBackendResponse(textToSpeak || JSON.stringify(parsedData)); - - // Instead of speaking immediately, add the text to the speech queue if (textToSpeak) { - setSpeechQueue(prevQueue => [...prevQueue, textToSpeak]); + stopListening(); // Stop STT before speaking + setBackendResponse(textToSpeak); + speakResponse(textToSpeak); // Centralized speaking function } } catch (error) { console.error("Error processing WebSocket message:", error); - setBackendResponse(event.data); // Fallback to raw data + setBackendResponse(String(event.data)); } }; - ws.current.onerror = (error: Event) => { - console.error("WebSocket Error:", error); + ws.current.onerror = (ev: Event) => { + console.error("WebSocket Error:", ev); }; ws.current.onclose = () => { @@ -164,26 +81,49 @@ export default function HomeScreen() { }; return () => { - if (ws.current) { - ws.current.close(); - } + if (ws.current) ws.current.close(); }; }, []); - // Check permissions on component mount - useEffect(() => { - checkPermissions(); - startListening(); - }, []); - - // Handler for text-to-speech with muting - REMOVED - // Since we now handle text-to-speech directly in the WebSocket onmessage handler + // Centralized function to handle speaking + const speakResponse = (text: string) => { + if (recognizing) stopListening(); // Ensure STT is off + setIsSpeaking(true); - const handleTextToSpeech = () => { - const thingToSay = transcript; - Speech.speak(thingToSay); + Speech.speak(text, { + rate: 0.8, + pitch: 1.0, + language: "en-US", + onStart: () => setIsSpeaking(true), + onDone: () => { + setIsSpeaking(false); + setTimeout(startListening, 1500); // Delay restart of STT + }, + onStopped: () => { + setIsSpeaking(false); + setTimeout(startListening, 1500); + }, + onError: () => { + setIsSpeaking(false); + setTimeout(startListening, 1500); + }, + }); }; + // Check permissions and initialize listening + useEffect(() => { + const initialize = async () => { + await checkPermissions(); + if (hasPermission) { + startListening(); // Start listening immediately if permissions are granted + } else { + await requestPermissions(); + if (hasPermission) startListening(); // Start after requesting permissions + } + }; + initialize(); + }, []); + const checkPermissions = async () => { const { status, granted } = await ExpoSpeechRecognitionModule.getPermissionsAsync(); console.log("Permissions Status:", status); @@ -203,25 +143,21 @@ export default function HomeScreen() { // Event listeners for speech recognition useSpeechRecognitionEvent("start", () => setRecognizing(true)); useSpeechRecognitionEvent("end", () => setRecognizing(false)); - useSpeechRecognitionEvent("result", (event: SpeechRecognitionResultEvent) => { + useSpeechRecognitionEvent("result", (event) => { const speechResult = event.results[0]?.transcript || ""; setTranscript(speechResult); console.log("Speech to Text Result:", speechResult); - }); - useSpeechRecognitionEvent("error", (event: SpeechRecognitionErrorEvent) => { + useSpeechRecognitionEvent("error", (event) => { console.log("Error:", event.error, "Message:", event.message); }); const startListening = async () => { - // Don't start listening if TTS is active - if (isSpeaking) return; + if (isSpeaking || !hasPermission) return; // Prevent starting if speaking - if (!hasPermission) { - await requestPermissions(); - } + await requestPermissions(); + if (!hasPermission) return; - console.log("Starting speech recognition..."); ExpoSpeechRecognitionModule.start({ lang: "en-US", interimResults: true, @@ -230,19 +166,9 @@ export default function HomeScreen() { requiresOnDeviceRecognition: false, addsPunctuation: false, contextualStrings: [ - "weather", - "temperature", - "city", - "weather in", - "ai", - "chat", - "conversation", - "start navigation", - "navigate to", - "take me to", - "directions to", - "how do I get to", - "route to" + "weather", "temperature", "city", "weather in", "ai", "chat", "conversation", + "start navigation", "navigate to", "take me to", "directions to", + "how do I get to", "route to" ], }); setRecognizing(true); @@ -261,511 +187,516 @@ export default function HomeScreen() { } }; - // Function to send the transcript to the backend + // Navigation function + const startNavigation = async (dest: string) => { + try { + setLoading(true); + const destination = dest || "2831 W 15th St Ste 200, Plano, Tx 75075"; + console.log(`Starting navigation to: ${destination}`); + + const response = await axios.post("http://localhost:8000/startSimulationDirections", { + destination, + }); + + console.log("Navigation started:", response.data); + setBackendResponse(`Starting navigation to ${destination}`); + speakResponse(`Starting navigation to ${destination}`); + } catch (error) { + console.error("Error starting navigation:", error); + setBackendResponse("Sorry, there was an error starting navigation."); + speakResponse("Sorry, there was an error starting navigation."); + } finally { + setLoading(false); + } + }; + + // Send speech to backend const sendSpeechToBackend = async () => { - if (!transcript) return; + if (!transcript || isSpeaking) return; try { setLoading(true); - - // Check if transcript contains navigation keywords - const navigationKeywords = ["start navigation", "navigate to", "take me to", "directions to", "how do I get to", "route to"]; + const navigationKeywords = [ + "start navigation", "navigate to", "take me to", "directions to", + "how do I get to", "route to" + ]; const isNavigationRequest = navigationKeywords.some(keyword => transcript.toLowerCase().includes(keyword) ); - // Extract destination from the transcript - let destination = ""; + let extractedDestination = ""; if (isNavigationRequest) { - // Parse to extract destination for (const keyword of navigationKeywords) { if (transcript.toLowerCase().includes(keyword)) { - destination = transcript.toLowerCase().split(keyword)[1].trim(); + extractedDestination = transcript.toLowerCase().split(keyword)[1]?.trim() || ""; break; } } - } - - // If not currently speaking - if (!isSpeaking) { - // Choose the appropriate endpoint based on the command type - if (isNavigationRequest && destination) { - // Get current location (in a real app, you'd use geolocation) - // For this example, we'll use a hardcoded origin - const origin = "2800 Waterview Pkwy, Richardson, TX 75080"; // Default origin - const destination = "2831 W 15th St Ste 200, Plano, Tx 75075" - console.log(`Starting navigation to: ${destination}`); - - // Call navigation-specific endpoint - const response = await axios.post('http://localhost:8000/startSimulationDirections', { - destination: destination - }); - - console.log("Navigation started:", response.data); - setBackendResponse(`Starting navigation to ${destination}`); - } else { - // Use the general command endpoint for other requests - const response = await axios.post('http://localhost:8000/command', { - userInput: transcript, - sessionId: 'unique-session-id', - }); - - console.log("Backend response:", response.data); - - if (response.data && response.data.response) { - setBackendResponse(response.data.response); - } else { - setBackendResponse("No response received"); - } - } + startNavigation(extractedDestination); + } else { + const response = await axios.post("http://localhost:8000/command", { + userInput: transcript, + sessionId: "unique-session-id", + }); + console.log("Backend response:", response.data); // Log backend result as in original + const responseText = response.data.response || "No response received"; + setBackendResponse(responseText); + speakResponse(responseText); } } catch (error) { console.error("Error sending speech to backend:", error); setBackendResponse("Sorry, there was an error processing your request."); + speakResponse("Sorry, there was an error processing your request."); } finally { setLoading(false); } }; - // Modified useEffect with debouncing to prevent multiple API calls - // Modified useEffect for sending speech to backend + // Debounced transcript processing useEffect(() => { - let debounceTimer = null; - - // Only process speech when we have transcript AND we're not speaking - if (transcript && !isSpeaking && recognizing) { - // Clear any existing timer - if (debounceTimer) clearTimeout(debounceTimer); - - // Set a new timer - debounceTimer = setTimeout(() => { - sendSpeechToBackend(); - }, 1000); // 1 second delay to stabilize transcript - } - - // Cleanup function - return () => { - if (debounceTimer) clearTimeout(debounceTimer); - }; - }, [transcript, isSpeaking, recognizing]); - - return ( - - {/* Top-left section with logo and app name */} - - - CoDriver - - - - {/* Main container to align the content */} - - - - - - {/* Text container for song title */} - - - The Color Violet · Tory Lanez - - - {/* Bluetooth icon + BeatSpill+ in a row */} - - - iPhone 16 - - - - {/* Second Bluetooth icon */} - - - - - - - - - - - - - {/* Background AI-themed blob image */} - - - - {/* Destination Button at the bottom */} - - {/* Left Section: Icon */} - - - - - {/* Right Section: Text Input */} - - - - - - - - - - - - {recognizing ? "Mute" : "Unmute"} - - - - - ); + const debounceTimer = setTimeout(() => { + if (transcript && !isSpeaking) sendSpeechToBackend(); + }, 1000); + + return () => clearTimeout(debounceTimer); + }, [transcript]); + + + return ( + + {/* Top-left section with logo and app name */} + + + CoDriver + + + + + {/* Main container to align the content */} + + + + + + + {/* Text container for song title */} + + + The Color Violet · Tory Lanez + + + + {/* Bluetooth icon + BeatSpill+ in a row */} + + + iPhone 16 + + + + + {/* Second Bluetooth icon */} + + + + + + + + + + + + + + + {/* Background AI-themed blob image */} + + + + + {/* Destination Button at the bottom */} + + {/* Left Section: Icon */} + + + + + + {/* Right Section: Text Input */} + + + + + + + + + + + + + {recognizing ? "Mute" : "Unmute"} + + + + + ); } + const styles = StyleSheet.create({ - destinationInput: { - flex: 1, - fontSize: 18, - color: '#AE9A8C', - paddingVertical: 10, - paddingHorizontal: 10, - backgroundColor: 'rgba(0, 0, 0, 0)', // Slightly transparent background - borderRadius: 20, - marginLeft: -10, // Space between the icon and input - width: '30%' - }, - muteButton: { - position: 'absolute', - bottom: 40, - backgroundColor: '#2D2A38', - paddingVertical: 12, - borderRadius: 30, - width: '40%', - alignItems: 'center', - justifyContent: 'center', // Ensure content is centered - marginBottom: 80, - marginRight: 120, - marginLeft: 120, - }, - - destinationTabText2: { - fontSize: 18, - fontWeight: '600', - color: '#AE9A8C', - textAlign: 'center', // Center text inside the button - width: '110%', // Ensure text takes full width to remain centered - }, - - - leftSection3: { - flexDirection: 'row', // Align icon horizontally in the left section - justifyContent: 'center', - alignItems: 'center', // Center the icon vertically - }, - rightSection3: { - flexDirection: 'row', // Align text and icon horizontally in the right section - justifyContent: 'center', - alignItems: 'center', // Center them vertically - marginLeft: 20 - }, - container: { - flex: 1, - backgroundColor: 'black', - }, - - topLeftContainer: { - position: 'absolute', - top: 40, - left: 20, - flexDirection: 'row', - alignItems: 'center', - }, - logoImage: { - width: 40, - height: 40, - marginRight: 8, - }, - topLeftText: { - color: 'white', - fontSize: 24, - fontWeight: 'bold', - }, - blobImage: { - position: 'absolute', - width: 670, - height: 630, - marginTop: -680, - top: '25%', - alignSelf: 'center', - }, - modalContainer: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - backgroundColor: 'rgba(0, 0, 0, 0.5)', - }, - popup: { - width: '85%', - height: 300, - backgroundColor: 'rgba(255, 255, 255, 0.3)', - padding: 25, - borderRadius: 15, - alignItems: 'center', - }, - closeButton: { - position: 'absolute', - top: 5, - right: 10, - padding: 5, - }, - closeText: { - fontSize: 24, - color: 'white', - }, - popupTitle: { - fontSize: 40, - fontWeight: 'bold', - color: 'white', - alignSelf: 'flex-start', - textAlign: 'left', - marginBottom: 10, - }, - input: { - width: '100%', - height: 50, - backgroundColor: 'rgba(255, 255, 255, 0.2)', - borderRadius: 5, - paddingHorizontal: 10, - color: 'white', - fontSize: 16, - marginBottom: 20, - textAlignVertical: 'center', - }, - button: { - backgroundColor: 'black', - paddingVertical: 10, - paddingHorizontal: 20, - borderRadius: 8, - }, - buttonText: { - color: 'white', - fontSize: 16, - }, - spotifyTab: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - borderRadius: 15, - paddingVertical: 12, - paddingHorizontal: -20, // Keeps original padding - borderWidth: 1, - backgroundColor: '#2D2A38', - borderColor: '#2D2A38', - marginTop: 706, - alignSelf: 'center', - marginLeft: 10, - marginRight: 10, - paddingLeft: -10, - zIndex: 10 - }, - spotifyTabText: { - fontSize: 18, - lineHeight: 26, - fontWeight: '600', - color: '#AE9A8C', - flexShrink: 1, // Ensures text doesn't wrap - textAlign: 'center', // Keeps text aligned within the button - paddingLeft: -100 - }, - destinationTabText: { - fontSize: 18, - lineHeight: 26, - fontWeight: '600', - color: '#AE9A8C', - flexShrink: 1, // Ensures text doesn't wrap - textAlign: 'center', // Keeps text aligned within the button - }, - albumCover: { - width: 60, // Adjust width as needed - height: 60, // Adjust height as needed - marginRight: 10, - marginLeft: 20, - borderRadius: 5, // Optional: for rounded corners - }, - SearchIcon: { - width: 30, // Adjust width as needed - height: 30, // Adjust height as needed - marginRight: 10, // Space between the icon and the text - }, - bluetooth: { - width: 30, // Adjust width as needed - height: 30, // Adjust height as needed - marginRight: 50, - marginLeft: -15, - borderRadius: 5, // Optional: for rounded corners - paddingLeft: 15, - paddingRight: 0 - }, - leftSection: { - flexDirection: 'column', // Align album cover vertically in the left section - justifyContent: 'center', // Center the content vertically in the left section - marginRight: 10, // Space between the album cover and the text - }, - leftSection2: { - flexDirection: 'row', // Align icon horizontally in the left section - justifyContent: 'center', - alignItems: 'center', // Center the icon vertically - }, - - rightSection2: { - flexDirection: 'row', // Align text and icon horizontally in the right section - justifyContent: 'center', - alignItems: 'center', // Center them vertically - width: '87%' - }, - rightSection: { - flexDirection: 'row', // Arrange items horizontally - justifyContent: 'center', - alignItems: 'center', // Align items vertically in the center - }, - bluetooth2: { - width: 40, // Increase the size of the Bluetooth icon - height: 40, // Increase the size of the Bluetooth icon - marginLeft: 10, // Adjust spacing between the icons - borderRadius: 5, // Optional: for rounded corners - }, - Voice2: { - width: 30, // Adjust width as needed - height: 30, // Adjust height as needed - marginLeft: 10, // Space between the text and the icon - }, - bluetoothText: { - fontSize: 16, - color: '#AE9A8C', - textAlign: 'left', - marginTop: 2, // Keeps slight spacing between song text and BeatSpill - marginLeft: -40, // Moves text closer to Bluetooth 1 logo - }, - beatspillContainer: { - flexDirection: 'row', // Align Bluetooth and text in a row - alignItems: 'center', // Center them vertically - marginTop: 2, // Small spacing below song title - }, - bottomSection: { - position: 'absolute', - bottom: 0, // Adjust as needed - left: 20, // Align to the left - width: '80%', // Adjust width as needed - flexDirection: 'column', // Stack the bars vertically - alignItems: 'flex-start', // Align the progress bars to the left - }, - progressBar: { - width: '90%', // Adjust width of the first progress bar - height: 7, // Adjust height as needed - marginLeft: -2 - }, - progressBar2: { - width: '118%', // Adjust width of the second progress bar - height: 7, // Adjust height as needed - opacity: 0.6, // Lighter shade by reducing opacity - tintColor: 'lightgray', // Lighter color shade - }, - - googleBtn: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - borderRadius: 30, - paddingVertical: 10, - paddingHorizontal: 20, - backgroundColor: '#FFFFFF', - }, - googleBtnText: { - fontSize: 18, - fontWeight: '600', - color: '#000', - }, - - // New Destination Button Styles - destinationButton: { - position: 'absolute', - bottom: 530, - transform: [{ translateX: -50 }], - backgroundColor: '#2D2A38', // Button color - paddingVertical: 12, - paddingHorizontal: 20, - borderRadius: 30, - width: '80%', - alignItems: 'center', - flexDirection: 'row', // Align items horizontally - //justifyContent: 'space-between', // Space between left and right sections - marginBottom: 80, - marginRight: 120, - marginLeft: 90, - }, - destinationButtonText: { - color: 'white', - fontSize: 18, - fontWeight: 'bold', - }, - image: { - position: 'absolute', - width: 20, // Adjust the width as needed - height: 20, // Adjust the height as needed - }, - left: { - //marginLeft: 50, - marginRight: 200, - //top: '50%', // Vertically centered - //transform: [{ translateY: -10 }], // Adjust for proper vertical alignment - }, - right: { - right: -20, // Adjust position to the right - top: '50%', // Vertically centered - transform: [{ translateY: -10 }], // Adjust for proper vertical alignment - }, - bottom: { - bottom: -20, // Adjust position to the bottom - left: '50%', // Horizontally centered - transform: [{ translateX: -10 }], // Adjust for proper horizontal alignment - }, -}); \ No newline at end of file + destinationInput: { + flex: 1, + fontSize: 18, + color: '#AE9A8C', + paddingVertical: 10, + paddingHorizontal: 10, + backgroundColor: 'rgba(0, 0, 0, 0)', // Slightly transparent background + borderRadius: 20, + marginLeft: -10, // Space between the icon and input + width: '30%' + }, + muteButton: { + position: 'absolute', + bottom: 40, + backgroundColor: '#2D2A38', + paddingVertical: 12, + borderRadius: 30, + width: '40%', + alignItems: 'center', + justifyContent: 'center', // Ensure content is centered + marginBottom: 80, + marginRight: 120, + marginLeft: 120, + }, + + + destinationTabText2: { + fontSize: 18, + fontWeight: '600', + color: '#AE9A8C', + textAlign: 'center', // Center text inside the button + width: '110%', // Ensure text takes full width to remain centered + }, + + + + + leftSection3: { + flexDirection: 'row', // Align icon horizontally in the left section + justifyContent: 'center', + alignItems: 'center', // Center the icon vertically + }, + rightSection3: { + flexDirection: 'row', // Align text and icon horizontally in the right section + justifyContent: 'center', + alignItems: 'center', // Center them vertically + marginLeft: 20 + }, + container: { + flex: 1, + backgroundColor: 'black', + }, + + + topLeftContainer: { + position: 'absolute', + top: 40, + left: 20, + flexDirection: 'row', + alignItems: 'center', + }, + logoImage: { + width: 40, + height: 40, + marginRight: 8, + }, + topLeftText: { + color: 'white', + fontSize: 24, + fontWeight: 'bold', + }, + blobImage: { + position: 'absolute', + width: 670, + height: 630, + marginTop: -680, + top: '25%', + alignSelf: 'center', + }, + modalContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: 'rgba(0, 0, 0, 0.5)', + }, + popup: { + width: '85%', + height: 300, + backgroundColor: 'rgba(255, 255, 255, 0.3)', + padding: 25, + borderRadius: 15, + alignItems: 'center', + }, + closeButton: { + position: 'absolute', + top: 5, + right: 10, + padding: 5, + }, + closeText: { + fontSize: 24, + color: 'white', + }, + popupTitle: { + fontSize: 40, + fontWeight: 'bold', + color: 'white', + alignSelf: 'flex-start', + textAlign: 'left', + marginBottom: 10, + }, + input: { + width: '100%', + height: 50, + backgroundColor: 'rgba(255, 255, 255, 0.2)', + borderRadius: 5, + paddingHorizontal: 10, + color: 'white', + fontSize: 16, + marginBottom: 20, + textAlignVertical: 'center', + }, + button: { + backgroundColor: 'black', + paddingVertical: 10, + paddingHorizontal: 20, + borderRadius: 8, + }, + buttonText: { + color: 'white', + fontSize: 16, + }, + spotifyTab: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + borderRadius: 15, + paddingVertical: 12, + paddingHorizontal: -20, // Keeps original padding + borderWidth: 1, + backgroundColor: '#2D2A38', + borderColor: '#2D2A38', + marginTop: 706, + alignSelf: 'center', + marginLeft: 10, + marginRight: 10, + paddingLeft: -10, + zIndex: 10 + }, + spotifyTabText: { + fontSize: 18, + lineHeight: 26, + fontWeight: '600', + color: '#AE9A8C', + flexShrink: 1, // Ensures text doesn't wrap + textAlign: 'center', // Keeps text aligned within the button + paddingLeft: -100 + }, + destinationTabText: { + fontSize: 18, + lineHeight: 26, + fontWeight: '600', + color: '#AE9A8C', + flexShrink: 1, // Ensures text doesn't wrap + textAlign: 'center', // Keeps text aligned within the button + }, + albumCover: { + width: 60, // Adjust width as needed + height: 60, // Adjust height as needed + marginRight: 10, + marginLeft: 20, + borderRadius: 5, // Optional: for rounded corners + }, + SearchIcon: { + width: 30, // Adjust width as needed + height: 30, // Adjust height as needed + marginRight: 10, // Space between the icon and the text + }, + bluetooth: { + width: 30, // Adjust width as needed + height: 30, // Adjust height as needed + marginRight: 50, + marginLeft: -15, + borderRadius: 5, // Optional: for rounded corners + paddingLeft: 15, + paddingRight: 0 + }, + leftSection: { + flexDirection: 'column', // Align album cover vertically in the left section + justifyContent: 'center', // Center the content vertically in the left section + marginRight: 10, // Space between the album cover and the text + }, + leftSection2: { + flexDirection: 'row', // Align icon horizontally in the left section + justifyContent: 'center', + alignItems: 'center', // Center the icon vertically + }, + + + rightSection2: { + flexDirection: 'row', // Align text and icon horizontally in the right section + justifyContent: 'center', + alignItems: 'center', // Center them vertically + width: '87%' + }, + rightSection: { + flexDirection: 'row', // Arrange items horizontally + justifyContent: 'center', + alignItems: 'center', // Align items vertically in the center + }, + bluetooth2: { + width: 40, // Increase the size of the Bluetooth icon + height: 40, // Increase the size of the Bluetooth icon + marginLeft: 10, // Adjust spacing between the icons + borderRadius: 5, // Optional: for rounded corners + }, + Voice2: { + width: 30, // Adjust width as needed + height: 30, // Adjust height as needed + marginLeft: 10, // Space between the text and the icon + }, + bluetoothText: { + fontSize: 16, + color: '#AE9A8C', + textAlign: 'left', + marginTop: 2, // Keeps slight spacing between song text and BeatSpill + marginLeft: -40, // Moves text closer to Bluetooth 1 logo + }, + beatspillContainer: { + flexDirection: 'row', // Align Bluetooth and text in a row + alignItems: 'center', // Center them vertically + marginTop: 2, // Small spacing below song title + }, + bottomSection: { + position: 'absolute', + bottom: 0, // Adjust as needed + left: 20, // Align to the left + width: '80%', // Adjust width as needed + flexDirection: 'column', // Stack the bars vertically + alignItems: 'flex-start', // Align the progress bars to the left + }, + progressBar: { + width: '90%', // Adjust width of the first progress bar + height: 7, // Adjust height as needed + marginLeft: -2 + }, + progressBar2: { + width: '118%', // Adjust width of the second progress bar + height: 7, // Adjust height as needed + opacity: 0.6, // Lighter shade by reducing opacity + tintColor: 'lightgray', // Lighter color shade + }, + + + googleBtn: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + borderRadius: 30, + paddingVertical: 10, + paddingHorizontal: 20, + backgroundColor: '#FFFFFF', + }, + googleBtnText: { + fontSize: 18, + fontWeight: '600', + color: '#000', + }, + + + // New Destination Button Styles + destinationButton: { + position: 'absolute', + bottom: 530, + transform: [{ translateX: -50 }], + backgroundColor: '#2D2A38', // Button color + paddingVertical: 12, + paddingHorizontal: 20, + borderRadius: 30, + width: '80%', + alignItems: 'center', + flexDirection: 'row', // Align items horizontally + //justifyContent: 'space-between', // Space between left and right sections + marginBottom: 80, + marginRight: 120, + marginLeft: 90, + }, + destinationButtonText: { + color: 'white', + fontSize: 18, + fontWeight: 'bold', + }, + image: { + position: 'absolute', + width: 20, // Adjust the width as needed + height: 20, // Adjust the height as needed + }, + left: { + //marginLeft: 50, + marginRight: 200, + //top: '50%', // Vertically centered + //transform: [{ translateY: -10 }], // Adjust for proper vertical alignment + }, + right: { + right: -20, // Adjust position to the right + top: '50%', // Vertically centered + transform: [{ translateY: -10 }], // Adjust for proper vertical alignment + }, + bottom: { + bottom: -20, // Adjust position to the bottom + left: '50%', // Horizontally centered + transform: [{ translateX: -10 }], // Adjust for proper horizontal alignment + }, +}); + From 7a35b373691baa2770d7952c77bcbfea5dab1897 Mon Sep 17 00:00:00 2001 From: Siddahartha Date: Mon, 14 Apr 2025 20:12:00 -0500 Subject: [PATCH 2/2] added new voice --- frontend/app/(tabs)/home.tsx | 1028 +++++++++++++++++++--------------- frontend/package-lock.json | 246 +++++++- frontend/package.json | 2 + 3 files changed, 811 insertions(+), 465 deletions(-) diff --git a/frontend/app/(tabs)/home.tsx b/frontend/app/(tabs)/home.tsx index cfd2fda6..d637d0a1 100644 --- a/frontend/app/(tabs)/home.tsx +++ b/frontend/app/(tabs)/home.tsx @@ -8,6 +8,10 @@ import { import { Text, TextInput, TouchableOpacity, Image, Modal } from "react-native"; import { Animated, Easing } from "react-native"; import * as Speech from "expo-speech"; +import { ElevenLabsClient, play } from "elevenlabs"; +import { Audio } from "expo-av"; +import * as FileSystem from "expo-file-system"; +import { Buffer } from 'buffer'; // Define interfaces for WebSocket messages interface InstructionMessage { @@ -22,6 +26,73 @@ interface InstructionMessage { type WebSocketType = WebSocket | null; export default function HomeScreen() { + // Lock to prevent concurrent playback + const audioLock = useRef(false); + + // Enqueue initial navigation message with delay + const enqueueInitialNavigation = async (destination: string) => { + const initialMessage = `Starting navigation to ${destination}`; + await speakResponse(initialMessage); + await new Promise((resolve) => setTimeout(resolve, 500)); + }; + const audioQueue = useRef([]); // Queue for audio requests + const isPlayingRef = useRef(false); // Track if audio is playing + // Update the speakWithElevenLabs function to return a Promise + // ElevenLabs TTS function with queue support + const speakWithElevenLabs = async (text: string): Promise => { + if (!text) return; + let path = ""; + try { + const voiceId = "21m00Tcm4TlvDq8ikWAM"; + const url = `https://api.elevenlabs.io/v1/text-to-speech/${voiceId}`; + const response = await fetch(url, { + method: "POST", + headers: { + "xi-api-key": "sk_550053542174a9cb083e460d2d6683efec0b83e2554edec2", // Replace with secure storage + "Content-Type": "application/json", + "Accept": "audio/mpeg", + }, + body: JSON.stringify({ + text, + model_id: "eleven_multilingual_v2", + output_format: "mp3_44100_128", + }), + }); + + if (!response.ok) { + throw new Error(`ElevenLabs API error: ${await response.text()}`); + } + + const arrayBuffer = await response.arrayBuffer(); + path = `${FileSystem.documentDirectory}speech-${Date.now()}.mp3`; + await FileSystem.writeAsStringAsync(path, Buffer.from(arrayBuffer).toString("base64"), { + encoding: FileSystem.EncodingType.Base64, + }); + + const { sound } = await Audio.Sound.createAsync({ uri: path }, { shouldPlay: true }); + isPlayingRef.current = true; + + await new Promise((resolve) => { + sound.setOnPlaybackStatusUpdate((status) => { + if (status.isLoaded && status.didJustFinish) { + sound.unloadAsync(); + isPlayingRef.current = false; + resolve(); + } + }); + }); + } catch (error) { + console.error("Error in speakWithElevenLabs:", error); + isPlayingRef.current = false; + throw error; + } finally { + if (path) { + FileSystem.deleteAsync(path).catch((err) => console.warn("File cleanup failed:", err)); + } + } + }; + + const ws = useRef(null); const [recognizing, setRecognizing] = useState(true); // Start with true to unmute const [transcript, setTranscript] = useState(""); @@ -50,7 +121,7 @@ export default function HomeScreen() { console.log("WebSocket connected"); }; - ws.current.onmessage = (event: WebSocketMessageEvent) => { + ws.current.onmessage = async (event: WebSocketMessageEvent) => { try { const parsedData = JSON.parse(event.data as string) as InstructionMessage; let textToSpeak = ""; @@ -62,9 +133,14 @@ export default function HomeScreen() { } if (textToSpeak) { - stopListening(); // Stop STT before speaking + // Wait for initial message to finish + while (audioQueue.current.length > 0 || isPlayingRef.current) { + await new Promise((resolve) => setTimeout(resolve, 100)); + } + + stopListening(); setBackendResponse(textToSpeak); - speakResponse(textToSpeak); // Centralized speaking function + await speakResponse(textToSpeak); } } catch (error) { console.error("Error processing WebSocket message:", error); @@ -86,30 +162,40 @@ export default function HomeScreen() { }, []); // Centralized function to handle speaking - const speakResponse = (text: string) => { - if (recognizing) stopListening(); // Ensure STT is off - setIsSpeaking(true); - - Speech.speak(text, { - rate: 0.8, - pitch: 1.0, - language: "en-US", - onStart: () => setIsSpeaking(true), - onDone: () => { - setIsSpeaking(false); - setTimeout(startListening, 1500); // Delay restart of STT - }, - onStopped: () => { - setIsSpeaking(false); - setTimeout(startListening, 1500); - }, - onError: () => { + // Modified speakResponse function to use ElevenLabs + // Modified speakResponse with lock and sequential queue processing + const speakResponse = async (text: string) => { + if (!text) return; + + audioQueue.current.push(text); + if (audioLock.current) return; + + audioLock.current = true; + + while (audioQueue.current.length > 0) { + const nextText = audioQueue.current[0]; + if (recognizing) stopListening(); + setIsSpeaking(true); + + try { + await speakWithElevenLabs(nextText); + audioQueue.current.shift(); + } catch (error) { + console.error("TTS error:", error); + audioQueue.current.shift(); + } finally { setIsSpeaking(false); - setTimeout(startListening, 1500); - }, - }); - }; + } + await new Promise((resolve) => setTimeout(resolve, 100)); + } + + audioLock.current = false; + + if (audioQueue.current.length === 0) { + setTimeout(startListening, 500); + } + }; // Check permissions and initialize listening useEffect(() => { const initialize = async () => { @@ -188,6 +274,7 @@ export default function HomeScreen() { }; // Navigation function + // Updated startNavigation to use enqueueInitialNavigation const startNavigation = async (dest: string) => { try { setLoading(true); @@ -200,11 +287,11 @@ export default function HomeScreen() { console.log("Navigation started:", response.data); setBackendResponse(`Starting navigation to ${destination}`); - speakResponse(`Starting navigation to ${destination}`); + await enqueueInitialNavigation(destination); } catch (error) { console.error("Error starting navigation:", error); setBackendResponse("Sorry, there was an error starting navigation."); - speakResponse("Sorry, there was an error starting navigation."); + await speakResponse("Sorry, there was an error starting navigation."); } finally { setLoading(false); } @@ -262,441 +349,464 @@ export default function HomeScreen() { }, [transcript]); - return ( - - {/* Top-left section with logo and app name */} - - - CoDriver - - - - - {/* Main container to align the content */} - - - - - - - {/* Text container for song title */} - - - The Color Violet · Tory Lanez - - - - {/* Bluetooth icon + BeatSpill+ in a row */} - - - iPhone 16 - - - - - {/* Second Bluetooth icon */} - - - - - - - - - - - - - - - {/* Background AI-themed blob image */} - - - - - {/* Destination Button at the bottom */} - - {/* Left Section: Icon */} - - - - - - {/* Right Section: Text Input */} - - - - - - - - - - - - - {recognizing ? "Mute" : "Unmute"} - - - - - ); + return ( + + { + // Test a voice - change the identifier to test different voices + speakWithElevenLabs("This is a test of the text to speech functionality using this voice"); + }} + > + Test Voice + + {/* Top-left section with logo and app name */} + + + CoDriver + + + + + {/* Main container to align the content */} + + + + + + + {/* Text container for song title */} + + + The Color Violet · Tory Lanez + + + + {/* Bluetooth icon + BeatSpill+ in a row */} + + + iPhone 16 + + + + + {/* Second Bluetooth icon */} + + + + + + + + + + + + + + + {/* Background AI-themed blob image */} + + + + + {/* Destination Button at the bottom */} + + {/* Left Section: Icon */} + + + + + + {/* Right Section: Text Input */} + + + + + + + + + + + + + {recognizing ? "Mute" : "Unmute"} + + + + + ); } const styles = StyleSheet.create({ - destinationInput: { - flex: 1, - fontSize: 18, - color: '#AE9A8C', - paddingVertical: 10, - paddingHorizontal: 10, - backgroundColor: 'rgba(0, 0, 0, 0)', // Slightly transparent background - borderRadius: 20, - marginLeft: -10, // Space between the icon and input - width: '30%' - }, - muteButton: { - position: 'absolute', - bottom: 40, - backgroundColor: '#2D2A38', - paddingVertical: 12, - borderRadius: 30, - width: '40%', - alignItems: 'center', - justifyContent: 'center', // Ensure content is centered - marginBottom: 80, - marginRight: 120, - marginLeft: 120, - }, - - - destinationTabText2: { - fontSize: 18, - fontWeight: '600', - color: '#AE9A8C', - textAlign: 'center', // Center text inside the button - width: '110%', // Ensure text takes full width to remain centered - }, - - - - - leftSection3: { - flexDirection: 'row', // Align icon horizontally in the left section - justifyContent: 'center', - alignItems: 'center', // Center the icon vertically - }, - rightSection3: { - flexDirection: 'row', // Align text and icon horizontally in the right section - justifyContent: 'center', - alignItems: 'center', // Center them vertically - marginLeft: 20 - }, - container: { - flex: 1, - backgroundColor: 'black', - }, - - - topLeftContainer: { - position: 'absolute', - top: 40, - left: 20, - flexDirection: 'row', - alignItems: 'center', - }, - logoImage: { - width: 40, - height: 40, - marginRight: 8, - }, - topLeftText: { - color: 'white', - fontSize: 24, - fontWeight: 'bold', - }, - blobImage: { - position: 'absolute', - width: 670, - height: 630, - marginTop: -680, - top: '25%', - alignSelf: 'center', - }, - modalContainer: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - backgroundColor: 'rgba(0, 0, 0, 0.5)', - }, - popup: { - width: '85%', - height: 300, - backgroundColor: 'rgba(255, 255, 255, 0.3)', - padding: 25, - borderRadius: 15, - alignItems: 'center', - }, - closeButton: { - position: 'absolute', - top: 5, - right: 10, - padding: 5, - }, - closeText: { - fontSize: 24, - color: 'white', - }, - popupTitle: { - fontSize: 40, - fontWeight: 'bold', - color: 'white', - alignSelf: 'flex-start', - textAlign: 'left', - marginBottom: 10, - }, - input: { - width: '100%', - height: 50, - backgroundColor: 'rgba(255, 255, 255, 0.2)', - borderRadius: 5, - paddingHorizontal: 10, - color: 'white', - fontSize: 16, - marginBottom: 20, - textAlignVertical: 'center', - }, - button: { - backgroundColor: 'black', - paddingVertical: 10, - paddingHorizontal: 20, - borderRadius: 8, - }, - buttonText: { - color: 'white', - fontSize: 16, - }, - spotifyTab: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - borderRadius: 15, - paddingVertical: 12, - paddingHorizontal: -20, // Keeps original padding - borderWidth: 1, - backgroundColor: '#2D2A38', - borderColor: '#2D2A38', - marginTop: 706, - alignSelf: 'center', - marginLeft: 10, - marginRight: 10, - paddingLeft: -10, - zIndex: 10 - }, - spotifyTabText: { - fontSize: 18, - lineHeight: 26, - fontWeight: '600', - color: '#AE9A8C', - flexShrink: 1, // Ensures text doesn't wrap - textAlign: 'center', // Keeps text aligned within the button - paddingLeft: -100 - }, - destinationTabText: { - fontSize: 18, - lineHeight: 26, - fontWeight: '600', - color: '#AE9A8C', - flexShrink: 1, // Ensures text doesn't wrap - textAlign: 'center', // Keeps text aligned within the button - }, - albumCover: { - width: 60, // Adjust width as needed - height: 60, // Adjust height as needed - marginRight: 10, - marginLeft: 20, - borderRadius: 5, // Optional: for rounded corners - }, - SearchIcon: { - width: 30, // Adjust width as needed - height: 30, // Adjust height as needed - marginRight: 10, // Space between the icon and the text - }, - bluetooth: { - width: 30, // Adjust width as needed - height: 30, // Adjust height as needed - marginRight: 50, - marginLeft: -15, - borderRadius: 5, // Optional: for rounded corners - paddingLeft: 15, - paddingRight: 0 - }, - leftSection: { - flexDirection: 'column', // Align album cover vertically in the left section - justifyContent: 'center', // Center the content vertically in the left section - marginRight: 10, // Space between the album cover and the text - }, - leftSection2: { - flexDirection: 'row', // Align icon horizontally in the left section - justifyContent: 'center', - alignItems: 'center', // Center the icon vertically - }, - - - rightSection2: { - flexDirection: 'row', // Align text and icon horizontally in the right section - justifyContent: 'center', - alignItems: 'center', // Center them vertically - width: '87%' - }, - rightSection: { - flexDirection: 'row', // Arrange items horizontally - justifyContent: 'center', - alignItems: 'center', // Align items vertically in the center - }, - bluetooth2: { - width: 40, // Increase the size of the Bluetooth icon - height: 40, // Increase the size of the Bluetooth icon - marginLeft: 10, // Adjust spacing between the icons - borderRadius: 5, // Optional: for rounded corners - }, - Voice2: { - width: 30, // Adjust width as needed - height: 30, // Adjust height as needed - marginLeft: 10, // Space between the text and the icon - }, - bluetoothText: { - fontSize: 16, - color: '#AE9A8C', - textAlign: 'left', - marginTop: 2, // Keeps slight spacing between song text and BeatSpill - marginLeft: -40, // Moves text closer to Bluetooth 1 logo - }, - beatspillContainer: { - flexDirection: 'row', // Align Bluetooth and text in a row - alignItems: 'center', // Center them vertically - marginTop: 2, // Small spacing below song title - }, - bottomSection: { - position: 'absolute', - bottom: 0, // Adjust as needed - left: 20, // Align to the left - width: '80%', // Adjust width as needed - flexDirection: 'column', // Stack the bars vertically - alignItems: 'flex-start', // Align the progress bars to the left - }, - progressBar: { - width: '90%', // Adjust width of the first progress bar - height: 7, // Adjust height as needed - marginLeft: -2 - }, - progressBar2: { - width: '118%', // Adjust width of the second progress bar - height: 7, // Adjust height as needed - opacity: 0.6, // Lighter shade by reducing opacity - tintColor: 'lightgray', // Lighter color shade - }, - - - googleBtn: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - borderRadius: 30, - paddingVertical: 10, - paddingHorizontal: 20, - backgroundColor: '#FFFFFF', - }, - googleBtnText: { - fontSize: 18, - fontWeight: '600', - color: '#000', - }, - - - // New Destination Button Styles - destinationButton: { - position: 'absolute', - bottom: 530, - transform: [{ translateX: -50 }], - backgroundColor: '#2D2A38', // Button color - paddingVertical: 12, - paddingHorizontal: 20, - borderRadius: 30, - width: '80%', - alignItems: 'center', - flexDirection: 'row', // Align items horizontally - //justifyContent: 'space-between', // Space between left and right sections - marginBottom: 80, - marginRight: 120, - marginLeft: 90, - }, - destinationButtonText: { - color: 'white', - fontSize: 18, - fontWeight: 'bold', - }, - image: { - position: 'absolute', - width: 20, // Adjust the width as needed - height: 20, // Adjust the height as needed - }, - left: { - //marginLeft: 50, - marginRight: 200, - //top: '50%', // Vertically centered - //transform: [{ translateY: -10 }], // Adjust for proper vertical alignment - }, - right: { - right: -20, // Adjust position to the right - top: '50%', // Vertically centered - transform: [{ translateY: -10 }], // Adjust for proper vertical alignment - }, - bottom: { - bottom: -20, // Adjust position to the bottom - left: '50%', // Horizontally centered - transform: [{ translateX: -10 }], // Adjust for proper horizontal alignment - }, + voiceTestButton: { + position: 'absolute', + top: 50, // Adjust position as needed + right: 20, + backgroundColor: '#2D2A38', + paddingVertical: 10, + paddingHorizontal: 15, + borderRadius: 20, + }, + buttonText2: { + color: '#AE9A8C', + fontSize: 16, + fontWeight: '600', + }, + destinationInput: { + flex: 1, + fontSize: 18, + color: '#AE9A8C', + paddingVertical: 10, + paddingHorizontal: 10, + backgroundColor: 'rgba(0, 0, 0, 0)', // Slightly transparent background + borderRadius: 20, + marginLeft: -10, // Space between the icon and input + width: '30%' + }, + muteButton: { + position: 'absolute', + bottom: 40, + backgroundColor: '#2D2A38', + paddingVertical: 12, + borderRadius: 30, + width: '40%', + alignItems: 'center', + justifyContent: 'center', // Ensure content is centered + marginBottom: 80, + marginRight: 120, + marginLeft: 120, + }, + + + destinationTabText2: { + fontSize: 18, + fontWeight: '600', + color: '#AE9A8C', + textAlign: 'center', // Center text inside the button + width: '110%', // Ensure text takes full width to remain centered + }, + + + + + leftSection3: { + flexDirection: 'row', // Align icon horizontally in the left section + justifyContent: 'center', + alignItems: 'center', // Center the icon vertically + }, + rightSection3: { + flexDirection: 'row', // Align text and icon horizontally in the right section + justifyContent: 'center', + alignItems: 'center', // Center them vertically + marginLeft: 20 + }, + container: { + flex: 1, + backgroundColor: 'black', + }, + + + topLeftContainer: { + position: 'absolute', + top: 40, + left: 20, + flexDirection: 'row', + alignItems: 'center', + }, + logoImage: { + width: 40, + height: 40, + marginRight: 8, + }, + topLeftText: { + color: 'white', + fontSize: 24, + fontWeight: 'bold', + }, + blobImage: { + position: 'absolute', + width: 670, + height: 630, + marginTop: -680, + top: '25%', + alignSelf: 'center', + }, + modalContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: 'rgba(0, 0, 0, 0.5)', + }, + popup: { + width: '85%', + height: 300, + backgroundColor: 'rgba(255, 255, 255, 0.3)', + padding: 25, + borderRadius: 15, + alignItems: 'center', + }, + closeButton: { + position: 'absolute', + top: 5, + right: 10, + padding: 5, + }, + closeText: { + fontSize: 24, + color: 'white', + }, + popupTitle: { + fontSize: 40, + fontWeight: 'bold', + color: 'white', + alignSelf: 'flex-start', + textAlign: 'left', + marginBottom: 10, + }, + input: { + width: '100%', + height: 50, + backgroundColor: 'rgba(255, 255, 255, 0.2)', + borderRadius: 5, + paddingHorizontal: 10, + color: 'white', + fontSize: 16, + marginBottom: 20, + textAlignVertical: 'center', + }, + button: { + backgroundColor: 'black', + paddingVertical: 10, + paddingHorizontal: 20, + borderRadius: 8, + }, + buttonText: { + color: 'white', + fontSize: 16, + }, + spotifyTab: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + borderRadius: 15, + paddingVertical: 12, + paddingHorizontal: -20, // Keeps original padding + borderWidth: 1, + backgroundColor: '#2D2A38', + borderColor: '#2D2A38', + marginTop: 706, + alignSelf: 'center', + marginLeft: 10, + marginRight: 10, + paddingLeft: -10, + zIndex: 10 + }, + spotifyTabText: { + fontSize: 18, + lineHeight: 26, + fontWeight: '600', + color: '#AE9A8C', + flexShrink: 1, // Ensures text doesn't wrap + textAlign: 'center', // Keeps text aligned within the button + paddingLeft: -100 + }, + destinationTabText: { + fontSize: 18, + lineHeight: 26, + fontWeight: '600', + color: '#AE9A8C', + flexShrink: 1, // Ensures text doesn't wrap + textAlign: 'center', // Keeps text aligned within the button + }, + albumCover: { + width: 60, // Adjust width as needed + height: 60, // Adjust height as needed + marginRight: 10, + marginLeft: 20, + borderRadius: 5, // Optional: for rounded corners + }, + SearchIcon: { + width: 30, // Adjust width as needed + height: 30, // Adjust height as needed + marginRight: 10, // Space between the icon and the text + }, + bluetooth: { + width: 30, // Adjust width as needed + height: 30, // Adjust height as needed + marginRight: 50, + marginLeft: -15, + borderRadius: 5, // Optional: for rounded corners + paddingLeft: 15, + paddingRight: 0 + }, + leftSection: { + flexDirection: 'column', // Align album cover vertically in the left section + justifyContent: 'center', // Center the content vertically in the left section + marginRight: 10, // Space between the album cover and the text + }, + leftSection2: { + flexDirection: 'row', // Align icon horizontally in the left section + justifyContent: 'center', + alignItems: 'center', // Center the icon vertically + }, + + + rightSection2: { + flexDirection: 'row', // Align text and icon horizontally in the right section + justifyContent: 'center', + alignItems: 'center', // Center them vertically + width: '87%' + }, + rightSection: { + flexDirection: 'row', // Arrange items horizontally + justifyContent: 'center', + alignItems: 'center', // Align items vertically in the center + }, + bluetooth2: { + width: 40, // Increase the size of the Bluetooth icon + height: 40, // Increase the size of the Bluetooth icon + marginLeft: 10, // Adjust spacing between the icons + borderRadius: 5, // Optional: for rounded corners + }, + Voice2: { + width: 30, // Adjust width as needed + height: 30, // Adjust height as needed + marginLeft: 10, // Space between the text and the icon + }, + bluetoothText: { + fontSize: 16, + color: '#AE9A8C', + textAlign: 'left', + marginTop: 2, // Keeps slight spacing between song text and BeatSpill + marginLeft: -40, // Moves text closer to Bluetooth 1 logo + }, + beatspillContainer: { + flexDirection: 'row', // Align Bluetooth and text in a row + alignItems: 'center', // Center them vertically + marginTop: 2, // Small spacing below song title + }, + bottomSection: { + position: 'absolute', + bottom: 0, // Adjust as needed + left: 20, // Align to the left + width: '80%', // Adjust width as needed + flexDirection: 'column', // Stack the bars vertically + alignItems: 'flex-start', // Align the progress bars to the left + }, + progressBar: { + width: '90%', // Adjust width of the first progress bar + height: 7, // Adjust height as needed + marginLeft: -2 + }, + progressBar2: { + width: '118%', // Adjust width of the second progress bar + height: 7, // Adjust height as needed + opacity: 0.6, // Lighter shade by reducing opacity + tintColor: 'lightgray', // Lighter color shade + }, + + + googleBtn: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + borderRadius: 30, + paddingVertical: 10, + paddingHorizontal: 20, + backgroundColor: '#FFFFFF', + }, + googleBtnText: { + fontSize: 18, + fontWeight: '600', + color: '#000', + }, + + + // New Destination Button Styles + destinationButton: { + position: 'absolute', + bottom: 530, + transform: [{ translateX: -50 }], + backgroundColor: '#2D2A38', // Button color + paddingVertical: 12, + paddingHorizontal: 20, + borderRadius: 30, + width: '80%', + alignItems: 'center', + flexDirection: 'row', // Align items horizontally + //justifyContent: 'space-between', // Space between left and right sections + marginBottom: 80, + marginRight: 120, + marginLeft: 90, + }, + destinationButtonText: { + color: 'white', + fontSize: 18, + fontWeight: 'bold', + }, + image: { + position: 'absolute', + width: 20, // Adjust the width as needed + height: 20, // Adjust the height as needed + }, + left: { + //marginLeft: 50, + marginRight: 200, + //top: '50%', // Vertically centered + //transform: [{ translateY: -10 }], // Adjust for proper vertical alignment + }, + right: { + right: -20, // Adjust position to the right + top: '50%', // Vertically centered + transform: [{ translateY: -10 }], // Adjust for proper vertical alignment + }, + bottom: { + bottom: -20, // Adjust position to the bottom + left: '50%', // Horizontally centered + transform: [{ translateX: -10 }], // Adjust for proper horizontal alignment + }, }); diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 214af91a..fcb147fe 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -15,7 +15,9 @@ "@react-navigation/stack": "^7.1.2", "assemblyai": "^4.9.0", "axios": "^1.8.2", + "buffer": "^6.0.3", "dotenv": "^16.4.7", + "elevenlabs": "^1.56.1", "expo": "~52.0.38", "expo-audio": "^0.3.5", "expo-av": "^15.0.2", @@ -7349,6 +7351,31 @@ "readable-stream": "^3.4.0" } }, + "node_modules/bl/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -7452,9 +7479,9 @@ } }, "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", "funding": [ { "type": "github", @@ -7472,7 +7499,7 @@ "license": "MIT", "dependencies": { "base64-js": "^1.3.1", - "ieee754": "^1.1.13" + "ieee754": "^1.2.1" } }, "node_modules/buffer-alloc": { @@ -8758,6 +8785,143 @@ "integrity": "sha512-eHhqaja8tE/FNpIiBrvBjFV/SSKpyWHLvxuR9dPTdo+3V9ppdLmFB7ZZQ98qNovcngPLYIz0oOBF9P0FfZef5Q==", "license": "ISC" }, + "node_modules/elevenlabs": { + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/elevenlabs/-/elevenlabs-1.56.1.tgz", + "integrity": "sha512-q08c2DFjqhPDUpSyECfuWEtZhXGLerbYdNF04YnHdJDucaMfi84/TqyVO/d2ltppRLFE+1GCsJ32vLG/8EB17g==", + "license": "MIT", + "dependencies": { + "command-exists": "^1.2.9", + "execa": "^5.1.1", + "form-data": "^4.0.0", + "form-data-encoder": "^4.0.2", + "formdata-node": "^6.0.3", + "node-fetch": "^2.7.0", + "qs": "^6.13.1", + "readable-stream": "^4.5.2", + "url-join": "4.0.1" + } + }, + "node_modules/elevenlabs/node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/elevenlabs/node_modules/form-data": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/elevenlabs/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/elevenlabs/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/elevenlabs/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/elevenlabs/node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/elevenlabs/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/elevenlabs/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/elevenlabs/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, "node_modules/emittery": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", @@ -9731,7 +9895,6 @@ "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.8.x" } @@ -10864,6 +11027,24 @@ "node": ">= 6" } }, + "node_modules/form-data-encoder": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-4.0.2.tgz", + "integrity": "sha512-KQVhvhK8ZkWzxKxOr56CPulAhH3dobtuQ4+hNQ+HekH/Wp5gSOafqRAeTphQUJAIk0GBvHZgJ2ZGRWd5kphMuw==", + "license": "MIT", + "engines": { + "node": ">= 18" + } + }, + "node_modules/formdata-node": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-6.0.3.tgz", + "integrity": "sha512-8e1++BCiTzUno9v5IZ2J6bv4RU+3UKDmqWUQD0MIMVCd9AdhWkO1gw57oo1mNEX1dMq2EGI+FbWz4B92pscSQg==", + "license": "MIT", + "engines": { + "node": ">= 18" + } + }, "node_modules/freeport-async": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/freeport-async/-/freeport-async-2.0.0.tgz", @@ -15903,6 +16084,15 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/progress": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", @@ -16012,6 +16202,21 @@ "qrcode-terminal": "bin/qrcode-terminal.js" } }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/query-string": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz", @@ -17987,7 +18192,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "devOptional": true, "license": "MIT", "dependencies": { "safe-buffer": "~5.2.0" @@ -19231,6 +19435,12 @@ "punycode": "^2.1.0" } }, + "node_modules/url-join": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", + "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", + "license": "MIT" + }, "node_modules/url-parse": { "version": "1.5.10", "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", @@ -19501,6 +19711,30 @@ "node": ">=10" } }, + "node_modules/whatwg-url-without-unicode/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/whatwg-url-without-unicode/node_modules/webidl-conversions": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 99789545..d0d60cb5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -22,7 +22,9 @@ "@react-navigation/stack": "^7.1.2", "assemblyai": "^4.9.0", "axios": "^1.8.2", + "buffer": "^6.0.3", "dotenv": "^16.4.7", + "elevenlabs": "^1.56.1", "expo": "~52.0.38", "expo-audio": "^0.3.5", "expo-av": "^15.0.2",