Athspi's picture
Update static/index.html
4b26a3a verified
<!DOCTYPE html>
<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>