|
<!DOCTYPE html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>AI Media Generator - Images & Videos</title> |
|
<script src="https://cdn.tailwindcss.com"></script> |
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> |
|
<style> |
|
.gradient-bg { |
|
background: linear-gradient(135deg, #6E45E2 0%, #88D3CE 100%); |
|
} |
|
.btn-hover:hover { |
|
transform: translateY(-2px); |
|
box-shadow: 0 10px 25px -5px rgba(110, 69, 226, 0.4); |
|
} |
|
.fade-in { |
|
animation: fadeIn 0.5s ease-in; |
|
} |
|
.media-placeholder { |
|
background: linear-gradient(45deg, #f3f4f6 25%, #e5e7eb 25%, #e5e7eb 50%, #f3f4f6 50%, #f3f4f6 75%, #e5e7eb 75%); |
|
background-size: 20px 20px; |
|
} |
|
@keyframes fadeIn { |
|
from { opacity: 0; transform: translateY(10px); } |
|
to { opacity: 1; transform: translateY(0); } |
|
} |
|
@keyframes pulse { |
|
0%, 100% { opacity: 1; } |
|
50% { opacity: 0.5; } |
|
} |
|
.animate-pulse { |
|
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; |
|
} |
|
.tab-active { |
|
border-bottom: 3px solid #6E45E2; |
|
color: #6E45E2; |
|
font-weight: 600; |
|
} |
|
</style> |
|
</head> |
|
<body class="bg-gray-50 font-sans antialiased"> |
|
|
|
<header class="gradient-bg text-white shadow-lg sticky top-0 z-50"> |
|
<div class="container mx-auto px-4 py-3 flex justify-between items-center"> |
|
<div class="flex items-center space-x-2"> |
|
<i class="fas fa-magic text-2xl"></i> |
|
<h1 class="text-xl font-bold tracking-tight">AI Media Generator</h1> |
|
</div> |
|
<nav class="hidden md:block"> |
|
<ul class="flex space-x-6"> |
|
<li><a href="#" class="hover:opacity-80 transition">Home</a></li> |
|
<li><a href="#" class="hover:opacity-80 transition">API Docs</a></li> |
|
<li><a href="#" class="hover:opacity-80 transition">Examples</a></li> |
|
<li><a href="#" class="hover:opacity-80 transition">Help</a></li> |
|
</ul> |
|
</nav> |
|
<div class="flex items-center space-x-4"> |
|
<button id="api-key-btn" class="px-3 py-1.5 text-sm rounded-full bg-white bg-opacity-20 hover:bg-opacity-30 transition"> |
|
<i class="fas fa-key mr-1"></i> API Key |
|
</button> |
|
</div> |
|
</div> |
|
</header> |
|
|
|
<main class="container mx-auto px-4 py-8"> |
|
|
|
<section class="text-center mb-12"> |
|
<div class="max-w-3xl mx-auto"> |
|
<h1 class="text-4xl md:text-5xl font-bold mb-4 bg-gradient-to-r from-indigo-500 to-teal-400 bg-clip-text text-transparent"> |
|
Create Stunning AI Media |
|
</h1> |
|
<p class="text-lg text-gray-600 mb-8"> |
|
Generate images and videos from text using cutting-edge AI models from Hugging Face |
|
</p> |
|
<div class="flex flex-wrap justify-center gap-3"> |
|
<span class="px-3 py-1 bg-indigo-100 text-indigo-800 rounded-full text-sm font-medium"> |
|
<i class="fas fa-bolt mr-1"></i> Stable Diffusion |
|
</span> |
|
<span class="px-3 py-1 bg-purple-100 text-purple-800 rounded-full text-sm font-medium"> |
|
<i class="fas fa-image mr-1"></i> Kandinsky |
|
</span> |
|
<span class="px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-sm font-medium"> |
|
<i class="fas fa-video mr-1"></i> ModelScope |
|
</span> |
|
</div> |
|
</div> |
|
</section> |
|
|
|
|
|
<section class="mb-16 bg-white rounded-2xl shadow-lg overflow-hidden border border-gray-100 max-w-6xl mx-auto"> |
|
|
|
<div class="flex border-b border-gray-100"> |
|
<button id="image-tab" class="tab-button flex-1 py-4 px-6 font-medium text-gray-500 text-center tab-active" data-media-type="image"> |
|
<i class="fas fa-image mr-2"></i> Image Generation |
|
</button> |
|
<button id="video-tab" class="tab-button flex-1 py-4 px-6 font-medium text-gray-500 text-center" data-media-type="video"> |
|
<i class="fas fa-video mr-2"></i> Video Generation |
|
</button> |
|
</div> |
|
|
|
<div class="grid grid-cols-1 lg:grid-cols-3"> |
|
|
|
<div class="lg:col-span-2 p-6 md:p-8 border-b lg:border-b-0 lg:border-r border-gray-100"> |
|
<div class="space-y-6"> |
|
|
|
<div> |
|
<div class="flex justify-between items-center mb-2"> |
|
<label class="flex items-center text-sm font-medium text-gray-700"> |
|
<i class="fas fa-comment-dots mr-2 text-indigo-500"></i> |
|
<span id="prompt-label">Image Prompt</span> |
|
</label> |
|
<button id="suggest-btn" class="text-xs text-indigo-600 hover:text-indigo-800"> |
|
<i class="fas fa-lightbulb mr-1"></i> Need inspiration? |
|
</button> |
|
</div> |
|
<div class="relative"> |
|
<textarea id="prompt" rows="5" class="w-full p-4 border border-gray-200 rounded-lg focus:ring-2 focus:ring-indigo-300 focus:border-indigo-300 resize-none" placeholder="Describe what you want to generate..."></textarea> |
|
<div id="char-count" class="absolute bottom-2 right-2 text-xs text-gray-400 bg-white px-1 rounded">0/500</div> |
|
</div> |
|
</div> |
|
|
|
|
|
<div> |
|
<label class="block text-sm font-medium text-gray-700 mb-2 flex items-center"> |
|
<i class="fas fa-robot mr-2 text-indigo-500"></i> AI Model |
|
</label> |
|
<div class="grid grid-cols-2 sm:grid-cols-3 gap-3" id="model-selectors"> |
|
|
|
</div> |
|
</div> |
|
|
|
|
|
<div class="border-t border-gray-100 pt-4"> |
|
<div class="flex justify-between items-center mb-2"> |
|
<label class="flex items-center text-sm font-medium text-gray-700 cursor-pointer" id="toggle-advanced"> |
|
<i class="fas fa-sliders-h mr-2 text-indigo-500"></i> Advanced Options |
|
<i class="fas fa-chevron-down text-xs ml-2 transition-transform" id="advanced-arrow"></i> |
|
</label> |
|
</div> |
|
<div id="advanced-options" class="hidden space-y-4"> |
|
|
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="p-6 md:p-8"> |
|
<div class="h-full flex flex-col"> |
|
<h2 class="text-lg font-semibold mb-4 text-gray-800 flex items-center"> |
|
<i class="fas fa-eye mr-2 text-indigo-500"></i> |
|
<span id="preview-label">Image Preview</span> |
|
</h2> |
|
|
|
|
|
<div class="flex-1 flex flex-col"> |
|
<div id="media-placeholder" class="media-placeholder rounded-lg overflow-hidden mb-4 flex items-center justify-center" style="aspect-ratio: 1/1;"> |
|
<div class="text-center p-6"> |
|
<i class="fas fa-image text-4xl text-gray-300 mb-3" id="placeholder-icon"></i> |
|
<p class="text-gray-400 text-sm" id="placeholder-text">Your generated image will appear here</p> |
|
</div> |
|
</div> |
|
<div id="image-preview" class="hidden rounded-lg overflow-hidden mb-4" style="aspect-ratio: 1/1;"> |
|
<img id="generated-image" class="w-full h-full object-contain bg-white"> |
|
</div> |
|
<div id="video-preview" class="hidden rounded-lg overflow-hidden mb-4" style="aspect-ratio: 16/9;"> |
|
<video id="generated-video" controls class="w-full h-full bg-black"> |
|
Your browser does not support the video tag. |
|
</video> |
|
</div> |
|
|
|
|
|
<button id="generate-btn" class="generate-btn gradient-bg text-white rounded-lg font-medium py-3 px-4 mt-auto btn-hover transition transform flex items-center justify-center"> |
|
<i class="fas fa-magic mr-2"></i> Generate Image |
|
</button> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</section> |
|
|
|
|
|
<section class="mb-12"> |
|
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center mb-6"> |
|
<h2 class="text-2xl font-bold text-gray-800 mb-3 sm:mb-0">Your Creations</h2> |
|
<div class="flex space-x-3"> |
|
<button id="clear-history" class="text-sm px-3 py-1.5 border border-gray-300 rounded-lg hover:bg-gray-50 transition"> |
|
<i class="fas fa-trash mr-1"></i> Clear History |
|
</button> |
|
</div> |
|
</div> |
|
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6" id="media-grid"> |
|
|
|
</div> |
|
|
|
<div id="empty-state" class="text-center py-12"> |
|
<i class="fas fa-image text-4xl text-gray-300 mb-3"></i> |
|
<h3 class="text-lg font-medium text-gray-500 mb-1">No generated media yet</h3> |
|
<p class="text-gray-400 text-sm">Create your first image or video using the generator above</p> |
|
</div> |
|
</section> |
|
</main> |
|
|
|
|
|
<div id="api-key-modal" class="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4 hidden"> |
|
<div class="bg-white rounded-xl max-w-md w-full p-6 animate-fade-in"> |
|
<div class="flex justify-between items-center mb-4"> |
|
<h3 class="text-lg font-semibold text-gray-800">Hugging Face API Key</h3> |
|
<button id="close-api-modal" class="text-gray-400 hover:text-gray-500"> |
|
<i class="fas fa-times"></i> |
|
</button> |
|
</div> |
|
|
|
<div class="space-y-4"> |
|
<div> |
|
<label class="block text-sm font-medium text-gray-700 mb-1">Your Hugging Face Token</label> |
|
<input id="api-key-input" type="password" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-300 focus:border-indigo-300" placeholder="hf_xxxxxxxxxxxxxxxx"> |
|
<p class="text-xs text-gray-500 mt-1">Get your token from <a href="https://huggingface.co/settings/tokens" target="_blank" class="text-indigo-600 hover:underline">Hugging Face settings</a></p> |
|
</div> |
|
|
|
<div class="bg-blue-50 border border-blue-100 rounded-lg p-3"> |
|
<div class="flex"> |
|
<i class="fas fa-info-circle text-blue-500 mt-0.5 mr-2"></i> |
|
<p class="text-xs text-blue-700">This key is stored locally in your browser and used to authenticate with Hugging Face API.</p> |
|
</div> |
|
</div> |
|
|
|
<div class="flex justify-end space-x-3 pt-2"> |
|
<button id="cancel-api-btn" class="px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 transition">Cancel</button> |
|
<button id="save-api-btn" class="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition">Save Key</button> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
|
|
<div id="generation-modal" class="fixed inset-0 bg-black bg-opacity-80 z-50 flex items-center justify-center p-4 hidden"> |
|
<div class="bg-white rounded-xl max-w-md w-full overflow-hidden animate-fade-in"> |
|
<div class="p-6"> |
|
<div class="text-center mb-4"> |
|
<div class="w-16 h-16 bg-indigo-100 rounded-full flex items-center justify-center mx-auto mb-4"> |
|
<i class="fas fa-cog fa-spin text-2xl text-indigo-600"></i> |
|
</div> |
|
<h3 class="text-lg font-semibold mb-1" id="modal-title">Generating Your Media</h3> |
|
<p class="text-gray-500 text-sm" id="modal-subtitle">This may take a moment depending on the model</p> |
|
</div> |
|
|
|
<div class="mb-4"> |
|
<div class="w-full bg-gray-200 rounded-full h-2.5 mb-2"> |
|
<div id="progress-bar" class="bg-gradient-to-r from-indigo-500 to-teal-400 h-2.5 rounded-full" style="width: 0%"></div> |
|
</div> |
|
<div class="flex justify-between text-xs text-gray-500"> |
|
<span id="progress-text">0%</span> |
|
<span id="time-remaining">Estimating time...</span> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div class="border-t border-gray-100 px-6 py-3"> |
|
<button id="cancel-generation" class="text-gray-500 text-sm hover:text-gray-700 transition w-full py-2"> |
|
Cancel Generation |
|
</button> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
|
|
<footer class="bg-gray-800 text-white py-8"> |
|
<div class="container mx-auto px-4"> |
|
<div class="flex flex-col md:flex-row justify-between items-center"> |
|
<div class="mb-4 md:mb-0"> |
|
<div class="flex items-center"> |
|
<i class="fas fa-magic text-xl mr-2"></i> |
|
<span class="font-medium">AI Media Generator</span> |
|
</div> |
|
<p class="text-gray-400 text-sm mt-1">Powered by Hugging Face models</p> |
|
</div> |
|
<div class="flex space-x-6"> |
|
<a href="#" class="hover:text-indigo-300 transition">Terms</a> |
|
<a href="#" class="hover:text-indigo-300 transition">Privacy</a> |
|
<a href="#" class="hover:text-indigo-300 transition">Docs</a> |
|
<a href="#" class="hover:text-indigo-300 transition">GitHub</a> |
|
</div> |
|
</div> |
|
<div class="border-t border-gray-700 mt-6 pt-6 text-center text-gray-400 text-sm"> |
|
<p>© 2023 AI Media Generator. Not affiliated with Hugging Face or any model providers.</p> |
|
</div> |
|
</div> |
|
</footer> |
|
|
|
<script> |
|
|
|
const MODELS = { |
|
image: { |
|
"stablediffusion": { |
|
name: "Stable Diffusion", |
|
endpoint: "https://api-inference.huggingface.co/models/runwayml/stable-diffusion-v1-5", |
|
description: "High quality images (default model)", |
|
icon: "fas fa-bolt", |
|
free: true, |
|
params: { |
|
height: 512, |
|
width: 512, |
|
steps: 25, |
|
guidance_scale: 7.5 |
|
} |
|
}, |
|
"kandinsky": { |
|
name: "Kandinsky 2.2", |
|
endpoint: "https://api-inference.huggingface.co/models/kandinsky-community/kandinsky-2-2-decoder", |
|
description: "Excellent artistic generations", |
|
icon: "fas fa-paint-brush", |
|
free: true, |
|
params: { |
|
height: 512, |
|
width: 512, |
|
prior_steps: 25, |
|
decoder_steps: 50, |
|
guidance_scale: 4.0 |
|
} |
|
} |
|
}, |
|
video: { |
|
"modelscope": { |
|
name: "ModelScope", |
|
endpoint: "https://api-inference.huggingface.co/models/damo-vilab/text-to-video-ms-1.7b", |
|
description: "Text-to-video (basic quality)", |
|
icon: "fas fa-video", |
|
free: true, |
|
params: { |
|
height: 576, |
|
width: 1024, |
|
num_frames: 24, |
|
num_inference_steps: 50, |
|
fps: 8 |
|
} |
|
}, |
|
"zeroscope": { |
|
name: "Zeroscope V2", |
|
endpoint: "https://api-inference.huggingface.co/models/cerspense/zeroscope_v2_576w", |
|
description: "Higher quality video", |
|
icon: "fas fa-film", |
|
free: true, |
|
params: { |
|
height: 576, |
|
width: 1024, |
|
num_frames: 24, |
|
num_inference_steps: 35, |
|
fps: 12 |
|
} |
|
} |
|
} |
|
}; |
|
|
|
|
|
const DEFAULT_MODELS = { |
|
image: "stablediffusion", |
|
video: "zeroscope" |
|
}; |
|
|
|
|
|
let state = { |
|
apiKey: localStorage.getItem('hf_api_key') || '', |
|
mediaType: 'image', |
|
selectedModel: DEFAULT_MODELS.image, |
|
videos: JSON.parse(localStorage.getItem('generated_media')) || [], |
|
advancedOptions: { |
|
image: { |
|
resolution: 512, |
|
steps: 25, |
|
cfgScale: 7.5, |
|
seed: null |
|
}, |
|
video: { |
|
resolution: 576, |
|
duration: 4, |
|
fps: 12, |
|
seed: null |
|
} |
|
} |
|
}; |
|
|
|
|
|
const promptInput = document.getElementById('prompt'); |
|
const charCount = document.getElementById('char-count'); |
|
const generateBtn = document.getElementById('generate-btn'); |
|
const mediaPlaceholder = document.getElementById('media-placeholder'); |
|
const imagePreview = document.getElementById('image-preview'); |
|
const generatedImage = document.getElementById('generated-image'); |
|
const videoPreview = document.getElementById('video-preview'); |
|
const generatedVideo = document.getElementById('generated-video'); |
|
const mediaGrid = document.getElementById('media-grid'); |
|
const emptyState = document.getElementById('empty-state'); |
|
const clearHistoryBtn = document.getElementById('clear-history'); |
|
const suggestBtn = document.getElementById('suggest-btn'); |
|
const advancedOptions = document.getElementById('advanced-options'); |
|
const advancedArrow = document.getElementById('advanced-arrow'); |
|
const toggleAdvanced = document.getElementById('toggle-advanced'); |
|
const modelSelectors = document.getElementById('model-selectors'); |
|
const imageTab = document.getElementById('image-tab'); |
|
const videoTab = document.getElementById('video-tab'); |
|
const promptLabel = document.getElementById('prompt-label'); |
|
const previewLabel = document.getElementById('preview-label'); |
|
const placeholderIcon = document.getElementById('placeholder-icon'); |
|
const placeholderText = document.getElementById('placeholder-text'); |
|
|
|
|
|
const apiKeyModal = document.getElementById('api-key-modal'); |
|
const apiKeyInput = document.getElementById('api-key-input'); |
|
const saveApiBtn = document.getElementById('save-api-btn'); |
|
const cancelApiBtn = document.getElementById('cancel-api-btn'); |
|
const closeApiModal = document.getElementById('close-api-modal'); |
|
const apiKeyBtn = document.getElementById('api-key-btn'); |
|
const generationModal = document.getElementById('generation-modal'); |
|
const progressBar = document.getElementById('progress-bar'); |
|
const progressText = document.getElementById('progress-text'); |
|
const modalTitle = document.getElementById('modal-title'); |
|
const modalSubtitle = document.getElementById('modal-subtitle'); |
|
const cancelGeneration = document.getElementById('cancel-generation'); |
|
const timeRemainingEl = document.getElementById('time-remaining'); |
|
|
|
|
|
function init() { |
|
|
|
setupEventListeners(); |
|
|
|
|
|
updateCharCount(); |
|
|
|
|
|
updateMediaTypeUI(); |
|
renderModelSelectors(); |
|
renderAdvancedOptions(); |
|
|
|
|
|
renderMedia(); |
|
} |
|
|
|
|
|
function setupEventListeners() { |
|
|
|
apiKeyBtn.addEventListener('click', () => { |
|
apiKeyInput.value = state.apiKey; |
|
apiKeyModal.classList.remove('hidden'); |
|
}); |
|
|
|
closeApiModal.addEventListener('click', () => apiKeyModal.classList.add('hidden')); |
|
cancelApiBtn.addEventListener('click', () => apiKeyModal.classList.add('hidden')); |
|
|
|
saveApiBtn.addEventListener('click', () => { |
|
const newKey = apiKeyInput.value.trim(); |
|
if (newKey) { |
|
state.apiKey = newKey; |
|
localStorage.setItem('hf_api_key', newKey); |
|
apiKeyModal.classList.add('hidden'); |
|
|
|
|
|
if (newKey) { |
|
testHuggingFaceAPI(newKey); |
|
} |
|
} |
|
}); |
|
|
|
|
|
promptInput.addEventListener('input', updateCharCount); |
|
|
|
|
|
imageTab.addEventListener('click', () => switchMediaType('image')); |
|
videoTab.addEventListener('click', () => switchMediaType('video')); |
|
|
|
|
|
generateBtn.addEventListener('click', generateMedia); |
|
|
|
|
|
clearHistoryBtn.addEventListener('click', clearHistory); |
|
|
|
|
|
suggestBtn.addEventListener('click', suggestPrompt); |
|
|
|
|
|
toggleAdvanced.addEventListener('click', () => { |
|
advancedOptions.classList.toggle('hidden'); |
|
advancedArrow.classList.toggle('rotate-180'); |
|
}); |
|
|
|
|
|
cancelGeneration.addEventListener('click', () => { |
|
generationModal.classList.add('hidden'); |
|
}); |
|
} |
|
|
|
|
|
async function testHuggingFaceAPI(apiKey) { |
|
try { |
|
const response = await fetch('https://api-inference.huggingface.co/models/runwayml/stable-diffusion-v1-5', { |
|
method: 'GET', |
|
headers: { |
|
'Authorization': `Bearer ${apiKey}` |
|
} |
|
}); |
|
|
|
if (response.ok) { |
|
showToast('API key is valid and working'); |
|
} else { |
|
showToast('API key might not have correct permissions'); |
|
} |
|
} catch (error) { |
|
showToast('Error testing API key'); |
|
console.error('API test failed:', error); |
|
} |
|
} |
|
|
|
|
|
function switchMediaType(type) { |
|
if (state.mediaType === type) return; |
|
|
|
state.mediaType = type; |
|
state.selectedModel = DEFAULT_MODELS[type]; |
|
|
|
|
|
updateMediaTypeUI(); |
|
renderModelSelectors(); |
|
renderAdvancedOptions(); |
|
|
|
|
|
resetPreview(); |
|
} |
|
|
|
|
|
function updateMediaTypeUI() { |
|
|
|
document.querySelectorAll('.tab-button').forEach(btn => { |
|
btn.classList.remove('tab-active', 'text-indigo-600'); |
|
btn.classList.add('text-gray-500'); |
|
}); |
|
|
|
const activeTab = document.getElementById(`${state.mediaType}-tab`); |
|
activeTab.classList.add('tab-active', 'text-indigo-600'); |
|
activeTab.classList.remove('text-gray-500'); |
|
|
|
|
|
const mediaType = state.mediaType; |
|
promptLabel.textContent = `${mediaType.charAt(0).toUpperCase() + mediaType.slice(1)} Prompt`; |
|
previewLabel.textContent = `${mediaType.charAt(0).toUpperCase() + mediaType.slice(1)} Preview`; |
|
placeholderIcon.className = mediaType === 'image' ? 'fas fa-image text-4xl text-gray-300 mb-3' : 'fas fa-video text-4xl text-gray-300 mb-3'; |
|
placeholderText.textContent = mediaType === 'image' ? 'Your generated image will appear here' : 'Your generated video will appear here'; |
|
|
|
|
|
generateBtn.innerHTML = `<i class="fas fa-magic mr-2"></i> Generate ${mediaType.charAt(0).toUpperCase() + mediaType.slice(1)}`; |
|
} |
|
|
|
|
|
function renderModelSelectors() { |
|
modelSelectors.innerHTML = ''; |
|
|
|
const models = MODELS[state.mediaType]; |
|
|
|
for (const [key, model] of Object.entries(models)) { |
|
const isSelected = key === state.selectedModel; |
|
const btn = document.createElement('button'); |
|
btn.className = `model-select-btn px-3 py-2 border rounded-lg text-left transition ${isSelected ? 'bg-indigo-50 border-indigo-200' : 'hover:bg-gray-50 border-gray-200'}`; |
|
btn.dataset.model = key; |
|
|
|
btn.innerHTML = ` |
|
<div class="font-medium ${isSelected ? 'text-indigo-800' : 'text-gray-800'}">${model.name}</div> |
|
<div class="text-xs ${isSelected ? 'text-indigo-600' : 'text-gray-600'}">${model.description}</div> |
|
${model.free ? '<span class="absolute top-1 right-1 text-xs px-1 bg-green-100 text-green-800 rounded">FREE</span>' : ''} |
|
`; |
|
|
|
btn.addEventListener('click', () => { |
|
state.selectedModel = key; |
|
renderModelSelectors(); |
|
}); |
|
|
|
modelSelectors.appendChild(btn); |
|
} |
|
} |
|
|
|
|
|
function renderAdvancedOptions() { |
|
const optionsContainer = document.getElementById('advanced-options'); |
|
const options = state.advancedOptions[state.mediaType]; |
|
|
|
if (state.mediaType === 'image') { |
|
optionsContainer.innerHTML = ` |
|
<!-- Resolution --> |
|
<div> |
|
<label class="block text-xs font-medium text-gray-500 mb-1">Resolution</label> |
|
<div class="grid grid-cols-3 gap-2"> |
|
<button data-res="512" class="res-btn px-2 py-1 text-xs border rounded transition ${options.resolution === 512 ? 'bg-indigo-50 border-indigo-200' : 'hover:bg-gray-50 border-gray-200'}">512x512</button> |
|
<button data-res="768" class="res-btn px-2 py-1 text-xs border rounded transition ${options.resolution === 768 ? 'bg-indigo-50 border-indigo-200' : 'hover:bg-gray-50 border-gray-200'}">768x768</button> |
|
<button data-res="1024" class="res-btn px-2 py-1 text-xs border rounded transition ${options.resolution === 1024 ? 'bg-indigo-50 border-indigo-200' : 'hover:bg-gray-50 border-gray-200'}">1024x1024</button> |
|
</div> |
|
</div> |
|
|
|
<!-- Steps --> |
|
<div> |
|
<label class="block text-xs font-medium text-gray-500 mb-1">Steps (quality vs speed)</label> |
|
<input type="range" id="steps" min="10" max="50" step="1" value="${options.steps}" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer"> |
|
<div class="flex justify-between text-xs text-gray-500 mt-1"> |
|
<span>Fast</span> |
|
<span id="steps-value">${options.steps}</span> |
|
<span>Quality</span> |
|
</div> |
|
</div> |
|
|
|
<!-- Guidance Scale --> |
|
<div> |
|
<label class="block text-xs font-medium text-gray-500 mb-1">Guidance Scale (creativity)</label> |
|
<input type="range" id="cfg-scale" min="1" max="20" step="0.5" value="${options.cfgScale}" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer"> |
|
<div class="flex justify-between text-xs text-gray-500 mt-1"> |
|
<span>Creative</span> |
|
<span id="cfg-scale-value">${options.cfgScale}</span> |
|
<span>Precise</span> |
|
</div> |
|
</div> |
|
|
|
<!-- Seed --> |
|
<div> |
|
<label for="seed" class="block text-xs font-medium text-gray-500 mb-1">Seed (optional)</label> |
|
<input type="number" id="seed" class="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm" placeholder="Random" ${options.seed ? `value="${options.seed}"` : ''}> |
|
</div> |
|
`; |
|
} else { |
|
|
|
optionsContainer.innerHTML = ` |
|
<!-- Resolution --> |
|
<div> |
|
<label class="block text-xs font-medium text-gray-500 mb-1">Resolution</label> |
|
<div class="grid grid-cols-2 gap-2"> |
|
<button data-res="576" class="res-btn px-2 py-1 text-xs border rounded transition ${options.resolution === 576 ? 'bg-indigo-50 border-indigo-200' : 'hover:bg-gray-50 border-gray-200'}">576p (SD)</button> |
|
<button data-res="720" class="res-btn px-2 py-1 text-xs border rounded transition ${options.resolution === 720 ? 'bg-indigo-50 border-indigo-200' : 'hover:bg-gray-50 border-gray-200'}">720p (HD)</button> |
|
</div> |
|
</div> |
|
|
|
<!-- Duration --> |
|
<div> |
|
<label class="block text-xs font-medium text-gray-500 mb-1">Duration (seconds)</label> |
|
<input type="range" id="duration" min="2" max="10" step="1" value="${options.duration}" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer"> |
|
<div class="flex justify-between text-xs text-gray-500 mt-1"> |
|
<span>2s</span> |
|
<span id="duration-value">${options.duration}s</span> |
|
<span>10s</span> |
|
</div> |
|
</div> |
|
|
|
<!-- FPS --> |
|
<div> |
|
<label class="block text-xs font-medium text-gray-500 mb-1">Frame Rate (FPS)</label> |
|
<input type="range" id="fps" min="4" max="24" step="2" value="${options.fps}" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer"> |
|
<div class="flex justify-between text-xs text-gray-500 mt-1"> |
|
<span>4</span> |
|
<span id="fps-value">${options.fps}</span> |
|
<span>24</span> |
|
</div> |
|
</div> |
|
|
|
<!-- Seed --> |
|
<div> |
|
<label for="seed" class="block text-xs font-medium text-gray-500 mb-1">Seed (optional)</label> |
|
<input type="number" id="seed" class="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm" placeholder="Random" ${options.seed ? `value="${options.seed}"` : ''}> |
|
</div> |
|
`; |
|
} |
|
|
|
|
|
setupDynamicEventListeners(); |
|
} |
|
|
|
|
|
function setupDynamicEventListeners() { |
|
|
|
document.querySelectorAll('.res-btn').forEach(btn => { |
|
btn.addEventListener('click', () => { |
|
const resolution = parseInt(btn.dataset.res); |
|
state.advancedOptions[state.mediaType].resolution = resolution; |
|
|
|
|
|
document.querySelectorAll('.res-btn').forEach(b => { |
|
b.classList.remove('bg-indigo-50', 'border-indigo-200'); |
|
b.classList.add('border-gray-200'); |
|
}); |
|
|
|
btn.classList.add('bg-indigo-50', 'border-indigo-200'); |
|
}); |
|
}); |
|
|
|
if (state.mediaType === 'image') { |
|
|
|
const stepsSlider = document.getElementById('steps'); |
|
const stepsValue = document.getElementById('steps-value'); |
|
const cfgScaleSlider = document.getElementById('cfg-scale'); |
|
const cfgScaleValue = document.getElementById('cfg-scale-value'); |
|
const seedInput = document.getElementById('seed'); |
|
|
|
stepsSlider.addEventListener('input', () => { |
|
const value = stepsSlider.value; |
|
stepsValue.textContent = value; |
|
state.advancedOptions.image.steps = parseInt(value); |
|
}); |
|
|
|
cfgScaleSlider.addEventListener('input', () => { |
|
const value = cfgScaleSlider.value; |
|
cfgScaleValue.textContent = value; |
|
state.advancedOptions.image.cfgScale = parseFloat(value); |
|
}); |
|
|
|
seedInput.addEventListener('change', () => { |
|
const value = seedInput.value.trim(); |
|
state.advancedOptions.image.seed = value ? parseInt(value) : null; |
|
}); |
|
} else { |
|
|
|
const durationSlider = document.getElementById('duration'); |
|
const durationValue = document.getElementById('duration-value'); |
|
const fpsSlider = document.getElementById('fps'); |
|
const fpsValue = document.getElementById('fps-value'); |
|
const seedInput = document.getElementById('seed'); |
|
|
|
durationSlider.addEventListener('input', () => { |
|
const value = durationSlider.value; |
|
durationValue.textContent = `${value}s`; |
|
state.advancedOptions.video.duration = parseInt(value); |
|
}); |
|
|
|
fpsSlider.addEventListener('input', () => { |
|
const value = fpsSlider.value; |
|
fpsValue.textContent = value; |
|
state.advancedOptions.video.fps = parseInt(value); |
|
}); |
|
|
|
seedInput.addEventListener('change', () => { |
|
const value = seedInput.value.trim(); |
|
state.advancedOptions.video.seed = value ? parseInt(value) : null; |
|
}); |
|
} |
|
} |
|
|
|
|
|
function resetPreview() { |
|
mediaPlaceholder.classList.remove('hidden'); |
|
imagePreview.classList.add('hidden'); |
|
videoPreview.classList.add('hidden'); |
|
} |
|
|
|
|
|
function updateCharCount() { |
|
const count = promptInput.value.length; |
|
charCount.textContent = `${count}/500`; |
|
|
|
if (count > 500) { |
|
charCount.classList.add('text-red-500'); |
|
} else { |
|
charCount.classList.remove('text-red-500'); |
|
} |
|
} |
|
|
|
|
|
async function generateMedia() { |
|
const prompt = promptInput.value.trim(); |
|
|
|
if (!prompt) { |
|
showToast('Please enter a description'); |
|
return; |
|
} |
|
|
|
if (prompt.length > 500) { |
|
showToast('Description should be 500 characters or less'); |
|
return; |
|
} |
|
|
|
if (!state.apiKey) { |
|
showToast('Please set your Hugging Face API key first'); |
|
apiKeyModal.classList.remove('hidden'); |
|
return; |
|
} |
|
|
|
|
|
showLoading(true); |
|
|
|
|
|
generationModal.classList.remove('hidden'); |
|
progressBar.style.width = '0%'; |
|
progressText.textContent = '0%'; |
|
|
|
try { |
|
const modelConfig = MODELS[state.mediaType][state.selectedModel]; |
|
const advOptions = state.advancedOptions[state.mediaType]; |
|
|
|
|
|
modalTitle.textContent = `Generating ${state.mediaType.charAt(0).toUpperCase() + state.mediaType.slice(1)}`; |
|
modalSubtitle.textContent = 'Initializing model...'; |
|
|
|
|
|
let requestData = { |
|
inputs: prompt, |
|
options: { |
|
use_cache: false, |
|
wait_for_model: true |
|
} |
|
}; |
|
|
|
|
|
if (state.mediaType === 'image') { |
|
|
|
const size = advOptions.resolution; |
|
Object.assign(requestData, { |
|
parameters: { |
|
height: size, |
|
width: size, |
|
num_inference_steps: advOptions.steps, |
|
guidance_scale: advOptions.cfgScale, |
|
seed: advOptions.seed || undefined |
|
} |
|
}); |
|
} else { |
|
|
|
Object.assign(requestData, { |
|
parameters: { |
|
height: advOptions.resolution, |
|
width: Math.round(advOptions.resolution * (16/9)), |
|
num_frames: advOptions.duration * advOptions.fps, |
|
num_inference_steps: modelConfig.params.num_inference_steps, |
|
fps: advOptions.fps, |
|
seed: advOptions.seed || undefined |
|
} |
|
}); |
|
} |
|
|
|
|
|
await updateProgress(10, 'Preparing request...'); |
|
const result = await callHuggingFaceAPI(modelConfig.endpoint, requestData); |
|
|
|
if (!result.ok) { |
|
throw new Error(await getErrorMessage(result)); |
|
} |
|
|
|
await updateProgress(50, 'Processing response...'); |
|
|
|
|
|
let mediaData; |
|
if (state.mediaType === 'image') { |
|
|
|
const imageBlob = await result.blob(); |
|
const imageUrl = URL.createObjectURL(imageBlob); |
|
|
|
mediaData = createMediaObject(prompt, modelConfig.name, { |
|
url: imageUrl, |
|
type: 'image', |
|
resolution: `${advOptions.resolution}x${advOptions.resolution}`, |
|
seed: advOptions.seed || Math.floor(Math.random() * 1000000) |
|
}); |
|
} else { |
|
|
|
const videoBlob = await result.blob(); |
|
const videoUrl = URL.createObjectURL(videoBlob); |
|
|
|
mediaData = createMediaObject(prompt, modelConfig.name, { |
|
url: videoUrl, |
|
type: 'video', |
|
resolution: `${advOptions.resolution}p`, |
|
duration: `${advOptions.duration}s`, |
|
fps: advOptions.fps, |
|
seed: advOptions.seed || Math.floor(Math.random() * 1000000) |
|
}); |
|
} |
|
|
|
|
|
await updateProgress(100, 'Generation complete!'); |
|
|
|
|
|
state.videos.unshift(mediaData); |
|
localStorage.setItem('generated_media', JSON.stringify(state.videos)); |
|
|
|
|
|
showGeneratedMedia(mediaData); |
|
|
|
|
|
setTimeout(() => { |
|
generationModal.classList.add('hidden'); |
|
}, 500); |
|
} catch (error) { |
|
console.error('Error generating media:', error); |
|
handleGenerationError(error.message || 'Failed to generate media'); |
|
} finally { |
|
showLoading(false); |
|
} |
|
} |
|
|
|
|
|
function createMediaObject(prompt, model, additionalData) { |
|
return { |
|
id: Date.now(), |
|
prompt: prompt, |
|
model: model, |
|
timestamp: new Date().toISOString(), |
|
...additionalData |
|
}; |
|
} |
|
|
|
|
|
async function callHuggingFaceAPI(endpoint, data) { |
|
let response; |
|
|
|
try { |
|
response = await fetch(endpoint, { |
|
method: 'POST', |
|
headers: { |
|
'Authorization': `Bearer ${state.apiKey}`, |
|
'Content-Type': 'application/json' |
|
}, |
|
body: JSON.stringify(data) |
|
}); |
|
|
|
if (!response.ok) { |
|
throw new Error(`API responded with status ${response.status}`); |
|
} |
|
|
|
return response; |
|
} catch (error) { |
|
|
|
if (response && response.status === 429) { |
|
const rateLimitReset = response.headers.get('X-RateLimit-Reset'); |
|
if (rateLimitReset) { |
|
const resetTime = new Date(parseInt(rateLimitReset) * 1000); |
|
const timeRemaining = Math.ceil((resetTime - new Date()) / 1000 / 60); |
|
throw new Error(`API rate limit exceeded. Please try again in about ${timeRemaining} minutes.`); |
|
} |
|
} |
|
throw error; |
|
} |
|
} |
|
|
|
|
|
async function getErrorMessage(response) { |
|
try { |
|
const errorData = await response.json(); |
|
if (errorData.error) { |
|
return errorData.error; |
|
} |
|
return `API error: ${response.status} ${response.statusText}`; |
|
} catch (e) { |
|
return `API error: ${response.status} ${response.statusText}`; |
|
} |
|
} |
|
|
|
|
|
function showLoading(isLoading) { |
|
generateBtn.disabled = isLoading; |
|
if (isLoading) { |
|
generateBtn.innerHTML = '<i class="fas fa-cog fa-spin mr-2"></i> Generating...'; |
|
generateBtn.classList.add('cursor-not-allowed', 'opacity-75'); |
|
} else { |
|
generateBtn.innerHTML = `<i class="fas fa-magic mr-2"></i> Generate ${state.mediaType.charAt(0).toUpperCase() + state.mediaType.slice(1)}`; |
|
generateBtn.classList.remove('cursor-not-allowed', 'opacity-75'); |
|
} |
|
} |
|
|
|
|
|
async function updateProgress(percent, message) { |
|
return new Promise(resolve => { |
|
progressBar.style.width = `${percent}%`; |
|
progressText.textContent = `${percent}%`; |
|
modalSubtitle.textContent = message; |
|
|
|
|
|
if (percent < 20) { |
|
const remaining = state.mediaType === 'image' ? '1-2 minutes' : '2-4 minutes'; |
|
timeRemainingEl.textContent = `Approx. ${remaining} remaining`; |
|
} else if (percent < 70) { |
|
timeRemainingEl.textContent = 'Processing...'; |
|
} else { |
|
timeRemainingEl.textContent = 'Finalizing...'; |
|
} |
|
|
|
|
|
setTimeout(resolve, 200); |
|
}); |
|
} |
|
|
|
|
|
function showGeneratedMedia(mediaData) { |
|
|
|
mediaPlaceholder.classList.add('hidden'); |
|
|
|
if (mediaData.type === 'image') { |
|
imagePreview.classList.remove('hidden'); |
|
generatedImage.src = mediaData.url; |
|
generatedImage.alt = mediaData.prompt; |
|
videoPreview.classList.add('hidden'); |
|
} else { |
|
videoPreview.classList.remove('hidden'); |
|
generatedVideo.src = mediaData.url; |
|
generatedVideo.load(); |
|
imagePreview.classList.add('hidden'); |
|
} |
|
|
|
|
|
renderMedia(); |
|
} |
|
|
|
|
|
function handleGenerationError(message) { |
|
modalTitle.textContent = 'Generation Failed'; |
|
modalSubtitle.textContent = message; |
|
progressBar.style.width = '0%'; |
|
progressText.textContent = '0%'; |
|
|
|
|
|
console.error('Generation error:', message); |
|
|
|
|
|
setTimeout(() => { |
|
generationModal.classList.add('hidden'); |
|
}, 5000); |
|
} |
|
|
|
|
|
function renderMedia() { |
|
if (state.videos.length === 0) { |
|
emptyState.classList.remove('hidden'); |
|
mediaGrid.classList.add('hidden'); |
|
return; |
|
} |
|
|
|
emptyState.classList.add('hidden'); |
|
mediaGrid.classList.remove('hidden'); |
|
mediaGrid.innerHTML = ''; |
|
|
|
state.videos.forEach(media => { |
|
const mediaElement = createMediaCard(media); |
|
mediaGrid.appendChild(mediaElement); |
|
|
|
|
|
if (media.id === state.videos[0].id) { |
|
setTimeout(() => { |
|
mediaElement.classList.add('fade-in'); |
|
}, 50); |
|
} |
|
}); |
|
} |
|
|
|
|
|
function createMediaCard(media) { |
|
const modelConfig = MODELS[media.type][media.model] || MODELS.image.stablediffusion; |
|
const date = new Date(media.timestamp); |
|
const formattedDate = date.toLocaleString(undefined, { |
|
month: 'short', |
|
day: 'numeric', |
|
hour: '2-digit', |
|
minute: '2-digit' |
|
}); |
|
|
|
const card = document.createElement('div'); |
|
card.className = 'bg-white rounded-xl shadow-sm overflow-hidden border border-gray-100 transition hover:shadow-md'; |
|
|
|
|
|
const actionButtons = ` |
|
<div class="absolute top-3 right-3 flex space-x-2"> |
|
<button class="w-8 h-8 bg-white rounded-full flex items-center justify-center shadow-sm hover:bg-gray-50" title="Like"> |
|
<i class="fas fa-heart text-gray-400 hover:text-red-500"></i> |
|
</button> |
|
<a href="${media.url}" download="${media.type}-${media.id}.${media.type === 'image' ? 'png' : 'mp4'}" |
|
class="w-8 h-8 bg-white rounded-full flex items-center justify-center shadow-sm hover:bg-gray-50" title="Download"> |
|
<i class="fas fa-download text-gray-400 hover:text-indigo-500"></i> |
|
</a> |
|
</div> |
|
<div class="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/80 to-transparent p-4 pt-8"> |
|
<h3 class="text-white font-medium text-sm">${media.prompt.substring(0, 40)}${media.prompt.length > 40 ? '...' : ''}</h3> |
|
</div> |
|
`; |
|
|
|
const infoSection = ` |
|
<div class="p-3"> |
|
<div class="flex justify-between items-center mb-1"> |
|
<div class="flex items-center space-x-2"> |
|
<span class="text-xs px-2 py-1 rounded-full ${media.model === 'stablediffusion' ? 'bg-indigo-100 text-indigo-800' : |
|
media.model === 'zeroscope' ? 'bg-blue-100 text-blue-800' : 'bg-gray-100 text-gray-800'}"> |
|
<i class="${modelConfig.icon} mr-1"></i> ${modelConfig.name} |
|
</span> |
|
<span class="text-xs text-gray-500">${media.resolution}</span> |
|
${media.duration ? `<span class="text-xs text-gray-500">${media.duration}</span>` : ''} |
|
</div> |
|
<button class="text-xs text-indigo-600 hover:text-indigo-800" onclick="regenerateMedia('${media.id}')"> |
|
<i class="fas fa-sync-alt mr-1"></i> Redo |
|
</button> |
|
</div> |
|
<div class="flex justify-between items-center text-xs text-gray-500"> |
|
<span>${formattedDate}</span> |
|
<button class="text-red-500 hover:text-red-700" onclick="deleteMedia('${media.id}', this)"> |
|
<i class="fas fa-trash"></i> |
|
</button> |
|
</div> |
|
</div> |
|
`; |
|
|
|
if (media.type === 'image') { |
|
card.innerHTML = ` |
|
<div class="relative"> |
|
<img src="${media.url}" alt="${media.prompt}" class="w-full h-48 object-cover"> |
|
${actionButtons} |
|
</div> |
|
${infoSection} |
|
`; |
|
} else { |
|
card.innerHTML = ` |
|
<div class="relative" style="height: 192px;"> |
|
<video src="${media.url}" class="w-full h-full object-cover" muted loop playsinline></video> |
|
${actionButtons} |
|
</div> |
|
${infoSection} |
|
`; |
|
|
|
|
|
const observer = new IntersectionObserver((entries) => { |
|
entries.forEach(entry => { |
|
const video = entry.target.querySelector('video'); |
|
if (!video) return; |
|
|
|
if (entry.isIntersecting) { |
|
video.play().catch(() => {}); |
|
} else { |
|
video.pause(); |
|
} |
|
}); |
|
}, { threshold: 0.1 }); |
|
|
|
observer.observe(card); |
|
} |
|
|
|
return card; |
|
} |
|
|
|
|
|
function clearHistory() { |
|
if (confirm('Are you sure you want to clear your generation history?')) { |
|
state.videos = []; |
|
localStorage.setItem('generated_media', JSON.stringify(state.videos)); |
|
renderMedia(); |
|
} |
|
} |
|
|
|
|
|
function suggestPrompt() { |
|
const imageSuggestions = [ |
|
"A futuristic cityscape at sunset with flying cars and neon lights, cinematic shot", |
|
"Majestic eagle soaring over snow-capped mountains in slow motion", |
|
"Underwater coral reef with tropical fish and sunlight streaming through", |
|
"Time-lapse of flowers blooming in a vibrant spring garden", |
|
"Cyberpunk hacker working in a neon-lit digital workspace", |
|
"Close-up of a robot preparing coffee in a futuristic cafe" |
|
]; |
|
|
|
const videoSuggestions = [ |
|
"Aerial view of a winding road through autumn forests", |
|
"Panda cub playing in a bamboo forest, slow motion", |
|
"Surreal landscape with floating islands and waterfalls", |
|
"Abstract liquid motion in vibrant colors, psychedelic", |
|
"Robot dancing in a futuristic nightclub, neon lights", |
|
"Time-lapse of clouds moving over a mountain range" |
|
]; |
|
|
|
const suggestions = state.mediaType === 'image' ? imageSuggestions : videoSuggestions; |
|
const randomSuggestion = suggestions[Math.floor(Math.random() * suggestions.length)]; |
|
|
|
promptInput.value = randomSuggestion; |
|
updateCharCount(); |
|
} |
|
|
|
|
|
window.regenerateMedia = function(id) { |
|
const media = state.videos.find(v => v.id == id); |
|
if (media) { |
|
|
|
if (state.mediaType !== media.type) { |
|
state.mediaType = media.type; |
|
updateMediaTypeUI(); |
|
renderModelSelectors(); |
|
renderAdvancedOptions(); |
|
} |
|
|
|
|
|
promptInput.value = media.prompt; |
|
state.selectedModel = media.model; |
|
|
|
|
|
renderModelSelectors(); |
|
|
|
|
|
window.scrollTo({ top: 0, behavior: 'smooth' }); |
|
|
|
|
|
promptInput.focus(); |
|
|
|
|
|
showToast(`Loaded settings from previous generation. Click "Generate" to recreate.`); |
|
} |
|
}; |
|
|
|
|
|
window.deleteMedia = function(id, button) { |
|
if (confirm('Delete this generated media?')) { |
|
state.videos = state.videos.filter(v => v.id != id); |
|
localStorage.setItem('generated_media', JSON.stringify(state.videos)); |
|
|
|
|
|
const card = button.closest('.bg-white'); |
|
card.classList.add('opacity-0', 'scale-95', 'transition-all', 'duration-300'); |
|
setTimeout(() => { |
|
renderMedia(); |
|
}, 300); |
|
} |
|
}; |
|
|
|
|
|
function showToast(message) { |
|
const toast = document.createElement('div'); |
|
toast.className = 'fixed bottom-4 left-1/2 transform -translate-x-1/2 bg-gray-800 text-white px-4 py-2 rounded-lg shadow-lg flex items-center animate-fade-in'; |
|
toast.innerHTML = ` |
|
<i class="fas fa-info-circle mr-2"></i> |
|
<span>${message}</span> |
|
`; |
|
|
|
document.body.appendChild(toast); |
|
|
|
setTimeout(() => { |
|
toast.classList.add('opacity-0', 'transition-opacity', 'duration-300'); |
|
setTimeout(() => toast.remove(), 300); |
|
}, 3000); |
|
} |
|
|
|
|
|
document.addEventListener('DOMContentLoaded', init); |
|
</script> |
|
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=AV791961/video" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> |
|
</html> |