24 KiB
生图链路重构交接文档
给后续 AI 开发者的实施清单。当前代码的整体骨架已搭好(模板、Pack、Manifest、Seedance、GPT provider),但一致性机制是假的,需要按本文档重构。
路径:
/Users/kangwan/Projects/code/20260518-ai-toy-patent-workflow/HANDOFF_IMAGE_PIPELINE.md
0. 一句话目标
让"上传图 + 风格 → 意向图 → 选中主方案 → 专利包 → 配件包 → 生产包 → 宣发包 → 视频"的整条链路里,每张图都基于上游锚图生成、每张图都能单独重做且不脱链。专利申请要求前后一致,配件六视图也必须自成体系。
1. 用户期望的目标流程
[上传图(单张/多张) + 选风格(风格库可视化)]
↓
批量生图(4/8/12 张候选)
↓
九宫格快筛 → 选中主方案
↓
锁定 CharacterSpec(角色基线)+ 生成 L1 净化锚图
↓
按顺序生成(每步可单独重做,但参考链不能脱):
├─ 专利包(六视图 + 立体图 + 局部图)
├─ 配件包(先识别配件 → 每件孤立锚图 → 每件 6 视图 → 组合图)
├─ 生产包(尺寸/材料/拆件/包装)
├─ 宣发包(白底/场景/卖点/详情页)
└─ Seedance 视频(用宣发白底图当锚)
核心约束:
- 风格库要可视化(缩略图代表)+ 内容完整(lighting/composition/material/negative)
- 每张图都有明确的上游 anchor,参考链不能跨级
- 单张重做必须沿用同一个 anchor
- 配件六视图必须基于配件孤立锚图,不是娃娃源图
- 视频参考宣发白底图,不是意向图
2. 现状的 6 个关键 Gap
Gap 1:参考图根本没传给模型(最严重)
位置:src/lib/providers.ts:62-65
const refHint = opts.refImages?.length
? `\n参考图 URL,用于保持角色一致:\n${opts.refImages.join('\n')}`
: '';
问题:把参考图 URL 当文本拼在 prompt 末尾发给 /images/generations。这个端点根本不读图,模型只看到一串文本 URL,等于没参考。
真正的图生图必须走 /images/edits + multipart 上传图像字节,或者用 /responses + vision input 让 GPT 先描述再二次生图。
Gap 2:风格库太薄、看不到样子
位置:src/components/PromptPanel.tsx:5-12
只有 6 个文字按钮:毛绒玩偶 / 机甲风 / 可爱萌系 / 专利蓝图 / 赛博朋克 / 极简。
问题:
- 没有代表缩略图(用户看不到"这个风格长什么样")
- 没有完整的 style block(lighting、composition、color palette、material hint、negative prompt)
- 只是简单拼成
prompt + ", style: 机甲风"
Gap 3:所有后续图都参考"最初那张意向图",逐级漂移
位置:src/lib/packGenerator.ts:144
const prompt = renderPrompt(template.promptTemplate, characterSpec, opts.sourceImage.url);
问题:专利右视图、宣发场景图、配件六视图……全部参考同一张意向图。
正确做法 — 锚图链:
L0 锚图 = 用户选中的意向图(可能还有背景、不够干净)
L1 锚图 = L0 净化后的白底正面图(CharacterSpec 锁定时生成)
L2 锚图 = 各包的首图(patent_front / acc_inventory / mkt_white_front)
L3 节点 = pack 内其他图都参考自己包的 L2
现在所有 L2/L3 都跨过 L1 直接参考 L0,6-30 张图相互之间没有锚定,越生越漂。
Gap 4:配件链路是假的
位置:src/lib/packGenerator.ts:36-37
if (prompt.includes('机甲')) accessories.unshift('机甲头盔');
if (prompt.includes('M logo')) accessories.unshift('胸前 M 标识');
问题:
- 配件清单是关键词硬匹配,换个 prompt 就识别不出
- 配件六视图都把"主娃娃源图"当 ref,配件本身轮廓、材质会被娃娃身体盖住
正确做法:
- 锁定主方案后,调 GPT Vision 解析 L1 锚图 → 输出
[{name, isolatedDescription, bbox}] - 为每个配件生成配件孤立锚图(白底、单件、无娃娃)
- 配件六视图基于自己的孤立锚图
- 最后单独生成"配件+娃娃组合图"
Gap 5:单张重做会脱链
代码里只有 generateAssetPack(整包重做),没有单张重做接口。
如果右视图不满意要单独重做,需要:
- 知道这张图的锚图是谁(同包主视图)
- 锚图必须存在且未变
- 可选传
userRefinement文本补充
需要新增 API POST /api/assets/[assetId]/regenerate,并在 ToyAsset 类型上加 anchorAssetId 字段。
Gap 6:视频参考也不一致
位置:src/app/page.tsx:161
imageUrl: image.url // 还是那张原始意向图
问题:视频里的玩具和已定稿的电商图长得不一样。
正确做法:视频参考宣发白底主图(mkt_white_front),如果未生成就退到专利主图。
3. 实施方案(按依赖顺序)
阶段 1:真图生图链路(最高优先级,1+2+3+5 是最小可用版本)
1.1 新增 generateGptImageEdit(providers.ts)
export async function generateGptImageEdit(opts: {
prompt: string;
anchorImage: Buffer | string; // 真实图片字节或本地路径
maskImage?: Buffer; // 可选 mask(局部重绘)
size?: '1024x1024' | '1024x1536' | '1536x1024';
}): Promise<GenImage>
实现要点:
- 走
https://api.openai.com/v1/images/edits multipart/form-data:image(Buffer)、prompt、model=gpt-image-1、size- 必须传图字节而不是 URL
- 返回
b64_json→ 解码为 data URL
保留现有 generateGptImages 用于 L0 意向图阶段(无锚图,纯文本生图)。
1.2 数据模型扩展(types.ts)
export type ToyAsset = {
// ...existing
anchorAssetId?: string; // 上游锚图 asset id
anchorImageUrl?: string; // 解析后的锚图实际 URL
derivationLevel: 0 | 1 | 2 | 3;
};
export type CharacterSpec = {
// ...existing
cleanReferenceImageUrl?: string; // L1 净化锚图(白底正面)
};
export type AssetTemplate = {
// ...existing
anchorTemplateId?: string; // 显式指明上游锚图模板
};
1.3 模板加 anchorTemplateId(templates.ts)
{ id: 'patent_front', anchorTemplateId: undefined, ... } // 用 L1 锚图
{ id: 'patent_back', anchorTemplateId: 'patent_front', ... } // 用包内主图
{ id: 'patent_left', anchorTemplateId: 'patent_front', ... }
// 配件同理
{ id: 'acc_inventory_sheet', anchorTemplateId: undefined, ... } // 用 L1 锚图
{ id: 'acc_front', anchorTemplateId: 'acc_inventory_sheet', ... }
{ id: 'acc_back', anchorTemplateId: 'acc_inventory_sheet', ... }
1.4 新增 API:净化锚图
POST /api/character/cleanup
逻辑:
- 输入:
sessionId + imageId - 调
generateGptImageEdit,prompt = "保持角色完全一致,把背景换成纯白色,产品居中,无任何文字水印,光线均匀" - 输出图 URL 写回
session.characterSpec.cleanReferenceImageUrl
1.5 改造 generateAssetPack(packGenerator.ts)
async function resolveAnchorImage(template, packAssets, characterSpec) {
if (!template.anchorTemplateId) {
// 用 L1 净化锚图,没有则退到 L0
return characterSpec.cleanReferenceImageUrl ?? characterSpec.sourceImageUrl;
}
const upstream = packAssets.find(a => a.templateId === template.anchorTemplateId);
if (!upstream) throw new Error(`anchor ${template.anchorTemplateId} not generated yet`);
return upstream.url;
}
按模板拓扑顺序生成(先无 anchor 的,再依次):
- 第一张 → 走
generateGptImageEdit(prompt, L1Buffer) - 其它张 → 走
generateGptImageEdit(prompt, 同包首图 Buffer)
需要新增工具函数:从 URL(如 /api/img/packs/xxx.png)读回 Buffer,供 multipart 上传。
阶段 2:单张重做
2.1 新增 API
POST /api/assets/[assetId]/regenerate
Body: { sessionId, userRefinement?: string }
逻辑:
- 找到这个 asset 在哪个 pack
- 解析它的 anchor(按 template.anchorTemplateId)
- 走
generateGptImageEdit,prompt 末尾追加userRefinement - 替换原 asset,保留 id 不变
- 更新 session JSON
2.2 UI
PackPanel.tsx 里每个 AssetRow 加"重做"按钮和"refinement"输入框。
阶段 3:风格库可视化
3.1 新增 src/lib/styles.ts
export type StylePreset = {
id: string;
label: string;
thumbnailUrl: string; // /styles/plush-classic.png
promptBlock: string; // 完整 style prompt 段
negativePrompt: string;
recommendedPalette: string[];
recommendedMaterials: string[];
goodFor: PackKind[];
};
至少 12-16 个预设,每个对应一张 256×256 缩略图放 public/styles/。
建议初始风格列表:
- 经典毛绒、长毛毛绒、超柔短绒、卡通圆胖
- 机甲风、赛博朋克、未来科技
- 可爱萌系、治愈系、Kuromi 暗黑可爱
- 复古玩具、迪士尼风、皮克斯风
- 黏土材质、绒线编织、3D 渲染、专利蓝图
3.2 改 PromptPanel
风格选择从 6 个按钮 → 4 列网格的图卡(缩略图 + 名称 + 适用包 tag)。
风格切换时:
promptBlock合并到生图 promptnegativePrompt单独传给 provider(GPT image edit 支持 negative)recommendedPalette/Materials自动填到 CharacterSpec 默认值
阶段 4:配件 Vision 识别
4.1 新增 src/lib/accessoryDetector.ts
export type DetectedAccessory = {
id: string;
name: string;
isolatedDescription: string;
recommendedColors: string[];
approximateBBox?: { x: number; y: number; w: number; h: number };
};
export async function detectAccessories(anchorImageUrl: string): Promise<DetectedAccessory[]>
实现:
- 走
/responses端点 + vision input(GPT-4.1-vision 或 gpt-5.5 多模态) - 把 L1 锚图作为图像输入
- prompt = "识别图中玩具身上所有独立配件,输出 JSON 数组,每项包含 name、isolatedDescription、recommendedColors。不包括玩具主体本身。"
- 严格 JSON 输出
4.2 配件包生成流程重构
1. 调 detectAccessories(L1_anchor) → [帽子, 背包, 标牌, ...]
2. 把每个 accessory 加入 session.characterSpec.accessoriesDetected[]
3. 为每个 accessory 生成 isolated_anchor(白底、孤立、单件)
- 走 generateGptImageEdit(L1锚图, "只保留 ${name},其它部分擦除,白底,居中")
4. 每个 accessory 的 6 视图(front/back/left/right/top/bottom/perspective)
都基于自己的 isolated_anchor
5. 最后生成 with_doll_assembly(参考 L1锚图 + isolated_anchors 组合)
数据模型加:
export type AccessoryGroup = {
id: string;
name: string;
isolatedAnchorUrl: string;
views: ToyAsset[]; // 6+ 视图
};
export type AssetPack = {
// ...existing
accessoryGroups?: AccessoryGroup[]; // 仅 kind === 'accessories' 用
};
阶段 5:视频参考一致性
5.1 改 handleGenerateVideo(page.tsx)
const mktFront = packs
.find(p => p.kind === 'marketing')?.assets
.find(a => a.templateId === 'mkt_white_front');
const patentFront = packs
.find(p => p.kind === 'patent')?.assets
.find(a => a.templateId === 'patent_front');
const videoAnchor =
mktFront?.url ??
patentFront?.url ??
session.characterSpec?.cleanReferenceImageUrl ??
image.url;
UI 上视频按钮旁边显示「参考:宣发白底图 / 专利主图 / 意向图」,用户清楚视频基于哪张。
如果宣发主图未生成,按钮可选「强制要求先生成宣发主图」或「使用专利主图」。
阶段 6:UI 锚图链可视化
PackPanel 顶部"角色锁定 & 资产清单"卡片下方加一个可视化树:
L0 意向图 ──→ L1 白底锚图 ──┬──→ 专利主图 ──→ 专利右视图 / 左视图 / ...
├──→ 配件锚图 ──→ 帽子 6 视图 / 背包 6 视图
├──→ 宣发白底图 ──→ 视频任务
└──→ 生产主图 ──→ 尺寸图 / 拆件图
让用户一眼看到每张图沿用哪张作为基准,重做某个节点会影响下游哪些。
可以用简单的 flexbox 树或 SVG 连线。
4. 实施 Checklist
最小可用版本(先做这 4 项)
- 1.1 新增
generateGptImageEdit(multipart upload) - 1.2 数据模型加
anchorAssetId / anchorImageUrl / derivationLevel / cleanReferenceImageUrl / anchorTemplateId - 1.3 模板加
anchorTemplateId - 1.5
generateAssetPack按拓扑生成、用真图生图 - 1.4
POST /api/character/cleanup生成 L1 锚图
单张重做
- 2.1
POST /api/assets/[assetId]/regenerate - 2.2 UI 加重做按钮 + refinement 输入框
风格库
- 3.1
src/lib/styles.ts+ 12-16 张 thumbnails(public/styles/) - 3.2
PromptPanel改成图卡选择器
配件 Vision
- 4.1
accessoryDetector.ts用 GPT Vision - 4.2 配件包改成「识别 → 孤立锚图 → 6 视图 → 组合图」
视频和可视化
- 5.1 视频参考切到宣发主图
- 6.1
PackPanel加锚图链可视化
5. 关键文件清单
| 用途 | 路径 |
|---|---|
| GPT provider | src/lib/providers.ts |
| 视频 provider | src/lib/videoProviders.ts |
| 包生成主逻辑 | src/lib/packGenerator.ts |
| 模板定义 | src/lib/templates.ts |
| 类型定义 | src/lib/types.ts |
| 存储 | src/lib/storage.ts |
| 主页 | src/app/page.tsx |
| 输入面板 | src/components/PromptPanel.tsx |
| 九宫格 | src/components/ResultGrid.tsx |
| 资产面板 | src/components/PackPanel.tsx |
| 生图 API | src/app/api/generate/route.ts |
| 模板查询 API | src/app/api/templates/route.ts |
| 角色锁定 API | src/app/api/character/lock/route.ts |
| 单包生成 API | src/app/api/packs/generate/route.ts |
| 全包生成 API | src/app/api/packs/generate-all/route.ts |
| 视频生成 API | src/app/api/video/generate/route.ts |
6. 模型/环境变量约定
- 文本 / 结构化 / Vision:
OPENAI_API_KEY+GPT_TEXT_MODEL(默认gpt-5.5) - 图像生成 / 编辑:
OPENAI_API_KEY+GPT_IMAGE_MODEL(默认gpt-image-2,edits 端点可能要gpt-image-1,按 OpenAI 实际支持调整) - 视频:
SEEDANCE_API_KEY+SEEDANCE_MODEL(默认seedance-1-0-pro)
重要:本项目"文本 / 图片统一走 GPT 最高规格,视频固定 Seedance"是硬约束。不要引入其他供应商。
7. 验收标准
完成最小可用版本后,应该满足:
- 选中意向图 → 锁定 → 自动生成 L1 净化锚图
- 生成专利包,主视图基于 L1,其它五视图基于专利主图(实际传图,不是文本 URL)
- 重做任意一张图,UI 显示它的 anchor 是谁,并能单独重做
- 风格切换有可视化预览
- 配件包能自动识别玩具上有几个配件,分别生成 6 视图
- Seedance 视频参考用的是宣发白底图
实测时拿一张复杂玩具图(带帽子、背包、标牌)跑全链路,所有图角色一致、配件清晰、视频与电商图一致。
8. 上传图入口的三种模式(二创 / 复刻 / 复刻+补全)
8.1 场景
用户的实际使用场景不止"从一句话开始",还有:
- 已经有玩具/IP 图(手稿、成品照、参考海报、其他设计师的稿)
- 只缺某些视角(手上有正面图,要补侧/背/俯视)
- 想做风格化变体(已有原型,但要换成赛博朋克版/绒线编织版/迪士尼风)
当前 PromptPanel 是 prompt-first,上传图只是被丢进 prompt 末尾当文本提示(见 Gap 1),完全没真正参与生成。需要重新设计上传图的语义。
8.2 三种入口模式
Mode A · 二创(Remix)
用户:[上传图 1-4 张] + [选风格 + 描述变化方向]
系统:保留核心识别(轮廓/五官/品牌符号),按用户要求做风格/材质/配色变化
输出:4-12 张候选变体
后续:选中 → Lock → 正常 Pack 流程
技术实现:
- 走
/images/editsmultipart,传第 1 张为主参考图 - prompt 拼接:
{风格 promptBlock} + {用户变化描述} + 强制 negative:不改变身体比例、五官相对位置、品牌符号、配件轮廓 n=4/8/12生成多张候选
Mode B · 复刻(Replicate)
用户:[上传图 1 张],标记为"主体图"
系统:跳过批量生图,直接以这张图作为 L0 锚图
→ 立刻做 L1 净化(白底、保真)
→ 调 Vision 识别配件、推断 CharacterSpec
→ 用户确认后进入 Pack
输出:完整专利/配件/生产/宣发包
适用场景:手上已有终稿原画,只需要把它"扩展"成完整素材包。
技术实现:
- 上传图后跳过
/api/generate,直接构造一个GenImage写入 session,status='selected' - 自动触发
/api/character/cleanup生成 L1 - 自动触发
/api/character/lock(CharacterSpec 让 Vision 推断,用户可编辑) - 用户点"进入 Pack"才开始 pack 生成
Mode C · 复刻 + 补全(Extend)
用户:[上传图 1-4 张],每张标记为"主体 / 同角色另一视角 / 配件孤立图"
系统:先把"主体"作为 L0
已有视角直接占用对应 slot(如上传了"正面图" → 占用 patent_front)
缺失的 slot 才调 API 生成
输出:节省 80% 算力,前后一致性最强
适用场景:用户已经手工画了正面+背面,要补侧视图+俯视图+配件六视图。
技术实现:
- 上传时弹出 tagging UI,让用户选每张图的"槽位"
- 后端在
generateAssetPack前先合并预占的 slot - 已占 slot 跳过 API 调用,直接复用上传图
8.3 UI 入口设计
PromptPanel 改成 3 个 tab:
┌─────────────────────────────────────────┐
│ [💡 想法] [🎨 二创] [📐 复刻] │
├─────────────────────────────────────────┤
│ Mode A 二创 tab 内容: │
│ - 上传图(1-4 张,第 1 张为主参考) │
│ - 风格选择(图卡,引用 §3 风格库) │
│ - 变化方向描述(textarea) │
│ - 数量 4/8/12 │
│ - [生成变体] │
└─────────────────────────────────────────┘
Mode B 和 Mode C 共享一个 tab,差异在上传后的处理:
- 只传 1 张且不标记 → Mode B
- 传多张并标注槽位 → Mode C
8.4 上传图的元数据(共用)
每张上传图都要附带:
export type UploadedImage = {
id: string;
url: string; // /api/img/uploads/xxx.png
filename: string;
uploadedAt: number;
role: 'reference' // 二创模式的灵感图
| 'subject' // 复刻模式的主体
| 'view-front' // 已有的视角,占用对应 slot
| 'view-back'
| 'view-left'
| 'view-right'
| 'view-top'
| 'view-bottom'
| 'accessory-isolated' // 配件孤立图
| 'accessory-named'; // 已命名的配件图
accessoryName?: string; // 仅 accessory-* 时有效
needsCleanup: boolean; // 是否需要先净化才能用
};
存储路径:data/uploads/。
8.5 新增 API
POST /api/uploads
Body: multipart, image file + role + accessoryName?
Resp: UploadedImage
POST /api/projects/from-upload
Body: { uploadedImages: UploadedImage[], mode: 'remix' | 'replicate' | 'extend',
remixPrompt?: string, styleId?: string, count?: number }
Resp:
- mode=remix: { sessionId, images } (4-12 变体)
- mode=replicate: { sessionId, characterSpec, l1AnchorUrl }
- mode=extend: { sessionId, characterSpec, l1AnchorUrl, preFilledSlots }
POST /api/character/lock-from-upload
Body: { sessionId, subjectImageId }
Resp: 调 Vision 推断 CharacterSpec
8.6 Vision 推断 CharacterSpec
Mode B 和 C 没有原始 prompt,需要 GPT Vision 看图推断。
src/lib/providers.ts 新增:
export async function inferCharacterSpecFromImage(opts: {
imageUrl: string;
userHint?: string; // 用户可选输入"这是我的 IP, 名字叫小桔"
}): Promise<CharacterSpec>
实现:
- 走
/responses+ vision input - prompt = "你是玩具产品经理,根据图片推断 CharacterSpec。严格 JSON,包含 name/oneLiner/speciesShape/bodyRatio/faceFeatures/colorPalette/materials/accessories/signatureElements/patentFocus/negativePrompt 字段"
- 如果 userHint 有值,name 用它,否则让 GPT 生成
8.7 复刻模式的 L1 净化注意事项
复刻模式比"从 prompt 开始"对净化要求更高:不能改任何角色细节。
/api/character/cleanup 在复刻模式下要使用更强的约束 prompt:
保持原图完全一致,仅做以下修改:
1. 把背景换成纯白色
2. 去除任何水印、文字、价格标签、网页 UI 元素
3. 居中并适当裁剪到正方形构图
绝对不要修改:
- 角色五官、表情、姿态
- 主体配色、材质、纹理
- 配件位置、轮廓、细节
- 任何品牌符号(如胸前 logo)
输出风格:商业产品图,柔和均匀打光,无阴影。
可以在 cleanup API 里加 preserveLevel: 'strict' | 'normal' 参数。
8.8 已有视角槽位的智能匹配(Mode C)
用户上传时标的 role: view-front 等,直接 1:1 占用专利包/配件包的对应模板:
| 上传 role | 占用模板 ID |
|---|---|
view-front |
patent_front + acc_front(如是配件) |
view-back |
patent_back |
view-left |
patent_left |
view-right |
patent_right |
view-top |
patent_top |
view-bottom |
patent_bottom |
accessory-isolated |
该配件的 isolated_anchor |
generateAssetPack 在生成前先检查 session.preFilledSlots,已占用的 template 直接复用上传图,不调 API。
8.9 合规和版权提示
UI 上传图区域要醒目提示:
⚠️ 上传图必须为你拥有或有合法授权使用的素材。请勿上传迪士尼、三丽鸥、泡泡玛特等已注册 IP 的图像;生成结果用于专利申请时,需自行确认不与他人在先权利冲突。
后端可选做基础检测:
- 调 GPT Vision 看图,若识别为已知品牌 IP("Hello Kitty"、"Mickey Mouse"、"Labubu")→ 阻止上传并给出明确警告
- 这一步初版可不做,但要在 UI 留位
8.10 实施 Checklist 增量
在 §4 Checklist 之外,额外加:
- 8.A 上传 API + 存储
data/uploads/ - 8.B
PromptPanel改 3-tab:想法 / 二创 / 复刻 - 8.C Mode A(二创):multipart +
/images/edits批量变体 - 8.D Mode B(复刻):跳过批量生图,直接 lock + cleanup
- 8.E
inferCharacterSpecFromImageVision 推断 - 8.F Mode C:上传图 role 标注 UI +
preFilledSlots合并 - 8.G cleanup 加
preserveLevel参数,复刻模式用 strict - 8.H 版权合规提示 UI
8.11 优先级建议
最实用顺序:
- Mode B 复刻(最常用:用户已有玩具图想做素材包)
- Mode A 二创(次常用:用户有概念图想做风格变体)
- Mode C 复刻+补全(高级:用户有部分视图想补齐)
如果只做一个,先做 Mode B —— 它对"前后一致"的帮助最直接,相当于直接拿用户图当 L0 锚图,跳过最容易漂移的"prompt → 意向图"阶段。