Spaces:
Build error
Build error
| <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> | |