diff --git a/.memory/worklog.json b/.memory/worklog.json index b253c11..c218ee1 100644 --- a/.memory/worklog.json +++ b/.memory/worklog.json @@ -2605,6 +2605,13 @@ "type": "session-heartbeat", "message": "Claude 会话活跃 · 最近命令:claude · 1 项未提交变更 · 最近提交:auto-save 2026-05-13 23:51 (~1)", "files_changed": 1 + }, + { + "ts": "2026-05-13T23:57:28+08:00", + "type": "commit", + "message": "auto-save 2026-05-13 23:57 (~2)", + "hash": "12daaa2", + "files_changed": 2 } ] } diff --git a/.playwright-mcp/page-2026-05-13T15-59-43-994Z.yml b/.playwright-mcp/page-2026-05-13T15-59-43-994Z.yml new file mode 100644 index 0000000..26f5863 --- /dev/null +++ b/.playwright-mcp/page-2026-05-13T15-59-43-994Z.yml @@ -0,0 +1,120 @@ +- generic [active] [ref=e1]: + - main [ref=e3]: + - button "切到明亮主题" [ref=e5]: + - img [ref=e6] + - application [ref=e14]: + - generic [ref=e16]: + - generic: + - generic: + - img: + - group "Edge from input to keyframe" [ref=e17] [cursor=pointer] + - img: + - group "Edge from input to asr" [ref=e20] [cursor=pointer] + - img: + - group "Edge from asr to translate" + - img: + - group "Edge from translate to rewrite" [ref=e23] [cursor=pointer] + - img: + - group "Edge from keyframe to storyboard" [ref=e26] [cursor=pointer] + - img: + - group "Edge from rewrite to storyboard" [ref=e29] [cursor=pointer] + - img: + - group "Edge from storyboard to videogen" [ref=e32] [cursor=pointer] + - img: + - group "Edge from videogen to compose" [ref=e35] [cursor=pointer] + - img: + - group "Edge from rewrite to compose" [ref=e38] [cursor=pointer] + - generic: + - group [ref=e41]: + - generic [ref=e43]: + - generic [ref=e44]: + - img [ref=e46] + - generic [ref=e49]: 输入 · Input + - generic [ref=e52]: + - generic [ref=e53]: STEP 1 · 待运行 + - textbox "粘贴 TikTok 链接" [ref=e54] + - generic [ref=e55]: + - button "提交链接" [disabled] [ref=e56] + - button "上传" [ref=e57]: + - img [ref=e58] + - text: 上传 + - group [ref=e64]: + - generic [ref=e66]: + - generic [ref=e68]: + - img [ref=e70] + - generic [ref=e74]: 镜头拆解 · 元素提取 + - generic [ref=e77]: + - generic [ref=e78]: STEP 2 · 等待抽取 · 待运行 + - generic [ref=e79]: 等待解析(默认 5 张) + - group [ref=e83]: + - generic [ref=e84]: + - generic [ref=e86]: + - img [ref=e88] + - generic [ref=e91]: 声音文案 · ASR + - generic [ref=e94]: + - generic [ref=e95]: STEP 3 · 可选文案轨 · 待运行 + - generic [ref=e96]: Gemini 2.5 · 英文带时间戳分段 + - group [ref=e100]: + - generic [ref=e101]: + - generic [ref=e103]: + - img [ref=e105] + - generic [ref=e109]: 翻译理解 · Translate + - generic [ref=e112]: + - generic [ref=e113]: STEP 4 · EN → ZH · 待运行 + - generic [ref=e114]: 中文翻译 · 段落级 · 实时输出 + - group [ref=e118]: + - generic [ref=e120]: + - generic [ref=e122]: + - img [ref=e124] + - generic [ref=e129]: 元素改造 · Storyboard + - generic [ref=e132]: + - generic [ref=e133]: STEP 6 · 参考元素 → SKG 画面 · 待运行 + - generic [ref=e134]: + - text: 不是复刻原视频:先把参考图里的主体 / 场景 / 动作 / 道具拆出来,再替换成 SKG 产品画面。 + - generic [ref=e135]: 已有 0 个提取元素 · 0 个分镜进入编排 + - button "进入分镜编排" [disabled] [ref=e136] + - group [ref=e140]: + - generic [ref=e141]: + - generic [ref=e143]: + - img [ref=e145] + - generic [ref=e149]: 产品文案 · Rewrite + - generic [ref=e152]: + - generic [ref=e153]: STEP 5 · 接 SKG 卖点 · 待运行 + - textbox "粘贴 SKG 产品信息 / 关键卖点(可作为视频脚本和镜头动作参考)" [disabled] [ref=e154] + - generic [ref=e155]: 下一冲刺接入 + - group [ref=e159]: + - generic [ref=e161]: + - generic [ref=e163]: + - img [ref=e165] + - generic [ref=e167]: 生成视频 · Video Gen + - generic [ref=e170]: + - generic [ref=e171]: STEP 7 · 首帧 + 动作 prompt · 待运行 + - generic [ref=e172]: + - generic [ref=e173]: Seedance + - generic [ref=e174]: Kling + - generic [ref=e175]: Veo 3 + - group [ref=e179]: + - generic [ref=e180]: + - generic [ref=e182]: + - img [ref=e184] + - generic [ref=e188]: 合成成品 · Compose + - generic [ref=e191]: + - generic [ref=e192]: STEP 8 · ffmpeg + 字幕 · 待运行 + - generic [ref=e193]: + - text: 视频片段 + 字幕 / TTS + - text: → 最终 mp4 输出 + - img + - generic "Control Panel" [ref=e196]: + - button "Zoom In" [ref=e197] [cursor=pointer]: + - img [ref=e198] + - button "Zoom Out" [ref=e200] [cursor=pointer]: + - img [ref=e201] + - button "Fit View" [ref=e203] [cursor=pointer]: + - img [ref=e204] + - button "Toggle Interactivity" [ref=e206] [cursor=pointer]: + - img [ref=e207] + - img "Mini Map" [ref=e210] + - region "Notifications alt+T" + - button "Open Next.js Dev Tools" [ref=e225] [cursor=pointer]: + - img [ref=e226] + - alert [ref=e231] \ No newline at end of file diff --git a/.playwright-mcp/page-2026-05-13T15-59-48-897Z.yml b/.playwright-mcp/page-2026-05-13T15-59-48-897Z.yml new file mode 100644 index 0000000..c8f785e --- /dev/null +++ b/.playwright-mcp/page-2026-05-13T15-59-48-897Z.yml @@ -0,0 +1,335 @@ +- generic [active] [ref=e1]: + - main [ref=e3]: + - button "切到明亮主题" [ref=e5]: + - img [ref=e6] + - generic [ref=e12]: + - generic [ref=e234]: + - generic [ref=e235]: + - img [ref=e236] + - generic [ref=e241]: 分镜头编排 + - generic [ref=e242]: 1 分镜 · 3 元素 + - generic [ref=e243]: · 组织分镜画面 → 为生成视频做准备 + - button "展开编排" [ref=e245]: + - img [ref=e246] + - text: 展开编排 + - application [ref=e14]: + - generic [ref=e16]: + - generic: + - generic: + - img: + - group "Edge from input to keyframe" [ref=e17] [cursor=pointer] + - img: + - group "Edge from input to asr" [ref=e20] [cursor=pointer] + - img: + - group "Edge from asr to translate" + - img: + - group "Edge from translate to rewrite" [ref=e23] [cursor=pointer] + - img: + - group "Edge from keyframe to storyboard" [ref=e26] [cursor=pointer] + - img: + - group "Edge from rewrite to storyboard" [ref=e29] [cursor=pointer] + - img: + - group "Edge from storyboard to videogen" [ref=e32] [cursor=pointer] + - img: + - group "Edge from videogen to compose" [ref=e35] [cursor=pointer] + - img: + - group "Edge from rewrite to compose" [ref=e38] [cursor=pointer] + - generic: + - group [ref=e41]: + - generic [ref=e42]: + - generic [ref=e248]: + - button "再上传一个视频" [ref=e249]: + - img [ref=e250] + - button "72.4s" [ref=e251]: + - generic [ref=e253]: 72.4s + - button "64.5s" [ref=e254]: + - generic [ref=e256]: 64.5s + - button "71.4s" [ref=e257]: + - generic [ref=e259]: 71.4s + - button "72.4s" [ref=e260]: + - generic [ref=e262]: 72.4s + - button "64.5s" [ref=e263]: + - generic [ref=e265]: 64.5s + - button "71.4s" [ref=e266]: + - generic [ref=e268]: 71.4s + - button "71.4s" [ref=e269]: + - generic [ref=e271]: 71.4s + - button "71.4s" [ref=e272]: + - generic [ref=e274]: 71.4s + - button "71.4s" [ref=e275]: + - generic [ref=e277]: 71.4s + - button "71.4s" [ref=e278]: + - generic [ref=e280]: 71.4s + - button "8.0s" [ref=e281]: + - generic [ref=e283]: 8.0s + - button "8.0s" [ref=e284]: + - generic [ref=e286]: 8.0s + - button "8.0s" [ref=e287]: + - generic [ref=e289]: 8.0s + - button "8.0s" [ref=e290]: + - generic [ref=e292]: 8.0s + - button "…" [ref=e293]: + - img [ref=e295] + - generic [ref=e297]: … + - button "…" [ref=e298]: + - img [ref=e300] + - generic [ref=e302]: … + - button "…" [ref=e303]: + - img [ref=e305] + - generic [ref=e307]: … + - generic [ref=e43]: + - generic [ref=e44]: + - img [ref=e46] + - generic [ref=e49]: 输入 · Input + - img [ref=e308] + - generic [ref=e52]: + - generic [ref=e53]: STEP 1 · 视频就绪 · 完成 + - textbox "再加一个 TK 链接" [ref=e311] + - generic [ref=e55]: + - button "+ 加链接" [disabled] [ref=e312] + - button "再传一个" [ref=e313]: + - img [ref=e58] + - text: 再传一个 + - generic [ref=e314]: + - generic [ref=e315]: 576×1024 · 72.4s + - generic [ref=e316]: 📎 上传 + - button "重新解析" [ref=e317] + - group [ref=e64]: + - generic [ref=e65]: + - generic [ref=e318]: + - generic [ref=e319]: + - button "frame 6 1.3s" [ref=e320]: + - img "frame 6" [ref=e321] + - generic [ref=e322]: 1.3s + - button "📋" [ref=e323] + - button "删除该关键帧" [ref=e324]: + - img [ref=e325] + - generic: + - generic: + - generic: + - generic: 分镜 7 + - generic: 1.33s + - generic [ref=e328]: + - button "frame 5 3 7.4s" [ref=e329]: + - img "frame 5" [ref=e330] + - generic "3 个元素已抠图" [ref=e332]: "3" + - generic [ref=e333]: 7.4s + - button "📋" [ref=e334] + - button "删除该关键帧" [ref=e335]: + - img [ref=e336] + - generic: + - generic: + - generic: + - generic: 分镜 6 + - generic: 7.39s + - generic [ref=e339]: + - button "frame 0 11.7s" [ref=e340]: + - img "frame 0" [ref=e341] + - generic [ref=e342]: 11.7s + - button "📋" [ref=e343] + - button "删除该关键帧" [ref=e344]: + - img [ref=e345] + - generic: + - generic: + - generic: + - generic: 分镜 1 + - generic: 11.68s + - generic [ref=e348]: + - button "frame 7 18.0s" [ref=e349]: + - img "frame 7" [ref=e350] + - generic [ref=e351]: 18.0s + - button "📋" [ref=e352] + - button "删除该关键帧" [ref=e353]: + - img [ref=e354] + - generic: + - generic: + - generic: + - generic: 分镜 8 + - generic: 18.00s + - generic [ref=e357]: + - button "frame 1 23.4s" [ref=e358]: + - img "frame 1" [ref=e359] + - generic [ref=e360]: 23.4s + - button "📋" [ref=e361] + - button "删除该关键帧" [ref=e362]: + - img [ref=e363] + - generic: + - generic: + - generic: + - generic: 分镜 2 + - generic: 23.37s + - generic [ref=e366]: + - button "frame 2 32.7s" [ref=e367]: + - img "frame 2" [ref=e368] + - generic [ref=e369]: 32.7s + - button "📋" [ref=e370] + - button "删除该关键帧" [ref=e371]: + - img [ref=e372] + - generic: + - generic: + - generic: + - generic: 分镜 3 + - generic: 32.72s + - generic [ref=e375]: + - button "frame 3 49.1s" [ref=e376]: + - img "frame 3" [ref=e377] + - generic [ref=e378]: 49.1s + - button "📋" [ref=e379] + - button "删除该关键帧" [ref=e380]: + - img [ref=e381] + - generic: + - generic: + - generic: + - generic: 分镜 4 + - generic: 49.08s + - generic [ref=e384]: + - button "frame 8 52.8s" [ref=e385]: + - img "frame 8" [ref=e386] + - generic [ref=e387]: 52.8s + - button "📋" [ref=e388] + - button "删除该关键帧" [ref=e389]: + - img [ref=e390] + - generic: + - generic: + - generic: + - generic: 分镜 9 + - generic: 52.80s + - generic [ref=e393]: + - button "frame 9 55.5s" [ref=e394]: + - img "frame 9" [ref=e395] + - generic [ref=e396]: 55.5s + - button "📋" [ref=e397] + - button "删除该关键帧" [ref=e398]: + - img [ref=e399] + - generic: + - generic: + - generic: + - generic: 分镜 10 + - generic: 55.50s + - generic [ref=e402]: + - button "frame 4 65.4s" [ref=e403]: + - img "frame 4" [ref=e404] + - generic [ref=e405]: 65.4s + - button "📋" [ref=e406] + - button "删除该关键帧" [ref=e407]: + - img [ref=e408] + - generic: + - generic: + - generic: + - generic: 分镜 5 + - generic: 65.43s + - generic [ref=e66]: + - generic [ref=e68]: + - img [ref=e70] + - generic [ref=e74]: 镜头拆解 · 元素提取 + - img [ref=e411] + - generic [ref=e77]: + - generic [ref=e78]: STEP 2 · 1/10 入编排 · 完成 + - generic [ref=e79]: + - text: 自动 10 张 · + - generic [ref=e414]: 0 已清洗 + - text: · + - generic [ref=e415]: 3/3 已抠图 + - text: 点缩略图 → 清洗水印 / 提取可借鉴元素 → 改造成 SKG 画面素材 + - group [ref=e83]: + - generic [ref=e84]: + - generic [ref=e86]: + - img [ref=e88] + - generic [ref=e91]: 声音文案 · ASR + - generic [ref=e94]: + - generic [ref=e95]: STEP 3 · 可选文案轨 · 待运行 + - generic [ref=e96]: Gemini 2.5 · 英文带时间戳分段 + - group [ref=e100]: + - generic [ref=e101]: + - generic [ref=e103]: + - img [ref=e105] + - generic [ref=e109]: 翻译理解 · Translate + - generic [ref=e112]: + - generic [ref=e113]: STEP 4 · EN → ZH · 待运行 + - generic [ref=e114]: 中文翻译 · 段落级 · 实时输出 + - group [ref=e118]: + - generic [ref=e119]: + - generic [ref=e416]: + - generic [ref=e417]: + - button "病人骨骼" [ref=e418]: + - img "病人骨骼" [ref=e419] + - button "📋" [ref=e420] + - generic: + - generic: + - generic: + - generic: 分镜 6 + - generic: 7.39s + - generic [ref=e421]: + - button "医生骨骼" [ref=e422]: + - img "医生骨骼" [ref=e423] + - button "📋" [ref=e424] + - generic: + - generic: + - generic: + - generic: 分镜 6 + - generic: 7.39s + - generic [ref=e425]: + - button "检查台" [ref=e426]: + - img "检查台" [ref=e427] + - button "📋" [ref=e428] + - generic: + - generic: + - generic: + - generic: 分镜 6 + - generic: 7.39s + - generic [ref=e120]: + - generic [ref=e122]: + - img [ref=e124] + - generic [ref=e129]: 元素改造 · Storyboard + - img [ref=e429] + - generic [ref=e132]: + - generic [ref=e133]: STEP 6 · 参考元素 → SKG 画面 · 1 分镜 · 完成 + - generic [ref=e134]: + - text: 不是复刻原视频:先把参考图里的主体 / 场景 / 动作 / 道具拆出来,再替换成 SKG 产品画面。 + - generic [ref=e135]: 已有 3 个提取元素 · 1 个分镜进入编排 + - button "进入分镜编排" [ref=e136] + - group [ref=e140]: + - generic [ref=e141]: + - generic [ref=e143]: + - img [ref=e145] + - generic [ref=e149]: 产品文案 · Rewrite + - generic [ref=e152]: + - generic [ref=e153]: STEP 5 · 接 SKG 卖点 · 待运行 + - textbox "粘贴 SKG 产品信息 / 关键卖点(可作为视频脚本和镜头动作参考)" [disabled] [ref=e154] + - generic [ref=e155]: 下一冲刺接入 + - group [ref=e159]: + - generic [ref=e161]: + - generic [ref=e163]: + - img [ref=e165] + - generic [ref=e167]: 生成视频 · Video Gen + - generic [ref=e170]: + - generic [ref=e171]: STEP 7 · 首帧 + 动作 prompt · 待运行 + - generic [ref=e172]: + - generic [ref=e173]: Seedance + - generic [ref=e174]: Kling + - generic [ref=e175]: Veo 3 + - group [ref=e179]: + - generic [ref=e180]: + - generic [ref=e182]: + - img [ref=e184] + - generic [ref=e188]: 合成成品 · Compose + - generic [ref=e191]: + - generic [ref=e192]: STEP 8 · ffmpeg + 字幕 · 待运行 + - generic [ref=e193]: + - text: 视频片段 + 字幕 / TTS + - text: → 最终 mp4 输出 + - img + - generic "Control Panel" [ref=e196]: + - button "Zoom In" [ref=e197] [cursor=pointer]: + - img [ref=e198] + - button "Zoom Out" [ref=e200] [cursor=pointer]: + - img [ref=e201] + - button "Fit View" [ref=e203] [cursor=pointer]: + - img [ref=e204] + - button "Toggle Interactivity" [ref=e206] [cursor=pointer]: + - img [ref=e207] + - img "Mini Map" [ref=e210] + - region "Notifications alt+T" + - button "Open Next.js Dev Tools" [ref=e225] [cursor=pointer]: + - img [ref=e226] + - alert [ref=e231] \ No newline at end of file diff --git a/web/app/page.tsx b/web/app/page.tsx index b82b368..8ae3f58 100644 --- a/web/app/page.tsx +++ b/web/app/page.tsx @@ -39,17 +39,29 @@ const KEYFRAME_PANEL_ID = "keyframe-detail-panel" // 合并 input + download + split 为一个节点 // 分叉:上路 input → keyframe → storyboard → videogen ↘ // 下路 input → asr → translate → rewrite ──────→ storyboard / compose -const LAYOUT: Array<{ id: string; type: keyof typeof NODE_TYPES; x: number; y: number }> = [ - { id: "input", type: "input", x: 40, y: 240 }, - { id: "keyframe", type: "keyframe", x: 460, y: 60 }, - { id: "asr", type: "asr", x: 460, y: 440 }, - { id: "translate", type: "translate", x: 840, y: 440 }, - { id: "storyboard", type: "storyboard", x: 880, y: 60 }, - { id: "rewrite", type: "rewrite", x: 1220, y: 440 }, - { id: "videogen", type: "videogen", x: 1260, y: 60 }, - { id: "compose", type: "compose", x: 1640, y: 240 }, +const LAYOUT: Array<{ id: string; type: keyof typeof NODE_TYPES; x: number; y: number; w: number }> = [ + { id: "input", type: "input", x: 40, y: 240, w: 320 }, + { id: "keyframe", type: "keyframe", x: 460, y: 60, w: 360 }, + { id: "asr", type: "asr", x: 460, y: 440, w: 320 }, + { id: "translate", type: "translate", x: 840, y: 440, w: 320 }, + { id: "storyboard", type: "storyboard", x: 880, y: 60, w: 360 }, + { id: "rewrite", type: "rewrite", x: 1220, y: 440, w: 320 }, + { id: "videogen", type: "videogen", x: 1260, y: 60, w: 280 }, + { id: "compose", type: "compose", x: 1640, y: 240, w: 320 }, ] +const NODE_WIDTHS_KEY = "skg-tk:node-widths:v1" + +function loadNodeWidths(): Record { + if (typeof window === "undefined") return {} + try { + const raw = window.localStorage.getItem(NODE_WIDTHS_KEY) + return raw ? JSON.parse(raw) : {} + } catch { + return {} + } +} + const EDGES_RAW: Array<[string, string]> = [ ["input", "keyframe"], ["input", "asr"], @@ -433,6 +445,7 @@ export default function Home() { }), [job, jobs, activeJobId, submitting, analyzing, selectedFrames, expandedFrame, framePanelScale, framePanelPinned, handleSubmit, handleUpload, handleAnalyze, handleToggleFrame, handleOpenFramePanel, handleFramePanelScaleChange, handleAddManualFrame, handleSwitchJob, setJob, handleDeleteFrame, handleDeleteGenerated, handleDeleteVideo, handleCopyImage]) // 用 useNodesState 让 ReactFlow 自己管位置(避免轮询时重置 drag) + const savedWidths = useMemo(() => loadNodeWidths(), []) const [nodes, setNodes, onNodesChange] = useNodesState( LAYOUT.map((n) => ({ id: n.id, @@ -440,8 +453,21 @@ export default function Home() { position: { x: n.x, y: n.y }, data: nodeData, draggable: true, + width: savedWidths[n.id] ?? n.w, + style: { width: savedWidths[n.id] ?? n.w }, })), ) + + // 持久化每个节点宽度到 localStorage(KeyframePanelNode 自己管尺寸,不写回) + useEffect(() => { + const widths: Record = {} + for (const n of nodes) { + if (n.id === KEYFRAME_PANEL_ID) continue + const w = (n.style?.width ?? n.width) as number | string | undefined + if (typeof w === "number") widths[n.id] = Math.round(w) + } + try { window.localStorage.setItem(NODE_WIDTHS_KEY, JSON.stringify(widths)) } catch {} + }, [nodes]) const [edges, setEdges, onEdgesChange] = useEdgesState( EDGES_RAW.map(([from, to], i) => ({ id: `e${i}`, source: from, target: to, animated: false, type: "default", diff --git a/web/components/nodes/index.tsx b/web/components/nodes/index.tsx index f0959d8..d875237 100644 --- a/web/components/nodes/index.tsx +++ b/web/components/nodes/index.tsx @@ -103,7 +103,7 @@ export function InputNode({ data, selected }: NodeProps<{ data: NodeData }> | an const inputLocked = isDownloading || d.submitting return ( -
+
{/* 多视频缩略图浮条 — 「+」在最左,job 按时间倒序(最新靠左高亮),统一高度 64,宽度按视频原比例,一行横滚 */} {!videoExpanded && d.jobs.length > 0 && (
0 ? `${d.job.width}/${d.job.height}` : "9/16" return ( -
+
{/* 缩略图浮条(节点上方,最多 5 个一行,多行向上扩展) */} {frames.length > 0 && jobId && (
0 ? `${job.width}/${job.height}` : "9/16" return ( -
+
{/* 节点上方:所有元素 crop 图(编排输入素材)· 跟 keyframe 节点样式一致 */} {elementCrops.length > 0 && job && (
+
{videos.length > 0 && (
{hasSource && } +
) } diff --git a/web/components/nodes/resize-handle.tsx b/web/components/nodes/resize-handle.tsx new file mode 100644 index 0000000..1455ad3 --- /dev/null +++ b/web/components/nodes/resize-handle.tsx @@ -0,0 +1,27 @@ +"use client" +import { NodeResizeControl } from "@xyflow/react" + +/** 节点右边缘 resize 把手:高度跟随节点,宽度可拖(240–1200px)。平时透明,hover 显紫色细条。 */ +export function ResizeRight({ minWidth = 240, maxWidth = 1200 }: { minWidth?: number; maxWidth?: number }) { + return ( + +
+ + ) +}