auto-save 2026-05-17 16:00 (~3)
This commit is contained in:
@@ -1,122 +1,5 @@
|
|||||||
{
|
{
|
||||||
"entries": [
|
"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,
|
"files_changed": 1,
|
||||||
"message": "Codex 会话活跃 · 最近命令:codex · 1 项未提交变更 · 最近提交:auto-save 2026-05-15 09:50 (~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",
|
"message": "refactor: place audio results side by side",
|
||||||
"hash": "78d47b8",
|
"hash": "78d47b8",
|
||||||
"files_changed": 3
|
"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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -569,12 +569,13 @@
|
|||||||
|
|
||||||
<section id="pipeline" data-search>
|
<section id="pipeline" data-search>
|
||||||
<h2>业务管线</h2>
|
<h2>业务管线</h2>
|
||||||
<p>当前产品方向已收窄为“信息流广告快速复刻第一步”:主界面左侧是素材输入列,右侧是音频解析工作表。用户粘贴 TK 链接或上传视频后点击“开始”,系统自动下载源视频;下载完成后优先提取原音频文案/字幕,并分析讲话人、语速节奏、背景音乐/环境声/音效。抽帧、分镜规划、产品融入、元素 6 视图和视频合成暂作为后续能力保留,不在当前开始流程里自动触发。</p>
|
<p>当前产品方向已收窄为“信息流广告快速复刻”:主界面左侧是素材输入列,右侧先完成音频解析,再进入信息流复刻分镜工作台。用户粘贴 TK 链接或上传视频后点击“开始”,系统自动下载源视频;下载完成后优先提取原音频文案/字幕,并分析讲话人、语速节奏、背景音乐/环境声/音效。分镜规划按逐句时间轴生成,抽帧和视频生成由用户按单条分镜触发,不在当前开始流程里自动全量运行。</p>
|
||||||
<div class="pipeline">
|
<div class="pipeline">
|
||||||
<div class="step"><div class="num">1</div><h3>导入素材</h3><p>粘贴 TK / 信息流视频链接或上传本地视频;“开始”只把任务放入第一步队列。</p></div>
|
<div class="step"><div class="num">1</div><h3>导入素材</h3><p>粘贴 TK / 信息流视频链接或上传本地视频;“开始”只把任务放入第一步队列。</p></div>
|
||||||
<div class="step"><div class="num">2</div><h3>下载源视频</h3><p>后端用 yt-dlp 或本地上传文件落 <code>source.mp4</code>,记录时长、尺寸和视频只读地址。</p></div>
|
<div class="step"><div class="num">2</div><h3>下载源视频</h3><p>后端用 yt-dlp 或本地上传文件落 <code>source.mp4</code>,记录时长、尺寸和视频只读地址。</p></div>
|
||||||
<div class="step"><div class="num">3</div><h3>解析音频</h3><p>从 <code>source.mp4</code> 提取 <code>audio.wav</code>,ASR 提取原文案,翻译成中文,并写入逐句时间轴。</p></div>
|
<div class="step"><div class="num">3</div><h3>解析音频</h3><p>从 <code>source.mp4</code> 提取 <code>audio.wav</code>,ASR 提取原文案,翻译成中文,并写入逐句时间轴。</p></div>
|
||||||
<div class="step"><div class="num">4</div><h3>声音分析</h3><p>用音频模型分析讲话人、口播节奏、停顿、背景音乐/环境声/音效;不默认改写配音或生成视频。</p></div>
|
<div class="step"><div class="num">4</div><h3>声音分析</h3><p>用音频模型分析讲话人、口播节奏、停顿、背景音乐/环境声/音效;不默认改写配音或生成视频。</p></div>
|
||||||
|
<div class="step"><div class="num">5</div><h3>分镜生成</h3><p>按逐句时间轴生成竖向分镜行,单行内从左到右承接原内容、新口播、画面规划、参考帧和候选视频。</p></div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -588,7 +589,7 @@
|
|||||||
<tr><td><code>web/next.config.mjs</code></td><td>Next.js 构建配置:静态导出、图片不走优化、禁用开发环境左下角 Next Dev Indicator,并移除 Next 16 已不支持的 <code>eslint</code> 顶层配置,避免本地 dev 出现配置 Issue 提示。</td></tr>
|
<tr><td><code>web/next.config.mjs</code></td><td>Next.js 构建配置:静态导出、图片不走优化、禁用开发环境左下角 Next Dev Indicator,并移除 Next 16 已不支持的 <code>eslint</code> 顶层配置,避免本地 dev 出现配置 Issue 提示。</td></tr>
|
||||||
<tr><td><code>web/app/globals.css</code></td><td>全局主题变量、登录页视觉样式、ReactFlow 样式引用,以及本地开发态 <code>nextjs-portal</code> 遮挡隐藏规则。</td></tr>
|
<tr><td><code>web/app/globals.css</code></td><td>全局主题变量、登录页视觉样式、ReactFlow 样式引用,以及本地开发态 <code>nextjs-portal</code> 遮挡隐藏规则。</td></tr>
|
||||||
<tr><td><code>web/app/page.tsx</code></td><td>产品工作台主状态:jobs、activeJobId、生成任务状态;主渲染为全屏素材输入列 + 音频解析工作表;“开始”编排状态只负责在下载完成后自动触发 <code>triggerTranscribe</code>,不再默认触发抽帧、Vision 扫描或分镜初稿保存;底部吸附音频条不再从主界面渲染。</td></tr>
|
<tr><td><code>web/app/page.tsx</code></td><td>产品工作台主状态:jobs、activeJobId、生成任务状态;主渲染为全屏素材输入列 + 音频解析工作表;“开始”编排状态只负责在下载完成后自动触发 <code>triggerTranscribe</code>,不再默认触发抽帧、Vision 扫描或分镜初稿保存;底部吸附音频条不再从主界面渲染。</td></tr>
|
||||||
<tr><td><code>web/components/ad-recreation-board.tsx</code></td><td>信息流广告音频解析工作表:左侧素材输入;右侧展示视频下载状态、默认折叠的音频文案依据,以及统一的音频解析结果面板;面板顶部是一行讲话人/节奏/背景音摘要,下方左侧为原视频播放器、右侧为逐句时间轴,底部横向音频波形用参考图式的连续灰色包络显示响度、停顿和密集爆点。视频播放时通过 <code>requestAnimationFrame</code> 平滑驱动波形播放线,同时同步高亮并滚动当前句;点击音频波形或字幕行会跳转原视频时间。音频结果下方新增 SKG 分镜规划表:按逐句时间轴生成结构作用、SKG 新文案、画面规划、产品融入,并支持逐行定向抽参考帧。旧分镜卡、抽帧控制和视频生成组件仍保留在文件里,但当前主路径不渲染。</td></tr>
|
<tr><td><code>web/components/ad-recreation-board.tsx</code></td><td>信息流广告音频解析工作表:左侧素材输入;右侧展示视频下载状态、默认折叠的音频文案依据,以及统一的音频解析结果面板;面板顶部是一行讲话人/节奏/背景音摘要,下方左侧为原视频播放器、右侧为逐句时间轴,底部横向音频波形用参考图式的连续灰色包络显示响度、停顿和密集爆点。视频播放时通过 <code>requestAnimationFrame</code> 平滑驱动波形播放线,同时同步高亮并滚动当前句;点击音频波形或字幕行会跳转原视频时间。音频结果下方是信息流复刻分镜工作台:每条音频分镜纵向排列,行内从左到右串起原内容、新口播文案、画面规划/产品融入、参考帧/关键元素和生成视频;单条生成会先把该行规划保存为对应关键帧分镜,再复用现有生视频接口提交 Seedance 候选。旧分镜卡、抽帧控制和视频生成组件仍保留在文件里,但当前主路径不渲染。</td></tr>
|
||||||
<tr><td><code>web/app/login/page.tsx</code></td><td>生产登录页:访问账号/访问密钥表单、保持登录、错误/成功状态;当前只在原版 Digital Oasis 动态背景上叠加一个组合登录框,桌面端左侧是动态角色,右侧是图标化登录表单;面板左上角展示官网 SKG 字标和中文“营销内容工作台”系统标识。</td></tr>
|
<tr><td><code>web/app/login/page.tsx</code></td><td>生产登录页:访问账号/访问密钥表单、保持登录、错误/成功状态;当前只在原版 Digital Oasis 动态背景上叠加一个组合登录框,桌面端左侧是动态角色,右侧是图标化登录表单;面板左上角展示官网 SKG 字标和中文“营销内容工作台”系统标识。</td></tr>
|
||||||
<tr><td><code>web/app/login/layout.tsx</code></td><td>登录路由专属 layout:覆盖全站默认网页标题和描述为空,避免 <code>/login</code> 继承工作台 metadata 后在页面源码里继续出现登录界面文字以外的文案。</td></tr>
|
<tr><td><code>web/app/login/layout.tsx</code></td><td>登录路由专属 layout:覆盖全站默认网页标题和描述为空,避免 <code>/login</code> 继承工作台 metadata 后在页面源码里继续出现登录界面文字以外的文案。</td></tr>
|
||||||
<tr><td><code>web/components/login/oasis-canvas.tsx</code></td><td>登录页全屏动态视觉层:用 iframe 直接承载下载包 <code>web/public/oasis-source/index.html</code> 的原 WebGPU / Three.js 草场源码;父级登录页只覆盖自己的文案和表单,并在捕获阶段把全局鼠标坐标同时用原生事件和 <code>postMessage</code> 转发给 iframe,避免登录面板或输入框遮挡时草地失去鼠标响应。</td></tr>
|
<tr><td><code>web/components/login/oasis-canvas.tsx</code></td><td>登录页全屏动态视觉层:用 iframe 直接承载下载包 <code>web/public/oasis-source/index.html</code> 的原 WebGPU / Three.js 草场源码;父级登录页只覆盖自己的文案和表单,并在捕获阶段把全局鼠标坐标同时用原生事件和 <code>postMessage</code> 转发给 iframe,避免登录面板或输入框遮挡时草地失去鼠标响应。</td></tr>
|
||||||
@@ -626,7 +627,7 @@ web/app/page.tsx
|
|||||||
-> 音频解析工作表:web/components/ad-recreation-board.tsx
|
-> 音频解析工作表:web/components/ad-recreation-board.tsx
|
||||||
-> 开始:创建/激活 job → 下载完成后自动触发音频处理
|
-> 开始:创建/激活 job → 下载完成后自动触发音频处理
|
||||||
-> 左侧素材输入列 + 右侧默认折叠的音频文案依据 + 统一音频解析结果面板(声音摘要在上,原视频与逐句时间轴并排,底部连续响度波形联动)
|
-> 左侧素材输入列 + 右侧默认折叠的音频文案依据 + 统一音频解析结果面板(声音摘要在上,原视频与逐句时间轴并排,底部连续响度波形联动)
|
||||||
-> SKG 分镜规划表:逐句时间轴 → 结构作用 / SKG 新文案 / 画面规划 / 产品融入 → 定向抽参考帧
|
-> 信息流复刻分镜工作台:逐句时间轴 → 原内容 / 新口播文案 / 画面规划与产品融入 / 参考帧与关键元素 / 对应候选视频
|
||||||
-> 底部音频条:不再渲染,音频结果集中到右侧工作表
|
-> 底部音频条:不再渲染,音频结果集中到右侧工作表
|
||||||
-> 旧节点/深度素材面板:web/components/nodes/index.tsx、web/components/lightbox.tsx、web/components/storyboard-workbench.tsx(底层保留,当前不作为主入口)
|
-> 旧节点/深度素材面板:web/components/nodes/index.tsx、web/components/lightbox.tsx、web/components/storyboard-workbench.tsx(底层保留,当前不作为主入口)
|
||||||
-> API 契约:web/lib/api.ts
|
-> API 契约:web/lib/api.ts
|
||||||
@@ -652,9 +653,9 @@ api/main.py
|
|||||||
<div><strong>适合怎么描述</strong><span>“原视频播放、连续响度波形、逐句时间轴滚动、高亮和跳转联动还需要怎么调整”。</span></div>
|
<div><strong>适合怎么描述</strong><span>“原视频播放、连续响度波形、逐句时间轴滚动、高亮和跳转联动还需要怎么调整”。</span></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flow-row">
|
<div class="flow-row">
|
||||||
<div><strong>你看到的区域</strong><span>SKG 分镜规划表</span></div>
|
<div><strong>你看到的区域</strong><span>信息流复刻分镜工作台</span></div>
|
||||||
<div><strong>主要源码</strong><span><code>AudioStoryboardPlanPanel</code>、<code>buildAudioStoryboardRows</code> in <code>web/components/ad-recreation-board.tsx</code>;逐行定向抽帧复用 <code>onAddManualFrameForJob</code>。</span></div>
|
<div><strong>主要源码</strong><span><code>AudioStoryboardPlanPanel</code>、<code>buildAudioStoryboardRows</code>、<code>buildStoryboardSceneFromAudioRow</code> in <code>web/components/ad-recreation-board.tsx</code>;逐行定向抽帧复用 <code>onAddManualFrameForJob</code>,单条生成复用 <code>onGenerateVideo</code> 和 <code>PUT /frames/{idx}/storyboard</code>。</span></div>
|
||||||
<div><strong>适合怎么描述</strong><span>“按音频逐句生成产品分镜、每行怎样改写 SKG 文案、如何定向抽帧和进入元素提取”。</span></div>
|
<div><strong>适合怎么描述</strong><span>“按音频逐句生成产品分镜、每行怎样改写口播、如何抽参考帧、生成的视频应该回显到哪一行”。</span></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flow-row">
|
<div class="flow-row">
|
||||||
<div><strong>你看到的区域</strong><span>旧深度素材面板(当前不作为主路径)</span></div>
|
<div><strong>你看到的区域</strong><span>旧深度素材面板(当前不作为主路径)</span></div>
|
||||||
@@ -949,14 +950,14 @@ SubjectAsset {
|
|||||||
<div class="changelog">
|
<div class="changelog">
|
||||||
<article class="change">
|
<article class="change">
|
||||||
<header>
|
<header>
|
||||||
<h3>2026-05-17 · 新增 SKG 分镜规划表</h3>
|
<h3>2026-05-17 · 新增信息流复刻分镜工作台</h3>
|
||||||
<span class="tag rose">UI</span>
|
<span class="tag rose">UI</span>
|
||||||
<span class="tag cyan">Workflow</span>
|
<span class="tag cyan">Workflow</span>
|
||||||
</header>
|
</header>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<p><strong>问题:</strong>信息流复刻不应该先全量抽帧和提元素,也不能空写分镜;主线应先按音频内容规划产品分镜,再按每条分镜定向抽参考帧。</p>
|
<p><strong>问题:</strong>信息流复刻不应该先全量抽帧和提元素,也不能空写分镜;主线应先按音频内容规划产品分镜,再按每条分镜定向抽参考帧,并把生成出的视频回挂到对应分镜。</p>
|
||||||
<p><strong>改动:</strong><code>web/components/ad-recreation-board.tsx</code> 新增 <code>AudioStoryboardPlanPanel</code>:从 <code>job.transcript</code> 生成逐行 SKG 分镜规划表,包含时间段、原内容、结构作用、SKG 新文案、画面规划、产品融入和参考帧状态。每行“抽参考帧”调用现有手动加帧接口,在对应时间段中点抽取原视频参考帧。</p>
|
<p><strong>改动:</strong><code>web/components/ad-recreation-board.tsx</code> 新增 <code>AudioStoryboardPlanPanel</code>:从 <code>job.transcript</code> 生成纵向分镜行,每行内部从左到右展示时间/结构、原内容、新口播文案、画面规划/产品融入、参考帧/关键元素和生成视频。每行“抽参考帧”调用现有手动加帧接口,在对应时间段中点抽取原视频参考帧;“生成本条”会把该行规划保存到对应关键帧分镜,并复用现有 <code>onGenerateVideo</code> 提交 Seedance 候选,候选视频按 <code>frame_idx</code> 回显在该行右侧。</p>
|
||||||
<p><strong>影响:</strong><code>web/components/ad-recreation-board.tsx</code>、<code>docs/source-analysis.html</code>。当前第一版先做前端规划和定向抽帧入口,暂不新增后端持久化字段;后续可在此表基础上接入模型改写、元素提取和单条视频生成。</p>
|
<p><strong>影响:</strong><code>web/components/ad-recreation-board.tsx</code>、<code>docs/source-analysis.html</code>。当前不新增后端字段,继续复用 <code>KeyFrame.storyboard</code> 和 <code>GeneratedVideo.frame_idx</code>;后续模型改写、关键元素 6 视图和完整合成可以在该分镜行内继续接入。</p>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
<article class="change">
|
<article class="change">
|
||||||
|
|||||||
@@ -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({
|
export function AdRecreationBoard({
|
||||||
data,
|
data,
|
||||||
onGenerateVideo,
|
onGenerateVideo,
|
||||||
@@ -547,6 +560,8 @@ export function AdRecreationBoard({
|
|||||||
job={job}
|
job={job}
|
||||||
onAddFrame={data.onAddManualFrameForJob}
|
onAddFrame={data.onAddManualFrameForJob}
|
||||||
onOpenFrame={data.onOpenFramePanel}
|
onOpenFrame={data.onOpenFramePanel}
|
||||||
|
onJobUpdate={data.onJobUpdate}
|
||||||
|
onGenerateVideo={onGenerateVideo}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -885,18 +900,28 @@ function AudioStoryboardPlanPanel({
|
|||||||
job,
|
job,
|
||||||
onAddFrame,
|
onAddFrame,
|
||||||
onOpenFrame,
|
onOpenFrame,
|
||||||
|
onJobUpdate,
|
||||||
|
onGenerateVideo,
|
||||||
}: {
|
}: {
|
||||||
job: Job | null
|
job: Job | null
|
||||||
onAddFrame?: (jobId: string, t: number) => Promise<void> | void
|
onAddFrame?: (jobId: string, t: number) => Promise<void> | void
|
||||||
onOpenFrame?: (idx: number) => void
|
onOpenFrame?: (idx: number) => void
|
||||||
|
onJobUpdate?: (job: Job) => void
|
||||||
|
onGenerateVideo?: (frameIdx: number, scene: StoryboardScene, model: string) => Promise<void> | void
|
||||||
}) {
|
}) {
|
||||||
const [busyRow, setBusyRow] = useState<number | null>(null)
|
const [busyRow, setBusyRow] = useState<number | null>(null)
|
||||||
|
const [videoBusyRow, setVideoBusyRow] = useState<number | null>(null)
|
||||||
const rows = useMemo(() => buildAudioStoryboardRows(job), [job])
|
const rows = useMemo(() => buildAudioStoryboardRows(job), [job])
|
||||||
const orderedFrames = useMemo(() => job ? [...job.frames].sort((a, b) => a.timestamp - b.timestamp) : [], [job])
|
const orderedFrames = useMemo(() => job ? [...job.frames].sort((a, b) => a.timestamp - b.timestamp) : [], [job])
|
||||||
|
|
||||||
const framesForRow = (row: AudioStoryboardRow) =>
|
const framesForRow = (row: AudioStoryboardRow) =>
|
||||||
orderedFrames.filter((frame) => frame.timestamp >= row.start - 0.2 && frame.timestamp <= row.end + 0.2).slice(0, 3)
|
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) => {
|
const addReferenceFrame = async (row: AudioStoryboardRow) => {
|
||||||
if (!job || !onAddFrame) return
|
if (!job || !onAddFrame) return
|
||||||
const t = clampNumber((row.start + row.end) / 2, 0, Math.max(job.duration || row.end, row.end))
|
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
|
if (!job) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="mt-3 rounded-lg border border-white/10 bg-black/28 p-2.5">
|
<section className="mt-3 rounded-lg border border-white/10 bg-black/28 p-2.5">
|
||||||
<div className="mb-2 flex items-start justify-between gap-3">
|
<div className="mb-2 flex items-start justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<SectionTitle icon={<Sparkles className="h-4 w-4" />} title="SKG 分镜规划表" />
|
<SectionTitle icon={<Sparkles className="h-4 w-4" />} title="信息流复刻分镜工作台" />
|
||||||
<p className="mt-1 text-[11px] leading-snug text-white/42">先按音频内容规划产品分镜,再按每条分镜定向抽参考帧和关键元素。</p>
|
<p className="mt-1 text-[11px] leading-snug text-white/42">每条分镜纵向排列;行内从左到右完成原内容、新文案、画面/产品、参考帧和生成视频。</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid shrink-0 grid-cols-3 gap-2 text-[11px] text-white/45">
|
<div className="grid shrink-0 grid-cols-3 gap-2 text-[11px] text-white/45">
|
||||||
<Requirement label="分镜" ready={rows.length > 0} detail={rows.length ? `${rows.length} 条` : "待音频"} />
|
<Requirement label="分镜" ready={rows.length > 0} detail={rows.length ? `${rows.length} 条` : "待音频"} />
|
||||||
<Requirement label="参考帧" ready={orderedFrames.length > 0} detail={orderedFrames.length ? `${orderedFrames.length} 张` : "待抽帧"} />
|
<Requirement label="参考帧" ready={orderedFrames.length > 0} detail={orderedFrames.length ? `${orderedFrames.length} 张` : "待抽帧"} />
|
||||||
<Requirement label="主线" ready={rows.length > 0} detail="先规划" />
|
<Requirement label="生成" ready={(job.generated_videos?.length ?? 0) > 0} detail={`${job.generated_videos?.length ?? 0} 条`} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{rows.length ? (
|
{rows.length ? (
|
||||||
<div className="overflow-hidden rounded-md border border-white/10">
|
<div className="max-h-[560px] space-y-2 overflow-y-auto pr-1">
|
||||||
<div className="grid grid-cols-[78px_86px_minmax(150px,0.9fr)_minmax(180px,1fr)_minmax(170px,1fr)_140px_112px] border-b border-white/10 bg-white/[0.04] px-3 py-2 text-[11px] font-semibold text-white/50">
|
{rows.map((row) => {
|
||||||
<div>时间</div>
|
const refs = framesForRow(row)
|
||||||
<div>结构</div>
|
const rowVideos = videosForRow(refs)
|
||||||
<div>原内容</div>
|
const busy = busyRow === row.index
|
||||||
<div>SKG 新文案</div>
|
const generating = videoBusyRow === row.index
|
||||||
<div>画面规划 / 产品融入</div>
|
return (
|
||||||
<div>参考帧</div>
|
<article
|
||||||
<div>下一步</div>
|
key={row.index}
|
||||||
</div>
|
className="grid overflow-hidden rounded-md border border-white/10 bg-black/24 text-[11.5px] leading-snug text-white/64 xl:grid-cols-[82px_minmax(130px,0.8fr)_minmax(150px,1fr)_minmax(180px,1.1fr)_minmax(150px,0.9fr)_146px]"
|
||||||
<div className="max-h-[420px] overflow-y-auto">
|
>
|
||||||
{rows.map((row) => {
|
<StoryboardPlanCell label="分镜">
|
||||||
const refs = framesForRow(row)
|
|
||||||
const busy = busyRow === row.index
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={row.index}
|
|
||||||
className="grid grid-cols-[78px_86px_minmax(150px,0.9fr)_minmax(180px,1fr)_minmax(170px,1fr)_140px_112px] gap-3 border-b border-white/8 px-3 py-2 text-[11.5px] leading-snug text-white/64 last:border-b-0"
|
|
||||||
>
|
|
||||||
<div className="font-mono text-[11px] text-white/40">{row.start.toFixed(1)}-{row.end.toFixed(1)}s</div>
|
<div className="font-mono text-[11px] text-white/40">{row.start.toFixed(1)}-{row.end.toFixed(1)}s</div>
|
||||||
<div>
|
<div className="mt-2 inline-flex max-w-full rounded-md border border-emerald-300/15 bg-emerald-300/[0.08] px-2 py-1 text-[11px] text-emerald-100/80">
|
||||||
<span className="rounded-md border border-white/10 bg-black/35 px-2 py-1 text-[11px] text-emerald-100/75">{row.role}</span>
|
{row.role}
|
||||||
</div>
|
</div>
|
||||||
<div className="line-clamp-3" title={row.source}>{row.source}</div>
|
</StoryboardPlanCell>
|
||||||
<div className="line-clamp-3 text-white/78" title={row.skgCopy}>{row.skgCopy}</div>
|
|
||||||
<div className="space-y-1">
|
<StoryboardPlanCell label="原内容">
|
||||||
<div className="line-clamp-2" title={row.visualPlan}>{row.visualPlan}</div>
|
<p className="line-clamp-5" title={row.source}>{row.source}</p>
|
||||||
<div className="line-clamp-2 text-white/42" title={row.productIntegration}>
|
</StoryboardPlanCell>
|
||||||
<Package className="mr-1 inline h-3 w-3 text-rose-200/70" />
|
|
||||||
{row.productIntegration}
|
<StoryboardPlanCell label="新口播文案">
|
||||||
|
<p className="line-clamp-5 text-white/82" title={row.skgCopy}>{row.skgCopy}</p>
|
||||||
|
</StoryboardPlanCell>
|
||||||
|
|
||||||
|
<StoryboardPlanCell label="画面规划 / 产品融入">
|
||||||
|
<p className="line-clamp-3" title={row.visualPlan}>{row.visualPlan}</p>
|
||||||
|
<p className="mt-1.5 line-clamp-3 text-white/45" title={row.productIntegration}>
|
||||||
|
<Package className="mr-1 inline h-3 w-3 text-rose-200/75" />
|
||||||
|
{row.productIntegration}
|
||||||
|
</p>
|
||||||
|
</StoryboardPlanCell>
|
||||||
|
|
||||||
|
<StoryboardPlanCell label="参考帧 / 关键元素">
|
||||||
|
{refs.length ? (
|
||||||
|
<div className="mb-2 flex gap-1.5 overflow-x-auto pb-1">
|
||||||
|
{refs.map((frame) => (
|
||||||
|
<button
|
||||||
|
key={frame.index}
|
||||||
|
type="button"
|
||||||
|
onClick={() => onOpenFrame?.(frame.index)}
|
||||||
|
className="h-16 w-10 shrink-0 overflow-hidden rounded border border-white/10 bg-black/45 transition hover:border-cyan-300/40"
|
||||||
|
title={`参考帧 ${frame.timestamp.toFixed(1)}s`}
|
||||||
|
>
|
||||||
|
<img src={effectiveFrameUrl(job.id, frame)} alt="" className="h-full w-full object-cover" />
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="mb-2 line-clamp-2 text-white/34" title={row.referencePlan}>{row.referencePlan}</p>
|
||||||
|
)}
|
||||||
|
<div className="line-clamp-2 text-[10.5px] text-white/38" title={row.keyElements}>
|
||||||
|
<ImageIcon className="mr-1 inline h-3 w-3" />
|
||||||
|
{row.keyElements}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<button
|
||||||
{refs.length ? (
|
type="button"
|
||||||
<div className="flex items-center gap-1.5">
|
onClick={() => addReferenceFrame(row)}
|
||||||
{refs.map((frame) => (
|
disabled={!onAddFrame || busy}
|
||||||
<button
|
className="mt-2 inline-flex h-8 w-full items-center justify-center gap-1 rounded-md border border-white/10 bg-white/[0.055] px-2 text-[11px] text-white/70 transition hover:border-white/25 hover:bg-white/[0.1] disabled:cursor-not-allowed disabled:opacity-45"
|
||||||
key={frame.index}
|
>
|
||||||
type="button"
|
{busy ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Scissors className="h-3.5 w-3.5" />}
|
||||||
onClick={() => onOpenFrame?.(frame.index)}
|
{refs.length ? "补抽参考帧" : "抽参考帧"}
|
||||||
className="h-12 w-8 overflow-hidden rounded border border-white/10 bg-black/45"
|
</button>
|
||||||
title={`参考帧 ${frame.timestamp.toFixed(1)}s`}
|
</StoryboardPlanCell>
|
||||||
>
|
|
||||||
<img src={effectiveFrameUrl(job.id, frame)} alt="" className="h-full w-full object-cover" />
|
<StoryboardPlanCell label="生成视频" className="xl:border-r-0">
|
||||||
</button>
|
{rowVideos.length > 0 ? (
|
||||||
))}
|
<div className="mb-2 flex gap-1.5 overflow-x-auto pb-1">
|
||||||
</div>
|
{rowVideos.map((video) => (
|
||||||
) : (
|
<StoryboardVideoPreview key={video.id} job={job} video={video} />
|
||||||
<span className="text-white/32">{row.referencePlan}</span>
|
))}
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1.5">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => addReferenceFrame(row)}
|
|
||||||
disabled={!onAddFrame || busy}
|
|
||||||
className="inline-flex h-8 w-full items-center justify-center gap-1 rounded-md border border-white/10 bg-white/[0.055] px-2 text-[11px] text-white/70 transition hover:border-white/25 hover:bg-white/[0.1] disabled:cursor-not-allowed disabled:opacity-45"
|
|
||||||
>
|
|
||||||
{busy ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Scissors className="h-3.5 w-3.5" />}
|
|
||||||
抽参考帧
|
|
||||||
</button>
|
|
||||||
<div className="flex items-center gap-1 text-[10px] text-white/35">
|
|
||||||
<ImageIcon className="h-3 w-3" />
|
|
||||||
{refs.length ? "下一步提元素" : "先抽帧"}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
) : (
|
||||||
</div>
|
<div className="mb-2 flex h-20 items-center justify-center rounded-md border border-dashed border-white/12 bg-black/25 text-[11px] text-white/30">
|
||||||
)
|
{refs.length ? "等待生成" : "先抽参考帧"}
|
||||||
})}
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => generateRowVideo(row, refs)}
|
||||||
|
disabled={!refs.length || !onGenerateVideo || generating}
|
||||||
|
className="inline-flex h-8 w-full items-center justify-center gap-1 rounded-md bg-white px-2 text-[11px] font-semibold text-black transition hover:bg-white/90 disabled:cursor-not-allowed disabled:opacity-40"
|
||||||
|
>
|
||||||
|
{generating ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Play className="h-3.5 w-3.5" />}
|
||||||
|
生成本条
|
||||||
|
</button>
|
||||||
|
</StoryboardPlanCell>
|
||||||
|
</article>
|
||||||
|
)
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<EmptyState text="音频解析完成后,这里会按逐句时间轴生成 SKG 分镜规划表。先看结构,再按分镜定向抽参考帧。" />
|
<EmptyState text="音频解析完成后,这里会按逐句时间轴生成信息流复刻分镜工作台。先看结构,再按分镜定向抽参考帧和生成视频。" />
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function StoryboardPlanCell({ label, children, className = "" }: { label: string; children: ReactNode; className?: string }) {
|
||||||
|
return (
|
||||||
|
<div className={`min-w-0 border-b border-white/8 p-2.5 xl:border-b-0 xl:border-r ${className}`}>
|
||||||
|
<div className="mb-1.5 text-[10px] font-semibold uppercase tracking-[0.12em] text-white/32">{label}</div>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<a
|
||||||
|
href={src || undefined}
|
||||||
|
target={src ? "_blank" : undefined}
|
||||||
|
rel={src ? "noreferrer" : undefined}
|
||||||
|
className="group relative h-20 w-12 shrink-0 overflow-hidden rounded border border-white/10 bg-black/45"
|
||||||
|
title={`${video.model} · ${video.status}`}
|
||||||
|
>
|
||||||
|
{src && video.status === "completed" ? (
|
||||||
|
<video src={src} poster={poster} muted playsInline className="h-full w-full object-cover" />
|
||||||
|
) : poster ? (
|
||||||
|
<img src={poster} alt={`片段 ${shortId(video.id)}`} className="h-full w-full object-cover opacity-80" />
|
||||||
|
) : (
|
||||||
|
<div className="flex h-full w-full items-center justify-center text-white/30"><Film className="h-4 w-4" /></div>
|
||||||
|
)}
|
||||||
|
<span className="absolute bottom-1 left-1 right-1 truncate rounded bg-black/70 px-1 py-0.5 text-center font-mono text-[9px] text-white/62">
|
||||||
|
{running ? "生成中" : video.status === "failed" ? "失败" : shortId(video.id)}
|
||||||
|
</span>
|
||||||
|
{running && <Loader2 className="absolute right-1 top-1 h-3 w-3 animate-spin text-cyan-100" />}
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function AudioWaveform({
|
function AudioWaveform({
|
||||||
features,
|
features,
|
||||||
status,
|
status,
|
||||||
|
|||||||
Reference in New Issue
Block a user