From e77e77fadafd4d63bcbf78939017e5df611547af Mon Sep 17 00:00:00 2001 From: kang Date: Mon, 25 May 2026 14:23:09 +0800 Subject: [PATCH] fix: align generation size and duration options --- RULES.md | 2 +- api/main.py | 138 ++++++++++++++++++++++++++++++++++++-- docs/source-analysis.html | 29 +++++--- web/app/page.tsx | 77 +++++++++++++++++++-- web/lib/api.ts | 16 ++++- 5 files changed, 239 insertions(+), 23 deletions(-) diff --git a/RULES.md b/RULES.md index d61b316..d9d3fb3 100644 --- a/RULES.md +++ b/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-25 单对话框版):默认首页彻底从“信息流广告复刻管线”切换为多人通用的 SKG 营销内容生成入口,服务约 6 名公司成员同时使用。首页默认只保留一个中央对话框,不再显示侧栏、灵感区、任务列表或大结果面板;用户先选择四种生成方式之一:文生视频、文生图、首帧生视频、首尾帧生视频,然后手写提示词并点击生成。首帧 / 首尾帧模式只露必要图片上传位,视频模式只保留时长选择。后端 `/health` 向前端返回可选图片 / 视频模型,首页允许用户选择图片模型(自动、GPT Image 2、Gemini 图片兜底)和视频模型(Seedance、Kling、Veo 3 等别名;实际可用模型以环境变量映射为准)。用户登录后仍只看到自己的任务、结果和详情页,继续沿用后端 owner 隔离;结果生成后从对话框下方进入 `/detail/?job=` 沉淀参考图、生成图、视频候选和提示词。旧 TK 复刻工作台、Agent Cut 一键出片和营销图文方案保留为高级/详情页能力,不再作为默认首页入口或默认理解框架。 +- 当前产品方向(2026-05-25 单对话框版):默认首页彻底从“信息流广告复刻管线”切换为多人通用的 SKG 营销内容生成入口,服务约 6 名公司成员同时使用。首页默认只保留一个中央对话框,不再显示侧栏、灵感区、任务列表或大结果面板;用户先选择四种生成方式之一:文生视频、文生图、首帧生视频、首尾帧生视频,然后手写提示词并点击生成。首帧 / 首尾帧模式只露必要图片上传位,图片模式显示尺寸选择,视频模式显示画幅和真实可用时长选择。后端 `/health` 向前端返回可选图片 / 视频模型、图片尺寸、视频画幅和视频时长,首页允许用户选择图片模型(自动、GPT Image 2、Gemini 图片兜底)和视频模型(Seedance、Kling、Veo 3 等别名;实际可用模型以环境变量映射为准)。当前 Doubao / Seedance 生产链路单条视频最长按 15 秒暴露,不在 UI 显示 30 秒;如后续要 30 秒,需要改成多段生成后合成。用户登录后仍只看到自己的任务、结果和详情页,继续沿用后端 owner 隔离;结果生成后从对话框下方进入 `/detail/?job=` 沉淀参考图、生成图、视频候选和提示词。旧 TK 复刻工作台、Agent Cut 一键出片和营销图文方案保留为高级/详情页能力,不再作为默认首页入口或默认理解框架。 ## 部署事实 - 平台:VPS `76.13.31.179`(Ubuntu 24.04 / Docker Compose / Coolify Traefik) diff --git a/api/main.py b/api/main.py index b5b926f..673678e 100644 --- a/api/main.py +++ b/api/main.py @@ -124,6 +124,58 @@ IMAGE_FALLBACK_ENABLED = os.getenv("IMAGE_FALLBACK_ENABLED", "true").strip().low IMAGE_MODEL = GPT_IMAGE_MODEL PRODUCT_VIEW_MODEL = GPT_IMAGE_MODEL SUBJECT_ASSET_IMAGE_MODEL = GPT_IMAGE_MODEL +IMAGE_SIZE_CHOICES = [ + { + "id": "auto", + "label": "自动", + "value": "auto", + "description": "由图片模型自行决定输出尺寸", + }, + { + "id": "1024x1536", + "label": "竖图 2:3", + "value": "1024x1536", + "description": "适合信息流营销图、人物和产品竖版构图", + }, + { + "id": "1024x1024", + "label": "方图 1:1", + "value": "1024x1024", + "description": "适合头像、方形素材和电商图", + }, + { + "id": "1536x1024", + "label": "横图 3:2", + "value": "1536x1024", + "description": "适合横版封面和详情页配图", + }, +] +VIDEO_SIZE_CHOICES = [ + { + "id": "720x1280", + "label": "竖屏 9:16", + "value": "720x1280", + "description": "适合抖音、短视频和飞书内预览", + }, + { + "id": "1280x720", + "label": "横屏 16:9", + "value": "1280x720", + "description": "适合横版展示和网页视频", + }, + { + "id": "1024x1024", + "label": "方形 1:1", + "value": "1024x1024", + "description": "适合方形广告位", + }, + { + "id": "960x1280", + "label": "竖屏 3:4", + "value": "960x1280", + "description": "适合更接近图文卡片的竖版素材", + }, +] SubjectModelBundle = Literal["gpt", "gemini"] SubjectAgentMode = Literal["realistic", "cartoon", "elements", "custom"] SUBJECT_AGENT_GPT_MODEL = gpt_model_env("SUBJECT_AGENT_GPT_MODEL", VISION_MODEL) @@ -4134,6 +4186,67 @@ def image_model_options() -> list[dict]: return options +def image_size_options() -> list[dict]: + return IMAGE_SIZE_CHOICES + + +def _normalize_image_size(raw: str | None) -> str: + value = (raw or "auto").strip().lower() + aliases = { + "vertical": "1024x1536", + "portrait": "1024x1536", + "竖图": "1024x1536", + "square": "1024x1024", + "方图": "1024x1024", + "horizontal": "1536x1024", + "landscape": "1536x1024", + "横图": "1536x1024", + } + value = aliases.get(value, value) + allowed = {str(item["value"]) for item in IMAGE_SIZE_CHOICES} + if value not in allowed: + raise HTTPException(400, f"unsupported image size: {raw}") + return value + + +def _image_size_payload(raw: str | None) -> dict: + size = _normalize_image_size(raw) + return {} if size == "auto" else {"size": size} + + +def video_duration_options() -> list[int]: + if video_uses_ark(): + return [5, 8, 10, 12, 15] + return [4, 8, 12] + + +def video_size_options() -> list[dict]: + return VIDEO_SIZE_CHOICES + + +def _normalize_video_size(raw: str | None) -> str: + value = (raw or "720x1280").strip().lower().replace(" ", "") + aliases = { + "vertical": "720x1280", + "portrait": "720x1280", + "9:16": "720x1280", + "竖屏": "720x1280", + "horizontal": "1280x720", + "landscape": "1280x720", + "16:9": "1280x720", + "横屏": "1280x720", + "square": "1024x1024", + "1:1": "1024x1024", + "方形": "1024x1024", + "3:4": "960x1280", + } + value = aliases.get(value, value) + allowed = {str(item["value"]) for item in VIDEO_SIZE_CHOICES} + if value not in allowed: + raise HTTPException(400, f"unsupported video size: {raw}") + return value + + def video_model_options() -> list[dict]: label_map = { "seedance": "Seedance", @@ -4156,7 +4269,10 @@ def video_model_options() -> list[dict]: "id": key, "label": label_map.get(key, key), "model": model, - "description": "当前视频网关可选模型", + "description": f"当前视频网关可选模型;单次时长最高 {max(video_duration_options())} 秒", + "duration_options": video_duration_options(), + "size_options": video_size_options(), + "max_duration_seconds": max(video_duration_options()), "available": bool(video_api_key()), }) default_model = resolve_video_model(VIDEO_MODEL) @@ -4166,6 +4282,9 @@ def video_model_options() -> list[dict]: "label": label_map.get(VIDEO_MODEL, VIDEO_MODEL), "model": default_model, "description": "默认视频模型", + "duration_options": video_duration_options(), + "size_options": video_size_options(), + "max_duration_seconds": max(video_duration_options()), "available": bool(video_api_key()), }) return options @@ -4252,12 +4371,12 @@ def _image_endpoint(path: str) -> str: return f"{base}/{path.lstrip('/')}" -def _image_generation_response(prompt: str, model: str) -> dict: +def _image_generation_response(prompt: str, model: str, size: str | None = "auto") -> dict: with ai_http_client(timeout=IMAGE_REQUEST_TIMEOUT_SECONDS) as client: r = client.post( _image_endpoint("/images/generations"), headers={"Authorization": f"Bearer {IMAGE_API_KEY}"}, - json={"model": model, "prompt": prompt, "n": 1}, + json={"model": model, "prompt": prompt, "n": 1, **_image_size_payload(size)}, ) r.raise_for_status() return r.json() @@ -5198,6 +5317,7 @@ def health() -> dict: "image_base_url": IMAGE_BASE_URL or LLM_BASE_URL or "openai-default", "image_request_timeout_seconds": IMAGE_REQUEST_TIMEOUT_SECONDS, "image_options": image_model_options(), + "image_size_options": image_size_options(), "ai_proxy_configured": bool(AI_HTTP_PROXY), "image_fallbacks": _image_fallback_models(), "image_circuit": _image_circuit_snapshot(), @@ -5213,6 +5333,9 @@ def health() -> dict: "video": VIDEO_MODEL, "video_aliases": VIDEO_MODEL_ALIASES, "video_options": video_model_options(), + "video_duration_options": video_duration_options(), + "video_max_duration_seconds": max(video_duration_options()), + "video_size_options": video_size_options(), "video_provider": video_provider_name(), "video_base_url": video_api_base(), "video_configured": bool(video_api_key()), @@ -5666,6 +5789,7 @@ class GenerateReq(BaseModel): extra_prompt: str = "" # ✓ 需要的元素(正向) negative_prompt: str = "" # ✗ 不需要的元素(负向) model: str = "auto" # auto / gpt-image-2 / gemini-3-pro-image-preview + size: str = "auto" # auto / 1024x1536 / 1024x1024 / 1536x1024 mode: str = "edit" # "edit" 带参考图,"text" 纯文字 from_selected: bool = False # True 时优先用 frame.selected 的生成图作 reference(迭代),否则原关键帧 @@ -5702,6 +5826,7 @@ def generate_image(job_id: str, idx: int, req: GenerateReq) -> Job: if not raw_prompt: raise HTTPException(400, "prompt required") full_prompt = _ensure_english(raw_prompt) + image_size = _normalize_image_size(req.size) if not IMAGE_API_KEY: raise HTTPException(503, "IMAGE_API_KEY 或 LLM_API_KEY 未配置") @@ -5742,14 +5867,14 @@ def generate_image(job_id: str, idx: int, req: GenerateReq) -> Job: headers={ "Authorization": f"Bearer {IMAGE_API_KEY}", }, - data={"model": current_model, "prompt": full_prompt, "n": "1"}, + data={"model": current_model, "prompt": full_prompt, "n": "1", **_image_size_payload(image_size)}, files={"image": ("reference.jpg", img_bytes_in, "image/jpeg")}, ) r.raise_for_status() resp_data = r.json() else: # text-only - resp_data = _image_generation_response(full_prompt, current_model) + resp_data = _image_generation_response(full_prompt, current_model, image_size) if resp_data.get("data"): effective_mode = f"{current_mode}:{current_model}" @@ -7870,6 +7995,7 @@ def _enqueue_storyboard_videos(job: Job, frame: KeyFrame, req: GenerateStoryboar model = resolve_video_model(req.model) seconds = video_seconds(float(req.duration or 4)) + video_size = _normalize_video_size(req.size) source_ref = req.source_ref if source_ref and source_ref.kind == "source_video" and not source_ref.url: source_ref = None @@ -7894,7 +8020,7 @@ def _enqueue_storyboard_videos(job: Job, frame: KeyFrame, req: GenerateStoryboar progress=0, created_at=time.time(), )) - task_args = (job.id, local_id, "", ref_path, variant_prompt, model, seconds, req.size, source_ref, last_ref_path, reference_ref_paths, primary_role) + task_args = (job.id, local_id, "", ref_path, variant_prompt, model, seconds, video_size, source_ref, last_ref_path, reference_ref_paths, primary_role) if bg is not None: bg.add_task(render_storyboard_video, *task_args) else: diff --git a/docs/source-analysis.html b/docs/source-analysis.html index 1758faf..80462fe 100644 --- a/docs/source-analysis.html +++ b/docs/source-analysis.html @@ -572,16 +572,16 @@

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

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

-

2026-05-25 单对话框版:默认首页再收敛为一个中央对话框,首页只让用户选文生视频、文生图、首帧生视频、首尾帧生视频,然后手写提示词生成。首帧 / 首尾帧模式只出现必要上传位;营销图文不再作为首页默认入口。后端 /health 返回可选图片 / 视频模型,首页按返回值显示模型选择。

+

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

-

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

+

当前默认业务管线是“个人隔离任务 → 在中央对话框选择生成方式 → 选择模型和规格 → 必要时上传首帧 / 尾帧 → 手写提示词 → 生成图片或视频 → 进入详情页继续沉淀”。首页不再渲染侧栏、灵感区、最近任务列表、自动设置或营销图文入口;默认只做四件事:文生视频、文生图、首帧生视频、首尾帧生视频。底层仍复用既有 /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/,营销图文能力仍在详情页和接口中保留,但不作为默认首页路径。

01

个人任务

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

02

选择方式

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

-
03

选择模型

GET /health 返回 image_optionsvideo_options;首页按当前生成方式切换模型下拉。

+
03

选择模型和规格

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

04

上传帧 / 空白任务

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

05

手写提示词

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

-
06

生成图片 / 视频

generateImagemode=text 和图片模型;generateStoryboardVideo 提交文本、模型、可选 first_image 和可选 last_image

+
06

生成图片 / 视频

generateImagemode=text、图片模型和图片尺寸;generateStoryboardVideo 提交文本、模型、画幅、时长、可选 first_image 和可选 last_image

07

结果沉淀

首页只在对话框下方显示最新图片或视频;所有图片/视频缩略图继续复用 MediaAssetTile

08

详情页

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

09

高级复刻

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

@@ -597,7 +597,7 @@ 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当前默认首页:单对话框生成台。页面只保留顶部极轻量品牌和中央对话框,四个主按钮是文生视频、文生图、首帧生视频、首尾帧生视频;首帧 / 首尾帧模式才显示上传位,视频模式只保留时长选择,用户必须手写提示词后点击生成。页面启动时读取 getRuntimeHealth,按 image_options / video_options 显示模型下拉。每次生成都会创建新的轻量 Job,文生图调用 generateImage 并传图片模型,视频调用 generateStoryboardVideo 并传视频模型;首尾帧模式先用 createCreativeImageJob 保存首帧,再用 uploadReferenceFrame 保存尾帧并以 last_image 提交。图片/视频缩略图统一复用 MediaAssetTile,支持顶层 hover 预览和删除;旧 TK 复刻工作台组件仍保留在 web/components/ad-recreation-board.tsx,但不再作为默认首页渲染。 + web/app/page.tsx当前默认首页:单对话框生成台。页面只保留顶部极轻量品牌和中央对话框,四个主按钮是文生视频、文生图、首帧生视频、首尾帧生视频;首帧 / 首尾帧模式才显示上传位,用户必须手写提示词后点击生成。页面启动时读取 getRuntimeHealth,按 image_options / video_options 显示模型下拉,按 image_size_options 显示文生图尺寸,按 video_size_optionsvideo_duration_options 显示视频画幅和真实可用时长;当前 Doubao / Seedance 生产链路最多暴露 15 秒,不再把 30 秒作为单条可选项。每次生成都会创建新的轻量 Job,文生图调用 generateImage 并传图片模型和尺寸,视频调用 generateStoryboardVideo 并传视频模型、画幅和时长;首尾帧模式先用 createCreativeImageJob 保存首帧,再用 uploadReferenceFrame 保存尾帧并以 last_image 提交。图片/视频缩略图统一复用 MediaAssetTile,支持顶层 hover 预览和删除;旧 TK 复刻工作台组件仍保留在 web/components/ad-recreation-board.tsx,但不再作为默认首页渲染。 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 链路和短时熔断规则,点击后用固定浮层展示模型链路、输入输出和回退逻辑。旧分镜卡、抽帧控制和视频生成组件仍保留在文件里,但当前主路径不渲染。 @@ -619,7 +619,7 @@ web/components/product-library-picker.tsxSKG 内置白底产品图库选择器:搜索、品类筛选、预览尺寸,并把库内图片复制为当前 job 的 assetweb/components/storyboard-bar.tsx顶部分镜编排条:展示选入编排的关键帧,并作为唯一分镜导航。 web/components/storyboard-workbench.tsx顶部分镜编排条下方的明细区:4 图槽、改造目标、时长、自动保存。 - web/lib/api.ts前端类型和 API client,是前后端数据契约镜像;RuntimeHealth / RuntimeModels 读取 GET /health,把 ASR、翻译、视觉、图像、视频等模型名作为前端模型标注的真源。默认首页主要使用 createCreativeImageJobuploadReferenceFramegenerateImagegenerateStoryboardVideogenerateCreativeCopy 仍保留给详情页和后续高级能力。资源库相关类型和 CRUD/use/copy-to-job 函数继续保留给旧工作台和后续资源中心。 + web/lib/api.ts前端类型和 API client,是前后端数据契约镜像;RuntimeHealth / RuntimeModels 读取 GET /health,把 ASR、翻译、视觉、图像、视频等模型名作为前端模型标注的真源,并同步承接 RuntimeSizeOption、图片尺寸、视频画幅、视频时长和最大单条秒数。默认首页主要使用 createCreativeImageJobuploadReferenceFramegenerateImagegenerateStoryboardVideogenerateImage 请求体现在可传 sizegenerateCreativeCopy 仍保留给详情页和后续高级能力。资源库相关类型和 CRUD/use/copy-to-job 函数继续保留给旧工作台和后续资源中心。
@@ -627,7 +627,7 @@

后端核心

- + @@ -1034,7 +1034,7 @@ ProductRefStateItem { - + @@ -1183,6 +1183,19 @@ ProductRefStateItem {

变更记录

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

+
+
+

2026-05-25 · 首页按模型能力暴露尺寸和时长

+ API + UI + Docs +
+
+

问题:首页视频时长写死显示 20 秒和 30 秒,但后端当前 Ark / Doubao 视频提交会把时长截到最高 15 秒,Poe / 自定义视频通道最高只发 12 秒;文生图也没有尺寸选择,前端无法把竖图、方图或横图需求传给图片模型。

+

改动:api/main.py/health 新增 image_size_optionsvideo_size_optionsvideo_duration_optionsvideo_max_duration_secondsGenerateReq 新增 size,文生图和图生图都会把尺寸传给 /images/generations/images/editsweb/app/page.tsx 改为按后端能力渲染图片尺寸、视频画幅和视频时长,视频不再显示无法单条提交的 30 秒。

+

影响:当前 Doubao / Seedance 生产配置下,首页视频时长只应显示 5、8、10、12、15 秒;如果业务要 30 秒视频,应新增“分段生成 15 + 15 秒并合成”的工作流,而不是把单次生成时长直接改成 30。

+
+

2026-05-25 · 默认首页收敛为单对话框四模式生成

diff --git a/web/app/page.tsx b/web/app/page.tsx index 9f303db..0e6d62e 100644 --- a/web/app/page.tsx +++ b/web/app/page.tsx @@ -29,6 +29,7 @@ import { type GeneratedVideo, type Job, type RuntimeModelOption, + type RuntimeSizeOption, } from "@/lib/api" type CreationMode = "text-video" | "text-image" | "first-frame-video" | "first-last-frame-video" @@ -99,6 +100,19 @@ function isVideoMode(mode: CreationMode) { return mode !== "text-image" } +const DEFAULT_IMAGE_SIZE_OPTIONS: RuntimeSizeOption[] = [ + { id: "1024x1536", label: "竖图 2:3", value: "1024x1536" }, + { id: "1024x1024", label: "方图 1:1", value: "1024x1024" }, + { id: "1536x1024", label: "横图 3:2", value: "1536x1024" }, + { id: "auto", label: "自动", value: "auto" }, +] + +const DEFAULT_VIDEO_SIZE_OPTIONS: RuntimeSizeOption[] = [ + { id: "720x1280", label: "竖屏 9:16", value: "720x1280" }, + { id: "1280x720", label: "横屏 16:9", value: "1280x720" }, + { id: "1024x1024", label: "方形 1:1", value: "1024x1024" }, +] + export default function Home() { const [mode, setMode] = useState("text-video") const [prompt, setPrompt] = useState("") @@ -109,12 +123,17 @@ export default function Home() { const [lastFramePreview, setLastFramePreview] = useState("") const [imageModel, setImageModel] = useState("auto") const [videoModel, setVideoModel] = useState("seedance") + const [imageSize, setImageSize] = useState("1024x1536") + const [videoSize, setVideoSize] = useState("720x1280") + const [videoDurationOptions, setVideoDurationOptions] = useState([5, 8, 10, 12, 15]) const [imageOptions, setImageOptions] = useState([ { id: "auto", label: "自动", model: "gpt-image-2", available: true }, ]) const [videoOptions, setVideoOptions] = useState([ { id: "seedance", label: "Seedance", model: "seedance", available: true }, ]) + const [imageSizeOptions, setImageSizeOptions] = useState(DEFAULT_IMAGE_SIZE_OPTIONS) + const [videoSizeOptions, setVideoSizeOptions] = useState(DEFAULT_VIDEO_SIZE_OPTIONS) const [job, setJob] = useState(null) const [busy, setBusy] = useState(null) const [error, setError] = useState("") @@ -140,14 +159,26 @@ export default function Home() { const nextVideoOptions = models?.video_options?.length ? models.video_options : [{ id: models?.video || "seedance", label: "Seedance", model: models?.video || "seedance", available: !!models?.video_configured }] + const nextImageSizeOptions = models?.image_size_options?.length ? models.image_size_options : DEFAULT_IMAGE_SIZE_OPTIONS + const nextVideoSizeOptions = models?.video_size_options?.length ? models.video_size_options : DEFAULT_VIDEO_SIZE_OPTIONS + const nextDurationOptions = models?.video_duration_options?.length ? models.video_duration_options : [5, 8, 10, 12, 15] setImageOptions(nextImageOptions) setVideoOptions(nextVideoOptions) + setImageSizeOptions(nextImageSizeOptions) + setVideoSizeOptions(nextVideoSizeOptions) + setVideoDurationOptions(nextDurationOptions) if (!nextImageOptions.some((item) => item.id === imageModel)) setImageModel(nextImageOptions[0]?.id || "auto") if (!nextVideoOptions.some((item) => item.id === videoModel)) setVideoModel(nextVideoOptions[0]?.id || "seedance") + if (!nextImageSizeOptions.some((item) => item.value === imageSize)) setImageSize(nextImageSizeOptions[0]?.value || "1024x1536") + if (!nextVideoSizeOptions.some((item) => item.value === videoSize)) setVideoSize(nextVideoSizeOptions[0]?.value || "720x1280") + if (!nextDurationOptions.includes(seconds)) setSeconds(nextDurationOptions.includes(12) ? 12 : (nextDurationOptions[0] ?? 5)) }) .catch(() => { setImageOptions([{ id: "auto", label: "自动", model: "gpt-image-2", available: true }]) setVideoOptions([{ id: "seedance", label: "Seedance", model: "seedance", available: true }]) + setImageSizeOptions(DEFAULT_IMAGE_SIZE_OPTIONS) + setVideoSizeOptions(DEFAULT_VIDEO_SIZE_OPTIONS) + setVideoDurationOptions([5, 8, 10, 12, 15]) }) }, []) @@ -246,6 +277,7 @@ export default function Home() { prompt: promptWithGuardrails(), mode: "text", model: imageModel, + size: imageSize, }) setJob(updated) toast.success("图片已生成") @@ -271,7 +303,7 @@ export default function Home() { count: 1, first_image: activeMode.needsFirstFrame ? { kind: "keyframe", frame_idx: 0 } : null, last_image: activeMode.needsLastFrame && lastFrame ? { kind: "keyframe", frame_idx: lastFrame.index } : null, - size: "720x1280", + size: videoSize, model: videoModel, }) setJob(updated) @@ -423,17 +455,48 @@ export default function Home() { {isVideoMode(mode) ? ( + <> + + + + ) : ( - ) : null} + )} {activeMode.needsFirstFrame ? "图片作为参考帧" : "只根据文字生成"}
diff --git a/web/lib/api.ts b/web/lib/api.ts index 84a19a5..db1b2aa 100644 --- a/web/lib/api.ts +++ b/web/lib/api.ts @@ -260,6 +260,16 @@ export interface RuntimeModelOption { model: string description?: string available?: boolean + duration_options?: number[] + max_duration_seconds?: number + size_options?: RuntimeSizeOption[] +} + +export interface RuntimeSizeOption { + id: string + label: string + value: string + description?: string } export interface RuntimeModels { @@ -280,6 +290,7 @@ export interface RuntimeModels { image?: string image_base_url?: string image_options?: RuntimeModelOption[] + image_size_options?: RuntimeSizeOption[] image_fallbacks?: string[] image_circuit?: { primary?: string @@ -303,6 +314,9 @@ export interface RuntimeModels { video?: string video_aliases?: Record video_options?: RuntimeModelOption[] + video_duration_options?: number[] + video_max_duration_seconds?: number + video_size_options?: RuntimeSizeOption[] video_provider?: string video_base_url?: string video_configured?: boolean @@ -1231,7 +1245,7 @@ export async function translateText(text: string, target: "en" | "zh" = "en"): P export async function generateImage( jobId: string, frameIdx: number, - body: { prompt: string; extra_prompt?: string; negative_prompt?: string; model?: string; mode?: "edit" | "text"; from_selected?: boolean }, + body: { prompt: string; extra_prompt?: string; negative_prompt?: string; model?: string; size?: string; mode?: "edit" | "text"; from_selected?: boolean }, ): Promise { const res = await fetch(`${API_BASE}/jobs/${jobId}/frames/${frameIdx}/generate`, { method: "POST",
api/main.pyFastAPI 单文件后端:登录会话、状态模型、任务恢复、下载、抽帧、Vision、清洗、元素、分镜、原音频转写/翻译、声音与背景音分析、后续口播改写/TTS、文件返回;同时承载全局 prompt_libraryasset_library 的磁盘索引、CRUD、删除保护和复制到 job API。轻量创作入口 POST /creative/jobs/image 把上传图片或空白底图写成一个只有 0 号关键帧的 Job,让首页直接复用生图/生视频接口;/health 新增 image_optionsvideo_options/frames/{idx}/generatemodel 字段现在用于图片模型偏好,/storyboard/video 继续使用 model 字段选择视频别名。旧 AgentRun 一键出片状态机、TK 复刻接口和 POST /creative/copy 继续保留。
api/main.pyFastAPI 单文件后端:登录会话、状态模型、任务恢复、下载、抽帧、Vision、清洗、元素、分镜、原音频转写/翻译、声音与背景音分析、后续口播改写/TTS、文件返回;同时承载全局 prompt_libraryasset_library 的磁盘索引、CRUD、删除保护和复制到 job API。轻量创作入口 POST /creative/jobs/image 把上传图片或空白底图写成一个只有 0 号关键帧的 Job,让首页直接复用生图/生视频接口;/health 返回 image_optionsimage_size_optionsvideo_optionsvideo_size_optionsvideo_duration_optionsvideo_max_duration_seconds/frames/{idx}/generatemodel 字段用于图片模型偏好,size 字段用于图片输出尺寸;/storyboard/video 继续使用 model 字段选择视频别名,并先校验画幅与时长能力边界。旧 AgentRun 一键出片状态机、TK 复刻接口和 POST /creative/copy 继续保留。
api/product_library/skg-products内置 SKG 白底产品图库:manifest.json 记录从桌面产品图筛出的 gallery 白底图和桌面 4 张产品角度图,images/ 存 45 张参考图。
api/character_library/skg-characters内置相似主体形象库:从桌面 5 套策划形象导入,manifest.json 记录运动阳光男、都市型男、优雅白领女、运动辣妹、绅士大叔,每套含 7 张透明骨架参考图和一段 prompt_brief。相似主体生成时优先使用文字 brief 作为创意方向,避免把内置图作为强参考图复制。
asset_library/全局素材库目录,和 jobs/ 平级,不写入任何 job state。四类目录为 subjectsproductsscenesvideos;每个素材自带 manifest.json 和图片/视频文件,index.json 只是启动扫描重建出来的缓存。库素材选用到 job 时必须复制文件到 jobs/<jobId>/assetsstoryboard-videos,禁止直接保存 library 引用。
网页登录 / 飞书免登录GET /auth/configGET /auth/feishu/startGET /auth/feishu/callbackPOST /auth/loginGET /auth/checkGET /auth/mePOST /auth/logoutweb/app/login/page.tsx、Nginx auth_request登录页先读 /api/auth/config 判断是否显示飞书按钮;飞书 OAuth 成功后后端用 open_id / union_id / email 生成多用户会话并设置 HttpOnly Cookie。账号密码登录保留为备用方式。生产 Nginx 对工作台和 /api//auth/check 做统一校验,未登录页面跳 /login/,API 返回 JSON 401。
运行配置 / 模型标注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_circuit、主体 6 视图模型链路、Azure OpenAI TTS、视频别名和 Seedance 服务商。当前 REWRITE_MODELAUDIO_REWRITE_MODELVISION_MODEL 默认使用 gpt-4o;如果旧环境变量仍写 gemini-*,后端会归一化回 GPT_TEXT_MODEL / REWRITE_MODEL。语音只走 Azure OpenAI TTS,models.voice_tts_paths 会回传当前尝试的语音路径,方便区分路径错误和语音服务不可用。前端所有当前主路径里会调用模型的按钮旁显示模型名,点击弹出小窗口查看模型链路和输入输出逻辑;不返回 API Key 或敏感凭证。
运行配置 / 模型标注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 /agent-runs
GET /agent-runs
GET /agent-runs/{id}
GET /agent-runs/{id}/final.mp4
GET /agent-runs/{id}/contact.jpg
web/app/agent/page.tsx快速出片页的唯一主接口。前端提交 TikTok 链接和最多 6 张产品图;后端创建同 owner 的 JobAgentRun,后台执行下载、产品图归一化、透明骨架主体参考复制、12 段镜头计划、视频生成、失败镜头自动重跑一次、审片接触表和 ffmpeg 最终合成。列表、详情、最终 mp4 和接触表同样按 owner 隔离。