auto-save 2026-05-14 09:24 (~2)

This commit is contained in:
2026-05-14 09:25:05 +08:00
parent 1ac55e5cac
commit 521c142743
2 changed files with 254 additions and 153 deletions

View File

@@ -1,19 +1,5 @@
{
"entries": [
{
"files_changed": 1,
"hash": "8e3b365",
"message": "auto-save 2026-05-12 22:36 (~1)",
"ts": "2026-05-12T22:36:36+08:00",
"type": "commit"
},
{
"files_changed": 1,
"hash": "2009c43",
"message": "auto-save 2026-05-12 22:41 (~1)",
"ts": "2026-05-12T22:42:07+08:00",
"type": "commit"
},
{
"files_changed": 1,
"hash": "9279e55",
@@ -3331,6 +3317,19 @@
"type": "session-heartbeat",
"message": "Codex 会话活跃 · 最近命令codex · 1 项未提交变更 · 最近提交auto-save 2026-05-14 09:13 (~1)",
"files_changed": 1
},
{
"ts": "2026-05-14T09:19:35+08:00",
"type": "commit",
"message": "auto-save 2026-05-14 09:19 (~2)",
"hash": "1ac55e5",
"files_changed": 2
},
{
"ts": "2026-05-14T01:20:31Z",
"type": "session-heartbeat",
"message": "Claude 会话活跃 · 最近命令claude · 1 项未提交变更 · 最近提交auto-save 2026-05-14 09:19 (~2)",
"files_changed": 1
}
]
}

View File

@@ -696,7 +696,7 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
} : {
top: 80,
right: 16,
width: 740,
width: isProductTab ? "min(1280px, calc(100vw - 32px))" : 740,
maxHeight: "calc(100vh - 96px)",
boxShadow: "0 40px 100px -20px rgba(0,0,0,0.7), 0 0 0 1px rgba(255,255,255,0.05)",
animation: "drawer-in 0.24s cubic-bezier(0.32, 0.72, 0, 1)",
@@ -930,118 +930,237 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
</span>
</div>
<div className="mb-2 rounded-md border border-white/10 bg-black/25 px-2 py-1.5 text-[10px] leading-relaxed text-white/50">
6
</div>
<div className="mb-3 space-y-1.5">
<div className="space-y-2">
{fusionShots.map((shot, i) => {
const active = i === activeFusionShot
const productUrl = shot.product_image ? resolveImageRefUrl(jobId, shot.product_image) : ""
const personUrl = shot.person_image ? resolveImageRefUrl(jobId, shot.person_image) : ""
const sceneUrl = shot.scene_image ? resolveImageRefUrl(jobId, shot.scene_image) : ""
const ready = !!(shot.product_image && shot.person_image && shot.product_region && shot.scene_image && shot.action_text?.trim())
const busy = fusionGenerating === i || fusionGenerating === "all"
const pasteIntoSlot = (slot: "product_image" | "person_image" | "scene_image", label: string) => {
setActiveFusionShot(i)
if (clipboard) {
assignFusionImage(slot, clipboard, i)
toast.success(`已粘贴到镜头 ${i + 1}${label}」:${clipboard.label || "剪贴板图片"}`)
return
}
setFusionUploadTarget(slot)
toast.message(`镜头 ${i + 1} 已选中「${label}」槽位,现在可 Cmd+V 粘贴系统图片`)
}
const imageSlot = (slot: "product_image" | "scene_image", label: string, url: string) => {
const ref = shot[slot]
return (
<div className="overflow-hidden rounded-md border border-white/10 bg-black/24">
<div className="relative aspect-square bg-white">
{url ? (
<button
type="button"
onClick={() => setActiveFusionShot(i)}
className="absolute inset-0 cursor-pointer"
title={`选中镜头 ${i + 1}`}
>
<img src={url} alt={label} className="h-full w-full object-contain" draggable={false} />
</button>
) : (
<button
type="button"
onClick={() => openFusionUpload(slot, i)}
className="absolute inset-0 flex flex-col items-center justify-center gap-1 text-[9.5px] text-black/35 hover:text-black/65"
>
<Upload className="h-3.5 w-3.5" />
</button>
)}
</div>
<div className="border-t border-white/10 px-1 py-1">
<div className="mb-1 truncate text-[8.5px] text-white/42">{ref?.label || label}</div>
<div className="grid grid-cols-2 gap-1">
<button
type="button"
onClick={() => pasteIntoSlot(slot, label)}
className={`rounded px-1 py-0.5 text-[8.5px] transition ${
clipboard ? "bg-violet-500/60 text-white hover:bg-violet-400/70" : "bg-white/10 text-white/58 hover:bg-white/18 hover:text-white"
}`}
>
</button>
<button
type="button"
onClick={() => openFusionUpload(slot, i)}
className="rounded bg-white/10 px-1 py-0.5 text-[8.5px] text-white/65 transition hover:bg-white/18 hover:text-white"
>
</button>
</div>
</div>
</div>
)
}
return (
<button
<div
key={shot.id}
type="button"
onClick={() => setActiveFusionShot(i)}
className={`grid w-full grid-cols-[40px_1fr_1fr_54px] items-center gap-1.5 rounded-md border px-2 py-1.5 text-left transition ${
className={`rounded-lg border p-2 transition ${
active
? "border-amber-300/70 bg-amber-500/18 text-white"
: "border-white/10 bg-black/22 text-white/55 hover:border-amber-300/35 hover:text-white"
? "border-amber-300/70 bg-amber-500/16 shadow-[0_0_0_1px_rgba(251,191,36,0.14)]"
: "border-white/10 bg-black/20 hover:border-amber-300/35"
}`}
>
<span className="font-mono text-[10px]">#{i + 1}</span>
<span className="truncate text-[10px]">{shot.product_image?.label || "产品图空"}</span>
<span className="truncate text-[10px]">{shot.scene_image?.label || "场景图空"}</span>
<span className={`rounded px-1 py-0.5 text-center text-[9px] ${ready ? "bg-emerald-400/80 text-black" : "bg-white/10 text-white/45"}`}>
{ready ? "就绪" : "待补"}
</span>
</button>
)
})}
</div>
<div className="grid grid-cols-3 gap-2">
{([
["product_image", "产品图", currentFusionProductUrl],
["person_image", "白底人物图", currentFusionPersonUrl],
["scene_image", "场景图", currentFusionSceneUrl],
] as const).map(([slot, label, url]) => (
<div key={slot} className="overflow-hidden rounded-md border border-white/10 bg-black/25">
<div className="relative bg-white" style={{ aspectRatio: "1/1" }}>
{url ? (
<img src={url} alt={label} className="absolute inset-0 h-full w-full object-contain" />
) : (
<button
type="button"
onClick={() => openFusionUpload(slot)}
className="absolute inset-0 flex flex-col items-center justify-center gap-1 text-[10px] text-black/35 hover:text-black/65"
>
<Upload className="h-4 w-4" />
/
</button>
)}
</div>
<div className="flex items-center justify-between gap-1 border-t border-white/10 px-1.5 py-1">
<span className="truncate text-[9.5px] text-white/55">{label}</span>
<div className="flex items-center gap-1">
<button
type="button"
onClick={() => {
if (clipboard) {
assignFusionImage(slot, clipboard)
toast.success(`已粘贴到「${label}」:${clipboard.label || "剪贴板图片"}`)
return
}
setFusionUploadTarget(slot)
toast.message(`应用剪贴板为空;已选中「${label}」槽位,现在可 Cmd+V 粘贴系统图片`)
}}
className={`rounded px-1.5 py-0.5 text-[9px] hover:bg-white/18 hover:text-white ${
clipboard ? "bg-violet-500/60 text-white" : "bg-white/10 text-white/60"
}`}
title={clipboard ? `粘贴应用剪贴板:${clipboard.label || "图片"}` : "应用剪贴板为空;可先点槽位后 Cmd+V 粘贴系统图片"}
>
</button>
<button
type="button"
onClick={() => openFusionUpload(slot)}
className="rounded bg-white/10 px-1.5 py-0.5 text-[9px] text-white/70 hover:bg-white/18 hover:text-white"
>
</button>
<div className="grid grid-cols-[34px_82px_112px_82px_minmax(150px,1fr)_78px] items-start gap-2">
<div className="flex flex-col items-center gap-1 pt-1">
<button
type="button"
onClick={() => setActiveFusionShot(i)}
className={`h-8 w-8 rounded-md border text-[10px] font-mono transition ${
active
? "border-amber-200/70 bg-amber-300/20 text-amber-50"
: "border-white/10 bg-white/7 text-white/48 hover:border-amber-300/35 hover:text-white"
}`}
title={`切到镜头 ${i + 1}`}
>
{String(i + 1).padStart(2, "0")}
</button>
<span className={`rounded px-1 py-0.5 text-[8.5px] ${ready ? "bg-emerald-400/80 text-black" : "bg-white/10 text-white/45"}`}>
{ready ? "就绪" : "待补"}
</span>
</div>
{imageSlot("product_image", "产品图", productUrl)}
<div className="overflow-hidden rounded-md border border-white/10 bg-black/24">
<div
ref={active ? fusionPersonWrapRef : undefined}
onMouseDown={personUrl ? (ev) => {
setActiveFusionShot(i)
if (active) onFusionRegionDown(ev)
} : undefined}
onMouseMove={active ? onFusionRegionMove : undefined}
onMouseUp={active ? onFusionRegionUp : undefined}
onMouseLeave={active ? onFusionRegionUp : undefined}
className={`relative aspect-[4/5] bg-white ${
personUrl ? (active ? "cursor-crosshair" : "cursor-pointer") : ""
}`}
title={personUrl ? (active ? "拖动画出产品融合区域" : "点击后编辑此镜头区域") : "上传白底人物图"}
>
{personUrl ? (
<>
<img src={personUrl} alt="白底人物图" className="h-full w-full select-none object-contain" draggable={false} />
{!active && (
<div className="absolute inset-x-1 bottom-1 rounded bg-black/55 px-1 py-0.5 text-center text-[8px] text-white/75">
</div>
)}
</>
) : (
<button
type="button"
onClick={() => openFusionUpload("person_image", i)}
className="absolute inset-0 flex flex-col items-center justify-center gap-1 text-[9.5px] text-black/35 hover:text-black/65"
>
<Upload className="h-3.5 w-3.5" />
</button>
)}
{[shot.product_region, active ? fusionDraftRegion : null].filter(Boolean).map((region, regionIdx) => region && (
<div
key={regionIdx}
className={`absolute pointer-events-none border-2 ${regionIdx === 0 ? "border-amber-300 bg-amber-300/10" : "border-dashed border-cyan-300"}`}
style={{
left: `${region.x * 100}%`,
top: `${region.y * 100}%`,
width: `${region.w * 100}%`,
height: `${region.h * 100}%`,
}}
/>
))}
</div>
<div className="border-t border-white/10 px-1 py-1">
<div className="mb-1 truncate text-[8.5px] text-white/42">
{shot.person_image?.label || (shot.product_region ? "人物图 · 已画区域" : "白底人物图")}
</div>
<div className="grid grid-cols-2 gap-1">
<button
type="button"
onClick={() => pasteIntoSlot("person_image", "白底人物图")}
className={`rounded px-1 py-0.5 text-[8.5px] transition ${
clipboard ? "bg-violet-500/60 text-white hover:bg-violet-400/70" : "bg-white/10 text-white/58 hover:bg-white/18 hover:text-white"
}`}
>
</button>
<button
type="button"
onClick={() => openFusionUpload("person_image", i)}
className="rounded bg-white/10 px-1 py-0.5 text-[8.5px] text-white/65 transition hover:bg-white/18 hover:text-white"
>
</button>
</div>
</div>
</div>
{imageSlot("scene_image", "场景图", sceneUrl)}
<label className="block">
<div className="mb-1 flex items-center justify-between gap-2">
<span className="text-[9px] text-white/38"> · </span>
<span className="text-[8.5px] text-white/30">#{i + 1}</span>
</div>
<textarea
value={shot.action_text ?? ""}
onFocus={() => setActiveFusionShot(i)}
onChange={(e) => updateFusionShot(i, { action_text: e.target.value })}
onBlur={(e) => {
const next = fusionShots.map((item, idx) => (idx === i ? { ...item, action_text: e.currentTarget.value } : item))
void persistFusionShots(next)
}}
placeholder="描述这个镜头里人物和产品的动作、位置、节奏。"
className="h-[92px] w-full resize-none rounded-md border border-white/10 bg-black/35 px-2 py-1.5 text-[10px] leading-relaxed text-white/75 outline-none placeholder:text-white/25 focus:border-amber-300/45"
/>
</label>
<div className="flex flex-col gap-1.5">
<select
value={shot.duration ?? 5}
onFocus={() => setActiveFusionShot(i)}
onChange={(e) => updateFusionShot(i, { duration: Number(e.target.value) }, true)}
className="h-7 w-full rounded-md border border-white/10 bg-black/35 px-1.5 text-[10px] text-white/75 outline-none focus:border-amber-300/45"
title="视频秒数"
>
{FUSION_DURATIONS.map((seconds) => (
<option key={seconds} value={seconds}>{seconds}s</option>
))}
</select>
<button
type="button"
onClick={() => {
setActiveFusionShot(i)
void runFusionVideo(i)
}}
disabled={!!fusionGenerating || !onGenerateProductFusionVideo}
className="inline-flex h-9 items-center justify-center gap-1 rounded-md bg-amber-500/75 px-1.5 text-[10px] font-medium text-white transition hover:bg-amber-400 disabled:cursor-not-allowed disabled:opacity-40"
>
{busy ? <Loader2 className="h-3 w-3 animate-spin" /> : <Play className="h-3 w-3" />}
</button>
<button
type="button"
onClick={() => setActiveFusionShot(i)}
className={`h-7 rounded-md border px-1.5 text-[9px] transition ${
active
? "border-amber-300/55 bg-amber-500/18 text-amber-50"
: "border-white/10 bg-white/7 text-white/50 hover:border-amber-300/35 hover:text-white"
}`}
>
{active ? "当前" : "编辑"}
</button>
</div>
</div>
</div>
</div>
))}
</div>
<div className="mt-3 rounded-lg border border-white/10 bg-black/30 p-2">
<div className="mb-2 flex items-center justify-between gap-2">
<div className="text-[11px] font-semibold text-white"> · </div>
<span className="text-[9px] text-white/35"></span>
</div>
<div
ref={fusionPersonWrapRef}
onMouseDown={onFusionRegionDown}
onMouseMove={onFusionRegionMove}
onMouseUp={onFusionRegionUp}
onMouseLeave={onFusionRegionUp}
className={`relative overflow-hidden rounded-md border border-white/10 bg-white ${currentFusionPersonUrl ? "cursor-crosshair" : ""}`}
>
{currentFusionPersonUrl ? (
<img src={currentFusionPersonUrl} alt="fusion person" className="block w-full select-none object-contain" draggable={false} />
) : (
<div className="flex h-64 items-center justify-center text-[11px] text-black/35"></div>
)}
{[currentFusionShot?.product_region, fusionDraftRegion].filter(Boolean).map((region, i) => region && (
<div
key={i}
className={`absolute pointer-events-none border-2 ${i === 0 ? "border-amber-300 bg-amber-300/10" : "border-dashed border-cyan-300"}`}
style={{
left: `${region.x * 100}%`,
top: `${region.y * 100}%`,
width: `${region.w * 100}%`,
height: `${region.h * 100}%`,
}}
/>
))}
</div>
)
})}
</div>
</section>
) : (
@@ -1412,7 +1531,7 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
<>
<section className="rounded-lg border border-amber-300/18 bg-amber-500/[0.08] p-2.5 text-[10.5px] leading-relaxed text-white/58">
<div className="mb-2 flex items-center justify-between gap-2">
<div className="text-[12px] font-semibold text-white"> {activeFusionShot + 1} </div>
<div className="text-[12px] font-semibold text-white"></div>
<span className="text-[9px] text-white/40">{fusionSaving ? "保存中" : "自动保存"}</span>
</div>
<div className="mb-2 grid grid-cols-2 gap-1.5">
@@ -1425,46 +1544,29 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
<div className="text-white/80">Seedance</div>
</div>
</div>
<label className="mb-2 block">
<span className="mb-1 block text-[9px] text-white/35"></span>
<select
value={currentFusionShot?.duration ?? 5}
onChange={(e) => updateFusionShot(activeFusionShot, { duration: Number(e.target.value) }, true)}
className="h-8 w-full rounded-md border border-white/10 bg-black/35 px-2 text-[11px] text-white/75 outline-none"
>
{FUSION_DURATIONS.map((seconds) => (
<option key={seconds} value={seconds}>{seconds} </option>
))}
</select>
</label>
<label className="mb-2 block">
<div className="mb-2 rounded-md border border-white/10 bg-black/25 px-2 py-1.5">
<div className="mb-1 flex items-center justify-between gap-2">
<span className="text-[9px] text-white/35"> · </span>
<button
type="button"
onClick={draftFusionDescriptions}
className="rounded bg-white/10 px-1.5 py-0.5 text-[9.5px] text-white/60 hover:bg-white/18 hover:text-white"
>
AI 6
</button>
<span className="font-mono text-[10px] text-amber-100"> {activeFusionShot + 1}</span>
<span className="text-[9px] text-white/38">{currentFusionShot?.duration ?? 5}s</span>
</div>
<textarea
value={currentFusionShot?.action_text ?? ""}
onChange={(e) => updateFusionShot(activeFusionShot, { action_text: e.target.value })}
onBlur={() => void persistFusionShots(fusionShots)}
placeholder="例如:人物把 SKG 颈部按摩仪戴到脖子上,手部轻轻调整两侧机身,场景是办公桌前。"
className="h-24 w-full resize-none rounded-md border border-white/10 bg-black/35 px-2 py-1.5 text-[10px] leading-relaxed text-white/75 outline-none placeholder:text-white/25"
/>
</label>
<div className="grid grid-cols-2 gap-x-2 gap-y-1 text-[9.5px]">
<span className={currentFusionShot?.product_image ? "text-emerald-200/80" : "text-white/35"}></span>
<span className={currentFusionShot?.person_image ? "text-emerald-200/80" : "text-white/35"}></span>
<span className={currentFusionShot?.product_region ? "text-emerald-200/80" : "text-white/35"}></span>
<span className={currentFusionShot?.scene_image ? "text-emerald-200/80" : "text-white/35"}></span>
</div>
<div className={`mt-1 truncate text-[9.5px] ${currentFusionShot?.action_text?.trim() ? "text-white/58" : "text-white/32"}`}>
{currentFusionShot?.action_text?.trim() || "描述词未填写"}
</div>
</div>
<div className="grid grid-cols-2 gap-1.5">
<button
type="button"
onClick={() => void runFusionVideo(activeFusionShot)}
disabled={!!fusionGenerating || !onGenerateProductFusionVideo}
className="rounded-md bg-amber-500/75 px-2 py-1.5 text-[11px] font-medium text-white transition hover:bg-amber-400 disabled:cursor-not-allowed disabled:opacity-40 inline-flex items-center justify-center gap-1"
onClick={draftFusionDescriptions}
className="rounded-md bg-white/10 px-2 py-1.5 text-[11px] font-medium text-white/75 transition hover:bg-white/18 hover:text-white inline-flex items-center justify-center gap-1"
>
{fusionGenerating === activeFusionShot ? <Loader2 className="h-3 w-3 animate-spin" /> : <Play className="h-3 w-3" />}
<Sparkles className="h-3 w-3" />
AI 6
</button>
<button
type="button"
@@ -1481,8 +1583,8 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
jobId={jobId}
compact
buttonLabel="选用"
title="当前镜头产品图"
onPick={(ref) => assignFusionImage("product_image", ref)}
title={`镜头 ${activeFusionShot + 1} 产品图`}
onPick={(ref) => assignFusionImage("product_image", ref, activeFusionShot)}
/>
</>
)}