From 0b6a4639437355f62896a0fa147bce9c29221ade Mon Sep 17 00:00:00 2001 From: kang Date: Wed, 13 May 2026 20:12:54 +0800 Subject: [PATCH] auto-save 2026-05-13 20:12 (~5) --- .memory/worklog.json | 13 +++++++ web/app/page.tsx | 51 ++++++++++++++++++++++++- web/components/nodes/index.tsx | 6 ++- web/components/storyboard-workbench.tsx | 22 +++++++---- web/lib/api.ts | 12 ++++++ 5 files changed, 94 insertions(+), 10 deletions(-) diff --git a/.memory/worklog.json b/.memory/worklog.json index 8025b90..203133f 100644 --- a/.memory/worklog.json +++ b/.memory/worklog.json @@ -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 } ] } diff --git a/web/app/page.tsx b/web/app/page.tsx index 7bd7884..b444755 100644 --- a/web/app/page.tsx +++ b/web/app/page.tsx @@ -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(null) const [workbenchOpen, setWorkbenchOpen] = useState(false) const [clipboard, setClipboard] = useState(null) + const [videoDrafts, setVideoDrafts] = useState([]) const flowRef = useRef(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 | 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( @@ -429,6 +475,7 @@ export default function Home() { onJobUpdate={setJob as any} clipboard={clipboard} focusedFrame={storyboardFrame} + onGenerateVideo={handleQuickGenerateVideo} />
diff --git a/web/components/nodes/index.tsx b/web/components/nodes/index.tsx index 196b7ce..dbe5f9b 100644 --- a/web/components/nodes/index.tsx +++ b/web/components/nodes/index.tsx @@ -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 diff --git a/web/components/storyboard-workbench.tsx b/web/components/storyboard-workbench.tsx index ce890fa..44644d0 100644 --- a/web/components/storyboard-workbench.tsx +++ b/web/components/storyboard-workbench.tsx @@ -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 (
- {/* 生成按钮(Phase 2 占位) */} + {/* 快速生成:先产出视频 prompt / 任务卡,结果显示到 Video Gen 节点 */}
- 下一阶段:基于 4 图槽 + 改造目标 + 时长,先生成符合 SKG 产品的首帧,再调 Seedance / Kling / Veo3 生成视频片段 + 当前先生成可交付的视频 prompt / 任务卡并显示到 Video Gen 节点;后续接 Seedance / Kling / Veo 3 时直接替换为真实视频输出。
diff --git a/web/lib/api.ts b/web/lib/api.ts index b4fa832..3367b58 100644 --- a/web/lib/api.ts +++ b/web/lib/api.ts @@ -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") {