UI-VieNeu / templates /studio.html
HuuDatLego's picture
Upload folder using huggingface_hub
911c66e verified
<!DOCTYPE html>
<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} &nbsp;|&nbsp; <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>