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; | |
| } | |
| .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; | |
| } | |
| 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; | |
| } | |
| .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; | |
| margin-top: auto; | |
| } | |
| .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: 45vh; | |
| border-radius: 32px 32px 0 0; | |
| padding-bottom: 2.5rem; | |
| border-bottom: none; | |
| border-left: none; | |
| border-right: none; | |
| } | |
| .workspace { | |
| height: 55vh; | |
| 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> | |
| <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 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 { | |
| // UI updates for loading | |
| generateBtn.disabled = true; | |
| loadingOverlay.classList.add('active'); | |
| statusText.textContent = "Connecting to backend..."; | |
| const client = await Client.connect(window.location.origin); | |
| statusText.textContent = "Generating..."; | |
| // Hardcoded defaults for clean UI | |
| const params = { | |
| prompt: promptStr, | |
| width: 1024, | |
| height: 1024, | |
| guidance_scale: 1.0, | |
| num_inference_steps: 8 | |
| }; | |
| // Call endpoint | |
| const result = await client.predict("/generate_image", params); | |
| if(result && result.data && result.data[0]) { | |
| const imgUrl = result.data[0].url; | |
| // Update UI with new image | |
| 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> | |