AI 玩具专利生成工作流 · 编排逻辑

项目:20260518-ai-toy-patent-workflow 分支:master @ e519627 文档生成:2026-05-23 真源:仓库当前代码 + RULES.md

本文件是从源代码反向归纳的编排说明,不是规约。出现差异时以 src/lib/templates.tsPACK_ORDERPACK_TEMPLATESVIDEO_TEMPLATES 以及 src/app/api/** 的路由实现为准。

0 · 目录

  1. 顶层一图:4 阶段串行 + 平行视频
  2. 数据真源与冻结版本
  3. 阶段 A:输入 → 候选图
  4. 阶段 B:九宫格选中
  5. 阶段 C:角色锁定(CharacterSpec + L1)
  6. 阶段 D:四个图片包串行
  7. 阶段 E:文案模板(18 条)
  8. 阶段 F:视频任务(Seedance, 5 条)
  9. 横切:持久化、审计、鉴权、轮询
  10. 编排约束与"规约 vs 实现"差异
  11. 已落地导出 / 未落地路线

1 · 顶层一图:4 阶段串行 + 平行视频

整个工作流是一条带 gate 的状态机,一个 GenSession 串起所有阶段的产物。横向四个图片包严格串行,包内单图4 并发 + 拓扑排序,文案 / 视频在 characterSpec 锁定后即可触发,但前端按"四包完成后再开"做 UX 引导。

┌─────────── A. 输入入口 ───────────┐
│  idea   POST /api/generate         │──┐
│  remix  POST /api/projects/from-…  │  │  → GenSession 落盘(data/sessions/)
│  replicate / extend ↑              │  │
└────────────────────────────────────┘  │
                                        ▼
                        ┌──── B. 九宫格选中 ────┐
                        │ POST /api/select      │
                        │ 选中图复制到 selected/│
                        └───────────┬───────────┘
                                    ▼
                ┌──── C. 角色锁定(gate #1)────┐
                │ POST /api/character/lock      │  → CharacterSpec
                │   (replicate/extend 走 strict)│  + cleanReferenceImageUrl
                └───────────┬──────────────────┘     (L1 白底净化锚图)
                            ▼
       ┌────────── D. 四个图片包(严格串行)──────────┐
       │  ① patent ▶ ② accessories ▶ ③ production ▶  │
       │      ④ marketing                              │
       │   gate #2:前一包 status='complete' 才解锁   │
       │   gate #3:同 session+image+kind 并发锁      │
       │   包内:拓扑排序 + 4 并发 + 增量回写         │
       └───────────┬──────────────────────────────────┘
                   ▼  (前端 UX:四包齐了再开下一段)
       ┌────────── E. 文案 18 模板 ──────────┐
       │  POST /api/text/generate            │
       │  gate:characterSpec 必须存在        │
       └─────────────────────────────────────┘
       ┌────────── F. 视频 5 模板(Seedance)─────────┐
       │  POST /api/video/generate(异步任务)         │
       │  GET  /api/video/status/[taskId](轮询 15s)  │
       │  锚图优先级:mkt_white_front → patent_front   │
       │              → cleanReferenceImageUrl → L0    │
       └──────────────────────────────────────────────┘
                   ▼
            导出(已落地:ZIP;路线图:PDF)

一句话总结

选中图 (L0) → 净化为 L1 → 用 L1 作为根锚图生成各包根模板 (L2) → 包内其它模板基于 L2 派生 (L3) → 全程通过 GPT image edit 而不是文本拼 URL,保证角色一致。

2 · 数据真源与冻结版本

符号代码位置值 / 含义
PACK_ORDERsrc/lib/templates.ts:13['patent', 'accessories', 'production', 'marketing'] — gate 校验唯一来源
PACK_LABELSsrc/lib/templates.ts:6patent=专利包 / accessories=配件包 / production=生产打样包 / marketing=宣发包
TEMPLATE_FREEZE_VERSIONsrc/lib/templates.ts:4toy-pack-templates-v01 — 写入每个 ToyAsset.meta 和 ExportManifest
FILENAME_SCHEMAsrc/lib/templates.ts:3{sessionId}_{characterSlug}_{pack}_{view}_{version}.{ext}
PACK_TEMPLATESsrc/lib/templates.ts:10944 个包各自的模板数组,每个包指定根模板(其它模板的 anchorTemplateId 全部指向根)
PACK_ASSET_CONCURRENCYsrc/lib/packGenerator.ts:1554 — 包内单图并发上限
VIDEO_TEMPLATESsrc/lib/templates.ts:155 条:旋转 / 开箱 / 触感 / 角色故事 / 工厂预览
TEXT_TEMPLATESsrc/lib/templates.ts:10618 条:项目 / 专利 / 生产 / 配件 / 宣发 / 视频脚本

各包模板规模与根锚

kind根模板(L2 锚)模板总数必需可选
专利包patentpatent_front1275
配件包accessoriesacc_inventory_sheet13121
生产打样包productionprod_front_spec19154
宣发包marketingmkt_white_front221111

规模来源 PACK_TEMPLATE_SUMMARYsrc/lib/templates.ts:1101)。宣发包末尾 5 条 video_* 是分镜板(图片),与 VIDEO_TEMPLATES 的真实视频任务同名但不同源。

3 · 阶段 A:输入 → 候选图

3.1 三种输入模式(ProjectInputMode

模式API九宫格生成L0 是什么角色锁定
idea
想法
POST /api/generate GPT images/generations × N(4/8/12),ref 图作为文本提示拼接 用户从九宫格选中的图 用户手动点 /api/character/lock,normal 净化
remix
二创
POST /api/projects/from-upload GPT images/edits 基于上传图 × N,强制"原创化"提示 用户从九宫格选中的图 同 idea
replicate
复刻
POST /api/projects/from-upload 跳过,上传图直接作为 L0 selected 上传的主体图 自动调 buildCharacterSpec + strict 净化
extend
扩展
POST /api/projects/from-upload 同 replicate 同 replicate 同 replicate,且把上传图按 role 预填到专利六视图槽位(preFilledSlots

3.2 上传 role → 专利槽位映射(extend 模式)

src/app/api/projects/from-upload/route.ts:19

UploadedImageRole映射到 AssetTemplate.id
view-frontpatent_front
view-backpatent_back
view-leftpatent_left
view-rightpatent_right
view-toppatent_top
view-bottompatent_bottom

命中预填槽的 pack asset 不会调 GPT,直接复用上传 URL(packGenerator.ts:326-356)。

3.3 Provider 选择

// src/lib/providers.ts:10
export function detectProvider(): Provider {
  return process.env.OPENAI_API_KEY ? 'gpt' : 'mock';
}

4 · 阶段 B:九宫格选中

POST /api/selectsrc/app/api/select/route.ts)支持 action: 'select' | 'reject' | 'reset'select 时把图从 data/generated/ 复制到 data/selected/ 并把新 URL 写回 img.meta.selectedUrl

前端键盘约定(src/components/PromptPanel.tsx):1-9 选中,Shift+1-9 打叉。被打叉的图保留可见,不会进入后续阶段,但仍在 audit DB 留痕。

5 · 阶段 C:角色锁定(CharacterSpec + L1 锚图)

5.1 两条路径

路径 1 — 普通锁定

POST /api/character/lock

  1. 幂等:未 force 且当前 spec.sourceImageId == imageId,直接返回缓存
  2. buildCharacterSpec():调 GPT JSON 结构化输出
  3. cleanupCharacterAnchor()normal prompt 净化为白底
  4. 写入 characterSpec.cleanReferenceImageUrl = L1 锚图 URL

路径 2 — 上传/复刻锁定

POST /api/character/lock-from-uploadfrom-upload 自动触发

  1. 有 userHint 时覆盖 session.prompt
  2. buildCharacterSpec() 在 replicate/extend/upload 分支走 inferCharacterSpecFromImage()(Vision 推断)
  3. cleanupCharacterAnchor()strict prompt:仅抽取最大最完整的单一主角色,丢弃多宫格 / 包装 / 海报版式
  4. 强制 force=true,每次都重算并覆盖 L1

5.2 CharacterSpec 字段(src/lib/types.ts:76

15 个语义字段 + 3 个图像引用 + lockedAt。详见 CHARACTER_SPEC_FIELDStemplates.ts:58)。关键三项:

5.3 strict 净化的关键约束(节选)

src/lib/packGenerator.ts:171-200

6 · 阶段 D:四个图片包串行

6.1 三道 gate

每次 POST /api/packs/generate 前后端都过的 gate

  1. characterSpec 必须存在 — 否则 409 "请先锁定角色设定"(packs/generate/route.ts:43
  2. 前一包必须 completePACK_ORDER 中前一项必须满足 pack.status === 'complete' 且模板覆盖率 100%(packs/generate/route.ts:25-58
  3. 并发互斥 — 同一 session:image:kind 已在跑则返回 202 running(generationLocks.ts
  4. 额外约束:源图 status 必须 = selected

6.2 包内编排(generateAssetPackpackGenerator.ts:276

sortTemplatesByAnchor(getPackTemplates(kind))   // 拓扑排序
        │
        ▼
取/建 CharacterSpec → cleanupCharacterAnchor  // 兜底确保 L1 存在
        │
        ▼
existingPack 合并:从断点续生(按 templateId 去重)
        │
        ▼
takeReadyTemplate()  // 依赖已就绪的模板进入候选
        │
        ▼
inFlight ≤ PACK_ASSET_CONCURRENCY (=4)        // 并发槽
        │
        ▼
对每张模板:
  · 若命中 preFilledSlot → 直接复用上传图,不调 GPT
  · 否则 generateAssetImage():
      · anchorImageUrl = anchorAsset.url   // L3:基于已生成根模板
                       ?? L1.cleanReferenceImageUrl  // L2:用净化锚图
                       ?? L0.url
      · GPT images/edits 真正的图生图(读 anchor 字节 → multipart)
      · data: 开头则落盘到 data/packs/{packId}_{assetId}.{ext}
        │
        ▼
async onProgress(pack) → persistPackProgress (每张都回写 session JSON)
        │
        ▼
全部就绪后 pack.status = 'complete',写 ExportManifest 到 data/exports/

6.3 派生层级(ToyAsset.derivationLevel

含义来源 URL触发条件
L0用户选中 / 上传主体图img.url选中 / 复刻
L1白底净化锚图characterSpec.cleanReferenceImageUrl角色锁定
L2每个包的根模板图data/packs/...包内 anchorTemplateId == undefined 的模板(每包仅一张:patent_front / acc_inventory_sheet / prod_front_spec / mkt_white_front
L3包内其它图同上,basedOn = L2所有 anchorTemplateId 指向根的模板

代码里 derivationLevel 只被赋值 2(无 anchorAsset)或 3(有 anchorAsset)。0/1 出现在类型定义中,运行时由 L0 图片本身和 cleanReferenceImageUrl 隐式承担。

6.4 单张重做(POST /api/assets/[assetId]/regenerate

双重 gate

6.5 增量回写与断点续跑

onProgress 在每张生成完成后 reload session JSON、用最新 pack 替换旧版本(按 kind + sourceImageId 匹配),再写回。generateAssetPack 启动时会取出未完成的 existingPack,按已落地的 templateId 跳过、只生成剩余项 → 断网或失败可重试。

7 · 阶段 E:文案模板(18 条)

7.1 路由

POST /api/text/generate,body {sessionId, templateIds?}后端唯一 gatesession.characterSpec 必须存在(text/generate/route.ts:18),不强制四包完成。

7.2 实现

src/lib/textGenerator.ts

7.3 18 条文案模板按 kind 分组

kind条数典型 templateId(必需打 ★)
project2★ text_project_design_brief · ★ text_character_setting
patent7★ product_name · ★ product_use · ★ design_points · ★ representative_view · ★ view_brief · color_claim
production4★ brief · ★ cmf · ★ bom · ★ qc
accessories2★ accessory_brief · ★ accessory_bom
marketing3★ core_copy · ★ detail_page · social_posts
video1video_script_pack(脚本文字包)

8 · 阶段 F:视频任务(Seedance)

8.1 五条视频模板(VIDEO_TEMPLATES

id标题比例时长
video_turntable360 度旋转展示16:96 s
video_unboxing开箱短片9:168 s
video_touch_detail触感细节9:166 s
video_story_intro角色故事介绍16:98 s
video_factory_preview工厂预览短片16:98 s

8.2 提交 + 轮询

POST /api/video/generate              GET /api/video/status/[taskId]
       │                                       ▲
       ▼                                       │  前端每 15 s 轮询
generateSeedanceVideo()                        │  最多 30 次
   ↓                                           │
POST {SEEDANCE_API_BASE}                       │
   /contents/generations/tasks                 │
   ↓ task_id, status='submitted'               │
保存到 session.videoTasks[]      ──────────────┘
       │
       ▼
status='succeeded' 时 videoUrl 用 saveRemoteVideo() 拉到 data/videos/,
返回 /api/video-file/{filename} 本地路径

8.3 锚图优先级(page.tsx:580-589

  1. mkt_white_front — 宣发白底正面图(最稳定)
  2. patent_front — 专利主视图
  3. characterSpec.cleanReferenceImageUrl — L1 净化锚图
  4. 当前选中意向图 L0

8.4 PUBLIC_APP_URL 注入

Seedance 需要从公网拉参考图,所以 publicUrlOrUndefined()/api/img/...PUBLIC_APP_URL(生产 = https://ai-toy.kang-kang.com)转成绝对 URL。localhost / 127.0.0.1 / 私有 IP 一律丢弃。

8.5 视频任务去重

每次新提交按 templateId 去重覆盖(video/generate/route.ts:46),保证 5 个模板各最多一个最新任务。fix: dedupe suffixed video tasks7abbb7d)专门处理 video_turntable_60s 等带后缀的真实成片回流到默认模板卡。

9 · 横切:持久化、审计、鉴权、轮询

9.1 八个存储桶(src/lib/storage.ts

URL 前缀放什么
data/sessions/每个 session 一个 JSON,含 images / packs / textAssets / videoTasks / exports 全量
data/generated//api/img/generated/九宫格候选图原图
data/selected//api/img/selected/选中后复制一份(保留生成版本不被覆盖)
data/refs//api/img/refs/idea 模式上传的参考图
data/uploads//api/img/uploads/remix / replicate / extend 的上传图
data/anchors//api/img/anchors/L1 净化锚图 {sessionId}_{imageId}_clean.{ext}
data/packs//api/img/packs/四个包的所有 ToyAsset 图片
data/videos//api/video-file/Seedance 成片从公网拉回的本地副本
data/exports//api/export/ExportManifest JSON(每个 pack 一份)

9.2 审计:SQLite + 兜底 JSONL

src/lib/auditDb.ts。每个 API 路由的关键节点(started / completed / failed / blocked / saved)都调 recordEvent(),落到 data/app.db。Docker 镜像内置 sqlite3;非 Docker 本地缺 sqlite3 时降级写 data/audit-fallback.jsonl,不阻断流程。

每张图也通过 upsertImageAsset() 写入 image_assets 表,包含 bucket / width / height / sizeBytes / kind / templateId / origin,是 /api/gallery/[sessionId] 的真源。

9.3 鉴权(src/middleware.ts

9.4 轮询节奏(前端)

对象间隔最大次数终止条件
pack 生成(scheduleSessionRefresh5 s90无 status='draft' 的 pack;前 6 次无论如何都跑
视频任务(scheduleVideoRefresh15 s30status 不再是 submitted/processing

10 · 编排约束与"规约 vs 实现"差异

差异 1:RULES.md 说"四个图片包完成后才解锁文案和视频"

后端实际只校验 session.characterSpec 存在:

这条规约靠前端 UX 引导执行,不是后端 enforce。绕过前端可以在锁定角色后立刻发文案/视频请求。

差异 2:视频不 mock

没配 SEEDANCE_API_KEY/api/video/generate/api/video/status 返回 503,不会回退到占位视频。文档和 RULES.md 一致。

差异 3:宣发包里 5 条 video_* 模板是分镜板(图片),不是真实视频

marketing 包模板列表里 video_turntable / video_unboxing 等 5 条id 与 VIDEO_TEMPLATES 重名,但 kind=marketing、aspectRatio=16:99:16,走的是 GPT image edit,产出 PNG 分镜板。真实视频由 Seedance 异步任务单独产出,存 session.videoTasks[]。两者完全独立,前端按 templateId 关联展示。

差异 4:派生层级运行时只用 2 / 3

类型定义 derivationLevel: 0 | 1 | 2 | 3 给出了完整四级,但 generateAssetPack 只赋值 2(包根模板)和 3(包内其它)。L0/L1 由 GenImage 和 CharacterSpec.cleanReferenceImageUrl 隐式承担,不写入 ToyAsset.derivationLevel。

差异 5:preFilledSlot 命中后 derivationLevel

命中预填上传图时仍按 anchor 存在与否赋 2/3(packGenerator.ts:347),但实际生成 URL 是上传桶 URL,不是 packs 桶。导出 ZIP 时 extFromAsset 会从 URL 抓扩展名,readImageUrl 回到 uploads 桶读字节。

11 · 已落地导出 / 未落地路线

11.1 已落地

11.2 未落地(RULES.md 路线)

12 · 关键 API 速查

方法路径gate / 关键行为
POST/api/uploadsmultipart,role 必传
POST/api/generateidea 模式批量生图(4/8/12)
POST/api/projects/from-uploadmode ∈ {remix, replicate, extend},replicate/extend 自动锁定 strict
POST/api/selectaction ∈ {select, reject, reset},select 时复制到 selected/
POST/api/character/lock普通净化;force 控制是否重算
POST/api/character/lock-from-uploadstrict 净化;force 总是 true
POST/api/character/cleanup独立触发 cleanupCharacterAnchor
POST/api/packs/generate三道 gate;background=true 时返 202 异步跑
POST/api/assets/[assetId]/regenerate必传 confirmCost=true;并发锁
GET/api/packs/download?sessionId=&kind=按选中图找该 kind 的 pack 打 ZIP
POST/api/text/generate必须 characterSpec;可传 templateIds 子集
POST/api/video/generate必须 Seedance Key;按 templateId 去重覆盖
GET/api/video/status/[taskId]?sessionId=查 Seedance + 写回本地副本
GET/api/sessions按 createdAt desc 列全部 session 元信息
GET/api/templates把 PACK_TEMPLATES / TEXT / VIDEO 暴露给前端
GET/api/gallery/[sessionId]从 image_assets 表 + filesystem 拼图库
GET/api/audit/[sessionId]读 events 表事件流水
GET/api/img/[bucket]/[filename]公开,Seedance 拉参考图依赖
GET/api/video-file/[filename]本地视频副本
POST/api/auth/login / /logoutHMAC HttpOnly Cookie

附录 · 文件锚点

关键概念代码位置
串行顺序 PACK_ORDERsrc/lib/templates.ts:13
包模板冻结版本src/lib/templates.ts:4
包内并发上限src/lib/packGenerator.ts:155
包 gate 三道src/app/api/packs/generate/route.ts:42-91
包内拓扑 + 并发调度src/lib/packGenerator.ts:392-424
L1 strict / normal promptsrc/lib/packGenerator.ts:171-200
L1 净化路径src/lib/packGenerator.ts:157
L0/L1/L2/L3 派生src/lib/packGenerator.ts:316-389
preFilledSlot 映射src/app/api/projects/from-upload/route.ts:19
视频锚图优先级src/app/page.tsx:580-589
视频任务 templateId 去重src/app/api/video/generate/route.ts:46
pack 进度轮询src/app/page.tsx:536-543
video 状态轮询src/app/page.tsx:545-557
generationLocks 全局并发锁src/lib/generationLocks.ts
ZIP 打包src/app/api/packs/download/route.ts
HMAC Cookie 鉴权src/middleware.ts
审计写库src/lib/auditDb.ts

— 文档生成基于 commit e519627。结构性改动后请重跑 npm run docs:orchestration(如已配脚本)或重新执行 docs/orchestration.html 的生成命令。