auto-save 2026-05-13 16:28 (~3)
This commit is contained in:
@@ -1888,6 +1888,19 @@
|
|||||||
"message": "auto-save 2026-05-13 16:17 (~3)",
|
"message": "auto-save 2026-05-13 16:17 (~3)",
|
||||||
"hash": "f891cbc",
|
"hash": "f891cbc",
|
||||||
"files_changed": 3
|
"files_changed": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ts": "2026-05-13T16:23:35+08:00",
|
||||||
|
"type": "commit",
|
||||||
|
"message": "auto-save 2026-05-13 16:23 (~6)",
|
||||||
|
"hash": "467e8f6",
|
||||||
|
"files_changed": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ts": "2026-05-13T08:27:40Z",
|
||||||
|
"type": "session-heartbeat",
|
||||||
|
"message": "Claude 会话活跃 · 最近命令:claude · 3 项未提交变更 · 最近提交:auto-save 2026-05-13 16:23 (~6)",
|
||||||
|
"files_changed": 3
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -386,6 +386,7 @@ export default function Home() {
|
|||||||
open={workbenchOpen}
|
open={workbenchOpen}
|
||||||
onClose={() => setWorkbenchOpen(false)}
|
onClose={() => setWorkbenchOpen(false)}
|
||||||
onJobUpdate={setJob as any}
|
onJobUpdate={setJob as any}
|
||||||
|
clipboard={clipboard}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
import { useEffect, useState, useRef, type ReactNode } from "react"
|
import { useEffect, useState, useRef, type ReactNode } from "react"
|
||||||
import { createPortal } from "react-dom"
|
import { createPortal } from "react-dom"
|
||||||
import { X, LayoutGrid, Loader2, Check, Sparkle, Wand2 } from "lucide-react"
|
import { X, LayoutGrid, Loader2, Check, Wand2 } from "lucide-react"
|
||||||
import {
|
import {
|
||||||
type Job, type StoryboardScene, type ImageRef,
|
type Job, type StoryboardScene, type ImageRef,
|
||||||
effectiveFrameUrl, updateStoryboard, resolveImageRefUrl,
|
effectiveFrameUrl, updateStoryboard, resolveImageRefUrl,
|
||||||
@@ -83,21 +83,6 @@ export function StoryboardWorkbench({ job, selectedFrames, open, onClose, onJobU
|
|||||||
}, 600)
|
}, 600)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 参考图候选 = 全部已推送图
|
|
||||||
const pushedImages = job.storyboard_images ?? []
|
|
||||||
const refOptions = pushedImages
|
|
||||||
.map((p) => {
|
|
||||||
const url = p.kind === "keyframe"
|
|
||||||
? effectiveFrameUrl(job.id, { index: p.frame_idx, cleaned_applied: false })
|
|
||||||
: (p.element_id && p.cutout_id
|
|
||||||
? (p.cutout_id === p.element_id
|
|
||||||
? cutoutUrl(job.id, p.frame_idx, p.element_id)
|
|
||||||
: cutoutUrl(job.id, p.frame_idx, p.element_id, p.cutout_id))
|
|
||||||
: "")
|
|
||||||
return { ref_id: p.ref_id, url, label: p.label || "", frame_idx: p.frame_idx, kind: p.kind }
|
|
||||||
})
|
|
||||||
.filter((r) => r.url)
|
|
||||||
|
|
||||||
const aspect = job.height > 0 ? `${job.width}/${job.height}` : "9/16"
|
const aspect = job.height > 0 ? `${job.width}/${job.height}` : "9/16"
|
||||||
|
|
||||||
return createPortal(
|
return createPortal(
|
||||||
@@ -111,7 +96,7 @@ export function StoryboardWorkbench({ job, selectedFrames, open, onClose, onJobU
|
|||||||
<LayoutGrid className="h-4 w-4 text-violet-300" />
|
<LayoutGrid className="h-4 w-4 text-violet-300" />
|
||||||
<span className="text-[14px] font-semibold">分镜头编排工作台</span>
|
<span className="text-[14px] font-semibold">分镜头编排工作台</span>
|
||||||
<span className="text-[11px] text-white/40 font-mono ml-2">
|
<span className="text-[11px] text-white/40 font-mono ml-2">
|
||||||
{frames.length} 分镜 · {pushedImages.length} 素材
|
{frames.length} 分镜
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
@@ -184,106 +169,104 @@ export function StoryboardWorkbench({ job, selectedFrames, open, onClose, onJobU
|
|||||||
)}
|
)}
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
{/* 右侧详情 */}
|
{/* 右侧详情 — 4 图槽 + 时长 */}
|
||||||
<main className="flex-1 overflow-y-auto">
|
<main className="flex-1 overflow-y-auto">
|
||||||
{!focusFrame ? (
|
{!focusFrame ? (
|
||||||
<div className="p-8 text-[13px] text-white/40">从左侧选一个分镜开始编排</div>
|
<div className="p-8 text-[13px] text-white/40">从左侧选一个分镜开始编排</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="max-w-5xl mx-auto p-6 space-y-5">
|
<div className="max-w-5xl mx-auto p-6 space-y-5">
|
||||||
{/* 顶栏 */}
|
{/* 顶栏:分镜信息 + 剪贴板提示 + 时长 */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between gap-4">
|
||||||
<div className="text-[15.5px] font-semibold text-white">
|
<div className="text-[15.5px] font-semibold text-white">
|
||||||
分镜 {focusSeq} <span className="text-white/40 text-[12px] font-mono ml-2">{focusFrame.timestamp.toFixed(2)}s</span>
|
分镜 {focusSeq}
|
||||||
|
<span className="text-white/40 text-[12px] font-mono ml-2">{focusFrame.timestamp.toFixed(2)}s</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{clipboard ? (
|
||||||
|
<div className="text-[11px] text-emerald-300 inline-flex items-center gap-1">
|
||||||
|
📋 剪贴板:<span className="text-white font-medium truncate max-w-[180px]">{clipboard.label || (clipboard.kind === "keyframe" ? "关键帧" : "元素")}</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-[11px] text-white/40">剪贴板为空 · 在 DAG / lightbox 上点 📋 复制</div>
|
||||||
|
)}
|
||||||
|
<label className="inline-flex items-center gap-1.5 text-[11px] text-white/55">
|
||||||
|
时长
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="0.1"
|
||||||
|
min="0"
|
||||||
|
value={form.duration || ""}
|
||||||
|
onChange={(e) => queueSave({ ...form, duration: parseFloat(e.target.value) || 0 })}
|
||||||
|
placeholder="3.5"
|
||||||
|
className="w-16 text-[12.5px] px-2 py-1 rounded-md bg-black/40 border border-white/15 outline-none text-white placeholder:text-white/30 focus:ring-2 focus:ring-violet-400/40 focus:border-violet-400/40 text-center"
|
||||||
|
/>
|
||||||
|
秒
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 左大图 + 右字段 */}
|
{/* 4 图槽 grid */}
|
||||||
<div className="flex gap-5">
|
<div className="grid grid-cols-4 gap-4">
|
||||||
<div className="flex-shrink-0">
|
{([
|
||||||
<img
|
{ key: "subject_image" as const, label: "主体", placeholder: "戴头带的骨架人" },
|
||||||
src={effectiveFrameUrl(job.id, focusFrame)}
|
{ key: "scene_image" as const, label: "场景", placeholder: "药店柜台 / 卧室" },
|
||||||
alt=""
|
{ key: "product_image" as const, label: "产品", placeholder: "Goli 营养软糖" },
|
||||||
className="rounded-lg bg-black object-contain"
|
{ key: "action_image" as const, label: "在干什么", placeholder: "递糖给顾客" },
|
||||||
style={{ width: 320, maxHeight: "52vh" }}
|
]).map(({ key, label, placeholder }) => {
|
||||||
/>
|
const ref = form[key]
|
||||||
<div className="mt-1.5 text-[10.5px] text-white/50 text-center">
|
const url = ref ? resolveImageRefUrl(job.id, ref) : ""
|
||||||
{focusFrame.cleaned_applied ? "✨ 已清洗版" : "原图"}
|
return (
|
||||||
</div>
|
<div key={key} className="rounded-lg bg-white/[0.04] border border-white/10 p-2.5">
|
||||||
</div>
|
<div className="text-[12px] text-white font-semibold mb-2 flex items-center justify-between">
|
||||||
|
<span>{label}</span>
|
||||||
<div className="flex-1 min-w-0 space-y-3">
|
{ref && (
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<button
|
||||||
<FieldText label="主体" placeholder="如:戴头带的骨架人"
|
onClick={() => queueSave({ ...form, [key]: null })}
|
||||||
value={form.subject} onChange={(v) => queueSave({ ...form, subject: v })} />
|
className="text-[10px] text-white/40 hover:text-rose-300"
|
||||||
<FieldText label="产品" placeholder="如:Goli 营养软糖"
|
title="清空"
|
||||||
value={form.product} onChange={(v) => queueSave({ ...form, product: v })} />
|
>
|
||||||
<FieldText label="场景" placeholder="如:药店柜台 / 夜晚卧室"
|
✕ 清空
|
||||||
value={form.scene} onChange={(v) => queueSave({ ...form, scene: v })} />
|
</button>
|
||||||
<FieldNum label="时长 (秒)" placeholder="3.5"
|
)}
|
||||||
value={form.duration} onChange={(v) => queueSave({ ...form, duration: v })} />
|
</div>
|
||||||
</div>
|
<div
|
||||||
<FieldTextarea
|
className="relative rounded-md overflow-hidden border-2 border-dashed border-white/15 bg-white/[0.03] flex items-center justify-center mb-2"
|
||||||
label="在干什么"
|
style={{ aspectRatio: "1/1" }}
|
||||||
placeholder="如:骨架人递给顾客一瓶 Goli · 顾客接过并查看 · 灯光柔和"
|
>
|
||||||
value={form.action}
|
{ref && url ? (
|
||||||
onChange={(v) => queueSave({ ...form, action: v })}
|
<img src={url} alt={label} className="absolute inset-0 w-full h-full object-contain bg-white" />
|
||||||
rows={4}
|
) : (
|
||||||
/>
|
<div className="text-center text-white/30 text-[10.5px] p-3 leading-relaxed">
|
||||||
</div>
|
空 · 点下方<br />「粘贴剪贴板」<br />填入图片
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 参考图选择 */}
|
|
||||||
<section>
|
|
||||||
<div className="flex items-center justify-between mb-2">
|
|
||||||
<div className="text-[12.5px] font-semibold text-white inline-flex items-center gap-1.5">
|
|
||||||
<Sparkle className="h-3.5 w-3.5 text-violet-300" />
|
|
||||||
选用参考图
|
|
||||||
<span className="text-[10px] text-white/40 font-mono">
|
|
||||||
· 选用 {form.reference_ids.length} / 可选 {refOptions.length}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{refOptions.length === 0 ? (
|
|
||||||
<div className="rounded-md border border-dashed border-white/15 p-3 text-[11px] text-white/40">
|
|
||||||
暂无可选参考图 · 到关键帧节点 / 元素提取图 / 分镜头编排节点等处点 ⬆ 上推到这里
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="grid grid-cols-8 gap-1.5">
|
|
||||||
{refOptions.map((r) => {
|
|
||||||
const checked = form.reference_ids.includes(r.ref_id)
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={r.ref_id}
|
|
||||||
onClick={() => {
|
|
||||||
const next = checked
|
|
||||||
? form.reference_ids.filter((x) => x !== r.ref_id)
|
|
||||||
: [...form.reference_ids, r.ref_id]
|
|
||||||
queueSave({ ...form, reference_ids: next })
|
|
||||||
}}
|
|
||||||
title={r.label}
|
|
||||||
className={`relative rounded-md overflow-hidden border-2 transition bg-white ${
|
|
||||||
checked ? "border-emerald-400 ring-2 ring-emerald-400/40" : "border-white/15 hover:border-white/40"
|
|
||||||
}`}
|
|
||||||
style={{ aspectRatio: "1/1" }}
|
|
||||||
>
|
|
||||||
<img src={r.url} alt={r.label} className="absolute inset-0 w-full h-full object-contain" />
|
|
||||||
{checked && (
|
|
||||||
<div className="absolute top-0.5 right-0.5 h-4 w-4 rounded-full bg-emerald-500 text-white inline-flex items-center justify-center">
|
|
||||||
<Check className="h-2.5 w-2.5" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{r.kind === "keyframe" && (
|
|
||||||
<div className="absolute top-0.5 left-0.5 text-[8px] text-white bg-amber-500/85 px-1 py-0.5 rounded font-bold leading-none">KF</div>
|
|
||||||
)}
|
|
||||||
<div className="absolute bottom-0 left-0 right-0 px-1 py-0.5 text-[8px] text-white bg-black/70 truncate leading-tight">
|
|
||||||
{r.label}
|
|
||||||
</div>
|
</div>
|
||||||
</button>
|
)}
|
||||||
)
|
</div>
|
||||||
})}
|
<button
|
||||||
</div>
|
onClick={() => {
|
||||||
)}
|
if (!clipboard) {
|
||||||
</section>
|
toast.error("先在某张图上点 📋 复制")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
queueSave({ ...form, [key]: clipboard })
|
||||||
|
toast.success(`已粘贴到「${label}」`)
|
||||||
|
}}
|
||||||
|
disabled={!clipboard}
|
||||||
|
className={`w-full text-[11.5px] py-1.5 rounded-md inline-flex items-center justify-center gap-1 transition font-medium ${
|
||||||
|
clipboard
|
||||||
|
? "bg-violet-500 hover:bg-violet-400 text-white"
|
||||||
|
: "bg-white/[0.04] text-white/30 cursor-not-allowed"
|
||||||
|
}`}
|
||||||
|
title={clipboard ? `粘贴剪贴板的图:${clipboard.label || ""}` : "剪贴板为空"}
|
||||||
|
>
|
||||||
|
📋 粘贴
|
||||||
|
</button>
|
||||||
|
<div className="mt-1 text-[9.5px] text-white/30 truncate text-center">
|
||||||
|
{ref?.label || placeholder}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 生成按钮(Phase 2 占位) */}
|
{/* 生成按钮(Phase 2 占位) */}
|
||||||
<section>
|
<section>
|
||||||
@@ -296,7 +279,7 @@ export function StoryboardWorkbench({ job, selectedFrames, open, onClose, onJobU
|
|||||||
⚡ 生成此分镜视频片段(Phase 2 待实施)
|
⚡ 生成此分镜视频片段(Phase 2 待实施)
|
||||||
</button>
|
</button>
|
||||||
<div className="mt-2 text-[10.5px] text-white/35 leading-relaxed">
|
<div className="mt-2 text-[10.5px] text-white/35 leading-relaxed">
|
||||||
下一阶段:基于上述字段 + 参考图,调视频生成模型(Seedance / Kling / Veo3)生成该分镜的视频片段
|
下一阶段:基于 4 图槽(主体 / 场景 / 产品 / 动作)+ 时长,调视频生成模型(Seedance / Kling / Veo3)生成该分镜的视频片段
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user