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>Animation Studio | VieNeu AI</title> | |
| <!-- Tailwind CSS CDN --> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script> | |
| tailwind.config = { | |
| darkMode: 'class', | |
| theme: { | |
| extend: { | |
| colors: { | |
| primary: '#8b5cf6', | |
| secondary: '#ec4899', | |
| dark: '#0f172a', | |
| card: 'rgba(30, 41, 59, 0.7)' | |
| } | |
| } | |
| } | |
| } | |
| </script> | |
| <link rel="stylesheet" href="/static/styles.css"> | |
| <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 font-inter"> | |
| <!-- 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-7xl 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-orange-500 to-rose-600 flex items-center justify-center shadow-lg shadow-rose-500/20"> | |
| <i data-feather="film" 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"> | |
| Animation Studio <span class="font-normal text-sm text-rose-400 ml-1">Beta</span> | |
| </h1> | |
| </div> | |
| <nav class="flex gap-4 text-sm font-medium"> | |
| <a href="/" class="text-slate-400 hover:text-white transition-colors">Video AI</a> | |
| <a href="/tts" class="text-slate-400 hover:text-white transition-colors">Text to Speech</a> | |
| <a href="/studio" class="text-white border-b-2 border-primary pb-1">Animation Studio</a> | |
| </nav> | |
| </div> | |
| </header> | |
| <main class="max-w-7xl mx-auto px-4 py-8 grid grid-cols-1 lg:grid-cols-12 gap-6"> | |
| <!-- CỘT 1: NHẬP KỊCH BẢN (4 Cols) --> | |
| <div class="lg:col-span-4 space-y-6"> | |
| <div class="glass-panel rounded-2xl p-5 shadow-xl"> | |
| <h2 class="text-md font-semibold mb-3 text-white flex items-center gap-2"> | |
| <i data-feather="edit-3" class="w-4 h-4 text-primary"></i> Đạo diễn Phân cảnh | |
| </h2> | |
| <div class="mb-3 text-xs text-slate-400 bg-slate-800/50 p-3 rounded-lg border border-slate-700"> | |
| 💡 <b>Cách dùng:</b> <code>[v:tên_giọng]</code> để đổi giọng, <code>[s:1.2]</code> để chỉnh tốc độ, <code>[p:500]</code> để nghỉ. <br> | |
| <i>VD: [Chào] [v:static/voice/ngoc-oanh.mp3] Hello, [p:500] [v:thien-tam] [s:1.2] Chào bạn!</i> | |
| </div> | |
| <textarea id="scriptText" rows="10" placeholder="[Chào mừng] Chào các bạn đã quay trở lại kênh..." | |
| class="w-full bg-slate-900/50 border border-slate-700 text-slate-200 rounded-xl p-3 focus:ring-2 focus:ring-primary focus:border-transparent outline-none transition-all placeholder:text-slate-600 resize-none font-medium text-sm leading-relaxed mb-4"></textarea> | |
| <div class="space-y-4"> | |
| <div> | |
| <label class="block text-xs font-medium text-slate-400 mb-1">Giọng đọc (Dùng TTS)</label> | |
| <select id="voicePreset" class="w-full bg-slate-900 border border-slate-700 text-sm rounded-lg px-3 py-2 text-slate-200"> | |
| <option value="bich_ngoc">Bích Ngọc (Nữ)</option> | |
| <option value="pham_tuyen">Phạm Tuyên (Nam)</option> | |
| <option value="static/voice/giong-google.mp3">Giọng Google</option> | |
| <option value="static/voice/thien-tam.mp3">Thiện Tâm (Nam - Giọng trầm)</option> | |
| <option value="static/voice/nguyet-nga.mp3">Nguyệt Nga (Review phim)</option> | |
| <option value="static/voice/ngoc-oanh.mp3">Ngọc Oanh (Kể chuyện)</option> | |
| <option value="static/voice/giong-nu-ke-chuyen.mp3">Giọng Nữ Kể chuyện (Mới)</option> | |
| <option value="static/voice/giong-nu-review.mp3">Giọng Nữ Review SP (Mới)</option> | |
| <option value="static/voice/giong-nu-tiktok-hay.mp3">Giọng nữ TikTok hay (Mới)</option> | |
| <option value="static/voice/male1.mp3">Giọng phóng viên</option> | |
| <!-- Note: Vì đây là Studio thu gọn, ta tạm dùng giọng có sẵn cho nhanh, hoặc có thể mở rộng add giọng clone sau --> | |
| </select> | |
| </div> | |
| <!-- Temperature Slider --> | |
| <div class="pt-2"> | |
| <div class="flex justify-between items-center mb-2"> | |
| <label class="text-xs font-medium text-slate-400 flex items-center gap-1"> | |
| <i data-feather="thermometer" class="w-3 h-3 text-rose-400"></i> Nhiệt độ (Temperature) | |
| </label> | |
| <span id="tempValue" class="text-[10px] font-mono bg-slate-800 px-1.5 py-0.5 rounded text-rose-400 border border-slate-700">0.5</span> | |
| </div> | |
| <input type="range" id="temperature" min="0.1" max="1.5" step="0.1" value="0.5" | |
| class="w-full h-1.5 bg-slate-700 rounded-lg appearance-none cursor-pointer accent-rose-500"> | |
| <p class="text-[10px] text-slate-500 mt-1">Thấp = Ít lỗi | Cao = Biểu cảm hơn.</p> | |
| </div> | |
| <!-- BGM Settings --> | |
| <div class="pt-4 border-t border-slate-700/50"> | |
| <label class="text-xs font-medium text-slate-400 mb-2 block flex items-center gap-1"> | |
| <i data-feather="music" class="w-3 h-3 text-emerald-400"></i> Nhạc nền (BGM) | |
| </label> | |
| <select id="bgmPreset" class="w-full bg-slate-900 border border-slate-700 text-[10px] rounded-lg px-2 py-1 text-slate-200 mb-2"> | |
| <option value="">-- Chọn nhạc mặc định --</option> | |
| <option value="static/music/Cheel - Blue Dream.mp3">Cheel - Blue Dream</option> | |
| <option value="static/music/Soft Feeling - Cheel.mp3">Soft Feeling - Cheel</option> | |
| <option value="static/music/Sunset Dream - Cheel.mp3">Sunset Dream - Cheel</option> | |
| <option value="static/music/Morning Mandolin - Chris Haugen.mp3">Morning Mandolin</option> | |
| <option value="static/music/Kiss the Sky - Aakash Gandhi.mp3">Kiss the Sky</option> | |
| </select> | |
| <div class="text-[9px] text-slate-500 mb-1">Hoặc tải file lên:</div> | |
| <input type="file" id="bgmAudio" accept="audio/*" class="text-[10px] text-slate-400 mb-2 block w-full | |
| file:mr-2 file:py-1 file:px-2 file:rounded-md file:border-0 | |
| file:text-[10px] file:font-semibold file:bg-slate-800 file:text-emerald-400 | |
| hover:file:bg-slate-700 cursor-pointer"> | |
| <div class="flex justify-between items-center mb-1"> | |
| <span class="text-[10px] text-slate-500">Âm lượng nhạc</span> | |
| <span id="bgmVolValue" class="text-[10px] font-mono text-emerald-400">0.1</span> | |
| </div> | |
| <input type="range" id="bgmVolume" min="0" max="0.5" step="0.01" value="0.1" | |
| class="w-full h-1 bg-slate-700 rounded-lg appearance-none cursor-pointer accent-emerald-500"> | |
| </div> | |
| </div> | |
| <button id="submitBtn" class="w-full mt-6 bg-gradient-to-r from-rose-500 to-orange-500 text-white font-bold py-3 px-4 rounded-xl shadow-lg hover:shadow-orange-500/30 transition-all flex items-center justify-center gap-2"> | |
| <i data-feather="play-circle" class="w-5 h-5"></i> Bắt đầu Diễn xuất | |
| </button> | |
| </div> | |
| </div> | |
| <!-- CỘT 2: SÂN KHẤU (5 Cols) --> | |
| <div class="lg:col-span-5"> | |
| <div class="glass-panel flex flex-col rounded-2xl h-[550px] shadow-xl sticky top-24 overflow-hidden border border-slate-700/50"> | |
| <div class="p-4 bg-slate-800/50 border-b border-slate-700 flex items-center justify-between"> | |
| <h3 class="font-semibold text-white flex items-center gap-2 text-sm"> | |
| <i data-feather="monitor" class="w-4 h-4 text-rose-400"></i> Sân khấu Preview | |
| </h3> | |
| <span id="currentTagDisplay" class="text-xs bg-slate-900 px-2 py-1 rounded text-primary font-mono hidden">Tag: init</span> | |
| </div> | |
| <div class="flex-1 bg-slate-900/80 relative flex items-center justify-center p-4"> | |
| <!-- Ảnh Nhân vật (Dùng để preview từ Kho biểu cảm) --> | |
| <img id="characterStage" src="/static/placeholder.png" alt="Character" class="max-h-[350px] object-contain transition-opacity duration-300"> | |
| <!-- Trình phát Video (Hiện khi render xong) --> | |
| <video id="videoPlayer" controls class="w-full h-full object-contain hidden" style="max-height: 450px;"></video> | |
| <!-- Loading Overlay --> | |
| <div id="loadingOverlay" class="absolute inset-0 bg-slate-900/80 backdrop-blur-sm z-10 hidden flex-col items-center justify-center"> | |
| <div class="spinner w-8 h-8 border-[3px] border-t-rose-400 mb-3 rounded-full animate-spin"></div> | |
| <p id="loadingText" class="text-rose-400 font-medium text-sm animate-pulse">Đang chuẩn bị...</p> | |
| <p id="progressDisplay" class="text-slate-300 text-xs mt-2 hidden bg-slate-800 px-3 py-1.5 rounded-full border border-slate-700"></p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- CỘT 3: KHO BIỂU CẢM (3 Cols) --> | |
| <div class="lg:col-span-3 space-y-4"> | |
| <div class="glass-panel rounded-2xl p-4 shadow-xl flex flex-col h-[550px]"> | |
| <h3 class="font-semibold text-white flex items-center gap-2 text-sm border-b border-slate-700 pb-3 mb-3"> | |
| <i data-feather="smile" class="w-4 h-4 text-orange-400"></i> Kho Biểu Cảm | |
| </h3> | |
| <!-- Search Box --> | |
| <div class="bg-slate-800/50 p-3 rounded-xl border border-slate-700 mb-4"> | |
| <input type="text" id="searchInput" placeholder="Tìm biểu cảm..." class="w-full bg-slate-900 border border-slate-700 text-xs rounded-lg px-3 py-2 text-white outline-none focus:ring-1 focus:ring-primary"> | |
| </div> | |
| <!-- Danh sách Cảm xúc --> | |
| <div id="expressionGallery" class="flex-1 overflow-y-auto custom-scrollbar pr-2 grid grid-cols-2 gap-2 content-start"> | |
| <!-- JS sẽ render image ở đây --> | |
| </div> | |
| </div> | |
| </div> | |
| </main> | |
| <style> | |
| .custom-scrollbar::-webkit-scrollbar { width: 4px; } | |
| .custom-scrollbar::-webkit-scrollbar-thumb { background: #475569; border-radius: 10px; } | |
| </style> | |
| <script> | |
| feather.replace(); | |
| // --------------------------------------------------------- | |
| // KHO BỘ LỌC VÀ BIỂU CẢM (TỪ STATIC FOLDER) | |
| // --------------------------------------------------------- | |
| let expressions = {}; // Sẽ load từ JSON | |
| const exprGallery = document.getElementById('expressionGallery'); | |
| const characterStage = document.getElementById('characterStage'); | |
| const searchInput = document.getElementById('searchInput'); | |
| // Hàm chuẩn hóa tiếng việt (bỏ dấu, bỏ ký tự đặc biệt) | |
| function normalizeTag(text) { | |
| return text.toString().toLowerCase() | |
| .normalize("NFD") | |
| .replace(/[\u0300-\u036f]/g, "") // bỏ dấu | |
| .replace(/[đĐ]/g, "d") | |
| .replace(/[^\w\s]+/g, '') // bỏ ký tự đặc biệt | |
| .replace(/\s+/g, ' ') // thu gọn khoảng trắng | |
| .trim(); | |
| } | |
| async function loadExpressions() { | |
| try { | |
| // Tải mapping.json | |
| const res = await fetch('/static/characters/mapping.json'); | |
| const mapping = await res.json(); | |
| // Map thành URL thực tế | |
| for (const [key, filename] of Object.entries(mapping)) { | |
| expressions[key] = `/static/characters/${filename}`; | |
| } | |
| renderGallery(); | |
| } catch (err) { | |
| console.error("Lỗi tải mapping:", err); | |
| } | |
| } | |
| function renderGallery(filter = '') { | |
| exprGallery.innerHTML = ''; | |
| const normalizedFilter = normalizeTag(filter); | |
| for (const [tag, url] of Object.entries(expressions)) { | |
| if (normalizedFilter && !normalizeTag(tag).includes(normalizedFilter)) { | |
| continue; // Skip nếu không khớp filter | |
| } | |
| exprGallery.innerHTML += ` | |
| <div class="relative group bg-slate-800 rounded-lg p-2 border border-slate-700 flex flex-col items-center cursor-pointer hover:bg-slate-700 transition" onclick="updateCharacterImage('${tag}')"> | |
| <img src="${url}" class="w-12 h-12 object-cover rounded mb-1"> | |
| <span class="text-[10px] bg-slate-900 px-2 py-0.5 rounded text-slate-300 truncate w-full text-center" title="[${tag}]"> | |
| [${tag}] | |
| </span> | |
| </div> | |
| `; | |
| } | |
| feather.replace(); | |
| // Default stage img | |
| const keys = Object.keys(expressions); | |
| if(keys.length > 0 && characterStage.getAttribute('src').includes('placeholder.png')) { | |
| characterStage.src = expressions["binh thuong"] || expressions[keys[0]]; | |
| } | |
| } | |
| if (searchInput) { | |
| searchInput.addEventListener('input', (e) => { | |
| renderGallery(e.target.value); | |
| }); | |
| } | |
| loadExpressions(); | |
| // --------------------------------------------------------- | |
| // LOGIC LÀM ANIMATION & ĐỒNG BỘ | |
| // --------------------------------------------------------- | |
| const tempSlider = document.getElementById('temperature'); | |
| const tempValue = document.getElementById('tempValue'); | |
| if (tempSlider && tempValue) { | |
| tempSlider.addEventListener('input', (e) => { | |
| tempValue.textContent = parseFloat(e.target.value).toFixed(1); | |
| }); | |
| } | |
| const bgmVolSlider = document.getElementById('bgmVolume'); | |
| const bgmVolValue = document.getElementById('bgmVolValue'); | |
| if (bgmVolSlider && bgmVolValue) { | |
| bgmVolSlider.addEventListener('input', (e) => { | |
| bgmVolValue.textContent = e.target.value; | |
| }); | |
| } | |
| const videoPlayer = document.getElementById('videoPlayer'); | |
| const submitBtn = document.getElementById('submitBtn'); | |
| const scriptText = document.getElementById('scriptText'); | |
| const loadingOverlay = document.getElementById('loadingOverlay'); | |
| const currentTagDisplay = document.getElementById('currentTagDisplay'); | |
| const loadingText = document.getElementById('loadingText'); | |
| const progressDisplay = document.getElementById('progressDisplay'); | |
| submitBtn.addEventListener('click', async () => { | |
| const rawText = scriptText.value; | |
| if (!rawText) return; | |
| // Gọi API Studio | |
| const formData = new FormData(); | |
| formData.append('script', rawText); | |
| formData.append('temperature', parseFloat(tempSlider.value)); | |
| formData.append('voice_preset', document.getElementById('voicePreset').value); | |
| const bgmFile = document.getElementById('bgmAudio').files[0]; | |
| if (bgmFile) { | |
| formData.append('bgm_audio', bgmFile); | |
| } | |
| formData.append('bgm_volume', parseFloat(bgmVolSlider.value)); | |
| formData.append('bgm_preset', document.getElementById('bgmPreset').value); | |
| submitBtn.disabled = true; | |
| loadingOverlay.classList.remove('hidden'); | |
| loadingOverlay.classList.add('flex'); | |
| loadingText.textContent = 'Đang khởi tạo job...'; | |
| progressDisplay.classList.add('hidden'); | |
| progressDisplay.textContent = ''; | |
| try { | |
| const res = await fetch('/api/v1/studio/generate', { method: 'POST', body: formData }); | |
| const data = await res.json(); | |
| // Polling | |
| let poll = setInterval(async () => { | |
| const statusRes = await fetch(`/api/v1/jobs/${data.job_id}`); | |
| const jobData = await statusRes.json(); | |
| if (jobData.progress) { | |
| loadingText.textContent = 'Đang xử lý âm thanh AI...'; | |
| progressDisplay.classList.remove('hidden'); | |
| progressDisplay.innerHTML = `<span class="text-orange-400 font-semibold">Đã chạy:</span> ${jobData.progress.elapsed} | <span class="text-emerald-400 font-semibold">Còn lại:</span> ${jobData.progress.remaining}`; | |
| } else if (jobData.status === 'processing' && !jobData.progress) { | |
| loadingText.textContent = 'Đang đóng gói Video MP4...'; | |
| } | |
| if (jobData.status === 'completed') { | |
| clearInterval(poll); | |
| // Hiển thị video, ẩn ảnh stage | |
| characterStage.classList.add('hidden'); | |
| videoPlayer.classList.remove('hidden'); | |
| videoPlayer.src = jobData.result_url; | |
| videoPlayer.play(); | |
| loadingOverlay.classList.add('hidden'); | |
| submitBtn.disabled = false; | |
| } else if (jobData.status === 'failed' || jobData.status === 'error') { | |
| clearInterval(poll); | |
| alert("Lỗi tạo Video MP4!"); | |
| loadingOverlay.classList.add('hidden'); | |
| submitBtn.disabled = false; | |
| } | |
| }, 2000); | |
| } catch(e) { | |
| alert("Lỗi kết nối"); | |
| loadingOverlay.classList.add('hidden'); | |
| submitBtn.disabled = false; | |
| } | |
| }); | |
| function updateCharacterImage(tag) { | |
| currentTagDisplay.classList.remove('hidden'); | |
| currentTagDisplay.textContent = `[${tag}]`; | |
| const normTag = normalizeTag(tag); | |
| // Tìm hình trong thư viện, nếu có thì đổi | |
| if (expressions[normTag]) { | |
| characterStage.src = expressions[normTag]; | |
| } else { | |
| // Không có hình ứng với Tag thì giữ nguyên hình cũ | |
| // console.warn("Missing expression image for tag:", tag); | |
| } | |
| } | |
| </script> | |
| </body> | |
| </html> | |