fix: enlarge filmstrip frames in place
This commit is contained in:
2
RULES.md
2
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`
|
||||
|
||||
@@ -598,7 +598,7 @@
|
||||
<tr><td><code>web/components/resource-library/library-drawer.tsx</code></td><td>全局资源中心浮窗:由工作台顶部“资源库”按钮打开,叠加在工作台上方但不阻塞主界面;尺寸、位置和当前 Tab 写入 <code>localStorage["skg-resource-library-drawer"]</code>。提示词 Tab 固定 5 列(场景描述、视频描述、主体描述、SKG 文案、产品角度),每列先显示 use_count 排名前 5 的“常用”,再按月份倒序分组;提示词节点常驻复制按钮,hover 可选英文/中文/双语复制,并调用 use 接口。素材 Tab 固定 4 列(主体、产品、场景、视频),节点不可拖动,按月份倒序硬编码排列;“应用到当前 job”只调用后端复制接口,得到普通 <code>ImageRef(kind="asset")</code> 后再写入产品素材池或复制 ID。浮窗顶部最近 24 小时横条混合显示提示词和素材;新建提示词、上传素材、删除前查引用、详情侧栏都在该组件内完成。</td></tr>
|
||||
<tr><td><code>AdRecreationBoard</code> 主题切换</td><td>顶部指标区左侧有“明亮/暗色”按钮,使用 <code>Sun</code> / <code>Moon</code> 图标切换 <code>skg-board-theme--light</code> 类名,并把选择写入 <code>localStorage["skg-board-theme"]</code>。暗色仍是默认模式;明亮模式只改变工作台外观,不改变任务、素材、分镜、模型调用或接口数据。</td></tr>
|
||||
<tr><td><code>SourceReferenceBuildPanel</code></td><td>“相似主体 / 主体模板”当前承担主体资产生成和主体模板复用的前端入口:顶部用 radio 区分“用模板生成”和“不用模板(从源视频关键帧创新)”,<code>源视频相似</code> 不再作为模板卡混进网格。模板库把 <code>GET /subject-templates</code> 数据库模板和 <code>GET /character-library/skg</code> 内置形象合并成 120px 竖排卡片,选中态统一用 SKG 金色;当选择“不用模板”时模板网格会收起,避免把生成按钮和结果缩略图挤到折叠区域之外。保存为主体模板的名称、备注和按钮固定在模板区底部一行。下方“生成主体视图”独立显示模型链路,支持透明骨架/真人、全部 10 / 常用 4 / 自定义视图;同时新增“主体设定”,默认随机组合性别表现、年龄段、着装风格、地域人种、肤色、体型比例、发型和气质场景,也可切到手动指定。随机组合会在点击生成时解析成一套固定 profile 并传给后端 <code>subject_profile</code>,整包视图共用同一人设,不会一张男一张女或一张年轻一张银发。已有生成结果会优先显示在生成区标题下方,再显示控制项,避免用户生成后还要继续向下找图。主体缩略图放大为可单张重生、删除和 hover 放大的媒体卡;生成中会显示本次请求锁定的素材 ID 和主体设定,切换其他模块不会改变已经提交的生成目标。前端仍传 <code>reconstruction_mode=similar</code>,后端先用 <code>VISION_MODEL</code> 把关键帧/模板图转成非身份化文字 brief;如果 brief 失败,则继续用用户方向、模板文字、内置形象 brief 和结构化主体设定。最终主体图只走 <code>gpt-image-2</code> 的 <code>/images/generations</code> 文字生图,不再把原帧或模板图作为强 image-edit 锚点。</td></tr>
|
||||
<tr><td><code>web/components/media-asset-tile.tsx</code></td><td>项目内媒体素材缩略图基底组件:图片、视频、抽帧、产品图、相似主体图、首尾帧和视频候选默认从这里获得统一交互。组件负责缩略图显示、顶层固定浮层 hover 放大、删除按钮、重新生成等操作按钮、忙碌遮罩和图片/视频共用预览,避免每个新板块重复手写不同的媒体交互。<code>previewSize</code> 支持普通和大预览,画面胶片等小缩略图使用大预览保证浮层盖过滚动框且更容易看清。</td></tr>
|
||||
<tr><td><code>web/components/media-asset-tile.tsx</code></td><td>项目内媒体素材缩略图基底组件:图片、视频、抽帧、产品图、相似主体图、首尾帧和视频候选默认从这里获得统一交互。组件负责缩略图显示、顶层固定浮层 hover 放大、删除按钮、重新生成等操作按钮、忙碌遮罩和图片/视频共用预览,避免每个新板块重复手写不同的媒体交互。画面胶片是例外:为了保持胶片原位浏览,不使用额外弹出预览,只让胶片缩略图自己在轨道内放大。</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/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>
|
||||
@@ -1116,8 +1116,8 @@ ProductRefStateItem {
|
||||
</header>
|
||||
<div class="body">
|
||||
<p><strong>问题:</strong>用户通常只需要 1-2 张关键帧,但只靠自动抽帧或当前播放点补帧,浏览大量候选画面不够快,直接抽很多帧又会污染正式关键帧池。</p>
|
||||
<p><strong>改动:</strong><code>AudioIntakePanel</code> 在 <code>AudioWaveform</code> 下方新增 <code>TimelineFilmstrip</code>,前端从源视频临时截取低/中/高密度胶片缩略图,倾斜重叠排列;hover 时缩略图轻微扶正上浮,大图预览由 <code>MediaAssetTile</code> 挂到 <code>document.body</code> 顶层固定浮层并使用更大的 <code>previewSize=large</code>,点击只跳转视频时间点,拖进 <code>SourceKeyframePicker</code> 参考帧池才调用现有手动抽帧入口。</p>
|
||||
<p><strong>影响:</strong>临时胶片不会写入 <code>job.frames</code>,只有拖入后才成为正式关键帧;放大预览不受胶片框、滚动容器或面板裁切。后续如果要调摆放密度或倾斜角度,改 <code>FILMSTRIP_DENSITIES</code> 和 <code>FILMSTRIP_TILT_CLASSES</code>。</p>
|
||||
<p><strong>改动:</strong><code>AudioIntakePanel</code> 在 <code>AudioWaveform</code> 下方新增 <code>TimelineFilmstrip</code>,前端从源视频临时截取低/中/高密度胶片缩略图,倾斜重叠排列;hover 时关闭额外弹出预览,直接让原胶片卡片扶正、上浮并放大,点击只跳转视频时间点,拖进 <code>SourceKeyframePicker</code> 参考帧池才调用现有手动抽帧入口。</p>
|
||||
<p><strong>影响:</strong>临时胶片不会写入 <code>job.frames</code>,只有拖入后才成为正式关键帧;胶片轨道增加上下留白,避免原位放大时被胶片框裁掉。后续如果要调摆放密度、倾斜角度或放大倍率,改 <code>FILMSTRIP_DENSITIES</code>、<code>FILMSTRIP_TILT_CLASSES</code> 和胶片 hover scale。</p>
|
||||
</div>
|
||||
</article>
|
||||
<article class="change">
|
||||
|
||||
@@ -2758,7 +2758,7 @@ function TimelineFilmstrip({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="min-h-[118px] overflow-x-auto overflow-y-hidden px-2 pb-8 pt-4">
|
||||
<div className="min-h-[202px] overflow-x-auto overflow-y-hidden px-14 pb-16 pt-14">
|
||||
{status === "loading" ? (
|
||||
<div className="flex h-[72px] items-center justify-center gap-2 rounded-md border border-dashed border-white/12 text-[11px] text-white/40">
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
@@ -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 · 拖到关键帧库才选取`}
|
||||
>
|
||||
<MediaAssetTile
|
||||
@@ -2798,8 +2798,7 @@ function TimelineFilmstrip({
|
||||
}`}
|
||||
mediaClassName="bg-black"
|
||||
objectFit="contain"
|
||||
previewObjectFit="contain"
|
||||
previewSize="large"
|
||||
disablePreview
|
||||
selected={selected}
|
||||
onClick={() => onSeek(frame.time)}
|
||||
title="点击跳到该时间点,拖入关键帧库才正式选取"
|
||||
|
||||
@@ -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<HTMLElement>, size: "normal" | "large" = "normal") {
|
||||
function previewPosition(event: ReactMouseEvent<HTMLElement>) {
|
||||
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<HTMLElement>) => {
|
||||
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(
|
||||
<div
|
||||
className={`pointer-events-none fixed z-[12000] ${previewWidthClass} rounded-xl border border-white/15 bg-black/94 p-3 shadow-[0_28px_80px_rgba(0,0,0,0.72)] ${previewClassName}`}
|
||||
className={`pointer-events-none fixed z-[10000] w-[min(520px,calc(100vw-32px))] rounded-xl border border-white/15 bg-black/94 p-3 shadow-[0_28px_80px_rgba(0,0,0,0.72)] ${previewClassName}`}
|
||||
style={{ left: position.left, top: position.top }}
|
||||
>
|
||||
<div className={`flex ${previewMaxHeightClass} items-center justify-center overflow-hidden rounded-lg bg-black`}>
|
||||
<div className="flex max-h-[min(76vh,720px)] items-center justify-center overflow-hidden rounded-lg bg-black">
|
||||
{kind === "video" && src ? (
|
||||
<video
|
||||
src={src}
|
||||
@@ -153,13 +148,13 @@ export function MediaAssetTile({
|
||||
playsInline
|
||||
autoPlay
|
||||
preload="auto"
|
||||
className={`${previewMediaHeightClass} max-w-full ${previewFit}`}
|
||||
className={`max-h-[min(74vh,700px)] max-w-full ${previewFit}`}
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={mediaSrc}
|
||||
alt=""
|
||||
className={`${previewMediaHeightClass} max-w-full ${previewFit}`}
|
||||
className={`max-h-[min(74vh,700px)] max-w-full ${previewFit}`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user