From 56a23847a19c181810d61b803cbb890202d830c8 Mon Sep 17 00:00:00 2001 From: kang Date: Tue, 26 May 2026 13:05:27 +0800 Subject: [PATCH] fix: align Gemini image sizes with official presets --- RULES.md | 2 +- api/main.py | 96 +++++++++++++++++-- docs/source-analysis.html | 14 +-- .../src/components/nodes/ImageConfigNode.vue | 18 +++- web/canvas-app/src/config/models.js | 37 ++++++- web/canvas-app/src/stores/models.js | 2 +- 6 files changed, 148 insertions(+), 21 deletions(-) diff --git a/RULES.md b/RULES.md index ec8d727..6febd7b 100644 --- a/RULES.md +++ b/RULES.md @@ -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-26 Postgres 持久化版):默认入口是多人通用的 SKG 营销内容生产平台,`https://marketing.skg.com` 登录后直接进入个人生成画布,`/canvas/` 只作为旧链接兼容跳转到根域名。终端可见品牌位只放 SKG logo,不在主界面展示“生图生视频”“SKG 生成画布”或长系统名。画布本体尽量恢复 `chatfire-AI/huobao-canvas` 的成熟交互,不再削成三模式单输入框:保留首页推荐词、画布底部推荐词、AI 润色、自动执行、公共工作流、我的工作流、首帧/尾帧/参考图节点、图片/视频/LLM 配置节点、模型配置和批量下载等上游能力;多角度分镜、故事板、图转视频、绘本等工作流按上游结构创建节点。API 接入是例外:生成调用继续走本项目后端 `/api` 和当前登录 Cookie,不要求员工在浏览器配置个人 API Key;AI 润色只扩写用户明确写出的主体、品牌、产品、平台、动作和镜头,用户没写 `SKG` 时绝不主动加入 SKG,也不能把未知主体润成人物或强行润成无人物;上传/生成的参考图如果本来就有人物,应在视频提示词里按 AI 生成的虚拟角色、非真人、非公众人物处理,继续允许 AI 人像素材参与图生视频;图片/视频模型选择只显示后端已经接通的媒体模型,不能让浏览器本地自定义或旧缓存模型进入生成下拉;生图配置必须显示真实像素尺寸和低/中/高画质,比例不能替代尺寸,生成结果也要显示实际输出像素。API 设置弹窗只保留模型/端点配置外观,不能出现上游注册链接或外部品牌。用户登录后仍只看到自己的任务、结果、详情页、画布项目和个人工作流模板,继续沿用后端 owner 隔离;画布项目和我的工作流以服务端 Postgres 为主持久化,浏览器 `localStorage` 只作为项目缓存和首次导入来源,图片/视频资产按登录用户写入后端 job。旧 TK 复刻工作台、Agent Cut 一键出片和营销图文方案保留为高级/详情页能力,不再作为默认首页入口或默认理解框架。 +- 当前产品方向(2026-05-26 Postgres 持久化版):默认入口是多人通用的 SKG 营销内容生产平台,`https://marketing.skg.com` 登录后直接进入个人生成画布,`/canvas/` 只作为旧链接兼容跳转到根域名。终端可见品牌位只放 SKG logo,不在主界面展示“生图生视频”“SKG 生成画布”或长系统名。画布本体尽量恢复 `chatfire-AI/huobao-canvas` 的成熟交互,不再削成三模式单输入框:保留首页推荐词、画布底部推荐词、AI 润色、自动执行、公共工作流、我的工作流、首帧/尾帧/参考图节点、图片/视频/LLM 配置节点、模型配置和批量下载等上游能力;多角度分镜、故事板、图转视频、绘本等工作流按上游结构创建节点。API 接入是例外:生成调用继续走本项目后端 `/api` 和当前登录 Cookie,不要求员工在浏览器配置个人 API Key;AI 润色只扩写用户明确写出的主体、品牌、产品、平台、动作和镜头,用户没写 `SKG` 时绝不主动加入 SKG,也不能把未知主体润成人物或强行润成无人物;上传/生成的参考图如果本来就有人物,应在视频提示词里按 AI 生成的虚拟角色、非真人、非公众人物处理,继续允许 AI 人像素材参与图生视频;图片/视频模型选择只显示后端已经接通的媒体模型,不能让浏览器本地自定义或旧缓存模型进入生成下拉;生图配置必须显示真实像素尺寸,比例不能替代尺寸,生成结果也要显示实际输出像素;GPT Image 2 / 自动模式可显示低/中/高画质和自定义像素输入,Gemini 图片只显示官方比例 + 1K/2K/4K 固定像素规格,不提供任意自定义尺寸。API 设置弹窗只保留模型/端点配置外观,不能出现上游注册链接或外部品牌。用户登录后仍只看到自己的任务、结果、详情页、画布项目和个人工作流模板,继续沿用后端 owner 隔离;画布项目和我的工作流以服务端 Postgres 为主持久化,浏览器 `localStorage` 只作为项目缓存和首次导入来源,图片/视频资产按登录用户写入后端 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 2c2e060..914c7c3 100644 --- a/api/main.py +++ b/api/main.py @@ -246,6 +246,36 @@ IMAGE_SIZE_CHOICES = [ "description": "适合高清横版视频封面和大屏展示", }, ] +GEMINI_IMAGE_SIZE_CHOICES = [ + { + "id": "auto", + "label": "自动", + "value": "auto", + "ratio": "auto", + "width": 0, + "height": 0, + "description": "由 Gemini 自行决定输出尺寸,生成后显示实际像素", + }, + {"id": "1024x1024", "label": "方图 1:1 · 1K · 1024×1024", "value": "1024x1024", "ratio": "1:1", "image_size": "1K", "width": 1024, "height": 1024}, + {"id": "848x1264", "label": "竖图 2:3 · 1K · 848×1264", "value": "848x1264", "ratio": "2:3", "image_size": "1K", "width": 848, "height": 1264}, + {"id": "1264x848", "label": "横图 3:2 · 1K · 1264×848", "value": "1264x848", "ratio": "3:2", "image_size": "1K", "width": 1264, "height": 848}, + {"id": "896x1200", "label": "竖图 3:4 · 1K · 896×1200", "value": "896x1200", "ratio": "3:4", "image_size": "1K", "width": 896, "height": 1200}, + {"id": "928x1152", "label": "竖图 4:5 · 1K · 928×1152", "value": "928x1152", "ratio": "4:5", "image_size": "1K", "width": 928, "height": 1152}, + {"id": "768x1376", "label": "竖屏 9:16 · 1K · 768×1376", "value": "768x1376", "ratio": "9:16", "image_size": "1K", "width": 768, "height": 1376}, + {"id": "1376x768", "label": "横屏 16:9 · 1K · 1376×768", "value": "1376x768", "ratio": "16:9", "image_size": "1K", "width": 1376, "height": 768}, + {"id": "2048x2048", "label": "方图 1:1 · 2K · 2048×2048", "value": "2048x2048", "ratio": "1:1", "image_size": "2K", "width": 2048, "height": 2048}, + {"id": "1696x2528", "label": "竖图 2:3 · 2K · 1696×2528", "value": "1696x2528", "ratio": "2:3", "image_size": "2K", "width": 1696, "height": 2528}, + {"id": "2528x1696", "label": "横图 3:2 · 2K · 2528×1696", "value": "2528x1696", "ratio": "3:2", "image_size": "2K", "width": 2528, "height": 1696}, + {"id": "1792x2400", "label": "竖图 3:4 · 2K · 1792×2400", "value": "1792x2400", "ratio": "3:4", "image_size": "2K", "width": 1792, "height": 2400}, + {"id": "1856x2304", "label": "竖图 4:5 · 2K · 1856×2304", "value": "1856x2304", "ratio": "4:5", "image_size": "2K", "width": 1856, "height": 2304}, + {"id": "1536x2752", "label": "竖屏 9:16 · 2K · 1536×2752", "value": "1536x2752", "ratio": "9:16", "image_size": "2K", "width": 1536, "height": 2752}, + {"id": "2752x1536", "label": "横屏 16:9 · 2K · 2752×1536", "value": "2752x1536", "ratio": "16:9", "image_size": "2K", "width": 2752, "height": 1536}, + {"id": "4096x4096", "label": "方图 1:1 · 4K · 4096×4096", "value": "4096x4096", "ratio": "1:1", "image_size": "4K", "width": 4096, "height": 4096}, + {"id": "3392x5056", "label": "竖图 2:3 · 4K · 3392×5056", "value": "3392x5056", "ratio": "2:3", "image_size": "4K", "width": 3392, "height": 5056}, + {"id": "5056x3392", "label": "横图 3:2 · 4K · 5056×3392", "value": "5056x3392", "ratio": "3:2", "image_size": "4K", "width": 5056, "height": 3392}, + {"id": "3072x5504", "label": "竖屏 9:16 · 4K · 3072×5504", "value": "3072x5504", "ratio": "9:16", "image_size": "4K", "width": 3072, "height": 5504}, + {"id": "5504x3072", "label": "横屏 16:9 · 4K · 5504×3072", "value": "5504x3072", "ratio": "16:9", "image_size": "4K", "width": 5504, "height": 3072}, +] IMAGE_QUALITY_CHOICES = [ { "id": "low", @@ -4684,6 +4714,9 @@ def image_model_options() -> list[dict]: "model": GPT_IMAGE_MODEL, "description": "优先 GPT Image 2,必要时按后端熔断和兜底策略切到备用图片模型", "available": bool(IMAGE_API_KEY), + "size_options": IMAGE_SIZE_CHOICES, + "quality_options": IMAGE_QUALITY_CHOICES, + "supports_custom_size": True, }, { "id": GPT_IMAGE_MODEL, @@ -4691,6 +4724,9 @@ def image_model_options() -> list[dict]: "model": GPT_IMAGE_MODEL, "description": "主生图模型,适合营销图和参考图重绘", "available": bool(IMAGE_API_KEY), + "size_options": IMAGE_SIZE_CHOICES, + "quality_options": IMAGE_QUALITY_CHOICES, + "supports_custom_size": True, }, ] if IMAGE_FALLBACK_ENABLED and IMAGE_FALLBACK_MODEL and IMAGE_FALLBACK_MODEL != GPT_IMAGE_MODEL: @@ -4698,8 +4734,11 @@ def image_model_options() -> list[dict]: "id": IMAGE_FALLBACK_MODEL, "label": "Gemini 图片", "model": IMAGE_FALLBACK_MODEL, - "description": "备用图片模型,适合主模型慢或失败时手动选择", + "description": "备用图片模型,使用 Gemini 官方比例和 1K/2K/4K 固定规格", "available": bool(IMAGE_API_KEY), + "size_options": GEMINI_IMAGE_SIZE_CHOICES, + "quality_options": [], + "supports_custom_size": False, }) return options @@ -4708,6 +4747,10 @@ def image_size_options() -> list[dict]: return IMAGE_SIZE_CHOICES +def gemini_image_size_options() -> list[dict]: + return GEMINI_IMAGE_SIZE_CHOICES + + def image_quality_options() -> list[dict]: return IMAGE_QUALITY_CHOICES @@ -4735,9 +4778,16 @@ def _validate_custom_image_size(width: int, height: int, raw: str) -> str: return f"{width}x{height}" -def _normalize_image_size(raw: str | None) -> str: +def _is_gemini_image_model(model: str | None) -> bool: + normalized = (model or "").strip().lower() + return bool(normalized and normalized.startswith("gemini")) or ( + bool(IMAGE_FALLBACK_MODEL) and normalized == IMAGE_FALLBACK_MODEL.lower() + ) + + +def _normalize_image_size(raw: str | None, model: str | None = GPT_IMAGE_MODEL, fallback_to_auto: bool = False) -> str: value = (raw or "auto").strip().lower() - aliases = { + gpt_aliases = { "9:16": "1088x1920", "9x16": "1088x1920", "16:9": "1280x720", @@ -4763,7 +4813,34 @@ def _normalize_image_size(raw: str | None) -> str: "横图": "1536x1024", "横屏": "1280x720", } - value = aliases.get(value, value) + gemini_aliases = { + "1:1": "1024x1024", + "1x1": "1024x1024", + "2:3": "848x1264", + "2x3": "848x1264", + "3:2": "1264x848", + "3x2": "1264x848", + "3:4": "896x1200", + "3x4": "896x1200", + "4:5": "928x1152", + "4x5": "928x1152", + "9:16": "768x1376", + "9x16": "768x1376", + "16:9": "1376x768", + "16x9": "1376x768", + "竖屏": "768x1376", + "横屏": "1376x768", + "方图": "1024x1024", + } + if _is_gemini_image_model(model): + value = gemini_aliases.get(value, value) + allowed = {str(item["value"]) for item in GEMINI_IMAGE_SIZE_CHOICES} + if value in allowed: + return value + if fallback_to_auto: + return "auto" + raise HTTPException(400, f"unsupported Gemini image size: {raw}") + value = gpt_aliases.get(value, value) allowed = {str(item["value"]) for item in IMAGE_SIZE_CHOICES} if value in allowed: return value @@ -4773,8 +4850,8 @@ def _normalize_image_size(raw: str | None) -> str: raise HTTPException(400, f"unsupported image size: {raw}") -def _image_size_payload(raw: str | None) -> dict: - size = _normalize_image_size(raw) +def _image_size_payload(raw: str | None, model: str | None = GPT_IMAGE_MODEL, fallback_to_auto: bool = False) -> dict: + size = _normalize_image_size(raw, model, fallback_to_auto=fallback_to_auto) return {} if size == "auto" else {"size": size} @@ -4808,7 +4885,7 @@ def _image_quality_payload(raw: str | None, model: str | None) -> dict: def _image_options_payload(size: str | None, quality: str | None, model: str | None) -> dict: - return {**_image_size_payload(size), **_image_quality_payload(quality, model)} + return {**_image_size_payload(size, model, fallback_to_auto=True), **_image_quality_payload(quality, model)} def video_duration_options() -> list[int]: @@ -6304,6 +6381,7 @@ def health() -> dict: "image_request_timeout_seconds": IMAGE_REQUEST_TIMEOUT_SECONDS, "image_options": image_model_options(), "image_size_options": image_size_options(), + "gemini_image_size_options": gemini_image_size_options(), "image_quality_options": image_quality_options(), "ai_proxy_configured": bool(AI_HTTP_PROXY), "image_fallbacks": _image_fallback_models(), @@ -6831,7 +6909,9 @@ 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) + requested_model = _normalize_image_model_preference(req.model) + strict_size_model = IMAGE_FALLBACK_MODEL if requested_model == IMAGE_FALLBACK_MODEL else GPT_IMAGE_MODEL + image_size = _normalize_image_size(req.size, strict_size_model) image_quality = _normalize_image_quality(req.quality) if not IMAGE_API_KEY: raise HTTPException(503, "IMAGE_API_KEY 或 LLM_API_KEY 未配置") diff --git a/docs/source-analysis.html b/docs/source-analysis.html index 0a4269a..c406d96 100644 --- a/docs/source-analysis.html +++ b/docs/source-analysis.html @@ -584,9 +584,9 @@

2026-05-26 公司沉淀版:画布项目从浏览器本地存储升级为服务端 Postgres 持久化;localStorage 只作为离线缓存和首次导入来源。后端同时建立用户、任务、资源索引和审计表,保留原有 state.json 文件作为任务详情真源,避免一次迁移动到大文件资产结构。

2026-05-26 AI 润色中性化:画布 AI 润色 不再复用 SKG 广告文案接口 /creative/copy。后端新增 POST /prompt/polish,前端 useChat、根画布输入框、文本节点和自动执行意图分析改走中性提示词/通用生成接口:只优化用户已经给出的主体、风格、镜头和细节,不主动添加 SKG、按摩产品、TikTok 广告话术或用户没有提到的品牌。当前润色链路会先清理上一次润色遗留的模板尾巴,再判断人物/无人/物体/场景/动物/未知主体;原文明确有人时才声明虚构 AI 角色,原文明确无人时才保留无人物约束,原文没写人时不主动造人但也不追加“必须无人物”的模板尾巴;当输入或参考图已经有人物时,按 AI 生成的虚拟角色继续描述,而不是把人物参考图判定为不可用。

2026-05-26 我的工作流云端版:工作流面板从只有公共模板扩展为“公共工作流 / 我的工作流”两类。当前画布可以保存成当前登录用户自己的云端工作流模板,后续在同一账号的其他电脑或浏览器打开后可插回画布;保存时只沉淀节点结构、连线、配置和提示词,主动清掉已生成图片、视频、任务进度、错误和运行态字段,避免把一次性生成结果误当模板复用。

-

2026-05-26 生图规格可视化版:画布生图配置把“比例”和“真实像素”拆开显示,尺寸下拉改为 竖屏 9:16 · 1088×1920 这类标签,并保留自定义 宽x高 输入;画质从旧的单一“标准”改为 低 / 中 / 高。后端按 GPT Image 2 的像素约束校验自定义尺寸,并把请求尺寸、画质和实际输出像素写回生成图片节点。

+

2026-05-26 生图规格可视化版:画布生图配置把“比例”和“真实像素”拆开显示。GPT Image 2 / 自动模式的尺寸下拉显示 竖屏 9:16 · 1088×1920 这类标签,并保留自定义 宽x高 输入;Gemini 图片按官方 aspectRatio + imageSize 能力只显示固定的 1K / 2K / 4K 像素规格,不提供任意自定义尺寸。画质从旧的单一“标准”改为 低 / 中 / 高,但 Gemini 的清晰度由尺寸规格里的 1K / 2K / 4K 表示。后端按模型分别校验尺寸,并把请求尺寸、画质和实际输出像素写回生成图片节点。

-

当前默认业务管线是“个人隔离任务 → 根域名进入个人画布 → 画布项目同步到服务端 Postgres → 用提示词、推荐词、AI 润色、公共工作流或我的工作流创建节点 → 画布自动执行或手动连接图片/视频/文本节点 → 生成结果沉淀在当前个人画布 → 可把当前节点结构保存为我的工作流 → 需要时进入详情页继续编辑”。画布不再被削成三模式入口;首帧、尾帧、参考图、图生视频、多角度分镜、故事板和绘本等上游概念按节点能力保留。底层生成仍由 web/canvas-app/src/hooks/useApi.js 适配到本项目 /creative/jobs/image/jobs/{id}/frames/{idx}/generate/jobs/{id}/frames/{idx}/storyboard/video,AI 润色和通用 LLM 文本生成走 /prompt/polish 并保持中性专业:不主动套入 SKG,不主动补产品、平台、广告语境或人物,只扩写用户明确写出的主体、动作、场景、镜头、光线和质量细节;视频提交若带参考图,会在最终提示词中条件声明“参考图里若有人物,应按 AI 生成的虚拟角色处理”,避免把 AI 人像素材误当成真实肖像。生成资产按当前登录用户写入个人 job。图片配置显示 低 / 中 / 高 画质和带真实像素的尺寸标签,可选 1024x15361088x19201440x25602048x2048 等预设,也可输入符合约束的自定义 宽x高;生成结果节点会显示实际输出像素。视频画幅只显示 720x12801280x7201024x1024960x1280;视频时长只显示 5/8/10/12/15 秒。多人互不影响依赖后端 owner_id、画布项目 owner、我的工作流 owner 和飞书 / 备用登录会话隔离。旧 React 单对话框首页、信息流复刻链路仍保留在源码里作为回滚/高级能力,但不作为生产默认入口。

+

当前默认业务管线是“个人隔离任务 → 根域名进入个人画布 → 画布项目同步到服务端 Postgres → 用提示词、推荐词、AI 润色、公共工作流或我的工作流创建节点 → 画布自动执行或手动连接图片/视频/文本节点 → 生成结果沉淀在当前个人画布 → 可把当前节点结构保存为我的工作流 → 需要时进入详情页继续编辑”。画布不再被削成三模式入口;首帧、尾帧、参考图、图生视频、多角度分镜、故事板和绘本等上游概念按节点能力保留。底层生成仍由 web/canvas-app/src/hooks/useApi.js 适配到本项目 /creative/jobs/image/jobs/{id}/frames/{idx}/generate/jobs/{id}/frames/{idx}/storyboard/video,AI 润色和通用 LLM 文本生成走 /prompt/polish 并保持中性专业:不主动套入 SKG,不主动补产品、平台、广告语境或人物,只扩写用户明确写出的主体、动作、场景、镜头、光线和质量细节;视频提交若带参考图,会在最终提示词中条件声明“参考图里若有人物,应按 AI 生成的虚拟角色处理”,避免把 AI 人像素材误当成真实肖像。生成资产按当前登录用户写入个人 job。图片配置显示真实像素尺寸:GPT Image 2 / 自动模式有 低 / 中 / 高 画质和自定义 宽x高;Gemini 图片只显示官方 1K / 2K / 4K 固定规格,例如 9:16 · 1K · 768×13769:16 · 2K · 1536×2752,不显示自定义尺寸输入。生成结果节点会显示实际输出像素。视频画幅只显示 720x12801280x7201024x1024960x1280;视频时长只显示 5/8/10/12/15 秒。多人互不影响依赖后端 owner_id、画布项目 owner、我的工作流 owner 和飞书 / 备用登录会话隔离。旧 React 单对话框首页、信息流复刻链路仍保留在源码里作为回滚/高级能力,但不作为生产默认入口。

01

个人任务

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

02

进入画布

用户直接在根域名个人画布里操作;项目列表优先读取服务端 /canvas-projects,本地旧项目会首次导入。

@@ -615,7 +615,7 @@ web/canvas-app/src/stores/workflows.js我的工作流 store:调用 GET/POST/DELETE /canvas-workflows 读取、保存和删除当前登录用户自己的云端工作流模板。保存前会清理节点里的 base64、生成 URL、任务进度、错误、视频结果和 LLM 输出等运行态字段,只保留可复用的节点结构、连线、配置和提示词。 web/canvas-app/src/views/Canvas.vue画布主交互:恢复上游底部 prompt composer、AI 润色自动执行、推荐词、节点菜单、工作流面板、API/模型设置入口和批量下载入口。自动执行会调用 useWorkflowOrchestrator 分析提示词,创建文生图、图转视频、故事板、多角度分镜或绘本节点组;手动模式只创建文本节点,用户自行连接节点。工作流面板支持公共模板和我的工作流:公共模板走本地 createNodes(),我的工作流从云端 workflow_data 插回当前画布,并重新生成节点 ID、按视口中心重排、按映射重连边。底部推荐词来自共享短词池,4 个一组单行展示,刷新按钮在 30 组内轮换,不改变输入面板高度。 web/canvas-app/src/config/suggestions.js首页和画布共用的推荐词配置:维护 QUICK_SUGGESTION_GROUPS,当前为 30 组 / 120 个短词,每组 4 个,控制刷新按钮的轮换范围;词条保持短小,避免推荐栏换行或顶起 composer。 - web/canvas-app/src/config/models.js画布媒体模型和规格的前端白名单:图片只内置 autogpt-image-2gemini-3-pro-image-preview,尺寸只内置 auto1024x15361024x10241536x1024;视频只内置 seedance / Seedance 2.0 Fast,画幅和时长对齐后端 /health 能力边界。useModelConfig.js 和 Pinia 模型 store 会忽略浏览器本地自定义图片/视频模型,防止旧缓存把不可用模型带回生成下拉。 + web/canvas-app/src/config/models.js画布媒体模型和规格的前端白名单:图片只内置 autogpt-image-2gemini-3-pro-image-preview。GPT Image 2 / 自动模式使用项目像素预设并允许自定义 宽x高;Gemini 使用独立 GEMINI_SIZE_OPTIONS,只列官方 1K / 2K / 4K 固定像素规格,supportsCustomSize=false。视频只内置 seedance / Seedance 2.0 Fast,画幅和时长对齐后端 /health 能力边界。useModelConfig.js 和 Pinia 模型 store 会忽略浏览器本地自定义图片/视频模型,防止旧缓存把不可用模型带回生成下拉。 web/canvas-app/src/hooks/useApi.js画布到本项目后端的适配层:不再读取浏览器 API Key,而是使用当前登录会话 Cookie 调用 /api。文生图 / 图生图先创建轻量 creative job,再调用 /frames/0/generate;文生视频 / 图生视频调用 /storyboard/video 并轮询 /jobs/{id},完成后把图片或 mp4 URL 写回画布节点。useChat 已从 SKG 广告文案接口切到 /prompt/polish:AI 润色显式使用 image/video prompt 模式,LLM 节点使用通用 chat 模式,避免自动注入用户没有提到的 SKG、产品、平台或营销语境;后端会清理旧润色模板尾巴、判断人物/无人/物体/场景意图,并在输出后检查“有人却禁止人物、无人却新增人物、未写 SKG 却出现 SKG”等冲突。图生视频实际提交到后端后,后端会对参考图追加 AI 虚拟角色条件说明,不要求前端判断图片里是否有人脸。 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 读取别人的任务。 @@ -647,7 +647,7 @@

后端核心

- + @@ -1113,7 +1113,7 @@ ProductRefStateItem { - + @@ -1286,8 +1286,8 @@ ProductRefStateItem {

问题:画布生图配置只显示“标准画质”和少量比例化尺寸,员工无法在生成前判断实际会出多大的图;生成后图片节点也只显示模型名,不显示请求尺寸或实际像素。

-

改动:api/main.py 扩展 IMAGE_SIZE_CHOICES,新增带 widthheightratio 的像素预设和自定义 宽x高 校验;GenerateReq 新增 quality,支持 low / medium / high,并把 GeneratedImage.sizequalitywidthheight 写回。ImageConfigNode.vue 的尺寸下拉改为“比例 · 像素”标签,增加自定义尺寸输入,画质下拉改为低/中/高;ImageNode.vue 显示模型、画质和实际输出像素。

-

影响:生图人员能在提交前看到真实像素规格,必要时输入自定义尺寸;画质切换不会再重置尺寸。Gemini 仍通过当前 SKG 兼容网关提交,界面会显示请求规格,最终以返回图片的实际像素为准。

+

改动:api/main.py 扩展 IMAGE_SIZE_CHOICES,新增带 widthheightratio 的 GPT 像素预设和自定义 宽x高 校验;同时新增 GEMINI_IMAGE_SIZE_CHOICES,按 Gemini 官方比例和 1K / 2K / 4K 固定规格列出可选尺寸,后端会拒绝 Gemini 的任意自定义尺寸。GenerateReq 新增 quality,支持 GPT 的 low / medium / high,并把 GeneratedImage.sizequalitywidthheight 写回。ImageConfigNode.vue 的尺寸下拉改为“比例 · 像素”标签;GPT 显示自定义尺寸输入,Gemini 不显示自定义尺寸输入;ImageNode.vue 显示模型、画质和实际输出像素。

+

影响:生图人员能在提交前看到真实像素规格;GPT 可按自定义尺寸提交,Gemini 只能选官方固定像素规格。画质切换不会再重置尺寸。Gemini 仍通过当前 SKG 兼容网关提交,最终仍以返回图片的实际像素为准。

diff --git a/web/canvas-app/src/components/nodes/ImageConfigNode.vue b/web/canvas-app/src/components/nodes/ImageConfigNode.vue index 36a6702..602212e 100644 --- a/web/canvas-app/src/components/nodes/ImageConfigNode.vue +++ b/web/canvas-app/src/components/nodes/ImageConfigNode.vue @@ -76,7 +76,7 @@ -
+
{ return config?.sizes && config.sizes.length > 0 }) +const supportsCustomSize = computed(() => { + const config = getModelConfig(localModel.value) + return config?.supportsCustomSize !== false +}) + // Display size with label | 显示尺寸(带标签) const displaySize = computed(() => { const option = sizeOptions.value.find(o => o.key === localSize.value) @@ -345,6 +350,16 @@ onMounted(() => { if (props.data?.quality !== localQuality.value) { updateNode(props.id, { quality: localQuality.value }) } + const mountedSizeOptions = getModelSizeOptions(localModel.value, localQuality.value) + if (mountedSizeOptions.length > 0 && !mountedSizeOptions.some(o => o.key === localSize.value)) { + const defaultSize = currentModelConfig.value?.defaultParams?.size + || mountedSizeOptions.find(o => o.key === DEFAULT_IMAGE_SIZE)?.key + || mountedSizeOptions[0].key + localSize.value = defaultSize + customSizeInput.value = defaultSize === 'auto' ? '' : defaultSize + customSizeError.value = '' + updateNode(props.id, { size: defaultSize }) + } }) // 解析 textNode 内容中的 @ 引用,转换为简短引用(如 图 1)并收集图片 @@ -577,6 +592,7 @@ const updateSize = () => { } const applyCustomSize = () => { + if (!supportsCustomSize.value) return const result = validateImageSize(customSizeInput.value) if (!result.ok) { customSizeError.value = result.message diff --git a/web/canvas-app/src/config/models.js b/web/canvas-app/src/config/models.js index 56dd326..d0bb814 100644 --- a/web/canvas-app/src/config/models.js +++ b/web/canvas-app/src/config/models.js @@ -20,6 +20,31 @@ export const SEEDREAM_SIZE_OPTIONS = [ { label: '横屏 16:9 · 2048×1152', key: '2048x1152', ratio: '16:9', width: 2048, height: 1152 } ] +// Gemini 3 Pro Image official aspect ratio + imageSize presets. +// Gemini does not support arbitrary custom pixel dimensions. +export const GEMINI_SIZE_OPTIONS = [ + { label: '自动 · 生成后显示实际像素', key: 'auto', ratio: 'auto' }, + { label: '方图 1:1 · 1K · 1024×1024', key: '1024x1024', ratio: '1:1', imageSize: '1K', width: 1024, height: 1024 }, + { label: '竖图 2:3 · 1K · 848×1264', key: '848x1264', ratio: '2:3', imageSize: '1K', width: 848, height: 1264 }, + { label: '横图 3:2 · 1K · 1264×848', key: '1264x848', ratio: '3:2', imageSize: '1K', width: 1264, height: 848 }, + { label: '竖图 3:4 · 1K · 896×1200', key: '896x1200', ratio: '3:4', imageSize: '1K', width: 896, height: 1200 }, + { label: '竖图 4:5 · 1K · 928×1152', key: '928x1152', ratio: '4:5', imageSize: '1K', width: 928, height: 1152 }, + { label: '竖屏 9:16 · 1K · 768×1376', key: '768x1376', ratio: '9:16', imageSize: '1K', width: 768, height: 1376 }, + { label: '横屏 16:9 · 1K · 1376×768', key: '1376x768', ratio: '16:9', imageSize: '1K', width: 1376, height: 768 }, + { label: '方图 1:1 · 2K · 2048×2048', key: '2048x2048', ratio: '1:1', imageSize: '2K', width: 2048, height: 2048 }, + { label: '竖图 2:3 · 2K · 1696×2528', key: '1696x2528', ratio: '2:3', imageSize: '2K', width: 1696, height: 2528 }, + { label: '横图 3:2 · 2K · 2528×1696', key: '2528x1696', ratio: '3:2', imageSize: '2K', width: 2528, height: 1696 }, + { label: '竖图 3:4 · 2K · 1792×2400', key: '1792x2400', ratio: '3:4', imageSize: '2K', width: 1792, height: 2400 }, + { label: '竖图 4:5 · 2K · 1856×2304', key: '1856x2304', ratio: '4:5', imageSize: '2K', width: 1856, height: 2304 }, + { label: '竖屏 9:16 · 2K · 1536×2752', key: '1536x2752', ratio: '9:16', imageSize: '2K', width: 1536, height: 2752 }, + { label: '横屏 16:9 · 2K · 2752×1536', key: '2752x1536', ratio: '16:9', imageSize: '2K', width: 2752, height: 1536 }, + { label: '方图 1:1 · 4K · 4096×4096', key: '4096x4096', ratio: '1:1', imageSize: '4K', width: 4096, height: 4096 }, + { label: '竖图 2:3 · 4K · 3392×5056', key: '3392x5056', ratio: '2:3', imageSize: '4K', width: 3392, height: 5056 }, + { label: '横图 3:2 · 4K · 5056×3392', key: '5056x3392', ratio: '3:2', imageSize: '4K', width: 5056, height: 3392 }, + { label: '竖屏 9:16 · 4K · 3072×5504', key: '3072x5504', ratio: '9:16', imageSize: '4K', width: 3072, height: 5504 }, + { label: '横屏 16:9 · 4K · 5504×3072', key: '5504x3072', ratio: '16:9', imageSize: '4K', width: 5504, height: 3072 } +] + // Kept for compatibility with upstream model helpers. export const SEEDREAM_4K_SIZE_OPTIONS = SEEDREAM_SIZE_OPTIONS @@ -46,7 +71,9 @@ export const IMAGE_MODELS = [ key: 'auto', provider: ['chatfire'], sizes: SEEDREAM_SIZE_OPTIONS.map(s => s.key), + sizeOptions: SEEDREAM_SIZE_OPTIONS, qualities: SEEDREAM_QUALITY_OPTIONS, + supportsCustomSize: true, defaultParams: { size: '1024x1536', quality: 'high', @@ -58,7 +85,9 @@ export const IMAGE_MODELS = [ key: 'gpt-image-2', provider: ['chatfire'], sizes: SEEDREAM_SIZE_OPTIONS.map(s => s.key), + sizeOptions: SEEDREAM_SIZE_OPTIONS, qualities: SEEDREAM_QUALITY_OPTIONS, + supportsCustomSize: true, defaultParams: { size: '1024x1536', quality: 'high', @@ -69,10 +98,12 @@ export const IMAGE_MODELS = [ label: 'Gemini 图片', key: 'gemini-3-pro-image-preview', provider: ['chatfire'], - sizes: SEEDREAM_SIZE_OPTIONS.map(s => s.key), - qualities: SEEDREAM_QUALITY_OPTIONS, + sizes: GEMINI_SIZE_OPTIONS.map(s => s.key), + sizeOptions: GEMINI_SIZE_OPTIONS, + qualities: [], + supportsCustomSize: false, defaultParams: { - size: '1024x1536', + size: '1024x1024', quality: 'high', style: 'vivid' } diff --git a/web/canvas-app/src/stores/models.js b/web/canvas-app/src/stores/models.js index 249cd15..42058cc 100644 --- a/web/canvas-app/src/stores/models.js +++ b/web/canvas-app/src/stores/models.js @@ -78,7 +78,7 @@ export const getModelSizeOptions = (modelKey, quality = 'high') => { if (!model?.sizes) return SEEDREAM_SIZE_OPTIONS // Convert sizes array to dropdown options | 转换 sizes 数组为下拉选项 - const sizeOptions = quality === '4k' ? SEEDREAM_4K_SIZE_OPTIONS : SEEDREAM_SIZE_OPTIONS + const sizeOptions = model.sizeOptions || (quality === '4k' ? SEEDREAM_4K_SIZE_OPTIONS : SEEDREAM_SIZE_OPTIONS) return model.sizes.map(size => { const option = sizeOptions.find(o => o.key === size) return option || { label: size, key: size }
api/main.pyFastAPI 单文件后端:登录会话、状态模型、任务恢复、下载、抽帧、Vision、清洗、元素、分镜、原音频转写/翻译、声音与背景音分析、后续口播改写/TTS、文件返回;同时承载全局 prompt_libraryasset_library 的磁盘索引、CRUD、删除保护和复制到 job API。启动时会初始化 Postgres schema、扫描现有 state.json / 资源库并写入索引;/canvas-projects 系列接口把画布项目按当前登录用户持久化,/canvas-workflows 系列接口把我的工作流按当前登录用户持久化为可复用模板。轻量创作入口 POST /creative/jobs/image 把上传图片或空白底图写成一个只有 0 号关键帧的 Job,让首页直接复用生图/生视频接口;该接口兼容无 body / JSON 空对象 / 正常 multipart 上传,避免无首帧文生图或文生视频时空 multipart 被 FastAPI 在业务前置解析阶段拒绝;POST /prompt/polish 用于中性 AI 润色和通用 LLM 文本生成,只保留用户明确给出的主体、品牌、产品、地点、风格和意图,不默认加入 SKG、按摩产品、平台或短视频广告话术。润色链路会先用 _strip_previous_polish_boilerplate 去掉旧模板尾巴,再用 _classify_prompt_intent 判断人物、无人、物体、场景、动物或未知主体,最后用 _repair_polished_prompt 修掉有人/无人矛盾、未写人却新增人物、未写 SKG 却出现 SKG 等冲突;_append_reference_image_person_guard 会在视频任务最终入队前给参考图请求追加条件提示,声明参考图里若有人物则按 AI 生成的虚拟角色处理;/health 返回 databaseimage_optionsimage_size_optionsimage_quality_optionsvideo_optionsvideo_size_optionsvideo_duration_optionsvideo_max_duration_seconds/frames/{idx}/generatemodel 字段用于图片模型偏好,size 字段用于图片输出尺寸或自定义像素,quality 字段用于 low / medium / high 画质;生成完成会把 GeneratedImage.sizequalitywidthheight 写回。/storyboard/video 继续使用 model 字段选择视频别名,并先校验画幅与时长能力边界,然后把 GeneratedVideo 写成 queued 占位并进入进程内视频队列。队列默认 VIDEO_QUEUE_MAX_CONCURRENT=2VIDEO_QUEUE_MAX_CONCURRENT_PER_USER=1,同一用户连续提交不会占满全局并发;排队任务会回写 queue_positionqueue_sizequeue_message。旧 AgentRun 一键出片状态机、TK 复刻接口和 POST /creative/copy 作为明确的 SKG 营销文案接口继续保留。
api/main.pyFastAPI 单文件后端:登录会话、状态模型、任务恢复、下载、抽帧、Vision、清洗、元素、分镜、原音频转写/翻译、声音与背景音分析、后续口播改写/TTS、文件返回;同时承载全局 prompt_libraryasset_library 的磁盘索引、CRUD、删除保护和复制到 job API。启动时会初始化 Postgres schema、扫描现有 state.json / 资源库并写入索引;/canvas-projects 系列接口把画布项目按当前登录用户持久化,/canvas-workflows 系列接口把我的工作流按当前登录用户持久化为可复用模板。轻量创作入口 POST /creative/jobs/image 把上传图片或空白底图写成一个只有 0 号关键帧的 Job,让首页直接复用生图/生视频接口;该接口兼容无 body / JSON 空对象 / 正常 multipart 上传,避免无首帧文生图或文生视频时空 multipart 被 FastAPI 在业务前置解析阶段拒绝;POST /prompt/polish 用于中性 AI 润色和通用 LLM 文本生成,只保留用户明确给出的主体、品牌、产品、地点、风格和意图,不默认加入 SKG、按摩产品、平台或短视频广告话术。润色链路会先用 _strip_previous_polish_boilerplate 去掉旧模板尾巴,再用 _classify_prompt_intent 判断人物、无人、物体、场景、动物或未知主体,最后用 _repair_polished_prompt 修掉有人/无人矛盾、未写人却新增人物、未写 SKG 却出现 SKG 等冲突;_append_reference_image_person_guard 会在视频任务最终入队前给参考图请求追加条件提示,声明参考图里若有人物则按 AI 生成的虚拟角色处理;/health 返回 databaseimage_optionsimage_size_optionsgemini_image_size_optionsimage_quality_optionsvideo_optionsvideo_size_optionsvideo_duration_optionsvideo_max_duration_seconds/frames/{idx}/generatemodel 字段用于图片模型偏好,size 字段按实际模型校验:GPT Image 2 可用项目像素预设和符合约束的自定义尺寸,Gemini 只接受 GEMINI_IMAGE_SIZE_CHOICES 里的官方固定像素;quality 字段用于 GPT 的 low / medium / high 画质。生成完成会把 GeneratedImage.sizequalitywidthheight 写回。/storyboard/video 继续使用 model 字段选择视频别名,并先校验画幅与时长能力边界,然后把 GeneratedVideo 写成 queued 占位并进入进程内视频队列。队列默认 VIDEO_QUEUE_MAX_CONCURRENT=2VIDEO_QUEUE_MAX_CONCURRENT_PER_USER=1,同一用户连续提交不会占满全局并发;排队任务会回写 queue_positionqueue_sizequeue_message。旧 AgentRun 一键出片状态机、TK 复刻接口和 POST /creative/copy 作为明确的 SKG 营销文案接口继续保留。
api/db.pyPostgres 适配层:在 DATABASE_URL 存在且 psycopg 可用时启用;负责建表、健康检查、用户 upsert、审计日志、画布项目 CRUD、我的工作流 CRUD,以及把 JobAgentRun、提示词库和素材库写入索引表。数据库不可用时本地开发会降级为 disabled,生产 verify-prod-docker.sh 会要求 database.connected=true
video_model_options()视频模型能力出口:如果 seedanceklingveo3veo 等业务别名实际都映射到同一个真实模型,会按真实模型去重,只给前端返回一个可用选项;当前生产真实模型为 doubao-seedance-2-0-fast-260128,前端显示为 Seedance 2.0 Fast。后续只有在服务器真的配置了不同可用视频模型时,才应把新的模型重新暴露给画布。
api/product_library/skg-products内置 SKG 白底产品图库:manifest.json 记录从桌面产品图筛出的 gallery 白底图和桌面 4 张产品角度图,images/ 存 45 张参考图。
网页登录 / 飞书免登录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 判断是否显示飞书按钮和密码表单;飞书客户端内且 feishu_enabled=true 时前端自动跳转授权入口,普通浏览器保留手动飞书按钮。飞书 OAuth 成功后后端用 open_id / union_id / email 生成多用户会话并设置 HttpOnly Cookie。当前生产 PASSWORD_AUTH_ENABLED=false,因此 password_enabled=false,账号密码表单不展示,POST /auth/login 返回未配置。生产 Nginx 对工作台和 /api//auth/check 做统一校验,未登录页面跳 /login/?next=$request_uri,API 返回 JSON 401。
运行配置 / 模型标注GET /healthgetRuntimeHealthModelTrace返回 database 健康状态和 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_quality_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 /healthgetRuntimeHealthModelTrace返回 database 健康状态和 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、GPT 图片尺寸 image_size_options、Gemini 图片尺寸 gemini_image_size_options、图片画质 image_quality_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
画布项目GET /canvas-projects
POST /canvas-projects
PUT /canvas-projects/{id}
GET /canvas-projects/{id}
DELETE /canvas-projects/{id}
POST /canvas-projects/import
web/canvas-app/src/stores/projects.js根域名画布项目的服务端持久化接口。列表和详情按当前登录用户过滤;写入时保存画布 JSON、缩略图、可见性、版本和更新时间;删除为软删除。首次上线后本地 localStorage 旧项目会通过 import 导入到当前用户,之后服务端 Postgres 是主存储。