diff --git a/docs/source-analysis.html b/docs/source-analysis.html
index 2b0cbc4..5455c64 100644
--- a/docs/source-analysis.html
+++ b/docs/source-analysis.html
@@ -894,7 +894,7 @@ ProductRefStateItem {
| 产品图入库到 job | POST /jobs/{id}/assets、POST /jobs/{id}/assets/product-library | uploadStoryboardAsset、copyProductLibraryAsset | 上传产品图或把内置产品图库条目复制为当前 job 的普通 asset。后端统一生成最长边 1600px、JPEG 92 的 AI 工作副本,透明底铺白,过大/过小图片会在 ImageRef.asset_meta 里返回转换动作和风险;黑底/白底背景本身不强行转换。注意该接口只写图片文件,产品素材池列表另由 PUT /jobs/{id}/product-refs 持久化。 |
| 产品素材池保存 | PUT /jobs/{id}/product-refs | saveProductRefs | 把当前 job 的产品素材池列表、识别视角、用途标签、方向、结构点、备注、AI 补图和删除结果保存到 Job.product_refs / state.json。前端上传、识别完成、补角度、编辑备注和删除时都会同步保存;刷新页面或热更新后从 job 恢复,不再要求重新上传和重新识别。 |
| 产品视角识别 | POST /jobs/{id}/assets/product-views/analyze | analyzeProductViews | 读取同一产品素材池,按批次把多张图一次性提交给视觉模型,不限制只看前 6 张;识别对象被固定为套在脖子上的 U 形肩颈按摩仪。返回 view、background、use_tags、orientation、landmarks、中文备注、生成风险和置信度;orientation 明确佩戴者左/右、上/下、内外侧和开口方向对应图中哪边,避免把图片左右误当产品左右。前端不再要求用户手动选择视角,也不做不同产品身份判断。 |
- | 产品缺角度补图 | POST /jobs/{id}/assets/product-angle | generateProductAngleAsset | 用当前产品白底图作为参考,通过图像模型自动补全缺失视角,输出新的 ImageRef(kind="asset")。Prompt 会约束白底产品图、左右非对称、厚度、内侧触点和肩颈真实佩戴比例;前端只在自动补图失败时暴露重试入口。 |
+ | 产品缺角度补图 | POST /jobs/{id}/assets/product-angle | generateProductAngleAsset | 用当前产品白底图作为参考,通过图像模型自动补全缺失视角,输出新的 ImageRef(kind="asset")。Prompt 会约束白底产品图、左右非对称、厚度、内侧触点和肩颈真实佩戴比例;图生图通过 /images/edits multipart 提交参考图,不再把 image 当 JSON 参数塞进 /images/generations;遇到 gpt-image-2 上游 429 / saturated 会按退避节奏重试,最终仍失败时返回 503 和可读提示。前端只在自动补图失败时暴露重试入口。 |
| 角色库 | GET /character-library/skg | listCharacterLibrary | 读取内置 5 个透明骨架人角色 manifest,每个角色含正面、左右 45 度、侧面、背面、半身近景和背部特写 7 张参考图。 |
| 角色图入库到 job | POST /jobs/{id}/assets/character-library | copyCharacterLibraryAssets | 把所选角色的 7 张参考图复制为当前 job asset,返回 subject_images,产品融合生成视频时作为人物身份参考图提交。 |
| 产品融合引导图 | POST /jobs/{id}/product-fusion/guide | createProductFusionGuide | 旧流程兼容接口:读取产品图和白底人物图,按 product_region 合成位置引导图。当前内置角色 + 产品 + 描述流程不再主动调用它。 |
@@ -1004,6 +1004,18 @@ ProductRefStateItem {
变更记录
这个记录不是 git log 的替代品。它记录“产品理解发生了什么变化、影响了哪些源码、你以后描述需求时该怎么说”。后续每次改功能都要补一条。
+
+
+ 2026-05-18 · gpt-image-2 图生图改用 edits 并处理上游饱和
+ API
+ Reliability
+
+
+
问题:产品补角度遇到 gpt-image-2 上游 429 saturated 时,后端只间隔 1 秒重试 3 次,很容易直接失败;相似主体重构还把参考图作为 JSON image 参数提交到 /images/generations,在 GPT 图片接口下会返回 Unknown parameter: 'image'。
+
改动:_image_edit_call 和旧分镜图生图入口改为调用 /images/edits,用 multipart image 文件传参考图,继续固定模型 gpt-image-2。图像调用新增 429 / saturated 识别和退避重试;产品补角度单次请求提高到 5 次尝试,耗尽后返回 503 和“上游负载饱和,请稍后重试”的可读错误。web/lib/api.ts 对产品补角度和主体资产接口解析 FastAPI detail,不再把整段 JSON 原样堆进 toast。
+
影响:api/main.py、web/lib/api.ts、docs/source-analysis.html。后续生图仍然只走 gpt-image-2,但图生图必须走 edits 形态,不能再往 generations JSON 里传 image。
+
+
2026-05-18 · 相似主体缩略图压缩并支持单张重生/删除
@@ -1052,7 +1064,7 @@ ProductRefStateItem {
问题:之前图片、文本、音频分析共用 LLM_BASE_URL,配音默认仍是 MiniMax,视频虽然已接豆包/Seedance,但模型标注没有把“生图 GPT / 语音 Azure / 视频 Seedance”三条高优先级链路清楚拆开。
-
改动:api/main.py 新增 IMAGE_BASE_URL、IMAGE_API_KEY、VOICE_PROVIDER、AZURE_OPENAI_BASE_URL、AZURE_OPENAI_API_KEY、AZURE_TTS_MODEL 等配置;所有 /images/generations 调用改走图片专用 OpenAI-compatible client,默认 gpt-image-2;TTS 新增 Azure OpenAI 协议 /audio/speech 通道,默认 VOICE_PROVIDER=azure_openai;GET /health 回传图片、主体、语音和视频的实际模型与 base URL 供前端模型标注使用。
+
改动:api/main.py 新增 IMAGE_BASE_URL、IMAGE_API_KEY、VOICE_PROVIDER、AZURE_OPENAI_BASE_URL、AZURE_OPENAI_API_KEY、AZURE_TTS_MODEL 等配置;图片调用改走图片专用 OpenAI-compatible client,文字生图走 /images/generations,图生图后续已收敛到 /images/edits,默认 gpt-image-2;TTS 新增 Azure OpenAI 协议 /audio/speech 通道,默认 VOICE_PROVIDER=azure_openai;GET /health 回传图片、主体、语音和视频的实际模型与 base URL 供前端模型标注使用。
影响:api/main.py、web/lib/api.ts、RULES.md、.project.json、docs/source-analysis.html。真实 key 仍只写本地 api/.env / 生产环境变量,不能入库。
diff --git a/web/lib/api.ts b/web/lib/api.ts
index c51065f..3d11287 100644
--- a/web/lib/api.ts
+++ b/web/lib/api.ts
@@ -233,7 +233,7 @@ export async function generateProductAngleAsset(
})
if (!res.ok) {
const txt = await res.text().catch(() => "")
- throw new Error(`generateProductAngleAsset ${res.status} ${txt.slice(0, 300)}`)
+ throw apiError("generateProductAngleAsset", res.status, txt)
}
return res.json()
}
@@ -1098,7 +1098,7 @@ export async function generateSubjectAssets(
})
if (!res.ok) {
const txt = await res.text().catch(() => "")
- throw new Error(`subjectAssets ${res.status} ${txt.slice(0, 300)}`)
+ throw apiError("subjectAssets", res.status, txt)
}
return res.json()
}