Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>🎙️ Voice Translator </title> | |
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet"> | |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
<style> | |
:root { | |
--primary-color: #8c6db0; | |
--primary-light: #b995e5; | |
--primary-dark: #6a4c91; | |
--accent-color: #30b8a8; | |
--accent-dark: #228b7f; | |
--gradient-start: #9575cd; | |
--gradient-end: #7986cb; | |
--text-color: #3a3a3a; | |
--text-secondary: #6b7280; | |
--light-bg: #f8f9fa; | |
--card-bg: #ffffff; | |
--panel-bg: #f5f7fb; | |
--border-radius: 16px; | |
--box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1); | |
} | |
body { | |
background: linear-gradient(135deg, var(--light-bg) 0%, #edf2f7 100%); | |
color: var(--text-color); | |
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
line-height: 1.6; | |
min-height: 100vh; | |
overflow-x: hidden; | |
} | |
.app-container { | |
max-width: 900px; | |
margin: 40px auto; | |
padding: 0 20px; | |
} | |
.translator-card { | |
background: var(--card-bg); | |
border-radius: var(--border-radius); | |
box-shadow: var(--box-shadow); | |
padding: 30px; | |
margin-bottom: 30px; | |
border: none; | |
position: relative; | |
overflow: hidden; | |
} | |
.translator-card::before { | |
content: ''; | |
position: absolute; | |
top: 0; | |
left: 0; | |
right: 0; | |
height: 6px; | |
background: linear-gradient(90deg, var(--gradient-start), var(--gradient-end), var(--accent-color)); | |
border-radius: var(--border-radius) var(--border-radius) 0 0; | |
} | |
.app-header { | |
text-align: center; | |
margin-bottom: 30px; | |
position: relative; | |
} | |
.app-title { | |
font-weight: 700; | |
margin-bottom: 10px; | |
font-size: 2.5rem; | |
background: linear-gradient(90deg, var(--primary-color), var(--accent-color)); | |
-webkit-background-clip: text; | |
background-clip: text; | |
-webkit-text-fill-color: transparent; | |
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.1)); | |
} | |
.app-subtitle { | |
color: var(--text-secondary); | |
font-size: 1.1rem; | |
margin-bottom: 0; | |
} | |
.language-select-container { | |
position: relative; | |
margin-bottom: 30px; | |
} | |
.language-select { | |
background-color: var(--panel-bg); | |
border: 2px solid rgba(0, 0, 0, 0.05); | |
border-radius: var(--border-radius); | |
padding: 14px 20px; | |
color: var(--text-color); | |
font-size: 1.1rem; | |
transition: all 0.3s ease; | |
width: 100%; | |
appearance: none; | |
} | |
.language-select:focus { | |
border-color: var(--accent-color); | |
box-shadow: 0 0 0 3px rgba(48, 184, 168, 0.25); | |
outline: none; | |
} | |
.language-select-icon { | |
position: absolute; | |
right: 15px; | |
top: 50%; | |
transform: translateY(-50%); | |
color: var(--accent-color); | |
pointer-events: none; | |
} | |
.control-buttons { | |
display: flex; | |
gap: 20px; | |
justify-content: center; | |
margin: 25px 0 35px; | |
} | |
.btn { | |
padding: 14px 30px; | |
border-radius: 50px; | |
font-weight: 600; | |
font-size: 1.1rem; | |
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); | |
position: relative; | |
overflow: hidden; | |
z-index: 1; | |
} | |
.btn::after { | |
content: ''; | |
position: absolute; | |
bottom: -50%; | |
left: -10%; | |
width: 120%; | |
height: 200%; | |
background: rgba(255, 255, 255, 0.3); | |
border-radius: 40%; | |
transform: scale(0); | |
transition: transform 0.5s; | |
z-index: -1; | |
} | |
.btn:hover::after { | |
transform: scale(1); | |
} | |
.btn:active { | |
transform: scale(0.97); | |
} | |
.btn-primary { | |
background: linear-gradient(135deg, var(--primary-color), var(--gradient-start)); | |
border: none; | |
box-shadow: 0 4px 15px rgba(140, 109, 176, 0.3); | |
color: white; | |
} | |
.btn-primary:hover { | |
box-shadow: 0 6px 20px rgba(140, 109, 176, 0.4); | |
background: linear-gradient(135deg, var(--primary-light), var(--gradient-start)); | |
} | |
.btn-danger { | |
background: linear-gradient(135deg, var(--accent-dark), var(--accent-color)); | |
border: none; | |
box-shadow: 0 4px 15px rgba(48, 184, 168, 0.3); | |
color: white; | |
} | |
.btn-danger:hover { | |
box-shadow: 0 6px 20px rgba(48, 184, 168, 0.4); | |
background: linear-gradient(135deg, var(--accent-color), #44d6c6); | |
} | |
.btn-icon { | |
margin-right: 10px; | |
font-size: 1.2rem; | |
} | |
.text-panel-container { | |
position: relative; | |
margin-bottom: 30px; | |
} | |
.text-panel { | |
border-radius: var(--border-radius); | |
background-color: var(--panel-bg); | |
padding: 25px; | |
min-height: 150px; | |
margin-top: 10px; | |
white-space: pre-wrap; | |
border: 2px solid rgba(0, 0, 0, 0.05); | |
transition: all 0.3s ease; | |
color: var(--text-color); | |
font-size: 1.1rem; | |
line-height: 1.7; | |
position: relative; | |
} | |
.text-panel.active { | |
border-color: var(--accent-color); | |
box-shadow: 0 0 20px rgba(48, 184, 168, 0.15); | |
} | |
.text-panel-header { | |
display: flex; | |
align-items: center; | |
margin-bottom: 15px; | |
} | |
.text-panel-icon { | |
width: 42px; | |
height: 42px; | |
background: linear-gradient(135deg, var(--primary-light), var(--primary-color)); | |
border-radius: 50%; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
margin-right: 15px; | |
color: white; | |
font-size: 1.3rem; | |
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); | |
} | |
.panel-title { | |
font-weight: 600; | |
font-size: 1.2rem; | |
margin: 0; | |
letter-spacing: 0.5px; | |
color: var(--primary-dark); | |
} | |
.footer { | |
text-align: center; | |
margin-top: 40px; | |
color: var(--text-secondary); | |
font-size: 0.95rem; | |
opacity: 0.8; | |
} | |
.pulse-animation { | |
animation: pulse 2s infinite; | |
} | |
@keyframes pulse { | |
0% { | |
box-shadow: 0 0 0 0 rgba(48, 184, 168, 0.4); | |
} | |
70% { | |
box-shadow: 0 0 0 15px rgba(48, 184, 168, 0); | |
} | |
100% { | |
box-shadow: 0 0 0 0 rgba(48, 184, 168, 0); | |
} | |
} | |
.status-indicator { | |
display: none; | |
align-items: center; | |
justify-content: center; | |
margin: 20px 0; | |
color: var(--accent-color); | |
font-weight: 500; | |
font-size: 1.1rem; | |
letter-spacing: 0.5px; | |
} | |
.status-indicator.active { | |
display: flex; | |
} | |
.audio-wave { | |
display: inline-flex; | |
align-items: flex-end; | |
height: 20px; | |
margin-right: 12px; | |
} | |
.audio-wave span { | |
display: inline-block; | |
width: 3px; | |
margin-right: 3px; | |
background: var(--accent-color); | |
animation: wave 1.2s infinite ease-in-out; | |
} | |
.audio-wave span:nth-child(1) { height: 8px; animation-delay: 0s; } | |
.audio-wave span:nth-child(2) { height: 16px; animation-delay: 0.2s; } | |
.audio-wave span:nth-child(3) { height: 12px; animation-delay: 0.3s; } | |
.audio-wave span:nth-child(4) { height: 20px; animation-delay: 0.4s; } | |
.audio-wave span:nth-child(5) { height: 14px; animation-delay: 0.5s; } | |
@keyframes wave { | |
0%, 100% { transform: scaleY(1); } | |
50% { transform: scaleY(0.5); } | |
} | |
.floating-icon { | |
position: absolute; | |
font-size: 8rem; | |
color: rgba(140, 109, 176, 0.07); | |
z-index: 0; | |
pointer-events: none; | |
} | |
.floating-icon.globe { | |
bottom: -2rem; | |
right: -2rem; | |
} | |
.floating-icon.mic { | |
top: -1rem; | |
left: -1rem; | |
transform: rotate(-15deg); | |
font-size: 6rem; | |
} | |
.translate-animation { | |
position: relative; | |
} | |
.translate-animation::before { | |
content: ''; | |
position: absolute; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 100%; | |
background: linear-gradient(90deg, | |
transparent, | |
rgba(48, 184, 168, 0.2), | |
transparent); | |
transform: translateX(-100%); | |
animation: translateShimmer 2s infinite; | |
} | |
@keyframes translateShimmer { | |
100% { transform: translateX(100%); } | |
} | |
.spinner { | |
display: inline-block; | |
width: 18px; | |
height: 18px; | |
border: 3px solid rgba(48, 184, 168, 0.3); | |
border-top-color: var(--accent-color); | |
border-radius: 50%; | |
margin-right: 10px; | |
animation: spin 1s ease-in-out infinite; | |
} | |
@keyframes spin { | |
to { transform: rotate(360deg); } | |
} | |
.language-badge { | |
position: absolute; | |
top: 15px; | |
right: 15px; | |
background: rgba(48, 184, 168, 0.1); | |
color: var(--accent-dark); | |
padding: 5px 12px; | |
border-radius: 20px; | |
font-size: 0.85rem; | |
font-weight: 600; | |
display: flex; | |
align-items: center; | |
gap: 5px; | |
} | |
/* Responsive adjustments */ | |
@media (max-width: 768px) { | |
.app-title { | |
font-size: 2rem; | |
} | |
.app-subtitle { | |
font-size: 0.95rem; | |
} | |
.control-buttons { | |
flex-direction: column; | |
gap: 15px; | |
} | |
.btn { | |
width: 100%; | |
padding: 12px; | |
} | |
.text-panel { | |
min-height: 120px; | |
padding: 20px; | |
font-size: 1rem; | |
} | |
} | |
/* Light scrollbar */ | |
::-webkit-scrollbar { | |
width: 6px; | |
height: 6px; | |
} | |
::-webkit-scrollbar-track { | |
background: var(--panel-bg); | |
} | |
::-webkit-scrollbar-thumb { | |
background: var(--primary-light); | |
border-radius: 10px; | |
} | |
::-webkit-scrollbar-thumb:hover { | |
background: var(--primary-color); | |
} | |
</style> | |
</head> | |
<body> | |
<div class="app-container"> | |
<div class="translator-card"> | |
<div class="floating-icon mic"> | |
<i class="fas fa-microphone"></i> | |
</div> | |
<div class="floating-icon globe"> | |
<i class="fas fa-globe"></i> | |
</div> | |
<div class="app-header"> | |
<h1 class="app-title"><i class="fas fa-microphone-alt"></i> Voice Translator </h1> | |
<p class="app-subtitle">Speak naturally. Translate instantly. Connect globally.</p> | |
</div> | |
<div class="row"> | |
<div class="col-md-8 offset-md-2"> | |
<div class="language-select-container"> | |
<select id="lang" class="form-select language-select"> | |
{% for name, code in languages.items() %} | |
<option value="{{ code }}">{{ name }}</option> | |
{% endfor %} | |
</select> | |
<div class="language-select-icon"> | |
<i class="fas fa-language fa-lg"></i> | |
</div> | |
</div> | |
</div> | |
</div> | |
<div class="control-buttons"> | |
<button id="startBtn" class="btn btn-primary"> | |
<i class="fas fa-play-circle btn-icon"></i> Start Listening | |
</button> | |
<button id="stopBtn" class="btn btn-danger" disabled> | |
<i class="fas fa-stop-circle btn-icon"></i> Stop & Translate | |
</button> | |
</div> | |
<div class="status-indicator" id="listeningIndicator"> | |
<div class="audio-wave"> | |
<span></span> | |
<span></span> | |
<span></span> | |
<span></span> | |
<span></span> | |
</div> | |
Listening to your voice... | |
</div> | |
<div class="text-panel-container"> | |
<div class="text-panel-header"> | |
<div class="text-panel-icon"> | |
<i class="fas fa-headphones"></i> | |
</div> | |
<h5 class="panel-title">Recognized Speech</h5> | |
</div> | |
<div class="language-badge"> | |
<i class="fas fa-flag"></i> English | |
</div> | |
<div id="recognized" class="text-panel"></div> | |
</div> | |
<div class="text-panel-container"> | |
<div class="text-panel-header"> | |
<div class="text-panel-icon"> | |
<i class="fas fa-language"></i> | |
</div> | |
<h5 class="panel-title">Translation</h5> | |
</div> | |
<div class="language-badge" id="targetLanguageBadge"> | |
<i class="fas fa-flag"></i> <span id="langDisplay">Hindi</span> | |
</div> | |
<div id="translated" class="text-panel"></div> | |
</div> | |
</div> | |
<div class="footer"> | |
<p> | |
<i class="fas fa-magic"></i> Powered by Flask | |
<br> | |
<small>© 2025 Voice Translator • Speak clearly for optimal recognition</small> | |
</p> | |
</div> | |
</div> | |
<script> | |
// Check for browser support | |
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; | |
if (!SpeechRecognition) { | |
alert('Your browser does not support Speech Recognition API.'); | |
} | |
const recognition = new SpeechRecognition(); | |
recognition.continuous = true; | |
recognition.interimResults = true; | |
recognition.lang = 'en-US'; | |
const startBtn = document.getElementById('startBtn'); | |
const stopBtn = document.getElementById('stopBtn'); | |
const recognizedDiv = document.getElementById('recognized'); | |
const translatedDiv = document.getElementById('translated'); | |
const listeningIndicator = document.getElementById('listeningIndicator'); | |
const langSelect = document.getElementById('lang'); | |
const langDisplay = document.getElementById('langDisplay'); | |
// Set initial language display | |
updateLanguageDisplay(); | |
let finalTranscript = ''; | |
function updateLanguageDisplay() { | |
const selectedOption = langSelect.options[langSelect.selectedIndex]; | |
langDisplay.textContent = selectedOption.text; | |
} | |
langSelect.addEventListener('change', updateLanguageDisplay); | |
recognition.onresult = (event) => { | |
let interimTranscript = ''; | |
for (let i = event.resultIndex; i < event.results.length; ++i) { | |
const transcript = event.results[i][0].transcript; | |
if (event.results[i].isFinal) { | |
finalTranscript += transcript + ' '; | |
} else { | |
interimTranscript += transcript; | |
} | |
} | |
// Display live recognized speech | |
recognizedDiv.innerHTML = finalTranscript + | |
`<span style="opacity: 0.7;">${interimTranscript}</span>`; | |
}; | |
recognition.onstart = () => { | |
listeningIndicator.classList.add('active'); | |
recognizedDiv.classList.add('active'); | |
recognizedDiv.classList.add('pulse-animation'); | |
}; | |
recognition.onend = () => { | |
listeningIndicator.classList.remove('active'); | |
recognizedDiv.classList.remove('pulse-animation'); | |
}; | |
recognition.onerror = (event) => { | |
console.error('Speech recognition error', event.error); | |
listeningIndicator.classList.remove('active'); | |
recognizedDiv.classList.remove('pulse-animation'); | |
recognizedDiv.classList.remove('active'); | |
if (event.error === 'no-speech') { | |
recognizedDiv.innerHTML = '<span style="color: var(--accent-color);">No speech detected. Please try again.</span>'; | |
} else { | |
recognizedDiv.innerHTML = `<span style="color: var(--accent-color);">Error: ${event.error}. Please try again.</span>`; | |
} | |
}; | |
startBtn.onclick = () => { | |
finalTranscript = ''; | |
recognizedDiv.innerText = ''; | |
translatedDiv.innerText = ''; | |
translatedDiv.classList.remove('active'); | |
recognition.start(); | |
startBtn.disabled = true; | |
stopBtn.disabled = false; | |
// Add animation effects | |
startBtn.style.transform = 'scale(0.95)'; | |
setTimeout(() => startBtn.style.transform = '', 200); | |
}; | |
stopBtn.onclick = async () => { | |
recognition.stop(); | |
startBtn.disabled = false; | |
stopBtn.disabled = true; | |
recognizedDiv.classList.remove('active'); | |
// Add animation effects | |
stopBtn.style.transform = 'scale(0.95)'; | |
setTimeout(() => stopBtn.style.transform = '', 200); | |
const textToTranslate = finalTranscript.trim(); | |
if (!textToTranslate) { | |
translatedDiv.innerHTML = '<span style="color: var(--accent-color);">No speech detected. Please speak before stopping.</span>'; | |
return; | |
} | |
// Show loading state | |
translatedDiv.innerHTML = '<div class="text-center"><span class="spinner"></span> Translating your speech...</div>'; | |
translatedDiv.classList.add('translate-animation'); | |
try { | |
// Send text for translation | |
const response = await fetch('/translate', { | |
method: 'POST', | |
headers: { 'Content-Type': 'application/json' }, | |
body: JSON.stringify({ text: textToTranslate, target: document.getElementById('lang').value }) | |
}); | |
const data = await response.json(); | |
setTimeout(() => { | |
translatedDiv.classList.remove('translate-animation'); | |
translatedDiv.classList.add('active'); | |
translatedDiv.innerText = data.translation; | |
}, 500); | |
} catch (error) { | |
translatedDiv.classList.remove('translate-animation'); | |
translatedDiv.innerHTML = '<span style="color: var(--accent-color);">Error: Could not complete translation. Please try again.</span>'; | |
} | |
}; | |
// Add some fancy animations on page load | |
document.addEventListener('DOMContentLoaded', () => { | |
document.querySelectorAll('.translator-card, .app-title, .app-subtitle, .text-panel-container').forEach((el, i) => { | |
el.style.opacity = '0'; | |
el.style.transform = 'translateY(20px)'; | |
el.style.transition = `opacity 0.5s ease-out ${i * 0.1}s, transform 0.5s ease-out ${i * 0.1}s`; | |
setTimeout(() => { | |
el.style.opacity = '1'; | |
el.style.transform = 'translateY(0)'; | |
}, 100); | |
}); | |
}); | |
</script> | |
</body> | |
</html> |