diff --git a/.memory/worklog.json b/.memory/worklog.json index 65d5e46..e38b6e7 100644 --- a/.memory/worklog.json +++ b/.memory/worklog.json @@ -346,6 +346,19 @@ "type": "session-heartbeat", "message": "Codex 会话活跃 · 最近命令:codex · 分支 master · 1 项未提交变更 · 最近提交:fix: align model provider configuration", "files_changed": 1 + }, + { + "ts": "2026-05-19T09:18:59+08:00", + "type": "commit", + "message": "auto-save 2026-05-19 09:18 (+3, ~2)", + "hash": "5d8e2da", + "files_changed": 5 + }, + { + "ts": "2026-05-19T01:19:59Z", + "type": "session-heartbeat", + "message": "Codex 会话活跃 · 最近命令:codex · 分支 master · 1 项未提交变更 · 最近提交:auto-save 2026-05-19 09:18 (+3, ~2)", + "files_changed": 1 } ] } diff --git a/HANDOFF_IMAGE_PIPELINE.md b/HANDOFF_IMAGE_PIPELINE.md new file mode 100644 index 0000000..a45befe --- /dev/null +++ b/HANDOFF_IMAGE_PIPELINE.md @@ -0,0 +1,442 @@ +# 生图链路重构交接文档 + +> 给后续 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 视频(用宣发白底图当锚) +``` + +**核心约束**: +1. 风格库要可视化(缩略图代表)+ 内容完整(lighting/composition/material/negative) +2. 每张图都有明确的上游 anchor,参考链不能跨级 +3. 单张重做必须沿用同一个 anchor +4. 配件六视图必须基于配件孤立锚图,不是娃娃源图 +5. 视频参考宣发白底图,不是意向图 + +--- + +## 2. 现状的 6 个关键 Gap + +### Gap 1:参考图根本没传给模型(最严重) + +**位置**:`src/lib/providers.ts:62-65` + +```typescript +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` + +```typescript +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` + +```typescript +if (prompt.includes('机甲')) accessories.unshift('机甲头盔'); +if (prompt.includes('M logo')) accessories.unshift('胸前 M 标识'); +``` + +**问题**: +- 配件清单是**关键词硬匹配**,换个 prompt 就识别不出 +- 配件六视图都把"主娃娃源图"当 ref,配件本身轮廓、材质会被娃娃身体盖住 + +**正确做法**: +1. 锁定主方案后,调 GPT Vision 解析 L1 锚图 → 输出 `[{name, isolatedDescription, bbox}]` +2. 为每个配件生成**配件孤立锚图**(白底、单件、无娃娃) +3. 配件六视图基于自己的孤立锚图 +4. 最后单独生成"配件+娃娃组合图" + +### Gap 5:单张重做会脱链 + +代码里只有 `generateAssetPack`(整包重做),**没有单张重做接口**。 + +如果右视图不满意要单独重做,需要: +- 知道这张图的锚图是谁(同包主视图) +- 锚图必须存在且未变 +- 可选传 `userRefinement` 文本补充 + +需要新增 API `POST /api/assets/[assetId]/regenerate`,并在 `ToyAsset` 类型上加 `anchorAssetId` 字段。 + +### Gap 6:视频参考也不一致 + +**位置**:`src/app/page.tsx:161` + +```typescript +imageUrl: image.url // 还是那张原始意向图 +``` + +**问题**:视频里的玩具和已定稿的电商图长得不一样。 + +**正确做法**:视频参考宣发白底主图(`mkt_white_front`),如果未生成就退到专利主图。 + +--- + +## 3. 实施方案(按依赖顺序) + +### 阶段 1:真图生图链路(最高优先级,1+2+3+5 是最小可用版本) + +#### 1.1 新增 `generateGptImageEdit`(providers.ts) + +```typescript +export async function generateGptImageEdit(opts: { + prompt: string; + anchorImage: Buffer | string; // 真实图片字节或本地路径 + maskImage?: Buffer; // 可选 mask(局部重绘) + size?: '1024x1024' | '1024x1536' | '1536x1024'; +}): Promise +``` + +实现要点: +- 走 `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) + +```typescript +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) + +```typescript +{ 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) + +```typescript +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 的,再依次): +1. 第一张 → 走 `generateGptImageEdit(prompt, L1Buffer)` +2. 其它张 → 走 `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` + +```typescript +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` 合并到生图 prompt +- `negativePrompt` 单独传给 provider(GPT image edit 支持 negative) +- `recommendedPalette/Materials` 自动填到 CharacterSpec 默认值 + +### 阶段 4:配件 Vision 识别 + +#### 4.1 新增 `src/lib/accessoryDetector.ts` + +```typescript +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 +``` + +实现: +- 走 `/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 组合) +``` + +数据模型加: + +```typescript +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) + +```typescript +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. 验收标准 + +完成最小可用版本后,应该满足: + +1. 选中意向图 → 锁定 → 自动生成 L1 净化锚图 +2. 生成专利包,主视图基于 L1,其它五视图基于专利主图(实际传图,不是文本 URL) +3. 重做任意一张图,UI 显示它的 anchor 是谁,并能单独重做 +4. 风格切换有可视化预览 +5. 配件包能自动识别玩具上有几个配件,分别生成 6 视图 +6. Seedance 视频参考用的是宣发白底图 + +实测时拿一张复杂玩具图(带帽子、背包、标牌)跑全链路,所有图角色一致、配件清晰、视频与电商图一致。