fix: scale workbench frame to viewport
This commit is contained in:
2
RULES.md
2
RULES.md
@@ -11,7 +11,7 @@
|
||||
- 详见 `CLAUDE.md` 立项决策段 + `.memory/plan.md` 七步管线拆解
|
||||
- 风格:`04-Dark-Gallery-Ambient`(路径:`~/Projects/research/20260305-网页风格库/04-Dark-Gallery-Ambient.md`)
|
||||
- 第一冲刺:步骤 1-4(下载 / 拆轨 / 关键帧 / ASR+翻译)
|
||||
- 当前产品方向(2026-05-20 再确认):信息流广告快速复刻默认进入“三字段候选生成”工作流。主界面为“左侧素材输入列 + 右侧信息流复刻工作表”;工作台使用固定 1800x1000 操作画布,不同显示器或浏览器宽度下保持同一框架,窗口变小时只滚动查看,不通过 `xl/2xl` 断点重排核心操作区。用户粘贴 TK 链接或上传视频后点击“开始分析”,系统自动下载源视频;下载完成后并行启动两条路:音频文案路提取原音频文案/字幕,并分析讲话人、语速节奏、背景音乐/环境声/音效;视频视觉路自动抽取参考帧。源视频工作区右侧主体链路是“参考帧池 → 转换层 → 主体元素”:参考帧池竖向排列;转换层是轻量对话式生图确认区,参考图可通过左侧缩略图 `+`、参考帧拖拽、胶片拖拽或本地图片拖入进入转换层,用户选择 GPT/Gemini 套件后先分析参考图;识别结果里的特征 chip 只作为“保留元素”本地选择,点亮=保留、再点取消,点击不立即请求模型,随下一条发送消息提交;用户再在下方消息输入区发送复刻/创新/卡通和画面要求,生成数量通过发送区旁边的张数控件控制;后端返回英文出图 prompt 后必须弹窗确认,用户点确认才生成对应数量的统一多角度套图。右侧主体元素区的套图输出、文件夹分组、单张重生、删除和 hover 预览逻辑保持不变。旧下方“相似主体 / 主体模板库”不再作为主路径。波形下方的画面胶片只是临时预览,点击只跳转原视频时间点,双击或拖进参考帧池才正式加入关键帧,已加入的胶片直接显示“已添加”。产品图上传后独立形成产品资产包,自动识别视角/结构/比例并补缺角度。分镜工作台按逐句时间轴默认只露“文案 / 场景一句话 / 人物+产品+动作”,产品素材池、批量控制、三字段、视频候选和高级区都必须可折叠;视频候选无内容时默认不占大面积,有候选时默认只显示迷你缩略条,展开后才显示 4-grid。单条默认生成 4 个视频候选,顶部支持整片批量生成候选;首尾帧、视觉规划、产品出现方式和旧 6 字段保留在“高级”抽屉与后端 quick-plan 自动展开中,不能再作为客户默认闸门。
|
||||
- 当前产品方向(2026-05-20 再确认):信息流广告快速复刻默认进入“三字段候选生成”工作流。主界面为“左侧素材输入列 + 右侧信息流复刻工作表”;工作台以 1800x1000 为基准操作画布,不同显示器或浏览器宽度下保持同一框架,并按可见视口在 0.72-1.6 倍范围内等比放大/缩小,不通过 `xl/2xl` 断点重排核心操作区。用户粘贴 TK 链接或上传视频后点击“开始分析”,系统自动下载源视频;下载完成后并行启动两条路:音频文案路提取原音频文案/字幕,并分析讲话人、语速节奏、背景音乐/环境声/音效;视频视觉路自动抽取参考帧。源视频工作区右侧主体链路是“参考帧池 → 转换层 → 主体元素”:参考帧池竖向排列;转换层是轻量对话式生图确认区,参考图可通过左侧缩略图 `+`、参考帧拖拽、胶片拖拽或本地图片拖入进入转换层,用户选择 GPT/Gemini 套件后先分析参考图;识别结果里的特征 chip 只作为“保留元素”本地选择,点亮=保留、再点取消,点击不立即请求模型,随下一条发送消息提交;用户再在下方消息输入区发送复刻/创新/卡通和画面要求,生成数量通过发送区旁边的张数控件控制;后端返回英文出图 prompt 后必须弹窗确认,用户点确认才生成对应数量的统一多角度套图。右侧主体元素区的套图输出、文件夹分组、单张重生、删除和 hover 预览逻辑保持不变。旧下方“相似主体 / 主体模板库”不再作为主路径。波形下方的画面胶片只是临时预览,点击只跳转原视频时间点,双击或拖进参考帧池才正式加入关键帧,已加入的胶片直接显示“已添加”。产品图上传后独立形成产品资产包,自动识别视角/结构/比例并补缺角度。分镜工作台按逐句时间轴默认只露“文案 / 场景一句话 / 人物+产品+动作”,产品素材池、批量控制、三字段、视频候选和高级区都必须可折叠;视频候选无内容时默认不占大面积,有候选时默认只显示迷你缩略条,展开后才显示 4-grid。单条默认生成 4 个视频候选,顶部支持整片批量生成候选;首尾帧、视觉规划、产品出现方式和旧 6 字段保留在“高级”抽屉与后端 quick-plan 自动展开中,不能再作为客户默认闸门。
|
||||
|
||||
## 部署事实
|
||||
- 平台:VPS `76.13.31.179`(Ubuntu 24.04 / Docker Compose / Coolify Traefik)
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -103,6 +103,10 @@ type BoardThemeMode = "dark" | "light"
|
||||
type AudioStoryboardRole = "hook" | "pain" | "proof" | "solution" | "cta" | "bridge"
|
||||
|
||||
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
|
||||
|
||||
type DraftSegment = {
|
||||
id: string
|
||||
@@ -2217,8 +2221,10 @@ export function AdRecreationBoard({
|
||||
const [generatingAll, setGeneratingAll] = useState(false)
|
||||
const [runtimeModels, setRuntimeModels] = useState<RuntimeModels | undefined>()
|
||||
const [boardTheme, setBoardTheme] = useState<BoardThemeMode>("dark")
|
||||
const [boardScale, setBoardScale] = useState(1)
|
||||
const [libraryOpen, setLibraryOpen] = useState(false)
|
||||
const fileRef = useRef<HTMLInputElement | null>(null)
|
||||
const boardViewportRef = useRef<HTMLElement | null>(null)
|
||||
const selectedFrames = job
|
||||
? job.frames.filter((frame) => data.selectedFrames.has(frame.index)).sort((a, b) => a.timestamp - b.timestamp)
|
||||
: []
|
||||
@@ -2266,6 +2272,29 @@ export function AdRecreationBoard({
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const updateBoardScale = () => {
|
||||
const node = boardViewportRef.current
|
||||
if (!node) return
|
||||
const rawScale = Math.min(
|
||||
node.clientWidth / BOARD_FRAME_WIDTH,
|
||||
node.clientHeight / BOARD_FRAME_HEIGHT,
|
||||
)
|
||||
const nextScale = Math.round(clampNumber(rawScale, BOARD_MIN_SCALE, BOARD_MAX_SCALE) * 1000) / 1000
|
||||
setBoardScale((current) => (Math.abs(current - nextScale) < 0.001 ? current : nextScale))
|
||||
}
|
||||
|
||||
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(() => {
|
||||
let cancelled = false
|
||||
getRuntimeHealth()
|
||||
@@ -2467,10 +2496,17 @@ export function AdRecreationBoard({
|
||||
}
|
||||
}
|
||||
|
||||
const boardScaledWidth = Math.round(BOARD_FRAME_WIDTH * boardScale)
|
||||
const boardScaledHeight = Math.round(BOARD_FRAME_HEIGHT * boardScale)
|
||||
|
||||
return (
|
||||
<section className={`skg-board-theme ${boardTheme === "light" ? "skg-board-theme--light" : ""} relative z-20 h-screen w-screen overflow-auto bg-black text-white`}>
|
||||
<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`}>
|
||||
<div className="skg-board-ambient pointer-events-none fixed inset-0" />
|
||||
<div className="relative z-10 mx-auto flex h-[1000px] w-[1800px] max-w-none flex-col px-4 py-4">
|
||||
<div className="relative z-10 mx-auto" style={{ width: boardScaledWidth, height: boardScaledHeight }}>
|
||||
<div
|
||||
className="flex h-[1000px] w-[1800px] max-w-none flex-col px-4 py-4"
|
||||
style={{ transform: `scale(${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">
|
||||
<div className="skg-board-brand">
|
||||
<div className="skg-board-brand__logo-chip" aria-hidden="true">
|
||||
@@ -2573,6 +2609,7 @@ export function AdRecreationBoard({
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<LibraryDrawer
|
||||
open={libraryOpen}
|
||||
currentJobId={job?.id}
|
||||
|
||||
Reference in New Issue
Block a user