auto-save 2026-05-13 11:45 (~4)
This commit is contained in:
68
api/main.py
68
api/main.py
@@ -68,10 +68,12 @@ class KeyElement(BaseModel):
|
||||
id: str # uuid hex 8
|
||||
name_zh: str
|
||||
name_en: str = ""
|
||||
position: str = "" # 在画面中的位置描述(vision 给的)
|
||||
source: Literal["auto", "manual", "region"] = "manual" # auto=vision / manual=用户加 / region=画框
|
||||
region: dict | None = None # 用户画框的相对坐标 {x,y,w,h}(用于精准抠图)
|
||||
cutout_id: str | None = None # 已抠图 → /jobs/{id}/frames/{idx}/elements/{element_id}/cutout.png
|
||||
position: str = ""
|
||||
source: Literal["auto", "manual", "region"] = "manual"
|
||||
region: dict | None = None
|
||||
# 抠图(注:Gemini 输出 JPEG 不支持真透明,所以让模型在纯白 / 纯黑底上输出)
|
||||
cutout_id: str | None = None # 已抠图 → /jobs/{id}/frames/{idx}/elements/{element_id}/cutout.jpg
|
||||
cutout_background: Literal["white", "black"] = "white"
|
||||
created_at: float = 0.0
|
||||
|
||||
|
||||
@@ -1205,12 +1207,11 @@ def delete_element(job_id: str, idx: int, element_id: str) -> Job:
|
||||
removed = len(f.elements) < before
|
||||
# 若有抠图文件也删
|
||||
if removed:
|
||||
cutout = job_dir(job_id) / "elements" / f"{idx:03d}_{element_id}.png"
|
||||
if cutout.exists():
|
||||
try:
|
||||
cutout.unlink()
|
||||
except OSError:
|
||||
pass
|
||||
for ext in ("jpg", "png"):
|
||||
cutout = job_dir(job_id) / "elements" / f"{idx:03d}_{element_id}.{ext}"
|
||||
if cutout.exists():
|
||||
try: cutout.unlink()
|
||||
except OSError: pass
|
||||
new_frames.append(f)
|
||||
if not removed:
|
||||
raise HTTPException(404, "element not found")
|
||||
@@ -1218,10 +1219,14 @@ def delete_element(job_id: str, idx: int, element_id: str) -> Job:
|
||||
return job
|
||||
|
||||
|
||||
class CutoutReq(BaseModel):
|
||||
background: Literal["white", "black"] = "white"
|
||||
|
||||
|
||||
@app.post("/jobs/{job_id}/frames/{idx}/elements/{element_id}/cutout", response_model=Job)
|
||||
def cutout_element(job_id: str, idx: int, element_id: str) -> Job:
|
||||
"""单元素抠图:调 nano-banana image edit 输出透明背景元素图"""
|
||||
import time as _time
|
||||
def cutout_element(job_id: str, idx: int, element_id: str, req: CutoutReq | None = None) -> Job:
|
||||
"""单元素抠图:调 nano-banana image edit,输出纯白底 / 纯黑底元素图。
|
||||
注:Gemini 输出 JPEG 不支持真 alpha,因此用纯色背景。"""
|
||||
job = JOBS.get(job_id)
|
||||
if not job:
|
||||
raise HTTPException(404, "job not found")
|
||||
@@ -1238,18 +1243,21 @@ def cutout_element(job_id: str, idx: int, element_id: str) -> Job:
|
||||
if not src.exists():
|
||||
raise HTTPException(404, "source frame file missing")
|
||||
|
||||
background = (req.background if req else "white") or "white"
|
||||
bg_phrase = f"pure {background}"
|
||||
|
||||
target = (el.name_en or el.name_zh).strip()
|
||||
region_phrase = _region_to_phrase(el.region) if el.region else ""
|
||||
if region_phrase:
|
||||
prompt = (
|
||||
f"Extract whatever is in the {region_phrase} part of the image as a standalone asset. "
|
||||
"Output on transparent background, isolated, no other objects."
|
||||
f"Place it on a {bg_phrase} background, isolated, no other objects."
|
||||
)
|
||||
else:
|
||||
position_hint = f" Located in the {el.position} area." if el.position else ""
|
||||
prompt = (
|
||||
f"Extract the {target} from this image as a standalone asset.{position_hint} "
|
||||
"Output on transparent background, isolated, no other objects."
|
||||
f"Place it on a {bg_phrase} background, isolated, no other objects."
|
||||
)
|
||||
try:
|
||||
img_bytes, _mode = _image_edit_call(src, prompt, fallback_text=False, max_attempts=3)
|
||||
@@ -1258,26 +1266,37 @@ def cutout_element(job_id: str, idx: int, element_id: str) -> Job:
|
||||
|
||||
out_dir = job_dir(job_id) / "elements"
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
out_path = out_dir / f"{idx:03d}_{element_id}.png"
|
||||
# 实际是 JPEG bytes,文件用 .jpg 真名
|
||||
out_path = out_dir / f"{idx:03d}_{element_id}.jpg"
|
||||
out_path.write_bytes(img_bytes)
|
||||
# 旧版的 .png 文件(错命名为 .png 的 JPEG)也清理掉
|
||||
old_png = out_dir / f"{idx:03d}_{element_id}.png"
|
||||
if old_png.exists():
|
||||
try: old_png.unlink()
|
||||
except OSError: pass
|
||||
|
||||
new_frames = []
|
||||
for f in job.frames:
|
||||
if f.index == idx:
|
||||
for e in f.elements:
|
||||
if e.id == element_id:
|
||||
e.cutout_id = element_id # marker that cutout exists; URL derived from id
|
||||
e.cutout_id = element_id
|
||||
e.cutout_background = background
|
||||
new_frames.append(f)
|
||||
update(job, frames=new_frames, message=f"抠图完成 · {el.name_zh}")
|
||||
update(job, frames=new_frames, message=f"抠图完成 · {el.name_zh}({background} 底)")
|
||||
return job
|
||||
|
||||
|
||||
@app.get("/jobs/{job_id}/frames/{idx}/elements/{element_id}/cutout.png")
|
||||
@app.get("/jobs/{job_id}/frames/{idx}/elements/{element_id}/cutout.jpg")
|
||||
def get_cutout(job_id: str, idx: int, element_id: str):
|
||||
p = job_dir(job_id) / "elements" / f"{idx:03d}_{element_id}.png"
|
||||
p = job_dir(job_id) / "elements" / f"{idx:03d}_{element_id}.jpg"
|
||||
if not p.exists():
|
||||
# 兼容老数据
|
||||
legacy = job_dir(job_id) / "elements" / f"{idx:03d}_{element_id}.png"
|
||||
if legacy.exists():
|
||||
return FileResponse(legacy, media_type="image/jpeg")
|
||||
raise HTTPException(404, "cutout not found")
|
||||
return FileResponse(p, media_type="image/png")
|
||||
return FileResponse(p, media_type="image/jpeg")
|
||||
|
||||
|
||||
# ---------- 删除:关键帧 / 单张生成图 ----------
|
||||
@@ -1305,9 +1324,10 @@ def delete_frame(job_id: str, idx: int) -> Job:
|
||||
# 该帧的所有元素抠图(命名前缀 {idx:03d}_)
|
||||
elements_dir = d / "elements"
|
||||
if elements_dir.exists():
|
||||
for p in elements_dir.glob(f"{idx:03d}_*.png"):
|
||||
try: p.unlink()
|
||||
except OSError: pass
|
||||
for ext in ("png", "jpg"):
|
||||
for p in elements_dir.glob(f"{idx:03d}_*.{ext}"):
|
||||
try: p.unlink()
|
||||
except OSError: pass
|
||||
# 该帧的所有生成图
|
||||
gen_dir = d / "gen"
|
||||
if gen_dir.exists():
|
||||
|
||||
Reference in New Issue
Block a user