revert: restore original image generation config
This commit is contained in:
4
RULES.md
4
RULES.md
@@ -12,7 +12,7 @@
|
|||||||
- 详见 `CLAUDE.md` 立项决策段 + `.memory/plan.md` 七步管线拆解
|
- 详见 `CLAUDE.md` 立项决策段 + `.memory/plan.md` 七步管线拆解
|
||||||
- 风格:`04-Dark-Gallery-Ambient`(路径:`~/Projects/research/20260305-网页风格库/04-Dark-Gallery-Ambient.md`)
|
- 风格:`04-Dark-Gallery-Ambient`(路径:`~/Projects/research/20260305-网页风格库/04-Dark-Gallery-Ambient.md`)
|
||||||
- 第一冲刺:步骤 1-4(下载 / 拆轨 / 关键帧 / ASR+翻译)
|
- 第一冲刺:步骤 1-4(下载 / 拆轨 / 关键帧 / ASR+翻译)
|
||||||
- 当前产品方向(2026-05-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 一键出片和营销图文方案保留为高级/详情页能力,不再作为默认首页入口或默认理解框架。
|
- 当前产品方向(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 人像素材参与图生视频;图片/视频模型选择只显示后端已经接通的媒体模型,不能让浏览器本地自定义或旧缓存模型进入生成下拉;生图配置恢复最初简单版,图片模型显示 `auto`、`gpt-image-2`、`gemini-3-pro-image-preview`,尺寸只显示 `auto`、`1024x1536`、`1024x1024`、`1536x1024`,画质只保留标准项。API 设置弹窗只保留模型/端点配置外观,不能出现上游注册链接或外部品牌。用户登录后仍只看到自己的任务、结果、详情页、画布项目和个人工作流模板,继续沿用后端 owner 隔离;画布项目和我的工作流以服务端 Postgres 为主持久化,浏览器 `localStorage` 只作为项目缓存和首次导入来源,图片/视频资产按登录用户写入后端 job。旧 TK 复刻工作台、Agent Cut 一键出片和营销图文方案保留为高级/详情页能力,不再作为默认首页入口或默认理解框架。
|
||||||
|
|
||||||
## 部署事实
|
## 部署事实
|
||||||
- 平台:VPS `76.13.31.179`(Ubuntu 24.04 / Docker Compose / Coolify Traefik)
|
- 平台:VPS `76.13.31.179`(Ubuntu 24.04 / Docker Compose / Coolify Traefik)
|
||||||
@@ -28,8 +28,6 @@
|
|||||||
- AI 润色中性化(2026-05-26):`509bd9b` 已通过 `./scripts/deploy-prod-safe.sh` 部署到 `/opt/skg-marketing-studio`,部署前备份为 `/opt/skg-marketing-studio-backups/skg-marketing-preserve-20260526020846.tgz`。本次把画布 `AI 润色`、LLM 节点和自动执行意图分析从 SKG 广告文案接口 `/creative/copy` 拆出,新增中性 `POST /prompt/polish`:只优化用户已经写明的主体、品牌、产品、地点、风格和镜头,不主动添加 SKG、按摩产品、TikTok/Reels 广告话术、标题或 hashtag;`/creative/copy` 继续保留给明确的 SKG 营销文案场景。脚本内验证通过(web/API/Postgres Up、`/` 302、`/login/` 200、未登录 `/api/health` 401、`api:health ok db connected`),并在生产 API 容器内确认普通雨夜街头摊位提示词经 `/prompt/polish` 兜底输出不包含 SKG、massage 或 TikTok。
|
- AI 润色中性化(2026-05-26):`509bd9b` 已通过 `./scripts/deploy-prod-safe.sh` 部署到 `/opt/skg-marketing-studio`,部署前备份为 `/opt/skg-marketing-studio-backups/skg-marketing-preserve-20260526020846.tgz`。本次把画布 `AI 润色`、LLM 节点和自动执行意图分析从 SKG 广告文案接口 `/creative/copy` 拆出,新增中性 `POST /prompt/polish`:只优化用户已经写明的主体、品牌、产品、地点、风格和镜头,不主动添加 SKG、按摩产品、TikTok/Reels 广告话术、标题或 hashtag;`/creative/copy` 继续保留给明确的 SKG 营销文案场景。脚本内验证通过(web/API/Postgres Up、`/` 302、`/login/` 200、未登录 `/api/health` 401、`api:health ok db connected`),并在生产 API 容器内确认普通雨夜街头摊位提示词经 `/prompt/polish` 兜底输出不包含 SKG、massage 或 TikTok。
|
||||||
- AI 润色人物安全词分流(2026-05-26):`daec523` 已通过 `./scripts/deploy-prod-safe.sh` 部署到 `/opt/skg-marketing-studio`,部署前备份为 `/opt/skg-marketing-studio-backups/skg-marketing-preserve-20260526022320.tgz`。本次在 `/prompt/polish` 增加人物意图检测:原提示词没有人物语义或明确写无人时,润色只补“保持 object-only / scene-only / product-only 构图,不新增 people、faces、bodies、hands、avatars、characters、crowds”;原提示词明确有人像、模特、角色、数字人或脸时,才补“fully fictional synthetic AI character / virtual avatar / not based on any real person”。脚本内验证通过(web/API/Postgres Up、`/` 302、`/login/` 200、未登录 `/api/health` 401、`api:health ok db connected`),并在生产 API 容器内确认雨夜章鱼烧摊位不会出现虚构角色安全词,年轻女生人像会出现虚构 AI 角色安全词。
|
- AI 润色人物安全词分流(2026-05-26):`daec523` 已通过 `./scripts/deploy-prod-safe.sh` 部署到 `/opt/skg-marketing-studio`,部署前备份为 `/opt/skg-marketing-studio-backups/skg-marketing-preserve-20260526022320.tgz`。本次在 `/prompt/polish` 增加人物意图检测:原提示词没有人物语义或明确写无人时,润色只补“保持 object-only / scene-only / product-only 构图,不新增 people、faces、bodies、hands、avatars、characters、crowds”;原提示词明确有人像、模特、角色、数字人或脸时,才补“fully fictional synthetic AI character / virtual avatar / not based on any real person”。脚本内验证通过(web/API/Postgres Up、`/` 302、`/login/` 200、未登录 `/api/health` 401、`api:health ok db connected`),并在生产 API 容器内确认雨夜章鱼烧摊位不会出现虚构角色安全词,年轻女生人像会出现虚构 AI 角色安全词。
|
||||||
- AI 润色意图校验和参考图人物提示(2026-05-26):`f5be97b` 已通过 `./scripts/deploy-prod-safe.sh` 部署到 `/opt/skg-marketing-studio`,部署前备份为 `/opt/skg-marketing-studio-backups/skg-marketing-preserve-20260526035016.tgz`。本次把 `/prompt/polish` 改为清理旧模板尾巴、分类人物/无人/物体/场景/动物/未知主体并做冲突修复:不主动加入 SKG、产品、平台、广告语境或人物,也不把未知主体强行润成无人物;同时 `/storyboard/video` 最终入队前会给参考图请求追加条件提示,说明参考图里若有人物、脸、身体、手、头像或角色,应按 AI 生成的虚拟角色、非真人、非公众人物处理,允许员工继续用 AI 人像素材做图生视频。部署脚本内首次验证在容器启动 3 秒时遇到根路径临时 500,随后复跑 `./scripts/verify-prod-docker.sh root@76.13.31.179` 通过(web/API/Postgres Up、`/` 302、`/login/` 200、未登录 `/api/health` 401、`api:health ok db connected`),并在生产 API 容器内确认参考图提示词追加和 `InputImageSensitiveContentDetected.PrivacyInformation` 中文错误解释已生效。
|
- AI 润色意图校验和参考图人物提示(2026-05-26):`f5be97b` 已通过 `./scripts/deploy-prod-safe.sh` 部署到 `/opt/skg-marketing-studio`,部署前备份为 `/opt/skg-marketing-studio-backups/skg-marketing-preserve-20260526035016.tgz`。本次把 `/prompt/polish` 改为清理旧模板尾巴、分类人物/无人/物体/场景/动物/未知主体并做冲突修复:不主动加入 SKG、产品、平台、广告语境或人物,也不把未知主体强行润成无人物;同时 `/storyboard/video` 最终入队前会给参考图请求追加条件提示,说明参考图里若有人物、脸、身体、手、头像或角色,应按 AI 生成的虚拟角色、非真人、非公众人物处理,允许员工继续用 AI 人像素材做图生视频。部署脚本内首次验证在容器启动 3 秒时遇到根路径临时 500,随后复跑 `./scripts/verify-prod-docker.sh root@76.13.31.179` 通过(web/API/Postgres Up、`/` 302、`/login/` 200、未登录 `/api/health` 401、`api:health ok db connected`),并在生产 API 容器内确认参考图提示词追加和 `InputImageSensitiveContentDetected.PrivacyInformation` 中文错误解释已生效。
|
||||||
- 生图规格可视化(2026-05-26):`5d047af` 已通过 `./scripts/deploy-prod-safe.sh` 部署到 `/opt/skg-marketing-studio`,部署前备份为 `/opt/skg-marketing-studio-backups/skg-marketing-preserve-20260526042230.tgz`。本次把画布生图配置改为显示真实像素尺寸和低/中/高画质:预设包含 `1024x1536`、`1536x2304`、`1088x1920`、`1440x2560`、`960x1280`、`1536x1920`、`1024x1024`、`2048x2048`、`1536x1024`、`2304x1536`、`1280x720`、`2048x1152` 和 `auto`,并支持符合后端约束的自定义 `宽x高`;生成结果会写回请求尺寸、画质和实际输出像素。脚本内验证通过(web/API/Postgres Up、`/` 302、`/login/` 200、未登录 `/api/health` 401、`api:health ok db connected`);生产 API 容器内确认 `image_size_options` 共 13 项、`image_quality_options=low,medium,high`,生产 web 静态 bundle 命中 `1088×1920`、`低 · 快速草稿` 和 `自定义 1088x1920`。
|
|
||||||
- Gemini 生图规格收口(2026-05-26):`56a2384` 已通过 `./scripts/deploy-prod-safe.sh` 部署到 `/opt/skg-marketing-studio`,部署前备份为 `/opt/skg-marketing-studio-backups/skg-marketing-preserve-20260526050539.tgz`。本次按 Google Gemini 官方 image generation 文档把 Gemini 图片模型收口为官方比例 + `1K/2K/4K` 固定像素规格,前端选择 Gemini 时不再显示任意自定义尺寸;GPT Image 2 / 自动模式继续保留低/中/高画质和符合后端约束的自定义像素输入。脚本内验证通过(web/API/Postgres Up、`/` 302、`/login/` 200、缺失 `_next` 资源 404、未登录 `/api/health` 401、`api:health ok db connected`);生产 API 容器内确认 `gemini_image_size_options` 共 20 项且 `supports_custom_size=false`,`768x1376` 和 `9:16` 均归一到 `768x1376`,`1088x1920` 被拒绝为 `unsupported Gemini image size: 1088x1920`;生产 web 静态 bundle 命中 `768×1376`、`1536×2752` 和 `supportsCustomSize`。
|
|
||||||
- 推荐词轮换(2026-05-26):`d01fdc5` 已通过 `./scripts/deploy-prod-safe.sh` 部署到 `/opt/skg-marketing-studio`,部署前备份为 `/opt/skg-marketing-studio-backups/skg-marketing-preserve-20260526023923.tgz`。本次把画布和首页推荐词从固定数组改为 4 个一组的短词池,刷新按钮绑定为切换下一组;推荐栏固定单行高度并截断过长 chip,避免把底部输入框顶高。脚本内验证通过(web/API/Postgres Up、`/` 302、`/login/` 200、未登录 `/api/health` 401、`api:health ok db connected`),并在生产 web 容器静态 bundle 中确认命中 `换一组推荐`、`魔法森林`、`无人物街景` 等新文案。
|
- 推荐词轮换(2026-05-26):`d01fdc5` 已通过 `./scripts/deploy-prod-safe.sh` 部署到 `/opt/skg-marketing-studio`,部署前备份为 `/opt/skg-marketing-studio-backups/skg-marketing-preserve-20260526023923.tgz`。本次把画布和首页推荐词从固定数组改为 4 个一组的短词池,刷新按钮绑定为切换下一组;推荐栏固定单行高度并截断过长 chip,避免把底部输入框顶高。脚本内验证通过(web/API/Postgres Up、`/` 302、`/login/` 200、未登录 `/api/health` 401、`api:health ok db connected`),并在生产 web 容器静态 bundle 中确认命中 `换一组推荐`、`魔法森林`、`无人物街景` 等新文案。
|
||||||
- 推荐词扩展(2026-05-26):`7f3a6cc` 已通过 `./scripts/deploy-prod-safe.sh` 部署到 `/opt/skg-marketing-studio`,部署前备份为 `/opt/skg-marketing-studio-backups/skg-marketing-preserve-20260526024847.tgz`。本次新增 `web/canvas-app/src/config/suggestions.js`,把首页和画布推荐词统一为 30 组 / 120 个短词共享池,每次仍显示 4 个并按组轮换,保持单行不顶起 composer。本地验证 `groups=30`、`items=120`、最长词 5 个字符;本地 `npm run build` 和生产 Docker 构建通过,`./scripts/verify-prod-docker.sh` 复验通过(web/API/Postgres Up、`/` 302、`/login/` 200、未登录 `/api/health` 401、`api:health ok db connected`),并在生产 web 容器静态 bundle 中确认命中 `银河帐篷`。
|
- 推荐词扩展(2026-05-26):`7f3a6cc` 已通过 `./scripts/deploy-prod-safe.sh` 部署到 `/opt/skg-marketing-studio`,部署前备份为 `/opt/skg-marketing-studio-backups/skg-marketing-preserve-20260526024847.tgz`。本次新增 `web/canvas-app/src/config/suggestions.js`,把首页和画布推荐词统一为 30 组 / 120 个短词共享池,每次仍显示 4 个并按组轮换,保持单行不顶起 composer。本地验证 `groups=30`、`items=120`、最长词 5 个字符;本地 `npm run build` 和生产 Docker 构建通过,`./scripts/verify-prod-docker.sh` 复验通过(web/API/Postgres Up、`/` 302、`/login/` 200、未登录 `/api/health` 401、`api:health ok db connected`),并在生产 web 容器静态 bundle 中确认命中 `银河帐篷`。
|
||||||
- 最近部署验证(2026-05-25):`cce9779` 已通过 `./scripts/deploy-prod-safe.sh` 部署到 `/opt/skg-marketing-studio`,恢复 `chatfire-AI/huobao-canvas` 上游画布能力但保留 SKG 后端 `/api` 接入。部署前脚本已备份生产私有环境、任务数据、资源库和 secrets 到 `/opt/skg-marketing-studio-backups/skg-marketing-preserve-20260525102857.tgz`;生产 Docker 重建后脚本内验证通过(web/API 容器 Up、`/` 302、`/login/` 200、缺失 `_next` 资源 404、未登录 `/api/health` 401、容器内 `api:health ok`、`api:ytdlp_cookie_args []`、未发现本地 API/dev URL 泄漏)。补验:外部访问 `https://marketing.skg.com/` 未登录返回 302 到 `/login/?next=/`,`https://marketing.skg.com/canvas/` 返回 308 到 `/`,`https://marketing.skg.com/p/test` 未登录返回 302 到 `/login/?next=/p/test`;容器内静态 bundle 命中 `AI 润色 / 自动执行 / 推荐: / 首帧 / 尾帧 / 多角度分镜 / 儿童绘本 / 工作流模板 / 批量下载素材`,未命中上游注册链接、火宝欢迎文案、GitHub 入口或 `/huobao-canvas`。
|
- 最近部署验证(2026-05-25):`cce9779` 已通过 `./scripts/deploy-prod-safe.sh` 部署到 `/opt/skg-marketing-studio`,恢复 `chatfire-AI/huobao-canvas` 上游画布能力但保留 SKG 后端 `/api` 接入。部署前脚本已备份生产私有环境、任务数据、资源库和 secrets 到 `/opt/skg-marketing-studio-backups/skg-marketing-preserve-20260525102857.tgz`;生产 Docker 重建后脚本内验证通过(web/API 容器 Up、`/` 302、`/login/` 200、缺失 `_next` 资源 404、未登录 `/api/health` 401、容器内 `api:health ok`、`api:ytdlp_cookie_args []`、未发现本地 API/dev URL 泄漏)。补验:外部访问 `https://marketing.skg.com/` 未登录返回 302 到 `/login/?next=/`,`https://marketing.skg.com/canvas/` 返回 308 到 `/`,`https://marketing.skg.com/p/test` 未登录返回 302 到 `/login/?next=/p/test`;容器内静态 bundle 命中 `AI 润色 / 自动执行 / 推荐: / 首帧 / 尾帧 / 多角度分镜 / 儿童绘本 / 工作流模板 / 批量下载素材`,未命中上游注册链接、火宝欢迎文案、GitHub 入口或 `/huobao-canvas`。
|
||||||
|
|||||||
326
api/main.py
326
api/main.py
@@ -132,169 +132,26 @@ IMAGE_SIZE_CHOICES = [
|
|||||||
"id": "auto",
|
"id": "auto",
|
||||||
"label": "自动",
|
"label": "自动",
|
||||||
"value": "auto",
|
"value": "auto",
|
||||||
"ratio": "auto",
|
"description": "由图片模型自行决定输出尺寸",
|
||||||
"width": 0,
|
|
||||||
"height": 0,
|
|
||||||
"description": "由图片模型自行决定输出尺寸,生成后显示实际像素",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "1024x1536",
|
"id": "1024x1536",
|
||||||
"label": "竖图 2:3 · 1024×1536",
|
"label": "竖图 2:3",
|
||||||
"value": "1024x1536",
|
"value": "1024x1536",
|
||||||
"ratio": "2:3",
|
|
||||||
"width": 1024,
|
|
||||||
"height": 1536,
|
|
||||||
"description": "适合信息流营销图、人物和产品竖版构图",
|
"description": "适合信息流营销图、人物和产品竖版构图",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "1536x2304",
|
|
||||||
"label": "竖图 2:3 · 1536×2304",
|
|
||||||
"value": "1536x2304",
|
|
||||||
"ratio": "2:3",
|
|
||||||
"width": 1536,
|
|
||||||
"height": 2304,
|
|
||||||
"description": "适合高精细竖版海报和后期裁切",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "1088x1920",
|
|
||||||
"label": "竖屏 9:16 · 1088×1920",
|
|
||||||
"value": "1088x1920",
|
|
||||||
"ratio": "9:16",
|
|
||||||
"width": 1088,
|
|
||||||
"height": 1920,
|
|
||||||
"description": "接近 1080p 竖屏,宽度按 16 像素倍数提交",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "1440x2560",
|
|
||||||
"label": "竖屏 9:16 · 1440×2560",
|
|
||||||
"value": "1440x2560",
|
|
||||||
"ratio": "9:16",
|
|
||||||
"width": 1440,
|
|
||||||
"height": 2560,
|
|
||||||
"description": "适合短视频封面、竖屏高清素材和二次裁切",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "960x1280",
|
|
||||||
"label": "竖图 3:4 · 960×1280",
|
|
||||||
"value": "960x1280",
|
|
||||||
"ratio": "3:4",
|
|
||||||
"width": 960,
|
|
||||||
"height": 1280,
|
|
||||||
"description": "适合偏人物或产品竖图,文件体积较轻",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "1536x1920",
|
|
||||||
"label": "竖图 4:5 · 1536×1920",
|
|
||||||
"value": "1536x1920",
|
|
||||||
"ratio": "4:5",
|
|
||||||
"width": 1536,
|
|
||||||
"height": 1920,
|
|
||||||
"description": "适合小红书、社媒封面和产品展示图",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "1024x1024",
|
"id": "1024x1024",
|
||||||
"label": "方图 1:1 · 1024×1024",
|
"label": "方图 1:1",
|
||||||
"value": "1024x1024",
|
"value": "1024x1024",
|
||||||
"ratio": "1:1",
|
|
||||||
"width": 1024,
|
|
||||||
"height": 1024,
|
|
||||||
"description": "适合头像、方形素材和电商图",
|
"description": "适合头像、方形素材和电商图",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "2048x2048",
|
|
||||||
"label": "方图 1:1 · 2048×2048",
|
|
||||||
"value": "2048x2048",
|
|
||||||
"ratio": "1:1",
|
|
||||||
"width": 2048,
|
|
||||||
"height": 2048,
|
|
||||||
"description": "适合高清方形素材和后期抠图",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "1536x1024",
|
"id": "1536x1024",
|
||||||
"label": "横图 3:2 · 1536×1024",
|
"label": "横图 3:2",
|
||||||
"value": "1536x1024",
|
"value": "1536x1024",
|
||||||
"ratio": "3:2",
|
|
||||||
"width": 1536,
|
|
||||||
"height": 1024,
|
|
||||||
"description": "适合横版封面和详情页配图",
|
"description": "适合横版封面和详情页配图",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "2304x1536",
|
|
||||||
"label": "横图 3:2 · 2304×1536",
|
|
||||||
"value": "2304x1536",
|
|
||||||
"ratio": "3:2",
|
|
||||||
"width": 2304,
|
|
||||||
"height": 1536,
|
|
||||||
"description": "适合高清横版主视觉和详情页大图",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "1280x720",
|
|
||||||
"label": "横屏 16:9 · 1280×720",
|
|
||||||
"value": "1280x720",
|
|
||||||
"ratio": "16:9",
|
|
||||||
"width": 1280,
|
|
||||||
"height": 720,
|
|
||||||
"description": "适合轻量横版封面、网页首屏和视频首帧",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "2048x1152",
|
|
||||||
"label": "横屏 16:9 · 2048×1152",
|
|
||||||
"value": "2048x1152",
|
|
||||||
"ratio": "16:9",
|
|
||||||
"width": 2048,
|
|
||||||
"height": 1152,
|
|
||||||
"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",
|
|
||||||
"label": "低 · 快速草稿",
|
|
||||||
"value": "low",
|
|
||||||
"description": "更快生成,适合批量试方向",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "medium",
|
|
||||||
"label": "中 · 常规出图",
|
|
||||||
"value": "medium",
|
|
||||||
"description": "速度和质量折中,适合日常迭代",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "high",
|
|
||||||
"label": "高 · 最终稿",
|
|
||||||
"value": "high",
|
|
||||||
"description": "质量优先,适合定稿和高清素材",
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
VIDEO_SIZE_CHOICES = [
|
VIDEO_SIZE_CHOICES = [
|
||||||
{
|
{
|
||||||
@@ -597,10 +454,6 @@ class GeneratedImage(BaseModel):
|
|||||||
model: str
|
model: str
|
||||||
mode: str = "edit" # "edit"(带参考图) | "text"(纯文字)
|
mode: str = "edit" # "edit"(带参考图) | "text"(纯文字)
|
||||||
url: str # /jobs/{job_id}/frames/{idx}/gen/{id}.jpg
|
url: str # /jobs/{job_id}/frames/{idx}/gen/{id}.jpg
|
||||||
size: str = ""
|
|
||||||
quality: str = ""
|
|
||||||
width: int = 0
|
|
||||||
height: int = 0
|
|
||||||
selected: bool = False
|
selected: bool = False
|
||||||
created_at: float = 0.0
|
created_at: float = 0.0
|
||||||
|
|
||||||
@@ -4714,9 +4567,6 @@ def image_model_options() -> list[dict]:
|
|||||||
"model": GPT_IMAGE_MODEL,
|
"model": GPT_IMAGE_MODEL,
|
||||||
"description": "优先 GPT Image 2,必要时按后端熔断和兜底策略切到备用图片模型",
|
"description": "优先 GPT Image 2,必要时按后端熔断和兜底策略切到备用图片模型",
|
||||||
"available": bool(IMAGE_API_KEY),
|
"available": bool(IMAGE_API_KEY),
|
||||||
"size_options": IMAGE_SIZE_CHOICES,
|
|
||||||
"quality_options": IMAGE_QUALITY_CHOICES,
|
|
||||||
"supports_custom_size": True,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": GPT_IMAGE_MODEL,
|
"id": GPT_IMAGE_MODEL,
|
||||||
@@ -4724,9 +4574,6 @@ def image_model_options() -> list[dict]:
|
|||||||
"model": GPT_IMAGE_MODEL,
|
"model": GPT_IMAGE_MODEL,
|
||||||
"description": "主生图模型,适合营销图和参考图重绘",
|
"description": "主生图模型,适合营销图和参考图重绘",
|
||||||
"available": bool(IMAGE_API_KEY),
|
"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:
|
if IMAGE_FALLBACK_ENABLED and IMAGE_FALLBACK_MODEL and IMAGE_FALLBACK_MODEL != GPT_IMAGE_MODEL:
|
||||||
@@ -4734,11 +4581,8 @@ def image_model_options() -> list[dict]:
|
|||||||
"id": IMAGE_FALLBACK_MODEL,
|
"id": IMAGE_FALLBACK_MODEL,
|
||||||
"label": "Gemini 图片",
|
"label": "Gemini 图片",
|
||||||
"model": IMAGE_FALLBACK_MODEL,
|
"model": IMAGE_FALLBACK_MODEL,
|
||||||
"description": "备用图片模型,使用 Gemini 官方比例和 1K/2K/4K 固定规格",
|
"description": "备用图片模型,适合主模型慢或失败时手动选择",
|
||||||
"available": bool(IMAGE_API_KEY),
|
"available": bool(IMAGE_API_KEY),
|
||||||
"size_options": GEMINI_IMAGE_SIZE_CHOICES,
|
|
||||||
"quality_options": [],
|
|
||||||
"supports_custom_size": False,
|
|
||||||
})
|
})
|
||||||
return options
|
return options
|
||||||
|
|
||||||
@@ -4747,145 +4591,28 @@ def image_size_options() -> list[dict]:
|
|||||||
return IMAGE_SIZE_CHOICES
|
return IMAGE_SIZE_CHOICES
|
||||||
|
|
||||||
|
|
||||||
def gemini_image_size_options() -> list[dict]:
|
def _normalize_image_size(raw: str | None) -> str:
|
||||||
return GEMINI_IMAGE_SIZE_CHOICES
|
|
||||||
|
|
||||||
|
|
||||||
def image_quality_options() -> list[dict]:
|
|
||||||
return IMAGE_QUALITY_CHOICES
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_image_dimensions(value: str) -> tuple[int, int] | None:
|
|
||||||
normalized = value.strip().lower().replace("×", "x")
|
|
||||||
m = re.fullmatch(r"(\d{3,4})\s*x\s*(\d{3,4})", normalized)
|
|
||||||
if not m:
|
|
||||||
return None
|
|
||||||
return int(m.group(1)), int(m.group(2))
|
|
||||||
|
|
||||||
|
|
||||||
def _validate_custom_image_size(width: int, height: int, raw: str) -> str:
|
|
||||||
pixels = width * height
|
|
||||||
long_edge = max(width, height)
|
|
||||||
short_edge = min(width, height)
|
|
||||||
if width % 16 != 0 or height % 16 != 0:
|
|
||||||
raise HTTPException(400, f"unsupported image size: {raw} (宽高必须都是 16 的倍数,例如 1088x1920)")
|
|
||||||
if long_edge > 3840:
|
|
||||||
raise HTTPException(400, f"unsupported image size: {raw} (最长边不能超过 3840px)")
|
|
||||||
if long_edge / short_edge > 3:
|
|
||||||
raise HTTPException(400, f"unsupported image size: {raw} (画幅比例不能超过 3:1)")
|
|
||||||
if pixels < 655_360 or pixels > 8_294_400:
|
|
||||||
raise HTTPException(400, f"unsupported image size: {raw} (总像素需在 655360 到 8294400 之间)")
|
|
||||||
return f"{width}x{height}"
|
|
||||||
|
|
||||||
|
|
||||||
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()
|
value = (raw or "auto").strip().lower()
|
||||||
gpt_aliases = {
|
aliases = {
|
||||||
"9:16": "1088x1920",
|
|
||||||
"9x16": "1088x1920",
|
|
||||||
"16:9": "1280x720",
|
|
||||||
"16x9": "1280x720",
|
|
||||||
"1:1": "1024x1024",
|
|
||||||
"1x1": "1024x1024",
|
|
||||||
"2:3": "1024x1536",
|
|
||||||
"2x3": "1024x1536",
|
|
||||||
"3:2": "1536x1024",
|
|
||||||
"3x2": "1536x1024",
|
|
||||||
"3:4": "960x1280",
|
|
||||||
"3x4": "960x1280",
|
|
||||||
"4:5": "1536x1920",
|
|
||||||
"4x5": "1536x1920",
|
|
||||||
"vertical": "1024x1536",
|
"vertical": "1024x1536",
|
||||||
"portrait": "1024x1536",
|
"portrait": "1024x1536",
|
||||||
"竖图": "1024x1536",
|
"竖图": "1024x1536",
|
||||||
"竖屏": "1088x1920",
|
|
||||||
"square": "1024x1024",
|
"square": "1024x1024",
|
||||||
"方图": "1024x1024",
|
"方图": "1024x1024",
|
||||||
"horizontal": "1536x1024",
|
"horizontal": "1536x1024",
|
||||||
"landscape": "1536x1024",
|
"landscape": "1536x1024",
|
||||||
"横图": "1536x1024",
|
"横图": "1536x1024",
|
||||||
"横屏": "1280x720",
|
|
||||||
}
|
|
||||||
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
|
|
||||||
dimensions = _parse_image_dimensions(value)
|
|
||||||
if dimensions:
|
|
||||||
return _validate_custom_image_size(dimensions[0], dimensions[1], raw or value)
|
|
||||||
raise HTTPException(400, f"unsupported 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}
|
|
||||||
|
|
||||||
|
|
||||||
def _normalize_image_quality(raw: str | None) -> str:
|
|
||||||
value = (raw or "high").strip().lower()
|
|
||||||
aliases = {
|
|
||||||
"standard": "high",
|
|
||||||
"hd": "high",
|
|
||||||
"best": "high",
|
|
||||||
"高": "high",
|
|
||||||
"high-quality": "high",
|
|
||||||
"normal": "medium",
|
|
||||||
"regular": "medium",
|
|
||||||
"中": "medium",
|
|
||||||
"medium-quality": "medium",
|
|
||||||
"draft": "low",
|
|
||||||
"fast": "low",
|
|
||||||
"低": "low",
|
|
||||||
"low-quality": "low",
|
|
||||||
}
|
}
|
||||||
value = aliases.get(value, value)
|
value = aliases.get(value, value)
|
||||||
allowed = {str(item["value"]) for item in IMAGE_QUALITY_CHOICES}
|
allowed = {str(item["value"]) for item in IMAGE_SIZE_CHOICES}
|
||||||
if value not in allowed:
|
if value not in allowed:
|
||||||
raise HTTPException(400, f"unsupported image quality: {raw}")
|
raise HTTPException(400, f"unsupported image size: {raw}")
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
def _image_quality_payload(raw: str | None, model: str | None) -> dict:
|
def _image_size_payload(raw: str | None) -> dict:
|
||||||
quality = _normalize_image_quality(raw)
|
size = _normalize_image_size(raw)
|
||||||
return {"quality": quality} if model == GPT_IMAGE_MODEL else {}
|
return {} if size == "auto" else {"size": size}
|
||||||
|
|
||||||
|
|
||||||
def _image_options_payload(size: str | None, quality: str | None, model: str | None) -> dict:
|
|
||||||
return {**_image_size_payload(size, model, fallback_to_auto=True), **_image_quality_payload(quality, model)}
|
|
||||||
|
|
||||||
|
|
||||||
def video_duration_options() -> list[int]:
|
def video_duration_options() -> list[int]:
|
||||||
@@ -5047,12 +4774,12 @@ def _image_endpoint(path: str) -> str:
|
|||||||
return f"{base}/{path.lstrip('/')}"
|
return f"{base}/{path.lstrip('/')}"
|
||||||
|
|
||||||
|
|
||||||
def _image_generation_response(prompt: str, model: str, size: str | None = "auto", quality: str | None = "high") -> 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:
|
with ai_http_client(timeout=IMAGE_REQUEST_TIMEOUT_SECONDS) as client:
|
||||||
r = client.post(
|
r = client.post(
|
||||||
_image_endpoint("/images/generations"),
|
_image_endpoint("/images/generations"),
|
||||||
headers={"Authorization": f"Bearer {IMAGE_API_KEY}"},
|
headers={"Authorization": f"Bearer {IMAGE_API_KEY}"},
|
||||||
json={"model": model, "prompt": prompt, "n": 1, **_image_options_payload(size, quality, model)},
|
json={"model": model, "prompt": prompt, "n": 1, **_image_size_payload(size)},
|
||||||
)
|
)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
return r.json()
|
return r.json()
|
||||||
@@ -6381,8 +6108,6 @@ def health() -> dict:
|
|||||||
"image_request_timeout_seconds": IMAGE_REQUEST_TIMEOUT_SECONDS,
|
"image_request_timeout_seconds": IMAGE_REQUEST_TIMEOUT_SECONDS,
|
||||||
"image_options": image_model_options(),
|
"image_options": image_model_options(),
|
||||||
"image_size_options": image_size_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),
|
"ai_proxy_configured": bool(AI_HTTP_PROXY),
|
||||||
"image_fallbacks": _image_fallback_models(),
|
"image_fallbacks": _image_fallback_models(),
|
||||||
"image_circuit": _image_circuit_snapshot(),
|
"image_circuit": _image_circuit_snapshot(),
|
||||||
@@ -6871,8 +6596,7 @@ class GenerateReq(BaseModel):
|
|||||||
extra_prompt: str = "" # ✓ 需要的元素(正向)
|
extra_prompt: str = "" # ✓ 需要的元素(正向)
|
||||||
negative_prompt: str = "" # ✗ 不需要的元素(负向)
|
negative_prompt: str = "" # ✗ 不需要的元素(负向)
|
||||||
model: str = "auto" # auto / gpt-image-2 / gemini-3-pro-image-preview
|
model: str = "auto" # auto / gpt-image-2 / gemini-3-pro-image-preview
|
||||||
size: str = "auto" # auto / 1024x1536 / 1088x1920 / custom WxH
|
size: str = "auto" # auto / 1024x1536 / 1024x1024 / 1536x1024
|
||||||
quality: str = "high" # low / medium / high
|
|
||||||
mode: str = "edit" # "edit" 带参考图,"text" 纯文字
|
mode: str = "edit" # "edit" 带参考图,"text" 纯文字
|
||||||
from_selected: bool = False # True 时优先用 frame.selected 的生成图作 reference(迭代),否则原关键帧
|
from_selected: bool = False # True 时优先用 frame.selected 的生成图作 reference(迭代),否则原关键帧
|
||||||
|
|
||||||
@@ -6909,10 +6633,7 @@ def generate_image(job_id: str, idx: int, req: GenerateReq) -> Job:
|
|||||||
if not raw_prompt:
|
if not raw_prompt:
|
||||||
raise HTTPException(400, "prompt required")
|
raise HTTPException(400, "prompt required")
|
||||||
full_prompt = _ensure_english(raw_prompt)
|
full_prompt = _ensure_english(raw_prompt)
|
||||||
requested_model = _normalize_image_model_preference(req.model)
|
image_size = _normalize_image_size(req.size)
|
||||||
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:
|
if not IMAGE_API_KEY:
|
||||||
raise HTTPException(503, "IMAGE_API_KEY 或 LLM_API_KEY 未配置")
|
raise HTTPException(503, "IMAGE_API_KEY 或 LLM_API_KEY 未配置")
|
||||||
|
|
||||||
@@ -6953,14 +6674,14 @@ def generate_image(job_id: str, idx: int, req: GenerateReq) -> Job:
|
|||||||
headers={
|
headers={
|
||||||
"Authorization": f"Bearer {IMAGE_API_KEY}",
|
"Authorization": f"Bearer {IMAGE_API_KEY}",
|
||||||
},
|
},
|
||||||
data={"model": current_model, "prompt": full_prompt, "n": "1", **_image_options_payload(image_size, image_quality, current_model)},
|
data={"model": current_model, "prompt": full_prompt, "n": "1", **_image_size_payload(image_size)},
|
||||||
files={"image": ("reference.jpg", img_bytes_in, "image/jpeg")},
|
files={"image": ("reference.jpg", img_bytes_in, "image/jpeg")},
|
||||||
)
|
)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
resp_data = r.json()
|
resp_data = r.json()
|
||||||
else:
|
else:
|
||||||
# text-only
|
# text-only
|
||||||
resp_data = _image_generation_response(full_prompt, current_model, image_size, image_quality)
|
resp_data = _image_generation_response(full_prompt, current_model, image_size)
|
||||||
|
|
||||||
if resp_data.get("data"):
|
if resp_data.get("data"):
|
||||||
effective_mode = f"{current_mode}:{current_model}"
|
effective_mode = f"{current_mode}:{current_model}"
|
||||||
@@ -7025,13 +6746,6 @@ def generate_image(job_id: str, idx: int, req: GenerateReq) -> Job:
|
|||||||
gen_dir.mkdir(parents=True, exist_ok=True)
|
gen_dir.mkdir(parents=True, exist_ok=True)
|
||||||
out_path = gen_dir / f"{idx:03d}_{gen_id}.jpg"
|
out_path = gen_dir / f"{idx:03d}_{gen_id}.jpg"
|
||||||
out_path.write_bytes(out_bytes)
|
out_path.write_bytes(out_bytes)
|
||||||
actual_width = 0
|
|
||||||
actual_height = 0
|
|
||||||
try:
|
|
||||||
with Image.open(io.BytesIO(out_bytes)) as im:
|
|
||||||
actual_width, actual_height = im.size
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
new_gen = GeneratedImage(
|
new_gen = GeneratedImage(
|
||||||
id=gen_id,
|
id=gen_id,
|
||||||
@@ -7039,10 +6753,6 @@ def generate_image(job_id: str, idx: int, req: GenerateReq) -> Job:
|
|||||||
model=model,
|
model=model,
|
||||||
mode=effective_mode,
|
mode=effective_mode,
|
||||||
url=f"/jobs/{job_id}/frames/{idx}/gen/{gen_id}.jpg",
|
url=f"/jobs/{job_id}/frames/{idx}/gen/{gen_id}.jpg",
|
||||||
size=image_size,
|
|
||||||
quality=image_quality,
|
|
||||||
width=actual_width,
|
|
||||||
height=actual_height,
|
|
||||||
selected=False,
|
selected=False,
|
||||||
created_at=_time.time(),
|
created_at=_time.time(),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -584,9 +584,9 @@
|
|||||||
<p><strong>2026-05-26 公司沉淀版:</strong>画布项目从浏览器本地存储升级为服务端 Postgres 持久化;<code>localStorage</code> 只作为离线缓存和首次导入来源。后端同时建立用户、任务、资源索引和审计表,保留原有 <code>state.json</code> 文件作为任务详情真源,避免一次迁移动到大文件资产结构。</p>
|
<p><strong>2026-05-26 公司沉淀版:</strong>画布项目从浏览器本地存储升级为服务端 Postgres 持久化;<code>localStorage</code> 只作为离线缓存和首次导入来源。后端同时建立用户、任务、资源索引和审计表,保留原有 <code>state.json</code> 文件作为任务详情真源,避免一次迁移动到大文件资产结构。</p>
|
||||||
<p><strong>2026-05-26 AI 润色中性化:</strong>画布 <code>AI 润色</code> 不再复用 SKG 广告文案接口 <code>/creative/copy</code>。后端新增 <code>POST /prompt/polish</code>,前端 <code>useChat</code>、根画布输入框、文本节点和自动执行意图分析改走中性提示词/通用生成接口:只优化用户已经给出的主体、风格、镜头和细节,不主动添加 SKG、按摩产品、TikTok 广告话术或用户没有提到的品牌。当前润色链路会先清理上一次润色遗留的模板尾巴,再判断人物/无人/物体/场景/动物/未知主体;原文明确有人时才声明虚构 AI 角色,原文明确无人时才保留无人物约束,原文没写人时不主动造人但也不追加“必须无人物”的模板尾巴;当输入或参考图已经有人物时,按 AI 生成的虚拟角色继续描述,而不是把人物参考图判定为不可用。</p>
|
<p><strong>2026-05-26 AI 润色中性化:</strong>画布 <code>AI 润色</code> 不再复用 SKG 广告文案接口 <code>/creative/copy</code>。后端新增 <code>POST /prompt/polish</code>,前端 <code>useChat</code>、根画布输入框、文本节点和自动执行意图分析改走中性提示词/通用生成接口:只优化用户已经给出的主体、风格、镜头和细节,不主动添加 SKG、按摩产品、TikTok 广告话术或用户没有提到的品牌。当前润色链路会先清理上一次润色遗留的模板尾巴,再判断人物/无人/物体/场景/动物/未知主体;原文明确有人时才声明虚构 AI 角色,原文明确无人时才保留无人物约束,原文没写人时不主动造人但也不追加“必须无人物”的模板尾巴;当输入或参考图已经有人物时,按 AI 生成的虚拟角色继续描述,而不是把人物参考图判定为不可用。</p>
|
||||||
<p><strong>2026-05-26 我的工作流云端版:</strong>工作流面板从只有公共模板扩展为“公共工作流 / 我的工作流”两类。当前画布可以保存成当前登录用户自己的云端工作流模板,后续在同一账号的其他电脑或浏览器打开后可插回画布;保存时只沉淀节点结构、连线、配置和提示词,主动清掉已生成图片、视频、任务进度、错误和运行态字段,避免把一次性生成结果误当模板复用。</p>
|
<p><strong>2026-05-26 我的工作流云端版:</strong>工作流面板从只有公共模板扩展为“公共工作流 / 我的工作流”两类。当前画布可以保存成当前登录用户自己的云端工作流模板,后续在同一账号的其他电脑或浏览器打开后可插回画布;保存时只沉淀节点结构、连线、配置和提示词,主动清掉已生成图片、视频、任务进度、错误和运行态字段,避免把一次性生成结果误当模板复用。</p>
|
||||||
<p><strong>2026-05-26 生图规格可视化版:</strong>画布生图配置把“比例”和“真实像素”拆开显示。GPT Image 2 / 自动模式的尺寸下拉显示 <code>竖屏 9:16 · 1088×1920</code> 这类标签,并保留自定义 <code>宽x高</code> 输入;Gemini 图片按官方 <code>aspectRatio + imageSize</code> 能力只显示固定的 1K / 2K / 4K 像素规格,不提供任意自定义尺寸。画质从旧的单一“标准”改为 <code>低 / 中 / 高</code>,但 Gemini 的清晰度由尺寸规格里的 1K / 2K / 4K 表示。后端按模型分别校验尺寸,并把请求尺寸、画质和实际输出像素写回生成图片节点。</p>
|
<p><strong>2026-05-26 生图配置恢复版:</strong>按用户要求撤回后续“低/中/高画质、自定义尺寸、Gemini 官方 1K/2K/4K 尺寸、取消自动模型”的实验改动,恢复最初简单配置:图片模型为 <code>auto</code>、<code>gpt-image-2</code>、<code>gemini-3-pro-image-preview</code>,尺寸只保留 <code>auto</code>、<code>1024x1536</code>、<code>1024x1024</code>、<code>1536x1024</code>,画质回到单一标准项;<code>auto</code> 仍按后端既有策略优先 GPT Image 2,必要时由熔断/兜底走 Gemini。</p>
|
||||||
</div>
|
</div>
|
||||||
<p>当前默认业务管线是“个人隔离任务 → 根域名进入个人画布 → 画布项目同步到服务端 Postgres → 用提示词、推荐词、AI 润色、公共工作流或我的工作流创建节点 → 画布自动执行或手动连接图片/视频/文本节点 → 生成结果沉淀在当前个人画布 → 可把当前节点结构保存为我的工作流 → 需要时进入详情页继续编辑”。画布不再被削成三模式入口;首帧、尾帧、参考图、图生视频、多角度分镜、故事板和绘本等上游概念按节点能力保留。底层生成仍由 <code>web/canvas-app/src/hooks/useApi.js</code> 适配到本项目 <code>/creative/jobs/image</code>、<code>/jobs/{id}/frames/{idx}/generate</code>、<code>/jobs/{id}/frames/{idx}/storyboard/video</code>,AI 润色和通用 LLM 文本生成走 <code>/prompt/polish</code> 并保持中性专业:不主动套入 SKG,不主动补产品、平台、广告语境或人物,只扩写用户明确写出的主体、动作、场景、镜头、光线和质量细节;视频提交若带参考图,会在最终提示词中条件声明“参考图里若有人物,应按 AI 生成的虚拟角色处理”,避免把 AI 人像素材误当成真实肖像。生成资产按当前登录用户写入个人 job。图片配置显示真实像素尺寸:GPT Image 2 / 自动模式有 <code>低 / 中 / 高</code> 画质和自定义 <code>宽x高</code>;Gemini 图片只显示官方 1K / 2K / 4K 固定规格,例如 <code>9:16 · 1K · 768×1376</code>、<code>9:16 · 2K · 1536×2752</code>,不显示自定义尺寸输入。生成结果节点会显示实际输出像素。视频画幅只显示 <code>720x1280</code>、<code>1280x720</code>、<code>1024x1024</code>、<code>960x1280</code>;视频时长只显示 <code>5/8/10/12/15</code> 秒。多人互不影响依赖后端 <code>owner_id</code>、画布项目 owner、我的工作流 owner 和飞书 / 备用登录会话隔离。旧 React 单对话框首页、信息流复刻链路仍保留在源码里作为回滚/高级能力,但不作为生产默认入口。</p>
|
<p>当前默认业务管线是“个人隔离任务 → 根域名进入个人画布 → 画布项目同步到服务端 Postgres → 用提示词、推荐词、AI 润色、公共工作流或我的工作流创建节点 → 画布自动执行或手动连接图片/视频/文本节点 → 生成结果沉淀在当前个人画布 → 可把当前节点结构保存为我的工作流 → 需要时进入详情页继续编辑”。画布不再被削成三模式入口;首帧、尾帧、参考图、图生视频、多角度分镜、故事板和绘本等上游概念按节点能力保留。底层生成仍由 <code>web/canvas-app/src/hooks/useApi.js</code> 适配到本项目 <code>/creative/jobs/image</code>、<code>/jobs/{id}/frames/{idx}/generate</code>、<code>/jobs/{id}/frames/{idx}/storyboard/video</code>,AI 润色和通用 LLM 文本生成走 <code>/prompt/polish</code> 并保持中性专业:不主动套入 SKG,不主动补产品、平台、广告语境或人物,只扩写用户明确写出的主体、动作、场景、镜头、光线和质量细节;视频提交若带参考图,会在最终提示词中条件声明“参考图里若有人物,应按 AI 生成的虚拟角色处理”,避免把 AI 人像素材误当成真实肖像。生成资产按当前登录用户写入个人 job。图片尺寸只显示 <code>auto</code>、<code>1024x1536</code>、<code>1024x1024</code>、<code>1536x1024</code>;视频画幅只显示 <code>720x1280</code>、<code>1280x720</code>、<code>1024x1024</code>、<code>960x1280</code>;视频时长只显示 <code>5/8/10/12/15</code> 秒。多人互不影响依赖后端 <code>owner_id</code>、画布项目 owner、我的工作流 owner 和飞书 / 备用登录会话隔离。旧 React 单对话框首页、信息流复刻链路仍保留在源码里作为回滚/高级能力,但不作为生产默认入口。</p>
|
||||||
<div class="pipeline">
|
<div class="pipeline">
|
||||||
<div class="step"><div class="num">01</div><h3>个人任务</h3><p><code>GET /jobs</code> 按当前登录用户过滤;旧无 owner 任务只对备用账号可见。</p></div>
|
<div class="step"><div class="num">01</div><h3>个人任务</h3><p><code>GET /jobs</code> 按当前登录用户过滤;旧无 owner 任务只对备用账号可见。</p></div>
|
||||||
<div class="step"><div class="num">02</div><h3>进入画布</h3><p>用户直接在根域名个人画布里操作;项目列表优先读取服务端 <code>/canvas-projects</code>,本地旧项目会首次导入。</p></div>
|
<div class="step"><div class="num">02</div><h3>进入画布</h3><p>用户直接在根域名个人画布里操作;项目列表优先读取服务端 <code>/canvas-projects</code>,本地旧项目会首次导入。</p></div>
|
||||||
@@ -615,7 +615,7 @@
|
|||||||
<tr><td><code>web/canvas-app/src/stores/workflows.js</code></td><td>我的工作流 store:调用 <code>GET/POST/DELETE /canvas-workflows</code> 读取、保存和删除当前登录用户自己的云端工作流模板。保存前会清理节点里的 <code>base64</code>、生成 URL、任务进度、错误、视频结果和 LLM 输出等运行态字段,只保留可复用的节点结构、连线、配置和提示词。</td></tr>
|
<tr><td><code>web/canvas-app/src/stores/workflows.js</code></td><td>我的工作流 store:调用 <code>GET/POST/DELETE /canvas-workflows</code> 读取、保存和删除当前登录用户自己的云端工作流模板。保存前会清理节点里的 <code>base64</code>、生成 URL、任务进度、错误、视频结果和 LLM 输出等运行态字段,只保留可复用的节点结构、连线、配置和提示词。</td></tr>
|
||||||
<tr><td><code>web/canvas-app/src/views/Canvas.vue</code></td><td>画布主交互:恢复上游底部 prompt composer、<code>AI 润色</code>、<code>自动执行</code>、推荐词、节点菜单、工作流面板、API/模型设置入口和批量下载入口。自动执行会调用 <code>useWorkflowOrchestrator</code> 分析提示词,创建文生图、图转视频、故事板、多角度分镜或绘本节点组;手动模式只创建文本节点,用户自行连接节点。工作流面板支持公共模板和我的工作流:公共模板走本地 <code>createNodes()</code>,我的工作流从云端 <code>workflow_data</code> 插回当前画布,并重新生成节点 ID、按视口中心重排、按映射重连边。底部推荐词来自共享短词池,4 个一组单行展示,刷新按钮在 30 组内轮换,不改变输入面板高度。</td></tr>
|
<tr><td><code>web/canvas-app/src/views/Canvas.vue</code></td><td>画布主交互:恢复上游底部 prompt composer、<code>AI 润色</code>、<code>自动执行</code>、推荐词、节点菜单、工作流面板、API/模型设置入口和批量下载入口。自动执行会调用 <code>useWorkflowOrchestrator</code> 分析提示词,创建文生图、图转视频、故事板、多角度分镜或绘本节点组;手动模式只创建文本节点,用户自行连接节点。工作流面板支持公共模板和我的工作流:公共模板走本地 <code>createNodes()</code>,我的工作流从云端 <code>workflow_data</code> 插回当前画布,并重新生成节点 ID、按视口中心重排、按映射重连边。底部推荐词来自共享短词池,4 个一组单行展示,刷新按钮在 30 组内轮换,不改变输入面板高度。</td></tr>
|
||||||
<tr><td><code>web/canvas-app/src/config/suggestions.js</code></td><td>首页和画布共用的推荐词配置:维护 <code>QUICK_SUGGESTION_GROUPS</code>,当前为 30 组 / 120 个短词,每组 4 个,控制刷新按钮的轮换范围;词条保持短小,避免推荐栏换行或顶起 composer。</td></tr>
|
<tr><td><code>web/canvas-app/src/config/suggestions.js</code></td><td>首页和画布共用的推荐词配置:维护 <code>QUICK_SUGGESTION_GROUPS</code>,当前为 30 组 / 120 个短词,每组 4 个,控制刷新按钮的轮换范围;词条保持短小,避免推荐栏换行或顶起 composer。</td></tr>
|
||||||
<tr><td><code>web/canvas-app/src/config/models.js</code></td><td>画布媒体模型和规格的前端白名单:图片只内置 <code>auto</code>、<code>gpt-image-2</code>、<code>gemini-3-pro-image-preview</code>。GPT Image 2 / 自动模式使用项目像素预设并允许自定义 <code>宽x高</code>;Gemini 使用独立 <code>GEMINI_SIZE_OPTIONS</code>,只列官方 1K / 2K / 4K 固定像素规格,<code>supportsCustomSize=false</code>。视频只内置 <code>seedance</code> / <code>Seedance 2.0 Fast</code>,画幅和时长对齐后端 <code>/health</code> 能力边界。<code>useModelConfig.js</code> 和 Pinia 模型 store 会忽略浏览器本地自定义图片/视频模型,防止旧缓存把不可用模型带回生成下拉。</td></tr>
|
<tr><td><code>web/canvas-app/src/config/models.js</code></td><td>画布媒体模型和规格的前端白名单:图片只内置 <code>auto</code>、<code>gpt-image-2</code>、<code>gemini-3-pro-image-preview</code>,尺寸只内置 <code>auto</code>、<code>1024x1536</code>、<code>1024x1024</code>、<code>1536x1024</code>;视频只内置 <code>seedance</code> / <code>Seedance 2.0 Fast</code>,画幅和时长对齐后端 <code>/health</code> 能力边界。<code>useModelConfig.js</code> 和 Pinia 模型 store 会忽略浏览器本地自定义图片/视频模型,防止旧缓存把不可用模型带回生成下拉。</td></tr>
|
||||||
<tr><td><code>web/canvas-app/src/hooks/useApi.js</code></td><td>画布到本项目后端的适配层:不再读取浏览器 API Key,而是使用当前登录会话 Cookie 调用 <code>/api</code>。文生图 / 图生图先创建轻量 creative job,再调用 <code>/frames/0/generate</code>;文生视频 / 图生视频调用 <code>/storyboard/video</code> 并轮询 <code>/jobs/{id}</code>,完成后把图片或 mp4 URL 写回画布节点。<code>useChat</code> 已从 SKG 广告文案接口切到 <code>/prompt/polish</code>:AI 润色显式使用 image/video prompt 模式,LLM 节点使用通用 chat 模式,避免自动注入用户没有提到的 SKG、产品、平台或营销语境;后端会清理旧润色模板尾巴、判断人物/无人/物体/场景意图,并在输出后检查“有人却禁止人物、无人却新增人物、未写 SKG 却出现 SKG”等冲突。图生视频实际提交到后端后,后端会对参考图追加 AI 虚拟角色条件说明,不要求前端判断图片里是否有人脸。</td></tr>
|
<tr><td><code>web/canvas-app/src/hooks/useApi.js</code></td><td>画布到本项目后端的适配层:不再读取浏览器 API Key,而是使用当前登录会话 Cookie 调用 <code>/api</code>。文生图 / 图生图先创建轻量 creative job,再调用 <code>/frames/0/generate</code>;文生视频 / 图生视频调用 <code>/storyboard/video</code> 并轮询 <code>/jobs/{id}</code>,完成后把图片或 mp4 URL 写回画布节点。<code>useChat</code> 已从 SKG 广告文案接口切到 <code>/prompt/polish</code>:AI 润色显式使用 image/video prompt 模式,LLM 节点使用通用 chat 模式,避免自动注入用户没有提到的 SKG、产品、平台或营销语境;后端会清理旧润色模板尾巴、判断人物/无人/物体/场景意图,并在输出后检查“有人却禁止人物、无人却新增人物、未写 SKG 却出现 SKG”等冲突。图生视频实际提交到后端后,后端会对参考图追加 AI 虚拟角色条件说明,不要求前端判断图片里是否有人脸。</td></tr>
|
||||||
<tr><td><code>web/scripts/sync-canvas-root.mjs</code></td><td>构建桥接脚本:在 <code>next build</code> 静态导出完成后,把 Vite 画布产物 <code>web/canvas-app/dist</code> 覆盖到 <code>web/out</code> 根目录,使 <code>https://marketing.skg.com</code> 登录后直接进入画布;旧 <code>web/scripts/sync-canvas-dist.mjs</code> 保留但不再由生产构建调用。</td></tr>
|
<tr><td><code>web/scripts/sync-canvas-root.mjs</code></td><td>构建桥接脚本:在 <code>next build</code> 静态导出完成后,把 Vite 画布产物 <code>web/canvas-app/dist</code> 覆盖到 <code>web/out</code> 根目录,使 <code>https://marketing.skg.com</code> 登录后直接进入画布;旧 <code>web/scripts/sync-canvas-dist.mjs</code> 保留但不再由生产构建调用。</td></tr>
|
||||||
<tr><td><code>web/app/detail/page.tsx</code></td><td>任务详情页:静态导出路由 <code>/detail/?job=<id></code>,通过 query 读取 job id,调用 <code>getJob</code> 恢复同一任务。页面展示参考图、全部生成图、视频候选、营销图文方案和历史提示词,可继续调用 <code>generateImage</code>、<code>generateStoryboardVideo</code>、<code>generateCreativeCopy</code>,并支持删除图片/视频。该页继续依赖后端 owner 过滤,用户不能通过切换 URL 读取别人的任务。</td></tr>
|
<tr><td><code>web/app/detail/page.tsx</code></td><td>任务详情页:静态导出路由 <code>/detail/?job=<id></code>,通过 query 读取 job id,调用 <code>getJob</code> 恢复同一任务。页面展示参考图、全部生成图、视频候选、营销图文方案和历史提示词,可继续调用 <code>generateImage</code>、<code>generateStoryboardVideo</code>、<code>generateCreativeCopy</code>,并支持删除图片/视频。该页继续依赖后端 owner 过滤,用户不能通过切换 URL 读取别人的任务。</td></tr>
|
||||||
@@ -639,7 +639,7 @@
|
|||||||
<tr><td><code>web/components/product-library-picker.tsx</code></td><td>SKG 内置白底产品图库选择器:搜索、品类筛选、预览尺寸,并把库内图片复制为当前 job 的 <code>asset</code>。</td></tr>
|
<tr><td><code>web/components/product-library-picker.tsx</code></td><td>SKG 内置白底产品图库选择器:搜索、品类筛选、预览尺寸,并把库内图片复制为当前 job 的 <code>asset</code>。</td></tr>
|
||||||
<tr><td><code>web/components/storyboard-bar.tsx</code></td><td>顶部分镜编排条:展示选入编排的关键帧,并作为唯一分镜导航。</td></tr>
|
<tr><td><code>web/components/storyboard-bar.tsx</code></td><td>顶部分镜编排条:展示选入编排的关键帧,并作为唯一分镜导航。</td></tr>
|
||||||
<tr><td><code>web/components/storyboard-workbench.tsx</code></td><td>顶部分镜编排条下方的明细区:4 图槽、改造目标、时长、自动保存。</td></tr>
|
<tr><td><code>web/components/storyboard-workbench.tsx</code></td><td>顶部分镜编排条下方的明细区:4 图槽、改造目标、时长、自动保存。</td></tr>
|
||||||
<tr><td><code>web/lib/api.ts</code></td><td>前端类型和 API client,是前后端数据契约镜像;<code>RuntimeHealth</code> / <code>RuntimeModels</code> 读取 <code>GET /health</code>,把 ASR、翻译、视觉、图像、视频等模型名作为前端模型标注的真源,并同步承接 <code>RuntimeSizeOption</code>、<code>RuntimeQualityOption</code>、图片尺寸、图片画质、视频画幅、视频时长和最大单条秒数。<code>GeneratedImage</code> 额外承接 <code>size</code>、<code>quality</code>、<code>width</code>、<code>height</code>,用于画布和旧首页显示请求规格与实际输出像素;<code>GeneratedVideo</code> 额外承接 <code>queue_position</code>、<code>queue_size</code> 和 <code>queue_message</code>,用于首页和后续个人画布显示视频生成队列。默认首页主要使用 <code>createCreativeImageJob</code>、<code>uploadReferenceFrame</code>、<code>generateImage</code> 和 <code>generateStoryboardVideo</code>;<code>generateImage</code> 请求体可传 <code>size</code> 和 <code>quality</code>。<code>generateCreativeCopy</code> 仍保留给详情页和后续高级能力。资源库相关类型和 CRUD/use/copy-to-job 函数继续保留给旧工作台和后续资源中心。</td></tr>
|
<tr><td><code>web/lib/api.ts</code></td><td>前端类型和 API client,是前后端数据契约镜像;<code>RuntimeHealth</code> / <code>RuntimeModels</code> 读取 <code>GET /health</code>,把 ASR、翻译、视觉、图像、视频等模型名作为前端模型标注的真源,并同步承接 <code>RuntimeSizeOption</code>、图片尺寸、视频画幅、视频时长和最大单条秒数。<code>GeneratedVideo</code> 额外承接 <code>queue_position</code>、<code>queue_size</code> 和 <code>queue_message</code>,用于首页和后续个人画布显示视频生成队列。默认首页主要使用 <code>createCreativeImageJob</code>、<code>uploadReferenceFrame</code>、<code>generateImage</code> 和 <code>generateStoryboardVideo</code>;<code>generateImage</code> 请求体现在可传 <code>size</code>。<code>generateCreativeCopy</code> 仍保留给详情页和后续高级能力。资源库相关类型和 CRUD/use/copy-to-job 函数继续保留给旧工作台和后续资源中心。</td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@@ -647,7 +647,7 @@
|
|||||||
<h3>后端核心</h3>
|
<h3>后端核心</h3>
|
||||||
<table>
|
<table>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr><td><code>api/main.py</code></td><td>FastAPI 单文件后端:登录会话、状态模型、任务恢复、下载、抽帧、Vision、清洗、元素、分镜、原音频转写/翻译、声音与背景音分析、后续口播改写/TTS、文件返回;同时承载全局 <code>prompt_library</code> 和 <code>asset_library</code> 的磁盘索引、CRUD、删除保护和复制到 job API。启动时会初始化 Postgres schema、扫描现有 <code>state.json</code> / 资源库并写入索引;<code>/canvas-projects</code> 系列接口把画布项目按当前登录用户持久化,<code>/canvas-workflows</code> 系列接口把我的工作流按当前登录用户持久化为可复用模板。轻量创作入口 <code>POST /creative/jobs/image</code> 把上传图片或空白底图写成一个只有 0 号关键帧的 <code>Job</code>,让首页直接复用生图/生视频接口;该接口兼容无 body / JSON 空对象 / 正常 multipart 上传,避免无首帧文生图或文生视频时空 multipart 被 FastAPI 在业务前置解析阶段拒绝;<code>POST /prompt/polish</code> 用于中性 AI 润色和通用 LLM 文本生成,只保留用户明确给出的主体、品牌、产品、地点、风格和意图,不默认加入 SKG、按摩产品、平台或短视频广告话术。润色链路会先用 <code>_strip_previous_polish_boilerplate</code> 去掉旧模板尾巴,再用 <code>_classify_prompt_intent</code> 判断人物、无人、物体、场景、动物或未知主体,最后用 <code>_repair_polished_prompt</code> 修掉有人/无人矛盾、未写人却新增人物、未写 SKG 却出现 SKG 等冲突;<code>_append_reference_image_person_guard</code> 会在视频任务最终入队前给参考图请求追加条件提示,声明参考图里若有人物则按 AI 生成的虚拟角色处理;<code>/health</code> 返回 <code>database</code>、<code>image_options</code>、<code>image_size_options</code>、<code>gemini_image_size_options</code>、<code>image_quality_options</code>、<code>video_options</code>、<code>video_size_options</code>、<code>video_duration_options</code> 和 <code>video_max_duration_seconds</code>;<code>/frames/{idx}/generate</code> 的 <code>model</code> 字段用于图片模型偏好,<code>size</code> 字段按实际模型校验:GPT Image 2 可用项目像素预设和符合约束的自定义尺寸,Gemini 只接受 <code>GEMINI_IMAGE_SIZE_CHOICES</code> 里的官方固定像素;<code>quality</code> 字段用于 GPT 的 <code>low / medium / high</code> 画质。生成完成会把 <code>GeneratedImage.size</code>、<code>quality</code>、<code>width</code>、<code>height</code> 写回。<code>/storyboard/video</code> 继续使用 <code>model</code> 字段选择视频别名,并先校验画幅与时长能力边界,然后把 <code>GeneratedVideo</code> 写成 <code>queued</code> 占位并进入进程内视频队列。队列默认 <code>VIDEO_QUEUE_MAX_CONCURRENT=2</code>、<code>VIDEO_QUEUE_MAX_CONCURRENT_PER_USER=1</code>,同一用户连续提交不会占满全局并发;排队任务会回写 <code>queue_position</code>、<code>queue_size</code>、<code>queue_message</code>。旧 <code>AgentRun</code> 一键出片状态机、TK 复刻接口和 <code>POST /creative/copy</code> 作为明确的 SKG 营销文案接口继续保留。</td></tr>
|
<tr><td><code>api/main.py</code></td><td>FastAPI 单文件后端:登录会话、状态模型、任务恢复、下载、抽帧、Vision、清洗、元素、分镜、原音频转写/翻译、声音与背景音分析、后续口播改写/TTS、文件返回;同时承载全局 <code>prompt_library</code> 和 <code>asset_library</code> 的磁盘索引、CRUD、删除保护和复制到 job API。启动时会初始化 Postgres schema、扫描现有 <code>state.json</code> / 资源库并写入索引;<code>/canvas-projects</code> 系列接口把画布项目按当前登录用户持久化,<code>/canvas-workflows</code> 系列接口把我的工作流按当前登录用户持久化为可复用模板。轻量创作入口 <code>POST /creative/jobs/image</code> 把上传图片或空白底图写成一个只有 0 号关键帧的 <code>Job</code>,让首页直接复用生图/生视频接口;该接口兼容无 body / JSON 空对象 / 正常 multipart 上传,避免无首帧文生图或文生视频时空 multipart 被 FastAPI 在业务前置解析阶段拒绝;<code>POST /prompt/polish</code> 用于中性 AI 润色和通用 LLM 文本生成,只保留用户明确给出的主体、品牌、产品、地点、风格和意图,不默认加入 SKG、按摩产品、平台或短视频广告话术。润色链路会先用 <code>_strip_previous_polish_boilerplate</code> 去掉旧模板尾巴,再用 <code>_classify_prompt_intent</code> 判断人物、无人、物体、场景、动物或未知主体,最后用 <code>_repair_polished_prompt</code> 修掉有人/无人矛盾、未写人却新增人物、未写 SKG 却出现 SKG 等冲突;<code>_append_reference_image_person_guard</code> 会在视频任务最终入队前给参考图请求追加条件提示,声明参考图里若有人物则按 AI 生成的虚拟角色处理;<code>/health</code> 返回 <code>database</code>、<code>image_options</code>、<code>image_size_options</code>、<code>video_options</code>、<code>video_size_options</code>、<code>video_duration_options</code> 和 <code>video_max_duration_seconds</code>;<code>/frames/{idx}/generate</code> 的 <code>model</code> 字段用于图片模型偏好,<code>size</code> 字段用于图片输出尺寸;<code>/storyboard/video</code> 继续使用 <code>model</code> 字段选择视频别名,并先校验画幅与时长能力边界,然后把 <code>GeneratedVideo</code> 写成 <code>queued</code> 占位并进入进程内视频队列。队列默认 <code>VIDEO_QUEUE_MAX_CONCURRENT=2</code>、<code>VIDEO_QUEUE_MAX_CONCURRENT_PER_USER=1</code>,同一用户连续提交不会占满全局并发;排队任务会回写 <code>queue_position</code>、<code>queue_size</code>、<code>queue_message</code>。旧 <code>AgentRun</code> 一键出片状态机、TK 复刻接口和 <code>POST /creative/copy</code> 作为明确的 SKG 营销文案接口继续保留。</td></tr>
|
||||||
<tr><td><code>api/db.py</code></td><td>Postgres 适配层:在 <code>DATABASE_URL</code> 存在且 <code>psycopg</code> 可用时启用;负责建表、健康检查、用户 upsert、审计日志、画布项目 CRUD、我的工作流 CRUD,以及把 <code>Job</code>、<code>AgentRun</code>、提示词库和素材库写入索引表。数据库不可用时本地开发会降级为 disabled,生产 <code>verify-prod-docker.sh</code> 会要求 <code>database.connected=true</code>。</td></tr>
|
<tr><td><code>api/db.py</code></td><td>Postgres 适配层:在 <code>DATABASE_URL</code> 存在且 <code>psycopg</code> 可用时启用;负责建表、健康检查、用户 upsert、审计日志、画布项目 CRUD、我的工作流 CRUD,以及把 <code>Job</code>、<code>AgentRun</code>、提示词库和素材库写入索引表。数据库不可用时本地开发会降级为 disabled,生产 <code>verify-prod-docker.sh</code> 会要求 <code>database.connected=true</code>。</td></tr>
|
||||||
<tr><td><code>video_model_options()</code></td><td>视频模型能力出口:如果 <code>seedance</code>、<code>kling</code>、<code>veo3</code>、<code>veo</code> 等业务别名实际都映射到同一个真实模型,会按真实模型去重,只给前端返回一个可用选项;当前生产真实模型为 <code>doubao-seedance-2-0-fast-260128</code>,前端显示为 <code>Seedance 2.0 Fast</code>。后续只有在服务器真的配置了不同可用视频模型时,才应把新的模型重新暴露给画布。</td></tr>
|
<tr><td><code>video_model_options()</code></td><td>视频模型能力出口:如果 <code>seedance</code>、<code>kling</code>、<code>veo3</code>、<code>veo</code> 等业务别名实际都映射到同一个真实模型,会按真实模型去重,只给前端返回一个可用选项;当前生产真实模型为 <code>doubao-seedance-2-0-fast-260128</code>,前端显示为 <code>Seedance 2.0 Fast</code>。后续只有在服务器真的配置了不同可用视频模型时,才应把新的模型重新暴露给画布。</td></tr>
|
||||||
<tr><td><code>api/product_library/skg-products</code></td><td>内置 SKG 白底产品图库:<code>manifest.json</code> 记录从桌面产品图筛出的 gallery 白底图和桌面 4 张产品角度图,<code>images/</code> 存 45 张参考图。</td></tr>
|
<tr><td><code>api/product_library/skg-products</code></td><td>内置 SKG 白底产品图库:<code>manifest.json</code> 记录从桌面产品图筛出的 gallery 白底图和桌面 4 张产品角度图,<code>images/</code> 存 45 张参考图。</td></tr>
|
||||||
@@ -843,16 +843,6 @@ audit_events</pre>
|
|||||||
elements: KeyElement[],
|
elements: KeyElement[],
|
||||||
storyboard: StoryboardScene,
|
storyboard: StoryboardScene,
|
||||||
generated_images: GeneratedImage[]
|
generated_images: GeneratedImage[]
|
||||||
}</pre>
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
<h3>GeneratedImage</h3>
|
|
||||||
<p>画布和详情页的图片生成结果。<code>size</code> / <code>quality</code> 是用户请求的规格,<code>width</code> / <code>height</code> 是实际返回图片像素;当用户选择自动尺寸或上游模型调整输出时,以实际像素作为最终展示依据。</p>
|
|
||||||
<pre>GeneratedImage {
|
|
||||||
id, prompt, model, mode, url,
|
|
||||||
size, quality,
|
|
||||||
width, height,
|
|
||||||
selected, created_at
|
|
||||||
}</pre>
|
}</pre>
|
||||||
</div>
|
</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
@@ -1113,12 +1103,12 @@ ProductRefStateItem {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr><td>网页登录 / 飞书免登录</td><td><code>GET /auth/config</code>、<code>GET /auth/feishu/start</code>、<code>GET /auth/feishu/callback</code>、<code>POST /auth/login</code>、<code>GET /auth/check</code>、<code>GET /auth/me</code>、<code>POST /auth/logout</code></td><td><code>web/app/login/page.tsx</code>、Nginx <code>auth_request</code></td><td>登录页先读 <code>/api/auth/config</code> 判断是否显示飞书按钮和密码表单;飞书客户端内且 <code>feishu_enabled=true</code> 时前端自动跳转授权入口,普通浏览器保留手动飞书按钮。飞书 OAuth 成功后后端用 open_id / union_id / email 生成多用户会话并设置 HttpOnly Cookie。当前生产 <code>PASSWORD_AUTH_ENABLED=false</code>,因此 <code>password_enabled=false</code>,账号密码表单不展示,<code>POST /auth/login</code> 返回未配置。生产 Nginx 对工作台和 <code>/api/</code> 调 <code>/auth/check</code> 做统一校验,未登录页面跳 <code>/login/?next=$request_uri</code>,API 返回 JSON 401。</td></tr>
|
<tr><td>网页登录 / 飞书免登录</td><td><code>GET /auth/config</code>、<code>GET /auth/feishu/start</code>、<code>GET /auth/feishu/callback</code>、<code>POST /auth/login</code>、<code>GET /auth/check</code>、<code>GET /auth/me</code>、<code>POST /auth/logout</code></td><td><code>web/app/login/page.tsx</code>、Nginx <code>auth_request</code></td><td>登录页先读 <code>/api/auth/config</code> 判断是否显示飞书按钮和密码表单;飞书客户端内且 <code>feishu_enabled=true</code> 时前端自动跳转授权入口,普通浏览器保留手动飞书按钮。飞书 OAuth 成功后后端用 open_id / union_id / email 生成多用户会话并设置 HttpOnly Cookie。当前生产 <code>PASSWORD_AUTH_ENABLED=false</code>,因此 <code>password_enabled=false</code>,账号密码表单不展示,<code>POST /auth/login</code> 返回未配置。生产 Nginx 对工作台和 <code>/api/</code> 调 <code>/auth/check</code> 做统一校验,未登录页面跳 <code>/login/?next=$request_uri</code>,API 返回 JSON 401。</td></tr>
|
||||||
<tr><td>运行配置 / 模型标注</td><td><code>GET /health</code></td><td><code>getRuntimeHealth</code>、<code>ModelTrace</code></td><td>返回 <code>database</code> 健康状态和 <code>models</code>:ASR、<code>asr_language</code>(默认 <code>auto</code>,表示中文/英文/多语言自动识别)、<code>asr_base_url</code>、<code>asr_remote_enabled</code>、<code>asr_local_fallback_enabled</code>、<code>asr_audio_fallback_enabled</code>、<code>faster_whisper</code>、本机 ASR、ASR fallback、翻译、GPT 改写、GPT 画面理解、产品视角识别 <code>product_view</code>、主图像模型 <code>gpt-image-2</code>、图片故障兜底 <code>image_fallbacks</code>、GPT 图片尺寸 <code>image_size_options</code>、Gemini 图片尺寸 <code>gemini_image_size_options</code>、图片画质 <code>image_quality_options</code>、短时熔断状态 <code>image_circuit</code>、主体 6 视图模型链路、Azure OpenAI TTS、视频别名、视频画幅 <code>video_size_options</code>、真实可用视频时长 <code>video_duration_options</code>、单条最大秒数 <code>video_max_duration_seconds</code> 和 Seedance 服务商。当前 <code>REWRITE_MODEL</code>、<code>AUDIO_REWRITE_MODEL</code> 和 <code>VISION_MODEL</code> 默认使用 <code>gpt-4o</code>;如果旧环境变量仍写 <code>gemini-*</code>,后端会归一化回 <code>GPT_TEXT_MODEL</code> / <code>REWRITE_MODEL</code>。语音只走 Azure OpenAI TTS,<code>models.voice_tts_paths</code> 会回传当前尝试的语音路径,方便区分路径错误和语音服务不可用。前端所有当前主路径里会调用模型的按钮旁显示模型名,点击弹出小窗口查看模型链路和输入输出逻辑;不返回 API Key、数据库密码或敏感凭证。</td></tr>
|
<tr><td>运行配置 / 模型标注</td><td><code>GET /health</code></td><td><code>getRuntimeHealth</code>、<code>ModelTrace</code></td><td>返回 <code>database</code> 健康状态和 <code>models</code>:ASR、<code>asr_language</code>(默认 <code>auto</code>,表示中文/英文/多语言自动识别)、<code>asr_base_url</code>、<code>asr_remote_enabled</code>、<code>asr_local_fallback_enabled</code>、<code>asr_audio_fallback_enabled</code>、<code>faster_whisper</code>、本机 ASR、ASR fallback、翻译、GPT 改写、GPT 画面理解、产品视角识别 <code>product_view</code>、主图像模型 <code>gpt-image-2</code>、图片故障兜底 <code>image_fallbacks</code>、图片尺寸 <code>image_size_options</code>、短时熔断状态 <code>image_circuit</code>、主体 6 视图模型链路、Azure OpenAI TTS、视频别名、视频画幅 <code>video_size_options</code>、真实可用视频时长 <code>video_duration_options</code>、单条最大秒数 <code>video_max_duration_seconds</code> 和 Seedance 服务商。当前 <code>REWRITE_MODEL</code>、<code>AUDIO_REWRITE_MODEL</code> 和 <code>VISION_MODEL</code> 默认使用 <code>gpt-4o</code>;如果旧环境变量仍写 <code>gemini-*</code>,后端会归一化回 <code>GPT_TEXT_MODEL</code> / <code>REWRITE_MODEL</code>。语音只走 Azure OpenAI TTS,<code>models.voice_tts_paths</code> 会回传当前尝试的语音路径,方便区分路径错误和语音服务不可用。前端所有当前主路径里会调用模型的按钮旁显示模型名,点击弹出小窗口查看模型链路和输入输出逻辑;不返回 API Key、数据库密码或敏感凭证。</td></tr>
|
||||||
<tr><td>历史列表</td><td><code>GET /jobs</code></td><td><code>listJobs</code></td><td>当前登录用户可见 job 精简列表(id/url/status/thumbnail/mtime/owner…),按 state.json mtime 倒序。前端 URL 无 <code>?job=</code> 时拉它回填本人历史;带 <code>limit</code> 可截断。开启数据隔离时,飞书用户只看到自己的任务,历史无 owner 的旧任务只对备用账号可见。</td></tr>
|
<tr><td>历史列表</td><td><code>GET /jobs</code></td><td><code>listJobs</code></td><td>当前登录用户可见 job 精简列表(id/url/status/thumbnail/mtime/owner…),按 state.json mtime 倒序。前端 URL 无 <code>?job=</code> 时拉它回填本人历史;带 <code>limit</code> 可截断。开启数据隔离时,飞书用户只看到自己的任务,历史无 owner 的旧任务只对备用账号可见。</td></tr>
|
||||||
<tr><td>创建任务</td><td><code>POST /jobs</code></td><td><code>createJob</code></td><td>提交 TK 链接,后台开始下载;后端会把当前登录用户写入 <code>Job.owner_*</code>,后续详情、素材文件、删除和生成接口都通过统一中间件校验归属。下载阶段默认不带 cookies;生产环境必须显式保持 <code>YTDLP_COOKIES_FILE=</code> 和 <code>YTDLP_COOKIES_FROM_BROWSER=</code> 为空,避免容器内误读被打进镜像的开发 <code>api/.env</code>。</td></tr>
|
<tr><td>创建任务</td><td><code>POST /jobs</code></td><td><code>createJob</code></td><td>提交 TK 链接,后台开始下载;后端会把当前登录用户写入 <code>Job.owner_*</code>,后续详情、素材文件、删除和生成接口都通过统一中间件校验归属。下载阶段默认不带 cookies;生产环境必须显式保持 <code>YTDLP_COOKIES_FILE=</code> 和 <code>YTDLP_COOKIES_FROM_BROWSER=</code> 为空,避免容器内误读被打进镜像的开发 <code>api/.env</code>。</td></tr>
|
||||||
<tr><td>画布项目</td><td><code>GET /canvas-projects</code><br><code>POST /canvas-projects</code><br><code>PUT /canvas-projects/{id}</code><br><code>GET /canvas-projects/{id}</code><br><code>DELETE /canvas-projects/{id}</code><br><code>POST /canvas-projects/import</code></td><td><code>web/canvas-app/src/stores/projects.js</code></td><td>根域名画布项目的服务端持久化接口。列表和详情按当前登录用户过滤;写入时保存画布 JSON、缩略图、可见性、版本和更新时间;删除为软删除。首次上线后本地 <code>localStorage</code> 旧项目会通过 import 导入到当前用户,之后服务端 Postgres 是主存储。</td></tr>
|
<tr><td>画布项目</td><td><code>GET /canvas-projects</code><br><code>POST /canvas-projects</code><br><code>PUT /canvas-projects/{id}</code><br><code>GET /canvas-projects/{id}</code><br><code>DELETE /canvas-projects/{id}</code><br><code>POST /canvas-projects/import</code></td><td><code>web/canvas-app/src/stores/projects.js</code></td><td>根域名画布项目的服务端持久化接口。列表和详情按当前登录用户过滤;写入时保存画布 JSON、缩略图、可见性、版本和更新时间;删除为软删除。首次上线后本地 <code>localStorage</code> 旧项目会通过 import 导入到当前用户,之后服务端 Postgres 是主存储。</td></tr>
|
||||||
<tr><td>我的工作流</td><td><code>GET /canvas-workflows</code><br><code>POST /canvas-workflows</code><br><code>PUT /canvas-workflows/{id}</code><br><code>DELETE /canvas-workflows/{id}</code></td><td><code>web/canvas-app/src/stores/workflows.js</code><br><code>WorkflowPanel.vue</code></td><td>工作流面板“我的工作流”页的个人模板接口。列表、保存、更新和删除都按当前登录用户过滤;保存的是清理过运行态的 <code>workflow_data.nodes/edges/viewport</code>,用于跨设备复用画布结构。插回画布时前端会按当前视口中心重排节点、重建节点 ID,并用旧 ID 到新 ID 的映射重连边,避免和现有画布节点冲突。</td></tr>
|
<tr><td>我的工作流</td><td><code>GET /canvas-workflows</code><br><code>POST /canvas-workflows</code><br><code>PUT /canvas-workflows/{id}</code><br><code>DELETE /canvas-workflows/{id}</code></td><td><code>web/canvas-app/src/stores/workflows.js</code><br><code>WorkflowPanel.vue</code></td><td>工作流面板“我的工作流”页的个人模板接口。列表、保存、更新和删除都按当前登录用户过滤;保存的是清理过运行态的 <code>workflow_data.nodes/edges/viewport</code>,用于跨设备复用画布结构。插回画布时前端会按当前视口中心重排节点、重建节点 ID,并用旧 ID 到新 ID 的映射重连边,避免和现有画布节点冲突。</td></tr>
|
||||||
<tr><td>画布生成</td><td><code>POST /creative/jobs/image</code><br><code>POST /jobs/{id}/frames/upload</code><br><code>POST /jobs/{id}/frames/{idx}/generate</code><br><code>POST /jobs/{id}/frames/{idx}/storyboard/video</code><br><code>GET /jobs/{id}</code></td><td><code>web/canvas-app/src/hooks/useApi.js</code></td><td>画布项目结构保存在 <code>/canvas-projects</code>;一旦生成图片或视频,就通过同一套 creative job / frame / storyboard video 接口写入当前登录用户自己的 job 目录。文生图会创建空白 creative job 后生成图片,提交 <code>model</code>、<code>size</code> 和 <code>quality</code>;图生图会把参考图片作为 edit 输入并沿用同一套规格。后端返回的 <code>GeneratedImage</code> 带请求尺寸、画质和实际 <code>width×height</code>,画布图片节点据此显示真实像素。图生视频会把上传图转成 frame 并作为视频参考图提交,提交视频后用 <code>skg:{jobId}:{videoId}</code> 作为画布侧任务 id 轮询 <code>/jobs/{id}</code>,直到视频状态完成或失败。视频任务最终 prompt 会条件说明参考图人物是 AI 生成的虚拟角色,避免员工上传的 AI 人像素材被当成真实肖像处理。</td></tr>
|
<tr><td>画布生成</td><td><code>POST /creative/jobs/image</code><br><code>POST /jobs/{id}/frames/upload</code><br><code>POST /jobs/{id}/frames/{idx}/generate</code><br><code>POST /jobs/{id}/frames/{idx}/storyboard/video</code><br><code>GET /jobs/{id}</code></td><td><code>web/canvas-app/src/hooks/useApi.js</code></td><td>画布项目结构保存在 <code>/canvas-projects</code>;一旦生成图片或视频,就通过同一套 creative job / frame / storyboard video 接口写入当前登录用户自己的 job 目录。文生图会创建空白 creative job 后生成图片;图生视频会把上传图转成 frame 并作为视频参考图提交,提交视频后用 <code>skg:{jobId}:{videoId}</code> 作为画布侧任务 id 轮询 <code>/jobs/{id}</code>,直到视频状态完成或失败。视频任务最终 prompt 会条件说明参考图人物是 AI 生成的虚拟角色,避免员工上传的 AI 人像素材被当成真实肖像处理。</td></tr>
|
||||||
<tr><td>AI 润色 / LLM 节点</td><td><code>POST /prompt/polish</code></td><td><code>web/canvas-app/src/hooks/useApi.js</code><br><code>web/canvas-app/src/api/chat.js</code></td><td>中性的提示词润色和通用文本生成接口。根画布和文本节点传 <code>mode=image</code>、默认输出英文提示词;LLM 节点和自动执行意图分析传 <code>mode=chat</code>、保持输入语言。接口会清掉上一次润色遗留的模板尾巴,只保留用户明确写出的主体、品牌、产品、地点、平台、风格和意图;用户没写 <code>SKG</code> 时绝不主动加入 SKG,也不主动补产品、平台、广告语境、slogan 或 hashtag。人物安全词按输入条件加入:原文明确有人像、模特、角色、数字人等语义时才声明“虚构 AI 角色、非真人、非公众人物”;原文明确无人时才保留无人物约束;原文没写人时不主动造人,也不主动追加无人物禁令;输入提到参考图、首帧或尾帧时,提示词只条件保留已有可见人物,不凭空新增人物。</td></tr>
|
<tr><td>AI 润色 / LLM 节点</td><td><code>POST /prompt/polish</code></td><td><code>web/canvas-app/src/hooks/useApi.js</code><br><code>web/canvas-app/src/api/chat.js</code></td><td>中性的提示词润色和通用文本生成接口。根画布和文本节点传 <code>mode=image</code>、默认输出英文提示词;LLM 节点和自动执行意图分析传 <code>mode=chat</code>、保持输入语言。接口会清掉上一次润色遗留的模板尾巴,只保留用户明确写出的主体、品牌、产品、地点、平台、风格和意图;用户没写 <code>SKG</code> 时绝不主动加入 SKG,也不主动补产品、平台、广告语境、slogan 或 hashtag。人物安全词按输入条件加入:原文明确有人像、模特、角色、数字人等语义时才声明“虚构 AI 角色、非真人、非公众人物”;原文明确无人时才保留无人物约束;原文没写人时不主动造人,也不主动追加无人物禁令;输入提到参考图、首帧或尾帧时,提示词只条件保留已有可见人物,不凭空新增人物。</td></tr>
|
||||||
<tr><td>一键出片终端</td><td><code>POST /agent-runs</code><br><code>GET /agent-runs</code><br><code>GET /agent-runs/{id}</code><br><code>GET /agent-runs/{id}/final.mp4</code><br><code>GET /agent-runs/{id}/contact.jpg</code></td><td><code>web/app/agent/page.tsx</code></td><td>快速出片页的唯一主接口。前端提交 TikTok 链接和最多 6 张产品图;后端创建同 owner 的 <code>Job</code> 与 <code>AgentRun</code>,后台执行下载、产品图归一化、透明骨架主体参考复制、12 段镜头计划、视频生成、失败镜头自动重跑一次、审片接触表和 ffmpeg 最终合成。列表、详情、最终 mp4 和接触表同样按 owner 隔离。</td></tr>
|
<tr><td>一键出片终端</td><td><code>POST /agent-runs</code><br><code>GET /agent-runs</code><br><code>GET /agent-runs/{id}</code><br><code>GET /agent-runs/{id}/final.mp4</code><br><code>GET /agent-runs/{id}/contact.jpg</code></td><td><code>web/app/agent/page.tsx</code></td><td>快速出片页的唯一主接口。前端提交 TikTok 链接和最多 6 张产品图;后端创建同 owner 的 <code>Job</code> 与 <code>AgentRun</code>,后台执行下载、产品图归一化、透明骨架主体参考复制、12 段镜头计划、视频生成、失败镜头自动重跑一次、审片接触表和 ffmpeg 最终合成。列表、详情、最终 mp4 和接触表同样按 owner 隔离。</td></tr>
|
||||||
<tr><td>重试下载</td><td><code>POST /jobs/{id}/download/retry</code></td><td><code>retryJobDownload</code></td><td>用于 TK 链接下载失败且没有 <code>video_url</code> 的素材;清空错误、重新进入下载状态,并在后台再次执行 <code>pipeline_download</code>。上传视频不能重下载,需要重新上传文件。</td></tr>
|
<tr><td>重试下载</td><td><code>POST /jobs/{id}/download/retry</code></td><td><code>retryJobDownload</code></td><td>用于 TK 链接下载失败且没有 <code>video_url</code> 的素材;清空错误、重新进入下载状态,并在后台再次执行 <code>pipeline_download</code>。上传视频不能重下载,需要重新上传文件。</td></tr>
|
||||||
@@ -1160,7 +1150,7 @@ ProductRefStateItem {
|
|||||||
<tr><td>AI 改文案</td><td><code>POST /jobs/{job_id}/frames/{idx}/storyboard/refine</code></td><td><code>refineStoryboard</code></td><td>输入当前三字段和中文反馈,返回新的三字段中英镜像。前端必须先弹改前/改后预览,用户点应用后才写入行状态。</td></tr>
|
<tr><td>AI 改文案</td><td><code>POST /jobs/{job_id}/frames/{idx}/storyboard/refine</code></td><td><code>refineStoryboard</code></td><td>输入当前三字段和中文反馈,返回新的三字段中英镜像。前端必须先弹改前/改后预览,用户点应用后才写入行状态。</td></tr>
|
||||||
<tr><td>单条视频候选生成</td><td><code>POST /jobs/{job_id}/frames/{idx}/storyboard/video</code></td><td><code>generateStoryboardVideo</code></td><td>新增 <code>count</code>、<code>seed</code> 和 <code>storyboard_row_idx</code>,默认一次创建 4 个 <code>GeneratedVideo</code> 任务并立即返回 job;每个候选独立排队、生成、失败或成功。前端提交 prompt 前用 quick-plan 展开,高级首尾帧存在时继续带上,不存在时后端用参考帧/主体图/产品图透明兜底。最终提交给视频模型前,后端会为参考图追加 AI 虚拟角色条件提示:参考图若包含人物、脸、身体、手、头像或角色,就按虚构 AI 角色处理,不按真人或公众人物处理。视频候选显示必须优先按 <code>storyboard_row_idx</code> 归属到音频分镜行,而不是只按 <code>frame_idx</code>。</td></tr>
|
<tr><td>单条视频候选生成</td><td><code>POST /jobs/{job_id}/frames/{idx}/storyboard/video</code></td><td><code>generateStoryboardVideo</code></td><td>新增 <code>count</code>、<code>seed</code> 和 <code>storyboard_row_idx</code>,默认一次创建 4 个 <code>GeneratedVideo</code> 任务并立即返回 job;每个候选独立排队、生成、失败或成功。前端提交 prompt 前用 quick-plan 展开,高级首尾帧存在时继续带上,不存在时后端用参考帧/主体图/产品图透明兜底。最终提交给视频模型前,后端会为参考图追加 AI 虚拟角色条件提示:参考图若包含人物、脸、身体、手、头像或角色,就按虚构 AI 角色处理,不按真人或公众人物处理。视频候选显示必须优先按 <code>storyboard_row_idx</code> 归属到音频分镜行,而不是只按 <code>frame_idx</code>。</td></tr>
|
||||||
<tr><td>整片一键生成候选</td><td><code>POST /jobs/{job_id}/storyboard/batch-generate-all</code></td><td>当前主路径改为逐行调用 <code>generateStoryboardVideo</code></td><td>用户选择“每行 N 条”后,前端按音频分镜逐行提交,确保每个候选都带 <code>storyboard_row_idx</code>。后端批量接口保留为兼容能力,默认 <code>concurrency=1</code>,但当前 UI 不再用它做主路径。</td></tr>
|
<tr><td>整片一键生成候选</td><td><code>POST /jobs/{job_id}/storyboard/batch-generate-all</code></td><td>当前主路径改为逐行调用 <code>generateStoryboardVideo</code></td><td>用户选择“每行 N 条”后,前端按音频分镜逐行提交,确保每个候选都带 <code>storyboard_row_idx</code>。后端批量接口保留为兼容能力,默认 <code>concurrency=1</code>,但当前 UI 不再用它做主路径。</td></tr>
|
||||||
<tr><td>生图</td><td><code>POST /frames/{idx}/generate</code></td><td><code>generateImage</code></td><td>基于关键帧、已选生成图或空白 creative job 做图生图 / 文生图;请求体支持 <code>model</code>、<code>size</code>、<code>quality</code>,返回结果写入请求规格和实际输出像素。</td></tr>
|
<tr><td>生图</td><td><code>POST /frames/{idx}/generate</code></td><td><code>generateImage</code></td><td>基于关键帧或已选生成图做 image-to-image,目前可用。</td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</section>
|
</section>
|
||||||
@@ -1277,19 +1267,6 @@ ProductRefStateItem {
|
|||||||
<h2>变更记录</h2>
|
<h2>变更记录</h2>
|
||||||
<p>这个记录不是 git log 的替代品。它记录“产品理解发生了什么变化、影响了哪些源码、你以后描述需求时该怎么说”。后续每次改功能都要补一条。</p>
|
<p>这个记录不是 git log 的替代品。它记录“产品理解发生了什么变化、影响了哪些源码、你以后描述需求时该怎么说”。后续每次改功能都要补一条。</p>
|
||||||
<div class="changelog">
|
<div class="changelog">
|
||||||
<article class="change">
|
|
||||||
<header>
|
|
||||||
<h3>2026-05-26 · 生图配置显示真实尺寸和中高低画质</h3>
|
|
||||||
<span class="tag blue">Backend</span>
|
|
||||||
<span class="tag violet">Canvas</span>
|
|
||||||
<span class="tag green">Data</span>
|
|
||||||
</header>
|
|
||||||
<div class="body">
|
|
||||||
<p><strong>问题:</strong>画布生图配置只显示“标准画质”和少量比例化尺寸,员工无法在生成前判断实际会出多大的图;生成后图片节点也只显示模型名,不显示请求尺寸或实际像素。</p>
|
|
||||||
<p><strong>改动:</strong><code>api/main.py</code> 扩展 <code>IMAGE_SIZE_CHOICES</code>,新增带 <code>width</code>、<code>height</code>、<code>ratio</code> 的 GPT 像素预设和自定义 <code>宽x高</code> 校验;同时新增 <code>GEMINI_IMAGE_SIZE_CHOICES</code>,按 Gemini 官方比例和 1K / 2K / 4K 固定规格列出可选尺寸,后端会拒绝 Gemini 的任意自定义尺寸。<code>GenerateReq</code> 新增 <code>quality</code>,支持 GPT 的 <code>low / medium / high</code>,并把 <code>GeneratedImage.size</code>、<code>quality</code>、<code>width</code>、<code>height</code> 写回。<code>ImageConfigNode.vue</code> 的尺寸下拉改为“比例 · 像素”标签;GPT 显示自定义尺寸输入,Gemini 不显示自定义尺寸输入;<code>ImageNode.vue</code> 显示模型、画质和实际输出像素。</p>
|
|
||||||
<p><strong>影响:</strong>生图人员能在提交前看到真实像素规格;GPT 可按自定义尺寸提交,Gemini 只能选官方固定像素规格。画质切换不会再重置尺寸。Gemini 仍通过当前 SKG 兼容网关提交,最终仍以返回图片的实际像素为准。</p>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
<article class="change">
|
<article class="change">
|
||||||
<header>
|
<header>
|
||||||
<h3>2026-05-26 · AI 润色改为意图分类和冲突校验</h3>
|
<h3>2026-05-26 · AI 润色改为意图分类和冲突校验</h3>
|
||||||
@@ -1414,6 +1391,19 @@ ProductRefStateItem {
|
|||||||
<p><strong>影响:</strong>画布项目开始具备跨浏览器、跨设备恢复的服务端主存储;默认仍按 owner 私有隔离,后续可在同一表上扩展 team/company 可见性。完整 job state 和媒体文件仍保留在原有文件目录,避免把大文件一次性搬进数据库。</p>
|
<p><strong>影响:</strong>画布项目开始具备跨浏览器、跨设备恢复的服务端主存储;默认仍按 owner 私有隔离,后续可在同一表上扩展 team/company 可见性。完整 job state 和媒体文件仍保留在原有文件目录,避免把大文件一次性搬进数据库。</p>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
<article class="change">
|
||||||
|
<header>
|
||||||
|
<h3>2026-05-26 · 恢复最初生图配置</h3>
|
||||||
|
<span class="tag amber">API</span>
|
||||||
|
<span class="tag rose">UI</span>
|
||||||
|
<span class="tag blue">Docs</span>
|
||||||
|
</header>
|
||||||
|
<div class="body">
|
||||||
|
<p><strong>问题:</strong>连续加入真实像素、低/中/高画质、Gemini 官方 1K/2K/4K 尺寸和显式模型选择后,生图配置变复杂,员工容易选到不稳定或不符合预期的组合。</p>
|
||||||
|
<p><strong>改动:</strong>按用户要求恢复到最初简单版:<code>api/main.py</code> 只返回四个图片尺寸 <code>auto</code>、<code>1024x1536</code>、<code>1024x1024</code>、<code>1536x1024</code>,<code>GenerateReq</code> 不再接收画质字段;<code>web/canvas-app/src/config/models.js</code> 恢复 <code>auto</code>、<code>gpt-image-2</code>、<code>gemini-3-pro-image-preview</code> 三个图片模型和单一“标准”画质。</p>
|
||||||
|
<p><strong>影响:</strong><code>auto</code> 图片模型重新启用后端既有策略:优先 GPT Image 2,遇到超时、限流或上游异常时可按熔断/兜底策略走 Gemini;不再显示自定义尺寸输入,也不再展示 Gemini 1K/2K/4K 长列表。</p>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
<article class="change">
|
<article class="change">
|
||||||
<header>
|
<header>
|
||||||
<h3>2026-05-25 · 媒体模型选择对齐真实后端能力</h3>
|
<h3>2026-05-25 · 媒体模型选择对齐真实后端能力</h3>
|
||||||
|
|||||||
@@ -101,11 +101,10 @@ function isVideoMode(mode: CreationMode) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_IMAGE_SIZE_OPTIONS: RuntimeSizeOption[] = [
|
const DEFAULT_IMAGE_SIZE_OPTIONS: RuntimeSizeOption[] = [
|
||||||
{ id: "1024x1536", label: "竖图 2:3 · 1024×1536", value: "1024x1536" },
|
{ id: "1024x1536", label: "竖图 2:3", value: "1024x1536" },
|
||||||
{ id: "1088x1920", label: "竖屏 9:16 · 1088×1920", value: "1088x1920" },
|
{ id: "1024x1024", label: "方图 1:1", value: "1024x1024" },
|
||||||
{ id: "1024x1024", label: "方图 1:1 · 1024×1024", value: "1024x1024" },
|
{ id: "1536x1024", label: "横图 3:2", value: "1536x1024" },
|
||||||
{ id: "1536x1024", label: "横图 3:2 · 1536×1024", value: "1536x1024" },
|
{ id: "auto", label: "自动", value: "auto" },
|
||||||
{ id: "auto", label: "自动 · 生成后显示实际像素", value: "auto" },
|
|
||||||
]
|
]
|
||||||
|
|
||||||
const DEFAULT_VIDEO_SIZE_OPTIONS: RuntimeSizeOption[] = [
|
const DEFAULT_VIDEO_SIZE_OPTIONS: RuntimeSizeOption[] = [
|
||||||
|
|||||||
@@ -61,35 +61,18 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Size selector | 尺寸选择 -->
|
<!-- Size selector | 尺寸选择 -->
|
||||||
<div v-if="hasSizeOptions" class="space-y-1.5">
|
<div v-if="hasSizeOptions" class="flex items-center justify-between">
|
||||||
<div class="flex items-center justify-between gap-3">
|
<span class="text-xs text-[var(--text-secondary)]">尺寸</span>
|
||||||
<span class="text-xs text-[var(--text-secondary)] flex-shrink-0">尺寸</span>
|
<div class="flex items-center gap-2">
|
||||||
<div class="flex items-center gap-2 min-w-0">
|
<n-dropdown :options="sizeOptions" @select="handleSizeSelect">
|
||||||
<n-dropdown :options="sizeOptions" @select="handleSizeSelect">
|
<button
|
||||||
<button
|
class="flex items-center gap-1 text-sm text-[var(--text-primary)] hover:text-[var(--accent-color)]">
|
||||||
class="flex items-center justify-end gap-1 text-sm text-right text-[var(--text-primary)] hover:text-[var(--accent-color)]">
|
{{ displaySize }}
|
||||||
{{ displaySize }}
|
<n-icon :size="12">
|
||||||
<n-icon :size="12">
|
<ChevronForwardOutline />
|
||||||
<ChevronForwardOutline />
|
</n-icon>
|
||||||
</n-icon>
|
</button>
|
||||||
</button>
|
</n-dropdown>
|
||||||
</n-dropdown>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="supportsCustomSize" class="flex items-center gap-1.5">
|
|
||||||
<input
|
|
||||||
v-model="customSizeInput"
|
|
||||||
@keydown.enter.prevent="applyCustomSize"
|
|
||||||
class="flex-1 min-w-0 rounded-md border border-[var(--border-color)] bg-[var(--bg-primary)] px-2 py-1 text-xs text-[var(--text-primary)] outline-none focus:border-[var(--accent-color)]"
|
|
||||||
placeholder="自定义 1088x1920"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
@click="applyCustomSize"
|
|
||||||
class="flex-shrink-0 rounded-md border border-[var(--border-color)] px-2 py-1 text-xs text-[var(--text-secondary)] hover:border-[var(--accent-color)] hover:text-[var(--accent-color)]"
|
|
||||||
>应用</button>
|
|
||||||
</div>
|
|
||||||
<div v-if="customSizeError" class="text-[10px] leading-tight text-red-500">
|
|
||||||
{{ customSizeError }}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -183,7 +166,7 @@ import { useImageGeneration } from '../../hooks'
|
|||||||
import { updateNode, addNode, addEdge, nodes, edges, duplicateNode, removeNode } from '../../stores/canvas'
|
import { updateNode, addNode, addEdge, nodes, edges, duplicateNode, removeNode } from '../../stores/canvas'
|
||||||
import NodeHandleMenu from './NodeHandleMenu.vue'
|
import NodeHandleMenu from './NodeHandleMenu.vue'
|
||||||
import { useModelStore } from '../../stores/pinia'
|
import { useModelStore } from '../../stores/pinia'
|
||||||
import { getModelSizeOptions, getModelQualityOptions, getModelConfig, DEFAULT_IMAGE_MODEL, DEFAULT_IMAGE_SIZE, DEFAULT_IMAGE_QUALITY } from '../../stores/models'
|
import { getModelSizeOptions, getModelQualityOptions, getModelConfig, DEFAULT_IMAGE_MODEL, DEFAULT_IMAGE_SIZE } from '../../stores/models'
|
||||||
import { parseMentions } from '../../hooks/useNodeRef'
|
import { parseMentions } from '../../hooks/useNodeRef'
|
||||||
|
|
||||||
// 使用 Pinia store 获取模型选项(根据渠道过滤)
|
// 使用 Pinia store 获取模型选项(根据渠道过滤)
|
||||||
@@ -204,16 +187,10 @@ const isConfigured = computed(() => !!modelStore.currentApiKey)
|
|||||||
const { loading, error, images: generatedImages, generate } = useImageGeneration()
|
const { loading, error, images: generatedImages, generate } = useImageGeneration()
|
||||||
|
|
||||||
// Local state | 本地状态
|
// Local state | 本地状态
|
||||||
const normalizeQualityKey = (quality) => {
|
|
||||||
if (quality === 'standard' || quality === 'hd') return 'high'
|
|
||||||
return quality || DEFAULT_IMAGE_QUALITY
|
|
||||||
}
|
|
||||||
const showHandleMenu = ref(false)
|
const showHandleMenu = ref(false)
|
||||||
const localModel = ref(props.data?.model || DEFAULT_IMAGE_MODEL)
|
const localModel = ref(props.data?.model || DEFAULT_IMAGE_MODEL)
|
||||||
const localSize = ref(props.data?.size || DEFAULT_IMAGE_SIZE)
|
const localSize = ref(props.data?.size || DEFAULT_IMAGE_SIZE)
|
||||||
const localQuality = ref(normalizeQualityKey(props.data?.quality))
|
const localQuality = ref(props.data?.quality || 'standard')
|
||||||
const customSizeInput = ref(localSize.value)
|
|
||||||
const customSizeError = ref('')
|
|
||||||
|
|
||||||
// Label editing state | Label 编辑状态
|
// Label editing state | Label 编辑状态
|
||||||
const isEditingLabel = ref(false)
|
const isEditingLabel = ref(false)
|
||||||
@@ -283,7 +260,7 @@ const hasQualityOptions = computed(() => {
|
|||||||
// Display quality | 显示画质
|
// Display quality | 显示画质
|
||||||
const displayQuality = computed(() => {
|
const displayQuality = computed(() => {
|
||||||
const option = qualityOptions.value.find(o => o.key === localQuality.value)
|
const option = qualityOptions.value.find(o => o.key === localQuality.value)
|
||||||
return option?.label || '高 · 最终稿'
|
return option?.label || '标准画质'
|
||||||
})
|
})
|
||||||
|
|
||||||
// Size options based on model and quality | 基于模型和画质的尺寸选项
|
// Size options based on model and quality | 基于模型和画质的尺寸选项
|
||||||
@@ -297,44 +274,12 @@ const hasSizeOptions = computed(() => {
|
|||||||
return config?.sizes && config.sizes.length > 0
|
return config?.sizes && config.sizes.length > 0
|
||||||
})
|
})
|
||||||
|
|
||||||
const supportsCustomSize = computed(() => {
|
|
||||||
const config = getModelConfig(localModel.value)
|
|
||||||
return config?.supportsCustomSize !== false
|
|
||||||
})
|
|
||||||
|
|
||||||
// Display size with label | 显示尺寸(带标签)
|
// Display size with label | 显示尺寸(带标签)
|
||||||
const displaySize = computed(() => {
|
const displaySize = computed(() => {
|
||||||
const option = sizeOptions.value.find(o => o.key === localSize.value)
|
const option = sizeOptions.value.find(o => o.key === localSize.value)
|
||||||
return option?.label || formatSizeLabel(localSize.value)
|
return option?.label || localSize.value
|
||||||
})
|
})
|
||||||
|
|
||||||
const parseImageSize = (value) => {
|
|
||||||
const normalized = String(value || '').trim().toLowerCase().replace('×', 'x')
|
|
||||||
const match = normalized.match(/^(\d{3,4})\s*x\s*(\d{3,4})$/)
|
|
||||||
if (!match) return null
|
|
||||||
return { width: Number(match[1]), height: Number(match[2]), key: `${Number(match[1])}x${Number(match[2])}` }
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatSizeLabel = (value) => {
|
|
||||||
if (value === 'auto') return '自动 · 生成后显示实际像素'
|
|
||||||
const parsed = parseImageSize(value)
|
|
||||||
return parsed ? `自定义 · ${parsed.width}×${parsed.height}` : value
|
|
||||||
}
|
|
||||||
|
|
||||||
const validateImageSize = (value) => {
|
|
||||||
const parsed = parseImageSize(value)
|
|
||||||
if (!parsed) return { ok: false, message: '格式用 1088x1920' }
|
|
||||||
const { width, height } = parsed
|
|
||||||
const pixels = width * height
|
|
||||||
const longEdge = Math.max(width, height)
|
|
||||||
const shortEdge = Math.min(width, height)
|
|
||||||
if (width % 16 !== 0 || height % 16 !== 0) return { ok: false, message: '宽高需为 16 的倍数' }
|
|
||||||
if (longEdge > 3840) return { ok: false, message: '最长边不能超过 3840px' }
|
|
||||||
if (longEdge / shortEdge > 3) return { ok: false, message: '比例不能超过 3:1' }
|
|
||||||
if (pixels < 655360 || pixels > 8294400) return { ok: false, message: '总像素超出模型范围' }
|
|
||||||
return { ok: true, key: parsed.key }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize on mount | 挂载时初始化
|
// Initialize on mount | 挂载时初始化
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// 检查当前模型是否在可用模型列表中
|
// 检查当前模型是否在可用模型列表中
|
||||||
@@ -347,19 +292,6 @@ onMounted(() => {
|
|||||||
localModel.value = selected || availableModels[0]?.key || DEFAULT_IMAGE_MODEL
|
localModel.value = selected || availableModels[0]?.key || DEFAULT_IMAGE_MODEL
|
||||||
updateNode(props.id, { model: localModel.value })
|
updateNode(props.id, { model: localModel.value })
|
||||||
}
|
}
|
||||||
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)并收集图片
|
// 解析 textNode 内容中的 @ 引用,转换为简短引用(如 图 1)并收集图片
|
||||||
@@ -561,8 +493,6 @@ const handleModelSelect = (key) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
localSize.value = defaultSize
|
localSize.value = defaultSize
|
||||||
customSizeInput.value = defaultSize === 'auto' ? '' : defaultSize
|
|
||||||
customSizeError.value = ''
|
|
||||||
|
|
||||||
// 更新节点数据
|
// 更新节点数据
|
||||||
updateNode(props.id, {
|
updateNode(props.id, {
|
||||||
@@ -575,14 +505,20 @@ const handleModelSelect = (key) => {
|
|||||||
// Handle quality selection | 处理画质选择
|
// Handle quality selection | 处理画质选择
|
||||||
const handleQualitySelect = (quality) => {
|
const handleQualitySelect = (quality) => {
|
||||||
localQuality.value = quality
|
localQuality.value = quality
|
||||||
updateNode(props.id, { quality })
|
// Update size to first option of new quality | 更新尺寸为新画质的第一个选项
|
||||||
|
const newSizeOptions = getModelSizeOptions(localModel.value, quality)
|
||||||
|
if (newSizeOptions.length > 0) {
|
||||||
|
const defaultSize = newSizeOptions.find(o => o.key === DEFAULT_IMAGE_SIZE)?.key
|
||||||
|
localSize.value = defaultSize || newSizeOptions[0].key
|
||||||
|
updateNode(props.id, { quality, size: localSize.value })
|
||||||
|
} else {
|
||||||
|
updateNode(props.id, { quality })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle size selection | 处理尺寸选择
|
// Handle size selection | 处理尺寸选择
|
||||||
const handleSizeSelect = (size) => {
|
const handleSizeSelect = (size) => {
|
||||||
localSize.value = size
|
localSize.value = size
|
||||||
customSizeInput.value = size === 'auto' ? '' : size
|
|
||||||
customSizeError.value = ''
|
|
||||||
updateNode(props.id, { size })
|
updateNode(props.id, { size })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -591,19 +527,6 @@ const updateSize = () => {
|
|||||||
updateNode(props.id, { size: localSize.value })
|
updateNode(props.id, { size: localSize.value })
|
||||||
}
|
}
|
||||||
|
|
||||||
const applyCustomSize = () => {
|
|
||||||
if (!supportsCustomSize.value) return
|
|
||||||
const result = validateImageSize(customSizeInput.value)
|
|
||||||
if (!result.ok) {
|
|
||||||
customSizeError.value = result.message
|
|
||||||
return
|
|
||||||
}
|
|
||||||
customSizeError.value = ''
|
|
||||||
localSize.value = result.key
|
|
||||||
customSizeInput.value = result.key
|
|
||||||
updateNode(props.id, { size: result.key })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Created image node ID | 创建的图片节点 ID
|
// Created image node ID | 创建的图片节点 ID
|
||||||
const createdImageNodeId = ref(null)
|
const createdImageNodeId = ref(null)
|
||||||
|
|
||||||
@@ -673,16 +596,7 @@ const handleGenerate = async (mode = 'auto') => {
|
|||||||
// Replace mode: find any connected image node | 替换模式:查找任意连接的图片节点
|
// Replace mode: find any connected image node | 替换模式:查找任意连接的图片节点
|
||||||
imageNodeId = findConnectedOutputImageNode(false)
|
imageNodeId = findConnectedOutputImageNode(false)
|
||||||
if (imageNodeId) {
|
if (imageNodeId) {
|
||||||
updateNode(imageNodeId, {
|
updateNode(imageNodeId, { loading: true, url: '' })
|
||||||
loading: true,
|
|
||||||
url: '',
|
|
||||||
error: '',
|
|
||||||
model: localModel.value,
|
|
||||||
size: localSize.value,
|
|
||||||
quality: localQuality.value,
|
|
||||||
width: 0,
|
|
||||||
height: 0
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
} else if (mode === 'new') {
|
} else if (mode === 'new') {
|
||||||
// New mode: always create new node | 新建模式:始终创建新节点
|
// New mode: always create new node | 新建模式:始终创建新节点
|
||||||
@@ -691,15 +605,7 @@ const handleGenerate = async (mode = 'auto') => {
|
|||||||
// Auto mode: check for empty connected node first | 自动模式:先检查空白连接节点
|
// Auto mode: check for empty connected node first | 自动模式:先检查空白连接节点
|
||||||
imageNodeId = findConnectedOutputImageNode(true)
|
imageNodeId = findConnectedOutputImageNode(true)
|
||||||
if (imageNodeId) {
|
if (imageNodeId) {
|
||||||
updateNode(imageNodeId, {
|
updateNode(imageNodeId, { loading: true })
|
||||||
loading: true,
|
|
||||||
error: '',
|
|
||||||
model: localModel.value,
|
|
||||||
size: localSize.value,
|
|
||||||
quality: localQuality.value,
|
|
||||||
width: 0,
|
|
||||||
height: 0
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -720,10 +626,7 @@ const handleGenerate = async (mode = 'auto') => {
|
|||||||
imageNodeId = addNode('image', { x: nodeX + 400, y: nodeY + yOffset }, {
|
imageNodeId = addNode('image', { x: nodeX + 400, y: nodeY + yOffset }, {
|
||||||
url: '',
|
url: '',
|
||||||
loading: true,
|
loading: true,
|
||||||
label: '图像生成结果',
|
label: '图像生成结果'
|
||||||
model: localModel.value,
|
|
||||||
size: localSize.value,
|
|
||||||
quality: localQuality.value
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Auto-connect imageConfig → image | 自动连接 生图配置 → 图片
|
// Auto-connect imageConfig → image | 自动连接 生图配置 → 图片
|
||||||
@@ -765,11 +668,7 @@ const handleGenerate = async (mode = 'auto') => {
|
|||||||
url: result[0].url,
|
url: result[0].url,
|
||||||
loading: false,
|
loading: false,
|
||||||
label: '文生图',
|
label: '文生图',
|
||||||
model: result[0].model || localModel.value,
|
model: localModel.value,
|
||||||
size: result[0].size || localSize.value,
|
|
||||||
quality: result[0].quality || localQuality.value,
|
|
||||||
width: result[0].width || 0,
|
|
||||||
height: result[0].height || 0,
|
|
||||||
updatedAt: Date.now()
|
updatedAt: Date.now()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -843,25 +742,10 @@ watch(() => props.data?.model, (newModel) => {
|
|||||||
// 同步 Size
|
// 同步 Size
|
||||||
if (config?.defaultParams?.size) {
|
if (config?.defaultParams?.size) {
|
||||||
localSize.value = config.defaultParams.size
|
localSize.value = config.defaultParams.size
|
||||||
customSizeInput.value = localSize.value === 'auto' ? '' : localSize.value
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(() => props.data?.size, (newSize) => {
|
|
||||||
if (newSize && newSize !== localSize.value) {
|
|
||||||
localSize.value = newSize
|
|
||||||
customSizeInput.value = newSize === 'auto' ? '' : newSize
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(() => props.data?.quality, (newQuality) => {
|
|
||||||
const normalized = normalizeQualityKey(newQuality)
|
|
||||||
if (normalized !== localQuality.value) {
|
|
||||||
localQuality.value = normalized
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 修复 Vue Flow visibility: hidden 问题
|
// 修复 Vue Flow visibility: hidden 问题
|
||||||
watch(() => props.data, () => {
|
watch(() => props.data, () => {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
|
|||||||
@@ -95,9 +95,9 @@
|
|||||||
</n-tooltip>
|
</n-tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Model and output metadata | 模型与输出规格 -->
|
<!-- Model name | 模型名称 -->
|
||||||
<div v-if="imageMetaText" class="mt-1 text-xs text-[var(--text-secondary)] truncate">
|
<div v-if="data.model" class="mt-1 text-xs text-[var(--text-secondary)] truncate">
|
||||||
{{ imageMetaText }}
|
{{ data.model }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -371,21 +371,6 @@ const isPublic = computed(() => {
|
|||||||
return props.data?.publicProps?.name != null && props.data?.publicProps?.name !== ''
|
return props.data?.publicProps?.name != null && props.data?.publicProps?.name !== ''
|
||||||
})
|
})
|
||||||
|
|
||||||
const imageMetaText = computed(() => {
|
|
||||||
const parts = []
|
|
||||||
if (props.data?.model) parts.push(props.data.model)
|
|
||||||
if (props.data?.quality) {
|
|
||||||
const qualityMap = { low: '低', medium: '中', high: '高', standard: '高', hd: '高' }
|
|
||||||
parts.push(`${qualityMap[props.data.quality] || props.data.quality}画质`)
|
|
||||||
}
|
|
||||||
if (props.data?.width && props.data?.height) {
|
|
||||||
parts.push(`${props.data.width}×${props.data.height}`)
|
|
||||||
} else if (props.data?.size) {
|
|
||||||
parts.push(String(props.data.size).replace('x', '×'))
|
|
||||||
}
|
|
||||||
return parts.join(' · ')
|
|
||||||
})
|
|
||||||
|
|
||||||
// Handle toggle public | 处理切换公开状态
|
// Handle toggle public | 处理切换公开状态
|
||||||
const handleTogglePublic = (value) => {
|
const handleTogglePublic = (value) => {
|
||||||
if (value) {
|
if (value) {
|
||||||
@@ -434,7 +419,6 @@ const handleSelect = (item) => {
|
|||||||
const configNodeId = addNode('imageConfig', { x: nodeX + 900, y: nodeY }, {
|
const configNodeId = addNode('imageConfig', { x: nodeX + 900, y: nodeY }, {
|
||||||
model: 'auto',
|
model: 'auto',
|
||||||
size: '1024x1536',
|
size: '1024x1536',
|
||||||
quality: 'high',
|
|
||||||
label: '生图配置'
|
label: '生图配置'
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -644,7 +628,6 @@ const createInpaintWorkflow = () => {
|
|||||||
const configNodeId = addNode('imageConfig', { x: nodeX + 600, y: nodeY }, {
|
const configNodeId = addNode('imageConfig', { x: nodeX + 600, y: nodeY }, {
|
||||||
model: 'auto',
|
model: 'auto',
|
||||||
size: '1024x1536',
|
size: '1024x1536',
|
||||||
quality: 'high',
|
|
||||||
label: '局部重绘',
|
label: '局部重绘',
|
||||||
inpaintMode: true
|
inpaintMode: true
|
||||||
})
|
})
|
||||||
@@ -867,7 +850,6 @@ const handleImageGen = () => {
|
|||||||
const configNodeId = addNode('imageConfig', { x: nodeX + 900, y: nodeY }, {
|
const configNodeId = addNode('imageConfig', { x: nodeX + 900, y: nodeY }, {
|
||||||
model: 'auto',
|
model: 'auto',
|
||||||
size: '1024x1536',
|
size: '1024x1536',
|
||||||
quality: 'high',
|
|
||||||
label: '生图配置'
|
label: '生图配置'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -417,7 +417,7 @@ const handleSelect = (item) => {
|
|||||||
const nodeY = currentNode?.position?.y || 0
|
const nodeY = currentNode?.position?.y || 0
|
||||||
|
|
||||||
const defaultData = {
|
const defaultData = {
|
||||||
imageConfig: { model: 'auto', size: '1024x1536', quality: 'high', label: '文生图' },
|
imageConfig: { model: 'auto', size: '1024x1536', label: '文生图' },
|
||||||
videoConfig: { label: '视频生成' },
|
videoConfig: { label: '视频生成' },
|
||||||
text: { content: '', label: '文本输入' }
|
text: { content: '', label: '文本输入' }
|
||||||
}
|
}
|
||||||
@@ -1008,8 +1008,7 @@ const handleSplitToTextWithImage = () => {
|
|||||||
data: {
|
data: {
|
||||||
label: `图片 ${i + 1}`,
|
label: `图片 ${i + 1}`,
|
||||||
model: 'auto',
|
model: 'auto',
|
||||||
size: '1024x1536',
|
size: '1024x1536'
|
||||||
quality: 'high'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
nodeSpecs.push(imageConfigSpec)
|
nodeSpecs.push(imageConfigSpec)
|
||||||
|
|||||||
@@ -442,7 +442,7 @@ const handleSelect = (item) => {
|
|||||||
const nodeY = currentNode?.position?.y || 0
|
const nodeY = currentNode?.position?.y || 0
|
||||||
|
|
||||||
const defaultData = {
|
const defaultData = {
|
||||||
imageConfig: { model: 'auto', size: '1024x1536', quality: 'high', label: '文生图' },
|
imageConfig: { model: 'auto', size: '1024x1536', label: '文生图' },
|
||||||
videoConfig: { label: '视频生成' },
|
videoConfig: { label: '视频生成' },
|
||||||
llmConfig: { label: 'LLM文本生成' }
|
llmConfig: { label: 'LLM文本生成' }
|
||||||
}
|
}
|
||||||
@@ -699,7 +699,6 @@ const handleImageGen = () => {
|
|||||||
const configNodeId = addNode('imageConfig', { x: nodeX + 400, y: nodeY }, {
|
const configNodeId = addNode('imageConfig', { x: nodeX + 400, y: nodeY }, {
|
||||||
model: 'auto',
|
model: 'auto',
|
||||||
size: '1024x1536',
|
size: '1024x1536',
|
||||||
quality: 'high',
|
|
||||||
label: '文生图'
|
label: '文生图'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -5,53 +5,18 @@
|
|||||||
|
|
||||||
// SKG backend image size options | SKG 后端图片尺寸选项
|
// SKG backend image size options | SKG 后端图片尺寸选项
|
||||||
export const SEEDREAM_SIZE_OPTIONS = [
|
export const SEEDREAM_SIZE_OPTIONS = [
|
||||||
{ label: '自动 · 生成后显示实际像素', key: 'auto', ratio: 'auto' },
|
{ label: '自动', key: 'auto' },
|
||||||
{ label: '竖图 2:3 · 1024×1536', key: '1024x1536', ratio: '2:3', width: 1024, height: 1536 },
|
{ label: '竖图 2:3', key: '1024x1536' },
|
||||||
{ label: '竖图 2:3 · 1536×2304', key: '1536x2304', ratio: '2:3', width: 1536, height: 2304 },
|
{ label: '方图 1:1', key: '1024x1024' },
|
||||||
{ label: '竖屏 9:16 · 1088×1920', key: '1088x1920', ratio: '9:16', width: 1088, height: 1920 },
|
{ label: '横图 3:2', key: '1536x1024' }
|
||||||
{ label: '竖屏 9:16 · 1440×2560', key: '1440x2560', ratio: '9:16', width: 1440, height: 2560 },
|
|
||||||
{ label: '竖图 3:4 · 960×1280', key: '960x1280', ratio: '3:4', width: 960, height: 1280 },
|
|
||||||
{ label: '竖图 4:5 · 1536×1920', key: '1536x1920', ratio: '4:5', width: 1536, height: 1920 },
|
|
||||||
{ label: '方图 1:1 · 1024×1024', key: '1024x1024', ratio: '1:1', width: 1024, height: 1024 },
|
|
||||||
{ label: '方图 1:1 · 2048×2048', key: '2048x2048', ratio: '1:1', width: 2048, height: 2048 },
|
|
||||||
{ label: '横图 3:2 · 1536×1024', key: '1536x1024', ratio: '3:2', width: 1536, height: 1024 },
|
|
||||||
{ label: '横图 3:2 · 2304×1536', key: '2304x1536', ratio: '3:2', width: 2304, height: 1536 },
|
|
||||||
{ label: '横屏 16:9 · 1280×720', key: '1280x720', ratio: '16:9', width: 1280, height: 720 },
|
|
||||||
{ 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.
|
// Kept for compatibility with upstream model helpers.
|
||||||
export const SEEDREAM_4K_SIZE_OPTIONS = SEEDREAM_SIZE_OPTIONS
|
export const SEEDREAM_4K_SIZE_OPTIONS = SEEDREAM_SIZE_OPTIONS
|
||||||
|
|
||||||
|
// SKG backend currently exposes model choice and size; quality is retained as a no-op UI field.
|
||||||
export const SEEDREAM_QUALITY_OPTIONS = [
|
export const SEEDREAM_QUALITY_OPTIONS = [
|
||||||
{ label: '低 · 快速草稿', key: 'low' },
|
{ label: '标准', key: 'standard' }
|
||||||
{ label: '中 · 常规出图', key: 'medium' },
|
|
||||||
{ label: '高 · 最终稿', key: 'high' }
|
|
||||||
]
|
]
|
||||||
|
|
||||||
export const BANANA_SIZE_OPTIONS = [
|
export const BANANA_SIZE_OPTIONS = [
|
||||||
@@ -71,12 +36,10 @@ export const IMAGE_MODELS = [
|
|||||||
key: 'auto',
|
key: 'auto',
|
||||||
provider: ['chatfire'],
|
provider: ['chatfire'],
|
||||||
sizes: SEEDREAM_SIZE_OPTIONS.map(s => s.key),
|
sizes: SEEDREAM_SIZE_OPTIONS.map(s => s.key),
|
||||||
sizeOptions: SEEDREAM_SIZE_OPTIONS,
|
|
||||||
qualities: SEEDREAM_QUALITY_OPTIONS,
|
qualities: SEEDREAM_QUALITY_OPTIONS,
|
||||||
supportsCustomSize: true,
|
|
||||||
defaultParams: {
|
defaultParams: {
|
||||||
size: '1024x1536',
|
size: '1024x1536',
|
||||||
quality: 'high',
|
quality: 'standard',
|
||||||
style: 'vivid'
|
style: 'vivid'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -85,12 +48,10 @@ export const IMAGE_MODELS = [
|
|||||||
key: 'gpt-image-2',
|
key: 'gpt-image-2',
|
||||||
provider: ['chatfire'],
|
provider: ['chatfire'],
|
||||||
sizes: SEEDREAM_SIZE_OPTIONS.map(s => s.key),
|
sizes: SEEDREAM_SIZE_OPTIONS.map(s => s.key),
|
||||||
sizeOptions: SEEDREAM_SIZE_OPTIONS,
|
|
||||||
qualities: SEEDREAM_QUALITY_OPTIONS,
|
qualities: SEEDREAM_QUALITY_OPTIONS,
|
||||||
supportsCustomSize: true,
|
|
||||||
defaultParams: {
|
defaultParams: {
|
||||||
size: '1024x1536',
|
size: '1024x1536',
|
||||||
quality: 'high',
|
quality: 'standard',
|
||||||
style: 'vivid'
|
style: 'vivid'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -98,13 +59,11 @@ export const IMAGE_MODELS = [
|
|||||||
label: 'Gemini 图片',
|
label: 'Gemini 图片',
|
||||||
key: 'gemini-3-pro-image-preview',
|
key: 'gemini-3-pro-image-preview',
|
||||||
provider: ['chatfire'],
|
provider: ['chatfire'],
|
||||||
sizes: GEMINI_SIZE_OPTIONS.map(s => s.key),
|
sizes: SEEDREAM_SIZE_OPTIONS.map(s => s.key),
|
||||||
sizeOptions: GEMINI_SIZE_OPTIONS,
|
qualities: SEEDREAM_QUALITY_OPTIONS,
|
||||||
qualities: [],
|
|
||||||
supportsCustomSize: false,
|
|
||||||
defaultParams: {
|
defaultParams: {
|
||||||
size: '1024x1024',
|
size: '1024x1536',
|
||||||
quality: 'high',
|
quality: 'standard',
|
||||||
style: 'vivid'
|
style: 'vivid'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -159,12 +118,16 @@ export const CHAT_MODELS = [
|
|||||||
|
|
||||||
// Image size options | 图片尺寸选项
|
// Image size options | 图片尺寸选项
|
||||||
export const IMAGE_SIZE_OPTIONS = [
|
export const IMAGE_SIZE_OPTIONS = [
|
||||||
...SEEDREAM_SIZE_OPTIONS
|
{ label: '自动', key: 'auto' },
|
||||||
|
{ label: '竖图 2:3', key: '1024x1536' },
|
||||||
|
{ label: '方图 1:1', key: '1024x1024' },
|
||||||
|
{ label: '横图 3:2', key: '1536x1024' }
|
||||||
]
|
]
|
||||||
|
|
||||||
// Image quality options | 图片质量选项
|
// Image quality options | 图片质量选项
|
||||||
export const IMAGE_QUALITY_OPTIONS = [
|
export const IMAGE_QUALITY_OPTIONS = [
|
||||||
...SEEDREAM_QUALITY_OPTIONS
|
{ label: '标准', key: 'standard' },
|
||||||
|
{ label: '高清', key: 'hd' }
|
||||||
]
|
]
|
||||||
|
|
||||||
// Image style options | 图片风格选项
|
// Image style options | 图片风格选项
|
||||||
@@ -190,7 +153,6 @@ export const DEFAULT_IMAGE_MODEL = 'auto'
|
|||||||
export const DEFAULT_VIDEO_MODEL = 'seedance'
|
export const DEFAULT_VIDEO_MODEL = 'seedance'
|
||||||
export const DEFAULT_CHAT_MODEL = 'gpt-4o-mini'
|
export const DEFAULT_CHAT_MODEL = 'gpt-4o-mini'
|
||||||
export const DEFAULT_IMAGE_SIZE = '1024x1536'
|
export const DEFAULT_IMAGE_SIZE = '1024x1536'
|
||||||
export const DEFAULT_IMAGE_QUALITY = 'high'
|
|
||||||
export const DEFAULT_VIDEO_RATIO = '720x1280'
|
export const DEFAULT_VIDEO_RATIO = '720x1280'
|
||||||
export const DEFAULT_VIDEO_DURATION = 10
|
export const DEFAULT_VIDEO_DURATION = 10
|
||||||
|
|
||||||
|
|||||||
@@ -192,7 +192,6 @@ export const useImageGeneration = () => {
|
|||||||
prompt: params.prompt || '',
|
prompt: params.prompt || '',
|
||||||
model: params.model || 'auto',
|
model: params.model || 'auto',
|
||||||
size: params.size || '1024x1536',
|
size: params.size || '1024x1536',
|
||||||
quality: params.quality || 'high',
|
|
||||||
mode: firstRef ? 'edit' : 'text'
|
mode: firstRef ? 'edit' : 'text'
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -244,7 +244,7 @@ const getDefaultNodeData = (type) => {
|
|||||||
prompt: '',
|
prompt: '',
|
||||||
model: DEFAULT_IMAGE_MODEL,
|
model: DEFAULT_IMAGE_MODEL,
|
||||||
size: imageModel?.defaultParams?.size || '1x1',
|
size: imageModel?.defaultParams?.size || '1x1',
|
||||||
quality: imageModel?.defaultParams?.quality || 'high',
|
quality: imageModel?.defaultParams?.quality || 'standard',
|
||||||
label: '文生图'
|
label: '文生图'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -402,8 +402,7 @@ export const initSampleData = () => {
|
|||||||
addNode('imageConfig', { x: 450, y: 150 }, {
|
addNode('imageConfig', { x: 450, y: 150 }, {
|
||||||
prompt: '',
|
prompt: '',
|
||||||
model: 'auto',
|
model: 'auto',
|
||||||
size: '1024x1536',
|
ratio: '16:9 | 4张 | 高清',
|
||||||
quality: 'high',
|
|
||||||
label: '文生图'
|
label: '文生图'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import {
|
|||||||
DEFAULT_VIDEO_MODEL,
|
DEFAULT_VIDEO_MODEL,
|
||||||
DEFAULT_CHAT_MODEL,
|
DEFAULT_CHAT_MODEL,
|
||||||
DEFAULT_IMAGE_SIZE,
|
DEFAULT_IMAGE_SIZE,
|
||||||
DEFAULT_IMAGE_QUALITY,
|
|
||||||
DEFAULT_VIDEO_RATIO,
|
DEFAULT_VIDEO_RATIO,
|
||||||
DEFAULT_VIDEO_DURATION
|
DEFAULT_VIDEO_DURATION
|
||||||
} from '@/config/models'
|
} from '@/config/models'
|
||||||
@@ -67,7 +66,7 @@ export const getModelConfig = (modelKey) => {
|
|||||||
* Get size options for image model | 获取图片模型尺寸选项
|
* Get size options for image model | 获取图片模型尺寸选项
|
||||||
* Returns options based on model's sizes array and quality
|
* Returns options based on model's sizes array and quality
|
||||||
*/
|
*/
|
||||||
export const getModelSizeOptions = (modelKey, quality = 'high') => {
|
export const getModelSizeOptions = (modelKey, quality = 'standard') => {
|
||||||
const model = IMAGE_MODELS.find(m => m.key === modelKey)
|
const model = IMAGE_MODELS.find(m => m.key === modelKey)
|
||||||
|
|
||||||
// If model has getSizesByQuality function, use it | 如果模型有 getSizesByQuality 函数,使用它
|
// If model has getSizesByQuality function, use it | 如果模型有 getSizesByQuality 函数,使用它
|
||||||
@@ -78,7 +77,7 @@ export const getModelSizeOptions = (modelKey, quality = 'high') => {
|
|||||||
if (!model?.sizes) return SEEDREAM_SIZE_OPTIONS
|
if (!model?.sizes) return SEEDREAM_SIZE_OPTIONS
|
||||||
|
|
||||||
// Convert sizes array to dropdown options | 转换 sizes 数组为下拉选项
|
// Convert sizes array to dropdown options | 转换 sizes 数组为下拉选项
|
||||||
const sizeOptions = model.sizeOptions || (quality === '4k' ? SEEDREAM_4K_SIZE_OPTIONS : SEEDREAM_SIZE_OPTIONS)
|
const sizeOptions = quality === '4k' ? SEEDREAM_4K_SIZE_OPTIONS : SEEDREAM_SIZE_OPTIONS
|
||||||
return model.sizes.map(size => {
|
return model.sizes.map(size => {
|
||||||
const option = sizeOptions.find(o => o.key === size)
|
const option = sizeOptions.find(o => o.key === size)
|
||||||
return option || { label: size, key: size }
|
return option || { label: size, key: size }
|
||||||
@@ -203,7 +202,6 @@ export {
|
|||||||
DEFAULT_VIDEO_MODEL,
|
DEFAULT_VIDEO_MODEL,
|
||||||
DEFAULT_CHAT_MODEL,
|
DEFAULT_CHAT_MODEL,
|
||||||
DEFAULT_IMAGE_SIZE,
|
DEFAULT_IMAGE_SIZE,
|
||||||
DEFAULT_IMAGE_QUALITY,
|
|
||||||
DEFAULT_VIDEO_RATIO,
|
DEFAULT_VIDEO_RATIO,
|
||||||
DEFAULT_VIDEO_DURATION
|
DEFAULT_VIDEO_DURATION
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -891,9 +891,6 @@ const sendMessage = async () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const imageConfigNodeId = addNode('imageConfig', { x: baseX + 400, y: baseY }, {
|
const imageConfigNodeId = addNode('imageConfig', { x: baseX + 400, y: baseY }, {
|
||||||
model: 'auto',
|
|
||||||
size: '1024x1536',
|
|
||||||
quality: 'high',
|
|
||||||
label: '文生图'
|
label: '文生图'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -42,10 +42,6 @@ export interface GeneratedImage {
|
|||||||
model: string
|
model: string
|
||||||
mode: string
|
mode: string
|
||||||
url: string
|
url: string
|
||||||
size?: string
|
|
||||||
quality?: string
|
|
||||||
width?: number
|
|
||||||
height?: number
|
|
||||||
selected: boolean
|
selected: boolean
|
||||||
created_at: number
|
created_at: number
|
||||||
}
|
}
|
||||||
@@ -273,16 +269,6 @@ export interface RuntimeModelOption {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface RuntimeSizeOption {
|
export interface RuntimeSizeOption {
|
||||||
id: string
|
|
||||||
label: string
|
|
||||||
value: string
|
|
||||||
ratio?: string
|
|
||||||
width?: number
|
|
||||||
height?: number
|
|
||||||
description?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RuntimeQualityOption {
|
|
||||||
id: string
|
id: string
|
||||||
label: string
|
label: string
|
||||||
value: string
|
value: string
|
||||||
@@ -308,7 +294,6 @@ export interface RuntimeModels {
|
|||||||
image_base_url?: string
|
image_base_url?: string
|
||||||
image_options?: RuntimeModelOption[]
|
image_options?: RuntimeModelOption[]
|
||||||
image_size_options?: RuntimeSizeOption[]
|
image_size_options?: RuntimeSizeOption[]
|
||||||
image_quality_options?: RuntimeQualityOption[]
|
|
||||||
image_fallbacks?: string[]
|
image_fallbacks?: string[]
|
||||||
image_circuit?: {
|
image_circuit?: {
|
||||||
primary?: string
|
primary?: string
|
||||||
@@ -1272,7 +1257,7 @@ export async function translateText(text: string, target: "en" | "zh" = "en"): P
|
|||||||
export async function generateImage(
|
export async function generateImage(
|
||||||
jobId: string,
|
jobId: string,
|
||||||
frameIdx: number,
|
frameIdx: number,
|
||||||
body: { prompt: string; extra_prompt?: string; negative_prompt?: string; model?: string; size?: string; quality?: 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<Job> {
|
): Promise<Job> {
|
||||||
const res = await fetch(`${API_BASE}/jobs/${jobId}/frames/${frameIdx}/generate`, {
|
const res = await fetch(`${API_BASE}/jobs/${jobId}/frames/${frameIdx}/generate`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|||||||
Reference in New Issue
Block a user