auto-save 2026-05-14 07:23 (~4)
This commit is contained in:
92
api/main.py
92
api/main.py
@@ -156,6 +156,7 @@ class StoryboardScene(BaseModel):
|
||||
first_image: dict | None = None
|
||||
last_image: dict | None = None
|
||||
product_images: list[dict] = Field(default_factory=list)
|
||||
product_fusion_shots: list[dict] = Field(default_factory=list)
|
||||
# 4 图槽:dict 含 {kind, frame_idx, element_id?, cutout_id?, label}
|
||||
subject_image: dict | None = None
|
||||
scene_image: dict | None = None
|
||||
@@ -236,6 +237,26 @@ class ProductLibraryItem(BaseModel):
|
||||
tags: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class ProductFusionRegion(BaseModel):
|
||||
x: float = 0
|
||||
y: float = 0
|
||||
w: float = 0
|
||||
h: float = 0
|
||||
|
||||
|
||||
class ProductFusionShot(BaseModel):
|
||||
id: str = ""
|
||||
product_image: dict | None = None
|
||||
person_image: dict | None = None
|
||||
product_region: ProductFusionRegion | None = None
|
||||
scene_image: dict | None = None
|
||||
action_text: str = ""
|
||||
duration: float = 5
|
||||
image_model: str = "gpt-image-2"
|
||||
video_model: str = "seedance"
|
||||
guide_image: dict | None = None
|
||||
|
||||
|
||||
class KeyElement(BaseModel):
|
||||
"""关键帧里识别 / 用户提取的元素 · 多次提取累积多张图,让用户挑选满意的"""
|
||||
id: str # uuid hex 8
|
||||
@@ -2488,6 +2509,10 @@ def delete_cutout(job_id: str, idx: int, element_id: str, cutout_id: str) -> Job
|
||||
|
||||
class UpdateStoryboardReq(BaseModel):
|
||||
duration: float = 0
|
||||
first_image: dict | None = None
|
||||
last_image: dict | None = None
|
||||
product_images: list[dict] = Field(default_factory=list)
|
||||
product_fusion_shots: list[dict] = Field(default_factory=list)
|
||||
subject_image: dict | None = None
|
||||
scene_image: dict | None = None
|
||||
product_image: dict | None = None
|
||||
@@ -2909,6 +2934,69 @@ def copy_product_library_asset(job_id: str, req: CopyProductLibraryAssetReq) ->
|
||||
}
|
||||
|
||||
|
||||
def product_image_alpha(img: Image.Image) -> Image.Image:
|
||||
rgba = img.convert("RGBA")
|
||||
rgb = rgba.convert("RGB")
|
||||
diff = ImageChops.difference(rgb, Image.new("RGB", rgb.size, (255, 255, 255)))
|
||||
mask = diff.convert("L").point(lambda p: 0 if p < 18 else min(255, int(p * 2.4)))
|
||||
mask = mask.filter(ImageFilter.GaussianBlur(0.7))
|
||||
rgba.putalpha(mask)
|
||||
return rgba
|
||||
|
||||
|
||||
@app.post("/jobs/{job_id}/product-fusion/guide")
|
||||
def create_product_fusion_guide(job_id: str, req: ProductFusionShot) -> dict:
|
||||
if job_id not in JOBS:
|
||||
raise HTTPException(404, "job not found")
|
||||
person_path = storyboard_ref_path(job_id, req.person_image)
|
||||
product_path = storyboard_ref_path(job_id, req.product_image)
|
||||
if not person_path or not person_path.exists():
|
||||
raise HTTPException(400, "person image required")
|
||||
if not product_path or not product_path.exists():
|
||||
raise HTTPException(400, "product image required")
|
||||
if not req.product_region or req.product_region.w <= 0 or req.product_region.h <= 0:
|
||||
raise HTTPException(400, "product region required")
|
||||
|
||||
region = req.product_region
|
||||
x = max(0.0, min(1.0, float(region.x)))
|
||||
y = max(0.0, min(1.0, float(region.y)))
|
||||
w = max(0.02, min(1.0 - x, float(region.w)))
|
||||
h = max(0.02, min(1.0 - y, float(region.h)))
|
||||
|
||||
try:
|
||||
base = Image.open(person_path).convert("RGB")
|
||||
base.thumbnail((1600, 1600), Image.Resampling.LANCZOS)
|
||||
product = product_image_alpha(Image.open(product_path))
|
||||
bw, bh = base.size
|
||||
box = (
|
||||
int(round(x * bw)),
|
||||
int(round(y * bh)),
|
||||
max(1, int(round(w * bw))),
|
||||
max(1, int(round(h * bh))),
|
||||
)
|
||||
product.thumbnail((box[2], box[3]), Image.Resampling.LANCZOS)
|
||||
px = box[0] + max(0, (box[2] - product.width) // 2)
|
||||
py = box[1] + max(0, (box[3] - product.height) // 2)
|
||||
guide = base.convert("RGBA")
|
||||
guide.alpha_composite(product, (px, py))
|
||||
out = guide.convert("RGB")
|
||||
asset_id = uuid.uuid4().hex[:12]
|
||||
out_dir = job_dir(job_id) / "assets"
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
out_path = out_dir / f"{asset_id}.jpg"
|
||||
out.save(out_path, "JPEG", quality=94)
|
||||
except Exception as e:
|
||||
raise HTTPException(400, f"product fusion guide failed: {e}")
|
||||
|
||||
return {
|
||||
"kind": "asset",
|
||||
"frame_idx": -1,
|
||||
"element_id": asset_id,
|
||||
"cutout_id": asset_id,
|
||||
"label": f"产品融合引导图 · {req.image_model or 'gpt-image-2'}",
|
||||
}
|
||||
|
||||
|
||||
@app.get("/jobs/{job_id}/assets/{asset_id}.jpg")
|
||||
def get_storyboard_asset(job_id: str, asset_id: str):
|
||||
p = job_dir(job_id) / "assets" / f"{asset_id}.jpg"
|
||||
@@ -2953,6 +3041,10 @@ def update_storyboard(job_id: str, idx: int, req: UpdateStoryboardReq) -> Job:
|
||||
if f.index == idx:
|
||||
f.storyboard = StoryboardScene(
|
||||
duration=max(0.0, float(req.duration)),
|
||||
first_image=req.first_image,
|
||||
last_image=req.last_image,
|
||||
product_images=list(req.product_images),
|
||||
product_fusion_shots=list(req.product_fusion_shots),
|
||||
subject_image=req.subject_image,
|
||||
scene_image=req.scene_image,
|
||||
product_image=req.product_image,
|
||||
|
||||
Reference in New Issue
Block a user