| | <!DOCTYPE html> |
| | <html lang="en"> |
| | <head> |
| | <meta charset="UTF-8"> |
| | <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| | <title>AI Video Generator Pro - Hailuo-Inspired</title> |
| | <style> |
| | * { |
| | margin: 0; |
| | padding: 0; |
| | box-sizing: border-box; |
| | } |
| | |
| | body { |
| | font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; |
| | background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| | background-attachment: fixed; |
| | min-height: 100vh; |
| | padding: 20px; |
| | animation: gradientShift 15s ease infinite; |
| | } |
| | |
| | @keyframes gradientShift { |
| | 0%, 100% { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); } |
| | 50% { background: linear-gradient(135deg, #764ba2 0%, #667eea 100%); } |
| | } |
| | |
| | .container { |
| | max-width: 1200px; |
| | margin: 0 auto; |
| | background: white; |
| | border-radius: 20px; |
| | box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); |
| | overflow: hidden; |
| | } |
| | |
| | .header { |
| | background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| | color: white; |
| | padding: 40px 40px; |
| | text-align: center; |
| | position: relative; |
| | overflow: hidden; |
| | } |
| | |
| | .header::before { |
| | content: ''; |
| | position: absolute; |
| | top: -50%; |
| | left: -50%; |
| | width: 200%; |
| | height: 200%; |
| | background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%); |
| | animation: headerPulse 10s ease-in-out infinite; |
| | } |
| | |
| | @keyframes headerPulse { |
| | 0%, 100% { transform: translate(0, 0); } |
| | 50% { transform: translate(10%, 10%); } |
| | } |
| | |
| | .header h1 { |
| | font-size: 36px; |
| | margin-bottom: 10px; |
| | position: relative; |
| | z-index: 1; |
| | text-shadow: 2px 2px 4px rgba(0,0,0,0.2); |
| | } |
| | |
| | .header p { |
| | font-size: 18px; |
| | opacity: 0.95; |
| | position: relative; |
| | z-index: 1; |
| | } |
| | |
| | .main-content { |
| | display: grid; |
| | grid-template-columns: 1fr 1fr; |
| | gap: 30px; |
| | padding: 40px; |
| | } |
| | |
| | .left-panel, .right-panel { |
| | display: flex; |
| | flex-direction: column; |
| | gap: 20px; |
| | } |
| | |
| | .section { |
| | background: #f8f9fa; |
| | padding: 20px; |
| | border-radius: 12px; |
| | } |
| | |
| | .section h3 { |
| | color: #333; |
| | margin-bottom: 15px; |
| | font-size: 18px; |
| | display: flex; |
| | align-items: center; |
| | gap: 8px; |
| | } |
| | |
| | label { |
| | display: block; |
| | color: #555; |
| | margin-bottom: 8px; |
| | font-weight: 500; |
| | font-size: 14px; |
| | } |
| | |
| | textarea, select, input[type="file"] { |
| | width: 100%; |
| | padding: 12px; |
| | border: 2px solid #e0e0e0; |
| | border-radius: 8px; |
| | font-size: 14px; |
| | font-family: inherit; |
| | transition: border-color 0.3s; |
| | } |
| | |
| | textarea { |
| | resize: vertical; |
| | min-height: 100px; |
| | } |
| | |
| | textarea:focus, select:focus { |
| | outline: none; |
| | border-color: #667eea; |
| | } |
| | |
| | .char-counter { |
| | text-align: right; |
| | font-size: 12px; |
| | color: #999; |
| | margin-top: 5px; |
| | } |
| | |
| | .options-grid { |
| | display: grid; |
| | grid-template-columns: 1fr 1fr; |
| | gap: 15px; |
| | } |
| | |
| | .option-group { |
| | display: flex; |
| | flex-direction: column; |
| | gap: 8px; |
| | } |
| | |
| | button { |
| | padding: 14px 24px; |
| | background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| | color: white; |
| | border: none; |
| | border-radius: 10px; |
| | font-size: 16px; |
| | font-weight: 600; |
| | cursor: pointer; |
| | transition: all 0.3s ease; |
| | position: relative; |
| | overflow: hidden; |
| | } |
| | |
| | button::before { |
| | content: ''; |
| | position: absolute; |
| | top: 50%; |
| | left: 50%; |
| | width: 0; |
| | height: 0; |
| | border-radius: 50%; |
| | background: rgba(255, 255, 255, 0.3); |
| | transform: translate(-50%, -50%); |
| | transition: width 0.6s, height 0.6s; |
| | } |
| | |
| | button:hover::before { |
| | width: 300px; |
| | height: 300px; |
| | } |
| | |
| | button:hover:not(:disabled) { |
| | transform: translateY(-2px); |
| | box-shadow: 0 8px 25px rgba(102, 126, 234, 0.5); |
| | } |
| | |
| | button:active:not(:disabled) { |
| | transform: translateY(0); |
| | } |
| | |
| | button:disabled { |
| | opacity: 0.6; |
| | cursor: not-allowed; |
| | } |
| | |
| | .generate-btn { |
| | width: 100%; |
| | padding: 16px; |
| | font-size: 18px; |
| | } |
| | |
| | .tabs { |
| | display: flex; |
| | gap: 10px; |
| | margin-bottom: 15px; |
| | } |
| | |
| | .tab { |
| | flex: 1; |
| | padding: 10px; |
| | background: white; |
| | border: 2px solid #e0e0e0; |
| | border-radius: 8px; |
| | cursor: pointer; |
| | text-align: center; |
| | transition: all 0.3s; |
| | font-weight: 500; |
| | } |
| | |
| | .tab.active { |
| | background: #667eea; |
| | color: white; |
| | border-color: #667eea; |
| | } |
| | |
| | .tab-content { |
| | display: none; |
| | } |
| | |
| | .tab-content.active { |
| | display: block; |
| | } |
| | |
| | .loader { |
| | border: 3px solid #f3f3f3; |
| | border-top: 3px solid #667eea; |
| | border-radius: 50%; |
| | width: 40px; |
| | height: 40px; |
| | animation: spin 1s linear infinite; |
| | margin: 20px auto; |
| | display: none; |
| | } |
| | |
| | @keyframes spin { |
| | 0% { transform: rotate(0deg); } |
| | 100% { transform: rotate(360deg); } |
| | } |
| | |
| | .status { |
| | padding: 12px; |
| | border-radius: 8px; |
| | text-align: center; |
| | font-size: 14px; |
| | display: none; |
| | margin-top: 15px; |
| | } |
| | |
| | .status.info { |
| | background: #e3f2fd; |
| | color: #1976d2; |
| | display: block; |
| | } |
| | |
| | .status.success { |
| | background: #e8f5e9; |
| | color: #388e3c; |
| | display: block; |
| | } |
| | |
| | .status.error { |
| | background: #ffebee; |
| | color: #d32f2f; |
| | display: block; |
| | } |
| | |
| | .video-container { |
| | background: #000; |
| | border-radius: 12px; |
| | overflow: hidden; |
| | display: none; |
| | } |
| | |
| | video { |
| | width: 100%; |
| | display: block; |
| | } |
| | |
| | .video-info { |
| | background: #f8f9fa; |
| | padding: 15px; |
| | margin-top: 10px; |
| | border-radius: 8px; |
| | font-size: 13px; |
| | color: #666; |
| | } |
| | |
| | .video-actions { |
| | display: flex; |
| | gap: 10px; |
| | margin-top: 10px; |
| | } |
| | |
| | .video-actions button { |
| | flex: 1; |
| | background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%); |
| | } |
| | |
| | .example-prompts { |
| | display: flex; |
| | flex-direction: column; |
| | gap: 10px; |
| | } |
| | |
| | .prompt-category { |
| | margin-bottom: 10px; |
| | } |
| | |
| | .prompt-category h4 { |
| | font-size: 13px; |
| | color: #666; |
| | margin-bottom: 8px; |
| | } |
| | |
| | .example-prompt { |
| | display: inline-block; |
| | padding: 8px 16px; |
| | margin: 6px 4px; |
| | background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); |
| | border: 2px solid transparent; |
| | border-radius: 25px; |
| | font-size: 13px; |
| | cursor: pointer; |
| | transition: all 0.3s ease; |
| | box-shadow: 0 2px 5px rgba(0,0,0,0.1); |
| | } |
| | |
| | .example-prompt:hover { |
| | background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| | color: white; |
| | border-color: #667eea; |
| | transform: translateY(-2px); |
| | box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4); |
| | } |
| | |
| | .example-prompt:active { |
| | transform: translateY(0); |
| | } |
| | |
| | .image-upload-area { |
| | border: 2px dashed #ddd; |
| | border-radius: 8px; |
| | padding: 20px; |
| | text-align: center; |
| | cursor: pointer; |
| | transition: all 0.3s; |
| | } |
| | |
| | .image-upload-area:hover { |
| | border-color: #667eea; |
| | background: #f8f9fa; |
| | } |
| | |
| | .image-upload-area.has-image { |
| | border-style: solid; |
| | border-color: #667eea; |
| | } |
| | |
| | .preview-image { |
| | max-width: 100%; |
| | max-height: 200px; |
| | border-radius: 8px; |
| | margin-top: 10px; |
| | } |
| | |
| | @media (max-width: 768px) { |
| | .main-content { |
| | grid-template-columns: 1fr; |
| | } |
| | |
| | .options-grid { |
| | grid-template-columns: 1fr; |
| | } |
| | } |
| | </style> |
| | </head> |
| | <body> |
| | <div class="container"> |
| | <div class="header"> |
| | <h1>🎬 AI Video Generator Pro</h1> |
| | <p>Create stunning videos with multiple AI models and Hailuo-inspired features</p> |
| | </div> |
| |
|
| | <div class="main-content"> |
| | |
| | <div class="left-panel"> |
| | |
| | <div class="section"> |
| | <h3>🎯 Generation Mode</h3> |
| | <div class="tabs"> |
| | <div class="tab active" onclick="switchMode('text')">📝 Text to Video</div> |
| | <div class="tab" onclick="switchMode('image')">🖼️ Image to Video</div> |
| | </div> |
| |
|
| | |
| | <div id="text-mode" class="tab-content active"> |
| | <label for="prompt">Enter your prompt:</label> |
| | <textarea |
| | id="prompt" |
| | rows="4" |
| | placeholder="Describe the video you want to create..." |
| | maxlength="1000" |
| | ></textarea> |
| | <div class="char-counter"> |
| | <span id="char-count">0</span>/1000 characters |
| | </div> |
| | </div> |
| |
|
| | |
| | <div id="image-mode" class="tab-content"> |
| | <label>Upload Image:</label> |
| | <div class="image-upload-area" id="upload-area" onclick="document.getElementById('image-input').click()"> |
| | <p>📤 Click to upload or drag & drop</p> |
| | <p style="font-size: 12px; color: #999; margin-top: 5px;">Supports JPG, PNG</p> |
| | <img id="preview-image" class="preview-image" style="display: none;"> |
| | </div> |
| | <input type="file" id="image-input" accept="image/*" style="display: none;" onchange="handleImageUpload(event)"> |
| | |
| | <label for="image-prompt" style="margin-top: 15px;">Animation prompt (optional):</label> |
| | <textarea |
| | id="image-prompt" |
| | rows="2" |
| | placeholder="Describe how you want the image to animate..." |
| | ></textarea> |
| | </div> |
| | </div> |
| |
|
| | |
| | <div class="section"> |
| | <h3>🤖 AI Model</h3> |
| | <select id="model-select" onchange="updateModelInfo()"> |
| | <option value="cogvideox-5b">🔥 CogVideoX-5B - Best Quality (6s, 720p)</option> |
| | <option value="cogvideox-2b">⚡ CogVideoX-2B - Faster Version</option> |
| | <option value="hunyuan-video">🌟 HunyuanVideo - SOTA by Tencent</option> |
| | <option value="stable-video-diffusion">🖼️ Stable Video Diffusion - Image Animation</option> |
| | <option value="demo">💡 Demo Mode - Test UI (Instant)</option> |
| | </select> |
| | <div id="model-info" style="margin-top: 10px; font-size: 13px; color: #666;"></div> |
| | </div> |
| |
|
| | |
| | <div class="section"> |
| | <h3>🎥 Advanced Options</h3> |
| | <div class="options-grid"> |
| | <div class="option-group"> |
| | <label for="camera-select">Camera Movement:</label> |
| | <select id="camera-select"> |
| | <option value="">Static (No movement)</option> |
| | </select> |
| | </div> |
| |
|
| | <div class="option-group"> |
| | <label for="effect-select">Visual Effects:</label> |
| | <select id="effect-select"> |
| | <option value="">None</option> |
| | </select> |
| | </div> |
| |
|
| | <div class="option-group"> |
| | <label for="style-select">Video Style:</label> |
| | <select id="style-select"> |
| | <option value="">Default</option> |
| | </select> |
| | </div> |
| | </div> |
| | </div> |
| |
|
| | |
| | <button class="generate-btn" onclick="generateVideo()"> |
| | 🎬 Generate Video |
| | </button> |
| | </div> |
| |
|
| | |
| | <div class="right-panel"> |
| | |
| | <div class="section"> |
| | <h3>🎞️ Generated Video</h3> |
| | <div class="loader" id="loader"></div> |
| | <div class="status" id="status"></div> |
| | |
| | <div class="video-container" id="video-container"> |
| | <video id="video-output" controls></video> |
| | </div> |
| |
|
| | <div id="video-info" class="video-info" style="display: none;"></div> |
| |
|
| | <div class="video-actions" id="video-actions" style="display: none;"> |
| | <button onclick="downloadVideo()">📥 Download</button> |
| | <button onclick="shareVideo()">🔗 Share</button> |
| | </div> |
| | </div> |
| |
|
| | |
| | <div class="section"> |
| | <h3>💡 Example Prompts</h3> |
| | <div class="example-prompts" id="example-prompts"> |
| | |
| | </div> |
| | </div> |
| | </div> |
| | </div> |
| | </div> |
| |
|
| | <script> |
| | |
| | let currentVideoUrl = null; |
| | let currentMode = 'text'; |
| | let uploadedImage = null; |
| | let modelsData = null; |
| | |
| | |
| | window.addEventListener('load', async () => { |
| | await loadModelsData(); |
| | checkServerHealth(); |
| | setupEventListeners(); |
| | }); |
| | |
| | function setupEventListeners() { |
| | const promptInput = document.getElementById('prompt'); |
| | const charCount = document.getElementById('char-count'); |
| | |
| | promptInput.addEventListener('input', () => { |
| | const length = promptInput.value.length; |
| | charCount.textContent = length; |
| | charCount.style.color = length > 900 ? '#d32f2f' : '#999'; |
| | }); |
| | |
| | promptInput.addEventListener('keydown', (e) => { |
| | if (e.ctrlKey && e.key === 'Enter') { |
| | generateVideo(); |
| | } |
| | }); |
| | } |
| | |
| | async function loadModelsData() { |
| | try { |
| | const response = await fetch('/api/models'); |
| | modelsData = await response.json(); |
| | |
| | populateCameraMovements(); |
| | populateVisualEffects(); |
| | populateVideoStyles(); |
| | populateExamplePrompts(); |
| | updateModelInfo(); |
| | } catch (error) { |
| | console.error('Failed to load models data:', error); |
| | |
| | useFallbackData(); |
| | } |
| | } |
| | |
| | function useFallbackData() { |
| | |
| | const fallbackPrompts = { |
| | "Nature": [ |
| | "A majestic waterfall cascading down mossy rocks in a lush rainforest", |
| | "Ocean waves crashing on a rocky shore at sunset with seagulls flying", |
| | "A field of sunflowers swaying in the breeze under a blue sky" |
| | ], |
| | "Animals": [ |
| | "A golden retriever running through a field of flowers at sunset", |
| | "A majestic eagle soaring through clouds above mountain peaks", |
| | "A playful dolphin jumping out of crystal clear ocean water" |
| | ], |
| | "Urban": [ |
| | "City street with cars and pedestrians at night, neon lights reflecting on wet pavement", |
| | "Time-lapse of clouds moving over modern skyscrapers in downtown", |
| | "A busy coffee shop with people working on laptops, warm lighting" |
| | ], |
| | "Fantasy": [ |
| | "A magical portal opening in an ancient forest with glowing particles", |
| | "A dragon flying over a medieval castle at dawn", |
| | "Floating islands in the sky connected by glowing bridges" |
| | ] |
| | }; |
| | |
| | const container = document.getElementById('example-prompts'); |
| | Object.entries(fallbackPrompts).forEach(([category, prompts]) => { |
| | const categoryDiv = document.createElement('div'); |
| | categoryDiv.className = 'prompt-category'; |
| | |
| | const title = document.createElement('h4'); |
| | title.textContent = category; |
| | categoryDiv.appendChild(title); |
| | |
| | prompts.forEach(prompt => { |
| | const span = document.createElement('span'); |
| | span.className = 'example-prompt'; |
| | span.textContent = prompt.substring(0, 50) + (prompt.length > 50 ? '...' : ''); |
| | span.title = prompt; |
| | span.onclick = () => setPrompt(prompt); |
| | categoryDiv.appendChild(span); |
| | }); |
| | |
| | container.appendChild(categoryDiv); |
| | }); |
| | } |
| | |
| | function populateCameraMovements() { |
| | const select = document.getElementById('camera-select'); |
| | modelsData.camera_movements.forEach(movement => { |
| | const option = document.createElement('option'); |
| | option.value = movement.tag; |
| | option.textContent = `${movement.name} - ${movement.description}`; |
| | select.appendChild(option); |
| | }); |
| | } |
| | |
| | function populateVisualEffects() { |
| | const select = document.getElementById('effect-select'); |
| | modelsData.visual_effects.forEach(effect => { |
| | const option = document.createElement('option'); |
| | option.value = effect.tag; |
| | option.textContent = `${effect.name} - ${effect.description}`; |
| | select.appendChild(option); |
| | }); |
| | } |
| | |
| | function populateVideoStyles() { |
| | const select = document.getElementById('style-select'); |
| | modelsData.video_styles.forEach(style => { |
| | const option = document.createElement('option'); |
| | option.value = style.tag; |
| | option.textContent = `${style.name} - ${style.description}`; |
| | select.appendChild(option); |
| | }); |
| | } |
| | |
| | function populateExamplePrompts() { |
| | const container = document.getElementById('example-prompts'); |
| | Object.entries(modelsData.example_prompts).forEach(([category, prompts]) => { |
| | const categoryDiv = document.createElement('div'); |
| | categoryDiv.className = 'prompt-category'; |
| | |
| | const title = document.createElement('h4'); |
| | title.textContent = category; |
| | categoryDiv.appendChild(title); |
| | |
| | prompts.forEach(prompt => { |
| | const span = document.createElement('span'); |
| | span.className = 'example-prompt'; |
| | span.textContent = prompt.substring(0, 50) + (prompt.length > 50 ? '...' : ''); |
| | span.title = prompt; |
| | span.onclick = () => setPrompt(prompt); |
| | categoryDiv.appendChild(span); |
| | }); |
| | |
| | container.appendChild(categoryDiv); |
| | }); |
| | } |
| | |
| | function updateModelInfo() { |
| | const modelId = document.getElementById('model-select').value; |
| | const model = modelsData.models[modelId]; |
| | const infoDiv = document.getElementById('model-info'); |
| | infoDiv.textContent = `${model.description} | Type: ${model.type}`; |
| | } |
| | |
| | function switchMode(mode) { |
| | currentMode = mode; |
| | |
| | |
| | document.querySelectorAll('.tab').forEach(tab => tab.classList.remove('active')); |
| | event.target.classList.add('active'); |
| | |
| | |
| | document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active')); |
| | document.getElementById(`${mode}-mode`).classList.add('active'); |
| | |
| | |
| | if (mode === 'image') { |
| | const modelSelect = document.getElementById('model-select'); |
| | modelSelect.value = 'stable-video-diffusion'; |
| | updateModelInfo(); |
| | } |
| | } |
| | |
| | function handleImageUpload(event) { |
| | const file = event.target.files[0]; |
| | if (!file) return; |
| | |
| | const reader = new FileReader(); |
| | reader.onload = (e) => { |
| | uploadedImage = e.target.result; |
| | const preview = document.getElementById('preview-image'); |
| | preview.src = uploadedImage; |
| | preview.style.display = 'block'; |
| | document.getElementById('upload-area').classList.add('has-image'); |
| | }; |
| | reader.readAsDataURL(file); |
| | } |
| | |
| | function setPrompt(text) { |
| | document.getElementById('prompt').value = text; |
| | document.getElementById('prompt').dispatchEvent(new Event('input')); |
| | } |
| | |
| | function showStatus(message, type) { |
| | const status = document.getElementById('status'); |
| | status.textContent = message; |
| | status.className = 'status ' + type; |
| | } |
| | |
| | async function generateVideo() { |
| | if (currentMode === 'text') { |
| | await generateTextToVideo(); |
| | } else { |
| | await generateImageToVideo(); |
| | } |
| | } |
| | |
| | async function generateTextToVideo() { |
| | const prompt = document.getElementById('prompt').value.trim(); |
| | const model = document.getElementById('model-select').value; |
| | const cameraMovement = document.getElementById('camera-select').value; |
| | const visualEffect = document.getElementById('effect-select').value; |
| | const style = document.getElementById('style-select').value; |
| | |
| | if (!prompt) { |
| | showStatus('Please enter a prompt', 'error'); |
| | return; |
| | } |
| | |
| | if (prompt.length < 3) { |
| | showStatus('Prompt must be at least 3 characters long', 'error'); |
| | return; |
| | } |
| | |
| | |
| | const generateBtn = document.querySelector('.generate-btn'); |
| | generateBtn.disabled = true; |
| | generateBtn.textContent = '🎬 Generating...'; |
| | document.getElementById('loader').style.display = 'block'; |
| | document.getElementById('video-container').style.display = 'none'; |
| | document.getElementById('video-actions').style.display = 'none'; |
| | document.getElementById('video-info').style.display = 'none'; |
| | showStatus('🎨 Creating your video... This may take 30-120 seconds depending on the model', 'info'); |
| | |
| | try { |
| | const response = await fetch('/api/generate-video', { |
| | method: 'POST', |
| | headers: { 'Content-Type': 'application/json' }, |
| | body: JSON.stringify({ |
| | prompt, |
| | |
| | model: model === 'cogvideox-5b' || model === 'cogvideox-2b' ? 'cogvideox' : (model === 'demo' ? 'demo' : 'hailuo'), |
| | camera_movement: cameraMovement, |
| | visual_effect: visualEffect, |
| | style: style |
| | }) |
| | }); |
| | |
| | const data = await response.json(); |
| | |
| | if (!response.ok || data.error) { |
| | throw new Error(data.error || 'Failed to generate video'); |
| | } |
| | |
| | |
| | currentVideoUrl = data.video_url; |
| | const videoOutput = document.getElementById('video-output'); |
| | videoOutput.src = currentVideoUrl; |
| | document.getElementById('video-container').style.display = 'block'; |
| | document.getElementById('video-actions').style.display = 'flex'; |
| | |
| | |
| | const videoInfo = document.getElementById('video-info'); |
| | videoInfo.innerHTML = ` |
| | <strong>Model:</strong> ${data.model_name}<br> |
| | <strong>Prompt:</strong> ${data.prompt}<br> |
| | ${data.enhanced_prompt !== data.prompt ? `<strong>Enhanced:</strong> ${data.enhanced_prompt}<br>` : ''} |
| | <strong>Generated:</strong> ${new Date(data.timestamp).toLocaleString()} |
| | `; |
| | videoInfo.style.display = 'block'; |
| | |
| | showStatus('✅ Video generated successfully!', 'success'); |
| | videoOutput.play().catch(() => {}); |
| | |
| | } catch (error) { |
| | console.error('Error:', error); |
| | showStatus('❌ Error: ' + error.message, 'error'); |
| | } finally { |
| | generateBtn.disabled = false; |
| | generateBtn.textContent = '🎬 Generate Video'; |
| | document.getElementById('loader').style.display = 'none'; |
| | } |
| | } |
| | |
| | async function generateImageToVideo() { |
| | |
| | showStatus('⚠️ Image-to-Video is not available in this hosted version. Use Local mode for this feature.', 'error'); |
| | } |
| | |
| | async function downloadVideo() { |
| | if (!currentVideoUrl) return; |
| | |
| | try { |
| | showStatus('📥 Preparing download...', 'info'); |
| | |
| | const response = await fetch(currentVideoUrl); |
| | const blob = await response.blob(); |
| | const url = window.URL.createObjectURL(blob); |
| | const a = document.createElement('a'); |
| | a.href = url; |
| | a.download = `ai-video-${Date.now()}.mp4`; |
| | document.body.appendChild(a); |
| | a.click(); |
| | window.URL.revokeObjectURL(url); |
| | document.body.removeChild(a); |
| | |
| | showStatus('✅ Download started!', 'success'); |
| | } catch (error) { |
| | console.error('Download error:', error); |
| | showStatus('❌ Failed to download video', 'error'); |
| | } |
| | } |
| | |
| | function shareVideo() { |
| | if (!currentVideoUrl) return; |
| | |
| | if (navigator.share) { |
| | navigator.share({ |
| | title: 'AI Generated Video', |
| | text: 'Check out this AI-generated video!', |
| | url: currentVideoUrl |
| | }).catch(err => console.log('Error sharing:', err)); |
| | } else { |
| | |
| | navigator.clipboard.writeText(currentVideoUrl).then(() => { |
| | showStatus('🔗 Video URL copied to clipboard!', 'success'); |
| | }); |
| | } |
| | } |
| | |
| | async function checkServerHealth() { |
| | try { |
| | const response = await fetch('/api/health'); |
| | const data = await response.json(); |
| | if (data.status === 'healthy') { |
| | console.log('✅ API is healthy'); |
| | } |
| | } catch (error) { |
| | showStatus('⚠️ Cannot connect to API. If deploying on Vercel, ensure functions are enabled.', 'error'); |
| | } |
| | } |
| | </script> |
| | </body> |
| | </html> |
| |
|