auto-save 2026-05-14 07:23 (~4)

This commit is contained in:
2026-05-14 07:23:13 +08:00
parent 76412d2395
commit a6773a8690
4 changed files with 456 additions and 25 deletions

View File

@@ -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,