auto-save 2026-05-13 14:38 (~4)
This commit is contained in:
86
api/main.py
86
api/main.py
@@ -1249,13 +1249,15 @@ def delete_element(job_id: str, idx: int, element_id: str) -> Job:
|
||||
before = len(f.elements)
|
||||
f.elements = [e for e in f.elements if e.id != element_id]
|
||||
removed = len(f.elements) < before
|
||||
# 若有抠图文件也删
|
||||
# 若有提取图也删(含多版本)
|
||||
if removed:
|
||||
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
|
||||
elements_dir = job_dir(job_id) / "elements"
|
||||
if elements_dir.exists():
|
||||
for pat in (f"{idx:03d}_{element_id}.jpg", f"{idx:03d}_{element_id}.png",
|
||||
f"{idx:03d}_{element_id}_*.jpg"):
|
||||
for p in elements_dir.glob(pat):
|
||||
try: p.unlink()
|
||||
except OSError: pass
|
||||
new_frames.append(f)
|
||||
if not removed:
|
||||
raise HTTPException(404, "element not found")
|
||||
@@ -1265,9 +1267,9 @@ def delete_element(job_id: str, idx: int, element_id: str) -> Job:
|
||||
|
||||
@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:
|
||||
"""元素裁切 / 抠图:
|
||||
- 有 region(用户画框)→ PIL crop 原帧(瞬时 · 原汁原味保留表情/形体)
|
||||
- 无 region(vision auto / 手动文字)→ 调 nano-banana 抠白底图(5-15s · 模型重画可能有差异)"""
|
||||
"""提取元素 · 每次调用累积一张新图(不覆盖之前的):
|
||||
- 有 region → PIL crop(瞬时 · 保留原表情/形体)
|
||||
- 无 region → 调 nano-banana 模型生成白底图(5-15s)"""
|
||||
from PIL import Image as _PILImage
|
||||
job = JOBS.get(job_id)
|
||||
if not job:
|
||||
@@ -1279,7 +1281,6 @@ def cutout_element(job_id: str, idx: int, element_id: str) -> Job:
|
||||
if not el:
|
||||
raise HTTPException(404, "element not found")
|
||||
|
||||
# 优先用 cleaned 版作 source
|
||||
cleaned_path = job_dir(job_id) / "cleaned" / f"{idx:03d}.jpg"
|
||||
src = cleaned_path if cleaned_path.exists() else job_dir(job_id) / "frames" / f"{idx:03d}.jpg"
|
||||
if not src.exists():
|
||||
@@ -1287,10 +1288,11 @@ 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}.jpg"
|
||||
# 新建一个 cutout_id append 到 element.cutouts(而非覆盖)
|
||||
new_cutout_id = uuid.uuid4().hex[:8]
|
||||
out_path = out_dir / f"{idx:03d}_{element_id}_{new_cutout_id}.jpg"
|
||||
|
||||
if el.region:
|
||||
# 路径 1:PIL crop(用户画的框,原汁原味)
|
||||
try:
|
||||
im = _PILImage.open(src).convert("RGB")
|
||||
W, H = im.size
|
||||
@@ -1302,15 +1304,14 @@ def cutout_element(job_id: str, idx: int, element_id: str) -> Job:
|
||||
left, top = int(x * W), int(y * H)
|
||||
right, bottom = int((x + w) * W), int((y + h) * H)
|
||||
if right - left < 4 or bottom - top < 4:
|
||||
raise HTTPException(400, "region 太小,无法裁切")
|
||||
raise HTTPException(400, "region 太小,无法提取")
|
||||
cropped = im.crop((left, top, right, bottom))
|
||||
cropped.save(out_path, format="JPEG", quality=92)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(500, f"crop failed: {e}")
|
||||
raise HTTPException(500, f"extract failed: {e}")
|
||||
else:
|
||||
# 路径 2:vision auto / 手动文字元素 → 调 nano-banana 抠白底图
|
||||
target = (el.name_en or el.name_zh).strip()
|
||||
position_hint = f" Located in the {el.position} area." if el.position else ""
|
||||
prompt = (
|
||||
@@ -1323,32 +1324,67 @@ def cutout_element(job_id: str, idx: int, element_id: str) -> Job:
|
||||
src, prompt, models=models, fallback_text=False, max_attempts=3,
|
||||
)
|
||||
except RuntimeError as e:
|
||||
raise HTTPException(500, f"cutout failed: {e}")
|
||||
raise HTTPException(500, f"extract failed: {e}")
|
||||
out_path.write_bytes(img_bytes)
|
||||
|
||||
# 清理旧版的 .png(旧版"抠图"产物)
|
||||
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
|
||||
e.cutouts = (e.cutouts or []) + [new_cutout_id]
|
||||
# 兼容:若旧字段 cutout_id 未设置,记一下让旧 UI 仍能读到一张
|
||||
if not e.cutout_id:
|
||||
e.cutout_id = new_cutout_id
|
||||
new_frames.append(f)
|
||||
msg_label = "裁切(PIL crop)" if el.region else "抠图(模型)"
|
||||
msg_label = "提取(PIL)" if el.region else "提取(模型)"
|
||||
update(job, frames=new_frames, message=f"{msg_label}完成 · {el.name_zh}")
|
||||
return job
|
||||
|
||||
|
||||
@app.delete("/jobs/{job_id}/frames/{idx}/elements/{element_id}/cutouts/{cutout_id}", response_model=Job)
|
||||
def delete_cutout(job_id: str, idx: int, element_id: str, cutout_id: str) -> Job:
|
||||
"""删除该元素的某张提取图"""
|
||||
job = JOBS.get(job_id)
|
||||
if not job:
|
||||
raise HTTPException(404, "job not found")
|
||||
p = job_dir(job_id) / "elements" / f"{idx:03d}_{element_id}_{cutout_id}.jpg"
|
||||
if p.exists():
|
||||
try: p.unlink()
|
||||
except OSError: pass
|
||||
|
||||
removed = False
|
||||
new_frames = []
|
||||
for f in job.frames:
|
||||
if f.index == idx:
|
||||
for e in f.elements:
|
||||
if e.id == element_id:
|
||||
if cutout_id in (e.cutouts or []):
|
||||
e.cutouts = [c for c in e.cutouts if c != cutout_id]
|
||||
removed = True
|
||||
# cutout_id 兼容字段:若指向被删的就清空 / 移到 cutouts 第一个
|
||||
if e.cutout_id == cutout_id:
|
||||
e.cutout_id = e.cutouts[0] if e.cutouts else None
|
||||
new_frames.append(f)
|
||||
if not removed:
|
||||
raise HTTPException(404, "cutout not found in element")
|
||||
update(job, frames=new_frames, message=f"删除提取图")
|
||||
return job
|
||||
|
||||
|
||||
@app.get("/jobs/{job_id}/frames/{idx}/elements/{element_id}/cutouts/{cutout_id}.jpg")
|
||||
def get_cutout_versioned(job_id: str, idx: int, element_id: str, cutout_id: str):
|
||||
p = job_dir(job_id) / "elements" / f"{idx:03d}_{element_id}_{cutout_id}.jpg"
|
||||
if not p.exists():
|
||||
raise HTTPException(404, "cutout not found")
|
||||
return FileResponse(p, media_type="image/jpeg")
|
||||
|
||||
|
||||
@app.get("/jobs/{job_id}/frames/{idx}/elements/{element_id}/cutout.jpg")
|
||||
def get_cutout(job_id: str, idx: int, element_id: str):
|
||||
"""旧路径兼容(v1 单图)→ 找 elements/{idx}_{element_id}.jpg 或 .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")
|
||||
|
||||
Reference in New Issue
Block a user