From 700fa2499217e565ce7f7f897108518a40fca5d4 Mon Sep 17 00:00:00 2001 From: kang Date: Wed, 13 May 2026 20:45:53 +0800 Subject: [PATCH] auto-save 2026-05-13 20:45 (~6) --- .memory/worklog.json | 7 +++++++ api/main.py | 22 ++++++++++++++++++++++ docs/source-analysis.html | 12 ++++++++++++ web/app/page.tsx | 16 ++++++++++++++-- web/components/nodes/index.tsx | 27 +++++++++++++++++++++++++++ web/lib/api.ts | 9 +++++++++ 6 files changed, 91 insertions(+), 2 deletions(-) diff --git a/.memory/worklog.json b/.memory/worklog.json index 6f6aa45..cef7798 100644 --- a/.memory/worklog.json +++ b/.memory/worklog.json @@ -2341,6 +2341,13 @@ "type": "session-heartbeat", "message": "Codex 会话活跃 · 最近命令:codex · 4 项未提交变更 · 最近提交:auto-save 2026-05-13 20:34 (~2)", "files_changed": 4 + }, + { + "ts": "2026-05-13T20:40:23+08:00", + "type": "commit", + "message": "auto-save 2026-05-13 20:40 (~4)", + "hash": "66f2495", + "files_changed": 4 } ] } diff --git a/api/main.py b/api/main.py index 1803705..6646556 100644 --- a/api/main.py +++ b/api/main.py @@ -1785,6 +1785,28 @@ def get_storyboard_video(job_id: str, video_id: str): return FileResponse(p, media_type="video/mp4") +@app.delete("/jobs/{job_id}/storyboard-videos/{video_id}", response_model=Job) +def delete_storyboard_video(job_id: str, video_id: str) -> Job: + """删除 Video Gen 节点里的一个视频任务(成功/失败/排队都可删)。""" + job = JOBS.get(job_id) + if not job: + raise HTTPException(404, "job not found") + before = len(job.generated_videos) + removed = next((v for v in job.generated_videos if v.id == video_id), None) + kept = [v for v in job.generated_videos if v.id != video_id] + if len(kept) == before: + raise HTTPException(404, "generated video not found") + out_dir = job_dir(job_id) / "storyboard_videos" / video_id + if out_dir.exists(): + try: + shutil.rmtree(out_dir) + except OSError: + pass + msg = f"删除视频任务 · 分镜 {removed.frame_idx + 1}" if removed else "删除视频任务" + update(job, generated_videos=kept, message=msg) + return job + + @app.put("/jobs/{job_id}/frames/{idx}/storyboard", response_model=Job) def update_storyboard(job_id: str, idx: int, req: UpdateStoryboardReq) -> Job: """更新分镜的编排字段(subject / product / scene / action / duration / reference_ids)""" diff --git a/docs/source-analysis.html b/docs/source-analysis.html index e104f16..47101ad 100644 --- a/docs/source-analysis.html +++ b/docs/source-analysis.html @@ -830,6 +830,18 @@ api/main.py

变更记录

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

+
+
+

2026-05-13 · Video Gen 卡片增加复制和删除

+ VideoGenNode + API +
+
+

问题:Video Gen 节点上方失败/完成任务卡只有整卡点击复制,不够明确;失败任务也无法从界面清掉。

+

改动:每张视频任务卡左上角增加复制 prompt 按钮,右上角增加删除任务按钮;后端新增 DELETE /jobs/{job_id}/storyboard-videos/{video_id},删除 generated_videos 记录并清理本地任务目录。

+

影响:web/components/nodes/index.tsxweb/app/page.tsxweb/lib/api.tsapi/main.py

+
+

2026-05-13 · 分镜编排接入真实生视频任务

diff --git a/web/app/page.tsx b/web/app/page.tsx index 48ae33f..2b75b44 100644 --- a/web/app/page.tsx +++ b/web/app/page.tsx @@ -17,7 +17,7 @@ import { StoryboardBar } from "@/components/storyboard-bar" import { StoryboardWorkbench } from "@/components/storyboard-workbench" import { addManualFrame, analyzeJob, createJob, getJob, uploadJob, deleteFrame, deleteGeneratedImage, - generateStoryboardVideo, + deleteGeneratedVideo, generateStoryboardVideo, type Job, type ImageRef, type StoryboardScene, } from "@/lib/api" import { VideoLightbox } from "@/components/video-lightbox" @@ -210,6 +210,17 @@ export default function Home() { } }, [activeJobId, setJob]) + const handleDeleteVideo = useCallback(async (videoId: string) => { + if (!activeJobId) return + try { + const updated = await deleteGeneratedVideo(activeJobId, videoId) + setJob(updated) + toast.success("视频任务已删除") + } catch (e) { + toast.error("删除失败:" + (e instanceof Error ? e.message : String(e))) + } + }, [activeJobId, setJob]) + const handleCopyImage = useCallback((ref: ImageRef) => { setClipboard(ref) toast.success(`已复制:${ref.label || (ref.kind === "keyframe" ? "关键帧" : "元素")} · 到分镜头编排工作台粘贴`) @@ -348,13 +359,14 @@ export default function Home() { onJobUpdate: setJob as any, onDeleteFrame: handleDeleteFrame, onDeleteGenerated: handleDeleteGenerated, + onDeleteVideo: handleDeleteVideo, onOpenStoryboard: (idx: number) => setStoryboardFrame(idx), onOpenWorkbench: (idx?: number) => { if (typeof idx === "number") setStoryboardFrame(idx) setWorkbenchOpen(true) }, onCopyImage: handleCopyImage, - }), [job, jobs, activeJobId, submitting, analyzing, selectedFrames, expandedFrame, framePanelScale, framePanelPinned, handleSubmit, handleUpload, handleAnalyze, handleToggleFrame, handleOpenFramePanel, handleFramePanelScaleChange, handleAddManualFrame, handleSwitchJob, setJob, handleDeleteFrame, handleDeleteGenerated, handleCopyImage]) + }), [job, jobs, activeJobId, submitting, analyzing, selectedFrames, expandedFrame, framePanelScale, framePanelPinned, handleSubmit, handleUpload, handleAnalyze, handleToggleFrame, handleOpenFramePanel, handleFramePanelScaleChange, handleAddManualFrame, handleSwitchJob, setJob, handleDeleteFrame, handleDeleteGenerated, handleDeleteVideo, handleCopyImage]) // 用 useNodesState 让 ReactFlow 自己管位置(避免轮询时重置 drag) const [nodes, setNodes, onNodesChange] = useNodesState( diff --git a/web/components/nodes/index.tsx b/web/components/nodes/index.tsx index 9052e29..3171ce6 100644 --- a/web/components/nodes/index.tsx +++ b/web/components/nodes/index.tsx @@ -5,7 +5,9 @@ import { type NodeProps, useReactFlow } from "@xyflow/react" import { Link2, Upload, Download, Scissors, Image as ImageIcon, Mic, Languages, FileEdit, Film, FileVideo, Loader2, Plus, X, LayoutGrid, Pin, Maximize2, + Copy, Trash2, } from "lucide-react" +import { toast } from "sonner" import { NodeShell, type NodeStatus, type NodeKind } from "./node-shell" import { type Job, type ImageRef, @@ -39,6 +41,7 @@ export interface NodeData { onOpenPanel?: (key: string) => void // 控制 sidebar 哪个 drawer 展开 onDeleteFrame?: (idx: number) => void // 删整张关键帧 onDeleteGenerated?: (frameIdx: number, genId: string) => void // 删单张生成图 + onDeleteVideo?: (videoId: string) => void // 删 Video Gen 任务 onOpenStoryboard?: (frameIdx: number) => void // 打开分镜头编排专属面板 onOpenWorkbench?: (frameIdx?: number) => void // 展开顶部分镜编排内嵌面板 onCopyImage?: (ref: ImageRef) => void // 复制图片到全局剪贴板(粘贴到分镜头编排插槽) @@ -982,6 +985,7 @@ export function VideoGenNode({ data, selected }: any) { onClick={(e) => { e.stopPropagation() void navigator.clipboard?.writeText(v.prompt).catch(() => {}) + toast.success("已复制视频 prompt") }} title={`分镜 ${v.frame_idx + 1} · ${modelLabel(v.model)} · 点击复制 prompt`} className="absolute inset-0 w-full h-full overflow-hidden rounded-md bg-black" @@ -1023,6 +1027,29 @@ export function VideoGenNode({ data, selected }: any) {
+ +
{ + const res = await fetch(`${API_BASE}/jobs/${jobId}/storyboard-videos/${videoId}`, { method: "DELETE" }) + if (!res.ok) { + const txt = await res.text().catch(() => "") + throw new Error(`deleteGeneratedVideo ${res.status} ${txt.slice(0, 300)}`) + } + return res.json() +} + export async function deleteCutout(jobId: string, frameIdx: number, elementId: string, cutoutId: string): Promise { const res = await fetch(`${API_BASE}/jobs/${jobId}/frames/${frameIdx}/elements/${elementId}/cutouts/${cutoutId}`, { method: "DELETE",