Spaces:
Running on Zero
Running on Zero
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> | |
| <title>ERNIE Image Turbo</title> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| :root { | |
| --bg-body: #f5f5f7; | |
| --bg-surface: rgba(255, 255, 255, 0.7); | |
| --border-subtle: rgba(0, 0, 0, 0.05); | |
| --text-primary: #1d1d1f; | |
| --text-secondary: #86868b; | |
| --accent: #000000; | |
| --accent-hover: #333333; | |
| --glass-shadow: 0 4px 24px rgba(0, 0, 0, 0.04); | |
| } | |
| @media (prefers-color-scheme: dark) { | |
| :root { | |
| --bg-body: #000000; | |
| --bg-surface: rgba(28, 28, 30, 0.7); | |
| --border-subtle: rgba(255, 255, 255, 0.1); | |
| --text-primary: #f5f5f7; | |
| --text-secondary: #86868b; | |
| --accent: #ffffff; | |
| --accent-hover: #e5e5e5; | |
| --glass-shadow: 0 4px 24px rgba(0, 0, 0, 0.4); | |
| } | |
| } | |
| * { | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0; | |
| -webkit-font-smoothing: antialiased; | |
| -moz-osx-font-smoothing: grayscale; | |
| } | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", Roboto, Helvetica, Arial, sans-serif; | |
| background-color: var(--bg-body); | |
| color: var(--text-primary); | |
| min-height: 100vh; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| padding: 2rem; | |
| overflow: hidden; | |
| transition: background-color 0.3s ease; | |
| } | |
| main { | |
| display: flex; | |
| width: 100%; | |
| max-width: 1200px; | |
| height: calc(100vh - 4rem); | |
| gap: 2rem; | |
| } | |
| /* Elements */ | |
| .panel { | |
| background: var(--bg-surface); | |
| backdrop-filter: blur(40px); | |
| -webkit-backdrop-filter: blur(40px); | |
| border: 1px solid var(--border-subtle); | |
| border-radius: 32px; | |
| box-shadow: var(--glass-shadow); | |
| } | |
| /* Inputs */ | |
| .sidebar { | |
| width: 320px; | |
| padding: 2.5rem; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 1.5rem; | |
| flex-shrink: 0; | |
| z-index: 2; | |
| overflow-y: auto; | |
| } | |
| .header h1 { | |
| font-size: 1.5rem; | |
| font-weight: 600; | |
| letter-spacing: -0.5px; | |
| margin-bottom: 0.25rem; | |
| } | |
| .header p { | |
| color: var(--text-secondary); | |
| font-size: 0.9rem; | |
| font-weight: 400; | |
| } | |
| .control-group { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 0.75rem; | |
| flex: 1; | |
| min-height: 80px; | |
| } | |
| textarea { | |
| flex: 1; | |
| width: 100%; | |
| background: rgba(120, 120, 128, 0.05); | |
| border: 1px solid var(--border-subtle); | |
| border-radius: 20px; | |
| padding: 1.25rem; | |
| color: var(--text-primary); | |
| font-family: inherit; | |
| font-size: 1.05rem; | |
| line-height: 1.4; | |
| resize: none; | |
| transition: all 0.2s ease; | |
| } | |
| textarea:focus { | |
| outline: none; | |
| background: rgba(120, 120, 128, 0.1); | |
| border-color: rgba(120, 120, 128, 0.2); | |
| } | |
| textarea::placeholder { | |
| color: var(--text-secondary); | |
| font-weight: 400; | |
| } | |
| /* Advanced controls */ | |
| .advanced-section { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 1rem; | |
| } | |
| .advanced-title { | |
| font-size: 0.72rem; | |
| font-weight: 600; | |
| letter-spacing: 0.08em; | |
| text-transform: uppercase; | |
| color: var(--text-secondary); | |
| } | |
| .preset-grid { | |
| display: grid; | |
| grid-template-columns: repeat(3, 1fr); | |
| gap: 0.4rem; | |
| } | |
| .preset-btn { | |
| background: rgba(120, 120, 128, 0.05); | |
| border: 1px solid var(--border-subtle); | |
| border-radius: 12px; | |
| padding: 0.5rem 0.2rem; | |
| font-size: 0.7rem; | |
| font-weight: 500; | |
| color: var(--text-secondary); | |
| cursor: pointer; | |
| transition: all 0.2s ease; | |
| text-align: center; | |
| line-height: 1.4; | |
| } | |
| .preset-btn:hover { | |
| background: rgba(120, 120, 128, 0.12); | |
| color: var(--text-primary); | |
| } | |
| .preset-btn.active { | |
| background: var(--accent); | |
| color: var(--bg-body); | |
| border-color: var(--accent); | |
| } | |
| .slider-group { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 0.4rem; | |
| } | |
| .slider-label { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| font-size: 0.82rem; | |
| color: var(--text-secondary); | |
| } | |
| .slider-label span:last-child { | |
| font-weight: 600; | |
| color: var(--text-primary); | |
| min-width: 2.5rem; | |
| text-align: right; | |
| } | |
| input[type="text"] { | |
| width: 100%; | |
| background: rgba(120, 120, 128, 0.05); | |
| border: 1px solid var(--border-subtle); | |
| border-radius: 12px; | |
| padding: 0.6rem 0.9rem; | |
| color: var(--text-primary); | |
| font-family: inherit; | |
| font-size: 0.85rem; | |
| transition: all 0.2s ease; | |
| } | |
| input[type="text"]:focus { | |
| outline: none; | |
| background: rgba(120, 120, 128, 0.1); | |
| border-color: rgba(120, 120, 128, 0.2); | |
| } | |
| input[type="text"]::placeholder { | |
| color: var(--text-secondary); | |
| } | |
| input[type="range"] { | |
| -webkit-appearance: none; | |
| appearance: none; | |
| width: 100%; | |
| height: 4px; | |
| background: rgba(120, 120, 128, 0.15); | |
| border-radius: 2px; | |
| outline: none; | |
| } | |
| input[type="range"]::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| appearance: none; | |
| width: 18px; | |
| height: 18px; | |
| background: var(--accent); | |
| border-radius: 50%; | |
| cursor: pointer; | |
| transition: transform 0.15s ease; | |
| } | |
| input[type="range"]::-webkit-slider-thumb:hover { | |
| transform: scale(1.2); | |
| } | |
| .toggle-row { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| font-size: 0.82rem; | |
| color: var(--text-secondary); | |
| } | |
| .toggle-switch { | |
| position: relative; | |
| width: 36px; | |
| height: 20px; | |
| flex-shrink: 0; | |
| } | |
| .toggle-switch input { | |
| opacity: 0; | |
| width: 0; | |
| height: 0; | |
| } | |
| .toggle-track { | |
| position: absolute; | |
| inset: 0; | |
| background: rgba(120, 120, 128, 0.2); | |
| border-radius: 20px; | |
| cursor: pointer; | |
| transition: background 0.2s ease; | |
| } | |
| .toggle-track::after { | |
| content: ''; | |
| position: absolute; | |
| left: 3px; | |
| top: 3px; | |
| width: 14px; | |
| height: 14px; | |
| background: white; | |
| border-radius: 50%; | |
| transition: transform 0.2s ease; | |
| } | |
| .toggle-switch input:checked + .toggle-track { | |
| background: var(--accent); | |
| } | |
| .toggle-switch input:checked + .toggle-track::after { | |
| transform: translateX(16px); | |
| } | |
| .btn-generate { | |
| background-color: var(--accent); | |
| color: var(--bg-body); | |
| border: none; | |
| padding: 1.1rem; | |
| border-radius: 100px; /* Pill shape */ | |
| font-size: 1.05rem; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: transform 0.2s cubic-bezier(0.2, 0.8, 0.2, 1), background-color 0.2s; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| gap: 0.5rem; | |
| flex-shrink: 0; | |
| } | |
| .btn-generate:hover { | |
| transform: scale(0.98); | |
| background-color: var(--accent-hover); | |
| } | |
| .btn-generate:disabled { | |
| opacity: 0.5; | |
| cursor: not-allowed; | |
| transform: none; | |
| } | |
| /* Workspace */ | |
| .workspace { | |
| flex: 1; | |
| position: relative; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| padding: 2rem; | |
| overflow: hidden; | |
| z-index: 1; | |
| } | |
| .image-container { | |
| width: 100%; | |
| height: 100%; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| position: relative; | |
| } | |
| #result-image { | |
| max-width: 100%; | |
| max-height: 100%; | |
| object-fit: contain; | |
| border-radius: 16px; | |
| opacity: 0; | |
| transform: scale(0.98); | |
| transition: opacity 0.6s ease, transform 0.6s cubic-bezier(0.2, 0.8, 0.2, 1); | |
| box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1); | |
| } | |
| #result-image.loaded { | |
| opacity: 1; | |
| transform: scale(1); | |
| } | |
| .empty-state { | |
| position: absolute; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 1rem; | |
| color: var(--text-secondary); | |
| text-align: center; | |
| transition: opacity 0.4s ease; | |
| } | |
| .empty-state i { | |
| font-size: 3rem; | |
| opacity: 0.5; | |
| margin-bottom: 0.5rem; | |
| } | |
| .empty-state h3 { | |
| font-size: 1.2rem; | |
| font-weight: 500; | |
| color: var(--text-primary); | |
| } | |
| /* Loading Overlay */ | |
| .loading-overlay { | |
| position: absolute; | |
| inset: 0; | |
| background: var(--bg-surface); | |
| backdrop-filter: blur(20px); | |
| -webkit-backdrop-filter: blur(20px); | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 1.2rem; | |
| opacity: 0; | |
| pointer-events: none; | |
| transition: opacity 0.3s ease; | |
| z-index: 10; | |
| border-radius: inherit; | |
| } | |
| .loading-overlay.active { | |
| opacity: 1; | |
| pointer-events: all; | |
| } | |
| .spinner { | |
| width: 40px; | |
| height: 40px; | |
| border: 3px solid var(--border-subtle); | |
| border-top-color: var(--text-primary); | |
| border-radius: 50%; | |
| animation: spin 1s linear infinite; | |
| } | |
| @keyframes spin { | |
| to { transform: rotate(360deg); } | |
| } | |
| .loading-text { | |
| font-size: 1rem; | |
| font-weight: 500; | |
| color: var(--text-primary); | |
| letter-spacing: -0.2px; | |
| animation: pulse 1.5s ease-in-out infinite; | |
| } | |
| @keyframes pulse { | |
| 0%, 100% { opacity: 1; } | |
| 50% { opacity: 0.6; } | |
| } | |
| /* Error */ | |
| .error-message { | |
| position: absolute; | |
| top: 2rem; | |
| left: 50%; | |
| transform: translateX(-50%) translateY(-20px); | |
| background: #ff3b30; | |
| color: white; | |
| padding: 1rem 1.5rem; | |
| border-radius: 100px; | |
| font-weight: 500; | |
| font-size: 0.95rem; | |
| opacity: 0; | |
| pointer-events: none; | |
| transition: all 0.4s cubic-bezier(0.2, 0.8, 0.2, 1); | |
| box-shadow: 0 4px 16px rgba(255, 59, 48, 0.4); | |
| z-index: 20; | |
| display: flex; | |
| align-items: center; | |
| gap: 0.75rem; | |
| } | |
| .error-message.visible { | |
| opacity: 1; | |
| transform: translateX(-50%) translateY(0); | |
| } | |
| .download-btn { | |
| position: absolute; | |
| bottom: 2rem; | |
| right: 2rem; | |
| background: var(--bg-surface); | |
| backdrop-filter: blur(20px); | |
| -webkit-backdrop-filter: blur(20px); | |
| border: 1px solid var(--border-subtle); | |
| color: var(--text-primary); | |
| width: 44px; | |
| height: 44px; | |
| border-radius: 50%; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| cursor: pointer; | |
| transition: all 0.2s ease; | |
| opacity: 0; | |
| pointer-events: none; | |
| z-index: 5; | |
| box-shadow: var(--glass-shadow); | |
| } | |
| .download-btn.visible { | |
| opacity: 1; | |
| pointer-events: all; | |
| } | |
| .download-btn:hover { | |
| transform: scale(1.05); | |
| } | |
| @media (max-width: 1024px) { | |
| body { | |
| padding: 0; | |
| align-items: flex-end; | |
| } | |
| main { | |
| flex-direction: column-reverse; /* Image on top, controls on bottom */ | |
| padding: 0; | |
| gap: 0; | |
| height: 100dvh; | |
| } | |
| .sidebar { | |
| width: 100%; | |
| height: 55vh; | |
| border-radius: 32px 32px 0 0; | |
| padding-bottom: 2.5rem; | |
| border-bottom: none; | |
| border-left: none; | |
| border-right: none; | |
| } | |
| .workspace { | |
| height: 45vh; | |
| border-radius: 0; | |
| border-top: none; | |
| border-left: none; | |
| border-right: none; | |
| background: transparent; | |
| box-shadow: none; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <main> | |
| <!-- Sidebar Controls --> | |
| <aside class="sidebar panel"> | |
| <div class="header"> | |
| <h1>ERNIE Image Turbo</h1> | |
| <p>High-Fidelity Generation</p> | |
| </div> | |
| <div class="control-group"> | |
| <textarea id="prompt" placeholder="A futuristic city with flying cars at sunset, highly detailed..."></textarea> | |
| </div> | |
| <!-- Resolution Presets --> | |
| <div class="advanced-section"> | |
| <div class="advanced-title">Resolution</div> | |
| <div class="preset-grid" id="preset-grid"> | |
| <button class="preset-btn active" data-w="1024" data-h="1024">1024×1024<br>Square</button> | |
| <button class="preset-btn" data-w="1264" data-h="848">1264×848<br>Landscape</button> | |
| <button class="preset-btn" data-w="848" data-h="1264">848×1264<br>Portrait</button> | |
| <button class="preset-btn" data-w="1376" data-h="768">1376×768<br>Wide</button> | |
| <button class="preset-btn" data-w="768" data-h="1376">768×1376<br>Tall</button> | |
| <button class="preset-btn" data-w="1200" data-h="896">1200×896<br>Photo</button> | |
| </div> | |
| </div> | |
| <!-- Guidance Scale --> | |
| <div class="advanced-section"> | |
| <div class="advanced-title">Parameters</div> | |
| <div class="slider-group"> | |
| <div class="slider-label"> | |
| <span>Guidance Scale</span> | |
| <span id="guidance-val">1.0</span> | |
| </div> | |
| <input type="range" id="guidance-scale" min="1.0" max="7.0" step="0.5" value="1.0"> | |
| </div> | |
| <div class="slider-group"> | |
| <div class="slider-label"> | |
| <span>Inference Steps</span> | |
| <span id="steps-val">8</span> | |
| </div> | |
| <input type="range" id="num-steps" min="4" max="30" step="1" value="8"> | |
| </div> | |
| <div class="toggle-row"> | |
| <span>Prompt Enhancer</span> | |
| <label class="toggle-switch"> | |
| <input type="checkbox" id="use-pe" checked> | |
| <span class="toggle-track"></span> | |
| </label> | |
| </div> | |
| </div> | |
| <!-- LoRA --> | |
| <div class="advanced-section"> | |
| <div class="advanced-title">LoRA</div> | |
| <input type="text" id="lora-id" placeholder="owner/my-lora (optional)"> | |
| <div class="slider-group" id="lora-scale-group" style="display:none"> | |
| <div class="slider-label"> | |
| <span>LoRA Scale</span> | |
| <span id="lora-scale-val">0.8</span> | |
| </div> | |
| <input type="range" id="lora-scale" min="0" max="1.5" step="0.05" value="0.8"> | |
| </div> | |
| </div> | |
| <button class="btn-generate" id="generate-btn"> | |
| Generate | |
| </button> | |
| </aside> | |
| <!-- Canvas Workspace --> | |
| <section class="workspace panel"> | |
| <div class="error-message" id="error-toast"> | |
| <i class="fas fa-exclamation-circle"></i> | |
| <span id="error-text">An error occurred.</span> | |
| </div> | |
| <div class="image-container"> | |
| <div class="empty-state" id="empty-state"> | |
| <i class="fa-solid fa-wand-magic-sparkles"></i> | |
| <h3>Imagine anything</h3> | |
| <p>Enter a prompt and hit generate</p> | |
| </div> | |
| <img id="result-image" alt="Generated Image" src="" /> | |
| </div> | |
| <a id="download-link" download="ernie-generation.png" class="download-btn"> | |
| <i class="fas fa-arrow-down"></i> | |
| </a> | |
| <div class="loading-overlay" id="loading-overlay"> | |
| <div class="spinner"></div> | |
| <div class="loading-text" id="status-text">Synthesizing...</div> | |
| </div> | |
| </section> | |
| </main> | |
| <!-- Gradio JS Client --> | |
| <script type="module"> | |
| import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js"; | |
| // ----- UI Elements ----- | |
| const promptInput = document.getElementById('prompt'); | |
| const generateBtn = document.getElementById('generate-btn'); | |
| const resultImage = document.getElementById('result-image'); | |
| const emptyState = document.getElementById('empty-state'); | |
| const loadingOverlay = document.getElementById('loading-overlay'); | |
| const statusText = document.getElementById('status-text'); | |
| const errorToast = document.getElementById('error-toast'); | |
| const errorText = document.getElementById('error-text'); | |
| const downloadBtn = document.getElementById('download-link'); | |
| const guidanceSlider = document.getElementById('guidance-scale'); | |
| const guidanceVal = document.getElementById('guidance-val'); | |
| const stepsSlider = document.getElementById('num-steps'); | |
| const stepsVal = document.getElementById('steps-val'); | |
| const presetGrid = document.getElementById('preset-grid'); | |
| const usePeToggle = document.getElementById('use-pe'); | |
| const loraIdInput = document.getElementById('lora-id'); | |
| const loraScaleSlider = document.getElementById('lora-scale'); | |
| const loraScaleVal = document.getElementById('lora-scale-val'); | |
| const loraScaleGroup = document.getElementById('lora-scale-group'); | |
| // ----- State ----- | |
| let selectedWidth = 1024; | |
| let selectedHeight = 1024; | |
| // ----- Preset Buttons ----- | |
| presetGrid.addEventListener('click', (e) => { | |
| const btn = e.target.closest('.preset-btn'); | |
| if (!btn) return; | |
| document.querySelectorAll('.preset-btn').forEach(b => b.classList.remove('active')); | |
| btn.classList.add('active'); | |
| selectedWidth = parseInt(btn.dataset.w); | |
| selectedHeight = parseInt(btn.dataset.h); | |
| }); | |
| // ----- Slider Labels ----- | |
| guidanceSlider.addEventListener('input', () => { | |
| guidanceVal.textContent = parseFloat(guidanceSlider.value).toFixed(1); | |
| }); | |
| stepsSlider.addEventListener('input', () => { | |
| stepsVal.textContent = stepsSlider.value; | |
| }); | |
| loraIdInput.addEventListener('input', () => { | |
| loraScaleGroup.style.display = loraIdInput.value.trim() ? 'flex' : 'none'; | |
| }); | |
| loraScaleSlider.addEventListener('input', () => { | |
| loraScaleVal.textContent = parseFloat(loraScaleSlider.value).toFixed(2); | |
| }); | |
| // ----- Error Helper ----- | |
| const showError = (message) => { | |
| errorText.textContent = message; | |
| errorToast.classList.add('visible'); | |
| setTimeout(() => { | |
| errorToast.classList.remove('visible'); | |
| }, 5000); | |
| }; | |
| // ----- Generation Logic ----- | |
| generateBtn.addEventListener('click', async () => { | |
| const promptStr = promptInput.value.trim(); | |
| if(!promptStr) { | |
| showError("Please enter a prompt first."); | |
| return; | |
| } | |
| try { | |
| generateBtn.disabled = true; | |
| loadingOverlay.classList.add('active'); | |
| statusText.textContent = "Connecting to backend..."; | |
| const client = await Client.connect(window.location.origin); | |
| statusText.textContent = "Generating..."; | |
| const loraId = loraIdInput.value.trim(); | |
| const params = { | |
| prompt: promptStr, | |
| width: selectedWidth, | |
| height: selectedHeight, | |
| guidance_scale: parseFloat(guidanceSlider.value), | |
| num_inference_steps: parseInt(stepsSlider.value), | |
| use_prompt_enhancer: usePeToggle.checked, | |
| ...(loraId && { | |
| lora_id: loraId, | |
| lora_scale: parseFloat(loraScaleSlider.value) | |
| }) | |
| }; | |
| const result = await client.predict("/generate_image", params); | |
| if(result && result.data && result.data[0]) { | |
| const imgUrl = result.data[0].url; | |
| resultImage.classList.remove('loaded'); | |
| await new Promise(resolve => setTimeout(resolve, 300)); | |
| resultImage.src = imgUrl; | |
| downloadBtn.href = imgUrl; | |
| resultImage.onload = () => { | |
| emptyState.style.opacity = '0'; | |
| resultImage.classList.add('loaded'); | |
| downloadBtn.classList.add('visible'); | |
| }; | |
| } else { | |
| showError("Failed to get image from backend."); | |
| } | |
| } catch (err) { | |
| console.error("Generation Error:", err); | |
| showError(err.message || "An unexpected error occurred during generation."); | |
| } finally { | |
| generateBtn.disabled = false; | |
| loadingOverlay.classList.remove('active'); | |
| } | |
| }); | |
| // Add Enter key support | |
| promptInput.addEventListener('keydown', (e) => { | |
| if (e.key === 'Enter' && !e.shiftKey) { | |
| e.preventDefault(); | |
| generateBtn.click(); | |
| } | |
| }); | |
| </script> | |
| </body> | |
| </html> | |