reelsx / templates /index.html
salomonsky's picture
Upload 142 files
eb9e866 verified
raw
history blame
27.3 kB
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Generador de Videos de Reacci贸n</title>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/tailwind.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/[email protected]/distribute/nouislider.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/[email protected]/distribute/nouislider.min.js"></script>
<style>
.image-container {
transition: all 0.3s ease;
border: 2px solid transparent;
border-radius: 0.5rem;
padding: 0.5rem;
}
.image-container.selected {
border-color: #3B82F6;
background-color: #EFF6FF;
}
#preview-video {
display: none;
}
#preview-video.show {
display: block;
}
#download-container {
display: none;
}
#download-container.show {
display: block;
}
/* Estilos para el checklist de procesos */
#process-checklist {
margin-top: 1rem;
border: 1px solid #E5E7EB;
}
#process-checklist li {
transition: all 0.3s ease;
}
#process-checklist span.w-5 {
transition: all 0.3s ease;
background-color: white;
}
#process-checklist span.w-5 svg {
transition: all 0.3s ease;
}
#process-checklist span.w-5:has(svg:not(.hidden)) {
border-color: #10B981;
background-color: #ECFDF5;
}
</style>
</head>
<body class="bg-gray-100 min-h-screen">
<div class="container mx-auto px-4 py-8">
<h1 class="text-4xl font-bold text-center mb-8">Generador de Videos de Reacci贸n</h1>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<!-- Columna Video Base -->
<div class="bg-white rounded-lg shadow-lg p-6">
<h2 class="text-2xl font-bold mb-6">Video Base</h2>
<div class="mb-6">
<label class="block text-gray-700 text-sm font-bold mb-2" for="youtube-url">
URL de YouTube
</label>
<input type="text" id="youtube-url"
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
placeholder="https://www.youtube.com/watch?v=..." value="https://www.youtube.com/shorts/XsXzSujxbKM">
<button onclick="getVideoInfo()"
class="mt-2 bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline">
Cargar Video
</button>
</div>
<div id="video-info" class="hidden mb-6">
<div class="flex items-center mb-4">
<img id="video-thumbnail" class="w-48 h-auto mr-4 rounded">
<div>
<h3 id="video-title" class="text-xl font-bold"></h3>
<p id="video-duration" class="text-gray-600"></p>
</div>
</div>
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2">
Seleccionar Segmento
</label>
<div id="time-slider" class="mb-2"></div>
<div class="flex justify-between text-sm text-gray-600">
<span id="start-time">0:00</span>
<span id="end-time">0:00</span>
</div>
</div>
</div>
<!-- 脕rea de Preview -->
<div id="preview-area" class="w-full h-96 relative mb-4">
<video id="preview-video" class="w-full h-full object-contain bg-gray-100 rounded-lg" controls>
Tu navegador no soporta la reproducci贸n de video.
</video>
<div id="download-container" class="mt-4 text-center">
<a id="download-link" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded inline-flex items-center">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/>
</svg>
Descargar Video
</a>
</div>
</div>
<!-- Checklist de Procesos -->
<div id="process-checklist" class="bg-white rounded-lg p-4 shadow-sm">
<h3 class="text-lg font-semibold mb-3">Estado de Procesos</h3>
<ul class="space-y-2">
<li class="flex items-center">
<span id="check-video-download" class="w-5 h-5 mr-2 inline-flex items-center justify-center rounded-full border">
<svg class="w-3 h-3 hidden text-green-500" fill="currentColor" viewBox="0 0 20 20">
<path d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"/>
</svg>
</span>
<span class="text-sm">Descarga y Recorte del Video Base</span>
</li>
<li class="flex items-center">
<span id="check-model-loading" class="w-5 h-5 mr-2 inline-flex items-center justify-center rounded-full border">
<svg class="w-3 h-3 hidden text-green-500" fill="currentColor" viewBox="0 0 20 20">
<path d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"/>
</svg>
</span>
<span class="text-sm">Carga de Modelo Wav2Lip</span>
</li>
<li class="flex items-center">
<span id="check-face-detection" class="w-5 h-5 mr-2 inline-flex items-center justify-center rounded-full border">
<svg class="w-3 h-3 hidden text-green-500" fill="currentColor" viewBox="0 0 20 20">
<path d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"/>
</svg>
</span>
<span class="text-sm">Detecci贸n de Rostro</span>
</li>
<li class="flex items-center">
<span id="check-audio-processing" class="w-5 h-5 mr-2 inline-flex items-center justify-center rounded-full border">
<svg class="w-3 h-3 hidden text-green-500" fill="currentColor" viewBox="0 0 20 20">
<path d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"/>
</svg>
</span>
<span class="text-sm">Procesamiento de Audio</span>
</li>
<li class="flex items-center">
<span id="check-inference" class="w-5 h-5 mr-2 inline-flex items-center justify-center rounded-full border">
<svg class="w-3 h-3 hidden text-green-500" fill="currentColor" viewBox="0 0 20 20">
<path d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"/>
</svg>
</span>
<span class="text-sm">Inferencia del Modelo</span>
</li>
<li class="flex items-center">
<span id="check-video-generation" class="w-5 h-5 mr-2 inline-flex items-center justify-center rounded-full border">
<svg class="w-3 h-3 hidden text-green-500" fill="currentColor" viewBox="0 0 20 20">
<path d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"/>
</svg>
</span>
<span class="text-sm">Generaci贸n de Video Final</span>
</li>
<li class="flex items-center">
<span id="check-video-fusion" class="w-5 h-5 mr-2 inline-flex items-center justify-center rounded-full border">
<svg class="w-3 h-3 hidden text-green-500" fill="currentColor" viewBox="0 0 20 20">
<path d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"/>
</svg>
</span>
<span class="text-sm">Fusi贸n de Videos</span>
</li>
</ul>
</div>
</div>
<!-- Columna Reacci贸n -->
<div class="bg-white rounded-lg shadow-lg p-6">
<h2 class="text-2xl font-bold mb-6">Reacci贸n</h2>
<div class="mb-6">
<label class="block text-gray-700 text-sm font-bold mb-2" for="reaction-text">
Texto de Reacci贸n
</label>
<textarea id="reaction-text"
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
rows="4" placeholder="Escribe tu reacci贸n aqu铆..."></textarea>
</div>
<div class="mb-6">
<label class="block text-gray-700 text-sm font-bold mb-2">
Voz para la Reacci贸n
</label>
<select id="tts-voice" class="w-full p-2 border rounded">
<option value="es-MX-JorgeNeural">Jorge (M茅xico)</option>
<option value="es-MX-DaliaNeural">Dalia (M茅xico)</option>
</select>
</div>
<button onclick="generateTTS()"
class="w-full bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline mb-6">
Generar Audio
</button>
<div id="audio-preview" class="hidden mb-6">
<audio id="audio-player" controls class="w-full"></audio>
</div>
<div class="mb-6">
<label class="block text-gray-700 text-sm font-bold mb-2">
Imagen de Reacci贸n
</label>
<div class="grid grid-cols-3 gap-4">
<div class="image-container cursor-pointer" onclick="selectImage('female')">
<img src="/static/female.png" class="w-24 h-24 object-cover rounded mx-auto mb-2">
<p class="text-sm text-center">Femenino</p>
</div>
<div class="image-container cursor-pointer" onclick="selectImage('male')">
<img src="/static/male.png" class="w-24 h-24 object-cover rounded mx-auto mb-2">
<p class="text-sm text-center">Masculino</p>
</div>
<div class="text-center">
<label class="cursor-pointer">
<div class="w-24 h-24 border-2 border-dashed border-gray-300 rounded flex items-center justify-center mx-auto mb-2">
<span class="text-gray-500">+</span>
</div>
<input type="file" id="face-image" class="hidden" onchange="previewImage(event)" accept="image/*">
<p class="text-sm text-center">Subir Imagen</p>
</label>
</div>
</div>
<div id="selected-image-container" class="hidden mt-4">
<p class="text-sm font-semibold mb-2">Imagen Seleccionada:</p>
<img id="image-preview" class="w-24 h-24 object-cover rounded">
</div>
</div>
<button onclick="generateWav2Lip()"
class="w-full bg-purple-500 hover:bg-purple-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline mb-4">
Generar Animaci贸n
</button>
<div id="wav2lip-preview" class="hidden mb-4">
<p class="text-sm font-semibold mb-2">Preview de la Animaci贸n:</p>
<video id="wav2lip-video" class="w-full rounded-lg bg-gray-100" controls>
Tu navegador no soporta la reproducci贸n de video.
</video>
<div class="mt-2 text-center">
<a id="wav2lip-download" class="text-blue-500 hover:text-blue-700 text-sm" download>
Descargar Animaci贸n
</a>
</div>
</div>
</div>
</div>
<!-- Secci贸n de Generaci贸n Final -->
<div class="mt-8">
<button onclick="processVideo()"
class="w-full bg-blue-600 hover:bg-blue-800 text-white font-bold py-4 px-6 rounded-lg focus:outline-none focus:shadow-outline text-xl">
Combinar Videos
</button>
<div id="progress" class="hidden mt-4">
<div class="bg-white rounded-lg shadow-lg p-6">
<div class="flex flex-col items-center justify-center">
<div class="w-full bg-gray-200 rounded-full h-4 mb-4 relative overflow-hidden">
<div id="progress-bar" class="bg-blue-500 h-4 rounded-full transition-all duration-300 absolute top-0 left-0" style="width: 0%"></div>
<div id="progress-text" class="absolute w-full text-center text-xs font-bold text-white leading-4">0%</div>
</div>
<div id="progress-step" class="text-lg font-semibold text-blue-600 mt-2">Iniciando proceso...</div>
<div id="progress-detail" class="text-sm text-gray-600 mt-1">Preparando archivos...</div>
</div>
</div>
</div>
</div>
</div>
<script>
let slider;
let videoDuration = 0;
let currentPreviewId = null;
let currentAudioId = null;
let currentWav2lipId = null;
let selectedImage = null;
// Funciones para el checklist de procesos
function updateProcessStatus(processId, status) {
const checkElement = document.querySelector(`#check-${processId} svg`);
if (status === 'completed') {
checkElement.classList.remove('hidden');
} else {
checkElement.classList.add('hidden');
}
}
function resetProcessChecklist() {
const processes = [
'video-download',
'model-loading',
'face-detection',
'audio-processing',
'inference',
'video-generation',
'video-fusion'
];
processes.forEach(process => updateProcessStatus(process, 'pending'));
}
async function getVideoInfo() {
const url = document.getElementById('youtube-url').value;
if (!url) return;
try {
const response = await fetch('/get_video_info', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ url }),
});
const data = await response.json();
if (response.ok) {
document.getElementById('video-info').classList.remove('hidden');
document.getElementById('video-thumbnail').src = data.thumbnail;
document.getElementById('video-title').textContent = data.title;
videoDuration = data.duration;
if (slider) {
slider.destroy();
}
slider = noUiSlider.create(document.getElementById('time-slider'), {
start: [0, data.duration],
connect: true,
range: {
'min': 0,
'max': data.duration
}
});
slider.on('update', function(values) {
document.getElementById('start-time').textContent = formatTime(values[0]);
document.getElementById('end-time').textContent = formatTime(values[1]);
});
} else {
alert(data.error);
}
} catch (error) {
alert('Error al obtener informaci贸n del video');
}
}
async function generateTTS() {
const text = document.getElementById('reaction-text').value;
const voice = document.getElementById('tts-voice').value;
if (!text) {
alert('Por favor, ingresa un texto para la reacci贸n');
return;
}
try {
const response = await fetch('/generate-tts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ text, voice }),
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Error al generar el audio');
}
currentAudioId = data.audio_id;
document.getElementById('audio-preview').classList.remove('hidden');
const audioPlayer = document.getElementById('audio-player');
audioPlayer.src = `/audio/${currentAudioId}`;
audioPlayer.onerror = function() {
alert('Error al cargar el audio. Por favor, intenta de nuevo.');
document.getElementById('audio-preview').classList.add('hidden');
};
updateProcessStatus('audio-processing', 'completed');
} catch (error) {
console.error('Error:', error);
alert(error.message || 'Error al generar el audio');
document.getElementById('audio-preview').classList.add('hidden');
}
}
function selectImage(type) {
window.selectedImage = type;
selectedImage = type;
const preview = document.getElementById('image-preview');
const container = document.getElementById('selected-image-container');
preview.src = `/static/${type}.png`;
container.classList.remove('hidden');
document.querySelectorAll('.image-container').forEach(container => {
container.classList.remove('selected');
});
const selectedContainer = document.querySelector(`.image-container img[src*="${type}"]`).parentElement;
selectedContainer.classList.add('selected');
updateProcessStatus('face-detection', 'completed');
}
function previewImage(event) {
const preview = document.getElementById('image-preview');
const container = document.getElementById('selected-image-container');
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = function(e) {
window.selectedImage = 'custom';
selectedImage = 'custom';
preview.src = e.target.result;
container.classList.remove('hidden');
document.querySelectorAll('.image-container').forEach(container => {
container.classList.remove('selected');
});
updateProcessStatus('face-detection', 'completed');
}
reader.readAsDataURL(file);
}
}
async function generateWav2Lip() {
if (!currentAudioId) {
alert('Por favor, genera el audio primero');
return;
}
if (!window.selectedImage) {
alert('Por favor, selecciona una imagen primero');
return;
}
resetProcessChecklist();
try {
updateProcessStatus('model-loading', 'completed');
const formData = new FormData();
formData.append('audio_id', currentAudioId);
if (window.selectedImage === 'custom') {
const fileInput = document.getElementById('face-image');
if (fileInput.files.length > 0) {
formData.append('image', fileInput.files[0]);
} else {
alert('Error al procesar la imagen personalizada');
return;
}
} else {
formData.append('default_image', window.selectedImage);
}
const response = await fetch('/generate-wav2lip', {
method: 'POST',
body: formData
});
if (response.ok) {
updateProcessStatus('face-detection', 'completed');
updateProcessStatus('audio-processing', 'completed');
const data = await response.json();
currentWav2lipId = data.wav2lip_id;
document.getElementById('wav2lip-preview').classList.remove('hidden');
document.getElementById('wav2lip-video').src = `/animation/${currentWav2lipId}`;
document.getElementById('wav2lip-download').href = `/download-animation/${currentWav2lipId}`;
updateProcessStatus('inference', 'completed');
updateProcessStatus('video-generation', 'completed');
} else {
const error = await response.json();
alert(error.error || 'Error al generar la animaci贸n');
}
} catch (error) {
console.error('Error:', error);
alert('Error al generar la animaci贸n');
}
}
async function processVideo() {
const url = document.getElementById('youtube-url').value;
if (!url) {
alert('Por favor, ingresa una URL de YouTube');
return;
}
if (!currentWav2lipId) {
alert('Por favor, genera la animaci贸n primero');
return;
}
resetProcessChecklist();
try {
const [startTime, endTime] = slider.get();
updateProcessStatus('video-download', 'completed');
const formData = new FormData();
formData.append('wav2lip_id', currentWav2lipId);
formData.append('preview_id', currentPreviewId);
formData.append('start_time', startTime);
formData.append('end_time', endTime);
const response = await fetch('/process-video', {
method: 'POST',
body: formData
});
if (response.ok) {
const data = await response.json();
document.getElementById('preview-video').classList.add('show');
document.getElementById('preview-video').src = `/static/final_results/${data.video_id}.mp4`;
document.getElementById('download-container').classList.add('show');
document.getElementById('download-link').href = `/static/final_results/${data.video_id}.mp4`;
updateProcessStatus('video-generation', 'completed');
updateProcessStatus('video-fusion', 'completed');
} else {
const error = await response.json();
alert(error.error || 'Error al procesar el video');
}
} catch (error) {
console.error('Error:', error);
alert('Error al procesar el video');
}
}
function formatTime(seconds) {
seconds = parseInt(seconds);
const minutes = Math.floor(seconds / 60);
seconds = seconds % 60;
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
}
</script>
</body>
</html>