auto-save 2026-05-13 20:12 (~5)
This commit is contained in:
@@ -2275,6 +2275,19 @@
|
||||
"message": "auto-save 2026-05-13 20:01 (~6)",
|
||||
"hash": "3f9075f",
|
||||
"files_changed": 6
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-13T20:07:24+08:00",
|
||||
"type": "commit",
|
||||
"message": "auto-save 2026-05-13 20:07 (~5)",
|
||||
"hash": "52c120c",
|
||||
"files_changed": 5
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-13T12:09:29Z",
|
||||
"type": "session-heartbeat",
|
||||
"message": "Codex 会话活跃 · 最近命令:codex · 1 项未提交变更 · 最近提交:auto-save 2026-05-13 20:07 (~5)",
|
||||
"files_changed": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -15,7 +15,11 @@ import {
|
||||
import { ThemeToggle } from "@/components/theme-toggle"
|
||||
import { StoryboardBar } from "@/components/storyboard-bar"
|
||||
import { StoryboardWorkbench } from "@/components/storyboard-workbench"
|
||||
import { addManualFrame, analyzeJob, createJob, getJob, uploadJob, deleteFrame, deleteGeneratedImage, type Job, type ImageRef } from "@/lib/api"
|
||||
import {
|
||||
addManualFrame, analyzeJob, createJob, getJob, uploadJob, deleteFrame, deleteGeneratedImage,
|
||||
effectiveFrameUrl, resolveImageRefUrl,
|
||||
type Job, type ImageRef, type StoryboardScene, type GeneratedVideoDraft,
|
||||
} from "@/lib/api"
|
||||
import { VideoLightbox } from "@/components/video-lightbox"
|
||||
|
||||
const NODE_TYPES = {
|
||||
@@ -73,6 +77,7 @@ export default function Home() {
|
||||
const [storyboardFrame, setStoryboardFrame] = useState<number | null>(null)
|
||||
const [workbenchOpen, setWorkbenchOpen] = useState(false)
|
||||
const [clipboard, setClipboard] = useState<ImageRef | null>(null)
|
||||
const [videoDrafts, setVideoDrafts] = useState<GeneratedVideoDraft[]>([])
|
||||
const flowRef = useRef<any>(null)
|
||||
|
||||
// 把 setJob(prev=>...) 翻译成 setJobs 里更新当前 active
|
||||
@@ -101,6 +106,7 @@ export default function Home() {
|
||||
const handleSwitchJob = useCallback((id: string) => {
|
||||
setActiveJobId(id)
|
||||
setSelectedFrames(new Set())
|
||||
setVideoDrafts([])
|
||||
}, [])
|
||||
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
||||
|
||||
@@ -211,6 +217,45 @@ export default function Home() {
|
||||
toast.success(`已复制:${ref.label || (ref.kind === "keyframe" ? "关键帧" : "元素")} · 到分镜头编排工作台粘贴`)
|
||||
}, [])
|
||||
|
||||
const handleQuickGenerateVideo = useCallback((frameIdx: number, scene: StoryboardScene) => {
|
||||
if (!job) return
|
||||
const frame = job.frames.find((f) => f.index === frameIdx)
|
||||
if (!frame) return
|
||||
|
||||
const labelOf = (ref?: ImageRef | null, fallback = "未提供") => ref?.label || fallback
|
||||
const posterRef = scene.product_image ?? scene.subject_image ?? scene.scene_image ?? scene.action_image ?? null
|
||||
const posterUrl = posterRef ? resolveImageRefUrl(job.id, posterRef) : effectiveFrameUrl(job.id, frame)
|
||||
const duration = scene.duration && scene.duration > 0 ? scene.duration : 5
|
||||
const prompt = [
|
||||
`Vertical 9:16 short product video for SKG, ${duration.toFixed(1)} seconds.`,
|
||||
"Use the reference materials only for composition, pose, scene mood and motion rhythm; do not copy the original video, text, watermark, logo, or non-SKG product.",
|
||||
`Reference subject: ${labelOf(scene.subject_image, "clean product demonstration subject")}.`,
|
||||
`Reference scene: ${labelOf(scene.scene_image, "clean modern wellness / home / retail scene")}.`,
|
||||
`SKG product reference: ${labelOf(scene.product_image, "SKG product as the hero object")}.`,
|
||||
`Reference action: ${labelOf(scene.action_image, "hands-on product demonstration action")}.`,
|
||||
scene.subject ? `Subject change: ${scene.subject}.` : "Subject change: clean, trustworthy product demo talent or hands, no medical skeleton unless explicitly requested.",
|
||||
scene.product ? `Product replacement: ${scene.product}.` : "Product replacement: make SKG product the visual focus, premium, clean, realistic, clearly visible.",
|
||||
scene.scene ? `Scene adaptation: ${scene.scene}.` : "Scene adaptation: borrow only the useful layout and credibility from the reference, convert it to SKG product context.",
|
||||
scene.action ? `Camera and action: ${scene.action}.` : "Camera and action: slow push-in, product reveal, close-up detail, natural hand interaction, stable commercial lighting.",
|
||||
"High quality realistic commercial video, clean background, no captions, no platform UI, no TikTok watermark, no extra text.",
|
||||
].join("\n")
|
||||
|
||||
const draft: GeneratedVideoDraft = {
|
||||
id: `quick-${frameIdx}-${Date.now().toString(36)}`,
|
||||
frame_idx: frameIdx,
|
||||
label: `分镜 ${frameIdx + 1} · 快速视频`,
|
||||
prompt,
|
||||
provider: "Quick Prompt",
|
||||
poster_url: posterUrl,
|
||||
duration,
|
||||
created_at: Date.now(),
|
||||
status: "ready",
|
||||
}
|
||||
setVideoDrafts((prev) => [draft, ...prev.filter((x) => x.id !== draft.id)].slice(0, 8))
|
||||
void navigator.clipboard?.writeText(prompt).catch(() => {})
|
||||
toast.success("已生成视频 prompt · 已显示到 Video Gen 节点")
|
||||
}, [job])
|
||||
|
||||
// URL ?job=xxx,yyy 自动恢复多个 job
|
||||
useEffect(() => {
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
@@ -287,6 +332,7 @@ export default function Home() {
|
||||
expandedFrame,
|
||||
framePanelScale,
|
||||
framePanelPinned,
|
||||
videoDrafts,
|
||||
onSubmitUrl: handleSubmit,
|
||||
onUploadFile: handleUpload,
|
||||
onAnalyze: handleAnalyze,
|
||||
@@ -308,7 +354,7 @@ export default function Home() {
|
||||
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, videoDrafts, handleSubmit, handleUpload, handleAnalyze, handleToggleFrame, handleOpenFramePanel, handleFramePanelScaleChange, handleAddManualFrame, handleSwitchJob, setJob, handleDeleteFrame, handleDeleteGenerated, handleCopyImage])
|
||||
|
||||
// 用 useNodesState 让 ReactFlow 自己管位置(避免轮询时重置 drag)
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState<Node>(
|
||||
@@ -429,6 +475,7 @@ export default function Home() {
|
||||
onJobUpdate={setJob as any}
|
||||
clipboard={clipboard}
|
||||
focusedFrame={storyboardFrame}
|
||||
onGenerateVideo={handleQuickGenerateVideo}
|
||||
/>
|
||||
</div>
|
||||
<div className="relative flex-1 min-h-0">
|
||||
|
||||
@@ -7,7 +7,10 @@ import {
|
||||
Mic, Languages, FileEdit, Film, FileVideo, Loader2, Plus, X, LayoutGrid, Pin, Maximize2,
|
||||
} from "lucide-react"
|
||||
import { NodeShell, type NodeStatus, type NodeKind } from "./node-shell"
|
||||
import { type Job, type ImageRef, effectiveFrameUrl, videoUrl, hasCutout, representativeCutoutUrl } from "@/lib/api"
|
||||
import {
|
||||
type Job, type ImageRef, type GeneratedVideoDraft,
|
||||
effectiveFrameUrl, videoUrl, hasCutout, representativeCutoutUrl,
|
||||
} from "@/lib/api"
|
||||
import { FrameLightbox } from "@/components/lightbox"
|
||||
|
||||
export interface NodeData {
|
||||
@@ -20,6 +23,7 @@ export interface NodeData {
|
||||
expandedFrame: number | null
|
||||
framePanelScale?: number
|
||||
framePanelPinned?: boolean
|
||||
videoDrafts?: GeneratedVideoDraft[]
|
||||
onSubmitUrl: (url: string) => void
|
||||
onUploadFile: (file: File) => void
|
||||
onAnalyze: () => void
|
||||
|
||||
@@ -15,13 +15,14 @@ interface Props {
|
||||
onJobUpdate?: (j: Job) => void
|
||||
clipboard: ImageRef | null // 全局剪贴板(page.tsx 提供)
|
||||
focusedFrame: number | null
|
||||
onGenerateVideo?: (frameIdx: number, scene: StoryboardScene) => void
|
||||
}
|
||||
|
||||
const emptyScene = (): StoryboardScene => ({
|
||||
subject: "", product: "", scene: "", action: "", duration: 0, reference_ids: [],
|
||||
})
|
||||
|
||||
export function StoryboardWorkbench({ job, selectedFrames, open, onClose, onJobUpdate, clipboard, focusedFrame }: Props) {
|
||||
export function StoryboardWorkbench({ job, selectedFrames, open, onClose, onJobUpdate, clipboard, focusedFrame, onGenerateVideo }: Props) {
|
||||
const [mounted, setMounted] = useState(false)
|
||||
useEffect(() => setMounted(true), [])
|
||||
|
||||
@@ -113,6 +114,8 @@ export function StoryboardWorkbench({ job, selectedFrames, open, onClose, onJobU
|
||||
window.addEventListener("pointerup", onUp)
|
||||
}
|
||||
|
||||
const hasVideoRefs = !!(form.subject_image || form.scene_image || form.product_image || form.action_image)
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative z-20 flex-shrink-0 border-t border-white/5 border-b border-white/10 bg-black/70 backdrop-blur-xl shadow-2xl"
|
||||
@@ -268,18 +271,23 @@ export function StoryboardWorkbench({ job, selectedFrames, open, onClose, onJobU
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 生成按钮(Phase 2 占位) */}
|
||||
{/* 快速生成:先产出视频 prompt / 任务卡,结果显示到 Video Gen 节点 */}
|
||||
<section>
|
||||
<button
|
||||
disabled
|
||||
className="w-full py-3 rounded-lg text-[13.5px] font-semibold inline-flex items-center justify-center gap-2 bg-gradient-to-r from-rose-500/35 to-violet-500/35 text-white/70 border border-violet-300/30 disabled:opacity-60 cursor-not-allowed"
|
||||
title="Phase 2 实施"
|
||||
disabled={!hasVideoRefs || focusedIdx === null}
|
||||
onClick={() => {
|
||||
if (focusedIdx === null) return
|
||||
queueSave(form)
|
||||
onGenerateVideo?.(focusedIdx, form)
|
||||
}}
|
||||
className="w-full py-3 rounded-lg text-[13.5px] font-semibold inline-flex items-center justify-center gap-2 bg-gradient-to-r from-rose-500 to-violet-500 text-white border border-violet-300/40 shadow-lg shadow-violet-500/20 hover:from-rose-400 hover:to-violet-400 disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
title={hasVideoRefs ? "根据当前 4 图槽和改造目标生成视频 prompt,并推送到 Video Gen 节点" : "先粘贴至少一张参考图"}
|
||||
>
|
||||
<Wand2 className="h-4 w-4" />
|
||||
生成此分镜首帧 / 视频片段(Phase 2 待实施)
|
||||
快速生成视频
|
||||
</button>
|
||||
<div className="mt-2 text-[10.5px] text-white/35 leading-relaxed">
|
||||
下一阶段:基于 4 图槽 + 改造目标 + 时长,先生成符合 SKG 产品的首帧,再调 Seedance / Kling / Veo3 生成视频片段
|
||||
当前先生成可交付的视频 prompt / 任务卡并显示到 Video Gen 节点;后续接 Seedance / Kling / Veo 3 时直接替换为真实视频输出。
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -69,6 +69,18 @@ export interface StoryboardScene {
|
||||
reference_ids?: string[]
|
||||
}
|
||||
|
||||
export interface GeneratedVideoDraft {
|
||||
id: string
|
||||
frame_idx: number
|
||||
label: string
|
||||
prompt: string
|
||||
provider: "Quick Prompt" | "Seedance" | "Kling" | "Veo 3"
|
||||
poster_url: string
|
||||
duration: number
|
||||
created_at: number
|
||||
status: "ready" | "queued" | "failed"
|
||||
}
|
||||
|
||||
// 把 ImageRef 解析成可显示的 src URL
|
||||
export function resolveImageRefUrl(jobId: string, ref: ImageRef): string {
|
||||
if (ref.kind === "keyframe") {
|
||||
|
||||
Reference in New Issue
Block a user