环境准备
- 操作系统:Ubuntu 24.04
- 显卡:RTX 3090 (24G) 或同等性能 NVIDIA GPU
- 基础软件:已安装 Docker 并配置
nvidia-container-toolkit
模型获取
从官方仓库下载 Tongyi-MAI/Z-Image-Turbo 模型权重文件,并解压至服务器指定目录(例如 /data/models/Z-Image-Turbo)。
构建服务代码
1. 后端服务 (zimage_server.py)
新建 Python 文件,代码如下。可根据实际情况修改端口和分辨率。
import os
import torch
import base64
import logging
import uvicorn
from io import BytesIO
from contextlib import asynccontextmanager
from fastapi import FastAPI, Form, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from diffusers import DiffusionPipeline
from fastapi.responses import FileResponse
# --- 配置 ---
MODEL_PATH = "/data/models/Z-Image-Turbo"
HOST = "0.0.0.0"
PORT = 8000
DEVICE = "cuda"
# 日志配置
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s')
logger = logging.getLogger(__name__)
ml_models = {}
@asynccontextmanager
async def lifespan(app: FastAPI):
logger.info(f"正在加载模型:{MODEL_PATH} ...")
try:
# --- 官方代码逻辑移植 1: 精度设置 ---
# 官方建议使用 bfloat16 以获得最佳性能并防止黑图
# 如果你的显卡比较旧(不支持 bfloat16),请改回 torch.float16
dtype = torch.bfloat16 if torch.cuda.is_bf16_supported() else torch.float16
pipe = DiffusionPipeline.from_pretrained(
MODEL_PATH,
torch_dtype=dtype,
use_safetensors=True
)
# 移动到 GPU
pipe.to(DEVICE)
# --- 官方代码逻辑移植 2: 内存优化 ---
# 如果显存不够,取消下面这行的注释
# pipe.enable_model_cpu_offload()
ml_models["pipe"] = pipe
logger.info(f">>> 模型加载成功 (Precision: {dtype})")
except Exception as e:
logger.error(f"模型加载失败:{e}")
raise e
yield
ml_models.clear()
if torch.cuda.is_available():
torch.cuda.empty_cache()
app = FastAPI(lifespan=lifespan)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"]
)
@app.get("/")
async def index():
return FileResponse("/workspace/index.html")
@app.post("/api/generate")
async def generate(prompt: str = Form(...)):
if "pipe" not in ml_models:
raise HTTPException(status_code=503, detail="Model not loaded")
logger.info(f"收到任务:{prompt[:50]}...")
try:
pipe = ml_models["pipe"]
# --- 官方代码逻辑移植 3: 关键推理参数 ---
image = pipe(
prompt=prompt,
height=1024, # 官方默认分辨率
width=1024,
num_inference_steps=9, # 官方设置:Turbo 模型只需要很少的步数
guidance_scale=0.0, # 【核心修复】Turbo 模型必须设为 0,否则会黑图或乱码
output_type="pil"
).images[0]
buffer = BytesIO()
image.save(buffer, format="PNG")
base64_img = base64.b64encode(buffer.getvalue()).decode()
return {"image": base64_img}
except Exception as e:
logger.error(f"生成出错:{e}")
raise HTTPException(status_code=500, detail=str(e))
if __name__ == "__main__":
uvicorn.run(app, host=HOST, port=PORT)
2. 前端页面 (index.html)
新建 HTML 文件,注意将 API 请求地址中的 IP 替换为实际服务器 IP。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Z-Image-Turbo WebUI</title>
<style>
:root {
--primary-color: #4a90e2;
--primary-hover: #357abd;
--bg-color: #f5f7fa;
--card-bg: #ffffff;
--text-color: #333333;
--border-radius: 12px;
--shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background-color: var(--bg-color);
color: var(--text-color);
margin: 0;
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
.container {
background-color: var(--card-bg);
padding: 40px;
border-radius: var(--border-radius);
box-shadow: var(--shadow);
width: 100%;
max-width: 550px;
text-align: center;
transition: transform 0.3s ease;
}
h2 {
margin-top: 0;
margin-bottom: 30px;
color: #2c3e50;
font-weight: 700;
letter-spacing: -0.5px;
}
.input-group {
margin-bottom: 25px;
text-align: left;
}
label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #4a5568;
font-size: 14px;
}
textarea {
width: 100%;
padding: 14px 16px;
font-size: 16px;
border: 2px solid #e2e8f0;
border-radius: 8px;
box-sizing: border-box;
transition: all 0.3s ease;
outline: none;
background-color: #f8fafc;
font-family: inherit;
resize: none;
min-height: 52px;
overflow-y: hidden;
line-height: 1.5;
}
textarea:focus {
border-color: var(--primary-color);
background-color: #fff;
box-shadow: 0 0 0 3px rgba(74, 144, 226, 0.1);
}
button {
background-color: var(--primary-color);
color: white;
border: none;
padding: 14px 30px;
font-size: 16px;
font-weight: 600;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
width: 100%;
box-shadow: 0 4px 6px rgba(74, 144, 226, 0.2);
}
button:hover {
background-color: var(--primary-hover);
transform: translateY(-1px);
box-shadow: 0 6px 8px rgba(74, 144, 226, 0.3);
}
button:active {
transform: translateY(0);
box-shadow: 0 2px 4px rgba(74, 144, 226, 0.2);
}
button:disabled {
background-color: #cbd5e0;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.download-btn {
background-color: #48bb78;
margin-top: 15px;
display: none; /* Hidden by default */
box-shadow: 0 4px 6px rgba(72, 187, 120, 0.2);
}
.download-btn:hover {
background-color: #38a169;
box-shadow: 0 6px 8px rgba(72, 187, 120, 0.3);
}
.download-btn:active {
box-shadow: 0 2px 4px rgba(72, 187, 120, 0.2);
}
#result {
margin-top: 30px;
min-height: 60px;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
img {
max-width: 100%;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
animation: fadeIn 0.6s cubic-bezier(0.22, 1, 0.36, 1);
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px) scale(0.98); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
.loading {
display: inline-block;
width: 30px;
height: 30px;
border: 3px solid #f3f3f3;
border-top: 3px solid var(--primary-color);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.error-message {
color: #e53e3e;
background: #fff5f5;
padding: 12px 16px;
border-radius: 8px;
font-size: 14px;
border: 1px solid #fed7d7;
width: 100%;
box-sizing: border-box;
}
.status-text {
color: #718096;
font-size: 14px;
margin-top: 10px;
}
</style>
</head>
<body>
<div class="container">
<h2>Z-Image-Turbo WebUI</h2>
<div class="input-group">
<label for="prompt">Prompt 提示词</label>
<textarea id="prompt" placeholder="输入提示词,例如:a cat in the snow" rows="1"></textarea>
</div>
<button id="generateBtn" onclick="generate()">生成图片</button>
<button id="downloadBtn" class="download-btn" onclick="downloadImage()">下载图片</button>
<div id="result"></div>
</div>
<script>
// Auto-resize textarea
const promptInput = document.getElementById("prompt");
promptInput.addEventListener("input", function(){
this.style.height = "auto";
this.style.height = this.scrollHeight + "px";
});
async function generate(){
const resultDiv = document.getElementById("result");
const btn = document.getElementById("generateBtn");
const downloadBtn = document.getElementById("downloadBtn");
const prompt = promptInput.value.trim();
if(!prompt){
// Shake animation for empty input
promptInput.style.borderColor = "#e53e3e";
promptInput.focus();
setTimeout(()=>{ promptInput.style.borderColor = ""; }, 2000);
return;
}
// UI Loading State
btn.disabled = true;
downloadBtn.style.display = "none"; // Hide download button during generation
btn.innerHTML = '<span></span> 生成中...';
resultDiv.innerHTML = ""; // Clear previous result
const formData = new FormData();
formData.append("prompt", prompt);
try{
let resp = await fetch("http://IP:8000/api/generate", {
method: "POST",
body: formData,
});
if(!resp.ok){
throw new Error(`HTTP error! status: ${resp.status}`);
}
let data = await resp.json();
if(data.image){
resultDiv.innerHTML = `<img src="data:image/png;base64,${data.image}" alt="Generated Image" />`;
downloadBtn.style.display = "block"; // Show download button
}else{
throw new Error("No image data received");
}
}catch(error){
console.error(error);
resultDiv.innerHTML = `<div>生成失败:${error.message}</div>`;
}finally{
btn.disabled = false;
btn.textContent = "生成图片";
}
}
function downloadImage(){
const img = document.querySelector("#result img");
if(img){
const a = document.createElement("a");
a.href = img.src;
a.download = `z-image-${Date.now()}.png`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
}
// Allow Enter key to submit (Shift+Enter for new line)
promptInput.addEventListener("keydown", function(event){
if(event.key === "Enter" && !event.shiftKey){
event.preventDefault();
generate();
}
});
</script>
</body>
</html>
镜像构建与运行
1. 构建 Dockerfile
将上述两个文件放在与 Dockerfile 同一目录下。
Dockerfile 内容:
# 1. 基础镜像:改用 CUDA 12.4 (目前 PyTorch 支持最好的高版本)
# 即使物理机驱动是 CUDA 13,容器内用 CUDA 12.4 也是完全兼容的
FROM nvidia/cuda:13.0.1-runtime-ubuntu22.04
# 2. 环境变量设置
ENV DEBIAN_FRONTEND=noninteractive \
LANG=C.UTF-8 \
LC_ALL=C.UTF-8 \
PYTHONUNBUFFERED=1 \
# 显存优化参数
PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True \
# 设置默认模型路径,方便 Python 代码读取
MODEL_PATH=/data/models/Z-Image-Turbo
# 3. 安装系统依赖
# 增加了 git (pip 安装 git+... 需要) 和 curl
RUN apt-get update && apt-get install -y --no-install-recommends \
python3 python3-pip python3-venv \
git build-essential wget curl ca-certificates \
libgl1 libglib2.0-0 libsm6 libxrender1 libxext6 \
&& rm -rf /var/lib/apt/lists/*
# 4. 升级 pip
RUN python3 -m pip install --upgrade pip
WORKDIR /workspace
# 5. 安装 PyTorch (CUDA 13 版本)
# 这里使用了 cu130 的源,确保下载到支持 GPU 的版本
RUN pip install --no-cache-dir torch torchvision --index-url https://download.pytorch.org/whl/cu130
# 6. 安装 Diffusers 和相关 AI 依赖
# 补充了 sentencepiece 和 protobuf (Transformers 必须)
RUN pip install --no-cache-dir git+https://github.com/huggingface/diffusers.git
RUN pip install --no-cache-dir transformers accelerate safetensors modelscope sentencepiece protobuf
# 7. 安装 Web 服务依赖
RUN pip install --no-cache-dir fastapi "uvicorn[standard]" pillow aiofiles python-multipart
# 8. 复制代码文件
COPY zimage_server.py /workspace/zimage_server.py
# 如果你有 index.html 也复制进去,没有则注释掉
COPY index.html /workspace/index.html
# 9. 暴露端口
EXPOSE 8000
# 10. 启动命令
# 确保 host 是 0.0.0.0 才能被外部访问
CMD ["uvicorn", "zimage_server:app", "--host", "0.0.0.0", "--port", "8000"]
2. 构建镜像
docker build -t zimage-server:v1 .
3. 运行容器
使用以下命令运行。请根据实际路径修改挂载点。
docker run --gpus all -d \
-p 8000:8000 \
-v /data/models/Z-Image-Turbo:/data/models/Z-Image-Turbo \
-v ./index.html:/workspace/index.html:ro \
-v ./zimage_server.py:/workspace/zimage_server.py:ro \
--name zimage-container \
zimage-server:v1
4. 验证运行状态
查看日志确认模型加载成功:
docker logs -f zimage-container
使用 Web 界面生图
在浏览器访问 http://服务器 IP:8000,输入提示词即可生成图片。
示例提示词: 一张融合'写实质感 + 二次元画风 + 高级 CG 渲染'的《鬼灭之刃》主题海报。主体是一位站在画面中央的灶门炭治郎,保持原有动漫面部特征与配色,但皮肤、眼睛、发丝与纹理采用写实级 CG 材质表现。角色手持日轮刀,刀刃释放淡蓝色'水之呼吸'CG 能量特效,能量呈流体状发光纹路,带有轻微粒子散射效果。他脚下是一块从雾气弥漫的浅水水面中浮出的深色湿润板岩,倒影清晰、略带二次元柔光边缘。光照与氛围(写实光影 + 动漫色调):金黄清晨体积光穿透森林树叶倾泻而下,形成真实物理感十足的体积雾与丁达尔效应,同时保留二次元式干净、高对比的光影边缘。斑驳的光斑落在炭治郎的脸部、刀刃与衣褶上,使写实材质与动漫色块互相融合。刀刃能量在水面上形成微光焦散 Caustics,与周围冷色雾气形成强烈视觉对比,制造史诗感。细节与材质(写实纹理 × 二次元线条):写实但带柔化处理的角色皮肤、精致的眼部渲染、半写实发丝质感。额头伤痕、耳饰与日轮刀纹路清晰,既保留原动画设计,又加入微微的金属反射与磨损细节。服装布料带有真实褶皱纹理,但边缘保留柔和的二次元描线。板岩湿润且带有微苔纹理,水面拥有真实反射与轻微折射,同时保持略微卡通化的干净色彩。背景灰烬微粒与落花片呈 CG 粒子效果,略带景深虚化。技术规格(CG 渲染 + 动漫视觉):采用虚幻引擎 5 超写实 CG 渲染风格混合高饱和、干净色块的二次元调色。8K 画质,面部与刀刃 Logo 区域超高清对焦。电影级光线追踪、漫反射 + 高光双层材质处理。动漫式色彩分级(高对比、清晰色层)与写实 CG 高动态范围光照融合。整体呈现电影海报级质感、CG 角色精度和动漫角色辨识度。

