Initial upload
This commit is contained in:
18
.gitignore
vendored
Normal file
18
.gitignore
vendored
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
node_modules/
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
*.log
|
||||||
|
*.tmp
|
||||||
|
*.temp
|
||||||
|
.DS_Store
|
||||||
|
*.sqlite
|
||||||
|
*.db
|
||||||
|
*.pem
|
||||||
|
*.key
|
||||||
|
*.p12
|
||||||
|
*.pfx
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
coverage/
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
66
index.html
Normal file
66
index.html
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Voice to Voice & Text Translator</title>
|
||||||
|
<link rel="stylesheet" href="styles.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="app">
|
||||||
|
<h1>Voice to Voice & Text Translator</h1>
|
||||||
|
<p>Speak, translate, and hear the answer in another language.</p>
|
||||||
|
|
||||||
|
<div class="controls">
|
||||||
|
<label>
|
||||||
|
From
|
||||||
|
<select id="sourceLang">
|
||||||
|
<option value="en">English</option>
|
||||||
|
<option value="es">Spanish</option>
|
||||||
|
<option value="fr">French</option>
|
||||||
|
<option value="de">German</option>
|
||||||
|
<option value="hi">Hindi</option>
|
||||||
|
<option value="ta">Tamil</option>
|
||||||
|
<option value="te">Telugu</option>
|
||||||
|
<option value="ml">Malayalam</option>
|
||||||
|
<option value="pa">Punjabi</option>
|
||||||
|
<option value="kn">Kannada</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
To
|
||||||
|
<select id="targetLang">
|
||||||
|
<option value="es">Spanish</option>
|
||||||
|
<option value="en">English</option>
|
||||||
|
<option value="fr">French</option>
|
||||||
|
<option value="de">German</option>
|
||||||
|
<option value="hi">Hindi</option>
|
||||||
|
<option value="ta">Tamil</option>
|
||||||
|
<option value="te">Telugu</option>
|
||||||
|
<option value="ml">Malayalam</option>
|
||||||
|
<option value="pa">Punjabi</option>
|
||||||
|
<option value="kn">Kannada</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel">
|
||||||
|
<textarea id="sourceText" placeholder="Type text or click voice to speak..."></textarea>
|
||||||
|
<div class="buttons">
|
||||||
|
<button id="voiceBtn">🎤 Voice Input</button>
|
||||||
|
<button id="speakBtn">🔊 Speak</button>
|
||||||
|
<button id="clearBtn">🧹 Clear</button>
|
||||||
|
</div>
|
||||||
|
<div id="status" class="status">Ready</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel output">
|
||||||
|
<h2>Translation</h2>
|
||||||
|
<div id="translatedText">Translation will appear here...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="script.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1074
package-lock.json
generated
Normal file
1074
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
16
package.json
Normal file
16
package.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"name": "voice-translator-app",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "server.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node server.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.7.7",
|
||||||
|
"body-parser": "^1.20.2",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"dotenv": "^16.0.3",
|
||||||
|
"express": "^4.21.2",
|
||||||
|
"google-tts-api": "^2.0.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
631
script.js
Normal file
631
script.js
Normal file
@@ -0,0 +1,631 @@
|
|||||||
|
// 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');
|
||||||
437
server.js
Normal file
437
server.js
Normal file
@@ -0,0 +1,437 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const cors = require('cors');
|
||||||
|
const bodyParser = require('body-parser');
|
||||||
|
const axios = require('axios');
|
||||||
|
const path = require('path');
|
||||||
|
const googleTTS = require('google-tts-api');
|
||||||
|
require('dotenv').config();
|
||||||
|
|
||||||
|
const HUGGINGFACE_API_TOKEN = process.env.HUGGINGFACEHUB_API_TOKEN || process.env.HF_API_TOKEN;
|
||||||
|
const huggingFaceLangMap = {
|
||||||
|
'en': 'eng',
|
||||||
|
'es': 'spa',
|
||||||
|
'fr': 'fra',
|
||||||
|
'de': 'deu',
|
||||||
|
'pt': 'por',
|
||||||
|
'it': 'ita',
|
||||||
|
'nl': 'nld',
|
||||||
|
'ru': 'rus',
|
||||||
|
'ar': 'ara',
|
||||||
|
'ja': 'jpn',
|
||||||
|
'ko': 'kor',
|
||||||
|
'zh': 'zho',
|
||||||
|
'hi': 'hin',
|
||||||
|
'ta': 'tam',
|
||||||
|
'te': 'tel',
|
||||||
|
'ml': 'mal',
|
||||||
|
'pa': 'pan',
|
||||||
|
'kn': 'kan',
|
||||||
|
'id': 'ind',
|
||||||
|
'vi': 'vie',
|
||||||
|
'tr': 'tur'
|
||||||
|
};
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const PORT = process.env.PORT || 3000;
|
||||||
|
|
||||||
|
// Middleware
|
||||||
|
app.use(cors());
|
||||||
|
app.use(bodyParser.json({ limit: '10mb' }));
|
||||||
|
app.use(bodyParser.urlencoded({ limit: '10mb', extended: true }));
|
||||||
|
app.use(bodyParser.raw({ type: 'audio/*', limit: '10mb' }));
|
||||||
|
app.use(express.static(path.join(__dirname)));
|
||||||
|
|
||||||
|
// Language code mapping
|
||||||
|
const langMap = {
|
||||||
|
'en': 'en',
|
||||||
|
'es': 'es',
|
||||||
|
'fr': 'fr',
|
||||||
|
'de': 'de',
|
||||||
|
'pt': 'pt',
|
||||||
|
'ja': 'ja',
|
||||||
|
'ko': 'ko',
|
||||||
|
'zh': 'zh-CN',
|
||||||
|
'ta': 'ta',
|
||||||
|
'te': 'te',
|
||||||
|
'ml': 'ml',
|
||||||
|
'hi': 'hi'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Translation API endpoint (using free mymemory translation API)
|
||||||
|
const translateViaMyMemory = async (text, sourceLang, targetLang) => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get('https://api.mymemory.translated.net/get', {
|
||||||
|
params: {
|
||||||
|
q: text,
|
||||||
|
langpair: `${sourceLang}|${targetLang}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.responseStatus === 200) {
|
||||||
|
return response.data.responseData.translatedText;
|
||||||
|
}
|
||||||
|
throw new Error('Translation failed');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('MyMemory translation error:', error.message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Hugging Face translation via M2M100
|
||||||
|
const translateViaHuggingFace = async (text, sourceLang, targetLang) => {
|
||||||
|
if (!HUGGINGFACE_API_TOKEN) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const srcCode = huggingFaceLangMap[sourceLang] || 'eng';
|
||||||
|
const tgtCode = huggingFaceLangMap[targetLang] || 'eng';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.post(
|
||||||
|
'https://api-inference.huggingface.co/models/facebook/m2m100_418M',
|
||||||
|
{
|
||||||
|
inputs: text,
|
||||||
|
parameters: {
|
||||||
|
src_lang: srcCode,
|
||||||
|
tgt_lang: tgtCode
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${HUGGINGFACE_API_TOKEN}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
timeout: 20000
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const data = response.data;
|
||||||
|
if (Array.isArray(data) && data[0]?.generated_text) {
|
||||||
|
return data[0].generated_text;
|
||||||
|
}
|
||||||
|
if (data?.generated_text) {
|
||||||
|
return data.generated_text;
|
||||||
|
}
|
||||||
|
if (typeof data === 'string') {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Hugging Face translation error:', error.message || error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Hugging Face text-to-speech
|
||||||
|
const ttsLangMap = {
|
||||||
|
'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 speakViaHuggingFace = async (text, targetLang) => {
|
||||||
|
const normalizedLang = (targetLang || 'en').toLowerCase();
|
||||||
|
if (!HUGGINGFACE_API_TOKEN) {
|
||||||
|
return await speakViaGoogleTts(text, normalizedLang);
|
||||||
|
}
|
||||||
|
const ttsLang = ttsLangMap[normalizedLang] || 'en-US';
|
||||||
|
const preferredModels = [];
|
||||||
|
|
||||||
|
if (['ta', 'te', 'ml', 'kn', 'pa', 'hi'].includes(normalizedLang)) {
|
||||||
|
const languageModelMap = {
|
||||||
|
'ta': 'facebook/mms-tts-ta',
|
||||||
|
'te': 'facebook/mms-tts-te',
|
||||||
|
'ml': 'facebook/mms-tts-ml',
|
||||||
|
'kn': 'facebook/mms-tts-kn',
|
||||||
|
'pa': 'facebook/mms-tts-pa',
|
||||||
|
'hi': 'facebook/mms-tts-hi'
|
||||||
|
};
|
||||||
|
preferredModels.push(languageModelMap[normalizedLang]);
|
||||||
|
}
|
||||||
|
|
||||||
|
preferredModels.push(
|
||||||
|
'microsoft/speecht5_tts',
|
||||||
|
'tts_models/multilingual/multi-dataset/your_tts'
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const model of preferredModels) {
|
||||||
|
try {
|
||||||
|
const payload = model.includes('mms-tts')
|
||||||
|
? { inputs: text }
|
||||||
|
: model.includes('speecht5')
|
||||||
|
? {
|
||||||
|
inputs: text,
|
||||||
|
parameters: {
|
||||||
|
language: ttsLang,
|
||||||
|
voice: 'alloy'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
inputs: text,
|
||||||
|
parameters: {
|
||||||
|
speaker: 'alloy',
|
||||||
|
language: ttsLang
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await axios.post(
|
||||||
|
`https://api-inference.huggingface.co/models/${model}`,
|
||||||
|
payload,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${HUGGINGFACE_API_TOKEN}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Accept: 'audio/*'
|
||||||
|
},
|
||||||
|
responseType: 'arraybuffer',
|
||||||
|
timeout: 60000
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const contentType = response.headers['content-type'] || 'audio/wav';
|
||||||
|
if (response.data && contentType.includes('audio')) {
|
||||||
|
return {
|
||||||
|
audio: Buffer.from(response.data).toString('base64'),
|
||||||
|
format: contentType.includes('mpeg') ? 'audio/mpeg' : 'audio/wav'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Hugging Face TTS model ${model} failed:`, error.message || error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return await speakViaGoogleTts(text, normalizedLang);
|
||||||
|
};
|
||||||
|
|
||||||
|
const speakViaGoogleTts = async (text, targetLang) => {
|
||||||
|
try {
|
||||||
|
const url = googleTTS.getAudioUrl(text, {
|
||||||
|
lang: targetLang,
|
||||||
|
slow: false,
|
||||||
|
host: 'https://translate.google.com',
|
||||||
|
splitPunctuation: '.?'
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await axios.get(url, {
|
||||||
|
responseType: 'arraybuffer',
|
||||||
|
headers: {
|
||||||
|
'User-Agent': 'Mozilla/5.0 (compatible; GoogleTTS/1.0)'
|
||||||
|
},
|
||||||
|
timeout: 20000
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data) {
|
||||||
|
return {
|
||||||
|
audio: Buffer.from(response.data).toString('base64'),
|
||||||
|
format: 'audio/mpeg'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Google TTS fallback failed:', error.message || error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Alternative: Using LibreTranslate if available locally
|
||||||
|
const translateViaLibreTranslate = async (text, sourceLang, targetLang) => {
|
||||||
|
try {
|
||||||
|
const response = await axios.post('http://localhost:5000/translate', {
|
||||||
|
q: text,
|
||||||
|
source: sourceLang,
|
||||||
|
target: targetLang
|
||||||
|
}, {
|
||||||
|
timeout: 5000
|
||||||
|
});
|
||||||
|
return response.data.translatedText;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('LibreTranslate not available, trying MyMemory...');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Speech-to-text endpoint using Hugging Face Whisper
|
||||||
|
app.post('/api/transcribe', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const audioBuffer = req.body;
|
||||||
|
if (!audioBuffer || audioBuffer.length === 0) {
|
||||||
|
return res.status(400).json({ error: 'No audio provided' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!HUGGINGFACE_API_TOKEN) {
|
||||||
|
return res.status(500).json({ error: 'Hugging Face token not configured' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentType = req.headers['content-type'] || 'application/octet-stream';
|
||||||
|
const response = await axios.post(
|
||||||
|
'https://api-inference.huggingface.co/models/openai/whisper-large-v3',
|
||||||
|
audioBuffer,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${HUGGINGFACE_API_TOKEN}`,
|
||||||
|
'Content-Type': contentType
|
||||||
|
},
|
||||||
|
timeout: 60000
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const data = response.data;
|
||||||
|
const transcript = Array.isArray(data) ? data[0]?.text : data?.text || '';
|
||||||
|
res.json({ transcript });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Speech-to-text error:', error.message || error);
|
||||||
|
res.status(500).json({ error: 'Speech-to-text failed' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Translation endpoint
|
||||||
|
app.post('/api/translate', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { text, sourceLang, targetLang } = req.body;
|
||||||
|
|
||||||
|
// Validate input
|
||||||
|
if (!text || !sourceLang || !targetLang) {
|
||||||
|
return res.status(400).json({ error: 'Missing required fields' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (text.length > 5000) {
|
||||||
|
return res.status(400).json({ error: 'Text exceeds 5000 character limit' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// If source and target languages are the same
|
||||||
|
if (sourceLang === targetLang) {
|
||||||
|
return res.json({ translatedText: text });
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Translating "${text.substring(0, 50)}..." from ${sourceLang} to ${targetLang}`);
|
||||||
|
|
||||||
|
// Try Hugging Face first if the key is configured
|
||||||
|
let translatedText = await translateViaHuggingFace(text, sourceLang, targetLang);
|
||||||
|
|
||||||
|
// Fallback to LibreTranslate if available locally
|
||||||
|
if (!translatedText) {
|
||||||
|
translatedText = await translateViaLibreTranslate(text, sourceLang, targetLang);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to MyMemory if other services are unavailable
|
||||||
|
if (!translatedText) {
|
||||||
|
translatedText = await translateViaMyMemory(text, sourceLang, targetLang);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!translatedText) {
|
||||||
|
return res.status(500).json({
|
||||||
|
error: 'Translation service unavailable',
|
||||||
|
translatedText: text // Fallback to original text
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ translatedText });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Translation error:', error);
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/speak', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { text, targetLang } = req.body;
|
||||||
|
|
||||||
|
if (!text || !targetLang) {
|
||||||
|
return res.status(400).json({ error: 'Missing required fields' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const speechResult = await speakViaHuggingFace(text, targetLang);
|
||||||
|
if (!speechResult?.audio) {
|
||||||
|
return res.status(500).json({ error: 'Speech service unavailable' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ audio: speechResult.audio, format: speechResult.format || 'audio/wav' });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Speech endpoint error:', error);
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Health check endpoint
|
||||||
|
app.get('/api/health', (req, res) => {
|
||||||
|
res.json({ status: 'healthy', message: 'Translator API is running' });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get supported languages
|
||||||
|
app.get('/api/languages', (req, res) => {
|
||||||
|
const languages = {
|
||||||
|
'en': 'English',
|
||||||
|
'es': 'Spanish',
|
||||||
|
'fr': 'French',
|
||||||
|
'de': 'German',
|
||||||
|
'pt': 'Portuguese',
|
||||||
|
'it': 'Italian',
|
||||||
|
'nl': 'Dutch',
|
||||||
|
'ru': 'Russian',
|
||||||
|
'ar': 'Arabic',
|
||||||
|
'ja': 'Japanese',
|
||||||
|
'ko': 'Korean',
|
||||||
|
'zh': 'Chinese (Simplified)',
|
||||||
|
'hi': 'Hindi',
|
||||||
|
'ta': 'Tamil',
|
||||||
|
'te': 'Telugu',
|
||||||
|
'ml': 'Malayalam',
|
||||||
|
'pa': 'Punjabi',
|
||||||
|
'kn': 'Kannada',
|
||||||
|
'id': 'Indonesian',
|
||||||
|
'vi': 'Vietnamese',
|
||||||
|
'tr': 'Turkish'
|
||||||
|
};
|
||||||
|
res.json(languages);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Main app page
|
||||||
|
app.get('/', (req, res) => {
|
||||||
|
res.sendFile(path.join(__dirname, 'index.html'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// 404 handler
|
||||||
|
app.use((req, res) => {
|
||||||
|
res.status(404).json({ error: 'Not Found' });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Error handler
|
||||||
|
app.use((err, req, res, next) => {
|
||||||
|
console.error('Server error:', err);
|
||||||
|
res.status(500).json({ error: 'Internal Server Error' });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start server
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`
|
||||||
|
========================================
|
||||||
|
🌍 Language Translator Server
|
||||||
|
========================================
|
||||||
|
Server running on: http://localhost:${PORT}
|
||||||
|
|
||||||
|
Available endpoints:
|
||||||
|
- POST /api/translate - Translate text
|
||||||
|
- GET /api/languages - Get supported languages
|
||||||
|
- GET /api/health - Health check
|
||||||
|
========================================
|
||||||
|
`);
|
||||||
|
});
|
||||||
69
styles.css
Normal file
69
styles.css
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
background: #0f172a;
|
||||||
|
color: #f8fafc;
|
||||||
|
margin: 0;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
.app {
|
||||||
|
max-width: 860px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: #111827;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 24px;
|
||||||
|
box-shadow: 0 10px 30px rgba(0,0,0,0.25);
|
||||||
|
}
|
||||||
|
.controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
select, textarea, button {
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid #334155;
|
||||||
|
padding: 10px 12px;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
textarea {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 140px;
|
||||||
|
resize: vertical;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
cursor: pointer;
|
||||||
|
background: #2563eb;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
button:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
.panel {
|
||||||
|
background: #1f2937;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
.output #translatedText {
|
||||||
|
min-height: 80px;
|
||||||
|
padding: 12px;
|
||||||
|
background: #111827;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
.status {
|
||||||
|
margin-top: 10px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: #93c5fd;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user