diff --git a/api.py b/api.py index c6f0935..caa815b 100644 --- a/api.py +++ b/api.py @@ -3,7 +3,8 @@ import base64 from io import BytesIO from fastapi import FastAPI, HTTPException from fastapi.middleware.cors import CORSMiddleware -from pydantic import BaseModel +from pydantic import BaseModel, model_validator +from typing import Any import torch from diffusers import FluxPipeline @@ -33,6 +34,20 @@ pipe.vae.enable_tiling() class ImageRequest(BaseModel): prompt: str 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") async def generate(request: ImageRequest): @@ -42,6 +57,8 @@ async def generate(request: ImageRequest): guidance_scale=0.0, num_inference_steps=4, max_sequence_length=256, + width=request.width, + height=request.height, ).images[0] buffered = BytesIO() diff --git a/flux-index.html b/flux-index.html index d67856b..6770976 100644 --- a/flux-index.html +++ b/flux-index.html @@ -4,195 +4,7 @@ Flux Image Generator - +

Flux Image Generator

@@ -203,8 +15,27 @@ +
+ + +
+
- +
+ + +
@@ -219,6 +50,8 @@ diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..1e61a49 --- /dev/null +++ b/styles.css @@ -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; +}