|
<!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> |
|
|
|
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'); |
|
|
|
|
|
let currentTaskId = null; |
|
let statusCheckInterval = null; |
|
let processingStartTime = 0; |
|
let videoDuration = 0; |
|
let elapsedTimer = null; |
|
|
|
|
|
fileInput.addEventListener('change', handleFileSelect); |
|
uploadArea.addEventListener('dragover', handleDragOver); |
|
uploadArea.addEventListener('dragleave', handleDragLeave); |
|
uploadArea.addEventListener('drop', handleDrop); |
|
processBtn.addEventListener('click', startProcessing); |
|
newVideoBtn.addEventListener('click', resetForm); |
|
|
|
|
|
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'; |
|
|
|
|
|
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'; |
|
|
|
|
|
resultContainer.style.display = 'none'; |
|
} |
|
} |
|
|
|
function startProcessing() { |
|
if (!fileInput.files.length) { |
|
alert('Please select a video file first.'); |
|
return; |
|
} |
|
|
|
|
|
const voice = document.querySelector('input[name="voice"]:checked').value; |
|
const cheerful = document.getElementById('tone-option').checked; |
|
|
|
|
|
const formData = new FormData(); |
|
formData.append('video', fileInput.files[0]); |
|
formData.append('voice', voice); |
|
formData.append('cheerful', cheerful); |
|
|
|
|
|
uploadContainer.style.display = 'none'; |
|
progressContainer.style.display = 'block'; |
|
progressFill.style.width = '0%'; |
|
progressMessage.textContent = 'Preparing to process your video...'; |
|
processingStartTime = Date.now(); |
|
|
|
|
|
startElapsedTimer(); |
|
|
|
|
|
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() { |
|
|
|
if (statusCheckInterval) { |
|
clearInterval(statusCheckInterval); |
|
} |
|
|
|
|
|
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; |
|
} |
|
|
|
|
|
progressFill.style.width = `${status.progress}%`; |
|
progressMessage.textContent = status.message; |
|
|
|
|
|
if (status.eta) { |
|
etaDisplay.textContent = `ETA: ${status.eta}`; |
|
remainingTimeDisplay.textContent = `Remaining: ${status.eta}`; |
|
} |
|
|
|
|
|
if (status.status === 'complete') { |
|
clearInterval(statusCheckInterval); |
|
clearInterval(elapsedTimer); |
|
progressMessage.textContent = 'Processing complete!'; |
|
|
|
|
|
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() { |
|
|
|
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> |