auto-save 2026-05-12 19:53 (~2)
This commit is contained in:
@@ -272,6 +272,13 @@
|
|||||||
"message": "auto-save 2026-05-12 19:42 (~3)",
|
"message": "auto-save 2026-05-12 19:42 (~3)",
|
||||||
"hash": "f901b71",
|
"hash": "f901b71",
|
||||||
"files_changed": 3
|
"files_changed": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ts": "2026-05-12T19:48:07+08:00",
|
||||||
|
"type": "commit",
|
||||||
|
"message": "auto-save 2026-05-12 19:47 (~2)",
|
||||||
|
"hash": "07766e0",
|
||||||
|
"files_changed": 2
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -128,17 +128,39 @@ export function InputNode({ data, selected }: NodeProps<{ data: NodeData }> | an
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 已下载:仅元数据 + 解析按钮(视频播放器和加帧 controls 在左侧看板 Keyframe section) */}
|
{/* 已下载:视频播放器 + 加帧 + 元数据 + 解析按钮 */}
|
||||||
{hasVideo && job && (
|
{hasVideo && job && (
|
||||||
<>
|
<>
|
||||||
<div className="rounded-md bg-black/30 border border-black/10 dark:border-white/10 px-3 py-2.5">
|
{/* 视频播放器(拖时间轴选帧) */}
|
||||||
<div className="flex items-center justify-between text-[10.5px] font-mono text-[var(--text-faint)] mb-1">
|
<video
|
||||||
<span>视频已下载</span>
|
ref={videoRef}
|
||||||
<span>{job.url.startsWith("upload://") ? "📎 上传" : "🔗 链接"}</span>
|
src={videoUrl(job.id)}
|
||||||
</div>
|
controls
|
||||||
<div className="text-[var(--text-strong)] text-[13px] font-mono">
|
onTimeUpdate={(e) => setVideoT((e.target as HTMLVideoElement).currentTime)}
|
||||||
{job.width}×{job.height} · {job.duration.toFixed(1)}s
|
className="block w-full rounded-md bg-black border border-black/10 dark:border-white/10"
|
||||||
</div>
|
/>
|
||||||
|
{/* 加帧按钮(已抽过帧才出现) */}
|
||||||
|
{hasFrames && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={addingFrame}
|
||||||
|
onClick={async (e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
const t = videoRef.current?.currentTime ?? videoT
|
||||||
|
setAddingFrame(true)
|
||||||
|
try { await d.onAddManualFrame(t) } finally { setAddingFrame(false) }
|
||||||
|
}}
|
||||||
|
className="mt-2 w-full text-[12px] py-1.5 rounded-md bg-emerald-500 hover:bg-emerald-400 text-white disabled:opacity-50 inline-flex items-center justify-center gap-1.5 font-medium"
|
||||||
|
title="把视频当前时间点抽为新关键帧"
|
||||||
|
>
|
||||||
|
{addingFrame ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Plus className="h-3.5 w-3.5" />}
|
||||||
|
{addingFrame ? "抽帧中…" : `+ 把 ${videoT.toFixed(1)}s 加为关键帧`}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{/* 元数据 */}
|
||||||
|
<div className="mt-2 rounded-md bg-black/30 border border-black/10 dark:border-white/10 px-3 py-2 flex items-center justify-between text-[10.5px] font-mono">
|
||||||
|
<span className="text-[var(--text-strong)]">{job.width}×{job.height} · {job.duration.toFixed(1)}s</span>
|
||||||
|
<span className="text-[var(--text-faint)]">{job.url.startsWith("upload://") ? "📎 上传" : "🔗 链接"}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@@ -229,19 +251,14 @@ export function KeyframeNode({ data, selected }: any) {
|
|||||||
const st = keyframeStatus(d.job)
|
const st = keyframeStatus(d.job)
|
||||||
const frames = d.job?.frames ?? []
|
const frames = d.job?.frames ?? []
|
||||||
const jobId = d.job?.id
|
const jobId = d.job?.id
|
||||||
const job = d.job
|
|
||||||
const hasVideo = !!job?.video_url
|
|
||||||
const videoRef = useRef<HTMLVideoElement>(null)
|
|
||||||
const [videoT, setVideoT] = useState(0)
|
|
||||||
const [addingFrame, setAddingFrame] = useState(false)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative" style={{ width: KEYFRAME_WIDTH }}>
|
<div className="relative" style={{ width: KEYFRAME_WIDTH }}>
|
||||||
{/* 缩略图浮条(节点上方) */}
|
{/* 缩略图浮条(节点上方,最多 5 个一行,多行向上扩展) */}
|
||||||
{frames.length > 0 && jobId && (
|
{frames.length > 0 && jobId && (
|
||||||
<div
|
<div
|
||||||
className="absolute left-0 right-0 -top-[68px] flex items-end justify-center"
|
className="absolute left-0 right-0 grid grid-cols-5 gap-1.5"
|
||||||
style={{ gap: THUMB_GAP }}
|
style={{ bottom: "calc(100% + 12px)" }}
|
||||||
>
|
>
|
||||||
{frames.map((f) => {
|
{frames.map((f) => {
|
||||||
const isSel = d.selectedFrames.has(f.index)
|
const isSel = d.selectedFrames.has(f.index)
|
||||||
@@ -250,12 +267,11 @@ export function KeyframeNode({ data, selected }: any) {
|
|||||||
key={f.index}
|
key={f.index}
|
||||||
onClick={(e) => { e.stopPropagation(); d.onExpandFrame(f.index) }}
|
onClick={(e) => { e.stopPropagation(); d.onExpandFrame(f.index) }}
|
||||||
title={`第 ${f.index + 1} 张 · ${f.timestamp.toFixed(1)}s · 点击放大`}
|
title={`第 ${f.index + 1} 张 · ${f.timestamp.toFixed(1)}s · 点击放大`}
|
||||||
className={`group relative rounded-md border transition shadow-lg hover:-translate-y-0.5 ${
|
className={`group relative rounded-md border transition shadow-lg hover:-translate-y-0.5 aspect-video ${
|
||||||
isSel
|
isSel
|
||||||
? "border-emerald-400 ring-2 ring-emerald-400/60"
|
? "border-emerald-400 ring-2 ring-emerald-400/60"
|
||||||
: "border-white/30 dark:border-white/20"
|
: "border-white/30 dark:border-white/20"
|
||||||
}`}
|
}`}
|
||||||
style={{ width: THUMB_W, height: Math.round(THUMB_W * 9 / 16) }}
|
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={frameUrl(jobId, f.index)}
|
src={frameUrl(jobId, f.index)}
|
||||||
@@ -313,39 +329,17 @@ export function KeyframeNode({ data, selected }: any) {
|
|||||||
width={KEYFRAME_WIDTH}
|
width={KEYFRAME_WIDTH}
|
||||||
selected={selected}
|
selected={selected}
|
||||||
>
|
>
|
||||||
{hasVideo && jobId ? (
|
{frames.length > 0 ? (
|
||||||
<div className="space-y-2">
|
<div className="text-[11.5px] leading-relaxed text-[var(--text-soft)]">
|
||||||
<video
|
自动 <span className="text-[var(--text-strong)] font-medium">{frames.length}</span> 张 · 选中 {d.selectedFrames.size} 张
|
||||||
ref={videoRef}
|
<br />
|
||||||
src={videoUrl(jobId)}
|
<span className="text-[10.5px] text-[var(--text-faint)]">
|
||||||
controls
|
上方缩略图点击放大 · 在 Input 节点拖时间轴可手动加帧
|
||||||
onTimeUpdate={(e) => setVideoT((e.target as HTMLVideoElement).currentTime)}
|
</span>
|
||||||
className="block w-full rounded-md bg-black border border-black/10 dark:border-white/10"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
disabled={addingFrame}
|
|
||||||
onClick={async (e) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
const t = videoRef.current?.currentTime ?? videoT
|
|
||||||
setAddingFrame(true)
|
|
||||||
try { await d.onAddManualFrame(t) } finally { setAddingFrame(false) }
|
|
||||||
}}
|
|
||||||
className="w-full text-[12px] py-2 rounded-md bg-emerald-500 hover:bg-emerald-400 text-white disabled:opacity-50 inline-flex items-center justify-center gap-1.5 font-medium"
|
|
||||||
title="把视频当前时间点抽为新关键帧"
|
|
||||||
>
|
|
||||||
{addingFrame ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Plus className="h-3.5 w-3.5" />}
|
|
||||||
{addingFrame ? "抽帧中…" : `+ 把 ${videoT.toFixed(1)}s 加为关键帧`}
|
|
||||||
</button>
|
|
||||||
<div className="text-[10.5px] text-[var(--text-faint)] text-center">
|
|
||||||
{frames.length > 0
|
|
||||||
? `自动 ${frames.length} 张 · 选中 ${d.selectedFrames.size} 张 · 上方缩略图可点击放大`
|
|
||||||
: "等待解析后抽取(默认 5 张)"}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-[11.5px] text-[var(--text-faint)] py-1">
|
<div className="text-[11.5px] text-[var(--text-faint)] py-1">
|
||||||
等待视频下载完成
|
等待解析(默认 5 张)
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</NodeShell>
|
</NodeShell>
|
||||||
|
|||||||
Reference in New Issue
Block a user