client / main.py
P01yH3dr0n's picture
tagcomplete update
87ad44c
Raw
History Blame Contribute Delete
14.8 kB
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()
@asynccontextmanager
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 ─────────────────────────────────
@app.get("/login", response_class=HTMLResponse)
async def login_page(request: Request):
if is_authenticated(request):
return RedirectResponse(url="/", status_code=302)
return HTMLResponse(login_page_html())
@app.post("/login")
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)
@app.get("/logout")
async def logout():
response = RedirectResponse(url="/login", status_code=302)
response.delete_cookie(SESSION_COOKIE)
return response
@app.get("/", response_class=HTMLResponse)
async def index(request: Request):
return templates.TemplateResponse("index.html", {"request": request})
@app.get("/api/cloud/dates")
async def cloud_dates():
try:
dates = cloud_storage.list_dates()
return {"dates": dates}
except Exception as e:
return {"error": str(e)}
@app.get("/api/cloud/images")
async def cloud_images(date: str = ""):
try:
images = cloud_storage.list_images(date)
return {"images": images}
except Exception as e:
return {"error": str(e)}
@app.get("/api/cloud/image")
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)}
@app.post("/api/cloud/delete")
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)}
@app.post("/api/cloud/sync")
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)}
@app.get("/api/cloud/search")
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)}
@app.post("/api/cloud/rebuild-index")
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)}
@app.get("/api/anlas")
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)}"}
@app.websocket("/ws/{client_id}")
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)