Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <meta name="viewport" content="width=device-width,initial-scale=1" /> | |
| <title>imagegen</title> | |
| <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600&display=swap" rel="stylesheet"> | |
| <!-- Tracking removed per request (Smartlook + Google Tag). TODO: add GTM later if needed. --> | |
| <style> | |
| *, *::before, *::after { box-sizing: border-box; } | |
| html, body { height: 100%; margin: 0; padding: 0; font-family: 'Poppins', sans-serif; -webkit-font-smoothing:antialiased; -moz-osx-font-smoothing:grayscale; } | |
| body { background: radial-gradient(circle at 20% 20%, rgba(140,60,200,0.08), transparent 40%), radial-gradient(circle at 80% 80%, rgba(200,60,255,0.05), transparent 40%), #000; color:#fff; overflow: hidden; } | |
| .topbar { | |
| position: fixed; top: 0; left: 0; right: 0; height: 56px; | |
| display: flex; align-items: center; justify-content: space-between; | |
| padding: 0 16px; gap: 12px; z-index: 1000; background: transparent; | |
| } | |
| .btn-small { | |
| background: #141414; border-radius: 10px; padding: 8px 12px; | |
| font-size: 13px; color: #fff; display:inline-flex; align-items:center; gap:10px; | |
| border:1px solid rgba(255,255,255,0.03); cursor: pointer; | |
| } | |
| .btn-small img { width:14px; height:14px; display:block; } | |
| .container { width: 70%; max-width: 1100px; margin: 5em auto 16px; padding: 0 12px; display: flex; flex-direction: column; gap: 12px; position: relative; } | |
| @media (max-width: 768px) { .container { width: 100%; padding: 0 12px; margin-top:5em; } } | |
| .input-row { display:flex; gap:10px; align-items:flex-start; } | |
| textarea#prompt { | |
| flex:1; min-width:0; max-height:250px; height:56px; resize:none; border-radius:12px; padding:12px; | |
| background:#0e0e0e; border:1px solid rgba(255,255,255,0.03); color:#fff; font-size:14px; line-height:1.35; overflow:auto; outline:none; | |
| } | |
| .generate-btn { | |
| background: linear-gradient(90deg,#ff93e9,#9b7cff); border:none; border-radius:12px; padding:12px 18px; font-size:14px; cursor:pointer; color:#fff; | |
| min-height:56px; display:inline-flex; align-items:center; justify-content:center; transition:opacity .15s; | |
| } | |
| .generate-btn[disabled] { opacity:0.6; cursor:default; } | |
| .options-row { display:flex; justify-content:space-between; align-items:center; } | |
| .left-group, .right-group { display:flex; gap:8px; align-items:center; } | |
| .modal-backdrop { display:none; position: fixed; inset: 0; background: rgba(0,0,0,0.72); align-items: center; justify-content: center; padding: 20px; z-index: 2000; } | |
| .modal { width: 100%; max-width: 720px; background: #0f0f0f; border-radius: 12px; padding: 18px; box-shadow: 0 10px 40px rgba(0,0,0,0.6); color: #fff; } | |
| @media (max-width:420px) { .modal { padding: 14px; max-width: calc(100% - 12px); border-radius: 10px; } } | |
| .styles-grid { display:grid; grid-template-columns: repeat(2, 1fr); gap:10px; margin-top:12px; } | |
| .style-card { background:#101010; border-radius:10px; overflow:hidden; cursor:pointer; text-align:center; border:2px solid transparent; padding-bottom:8px; font-size:13px; } | |
| .style-card img { width:100%; height:72px; object-fit:cover; display:block; } | |
| .style-card.selected { border-color:#ff93e9; box-shadow: 0 8px 30px rgba(155,124,255,0.06); } | |
| .settings-row { display:flex; gap:12px; align-items:center; margin-top:10px; flex-wrap:wrap; } | |
| .settings-row label { font-size:14px; display:flex; gap:8px; align-items:center; color:#eaeaea; } | |
| .settings-row input[type="number"], .settings-row select { background:#0c0c0c; border:1px solid rgba(255,255,255,0.04); color:#fff; padding:6px 8px; border-radius:8px; font-size:14px; min-width:90px; } | |
| .modal-actions { margin-top:14px; display:flex; justify-content:flex-end; gap:10px; } | |
| .modal-actions button { background:#1a1a1a; border-radius:10px; padding:9px 12px; font-size:14px; color:#fff; border:1px solid rgba(255,255,255,0.03); cursor:pointer; } | |
| #images { width: 100%; margin-top:8px; display:flex; gap:12px; flex-wrap:wrap; justify-content:center; align-items:flex-start; overflow:auto; padding:10px; border-radius:10px; background: linear-gradient(180deg, rgba(255,255,255,0.01), transparent); border:1px solid rgba(255,255,255,0.02); max-height: calc(100vh - 5em - 220px); } | |
| .image-card { background:#0b0b0b; border-radius:12px; padding:8px; min-height:120px; width: min(350px, 90vw); box-shadow: 0 10px 30px rgba(0,0,0,0.6); border:1px solid rgba(255,255,255,0.02); display:flex; flex-direction:column; gap:8px; align-items:center; justify-content:center; } | |
| .image-card img { width:100%; height:auto; display:block; border-radius:8px; } | |
| .img-actions { display:flex; gap:8px; flex-wrap:wrap; justify-content:flex-end; width:100%; } | |
| .img-actions button { background:#141414; border:1px solid rgba(255,255,255,0.08); color:#fff; border-radius:8px; padding:6px 10px; font-size:12px; cursor:pointer; } | |
| .loader { border:3px solid #222; border-top:3px solid #ff93e9; border-radius:50%; width:22px; height:22px; animation:spin 1s linear infinite; } | |
| @keyframes spin { 0%{ transform:rotate(0) } 100%{ transform:rotate(360deg) } } | |
| .muted { color:#9b9bb9; font-size:13px; } | |
| .note { font-size:13px; color:#cfcfcf; margin-top:6px; } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="topbar" role="banner"> | |
| <!-- Replaced Discord invite with Manus invite --> | |
| <div class="btn-small" onclick="window.open('https://manus.im/invitation/XHHHQTMBM0ZCTX')">Manus</div> | |
| <!-- Keep Buy Me a Coffee visible but remove link for now --> | |
| <!-- TODO: add Buy Me a Coffee link later --> | |
| <a class="btn-small" aria-disabled="true"> | |
| <img src="https://cdn.buymeacoffee.com/buttons/bmc-new-btn-logo.svg" alt="coffee"> | |
| <span>Buy me a coffee</span> | |
| </a> | |
| </div> | |
| <div class="container" role="main"> | |
| <div class="input-row" role="search"> | |
| <textarea id="prompt" placeholder="Enter your prompt..." aria-label="Prompt"></textarea> | |
| <button id="generateBtn" class="generate-btn" aria-label="Generate">Generate</button> | |
| </div> | |
| <div class="options-row"> | |
| <div class="left-group"> | |
| <div id="styleBtn" class="btn-small" aria-haspopup="dialog">🎨 Style</div> | |
| </div> | |
| <div class="right-group"> | |
| <div id="settingsBtn" class="btn-small" aria-haspopup="dialog">⚙️ Settings</div> | |
| </div> | |
| </div> | |
| <div class="note">Generated images (scroll this pane). New images appear at the top.</div> | |
| <div id="images" aria-live="polite"></div> | |
| </div> | |
| <!-- Style & Framing Modal --> | |
| <div id="styleModal" class="modal-backdrop" role="dialog" aria-modal="true" aria-hidden="true"> | |
| <div class="modal" role="document"> | |
| <h2 style="margin:0 0 6px 0;">Choose a style</h2> | |
| <div class="note">Click a style — it will be applied silently to your request.</div> | |
| <div class="styles-grid" id="stylesGrid"> | |
| <div class="style-card selected" data-name="none"><img src="none.jpg" alt="No style"><div>No style</div></div> | |
| <div class="style-card" data-name="cinema"><img src="https://xyplon.web.app/assets/Cinematic.jpeg" alt="Cinema"><div>Cinema</div></div> | |
| <div class="style-card" data-name="realistic"><img src="https://xyplon.web.app/assets/Realistic.jpeg" alt="Realistic"><div>Realistic</div></div> | |
| <div class="style-card" data-name="photography"><img src="https://xyplon.web.app/assets/Photography.jpeg" alt="Photography"><div>Photography</div></div> | |
| <div class="style-card" data-name="fantasy"><img src="https://xyplon.web.app/assets/Digital.jpeg" alt="Fantasy"><div>Fantasy</div></div> | |
| </div> | |
| <!-- Framing presets (one-line each) --> | |
| <div class="settings-row" style="margin-top:14px;"> | |
| <label>Framing: | |
| <select id="framing"> | |
| <option value="auto" selected>Auto</option> | |
| <option value="close">Close‑up</option> | |
| <option value="medium">Medium</option> | |
| <option value="full">Full‑body</option> | |
| </select> | |
| </label> | |
| </div> | |
| <div class="modal-actions"> | |
| <button id="closeStyle">Close</button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Settings Modal --> | |
| <div id="settingsModal" class="modal-backdrop" role="dialog" aria-modal="true" aria-hidden="true"> | |
| <div class="modal" role="document"> | |
| <h2 style="margin:0 0 6px 0;">API Settings</h2> | |
| <div class="settings-row"> | |
| <label>Model: | |
| <select id="model"> | |
| <option value="flux">flux</option> | |
| <option value="turbo">turbo (uncensored)</option> | |
| </select> | |
| </label> | |
| <label>Aspect: | |
| <select id="aspect"> | |
| <option value="custom" selected>Custom</option> | |
| <option value="1:1">1:1</option> | |
| <option value="16:9">16:9</option> | |
| <option value="9:16">9:16</option> | |
| <option value="3:2">3:2</option> | |
| <option value="2:3">2:3</option> | |
| <option value="4:5">4:5</option> | |
| </select> | |
| </label> | |
| <label>Width: | |
| <input id="width" type="number" min="64" max="2048" value="1024" /> | |
| </label> | |
| <label>Height: | |
| <input id="height" type="number" min="64" max="2048" value="1024" /> | |
| </label> | |
| <label>Enhance: | |
| <input id="enhance" type="checkbox" /> | |
| </label> | |
| <label>Seed: | |
| <input id="seed" type="number" value="42" /> | |
| </label> | |
| <label style="display:flex;align-items:center;gap:6px;"> | |
| <input id="randomSeed" type="checkbox" checked /> Random | |
| </label> | |
| </div> | |
| <div class="modal-actions"> | |
| <button id="closeSettings">Close</button> | |
| <button id="saveSettings">Save</button> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| const baseUrl = 'https://image.pollinations.ai'; | |
| let selectedStyle = null; | |
| let isGenerating = false; | |
| // --- Style templates (concise) --- | |
| const styleTemplates = { | |
| cinema: " cinematic lighting, letterbox aspect, rich color grading", | |
| realistic: " realistic stock photo", | |
| photography: "85mm lens, shallow depth of field, natural film grain", | |
| fantasy: " epic fantasy, vibrant colors, surreal composition" | |
| }; | |
| // --- Framing presets: one-line each --- | |
| const framingTemplates = { | |
| auto: "", | |
| close: " tight framing, head-and-shoulders, 85mm lens, shallow depth of field", | |
| medium: " waist-up, 50mm lens, natural perspective", | |
| full: " head-to-toe, 35mm lens, subject fully in frame" | |
| }; | |
| // --- Safer SYSTEM_PROMPT --- | |
| const SYSTEM_PROMPT = `You are an assistant that enhances user prompts for high-quality, creative image generation with the Flux model.\n\nSafety: Do not produce or encourage illegal, harmful, violent, hateful, or sexual content (including minors), or content that violates platform policies. Keep outputs appropriate and respectful.\n\nGuidelines:\n- Prefer technical clarity (camera type, lens, exposure, lighting, environment) over vague adjectives.\n- Keep color natural; avoid oversaturation unless the user asks.\n- Choose a single coherent style; avoid mixing incompatible styles (e.g., do not mix "realistic" with "photography" keywords redundantly).\n- If the user already specified lens/shot details, do not duplicate them.\n- Keep the final prompt concise and descriptive; return only the enhanced prompt text with no preamble.`; | |
| // --- Enhance prompt via text endpoint --- | |
| async function enhancePrompt(userPrompt) { | |
| try { | |
| const messages = [ | |
| { role: 'system', content: SYSTEM_PROMPT }, | |
| { role: 'user', content: `"${userPrompt}"` } | |
| ]; | |
| const res = await fetch('https://text.pollinations.ai/openai', { | |
| method: 'POST', headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ model: 'openai', messages }) | |
| }); | |
| if (!res.ok) throw new Error('HTTP ' + res.status); | |
| const data = await res.json(); | |
| return (data.choices?.[0]?.message?.content || userPrompt).trim(); | |
| } catch (e) { | |
| console.error('Enhance fail:', e); return userPrompt; | |
| } | |
| } | |
| // --- Persist settings --- | |
| function saveSettingsToStorage() { | |
| const s = { | |
| model: document.getElementById('model').value, | |
| aspect: document.getElementById('aspect').value, | |
| width: Number(document.getElementById('width').value || 1024), | |
| height: Number(document.getElementById('height').value || 1024), | |
| seed: Number(document.getElementById('seed').value || 42), | |
| randomSeed: document.getElementById('randomSeed').checked, | |
| enhance: document.getElementById('enhance').checked | |
| }; | |
| try { localStorage.setItem('ai_img_settings', JSON.stringify(s)); } catch(e){} | |
| } | |
| function loadSettingsFromStorage() { | |
| try { | |
| const s = JSON.parse(localStorage.getItem('ai_img_settings') || '{}'); | |
| if (s.model) document.getElementById('model').value = s.model; | |
| if (s.aspect) document.getElementById('aspect').value = s.aspect; | |
| if (s.width) document.getElementById('width').value = s.width; | |
| if (s.height) document.getElementById('height').value = s.height; | |
| if (s.seed || s.seed === 0) document.getElementById('seed').value = s.seed; | |
| document.getElementById('randomSeed').checked = (s.randomSeed !== undefined) ? s.randomSeed : true; | |
| document.getElementById('enhance').checked = (s.enhance !== undefined) ? s.enhance : true; // bug fixed | |
| } catch(e){} | |
| } | |
| loadSettingsFromStorage(); | |
| // --- Aspect helpers --- | |
| function parseAspect(aspectStr){ const [w,h] = aspectStr.split(':').map(Number); return (!w||!h)?1:(w/h); } | |
| function sizeForAspect(aspectStr, base=1024){ | |
| const r = parseAspect(aspectStr); | |
| if (r >= 1){ const width=base; const height=Math.max(64, Math.round(width/r)); return {width,height}; } | |
| else { const height=base; const width=Math.max(64, Math.round(height*r)); return {width,height}; } | |
| } | |
| function applyAspectToFields(){ | |
| const aspect = document.getElementById('aspect').value; | |
| if (aspect === 'custom') return; // respect manual values | |
| const {width, height} = sizeForAspect(aspect, 1024); | |
| document.getElementById('width').value = width; | |
| document.getElementById('height').value = height; | |
| } | |
| // --- Modal plumbing --- | |
| const styleModal = document.getElementById('styleModal'); | |
| const settingsModal = document.getElementById('settingsModal'); | |
| function openModal(modal) { modal.style.display = 'flex'; modal.setAttribute('aria-hidden','false'); document.body.style.overflow = 'hidden'; } | |
| function closeModal(modal) { modal.style.display = 'none'; modal.setAttribute('aria-hidden','true'); document.body.style.overflow = ''; } | |
| document.getElementById('styleBtn').addEventListener('click', () => openModal(styleModal)); | |
| document.getElementById('closeStyle').addEventListener('click', () => closeModal(styleModal)); | |
| document.getElementById('settingsBtn').addEventListener('click', () => openModal(settingsModal)); | |
| document.getElementById('closeSettings').addEventListener('click', () => closeModal(settingsModal)); | |
| document.getElementById('saveSettings').addEventListener('click', () => { | |
| // clamp + optional aspect auto-size | |
| const widthEl = document.getElementById('width'); | |
| const heightEl = document.getElementById('height'); | |
| widthEl.value = Math.max(64, Math.min(2048, Number(widthEl.value) || 1024)); | |
| heightEl.value = Math.max(64, Math.min(2048, Number(heightEl.value) || 1024)); | |
| applyAspectToFields(); | |
| saveSettingsToStorage(); | |
| closeModal(settingsModal); | |
| }); | |
| document.querySelectorAll('.modal-backdrop').forEach(back => back.addEventListener('click', (e) => { if (e.target === back) closeModal(back); })); | |
| window.addEventListener('keydown', (e) => { if (e.key === 'Escape') { if (styleModal.style.display === 'flex') closeModal(styleModal); if (settingsModal.style.display === 'flex') closeModal(settingsModal); } }); | |
| // Style select | |
| document.querySelectorAll('.style-card').forEach(card => { | |
| card.addEventListener('click', () => { | |
| document.querySelectorAll('.style-card').forEach(c => c.classList.remove('selected')); | |
| card.classList.add('selected'); | |
| selectedStyle = card.dataset.name === 'none' ? null : card.dataset.name; | |
| }); | |
| }); | |
| // Aspect change auto-size | |
| document.getElementById('aspect').addEventListener('change', applyAspectToFields); | |
| // Prompt textarea auto-size | |
| const promptEl = document.getElementById('prompt'); | |
| function autoResizeTextarea(){ promptEl.style.height='auto'; promptEl.style.height = Math.min(250, promptEl.scrollHeight) + 'px'; } | |
| promptEl.addEventListener('input', autoResizeTextarea); setTimeout(autoResizeTextarea, 0); | |
| // Seed UI | |
| const randomSeedEl = document.getElementById('randomSeed'); | |
| const seedEl = document.getElementById('seed'); | |
| randomSeedEl.addEventListener('change', () => { seedEl.disabled = randomSeedEl.checked; }); | |
| seedEl.disabled = randomSeedEl.checked; | |
| // Images + Generate | |
| const imagesContainer = document.getElementById('images'); | |
| const generateBtn = document.getElementById('generateBtn'); | |
| function randomInt(min, max){ return Math.floor(Math.random()*(max-min+1))+min; } | |
| function makeActionsBar(imgUrl, filename, finalPrompt, seed, width, height){ | |
| const bar = document.createElement('div'); | |
| bar.className = 'img-actions'; | |
| const dl = document.createElement('button'); | |
| dl.textContent = 'Download'; | |
| dl.addEventListener('click', () => { const a = document.createElement('a'); a.href = imgUrl; a.download = filename; document.body.appendChild(a); a.click(); a.remove(); }); | |
| const openBtn = document.createElement('button'); | |
| openBtn.textContent = 'Open'; | |
| openBtn.addEventListener('click', () => window.open(imgUrl,'_blank')); | |
| const cp = document.createElement('button'); | |
| cp.textContent = 'Copy prompt'; | |
| cp.addEventListener('click', async () => { try { await navigator.clipboard.writeText(finalPrompt); cp.textContent='Copied!'; setTimeout(()=>cp.textContent='Copy prompt',900);} catch(e){} }); | |
| const rerun = document.createElement('button'); | |
| rerun.textContent = 'Re‑generate'; | |
| rerun.addEventListener('click', () => { | |
| document.getElementById('randomSeed').checked = false; | |
| document.getElementById('seed').disabled = false; | |
| document.getElementById('seed').value = seed; | |
| document.getElementById('width').value = width; | |
| document.getElementById('height').value = height; | |
| document.getElementById('prompt').value = finalPrompt; | |
| autoResizeTextarea(); | |
| saveSettingsToStorage(); | |
| generateImage(); | |
| }); | |
| bar.append(dl, openBtn, cp, rerun); | |
| return bar; | |
| } | |
| async function generateImage(){ | |
| if (isGenerating) return; | |
| const rawPrompt = promptEl.value.trim(); | |
| if (!rawPrompt){ | |
| const temp=document.createElement('div'); temp.className='image-card'; temp.innerHTML='<div class="muted">Please enter a prompt</div>'; imagesContainer.prepend(temp); setTimeout(()=>temp.remove(),1200); return; | |
| } | |
| isGenerating = true; generateBtn.disabled=true; | |
| const card = document.createElement('div'); | |
| card.className='image-card'; | |
| card.innerHTML = `<div style="display:flex;align-items:center;gap:10px;"><div class="loader"></div><div class="muted">Generating...</div></div>`; | |
| imagesContainer.prepend(card); | |
| const enhanceOn = document.getElementById('enhance').checked; | |
| const framingChoice = document.getElementById('framing').value; | |
| // Build prompt (style + framing in concise one-liners) | |
| let promptWithMods = rawPrompt; | |
| if (selectedStyle && styleTemplates[selectedStyle]) promptWithMods += styleTemplates[selectedStyle]; | |
| if (framingTemplates[framingChoice]) promptWithMods += framingTemplates[framingChoice]; | |
| let finalPrompt = promptWithMods; | |
| if (enhanceOn){ | |
| card.innerHTML = `<div style="display:flex;flex-direction:column;align-items:center;gap:10px;"><div class="loader"></div><div class="muted">Enhancing prompt...</div></div>`; | |
| finalPrompt = await enhancePrompt(promptWithMods); | |
| card.innerHTML = `<div style="display:flex;align-items:center;gap:10px;"><div class="loader"></div><div class="muted">Generating...</div></div>`; | |
| } | |
| // Params | |
| const model = encodeURIComponent(document.getElementById('model').value || 'flux'); | |
| const width = Math.max(64, Math.min(2048, Number(document.getElementById('width').value) || 1024)); | |
| const height = Math.max(64, Math.min(2048, Number(document.getElementById('height').value) || 1024)); | |
| const seed = document.getElementById('randomSeed').checked ? randomInt(0, 4294967295) : (Number(document.getElementById('seed').value) || 42); | |
| try{ | |
| const encodedPrompt = encodeURIComponent(finalPrompt); | |
| const url = `${baseUrl}/prompt/${encodedPrompt}?model=${model}&width=${width}&height=${height}&seed=${seed}&nologo=true&safe=false`; | |
| const res = await fetch(url); | |
| if(!res.ok) throw new Error('Network '+res.status); | |
| const blob = await res.blob(); | |
| const imgUrl = URL.createObjectURL(blob); | |
| const filename = `image_${width}x${height}_seed${seed}.png`; | |
| card.innerHTML = `<img src="${imgUrl}" alt="generated image">`; | |
| card.appendChild(makeActionsBar(imgUrl, filename, finalPrompt, seed, width, height)); | |
| }catch(err){ | |
| console.error(err); | |
| card.innerHTML = `<div class="muted" style="color:#ff6b6b;">Error generating image</div>`; | |
| }finally{ | |
| isGenerating=false; generateBtn.disabled=false; saveSettingsToStorage(); | |
| } | |
| } | |
| document.getElementById('generateBtn').addEventListener('click', generateImage); | |
| promptEl.addEventListener('keydown', (e)=>{ if(e.key==='Enter'&&(e.ctrlKey||e.metaKey)) generateImage(); }); | |
| window.addEventListener('beforeunload', saveSettingsToStorage); | |
| </script> | |
| </body> | |
| </html> | |