// Mobile Detection const isMobileApp = () => window.Capacitor !== undefined; console.log('Mobile App:', isMobileApp()); // API URL Configuration const getApiUrl = () => { if (isMobileApp()) { // For mobile apps, use your deployed API URL // Or local IP for testing: 'http://192.168.1.100:3000' return localStorage.getItem('apiUrl') || 'http://localhost:3000'; } return ''; // Use relative URL for web }; const API_URL = getApiUrl(); console.log('API URL:', API_URL); // Initialize Speech Recognition API const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; const recognition = SpeechRecognition ? new SpeechRecognition() : null; function isVoiceInputAllowed() { return window.isSecureContext || location.protocol === 'https:' || location.hostname === 'localhost' || location.hostname === '127.0.0.1' || location.hostname.endsWith('.local'); } // Initialize Speech Synthesis API const speechSynthesis = window.speechSynthesis; let availableVoices = []; let voicesLoaded = false; let mediaRecorder = null; let audioStream = null; let audioChunks = []; let isRecording = false; let autoSpeakAfterVoice = false; // DOM Elements const sourceText = document.getElementById('sourceText'); const translatedText = document.getElementById('translatedText'); const sourceLang = document.getElementById('sourceLang'); const targetLang = document.getElementById('targetLang'); const voiceBtn = document.getElementById('voiceBtn'); const clearSourceBtn = document.getElementById('clearSourceBtn') || document.getElementById('clearBtn'); const speakBtn = document.getElementById('speakBtn'); const copyBtn = document.getElementById('copyBtn'); const swapBtn = document.getElementById('swapBtn'); const voiceStatus = document.getElementById('voiceStatus'); const statusMessage = document.getElementById('statusMessage') || document.getElementById('status'); const charCount = document.getElementById('charCount'); const copyFeedback = document.getElementById('copyFeedback'); // Language codes for speech recognition and synthesis const langMap = { 'en': 'en-US', 'es': 'es-ES', 'fr': 'fr-FR', 'de': 'de-DE', 'pt': 'pt-BR', 'it': 'it-IT', 'nl': 'nl-NL', 'ru': 'ru-RU', 'ar': 'ar-SA', 'ja': 'ja-JP', 'ko': 'ko-KR', 'zh': 'zh-CN', 'hi': 'hi-IN', 'ta': 'ta-IN', 'te': 'te-IN', 'ml': 'ml-IN', 'pa': 'pa-IN', 'kn': 'kn-IN', 'id': 'id-ID', 'vi': 'vi-VN', 'tr': 'tr-TR' }; const remoteTtsLanguages = new Set(['ta', 'te', 'ml', 'pa', 'kn']); // Initialize document.addEventListener('DOMContentLoaded', () => { setupEventListeners(); // Check for HTTPS/secure-context requirement for voice input if (!isVoiceInputAllowed() && !isMobileApp()) { console.warn('Voice input requires a secure context. Access via https://localhost:3443 for full functionality.'); } // Check voice input support const voiceSupportAvailable = !!(navigator.mediaDevices?.getUserMedia && window.MediaRecorder); if (!voiceSupportAvailable) { console.warn('Voice recording not supported in this browser.'); if (voiceBtn) { voiceBtn.disabled = true; voiceBtn.title = 'Voice input not supported in this browser'; voiceBtn.style.opacity = '0.5'; } } // Check speech synthesis support if (!speechSynthesis) { console.warn('Speech synthesis not supported in this browser.'); if (speakBtn) { speakBtn.disabled = true; speakBtn.title = 'Text-to-speech not supported in this browser'; speakBtn.style.opacity = '0.5'; } } else { loadVoices(); } showStatus('Ready to translate!', 'success'); }); // Setup Event Listeners function setupEventListeners() { const bind = (element, event, handler) => { if (element) { element.addEventListener(event, handler); } }; bind(sourceText, 'input', handleTextInput); bind(voiceBtn, 'click', handleVoiceInput); bind(clearSourceBtn, 'click', clearInput); bind(speakBtn, 'click', speakTranslation); bind(copyBtn, 'click', copyTranslation); bind(swapBtn, 'click', swapLanguages); bind(sourceLang, 'change', translateOnChange); bind(targetLang, 'change', translateOnChange); if (recognition) { recognition.addEventListener('start', () => { voiceBtn.classList.add('listening'); voiceStatus.textContent = '🎤 Listening...'; }); recognition.addEventListener('result', handleSpeechResult); recognition.addEventListener('end', () => { voiceBtn.classList.remove('listening'); voiceStatus.textContent = ''; }); recognition.addEventListener('error', (e) => { console.error('Speech recognition error:', e.error); let errorMessage = ''; switch(e.error) { case 'no-speech': errorMessage = 'No speech detected. Please try again.'; break; case 'audio-capture': errorMessage = 'Audio capture failed. Check your microphone.'; break; case 'not-allowed': errorMessage = 'Microphone permission denied.'; break; case 'network': errorMessage = 'Network error during speech recognition.'; break; case 'service-not-allowed': errorMessage = 'Speech recognition service not allowed.'; break; default: errorMessage = `Speech recognition error: ${e.error}`; } voiceStatus.textContent = `❌ ${errorMessage}`; voiceBtn.classList.remove('listening'); showStatus(errorMessage, 'error'); }); } } // Handle Text Input function handleTextInput(e) { const text = e.target.value; if (charCount) { charCount.textContent = text.length; } if (text.trim().length > 0) { debounceTranslate(); } else { translatedText.textContent = ''; } } // Debounce Translation let translateTimeout; function debounceTranslate() { clearTimeout(translateTimeout); translateTimeout = setTimeout(() => { translateText(); }, 500); } // Translate Text async function translateText() { const text = sourceText.value.trim(); if (!text) return; const source = sourceLang.value; const target = targetLang.value; if (source === target) { translatedText.textContent = text; return; } try { showStatus('Translating...', 'loading'); const response = await fetch(`${API_URL}/api/translate`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: text, sourceLang: source, targetLang: target }) }); if (!response.ok) { throw new Error('Translation failed'); } const data = await response.json(); translatedText.textContent = data.translatedText; showStatus('Translation complete!', 'success'); if (autoSpeakAfterVoice) { autoSpeakAfterVoice = false; speakTranslation(); } } catch (error) { console.error('Translation error:', error); // Fallback to client-side translation using simple mapping translatedText.textContent = text; // Fallback: show original text showStatus('Using direct translation (server unavailable)', 'error'); } } // Handle Voice Input async function handleVoiceInput() { if (!navigator.mediaDevices?.getUserMedia || !window.MediaRecorder) { showStatus('Voice recording is not supported in this browser.', 'error'); return; } if (!isVoiceInputAllowed() && !isMobileApp()) { showStatus('Voice input requires a secure context. Please access via https://localhost:3443', 'error'); return; } if (isRecording) { if (mediaRecorder && mediaRecorder.state !== 'inactive') { mediaRecorder.stop(); } isRecording = false; voiceBtn.classList.remove('listening'); voiceStatus.textContent = '🔄 Processing voice...'; return; } try { audioChunks = []; const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); audioStream = stream; const mimeType = MediaRecorder.isTypeSupported('audio/webm') ? 'audio/webm' : 'audio/mp4'; mediaRecorder = new MediaRecorder(stream, { mimeType }); mediaRecorder.ondataavailable = (event) => { if (event.data.size > 0) { audioChunks.push(event.data); } }; mediaRecorder.onstop = async () => { const blob = new Blob(audioChunks, { type: mediaRecorder.mimeType || 'audio/webm' }); if (audioStream) { audioStream.getTracks().forEach(track => track.stop()); audioStream = null; } await transcribeAudio(blob); }; mediaRecorder.start(); isRecording = true; voiceBtn.classList.add('listening'); voiceStatus.textContent = '🎤 Listening...'; showStatus('Listening for voice input...', 'success'); } catch (error) { console.error('Microphone permission denied:', error); showStatus('Microphone permission required for voice input', 'error'); voiceStatus.textContent = '❌ Microphone access denied'; } } async function transcribeAudio(blob) { try { const response = await fetch(`${API_URL}/api/transcribe`, { method: 'POST', headers: { 'Content-Type': blob.type || 'audio/webm' }, body: blob }); if (!response.ok) { throw new Error('Transcription failed'); } const data = await response.json(); const transcript = data.transcript || ''; sourceText.value = transcript; if (charCount) { charCount.textContent = transcript.length; } if (voiceStatus) { voiceStatus.textContent = '✓ Voice captured'; } autoSpeakAfterVoice = true; await translateText(); } catch (error) { console.error('Voice transcription error:', error); showStatus('Voice transcription failed', 'error'); voiceStatus.textContent = '❌ Voice transcription failed'; } } function loadVoices() { if (!speechSynthesis || typeof speechSynthesis.getVoices !== 'function') return; availableVoices = speechSynthesis.getVoices(); voicesLoaded = availableVoices.length > 0; if (!voicesLoaded) { speechSynthesis.onvoiceschanged = () => { availableVoices = speechSynthesis.getVoices(); voicesLoaded = availableVoices.length > 0; }; } } function getPreferredVoice(languageCode) { if (!speechSynthesis || !availableVoices.length) return null; const normalizedCode = languageCode.toLowerCase(); const directMatch = availableVoices.find(voice => voice.lang && voice.lang.toLowerCase() === normalizedCode); if (directMatch) return directMatch; const languageMatch = availableVoices.find(voice => voice.lang && voice.lang.toLowerCase().startsWith(normalizedCode)); if (languageMatch) return languageMatch; return availableVoices.find(voice => voice.localService) || availableVoices[0] || null; } // Handle Speech Result function handleSpeechResult(event) { let transcript = ''; let isFinal = false; for (let i = event.resultIndex; i < event.results.length; i++) { transcript += event.results[i][0].transcript; if (event.results[i].isFinal) { isFinal = true; } } // Show interim results sourceText.value = transcript; if (charCount) { charCount.textContent = transcript.length; } if (isFinal) { voiceStatus.textContent = '✓ Transcribed successfully'; setTimeout(() => { voiceStatus.textContent = ''; }, 2000); translateText(); } else { voiceStatus.textContent = '🎤 Listening... (interim)'; } } // Speak Translation function speakTranslation() { if (!speechSynthesis) { showStatus('Text-to-speech is not supported in this browser', 'error'); return; } const text = translatedText.textContent?.trim(); if (!text || text === 'Translation will appear here...') { showStatus('Nothing to speak', 'error'); return; } const speakText = () => { speechSynthesis.cancel(); speechSynthesis.resume(); const utterance = new SpeechSynthesisUtterance(text.replace(/\s+/g, ' ')); utterance.lang = langMap[targetLang.value] || 'en-US'; utterance.rate = 0.95; utterance.pitch = 1; utterance.volume = 1; const preferredVoice = getPreferredVoice(utterance.lang); const useRemote = remoteTtsLanguages.has(targetLang.value) && !!API_URL; if (useRemote) { speakWithRemoteTts(text, targetLang.value, utterance, preferredVoice); return; } if (preferredVoice) { utterance.voice = preferredVoice; } utterance.onstart = () => { speakBtn.disabled = true; showStatus('Speaking...', 'success'); }; utterance.onend = () => { speakBtn.disabled = false; showStatus('Done speaking', 'success'); }; utterance.onerror = (e) => { speakBtn.disabled = false; showStatus(`Speech error: ${e.error}`, 'error'); }; speechSynthesis.speak(utterance); }; if (!voicesLoaded && typeof speechSynthesis.getVoices === 'function') { loadVoices(); window.setTimeout(speakText, 250); } else { speakText(); } } async function speakWithRemoteTts(text, targetLang, utterance, preferredVoice) { if (!API_URL) { showStatus('Cannot generate speech without a server URL', 'error'); return; } try { showStatus('Generating speech...', 'loading'); speakBtn.disabled = true; const response = await fetch(`${API_URL}/api/speak`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text, targetLang }) }); if (!response.ok) { throw new Error('Remote speech generation failed'); } const data = await response.json(); if (!data.audio) { throw new Error('No audio returned from speech service'); } const audioType = data.format || 'audio/wav'; const audio = new Audio(`data:${audioType};base64,${data.audio}`); audio.onended = () => { speakBtn.disabled = false; showStatus('Done speaking', 'success'); }; audio.onerror = (e) => { console.error('Remote audio playback error', e); if (preferredVoice) { utterance.voice = preferredVoice; } utterance.onstart = () => { speakBtn.disabled = true; showStatus('Speaking...', 'success'); }; utterance.onend = () => { speakBtn.disabled = false; showStatus('Done speaking', 'success'); }; utterance.onerror = (err) => { speakBtn.disabled = false; showStatus(`Speech error: ${err.error}`, 'error'); }; speechSynthesis.speak(utterance); }; await audio.play(); } catch (error) { console.warn('Remote TTS failed, falling back to browser speech:', error); if (preferredVoice) { utterance.voice = preferredVoice; } utterance.onstart = () => { speakBtn.disabled = true; showStatus('Speaking...', 'success'); }; utterance.onend = () => { speakBtn.disabled = false; showStatus('Done speaking', 'success'); }; utterance.onerror = (err) => { speakBtn.disabled = false; showStatus(`Speech error: ${err.error}`, 'error'); }; speechSynthesis.speak(utterance); } } // Copy Translation function copyTranslation() { const text = translatedText.textContent; if (!text || text === 'Translation will appear here...') { showStatus('Nothing to copy', 'error'); return; } navigator.clipboard.writeText(text).then(() => { if (copyFeedback) { copyFeedback.textContent = '✓ Copied!'; setTimeout(() => { copyFeedback.textContent = ''; }, 2000); } showStatus('Copied to clipboard', 'success'); }).catch(err => { showStatus('Failed to copy', 'error'); }); } // Clear Input function clearInput() { sourceText.value = ''; translatedText.textContent = ''; if (charCount) { charCount.textContent = '0'; } if (voiceStatus) { voiceStatus.textContent = ''; } autoSpeakAfterVoice = false; showStatus('Cleared', 'success'); } // Swap Languages function swapLanguages() { const temp = sourceLang.value; sourceLang.value = targetLang.value; targetLang.value = temp; const tempText = sourceText.value; sourceText.value = translatedText.textContent === 'Translation will appear here...' ? '' : translatedText.textContent; translatedText.textContent = tempText || ''; if (charCount) { charCount.textContent = sourceText.value.length; } translateOnChange(); } // Translate on Language Change function translateOnChange() { if (sourceText.value.trim().length > 0) { translateText(); } } // Show Status Message function showStatus(message, type = 'info') { if (!statusMessage) return; statusMessage.textContent = message; statusMessage.className = type; if (type === 'loading') { statusMessage.style.opacity = '0.7'; } else { statusMessage.style.opacity = '1'; } } // Fallback: Simple offline translation (for demo purposes) const translationMap = { 'en-es': { 'hello': 'hola', 'goodbye': 'adiós', 'thank you': 'gracias', 'please': 'por favor', 'yes': 'sí', 'no': 'no' }, 'en-fr': { 'hello': 'bonjour', 'goodbye': 'au revoir', 'thank you': 'merci', 'please': 's\'il vous plaît', 'yes': 'oui', 'no': 'non' } }; console.log('✓ Translator app loaded successfully'); console.log('Features available:'); console.log('- Text translation'); console.log('- Voice input (if supported by browser)'); console.log('- Text-to-speech output'); console.log('- Language swapping'); console.log('- Copy to clipboard');