From cb991e7a1783507c57e7d5042b4b5ce990f1cdbc Mon Sep 17 00:00:00 2001 From: kang Date: Tue, 19 May 2026 17:42:02 +0800 Subject: [PATCH] fix: enlarge filmstrip frames in place --- RULES.md | 2 +- docs/source-analysis.html | 6 +++--- web/components/ad-recreation-board.tsx | 7 +++---- web/components/media-asset-tile.tsx | 21 ++++++++------------- 4 files changed, 15 insertions(+), 21 deletions(-) diff --git a/RULES.md b/RULES.md index 53d20eb..f2ccfae 100644 --- a/RULES.md +++ b/RULES.md @@ -15,7 +15,7 @@ ## 部署事实 - 平台:VPS `76.13.31.179`(Ubuntu 24.04 / Docker Compose / Coolify Traefik) -- 发布状态:已部署并验证(2026-05-19,逐句时间轴窄版面板 + 波形下方临时画面胶片选帧 + 胶片顶层大预览 + 隐藏源视频工作区音频解析摘要卡 + 隐藏工作区顶部状态提示条 + 三字段候选生成工作流 + 折叠紧凑候选区);`https://marketing.skg.com` 已启用应用内登录页,未登录 API 返回 401,认证后首页 200;容器内 `/health` 返回 `ok:true` +- 发布状态:已部署并验证(2026-05-19,逐句时间轴窄版面板 + 波形下方临时画面胶片选帧 + 胶片原位放大 + 隐藏源视频工作区音频解析摘要卡 + 隐藏工作区顶部状态提示条 + 三字段候选生成工作流 + 折叠紧凑候选区);`https://marketing.skg.com` 已启用应用内登录页,未登录 API 返回 401,认证后首页 200;容器内 `/health` 返回 `ok:true` - 主站 / 前端:`https://marketing.skg.com` - API / 后端:`https://marketing.skg.com/api` - 代码仓库 / Gitea:`https://git.kang-kang.com/kangwan/20260512-skg-tk` diff --git a/docs/source-analysis.html b/docs/source-analysis.html index 1bd634f..779d8ea 100644 --- a/docs/source-analysis.html +++ b/docs/source-analysis.html @@ -598,7 +598,7 @@ 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"]。暗色仍是默认模式;明亮模式只改变工作台外观,不改变任务、素材、分镜、模型调用或接口数据。 SourceReferenceBuildPanel“相似主体 / 主体模板”当前承担主体资产生成和主体模板复用的前端入口:顶部用 radio 区分“用模板生成”和“不用模板(从源视频关键帧创新)”,源视频相似 不再作为模板卡混进网格。模板库把 GET /subject-templates 数据库模板和 GET /character-library/skg 内置形象合并成 120px 竖排卡片,选中态统一用 SKG 金色;当选择“不用模板”时模板网格会收起,避免把生成按钮和结果缩略图挤到折叠区域之外。保存为主体模板的名称、备注和按钮固定在模板区底部一行。下方“生成主体视图”独立显示模型链路,支持透明骨架/真人、全部 10 / 常用 4 / 自定义视图;同时新增“主体设定”,默认随机组合性别表现、年龄段、着装风格、地域人种、肤色、体型比例、发型和气质场景,也可切到手动指定。随机组合会在点击生成时解析成一套固定 profile 并传给后端 subject_profile,整包视图共用同一人设,不会一张男一张女或一张年轻一张银发。已有生成结果会优先显示在生成区标题下方,再显示控制项,避免用户生成后还要继续向下找图。主体缩略图放大为可单张重生、删除和 hover 放大的媒体卡;生成中会显示本次请求锁定的素材 ID 和主体设定,切换其他模块不会改变已经提交的生成目标。前端仍传 reconstruction_mode=similar,后端先用 VISION_MODEL 把关键帧/模板图转成非身份化文字 brief;如果 brief 失败,则继续用用户方向、模板文字、内置形象 brief 和结构化主体设定。最终主体图只走 gpt-image-2/images/generations 文字生图,不再把原帧或模板图作为强 image-edit 锚点。 - web/components/media-asset-tile.tsx项目内媒体素材缩略图基底组件:图片、视频、抽帧、产品图、相似主体图、首尾帧和视频候选默认从这里获得统一交互。组件负责缩略图显示、顶层固定浮层 hover 放大、删除按钮、重新生成等操作按钮、忙碌遮罩和图片/视频共用预览,避免每个新板块重复手写不同的媒体交互。previewSize 支持普通和大预览,画面胶片等小缩略图使用大预览保证浮层盖过滚动框且更容易看清。 + web/components/media-asset-tile.tsx项目内媒体素材缩略图基底组件:图片、视频、抽帧、产品图、相似主体图、首尾帧和视频候选默认从这里获得统一交互。组件负责缩略图显示、顶层固定浮层 hover 放大、删除按钮、重新生成等操作按钮、忙碌遮罩和图片/视频共用预览,避免每个新板块重复手写不同的媒体交互。画面胶片是例外:为了保持胶片原位浏览,不使用额外弹出预览,只让胶片缩略图自己在轨道内放大。 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,避免登录面板或输入框遮挡时草地失去鼠标响应。 @@ -1116,8 +1116,8 @@ ProductRefStateItem {

问题:用户通常只需要 1-2 张关键帧,但只靠自动抽帧或当前播放点补帧,浏览大量候选画面不够快,直接抽很多帧又会污染正式关键帧池。

-

改动:AudioIntakePanelAudioWaveform 下方新增 TimelineFilmstrip,前端从源视频临时截取低/中/高密度胶片缩略图,倾斜重叠排列;hover 时缩略图轻微扶正上浮,大图预览由 MediaAssetTile 挂到 document.body 顶层固定浮层并使用更大的 previewSize=large,点击只跳转视频时间点,拖进 SourceKeyframePicker 参考帧池才调用现有手动抽帧入口。

-

影响:临时胶片不会写入 job.frames,只有拖入后才成为正式关键帧;放大预览不受胶片框、滚动容器或面板裁切。后续如果要调摆放密度或倾斜角度,改 FILMSTRIP_DENSITIESFILMSTRIP_TILT_CLASSES

+

改动:AudioIntakePanelAudioWaveform 下方新增 TimelineFilmstrip,前端从源视频临时截取低/中/高密度胶片缩略图,倾斜重叠排列;hover 时关闭额外弹出预览,直接让原胶片卡片扶正、上浮并放大,点击只跳转视频时间点,拖进 SourceKeyframePicker 参考帧池才调用现有手动抽帧入口。

+

影响:临时胶片不会写入 job.frames,只有拖入后才成为正式关键帧;胶片轨道增加上下留白,避免原位放大时被胶片框裁掉。后续如果要调摆放密度、倾斜角度或放大倍率,改 FILMSTRIP_DENSITIESFILMSTRIP_TILT_CLASSES 和胶片 hover scale。

diff --git a/web/components/ad-recreation-board.tsx b/web/components/ad-recreation-board.tsx index 0f7a069..1c37070 100644 --- a/web/components/ad-recreation-board.tsx +++ b/web/components/ad-recreation-board.tsx @@ -2758,7 +2758,7 @@ function TimelineFilmstrip({ -
+
{status === "loading" ? (
@@ -2785,7 +2785,7 @@ function TimelineFilmstrip({ onDragStart(frame.time) }} onDragEnd={onDragEnd} - className={`relative shrink-0 ${index ? "-ml-2.5" : ""} ${tiltClass} cursor-grab transition-transform duration-150 hover:z-30 hover:-translate-y-1.5 hover:rotate-0 active:cursor-grabbing`} + className={`relative shrink-0 ${index ? "-ml-2.5" : ""} ${tiltClass} cursor-grab transition-transform duration-150 hover:z-30 hover:-translate-y-3 hover:rotate-0 hover:scale-[2.45] active:cursor-grabbing`} title={`${frame.time.toFixed(1)}s · 拖到关键帧库才选取`} > onSeek(frame.time)} title="点击跳到该时间点,拖入关键帧库才正式选取" diff --git a/web/components/media-asset-tile.tsx b/web/components/media-asset-tile.tsx index 5896539..5432dec 100644 --- a/web/components/media-asset-tile.tsx +++ b/web/components/media-asset-tile.tsx @@ -29,7 +29,6 @@ type MediaAssetTileProps = { objectFit?: "contain" | "cover" previewObjectFit?: "contain" | "cover" previewClassName?: string - previewSize?: "normal" | "large" selected?: boolean disabled?: boolean busy?: boolean @@ -56,10 +55,10 @@ function mediaObjectClass(fit: "contain" | "cover") { return fit === "cover" ? "object-cover" : "object-contain" } -function previewPosition(event: ReactMouseEvent, size: "normal" | "large" = "normal") { +function previewPosition(event: ReactMouseEvent) { const margin = 16 - const previewWidth = Math.min(size === "large" ? 720 : 520, window.innerWidth - margin * 2) - const previewHeight = Math.min(size === "large" ? 880 : 760, window.innerHeight - margin * 2) + const previewWidth = Math.min(520, window.innerWidth - margin * 2) + const previewHeight = Math.min(760, window.innerHeight - margin * 2) let left = event.clientX + 18 let top = event.clientY + 18 if (left + previewWidth > window.innerWidth - margin) left = event.clientX - previewWidth - 18 @@ -82,7 +81,6 @@ export function MediaAssetTile({ objectFit = "contain", previewObjectFit, previewClassName = "", - previewSize = "normal", selected = false, disabled = false, busy = false, @@ -103,13 +101,10 @@ export function MediaAssetTile({ const canPreview = !!mediaSrc && !disablePreview const fit = mediaObjectClass(objectFit) const previewFit = mediaObjectClass(previewObjectFit ?? objectFit) - const previewWidthClass = previewSize === "large" ? "w-[min(720px,calc(100vw-32px))]" : "w-[min(520px,calc(100vw-32px))]" - const previewMaxHeightClass = previewSize === "large" ? "max-h-[min(84vh,860px)]" : "max-h-[min(76vh,720px)]" - const previewMediaHeightClass = previewSize === "large" ? "max-h-[min(82vh,840px)]" : "max-h-[min(74vh,700px)]" const updatePreview = (event: ReactMouseEvent) => { if (!canPreview) return - setPosition(previewPosition(event, previewSize)) + setPosition(previewPosition(event)) } const media = kind === "video" && src ? ( @@ -140,10 +135,10 @@ export function MediaAssetTile({ const preview = position && canPreview && typeof document !== "undefined" ? createPortal(
-
+
{kind === "video" && src ? (