diff --git a/.memory/worklog.json b/.memory/worklog.json index 7f0d72c..4eb7b72 100644 --- a/.memory/worklog.json +++ b/.memory/worklog.json @@ -1,122 +1,5 @@ { "entries": [ - { - "files_changed": 1, - "hash": "e9e2acc", - "message": "auto-save 2026-05-15 09:05 (~1)", - "ts": "2026-05-15T09:06:31+08:00", - "type": "commit" - }, - { - "files_changed": 1, - "message": "Codex 会话活跃 · 最近命令:codex · 1 项未提交变更 · 最近提交:auto-save 2026-05-15 09:05 (~1)", - "ts": "2026-05-15T01:11:06Z", - "type": "session-heartbeat" - }, - { - "files_changed": 1, - "hash": "d808666", - "message": "auto-save 2026-05-15 09:11 (~1)", - "ts": "2026-05-15T09:12:04+08:00", - "type": "commit" - }, - { - "files_changed": 1, - "message": "Codex 会话活跃 · 最近命令:codex · 1 项未提交变更 · 最近提交:auto-save 2026-05-15 09:11 (~1)", - "ts": "2026-05-15T01:13:35Z", - "type": "session-heartbeat" - }, - { - "files_changed": 1, - "hash": "3c93195", - "message": "auto-save 2026-05-15 09:17 (~1)", - "ts": "2026-05-15T09:17:35+08:00", - "type": "commit" - }, - { - "files_changed": 1, - "message": "Codex 会话活跃 · 最近命令:codex · 1 项未提交变更 · 最近提交:auto-save 2026-05-15 09:17 (~1)", - "ts": "2026-05-15T01:21:07Z", - "type": "session-heartbeat" - }, - { - "files_changed": 1, - "hash": "fccc272", - "message": "auto-save 2026-05-15 09:22 (~1)", - "ts": "2026-05-15T09:23:08+08:00", - "type": "commit" - }, - { - "files_changed": 1, - "message": "Codex 会话活跃 · 最近命令:codex · 1 项未提交变更 · 最近提交:auto-save 2026-05-15 09:22 (~1)", - "ts": "2026-05-15T01:23:35Z", - "type": "session-heartbeat" - }, - { - "files_changed": 1, - "hash": "cd700f1", - "message": "auto-save 2026-05-15 09:28 (~1)", - "ts": "2026-05-15T09:28:41+08:00", - "type": "commit" - }, - { - "files_changed": 1, - "message": "Codex 会话活跃 · 最近命令:codex · 1 项未提交变更 · 最近提交:auto-save 2026-05-15 09:28 (~1)", - "ts": "2026-05-15T01:31:07Z", - "type": "session-heartbeat" - }, - { - "files_changed": 1, - "message": "Codex 会话活跃 · 最近命令:codex · 1 项未提交变更 · 最近提交:auto-save 2026-05-15 09:28 (~1)", - "ts": "2026-05-15T01:33:35Z", - "type": "session-heartbeat" - }, - { - "files_changed": 1, - "hash": "055b9d8", - "message": "auto-save 2026-05-15 09:34 (~1)", - "ts": "2026-05-15T09:34:14+08:00", - "type": "commit" - }, - { - "files_changed": 1, - "hash": "cec7e66", - "message": "auto-save 2026-05-15 09:39 (~1)", - "ts": "2026-05-15T09:39:48+08:00", - "type": "commit" - }, - { - "files_changed": 1, - "message": "Codex 会话活跃 · 最近命令:codex · 1 项未提交变更 · 最近提交:auto-save 2026-05-15 09:39 (~1)", - "ts": "2026-05-15T01:41:07Z", - "type": "session-heartbeat" - }, - { - "files_changed": 1, - "message": "Codex 会话活跃 · 最近命令:codex · 1 项未提交变更 · 最近提交:auto-save 2026-05-15 09:39 (~1)", - "ts": "2026-05-15T01:43:35Z", - "type": "session-heartbeat" - }, - { - "files_changed": 1, - "hash": "0759520", - "message": "auto-save 2026-05-15 09:45 (~1)", - "ts": "2026-05-15T09:45:26+08:00", - "type": "commit" - }, - { - "files_changed": 1, - "hash": "ec7eaef", - "message": "auto-save 2026-05-15 09:50 (~1)", - "ts": "2026-05-15T09:50:59+08:00", - "type": "commit" - }, - { - "files_changed": 1, - "message": "Codex 会话活跃 · 最近命令:codex · 1 项未提交变更 · 最近提交:auto-save 2026-05-15 09:50 (~1)", - "ts": "2026-05-15T01:51:07Z", - "type": "session-heartbeat" - }, { "files_changed": 1, "message": "Codex 会话活跃 · 最近命令:codex · 1 项未提交变更 · 最近提交:auto-save 2026-05-15 09:50 (~1)", @@ -3272,6 +3155,123 @@ "message": "refactor: place audio results side by side", "hash": "78d47b8", "files_changed": 3 + }, + { + "ts": "2026-05-17T14:38:19+08:00", + "type": "commit", + "message": "refactor: unify audio result panel", + "hash": "27a6ef0", + "files_changed": 3 + }, + { + "ts": "2026-05-17T06:38:25Z", + "type": "session-heartbeat", + "message": "Codex 会话活跃 · 最近命令:codex · 分支 main · 1 项未提交变更 · 最近提交:refactor: unify audio result panel", + "files_changed": 1 + }, + { + "ts": "2026-05-17T14:44:33+08:00", + "type": "commit", + "message": "auto-save 2026-05-17 14:44 (~2)", + "hash": "c6eb3ae", + "files_changed": 2 + }, + { + "ts": "2026-05-17T06:48:25Z", + "type": "session-heartbeat", + "message": "Codex 会话活跃 · 最近命令:codex · 分支 main · 3 项未提交变更 · 最近提交:auto-save 2026-05-17 14:44 (~2)", + "files_changed": 3 + }, + { + "ts": "2026-05-17T14:49:55+08:00", + "type": "commit", + "message": "auto-save 2026-05-17 14:49 (~3)", + "hash": "38ed5bb", + "files_changed": 3 + }, + { + "ts": "2026-05-17T14:55:16+08:00", + "type": "commit", + "message": "auto-save 2026-05-17 14:55 (~2)", + "hash": "fbfbd59", + "files_changed": 2 + }, + { + "ts": "2026-05-17T14:58:12+08:00", + "type": "commit", + "message": "feat: add synced video waveform timeline", + "hash": "120dacf", + "files_changed": 2 + }, + { + "ts": "2026-05-17T06:58:25Z", + "type": "session-heartbeat", + "message": "Codex 会话活跃 · 最近命令:codex · 分支 main · 1 项未提交变更 · 最近提交:feat: add synced video waveform timeline", + "files_changed": 1 + }, + { + "ts": "2026-05-17T15:05:10+08:00", + "type": "commit", + "message": "fix: show real audio pitch waveform", + "hash": "365053a", + "files_changed": 2 + }, + { + "ts": "2026-05-17T07:08:25Z", + "type": "session-heartbeat", + "message": "Codex 会话活跃 · 最近命令:codex · 分支 main · 1 项未提交变更 · 最近提交:fix: show real audio pitch waveform", + "files_changed": 1 + }, + { + "ts": "2026-05-17T07:18:25Z", + "type": "session-heartbeat", + "message": "Codex 会话活跃 · 最近命令:codex · 分支 main · 3 项未提交变更 · 最近提交:fix: show real audio pitch waveform", + "files_changed": 3 + }, + { + "ts": "2026-05-17T15:21:09+08:00", + "type": "commit", + "message": "fix: render continuous audio waveform", + "hash": "9a95a53", + "files_changed": 2 + }, + { + "ts": "2026-05-17T15:27:06+08:00", + "type": "commit", + "message": "fix: smooth waveform playback cursor", + "hash": "68e7599", + "files_changed": 2 + }, + { + "ts": "2026-05-17T07:28:26Z", + "type": "session-heartbeat", + "message": "Codex 会话活跃 · 最近命令:codex · 分支 main · 1 项未提交变更 · 最近提交:fix: smooth waveform playback cursor", + "files_changed": 1 + }, + { + "ts": "2026-05-17T07:38:26Z", + "type": "session-heartbeat", + "message": "Codex 会话活跃 · 最近命令:codex · 分支 main · 1 项未提交变更 · 最近提交:fix: smooth waveform playback cursor", + "files_changed": 1 + }, + { + "ts": "2026-05-17T15:48:14+08:00", + "type": "commit", + "message": "feat: add audio storyboard planning table", + "hash": "cd135ae", + "files_changed": 2 + }, + { + "ts": "2026-05-17T07:48:26Z", + "type": "session-heartbeat", + "message": "Codex 会话活跃 · 最近命令:codex · 分支 main · 1 项未提交变更 · 最近提交:feat: add audio storyboard planning table", + "files_changed": 1 + }, + { + "ts": "2026-05-17T07:58:26Z", + "type": "session-heartbeat", + "message": "Codex 会话活跃 · 最近命令:codex · 分支 main · 3 项未提交变更 · 最近提交:feat: add audio storyboard planning table", + "files_changed": 3 } ] } diff --git a/docs/source-analysis.html b/docs/source-analysis.html index 97ce276..1d7199a 100644 --- a/docs/source-analysis.html +++ b/docs/source-analysis.html @@ -569,12 +569,13 @@

业务管线

-

当前产品方向已收窄为“信息流广告快速复刻第一步”:主界面左侧是素材输入列,右侧是音频解析工作表。用户粘贴 TK 链接或上传视频后点击“开始”,系统自动下载源视频;下载完成后优先提取原音频文案/字幕,并分析讲话人、语速节奏、背景音乐/环境声/音效。抽帧、分镜规划、产品融入、元素 6 视图和视频合成暂作为后续能力保留,不在当前开始流程里自动触发。

+

当前产品方向已收窄为“信息流广告快速复刻”:主界面左侧是素材输入列,右侧先完成音频解析,再进入信息流复刻分镜工作台。用户粘贴 TK 链接或上传视频后点击“开始”,系统自动下载源视频;下载完成后优先提取原音频文案/字幕,并分析讲话人、语速节奏、背景音乐/环境声/音效。分镜规划按逐句时间轴生成,抽帧和视频生成由用户按单条分镜触发,不在当前开始流程里自动全量运行。

1

导入素材

粘贴 TK / 信息流视频链接或上传本地视频;“开始”只把任务放入第一步队列。

2

下载源视频

后端用 yt-dlp 或本地上传文件落 source.mp4,记录时长、尺寸和视频只读地址。

3

解析音频

source.mp4 提取 audio.wav,ASR 提取原文案,翻译成中文,并写入逐句时间轴。

4

声音分析

用音频模型分析讲话人、口播节奏、停顿、背景音乐/环境声/音效;不默认改写配音或生成视频。

+
5

分镜生成

按逐句时间轴生成竖向分镜行,单行内从左到右承接原内容、新口播、画面规划、参考帧和候选视频。

@@ -588,7 +589,7 @@ web/next.config.mjsNext.js 构建配置:静态导出、图片不走优化、禁用开发环境左下角 Next Dev Indicator,并移除 Next 16 已不支持的 eslint 顶层配置,避免本地 dev 出现配置 Issue 提示。 web/app/globals.css全局主题变量、登录页视觉样式、ReactFlow 样式引用,以及本地开发态 nextjs-portal 遮挡隐藏规则。 web/app/page.tsx产品工作台主状态:jobs、activeJobId、生成任务状态;主渲染为全屏素材输入列 + 音频解析工作表;“开始”编排状态只负责在下载完成后自动触发 triggerTranscribe,不再默认触发抽帧、Vision 扫描或分镜初稿保存;底部吸附音频条不再从主界面渲染。 - web/components/ad-recreation-board.tsx信息流广告音频解析工作表:左侧素材输入;右侧展示视频下载状态、默认折叠的音频文案依据,以及统一的音频解析结果面板;面板顶部是一行讲话人/节奏/背景音摘要,下方左侧为原视频播放器、右侧为逐句时间轴,底部横向音频波形用参考图式的连续灰色包络显示响度、停顿和密集爆点。视频播放时通过 requestAnimationFrame 平滑驱动波形播放线,同时同步高亮并滚动当前句;点击音频波形或字幕行会跳转原视频时间。音频结果下方新增 SKG 分镜规划表:按逐句时间轴生成结构作用、SKG 新文案、画面规划、产品融入,并支持逐行定向抽参考帧。旧分镜卡、抽帧控制和视频生成组件仍保留在文件里,但当前主路径不渲染。 + web/components/ad-recreation-board.tsx信息流广告音频解析工作表:左侧素材输入;右侧展示视频下载状态、默认折叠的音频文案依据,以及统一的音频解析结果面板;面板顶部是一行讲话人/节奏/背景音摘要,下方左侧为原视频播放器、右侧为逐句时间轴,底部横向音频波形用参考图式的连续灰色包络显示响度、停顿和密集爆点。视频播放时通过 requestAnimationFrame 平滑驱动波形播放线,同时同步高亮并滚动当前句;点击音频波形或字幕行会跳转原视频时间。音频结果下方是信息流复刻分镜工作台:每条音频分镜纵向排列,行内从左到右串起原内容、新口播文案、画面规划/产品融入、参考帧/关键元素和生成视频;单条生成会先把该行规划保存为对应关键帧分镜,再复用现有生视频接口提交 Seedance 候选。旧分镜卡、抽帧控制和视频生成组件仍保留在文件里,但当前主路径不渲染。 web/app/login/page.tsx生产登录页:访问账号/访问密钥表单、保持登录、错误/成功状态;当前只在原版 Digital Oasis 动态背景上叠加一个组合登录框,桌面端左侧是动态角色,右侧是图标化登录表单;面板左上角展示官网 SKG 字标和中文“营销内容工作台”系统标识。 web/app/login/layout.tsx登录路由专属 layout:覆盖全站默认网页标题和描述为空,避免 /login 继承工作台 metadata 后在页面源码里继续出现登录界面文字以外的文案。 web/components/login/oasis-canvas.tsx登录页全屏动态视觉层:用 iframe 直接承载下载包 web/public/oasis-source/index.html 的原 WebGPU / Three.js 草场源码;父级登录页只覆盖自己的文案和表单,并在捕获阶段把全局鼠标坐标同时用原生事件和 postMessage 转发给 iframe,避免登录面板或输入框遮挡时草地失去鼠标响应。 @@ -626,7 +627,7 @@ web/app/page.tsx -> 音频解析工作表:web/components/ad-recreation-board.tsx -> 开始:创建/激活 job → 下载完成后自动触发音频处理 -> 左侧素材输入列 + 右侧默认折叠的音频文案依据 + 统一音频解析结果面板(声音摘要在上,原视频与逐句时间轴并排,底部连续响度波形联动) - -> SKG 分镜规划表:逐句时间轴 → 结构作用 / SKG 新文案 / 画面规划 / 产品融入 → 定向抽参考帧 + -> 信息流复刻分镜工作台:逐句时间轴 → 原内容 / 新口播文案 / 画面规划与产品融入 / 参考帧与关键元素 / 对应候选视频 -> 底部音频条:不再渲染,音频结果集中到右侧工作表 -> 旧节点/深度素材面板:web/components/nodes/index.tsx、web/components/lightbox.tsx、web/components/storyboard-workbench.tsx(底层保留,当前不作为主入口) -> API 契约:web/lib/api.ts @@ -652,9 +653,9 @@ api/main.py
适合怎么描述“原视频播放、连续响度波形、逐句时间轴滚动、高亮和跳转联动还需要怎么调整”。
-
你看到的区域SKG 分镜规划表
-
主要源码AudioStoryboardPlanPanelbuildAudioStoryboardRows in web/components/ad-recreation-board.tsx;逐行定向抽帧复用 onAddManualFrameForJob
-
适合怎么描述“按音频逐句生成产品分镜、每行怎样改写 SKG 文案、如何定向抽帧和进入元素提取”。
+
你看到的区域信息流复刻分镜工作台
+
主要源码AudioStoryboardPlanPanelbuildAudioStoryboardRowsbuildStoryboardSceneFromAudioRow in web/components/ad-recreation-board.tsx;逐行定向抽帧复用 onAddManualFrameForJob,单条生成复用 onGenerateVideoPUT /frames/{idx}/storyboard
+
适合怎么描述“按音频逐句生成产品分镜、每行怎样改写口播、如何抽参考帧、生成的视频应该回显到哪一行”。
你看到的区域旧深度素材面板(当前不作为主路径)
@@ -949,14 +950,14 @@ SubjectAsset {
-

2026-05-17 · 新增 SKG 分镜规划表

+

2026-05-17 · 新增信息流复刻分镜工作台

UI Workflow
-

问题:信息流复刻不应该先全量抽帧和提元素,也不能空写分镜;主线应先按音频内容规划产品分镜,再按每条分镜定向抽参考帧。

-

改动:web/components/ad-recreation-board.tsx 新增 AudioStoryboardPlanPanel:从 job.transcript 生成逐行 SKG 分镜规划表,包含时间段、原内容、结构作用、SKG 新文案、画面规划、产品融入和参考帧状态。每行“抽参考帧”调用现有手动加帧接口,在对应时间段中点抽取原视频参考帧。

-

影响:web/components/ad-recreation-board.tsxdocs/source-analysis.html。当前第一版先做前端规划和定向抽帧入口,暂不新增后端持久化字段;后续可在此表基础上接入模型改写、元素提取和单条视频生成。

+

问题:信息流复刻不应该先全量抽帧和提元素,也不能空写分镜;主线应先按音频内容规划产品分镜,再按每条分镜定向抽参考帧,并把生成出的视频回挂到对应分镜。

+

改动:web/components/ad-recreation-board.tsx 新增 AudioStoryboardPlanPanel:从 job.transcript 生成纵向分镜行,每行内部从左到右展示时间/结构、原内容、新口播文案、画面规划/产品融入、参考帧/关键元素和生成视频。每行“抽参考帧”调用现有手动加帧接口,在对应时间段中点抽取原视频参考帧;“生成本条”会把该行规划保存到对应关键帧分镜,并复用现有 onGenerateVideo 提交 Seedance 候选,候选视频按 frame_idx 回显在该行右侧。

+

影响:web/components/ad-recreation-board.tsxdocs/source-analysis.html。当前不新增后端字段,继续复用 KeyFrame.storyboardGeneratedVideo.frame_idx;后续模型改写、关键元素 6 视图和完整合成可以在该分镜行内继续接入。

diff --git a/web/components/ad-recreation-board.tsx b/web/components/ad-recreation-board.tsx index cdbbcfd..3fea8b9 100644 --- a/web/components/ad-recreation-board.tsx +++ b/web/components/ad-recreation-board.tsx @@ -287,6 +287,19 @@ function buildAudioStoryboardRows(job: Job | null): AudioStoryboardRow[] { }) } +function buildStoryboardSceneFromAudioRow(row: AudioStoryboardRow, frame: KeyFrame, nextFrame?: KeyFrame | null): StoryboardScene { + return { + duration: Number(Math.max(3.2, Math.min(6.5, row.end - row.start || 4.5)).toFixed(1)), + first_image: { kind: "keyframe", frame_idx: frame.index, label: `分镜 ${row.index + 1} 参考帧` }, + last_image: nextFrame ? { kind: "keyframe", frame_idx: nextFrame.index, label: `分镜 ${row.index + 1} 尾帧` } : null, + subject: row.keyElements, + scene: `${row.visualPlan}\n原音频依据:${row.source}`, + product: row.productIntegration, + action: row.skgCopy, + reference_ids: [], + } +} + export function AdRecreationBoard({ data, onGenerateVideo, @@ -547,6 +560,8 @@ export function AdRecreationBoard({ job={job} onAddFrame={data.onAddManualFrameForJob} onOpenFrame={data.onOpenFramePanel} + onJobUpdate={data.onJobUpdate} + onGenerateVideo={onGenerateVideo} />
@@ -885,18 +900,28 @@ function AudioStoryboardPlanPanel({ job, onAddFrame, onOpenFrame, + onJobUpdate, + onGenerateVideo, }: { job: Job | null onAddFrame?: (jobId: string, t: number) => Promise | void onOpenFrame?: (idx: number) => void + onJobUpdate?: (job: Job) => void + onGenerateVideo?: (frameIdx: number, scene: StoryboardScene, model: string) => Promise | void }) { const [busyRow, setBusyRow] = useState(null) + const [videoBusyRow, setVideoBusyRow] = useState(null) const rows = useMemo(() => buildAudioStoryboardRows(job), [job]) const orderedFrames = useMemo(() => job ? [...job.frames].sort((a, b) => a.timestamp - b.timestamp) : [], [job]) const framesForRow = (row: AudioStoryboardRow) => orderedFrames.filter((frame) => frame.timestamp >= row.start - 0.2 && frame.timestamp <= row.end + 0.2).slice(0, 3) + const videosForRow = (refs: KeyFrame[]) => { + const refIndices = new Set(refs.map((frame) => frame.index)) + return (job?.generated_videos ?? []).filter((video) => refIndices.has(video.frame_idx)) + } + const addReferenceFrame = async (row: AudioStoryboardRow) => { if (!job || !onAddFrame) return const t = clampNumber((row.start + row.end) / 2, 0, Math.max(job.duration || row.end, row.end)) @@ -908,101 +933,176 @@ function AudioStoryboardPlanPanel({ } } + const generateRowVideo = async (row: AudioStoryboardRow, refs: KeyFrame[]) => { + if (!job || !refs.length || !onGenerateVideo) return + const frame = refs[0] + const nextFrame = orderedFrames.find((item) => item.timestamp > frame.timestamp) ?? null + const scene = buildStoryboardSceneFromAudioRow(row, frame, nextFrame) + setVideoBusyRow(row.index) + try { + const updated = await updateStoryboard(job.id, frame.index, scene) + onJobUpdate?.(updated) + await onGenerateVideo(frame.index, scene, "seedance") + } catch (e) { + toast.error("生成本条视频失败:" + (e instanceof Error ? e.message : String(e))) + } finally { + setVideoBusyRow(null) + } + } + if (!job) return null return (
- } title="SKG 分镜规划表" /> -

先按音频内容规划产品分镜,再按每条分镜定向抽参考帧和关键元素。

+ } title="信息流复刻分镜工作台" /> +

每条分镜纵向排列;行内从左到右完成原内容、新文案、画面/产品、参考帧和生成视频。

0} detail={rows.length ? `${rows.length} 条` : "待音频"} /> 0} detail={orderedFrames.length ? `${orderedFrames.length} 张` : "待抽帧"} /> - 0} detail="先规划" /> + 0} detail={`${job.generated_videos?.length ?? 0} 条`} />
{rows.length ? ( -
-
-
时间
-
结构
-
原内容
-
SKG 新文案
-
画面规划 / 产品融入
-
参考帧
-
下一步
-
-
- {rows.map((row) => { - const refs = framesForRow(row) - const busy = busyRow === row.index - return ( -
+
+ {rows.map((row) => { + const refs = framesForRow(row) + const rowVideos = videosForRow(refs) + const busy = busyRow === row.index + const generating = videoBusyRow === row.index + return ( +
+
{row.start.toFixed(1)}-{row.end.toFixed(1)}s
-
- {row.role} +
+ {row.role}
-
{row.source}
-
{row.skgCopy}
-
-
{row.visualPlan}
-
- - {row.productIntegration} + + + +

{row.source}

+
+ + +

{row.skgCopy}

+
+ + +

{row.visualPlan}

+

+ + {row.productIntegration} +

+
+ + + {refs.length ? ( +
+ {refs.map((frame) => ( + + ))}
+ ) : ( +

{row.referencePlan}

+ )} +
+ + {row.keyElements}
-
- {refs.length ? ( -
- {refs.map((frame) => ( - - ))} -
- ) : ( - {row.referencePlan} - )} -
-
- -
- - {refs.length ? "下一步提元素" : "先抽帧"} + + + + + {rowVideos.length > 0 ? ( +
+ {rowVideos.map((video) => ( + + ))}
-
-
- ) - })} -
+ ) : ( +
+ {refs.length ? "等待生成" : "先抽参考帧"} +
+ )} + + +
+ ) + })}
) : ( - + )}
) } +function StoryboardPlanCell({ label, children, className = "" }: { label: string; children: ReactNode; className?: string }) { + return ( +
+
{label}
+ {children} +
+ ) +} + +function StoryboardVideoPreview({ job, video }: { job: Job; video: GeneratedVideo }) { + const src = videoSrc(video) + const poster = videoPoster(job, video) + const running = video.status === "queued" || video.status === "in_progress" + return ( + + {src && video.status === "completed" ? ( + + ) +} + function AudioWaveform({ features, status,