Spaces:
Sleeping
Sleeping
| import httpx | |
| import os | |
| from fastapi import FastAPI, Request, WebSocket, WebSocketDisconnect | |
| from fastapi.responses import RedirectResponse | |
| from starlette.middleware.base import BaseHTTPMiddleware | |
| from fastapi.responses import HTMLResponse | |
| from fastapi.staticfiles import StaticFiles | |
| from fastapi.templating import Jinja2Templates | |
| from contextlib import asynccontextmanager | |
| from server.queue import GenerationQueue | |
| from server.nai_api import generate_image, encode_vibes, augment_image | |
| from server.tagger import tagger_predictor | |
| from server.cloud_storage import cloud_storage | |
| from server.auth import ( | |
| is_authenticated, verify_credentials, create_session, | |
| login_page_html, auth_enabled, SESSION_COOKIE, SESSION_MAX_AGE, | |
| ) | |
| gen_queue = GenerationQueue() | |
| tagger_queue = GenerationQueue() | |
| async def lifespan(app: FastAPI): | |
| gen_queue.start(handle_task) | |
| tagger_queue.start(handle_tagger_task) | |
| yield | |
| app = FastAPI(title="NAI Studio", lifespan=lifespan) | |
| app.mount("/static", StaticFiles(directory="static"), name="static") | |
| app.mount("/data", StaticFiles(directory="data"), name="data") | |
| templates = Jinja2Templates(directory="templates") | |
| # ── Auth Middleware ────────────────────────────── | |
| class AuthMiddleware(BaseHTTPMiddleware): | |
| # Paths that don't require authentication | |
| PUBLIC_PATHS = {"/login", "/static/", "/favicon.ico"} | |
| async def dispatch(self, request: Request, call_next): | |
| if not auth_enabled(): | |
| return await call_next(request) | |
| path = request.url.path | |
| # Allow public paths | |
| for pp in self.PUBLIC_PATHS: | |
| if path == pp or path.startswith(pp): | |
| return await call_next(request) | |
| # Check auth | |
| if not is_authenticated(request): | |
| if path == "/" or path.startswith("/api/") or path.startswith("/data/"): | |
| return RedirectResponse(url="/login", status_code=302) | |
| # For WebSocket, just reject | |
| return RedirectResponse(url="/login", status_code=302) | |
| return await call_next(request) | |
| app.add_middleware(AuthMiddleware) | |
| # ── Queue task handler ────────────────────────── | |
| async def handle_task(task: dict, queue: GenerationQueue): | |
| client_id = task["client_id"] | |
| task_type = task["type"] | |
| try: | |
| if task_type == "generate": | |
| await queue.notify_client(client_id, { | |
| "type": "status", "status": "generating", | |
| "message": "正在生成图片...", | |
| }) | |
| result = await generate_image(task["params"]) | |
| queue.last_seeds[client_id] = result.get("seed", 0) | |
| # Cloud save if requested | |
| cloud_path = None | |
| if task["params"].get("save_to_cloud", False) and result.get("image"): | |
| try: | |
| import asyncio | |
| loop = asyncio.get_event_loop() | |
| seed = result.get("seed", 0) | |
| cloud_path = await loop.run_in_executor( | |
| None, cloud_storage.save_image, result["image"], seed | |
| ) | |
| # Store search text and upload in background | |
| if cloud_path: | |
| # Build search text from params | |
| p = task["params"] | |
| search_parts = [] | |
| for k in ["prompt", "quality_prompt", "negative_prompt", "model", "sampler"]: | |
| if p.get(k): | |
| search_parts.append(str(p[k])) | |
| if result.get("seed"): | |
| search_parts.append(str(result["seed"])) | |
| if p.get("steps"): | |
| search_parts.append(str(p["steps"])) | |
| # Character prompts | |
| char_data = p.get("character", {}) | |
| for ch in char_data.get("characters", []): | |
| if ch.get("prompt"): | |
| search_parts.append(ch["prompt"]) | |
| search_text = " ".join(search_parts) | |
| cloud_storage.set_search_text(cloud_path, search_text) | |
| loop.run_in_executor(None, cloud_storage.upload_file, cloud_path) | |
| except Exception as e: | |
| print(f"[Cloud] Save failed: {e}") | |
| await queue.notify_client(client_id, { | |
| "type": "result", "status": "completed", | |
| "image": result.get("image"), | |
| "info": result.get("info"), | |
| "seed": result.get("seed"), | |
| "cloud_path": cloud_path, | |
| }) | |
| elif task_type == "augment": | |
| await queue.notify_client(client_id, { | |
| "type": "status", "status": "augmenting", | |
| "message": "正在处理图片...", | |
| }) | |
| result = await augment_image(task["params"]) | |
| await queue.notify_client(client_id, { | |
| "type": "augment_result", "status": "completed", | |
| "images": result["images"], | |
| }) | |
| elif task_type == "vibe_encode": | |
| await queue.notify_client(client_id, { | |
| "type": "status", "status": "encoding", | |
| "message": "正在编码Vibe参考图片...", | |
| }) | |
| result = await encode_vibes(task["params"]) | |
| await queue.notify_client(client_id, { | |
| "type": "vibe_encode_result", "status": "completed", | |
| "model": task["params"]["model"], | |
| "results": result, | |
| }) | |
| except Exception as e: | |
| if task_type == "generate": | |
| error_type = "error" | |
| elif task_type == "augment": | |
| error_type = "augment_error" | |
| else: | |
| error_type = "vibe_encode_error" | |
| await queue.notify_client(client_id, { | |
| "type": error_type, "status": "error", | |
| "message": str(e), | |
| }) | |
| # ── Tagger queue task handler ─────────────────── | |
| async def handle_tagger_task(task: dict, queue: GenerationQueue): | |
| client_id = task["client_id"] | |
| try: | |
| await queue.notify_client(client_id, { | |
| "type": "status", "status": "tagging", | |
| "message": "正在加载模型并推理...", | |
| }) | |
| params = task["params"] | |
| import asyncio, functools | |
| loop = asyncio.get_event_loop() | |
| result = await loop.run_in_executor( | |
| None, | |
| functools.partial( | |
| tagger_predictor.predict, | |
| params["image"], | |
| params["model_repo"], | |
| params.get("general_thresh", 0.35), | |
| params.get("general_mcut", False), | |
| params.get("character_thresh", 0.85), | |
| params.get("character_mcut", False), | |
| ) | |
| ) | |
| await queue.notify_client(client_id, { | |
| "type": "tagger_result", "status": "completed", | |
| **result, | |
| }) | |
| except Exception as e: | |
| await queue.notify_client(client_id, { | |
| "type": "tagger_error", "status": "error", | |
| "message": str(e), | |
| }) | |
| # ── Routes ────────────────────────────────────── | |
| # ── Auth Routes ───────────────────────────────── | |
| async def login_page(request: Request): | |
| if is_authenticated(request): | |
| return RedirectResponse(url="/", status_code=302) | |
| return HTMLResponse(login_page_html()) | |
| async def login_submit(request: Request): | |
| form = await request.form() | |
| username = form.get("username", "") | |
| password = form.get("password", "") | |
| if verify_credentials(username, password): | |
| token = create_session() | |
| response = RedirectResponse(url="/", status_code=302) | |
| response.set_cookie( | |
| SESSION_COOKIE, token, | |
| max_age=SESSION_MAX_AGE, | |
| httponly=True, | |
| samesite="lax", | |
| ) | |
| return response | |
| else: | |
| return HTMLResponse(login_page_html("用户名或密码错误"), status_code=401) | |
| async def logout(): | |
| response = RedirectResponse(url="/login", status_code=302) | |
| response.delete_cookie(SESSION_COOKIE) | |
| return response | |
| async def index(request: Request): | |
| return templates.TemplateResponse("index.html", {"request": request}) | |
| async def cloud_dates(): | |
| try: | |
| dates = cloud_storage.list_dates() | |
| return {"dates": dates} | |
| except Exception as e: | |
| return {"error": str(e)} | |
| async def cloud_images(date: str = ""): | |
| try: | |
| images = cloud_storage.list_images(date) | |
| return {"images": images} | |
| except Exception as e: | |
| return {"error": str(e)} | |
| async def cloud_image(path: str): | |
| try: | |
| b64 = cloud_storage.get_image_b64(path) | |
| if b64 is None: | |
| return {"error": "Not found"} | |
| return {"image": b64} | |
| except Exception as e: | |
| return {"error": str(e)} | |
| async def cloud_delete(request: Request): | |
| data = await request.json() | |
| path = data.get("path", "") | |
| try: | |
| import asyncio | |
| loop = asyncio.get_event_loop() | |
| await loop.run_in_executor(None, cloud_storage.delete_file, path) | |
| return {"ok": True} | |
| except Exception as e: | |
| return {"error": str(e)} | |
| async def cloud_sync(): | |
| try: | |
| import asyncio | |
| loop = asyncio.get_event_loop() | |
| await loop.run_in_executor(None, cloud_storage.sync_download) | |
| return {"ok": True} | |
| except Exception as e: | |
| return {"error": str(e)} | |
| async def cloud_search(q: str = "", date: str = ""): | |
| try: | |
| images = cloud_storage.search_images(q, date) | |
| return {"images": images} | |
| except Exception as e: | |
| return {"error": str(e)} | |
| async def cloud_rebuild_index(): | |
| try: | |
| import asyncio | |
| loop = asyncio.get_event_loop() | |
| await loop.run_in_executor(None, cloud_storage.build_search_index_from_files) | |
| return {"ok": True} | |
| except Exception as e: | |
| return {"error": str(e)} | |
| async def get_anlas(): | |
| api_key = os.environ.get("NAI_API_KEY", "") | |
| if not api_key: | |
| return {"error": "NAI_API_KEY not set"} | |
| try: | |
| headers = { | |
| "Authorization": f"Bearer {api_key}", | |
| } | |
| async with httpx.AsyncClient(timeout=10.0) as client: | |
| resp = await client.get( | |
| "https://image.novelai.net/user/data", | |
| headers=headers, | |
| ) | |
| data = resp.json() | |
| steps = data["subscription"]["trainingStepsLeft"] | |
| anlas = steps["fixedTrainingStepsLeft"] + steps["purchasedTrainingSteps"] | |
| return {"anlas": anlas} | |
| except Exception as e: | |
| return {"error": f"获取失败: {str(e)}"} | |
| async def websocket_endpoint(websocket: WebSocket, client_id: str): | |
| # Check auth for WebSocket | |
| if auth_enabled(): | |
| token = websocket.cookies.get(SESSION_COOKIE, "") | |
| from server.auth import validate_session | |
| if not validate_session(token): | |
| await websocket.close(code=4001, reason="Unauthorized") | |
| return | |
| await websocket.accept() | |
| gen_queue.clients[client_id] = websocket | |
| tagger_queue.clients[client_id] = websocket | |
| try: | |
| while True: | |
| data = await websocket.receive_json() | |
| action = data.get("action") | |
| if action == "generate": | |
| params = data.get("params", {}) | |
| params["vibe"] = data.get("vibe", {}) | |
| params["extra_input"] = data.get("extra_input") | |
| params["character"] = data.get("character", {}) | |
| params["reference"] = data.get("reference") | |
| params["advanced"] = data.get("advanced", {}) | |
| task_id, queue_size = await gen_queue.add_task( | |
| client_id, "generate", params | |
| ) | |
| await websocket.send_json({ | |
| "type": "queued", "task_id": task_id, | |
| "position": queue_size, | |
| "message": f"已加入队列,当前位置: {queue_size}", | |
| }) | |
| elif action == "vibe_encode": | |
| encode_params = { | |
| "model": data.get("model", ""), | |
| "images": data.get("images", []), | |
| } | |
| task_id, queue_size = await gen_queue.add_task( | |
| client_id, "vibe_encode", encode_params | |
| ) | |
| await websocket.send_json({ | |
| "type": "queued", "task_id": task_id, | |
| "position": queue_size, | |
| "message": f"Vibe编码已加入队列,当前位置: {queue_size}", | |
| }) | |
| elif action == "tagger": | |
| tagger_params = data.get("params", {}) | |
| task_id, queue_size = await tagger_queue.add_task( | |
| client_id, "tagger", tagger_params | |
| ) | |
| await websocket.send_json({ | |
| "type": "queued", "task_id": task_id, | |
| "position": queue_size, | |
| "message": f"Tagger推理已加入队列,当前位置: {queue_size}", | |
| }) | |
| elif action == "augment": | |
| aug_params = data.get("params", {}) | |
| task_id, queue_size = await gen_queue.add_task( | |
| client_id, "augment", aug_params | |
| ) | |
| await websocket.send_json({ | |
| "type": "queued", "task_id": task_id, | |
| "position": queue_size, | |
| "message": f"定向修图已加入队列,当前位置: {queue_size}", | |
| }) | |
| elif action == "get_last_seed": | |
| seed = gen_queue.get_last_seed(client_id) | |
| await websocket.send_json({"type": "last_seed", "seed": seed}) | |
| except WebSocketDisconnect: | |
| gen_queue.clients.pop(client_id, None) | |
| tagger_queue.clients.pop(client_id, None) | |
| if __name__ == "__main__": | |
| import uvicorn | |
| uvicorn.run(app, host="0.0.0.0", port=7860) |