Refactor CSS into separate file, add size selector and prompt clear button

- Extract styles.css from inline <style> in flux-index.html
- Add image size selector with 9 preset aspect ratios (standard + XL)
- Add clear (✕) button in prompt input field
- Pass width/height to Flux pipeline API endpoint
This commit is contained in:
2026-09-27 15:55:21 -05:00
parent 72f40c7792
commit 9afe6bce7b
3 changed files with 279 additions and 192 deletions
+18 -1
View File
@@ -3,7 +3,8 @@ import base64
from io import BytesIO from io import BytesIO
from fastapi import FastAPI, HTTPException from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel from pydantic import BaseModel, model_validator
from typing import Any
import torch import torch
from diffusers import FluxPipeline from diffusers import FluxPipeline
@@ -33,6 +34,20 @@ pipe.vae.enable_tiling()
class ImageRequest(BaseModel): class ImageRequest(BaseModel):
prompt: str prompt: str
model: str = "flux-schnell" model: str = "flux-schnell"
width: int = 1024
height: int = 1024
@model_validator(mode="before")
@classmethod
def validate_dimensions(cls, data: Any) -> Any:
if isinstance(data, dict):
width = data.get("width", 1024)
height = data.get("height", 1024)
if width % 32 != 0 or height % 32 != 0:
raise ValueError("Width and height must be multiples of 32")
if width > 2048 or height > 2048:
raise ValueError("Maximum width and height is 2048")
return data
@app.post("/v1/images/generations") @app.post("/v1/images/generations")
async def generate(request: ImageRequest): async def generate(request: ImageRequest):
@@ -42,6 +57,8 @@ async def generate(request: ImageRequest):
guidance_scale=0.0, guidance_scale=0.0,
num_inference_steps=4, num_inference_steps=4,
max_sequence_length=256, max_sequence_length=256,
width=request.width,
height=request.height,
).images[0] ).images[0]
buffered = BytesIO() buffered = BytesIO()
+32 -191
View File
@@ -4,195 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Flux Image Generator</title> <title>Flux Image Generator</title>
<style> <link rel="stylesheet" href="styles.css">
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #0f0f13;
color: #e0e0e0;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding: 2rem 1rem;
}
h1 {
font-size: 1.8rem;
font-weight: 600;
margin-bottom: 0.3rem;
background: linear-gradient(135deg, #a78bfa, #60a5fa);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.subtitle {
color: #666;
font-size: 0.85rem;
margin-bottom: 2rem;
}
.input-area {
width: 100%;
max-width: 640px;
display: flex;
gap: 0.5rem;
margin-bottom: 2rem;
}
#prompt {
flex: 1;
padding: 0.85rem 1rem;
font-size: 1rem;
border: 1px solid #2a2a35;
border-radius: 10px;
background: #1a1a24;
color: #e0e0e0;
outline: none;
transition: border-color 0.2s;
}
#prompt:focus {
border-color: #a78bfa;
}
#prompt::placeholder {
color: #555;
}
#generate-btn {
padding: 0.85rem 1.5rem;
font-size: 1rem;
font-weight: 600;
border: none;
border-radius: 10px;
background: linear-gradient(135deg, #7c3aed, #3b82f6);
color: white;
cursor: pointer;
transition: opacity 0.2s, transform 0.1s;
white-space: nowrap;
}
#generate-btn:hover:not(:disabled) {
opacity: 0.9;
}
#generate-btn:active:not(:disabled) {
transform: scale(0.97);
}
#generate-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.result-area {
width: 100%;
max-width: 640px;
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
}
#image-container {
display: none;
flex-direction: column;
align-items: center;
gap: 1rem;
width: 100%;
}
#image-container.visible {
display: flex;
}
#result-image {
max-width: 100%;
border-radius: 12px;
border: 1px solid #2a2a35;
background: #1a1a24;
}
#download-btn {
padding: 0.7rem 1.4rem;
font-size: 0.9rem;
font-weight: 600;
border: 1px solid #2a2a35;
border-radius: 8px;
background: #1a1a24;
color: #a78bfa;
cursor: pointer;
transition: background 0.2s;
text-decoration: none;
}
#download-btn:hover {
background: #22222e;
}
.status {
font-size: 0.9rem;
color: #888;
text-align: center;
min-height: 1.4em;
}
.status.error {
color: #f87171;
}
.spinner {
display: inline-block;
width: 1em;
height: 1em;
margin-right: 0.5em;
vertical-align: -0.15em;
border: 2px solid #a78bfa;
border-right-color: transparent;
border-radius: 50%;
animation: spin 0.6s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.config {
width: 100%;
max-width: 640px;
margin-bottom: 1.5rem;
padding: 0.8rem 1rem;
background: #1a1a24;
border: 1px solid #2a2a35;
border-radius: 10px;
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.85rem;
color: #888;
}
.config label {
white-space: nowrap;
}
.config input {
flex: 1;
padding: 0.35rem 0.5rem;
font-size: 0.85rem;
border: 1px solid #2a2a35;
border-radius: 6px;
background: #0f0f13;
color: #e0e0e0;
outline: none;
}
.config input:focus {
border-color: #a78bfa;
}
</style>
</head> </head>
<body> <body>
<h1>Flux Image Generator</h1> <h1>Flux Image Generator</h1>
@@ -203,8 +15,27 @@
<input id="api-url" value="http://192.168.2.154:5005/v1/images/generations" /> <input id="api-url" value="http://192.168.2.154:5005/v1/images/generations" />
</div> </div>
<div class="config">
<label for="size-select">Size:</label>
<select id="size-select">
<option value="1024x1024">1024x1024 (1:1)</option>
<option value="1920x1088">1920x1088 (16:9)</option>
<option value="1088x1920">1088x1920 (9:16)</option>
<option value="1376x1024">1376x1024 (4:3)</option>
<option value="1024x1376">1024x1376 (3:4)</option>
<option value="1536x1024">1536x1024 (3:2)</option>
<option value="1024x1536">1024x1536 (2:3)</option>
<option value="2048x2048">2048x2048 (1:1 XL)</option>
<option value="2048x1152">2048x1152 (16:9 XL)</option>
<option value="1152x2048">1152x2048 (9:16 XL)</option>
</select>
</div>
<div class="input-area"> <div class="input-area">
<input id="prompt" type="text" placeholder="Describe the image you want to create..." autofocus /> <div class="prompt-wrapper">
<input id="prompt" type="text" placeholder="Describe the image you want to create..." autofocus />
<button id="clear-btn" class="clear-btn" title="Clear">✕</button>
</div>
<button id="generate-btn">Generate</button> <button id="generate-btn">Generate</button>
</div> </div>
@@ -219,6 +50,8 @@
<script> <script>
const promptInput = document.getElementById('prompt'); const promptInput = document.getElementById('prompt');
const clearBtn = document.getElementById('clear-btn');
const sizeSelect = document.getElementById('size-select');
const generateBtn = document.getElementById('generate-btn'); const generateBtn = document.getElementById('generate-btn');
const apiInput = document.getElementById('api-url'); const apiInput = document.getElementById('api-url');
const statusEl = document.getElementById('status'); const statusEl = document.getElementById('status');
@@ -250,10 +83,11 @@
imageContainer.classList.remove('visible'); imageContainer.classList.remove('visible');
try { try {
const [width, height] = sizeSelect.value.split('x').map(Number);
const response = await fetch(apiUrl, { const response = await fetch(apiUrl, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt, model: 'flux-schnell' }) body: JSON.stringify({ prompt, model: 'flux-schnell', width, height })
}); });
if (!response.ok) { if (!response.ok) {
@@ -290,6 +124,13 @@
promptInput.addEventListener('keydown', (e) => { promptInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') generate(); if (e.key === 'Enter') generate();
}); });
clearBtn.addEventListener('click', () => {
promptInput.value = '';
promptInput.focus();
});
promptInput.addEventListener('focus', () => {
promptInput.select();
});
</script> </script>
</body> </body>
</html> </html>
+229
View File
@@ -0,0 +1,229 @@
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #0f0f13;
color: #e0e0e0;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding: 2rem 1rem;
}
h1 {
font-size: 1.8rem;
font-weight: 600;
margin-bottom: 0.3rem;
background: linear-gradient(135deg, #a78bfa, #60a5fa);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.subtitle {
color: #666;
font-size: 0.85rem;
margin-bottom: 2rem;
}
.input-area {
width: 100%;
max-width: 640px;
display: flex;
gap: 0.5rem;
margin-bottom: 2rem;
}
#prompt {
flex: 1;
padding: 0.85rem 1rem;
font-size: 1rem;
border: 1px solid #2a2a35;
border-radius: 10px;
background: #1a1a24;
color: #e0e0e0;
outline: none;
transition: border-color 0.2s;
}
#prompt:focus {
border-color: #a78bfa;
}
#prompt::placeholder {
color: #555;
}
#generate-btn {
padding: 0.85rem 1.5rem;
font-size: 1rem;
font-weight: 600;
border: none;
border-radius: 10px;
background: linear-gradient(135deg, #7c3aed, #3b82f6);
color: white;
cursor: pointer;
transition: opacity 0.2s, transform 0.1s;
white-space: nowrap;
}
#generate-btn:hover:not(:disabled) {
opacity: 0.9;
}
#generate-btn:active:not(:disabled) {
transform: scale(0.97);
}
#generate-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.result-area {
width: 100%;
max-width: 640px;
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
}
#image-container {
display: none;
flex-direction: column;
align-items: center;
gap: 1rem;
width: 100%;
}
#image-container.visible {
display: flex;
}
#result-image {
max-width: 100%;
border-radius: 12px;
border: 1px solid #2a2a35;
background: #1a1a24;
}
#download-btn {
padding: 0.7rem 1.4rem;
font-size: 0.9rem;
font-weight: 600;
border: 1px solid #2a2a35;
border-radius: 8px;
background: #1a1a24;
color: #a78bfa;
cursor: pointer;
transition: background 0.2s;
text-decoration: none;
}
#download-btn:hover {
background: #22222e;
}
.status {
font-size: 0.9rem;
color: #888;
text-align: center;
min-height: 1.4em;
}
.status.error {
color: #f87171;
}
.spinner {
display: inline-block;
width: 1em;
height: 1em;
margin-right: 0.5em;
vertical-align: -0.15em;
border: 2px solid #a78bfa;
border-right-color: transparent;
border-radius: 50%;
animation: spin 0.6s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.config {
width: 100%;
max-width: 640px;
margin-bottom: 1.5rem;
padding: 0.8rem 1rem;
background: #1a1a24;
border: 1px solid #2a2a35;
border-radius: 10px;
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.85rem;
color: #888;
}
.config label {
white-space: nowrap;
}
.config input {
flex: 1;
padding: 0.35rem 0.5rem;
font-size: 0.85rem;
border: 1px solid #2a2a35;
border-radius: 6px;
background: #0f0f13;
color: #e0e0e0;
outline: none;
}
.config input:focus,
.config select:focus {
border-color: #a78bfa;
}
.config select {
flex: 1;
padding: 0.35rem 0.5rem;
font-size: 0.85rem;
border: 1px solid #2a2a35;
border-radius: 6px;
background: #0f0f13;
color: #e0e0e0;
outline: none;
cursor: pointer;
}
.prompt-wrapper {
flex: 1;
position: relative;
display: flex;
align-items: center;
}
.prompt-wrapper #prompt {
padding-right: 2.5rem;
}
.clear-btn {
position: absolute;
right: 0.6rem;
background: none;
border: none;
color: #555;
font-size: 1rem;
cursor: pointer;
padding: 0.3rem;
line-height: 1;
transition: color 0.2s;
z-index: 1;
}
.clear-btn:hover {
color: #a78bfa;
}