diff --git a/.project.json b/.project.json index 3c241d5..ee3e470 100644 --- a/.project.json +++ b/.project.json @@ -45,7 +45,7 @@ "type" : "oauth_app" } ], - "description" : "SKG 营销内容生产平台:默认首页面向公司团队成员的个人隔离创作空间,终端可见品牌位只保留 SKG logo。主路径为文生图、图生图、文生视频、图生视频和营销图文方案生成;每个登录用户只看到自己的任务和结果。任务详情页沉淀参考图、生成图、视频候选、提示词和图文方案,可继续生成、删除和复用。画布用于整理多次生成结果;旧 TK 复刻\/一键出片能力保留为高级入口。", + "description" : "SKG 营销内容生产平台:根域名 https:\/\/marketing.skg.com 登录后直接进入个人生成画布,终端可见品牌位只保留 SKG logo。主路径为文生图、文生视频、图生视频;每个登录用户只看到自己的任务和结果。画布用于整理多次生成结果,图片\/视频资产继续写入当前用户自己的后端 job;旧 TK 复刻\/一键出片能力保留为高级入口。", "kind" : "app", "name" : "SKG 营销内容生产平台", "ownership" : "company", @@ -84,11 +84,6 @@ "type" : "backend", "url" : "https:\/\/marketing.skg.com\/api" }, - { - "label" : "production-canvas", - "type" : "app", - "url" : "https:\/\/marketing.skg.com\/canvas\/" - }, { "label" : "agent-cut-preview", "type" : "app", diff --git a/RULES.md b/RULES.md index 742c38e..a84976b 100644 --- a/RULES.md +++ b/RULES.md @@ -4,7 +4,7 @@ - 后台启动(不弹 Terminal):`./scripts/start-dev-background.sh`(通过 macOS launchd 后台托管;前端 4290 + 后端 4291,日志写入 `.logs/`) - 后台停止:`./scripts/stop-dev-background.sh` - 前端 dev:`cd web && npm run dev`(Next.js 16,端口 4290) -- 画布 dev:`cd web && npm run dev:canvas`(Vue / Vite,端口 4292;生产构建会输出到 `/canvas/`) +- 画布 dev:`cd web && npm run dev:canvas`(Vue / Vite,端口 4292;生产构建会作为根域名工作台输出) - 后端 dev:`cd api && uvicorn main:app --host 127.0.0.1 --port 4291`(FastAPI,端口 4291,重任务用) - 注意:后端不要带 `--reload` 跑长下载 / 抽帧 / 音频任务;reload 会等待后台任务结束,导致 4291 端口占用但新请求卡住。 @@ -12,7 +12,7 @@ - 详见 `CLAUDE.md` 立项决策段 + `.memory/plan.md` 七步管线拆解 - 风格:`04-Dark-Gallery-Ambient`(路径:`~/Projects/research/20260305-网页风格库/04-Dark-Gallery-Ambient.md`) - 第一冲刺:步骤 1-4(下载 / 拆轨 / 关键帧 / ASR+翻译) -- 当前产品方向(2026-05-25 单对话框 + logo-only 画布版):默认首页彻底从“信息流广告复刻管线”切换为多人通用的 SKG 营销内容生产平台入口,服务公司内部成员同时使用。终端可见品牌位只放 SKG logo,不再把“生图生视频”“SKG 生成画布”或长系统名放在主界面上。首页默认只保留一个中央对话框,不再显示侧栏、灵感区、任务列表或大结果面板;用户先选择四种生成方式之一:文生视频、文生图、首帧生视频、首尾帧生视频,然后手写提示词并点击生成。首帧 / 首尾帧模式只露必要图片上传位,图片模式显示尺寸选择,视频模式显示画幅和真实可用时长选择。后端 `/health` 向前端返回可选图片 / 视频模型、图片尺寸、视频画幅和视频时长,首页允许用户选择图片模型(自动、GPT Image 2、Gemini 图片兜底)和视频模型(Seedance、Kling、Veo 3 等别名;实际可用模型以环境变量映射为准)。当前 Doubao / Seedance 生产链路单条视频最长按 15 秒暴露,不在 UI 显示 30 秒;如后续要 30 秒,需要改成多段生成后合成。用户登录后仍只看到自己的任务、结果和详情页,继续沿用后端 owner 隔离;结果生成后从对话框下方进入 `/detail/?job=` 沉淀参考图、生成图、视频候选和提示词。新增 `/canvas/` 作为个人画布入口,基于 huobao-canvas 交互逻辑改造为 SKG 内部版,界面去除原可见品牌/API 设置,生成调用本项目后端 `/api`,每个浏览器的画布项目先保存在本地 localStorage,图片/视频资产仍按登录用户写入后端 job。旧 TK 复刻工作台、Agent Cut 一键出片和营销图文方案保留为高级/详情页能力,不再作为默认首页入口或默认理解框架。 +- 当前产品方向(2026-05-25 三模式 + logo-only 根域名画布版):默认首页彻底从“信息流广告复刻管线”切换为多人通用的 SKG 营销内容生产平台入口,服务公司内部成员同时使用。`https://marketing.skg.com` 登录后直接进入个人生成画布,`/canvas/` 只作为旧链接兼容跳转到根域名。终端可见品牌位只放 SKG logo,不再把“生图生视频”“SKG 生成画布”或长系统名放在主界面上。首页和画布底部输入框都只保留三个用户能直接理解的入口:文生图、文生视频、图生视频;不再把“首帧生视频 / 首尾帧生视频”这类模型实现概念作为主入口。图生视频只显示“上传图片”,内部仍用后端 first_image 能力提交。用户选择生成方式、必要时上传图片、手写提示词并点击生成;图片模式显示尺寸选择,视频模式显示画幅和真实可用时长选择。后端 `/health` 向前端返回可选图片 / 视频模型、图片尺寸、视频画幅和视频时长,首页允许用户选择图片模型(自动、GPT Image 2、Gemini 图片兜底)和视频模型(Seedance、Kling、Veo 3 等别名;实际可用模型以环境变量映射为准)。当前 Doubao / Seedance 生产链路单条视频最长按 15 秒暴露,不在 UI 显示 30 秒;如后续要 30 秒,需要改成多段生成后合成。用户登录后仍只看到自己的任务、结果和详情页,继续沿用后端 owner 隔离;生成调用本项目后端 `/api`,每个浏览器的画布项目先保存在本地 localStorage,图片/视频资产仍按登录用户写入后端 job。旧 TK 复刻工作台、Agent Cut 一键出片和营销图文方案保留为高级/详情页能力,不再作为默认首页入口或默认理解框架。 ## 部署事实 - 平台:VPS `76.13.31.179`(Ubuntu 24.04 / Docker Compose / Coolify Traefik) @@ -64,7 +64,7 @@ - 最近部署验证(2026-05-20):`f1c710e` 已推送并部署到 `/opt/skg-marketing-studio`;本地 `web/npm run build` 通过,生产 Docker 重建后 `./scripts/verify-prod-docker.sh` 通过(web/API 容器 Up、`/login/` 200、缺失 `_next` 资源 404、未登录 `/api/health` 401、容器内 `api:health ok`)。转换层中间栏先清空为待重构占位,不再接收拖拽或触发 subject-agent / subject-assets;右侧主体元素输出逻辑保持不变。 - 最近部署验证(2026-05-20):`7e763cf` 已推送并部署到 `/opt/skg-marketing-studio`;本地 `web/npm run build` 通过,生产 Docker 重建后 `./scripts/verify-prod-docker.sh` 通过(web/API 容器 Up、`/login/` 200、缺失 `_next` 资源 404、未登录 `/api/health` 401、容器内 `api:health ok`)。转换层改为参考帧分析 + 对话生成提示词 + 弹窗确认后再生成主体套图;右侧主体元素输出逻辑保持不变。部署时发现服务器 `WEB_AUTH_*` 环境变量缺失导致 `/auth/check` 503,已从 `/root/skg-marketing-studio-login.txt` 和新 session secret 恢复服务器 `deploy/.env.production` 后重启验证通过;后续同步生产代码必须继续排除服务器真实 `deploy/.env.production`。 - 主站 / 前端:`https://marketing.skg.com` -- 画布:`https://marketing.skg.com/canvas/` +- 旧画布路径:`https://marketing.skg.com/canvas/`(仅兼容跳转到根域名) - API / 后端:`https://marketing.skg.com/api` - 代码仓库 / Gitea:`https://git.kang-kang.com/kangwan/20260512-skg-tk` - 文档 / 解析:`docs/source-analysis.html`(项目内独立文档,不公开挂主应用路由) @@ -73,7 +73,7 @@ - 生产部署唯一入口:`./scripts/deploy-prod-safe.sh`(先在服务器备份 `deploy/.env.production`、`data/jobs`、资源库和 `secrets`,再用受保护 rsync 同步代码,最后 Docker 重建并运行 `verify-prod-docker.sh`) - 生产容器重建命令:`docker compose -f docker-compose.prod.yml --env-file deploy/.env.production up -d --build`;只允许脚本内部或明确只重启容器时使用,不允许再用裸 `rsync --delete` 手动同步。 - 独立预览容器重建命令:服务器 `/opt/skg-marketing-studio` 下执行 `docker compose -f docker-compose.standalone.yml --env-file deploy/.env.production up -d --build`;Web 暴露 `0.0.0.0:4290->80`,后端仅在 compose 内部网络暴露,`/api/` 由 Web 容器 Nginx 反代并复用应用内登录校验。 -- 生产架构:`web` 容器用 Nginx 承载 Next 静态导出;`/login/`、`/_next/`、`/assets/`、`/skg-logo-black.svg`、`/oasis-source/` 等登录页必需静态资源公开访问;未登录访问工作台跳转 `/login/`;`/canvas/` 是受同一登录保护的 Vue / Vite 画布静态应用,Nginx fallback 到 `/canvas/index.html`;`/api/` 通过 Nginx `auth_request` 校验 FastAPI 会话 Cookie 后反代到 `skg-marketing-api:4291`;Traefik 通过 `coolify` 外部网络接入 80/443 +- 生产架构:`web` 容器用 Nginx 承载 Next 静态导出与根域名 Vue / Vite 画布静态应用;构建时先生成画布,再 Next 静态导出,最后用画布产物覆盖 `web/out/index.html` 和 `/assets/`,使登录后的 `/` 直接进入画布;`/canvas/` 只做 308 兼容跳转到 `/`。`/login/`、`/_next/`、`/assets/`、`/skg-logo-black.svg`、`/oasis-source/` 等登录页必需静态资源公开访问;未登录访问工作台跳转 `/login/`;`/api/` 通过 Nginx `auth_request` 校验 FastAPI 会话 Cookie 后反代到 `skg-marketing-api:4291`;Traefik 通过 `coolify` 外部网络接入 80/443 - Web 验收必须以生产 Docker 形态为准:前端是 `next export` 静态产物 + Nginx,不是 `next dev` / `next start`。任何 Web 改动部署后必须运行 `./scripts/verify-prod-docker.sh`,确认 `/login/`、`/_next/`、`/api/health`、本地 API 地址泄漏和 API 镜像 `.env` 污染检查通过;不能只用本地 `npm run build` 作为上线依据。 - 当前音频解析:`https://ai.skg.com/azure/v1` 的 `gpt-4o-transcribe` 当前返回 `DeploymentNotFound`,且官方 Azure OpenAI transcription 路径探测也未返回可用部署;生产临时复制本地成功策略,直接使用容器内多语言 `faster-whisper` 真实转写,默认语种为 `auto`,支持中文、英文和其他多语言原文识别,关闭 Gemini 多模态音频兜底。拿到真实 Azure ASR deployment 名后再恢复 `ASR_REMOTE_ENABLED=true`,并保持 `ASR_LANGUAGE` 为空或 `auto`,除非明确只想强制单一语种。 - 持久化目录:服务器 `./data/jobs` 挂载到后端 `/data/jobs`;全局资源中心持久化在 `./data/asset_library`、`./data/prompt_library` 和 `./data/_trash` diff --git a/deploy/nginx.conf b/deploy/nginx.conf index 772404f..6d54357 100644 --- a/deploy/nginx.conf +++ b/deploy/nginx.conf @@ -107,14 +107,11 @@ server { } location = /canvas { - return 308 /canvas/; + return 308 /; } - location /canvas/ { - auth_request /__auth; - error_page 401 = @login_redirect; - root /usr/share/nginx/html; - try_files $uri $uri/ /canvas/index.html; + location ~ ^/canvas/(.*)$ { + return 308 /$1$is_args$args; } location = /skg-logo-black.svg { diff --git a/docs/source-analysis.html b/docs/source-analysis.html index 330dc22..0304152 100644 --- a/docs/source-analysis.html +++ b/docs/source-analysis.html @@ -536,12 +536,12 @@ 生产站点 https://marketing.skg.com - 公司域名已解析到 VPS 76.13.31.179。线上由既有 Coolify / Traefik 负责 HTTPS 入口,项目 web 容器用 Nginx 承载静态前端;/login//_next//assets//skg-logo-black.svg/oasis-source/ 为公开登录页资源,未登录访问工作台跳转 /login/。登录页优先走飞书免登录,回调为 /api/auth/feishu/callback;账号密码保留为备用入口。/api/ 通过 auth_request 校验 FastAPI 会话 Cookie 后再反代,后端按 Cookie 里的用户身份隔离 JobAgentRun 数据。 + 公司域名已解析到 VPS 76.13.31.179。线上由既有 Coolify / Traefik 负责 HTTPS 入口,项目 web 容器用 Nginx 承载静态前端;登录后根路径直接进入 Vue / Vite 个人生成画布,/canvas/ 只作为旧链接兼容跳转到根路径。/login//_next//assets//skg-logo-black.svg/oasis-source/ 为公开登录页资源,未登录访问工作台跳转 /login/。登录页优先走飞书免登录,回调为 /api/auth/feishu/callback;账号密码保留为备用入口。/api/ 通过 auth_request 校验 FastAPI 会话 Cookie 后再反代,后端按 Cookie 里的用户身份隔离 JobAgentRun 数据。 - 生产画布 - https://marketing.skg.com/canvas/ - 受同一登录保护的 Vue / Vite 画布应用。构建时先执行 pnpm build:canvas,把 web/canvas-app/dist 同步到 web/public/canvas,再由 Next 静态导出和 Nginx 承载;Nginx 对 /canvas/auth_request,并 fallback 到 /canvas/index.html 支持前端路由。画布项目当前保存在浏览器 localStorage,生成出来的图片 / 视频资产通过本项目 /api 写入当前登录用户自己的后端 job。 + 画布构建 + cd web && npm run build + 受同一登录保护的 Vue / Vite 画布应用。构建时先执行 pnpm build:canvas 生成 web/canvas-app/dist,再执行 Next 静态导出,最后由 web/scripts/sync-canvas-root.mjs 把画布产物覆盖到 web/out 根目录;Nginx 登录校验后的 / fallback 到画布 index.html/canvas/ 只做 308 兼容跳转。画布项目当前保存在浏览器 localStorage,生成出来的图片 / 视频资产通过本项目 /api 写入当前登录用户自己的后端 job。 生产部署 @@ -577,16 +577,17 @@

2026-05-24 完整重设计:默认首页已从“TK 信息流复刻 / 三字段分镜管线”推倒,改为面向公司约 6 名成员同时使用的 SKG 营销内容多人创作平台。主路径是文生图、图生图、文生视频、图生视频和营销图文方案生成;每个登录用户只看到自己的任务和详情页结果。旧 TK 复刻工作台与 Agent Cut 一键出片保留为高级入口,不再作为默认工作台。

2026-05-25 即梦 generate 式简化:默认首页进一步压缩为窄导航栏、会话侧栏和中央 prompt composer,不再把四入口、参考图、我的任务和结果区平铺成三栏。图片 / 视频 / 图文模式、自动设置和参考上传都收进 composer 底部的小按钮;参考图是输入框左侧倾斜上传卡;结果只用右下角浮层提示,完整沉淀交给详情页。

-

2026-05-25 单对话框版:默认首页再收敛为一个中央对话框,首页只让用户选文生视频、文生图、首帧生视频、首尾帧生视频,然后手写提示词生成。首帧 / 首尾帧模式只出现必要上传位;营销图文不再作为首页默认入口。后端 /health 返回可选图片 / 视频模型、图片尺寸、视频画幅和真实可用视频时长,首页按返回值显示模型和规格选择;当前 Doubao / Seedance 生产链路单条最长 15 秒,不向用户暴露 30 秒按钮。

+

2026-05-25 三模式版:默认首页再收敛为一个中央对话框,首页和画布底部输入框只让用户选文生图、文生视频、图生视频,然后手写提示词生成。图生视频只显示“上传图片”,不再把首帧 / 首尾帧这类模型实现概念作为主入口;营销图文不再作为首页默认入口。后端 /health 返回可选图片 / 视频模型、图片尺寸、视频画幅和真实可用视频时长,首页按返回值显示模型和规格选择;当前 Doubao / Seedance 生产链路单条最长 15 秒,不向用户暴露 30 秒按钮。

+

2026-05-25 根域名画布版:https://marketing.skg.com 登录后直接进入个人生成画布,不再先进入 React 单对话框首页再点画布;/canvas/ 只保留为旧链接兼容跳转。后续优先少改成熟画布结构,只在必要时改模式文案、生成接入和结果/队列显示。

-

当前默认业务管线是“个人隔离任务 → 在中央对话框选择生成方式 → 选择模型和规格 → 必要时上传首帧 / 尾帧 → 手写提示词 → 生成图片或视频 → 进入详情页继续沉淀”。首页不再渲染侧栏、灵感区、最近任务列表、自动设置或营销图文入口;默认只做四件事:文生视频、文生图、首帧生视频、首尾帧生视频。底层仍复用既有 /creative/jobs/image/jobs/{id}/frames/upload/jobs/{id}/frames/{idx}/generate/jobs/{id}/frames/{idx}/storyboard/video;首尾帧视频会把尾帧作为第二张参考帧上传,并通过 last_image 提交给视频接口。生图接口现在按前端 modelsize 字段走 auto / gpt-image-2 / gemini-3-pro-image-preview1024x1536 / 1024x1024 / 1536x1024 等图片尺寸;视频接口继续按 model 字段走 seedance / kling / veo3 / veo 别名映射,并按后端返回的 video_size_optionsvideo_duration_options 提交画幅和时长,实际模型和上限以服务器环境变量为准。多人互不影响依赖后端 owner_id 和飞书 / 备用登录会话隔离。旧信息流复刻链路仍保留在 web/components/ad-recreation-board.tsx/agent/,营销图文能力仍在详情页和接口中保留,但不作为默认首页路径。

+

当前默认业务管线是“个人隔离任务 → 根域名进入个人画布 → 在画布底部选择生成方式 → 选择模型和规格 → 图生视频时上传图片 → 手写提示词 → 在画布生成图片或视频节点 → 进入详情页继续沉淀”。默认只做三件事:文生图、文生视频、图生视频。底层仍复用既有 /creative/jobs/image/jobs/{id}/frames/{idx}/generate/jobs/{id}/frames/{idx}/storyboard/video;图生视频把上传图片作为 first_image 提交给视频接口,但 UI 不展示“首帧”概念。生图接口现在按前端 modelsize 字段走 auto / gpt-image-2 / gemini-3-pro-image-preview1024x1536 / 1024x1024 / 1536x1024 等图片尺寸;视频接口继续按 model 字段走 seedance / kling / veo3 / veo 别名映射,并按后端返回的 video_size_optionsvideo_duration_options 提交画幅和时长,实际模型和上限以服务器环境变量为准。多人互不影响依赖后端 owner_id 和飞书 / 备用登录会话隔离。旧 React 单对话框首页、信息流复刻链路仍保留在源码里作为回滚/高级能力,但不作为生产默认入口。

01

个人任务

GET /jobs 按当前登录用户过滤;旧无 owner 任务只对备用账号可见。

-
02

选择方式

首页对话框只提供文生视频、文生图、首帧生视频、首尾帧生视频四个按钮。

+
02

选择方式

首页对话框只提供文生图、文生视频、图生视频三个按钮。

03

选择模型和规格

GET /health 返回 image_optionsimage_size_optionsvideo_optionsvideo_size_optionsvideo_duration_options;首页按当前生成方式切换模型、图片尺寸、视频画幅和视频时长。

-
04

上传帧 / 空白任务

POST /creative/jobs/image 创建 0 号关键帧;首尾帧模式再用 /frames/upload 上传尾帧。

+
04

上传图片 / 空白任务

POST /creative/jobs/image 创建轻量任务;文生图和文生视频可空白创建,图生视频上传一张图片作为视频参考。

05

手写提示词

首页不再生成营销文案或自动展开产品 / 人群配置,用户直接写图片或视频提示词。

-
06

生成图片 / 视频

generateImagemode=text、图片模型和图片尺寸;generateStoryboardVideo 提交文本、模型、画幅、时长、可选 first_image 和可选 last_image。视频提交后先写入 queued 占位,再由后端队列按并发上限启动。

+
06

生成图片 / 视频

generateImagemode=text、图片模型和图片尺寸;generateStoryboardVideo 提交文本、模型、画幅、时长,图生视频额外提交 first_image。视频提交后先写入 queued 占位,再由后端队列按并发上限启动。

07

结果沉淀

首页只在对话框下方显示最新图片或视频;视频会显示排队位置、生成进度、完成播放或失败可重试状态;所有图片/视频缩略图继续复用 MediaAssetTile

08

详情页

/detail/?job=<id> 展示参考图、全量生成图、视频候选、提示词和营销图文,并支持继续生成。

09

高级复刻

AdRecreationBoard/agent/ 作为高级入口保留,不再是默认路径。

@@ -602,11 +603,11 @@ web/next.config.mjsNext.js 构建配置:静态导出、图片不走优化、禁用开发环境左下角 Next Dev Indicator,并移除 Next 16 已不支持的 eslint 顶层配置,避免本地 dev 出现配置 Issue 提示。 web/app/globals.css全局主题变量、登录页视觉样式、信息流工作台玻璃拟态 token、ReactFlow 样式引用,以及本地开发态 nextjs-portal 遮挡隐藏规则。工作台在 skg-board-theme 内按 Figma 本地 MCP 参考改成黑灰玻璃系统:深灰背景、#383838 胶囊侧栏、rgba(255,255,255,.1) 玻璃面、backdrop-filter: blur(5px)20px 圆角、10px 10px 10px rgba(0,0,0,.3) 阴影和绿黄状态色;新增 skg-board-shellskg-board-railskg-glass-cardskg-glass-card--flatskg-status-orb 等样式。侧栏改为跟随视口拉满工作台可用高度的悬停胶囊,桌面最小 600px,展开时在同一侧栏内承载素材输入抽屉。明暗主题已分开维护 shell、panel、glass、stat、action 和音频波形 token;暗色压低灰雾和面板底色,明亮模式改为暖白工作台,避免指标卡、按钮和波形继续残留黑底/白线;顶部指标卡增加紫、黄绿、琥珀、青绿、绿色光斑变量,接近原版多色玻璃卡效果。主/次按钮、指标卡和空状态继续走统一类,避免各板块散写不同玻璃效果。 - web/app/page.tsx当前默认首页:单对话框生成台。页面只保留顶部 SKG logo 和中央对话框,四个主按钮是文生视频、文生图、首帧生视频、首尾帧生视频;首帧 / 首尾帧模式才显示上传位,用户必须手写提示词后点击生成。页面启动时读取 getRuntimeHealth,按 image_options / video_options 显示模型下拉,按 image_size_options 显示文生图尺寸,按 video_size_optionsvideo_duration_options 显示视频画幅和真实可用时长;当前 Doubao / Seedance 生产链路最多暴露 15 秒,不再把 30 秒作为单条可选项。每次生成都会创建新的轻量 Job,文生图调用 generateImage 并传图片模型和尺寸,视频调用 generateStoryboardVideo 并传视频模型、画幅和时长;首尾帧模式先用 createCreativeImageJob 保存首帧,再用 uploadReferenceFrame 保存尾帧并以 last_image 提交。首页视频提交后每 2.6 秒轮询 getJob,结果卡会把 queued 显示为“排队中 / 前方 N 个任务 / 你的上一个视频生成中”,把 in_progress 显示为生成进度,完成后直接显示可播放 controls,避免完成视频只是静态首帧看起来“没有效果”。图片/视频缩略图统一复用 MediaAssetTile,支持顶层 hover 预览和删除;顶部“画布”入口指向 /canvas/。旧 TK 复刻工作台组件仍保留在 web/components/ad-recreation-board.tsx,但不再作为默认首页渲染。 - web/canvas-app/SKG 内部画布应用:从 chatfire-AI/huobao-canvas 交互逻辑改造而来,保留 Vue Flow 节点画布、项目列表、节点连接和批量下载等核心画布能力;移除可见原品牌、GitHub 链接、本地 API Key 设置和第三方 base URL 配置,终端可见品牌收敛为 SKG logo。生产路径固定为 /canvas/,内部路由用 /canvas/p/:id?;来源说明保存在 THIRD_PARTY_NOTICES.md,不展示给终端用户。 - web/canvas-app/src/views/Canvas.vue画布主交互:底部悬浮 prompt composer 吸附在画布下方,提供文生视频、文生图、首帧生视频、首尾帧生视频四种模式;首帧 / 尾帧模式只显示必要上传位,底部不再常驻推荐提示词 chips,避免遮挡画布操作。提交后自动创建文本节点、参考图节点、图片配置节点或视频配置节点,并用 autoExecute 触发生成;首尾帧连线会用 imageRole 标记首帧和尾帧,方便视频节点按角色组织请求。 - web/canvas-app/src/hooks/useApi.js画布到本项目后端的适配层:不再读取浏览器 API Key,而是使用当前登录会话 Cookie 调用 /api。文生图 / 图生图先创建轻量 creative job,再调用 /frames/0/generate;文生视频 / 首帧 / 首尾帧视频调用 /storyboard/video 并轮询 /jobs/{id},完成后把图片或 mp4 URL 写回画布节点。 - web/scripts/sync-canvas-dist.mjs构建桥接脚本:把 Vite 产物 web/canvas-app/dist 清空复制到 web/public/canvas,使 Next 静态导出时把画布作为同域子路径一起打包。web/public/canvas/ 是生成产物,已加入 .gitignore。 + web/app/page.tsx旧 React 单对话框生成台源码仍保留,便于以后回滚或抽能力;当前生产根域名已经由 web/canvas-app/ 画布产物覆盖,不再把这个 React 首页作为默认首屏。该页面里的模式也已收敛为文生图、文生视频、图生视频;图生视频只显示“上传图片”,不把“首帧/首尾帧”作为用户入口。旧 TK 复刻工作台组件仍保留在 web/components/ad-recreation-board.tsx,但不再作为默认首页渲染。 + web/canvas-app/SKG 内部画布应用:从 chatfire-AI/huobao-canvas 交互逻辑改造而来,保留 Vue Flow 节点画布、项目列表、节点连接和批量下载等核心画布能力;移除可见原品牌、GitHub 链接、本地 API Key 设置和第三方 base URL 配置,终端可见品牌收敛为 SKG logo。生产路径固定为根域名 /,内部路由用 /p/:id?;来源说明保存在 THIRD_PARTY_NOTICES.md,不展示给终端用户。 + web/canvas-app/src/views/Canvas.vue画布主交互:底部悬浮 prompt composer 吸附在画布下方,提供文生图、文生视频、图生视频三种模式;图生视频只显示“上传图片”,底部不再常驻推荐提示词 chips,避免遮挡画布操作。提交后自动创建文本节点、参考图节点、图片配置节点或视频配置节点,并用 autoExecute 触发生成;图片连到视频节点时默认作为视频参考图。 + web/canvas-app/src/hooks/useApi.js画布到本项目后端的适配层:不再读取浏览器 API Key,而是使用当前登录会话 Cookie 调用 /api。文生图 / 图生图先创建轻量 creative job,再调用 /frames/0/generate;文生视频 / 图生视频调用 /storyboard/video 并轮询 /jobs/{id},完成后把图片或 mp4 URL 写回画布节点。 + web/scripts/sync-canvas-root.mjs构建桥接脚本:在 next build 静态导出完成后,把 Vite 画布产物 web/canvas-app/dist 覆盖到 web/out 根目录,使 https://marketing.skg.com 登录后直接进入画布;旧 web/scripts/sync-canvas-dist.mjs 保留但不再由生产构建调用。 web/app/detail/page.tsx任务详情页:静态导出路由 /detail/?job=<id>,通过 query 读取 job id,调用 getJob 恢复同一任务。页面展示参考图、全部生成图、视频候选、营销图文方案和历史提示词,可继续调用 generateImagegenerateStoryboardVideogenerateCreativeCopy,并支持删除图片/视频。该页继续依赖后端 owner 过滤,用户不能通过切换 URL 读取别人的任务。 web/app/agent/page.tsx新增一键出片终端页:只保留 TikTok 链接、产品图上传、实时 Agent Terminal 和最终成片播放器;通过 POST /agent-runs 创建受限后台状态机任务,通过 GET /agent-runs/{id} 轮询日志、进度、审片图和最终 mp4。该页不替代旧工作台深度编辑能力,只承接“用户只看成品”的快速出片主路径。 web/components/ad-recreation-board.tsx信息流广告复刻工作表:外壳按 Figma “Dashboard Glassmorphism”参考整体改为黑灰玻璃工作台,WorkbenchRail 默认收起为拉满工作台可用高度的 65px 胶囊工具条,只保留真实动作入口:素材任务、资源库和主题切换;鼠标移入或键盘聚焦侧栏时,skg-board-rail 切换 is-open 并从左侧展开 320px 素材输入抽屉,点击素材任务按钮可固定展开。顶部从登录页式 brand strip 改为轻量生产控制条,左侧显示 未来健康 · 营销内容工作台、主标题 营销内容工作台 和副标题 信息流广告复刻生产,右侧保留素材/当前/视频/文案段/背景音指标,并用紫、黄绿、琥珀、青绿、绿色光斑卡片增强原版玻璃拟态的颜色层次。主内容只保留源视频拆解工作区,素材输入的数据流、接口、模型调用和状态推导不变。工作台外层已取消 1800x1000 固定基准画布、ResizeObserver 档位计算和 CSS zoom 整页缩放,改为正常流式桌面容器:min-height: 100vhwidth: 100%max-width: 1920px,并保留 min-width: 1280px 作为最低操作宽度;核心列宽不再被整体缩放,文字、图标和边线由浏览器原生字号渲染,避免小数缩放导致发虚。buildWorkflowSteps 仍统一生成 01-09 流程顺序、状态和判定依据,WorkflowStepBadge / PipelineLane / 分镜列标题也继续共用同一套编号;但完整 WorkflowOrderBar、右侧素材/视频/音频/文案/参考帧需求 chips、文案依据下拉和“音频文案、抽帧参考、主体重构、产品素材池”四个状态条不再默认渲染在工作区顶部。侧边素材输入面板只负责链接/上传和任务切换,不再重复放横版原视频预览;主画布源视频工作区直接进入核心操作。讲话人、节奏和背景音分析仍写入 AudioScript,但不再作为“音频解析结果”卡片默认渲染;源视频工作区撤销右上“布局调节”临时面板,不再读取或写入 localStorage["skg-source-workspace-layout:v1"];当前固定为左侧原视频列 380px、9:16 视频高 500px、逐句时间轴最大高 360px、参考帧池 140px、主体空态 78px;转换层不再固定拉长,按内容自然高度显示,内容过多时最多到 560px 后在自身区域内滚动;上方是按 9:16 显示的竖版原视频播放器,播放器内覆盖“当前点抽帧”,按当前播放秒数手动补参考帧,播放器下方是逐句时间轴,英文和中文都最多显示两行;右侧上方是无标题的波形与切点参考框,下方主体链路改为上方参考帧池 + 转换层、下方主体元素结果栏。音频波形用参考图式的连续灰色包络显示响度、停顿和密集爆点,并通过 skg-audio-waveform 读取明暗主题变量,避免明亮模式继续使用黑底/白色波形;顶部把低/中/高密度按钮和当前播放秒数、总时长、鼠标指针停点秒数直接放在波形上方。视频播放时通过 requestAnimationFrame 平滑驱动波形播放线,同时同步高亮并滚动当前句;点击音频波形或字幕行会跳转原视频时间。音频波形下方同框渲染无标题的 TimelineFilmstrip 临时画面胶片,前端按低/中/高密度从源视频 canvas 截取预览缩略图,并按 frame.time / duration 的百分比定位到和波形同一条时间轴上;波形与胶片之间不显示分隔横线,胶片轨道贴近波形,缩略图轻微上下错落并倾斜重叠排列,hover 时用同一张胶片卡在原位置生成固定顶层克隆,约 4.8 倍放大并自动限制在视口内,避免被工作区、滚动容器或相邻面板遮挡;单击胶片只跳转视频时间点,不写入任务数据,双击胶片或拖进参考帧池时才调用手动抽帧并正式加入 job.frames,已加入的胶片显示“已添加”;胶片预览按 job、视频、密度和时长缓存,未切换低/中/高时返回页面不重新扫视频。参考帧池的主入口是“自动抽帧 12 张”,一键按动作峰值目标重新抽取 12 张源视频参考帧,优先抓手势、表情变化、节奏点和镜头变化;缩略图按竖版完整比例显示不裁切,点选状态直接叠在参考帧池缩略图上,鼠标停留会通过固定浮层放大展示完整帧。转换层改为轻量对话式生图确认区并拿到主操作宽度:左侧参考帧可点 + 或直接拖入转换层,本地图片拖入会通过 uploadReferenceFrame 保存为参考帧;转换层上方是参考输入区,下方不再显示当前要求摘要、保留元素副本或对话记录计数,只保留带张数控件的“发送消息”输入 composer;模型确认类回复不再逐条展示,生成英文 prompt 后发送区主按钮直接切换为“确认生成 N 张”,点击后才调用主体套图生成。主体元素结果栏在转换层下方,空态只占紧凑提示;有结果时按每次生成的套图文件夹显示,左侧横向展示当前套图,右侧切换套图包,保留单张重生和删除;缩略图上提供“重新生成这一张”和“删除这一张”,单张重生会用 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 链路和短时熔断规则,点击后用固定浮层展示模型链路、输入输出和回退逻辑。旧分镜卡、抽帧控制和视频生成组件仍保留在文件里,但当前主路径不渲染。 @@ -652,13 +653,13 @@
当前前端主链路:
-	web/app/page.tsx
-	  -> 单对话框:文生视频 / 文生图 / 首帧生视频 / 首尾帧生视频
+	web/canvas-app/(生产根域名 /)
+	  -> 画布底部对话框:文生图 / 文生视频 / 图生视频
   -> 模型选项:GET /health → image_options / video_options
-  -> 创建轻量任务:POST /creative/jobs/image → 生成只有 0 号关键帧的 Job;首尾帧时再 POST /jobs/{id}/frames/upload
+  -> 创建轻量任务:POST /creative/jobs/image → 生成轻量 Job;图生视频时把上传图片作为视频参考图
   -> 生图:generateImage(job.id, 0, { prompt, mode: text, model }) → jobs/<jobId>/gen
-  -> 生视频:generateStoryboardVideo(job.id, 0, { prompt, model, first_image?, last_image?, duration }) → jobs/<jobId>/storyboard_videos
-	  -> 当前结果:最新图片 / 视频只在对话框下方展示
+  -> 生视频:generateStoryboardVideo(job.id, 0, { prompt, model, first_image?, duration }) → jobs/<jobId>/storyboard_videos
+	  -> 当前结果:图片 / 视频节点自动排列到画布
 	  -> 任务详情页:web/app/detail/page.tsx?job=<id> → getJob → 展示参考图、生成图、视频、提示词、图文方案 → 可继续生成 / 删除 / 复制
 
 旧版 TK 复刻链路(最后版本保留):
@@ -686,7 +687,7 @@ api/main.py
           
你看到的区域SKG 首页
主要源码web/app/page.tsx;前端 API client 在 web/lib/api.ts;轻量创作后端在 api/main.py/creative/jobs/image/creative/copy,实际图片和视频生成继续复用 /jobs/{id}/frames/{idx}/generate/jobs/{id}/frames/{idx}/storyboard/video
-
适合怎么描述“首页只有一个对话框,四个模式是文生视频、文生图、首帧生视频、首尾帧生视频;用户上传必要帧并手写提示词后生成”。
+
适合怎么描述“首页只有一个对话框,三个模式是文生图、文生视频、图生视频;图生视频上传图片后手写提示词生成”。
你看到的区域任务详情页
@@ -1046,7 +1047,7 @@ ProductRefStateItem { 运行配置 / 模型标注GET /healthgetRuntimeHealthModelTrace返回 models:ASR、asr_language(默认 auto,表示中文/英文/多语言自动识别)、asr_base_urlasr_remote_enabledasr_local_fallback_enabledasr_audio_fallback_enabledfaster_whisper、本机 ASR、ASR fallback、翻译、GPT 改写、GPT 画面理解、产品视角识别 product_view、主图像模型 gpt-image-2、图片故障兜底 image_fallbacks、图片尺寸 image_size_options、短时熔断状态 image_circuit、主体 6 视图模型链路、Azure OpenAI TTS、视频别名、视频画幅 video_size_options、真实可用视频时长 video_duration_options、单条最大秒数 video_max_duration_seconds 和 Seedance 服务商。当前 REWRITE_MODELAUDIO_REWRITE_MODELVISION_MODEL 默认使用 gpt-4o;如果旧环境变量仍写 gemini-*,后端会归一化回 GPT_TEXT_MODEL / REWRITE_MODEL。语音只走 Azure OpenAI TTS,models.voice_tts_paths 会回传当前尝试的语音路径,方便区分路径错误和语音服务不可用。前端所有当前主路径里会调用模型的按钮旁显示模型名,点击弹出小窗口查看模型链路和输入输出逻辑;不返回 API Key 或敏感凭证。 历史列表GET /jobslistJobs当前登录用户可见 job 精简列表(id/url/status/thumbnail/mtime/owner…),按 state.json mtime 倒序。前端 URL 无 ?job= 时拉它回填本人历史;带 limit 可截断。开启数据隔离时,飞书用户只看到自己的任务,历史无 owner 的旧任务只对备用账号可见。 创建任务POST /jobscreateJob提交 TK 链接,后台开始下载;后端会把当前登录用户写入 Job.owner_*,后续详情、素材文件、删除和生成接口都通过统一中间件校验归属。下载阶段默认不带 cookies;生产环境必须显式保持 YTDLP_COOKIES_FILE=YTDLP_COOKIES_FROM_BROWSER= 为空,避免容器内误读被打进镜像的开发 api/.env。 - 画布生成POST /creative/jobs/image
POST /jobs/{id}/frames/upload
POST /jobs/{id}/frames/{idx}/generate
POST /jobs/{id}/frames/{idx}/storyboard/video
GET /jobs/{id}web/canvas-app/src/hooks/useApi.js/canvas/ 不单独保存后端画布表,画布项目当前在浏览器 localStorage;一旦生成图片或视频,就通过同一套 creative job / frame / storyboard video 接口写入当前登录用户自己的 job 目录。文生图会创建空白 creative job 后生成图片;首帧 / 首尾帧视频会把上传图转成 frame,提交视频后用 skg:{jobId}:{videoId} 作为画布侧任务 id 轮询 /jobs/{id},直到视频状态完成或失败。 + 画布生成POST /creative/jobs/image
POST /jobs/{id}/frames/upload
POST /jobs/{id}/frames/{idx}/generate
POST /jobs/{id}/frames/{idx}/storyboard/video
GET /jobs/{id}web/canvas-app/src/hooks/useApi.js根域名画布不单独保存后端画布表,画布项目当前在浏览器 localStorage;一旦生成图片或视频,就通过同一套 creative job / frame / storyboard video 接口写入当前登录用户自己的 job 目录。文生图会创建空白 creative job 后生成图片;图生视频会把上传图转成 frame 并作为视频参考图提交,提交视频后用 skg:{jobId}:{videoId} 作为画布侧任务 id 轮询 /jobs/{id},直到视频状态完成或失败。 一键出片终端POST /agent-runs
GET /agent-runs
GET /agent-runs/{id}
GET /agent-runs/{id}/final.mp4
GET /agent-runs/{id}/contact.jpgweb/app/agent/page.tsx快速出片页的唯一主接口。前端提交 TikTok 链接和最多 6 张产品图;后端创建同 owner 的 JobAgentRun,后台执行下载、产品图归一化、透明骨架主体参考复制、12 段镜头计划、视频生成、失败镜头自动重跑一次、审片接触表和 ffmpeg 最终合成。列表、详情、最终 mp4 和接触表同样按 owner 隔离。 重试下载POST /jobs/{id}/download/retryretryJobDownload用于 TK 链接下载失败且没有 video_url 的素材;清空错误、重新进入下载状态,并在后台再次执行 pipeline_download。上传视频不能重下载,需要重新上传文件。 上传视频POST /jobs/uploaduploadJob保存 source.mp4,然后同样进入下载完成状态;当前上传后也加入第一步队列,下载完成后自动解析音频。 @@ -1115,7 +1116,7 @@ ProductRefStateItem { 内容生产画布 承载个人自由排列的创作空间:用户在画布上用对话框生成文本、图片和视频节点,结果按节点位置沉淀,不和默认首页的单条结果卡互相挤压。画布项目先保存在浏览器本地,生成资产进入后端个人 job。 当前不做团队共享画布、管理员总览、多人协同编辑或跨浏览器同步;也不让员工在浏览器里配置上游 API Key。 - web/canvas-app/deploy/nginx.confweb/scripts/sync-canvas-dist.mjs + web/canvas-app/deploy/nginx.confweb/scripts/sync-canvas-root.mjs 音频条 @@ -1151,7 +1152,7 @@ ProductRefStateItem {
  • GPT Image 生图;当前 IMAGE_MODEL 和主体 6 视图链路默认使用 gpt-image-2,单次图片网关请求默认 60 秒超时;主模型超时、429、5xx 或网络错误时允许 gemini-3-pro-image-preview 兜底,并有 2 次失败 / 600 秒短时熔断。
  • 三字段分镜候选生成:默认行左侧露文案、场景一句话、人物+产品+动作,右侧直接展示横向视频轨;中文镜像失焦后会自动优化英文主值;支持 AI 改写预览、单条选择数量生成、追加生成、选中候选和整片按行排队提交。
  • 全局资源中心:提示词库和素材库可从顶部“资源库”打开;提示词可复制并计数,素材应用到 job 时会复制成本 job 内普通 asset。
  • -
  • 画布:/canvas/ 已作为登录后子应用接入,支持文生图、文生视频、首帧生视频、首尾帧生视频四种节点化生成;生成资产继续写入当前登录用户自己的后端 job。
  • +
  • 画布:https://marketing.skg.com 登录后直接进入个人生成画布,支持文生图、文生视频、图生视频三种节点化生成;生成资产继续写入当前登录用户自己的后端 job。
  • @@ -1195,7 +1196,7 @@ ProductRefStateItem {

    改画布

    -

    “我在 /canvas/ 画布里,左侧或底部对话框、节点类型、生成结果排列、排队状态、下载或删除行为要怎么改。”

    +

    “我在根域名画布里,左侧或底部对话框、节点类型、生成结果排列、排队状态、下载或删除行为要怎么改。”

    @@ -1204,6 +1205,32 @@ ProductRefStateItem {

    变更记录

    这个记录不是 git log 的替代品。它记录“产品理解发生了什么变化、影响了哪些源码、你以后描述需求时该怎么说”。后续每次改功能都要补一条。

    +
    +
    +

    2026-05-25 · 根域名直接进入个人生成画布

    + UI + Deploy + Docs +
    +
    +

    问题:用户已经确定用成熟画布版快速上架,不希望员工先进入单对话框首页再点 /canvas/,也不希望我们继续大幅重写成熟画布结构。

    +

    改动:web/canvas-app 的 Vite base 和 Vue Router base 改为根路径;web/package.json 生产构建改为先构建画布、再 Next 静态导出、最后用 web/scripts/sync-canvas-root.mjs 把画布产物覆盖到 web/out 根目录。deploy/nginx.conf/canvas/ 改成 308 兼容跳转到根域名。

    +

    影响:https://marketing.skg.com 登录后直接进入个人生成画布;旧 /canvas/ 链接不会失效,但不再是主入口。后续优先围绕模式文案、API 接入、队列和结果显示做小改,不反复拆成熟画布。

    +
    +
    +
    +
    +

    2026-05-25 · 生成入口收敛为三模式

    + UI + Product + Docs +
    +
    +

    问题:首帧生视频首尾帧生视频 是模型提交细节,不应该作为用户主入口;和成熟生成产品里的“文生图、文生视频、图生视频”概念相比显得混乱。

    +

    改动:web/app/page.tsxweb/canvas-app/src/views/Canvas.vue 的模式按钮统一收敛为文生图、文生视频、图生视频。图生视频只显示“上传图片”,内部仍用 first_image 提交视频;画布视频节点状态也从首帧/尾帧细节收敛为图片状态。

    +

    影响:终端用户不再需要理解首帧、尾帧等实现概念;生成能力保留文生图、文生视频和单图参考的视频生成。

    +
    +

    2026-05-25 · 移除画布底部推荐提示词

    diff --git a/web/app/page.tsx b/web/app/page.tsx index 29b849e..f21f330 100644 --- a/web/app/page.tsx +++ b/web/app/page.tsx @@ -8,7 +8,6 @@ import { Image as ImageIcon, Loader2, Plus, - Sparkles, Upload, X, type LucideIcon, @@ -24,7 +23,6 @@ import { generateStoryboardVideo, getRuntimeHealth, getJob, - uploadReferenceFrame, type GeneratedImage, type GeneratedVideo, type Job, @@ -32,26 +30,19 @@ import { type RuntimeSizeOption, } from "@/lib/api" -type CreationMode = "text-video" | "text-image" | "first-frame-video" | "first-last-frame-video" +type CreationMode = "text-image" | "text-video" | "image-video" type BusyTask = CreationMode | "job" | null -type UploadSlot = "first" | "last" +type UploadSlot = "first" type ModeConfig = { id: CreationMode label: string icon: LucideIcon placeholder: string - needsFirstFrame?: boolean - needsLastFrame?: boolean + needsImage?: boolean } const MODES: ModeConfig[] = [ - { - id: "text-video", - label: "文生视频", - icon: Clapperboard, - placeholder: "写清楚画面、人物动作、产品出现方式、镜头运动和风格。例如:15 秒竖屏,办公室午休,人物戴上 SKG 颈部按摩仪放松,镜头缓慢推进。", - }, { id: "text-image", label: "文生图", @@ -59,19 +50,17 @@ const MODES: ModeConfig[] = [ placeholder: "写清楚画面、主体、构图、光线和比例。例如:9:16 信息流营销图,真实办公室场景,SKG 颈部按摩仪佩戴清楚。", }, { - id: "first-frame-video", - label: "首帧生视频", - icon: Upload, - needsFirstFrame: true, - placeholder: "上传首帧后写视频变化:人物怎么动、镜头怎么动、产品要保持什么细节、时长多长。", + id: "text-video", + label: "文生视频", + icon: Clapperboard, + placeholder: "写清楚画面、人物动作、产品出现方式、镜头运动和风格。例如:15 秒竖屏,办公室午休,人物戴上 SKG 颈部按摩仪放松,镜头缓慢推进。", }, { - id: "first-last-frame-video", - label: "首尾帧生视频", - icon: Sparkles, - needsFirstFrame: true, - needsLastFrame: true, - placeholder: "上传首帧和尾帧后,写中间如何过渡、动作节奏、镜头运动和产品细节保持要求。", + id: "image-video", + label: "图生视频", + icon: Upload, + needsImage: true, + placeholder: "上传图片后,写它要怎么动、镜头怎么运动、产品细节怎么保持、视频节奏和时长。", }, ] @@ -129,9 +118,7 @@ export default function Home() { const [prompt, setPrompt] = useState("") const [seconds, setSeconds] = useState(12) const [firstFrameFile, setFirstFrameFile] = useState(null) - const [lastFrameFile, setLastFrameFile] = useState(null) const [firstFramePreview, setFirstFramePreview] = useState("") - const [lastFramePreview, setLastFramePreview] = useState("") const [imageModel, setImageModel] = useState("auto") const [videoModel, setVideoModel] = useState("seedance") const [imageSize, setImageSize] = useState("1024x1536") @@ -149,7 +136,6 @@ export default function Home() { const [busy, setBusy] = useState(null) const [error, setError] = useState("") const firstInputRef = useRef(null) - const lastInputRef = useRef(null) const activeMode = MODES.find((item) => item.id === mode) ?? MODES[0] const latestImage = latestGeneratedImage(job) @@ -203,16 +189,6 @@ export default function Home() { return () => URL.revokeObjectURL(url) }, [firstFrameFile]) - useEffect(() => { - if (!lastFrameFile) { - setLastFramePreview("") - return - } - const url = URL.createObjectURL(lastFrameFile) - setLastFramePreview(url) - return () => URL.revokeObjectURL(url) - }, [lastFrameFile]) - useEffect(() => { if (!job || !runningVideo) return const timer = window.setInterval(async () => { @@ -233,18 +209,13 @@ export default function Home() { const onModeChange = (nextMode: CreationMode) => { setMode(nextMode) resetResult() - if (nextMode === "text-video" || nextMode === "text-image") { + if (nextMode !== "image-video") { setFirstFrameFile(null) - setLastFrameFile(null) - } - if (nextMode === "first-frame-video") { - setLastFrameFile(null) } } const setUploadFile = (slot: UploadSlot, file: File | null) => { if (slot === "first") setFirstFrameFile(file) - if (slot === "last") setLastFrameFile(file) resetResult() } @@ -253,12 +224,8 @@ export default function Home() { toast.error("先写提示词") return false } - if (activeMode.needsFirstFrame && !firstFrameFile) { - toast.error("先上传首帧") - return false - } - if (activeMode.needsLastFrame && !lastFrameFile) { - toast.error("先上传尾帧") + if (activeMode.needsImage && !firstFrameFile) { + toast.error("先上传图片") return false } return true @@ -270,13 +237,10 @@ export default function Home() { const prepareJob = useCallback(async () => { setBusy("job") - let created = await createCreativeImageJob(firstFrameFile) - if (mode === "first-last-frame-video" && lastFrameFile) { - created = await uploadReferenceFrame(created.id, lastFrameFile) - } + const created = await createCreativeImageJob(firstFrameFile) setJob(created) return created - }, [firstFrameFile, lastFrameFile, mode]) + }, [firstFrameFile]) const runImage = async () => { if (!validate()) return @@ -307,13 +271,12 @@ export default function Home() { setError("") try { const target = await prepareJob() - const lastFrame = [...target.frames].sort((a, b) => b.index - a.index)[0] const updated = await generateStoryboardVideo(target.id, 0, { prompt: promptWithGuardrails(), duration: seconds, count: 1, - first_image: activeMode.needsFirstFrame ? { kind: "keyframe", frame_idx: 0 } : null, - last_image: activeMode.needsLastFrame && lastFrame ? { kind: "keyframe", frame_idx: lastFrame.index } : null, + first_image: activeMode.needsImage ? { kind: "keyframe", frame_idx: 0 } : null, + last_image: null, size: videoSize, model: videoModel, }) @@ -397,24 +360,15 @@ export default function Home() { })}
    - {(activeMode.needsFirstFrame || activeMode.needsLastFrame) ? ( -
    + {activeMode.needsImage ? ( +
    firstInputRef.current?.click()} onClear={() => setUploadFile("first", null)} /> - {activeMode.needsLastFrame ? ( - lastInputRef.current?.click()} - onClear={() => setUploadFile("last", null)} - /> - ) : null}
    ) : null} @@ -425,14 +379,6 @@ export default function Home() { className="hidden" onChange={(event) => setUploadFile("first", event.target.files?.[0] ?? null)} /> - setUploadFile("last", event.target.files?.[0] ?? null)} - /> -