auto-save 2026-05-14 02:58 (~6)
This commit is contained in:
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
13
api/main.py
13
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)
|
||||
|
||||
@@ -699,6 +699,7 @@ api/main.py
|
||||
<tr><td>历史列表</td><td><code>GET /jobs</code></td><td><code>listJobs</code></td><td>所有 job 精简列表(id/url/status/thumbnail/mtime…),按 state.json mtime 倒序。前端 URL 无 <code>?job=</code> 时拉它回填全部历史;带 <code>limit</code> 可截断。</td></tr>
|
||||
<tr><td>创建任务</td><td><code>POST /jobs</code></td><td><code>createJob</code></td><td>提交 TK 链接,后台开始下载,停在 downloaded 等用户点解析。</td></tr>
|
||||
<tr><td>上传视频</td><td><code>POST /jobs/upload</code></td><td><code>uploadJob</code></td><td>保存 source.mp4,然后同样进入下载完成状态。</td></tr>
|
||||
<tr><td>删除输入视频</td><td><code>DELETE /jobs/{id}</code></td><td><code>deleteJob</code></td><td>从任务队列、URL 和磁盘 <code>jobs/<id></code> 目录移除整个 job,包括源视频、关键帧、元素提取图和生成视频。</td></tr>
|
||||
<tr><td>解析视频</td><td><code>POST /jobs/{id}/analyze</code></td><td><code>analyzeJob</code></td><td>拆轨 + 抽关键帧。当前不自动跑 ASR,避免 audio 阻塞视觉管线。</td></tr>
|
||||
<tr><td>手动加帧</td><td><code>POST /jobs/{id}/frames?t=</code></td><td><code>addManualFrame</code></td><td>按视频时间戳抽一帧,index 递增但 frames 按 timestamp 排序。</td></tr>
|
||||
<tr><td>Vision 识别</td><td><code>POST /frames/{idx}/describe</code></td><td><code>describeFrame</code></td><td>写入 frame.description,后续可从 objects 加候选元素。</td></tr>
|
||||
@@ -815,6 +816,18 @@ api/main.py
|
||||
<h2>变更记录</h2>
|
||||
<p>这个记录不是 git log 的替代品。它记录“产品理解发生了什么变化、影响了哪些源码、你以后描述需求时该怎么说”。后续每次改功能都要补一条。</p>
|
||||
<div class="changelog">
|
||||
<article class="change">
|
||||
<header>
|
||||
<h3>2026-05-14 · 输入视频缩略图支持删除整个 job</h3>
|
||||
<span class="tag violet">Input</span>
|
||||
<span class="tag blue">API</span>
|
||||
</header>
|
||||
<div class="body">
|
||||
<p><strong>问题:</strong>画布顶部输入视频缩略图只有切换和预览,没有删除入口;用户清理视频队列时只能删关键帧或生成视频任务,不能删除整个输入视频。</p>
|
||||
<p><strong>改动:</strong>新增 <code>DELETE /jobs/{id}</code>,前端新增 <code>deleteJob</code> 和 <code>onDeleteJob</code>;Input 缩略图右上角常驻删除按钮,确认后移除该 job、清理 URL 参数,并删除本地 <code>jobs/<id></code> 目录。</p>
|
||||
<p><strong>影响:</strong><code>api/main.py</code>、<code>web/lib/api.ts</code>、<code>web/app/page.tsx</code>、<code>web/components/nodes/index.tsx</code>、<code>docs/source-analysis.html</code>。</p>
|
||||
</div>
|
||||
</article>
|
||||
<article class="change">
|
||||
<header>
|
||||
<h3>2026-05-14 · 大图预览改为尺寸感知 Fit / 1:1</h3>
|
||||
|
||||
@@ -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(), [])
|
||||
|
||||
@@ -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` : "…"}
|
||||
</div>
|
||||
</button>
|
||||
{d.onDeleteJob && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
if (confirm(`删除视频任务 ${j.id.slice(0, 8)}?源视频、关键帧、元素提取图和生成视频都会一并删除。`)) {
|
||||
d.onDeleteJob?.(j.id)
|
||||
}
|
||||
}}
|
||||
title="删除这个输入视频"
|
||||
className="absolute right-1.5 top-1.5 z-[70] inline-flex h-7 w-7 items-center justify-center rounded-full bg-rose-500/95 text-white shadow-lg backdrop-blur transition hover:scale-110 hover:bg-rose-400"
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -220,6 +220,15 @@ export async function getJob(id: string): Promise<Job> {
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user