video / index.html
AV791961's picture
Add 1 files
2abafce verified
<!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 -->
<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">
<!-- Hero Section -->
<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>
<!-- Generator Section -->
<section class="mb-16 bg-white rounded-2xl shadow-lg overflow-hidden border border-gray-100 max-w-6xl mx-auto">
<!-- Tabs -->
<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">
<!-- Input Panel -->
<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">
<!-- Prompt Input -->
<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>
<!-- Model Selection -->
<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">
<!-- Models will be loaded here based on media type -->
</div>
</div>
<!-- Advanced Options -->
<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">
<!-- Options will be loaded here based on media type -->
</div>
</div>
</div>
</div>
<!-- Preview Panel -->
<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>
<!-- Preview Container -->
<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>
<!-- Generation Button -->
<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>
<!-- Generated Media 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">
<!-- Media will be loaded here -->
</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>
<!-- API Key Modal -->
<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>
<!-- Generation Modal -->
<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 -->
<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>
// Configuration for models using free Hugging Face inference API endpoints
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
}
}
}
};
// Default options
const DEFAULT_MODELS = {
image: "stablediffusion",
video: "zeroscope" // Changed to zeroscope as it provides better quality
};
// App State
let state = {
apiKey: localStorage.getItem('hf_api_key') || '',
mediaType: 'image', // 'image' or 'video'
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
}
}
};
// DOM Elements
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');
// Modal Elements
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');
// Initialize the app
function init() {
// Set up event listeners
setupEventListeners();
// Update character counter
updateCharCount();
// Load initial UI
updateMediaTypeUI();
renderModelSelectors();
renderAdvancedOptions();
// Load saved media
renderMedia();
}
// Set up all event listeners
function setupEventListeners() {
// API Key Management
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');
// Test the API key if we have one
if (newKey) {
testHuggingFaceAPI(newKey);
}
}
});
// Prompt input
promptInput.addEventListener('input', updateCharCount);
// Media type tabs
imageTab.addEventListener('click', () => switchMediaType('image'));
videoTab.addEventListener('click', () => switchMediaType('video'));
// Generate button
generateBtn.addEventListener('click', generateMedia);
// Clear history button
clearHistoryBtn.addEventListener('click', clearHistory);
// Suggest button
suggestBtn.addEventListener('click', suggestPrompt);
// Advanced options toggle
toggleAdvanced.addEventListener('click', () => {
advancedOptions.classList.toggle('hidden');
advancedArrow.classList.toggle('rotate-180');
});
// Cancel generation
cancelGeneration.addEventListener('click', () => {
generationModal.classList.add('hidden');
});
}
// Test Hugging Face API key
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);
}
}
// Switch between image and video generation
function switchMediaType(type) {
if (state.mediaType === type) return;
state.mediaType = type;
state.selectedModel = DEFAULT_MODELS[type];
// Update UI
updateMediaTypeUI();
renderModelSelectors();
renderAdvancedOptions();
// Reset preview
resetPreview();
}
// Update UI when media type changes
function updateMediaTypeUI() {
// Update tabs
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');
// Update labels and placeholders
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';
// Update generate button
generateBtn.innerHTML = `<i class="fas fa-magic mr-2"></i> Generate ${mediaType.charAt(0).toUpperCase() + mediaType.slice(1)}`;
}
// Render model selectors based on current media type
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);
}
}
// Render advanced options based on current media type
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 {
// Video options
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>
`;
}
// Set up event listeners for dynamic elements
setupDynamicEventListeners();
}
// Set up event listeners for dynamically created elements
function setupDynamicEventListeners() {
// Resolution buttons
document.querySelectorAll('.res-btn').forEach(btn => {
btn.addEventListener('click', () => {
const resolution = parseInt(btn.dataset.res);
state.advancedOptions[state.mediaType].resolution = resolution;
// Update UI
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') {
// Image options
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 {
// Video options
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;
});
}
}
// Reset preview when switching media types
function resetPreview() {
mediaPlaceholder.classList.remove('hidden');
imagePreview.classList.add('hidden');
videoPreview.classList.add('hidden');
}
// Update character count
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');
}
}
// Generate media (image or video) using Hugging Face API
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;
}
// Show loading state
showLoading(true);
// Show generation modal
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];
// Update generation status
modalTitle.textContent = `Generating ${state.mediaType.charAt(0).toUpperCase() + state.mediaType.slice(1)}`;
modalSubtitle.textContent = 'Initializing model...';
// Prepare request data based on media type
let requestData = {
inputs: prompt,
options: {
use_cache: false,
wait_for_model: true
}
};
// Add model-specific parameters
if (state.mediaType === 'image') {
// Set width and height for square images
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 // Only include if not null
}
});
} else {
// Video parameters
Object.assign(requestData, {
parameters: {
height: advOptions.resolution,
width: Math.round(advOptions.resolution * (16/9)), // 16:9 aspect ratio
num_frames: advOptions.duration * advOptions.fps,
num_inference_steps: modelConfig.params.num_inference_steps,
fps: advOptions.fps,
seed: advOptions.seed || undefined // Only include if not null
}
});
}
// Make the API call
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...');
// Handle the response based on media type
let mediaData;
if (state.mediaType === 'image') {
// For images, we get image bytes directly
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 {
// For videos, we need to handle different response formats
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)
});
}
// Update progress to completion
await updateProgress(100, 'Generation complete!');
// Add to state and save
state.videos.unshift(mediaData);
localStorage.setItem('generated_media', JSON.stringify(state.videos));
// Show the generated media
showGeneratedMedia(mediaData);
// Hide modal after a delay
setTimeout(() => {
generationModal.classList.add('hidden');
}, 500);
} catch (error) {
console.error('Error generating media:', error);
handleGenerationError(error.message || 'Failed to generate media');
} finally {
showLoading(false);
}
}
// Create standard media object structure
function createMediaObject(prompt, model, additionalData) {
return {
id: Date.now(),
prompt: prompt,
model: model,
timestamp: new Date().toISOString(),
...additionalData
};
}
// Call Hugging Face API with proper error handling
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) {
// Check if the error includes rate limit information
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;
}
}
// Extract meaningful error message from response
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}`;
}
}
// Show loading state
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');
}
}
// Update generation progress
async function updateProgress(percent, message) {
return new Promise(resolve => {
progressBar.style.width = `${percent}%`;
progressText.textContent = `${percent}%`;
modalSubtitle.textContent = message;
// Estimate time remaining
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...';
}
// Simulate some delay for smooth animation
setTimeout(resolve, 200);
});
}
// Show generated media
function showGeneratedMedia(mediaData) {
// Hide placeholder
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(); // Ensure video is ready to play
imagePreview.classList.add('hidden');
}
// Add to media grid
renderMedia();
}
// Handle generation error
function handleGenerationError(message) {
modalTitle.textContent = 'Generation Failed';
modalSubtitle.textContent = message;
progressBar.style.width = '0%';
progressText.textContent = '0%';
// Show error details in console
console.error('Generation error:', message);
// Automatically hide after 5 seconds
setTimeout(() => {
generationModal.classList.add('hidden');
}, 5000);
}
// Render media in the grid
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);
// Add fade-in animation for new items
if (media.id === state.videos[0].id) {
setTimeout(() => {
mediaElement.classList.add('fade-in');
}, 50);
}
});
}
// Create media card element
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';
// Common elements
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}
`;
// Auto-play the video when card is visible
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;
}
// Clear history
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();
}
}
// Suggest a prompt
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();
}
// Regenerate a media item
window.regenerateMedia = function(id) {
const media = state.videos.find(v => v.id == id);
if (media) {
// Switch to the correct tab if needed
if (state.mediaType !== media.type) {
state.mediaType = media.type;
updateMediaTypeUI();
renderModelSelectors();
renderAdvancedOptions();
}
// Set the prompt and model
promptInput.value = media.prompt;
state.selectedModel = media.model;
// Update UI to show selected model
renderModelSelectors();
// Scroll to top
window.scrollTo({ top: 0, behavior: 'smooth' });
// Focus on prompt
promptInput.focus();
// Show toast
showToast(`Loaded settings from previous generation. Click "Generate" to recreate.`);
}
};
// Delete a media item
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));
// Remove the card with animation
const card = button.closest('.bg-white');
card.classList.add('opacity-0', 'scale-95', 'transition-all', 'duration-300');
setTimeout(() => {
renderMedia();
}, 300);
}
};
// Show notification toast
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);
}
// Initialize the app
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>