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