Files
hfut-bishe/python_server/model/Inpaint/flux_fill/loader.py
T
2026-05-15 00:55:49 +08:00

179 lines
6.1 KiB
Python
Raw 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.
from __future__ import annotations
import os
from pathlib import Path
from typing import Any, Callable
import numpy as np
from PIL import Image
def _align_spatial_dim_for_flux_fill(n: int, multiple: int = 16) -> int:
"""FLUX Fill 要求 H/W 为 (vae_scale_factor*2) 的倍数,常见为 16。"""
n = int(max(n, multiple))
return n - (n % multiple)
def _flux_clip_prompt_and_t5_full(pipe: Any, text_full: str) -> tuple[str, str]:
"""
CLIP 仅约 77 token:用与管线一致的 tokenizer 截断得到 prompt(池化向量)。
完整文本走 prompt_2 → T5max_sequence_length 可达 512),长中文不再整段喂给 CLIP。
"""
text_full = (text_full or "").strip()
if not text_full:
d = "Chinese ink painting background, natural texture, seamless fill, no figures"
return d, d
tok = getattr(pipe, "tokenizer", None)
if tok is None:
return text_full[:120], text_full
ml = int(getattr(tok, "model_max_length", 77) or 77)
enc = tok([text_full], padding="max_length", max_length=ml, truncation=True, return_tensors="pt")
clip_s = tok.batch_decode(enc.input_ids, skip_special_tokens=True)[0].strip()
if not clip_s:
clip_s = text_full[:80]
return clip_s, text_full
def make_flux_fill_predictor(
model_id: str,
device: str | None = None,
) -> Callable[[Image.Image, Image.Image, str, str], Image.Image]:
import torch
from diffusers import FluxFillPipeline
if device is None:
device = "cuda" if torch.cuda.is_available() else "cpu"
if device == "cuda" and torch.cuda.is_bf16_supported():
torch_dtype = torch.bfloat16
elif device == "cuda":
torch_dtype = torch.float16
else:
torch_dtype = torch.float32
root = Path(model_id.strip()).expanduser()
# 本机 diffusers 目录(含 model_index.json)时不访问 Hub,避免自动下载。
local_files_only = root.is_dir() and (root / "model_index.json").is_file()
pipe = FluxFillPipeline.from_pretrained(
str(root),
torch_dtype=torch_dtype,
token=None,
local_files_only=local_files_only,
)
# 显存:默认顺序 CPU offload(各子模块用时再上 GPU,峰值远低于 pipe.to(cuda))。
# 整卡加载易占满 32GB 再在推理时 OOM。环境变量 FLUX_FILL_OFFLOAD=none 可改回整卡(需有余量)。
# 勿用 enable_model_cpu_offload(与 transformers 5.x CLIP 的 _hf_hook 问题相关)。
used_sequential = False
if device == "cuda":
offload = os.environ.get("FLUX_FILL_OFFLOAD", "sequential").strip().lower()
try:
pipe.enable_vae_tiling()
except Exception:
pass
try:
pipe.enable_vae_slicing()
except Exception:
pass
try:
pipe.enable_attention_slicing()
except Exception:
pass
if offload not in ("none", "gpu", "0", "false", "no"):
try:
pipe.enable_sequential_cpu_offload(gpu_id=0)
used_sequential = True
except Exception:
used_sequential = False
if not used_sequential:
pipe.to("cuda")
else:
pipe.to(device)
def _predict(
image: Image.Image,
mask: Image.Image,
prompt: str,
negative_prompt: str = "",
strength: float = 1.0,
guidance_scale: float = 30.0,
num_inference_steps: int = 28,
max_side: int = 1024,
max_sequence_length: int = 512,
**_: Any,
) -> Image.Image:
_ = negative_prompt
image = image.convert("RGB")
mask = mask.convert("L")
orig_w, orig_h = image.size
run_w, run_h = orig_w, orig_h
if max(orig_w, orig_h) > max_side:
scale = max_side / float(max(orig_w, orig_h))
run_w = int(round(orig_w * scale))
run_h = int(round(orig_h * scale))
run_w = _align_spatial_dim_for_flux_fill(run_w)
run_h = _align_spatial_dim_for_flux_fill(run_h)
# 推理分辨率硬顶(像素域),防止极大 crop + 高 max_side 时注意力/VAE 爆显存;输出仍会缩回 orig。
run_cap = int(os.environ.get("FLUX_FILL_RUN_MAX_SIDE", "4096"))
if run_cap >= 256 and max(run_w, run_h) > run_cap:
s = run_cap / float(max(run_w, run_h))
run_w = _align_spatial_dim_for_flux_fill(int(round(run_w * s)))
run_h = _align_spatial_dim_for_flux_fill(int(round(run_h * s)))
if (run_w, run_h) != (orig_w, orig_h):
image_run = image.resize((run_w, run_h), resample=Image.BICUBIC)
mask_run = mask.resize((run_w, run_h), resample=Image.NEAREST)
else:
image_run = image
mask_run = mask
p_full = (prompt or "").strip()
if not p_full:
p_full = "Chinese ink painting background, natural texture, seamless fill, no figures"
p_clip, p_t5 = _flux_clip_prompt_and_t5_full(pipe, p_full)
st = float(strength)
st = min(1.0, max(0.05, st))
if device == "cuda":
try:
import gc
gc.collect()
torch.cuda.empty_cache()
except Exception:
pass
try:
out = pipe(
prompt=p_clip,
prompt_2=p_t5,
image=image_run,
mask_image=mask_run,
height=run_h,
width=run_w,
strength=st,
guidance_scale=float(guidance_scale),
num_inference_steps=int(num_inference_steps),
max_sequence_length=int(max_sequence_length),
).images[0]
finally:
if device == "cuda":
try:
import gc
gc.collect()
torch.cuda.empty_cache()
except Exception:
pass
out = out.convert("RGB")
if out.size != (orig_w, orig_h):
out = out.resize((orig_w, orig_h), resample=Image.BICUBIC)
return out
return _predict