UI-VieNeu / templates /index.html
HuuDatLego's picture
Upload folder using huggingface_hub
911c66e verified
Raw
History Blame Contribute Delete
18 kB
<!DOCTYPE html>
<html lang="vi" class="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>UI-VieNeu | AI Video Engine</title>
<!-- Tailwind CSS CDN -->
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
colors: {
primary: '#8b5cf6',
secondary: '#ec4899'
}
}
}
}
</script>
<!-- Custom Styles -->
<link rel="stylesheet" href="/static/styles.css">
<!-- Feather Icons -->
<script src="https://unpkg.com/feather-icons"></script>
</head>
<body class="text-slate-200 min-h-screen custom-bg selection:bg-primary selection:text-white">
<!-- Header -->
<header class="border-b border-slate-700/50 bg-slate-900/50 backdrop-blur-md sticky top-0 z-50">
<div class="max-w-6xl mx-auto px-6 py-4 flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-xl bg-gradient-to-tr from-violet-600 to-fuchsia-600 flex items-center justify-center shadow-lg shadow-primary/20">
<i data-feather="video" class="text-white w-5 h-5"></i>
</div>
<h1 class="text-xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-white to-slate-400">
UI-VieNeu <span class="font-normal text-sm text-primary ml-1">v1.0</span>
</h1>
</div>
<nav class="flex gap-4 text-sm font-medium">
<a href="/" class="text-white border-b-2 border-primary pb-1">Video AI</a>
<a href="/tts" class="text-slate-400 hover:text-white transition-colors">Text to Speech</a>
<a href="/studio" class="text-slate-400 hover:text-white transition-colors">Animation Studio</a>
</nav>
</div>
</header>
<!-- Main Content -->
<main class="max-w-6xl mx-auto px-6 py-10 grid grid-cols-1 lg:grid-cols-12 gap-8">
<!-- Left Column: Input Form -->
<div class="lg:col-span-5 space-y-6">
<div class="glass-panel rounded-2xl p-6 shadow-xl">
<h2 class="text-lg font-semibold mb-6 flex items-center gap-2 text-white">
<i data-feather="edit-3" class="w-5 h-5 text-primary"></i> Create New Video
</h2>
<form id="uploadForm" class="space-y-5">
<!-- Video Aspect Ratio -->
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-slate-300 mb-2">Tỷ lệ Video</label>
<select id="aspectRatio" class="w-full bg-slate-900 border border-slate-700 text-sm rounded-lg px-3 py-2.5 text-slate-200 focus:ring-2 focus:ring-primary focus:border-transparent outline-none transition-all">
<option value="9:16">Dọc (9:16) - TikTok/Reels</option>
<option value="16:9">Ngang (16:9) - YouTube</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-slate-300 mb-2">Kiểu Subtitle</label>
<select id="subStyle" class="w-full bg-slate-900 border border-slate-700 text-sm rounded-lg px-3 py-2.5 text-slate-200 focus:ring-2 focus:ring-primary focus:border-transparent outline-none transition-all">
<option value="karaoke">Chạy từng chữ (Karaoke)</option>
<option value="sentence">Hiện từng câu (Cơ bản)</option>
</select>
</div>
</div>
<!-- Subtitle Styling -->
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-slate-300 mb-2">Font chữ</label>
<select id="fontName" class="w-full bg-slate-900 border border-slate-700 text-sm rounded-lg px-3 py-2.5 text-slate-200 focus:ring-2 focus:ring-primary focus:border-transparent outline-none transition-all">
<option value="Arial">Arial (Mặc định)</option>
<option value="Montserrat">Montserrat</option>
<option value="Times New Roman">Times New Roman</option>
<option value="Be Vietnam Pro">Be Vietnam Pro</option>
</select>
<p class="text-[10px] text-slate-500 mt-1">Lưu ý: Bạn phải cài sẵn font này trên máy nều chạy local.</p>
</div>
<div>
<label class="block text-sm font-medium text-slate-300 mb-2">Màu chữ nổi bật</label>
<div class="flex items-center gap-2">
<input type="color" id="highlightColor" value="#00FDFF" class="w-10 h-10 p-0 border-0 rounded bg-slate-900 cursor-pointer">
<span class="text-sm text-slate-400">Chọn màu</span>
</div>
</div>
</div>
<!-- Voice Reference (Optional) -->
<div>
<label class="block text-sm font-medium text-slate-300 mb-2">Voice Reference Audio <span class="text-xs text-slate-500 font-normal">(Optional for cloning)</span></label>
<div class="relative group">
<input type="file" id="audioFile" accept="audio/*"
class="absolute inset-0 w-full h-full opacity-0 cursor-pointer z-10">
<div class="border border-slate-700 rounded-xl p-3 flex items-center gap-3 bg-slate-800/30 group-hover:bg-slate-800/80 transition-colors">
<i data-feather="mic" class="w-5 h-5 text-slate-500 group-hover:text-secondary transition-colors"></i>
<span id="audioFileName" class="text-sm text-slate-500">Select audio file...</span>
</div>
</div>
</div>
<!-- Script Input -->
<div>
<label class="block text-sm font-medium text-slate-300 mb-2">Voiceover Script & Subtitles</label>
<textarea id="scriptText" rows="6" required placeholder="Xin chào các bạn, hôm nay mình sẽ hướng dẫn..."
class="w-full bg-slate-900/50 border border-slate-700 text-slate-200 rounded-xl p-4 focus:ring-2 focus:ring-primary focus:border-transparent outline-none transition-all placeholder:text-slate-600 resize-none font-medium"></textarea>
</div>
<!-- Submit Button -->
<button type="submit" id="submitBtn" class="w-full relative overflow-hidden group bg-green-500 text-white font-semibold py-3.5 px-4 rounded-xl transition-all hover:scale-[1.02] hover:shadow-lg hover:shadow-green-500/20 flex items-center justify-center gap-2">
<span class="z-10 flex items-center gap-2">
<i data-feather="zap" class="w-5 h-5"></i> Generate Green Screen Subs
</span>
<div class="absolute inset-0 bg-gradient-to-r from-green-400 to-green-600 opacity-0 group-hover:opacity-100 transition-opacity"></div>
</button>
<p id="formError" class="text-red-400 text-sm font-medium hidden mt-2 text-center"></p>
</form>
</div>
</div>
<!-- Right Column: Result & Tracker -->
<div class="lg:col-span-7">
<div class="glass-panel rounded-2xl h-full shadow-xl flex flex-col overflow-hidden relative">
<!-- Status Bar -->
<div class="bg-slate-800/80 p-4 border-b border-slate-700 flex flex-wrap items-center justify-between gap-4">
<div class="flex items-center gap-3">
<div id="statusDot" class="w-3 h-3 rounded-full bg-slate-600"></div>
<span id="statusText" class="text-sm font-medium text-slate-300">Idle - Ready to render</span>
</div>
<div id="jobIdContainer" class="hidden">
<span class="text-xs font-mono text-slate-500 bg-slate-900 px-2 py-1 rounded-md border border-slate-700" id="jobIdLabel"></span>
</div>
</div>
<!-- Preview Area -->
<div class="flex-1 min-h-[400px] flex items-center justify-center p-6 bg-slate-900/30 relative">
<!-- Empty State -->
<div id="emptyState" class="text-center text-slate-500">
<div class="w-20 h-20 mx-auto bg-slate-800 rounded-full flex items-center justify-center mb-4 border border-slate-700">
<i data-feather="film" class="w-8 h-8 text-slate-600"></i>
</div>
<p class="font-medium">Your generated video will appear here</p>
</div>
<!-- Loading State -->
<div id="loadingState" class="hidden flex-col items-center justify-center absolute inset-0 bg-slate-900/80 backdrop-blur-sm z-10 transition-all duration-300">
<div class="spinner mb-4 border-t-primary"></div>
<h3 class="font-semibold text-lg text-white mb-1">Rendering in progress...</h3>
<p class="text-sm text-slate-400">This might take a while depending on server capacity.</p>
</div>
<!-- Result Output -->
<div id="resultState" class="hidden w-full h-full flex flex-col items-center justify-center">
<video id="resultVideo" controls class="max-w-full max-h-[500px] rounded-lg shadow-2xl border border-slate-700 bg-black"></video>
<div class="mt-6 flex gap-4">
<a id="downloadBtn" href="#" target="_blank" class="px-5 py-2 rounded-lg bg-primary/10 text-primary border border-primary/30 hover:bg-primary/20 transition-colors font-medium flex items-center gap-2">
<i data-feather="download" class="w-4 h-4"></i> Download Result
</a>
</div>
</div>
</div>
</div>
</div>
</main>
<script>
// Khởi tạo icons
feather.replace();
document.getElementById('audioFile').addEventListener('change', function(e) {
const fileName = e.target.files[0] ? e.target.files[0].name : 'Select audio file...';
document.getElementById('audioFileName').textContent = fileName;
});
// Xử lý Submit Form
const form = document.getElementById('uploadForm');
let pollInterval;
form.addEventListener('submit', async (e) => {
e.preventDefault();
const submitBtn = document.getElementById('submitBtn');
const errorTxt = document.getElementById('formError');
errorTxt.classList.add('hidden');
// Lấy dữ liệu
const audioFile = document.getElementById('audioFile').files[0];
const scriptText = document.getElementById('scriptText').value;
const aspectRatio = document.getElementById('aspectRatio').value;
const subStyle = document.getElementById('subStyle').value;
const fontName = document.getElementById('fontName').value;
const highlightColor = document.getElementById('highlightColor').value;
// Đóng gói FormData
const formData = new FormData();
formData.append('script', scriptText);
formData.append('aspect_ratio', aspectRatio);
formData.append('sub_style', subStyle);
formData.append('font_name', fontName);
formData.append('highlight_color', highlightColor);
if (audioFile) formData.append('ref_audio', audioFile);
// Cập nhật UI loading Submit
submitBtn.disabled = true;
submitBtn.innerHTML = '<div class="spinner w-5 h-5 border-2 border-slate-900 border-t-transparent"></div> Uploading...';
// Ẩn states & Bật loading rendering
document.getElementById('emptyState').classList.add('hidden');
document.getElementById('resultState').classList.add('hidden');
document.getElementById('loadingState').classList.remove('hidden');
updateBadge('warning', 'Uploading files to server...');
try {
// Đẩy lên API
const response = await fetch('/api/v1/jobs/submit', {
method: 'POST',
body: formData
});
if (!response.ok) {
throw new Error(`Upload failed! Check your Supabase configuration or try again (Error ${response.status})`);
}
const data = await response.json();
const jobId = data.job_id;
// Hiển thị Job ID
document.getElementById('jobIdContainer').classList.remove('hidden');
document.getElementById('jobIdLabel').textContent = `ID: ${jobId}`;
updateBadge('processing', 'In Queue - Polling status...');
// Bắt đầu pollling check status định kỳ mỗi 3 giây
pollInterval = setInterval(() => checkJobStatus(jobId), 3000);
} catch (err) {
console.error(err);
errorTxt.textContent = err.message;
errorTxt.classList.remove('hidden');
document.getElementById('loadingState').classList.add('hidden');
document.getElementById('emptyState').classList.remove('hidden');
updateBadge('error', 'Error occurred');
} finally {
// Khôi phục nút
submitBtn.disabled = false;
submitBtn.innerHTML = `
<span class="z-10 flex items-center gap-2">
<i data-feather="zap" class="w-5 h-5"></i> Generate Another
</span>
<div class="absolute inset-0 bg-gradient-to-r from-white to-slate-200 opacity-0 group-hover:opacity-100 transition-opacity"></div>
`;
feather.replace();
}
});
// Hàm kiểm tra trạng thái tiến trình
async function checkJobStatus(jobId) {
try {
const res = await fetch(`/api/v1/jobs/${jobId}`);
if (!res.ok) return;
const jobData = await res.json();
if (jobData.status === 'processing') {
updateBadge('processing', 'Rendering Engine Running...');
}
else if (jobData.status === 'completed') {
clearInterval(pollInterval);
updateBadge('success', 'Completed Successfully!');
showResult(jobData.result_url);
}
else if (jobData.status === 'failed' || jobData.status === 'error') {
clearInterval(pollInterval);
updateBadge('error', `Failed: ${jobData.error || 'Unknown rendering error'}`);
document.getElementById('loadingState').classList.add('hidden');
document.getElementById('emptyState').classList.remove('hidden');
alert("Render Task Failed! Check Celery Worker logs.");
}
} catch (e) {
console.error("Polling error", e);
}
}
// Cập nhật thẻ trạng thái (Trắng, Vàng, Xanh, Đỏ)
function updateBadge(state, text) {
const dot = document.getElementById('statusDot');
const txt = document.getElementById('statusText');
txt.textContent = text;
// Reset class
dot.className = 'w-3 h-3 rounded-full';
if (state === 'idle') dot.classList.add('bg-slate-600');
if (state === 'warning') dot.classList.add('bg-yellow-500', 'animate-pulse');
if (state === 'processing') dot.classList.add('bg-blue-500', 'animate-pulse', 'shadow-[0_0_10px_rgba(59,130,246,0.6)]');
if (state === 'success') dot.classList.add('bg-green-500', 'shadow-[0_0_10px_rgba(34,197,94,0.6)]');
if (state === 'error') dot.classList.add('bg-red-500');
}
// Hiện kết quả
function showResult(videoUrl) {
document.getElementById('loadingState').classList.add('hidden');
document.getElementById('resultState').classList.remove('hidden');
const vid = document.getElementById('resultVideo');
vid.src = videoUrl;
vid.load();
const btn = document.getElementById('downloadBtn');
btn.href = videoUrl;
}
</script>
</body>
</html>