From bdbaf7585023cee8b9ff5d2618129a729e5bef22 Mon Sep 17 00:00:00 2001 From: kang Date: Thu, 14 May 2026 02:58:36 +0800 Subject: [PATCH] auto-save 2026-05-14 02:58 (~6) --- .memory/worklog.json | 13 +++++++++++++ api/main.py | 13 +++++++++++++ docs/source-analysis.html | 13 +++++++++++++ web/app/page.tsx | 35 ++++++++++++++++++++++++++++++---- web/components/nodes/index.tsx | 16 ++++++++++++++++ web/lib/api.ts | 9 +++++++++ 6 files changed, 95 insertions(+), 4 deletions(-) diff --git a/.memory/worklog.json b/.memory/worklog.json index d1bb45e..1ce08fc 100644 --- a/.memory/worklog.json +++ b/.memory/worklog.json @@ -2966,6 +2966,19 @@ "type": "session-heartbeat", "message": "Codex 会话活跃 · 最近命令:codex · 3 项未提交变更 · 最近提交:auto-save 2026-05-14 02:47 (~1)", "files_changed": 3 + }, + { + "ts": "2026-05-14T02:53:06+08:00", + "type": "commit", + "message": "auto-save 2026-05-14 02:52 (~4)", + "hash": "3ab9da0", + "files_changed": 4 + }, + { + "ts": "2026-05-13T18:53:11Z", + "type": "session-heartbeat", + "message": "Claude 会话活跃 · 最近命令:claude · 2 项未提交变更 · 最近提交:auto-save 2026-05-14 02:52 (~4)", + "files_changed": 2 } ] } diff --git a/api/main.py b/api/main.py index d6e0451..230ae5f 100644 --- a/api/main.py +++ b/api/main.py @@ -989,6 +989,19 @@ def get_job(job_id: str) -> Job: return job +@app.delete("/jobs/{job_id}") +def delete_job(job_id: str) -> dict[str, bool | str]: + d = (JOBS_DIR / job_id).resolve() + if JOBS_DIR not in d.parents: + raise HTTPException(400, "invalid job id") + job = JOBS.pop(job_id, None) + if not job and not d.exists(): + raise HTTPException(404, "job not found") + if d.exists(): + shutil.rmtree(d) + return {"ok": True, "id": job_id} + + @app.post("/jobs/{job_id}/transcribe", response_model=Job) async def trigger_transcribe(job_id: str, bg: BackgroundTasks) -> Job: job = JOBS.get(job_id) diff --git a/docs/source-analysis.html b/docs/source-analysis.html index 645b04e..37c7a00 100644 --- a/docs/source-analysis.html +++ b/docs/source-analysis.html @@ -699,6 +699,7 @@ api/main.py 历史列表GET /jobslistJobs所有 job 精简列表(id/url/status/thumbnail/mtime…),按 state.json mtime 倒序。前端 URL 无 ?job= 时拉它回填全部历史;带 limit 可截断。 创建任务POST /jobscreateJob提交 TK 链接,后台开始下载,停在 downloaded 等用户点解析。 上传视频POST /jobs/uploaduploadJob保存 source.mp4,然后同样进入下载完成状态。 + 删除输入视频DELETE /jobs/{id}deleteJob从任务队列、URL 和磁盘 jobs/<id> 目录移除整个 job,包括源视频、关键帧、元素提取图和生成视频。 解析视频POST /jobs/{id}/analyzeanalyzeJob拆轨 + 抽关键帧。当前不自动跑 ASR,避免 audio 阻塞视觉管线。 手动加帧POST /jobs/{id}/frames?t=addManualFrame按视频时间戳抽一帧,index 递增但 frames 按 timestamp 排序。 Vision 识别POST /frames/{idx}/describedescribeFrame写入 frame.description,后续可从 objects 加候选元素。 @@ -815,6 +816,18 @@ api/main.py

变更记录

这个记录不是 git log 的替代品。它记录“产品理解发生了什么变化、影响了哪些源码、你以后描述需求时该怎么说”。后续每次改功能都要补一条。

+
+
+

2026-05-14 · 输入视频缩略图支持删除整个 job

+ Input + API +
+
+

问题:画布顶部输入视频缩略图只有切换和预览,没有删除入口;用户清理视频队列时只能删关键帧或生成视频任务,不能删除整个输入视频。

+

改动:新增 DELETE /jobs/{id},前端新增 deleteJobonDeleteJob;Input 缩略图右上角常驻删除按钮,确认后移除该 job、清理 URL 参数,并删除本地 jobs/<id> 目录。

+

影响:api/main.pyweb/lib/api.tsweb/app/page.tsxweb/components/nodes/index.tsxdocs/source-analysis.html

+
+

2026-05-14 · 大图预览改为尺寸感知 Fit / 1:1

diff --git a/web/app/page.tsx b/web/app/page.tsx index e24e647..ad2da43 100644 --- a/web/app/page.tsx +++ b/web/app/page.tsx @@ -15,7 +15,7 @@ import { } from "@/components/nodes" import { ThemeToggle } from "@/components/theme-toggle" import { - addManualFrame, analyzeJob, createJob, getJob, listJobs, uploadJob, deleteFrame, deleteGeneratedImage, + addManualFrame, analyzeJob, createJob, getJob, listJobs, uploadJob, deleteJob, deleteFrame, deleteGeneratedImage, deleteGeneratedVideo, deleteCutout, generateStoryboardVideo, type Job, type ImageRef, type StoryboardScene, } from "@/lib/api" @@ -233,6 +233,29 @@ export default function Home() { } }, [activeJobId, setJob]) + const handleDeleteJob = useCallback(async (jobId: string) => { + try { + await deleteJob(jobId) + setJobs((prev) => { + const idx = prev.findIndex((x) => x.id === jobId) + const next = prev.filter((x) => x.id !== jobId) + if (activeJobId === jobId) { + const fallback = next[idx] ?? next[idx - 1] ?? next[next.length - 1] ?? null + setActiveJobId(fallback?.id ?? null) + setSelectedFrames(new Set()) + setExpandedFrame(null) + setVideoLightboxOpen(false) + setStoryboardFrame(null) + setWorkbenchOpen(false) + } + return next + }) + toast.success("输入视频已删除") + } catch (e) { + toast.error("删除失败:" + (e instanceof Error ? e.message : String(e))) + } + }, [activeJobId]) + const handleDeleteCutout = useCallback(async (frameIdx: number, elementId: string, cutoutId: string) => { if (!activeJobId) return try { @@ -372,9 +395,12 @@ export default function Home() { // 写回 URL(所有 jobs id 用 , 分隔) useEffect(() => { - if (jobs.length === 0) return const url = new URL(window.location.href) - url.searchParams.set("job", jobs.map((j) => j.id).join(",")) + if (jobs.length > 0) { + url.searchParams.set("job", jobs.map((j) => j.id).join(",")) + } else { + url.searchParams.delete("job") + } window.history.replaceState({}, "", url.toString()) }, [jobs.length]) @@ -454,6 +480,7 @@ export default function Home() { onOpenVideoLightbox: () => setVideoLightboxOpen(true), onSwitchJob: handleSwitchJob, onJobUpdate: setJob as any, + onDeleteJob: handleDeleteJob, onDeleteFrame: handleDeleteFrame, onDeleteGenerated: handleDeleteGenerated, onDeleteVideo: handleDeleteVideo, @@ -466,7 +493,7 @@ export default function Home() { onCopyImage: handleCopyImage, pinnedNodes, onToggleNodePin: handleToggleNodePin, - }), [job, jobs, activeJobId, submitting, analyzing, selectedFrames, expandedFrame, framePanelScale, framePanelPinned, handleSubmit, handleUpload, handleAnalyze, handleToggleFrame, handleOpenFramePanel, handleFramePanelScaleChange, handleAddManualFrame, handleSwitchJob, setJob, handleDeleteFrame, handleDeleteGenerated, handleDeleteVideo, handleDeleteCutout, handleCopyImage, pinnedNodes, handleToggleNodePin]) + }), [job, jobs, activeJobId, submitting, analyzing, selectedFrames, expandedFrame, framePanelScale, framePanelPinned, handleSubmit, handleUpload, handleAnalyze, handleToggleFrame, handleOpenFramePanel, handleFramePanelScaleChange, handleAddManualFrame, handleSwitchJob, setJob, handleDeleteJob, handleDeleteFrame, handleDeleteGenerated, handleDeleteVideo, handleDeleteCutout, handleCopyImage, pinnedNodes, handleToggleNodePin]) // 用 useNodesState 让 ReactFlow 自己管位置(避免轮询时重置 drag) const savedSizes = useMemo(() => loadNodeSizes(), []) diff --git a/web/components/nodes/index.tsx b/web/components/nodes/index.tsx index f4be8c9..caec67a 100644 --- a/web/components/nodes/index.tsx +++ b/web/components/nodes/index.tsx @@ -44,6 +44,7 @@ export interface NodeData { onOpenVideoLightbox: () => void onSwitchJob: (id: string) => void onJobUpdate: (j: Job) => void + onDeleteJob?: (id: string) => void onOpenPanel?: (key: string) => void // 控制 sidebar 哪个 drawer 展开 onDeleteFrame?: (idx: number) => void // 删整张关键帧 onDeleteGenerated?: (frameIdx: number, genId: string) => void // 删单张生成图 @@ -411,6 +412,21 @@ export function InputNode({ data, selected }: NodeProps<{ data: NodeData }> | an {ready ? `${j.duration.toFixed(1)}s` : "…"}
+ {d.onDeleteJob && ( + + )} ) })} diff --git a/web/lib/api.ts b/web/lib/api.ts index 8c9b2b8..0ef54af 100644 --- a/web/lib/api.ts +++ b/web/lib/api.ts @@ -220,6 +220,15 @@ export async function getJob(id: string): Promise { return res.json() } +export async function deleteJob(id: string): Promise<{ ok: boolean; id: string }> { + const res = await fetch(`${API_BASE}/jobs/${id}`, { method: "DELETE" }) + if (!res.ok) { + const txt = await res.text().catch(() => "") + throw new Error(`deleteJob ${res.status} ${txt.slice(0, 200)}`) + } + return res.json() +} + export interface JobSummary { id: string url: string