Translate / templates /index.html
Athspi's picture
Update templates/index.html
5f57252 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI Video Dubbing</title>
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<style>
:root {
--primary: #4361ee;
--primary-light: #4895ef;
--secondary: #3f37c9;
--dark: #1a1a2e;
--light: #f8f9fa;
--gray: #6c757d;
--success: #4cc9f0;
--error: #f72585;
--border-radius: 12px;
--shadow: 0 10px 30px rgba(0,0,0,0.1);
--transition: all 0.3s ease;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Poppins', sans-serif;
background-color: #f5f7ff;
color: var(--dark);
line-height: 1.6;
padding: 20px;
}
.container {
max-width: 800px;
margin: 0 auto;
background: white;
border-radius: var(--border-radius);
box-shadow: var(--shadow);
overflow: hidden;
}
header {
background: linear-gradient(135deg, var(--primary), var(--secondary));
color: white;
padding: 2rem;
text-align: center;
}
h1 {
font-size: 2.2rem;
margin-bottom: 0.5rem;
}
.subtitle {
font-weight: 300;
opacity: 0.9;
}
.content {
padding: 2rem;
}
.upload-area {
border: 2px dashed var(--primary-light);
border-radius: var(--border-radius);
padding: 3rem 2rem;
text-align: center;
margin-bottom: 2rem;
transition: var(--transition);
cursor: pointer;
position: relative;
}
.upload-area:hover {
border-color: var(--primary);
background: rgba(67, 97, 238, 0.05);
}
.upload-icon {
font-size: 3rem;
color: var(--primary-light);
margin-bottom: 1rem;
}
.file-input {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
opacity: 0;
cursor: pointer;
}
.file-info {
margin-top: 1rem;
font-size: 0.9rem;
color: var(--gray);
}
.options {
background: #f8f9fe;
padding: 1.5rem;
border-radius: var(--border-radius);
margin-bottom: 2rem;
}
.option-group {
margin-bottom: 1.5rem;
}
.option-title {
font-weight: 600;
margin-bottom: 0.8rem;
color: var(--secondary);
display: flex;
align-items: center;
gap: 0.5rem;
}
.option-title i {
font-size: 1.1rem;
}
.radio-group, .checkbox-group {
display: flex;
flex-wrap: wrap;
gap: 1rem;
}
.radio-option, .checkbox-option {
display: flex;
align-items: center;
gap: 0.5rem;
background: white;
padding: 0.8rem 1.2rem;
border-radius: 8px;
box-shadow: 0 2px 5px rgba(0,0,0,0.05);
transition: var(--transition);
}
.radio-option:hover, .checkbox-option:hover {
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
}
.radio-option input, .checkbox-option input {
accent-color: var(--primary);
}
.btn {
background: var(--primary);
color: white;
border: none;
padding: 1rem;
border-radius: var(--border-radius);
font-size: 1.1rem;
font-weight: 500;
cursor: pointer;
width: 100%;
transition: var(--transition);
display: flex;
align-items: center;
justify-content: center;
gap: 0.8rem;
}
.btn:hover {
background: var(--secondary);
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(63, 55, 201, 0.3);
}
.btn:disabled {
background: var(--gray);
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.btn i {
font-size: 1.2rem;
}
.alert {
padding: 1rem;
border-radius: var(--border-radius);
margin-bottom: 1.5rem;
font-weight: 500;
display: flex;
align-items: center;
gap: 0.8rem;
}
.alert-success {
background: rgba(76, 201, 240, 0.2);
color: #0a9396;
border: 1px solid rgba(76, 201, 240, 0.3);
}
.alert-error {
background: rgba(247, 37, 133, 0.1);
color: var(--error);
border: 1px solid rgba(247, 37, 133, 0.2);
}
.progress-container {
margin: 2rem 0;
display: none;
}
.progress-header {
display: flex;
justify-content: space-between;
margin-bottom: 0.5rem;
}
.progress-bar {
height: 10px;
background: #e9ecef;
border-radius: 5px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: var(--primary);
width: 0%;
transition: width 0.3s ease;
}
.progress-details {
margin-top: 1rem;
font-size: 0.9rem;
color: var(--gray);
}
.time-estimate {
margin-top: 0.5rem;
display: flex;
justify-content: space-between;
font-size: 0.85rem;
color: var(--gray);
}
.result-container {
display: none;
margin-top: 2rem;
animation: fadeIn 0.5s ease;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.result-video {
width: 100%;
border-radius: var(--border-radius);
margin-bottom: 1.5rem;
aspect-ratio: 16/9;
background: black;
}
.script-container {
background: var(--dark);
color: white;
padding: 1.5rem;
border-radius: var(--border-radius);
max-height: 300px;
overflow-y: auto;
font-family: monospace;
white-space: pre-wrap;
line-height: 1.6;
}
.duration-notice {
margin-top: 1rem;
padding: 0.8rem;
background: #fff8e1;
border-radius: 8px;
border-left: 4px solid #ffc107;
font-size: 0.9rem;
}
@media (max-width: 768px) {
body {
padding: 10px;
}
header {
padding: 1.5rem;
}
h1 {
font-size: 1.8rem;
}
.content {
padding: 1.5rem;
}
.radio-group, .checkbox-group {
flex-direction: column;
gap: 0.8rem;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>AI Video Dubbing</h1>
<p class="subtitle">Transform your videos with AI-powered Tamil dubbing</p>
</header>
<div class="content">
<div id="upload-container">
<div class="upload-area" id="upload-area">
<div class="upload-icon">
<i class="fas fa-cloud-upload-alt"></i>
</div>
<h3>Drag & Drop Video File</h3>
<p>or click to browse (MP4, MOV, WEBM, AVI)</p>
<div id="file-info" class="file-info">No file selected</div>
<input type="file" id="video-input" class="file-input" accept="video/*">
</div>
<div class="options">
<div class="option-group">
<div class="option-title">
<i class="fas fa-microphone"></i>
Voice Style
</div>
<div class="radio-group" id="voice-options">
{% for voice, value in voices.items() %}
<label class="radio-option">
<input type="radio" name="voice" value="{{ value }}" {% if loop.first %}checked{% endif %}>
{{ voice }}
</label>
{% endfor %}
</div>
</div>
<div class="option-group">
<div class="option-title">
<i class="fas fa-adjust"></i>
Tone Options
</div>
<div class="checkbox-group">
<label class="checkbox-option">
<input type="checkbox" name="tone" id="tone-option">
Cheerful Tone
</label>
</div>
</div>
</div>
<button id="process-btn" class="btn" disabled>
<i class="fas fa-magic"></i>
Generate Dubbed Video
</button>
<div id="duration-notice" class="duration-notice" style="display: none;">
<i class="fas fa-clock"></i>
Note: Processing time is approximately 1.5x the video duration
</div>
</div>
<div class="progress-container" id="progress-container">
<div class="progress-header">
<span>Processing your video</span>
<span id="eta">Estimating time...</span>
</div>
<div class="progress-bar">
<div class="progress-fill" id="progress-fill"></div>
</div>
<div class="progress-details" id="progress-message">
Preparing to process your video...
</div>
<div class="time-estimate">
<span id="elapsed-time">Elapsed: 0s</span>
<span id="remaining-time">Remaining: Calculating...</span>
</div>
</div>
<div class="result-container" id="result-container">
<h2>Your Dubbed Video</h2>
<video controls class="result-video" id="result-video">
Your browser does not support the video tag.
</video>
<h2>Generated Script</h2>
<div class="script-container" id="script-container"></div>
<div class="btn-container" style="margin-top: 1.5rem;">
<button id="new-video-btn" class="btn">
<i class="fas fa-sync-alt"></i>
Process Another Video
</button>
</div>
</div>
</div>
</div>
<script>
// DOM Elements
const uploadArea = document.getElementById('upload-area');
const fileInput = document.getElementById('video-input');
const fileInfo = document.getElementById('file-info');
const processBtn = document.getElementById('process-btn');
const uploadContainer = document.getElementById('upload-container');
const progressContainer = document.getElementById('progress-container');
const progressFill = document.getElementById('progress-fill');
const progressMessage = document.getElementById('progress-message');
const etaDisplay = document.getElementById('eta');
const resultContainer = document.getElementById('result-container');
const resultVideo = document.getElementById('result-video');
const scriptContainer = document.getElementById('script-container');
const elapsedTimeDisplay = document.getElementById('elapsed-time');
const remainingTimeDisplay = document.getElementById('remaining-time');
const durationNotice = document.getElementById('duration-notice');
const newVideoBtn = document.getElementById('new-video-btn');
// State
let currentTaskId = null;
let statusCheckInterval = null;
let processingStartTime = 0;
let videoDuration = 0;
let elapsedTimer = null;
// Event Listeners
fileInput.addEventListener('change', handleFileSelect);
uploadArea.addEventListener('dragover', handleDragOver);
uploadArea.addEventListener('dragleave', handleDragLeave);
uploadArea.addEventListener('drop', handleDrop);
processBtn.addEventListener('click', startProcessing);
newVideoBtn.addEventListener('click', resetForm);
// Functions
function handleFileSelect() {
if (this.files.length > 0) {
fileInfo.textContent = this.files[0].name;
uploadArea.style.borderColor = '#4361ee';
uploadArea.style.backgroundColor = 'rgba(67, 97, 238, 0.05)';
processBtn.disabled = false;
durationNotice.style.display = 'block';
// Hide any previous results
resultContainer.style.display = 'none';
}
}
function handleDragOver(e) {
e.preventDefault();
uploadArea.style.borderColor = '#4361ee';
uploadArea.style.backgroundColor = 'rgba(67, 97, 238, 0.1)';
}
function handleDragLeave() {
uploadArea.style.borderColor = fileInput.files.length > 0 ? '#4361ee' : '#4895ef';
uploadArea.style.backgroundColor = fileInput.files.length > 0
? 'rgba(67, 97, 238, 0.05)'
: 'transparent';
}
function handleDrop(e) {
e.preventDefault();
uploadArea.style.borderColor = '#4361ee';
uploadArea.style.backgroundColor = 'rgba(67, 97, 238, 0.05)';
if (e.dataTransfer.files.length) {
fileInput.files = e.dataTransfer.files;
fileInfo.textContent = e.dataTransfer.files[0].name;
processBtn.disabled = false;
durationNotice.style.display = 'block';
// Hide any previous results
resultContainer.style.display = 'none';
}
}
function startProcessing() {
if (!fileInput.files.length) {
alert('Please select a video file first.');
return;
}
// Get processing options
const voice = document.querySelector('input[name="voice"]:checked').value;
const cheerful = document.getElementById('tone-option').checked;
// Prepare form data
const formData = new FormData();
formData.append('video', fileInput.files[0]);
formData.append('voice', voice);
formData.append('cheerful', cheerful);
// Show progress UI
uploadContainer.style.display = 'none';
progressContainer.style.display = 'block';
progressFill.style.width = '0%';
progressMessage.textContent = 'Preparing to process your video...';
processingStartTime = Date.now();
// Start elapsed timer
startElapsedTimer();
// Start processing
fetch('/upload', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.error) {
throw new Error(data.error);
}
currentTaskId = data.task_id;
videoDuration = data.video_duration || 0;
startStatusChecking();
})
.catch(error => {
showError(error.message);
});
}
function startStatusChecking() {
// Clear any existing interval
if (statusCheckInterval) {
clearInterval(statusCheckInterval);
}
// Check status every 3 seconds
statusCheckInterval = setInterval(() => {
fetch(`/status/${currentTaskId}`)
.then(response => response.json())
.then(updateStatus)
.catch(error => {
console.error('Status check failed:', error);
});
}, 3000);
}
function startElapsedTimer() {
if (elapsedTimer) {
clearInterval(elapsedTimer);
}
elapsedTimer = setInterval(() => {
const elapsedSeconds = Math.floor((Date.now() - processingStartTime) / 1000);
elapsedTimeDisplay.textContent = `Elapsed: ${formatTime(elapsedSeconds)}`;
}, 1000);
}
function formatTime(seconds) {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
}
function updateStatus(status) {
if (status.error) {
showError(status.error);
clearInterval(statusCheckInterval);
return;
}
// Update progress
progressFill.style.width = `${status.progress}%`;
progressMessage.textContent = status.message;
// Update ETA if available
if (status.eta) {
etaDisplay.textContent = `ETA: ${status.eta}`;
remainingTimeDisplay.textContent = `Remaining: ${status.eta}`;
}
// Handle completion
if (status.status === 'complete') {
clearInterval(statusCheckInterval);
clearInterval(elapsedTimer);
progressMessage.textContent = 'Processing complete!';
// Show results
resultVideo.src = status.result_url;
scriptContainer.textContent = status.script;
progressContainer.style.display = 'none';
resultContainer.style.display = 'block';
} else if (status.status === 'error') {
showError(status.message);
clearInterval(statusCheckInterval);
clearInterval(elapsedTimer);
}
}
function showError(message) {
progressContainer.style.display = 'none';
uploadContainer.style.display = 'block';
alert(`Error: ${message}`);
clearInterval(elapsedTimer);
}
function resetForm() {
// Reset all elements
fileInput.value = '';
fileInfo.textContent = 'No file selected';
resultVideo.src = '';
scriptContainer.textContent = '';
resultContainer.style.display = 'none';
uploadContainer.style.display = 'block';
processBtn.disabled = true;
uploadArea.style.borderColor = '#4895ef';
uploadArea.style.backgroundColor = 'transparent';
durationNotice.style.display = 'none';
}
</script>
</body>
</html>