Spaces:
Running
Running
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Celebrity LoRA Mix</title> | |
| <link rel="icon" type="image/x-icon" href="/static/favicon.ico"> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link href="https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@400;600;700&display=swap" rel="stylesheet"> | |
| <link href="https://unpkg.com/aos@2.3.1/dist/aos.css" rel="stylesheet"> | |
| <script src="https://unpkg.com/aos@2.3.1/dist/aos.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script> | |
| <script src="https://unpkg.com/feather-icons"></script> | |
| <style> | |
| body { | |
| font-family: 'Source Sans Pro', sans-serif; | |
| } | |
| .lora-card { | |
| transition: all 0.3s ease; | |
| } | |
| .lora-card:hover { | |
| transform: translateY(-5px); | |
| box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1); | |
| } | |
| .slider-container { | |
| background: linear-gradient(to right, #667eea 0%, #764ba2 100%); | |
| border-radius: 9999px; | |
| height: 8px; | |
| } | |
| .slider-thumb { | |
| background: white; | |
| border: 2px solid #4f46e5; | |
| box-shadow: 0 2px 4px rgba(0,0,0,0.2); | |
| } | |
| .accordion-content { | |
| max-height: 0; | |
| overflow: hidden; | |
| transition: max-height 0.3s ease-out; | |
| } | |
| .accordion-content.open { | |
| max-height: 1000px; | |
| } | |
| .generate-btn { | |
| background: linear-gradient(45deg, #667eea 0%, #764ba2 100%); | |
| transition: all 0.3s ease; | |
| } | |
| .generate-btn:hover { | |
| transform: scale(1.05); | |
| box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.2); | |
| } | |
| .progress-bar { | |
| transition: width 0.4s ease; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-50 min-h-screen"> | |
| <!-- Header --> | |
| <header class="bg-white shadow-sm py-4 px-6 flex items-center justify-between"> | |
| <div class="flex items-center space-x-2"> | |
| <div class="w-10 h-10 rounded-lg bg-indigo-600 flex items-center justify-center"> | |
| <i data-feather="camera" class="text-white"></i> | |
| </div> | |
| <h1 class="text-xl font-bold text-gray-800">Celebrity LoRA Mix</h1> | |
| </div> | |
| <button class="p-2 rounded-lg hover:bg-gray-100"> | |
| <i data-feather="settings"></i> | |
| </button> | |
| </header> | |
| <!-- Main Content --> | |
| <main class="container mx-auto px-4 py-8 max-w-4xl"> | |
| <!-- Prompt Input --> | |
| <section class="mb-8" data-aos="fade-up"> | |
| <label class="block text-lg font-semibold text-gray-800 mb-3">Your Creative Prompt</label> | |
| <div class="relative"> | |
| <textarea | |
| id="prompt-input" | |
| class="w-full p-4 border border-gray-300 rounded-xl focus:ring-2 focus:ring-indigo-500 focus:border-transparent shadow-sm resize-none" | |
| rows="3" | |
| placeholder="Describe your vision... (Select LoRAs first for trigger word suggestions)" | |
| ></textarea> | |
| <div class="absolute bottom-3 right-3"> | |
| <button class="p-2 rounded-lg bg-gray-100 hover:bg-gray-200"> | |
| <i data-feather="mic"></i> | |
| </button> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- LoRA Selection --> | |
| <section class="mb-8" data-aos="fade-up" data-aos-delay="100"> | |
| <div class="flex items-center justify-between mb-4"> | |
| <h2 class="text-lg font-semibold text-gray-800">LoRA Models</h2> | |
| <span class="text-sm text-gray-500">Select up to 4 models</span> | |
| </div> | |
| <div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-4" id="lora-grid"> | |
| <!-- LoRA cards will be dynamically inserted here --> | |
| </div> | |
| </section> | |
| <!-- Scale Sliders --> | |
| <section class="mb-8" data-aos="fade-up" data-aos-delay="200"> | |
| <h2 class="text-lg font-semibold text-gray-800 mb-4">Model Influence Scales</h2> | |
| <div id="sliders-container" class="space-y-6"> | |
| <!-- Sliders will be dynamically inserted here --> | |
| </div> | |
| </section> | |
| <!-- Advanced Parameters Accordion --> | |
| <section class="mb-8" data-aos="fade-up" data-aos-delay="300"> | |
| <div class="border border-gray-200 rounded-xl overflow-hidden"> | |
| <button id="accordion-toggle" class="w-full flex justify-between items-center p-4 bg-gray-50 hover:bg-gray-100"> | |
| <span class="font-semibold text-gray-800">Advanced Parameters</span> | |
| <i data-feather="chevron-down" id="accordion-icon"></i> | |
| </button> | |
| <div id="advanced-params" class="accordion-content open"> | |
| <div class="p-4 bg-white grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">CFG Scale</label> | |
| <input type="range" min="1" max="20" value="7" step="0.5" | |
| class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer slider" | |
| id="cfg-slider"> | |
| <div class="flex justify-between text-xs text-gray-500 mt-1"> | |
| <span>1</span> | |
| <span id="cfg-value">7</span> | |
| <span>20</span> | |
| </div> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Steps</label> | |
| <input type="range" min="10" max="100" value="30" step="5" | |
| class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer slider" | |
| id="steps-slider"> | |
| <div class="flex justify-between text-xs text-gray-500 mt-1"> | |
| <span>10</span> | |
| <span id="steps-value">30</span> | |
| <span>100</span> | |
| </div> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Width</label> | |
| <select class="w-full p-2 border border-gray-300 rounded-lg"> | |
| <option>512</option> | |
| <option selected>768</option> | |
| <option>1024</option> | |
| </select> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Height</label> | |
| <select class="w-full p-2 border border-gray-300 rounded-lg"> | |
| <option>512</option> | |
| <option selected>768</option> | |
| <option>1024</option> | |
| </select> | |
| </div> | |
| <div class="md:col-span-2"> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Seed</label> | |
| <div class="flex space-x-2"> | |
| <input type="text" placeholder="Leave blank for random" | |
| class="flex-1 p-2 border border-gray-300 rounded-lg"> | |
| <button class="px-4 py-2 bg-gray-100 hover:bg-gray-200 rounded-lg"> | |
| <i data-feather="refresh-cw"></i> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- Generate Button --> | |
| <section class="mb-8 text-center" data-aos="fade-up" data-aos-delay="400"> | |
| <button id="generate-btn" class="generate-btn text-white font-semibold py-4 px-8 rounded-xl shadow-lg w-full sm:w-auto"> | |
| <span id="btn-text">Generate Image</span> | |
| <div id="loading-spinner" class="hidden inline-block ml-2"> | |
| <i data-feather="loader" class="animate-spin"></i> | |
| </div> | |
| </button> | |
| <!-- Progress Bar --> | |
| <div id="progress-container" class="mt-4 hidden"> | |
| <div class="w-full bg-gray-200 rounded-full h-2.5"> | |
| <div id="progress-bar" class="progress-bar bg-indigo-600 h-2.5 rounded-full" style="width: 0%"></div> | |
| </div> | |
| <p id="progress-text" class="text-sm text-gray-600 mt-2">Preparing models...</p> | |
| </div> | |
| </section> | |
| <!-- Generated Image Display --> | |
| <section id="result-section" class="hidden" data-aos="fade-up" data-aos-delay="500"> | |
| <div class="border border-gray-200 rounded-xl overflow-hidden bg-white shadow-sm"> | |
| <div class="p-4 border-b border-gray-200"> | |
| <h2 class="text-lg font-semibold text-gray-800">Generated Image</h2> | |
| </div> | |
| <div class="p-4"> | |
| <div class="aspect-square bg-gray-100 rounded-lg flex items-center justify-center mb-4"> | |
| <img id="generated-image" src="" alt="Generated image" class="max-w-full max-h-full object-contain"> | |
| </div> | |
| <div class="flex justify-center"> | |
| <button class="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 flex items-center"> | |
| <i data-feather="download" class="mr-2"></i> | |
| Download | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- Error Message --> | |
| <div id="error-message" class="hidden p-4 mb-4 text-sm text-red-700 bg-red-100 rounded-lg" role="alert"> | |
| <span id="error-text"></span> | |
| </div> | |
| </main> | |
| <script> | |
| // Sample LoRA data (would come from backend in real implementation) | |
| const loraModels = [ | |
| { id: 1, name: "Taylor Swift", trigger_word: "taylor_swift", thumbnail: "http://static.photos/people/320x240/1" }, | |
| { id: 2, name: "Dwayne Johnson", trigger_word: "dwayne_johnson", thumbnail: "http://static.photos/people/320x240/2" }, | |
| { id: 3, name: "Scarlett Johansson", trigger_word: "scarlett_johansson", thumbnail: "http://static.photos/people/320x240/3" }, | |
| { id: 4, name: "Tom Hanks", trigger_word: "tom_hanks", thumbnail: "http://static.photos/people/320x240/4" }, | |
| { id: 5, name: "Ariana Grande", trigger_word: "ariana_grande", thumbnail: "http://static.photos/people/320x240/5" }, | |
| { id: 6, name: "Chris Evans", trigger_word: "chris_evans", thumbnail: "http://static.photos/people/320x240/6" } | |
| ]; | |
| // State management | |
| let selectedLoras = []; | |
| let isAccordionOpen = true; | |
| // Initialize UI | |
| document.addEventListener('DOMContentLoaded', function() { | |
| feather.replace(); | |
| AOS.init({ | |
| duration: 800, | |
| easing: 'ease-out-quart' | |
| }); | |
| renderLoraGrid(); | |
| setupEventListeners(); | |
| updateSliders(); | |
| }); | |
| function renderLoraGrid() { | |
| const grid = document.getElementById('lora-grid'); | |
| grid.innerHTML = ''; | |
| loraModels.forEach(model => { | |
| const isSelected = selectedLoras.includes(model.id); | |
| const card = document.createElement('div'); | |
| card.className = `lora-card border rounded-xl p-3 cursor-pointer transition-all ${isSelected ? 'border-indigo-500 bg-indigo-50' : 'border-gray-200 hover:border-gray-300'}`; | |
| card.dataset.id = model.id; | |
| card.innerHTML = ` | |
| <div class="aspect-square mb-2 rounded-lg overflow-hidden bg-gray-200"> | |
| <img src="${model.thumbnail}" alt="${model.name}" class="w-full h-full object-cover"> | |
| </div> | |
| <h3 class="font-semibold text-sm truncate">${model.name}</h3> | |
| <p class="text-xs text-gray-500 truncate">${model.trigger_word}</p> | |
| `; | |
| card.addEventListener('click', () => toggleLoraSelection(model.id)); | |
| grid.appendChild(card); | |
| }); | |
| } | |
| function toggleLoraSelection(id) { | |
| const index = selectedLoras.indexOf(id); | |
| if (index > -1) { | |
| selectedLoras.splice(index, 1); | |
| } else if (selectedLoras.length < 4) { | |
| selectedLoras.push(id); | |
| } | |
| renderLoraGrid(); | |
| updateSliders(); | |
| } | |
| function updateSliders() { | |
| const container = document.getElementById('sliders-container'); | |
| container.innerHTML = ''; | |
| selectedLoras.slice(0, 4).forEach((id, index) => { | |
| const model = loraModels.find(m => m.id === id); | |
| const slider = document.createElement('div'); | |
| slider.className = 'space-y-2'; | |
| slider.innerHTML = ` | |
| <div class="flex justify-between"> | |
| <label class="font-medium text-gray-700">${model.name} Influence</label> | |
| <span id="scale-${index}-value" class="text-indigo-600 font-medium">0.8</span> | |
| </div> | |
| <input type="range" min="0" max="1" step="0.05" value="0.8" | |
| class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer slider" | |
| id="scale-${index}"> | |
| <div class="flex justify-between text-xs text-gray-500"> | |
| <span>0</span> | |
| <span>1.0</span> | |
| </div> | |
| `; | |
| container.appendChild(slider); | |
| }); | |
| // Add event listeners to new sliders | |
| document.querySelectorAll('[id^="scale-"]').forEach((slider, index) => { | |
| slider.addEventListener('input', function() { | |
| document.getElementById(`scale-${index}-value`).textContent = this.value; | |
| }); | |
| }); | |
| } | |
| function setupEventListeners() { | |
| // Accordion toggle | |
| document.getElementById('accordion-toggle').addEventListener('click', function() { | |
| const content = document.getElementById('advanced-params'); | |
| const icon = document.getElementById('accordion-icon'); | |
| isAccordionOpen = !isAccordionOpen; | |
| if (isAccordionOpen) { | |
| content.classList.add('open'); | |
| icon.setAttribute('data-feather', 'chevron-up'); | |
| } else { | |
| content.classList.remove('open'); | |
| icon.setAttribute('data-feather', 'chevron-down'); | |
| } | |
| feather.replace(); | |
| }); | |
| // Slider value updates | |
| document.getElementById('cfg-slider').addEventListener('input', function() { | |
| document.getElementById('cfg-value').textContent = this.value; | |
| }); | |
| document.getElementById('steps-slider').addEventListener('input', function() { | |
| document.getElementById('steps-value').textContent = this.value; | |
| }); | |
| // Generate button | |
| document.getElementById('generate-btn').addEventListener('click', generateImage); | |
| } | |
| async function generateImage() { | |
| const prompt = document.getElementById('prompt-input').value.trim(); | |
| const btn = document.getElementById('generate-btn'); | |
| const btnText = document.getElementById('btn-text'); | |
| const spinner = document.getElementById('loading-spinner'); | |
| const progressContainer = document.getElementById('progress-container'); | |
| const progressBar = document.getElementById('progress-bar'); | |
| const progressText = document.getElementById('progress-text'); | |
| const errorContainer = document.getElementById('error-message'); | |
| const errorText = document.getElementById('error-text'); | |
| const resultSection = document.getElementById('result-section'); | |
| const generatedImage = document.getElementById('generated-image'); | |
| // Validation | |
| if (!prompt) { | |
| showError("Please enter a creative prompt"); | |
| return; | |
| } | |
| if (selectedLoras.length === 0) { | |
| showError("Please select at least one LoRA model"); | |
| return; | |
| } | |
| // Reset UI | |
| errorContainer.classList.add('hidden'); | |
| resultSection.classList.add('hidden'); | |
| // Show loading state | |
| btn.disabled = true; | |
| btnText.textContent = 'Generating...'; | |
| spinner.classList.remove('hidden'); | |
| progressContainer.classList.remove('hidden'); | |
| try { | |
| // Simulate progress updates | |
| const steps = ['Loading models...', 'Processing prompt...', 'Applying LoRA weights...', 'Generating image...', 'Finalizing...']; | |
| for (let i = 0; i < steps.length; i++) { | |
| await new Promise(resolve => setTimeout(resolve, 800)); | |
| const progress = ((i + 1) / steps.length) * 100; | |
| progressBar.style.width = `${progress}%`; | |
| progressText.textContent = steps[i]; | |
| } | |
| // Simulate image generation | |
| await new Promise(resolve => setTimeout(resolve, 500)); | |
| // Display result | |
| generatedImage.src = `http://static.photos/artistic/1024x1024/${Math.floor(Math.random() * 100)}`; | |
| resultSection.classList.remove('hidden'); | |
| } catch (error) { | |
| showError("Failed to generate image. Please try again."); | |
| } finally { | |
| // Reset button | |
| btn.disabled = false; | |
| btnText.textContent = 'Generate Image'; | |
| spinner.classList.add('hidden'); | |
| progressContainer.classList.add('hidden'); | |
| } | |
| } | |
| function showError(message) { | |
| const errorContainer = document.getElementById('error-message'); | |
| const errorText = document.getElementById('error-text'); | |
| errorText.textContent = message; | |
| errorContainer.classList.remove('hidden'); | |
| // Auto-hide after 5 seconds | |
| setTimeout(() => { | |
| errorContainer.classList.add('hidden'); | |
| }, 5000); | |
| } | |
| </script> | |
| </body> | |
| </html> |