Spaces:
Running
Running
| <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> |