Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Z-Image Turbo PC | High-Speed AI Generator</title> | |
| <!-- Import RemixIcon for modern UI icons --> | |
| <link href="https://cdn.jsdelivr.net/npm/remixicon@3.5.0/fonts/remixicon.css" rel="stylesheet"> | |
| <!-- Google Fonts --> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link | |
| href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;700&display=swap" | |
| rel="stylesheet"> | |
| <style> | |
| :root { | |
| /* Color Palette - Cyberpunk / Dark Tech */ | |
| --bg-body: #09090b; | |
| --bg-panel: #18181b; | |
| --bg-input: #27272a; | |
| --primary: #8b5cf6; | |
| /* Violet */ | |
| --primary-hover: #7c3aed; | |
| --primary-glow: rgba(139, 92, 246, 0.5); | |
| --accent: #06b6d4; | |
| /* Cyan */ | |
| --text-main: #f4f4f5; | |
| --text-muted: #a1a1aa; | |
| --border: #3f3f46; | |
| --danger: #ef4444; | |
| --success: #10b981; | |
| /* Spacing & Radius */ | |
| --radius-lg: 16px; | |
| --radius-md: 8px; | |
| --radius-sm: 4px; | |
| --spacing-xs: 4px; | |
| --spacing-sm: 8px; | |
| --spacing-md: 16px; | |
| --spacing-lg: 24px; | |
| /* Transitions */ | |
| --trans-fast: 0.2s ease; | |
| --trans-smooth: 0.4s cubic-bezier(0.16, 1, 0.3, 1); | |
| } | |
| * { | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0; | |
| } | |
| body { | |
| font-family: 'Inter', sans-serif; | |
| background-color: var(--bg-body); | |
| color: var(--text-main); | |
| min-height: 100vh; | |
| display: flex; | |
| flex-direction: column; | |
| overflow-x: hidden; | |
| } | |
| /* --- Animations --- */ | |
| @keyframes fadeIn { | |
| from { | |
| opacity: 0; | |
| transform: translateY(10px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| } | |
| @keyframes pulseGlow { | |
| 0% { | |
| box-shadow: 0 0 5px var(--primary-glow); | |
| } | |
| 50% { | |
| box-shadow: 0 0 20px var(--primary-glow); | |
| } | |
| 100% { | |
| box-shadow: 0 0 5px var(--primary-glow); | |
| } | |
| } | |
| /* --- Header --- */ | |
| header { | |
| background: rgba(9, 9, 11, 0.8); | |
| backdrop-filter: blur(12px); | |
| border-bottom: 1px solid var(--border); | |
| padding: var(--spacing-md) var(--spacing-lg); | |
| position: sticky; | |
| top: 0; | |
| z-index: 100; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| } | |
| .brand { | |
| display: flex; | |
| align-items: center; | |
| gap: var(--spacing-sm); | |
| font-weight: 700; | |
| font-size: 1.25rem; | |
| letter-spacing: -0.02em; | |
| } | |
| .brand i { | |
| color: var(--primary); | |
| font-size: 1.5rem; | |
| } | |
| .brand span { | |
| background: linear-gradient(to right, #fff, var(--text-muted)); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| } | |
| .header-links a { | |
| color: var(--text-muted); | |
| text-decoration: none; | |
| font-size: 0.875rem; | |
| transition: var(--trans-fast); | |
| display: flex; | |
| align-items: center; | |
| gap: 4px; | |
| } | |
| .header-links a:hover { | |
| color: var(--accent); | |
| } | |
| /* --- Main Layout --- */ | |
| main { | |
| flex: 1; | |
| display: grid; | |
| grid-template-columns: 350px 1fr; | |
| gap: var(--spacing-lg); | |
| padding: var(--spacing-lg); | |
| max-width: 1600px; | |
| margin: 0 auto; | |
| width: 100%; | |
| } | |
| @media (max-width: 1024px) { | |
| main { | |
| grid-template-columns: 1fr; | |
| grid-template-rows: auto auto; | |
| } | |
| } | |
| /* --- Controls Section --- */ | |
| .controls-panel { | |
| background: var(--bg-panel); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius-lg); | |
| padding: var(--spacing-lg); | |
| display: flex; | |
| flex-direction: column; | |
| gap: var(--spacing-lg); | |
| height: fit-content; | |
| animation: fadeIn 0.6s ease-out; | |
| } | |
| .input-group { | |
| display: flex; | |
| flex-direction: column; | |
| gap: var(--spacing-sm); | |
| } | |
| label { | |
| font-size: 0.85rem; | |
| font-weight: 600; | |
| color: var(--text-muted); | |
| display: flex; | |
| justify-content: space-between; | |
| } | |
| textarea { | |
| background: var(--bg-input); | |
| border: 1px solid var(--border); | |
| color: var(--text-main); | |
| border-radius: var(--radius-md); | |
| padding: var(--spacing-md); | |
| font-family: inherit; | |
| resize: vertical; | |
| min-height: 100px; | |
| transition: var(--trans-fast); | |
| font-size: 0.95rem; | |
| } | |
| textarea:focus { | |
| outline: none; | |
| border-color: var(--primary); | |
| box-shadow: 0 0 0 2px rgba(139, 92, 246, 0.2); | |
| } | |
| .settings-grid { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: var(--spacing-md); | |
| } | |
| select, | |
| input[type="text"], | |
| input[type="number"] { | |
| background: var(--bg-input); | |
| border: 1px solid var(--border); | |
| color: var(--text-main); | |
| border-radius: var(--radius-md); | |
| padding: 10px; | |
| width: 100%; | |
| font-family: inherit; | |
| } | |
| /* Custom Range Slider */ | |
| input[type="range"] { | |
| -webkit-appearance: none; | |
| width: 100%; | |
| background: transparent; | |
| margin: 10px 0; | |
| } | |
| input[type="range"]::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| height: 16px; | |
| width: 16px; | |
| border-radius: 50%; | |
| background: var(--primary); | |
| cursor: pointer; | |
| margin-top: -6px; | |
| box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); | |
| } | |
| input[type="range"]::-webkit-slider-runnable-track { | |
| width: 100%; | |
| height: 4px; | |
| cursor: pointer; | |
| background: var(--border); | |
| border-radius: 2px; | |
| } | |
| .btn-generate { | |
| background: linear-gradient(135deg, var(--primary), var(--accent)); | |
| color: white; | |
| border: none; | |
| padding: 14px; | |
| border-radius: var(--radius-md); | |
| font-weight: 600; | |
| font-size: 1rem; | |
| cursor: pointer; | |
| transition: var(--trans-fast); | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| gap: 8px; | |
| position: relative; | |
| overflow: hidden; | |
| margin-top: var(--spacing-sm); | |
| } | |
| .btn-generate:hover { | |
| filter: brightness(1.1); | |
| transform: translateY(-1px); | |
| } | |
| .btn-generate:active { | |
| transform: translateY(1px); | |
| } | |
| .btn-generate:disabled { | |
| opacity: 0.7; | |
| cursor: not-allowed; | |
| filter: grayscale(0.5); | |
| } | |
| /* --- Preview Section --- */ | |
| .preview-panel { | |
| display: flex; | |
| flex-direction: column; | |
| gap: var(--spacing-lg); | |
| animation: fadeIn 0.8s ease-out; | |
| } | |
| .viewport { | |
| background: #000; | |
| border-radius: var(--radius-lg); | |
| border: 1px solid var(--border); | |
| position: relative; | |
| width: 100%; | |
| min-height: 400px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| overflow: hidden; | |
| box-shadow: inset 0 0 50px rgba(0, 0, 0, 0.5); | |
| } | |
| .viewport img { | |
| max-width: 100%; | |
| max-height: 70vh; | |
| object-fit: contain; | |
| display: none; | |
| opacity: 0; | |
| transition: opacity 0.5s ease; | |
| } | |
| .viewport img.active { | |
| display: block; | |
| opacity: 1; | |
| } | |
| .placeholder-text { | |
| color: var(--text-muted); | |
| text-align: center; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .placeholder-text i { | |
| font-size: 3rem; | |
| opacity: 0.2; | |
| } | |
| /* Loading Overlay */ | |
| .loading-overlay { | |
| position: absolute; | |
| inset: 0; | |
| background: rgba(0, 0, 0, 0.8); | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| z-index: 10; | |
| opacity: 0; | |
| pointer-events: none; | |
| transition: opacity 0.3s ease; | |
| } | |
| .loading-overlay.active { | |
| opacity: 1; | |
| pointer-events: all; | |
| } | |
| .loader-ring { | |
| width: 50px; | |
| height: 50px; | |
| border: 3px solid var(--bg-input); | |
| border-top-color: var(--accent); | |
| border-radius: 50%; | |
| animation: spin 1s linear infinite; | |
| } | |
| @keyframes spin { | |
| to { | |
| transform: rotate(360deg); | |
| } | |
| } | |
| .loading-text { | |
| margin-top: 15px; | |
| font-family: 'JetBrains Mono', monospace; | |
| color: var(--accent); | |
| font-size: 0.9rem; | |
| } | |
| /* Actions Bar */ | |
| .actions-bar { | |
| display: flex; | |
| gap: var(--spacing-md); | |
| justify-content: flex-end; | |
| padding: var(--spacing-md); | |
| background: var(--bg-panel); | |
| border-radius: var(--radius-md); | |
| border: 1px solid var(--border); | |
| } | |
| .action-btn { | |
| background: transparent; | |
| border: 1px solid var(--border); | |
| color: var(--text-main); | |
| padding: 8px 16px; | |
| border-radius: var(--radius-sm); | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| font-size: 0.9rem; | |
| transition: var(--trans-fast); | |
| } | |
| .action-btn:hover { | |
| background: var(--bg-input); | |
| border-color: var(--text-muted); | |
| } | |
| .action-btn.primary { | |
| background: var(--bg-input); | |
| border-color: var(--primary); | |
| color: var(--primary); | |
| } | |
| .action-btn.primary:hover { | |
| background: var(--primary); | |
| color: white; | |
| } | |
| /* History Strip */ | |
| .history-section { | |
| margin-top: auto; | |
| } | |
| .history-header { | |
| font-size: 0.9rem; | |
| color: var(--text-muted); | |
| margin-bottom: var(--spacing-sm); | |
| display: flex; | |
| justify-content: space-between; | |
| } | |
| .history-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fill, minmax(80px, 1fr)); | |
| gap: 10px; | |
| } | |
| .history-item { | |
| aspect-ratio: 1; | |
| border-radius: var(--radius-sm); | |
| overflow: hidden; | |
| border: 1px solid var(--border); | |
| cursor: pointer; | |
| position: relative; | |
| transition: var(--trans-fast); | |
| } | |
| .history-item img { | |
| width: 100%; | |
| height: 100%; | |
| object-fit: cover; | |
| transition: transform 0.3s ease; | |
| } | |
| .history-item:hover { | |
| border-color: var(--primary); | |
| transform: scale(1.05); | |
| z-index: 2; | |
| } | |
| /* --- Toast Notification --- */ | |
| .toast-container { | |
| position: fixed; | |
| bottom: 20px; | |
| right: 20px; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 10px; | |
| z-index: 1000; | |
| } | |
| .toast { | |
| background: var(--bg-panel); | |
| border: 1px solid var(--border); | |
| padding: 12px 20px; | |
| border-radius: var(--radius-md); | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5); | |
| animation: slideIn 0.3s ease; | |
| max-width: 300px; | |
| } | |
| .toast.success { | |
| border-left: 4px solid var(--success); | |
| } | |
| .toast.error { | |
| border-left: 4px solid var(--danger); | |
| } | |
| .toast.info { | |
| border-left: 4px solid var(--accent); | |
| } | |
| @keyframes slideIn { | |
| from { | |
| transform: translateX(100%); | |
| opacity: 0; | |
| } | |
| to { | |
| transform: translateX(0); | |
| opacity: 1; | |
| } | |
| } | |
| /* --- API Key Modal (Hidden by default) --- */ | |
| .modal-overlay { | |
| position: fixed; | |
| inset: 0; | |
| background: rgba(0, 0, 0, 0.8); | |
| display: none; | |
| align-items: center; | |
| justify-content: center; | |
| z-index: 200; | |
| backdrop-filter: blur(5px); | |
| } | |
| .modal-overlay.active { | |
| display: flex; | |
| } | |
| .modal { | |
| background: var(--bg-panel); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius-lg); | |
| padding: var(--spacing-lg); | |
| width: 90%; | |
| max-width: 500px; | |
| box-shadow: 0 20px 50px rgba(0, 0, 0, 0.5); | |
| } | |
| .modal h2 { | |
| margin-bottom: var(--spacing-md); | |
| } | |
| .modal p { | |
| color: var(--text-muted); | |
| margin-bottom: var(--spacing-md); | |
| line-height: 1.5; | |
| font-size: 0.9rem; | |
| } | |
| .modal-actions { | |
| display: flex; | |
| justify-content: flex-end; | |
| gap: var(--spacing-md); | |
| margin-top: var(--spacing-lg); | |
| } | |
| /* Footer */ | |
| footer { | |
| margin-top: auto; | |
| border-top: 1px solid var(--border); | |
| padding: var(--spacing-lg); | |
| text-align: center; | |
| font-size: 0.8rem; | |
| color: var(--text-muted); | |
| } | |
| /* Utility classes */ | |
| .hidden { | |
| display: none ; | |
| } | |
| .tag-badge { | |
| background: rgba(139, 92, 246, 0.1); | |
| color: var(--primary); | |
| padding: 2px 6px; | |
| border-radius: 4px; | |
| font-size: 0.75rem; | |
| font-family: 'JetBrains Mono', monospace; | |
| border: 1px solid rgba(139, 92, 246, 0.2); | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- Header --> | |
| <header> | |
| <div class="brand"> | |
| <i class="ri-cpu-line"></i> | |
| <div> | |
| Z-Image <span style="font-weight: 300;">Turbo PC</span> | |
| </div> | |
| </div> | |
| <div class="header-links"> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank"> | |
| <i class="ri-code-s-slash-line"></i> Built with anycoder | |
| </a> | |
| </div> | |
| </header> | |
| <!-- Main Application --> | |
| <main> | |
| <!-- Controls Panel --> | |
| <aside class="controls-panel"> | |
| <div class="input-group"> | |
| <label for="prompt">Prompt <span class="tag-badge">Required</span></label> | |
| <textarea id="prompt" placeholder="Describe the image you want to generate... e.g., A futuristic cyberpunk city with neon lights, cinematic lighting, 8k"></textarea> | |
| </div> | |
| <div class="input-group"> | |
| <label for="negative-prompt">Negative Prompt</label> | |
| <textarea id="negative-prompt" placeholder="Things to avoid... e.g., blurry, low quality, distorted" style="min-height: 60px;"></textarea> | |
| </div> | |
| <div class="settings-grid"> | |
| <div class="input-group"> | |
| <label>Aspect Ratio</label> | |
| <select id="aspect-ratio"> | |
| <option value="1:1">1:1 (Square)</option> | |
| <option value="16:9">16:9 (Landscape)</option> | |
| <option value="9:16">9:16 (Portrait)</option> | |
| <option value="4:3">4:3 (Classic)</option> | |
| </select> | |
| </div> | |
| <div class="input-group"> | |
| <label>Style Preset</label> | |
| <select id="style-preset"> | |
| <option value="none">None</option> | |
| <option value="cinematic">Cinematic</option> | |
| <option value="anime">Anime</option> | |
| <option value="3d-model">3D Model</option> | |
| <option value="digital-art">Digital Art</option> | |
| <option value="photographic">Photographic</option> | |
| </select> | |
| </div> | |
| </div> | |
| <div class="input-group"> | |
| <label for="guidance-scale">Guidance Scale: <span id="guidance-value">7.5</span></label> | |
| <input type="range" id="guidance-scale" min="1" max="20" step="0.5" value="7.5"> | |
| </div> | |
| <div class="input-group"> | |
| <label for="inference-steps">Inference Steps: <span id="steps-value">4</span> <span class="tag-badge">Turbo</span></label> | |
| <input type="range" id="inference-steps" min="1" max="10" step="1" value="4"> | |
| <small style="color: var(--text-muted); font-size: 0.75rem;">Lower steps are faster for Turbo models.</small> | |
| </div> | |
| <button id="generate-btn" class="btn-generate"> | |
| <i class="ri-magic-line"></i> Generate Image | |
| </button> | |
| <div style="text-align: center;"> | |
| <button id="settings-btn" class="action-btn" style="width: 100%; justify-content: center;"> | |
| <i class="ri-settings-3-line"></i> API Configuration | |
| </button> | |
| </div> | |
| </aside> | |
| <!-- Preview Panel --> | |
| <section class="preview-panel"> | |
| <div class="viewport" id="viewport"> | |
| <div class="placeholder-text" id="placeholder"> | |
| <i class="ri-image-add-line"></i> | |
| <p>Enter a prompt and hit Generate to start</p> | |
| </div> | |
| <img id="result-image" src="" alt="Generated Image"> | |
| <!-- Loading Overlay --> | |
| <div class="loading-overlay" id="loader"> | |
| <div class="loader-ring"></div> | |
| <div class="loading-text" id="loading-text">Initializing...</div> | |
| </div> | |
| </div> | |
| <!-- Actions Bar --> | |
| <div class="actions-bar"> | |
| <div style="flex: 1; display: flex; flex-direction: column; justify-content: center; gap: 4px;"> | |
| <span style="font-size: 0.8rem; color: var(--text-muted);">Generation Time</span> | |
| <span id="gen-time" style="font-family: 'JetBrains Mono'; color: var(--accent);">-- ms</span> | |
| </div> | |
| <button class="action-btn" id="clear-btn"> | |
| <i class="ri-delete-bin-line"></i> Clear | |
| </button> | |
| <button class="action-btn primary" id="download-btn" disabled> | |
| <i class="ri-download-line"></i> Download | |
| </button> | |
| </div> | |
| <!-- History --> | |
| <div class="history-section"> | |
| <div class="history-header"> | |
| <span>Session History</span> | |
| <span id="history-count">0 items</span> | |
| </div> | |
| <div class="history-grid" id="history-grid"> | |
| <!-- History items injected here --> | |
| </div> | |
| </div> | |
| </section> | |
| </main> | |
| <!-- API Configuration Modal --> | |
| <div class="modal-overlay" id="api-modal"> | |
| <div class="modal"> | |
| <h2><i class="ri-key-2-line"></i> API Configuration</h2> | |
| <p>To use the real <strong>Z-Image Turbo</strong> model (via Hugging Face Inference API), enter your token below. | |
| If left blank, the app will run in <strong>Demo Mode</strong> (simulated results).</p> | |
| <div class="input-group"> | |
| <label>HF API Token</label> | |
| <input type="password" id="api-token" placeholder="hf_..."> | |
| </div> | |
| <div class="input-group" style="margin-top: 10px;"> | |
| <label>Model Endpoint (Optional)</label> | |
| <input type="text" id="model-endpoint" value="stabilityai/sd-turbo" placeholder="e.g. stabilityai/sd-turbo"> | |
| </div> | |
| <div class="modal-actions"> | |
| <button class="action-btn" id="close-modal">Cancel</button> | |
| <button class="action-btn primary" id="save-api">Save Configuration</button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Toast Container --> | |
| <div class="toast-container" id="toast-container"></div> | |
| <footer> | |
| © 2023 Z-Image Turbo PC Interface. Designed for High Performance. | |
| </footer> | |
| <script> | |
| /** | |
| * Z-Image Turbo PC Logic | |
| * Handles UI interactions, API calls (or simulation), and state management. | |
| */ | |
| // --- State Management --- | |
| const state = { | |
| isGenerating: false, | |
| apiKey: '', | |
| model: 'stabilityai/sd-turbo', | |
| history: [], | |
| currentImageBlob: null | |
| }; | |
| // --- DOM Elements --- | |
| const els = { | |
| prompt: document.getElementById('prompt'), | |
| negPrompt: document.getElementById('negative-prompt'), | |
| aspectRatio: document.getElementById('aspect-ratio'), | |
| stylePreset: document.getElementById('style-preset'), | |
| guidance: document.getElementById('guidance-scale'), | |
| guidanceVal: document.getElementById('guidance-value'), | |
| steps: document.getElementById('inference-steps'), | |
| stepsVal: document.getElementById('steps-value'), | |
| generateBtn: document.getElementById('generate-btn'), | |
| settingsBtn: document.getElementById('settings-btn'), | |
| apiModal: document.getElementById('api-modal'), | |
| closeModal: document.getElementById('close-modal'), | |
| saveApi: document.getElementById('save-api'), | |
| apiTokenInput: document.getElementById('api-token'), | |
| modelEndpointInput: document.getElementById('model-endpoint'), | |
| viewport: document.getElementById('viewport'), | |
| resultImage: document.getElementById('result-image'), | |
| placeholder: document.getElementById('placeholder'), | |
| loader: document.getElementById('loader'), | |
| loadingText: document.getElementById('loading-text'), | |
| downloadBtn: document.getElementById('download-btn'), | |
| clearBtn: document.getElementById('clear-btn'), | |
| genTime: document.getElementById('gen-time'), | |
| historyGrid: document.getElementById('history-grid'), | |
| historyCount: document.getElementById('history-count'), | |
| toastContainer: document.getElementById('toast-container') | |
| }; | |
| // --- Event Listeners --- | |
| // Sliders update text | |
| els.guidance.addEventListener('input', (e) => els.guidanceVal.innerText = e.target.value); | |
| els.steps.addEventListener('input', (e) => els.stepsVal.innerText = e.target.value); | |
| // Modal Logic | |
| els.settingsBtn.addEventListener('click', () => els.apiModal.classList.add('active')); | |
| els.closeModal.addEventListener('click', () => els.apiModal.classList.remove('active')); | |
| els.saveApi.addEventListener('click', () => { | |
| state.apiKey = els.apiTokenInput.value.trim(); | |
| state.model = els.modelEndpointInput.value.trim() || 'stabilityai/sd-turbo'; | |
| showToast('Configuration saved', 'success'); | |
| els.apiModal.classList.remove('active'); | |
| }); | |
| // Generate Button | |
| els.generateBtn.addEventListener('click', handleGenerate); | |
| // Download Button | |
| els.downloadBtn.addEventListener('click', () => { | |
| if (!state.currentImageBlob) return; | |
| const url = URL.createObjectURL(state.currentImageBlob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = `z-turbo-${Date.now()}.png`; | |
| document.body.appendChild(a); | |
| a.click(); | |
| document.body.removeChild(a); | |
| URL.revokeObjectURL(url); | |
| }); | |
| // Clear Button | |
| els.clearBtn.addEventListener('click', () => { | |
| els.resultImage.src = ''; | |
| els.resultImage.classList.remove('active'); | |
| els.placeholder.classList.remove('hidden'); | |
| els.downloadBtn.disabled = true; | |
| state.currentImageBlob = null; | |
| els.genTime.innerText = '-- ms'; | |
| }); | |
| // --- Helper Functions --- | |
| function simulateDelay(ms) { | |
| return new Promise(resolve => setTimeout(resolve, ms)); | |
| } | |
| function getDimensions() { | |
| const ratio = els.aspectRatio.value; | |
| // Standard dimensions for SD-Turbo (must be multiples of 8 or 16 usually) | |
| switch (ratio) { | |
| case '16:9': return { width: 768, height: 432 }; | |
| case '9:16': return { width: 432, height: 768 }; | |
| case '4:3': return { width: 640, height: 480 }; | |
| case '1:1': default: return { width: 512, height: 512 }; | |
| } | |
| } | |
| function setLoading(isLoading) { | |
| state.isGenerating = isLoading; | |
| els.generateBtn.disabled = isLoading; | |
| if (isLoading) { | |
| els.loader.classList.add('active'); | |
| } else { | |
| els.loader.classList.remove('active'); | |
| } | |
| } | |
| function updateLoadingText(text) { | |
| els.loadingText.innerText = text; | |
| } | |
| function displayImage(url) { | |
| els.placeholder.classList.add('hidden'); | |
| els.resultImage.onload = () => { | |
| els.resultImage.classList.add('active'); | |
| }; | |
| els.resultImage.src = url; | |
| } | |
| function updateHistory(url, prompt) { | |
| state.history.unshift({ url, prompt, timestamp: Date.now() }); | |
| // Limit history to 10 items | |
| if (state.history.length > 10) state.history.pop(); | |
| els.historyCount.innerText = `${state.history.length} items`; | |
| els.historyGrid.innerHTML = ''; | |
| state.history.forEach(item => { | |
| const div = document.createElement('div'); | |
| div.className = 'history-item'; | |
| div.title = item.prompt; | |
| const img = document.createElement('img'); | |
| img.src = item.url; | |
| div.appendChild(img); | |
| div.addEventListener('click', () => { | |
| displayImage(item.url); | |
| els.genTime.innerText = "From History"; | |
| }); | |
| els.historyGrid.appendChild(div); | |
| }); | |
| } | |
| function showToast(message, type = 'info') { | |
| const toast = document.createElement('div'); | |
| toast.className = `toast ${type}`; | |
| let icon = ''; | |
| if (type === 'success') icon = '<i class="ri-checkbox-circle-line" style="color:var(--success)"></i>'; | |
| else if (type === 'error') icon = '<i class="ri-error-warning-line" style="color:var(--danger)"></i>'; | |
| else icon = '<i class="ri-information-line" style="color:var(--accent)"></i>'; | |
| toast.innerHTML = `${icon}<span>${message}</span>`; | |
| els.toastContainer.appendChild(toast); | |
| setTimeout(() => { | |
| toast.style.opacity = '0'; | |
| toast.style.transform = 'translateX(100%)'; | |
| setTimeout(() => toast.remove(), 300); | |
| }, 3000); | |
| } | |
| // --- Core Functions --- | |
| async function handleGenerate() { | |
| const prompt = els.prompt.value.trim(); | |
| if (!prompt) { | |
| showToast('Please enter a prompt', 'error'); | |
| els.prompt.focus(); | |
| return; | |
| } | |
| setLoading(true); | |
| try { | |
| const startTime = performance.now(); | |
| let imageUrl; | |
| let blob; | |
| // Check if we have an API Key | |
| if (state.apiKey && state.apiKey.length > 5) { | |
| // REAL API CALL | |
| blob = await callHuggingFaceAPI(prompt); | |
| imageUrl = URL.createObjectURL(blob); | |
| } else { | |
| // DEMO MODE (Simulation) | |
| updateLoadingText("Running in Demo Mode (No API Key)..."); | |
| await simulateDelay(1500); // Fake network latency | |
| // Use picsum with seed based on prompt to make it deterministic but random-looking | |
| const seed = encodeURIComponent(prompt.substring(0, 20)); | |
| const width = getDimensions().width; | |
| const height = getDimensions().height; | |
| imageUrl = `https://picsum.photos/seed/${seed}/${width}/${height}`; | |
| // For demo, we need to fetch the blob to enable download | |
| const response = await fetch(imageUrl); | |
| blob = await response.blob(); | |
| } | |
| const endTime = performance.now(); | |
| const duration = Math.round(endTime - startTime); | |
| // Update UI | |
| displayImage(imageUrl); | |
| updateHistory(imageUrl, prompt); | |
| state.currentImageBlob = blob; | |
| els.downloadBtn.disabled = false; | |
| els.genTime.innerText = `${duration} ms`; | |
| showToast('Image generated successfully!', 'success'); | |
| } catch (error) { | |
| console.error(error); | |
| showToast(error.message || 'Generation failed', 'error'); | |
| } finally { | |
| setLoading(false); | |
| } | |
| } | |
| // Call Hugging Face Inference API with Retry Logic | |
| async function callHuggingFaceAPI(prompt) { | |
| updateLoadingText("Connecting to GPU..."); | |
| const dimensions = getDimensions(); | |
| const negative = els.negPrompt.value.trim(); | |
| // Construct payload for SD-Turbo or similar | |
| const payload = { | |
| inputs: prompt, | |
| parameters: { | |
| negative_prompt: negative, | |
| guidance_scale: parseFloat(els.guidance.value), | |
| num_inference_steps: parseInt(els.steps.value), | |
| // Some models require explicit width/height in payload | |
| width: dimensions.width, | |
| height: dimensions.height | |
| } | |
| }; | |
| const maxRetries = 3; | |
| let retryCount = 0; | |
| while (retryCount < maxRetries) { | |
| try { | |
| const response = await fetch( | |
| `https://api-inference.huggingface.co/models/${state.model}`, | |
| { | |
| headers: { | |
| Authorization: `Bearer ${state.apiKey}`, | |
| "Content-Type": "application/json", | |
| }, | |
| method: "POST", | |
| body: JSON.stringify(payload), | |
| } | |
| ); | |
| // 1. Handle Model Loading (503) - Common cause of failures | |
| if (response.status === 503) { | |
| updateLoadingText("Model is warming up..."); | |
| try { | |
| const data = await response.json(); | |
| // estimated_time is in seconds, convert to ms | |
| const waitTime = (data.estimated_time || 20) * 1000; | |
| await simulateDelay(waitTime); | |
| } catch (e) { | |
| // Fallback wait if JSON parsing fails | |
| await simulateDelay(20000); | |
| } | |
| retryCount++; | |
| continue; // Retry the request | |
| } | |
| // 2. Handle other HTTP errors | |
| if (!response.ok) { | |
| let errorDetail = "Unknown Error"; | |
| try { | |
| const errorJson = await response.json(); | |
| errorDetail = errorJson.error || `HTTP ${response.status}`; | |
| } catch (e) { | |
| errorDetail = response.statusText; | |
| } | |
| throw new Error(`API Error: ${errorDetail}`); | |
| } | |
| // 3. Success - Get Blob | |
| updateLoadingText("Processing pixels..."); | |
| const blob = await response.blob(); | |
| return blob; | |
| } catch (err) { | |
| // If it's a network error (not 503), we can retry or fail | |
| // For simplicity here, if max retries hit, throw error | |
| if (retryCount >= maxRetries - 1) { | |
| throw err; | |
| } | |
| retryCount++; | |
| await simulateDelay(1000); // Wait a bit before retrying network error | |
| } | |
| } | |
| throw new Error("Max retries reached. Please try again."); | |
| } | |
| </script> | |
| </body> | |
| </html> |