2 Commits

Author SHA1 Message Date
1f193e95f3 docs: record fluid board layout deployment 2026-05-20 23:20:23 +08:00
6597db312b fix: remove fixed board canvas scaling 2026-05-20 23:10:03 +08:00
3 changed files with 19 additions and 59 deletions

View File

@@ -11,11 +11,12 @@
- 详见 `CLAUDE.md` 立项决策段 + `.memory/plan.md` 七步管线拆解 - 详见 `CLAUDE.md` 立项决策段 + `.memory/plan.md` 七步管线拆解
- 风格:`04-Dark-Gallery-Ambient`(路径:`~/Projects/research/20260305-网页风格库/04-Dark-Gallery-Ambient.md` - 风格:`04-Dark-Gallery-Ambient`(路径:`~/Projects/research/20260305-网页风格库/04-Dark-Gallery-Ambient.md`
- 第一冲刺:步骤 1-4下载 / 拆轨 / 关键帧 / ASR+翻译) - 第一冲刺:步骤 1-4下载 / 拆轨 / 关键帧 / ASR+翻译)
- 当前产品方向2026-05-20 再确认):信息流广告快速复刻默认进入“三字段候选生成”工作流。主界面为“左侧素材输入列 + 右侧信息流复刻工作表”;工作台 1800x1000 为基准操作画布不同显示器或浏览器宽度下保持同一框架并按常见桌面宽度落到预设缩放档位0.72/0.76/0.8/0.86/0.92/1/1.06/1.16/1.24/1.34/1.48/1.6),保留适度左右呼吸感,缩放后高度小于视口时上下居中,必要时允许纵向滚动,不通过 `xl/2xl` 断点重排核心操作区。用户粘贴 TK 链接或上传视频后点击“开始分析”,系统自动下载源视频;下载完成后并行启动两条路:音频文案路提取原音频文案/字幕,并分析讲话人、语速节奏、背景音乐/环境声/音效;视频视觉路自动抽取参考帧。源视频工作区主体链路改为“上方参考帧池 + 转换层、下方主体元素结果栏”:参考帧池竖向排列;转换层是轻量对话式生图确认区,参考图可通过左侧缩略图 `+`、参考帧拖拽、胶片拖拽或本地图片拖入进入转换层,用户选择 GPT/Gemini 套件后先分析参考图;识别结果里的特征 chip 只作为“保留元素”本地选择,点亮=保留、再点取消,点击不立即请求模型,随下一条发送消息提交;用户再在下方发送区发送复刻/创新/卡通和画面要求,界面只保留生成要求输入框、张数控件和提示词就绪状态,不展示当前要求摘要、保留元素副本、收起记录计数或重复模型确认话术,生成数量通过发送区旁边的张数控件控制;后端返回英文出图 prompt 后不再自动弹窗,发送区主按钮直接切换为“确认生成 N 张”,用户点击才生成对应数量的统一多角度套图。主体元素结果栏在转换层下方横向展示套图输出、文件夹分组、单张重生、删除和 hover 预览,空态只保留紧凑提示,不再挤占右侧整列。旧下方“相似主体 / 主体模板库”不再作为主路径。波形下方的画面胶片只是临时预览,点击只跳转原视频时间点,双击或拖进参考帧池才正式加入关键帧,已加入的胶片直接显示“已添加”。产品图上传后独立形成产品资产包,自动识别视角/结构/比例并补缺角度。分镜工作台按逐句时间轴默认只露“文案 / 场景一句话 / 人物+产品+动作”,产品素材池、批量控制、三字段、视频候选和高级区都必须可折叠;视频候选无内容时默认不占大面积,有候选时默认只显示迷你缩略条,展开后才显示 4-grid。单条默认生成 4 个视频候选,顶部支持整片批量生成候选;首尾帧、视觉规划、产品出现方式和旧 6 字段保留在“高级”抽屉与后端 quick-plan 自动展开中,不能再作为客户默认闸门。 - 当前产品方向2026-05-20 再确认):信息流广告快速复刻默认进入“三字段候选生成”工作流。主界面为“左侧素材输入列 + 右侧信息流复刻工作表”;工作台已取消 1800x1000 固定画布和整页缩放,改为正常流式桌面容器,宽度跟随浏览器展开,只保留 1280px 最低操作宽度防止核心表格被压烂,不通过应用层 `zoom` 把整页缩小导致文字发虚。用户粘贴 TK 链接或上传视频后点击“开始分析”,系统自动下载源视频;下载完成后并行启动两条路:音频文案路提取原音频文案/字幕,并分析讲话人、语速节奏、背景音乐/环境声/音效;视频视觉路自动抽取参考帧。源视频工作区主体链路改为“上方参考帧池 + 转换层、下方主体元素结果栏”:参考帧池竖向排列;转换层是轻量对话式生图确认区,参考图可通过左侧缩略图 `+`、参考帧拖拽、胶片拖拽或本地图片拖入进入转换层,用户选择 GPT/Gemini 套件后先分析参考图;识别结果里的特征 chip 只作为“保留元素”本地选择,点亮=保留、再点取消,点击不立即请求模型,随下一条发送消息提交;用户再在下方发送区发送复刻/创新/卡通和画面要求,界面只保留生成要求输入框、张数控件和提示词就绪状态,不展示当前要求摘要、保留元素副本、收起记录计数或重复模型确认话术,生成数量通过发送区旁边的张数控件控制;后端返回英文出图 prompt 后不再自动弹窗,发送区主按钮直接切换为“确认生成 N 张”,用户点击才生成对应数量的统一多角度套图。主体元素结果栏在转换层下方横向展示套图输出、文件夹分组、单张重生、删除和 hover 预览,空态只保留紧凑提示,不再挤占右侧整列。旧下方“相似主体 / 主体模板库”不再作为主路径。波形下方的画面胶片只是临时预览,点击只跳转原视频时间点,双击或拖进参考帧池才正式加入关键帧,已加入的胶片直接显示“已添加”。产品图上传后独立形成产品资产包,自动识别视角/结构/比例并补缺角度。分镜工作台按逐句时间轴默认只露“文案 / 场景一句话 / 人物+产品+动作”,产品素材池、批量控制、三字段、视频候选和高级区都必须可折叠;视频候选无内容时默认不占大面积,有候选时默认只显示迷你缩略条,展开后才显示 4-grid。单条默认生成 4 个视频候选,顶部支持整片批量生成候选;首尾帧、视觉规划、产品出现方式和旧 6 字段保留在“高级”抽屉与后端 quick-plan 自动展开中,不能再作为客户默认闸门。
## 部署事实 ## 部署事实
- 平台VPS `76.13.31.179`Ubuntu 24.04 / Docker Compose / Coolify Traefik - 平台VPS `76.13.31.179`Ubuntu 24.04 / Docker Compose / Coolify Traefik
- 发布状态已部署并验证2026-05-20主体元素按套图文件夹分组展示主体生成接口提交后立即返回 queued 占位并后台逐视角生成、逐张回填;源视频工作区主体链路为上方竖向参考帧池 + 宽幅对话式转换层、下方主体元素结果栏;转换层通过参考帧 `+` 加入、参考图分析、生图对话,英文 prompt 就绪后由发送区主按钮切换为确认生成,点击后才触发主体套图生成;转换层不再固定 640px 长高,按内容自然高度显示,仅以 560px 最大高度兜底内部滚动;下方主体元素结果栏的套图输出、轮询、文件夹分组、单张重生、删除和 hover 预览逻辑保持不变;胶片双击/拖拽加入参考帧池 + 胶片缓存复用 + 音频解析失败可重试,参考帧缩略图保持小尺寸 9:16 比例 + hover 左侧紧凑预览,旧主体模板区移出主路径 + 逐句时间轴移到原版视频下方并支持双行文案 + 波形同框时间对齐画面胶片 + 胶片密度按钮上移波形顶部 + 去分隔线 + 胶片上下错落 + body 顶层原位大放大 + 隐藏源视频工作区音频解析摘要卡 + 隐藏工作区顶部状态提示条 + 三字段候选生成工作流 + 折叠紧凑候选区);`https://marketing.skg.com` 已启用应用内登录页,未登录 API 返回 401认证后首页 200容器内 `/health` 返回 `ok:true` - 发布状态已部署并验证2026-05-20主体元素按套图文件夹分组展示主体生成接口提交后立即返回 queued 占位并后台逐视角生成、逐张回填;工作台外层取消 1800x1000 固定画布和应用层 `zoom` 缩放,改为正常流式桌面容器,最低操作宽度 1280px源视频工作区主体链路为上方竖向参考帧池 + 宽幅对话式转换层、下方主体元素结果栏;转换层通过参考帧 `+` 加入、参考图分析、生图对话,英文 prompt 就绪后由发送区主按钮切换为确认生成,点击后才触发主体套图生成;转换层不再固定 640px 长高,按内容自然高度显示,仅以 560px 最大高度兜底内部滚动;下方主体元素结果栏的套图输出、轮询、文件夹分组、单张重生、删除和 hover 预览逻辑保持不变;胶片双击/拖拽加入参考帧池 + 胶片缓存复用 + 音频解析失败可重试,参考帧缩略图保持小尺寸 9:16 比例 + hover 左侧紧凑预览,旧主体模板区移出主路径 + 逐句时间轴移到原版视频下方并支持双行文案 + 波形同框时间对齐画面胶片 + 胶片密度按钮上移波形顶部 + 去分隔线 + 胶片上下错落 + body 顶层原位大放大 + 隐藏源视频工作区音频解析摘要卡 + 隐藏工作区顶部状态提示条 + 三字段候选生成工作流 + 折叠紧凑候选区);`https://marketing.skg.com` 已启用应用内登录页,未登录 API 返回 401认证后首页 200容器内 `/health` 返回 `ok:true`
- 最近部署验证2026-05-20`6597db3` 已通过 `./scripts/deploy-prod-safe.sh` 部署到 `/opt/skg-marketing-studio`;部署前备份为 `/opt/skg-marketing-studio-backups/skg-marketing-preserve-20260520151033.tgz`,生产 Docker 重建后脚本内验证通过(`web:/login/ 200``web:/api/health 401``api:health ok`)。线上登录后检查首页静态资源,当前加载 chunk `/_next/static/chunks/c48f07b9aef1cd29.js` 已包含 `min-w-[1280px]``max-w-[1920px]`,未再命中旧的 `h-[1000px]``w-[1800px]``BOARD_SCALE_PRESETS``boardScale`;对应工作台取消固定画布缩放,按浏览器正常流式布局渲染。
- 最近部署验证2026-05-20`2b842fd` 已通过 `./scripts/deploy-prod-safe.sh` 部署到 `/opt/skg-marketing-studio`;部署前备份为 `/opt/skg-marketing-studio-backups/skg-marketing-preserve-20260520145223.tgz`,生产 Docker 重建后脚本内验证通过(`web:/login/ 200``web:/api/health 401``api:health ok`)。线上登录后检查首页静态资源,当前加载 chunk `/_next/static/chunks/743b82648dfa9db9.js` 已包含 `h-32``maxHeight:560``提示词就绪``确认生成`,且未再命中旧的 `height:640` / `h-40`;对应转换层取消固定长高,生成要求输入区回到 128px底部仍由发送区主按钮确认生成。 - 最近部署验证2026-05-20`2b842fd` 已通过 `./scripts/deploy-prod-safe.sh` 部署到 `/opt/skg-marketing-studio`;部署前备份为 `/opt/skg-marketing-studio-backups/skg-marketing-preserve-20260520145223.tgz`,生产 Docker 重建后脚本内验证通过(`web:/login/ 200``web:/api/health 401``api:health ok`)。线上登录后检查首页静态资源,当前加载 chunk `/_next/static/chunks/743b82648dfa9db9.js` 已包含 `h-32``maxHeight:560``提示词就绪``确认生成`,且未再命中旧的 `height:640` / `h-40`;对应转换层取消固定长高,生成要求输入区回到 128px底部仍由发送区主按钮确认生成。
- 最近部署验证2026-05-20`ab31a98` 已通过 `./scripts/deploy-prod-safe.sh` 部署到 `/opt/skg-marketing-studio`;部署前备份为 `/opt/skg-marketing-studio-backups/skg-marketing-preserve-20260520144227.tgz`,生产 Docker 重建后脚本内验证通过(`web:/login/ 200``web:/api/health 401``api:health ok`)。线上登录后检查首页静态资源,当前加载 chunk `/_next/static/chunks/5bbecb6cf31316cb.js` 已包含 `h-40``提示词就绪``确认生成`,对应生成要求输入框加高到 160px出图提示词生成后不再自动弹窗底部主按钮直接切换为确认生成。 - 最近部署验证2026-05-20`ab31a98` 已通过 `./scripts/deploy-prod-safe.sh` 部署到 `/opt/skg-marketing-studio`;部署前备份为 `/opt/skg-marketing-studio-backups/skg-marketing-preserve-20260520144227.tgz`,生产 Docker 重建后脚本内验证通过(`web:/login/ 200``web:/api/health 401``api:health ok`)。线上登录后检查首页静态资源,当前加载 chunk `/_next/static/chunks/5bbecb6cf31316cb.js` 已包含 `h-40``提示词就绪``确认生成`,对应生成要求输入框加高到 160px出图提示词生成后不再自动弹窗底部主按钮直接切换为确认生成。
- 最近部署验证2026-05-20`215987a` 已通过 `./scripts/deploy-prod-safe.sh` 部署到 `/opt/skg-marketing-studio`;部署前备份为 `/opt/skg-marketing-studio-backups/skg-marketing-preserve-20260520142849.tgz`,生产 Docker 重建后脚本内验证通过(`web:/login/ 200``web:/api/health 401``api:health ok`)。线上登录后检查首页静态资源,当前加载 chunk `/_next/static/chunks/54e1ee55c5019be8.js` 已包含 `height:640`,对应转换层固定高度从 560px 扩到 640px。 - 最近部署验证2026-05-20`215987a` 已通过 `./scripts/deploy-prod-safe.sh` 部署到 `/opt/skg-marketing-studio`;部署前备份为 `/opt/skg-marketing-studio-backups/skg-marketing-preserve-20260520142849.tgz`,生产 Docker 重建后脚本内验证通过(`web:/login/ 200``web:/api/health 401``api:health ok`)。线上登录后检查首页静态资源,当前加载 chunk `/_next/static/chunks/54e1ee55c5019be8.js` 已包含 `height:640`,对应转换层固定高度从 560px 扩到 640px。

File diff suppressed because one or more lines are too long

View File

@@ -103,11 +103,6 @@ type BoardThemeMode = "dark" | "light"
type AudioStoryboardRole = "hook" | "pain" | "proof" | "solution" | "cta" | "bridge" type AudioStoryboardRole = "hook" | "pain" | "proof" | "solution" | "cta" | "bridge"
const BOARD_THEME_STORAGE_KEY = "skg-board-theme" const BOARD_THEME_STORAGE_KEY = "skg-board-theme"
const BOARD_FRAME_WIDTH = 1800
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_LEFT_COLUMN_WIDTH = 380 const SOURCE_LEFT_COLUMN_WIDTH = 380
const SOURCE_VIDEO_HEIGHT = 500 const SOURCE_VIDEO_HEIGHT = 500
const SOURCE_TRANSCRIPT_MAX_HEIGHT = 270 const SOURCE_TRANSCRIPT_MAX_HEIGHT = 270
@@ -115,12 +110,6 @@ const SOURCE_REFERENCE_POOL_WIDTH = 140
const SOURCE_CONVERSION_MAX_HEIGHT = 560 const SOURCE_CONVERSION_MAX_HEIGHT = 560
const SOURCE_SUBJECT_EMPTY_HEIGHT = 78 const SOURCE_SUBJECT_EMPTY_HEIGHT = 78
const resolveBoardScale = (viewportWidth: number) => {
const maxFitScale = clampNumber(viewportWidth / BOARD_FRAME_WIDTH, BOARD_MIN_SCALE, BOARD_MAX_SCALE)
const preset = BOARD_SCALE_PRESETS.reduce((best, candidate) => (candidate <= maxFitScale ? candidate : best), BOARD_MIN_SCALE)
return Math.round(preset * 1000) / 1000
}
type DraftSegment = { type DraftSegment = {
id: string id: string
frameIndex: number | null frameIndex: number | null
@@ -2234,11 +2223,8 @@ export function AdRecreationBoard({
const [generatingAll, setGeneratingAll] = useState(false) const [generatingAll, setGeneratingAll] = useState(false)
const [runtimeModels, setRuntimeModels] = useState<RuntimeModels | undefined>() const [runtimeModels, setRuntimeModels] = useState<RuntimeModels | undefined>()
const [boardTheme, setBoardTheme] = useState<BoardThemeMode>("dark") const [boardTheme, setBoardTheme] = useState<BoardThemeMode>("dark")
const [boardScale, setBoardScale] = useState(1)
const [boardViewportSize, setBoardViewportSize] = useState({ width: 0, height: 0 })
const [libraryOpen, setLibraryOpen] = useState(false) const [libraryOpen, setLibraryOpen] = useState(false)
const fileRef = useRef<HTMLInputElement | null>(null) const fileRef = useRef<HTMLInputElement | null>(null)
const boardViewportRef = useRef<HTMLElement | null>(null)
const selectedFrames = job const selectedFrames = job
? job.frames.filter((frame) => data.selectedFrames.has(frame.index)).sort((a, b) => a.timestamp - b.timestamp) ? job.frames.filter((frame) => data.selectedFrames.has(frame.index)).sort((a, b) => a.timestamp - b.timestamp)
: [] : []
@@ -2286,30 +2272,6 @@ export function AdRecreationBoard({
} }
}, []) }, [])
useEffect(() => {
const updateBoardScale = () => {
const node = boardViewportRef.current
if (!node) return
const nextWidth = node.clientWidth
const nextHeight = node.clientHeight
const nextScale = resolveBoardScale(nextWidth)
setBoardScale((current) => (Math.abs(current - nextScale) < 0.001 ? current : nextScale))
setBoardViewportSize((current) =>
current.width === nextWidth && current.height === nextHeight ? current : { width: nextWidth, height: nextHeight },
)
}
updateBoardScale()
const node = boardViewportRef.current
if (node && typeof ResizeObserver !== "undefined") {
const observer = new ResizeObserver(updateBoardScale)
observer.observe(node)
return () => observer.disconnect()
}
window.addEventListener("resize", updateBoardScale)
return () => window.removeEventListener("resize", updateBoardScale)
}, [])
useEffect(() => { useEffect(() => {
let cancelled = false let cancelled = false
getRuntimeHealth() getRuntimeHealth()
@@ -2511,23 +2473,10 @@ export function AdRecreationBoard({
} }
} }
const boardScaledWidth = Math.round(BOARD_FRAME_WIDTH * boardScale)
const boardScaledHeight = Math.round(BOARD_FRAME_HEIGHT * boardScale)
const boardViewportHeight = boardViewportSize.height || boardScaledHeight
const boardShouldCenterVertically = boardScaledHeight < boardViewportHeight
return ( return (
<section ref={boardViewportRef} className={`skg-board-theme ${boardTheme === "light" ? "skg-board-theme--light" : ""} relative z-20 h-screen w-screen overflow-auto bg-black text-white`}> <section className={`skg-board-theme ${boardTheme === "light" ? "skg-board-theme--light" : ""} relative z-20 min-h-screen w-full overflow-auto bg-black text-white`}>
<div className="skg-board-ambient pointer-events-none fixed inset-0" /> <div className="skg-board-ambient pointer-events-none fixed inset-0" />
<div <div className="relative z-10 mx-auto flex min-h-screen w-full min-w-[1280px] max-w-[1920px] flex-col px-4 py-4 xl:px-5">
className={`relative z-10 flex min-h-screen justify-center ${boardShouldCenterVertically ? "items-center" : "items-start"}`}
style={{ minWidth: boardScaledWidth, minHeight: Math.max(boardScaledHeight, boardViewportHeight) }}
>
<div style={{ width: boardScaledWidth, height: boardScaledHeight }}>
<div
className="flex h-[1000px] w-[1800px] max-w-none flex-col px-4 py-4"
style={{ zoom: boardScale, transformOrigin: "top left" }}
>
<header className="skg-board-topbar mb-3 flex items-center justify-between gap-4 rounded-lg border border-white/10 bg-white/[0.04] px-4 py-3"> <header className="skg-board-topbar mb-3 flex items-center justify-between gap-4 rounded-lg border border-white/10 bg-white/[0.04] px-4 py-3">
<div className="skg-board-brand"> <div className="skg-board-brand">
<div className="skg-board-brand__logo-chip" aria-hidden="true"> <div className="skg-board-brand__logo-chip" aria-hidden="true">
@@ -2630,8 +2579,6 @@ export function AdRecreationBoard({
</section> </section>
</div> </div>
</div> </div>
</div>
</div>
<LibraryDrawer <LibraryDrawer
open={libraryOpen} open={libraryOpen}
currentJobId={job?.id} currentJobId={job?.id}