diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3bc7858 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +apiserver/users.json diff --git a/api.py b/api.py deleted file mode 100644 index caa815b..0000000 --- a/api.py +++ /dev/null @@ -1,74 +0,0 @@ -import os -import base64 -from io import BytesIO -from fastapi import FastAPI, HTTPException -from fastapi.middleware.cors import CORSMiddleware -from pydantic import BaseModel, model_validator -from typing import Any -import torch -from diffusers import FluxPipeline - -app = FastAPI() - -app.add_middleware( - CORSMiddleware, - allow_origins=["*"], - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - -print("Initializing Flux-Schnell on RTX 5060 Ti (sm_120)...") -hf_token = os.environ.get("HF_TOKEN") - -pipe = FluxPipeline.from_pretrained( - "black-forest-labs/FLUX.1-schnell", - torch_dtype=torch.bfloat16, - token=hf_token -) - -# Offload execution stages sequentially to maintain 16GB headroom -pipe.enable_sequential_cpu_offload() -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): - try: - result = pipe( - request.prompt, - guidance_scale=0.0, - num_inference_steps=4, - max_sequence_length=256, - width=request.width, - height=request.height, - ).images[0] - - buffered = BytesIO() - result.save(buffered, format="PNG") - img_str = base64.b64encode(buffered.getvalue()).decode() - - return {"data": [{"b64_json": img_str}]} - except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) - -if __name__ == "__main__": - import uvicorn - uvicorn.run(app, host="0.0.0.0", port=5005) diff --git a/api_bk1.py b/api_bk1.py deleted file mode 100644 index 04e0d50..0000000 --- a/api_bk1.py +++ /dev/null @@ -1,59 +0,0 @@ -from fastapi import FastAPI, HTTPException -from fastapi.middleware.cors import CORSMiddleware -from pydantic import BaseModel -import torch -from diffusers import FluxPipeline -import base64 -from io import BytesIO - -app = FastAPI() - -app.add_middleware( - CORSMiddleware, - allow_origins=["*"], - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - -# Load FLUX.1-schnell (Best for 16GB VRAM + Fast Generation) -print("Initializing Flux-Schnell on RTX 5060 Ti (sm_120)...") -try: - pipe = FluxPipeline.from_pretrained( - "black-forest-labs/FLUX.1-schnell", - torch_dtype=torch.bfloat16 - ) - # Optimized for 16GB: Moves parts of the model to CPU when not in use - # pipe.enable_model_cpu_offload() - pipe.enable_sequential_cpu_offload() - pipe.vae.enable_tiling() -except Exception as e: - print(f"Error loading model: {e}") - -class ImageRequest(BaseModel): - prompt: str - model: str = "flux-schnell" # Matches the error fix from earlier - -@app.post("/v1/images/generations") -async def generate(request: ImageRequest): - try: - # Flux-Schnell generates high quality in just 4 steps - result = pipe( - request.prompt, - guidance_scale=0.0, - num_inference_steps=4, - max_sequence_length=256, - ).images[0] - - # Convert to Base64 for the API response - buffered = BytesIO() - result.save(buffered, format="PNG") - img_str = base64.b64encode(buffered.getvalue()).decode() - - return {"data": [{"b64_json": img_str}]} - except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) - -if __name__ == "__main__": - import uvicorn - uvicorn.run(app, host="0.0.0.0", port=5005) diff --git a/Dockerfile b/apiserver/Dockerfile similarity index 96% rename from Dockerfile rename to apiserver/Dockerfile index 824066a..3614a91 100644 --- a/Dockerfile +++ b/apiserver/Dockerfile @@ -12,7 +12,8 @@ RUN pip3 install --no-cache-dir \ diffusers==0.35.2 \ accelerate==0.30.1 \ sentencepiece \ - protobuf + protobuf \ + bcrypt # Set up the application WORKDIR /app diff --git a/apiserver/api.py b/apiserver/api.py new file mode 100644 index 0000000..f469d05 --- /dev/null +++ b/apiserver/api.py @@ -0,0 +1,191 @@ +import os +import json +import base64 +import time +import hmac +import hashlib +import secrets +from io import BytesIO +from fastapi import FastAPI, Request, HTTPException +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import JSONResponse +from pydantic import BaseModel, model_validator +from typing import Any +import torch +from diffusers import FluxPipeline +import bcrypt + +app = FastAPI() + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +print("Initializing Flux-Schnell on RTX 5060 Ti (sm_120)...") +hf_token = os.environ.get("HF_TOKEN") + +pipe = FluxPipeline.from_pretrained( + "black-forest-labs/FLUX.1-schnell", + torch_dtype=torch.bfloat16, + token=hf_token +) + +# Offload execution stages sequentially to maintain 16GB headroom +pipe.enable_sequential_cpu_offload() +pipe.vae.enable_tiling() + +# Auth config +USERS_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'users.json') +SESSION_SECRET = os.environ.get('SESSION_SECRET', secrets.token_hex(32)) +SESSION_DURATION = 86400 # 24 hours + + +def load_users(): + with open(USERS_FILE) as f: + return json.load(f) + + +def create_session(username: str) -> str: + payload = f"{username}:{int(time.time())}" + signature = hmac.new( + SESSION_SECRET.encode(), + payload.encode(), + hashlib.sha256 + ).hexdigest() + return f"{payload}.{signature}" + + +def verify_session(session: str) -> str | None: + try: + payload, sig = session.rsplit('.', 1) + expected_sig = hmac.new( + SESSION_SECRET.encode(), + payload.encode(), + hashlib.sha256 + ).hexdigest() + if not hmac.compare_digest(sig, expected_sig): + return None + username, timestamp = payload.split(':', 1) + if time.time() - int(timestamp) > SESSION_DURATION: + return None + users = load_users() + if username not in users: + return None + return username + except Exception: + return None + + +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.middleware("http") +async def auth_middleware(request: Request, call_next): + # Allow auth endpoints and static files through + if request.url.path.startswith(("/auth/login", "/auth/logout", "/auth/check")): + return await call_next(request) + + # Check session cookie for API requests + session = request.cookies.get("session") + username = verify_session(session) if session else None + + if not username: + return JSONResponse( + status_code=401, + content={"detail": "Not authenticated"} + ) + + request.state.username = username + return await call_next(request) + + +@app.post("/auth/login") +async def login(request: Request): + data = await request.json() + username = data.get('username', '') + password = data.get('password', '') + users = load_users() + + if username not in users: + raise HTTPException(401, "Invalid credentials") + + if not bcrypt.checkpw(password.encode(), users[username].encode()): + raise HTTPException(401, "Invalid credentials") + + session = create_session(username) + response = JSONResponse(content={"username": username}) + response.set_cookie( + key="session", + value=session, + httponly=True, + samesite="strict", + max_age=SESSION_DURATION, + path="/", + ) + return response + + +@app.post("/auth/logout") +async def logout(): + response = JSONResponse(content={"message": "Logged out"}) + response.set_cookie("session", "", expires=0, path="/") + return response + + +@app.get("/auth/check") +async def check(request: Request, session: str | None = None): + username = verify_session(session) if session else None + if not username: + raise HTTPException(401, "Not authenticated") + return {"username": username} + + +@app.post("/v1/images/generations") +async def generate(request: Request): + try: + body = await request.json() + req = ImageRequest(**body) + except Exception as e: + raise HTTPException(status_code=400, detail=str(e)) + + try: + result = pipe( + req.prompt, + guidance_scale=0.0, + num_inference_steps=4, + max_sequence_length=256, + width=req.width, + height=req.height, + ).images[0] + + buffered = BytesIO() + result.save(buffered, format="PNG") + img_str = base64.b64encode(buffered.getvalue()).decode() + + return {"data": [{"b64_json": img_str}]} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=5005) diff --git a/apiserver/docker.compose b/apiserver/docker.compose new file mode 100644 index 0000000..ff1602e --- /dev/null +++ b/apiserver/docker.compose @@ -0,0 +1,27 @@ +services: + flux-api: + image: flux-image:1.0 + container_name: flux-api + ports: + - "5005:5005" + ipc: host + ulimits: + memlock: -1 + stack: 67108864 + environment: + - NVIDIA_VISIBLE_DEVICES=all + - NVIDIA_DRIVER_CAPABILITIES=compute,utility + - HF_TOKEN=${HF_TOKEN} + - HF_HOME=/root/.cache/huggingface + - SESSION_SECRET=${SESSION_SECRET} + volumes: + - /opt/huggingface_cache:/root/.cache/huggingface + - /home/sysuser/Development/flux-api/users.json:/app/users.json + deploy: + resources: + reservations: + devices: + - driver: nvidia + count: all + capabilities: [gpu] + restart: unless-stopped diff --git a/downloads/flux_images/openai_gpt-image-2-medium_20260927_140913_ba2d985d.png b/downloads/flux_images/openai_gpt-image-2-medium_20260927_140913_ba2d985d.png new file mode 100644 index 0000000..a698c4a Binary files /dev/null and b/downloads/flux_images/openai_gpt-image-2-medium_20260927_140913_ba2d985d.png differ diff --git a/nginx.Dockerfile b/nginx.Dockerfile deleted file mode 100644 index 4a3a36e..0000000 --- a/nginx.Dockerfile +++ /dev/null @@ -1,4 +0,0 @@ -FROM nginx:alpine -COPY nginx.conf /etc/nginx/conf.d/default.conf -COPY index.html /usr/share/nginx/html/index.html -COPY styles.css /usr/share/nginx/html/styles.css diff --git a/nginx.conf b/nginx.conf deleted file mode 100644 index 70aa73e..0000000 --- a/nginx.conf +++ /dev/null @@ -1,9 +0,0 @@ -server { - listen 80; - root /usr/share/nginx/html; - index index.html; - - location / { - try_files $uri $uri/ =404; - } -} diff --git a/webserver/docker.compose b/webserver/docker.compose new file mode 100644 index 0000000..1066b51 --- /dev/null +++ b/webserver/docker.compose @@ -0,0 +1,11 @@ +services: + webserver: + image: nginx:alpine + volumes: + - /home/sysuser/html:/usr/share/nginx/html:ro + - /home/sysuser/nginx.conf:/etc/nginx/conf.d/default.conf:ro + + restart: unless-stopped + + ports: + - "8080:80" diff --git a/index.html b/webserver/index.html similarity index 63% rename from index.html rename to webserver/index.html index d16fc78..689e150 100644 --- a/index.html +++ b/webserver/index.html @@ -234,15 +234,101 @@ .clear-btn:hover { color: #a78bfa; } + + #login-overlay { + position: fixed; + inset: 0; + background: #0f0f13; + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + } + #login-overlay.hidden { display: none; } + .login-box { + width: 100%; + max-width: 360px; + padding: 2rem; + text-align: center; + } + .login-box h2 { + font-size: 1.5rem; + margin-bottom: 0.5rem; + background: linear-gradient(135deg, #a78bfa, #60a5fa); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + } + .login-subtitle { + color: #666; + font-size: 0.85rem; + margin-bottom: 1.5rem; + } + .login-box input[type="text"], + .login-box input[type="password"] { + width: 100%; + padding: 0.75rem 1rem; + margin-bottom: 0.75rem; + font-size: 1rem; + border: 1px solid #2a2a35; + border-radius: 8px; + background: #1a1a24; + color: #e0e0e0; + outline: none; + } + .login-box input:focus { border-color: #a78bfa; } + #login-btn { + width: 100%; + padding: 0.75rem; + font-size: 1rem; + font-weight: 600; + border: none; + border-radius: 8px; + background: linear-gradient(135deg, #7c3aed, #3b82f6); + color: white; + cursor: pointer; + } + #login-btn:disabled { opacity: 0.5; cursor: not-allowed; } + .login-error { + color: #f87171; + font-size: 0.85rem; + margin-bottom: 0.75rem; + } + #logout-btn { + position: fixed; + top: 1rem; + right: 1rem; + z-index: 999; + background: #1a1a24; + color: #a78bfa; + border: 1px solid #2a2a35; + padding: 0.5rem 1rem; + border-radius: 8px; + cursor: pointer; + font-size: 0.85rem; + } + #logout-btn:hover { background: #22222e; }
+Sign in to continue
+ +Powered by flux-schnell