Files
auto_agent/llm_server/image_gen_server.py

251 lines
8.6 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
Z-Image-Turbo 图像生成服务 —— FastAPI 封装。
使用 Tongyi-MAI/Z-Image-Turbo 模型(基于 Diffusers),默认 1024×1024 输出。
启动方式:
python image_gen_server.py --port 8904
"""
import os
import sys
import json
import base64
import time
import argparse
import warnings
import threading
from contextlib import asynccontextmanager
from pathlib import Path
from io import BytesIO
import torch
# ======================== 配置区域 ========================
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
DEFAULT_PORT = 8904
DEFAULT_MODEL_ID = "Tongyi-MAI/Z-Image-Turbo"
DEFAULT_MODEL_NAME = "Z-Image-Turbo" # 默认服务模型名(校验请求 body.model 用)
DEFAULT_API_KEY = "" # 默认空:不校验 API Key(任意值放行)
# 本地模型路径:优先读环境变量 LOCAL_MODEL_DIR,未设置则回退到远程 ID
LOCAL_MODEL_DIR = os.environ.get("LOCAL_MODEL_DIR", "")
MODEL_PATH = LOCAL_MODEL_DIR if LOCAL_MODEL_DIR and os.path.isdir(LOCAL_MODEL_DIR) else DEFAULT_MODEL_ID
# GPU 选择:CUDA_VISIBLE_DEVICES 或环境变量 SPECIFIC_GPU
SPECIFIC_GPU = os.environ.get("SPECIFIC_GPU", "")
if SPECIFIC_GPU:
os.environ["CUDA_VISIBLE_DEVICES"] = SPECIFIC_GPU
print(f"[Z-Image] 已设置 CUDA_VISIBLE_DEVICES={SPECIFIC_GPU}")
# 兼容性:将前导 warnings 移到 torch 导入后
warnings.filterwarnings("ignore", category=FutureWarning)
# ======================== FastAPI ========================
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import JSONResponse, Response
# 服务参数(--api-key / --model-name):
# api_key 为空:不校验(任意值放行);model_name 为空:不校验请求 model 字段
_model_args: dict = {"api_key": "", "model_name": ""}
def _check_api_key(request: Request):
"""校验请求 API Key:服务未配置 key(空)时放行任意值;否则校验 Authorization / X-API-Key。
校验失败抛出 HTTP 401。
"""
api_key = _model_args.get("api_key", "")
if not api_key:
return
header = request.headers.get("Authorization", "")
if header.startswith("Bearer "):
provided = header[len("Bearer "):].strip()
else:
provided = request.headers.get("X-API-Key", "").strip()
if provided != api_key:
raise HTTPException(status_code=401, detail="API Key 无效或缺失")
def _check_model_name(body: dict):
"""校验请求 modelname:服务未配置模型名(空)时放行任意值;否则要求 body.model 匹配。
校验失败抛出 HTTP 400。
"""
model_name = _model_args.get("model_name", "")
if not model_name:
return
if str(body.get("model") or "").strip() != model_name:
raise HTTPException(status_code=400, detail=f"模型名不匹配: 期望 '{model_name}',收到 '{body.get('model') or ''}'")
# Lazy-loaded pipeline singleton
_pipe = None
_pipe_lock = threading.Lock()
_device: str | None = None
def _get_device() -> str:
if torch.cuda.is_available():
return "cuda"
return "cpu"
def _get_pipe():
"""延迟加载 Z-Image pipeline(线程安全)。
使用 Diffusers DiffusionPipeline + trust_remote_code 加载模型仓库中的自定义 ZImagePipeline。
"""
global _pipe, _device
if _pipe is not None:
return _pipe, _device
with _pipe_lock:
if _pipe is not None:
return _pipe, _device
from diffusers import DiffusionPipeline
_device = _get_device()
load_kwargs = {
"torch_dtype": torch.bfloat16 if _device == "cuda" else torch.float32,
"low_cpu_mem_usage": False,
"trust_remote_code": True,
}
# 本地路径时跳过在线检查
if LOCAL_MODEL_DIR and os.path.isdir(LOCAL_MODEL_DIR):
load_kwargs["local_files_only"] = True
print(f"[Z-Image] 加载模型: {MODEL_PATH},设备: {_device}")
_pipe = DiffusionPipeline.from_pretrained(MODEL_PATH, **load_kwargs)
_pipe.to(_device)
print(f"[Z-Image] 模型加载成功")
return _pipe, _device
@asynccontextmanager
async def lifespan(app: FastAPI):
print(f"[Z-Image] 服务启动中,端口: {_model_args.get('port', DEFAULT_PORT)}")
# 启动时预加载模型
try:
_get_pipe()
except Exception as e:
print(f"[Z-Image] 模型预加载失败(将在首次请求时重试): {e}")
yield
print("[Z-Image] 服务关闭")
app = FastAPI(title="Z-Image-Turbo Gen", lifespan=lifespan)
@app.post("/generate")
async def generate_image(request: Request):
"""生成图像。
输入 JSON:
prompt: str — 必填,正向提示词
negative_prompt: str — 可选,负向提示词
height: int — 可选,输出高度(默认 1024,须为 64 的倍数)
width: int — 可选,输出宽度(默认 1024,须为 64 的倍数)
num_inference_steps: int — 可选,推理步数(Turbo 模型建议 9,即 8 次 DiT forward)
guidance_scale: float — 可选,CFG(Turbo 模型建议 0.0)
seed: int — 可选,随机种子(-1 表示随机)
return_format: str — 可选,"base64"(默认)或 "url"
输出 JSON:
success: bool
image: str — base64 编码的 PNG 或文件路径
format: str — "png"
width: int
height: int
"""
try:
body = await request.json()
except Exception:
raise HTTPException(status_code=400, detail="请求体必须为 JSON")
# 鉴权与模型名校验
_check_api_key(request)
_check_model_name(body)
prompt = (body.get("prompt") or "").strip()
if not prompt:
raise HTTPException(status_code=400, detail="prompt 不能为空")
height = int(body.get("height", 1024))
width = int(body.get("width", 1024))
steps = int(body.get("num_inference_steps", 11))
guidance = float(body.get("guidance_scale", 0.0))
seed = int(body.get("seed", -1))
negative_prompt = (body.get("negative_prompt") or "").strip() or None
return_format = (body.get("return_format") or "base64").strip().lower()
# 参数校验
if height % 64 != 0 or width % 64 != 0:
raise HTTPException(status_code=400, detail="height 和 width 须为 64 的整数倍")
if height < 256 or width < 256 or height > 2048 or width > 2048:
raise HTTPException(status_code=400, detail="尺寸须在 [256, 2048] 之间")
pipe, device = _get_pipe()
generator = None
if seed >= 0:
generator = torch.Generator(device).manual_seed(seed)
# 生成
print(f"[Z-Image] 生成中: {width}x{height}, steps={steps}, prompt={prompt[:80]}...")
t0 = time.time()
result = pipe(
prompt=prompt,
negative_prompt=negative_prompt,
height=height,
width=width,
num_inference_steps=steps,
guidance_scale=guidance,
generator=generator,
)
image = result.images[0]
elapsed = time.time() - t0
print(f"[Z-Image] 生成完成,耗时 {elapsed:.1f}s")
# 编码输出
buf = BytesIO()
image.save(buf, format="PNG")
buf.seek(0)
if return_format == "base64":
img_b64 = base64.b64encode(buf.read()).decode("ascii")
return JSONResponse({
"success": True,
"image": img_b64,
"format": "png",
"width": width,
"height": height,
"elapsed_seconds": round(elapsed, 2),
})
else:
return Response(content=buf.getvalue(), media_type="image/png")
@app.get("/health")
async def health():
return {"status": "ok"}
# ======================== 启动入口 ========================
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Z-Image-Turbo Image Generation Server")
parser.add_argument("--port", type=int, default=DEFAULT_PORT, help=f"服务端口(默认 {DEFAULT_PORT})")
parser.add_argument("--model-name", type=str, default=DEFAULT_MODEL_NAME,
help=f"服务模型名(校验请求 body.model;默认 {DEFAULT_MODEL_NAME},留空则不校验)")
parser.add_argument("--api-key", type=str, default=DEFAULT_API_KEY,
help="API Key(校验 Authorization: Bearer <key> 或 X-API-Key;默认空=任意值放行)")
args = parser.parse_args()
_model_args["port"] = args.port
_model_args["model_name"] = args.model_name
_model_args["api_key"] = args.api_key
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=args.port)