UI-VieNeu / templates /tts.html
HuuDatLego's picture
Upload folder using huggingface_hub
911c66e verified
Raw
History Blame Contribute Delete
26.2 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>Voice 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-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-indigo-500 to-purple-600 flex items-center justify-center shadow-lg shadow-primary/20">
<i data-feather="mic" 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">
Voice Studio <span class="font-normal text-sm text-primary ml-1">v2-Turbo</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-white border-b-2 border-primary pb-1">Text to Speech</a>
<a href="/studio" class="text-slate-400 hover:text-white transition-colors">Animation Studio</a>
</nav>
</div>
</header>
<main class="max-w-6xl mx-auto px-6 py-10 grid grid-cols-1 lg:grid-cols-12 gap-8">
<!-- Left Column: Controls (8 cols) -->
<div class="lg:col-span-8 space-y-6">
<!-- Text Input -->
<div class="glass-panel rounded-2xl p-6 shadow-xl">
<h2 class="text-lg font-semibold mb-4 text-white flex items-center gap-2">
<i data-feather="file-text" class="w-5 h-5 text-primary"></i> Nội dung kịch bản
</h2>
<textarea id="scriptText" rows="6" placeholder="Nhập kịch bản... Mẹo: [v:static/voice/ngoc-oanh.mp3] đổi giọng, [s:1.2] chỉnh tốc độ, [p:500] nghỉ."
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 text-lg leading-relaxed"></textarea>
</div>
<!-- Voice Settings -->
<div class="glass-panel rounded-2xl p-6 shadow-xl">
<!-- Custom Tabs -->
<div class="flex border-b border-slate-700 mb-6">
<button id="tabPreset" class="px-6 py-3 text-sm font-semibold text-primary border-b-2 border-primary flex gap-2 items-center transition-colors">
<i data-feather="users" class="w-4 h-4"></i> Giọng có sẵn
</button>
<button id="tabClone" class="px-6 py-3 text-sm font-semibold text-slate-400 hover:text-slate-200 flex gap-2 items-center transition-colors">
<i data-feather="copy" class="w-4 h-4"></i> Clone Giọng
</button>
</div>
<div id="presetContainer" class="space-y-6">
<!-- Dropdown Select -->
<div>
<label class="block text-sm font-medium text-slate-300 mb-2">Chọn nhân vật</label>
<div class="relative">
<select id="voicePreset" class="w-full appearance-none bg-slate-900/50 border border-slate-700 text-slate-200 rounded-xl px-4 py-3 focus:ring-2 focus:ring-primary outline-none cursor-pointer font-medium">
<option value="bich_ngoc">Bích Ngọc (Nữ - Miền Bắc)</option>
<option value="pham_tuyen">Phạm Tuyên (Nam - Miền Bắc)</option>
<option value="thuc_doan">Thục Đoan (Nữ - Miền Nam)</option>
<option value="xuan_vinh">Xuân Vĩnh (Nam - Miề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>
</select>
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-4 text-slate-400">
<i data-feather="chevron-down" class="w-4 h-4"></i>
</div>
</div>
</div>
</div>
<div id="cloneContainer" class="hidden space-y-6">
<!-- Character Name Input -->
<div>
<label class="block text-sm font-medium text-slate-300 mb-2">Tên nhân vật (Gợi nhớ)</label>
<input type="text" id="characterName" placeholder="Ví dụ: Giọng Sếp, Giọng Elon Musk..."
class="w-full bg-slate-900/50 border border-slate-700 text-slate-200 rounded-xl px-4 py-3 focus:ring-2 focus:ring-primary outline-none transition-all placeholder:text-slate-600">
</div>
<div>
<label class="block text-sm font-medium text-slate-300 mb-2">Upload file âm thanh giọng mẫu</label>
<div class="relative group">
<input type="file" id="cloneAudio" accept="audio/*" class="absolute inset-0 w-full h-full opacity-0 cursor-pointer z-10">
<div class="border-2 border-dashed border-slate-600 rounded-xl p-6 flex flex-col items-center justify-center gap-3 bg-slate-800/50 group-hover:bg-slate-800 transition-colors group-hover:border-primary/50">
<i data-feather="upload-cloud" class="w-8 h-8 text-slate-400 group-hover:text-primary transition-colors"></i>
<span id="cloneFileName" class="text-sm font-medium text-slate-400">Trích xuất giọng từ file (.mp3, .wav)</span>
</div>
</div>
</div>
<!-- Voice Library Section -->
<div id="librarySection" class="hidden mt-6 pt-6 border-t border-slate-700/50">
<h3 class="text-sm font-semibold text-slate-400 mb-3 flex items-center gap-2">
<i data-feather="bookmark" class="w-4 h-4"></i> Thư viện giọng của riêng bạn (Lịch sử)
</h3>
<div id="voiceList" class="grid grid-cols-1 sm:grid-cols-2 gap-3 max-h-48 overflow-y-auto pr-2 custom-scrollbar">
<!-- Items populated by JS -->
</div>
</div>
</div>
<!-- Advanced Settings: Temperature Slider -->
<div class="mt-8 pt-6 border-t border-slate-700/50">
<div class="flex justify-between items-center mb-2">
<label class="text-sm font-medium text-slate-300 flex items-center gap-2">
<i data-feather="thermometer" class="w-4 h-4 text-secondary"></i> Nhiệt độ (Temperature)
</label>
<span id="tempValue" class="text-xs font-mono bg-slate-800 px-2 py-1 rounded text-primary">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-2 bg-slate-700 rounded-lg appearance-none cursor-pointer accent-primary">
<p class="text-xs text-slate-500 mt-2">Thấp = Khô khan chuẩn xác | Cao = Cảm xúc sáng tạo hơn.</p>
</div>
<!-- BGM Settings -->
<div class="mt-6 pt-6 border-t border-slate-700/50">
<label class="text-sm font-medium text-slate-300 flex items-center gap-2 mb-4">
<i data-feather="music" class="w-4 h-4 text-emerald-400"></i> Nhạc nền (BGM)
</label>
<div class="relative mb-4">
<select id="bgmPreset" class="w-full appearance-none bg-slate-900/50 border border-slate-700 text-slate-200 rounded-xl px-4 py-3 focus:ring-2 focus:ring-emerald-500 outline-none cursor-pointer font-medium">
<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="pointer-events-none absolute inset-y-0 right-0 flex items-center px-4 text-slate-400">
<i data-feather="chevron-down" class="w-4 h-4"></i>
</div>
</div>
<div class="text-xs text-slate-500 mb-2">Hoặc tải file lên:</div>
<input type="file" id="bgmAudio" accept="audio/*" class="text-xs text-slate-400 mb-4 block w-full
file:mr-4 file:py-2 file:px-4 file:rounded-xl file:border-0
file:text-sm 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-2">
<span class="text-xs text-slate-400">Âm lượng nhạc nền</span>
<span id="bgmVolValue" class="text-xs font-mono text-emerald-400 bg-slate-800 px-2 py-1 rounded">0.1</span>
</div>
<input type="range" id="bgmVolume" min="0" max="0.5" step="0.01" value="0.1"
class="w-full h-2 bg-slate-700 rounded-lg appearance-none cursor-pointer accent-emerald-500">
</div>
</div>
</div>
<!-- Submit Button -->
<button id="submitBtn" class="w-full relative overflow-hidden group bg-gradient-to-r from-primary to-blue-600 text-white font-bold py-4 px-4 rounded-xl shadow-[0_0_20px_rgba(139,92,246,0.3)] hover:shadow-[0_0_30px_rgba(139,92,246,0.5)] transition-all flex items-center justify-center gap-2">
<span class="z-10 flex items-center gap-2 text-lg">
<i data-feather="zap" class="w-5 h-5"></i> Bắt đầu tổng hợp Audio
</span>
</button>
</div>
<!-- Right Column: Result (4 cols) -->
<div class="lg:col-span-4">
<div class="glass-panel flex flex-col rounded-2xl h-full shadow-xl sticky top-24">
<div class="p-6 border-b border-slate-700 flex items-center gap-2">
<i data-feather="headphones" class="w-5 h-5 text-secondary"></i>
<h3 class="font-semibold text-white">Audio Kết Quả</h3>
</div>
<div class="flex-1 p-6 flex flex-col items-center justify-center min-h-[300px] relative">
<!-- Empty State -->
<div id="emptyResult" class="text-center">
<i data-feather="music" class="w-12 h-12 text-slate-700 mx-auto mb-4"></i>
<p class="text-slate-500 text-sm font-medium">Bấm "Bắt đầu" để tạo âm thanh</p>
</div>
<!-- Loading State -->
<div id="loadingResult" class="hidden flex-col items-center">
<div class="spinner w-8 h-8 border-[3px] border-t-primary mb-4"></div>
<p class="text-primary font-medium animate-pulse">Đang tổng hợp...</p>
<p class="text-xs text-slate-500 mt-1">Sẽ mất vài giây để xử lý</p>
</div>
<!-- Audio Player Area -->
<div id="audioResult" class="hidden w-full flex flex-col items-center space-y-6">
<div class="w-20 h-20 bg-gradient-to-tr from-primary to-secondary rounded-full flex items-center justify-center shadow-[0_0_40px_rgba(236,72,153,0.3)] beep-anim">
<i data-feather="play" class="w-8 h-8 text-white ml-1"></i>
</div>
<audio id="player" controls class="w-full"></audio>
<a id="downloadBtn" href="#" download="vieneu_output.wav" class="text-sm font-medium text-slate-300 hover:text-white flex items-center gap-1 bg-slate-800 py-2 px-4 rounded-lg border border-slate-700 hover:border-slate-500 transition-colors">
<i data-feather="download" class="w-4 h-4"></i> Tải file .wav
</a>
</div>
</div>
</div>
</div>
</main>
<style>
.beep-anim {
animation: pulse-glow 2s infinite;
}
@keyframes pulse-glow {
0% { box-shadow: 0 0 0 0 rgba(236,72,153, 0.4); }
70% { box-shadow: 0 0 0 20px rgba(236,72,153, 0); }
100% { box-shadow: 0 0 0 0 rgba(236,72,153, 0); }
}
.custom-scrollbar::-webkit-scrollbar {
width: 4px;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: #475569;
border-radius: 10px;
}
.voice-card-active {
border-color: #8b5cf6 !important;
background: rgba(139,92,246, 0.1) !important;
}
</style>
<script>
feather.replace();
// Local Voice Library Management
let myVoices = JSON.parse(localStorage.getItem('myClonedVoices') || '[]');
let selectedLibraryPath = null;
function updateLibraryUI() {
const list = document.getElementById('voiceList');
const section = document.getElementById('librarySection');
if (myVoices.length === 0) {
section.classList.add('hidden');
return;
}
section.classList.remove('hidden');
list.innerHTML = '';
myVoices.forEach((v, index) => {
const card = document.createElement('div');
card.className = `p-3 rounded-lg border border-slate-700 bg-slate-800/40 cursor-pointer hover:border-slate-500 transition-all group flex items-center justify-between ${selectedLibraryPath === v.path ? 'voice-card-active' : ''}`;
card.onclick = () => selectVoiceFromLibrary(v.path);
card.innerHTML = `
<div class="flex items-center gap-2 overflow-hidden">
<i data-feather="user" class="w-3 h-3 text-primary"></i>
<span class="text-xs font-semibold truncate text-slate-300 group-hover:text-white transition-colors">${v.name}</span>
</div>
<button onclick="event.stopPropagation(); deleteVoice(${index})" class="opacity-0 group-hover:opacity-100 p-1 hover:text-red-400 transition-all">
<i data-feather="trash-2" class="w-3 h-3"></i>
</button>
`;
list.appendChild(card);
});
feather.replace();
}
function selectVoiceFromLibrary(path) {
if (selectedLibraryPath === path) {
selectedLibraryPath = null;
document.getElementById('cloneFileName').textContent = 'Trích xuất giọng từ file (.mp3, .wav)';
} else {
selectedLibraryPath = path;
// Clear file input UI
document.getElementById('cloneAudio').value = '';
document.getElementById('cloneFileName').textContent = 'Sử dụng giọng từ thư viện...';
}
updateLibraryUI();
}
function deleteVoice(index) {
if (myVoices[index].path === selectedLibraryPath) selectedLibraryPath = null;
myVoices.splice(index, 1);
localStorage.setItem('myClonedVoices', JSON.stringify(myVoices));
updateLibraryUI();
}
updateLibraryUI();
// Range Slider Value update
const tempSlider = document.getElementById('temperature');
const tempValue = document.getElementById('tempValue');
tempSlider.addEventListener('input', (e) => {
tempValue.textContent = parseFloat(e.target.value).toFixed(1);
});
// Tabs Logic
const tabPreset = document.getElementById('tabPreset');
const tabClone = document.getElementById('tabClone');
const presetContainer = document.getElementById('presetContainer');
const cloneContainer = document.getElementById('cloneContainer');
let isCloning = false;
const bgmVolSlider = document.getElementById('bgmVolume');
const bgmVolValue = document.getElementById('bgmVolValue');
bgmVolSlider.addEventListener('input', (e) => {
bgmVolValue.textContent = e.target.value;
});
tabPreset.addEventListener('click', () => {
isCloning = false;
tabPreset.className = "px-6 py-3 text-sm font-semibold text-primary border-b-2 border-primary flex gap-2 items-center transition-colors";
tabClone.className = "px-6 py-3 text-sm font-semibold text-slate-400 hover:text-slate-200 flex gap-2 items-center transition-colors border-b-2 border-transparent";
presetContainer.classList.remove('hidden');
cloneContainer.classList.add('hidden');
selectedLibraryPath = null;
updateLibraryUI();
});
tabClone.addEventListener('click', () => {
isCloning = true;
tabClone.className = "px-6 py-3 text-sm font-semibold text-primary border-b-2 border-primary flex gap-2 items-center transition-colors";
tabPreset.className = "px-6 py-3 text-sm font-semibold text-slate-400 hover:text-slate-200 flex gap-2 items-center transition-colors border-b-2 border-transparent";
presetContainer.classList.add('hidden');
cloneContainer.classList.remove('hidden');
updateLibraryUI();
});
document.getElementById('cloneAudio').addEventListener('change', function(e) {
const fileName = e.target.files[0] ? e.target.files[0].name : 'Trích xuất giọng từ file (.mp3, .wav)';
document.getElementById('cloneFileName').textContent = fileName;
if (e.target.files[0]) {
selectedLibraryPath = null;
updateLibraryUI();
}
});
// Submit logic
const submitBtn = document.getElementById('submitBtn');
let pollInterval;
submitBtn.addEventListener('click', async () => {
const text = document.getElementById('scriptText').value;
if(!text.trim()) {
alert("Vui lòng nhập kịch bản cần đọc!");
return;
}
const formData = new FormData();
formData.append('script', text);
formData.append('temperature', parseFloat(tempSlider.value));
// BGM logic
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);
let tempCharacterName = "";
if (isCloning) {
if (selectedLibraryPath) {
formData.append('existing_ref_path', selectedLibraryPath);
} else {
const cloneAudio = document.getElementById('cloneAudio').files[0];
if(!cloneAudio) {
alert("Vui lòng chọn file âm thanh hoặc chọn từ thư viện!");
return;
}
formData.append('ref_audio', cloneAudio);
tempCharacterName = document.getElementById('characterName').value.trim() || "Nhân vật mới";
}
} else {
formData.append('voice_preset', document.getElementById('voicePreset').value);
}
// Update UI
submitBtn.disabled = true;
submitBtn.classList.add('opacity-70');
document.getElementById('emptyResult').classList.add('hidden');
document.getElementById('audioResult').classList.add('hidden');
document.getElementById('loadingResult').classList.remove('hidden');
document.getElementById('loadingResult').classList.add('flex');
try {
const response = await fetch('/api/v1/tts/generate', {
method: 'POST',
body: formData
});
if (!response.ok) throw new Error("Yêu cầu thất bại");
const data = await response.json();
const jobId = data.job_id;
pollInterval = setInterval(() => checkTtsJobStatus(jobId, tempCharacterName), 1500);
} catch (err) {
alert("Lỗi: " + err);
resetSubmitUI();
}
});
async function checkTtsJobStatus(jobId, charName) {
try {
const res = await fetch(`/api/v1/jobs/${jobId}`);
if (!res.ok) return;
const jobData = await res.json();
if (jobData.status === 'completed') {
clearInterval(pollInterval);
// SAVE TO LIBRARY IF NEW
if (charName) {
const actualPath = jobData.raw_video_path; // We reused this field for ref_audio_path in TTS jobs
if (actualPath && actualPath !== "audio_only" && !myVoices.find(v => v.path === actualPath)) {
myVoices.push({ name: charName, path: actualPath });
localStorage.setItem('myClonedVoices', JSON.stringify(myVoices));
updateLibraryUI();
}
}
showResult(jobData.result_url);
resetSubmitUI();
} else if (jobData.status === 'failed' || jobData.status === 'error') {
clearInterval(pollInterval);
alert("Lỗi quá trình Render TTS!");
resetSubmitUI();
}
} catch(e) {}
}
function showResult(audioUrl) {
document.getElementById('loadingResult').classList.add('hidden');
document.getElementById('loadingResult').classList.remove('flex');
document.getElementById('audioResult').classList.remove('hidden');
const player = document.getElementById('player');
player.src = audioUrl;
player.play();
const btn = document.getElementById('downloadBtn');
btn.href = audioUrl;
}
function resetSubmitUI() {
submitBtn.disabled = false;
submitBtn.classList.remove('opacity-70');
if (document.getElementById('loadingResult').classList.contains('flex')) {
document.getElementById('loadingResult').classList.add('hidden');
document.getElementById('loadingResult').classList.remove('flex');
document.getElementById('emptyResult').classList.remove('hidden');
}
}
</script>
</body>
</html>