news / index.html
salomonsky's picture
Update index.html
abfbb45 verified
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Creador de Video con Fotos y Audio</title>
<style>
:root {
--primary-color: #2c3e50;
--secondary-color: #3498db;
--background-color: #ecf0f1;
--text-color: #34495e;
--white-color: #ffffff;
--border-color: #bdc3c7;
--error-color: #e74c3c;
--success-color: #2ecc71;
--disabled-color: #95a5a6;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
background-color: var(--background-color);
color: var(--text-color);
margin: 0;
padding: 20px;
display: flex;
justify-content: center;
align-items: flex-start;
min-height: 100vh;
}
.container {
width: 100%;
max-width: 700px;
background-color: var(--white-color);
padding: 20px 30px 30px;
border-radius: 12px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
margin: 20px 0;
}
h1 {
text-align: center;
color: var(--primary-color);
margin-bottom: 25px;
}
h2 {
color: var(--secondary-color);
border-bottom: 2px solid var(--secondary-color);
padding-bottom: 5px;
margin-top: 20px;
margin-bottom: 15px;
font-size: 1.2em;
}
.controls {
display: flex;
flex-direction: column;
gap: 15px;
margin-bottom: 25px;
}
textarea, select {
width: 100%;
padding: 12px;
border-radius: 6px;
border: 1px solid var(--border-color);
font-size: 16px;
box-sizing: border-box;
transition: border-color 0.3s, box-shadow 0.3s;
background-color: #fff;
}
textarea:focus, select:focus {
outline: none;
border-color: var(--secondary-color);
box-shadow: 0 0 5px rgba(52, 152, 219, 0.5);
}
textarea {
min-height: 100px;
resize: vertical;
}
.file-label {
display: block;
background-color: #f8f9fa;
border: 2px dashed var(--border-color);
padding: 20px;
text-align: center;
border-radius: 6px;
cursor: pointer;
transition: background-color 0.3s, border-color 0.3s;
}
.file-label:hover {
background-color: #e9ecef;
border-color: var(--secondary-color);
}
input[type="file"] {
display: none;
}
#file-count {
margin-top: 10px;
font-weight: bold;
color: var(--primary-color);
}
.button-group {
display: flex;
flex-direction: column;
gap: 10px;
}
#generate-btn, #download-btn {
padding: 15px 25px;
font-size: 18px;
font-weight: bold;
color: var(--white-color);
border: none;
border-radius: 6px;
cursor: pointer;
transition: background-color 0.3s, transform 0.2s;
display: block;
width: 100%;
text-align: center;
text-decoration: none;
}
#generate-btn {
background-color: var(--secondary-color);
}
#download-btn {
background-color: var(--success-color);
}
#generate-btn:hover:not(:disabled) {
background-color: #2980b9;
transform: translateY(-2px);
}
#download-btn:hover:not(:disabled) {
background-color: #27ae60;
transform: translateY(-2px);
}
#generate-btn:disabled, #download-btn:disabled {
background-color: var(--disabled-color);
cursor: not-allowed;
transform: none;
}
#video-container {
margin: 20px auto 0;
position: relative;
overflow: hidden;
border-radius: 8px;
display: none;
background-color: #000;
}
#video-container.aspect-16-9 {
width: 100%;
aspect-ratio: 16 / 9;
}
#video-container.aspect-9-16 {
height: 65vh;
aspect-ratio: 9 / 16;
max-width: 100%;
}
#video-container img {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
opacity: 0;
transition: opacity 0.5s ease-in-out;
}
#video-container img.active {
opacity: 1;
}
#status {
text-align: center;
margin-top: 15px;
font-size: 16px;
font-weight: 500;
min-height: 24px;
}
#download-note {
text-align: center;
font-size: 0.8em;
color: #7f8c8d;
margin-top: 5px;
}
.status-error { color: var(--error-color); }
.status-success { color: var(--success-color); }
.status-processing { color: var(--secondary-color); }
.kenburns { animation-duration: 7s; animation-timing-function: linear; animation-iteration-count: 1; }
.kb-top-left { animation-name: kb-top-left; }
.kb-top-right { animation-name: kb-top-right; }
.kb-bottom-left { animation-name: kb-bottom-left; }
.kb-bottom-right { animation-name: kb-bottom-right; }
@keyframes kb-top-left { 0% { transform: scale(1) translate(0, 0); } 100% { transform: scale(1.25) translate(15%, 15%); } }
@keyframes kb-top-right { 0% { transform: scale(1) translate(0, 0); } 100% { transform: scale(1.25) translate(-15%, 15%); } }
@keyframes kb-bottom-left { 0% { transform: scale(1) translate(0, 0); } 100% { transform: scale(1.25) translate(15%, -15%); } }
@keyframes kb-bottom-right { 0% { transform: scale(1) translate(0, 0); } 100% { transform: scale(1.25) translate(-15%, -15%); } }
</style>
</head>
<body>
<div class="container">
<h1>Creador de Video con IA</h1>
<div class="controls">
<div>
<h2>Paso 1: Escribe tu texto</h2>
<textarea id="text-input" placeholder="Escribe el texto que se convertirá en audio aquí..."></textarea>
</div>
<div style="display: flex; gap: 15px;">
<div style="flex: 1;">
<h2>Paso 2: Idioma</h2>
<select id="lang-select">
<option value="es-MX" selected>Español (México)</option>
<option value="es-ES">Español (España)</option>
<option value="en-US">Inglés (USA)</option>
<option value="en-GB">Inglés (UK)</option>
<option value="fr-FR">Francés</option>
<option value="de-DE">Alemán</option>
<option value="it-IT">Italiano</option>
</select>
</div>
<div style="flex: 1;">
<h2>Paso 3: Formato</h2>
<select id="aspect-ratio">
<option value="9:16">9:16 (Vertical)</option>
<option value="16:9">16:9 (Horizontal)</option>
</select>
</div>
</div>
<div>
<h2>Paso 4: Sube tus imágenes (1-20)</h2>
<label for="image-input" class="file-label">
<span>Haz clic para seleccionar</span>
<div id="file-count">0 imágenes seleccionadas</div>
</label>
<input type="file" id="image-input" multiple accept="image/*">
</div>
</div>
<div class="button-group">
<button id="generate-btn">Generar y Reproducir</button>
<a id="download-btn" href="#" style="display: none;" disabled>Descargar Video</a>
<p id="download-note" style="display: none;">Nota: El video descargado será silencioso debido a limitaciones del navegador.</p>
</div>
<div id="status"></div>
<div id="video-container"></div>
<canvas id="hidden-canvas" style="display: none;"></canvas>
</div>
<script>
const textInput = document.getElementById('text-input');
const langSelect = document.getElementById('lang-select');
const aspectRatioSelect = document.getElementById('aspect-ratio');
const imageInput = document.getElementById('image-input');
const generateBtn = document.getElementById('generate-btn');
const downloadBtn = document.getElementById('download-btn');
const downloadNote = document.getElementById('download-note');
const videoContainer = document.getElementById('video-container');
const statusDiv = document.getElementById('status');
const fileCountDiv = document.getElementById('file-count');
const canvas = document.getElementById('hidden-canvas');
const SLIDE_DURATION_MS = 7000;
const KENBURNS_CLASSES = ['kb-top-left', 'kb-top-right', 'kb-bottom-left', 'kb-bottom-right'];
let imageElements = [];
let slideInterval = null;
let recordingInterval = null;
let currentImageIndex = 0;
let mediaRecorder;
let recordedChunks = [];
imageInput.addEventListener('change', () => {
const fileCount = imageInput.files.length;
fileCountDiv.textContent = `${fileCount} ${fileCount === 1 ? 'imagen seleccionada' : 'imágenes seleccionadas'}`;
});
const setStatus = (message, type = '') => {
statusDiv.textContent = message;
statusDiv.className = type ? `status-${type}` : '';
};
const stopAllProcesses = () => {
window.speechSynthesis.cancel();
if (slideInterval) clearInterval(slideInterval);
if (recordingInterval) clearInterval(recordingInterval);
if (mediaRecorder && mediaRecorder.state === 'recording') mediaRecorder.stop();
slideInterval = null;
recordingInterval = null;
generateBtn.disabled = false;
};
const displayNextImage = () => {
if (imageElements.length === 0) return;
imageElements.forEach(img => img.classList.remove('active'));
const activeIndex = currentImageIndex % imageElements.length;
const currentImg = imageElements[activeIndex];
const randomAnimation = KENBURNS_CLASSES[Math.floor(Math.random() * KENBURNS_CLASSES.length)];
currentImg.className = 'kenburns ' + randomAnimation;
setTimeout(() => {
currentImg.classList.add('active');
}, 50);
currentImageIndex++;
};
const startSlideshow = () => {
videoContainer.style.display = 'block';
const aspectRatio = aspectRatioSelect.value;
videoContainer.className = aspectRatio === '16:9' ? 'aspect-16-9' : 'aspect-9-16';
currentImageIndex = 0;
displayNextImage();
slideInterval = setInterval(displayNextImage, SLIDE_DURATION_MS);
};
const startRecording = () => {
const ctx = canvas.getContext('2d');
const stream = canvas.captureStream(30);
recordedChunks = [];
const options = { mimeType: 'video/webm; codecs=vp9' };
if (!MediaRecorder.isTypeSupported(options.mimeType)) {
options.mimeType = 'video/webm; codecs=vp8';
if (!MediaRecorder.isTypeSupported(options.mimeType)) {
options.mimeType = 'video/webm';
}
}
mediaRecorder = new MediaRecorder(stream, options);
mediaRecorder.ondataavailable = event => {
if (event.data.size > 0) recordedChunks.push(event.data);
};
mediaRecorder.onstop = () => {
const blob = new Blob(recordedChunks, { type: 'video/webm' });
const url = URL.createObjectURL(blob);
downloadBtn.href = url;
downloadBtn.download = 'video_generado.webm';
downloadBtn.style.display = 'block';
downloadNote.style.display = 'block';
downloadBtn.disabled = false;
setStatus('¡Video listo para descargar!', 'success');
};
mediaRecorder.start();
recordingInterval = setInterval(() => {
const activeImg = document.querySelector('#video-container img.active');
if (activeImg) {
ctx.drawImage(activeImg, 0, 0, canvas.width, canvas.height);
}
}, 1000 / 30);
};
generateBtn.addEventListener('click', () => {
stopAllProcesses();
const text = textInput.value.trim();
const files = imageInput.files;
if (!text) {
setStatus('Por favor, introduce un texto.', 'error');
return;
}
if (files.length === 0 || files.length > 20) {
setStatus('Por favor, selecciona entre 1 y 20 imágenes.', 'error');
return;
}
generateBtn.disabled = true;
downloadBtn.style.display = 'none';
downloadNote.style.display = 'none';
downloadBtn.disabled = true;
setStatus('Preparando imágenes...', 'processing');
videoContainer.innerHTML = '';
const imageLoadPromises = Array.from(files).map(file => {
return new Promise((resolve) => {
const img = document.createElement('img');
img.src = URL.createObjectURL(file);
img.onload = () => resolve(img);
videoContainer.appendChild(img);
});
});
Promise.all(imageLoadPromises).then(loadedImages => {
imageElements = loadedImages;
const utterance = new SpeechSynthesisUtterance(text);
utterance.lang = langSelect.value;
utterance.onstart = () => {
setStatus('Reproduciendo y grabando video...', 'processing');
const [w, h] = aspectRatioSelect.value.split(':').map(Number);
canvas.width = w === 9 ? 720 : 1280;
canvas.height = w === 9 ? 1280 : 720;
startSlideshow();
startRecording();
};
utterance.onend = () => {
if (mediaRecorder && mediaRecorder.state === 'recording') {
mediaRecorder.stop();
}
if (recordingInterval) clearInterval(recordingInterval);
if(slideInterval) clearInterval(slideInterval);
generateBtn.disabled = false;
};
utterance.onerror = (event) => {
setStatus(`Error de audio: ${event.error}`, 'error');
stopAllProcesses();
};
window.speechSynthesis.speak(utterance);
});
});
window.addEventListener('beforeunload', stopAllProcesses);
</script>
</body>
</html>