Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>AI Audio Translator</title> | |
<style> | |
:root { | |
--primary-color: #6366f1; | |
--primary-light: #818cf8; | |
--primary-dark: #4f46e5; | |
--secondary-color: #f472b6; | |
--accent-color: #34d399; | |
--dark-color: #1f2937; | |
--light-color: #f9fafb; | |
--gray-color: #9ca3af; | |
--danger-color: #ef4444; | |
--success-color: #10b981; | |
} | |
* { | |
margin: 0; | |
padding: 0; | |
box-sizing: border-box; | |
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
} | |
body { | |
background-color: var(--light-color); | |
color: var(--dark-color); | |
min-height: 100vh; | |
display: flex; | |
flex-direction: column; | |
align-items: center; | |
justify-content: center; | |
padding: 2rem; | |
overflow-x: hidden; | |
} | |
.container { | |
width: 100%; | |
max-width: 1000px; | |
background-color: white; | |
border-radius: 16px; | |
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1); | |
overflow: hidden; | |
position: relative; | |
} | |
header { | |
background: linear-gradient(135deg, var(--primary-color), var(--primary-dark)); | |
color: white; | |
padding: 2rem; | |
text-align: center; | |
position: relative; | |
overflow: hidden; | |
} | |
header h1 { | |
font-size: 2.5rem; | |
margin-bottom: 0.5rem; | |
font-weight: 700; | |
animation: fadeInDown 0.8s ease-out; | |
} | |
header p { | |
font-size: 1.1rem; | |
opacity: 0.9; | |
max-width: 600px; | |
margin: 0 auto; | |
animation: fadeInUp 0.8s ease-out 0.2s both; | |
} | |
.bubbles { | |
position: absolute; | |
width: 100%; | |
height: 100%; | |
top: 0; | |
left: 0; | |
pointer-events: none; | |
z-index: 0; | |
} | |
.bubble { | |
position: absolute; | |
border-radius: 50%; | |
background: rgba(255, 255, 255, 0.1); | |
animation: bubble 15s linear infinite; | |
} | |
@keyframes bubble { | |
0% { | |
transform: translateY(0) rotate(0deg); | |
opacity: 1; | |
border-radius: 0; | |
} | |
100% { | |
transform: translateY(-1000px) rotate(720deg); | |
opacity: 0; | |
border-radius: 50%; | |
} | |
} | |
.main-content { | |
padding: 2rem; | |
display: flex; | |
flex-direction: column; | |
gap: 2rem; | |
position: relative; | |
z-index: 1; | |
} | |
.card { | |
background-color: white; | |
border-radius: 12px; | |
padding: 1.5rem; | |
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); | |
transition: all 0.3s ease; | |
} | |
.card:hover { | |
transform: translateY(-5px); | |
box-shadow: 0 8px 15px rgba(0, 0, 0, 0.1); | |
} | |
.card h2 { | |
color: var(--primary-dark); | |
margin-bottom: 1rem; | |
display: flex; | |
align-items: center; | |
gap: 0.5rem; | |
} | |
.audio-input { | |
display: flex; | |
flex-direction: column; | |
gap: 1rem; | |
} | |
.dropzone { | |
border: 2px dashed var(--gray-color); | |
border-radius: 8px; | |
padding: 2rem; | |
text-align: center; | |
cursor: pointer; | |
transition: all 0.3s ease; | |
background-color: rgba(99, 102, 241, 0.05); | |
} | |
.dropzone:hover, .dropzone.dragover { | |
border-color: var(--primary-color); | |
background-color: rgba(99, 102, 241, 0.1); | |
} | |
.dropzone p { | |
color: var(--gray-color); | |
margin: 0.5rem 0; | |
} | |
.dropzone .icon { | |
font-size: 2.5rem; | |
color: var(--primary-light); | |
margin-bottom: 0.5rem; | |
} | |
.custom-file-upload { | |
display: inline-block; | |
font-weight: 500; | |
cursor: pointer; | |
background-color: var(--primary-color); | |
color: white; | |
padding: 0.75rem 1.5rem; | |
border-radius: 6px; | |
transition: all 0.2s ease; | |
border: none; | |
font-size: 1rem; | |
} | |
.custom-file-upload:hover { | |
background-color: var(--primary-dark); | |
transform: translateY(-2px); | |
} | |
#audioFileInput { | |
display: none; | |
} | |
.record-option { | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
margin-top: 1rem; | |
gap: 1rem; | |
} | |
.record-btn { | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
gap: 0.5rem; | |
background-color: white; | |
border: 1px solid var(--primary-color); | |
color: var(--primary-color); | |
padding: 0.75rem 1.5rem; | |
border-radius: 6px; | |
cursor: pointer; | |
transition: all 0.2s ease; | |
font-weight: 500; | |
font-size: 1rem; | |
} | |
.record-btn:hover { | |
background-color: rgba(99, 102, 241, 0.1); | |
} | |
.record-btn.recording { | |
background-color: var(--danger-color); | |
color: white; | |
border-color: var(--danger-color); | |
animation: pulse 1.5s infinite; | |
} | |
@keyframes pulse { | |
0% { | |
box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.4); | |
} | |
70% { | |
box-shadow: 0 0 0 10px rgba(239, 68, 68, 0); | |
} | |
100% { | |
box-shadow: 0 0 0 0 rgba(239, 68, 68, 0); | |
} | |
} | |
.language-selection { | |
display: flex; | |
flex-direction: column; | |
gap: 1rem; | |
} | |
.language-dropdown { | |
position: relative; | |
} | |
.language-dropdown select { | |
width: 100%; | |
padding: 0.75rem 1rem; | |
border-radius: 6px; | |
border: 1px solid var(--gray-color); | |
background-color: white; | |
font-size: 1rem; | |
cursor: pointer; | |
appearance: none; | |
outline: none; | |
transition: all 0.2s ease; | |
} | |
.language-dropdown select:focus { | |
border-color: var(--primary-color); | |
box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.2); | |
} | |
.language-dropdown::after { | |
content: '▾'; | |
position: absolute; | |
right: 1rem; | |
top: 50%; | |
transform: translateY(-50%); | |
color: var(--gray-color); | |
pointer-events: none; | |
} | |
.results-container { | |
display: grid; | |
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); | |
gap: 1.5rem; | |
} | |
.result-card { | |
background-color: white; | |
border-radius: 12px; | |
padding: 1.5rem; | |
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); | |
transition: all 0.3s ease; | |
height: 100%; | |
display: flex; | |
flex-direction: column; | |
} | |
.result-card h3 { | |
color: var(--primary-dark); | |
margin-bottom: 1rem; | |
padding-bottom: 0.5rem; | |
border-bottom: 1px solid rgba(0, 0, 0, 0.1); | |
} | |
.result-content { | |
flex: 1; | |
overflow-y: auto; | |
max-height: 200px; | |
padding-right: 0.5rem; | |
margin-bottom: 1rem; | |
line-height: 1.6; | |
} | |
.result-content::-webkit-scrollbar { | |
width: 6px; | |
} | |
.result-content::-webkit-scrollbar-track { | |
background: rgba(0, 0, 0, 0.05); | |
border-radius: 10px; | |
} | |
.result-content::-webkit-scrollbar-thumb { | |
background: var(--primary-light); | |
border-radius: 10px; | |
} | |
.audio-player { | |
width: 100%; | |
height: 40px; | |
outline: none; | |
} | |
.translate-btn { | |
display: block; | |
width: 100%; | |
padding: 1rem; | |
background: linear-gradient(135deg, var(--primary-color), var(--primary-dark)); | |
color: white; | |
border: none; | |
border-radius: 8px; | |
font-size: 1.1rem; | |
font-weight: 600; | |
cursor: pointer; | |
transition: all 0.3s ease; | |
margin-top: 1rem; | |
position: relative; | |
overflow: hidden; | |
} | |
.translate-btn:hover { | |
transform: translateY(-2px); | |
box-shadow: 0 6px 15px rgba(79, 70, 229, 0.3); | |
} | |
.translate-btn:active { | |
transform: translateY(0); | |
} | |
.translate-btn .btn-content { | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
gap: 0.5rem; | |
position: relative; | |
z-index: 2; | |
} | |
.translate-btn::before { | |
content: ''; | |
position: absolute; | |
top: 0; | |
left: -100%; | |
width: 100%; | |
height: 100%; | |
background: linear-gradient(135deg, var(--primary-dark), var(--secondary-color)); | |
transition: all 0.5s ease; | |
z-index: 1; | |
} | |
.translate-btn:hover::before { | |
left: 0; | |
} | |
.loading { | |
display: none; | |
align-items: center; | |
justify-content: center; | |
flex-direction: column; | |
gap: 1rem; | |
padding: 2rem; | |
text-align: center; | |
} | |
.spinner { | |
width: 60px; | |
height: 60px; | |
border: 5px solid rgba(99, 102, 241, 0.2); | |
border-radius: 50%; | |
border-top-color: var(--primary-color); | |
animation: spin 1s linear infinite; | |
} | |
@keyframes spin { | |
to { | |
transform: rotate(360deg); | |
} | |
} | |
.hidden { | |
display: none; | |
} | |
.output-container { | |
display: none; | |
animation: fadeIn 0.5s ease-out; | |
} | |
.audio-visualization { | |
height: 60px; | |
background-color: rgba(99, 102, 241, 0.1); | |
border-radius: 8px; | |
overflow: hidden; | |
margin: 1rem 0; | |
position: relative; | |
} | |
.visualizer-bars { | |
display: flex; | |
align-items: flex-end; | |
height: 100%; | |
padding: 0 5px; | |
gap: 2px; | |
} | |
.visualizer-bar { | |
flex: 1; | |
background: linear-gradient(to top, var(--primary-color), var(--primary-light)); | |
max-width: 4px; | |
border-radius: 2px; | |
height: 0%; | |
transition: height 0.05s ease; | |
} | |
.controls { | |
display: flex; | |
gap: 0.5rem; | |
margin-top: 1rem; | |
} | |
.control-btn { | |
flex: 1; | |
padding: 0.75rem; | |
border: none; | |
border-radius: 6px; | |
font-weight: 500; | |
cursor: pointer; | |
transition: all 0.2s ease; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
gap: 0.5rem; | |
} | |
.download-btn { | |
background-color: var(--success-color); | |
color: white; | |
} | |
.download-btn:hover { | |
background-color: #0b9e6e; | |
} | |
.copy-btn { | |
background-color: var(--accent-color); | |
color: white; | |
} | |
.copy-btn:hover { | |
background-color: #2bbb89; | |
} | |
.toast { | |
position: fixed; | |
bottom: 2rem; | |
left: 50%; | |
transform: translateX(-50%) translateY(100px); | |
background-color: var(--dark-color); | |
color: white; | |
padding: 0.75rem 1.5rem; | |
border-radius: 6px; | |
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2); | |
z-index: 1000; | |
opacity: 0; | |
transition: all 0.3s ease; | |
} | |
.toast.show { | |
transform: translateX(-50%) translateY(0); | |
opacity: 1; | |
} | |
@keyframes fadeIn { | |
from { | |
opacity: 0; | |
transform: translateY(20px); | |
} | |
to { | |
opacity: 1; | |
transform: translateY(0); | |
} | |
} | |
@keyframes fadeInDown { | |
from { | |
opacity: 0; | |
transform: translateY(-20px); | |
} | |
to { | |
opacity: 1; | |
transform: translateY(0); | |
} | |
} | |
@keyframes fadeInUp { | |
from { | |
opacity: 0; | |
transform: translateY(20px); | |
} | |
to { | |
opacity: 1; | |
transform: translateY(0); | |
} | |
} | |
@media (max-width: 768px) { | |
.main-content { | |
padding: 1.5rem; | |
} | |
.results-container { | |
grid-template-columns: 1fr; | |
} | |
header h1 { | |
font-size: 2rem; | |
} | |
header p { | |
font-size: 1rem; | |
} | |
} | |
</style> | |
</head> | |
<body> | |
<div class="container"> | |
<header> | |
<div class="bubbles"> | |
<!-- Bubbles generated by JS --> | |
</div> | |
<h1>AI Audio Translator</h1> | |
<p>Upload an audio file or record via microphone, select a target language, and get the transcription, translation, and translated audio!</p> | |
</header> | |
<div class="main-content"> | |
<div class="audio-input card"> | |
<h2> | |
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
<path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3z"></path> | |
<path d="M19 10v2a7 7 0 0 1-14 0v-2"></path> | |
<line x1="12" y1="19" x2="12" y2="22"></line> | |
</svg> | |
Audio Input | |
</h2> | |
<div class="dropzone" id="dropzone"> | |
<div class="icon"> | |
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path> | |
<polyline points="17 8 12 3 7 8"></polyline> | |
<line x1="12" y1="3" x2="12" y2="15"></line> | |
</svg> | |
</div> | |
<p>Drag and drop your audio file here</p> | |
<p>or</p> | |
<label for="audioFileInput" class="custom-file-upload"> | |
Choose File | |
</label> | |
<input type="file" id="audioFileInput" accept="audio/*"> | |
<p id="fileInfo" class="hidden"></p> | |
</div> | |
<div class="record-option"> | |
<button id="recordBtn" class="record-btn"> | |
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
<circle cx="12" cy="12" r="10"></circle> | |
<circle cx="12" cy="12" r="3"></circle> | |
</svg> | |
Start Recording | |
</button> | |
</div> | |
<div id="audioVisualization" class="audio-visualization hidden"> | |
<div class="visualizer-bars" id="visualizerBars"> | |
<!-- Bars will be generated dynamically --> | |
</div> | |
</div> | |
</div> | |
<div class="language-selection card"> | |
<h2> | |
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
<path d="M5 8l6 6"></path> | |
<path d="M4 14l6-6 2-3"></path> | |
<path d="M2 5h12"></path> | |
<path d="M7 2h1"></path> | |
<path d="M22 22l-5-10-5 10"></path> | |
<path d="M14 18h6"></path> | |
</svg> | |
Target Language | |
</h2> | |
<div class="language-dropdown"> | |
<select id="languageSelect"> | |
<option value="" disabled selected>Select language</option> | |
<!-- Languages will be populated dynamically --> | |
</select> | |
</div> | |
<button id="translateBtn" class="translate-btn" disabled> | |
<span class="btn-content"> | |
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
<path d="M5 8l6 6"></path> | |
<path d="M4 14l6-6 2-3"></path> | |
<path d="M2 5h12"></path> | |
<path d="M7 2h1"></path> | |
<path d="M22 22l-5-10-5 10"></path> | |
<path d="M14 18h6"></path> | |
</svg> | |
Translate Audio | |
</span> | |
</button> | |
</div> | |
<div id="loadingSection" class="loading"> | |
<div class="spinner"></div> | |
<p>Processing your audio... This may take a moment.</p> | |
</div> | |
<div id="outputContainer" class="output-container"> | |
<div class="results-container"> | |
<div class="result-card"> | |
<h3>Original Transcription</h3> | |
<div id="originalText" class="result-content"> | |
<!-- Transcription will appear here --> | |
</div> | |
<audio id="originalAudio" class="audio-player" controls></audio> | |
<div class="controls"> | |
<button id="copyOriginal" class="control-btn copy-btn"> | |
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect> | |
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path> | |
</svg> | |
Copy Text | |
</button> | |
</div> | |
</div> | |
<div class="result-card"> | |
<h3>Translated Text</h3> | |
<div id="translatedText" class="result-content"> | |
<!-- Translation will appear here --> | |
</div> | |
<audio id="translatedAudio" class="audio-player" controls></audio> | |
<div class="controls"> | |
<button id="copyTranslated" class="control-btn copy-btn"> | |
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect> | |
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path> | |
</svg> | |
Copy Text | |
</button> | |
<button id="downloadTranslated" class="control-btn download-btn"> | |
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path> | |
<polyline points="7 10 12 15 17 10"></polyline> | |
<line x1="12" y1="15" x2="12" y2="3"></line> | |
</svg> | |
Download | |
</button> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<div id="toast" class="toast">Copied to clipboard!</div> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/1.3.4/axios.min.js"></script> | |
<script> | |
// Create bubble animation in header | |
const createBubbles = () => { | |
const bubblesContainer = document.querySelector('.bubbles'); | |
const bubbleCount = 10; | |
for (let i = 0; i < bubbleCount; i++) { | |
const bubble = document.createElement('div'); | |
bubble.classList.add('bubble'); | |
// Random sizes | |
const size = Math.random() * 60 + 20; | |
bubble.style.width = `${size}px`; | |
bubble.style.height = `${size}px`; | |
// Random positions | |
bubble.style.left = `${Math.random() * 100}%`; | |
bubble.style.top = `${Math.random() * 100}%`; | |
// Random animation delay and duration | |
const animationDuration = Math.random() * 10 + 5; | |
const animationDelay = Math.random() * 5; | |
bubble.style.animationDuration = `${animationDuration}s`; | |
bubble.style.animationDelay = `${animationDelay}s`; | |
bubblesContainer.appendChild(bubble); | |
} | |
}; | |
// DOM Elements | |
const dropzone = document.getElementById('dropzone'); | |
const fileInput = document.getElementById('audioFileInput'); | |
const fileInfo = document.getElementById('fileInfo'); | |
const recordBtn = document.getElementById('recordBtn'); | |
const translateBtn = document.getElementById('translateBtn'); | |
const languageSelect = document.getElementById('languageSelect'); | |
const loadingSection = document.getElementById('loadingSection'); | |
const outputContainer = document.getElementById('outputContainer'); | |
const originalText = document.getElementById('originalText'); | |
const translatedText = document.getElementById('translatedText'); | |
const originalAudio = document.getElementById('originalAudio'); | |
const translatedAudio = document.getElementById('translatedAudio'); | |
const copyOriginal = document.getElementById('copyOriginal'); | |
const copyTranslated = document.getElementById('copyTranslated'); | |
const downloadTranslated = document.getElementById('downloadTranslated'); | |
const toast = document.getElementById('toast'); | |
const audioVisualization = document.getElementById('audioVisualization'); | |
const visualizerBars = document.getElementById('visualizerBars'); | |
// Variables | |
let audioFile = null; | |
let mediaRecorder = null; | |
let audioChunks = []; | |
let isRecording = false; | |
let audioContext = null; | |
let analyser = null; | |
let visualizationInterval = null; | |
// Create visualizer bars | |
const createVisualizerBars = () => { | |
visualizerBars.innerHTML = ''; | |
const barCount = 50; | |
for (let i = 0; i < barCount; i++) { | |
const bar = document.createElement('div'); | |
bar.classList.add('visualizer-bar'); | |
visualizerBars.appendChild(bar); | |
} | |
}; | |
// Initialize audio context | |
const initAudioContext = () => { | |
if (!audioContext) { | |
audioContext = new (window.AudioContext || window.webkitAudioContext)(); | |
analyser = audioContext.createAnalyser(); | |
analyser.fftSize = 256; | |
} | |
}; | |
// Update visualization | |
const updateVisualization = (dataArray) => { | |
const bars = visualizerBars.querySelectorAll('.visualizer-bar'); | |
const bufferLength = analyser.frequencyBinCount; | |
analyser.getByteFrequencyData(dataArray); | |
for (let i = 0; i < bars.length; i++) { | |
const index = Math.floor(i * (bufferLength / bars.length)); | |
const value = dataArray[index] / 255; | |
const height = value * 100; | |
bars[i].style.height = `${height}%`; | |
} | |
}; | |
// Start visualization | |
const startVisualization = (stream) => { | |
initAudioContext(); | |
const source = audioContext.createMediaStreamSource(stream); | |
source.connect(analyser); | |
const dataArray = new Uint8Array(analyser.frequencyBinCount); | |
createVisualizerBars(); | |
audioVisualization.classList.remove('hidden'); | |
visualizationInterval = setInterval(() => { | |
updateVisualization(dataArray); | |
}, 50); | |
}; | |
// Stop visualization | |
const stopVisualization = () => { | |
if (visualizationInterval) { | |
clearInterval(visualizationInterval); | |
visualizationInterval = null; | |
} | |
audioVisualization.classList.add('hidden'); | |
}; | |
// Initialize | |
const init = () => { | |
createBubbles(); | |
// Drag and drop functionality | |
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { | |
dropzone.addEventListener(eventName, preventDefaults, false); | |
}); | |
function preventDefaults(e) { | |
e.preventDefault(); | |
e.stopPropagation(); | |
} | |
['dragenter', 'dragover'].forEach(eventName => { | |
dropzone.addEventListener(eventName, highlight, false); | |
}); | |
['dragleave', 'drop'].forEach(eventName => { | |
dropzone.addEventListener(eventName, unhighlight, false); | |
}); | |
function highlight() { | |
dropzone.classList.add('dragover'); | |
} | |
function unhighlight() { | |
dropzone.classList.remove('dragover'); | |
} | |
dropzone.addEventListener('drop', handleDrop, false); | |
function handleDrop(e) { | |
const dt = e.dataTransfer; | |
const files = dt.files; | |
if (files.length > 0) { | |
handleFile(files[0]); | |
} | |
} | |
// File input change handler | |
fileInput.addEventListener('change', (e) => { | |
if (e.target.files.length > 0) { | |
handleFile(e.target.files[0]); | |
} | |
}); | |
// Handle file selection | |
const handleFile = (file) => { | |
const allowedAudioTypes = ['audio/mpeg', 'audio/ogg', 'audio/wav', 'audio/webm', 'audio/x-wav', 'audio/mp3']; // Add more if needed | |
const maxFileSizeMB = 30; | |
const maxFileSize = maxFileSizeMB * 1024 * 1024; // 30MB in bytes | |
if (!allowedAudioTypes.includes(file.type)) { | |
showToast('Error: Invalid audio format. Please upload MP3, OGG, WAV, or WebM.'); | |
fileInput.value = ''; // Reset file input | |
audioFile = null; | |
fileInfo.classList.add('hidden'); | |
translateBtn.disabled = true; | |
return; | |
} | |
if (file.size > maxFileSize) { | |
showToast(`Error: File size exceeds ${maxFileSizeMB}MB limit.`); | |
fileInput.value = ''; // Reset file input | |
audioFile = null; | |
fileInfo.classList.add('hidden'); | |
translateBtn.disabled = true; | |
return; | |
} | |
audioFile = file; | |
fileInfo.textContent = `Selected file: ${file.name} (${(file.size / (1024 * 1024)).toFixed(2)} MB)`; | |
fileInfo.classList.remove('hidden'); | |
translateBtn.disabled = false; | |
}; | |
// Record button click handler | |
recordBtn.addEventListener('click', () => { | |
if (!isRecording) { | |
startRecording(); | |
} else { | |
stopRecording(); | |
} | |
}); | |
// Start recording | |
const startRecording = async () => { | |
try { | |
const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); | |
mediaRecorder = new MediaRecorder(stream); | |
mediaRecorder.start(); | |
isRecording = true; | |
recordBtn.classList.add('recording'); | |
recordBtn.innerHTML = ` | |
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
<circle cx="12" cy="12" r="10"></circle> | |
<rect x="6" y="6" width="12" height="12" rx="2"></rect> | |
</svg> | |
Stop Recording | |
`; | |
startVisualization(stream); | |
mediaRecorder.ondataavailable = (e) => { | |
audioChunks.push(e.data); | |
}; | |
mediaRecorder.onstop = () => { | |
const audioBlob = new Blob(audioChunks, { type: 'audio/wav' }); | |
audioFile = new File([audioBlob], 'recording.wav', { type: 'audio/wav' }); | |
fileInfo.textContent = `Recording saved: ${audioFile.name} (${(audioFile.size / (1024 * 1024)).toFixed(2)} MB)`; | |
fileInfo.classList.remove('hidden'); | |
translateBtn.disabled = false; | |
stopVisualization(); | |
}; | |
} catch (err) { | |
console.error('Error starting recording:', err); | |
if (err.name === 'NotAllowedError' || err.name === 'PermissionDeniedError') { | |
showToast('Error: Microphone access denied. Please check your browser permissions.'); | |
} else { | |
showToast('Error starting recording. Please ensure you have a microphone connected and permissions granted.'); | |
} | |
stopVisualization(); // Ensure visualization stops even on error | |
recordBtn.classList.remove('recording'); // Remove recording class in case of error | |
recordBtn.innerHTML = ` | |
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
<circle cx="12" cy="12" r="10"></circle> | |
<circle cx="12" cy="12" r="3"></circle> | |
</svg> | |
Start Recording | |
`; | |
isRecording = false; // Reset recording state in case of error | |
} | |
}; | |
// Stop recording | |
const stopRecording = () => { | |
if (mediaRecorder && mediaRecorder.state === 'recording') { // Check if mediaRecorder is initialized and recording | |
mediaRecorder.stop(); | |
isRecording = false; | |
recordBtn.classList.remove('recording'); | |
recordBtn.innerHTML = ` | |
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
<circle cx="12" cy="12" r="10"></circle> | |
<circle cx="12" cy="12" r="3"></circle> | |
</svg> | |
Start Recording | |
`; | |
audioChunks = []; | |
} | |
}; | |
// Translate button click handler | |
translateBtn.addEventListener('click', async () => { | |
if (!audioFile) { | |
showToast('Please select or record an audio file.'); | |
return; | |
} | |
if (!languageSelect.value) { | |
showToast('Please select a target language.'); | |
return; | |
} | |
loadingSection.style.display = 'flex'; | |
outputContainer.style.display = 'none'; | |
originalText.textContent = ''; // Clear previous text | |
translatedText.textContent = ''; // Clear previous text | |
originalAudio.src = ''; // Clear previous audio | |
translatedAudio.src = ''; // Clear previous audio | |
const formData = new FormData(); | |
formData.append('audio', audioFile); | |
formData.append('language', languageSelect.value); | |
try { | |
const response = await axios.post('/translate', formData); | |
if (response.data.error) { | |
throw new Error(response.data.error); | |
} | |
const { transcription, translation, audio_url } = response.data; | |
originalText.textContent = transcription; | |
translatedText.textContent = translation; | |
originalAudio.src = URL.createObjectURL(audioFile); | |
translatedAudio.src = audio_url; | |
loadingSection.style.display = 'none'; | |
outputContainer.style.display = 'block'; | |
} catch (error) { | |
console.error('Translation error:', error); | |
let errorMessage = 'An error occurred during translation.'; | |
if (error.response) { | |
// The request was made and the server responded with a status code | |
errorMessage = `Translation failed: ${error.response.data.message || error.response.statusText} (Status ${error.response.status})`; | |
console.error("Server response data:", error.response.data); | |
console.error("Server response status:", error.response.status); | |
} else if (error.request) { | |
// The request was made but no response was received | |
errorMessage = 'Translation failed: No response from server. Please check your network connection.'; | |
console.error("No response received:", error.request); | |
} else { | |
// Something happened in setting up the request that triggered an Error | |
errorMessage = `Translation failed: ${error.message}`; | |
console.error("Error during request setup:", error.message); | |
} | |
showToast(errorMessage); | |
loadingSection.style.display = 'none'; | |
outputContainer.style.display = 'none'; // Keep output container hidden on error | |
} | |
}); | |
// Copy original text to clipboard | |
copyOriginal.addEventListener('click', () => { | |
navigator.clipboard.writeText(originalText.textContent).then(() => { | |
showToast('Original text copied to clipboard!'); | |
}).catch(err => { | |
console.error("Clipboard copy error", err); | |
showToast('Failed to copy original text to clipboard.'); | |
}); | |
}); | |
// Copy translated text to clipboard | |
copyTranslated.addEventListener('click', () => { | |
navigator.clipboard.writeText(translatedText.textContent).then(() => { | |
showToast('Translated text copied to clipboard!'); | |
}).catch(err => { | |
console.error("Clipboard copy error", err); | |
showToast('Failed to copy translated text to clipboard.'); | |
}); | |
}); | |
// Download translated audio | |
downloadTranslated.addEventListener('click', () => { | |
if (translatedAudio.src) { | |
const link = document.createElement('a'); | |
link.href = translatedAudio.src; | |
link.download = `translated_audio.mp3`; // force mp3 download for broader compatibility, adjust as needed | |
link.click(); | |
} else { | |
showToast('No translated audio available to download.'); | |
} | |
}); | |
// Show toast message | |
const showToast = (message) => { | |
toast.textContent = message; | |
toast.classList.add('show'); | |
setTimeout(() => { | |
toast.classList.remove('show'); | |
}, 3000); | |
}; | |
// Load languages | |
fetch('/languages') | |
.then(response => response.json()) | |
.then(languages => { | |
languageSelect.innerHTML = '<option value="" disabled selected>Select language</option>' + | |
languages.map(lang => `<option value="${lang}">${lang}</option>`).join(''); | |
}).catch(error => { | |
console.error("Failed to load languages:", error); | |
showToast("Failed to load languages. Please check your connection."); | |
}); | |
}; | |
// Initialize the app | |
init(); | |
</script> | |
</body> | |
</html> |