From e4989f606595744ed26492c55c885d4b191ce54f Mon Sep 17 00:00:00 2001 From: kang Date: Wed, 13 May 2026 10:44:25 +0800 Subject: [PATCH] auto-save 2026-05-13 10:44 (~4) --- .memory/worklog.json | 7 ++++ api/main.py | 69 ++++++++++++++++++++++++++++++++++ web/components/nodes/index.tsx | 54 ++++++++++++++++++++++---- web/lib/api.ts | 18 +++++++++ 4 files changed, 141 insertions(+), 7 deletions(-) diff --git a/.memory/worklog.json b/.memory/worklog.json index 2b57512..d26c9e5 100644 --- a/.memory/worklog.json +++ b/.memory/worklog.json @@ -1250,6 +1250,13 @@ "type": "session-heartbeat", "message": "Claude 会话活跃 · 最近命令:claude · 5 项未提交变更 · 最近提交:auto-save 2026-05-13 10:33 (~2)", "files_changed": 5 + }, + { + "ts": "2026-05-13T10:38:52+08:00", + "type": "commit", + "message": "auto-save 2026-05-13 10:38 (~5)", + "hash": "98d4ecb", + "files_changed": 5 } ] } diff --git a/api/main.py b/api/main.py index e2c12f8..20db6d1 100644 --- a/api/main.py +++ b/api/main.py @@ -1172,3 +1172,72 @@ def get_cutout(job_id: str, idx: int, element_id: str): if not p.exists(): raise HTTPException(404, "cutout not found") return FileResponse(p, media_type="image/png") + + +# ---------- 删除:关键帧 / 单张生成图 ---------- + +@app.delete("/jobs/{job_id}/frames/{idx}", response_model=Job) +def delete_frame(job_id: str, idx: int) -> Job: + """删除整张关键帧,清理所有附属文件(原图 / 干净版 / 元素抠图 / 生成图)""" + job = JOBS.get(job_id) + if not job: + raise HTTPException(404, "job not found") + target = next((f for f in job.frames if f.index == idx), None) + if not target: + raise HTTPException(404, "frame not found") + + d = job_dir(job_id) + # 删文件 — 静默错误,文件可能不存在 + paths = [ + d / "frames" / f"{idx:03d}.jpg", + d / "cleaned" / f"{idx:03d}.jpg", + ] + for p in paths: + if p.exists(): + try: p.unlink() + except OSError: pass + # 该帧的所有元素抠图(命名前缀 {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 + # 该帧的所有生成图 + gen_dir = d / "gen" + if gen_dir.exists(): + for p in gen_dir.glob(f"{idx:03d}_*.jpg"): + try: p.unlink() + except OSError: pass + + new_frames = [f for f in job.frames if f.index != idx] + update(job, frames=new_frames, message=f"删除分镜 {idx + 1}") + return job + + +@app.delete("/jobs/{job_id}/frames/{idx}/gen/{gen_id}", response_model=Job) +def delete_generated(job_id: str, idx: int, gen_id: str) -> Job: + """删除该 frame 的某张生成图(文件 + 列表)""" + job = JOBS.get(job_id) + if not job: + raise HTTPException(404, "job not found") + frame = next((f for f in job.frames if f.index == idx), None) + if not frame: + raise HTTPException(404, "frame not found") + + p = job_dir(job_id) / "gen" / f"{idx:03d}_{gen_id}.jpg" + if p.exists(): + try: p.unlink() + except OSError: pass + + new_frames = [] + found = False + for f in job.frames: + if f.index == idx: + before = len(f.generated_images) + f.generated_images = [g for g in f.generated_images if g.id != gen_id] + found = len(f.generated_images) < before + new_frames.append(f) + if not found: + raise HTTPException(404, "generated image not found") + update(job, frames=new_frames, message=f"删除生成图 · 分镜 {idx + 1}") + return job diff --git a/web/components/nodes/index.tsx b/web/components/nodes/index.tsx index 3c9bb35..272f18c 100644 --- a/web/components/nodes/index.tsx +++ b/web/components/nodes/index.tsx @@ -3,7 +3,7 @@ import { useRef, useState } from "react" import { type NodeProps } from "@xyflow/react" import { Link2, Upload, Download, Scissors, Image as ImageIcon, - Mic, Languages, FileEdit, Sparkles, Film, FileVideo, Loader2, Plus, + Mic, Languages, FileEdit, Sparkles, Film, FileVideo, Loader2, Plus, X, } from "lucide-react" import { NodeShell, type NodeStatus, type NodeKind } from "./node-shell" import { type Job, frameUrl, videoUrl, generatedImageUrl } from "@/lib/api" @@ -27,6 +27,8 @@ export interface NodeData { onSwitchJob: (id: string) => void onJobUpdate: (j: Job) => void onOpenPanel?: (key: string) => void // 控制 sidebar 哪个 drawer 展开 + onDeleteFrame?: (idx: number) => void // 删整张关键帧 + onDeleteGenerated?: (frameIdx: number, genId: string) => void // 删单张生成图 } /* ---- 状态映射工具 ---- */ @@ -354,10 +356,8 @@ export function KeyframeNode({ data, selected }: any) { {frames.map((f) => { const isSel = d.selectedFrames.has(f.index) return ( - + {/* 删除按钮:hover 时右上角浮出 */} + {d.onDeleteFrame && ( + + )} + ) })} @@ -595,16 +616,19 @@ export function ImageGenNode({ data, selected }: any) { style={{ bottom: "calc(100% + 12px)" }} > {previews.map((p) => ( - + {/* 删除按钮:hover 时右上角浮出 */} + {d.onDeleteGenerated && ( + + )} + ))} )} diff --git a/web/lib/api.ts b/web/lib/api.ts index 630b9fc..c556d4a 100644 --- a/web/lib/api.ts +++ b/web/lib/api.ts @@ -242,6 +242,24 @@ export async function deleteElement(jobId: string, frameIdx: number, elementId: return res.json() } +export async function deleteFrame(jobId: string, frameIdx: number): Promise { + const res = await fetch(`${API_BASE}/jobs/${jobId}/frames/${frameIdx}`, { method: "DELETE" }) + if (!res.ok) { + const txt = await res.text().catch(() => "") + throw new Error(`deleteFrame ${res.status} ${txt.slice(0, 300)}`) + } + return res.json() +} + +export async function deleteGeneratedImage(jobId: string, frameIdx: number, genId: string): Promise { + const res = await fetch(`${API_BASE}/jobs/${jobId}/frames/${frameIdx}/gen/${genId}`, { method: "DELETE" }) + if (!res.ok) { + const txt = await res.text().catch(() => "") + throw new Error(`deleteGen ${res.status} ${txt.slice(0, 300)}`) + } + return res.json() +} + export async function cutoutElement(jobId: string, frameIdx: number, elementId: string): Promise { const res = await fetch(`${API_BASE}/jobs/${jobId}/frames/${frameIdx}/elements/${elementId}/cutout`, { method: "POST" }) if (!res.ok) {