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 nllbLangMap = { 'en': 'eng_Latn', 'es': 'spa_Latn', 'fr': 'fra_Latn', 'de': 'deu_Latn', 'pt': 'por_Latn', 'it': 'ita_Latn', 'nl': 'nld_Latn', 'ru': 'rus_Cyrl', 'ar': 'arb_Arab', 'ja': 'jpn_Jpan', 'ko': 'kor_Kore', 'zh': 'zho_Hans', 'hi': 'hin_Deva', 'ta': 'tam_Taml', 'te': 'tel_Telu', 'ml': 'mal_Mlym', 'pa': 'pan_Guru', 'kn': 'kan_Knda', 'id': 'ind_Latn', 'vi': 'vie_Latn', 'tr': 'tur_Latn' }; const m2m100LangMap = { 'en': 'en', 'es': 'es', 'fr': 'fr', 'de': 'de', 'pt': 'pt', 'it': 'it', 'nl': 'nl', 'ru': 'ru', 'ar': 'ar', 'ja': 'ja', 'ko': 'ko', 'zh': 'zh', 'hi': 'hi', 'ta': 'ta', 'te': 'te', 'ml': 'ml', 'pa': 'pa', 'kn': 'kn', 'id': 'id', 'vi': 'vi', 'tr': 'tr' }; const app = express(); const PORT = process.env.PORT || 3000; // HTTPS setup (optional). Serve HTTPS on 3443 using provided certs or generate a self-signed cert. const https = require('https'); const fs = require('fs'); const selfsigned = require('selfsigned'); const SSL_PORT = process.env.SSL_PORT || 3443; function loadSslOptions() { const keyPath = process.env.SSL_KEY_PATH; const certPath = process.env.SSL_CERT_PATH; if (keyPath && certPath && fs.existsSync(keyPath) && fs.existsSync(certPath)) { return { key: fs.readFileSync(keyPath), cert: fs.readFileSync(certPath) }; } // Generate a temporary self-signed certificate for localhost console.warn('No SSL cert/key provided. Generating a temporary self-signed certificate for https://localhost:' + SSL_PORT); const attrs = [{ name: 'commonName', value: 'localhost' }]; const pems = selfsigned.generate(attrs, { days: 365 }); return { key: pems.private, cert: pems.cert }; } // 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 NLLB or M2M100 const translateViaHuggingFace = async (text, sourceLang, targetLang) => { if (!HUGGINGFACE_API_TOKEN) { return null; } const isEnglishCentric = sourceLang === 'en' || targetLang === 'en'; const m2mSrcCode = m2m100LangMap[sourceLang]; const m2mTgtCode = m2m100LangMap[targetLang]; const nllbSrcCode = nllbLangMap[sourceLang]; const nllbTgtCode = nllbLangMap[targetLang]; const useM2M100 = isEnglishCentric && m2mSrcCode && m2mTgtCode; const model = useM2M100 ? 'https://api-inference.huggingface.co/models/facebook/m2m100_418M' : 'https://api-inference.huggingface.co/models/facebook/nllb-200-distilled-600M'; const srcCode = useM2M100 ? m2mSrcCode : nllbSrcCode; const tgtCode = useM2M100 ? m2mTgtCode : nllbTgtCode; if (!srcCode || !tgtCode) { return null; } const chosenModel = useM2M100 ? 'facebook/m2m100_418M' : 'facebook/nllb-200-distilled-600M'; console.log(`Hugging Face translation using ${useM2M100 ? 'M2M100' : 'NLLB'} model: ${chosenModel} (${srcCode} -> ${tgtCode})`); try { const response = await axios.post( model, { 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; let resultText = null; if (Array.isArray(data) && data[0]?.generated_text) { resultText = data[0].generated_text; } else if (data?.generated_text) { resultText = data.generated_text; } else if (typeof data === 'string') { resultText = data; } return resultText ? { translatedText: resultText, model: chosenModel } : 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 const hfResult = await translateViaHuggingFace(text, sourceLang, targetLang); let translatedText = hfResult?.translatedText || null; let translatedWith = hfResult?.model ? `Hugging Face (${hfResult.model})` : null; // Fallback to LibreTranslate if available locally if (!translatedText) { translatedText = await translateViaLibreTranslate(text, sourceLang, targetLang); translatedWith = translatedText ? 'LibreTranslate' : translatedWith; } // Fallback to MyMemory if other services are unavailable if (!translatedText) { translatedText = await translateViaMyMemory(text, sourceLang, targetLang); translatedWith = translatedText ? 'MyMemory' : translatedWith; } if (!translatedText) { return res.status(500).json({ error: 'Translation service unavailable', translatedText: text // Fallback to original text }); } res.json({ translatedText, translatedWith }); } 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 ======================================== `); }); // Start HTTPS server on SSL_PORT try { const sslOptions = loadSslOptions(); https.createServer(sslOptions, app).listen(SSL_PORT, () => { console.log(`HTTPS server running on: https://localhost:${SSL_PORT}`); }); } catch (err) { console.error('Failed to start HTTPS server:', err); }