This commit is contained in:
2026-05-14 13:30:06 +08:00
parent 974946cee4
commit e43171521d
91 changed files with 10485 additions and 3254 deletions
+1
View File
@@ -0,0 +1 @@
"""HFUT Model Server 应用包(FastAPI 路由与服务)。"""
+8
View File
@@ -0,0 +1,8 @@
from __future__ import annotations
from pathlib import Path
# python_server/ 根目录(app/ 的上一级)
APP_ROOT = Path(__file__).resolve().parent.parent
OUTPUT_DIR = APP_ROOT / "outputs"
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
+13
View File
@@ -0,0 +1,13 @@
from __future__ import annotations
from fastapi import FastAPI
from .routes import animate, depth, inpaint, meta, segment
app = FastAPI(title="HFUT Model Server", version="0.1.0")
app.include_router(meta.router)
app.include_router(depth.router)
app.include_router(segment.router)
app.include_router(inpaint.router)
app.include_router(animate.router)
+1
View File
@@ -0,0 +1 @@
"""HTTP 路由(按领域拆分)。"""
+135
View File
@@ -0,0 +1,135 @@
from __future__ import annotations
import datetime
import shutil
from typing import Any, Dict
from fastapi import APIRouter
from PIL import Image
from ..deps import OUTPUT_DIR
from ..schemas import AnimateRequest, CharacterAnimateRequest
from ..services.animation_frames import (
align_size_to_input,
composite_rgba_on_background,
list_png_frames,
parse_background_color,
remove_background_by_color,
)
from ..services.image_io import b64_to_pil_image, pil_image_to_png_b64
from ..services.predictors import get_animation_predictor
router = APIRouter(tags=["animation"])
@router.post("/animate")
def animate(req: AnimateRequest) -> Dict[str, Any]:
try:
model_name = req.model_name or "animatediff"
predictor = get_animation_predictor(model_name)
result_path = predictor(
prompt=req.prompt,
negative_prompt=req.negative_prompt or "",
num_inference_steps=req.num_inference_steps,
guidance_scale=req.guidance_scale,
width=req.width,
height=req.height,
video_length=req.video_length,
seed=req.seed,
)
out_dir = OUTPUT_DIR / "animation"
out_dir.mkdir(parents=True, exist_ok=True)
ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
out_path = out_dir / f"{model_name}_{ts}.gif"
shutil.copy2(result_path, out_path)
return {"success": True, "output_path": str(out_path)}
except Exception as e:
return {"success": False, "error": str(e)}
@router.post("/animate/character_sequence")
def animate_character_sequence(req: CharacterAnimateRequest) -> Dict[str, Any]:
"""
输入透明背景角色 PNG,生成 30fps 角色动画 PNG 序列。
流程:
- 将角色合成到指定纯色背景,作为 AnimateDiff 控制图
- 以与输入相近、且对齐到 8 的分辨率生成 PNG 序列
- 对每帧按背景色做色键剔除,返回透明背景 PNG 序列给前端
"""
try:
model_name = req.model_name or "animatediff"
background_rgb = parse_background_color(req.background_color)
character = b64_to_pil_image(req.image_b64).convert("RGBA")
orig_w, orig_h = character.size
run_w, run_h = align_size_to_input(orig_w, orig_h, req.max_side)
predictor = get_animation_predictor(model_name)
out_dir = OUTPUT_DIR / "animation" / "character_sequence"
out_dir.mkdir(parents=True, exist_ok=True)
ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
seq_dir = out_dir / f"{model_name}_{ts}"
seq_dir.mkdir(parents=True, exist_ok=True)
control_path = seq_dir / "control_image.png"
control_image = composite_rgba_on_background(
character,
background_rgb,
size=(run_w, run_h),
)
control_image.save(control_path)
frame_dir = predictor(
prompt=req.prompt,
negative_prompt=req.negative_prompt or "",
num_inference_steps=req.num_inference_steps,
guidance_scale=req.guidance_scale,
width=run_w,
height=run_h,
video_length=req.video_length,
seed=req.seed,
control_image_path=str(control_path),
output_format="png_sequence",
)
frame_paths = list_png_frames(frame_dir)
if not frame_paths:
raise FileNotFoundError(f"未找到 AnimateDiff 输出 PNG 序列: {frame_dir}")
frames: list[dict[str, Any]] = []
processed_dir = seq_dir / "frames"
processed_dir.mkdir(parents=True, exist_ok=True)
for idx, frame_path in enumerate(frame_paths):
frame = remove_background_by_color(
Image.open(frame_path),
background_rgb,
tolerance=req.background_tolerance,
output_size=(orig_w, orig_h),
)
out_path = processed_dir / f"{idx:04d}.png"
frame.save(out_path)
frames.append(
{
"index": idx,
"image_b64": pil_image_to_png_b64(frame),
"path": str(out_path),
}
)
return {
"success": True,
"fps": 30,
"frame_count": len(frames),
"width": orig_w,
"height": orig_h,
"background_color": req.background_color,
"output_dir": str(processed_dir),
"frames": frames,
}
except Exception as e:
return {"success": False, "error": str(e), "frames": []}
+32
View File
@@ -0,0 +1,32 @@
from __future__ import annotations
import numpy as np
from fastapi import APIRouter, HTTPException
from fastapi.responses import Response
from ..schemas import DepthRequest
from ..services.depth_io import depth_to_png16_bytes
from ..services.image_io import b64_to_pil_image
from ..services.predictors import get_depth_predictor
router = APIRouter(tags=["depth"])
@router.post("/depth")
def depth(req: DepthRequest):
"""
计算深度并直接返回二进制 PNG(16-bit 灰度)。
约束:
- 前端不传/不选模型;模型选择写死在后端 config.py
- 成功:HTTP 200 + Content-Type: image/png
- 失败:HTTP 500detail 为错误信息
"""
try:
pil = b64_to_pil_image(req.image_b64).convert("RGB")
predictor = get_depth_predictor()
depth_arr = predictor(pil) # type: ignore[misc]
png_bytes = depth_to_png16_bytes(np.asarray(depth_arr))
return Response(content=png_bytes, media_type="image/png")
except Exception as e:
raise HTTPException(status_code=500, detail=str(e)) from e
+47
View File
@@ -0,0 +1,47 @@
from __future__ import annotations
from typing import Any, Dict
from fastapi import APIRouter
from ..deps import OUTPUT_DIR
from ..schemas import InpaintRequest
from ..services.image_io import b64_to_pil_image, default_half_mask, pil_image_to_png_b64
from ..services.predictors import get_inpaint_predictor
router = APIRouter(tags=["inpaint"])
@router.post("/inpaint")
def inpaint(req: InpaintRequest) -> Dict[str, Any]:
try:
model_name = req.model_name or "sdxl_inpaint"
pil = b64_to_pil_image(req.image_b64).convert("RGB")
if req.mask_b64:
mask = b64_to_pil_image(req.mask_b64).convert("L")
else:
mask = default_half_mask(pil)
predictor = get_inpaint_predictor(model_name)
out = predictor(
pil,
mask,
req.prompt or "",
req.negative_prompt or "",
strength=req.strength,
max_side=req.max_side,
)
out_dir = OUTPUT_DIR / "inpaint"
out_dir.mkdir(parents=True, exist_ok=True)
out_path = out_dir / f"{model_name}_inpaint.png"
out.save(out_path)
return {
"success": True,
"output_path": str(out_path),
"output_image_b64": pil_image_to_png_b64(out),
}
except Exception as e:
return {"success": False, "error": str(e)}
+93
View File
@@ -0,0 +1,93 @@
from __future__ import annotations
from typing import Any, Dict
from fastapi import APIRouter
router = APIRouter(tags=["meta"])
@router.get("/models")
def get_models() -> Dict[str, Any]:
"""
返回一个兼容 Qt 前端的 schema:
{
"models": {
"depth": { "key": { "name": "..."} ... },
"segment": { ... },
"inpaint": { "key": { "name": "...", "params": [...] } ... }
}
}
"""
return {
"models": {
"depth": {
"midas": {"name": "MiDaS (default)"},
"zoedepth_n": {"name": "ZoeDepth (ZoeD_N)"},
"zoedepth_k": {"name": "ZoeDepth (ZoeD_K)"},
"zoedepth_nk": {"name": "ZoeDepth (ZoeD_NK)"},
"depth_anything_v2_vits": {"name": "Depth Anything V2 (vits)"},
"depth_anything_v2_vitb": {"name": "Depth Anything V2 (vitb)"},
"depth_anything_v2_vitl": {"name": "Depth Anything V2 (vitl)"},
"depth_anything_v2_vitg": {"name": "Depth Anything V2 (vitg)"},
"dpt_large": {"name": "DPT (large)"},
"dpt_hybrid": {"name": "DPT (hybrid)"},
"midas_dpt_beit_large_512": {"name": "MiDaS (dpt_beit_large_512)"},
"midas_dpt_swin2_large_384": {"name": "MiDaS (dpt_swin2_large_384)"},
"midas_dpt_swin2_tiny_256": {"name": "MiDaS (dpt_swin2_tiny_256)"},
"midas_dpt_levit_224": {"name": "MiDaS (dpt_levit_224)"},
},
"segment": {
"sam": {"name": "SAM (vit_h)"},
"mask2former_debug": {"name": "SAM (compat mask2former_debug)"},
"mask2former": {"name": "Mask2Former (not implemented)"},
},
"inpaint": {
"copy": {"name": "Copy (no-op)", "params": []},
"sdxl_inpaint": {
"name": "SDXL Inpaint",
"params": [
{"id": "prompt", "label": "提示词", "optional": True},
],
},
"lama": {"name": "LaMa (Erase / Remove Object)", "params": []},
"controlnet": {
"name": "ControlNet Inpaint (canny)",
"params": [
{"id": "prompt", "label": "提示词", "optional": True},
],
},
},
"animation": {
"animatediff": {
"name": "AnimateDiff (Text-to-Video)",
"params": [
{"id": "prompt", "label": "提示词", "optional": False},
{"id": "negative_prompt", "label": "负向提示词", "optional": True},
{"id": "num_inference_steps", "label": "采样步数", "optional": True},
{"id": "guidance_scale", "label": "CFG Scale", "optional": True},
{"id": "width", "label": "宽度", "optional": True},
{"id": "height", "label": "高度", "optional": True},
{"id": "video_length", "label": "帧数", "optional": True},
{"id": "seed", "label": "随机种子", "optional": True},
],
},
"character_sequence": {
"name": "Character PNG Sequence (30 FPS)",
"endpoint": "/animate/character_sequence",
"params": [
{"id": "image_b64", "label": "透明背景角色 PNG", "optional": False},
{"id": "prompt", "label": "动画提示词", "optional": False},
{"id": "background_color", "label": "纯色背景", "optional": True},
{"id": "video_length", "label": "帧数", "optional": True},
{"id": "background_tolerance", "label": "背景剔除阈值", "optional": True},
],
},
},
}
}
@router.get("/health")
def health() -> Dict[str, str]:
return {"status": "ok"}
+84
View File
@@ -0,0 +1,84 @@
from __future__ import annotations
from typing import Any, Dict
import numpy as np
from fastapi import APIRouter
from PIL import Image
from ..deps import OUTPUT_DIR
from ..schemas import SamPromptSegmentRequest, SegmentRequest
from ..services.image_io import b64_to_pil_image
from ..services.predictors import get_seg_predictor
from model.Seg.seg_loader import expand_mask, mask_to_contour_xy, run_sam_prompt
router = APIRouter(tags=["segment"])
@router.post("/segment")
def segment(req: SegmentRequest) -> Dict[str, Any]:
try:
model_name = req.model_name or "sam"
pil = b64_to_pil_image(req.image_b64).convert("RGB")
rgb = np.array(pil, dtype=np.uint8)
predictor = get_seg_predictor(model_name)
label_map = predictor(rgb).astype(np.int32)
out_dir = OUTPUT_DIR / "segment"
out_dir.mkdir(parents=True, exist_ok=True)
out_path = out_dir / f"{model_name}_label.png"
Image.fromarray(np.clip(label_map, 0, 255).astype(np.uint8), mode="L").save(out_path)
return {"success": True, "label_path": str(out_path)}
except Exception as e:
return {"success": False, "error": str(e)}
@router.post("/segment/sam_prompt")
def segment_sam_prompt(req: SamPromptSegmentRequest) -> Dict[str, Any]:
"""
交互式 SAM:裁剪图 + 点/框提示,返回掩膜外轮廓点列(裁剪像素坐标)。
"""
try:
pil = b64_to_pil_image(req.image_b64).convert("RGB")
rgb = np.array(pil, dtype=np.uint8)
h, w = rgb.shape[0], rgb.shape[1]
if req.overlay_b64:
ov = b64_to_pil_image(req.overlay_b64)
if ov.size != (w, h):
return {
"success": False,
"error": f"overlay 尺寸 {ov.size} 与 image {w}x{h} 不一致",
"contour": [],
}
if len(req.point_coords) != len(req.point_labels):
return {
"success": False,
"error": "point_coords 与 point_labels 长度不一致",
"contour": [],
}
if len(req.point_coords) < 1:
return {"success": False, "error": "至少需要一个提示点", "contour": []}
pc = np.array(req.point_coords, dtype=np.float32)
if pc.ndim != 2 or pc.shape[1] != 2:
return {"success": False, "error": "point_coords 每项须为 [x,y]", "contour": []}
pl = np.array(req.point_labels, dtype=np.int64)
box = np.array(req.box_xyxy, dtype=np.float32)
mask = run_sam_prompt(rgb, pc, pl, box_xyxy=box)
if not np.any(mask):
return {"success": False, "error": "SAM 未产生有效掩膜", "contour": []}
mask = expand_mask(mask, int(req.expand_px))
contour = mask_to_contour_xy(mask, epsilon_px=2.0)
if len(contour) < 3:
return {"success": False, "error": "轮廓点数不足", "contour": []}
return {"success": True, "contour": contour, "error": None}
except Exception as e:
return {"success": False, "error": str(e), "contour": []}
+93
View File
@@ -0,0 +1,93 @@
from __future__ import annotations
from typing import Optional
from pydantic import BaseModel, Field
class ImageInput(BaseModel):
image_b64: str = Field(..., description="PNG/JPG 编码后的 base64(不含 data: 前缀)")
model_name: Optional[str] = Field(None, description="模型 key(来自 /models")
class DepthRequest(ImageInput):
pass
class SegmentRequest(ImageInput):
pass
class SamPromptSegmentRequest(BaseModel):
image_b64: str = Field(..., description="裁剪后的 RGB 图 base64PNG/JPG")
overlay_b64: Optional[str] = Field(
None,
description="与裁剪同尺寸的标记叠加 PNG base64(可选;当前用于校验尺寸一致)",
)
point_coords: list[list[float]] = Field(
...,
description="裁剪坐标系下的提示点 [[x,y], ...]",
)
point_labels: list[int] = Field(
...,
description="与 point_coords 等长:1=前景,0=背景",
)
box_xyxy: list[float] = Field(
...,
description="裁剪内笔画紧包围盒 [x1,y1,x2,y2](像素)",
min_length=4,
max_length=4,
)
expand_px: int = Field(
2,
ge=0,
le=20,
description="对输出 mask 做轻微膨胀的像素半径(0 表示不扩大;建议 1~3)",
)
class InpaintRequest(ImageInput):
prompt: Optional[str] = Field("", description="补全 prompt")
strength: float = Field(0.8, ge=0.0, le=1.0)
negative_prompt: Optional[str] = Field("", description="负向 prompt")
mask_b64: Optional[str] = Field(None, description="mask PNG base64(可选)")
max_side: int = Field(1024, ge=128, le=2048)
class AnimateRequest(BaseModel):
model_name: Optional[str] = Field(None, description="模型 key(来自 /models")
prompt: str = Field(..., description="文本提示词")
negative_prompt: Optional[str] = Field("", description="负向提示词")
num_inference_steps: int = Field(25, ge=1, le=200)
guidance_scale: float = Field(8.0, ge=0.0, le=30.0)
width: int = Field(512, ge=128, le=2048)
height: int = Field(512, ge=128, le=2048)
video_length: int = Field(16, ge=1, le=128)
seed: int = Field(-1, description="-1 表示随机种子")
class CharacterAnimateRequest(BaseModel):
image_b64: str = Field(..., description="透明背景角色 PNG 的 base64(不含 data: 前缀)")
model_name: Optional[str] = Field(None, description="模型 key(来自 /models")
prompt: str = Field(..., description="角色动画提示词")
negative_prompt: Optional[str] = Field("", description="负向提示词")
background_color: str = Field(
"#00FF00",
description="生成时使用的纯色背景,支持 #RRGGBB 或 R,G,B",
)
background_tolerance: int = Field(
40,
ge=0,
le=255,
description="自动剔除背景的 RGB 色差阈值,越大剔除越激进",
)
num_inference_steps: int = Field(25, ge=1, le=200)
guidance_scale: float = Field(8.0, ge=0.0, le=30.0)
video_length: int = Field(16, ge=1, le=128, description="前端请求生成的帧数")
seed: int = Field(-1, description="-1 表示随机种子")
max_side: int = Field(
768,
ge=128,
le=2048,
description="推理分辨率长边上限;会保持输入比例并对齐到 8 的倍数",
)
+1
View File
@@ -0,0 +1 @@
"""业务服务层(预测器缓存、图像编解码等)。"""
@@ -0,0 +1,115 @@
from __future__ import annotations
from collections import deque
from pathlib import Path
import numpy as np
from PIL import Image
def parse_background_color(value: str) -> tuple[int, int, int]:
text = value.strip()
if text.startswith("#"):
hex_text = text[1:]
if len(hex_text) != 6:
raise ValueError("background_color 使用 #RRGGBB 时必须是 6 位十六进制")
try:
return tuple(int(hex_text[i : i + 2], 16) for i in (0, 2, 4)) # type: ignore[return-value]
except ValueError as e:
raise ValueError(f"background_color 不是有效十六进制颜色: {value}") from e
parts = [p.strip() for p in text.split(",")]
if len(parts) != 3:
raise ValueError("background_color 必须为 #RRGGBB 或 R,G,B")
try:
rgb = tuple(int(p) for p in parts)
except ValueError as e:
raise ValueError(f"background_color 不是有效 RGB 颜色: {value}") from e
if any(c < 0 or c > 255 for c in rgb):
raise ValueError("background_color 的 RGB 分量必须在 0~255")
return rgb # type: ignore[return-value]
def align_size_to_input(width: int, height: int, max_side: int) -> tuple[int, int]:
run_w, run_h = width, height
if max(width, height) > max_side:
scale = max_side / float(max(width, height))
run_w = int(round(width * scale))
run_h = int(round(height * scale))
run_w = max(8, run_w - (run_w % 8))
run_h = max(8, run_h - (run_h % 8))
return run_w, run_h
def composite_rgba_on_background(
image: Image.Image,
background_rgb: tuple[int, int, int],
size: tuple[int, int],
) -> Image.Image:
rgba = image.convert("RGBA")
if rgba.size != size:
rgba = rgba.resize(size, resample=Image.BICUBIC)
bg = Image.new("RGB", size, background_rgb)
alpha = rgba.getchannel("A")
bg.paste(rgba.convert("RGB"), mask=alpha)
return bg
def remove_background_by_color(
image: Image.Image,
background_rgb: tuple[int, int, int],
tolerance: int,
output_size: tuple[int, int] | None = None,
) -> Image.Image:
rgba = image.convert("RGBA")
if output_size is not None and rgba.size != output_size:
rgba = rgba.resize(output_size, resample=Image.BICUBIC)
arr = np.asarray(rgba, dtype=np.uint8).copy()
rgb = arr[:, :, :3].astype(np.int16)
bg = np.array(background_rgb, dtype=np.int16).reshape(1, 1, 3)
diff = np.abs(rgb - bg)
bg_like = np.max(diff, axis=2) <= int(tolerance)
bg_mask = _edge_connected_mask(bg_like)
arr[bg_mask, 3] = 0
return Image.fromarray(arr, mode="RGBA")
def list_png_frames(frame_dir: Path) -> list[Path]:
frames = [p for p in frame_dir.iterdir() if p.is_file() and p.suffix.lower() == ".png"]
return sorted(frames, key=lambda p: p.name)
def _edge_connected_mask(mask: np.ndarray) -> np.ndarray:
h, w = mask.shape
visited = np.zeros((h, w), dtype=bool)
queue: deque[tuple[int, int]] = deque()
for x in range(w):
if mask[0, x]:
queue.append((0, x))
if mask[h - 1, x]:
queue.append((h - 1, x))
for y in range(h):
if mask[y, 0]:
queue.append((y, 0))
if mask[y, w - 1]:
queue.append((y, w - 1))
while queue:
y, x = queue.popleft()
if visited[y, x] or not mask[y, x]:
continue
visited[y, x] = True
if y > 0:
queue.append((y - 1, x))
if y + 1 < h:
queue.append((y + 1, x))
if x > 0:
queue.append((y, x - 1))
if x + 1 < w:
queue.append((y, x + 1))
return visited
+22
View File
@@ -0,0 +1,22 @@
from __future__ import annotations
import io
import numpy as np
from PIL import Image
def depth_to_png16_bytes(depth: np.ndarray) -> bytes:
depth = np.asarray(depth, dtype=np.float32)
dmin = float(depth.min())
dmax = float(depth.max())
if dmax > dmin:
norm = (depth - dmin) / (dmax - dmin)
else:
norm = np.zeros_like(depth, dtype=np.float32)
# 前后端约定:最远=255,最近=0(8-bit
u8 = ((1.0 - norm) * 255.0).clip(0, 255).astype(np.uint8)
img = Image.fromarray(u8, mode="L")
buf = io.BytesIO()
img.save(buf, format="PNG")
return buf.getvalue()
+25
View File
@@ -0,0 +1,25 @@
from __future__ import annotations
import base64
import io
from PIL import Image, ImageDraw
def b64_to_pil_image(b64: str) -> Image.Image:
raw = base64.b64decode(b64)
return Image.open(io.BytesIO(raw))
def pil_image_to_png_b64(img: Image.Image) -> str:
buf = io.BytesIO()
img.save(buf, format="PNG")
return base64.b64encode(buf.getvalue()).decode("ascii")
def default_half_mask(img: Image.Image) -> Image.Image:
w, h = img.size
mask = Image.new("L", (w, h), 0)
draw = ImageDraw.Draw(mask)
draw.rectangle([w // 2, 0, w, h], fill=255)
return mask
+108
View File
@@ -0,0 +1,108 @@
from __future__ import annotations
from typing import Any
from PIL import Image
from config_loader import load_app_config, get_depth_backend_from_app
from model.Animation.animation_loader import (
UnifiedAnimationConfig,
AnimationBackend,
build_animation_predictor,
)
from model.Depth.depth_loader import UnifiedDepthConfig, DepthBackend, build_depth_predictor
from model.Inpaint.inpaint_loader import UnifiedInpaintConfig, InpaintBackend, build_inpaint_predictor
from model.Seg.seg_loader import UnifiedSegConfig, SegBackend, build_seg_predictor
_depth_predictor = None
_depth_backend: DepthBackend | None = None
_seg_cache: dict[str, Any] = {}
_inpaint_cache: dict[str, Any] = {}
_animation_cache: dict[str, Any] = {}
def ensure_depth_predictor() -> None:
global _depth_predictor, _depth_backend
if _depth_predictor is not None and _depth_backend is not None:
return
app_cfg = load_app_config()
backend_str = get_depth_backend_from_app(app_cfg)
try:
backend = DepthBackend(backend_str)
except Exception as e:
raise ValueError(f"config.py 中 depth.backend 不合法: {backend_str}") from e
_depth_predictor, _depth_backend = build_depth_predictor(UnifiedDepthConfig(backend=backend))
def get_depth_predictor():
ensure_depth_predictor()
return _depth_predictor
def get_seg_predictor(model_name: str):
if model_name == "mask2former_debug":
model_name = "sam"
if model_name in _seg_cache:
return _seg_cache[model_name]
if model_name == "sam":
pred, _ = build_seg_predictor(UnifiedSegConfig(backend=SegBackend.SAM))
_seg_cache[model_name] = pred
return pred
if model_name == "mask2former":
pred, _ = build_seg_predictor(UnifiedSegConfig(backend=SegBackend.MASK2FORMER))
_seg_cache[model_name] = pred
return pred
raise ValueError(f"未知 segment model_name: {model_name}")
def get_inpaint_predictor(model_name: str):
if model_name in _inpaint_cache:
return _inpaint_cache[model_name]
if model_name == "copy":
def _copy(image: Image.Image, *_args, **_kwargs) -> Image.Image:
return image.convert("RGB")
_inpaint_cache[model_name] = _copy
return _copy
if model_name == "sdxl_inpaint":
pred, _ = build_inpaint_predictor(UnifiedInpaintConfig(backend=InpaintBackend.SDXL_INPAINT))
_inpaint_cache[model_name] = pred
return pred
if model_name == "controlnet":
pred, _ = build_inpaint_predictor(UnifiedInpaintConfig(backend=InpaintBackend.CONTROLNET))
_inpaint_cache[model_name] = pred
return pred
if model_name == "lama":
pred, _ = build_inpaint_predictor(UnifiedInpaintConfig(backend=InpaintBackend.LAMA))
_inpaint_cache[model_name] = pred
return pred
raise ValueError(f"未知 inpaint model_name: {model_name}")
def get_animation_predictor(model_name: str):
if model_name in _animation_cache:
return _animation_cache[model_name]
if model_name == "animatediff":
pred, _ = build_animation_predictor(
UnifiedAnimationConfig(backend=AnimationBackend.ANIMATEDIFF)
)
_animation_cache[model_name] = pred
return pred
raise ValueError(f"未知 animation model_name: {model_name}")