diff --git a/docs/source-analysis.html b/docs/source-analysis.html
index c3620c6..2f7bb06 100644
--- a/docs/source-analysis.html
+++ b/docs/source-analysis.html
@@ -593,8 +593,9 @@
web/next.config.mjs | Next.js 构建配置:静态导出、图片不走优化、禁用开发环境左下角 Next Dev Indicator,并移除 Next 16 已不支持的 eslint 顶层配置,避免本地 dev 出现配置 Issue 提示。 |
web/app/globals.css | 全局主题变量、登录页视觉样式、信息流工作台同源品牌 token、ReactFlow 样式引用,以及本地开发态 nextjs-portal 遮挡隐藏规则。工作台在 skg-board-theme 内定义 --skg-gold-1、--skg-gold-2、--skg-cream、--skg-bg-*、--skg-text-*、--skg-radius-* 和按钮阴影等变量,并新增 skg-board-brand、skg-stat-card、skg-primary-action、skg-secondary-action、skg-empty-state 等样式。暗色工作台复用登录页金色聚焦、米白主按钮和弱暖光氛围;明亮模式通过 skg-board-theme--light 复用同一套结构,改成暖白底、白色 panel、黑底主 CTA 和深色文本,不另起一套界面。 |
web/app/page.tsx | 产品工作台主状态:jobs、activeJobId、生成任务状态;主渲染为全屏素材输入列 + 信息流广告复刻工作表;“开始分析”会把 job 放入并行素材分析队列,下载完成后触发 triggerTranscribe 解析音频,并触发 analyzeJob 自动抽 12 张参考帧,形成“音频文案路 + 视频视觉路”同步推进;音频失败时会忽略失败状态下残留的半成品 transcript,允许再次触发音频解析;底部吸附音频条和旧全局浮动主题按钮不再从主界面渲染,避免和工作台内的明暗模式切换重复。 |
- web/components/ad-recreation-board.tsx | 信息流广告复刻工作表:顶部先展示与登录页连续的 SKG brand strip,包含 SKG 字标、“未来健康 · 营销内容工作台”和“营销内容工作台 · TK 二创”;右侧素材/任务/视频/文案统计改为米白 stat 卡片,主动作按钮统一走 skg-primary-action,次动作走 skg-secondary-action,空状态复用 AnimatedLoginCharacters。工作台外层以 1800x1000 为基准画布,通过 ResizeObserver 按可见宽度选择人工缩放档位,并用 CSS zoom 等比放大/缩小;常见桌面宽度会落到稳定比例,保留适度左右呼吸感,必要时允许纵向滚动;不同显示器打开时核心列宽、主体管线和分镜行仍按同一套基准布局,不随 xl/2xl 断点重排,也避免 transform: scale() 小数缩放造成整屏文字发虚。buildWorkflowSteps 仍统一生成 01-09 流程顺序、状态和判定依据,WorkflowStepBadge / PipelineLane / 分镜列标题也继续共用同一套编号;但完整 WorkflowOrderBar、右侧素材/视频/音频/文案/参考帧需求 chips、文案依据下拉和“音频文案、抽帧参考、主体重构、产品素材池”四个状态条不再默认渲染在工作区顶部。左侧素材输入只负责链接/上传和任务切换,不再重复放横版原视频预览;右侧源视频工作区直接进入核心操作。讲话人、节奏和背景音分析仍写入 AudioScript,但不再作为“音频解析结果”卡片默认渲染;主工作区左侧宽度调整为 430-460px,上方是按 9:16 显示的竖版原视频播放器,播放器内覆盖“当前点抽帧”,按当前播放秒数手动补参考帧,播放器下方是逐句时间轴,英文和中文都最多显示两行;右侧上方是无标题的波形与切点参考框,下方主体链路改为上方参考帧池 + 转换层、下方主体元素结果栏。音频波形用参考图式的连续灰色包络显示响度、停顿和密集爆点,顶部把低/中/高密度按钮和当前播放秒数、总时长、鼠标指针停点秒数直接放在波形上方。视频播放时通过 requestAnimationFrame 平滑驱动波形播放线,同时同步高亮并滚动当前句;点击音频波形或字幕行会跳转原视频时间。音频波形下方同框渲染无标题的 TimelineFilmstrip 临时画面胶片,前端按低/中/高密度从源视频 canvas 截取预览缩略图,并按 frame.time / duration 的百分比定位到和波形同一条时间轴上;波形与胶片之间不显示分隔横线,胶片轨道贴近波形,缩略图轻微上下错落并倾斜重叠排列,hover 时用同一张胶片卡在原位置生成固定顶层克隆,约 4.8 倍放大并自动限制在视口内,避免被工作区、滚动容器或相邻面板遮挡;单击胶片只跳转原视频时间,不写入任务数据,双击胶片或拖进参考帧池时才调用手动抽帧并正式加入 job.frames,已加入的胶片显示“已添加”;胶片预览按 job、视频、密度和时长缓存,未切换低/中/高时返回页面不重新扫视频。参考帧池的主入口是“自动抽帧 12 张”,一键按动作峰值目标重新抽取 12 张源视频参考帧,优先抓手势、表情变化、节奏点和镜头变化;缩略图按竖版完整比例显示不裁切,点选状态直接叠在参考帧池缩略图上,鼠标停留会通过固定浮层放大展示完整帧。转换层改为轻量对话式生图确认区并拿到主操作宽度:左侧参考帧可点 + 或直接拖入转换层,本地图片拖入会通过 uploadReferenceFrame 保存为参考帧;转换层上方是参考输入区,下方只显示当前生成要求摘要、保留元素和收起的对话计数,并保留带张数控件的“发送消息”输入 composer;模型确认类回复不再逐条展示,生成英文 prompt 后仍在固定弹窗里确认后才调用主体套图生成。主体元素结果栏在转换层下方,空态只占紧凑提示;有结果时按每次生成的套图文件夹显示,左侧横向展示当前套图,右侧切换套图包,保留单张重生和删除;缩略图上提供“重新生成这一张”和“删除这一张”,单张重生会用 replace_views=true 替换同一视角。前端对卡通重构传 subject_style=cartoon_subject,其他方向传 subject_style=source_actor;形象锁定或自主描述空文本可走 reconstruction_mode=same,其他参考创新走 similar 并把参考帧作为 /images/edits 的 image refs 一起提交。主体生成完成后会形成 subject_consensus_brief。音频结果下方是信息流复刻分镜工作台:顶部产品参考区是“同一产品素材池”,不限量上传产品图,不做不同产品身份判断;上传原图推荐长边 1200-2000px、短边至少 600px,但后端会统一生成最长边 1600px、JPEG 92 的 AI 工作副本,并回显尺寸、自动转换和风险标注;上传后按“套在脖子上的 U 形肩颈按摩仪”进行同一产品批量识别,左/右按佩戴者身体左右、上/下按佩戴方向,额外标注内外侧、开口方向、局部结构点、背景类型、用途标签、生成风险和备注,用户只检查备注,鼠标悬停通过固定浮层显示大图预览,能盖过滚动容器和分镜框架;缺视角补图失败时保留重试入口。脚本区在分镜行上方提供“作者想法”和“整片改写”,每行新口播文案可直接编辑并可单段 AI 改写。每条音频分镜默认是左侧三字段、右侧横向视频候选轨;高级区仍保留首尾帧 prompt、产品出现方式和旧 6 字段。ModelTrace 会在音频解析、产品识别/补图、主体重构视图包、脚本改写等入口旁直接展示模型名;生图入口会显示 gpt-image-2 / gemini-3-pro-image-preview 链路和短时熔断规则,点击后用固定浮层展示模型链路、输入输出和回退逻辑。旧分镜卡、抽帧控制和视频生成组件仍保留在文件里,但当前主路径不渲染。 |
- SourceSubjectPipeline | 源视频工作区主体管线主路径:上方是竖向 参考帧池 和宽幅 转换层,下方是 主体元素 结果栏。参考帧池保留自动 12 张、胶片拖入正式成帧、点击勾选和删除;参考帧缩略图保持小尺寸固定宽度、aspect-[9/16] 和 object-contain 显示,hover 预览通过 MediaAssetTile 的左侧紧凑浮层显示,并新增 + 操作把参考帧送入转换层。转换层是轻量对话式生图确认区:顶部选择 GPT 套件或 Gemini 套件,参考输入区支持左侧 +、拖拽参考帧、胶片拖入和本地图片拖拽上传(上传图会写入 job.frames),下方固定为生成要求摘要和带张数控件的“发送消息”输入 composer;摘要只显示当前要求、保留元素和收起记录计数,不再逐条显示模型确认话术,复刻、参考创意换人物、卡通风格和人物占比等常用意图也不再显示为独立快捷 chip;识别结果里的特征 chip 是“保留元素”选择,点亮表示随下一条消息提交给 subject-agent/message,再次点击取消,清空按钮一次性取消全部,单次点击不再直接请求模型;subject-agent/message 返回英文 generation_prompt_en 后先显示待确认 prompt,并通过固定弹窗展示用户要求、最终英文提示词、模型套件、方向和数量,用户点“确定生成”才调用 generateSubjectAssets。后端会为每次主体套图注入同一份 pack bible:参考创新模式锁定同一个全新主体和同一套服装,源形象锁定模式锁定参考帧里的可见主体、体态、发型、服装和配色;后处理会裁出白底主体并允许放大到画布高度上限约 96%,实测典型主体有效高度约 90%,避免模型生成“小人 + 大白边”。主体元素结果栏按每次生成的 pack_id 组织成“套图文件夹”:左侧横向展开当前选中套图,右侧显示可滚动的套图包列表;同一方向可保留多套,生成中按 pack 显示 2/6 这类进度,单张完成就替换对应占位卡;空态只显示紧凑提示,不再占右侧整列。缩略图复用 MediaAssetTile,支持 hover 放大、单张重生和删除。旧下方 SourceReferenceBuildPanel 不再主路径渲染。 |
+ web/components/ad-recreation-board.tsx | 信息流广告复刻工作表:顶部先展示与登录页连续的 SKG brand strip,包含 SKG 字标、“未来健康 · 营销内容工作台”和“营销内容工作台 · TK 二创”;右侧素材/任务/视频/文案统计改为米白 stat 卡片,主动作按钮统一走 skg-primary-action,次动作走 skg-secondary-action,空状态复用 AnimatedLoginCharacters。工作台外层以 1800x1000 为基准画布,通过 ResizeObserver 按可见宽度选择人工缩放档位,并用 CSS zoom 等比放大/缩小;常见桌面宽度会落到稳定比例,保留适度左右呼吸感,必要时允许纵向滚动;不同显示器打开时核心列宽、主体管线和分镜行仍按同一套基准布局,不随 xl/2xl 断点重排,也避免 transform: scale() 小数缩放造成整屏文字发虚。buildWorkflowSteps 仍统一生成 01-09 流程顺序、状态和判定依据,WorkflowStepBadge / PipelineLane / 分镜列标题也继续共用同一套编号;但完整 WorkflowOrderBar、右侧素材/视频/音频/文案/参考帧需求 chips、文案依据下拉和“音频文案、抽帧参考、主体重构、产品素材池”四个状态条不再默认渲染在工作区顶部。左侧素材输入只负责链接/上传和任务切换,不再重复放横版原视频预览;右侧源视频工作区直接进入核心操作。讲话人、节奏和背景音分析仍写入 AudioScript,但不再作为“音频解析结果”卡片默认渲染;源视频工作区右上新增“布局调节”临时面板,默认左列 360px,并允许在当前浏览器内调左列宽、视频高度、逐句时间轴高度、参考帧池宽度、转换层高度和主体空态高度;上方是按 9:16 显示的竖版原视频播放器,播放器内覆盖“当前点抽帧”,按当前播放秒数手动补参考帧,播放器下方是逐句时间轴,英文和中文都最多显示两行;右侧上方是无标题的波形与切点参考框,下方主体链路改为上方参考帧池 + 转换层、下方主体元素结果栏。音频波形用参考图式的连续灰色包络显示响度、停顿和密集爆点,顶部把低/中/高密度按钮和当前播放秒数、总时长、鼠标指针停点秒数直接放在波形上方。视频播放时通过 requestAnimationFrame 平滑驱动波形播放线,同时同步高亮并滚动当前句;点击音频波形或字幕行会跳转原视频时间。音频波形下方同框渲染无标题的 TimelineFilmstrip 临时画面胶片,前端按低/中/高密度从源视频 canvas 截取预览缩略图,并按 frame.time / duration 的百分比定位到和波形同一条时间轴上;波形与胶片之间不显示分隔横线,胶片轨道贴近波形,缩略图轻微上下错落并倾斜重叠排列,hover 时用同一张胶片卡在原位置生成固定顶层克隆,约 4.8 倍放大并自动限制在视口内,避免被工作区、滚动容器或相邻面板遮挡;单击胶片只跳转原视频时间,不写入任务数据,双击胶片或拖进参考帧池时才调用手动抽帧并正式加入 job.frames,已加入的胶片显示“已添加”;胶片预览按 job、视频、密度和时长缓存,未切换低/中/高时返回页面不重新扫视频。参考帧池的主入口是“自动抽帧 12 张”,一键按动作峰值目标重新抽取 12 张源视频参考帧,优先抓手势、表情变化、节奏点和镜头变化;缩略图按竖版完整比例显示不裁切,点选状态直接叠在参考帧池缩略图上,鼠标停留会通过固定浮层放大展示完整帧。转换层改为轻量对话式生图确认区并拿到主操作宽度:左侧参考帧可点 + 或直接拖入转换层,本地图片拖入会通过 uploadReferenceFrame 保存为参考帧;转换层上方是参考输入区,下方只显示当前生成要求摘要、保留元素和收起的对话计数,并保留带张数控件的“发送消息”输入 composer;模型确认类回复不再逐条展示,生成英文 prompt 后仍在固定弹窗里确认后才调用主体套图生成。主体元素结果栏在转换层下方,空态只占紧凑提示;有结果时按每次生成的套图文件夹显示,左侧横向展示当前套图,右侧切换套图包,保留单张重生和删除;缩略图上提供“重新生成这一张”和“删除这一张”,单张重生会用 replace_views=true 替换同一视角。前端对卡通重构传 subject_style=cartoon_subject,其他方向传 subject_style=source_actor;形象锁定或自主描述空文本可走 reconstruction_mode=same,其他参考创新走 similar 并把参考帧作为 /images/edits 的 image refs 一起提交。主体生成完成后会形成 subject_consensus_brief。音频结果下方是信息流复刻分镜工作台:顶部产品参考区是“同一产品素材池”,不限量上传产品图,不做不同产品身份判断;上传原图推荐长边 1200-2000px、短边至少 600px,但后端会统一生成最长边 1600px、JPEG 92 的 AI 工作副本,并回显尺寸、自动转换和风险标注;上传后按“套在脖子上的 U 形肩颈按摩仪”进行同一产品批量识别,左/右按佩戴者身体左右、上/下按佩戴方向,额外标注内外侧、开口方向、局部结构点、背景类型、用途标签、生成风险和备注,用户只检查备注,鼠标悬停通过固定浮层显示大图预览,能盖过滚动容器和分镜框架;缺视角补图失败时保留重试入口。脚本区在分镜行上方提供“作者想法”和“整片改写”,每行新口播文案可直接编辑并可单段 AI 改写。每条音频分镜默认是左侧三字段、右侧横向视频候选轨;高级区仍保留首尾帧 prompt、产品出现方式和旧 6 字段。ModelTrace 会在音频解析、产品识别/补图、主体重构视图包、脚本改写等入口旁直接展示模型名;生图入口会显示 gpt-image-2 / gemini-3-pro-image-preview 链路和短时熔断规则,点击后用固定浮层展示模型链路、输入输出和回退逻辑。旧分镜卡、抽帧控制和视频生成组件仍保留在文件里,但当前主路径不渲染。 |
+ SourceSubjectPipeline | 源视频工作区主体管线主路径:上方是竖向 参考帧池 和宽幅 转换层,下方是 主体元素 结果栏。参考帧池宽度、转换层高度和主体空态高度可临时受 SourceWorkspaceLayoutPanel 调参值控制;转换层内容过多时在自身区域内滚动,不再继续撑高外层。参考帧池保留自动 12 张、胶片拖入正式成帧、点击勾选和删除;参考帧缩略图保持小尺寸固定宽度、aspect-[9/16] 和 object-contain 显示,hover 预览通过 MediaAssetTile 的左侧紧凑浮层显示,并新增 + 操作把参考帧送入转换层。转换层是轻量对话式生图确认区:顶部选择 GPT 套件或 Gemini 套件,参考输入区支持左侧 +、拖拽参考帧、胶片拖入和本地图片拖拽上传(上传图会写入 job.frames),下方固定为生成要求摘要和带张数控件的“发送消息”输入 composer;摘要只显示当前要求、保留元素和收起记录计数,不再逐条显示模型确认话术,复刻、参考创意换人物、卡通风格和人物占比等常用意图也不再显示为独立快捷 chip;识别结果里的特征 chip 是“保留元素”选择,点亮表示随下一条消息提交给 subject-agent/message,再次点击取消,清空按钮一次性取消全部,单次点击不再直接请求模型;subject-agent/message 返回英文 generation_prompt_en 后先显示待确认 prompt,并通过固定弹窗展示用户要求、最终英文提示词、模型套件、方向和数量,用户点“确定生成”才调用 generateSubjectAssets。后端会为每次主体套图注入同一份 pack bible:参考创新模式锁定同一个全新主体和同一套服装,源形象锁定模式锁定参考帧里的可见主体、体态、发型、服装和配色;后处理会裁出白底主体并允许放大到画布高度上限约 96%,实测典型主体有效高度约 90%,避免模型生成“小人 + 大白边”。主体元素结果栏按每次生成的 pack_id 组织成“套图文件夹”:左侧横向展开当前选中套图,右侧显示可滚动的套图包列表;同一方向可保留多套,生成中按 pack 显示 2/6 这类进度,单张完成就替换对应占位卡;空态只显示紧凑提示,不再占右侧整列。缩略图复用 MediaAssetTile,支持 hover 放大、单张重生和删除。旧下方 SourceReferenceBuildPanel 不再主路径渲染。 |
+ SourceWorkspaceLayoutPanel | 源视频工作区临时调参面板:右上“布局调节”展开后可调左列宽、视频高度、逐句时间轴高度、参考帧池宽度、转换层高度和主体空态高度;调参值写入 localStorage["skg-source-workspace-layout:v1"],只影响当前浏览器。该面板用于和用户现场确认合适比例,确认后再把参数固化为默认布局。 |
AudioStoryboardPlanPanel 三字段候选生成 | 当前分镜主路径:每行是左右双栏,左侧默认显示 skg_copy_*、scene_one_line_*、action_one_line_* 三组中英字段,右侧直接显示视频候选横向轨。用户改中文镜像后,字段失焦会通过 refineStoryboard 优化对应英文主值,失败时退回 translateText;英文仍是后续 prompt 主值。quickPlanStoryboard 把三字段和主体 brief 展开为完整 StoryboardScene,generateStoryboardVideo 的 count 可由单行数字控件选择,候选新生成后持续向右追加,不再用 4-grid 撑高每行。整片生成同样可选择每行数量,并以 concurrency=1 按行排队提交。产品素材池、批量控制、每行主体区和高级区都可折叠,高级抽屉仍展示旧 6 字段、首尾帧 prompt 和首尾帧资产槽,但客户默认不用先处理首尾帧。 |
web/components/resource-library/library-drawer.tsx | 全局资源中心浮窗:由工作台顶部“资源库”按钮打开,叠加在工作台上方但不阻塞主界面;尺寸、位置和当前 Tab 写入 localStorage["skg-resource-library-drawer"]。提示词 Tab 固定 5 列(场景描述、视频描述、主体描述、SKG 文案、产品角度),每列先显示 use_count 排名前 5 的“常用”,再按月份倒序分组;提示词节点常驻复制按钮,hover 可选英文/中文/双语复制,并调用 use 接口。素材 Tab 固定 4 列(主体、产品、场景、视频),节点不可拖动,按月份倒序硬编码排列;“应用到当前 job”只调用后端复制接口,得到普通 ImageRef(kind="asset") 后再写入产品素材池或复制 ID。浮窗顶部最近 24 小时横条混合显示提示词和素材;新建提示词、上传素材、删除前查引用、详情侧栏都在该组件内完成。 |
AdRecreationBoard 主题切换 | 顶部指标区左侧有“明亮/暗色”按钮,使用 Sun / Moon 图标切换 skg-board-theme--light 类名,并把选择写入 localStorage["skg-board-theme"]。暗色仍是默认模式;明亮模式只改变工作台外观,不改变任务、素材、分镜、模型调用或接口数据。 |
@@ -1132,6 +1133,18 @@ ProductRefStateItem {
变更记录
这个记录不是 git log 的替代品。它记录“产品理解发生了什么变化、影响了哪些源码、你以后描述需求时该怎么说”。后续每次改功能都要补一条。
+
+
+ 2026-05-20 · 源视频工作区增加临时布局调节
+ UI
+ Layout
+
+
+
问题:左侧 9:16 原版视频和逐句时间轴实际信息密度偏低,但固定列宽难以一次判断准确;转换层高度又会影响主体元素位置。
+
改动:AudioIntakePanel 新增“布局调节”面板,临时开放左列宽、视频高度、逐句时间轴高度、参考帧池宽度、转换层高度和主体空态高度 6 个滑杆,写入当前浏览器 localStorage["skg-source-workspace-layout:v1"]。转换层高度改为受控区域,内容过多时内部滚动。
+
影响:用户可以先在线调整到合适比例,后续再把确认后的参数固化为默认值;任务数据、模型调用和素材生成接口不受影响。
+
+
2026-05-20 · 主体元素下移为结果栏
diff --git a/web/components/ad-recreation-board.tsx b/web/components/ad-recreation-board.tsx
index c403106..09c70ee 100644
--- a/web/components/ad-recreation-board.tsx
+++ b/web/components/ad-recreation-board.tsx
@@ -4,7 +4,7 @@ import { type DragEvent as ReactDragEvent, type MouseEvent as ReactMouseEvent, t
import { createPortal } from "react-dom"
import {
AlertTriangle, BookOpen, Check, ChevronDown, Circle, Film, FileText, Image as ImageIcon, Info, Link2, Loader2, Minus,
- MessageSquare, Mic, Moon, Package, PanelRight, Play, Plus, RefreshCw, Scissors, Send, Sparkles, Sun, Trash2, Upload, Wand2,
+ MessageSquare, Mic, Moon, Package, PanelRight, Play, Plus, RefreshCw, RotateCcw, Scissors, Send, SlidersHorizontal, Sparkles, Sun, Trash2, Upload, Wand2,
} from "lucide-react"
import { toast } from "sonner"
import {
@@ -108,6 +108,34 @@ const BOARD_FRAME_HEIGHT = 1000
const BOARD_MIN_SCALE = 0.72
const BOARD_MAX_SCALE = 1.6
const BOARD_SCALE_PRESETS = [0.72, 0.76, 0.8, 0.86, 0.92, 1, 1.06, 1.16, 1.24, 1.34, 1.48, 1.6]
+const SOURCE_WORKSPACE_LAYOUT_STORAGE_KEY = "skg-source-workspace-layout:v1"
+
+type SourceWorkspaceLayout = {
+ leftWidth: number
+ videoHeight: number
+ transcriptHeight: number
+ referenceWidth: number
+ conversionHeight: number
+ subjectEmptyHeight: number
+}
+
+const DEFAULT_SOURCE_WORKSPACE_LAYOUT: SourceWorkspaceLayout = {
+ leftWidth: 360,
+ videoHeight: 510,
+ transcriptHeight: 260,
+ referenceWidth: 140,
+ conversionHeight: 500,
+ subjectEmptyHeight: 78,
+}
+
+const SOURCE_WORKSPACE_LAYOUT_LIMITS: Record = {
+ leftWidth: { min: 320, max: 460, step: 10, label: "左列宽", suffix: "px" },
+ videoHeight: { min: 430, max: 560, step: 10, label: "视频高", suffix: "px" },
+ transcriptHeight: { min: 180, max: 360, step: 10, label: "时间轴高", suffix: "px" },
+ referenceWidth: { min: 118, max: 180, step: 2, label: "参考池宽", suffix: "px" },
+ conversionHeight: { min: 420, max: 640, step: 10, label: "转换层高", suffix: "px" },
+ subjectEmptyHeight: { min: 56, max: 140, step: 4, label: "主体空态", suffix: "px" },
+}
const resolveBoardScale = (viewportWidth: number) => {
const maxFitScale = clampNumber(viewportWidth / BOARD_FRAME_WIDTH, BOARD_MIN_SCALE, BOARD_MAX_SCALE)
@@ -718,6 +746,25 @@ function clampNumber(value: number, min: number, max: number) {
return Math.min(max, Math.max(min, value))
}
+function normalizeSourceWorkspaceLayout(value: Partial = {}): SourceWorkspaceLayout {
+ const next = { ...DEFAULT_SOURCE_WORKSPACE_LAYOUT, ...value }
+ return Object.fromEntries(
+ (Object.keys(DEFAULT_SOURCE_WORKSPACE_LAYOUT) as Array).map((key) => {
+ const limits = SOURCE_WORKSPACE_LAYOUT_LIMITS[key]
+ return [key, clampNumber(Number(next[key]) || DEFAULT_SOURCE_WORKSPACE_LAYOUT[key], limits.min, limits.max)]
+ }),
+ ) as SourceWorkspaceLayout
+}
+
+function loadSourceWorkspaceLayout() {
+ if (typeof window === "undefined") return DEFAULT_SOURCE_WORKSPACE_LAYOUT
+ try {
+ return normalizeSourceWorkspaceLayout(JSON.parse(window.localStorage.getItem(SOURCE_WORKSPACE_LAYOUT_STORAGE_KEY) || "{}"))
+ } catch {
+ return DEFAULT_SOURCE_WORKSPACE_LAYOUT
+ }
+}
+
async function decodeAudioFeatures(url: string, targetFrames = 640): Promise {
const res = await fetch(url)
if (!res.ok) throw new Error(`audio ${res.status}`)
@@ -2729,6 +2776,62 @@ function MaterialColumn({
)
}
+function SourceWorkspaceLayoutPanel({
+ layout,
+ onChange,
+ onReset,
+}: {
+ layout: SourceWorkspaceLayout
+ onChange: (layout: SourceWorkspaceLayout) => void
+ onReset: () => void
+}) {
+ const update = (key: keyof SourceWorkspaceLayout, value: number) => {
+ onChange(normalizeSourceWorkspaceLayout({ ...layout, [key]: value }))
+ }
+
+ return (
+
+
+
+
临时布局调节
+
只保存在当前浏览器;你调准后再固化默认值。
+
+
+
+
+
+ )
+}
+
function AudioIntakePanel({
job,
selectedFrames,
@@ -2759,6 +2862,8 @@ function AudioIntakePanel({
const [filmstripStatus, setFilmstripStatus] = useState("idle")
const [filmstripDragTime, setFilmstripDragTime] = useState(null)
const [filmstripBusyTime, setFilmstripBusyTime] = useState(null)
+ const [layoutOpen, setLayoutOpen] = useState(false)
+ const [workspaceLayout, setWorkspaceLayout] = useState(() => loadSourceWorkspaceLayout())
const videoRef = useRef(null)
const transcriptScrollRef = useRef(null)
const rowRefs = useRef>({})
@@ -2784,6 +2889,12 @@ function AudioIntakePanel({
? `当前句 ${activeSegment.start.toFixed(1)}-${activeSegment.end.toFixed(1)}s`
: "指针 -"
+ useEffect(() => {
+ try {
+ window.localStorage.setItem(SOURCE_WORKSPACE_LAYOUT_STORAGE_KEY, JSON.stringify(workspaceLayout))
+ } catch { /* ignore unavailable storage */ }
+ }, [workspaceLayout])
+
useEffect(() => {
if (!job?.id || !audioSrcUrl) {
setAudioFeatures([])
@@ -2959,21 +3070,49 @@ function AudioIntakePanel({
} title="源视频工作区" />
-
-
{job.transcript.length} 段
-
{formatSeconds(job.duration)}
+
+
+ {job.transcript.length} 段
+ {formatSeconds(job.duration)}
+
+
+ {layoutOpen ? (
+
setWorkspaceLayout(DEFAULT_SOURCE_WORKSPACE_LAYOUT)}
+ />
+ ) : null}
-
+
} title="原版视频" />
{currentTime.toFixed(1)}s
-
+
{job.video_url ? (
@@ -3069,6 +3209,7 @@ function AudioIntakePanel({
runtimeModels={runtimeModels}
filmstripDragging={filmstripDragTime !== null}
onDropFilmstripFrame={(time) => addFilmstripFrame(time)}
+ layout={workspaceLayout}
/>
@@ -3085,6 +3226,7 @@ function TranscriptTimelinePanel({
activeSegmentIndex,
scrollRef,
rowRefs,
+ maxHeight,
onSeek,
}: {
job: Job
@@ -3092,6 +3234,7 @@ function TranscriptTimelinePanel({
activeSegmentIndex: number | null
scrollRef: RefObject
rowRefs: { current: Record }
+ maxHeight: number
onSeek: (time: number) => void
}) {
return (
@@ -3106,7 +3249,7 @@ function TranscriptTimelinePanel({
时间
原文 / 中文
-
+
{job.transcript.map((segment) => {
const active = activeSegmentIndex === segment.index
return (
@@ -3364,6 +3507,7 @@ function SourceSubjectPipeline({
runtimeModels,
filmstripDragging,
onDropFilmstripFrame,
+ layout,
}: {
job: Job
frames: KeyFrame[]
@@ -3377,6 +3521,7 @@ function SourceSubjectPipeline({
runtimeModels?: RuntimeModels
filmstripDragging?: boolean
onDropFilmstripFrame?: (time: number) => Promise
| KeyFrame | null | void
+ layout: SourceWorkspaceLayout
}) {
const [referenceDropActive, setReferenceDropActive] = useState(false)
const [agentDropActive, setAgentDropActive] = useState(false)
@@ -3939,7 +4084,10 @@ function SourceSubjectPipeline({
return (
<>
-
+
} title="参考帧池" />
@@ -3990,7 +4138,7 @@ function SourceSubjectPipeline({
{frames.length} 张
{filmstripDragging ? "松手加入" : "点击选择"}
-
+
{frames.map((frame, index) => {
const selected = selectedFrames.has(frame.index)
return (
@@ -4055,7 +4203,10 @@ function SourceSubjectPipeline({
{agentReferenceFrames.length ? `${agentReferenceFrames.length}/${RECONSTRUCTION_FRAME_LIMIT} 图` : "待选图"}
-
+
{SUBJECT_MODEL_BUNDLE_OPTIONS.map((option) => (
) : (
-
+
转换层生成完成后,这里会横向展示主体套图。
)}