quasi-final
This commit is contained in:
600
static/js/app.js
Normal file
600
static/js/app.js
Normal file
@@ -0,0 +1,600 @@
|
||||
// Main application JavaScript with PWA support
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Register service worker
|
||||
if ('serviceWorker' in navigator) {
|
||||
registerServiceWorker();
|
||||
}
|
||||
|
||||
// Initialize app
|
||||
initApp();
|
||||
|
||||
// Check for PWA installation prompts
|
||||
initInstallPrompt();
|
||||
});
|
||||
|
||||
// Service Worker Registration
|
||||
async function registerServiceWorker() {
|
||||
try {
|
||||
const registration = await navigator.serviceWorker.register('/service-worker.js');
|
||||
console.log('Service Worker registered with scope:', registration.scope);
|
||||
|
||||
// Setup periodic sync if available
|
||||
if ('periodicSync' in registration) {
|
||||
// Request permission for background sync
|
||||
const status = await navigator.permissions.query({
|
||||
name: 'periodic-background-sync',
|
||||
});
|
||||
|
||||
if (status.state === 'granted') {
|
||||
try {
|
||||
// Register for background sync to check for updates
|
||||
await registration.periodicSync.register('translation-updates', {
|
||||
minInterval: 24 * 60 * 60 * 1000, // once per day
|
||||
});
|
||||
console.log('Periodic background sync registered');
|
||||
} catch (error) {
|
||||
console.error('Periodic background sync could not be registered:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Setup push notification if available
|
||||
if ('PushManager' in window) {
|
||||
setupPushNotifications(registration);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Service Worker registration failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the main application
|
||||
function initApp() {
|
||||
// DOM elements
|
||||
const recordBtn = document.getElementById('recordBtn');
|
||||
const translateBtn = document.getElementById('translateBtn');
|
||||
const sourceText = document.getElementById('sourceText');
|
||||
const translatedText = document.getElementById('translatedText');
|
||||
const sourceLanguage = document.getElementById('sourceLanguage');
|
||||
const targetLanguage = document.getElementById('targetLanguage');
|
||||
const playSource = document.getElementById('playSource');
|
||||
const playTranslation = document.getElementById('playTranslation');
|
||||
const clearSource = document.getElementById('clearSource');
|
||||
const clearTranslation = document.getElementById('clearTranslation');
|
||||
const statusIndicator = document.getElementById('statusIndicator');
|
||||
const progressContainer = document.getElementById('progressContainer');
|
||||
const progressBar = document.getElementById('progressBar');
|
||||
const audioPlayer = document.getElementById('audioPlayer');
|
||||
const ttsServerAlert = document.getElementById('ttsServerAlert');
|
||||
const ttsServerMessage = document.getElementById('ttsServerMessage');
|
||||
const ttsServerUrl = document.getElementById('ttsServerUrl');
|
||||
const ttsApiKey = document.getElementById('ttsApiKey');
|
||||
const updateTtsServer = document.getElementById('updateTtsServer');
|
||||
|
||||
// Set initial values
|
||||
let isRecording = false;
|
||||
let mediaRecorder = null;
|
||||
let audioChunks = [];
|
||||
let currentSourceText = '';
|
||||
let currentTranslationText = '';
|
||||
let currentTtsServerUrl = '';
|
||||
|
||||
// Check TTS server status on page load
|
||||
checkTtsServer();
|
||||
|
||||
// Check for saved translations in IndexedDB
|
||||
loadSavedTranslations();
|
||||
|
||||
// Update TTS server URL and API key
|
||||
updateTtsServer.addEventListener('click', function() {
|
||||
const newUrl = ttsServerUrl.value.trim();
|
||||
const newApiKey = ttsApiKey.value.trim();
|
||||
|
||||
if (!newUrl && !newApiKey) {
|
||||
alert('Please provide at least one value to update');
|
||||
return;
|
||||
}
|
||||
|
||||
const updateData = {};
|
||||
if (newUrl) updateData.server_url = newUrl;
|
||||
if (newApiKey) updateData.api_key = newApiKey;
|
||||
|
||||
fetch('/update_tts_config', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(updateData)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
statusIndicator.textContent = 'TTS configuration updated';
|
||||
// Save URL to localStorage but not the API key for security
|
||||
if (newUrl) localStorage.setItem('ttsServerUrl', newUrl);
|
||||
// Check TTS server with new configuration
|
||||
checkTtsServer();
|
||||
} else {
|
||||
alert('Failed to update TTS configuration: ' + data.error);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Failed to update TTS config:', error);
|
||||
alert('Failed to update TTS configuration. See console for details.');
|
||||
});
|
||||
});
|
||||
|
||||
// Make sure target language is different from source
|
||||
if (targetLanguage.options[0].value === sourceLanguage.value) {
|
||||
targetLanguage.selectedIndex = 1;
|
||||
}
|
||||
|
||||
// Event listeners for language selection
|
||||
sourceLanguage.addEventListener('change', function() {
|
||||
if (targetLanguage.value === sourceLanguage.value) {
|
||||
for (let i = 0; i < targetLanguage.options.length; i++) {
|
||||
if (targetLanguage.options[i].value !== sourceLanguage.value) {
|
||||
targetLanguage.selectedIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
targetLanguage.addEventListener('change', function() {
|
||||
if (targetLanguage.value === sourceLanguage.value) {
|
||||
for (let i = 0; i < sourceLanguage.options.length; i++) {
|
||||
if (sourceLanguage.options[i].value !== targetLanguage.value) {
|
||||
sourceLanguage.selectedIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Record button click event
|
||||
recordBtn.addEventListener('click', function() {
|
||||
if (isRecording) {
|
||||
stopRecording();
|
||||
} else {
|
||||
startRecording();
|
||||
}
|
||||
});
|
||||
|
||||
// Function to start recording
|
||||
function startRecording() {
|
||||
navigator.mediaDevices.getUserMedia({ audio: true })
|
||||
.then(stream => {
|
||||
mediaRecorder = new MediaRecorder(stream);
|
||||
audioChunks = [];
|
||||
|
||||
mediaRecorder.addEventListener('dataavailable', event => {
|
||||
audioChunks.push(event.data);
|
||||
});
|
||||
|
||||
mediaRecorder.addEventListener('stop', () => {
|
||||
const audioBlob = new Blob(audioChunks, { type: 'audio/wav' });
|
||||
transcribeAudio(audioBlob);
|
||||
});
|
||||
|
||||
mediaRecorder.start();
|
||||
isRecording = true;
|
||||
recordBtn.classList.add('recording');
|
||||
recordBtn.classList.replace('btn-primary', 'btn-danger');
|
||||
recordBtn.innerHTML = '<i class="fas fa-stop"></i>';
|
||||
statusIndicator.textContent = 'Recording... Click to stop';
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error accessing microphone:', error);
|
||||
alert('Error accessing microphone. Please make sure you have given permission for microphone access.');
|
||||
});
|
||||
}
|
||||
|
||||
// Function to stop recording
|
||||
function stopRecording() {
|
||||
mediaRecorder.stop();
|
||||
isRecording = false;
|
||||
recordBtn.classList.remove('recording');
|
||||
recordBtn.classList.replace('btn-danger', 'btn-primary');
|
||||
recordBtn.innerHTML = '<i class="fas fa-microphone"></i>';
|
||||
statusIndicator.textContent = 'Processing audio...';
|
||||
|
||||
// Stop all audio tracks
|
||||
mediaRecorder.stream.getTracks().forEach(track => track.stop());
|
||||
}
|
||||
|
||||
// Function to transcribe audio
|
||||
function transcribeAudio(audioBlob) {
|
||||
const formData = new FormData();
|
||||
formData.append('audio', audioBlob);
|
||||
formData.append('source_lang', sourceLanguage.value);
|
||||
|
||||
showProgress();
|
||||
|
||||
fetch('/transcribe', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
hideProgress();
|
||||
|
||||
if (data.success) {
|
||||
currentSourceText = data.text;
|
||||
sourceText.innerHTML = `<p>${data.text}</p>`;
|
||||
playSource.disabled = false;
|
||||
translateBtn.disabled = false;
|
||||
statusIndicator.textContent = 'Transcription complete';
|
||||
|
||||
// Cache the transcription in IndexedDB
|
||||
saveToIndexedDB('transcriptions', {
|
||||
text: data.text,
|
||||
language: sourceLanguage.value,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
} else {
|
||||
sourceText.innerHTML = `<p class="text-danger">Error: ${data.error}</p>`;
|
||||
statusIndicator.textContent = 'Transcription failed';
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
hideProgress();
|
||||
console.error('Transcription error:', error);
|
||||
sourceText.innerHTML = `<p class="text-danger">Failed to transcribe audio. Please try again.</p>`;
|
||||
statusIndicator.textContent = 'Transcription failed';
|
||||
});
|
||||
}
|
||||
|
||||
// Translate button click event
|
||||
translateBtn.addEventListener('click', function() {
|
||||
if (!currentSourceText) {
|
||||
return;
|
||||
}
|
||||
|
||||
statusIndicator.textContent = 'Translating...';
|
||||
showProgress();
|
||||
|
||||
fetch('/translate', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
text: currentSourceText,
|
||||
source_lang: sourceLanguage.value,
|
||||
target_lang: targetLanguage.value
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
hideProgress();
|
||||
|
||||
if (data.success) {
|
||||
currentTranslationText = data.translation;
|
||||
translatedText.innerHTML = `<p>${data.translation}</p>`;
|
||||
playTranslation.disabled = false;
|
||||
statusIndicator.textContent = 'Translation complete';
|
||||
|
||||
// Cache the translation in IndexedDB
|
||||
saveToIndexedDB('translations', {
|
||||
sourceText: currentSourceText,
|
||||
sourceLanguage: sourceLanguage.value,
|
||||
targetText: data.translation,
|
||||
targetLanguage: targetLanguage.value,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
} else {
|
||||
translatedText.innerHTML = `<p class="text-danger">Error: ${data.error}</p>`;
|
||||
statusIndicator.textContent = 'Translation failed';
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
hideProgress();
|
||||
console.error('Translation error:', error);
|
||||
translatedText.innerHTML = `<p class="text-danger">Failed to translate. Please try again.</p>`;
|
||||
statusIndicator.textContent = 'Translation failed';
|
||||
});
|
||||
});
|
||||
|
||||
// Play source text
|
||||
playSource.addEventListener('click', function() {
|
||||
if (!currentSourceText) return;
|
||||
|
||||
playAudio(currentSourceText, sourceLanguage.value);
|
||||
statusIndicator.textContent = 'Playing source audio...';
|
||||
});
|
||||
|
||||
// Play translation
|
||||
playTranslation.addEventListener('click', function() {
|
||||
if (!currentTranslationText) return;
|
||||
|
||||
playAudio(currentTranslationText, targetLanguage.value);
|
||||
statusIndicator.textContent = 'Playing translation audio...';
|
||||
});
|
||||
|
||||
// Function to play audio via TTS
|
||||
function playAudio(text, language) {
|
||||
showProgress();
|
||||
|
||||
fetch('/speak', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
text: text,
|
||||
language: language
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
hideProgress();
|
||||
|
||||
if (data.success) {
|
||||
audioPlayer.src = data.audio_url;
|
||||
audioPlayer.onended = function() {
|
||||
statusIndicator.textContent = 'Ready';
|
||||
};
|
||||
audioPlayer.play();
|
||||
} else {
|
||||
statusIndicator.textContent = 'TTS failed';
|
||||
|
||||
// Show TTS server alert with error message
|
||||
ttsServerAlert.classList.remove('d-none');
|
||||
ttsServerAlert.classList.remove('alert-success');
|
||||
ttsServerAlert.classList.add('alert-warning');
|
||||
ttsServerMessage.textContent = data.error;
|
||||
|
||||
alert('Failed to play audio: ' + data.error);
|
||||
|
||||
// Check TTS server status again
|
||||
checkTtsServer();
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
hideProgress();
|
||||
console.error('TTS error:', error);
|
||||
statusIndicator.textContent = 'TTS failed';
|
||||
|
||||
// Show TTS server alert
|
||||
ttsServerAlert.classList.remove('d-none');
|
||||
ttsServerAlert.classList.remove('alert-success');
|
||||
ttsServerAlert.classList.add('alert-warning');
|
||||
ttsServerMessage.textContent = 'Failed to connect to TTS server';
|
||||
});
|
||||
}
|
||||
|
||||
// Clear buttons
|
||||
clearSource.addEventListener('click', function() {
|
||||
sourceText.innerHTML = '<p class="text-muted">Your transcribed text will appear here...</p>';
|
||||
currentSourceText = '';
|
||||
playSource.disabled = true;
|
||||
translateBtn.disabled = true;
|
||||
});
|
||||
|
||||
clearTranslation.addEventListener('click', function() {
|
||||
translatedText.innerHTML = '<p class="text-muted">Translation will appear here...</p>';
|
||||
currentTranslationText = '';
|
||||
playTranslation.disabled = true;
|
||||
});
|
||||
|
||||
// Function to check TTS server status
|
||||
function checkTtsServer() {
|
||||
fetch('/check_tts_server')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
currentTtsServerUrl = data.url;
|
||||
ttsServerUrl.value = currentTtsServerUrl;
|
||||
|
||||
// Load saved API key if available
|
||||
const savedApiKey = localStorage.getItem('ttsApiKeySet');
|
||||
if (savedApiKey === 'true') {
|
||||
ttsApiKey.placeholder = '••••••• (API key saved)';
|
||||
}
|
||||
|
||||
if (data.status === 'error' || data.status === 'auth_error') {
|
||||
ttsServerAlert.classList.remove('d-none');
|
||||
ttsServerAlert.classList.remove('alert-success');
|
||||
ttsServerAlert.classList.add('alert-warning');
|
||||
ttsServerMessage.textContent = data.message;
|
||||
|
||||
if (data.status === 'auth_error') {
|
||||
ttsServerMessage.textContent = 'Authentication error with TTS server. Please check your API key.';
|
||||
}
|
||||
} else {
|
||||
ttsServerAlert.classList.remove('d-none');
|
||||
ttsServerAlert.classList.remove('alert-warning');
|
||||
ttsServerAlert.classList.add('alert-success');
|
||||
ttsServerMessage.textContent = 'TTS server is online and ready.';
|
||||
setTimeout(() => {
|
||||
ttsServerAlert.classList.add('d-none');
|
||||
}, 3000);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Failed to check TTS server:', error);
|
||||
ttsServerAlert.classList.remove('d-none');
|
||||
ttsServerAlert.classList.remove('alert-success');
|
||||
ttsServerAlert.classList.add('alert-warning');
|
||||
ttsServerMessage.textContent = 'Failed to check TTS server status.';
|
||||
});
|
||||
}
|
||||
|
||||
// Progress indicator functions
|
||||
function showProgress() {
|
||||
progressContainer.classList.remove('d-none');
|
||||
let progress = 0;
|
||||
const interval = setInterval(() => {
|
||||
progress += 5;
|
||||
if (progress > 90) {
|
||||
clearInterval(interval);
|
||||
}
|
||||
progressBar.style.width = `${progress}%`;
|
||||
}, 100);
|
||||
progressBar.dataset.interval = interval;
|
||||
}
|
||||
|
||||
function hideProgress() {
|
||||
const interval = progressBar.dataset.interval;
|
||||
if (interval) {
|
||||
clearInterval(Number(interval));
|
||||
}
|
||||
progressBar.style.width = '100%';
|
||||
setTimeout(() => {
|
||||
progressContainer.classList.add('d-none');
|
||||
progressBar.style.width = '0%';
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
// IndexedDB functions for offline data storage
|
||||
function openIndexedDB() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open('VoiceTranslatorDB', 1);
|
||||
|
||||
request.onupgradeneeded = (event) => {
|
||||
const db = event.target.result;
|
||||
|
||||
// Create stores for transcriptions and translations
|
||||
if (!db.objectStoreNames.contains('transcriptions')) {
|
||||
db.createObjectStore('transcriptions', { keyPath: 'timestamp' });
|
||||
}
|
||||
|
||||
if (!db.objectStoreNames.contains('translations')) {
|
||||
db.createObjectStore('translations', { keyPath: 'timestamp' });
|
||||
}
|
||||
};
|
||||
|
||||
request.onsuccess = (event) => {
|
||||
resolve(event.target.result);
|
||||
};
|
||||
|
||||
request.onerror = (event) => {
|
||||
reject('IndexedDB error: ' + event.target.errorCode);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function saveToIndexedDB(storeName, data) {
|
||||
openIndexedDB().then(db => {
|
||||
const transaction = db.transaction([storeName], 'readwrite');
|
||||
const store = transaction.objectStore(storeName);
|
||||
store.add(data);
|
||||
}).catch(error => {
|
||||
console.error('Error saving to IndexedDB:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function loadSavedTranslations() {
|
||||
openIndexedDB().then(db => {
|
||||
const transaction = db.transaction(['translations'], 'readonly');
|
||||
const store = transaction.objectStore('translations');
|
||||
const request = store.getAll();
|
||||
|
||||
request.onsuccess = (event) => {
|
||||
const translations = event.target.result;
|
||||
if (translations && translations.length > 0) {
|
||||
// Could add a history section or recently used translations
|
||||
console.log('Loaded saved translations:', translations.length);
|
||||
}
|
||||
};
|
||||
}).catch(error => {
|
||||
console.error('Error loading from IndexedDB:', error);
|
||||
});
|
||||
}
|
||||
|
||||
// PWA installation prompt
|
||||
function initInstallPrompt() {
|
||||
let deferredPrompt;
|
||||
const installButton = document.createElement('button');
|
||||
installButton.style.display = 'none';
|
||||
installButton.classList.add('btn', 'btn-success', 'fixed-bottom', 'm-3');
|
||||
installButton.innerHTML = 'Install Voice Translator <i class="fas fa-download ml-2"></i>';
|
||||
document.body.appendChild(installButton);
|
||||
|
||||
window.addEventListener('beforeinstallprompt', (e) => {
|
||||
// Prevent Chrome 67 and earlier from automatically showing the prompt
|
||||
e.preventDefault();
|
||||
// Stash the event so it can be triggered later
|
||||
deferredPrompt = e;
|
||||
// Update UI to notify the user they can add to home screen
|
||||
installButton.style.display = 'block';
|
||||
|
||||
installButton.addEventListener('click', (e) => {
|
||||
// Hide our user interface that shows our install button
|
||||
installButton.style.display = 'none';
|
||||
// Show the prompt
|
||||
deferredPrompt.prompt();
|
||||
// Wait for the user to respond to the prompt
|
||||
deferredPrompt.userChoice.then((choiceResult) => {
|
||||
if (choiceResult.outcome === 'accepted') {
|
||||
console.log('User accepted the install prompt');
|
||||
} else {
|
||||
console.log('User dismissed the install prompt');
|
||||
}
|
||||
deferredPrompt = null;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Push notification setup
|
||||
function setupPushNotifications(swRegistration) {
|
||||
// First check if we already have permission
|
||||
if (Notification.permission === 'granted') {
|
||||
console.log('Notification permission already granted');
|
||||
subscribeToPushManager(swRegistration);
|
||||
} else if (Notification.permission !== 'denied') {
|
||||
// Otherwise, ask for permission
|
||||
Notification.requestPermission().then(function(permission) {
|
||||
if (permission === 'granted') {
|
||||
console.log('Notification permission granted');
|
||||
subscribeToPushManager(swRegistration);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function subscribeToPushManager(swRegistration) {
|
||||
try {
|
||||
// Get the server's public key
|
||||
const response = await fetch('/api/push-public-key');
|
||||
const data = await response.json();
|
||||
|
||||
// Convert the base64 string to Uint8Array
|
||||
function urlBase64ToUint8Array(base64String) {
|
||||
const padding = '='.repeat((4 - base64String.length % 4) % 4);
|
||||
const base64 = (base64String + padding)
|
||||
.replace(/-/g, '+')
|
||||
.replace(/_/g, '/');
|
||||
|
||||
const rawData = window.atob(base64);
|
||||
const outputArray = new Uint8Array(rawData.length);
|
||||
|
||||
for (let i = 0; i < rawData.length; ++i) {
|
||||
outputArray[i] = rawData.charCodeAt(i);
|
||||
}
|
||||
return outputArray;
|
||||
}
|
||||
|
||||
const convertedVapidKey = urlBase64ToUint8Array(data.publicKey);
|
||||
|
||||
// Subscribe to push notifications
|
||||
const subscription = await swRegistration.pushManager.subscribe({
|
||||
userVisibleOnly: true,
|
||||
applicationServerKey: convertedVapidKey
|
||||
});
|
||||
|
||||
// Send the subscription details to the server
|
||||
await fetch('/api/push-subscribe', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(subscription)
|
||||
});
|
||||
|
||||
console.log('User is subscribed to push notifications');
|
||||
} catch (error) {
|
||||
console.error('Failed to subscribe to push notifications:', error);
|
||||
}
|
||||
}
|
@@ -1,255 +0,0 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// DOM elements
|
||||
const sourceLanguage = document.getElementById('sourceLanguage');
|
||||
const targetLanguage = document.getElementById('targetLanguage');
|
||||
const swapButton = document.getElementById('swapLanguages');
|
||||
const sourceText = document.getElementById('sourceText');
|
||||
const translatedText = document.getElementById('translatedText');
|
||||
const recordSourceButton = document.getElementById('recordSource');
|
||||
const speakButton = document.getElementById('speak');
|
||||
const clearSourceButton = document.getElementById('clearSource');
|
||||
const copyTranslationButton = document.getElementById('copyTranslation');
|
||||
const translateButton = document.getElementById('translateButton');
|
||||
const statusMessage = document.getElementById('status');
|
||||
|
||||
// Audio recording variables
|
||||
let mediaRecorder;
|
||||
let audioChunks = [];
|
||||
let isRecording = false;
|
||||
|
||||
// Speech recognition setup
|
||||
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
||||
const recognition = SpeechRecognition ? new SpeechRecognition() : null;
|
||||
|
||||
if (recognition) {
|
||||
recognition.continuous = false;
|
||||
recognition.interimResults = false;
|
||||
}
|
||||
|
||||
// Event listeners
|
||||
swapButton.addEventListener('click', swapLanguages);
|
||||
translateButton.addEventListener('click', translateText);
|
||||
clearSourceButton.addEventListener('click', clearSource);
|
||||
copyTranslationButton.addEventListener('click', copyTranslation);
|
||||
|
||||
if (recognition) {
|
||||
recordSourceButton.addEventListener('click', toggleRecording);
|
||||
} else {
|
||||
recordSourceButton.textContent = "Speech API not supported";
|
||||
recordSourceButton.disabled = true;
|
||||
}
|
||||
|
||||
speakButton.addEventListener('click', speakTranslation);
|
||||
|
||||
// Functions (continued)
|
||||
function swapLanguages() {
|
||||
const tempLang = sourceLanguage.value;
|
||||
sourceLanguage.value = targetLanguage.value;
|
||||
targetLanguage.value = tempLang;
|
||||
|
||||
// Also swap the text if both fields have content
|
||||
if (sourceText.value && translatedText.value) {
|
||||
const tempText = sourceText.value;
|
||||
sourceText.value = translatedText.value;
|
||||
translatedText.value = tempText;
|
||||
}
|
||||
}
|
||||
|
||||
function clearSource() {
|
||||
sourceText.value = '';
|
||||
updateStatus('');
|
||||
}
|
||||
|
||||
function copyTranslation() {
|
||||
if (!translatedText.value) {
|
||||
updateStatus('Nothing to copy', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
navigator.clipboard.writeText(translatedText.value)
|
||||
.then(() => {
|
||||
updateStatus('Copied to clipboard!', 'success');
|
||||
setTimeout(() => updateStatus(''), 2000);
|
||||
})
|
||||
.catch(err => {
|
||||
updateStatus('Failed to copy: ' + err, 'error');
|
||||
});
|
||||
}
|
||||
|
||||
async function translateText() {
|
||||
const source = sourceText.value.trim();
|
||||
if (!source) {
|
||||
updateStatus('Please enter or speak some text to translate', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
updateStatus('Translating...');
|
||||
translatedText.value = '';
|
||||
|
||||
try {
|
||||
const response = await fetch('/translate', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
sourceLanguage: sourceLanguage.value,
|
||||
targetLanguage: targetLanguage.value,
|
||||
text: source
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
translatedText.value = data.translation;
|
||||
updateStatus('Translation complete', 'success');
|
||||
setTimeout(() => updateStatus(''), 2000);
|
||||
} else {
|
||||
updateStatus(data.error || 'Translation failed', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
updateStatus('Network error: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function toggleRecording() {
|
||||
if (!recognition) {
|
||||
updateStatus('Speech recognition not supported in this browser', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (isRecording) {
|
||||
stopRecording();
|
||||
} else {
|
||||
startRecording();
|
||||
}
|
||||
}
|
||||
|
||||
function startRecording() {
|
||||
sourceText.value = '';
|
||||
updateStatus('Listening...');
|
||||
|
||||
recognition.lang = getLanguageCode(sourceLanguage.value);
|
||||
recognition.onresult = function(event) {
|
||||
const transcript = event.results[0][0].transcript;
|
||||
sourceText.value = transcript;
|
||||
updateStatus('Recording completed', 'success');
|
||||
setTimeout(() => updateStatus(''), 2000);
|
||||
};
|
||||
|
||||
recognition.onerror = function(event) {
|
||||
updateStatus('Error in speech recognition: ' + event.error, 'error');
|
||||
stopRecording();
|
||||
};
|
||||
|
||||
recognition.onend = function() {
|
||||
stopRecording();
|
||||
};
|
||||
|
||||
try {
|
||||
recognition.start();
|
||||
isRecording = true;
|
||||
recordSourceButton.classList.add('recording');
|
||||
recordSourceButton.querySelector('.button-text').textContent = 'Stop';
|
||||
} catch (error) {
|
||||
updateStatus('Failed to start recording: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function stopRecording() {
|
||||
if (isRecording) {
|
||||
try {
|
||||
recognition.stop();
|
||||
} catch (error) {
|
||||
console.error('Error stopping recognition:', error);
|
||||
}
|
||||
|
||||
isRecording = false;
|
||||
recordSourceButton.classList.remove('recording');
|
||||
recordSourceButton.querySelector('.button-text').textContent = 'Record';
|
||||
}
|
||||
}
|
||||
|
||||
function speakTranslation() {
|
||||
const text = translatedText.value.trim();
|
||||
if (!text) {
|
||||
updateStatus('No translation to speak', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Use the browser's speech synthesis API
|
||||
const speech = new SpeechSynthesisUtterance(text);
|
||||
speech.lang = getLanguageCode(targetLanguage.value);
|
||||
speech.volume = 1;
|
||||
speech.rate = 1;
|
||||
speech.pitch = 1;
|
||||
|
||||
speech.onstart = function() {
|
||||
updateStatus('Speaking...');
|
||||
speakButton.disabled = true;
|
||||
};
|
||||
|
||||
speech.onend = function() {
|
||||
updateStatus('');
|
||||
speakButton.disabled = false;
|
||||
};
|
||||
|
||||
speech.onerror = function(event) {
|
||||
updateStatus('Speech synthesis error: ' + event.error, 'error');
|
||||
speakButton.disabled = false;
|
||||
};
|
||||
|
||||
window.speechSynthesis.speak(speech);
|
||||
}
|
||||
|
||||
function getLanguageCode(language) {
|
||||
// Map language names to BCP 47 language tags for speech recognition/synthesis
|
||||
const languageMap = {
|
||||
"arabic": "ar-SA",
|
||||
"armenian": "hy-AM",
|
||||
"azerbaijani": "az-AZ",
|
||||
"english": "en-US",
|
||||
"french": "fr-FR",
|
||||
"georgian": "ka-GE",
|
||||
"kazakh": "kk-KZ",
|
||||
"mandarin": "zh-CN",
|
||||
"persian": "fa-IR",
|
||||
"portuguese": "pt-PT",
|
||||
"russian": "ru-RU",
|
||||
"turkish": "tr-TR",
|
||||
"uzbek": "uz-UZ"
|
||||
};
|
||||
|
||||
return languageMap[language] || 'en-US';
|
||||
}
|
||||
|
||||
function updateStatus(message, type = '') {
|
||||
statusMessage.textContent = message;
|
||||
statusMessage.className = 'status-message';
|
||||
|
||||
if (type) {
|
||||
statusMessage.classList.add(type);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for microphone and speech support when page loads
|
||||
function checkSupportedFeatures() {
|
||||
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
||||
updateStatus('Microphone access is not supported in this browser', 'error');
|
||||
recordSourceButton.disabled = true;
|
||||
}
|
||||
|
||||
if (!window.SpeechRecognition && !window.webkitSpeechRecognition) {
|
||||
updateStatus('Speech recognition is not supported in this browser', 'error');
|
||||
recordSourceButton.disabled = true;
|
||||
}
|
||||
|
||||
if (!window.speechSynthesis) {
|
||||
updateStatus('Speech synthesis is not supported in this browser', 'error');
|
||||
speakButton.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
checkSupportedFeatures();
|
||||
});
|
Reference in New Issue
Block a user