192 lines
5.2 KiB
Python
192 lines
5.2 KiB
Python
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)
|