From 2eb629fe4b541d0278c7efc21407e1a141b13e29 Mon Sep 17 00:00:00 2001 From: luoqian <2769838458@qq.com> Date: Tue, 26 May 2026 15:45:46 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=9B=BE=E7=89=87=E7=94=9F?= =?UTF-8?q?=E6=88=90=E4=B8=8E=E4=B8=8A=E4=BC=A0=E6=89=A7=E8=A1=8C=E7=AE=A1?= =?UTF-8?q?=E7=BA=BF=EF=BC=9Anano-banana-2=E7=9C=9F=E5=AE=9E=E5=87=BA?= =?UTF-8?q?=E5=9B=BE=E2=86=92=E4=B8=8A=E4=BC=A0=E2=86=92=E5=85=AC=E7=BD=91?= =?UTF-8?q?URL=E5=9B=9E=E5=A1=AB=E2=86=92=E4=B8=8A=E4=BC=A0=E5=90=8E?= =?UTF-8?q?=E4=BA=8C=E6=AC=A1=E7=94=9F=E6=88=90API=E8=B0=83=E7=94=A8?= =?UTF-8?q?=E7=A8=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 image_generation_upload_rules.md、post_upload_call_draft_rules.md 规则文件,scripts/ 脚本目录 - 工作流新增阶段 5.1 图片生成与上传、5.2 上传后调用稿分支,调整原有步骤编号 - 所有模板和规则强制要求:API 图片输入使用上传后的公网 URL / asset:// URI,禁止本地路径或占位符 - 官网 Prompt 模板强制要求使用 @官网上传名,绑定已上传资产 - 附件/API 资产清单新增状态追踪列(pending_generation / pending_upload / generated_uploaded_ready 等)和 uploaded_assets.json 回填规则 - api_payload_rules.md 新增"上传后二次生成硬约束",要求 JSONL 必须在图片上传完成后基于真实 URL 重新生成 - image_model_provider_rules.md 明确 nano-banana-2 为默认执行资源(非仅提示词名称),新增图片生成/上传执行标记 --- 核心变更概括:工作流从"只写提示词"升级为"提示词→真实出图→上传→获取公网 URL→回填→二次生成调用稿"的完整资产管线。 --- workflows/novel_to_seedance/01_WORKFLOW.md | 70 +++++- .../references/api_payload_rules.md | 13 ++ .../references/attachment_reference_rules.md | 12 +- .../image_generation_upload_rules.md | 140 ++++++++++++ .../references/image_model_provider_rules.md | 7 + .../post_upload_call_draft_rules.md | 119 ++++++++++ .../scripts/generate_upload_assets.mjs | 208 ++++++++++++++++++ .../episode_09_official_site_prompts.md | 8 + .../templates/episode_10_api_payloads.jsonl | 2 +- .../global_05_image_asset_prompts.md | 41 ++++ .../global_06_jimeng_asset_prompts.md | 14 ++ .../global_08_attachment_manifest.md | 19 +- .../templates/global_09_api_asset_manifest.md | 28 ++- 13 files changed, 664 insertions(+), 17 deletions(-) create mode 100644 workflows/novel_to_seedance/references/image_generation_upload_rules.md create mode 100644 workflows/novel_to_seedance/references/post_upload_call_draft_rules.md create mode 100644 workflows/novel_to_seedance/scripts/generate_upload_assets.mjs diff --git a/workflows/novel_to_seedance/01_WORKFLOW.md b/workflows/novel_to_seedance/01_WORKFLOW.md index e2cf311..34ca90e 100644 --- a/workflows/novel_to_seedance/01_WORKFLOW.md +++ b/workflows/novel_to_seedance/01_WORKFLOW.md @@ -4,6 +4,8 @@ 把用户提供的小说文本,稳定拆解为可制作 AI 视频的一组文件:剧情分析、精彩连续分镜、人物与场景资产设计、nano-banana-2 图片资产提示词、Seedance 2.0 分镜视频提示词、转场衔接快照和质检表。 +图片资产阶段必须真实执行:生成 nano-banana-2 图片、上传图片、把上传后的公网 URL 和即梦官网素材名回填到附件清单与接口请求体。不得只停留在提示词输出。 + 本工作流不追求逐字复述小说,而是优先抽取适合影视化的冲突、行动、反转和情绪临界点。输出要能服务后续批量制作,而不是只写一份好看的文案。 ## 输入要求 @@ -44,6 +46,9 @@ outputs/novel_video_runs/输入项目名/ 07_平台安全规则.md 08_附件清单.md 09_接口资产清单.md + image_generation_jobs.json + uploaded_assets.json + assets/generated/ 10_前置验证.md 11_生成行为记录.md 12_系列维护.md @@ -102,6 +107,8 @@ outputs/novel_video_runs/输入项目名/ 10. `09_接口资产清单.md` API 资产清单,明确每个内部引用对应的官网上传名、公网 URL、火山 `asset://` URI。 + 图片生成并上传后,必须把上传 URL 写入 `公网URL`,把即梦官网素材名写入 `官网上传名`,状态更新为 `generated_uploaded_ready` 或明确失败状态。 + 11. `10_前置验证.md` 前置验证记录:挑战场景测试、对比测试、失败模式指纹和进入逐集执行的出口标准。 @@ -138,6 +145,7 @@ outputs/novel_video_runs/输入项目名/ - 平台写法规则写在 `references/`。 - 提示词硬约束、分级评分、类型预设、镜头链路预设和即梦安全写法统一写在 `references/prompt_constraint_system.md`。写分镜和视频 Prompt 时必须引用该文件,不得只按模板填空。 - 系列结构、人物三轴演化、场景情感残留、道具状态机、光线空间、声音设计和特殊场景技法统一写在 `references/series_design_rules.md`。写全局 Bible、人物场景设计、分镜和视频 Prompt 时必须引用该文件。 +- 图片生成、上传和即梦附件回填统一按 `references/image_generation_upload_rules.md` 执行。该步骤是资产链路的一部分,不是可选备注。 - Seedance 的实际 `Prompt` 代码块只写视频生成指令,不混入分析、QC、衔接表、素材清单或安全审查。 - 衔接写入 `06_转场衔接快照.md`,官网上传映射写入 `09_官网提示词.md`,API 写入 `10_接口请求体.jsonl`。 @@ -179,12 +187,17 @@ outputs/novel_video_runs/输入项目名/ 1. 先根据输入路径创建与输入项目名一致的输出目录。 2. 先创建全局文件,尤其是 `03_全剧设定总览.md`、`04_人物场景道具设计.md`、`05_图片资产提示词.md`、`06_即梦资产提示词.md`。系列视觉架构、人物、场景、道具、资产提示词必须在全局层一次性完成。 -3. 再为每一集创建独立目录,目录名必须是 `EP两位数字_中文集名`。 -4. 对每一集执行:分析 → 改编方案 → 分镜 → 全局资产引用清单 → Seedance 提示词 → 转场快照 → 平台安全检测 → 质检 → 官网/API 版本。 -5. 分集不得重复输出人物设定板、场景设定板、MJ/nano-banana-2 图片资产提示词、即梦资产提示词。分集只能引用全局资产,并说明本集使用的是哪个阶段状态。 -6. 每完成一集,更新全局 `02_分集切分决策.md` 中该集状态。 -7. 如果用户计划实际生成视频,先完成 `10_前置验证.md` 的最小可行验证;如果本轮只交付提示词,可先写“待实测”并保留验证位。 -8. 全部集完成后,输出 `99_全剧质检总结.md`。 +3. 从 `05_图片资产提示词.md` 的 nano-banana-2 版筛选后续视频必需参考图,生成 `image_generation_jobs.json`。 +4. 调用 `scripts/generate_upload_assets.mjs` 或等价实现执行 nano-banana-2 出图、保存本地图片、上传图片,生成 `uploaded_assets.json`。 +5. 将上传后的公网 URL、官网上传名和参考职责回填 `08_附件清单.md` 与 `09_接口资产清单.md`。如果上传响应未能自动解析 URL,必须标注 `generated_uploaded_url_unparsed` 并保留原始响应,不能把本地路径当成 API URL。 +6. 上传回填完成后,必须由 AI 再执行一次“即梦/Seedance 调用稿二次生成”:重新读取 `09_接口资产清单.md`,再生成或覆盖每集 `09_官网提示词.md` 与 `10_接口请求体.jsonl`。这一步必须发生在图片上传之后,不能在只有内部 `@角色/@场景` 时提前生成最终 JSONL。 +7. 再为每一集创建独立目录,目录名必须是 `EP两位数字_中文集名`。 +8. 对每一集执行:分析 → 改编方案 → 分镜 → 全局资产引用清单 → Seedance 提示词 → 转场快照 → 平台安全检测 → 质检 → 上传后二次生成官网/API 版本。 +9. 分集不得重复输出人物设定板、场景设定板、MJ/nano-banana-2 图片资产提示词、即梦资产提示词。分集只能引用全局资产,并说明本集使用的是哪个阶段状态。 +10. 分集 `09_官网提示词.md` 必须使用上传后的即梦官网素材名;分集 `10_接口请求体.jsonl` 必须使用上传后的公网 URL 或后续转换得到的 `asset://` URI。 +11. 每完成一集,更新全局 `02_分集切分决策.md` 中该集状态。 +12. 如果用户计划实际生成视频,先完成 `10_前置验证.md` 的最小可行验证;如果本轮只交付提示词,可先写“待实测”并保留验证位。 +13. 全部集完成后,输出 `99_全剧质检总结.md`。 如果文本过长导致一次输出不完,也要保留状态文件,下一轮从第一个 `pending` 或 `in_progress` 集继续,不得重新开始。 @@ -311,6 +324,51 @@ outputs/novel_video_runs/输入项目名/ - 如果项目 `config.md` 指定只输出某个服务商,可按配置精简;否则两版都输出。 - 不得在每集目录里重复生成图片资产提示词。分集视频提示词只引用全局图片资产的内部引用和官网上传名。 +### 5.1 图片生成与上传 + +完成 `05_图片资产提示词.md` 后,必须生成并执行 `image_generation_jobs.json`: + +- 图片生成模型默认使用 nano-banana-2。 +- 每个主要角色至少生成 1 张可用于视频引用的标准中景图;关键角色建议补充面部特写和侧光参考图。 +- 每个主要场景至少生成 1 张主视角宽幅参考图;复杂空间建议补充反向视角和局部锚点参考图。 +- 每个关键道具至少生成 1 张多角度拆解或手持比例参考图。 +- 图片生成后必须调用上传接口,把 base64 图片、文件名、contentType 和 metadata 提交到上传服务。 +- 上传后的 URL 是即梦/Seedance 后续引用的唯一可用图片地址;本地路径只用于审计和返修,不能写入 API `image_url.url`。 +- 上传结果必须写入 `uploaded_assets.json`,再回填 `08_附件清单.md` 和 `09_接口资产清单.md`。 +- 如果只完成提示词、未完成出图或上传,必须在最终交付中明确标注 `pending_generation` 或 `pending_upload`,不得标成可调用。 + +执行命令必须写入本次运行记录,执行到相关步骤可直接运行: + +```bash +node workflows/novel_to_seedance/scripts/generate_upload_assets.mjs \ + --jobs outputs/novel_video_runs/{输入项目名}/image_generation_jobs.json \ + --out-dir outputs/novel_video_runs/{输入项目名}/assets/generated \ + --manifest outputs/novel_video_runs/{输入项目名}/uploaded_assets.json +``` + +运行前检查: + +- `GEMINI_API_KEY` 或 `GOOGLE_API_KEY` 已配置。 +- `image_generation_jobs.json` 是合法 JSON 数组。 +- 每条任务都有 `internalRef`、`type`、`fileName`、`jimengUploadName`、`referenceDuty` 和 `prompt`。 +- 如需覆盖默认模型,设置 `NANO_BANANA_MODEL`。 +- 如需覆盖默认上传接口,设置 `IMAGE_UPLOAD_URL`。 + +运行后检查: + +- `assets/generated/` 中存在实际图片文件。 +- `uploaded_assets.json` 中每条资产都有 `publicUrl`,或状态明确为 `uploaded_url_unparsed` 并保留 `uploadRawResponse`。 +- `08_附件清单.md`、`09_接口资产清单.md`、分集 `09_官网提示词.md`、分集 `10_接口请求体.jsonl` 已使用上传后的公网 URL 或即梦官网素材名。 + +### 5.2 上传后调用稿分支 + +图片上传和 `09_接口资产清单.md` 回填完成后,进入“上传后调用稿分支”。 + +- 分支目标:由 AI 基于 `09_接口资产清单.md` 重新生成分集 `09_官网提示词.md` 与 `10_接口请求体.jsonl`。 +- 输入来源:全局 `09_接口资产清单.md`、分集 `04_资产引用.md`、分集 `05_Seedance视频提示词.md`。 +- 细则位置:上传和回填按 `references/image_generation_upload_rules.md`;上传后调用稿分支按 `references/post_upload_call_draft_rules.md`;API JSONL 多模态结构按 `references/api_payload_rules.md`。 +- 出口标准:最终 JSONL 不能只写内部 `@角色/@场景`,必须按规则包含真实图片 URL 或明确缺失警告。 + ### 6. 即梦资产提示词 即梦资产提示词集中输出到全局 `06_即梦资产提示词.md`。即梦中文理解强,但内容审核通常对血腥、色情、暴力、自残、违法、敏感政治、真实人物权益和未成年人风险更敏感。本工作流必须单独输出一版更温和的中文提示词: diff --git a/workflows/novel_to_seedance/references/api_payload_rules.md b/workflows/novel_to_seedance/references/api_payload_rules.md index cd6064e..edf0ff4 100644 --- a/workflows/novel_to_seedance/references/api_payload_rules.md +++ b/workflows/novel_to_seedance/references/api_payload_rules.md @@ -68,6 +68,19 @@ } ``` +## 上传后二次生成硬约束 + +`10_接口请求体.jsonl` 不能在图片上传前定稿。上传脚本完成后,AI 必须重新读取全局 `09_接口资产清单.md`,用其中真实可用的 `公网URL` 或 `asset://` URI 重新生成每集 JSONL。 + +硬性要求: + +- `@角色名`、`@场景名`、`@道具名` 只允许出现在文本说明、`source_ref` 或官网 Prompt 中,不能作为 API 图片输入。 +- API 图片输入必须写进 `content` 数组,字段形态为 `{"type":"image_url","image_url":{"url":"https://..."},"role":"reference_image"}`。 +- `image_url.url` 只能是 `https://...`、`http://...` 或 `asset://...`。不能是本地路径、`pending_upload`、`replace-with-uploaded-url`、空字符串或纯 `@素材名`。 +- 文本中的“参考图1/图2/图3”必须和 `content` 中图片对象顺序一致。 +- 资产未上传成功时,不要把它写入 `content[].image_url`;在同一 JSON 行增加 `asset_warnings`,列出缺失资产和状态。 +- 如果某个角色/场景在分集 `04_资产引用.md` 中出现,但 `10_接口请求体.jsonl` 没有对应图片 URL 或明确 `asset_warnings`,该集不合格。 + 建议每个片段生成如下草案。具体字段以你账号控制台文档为准: ```json diff --git a/workflows/novel_to_seedance/references/attachment_reference_rules.md b/workflows/novel_to_seedance/references/attachment_reference_rules.md index bf74993..bd56e7b 100644 --- a/workflows/novel_to_seedance/references/attachment_reference_rules.md +++ b/workflows/novel_to_seedance/references/attachment_reference_rules.md @@ -6,7 +6,7 @@ 在即梦官网、Seedance Web 或其他网页端: -- 必须手动上传图片/视频/音频附件。 +- 必须先生成并上传图片/视频/音频附件。图片资产优先按 `image_generation_upload_rules.md` 由 nano-banana-2 生成并上传。 - 提示词只能描述这些附件的用途,例如“参考已上传的角色图1作为姜尚离外貌”。 - 不要指望在纯文本里写 `@姜尚离` 就自动附带图片。 @@ -15,6 +15,7 @@ - 先上传图片或提供公网可访问 URL。 - 请求体里传 `image_urls`、`content` 或类似字段。 - 提示词里用平台规定的标签引用,例如 `@Image1`、`@Image2`。 +- 本地路径不能直接进入 API。图片上传接口返回的公网 URL 才能作为 `image_url.url`。 ## Web 官网手动上传写法 @@ -22,10 +23,10 @@ ```text 附件: -图1:姜尚离_百姓装_主形象.png -图2:霍念_粗布农夫_主形象.png -图3:伯府狗洞_泥地_场景.png -图4:旧玉佩_道具.png +图1:@姜尚离_百姓装_主形象 +图2:@霍念_粗布农夫_主形象 +图3:@伯府狗洞_泥地_场景 +图4:@旧玉佩_道具 提示词: 参考图1作为姜尚离的外貌、发型和服装;参考图2作为霍念的外貌和服装;参考图3作为场景空间和光线;参考图4作为旧玉佩道具。生成…… @@ -77,3 +78,4 @@ 4. `官网提示词`:用“参考图1/图2/图3”写。 5. `API映射`:`@Image1 = @姜尚离_百姓装`。 6. `API提示词`:用 `@Image1/@Image2` 或“参考图1/图2”写。 +7. `公网URL`:来自上传接口的 URL;未生成或未上传时写明 `pending_generation / pending_upload`,不得假造 URL。 diff --git a/workflows/novel_to_seedance/references/image_generation_upload_rules.md b/workflows/novel_to_seedance/references/image_generation_upload_rules.md new file mode 100644 index 0000000..b4ddd03 --- /dev/null +++ b/workflows/novel_to_seedance/references/image_generation_upload_rules.md @@ -0,0 +1,140 @@ +# 图片生成与上传执行规则 + +## 目标 + +图片资产不能停留在提示词层。全局 `05_图片资产提示词.md` 和 `06_即梦资产提示词.md` 完成后,必须进入执行阶段: + +1. 从 `05_图片资产提示词.md` 选定 nano-banana-2 版参考图提示词。 +2. 生成 `image_generation_jobs.json`,每个需要后续视频引用的人物、场景、道具至少一条任务。 +3. 调用 nano-banana-2 图片模型真实出图。 +4. 保存本地图片文件。 +5. 调用图片上传接口,得到公网 URL。 +6. 把公网 URL、官网上传名和参考职责回填到 `08_附件清单.md`、`09_接口资产清单.md`、分集 `09_官网提示词.md` 和 `10_接口请求体.jsonl`。 + +## 生成任务格式 + +在运行目录下创建: + +```text +outputs/novel_video_runs/{输入项目名}/image_generation_jobs.json +``` + +内容必须是 JSON 数组: + +```json +[ + { + "internalRef": "@姜尚离", + "type": "character", + "promptName": "标准中景参考图", + "fileName": "jiang-shangli_mid_reference.jpg", + "jimengUploadName": "姜尚离_标准中景", + "aspectRatio": "3:4", + "contentType": "image/jpeg", + "referenceDuty": "锁定姜尚离的脸型、发型、固定识别点和当前阶段服装,不参考背景和姿势", + "prompt": "这里粘贴 nano-banana-2 版可直接出图提示词" + } +] +``` + +字段要求: + +| 字段 | 要求 | +| --- | --- | +| `internalRef` | 必须对应工作流内部 `@角色/@场景/@道具`。 | +| `type` | `character / scene / prop / transition_frame`。 | +| `promptName` | 使用 `主设定板 / 标准中景 / 面部特写 / 主视角宽幅 / 局部锚点` 等稳定名称。 | +| `fileName` | 必须可作为上传文件名,优先 `.jpg`。 | +| `jimengUploadName` | 即梦官网里的素材名,分集提示词必须用这个名字。 | +| `referenceDuty` | 写清参考图只负责什么、不能继承什么。 | +| `prompt` | 使用最终安全版 nano-banana-2 图片提示词,不使用 MJ 参数。 | + +## 执行脚本 + +本工作流提供批量脚本: + +```bash +node workflows/novel_to_seedance/scripts/generate_upload_assets.mjs \ + --jobs outputs/novel_video_runs/{输入项目名}/image_generation_jobs.json \ + --out-dir outputs/novel_video_runs/{输入项目名}/assets/generated \ + --manifest outputs/novel_video_runs/{输入项目名}/uploaded_assets.json +``` + +环境变量: + +| 变量 | 用途 | +| --- | --- | +| `GEMINI_API_KEY` 或 `GOOGLE_API_KEY` | nano-banana-2/Gemini 图片接口密钥。 | +| `NANO_BANANA_MODEL` | 图片模型 ID;默认 `nano-banana-2`,如果账号控制台给出精确模型名,以控制台为准。 | +| `IMAGE_UPLOAD_URL` | 图片上传接口;默认使用本项目固定上传 URL。 | + +上传接口请求体固定为: + +```json +{ + "file": "图片的base64字符", + "fileName": "77c5320f08e282f3220ea96a759288b.jpg", + "contentType": "image/jpeg", + "metadata": { + "uploadTime": "2025-06-18T06:52:02.292Z", + "originalSize": "66828" + } +} +``` + +脚本会自动计算 `uploadTime` 和 `originalSize`。上传响应如果包含 `url / fileUrl / data.url / data.fileUrl / result.url / result.fileUrl`,会写入 `publicUrl`;如果响应格式不同,保留原始响应并将状态标为 `uploaded_url_unparsed`,人工从 `uploadRawResponse` 提取链接后回填。 + +## 上传后回填规则 + +`uploaded_assets.json` 是机器生成记录,正式交付仍必须回填 Markdown 清单。 + +### 08_附件清单.md + +`本地路径/URL` 填写: + +```text +本地:outputs/novel_video_runs/{项目}/assets/generated/jiang-shangli_mid_reference.jpg +URL:https://... +``` + +### 09_接口资产清单.md + +`公网URL` 必须填写上传后的 URL。`官网上传名` 必须填写即梦素材名。`状态` 使用: + +- `generated_uploaded_ready` +- `generated_uploaded_url_unparsed` +- `pending_generation` +- `pending_upload` +- `failed_generation` +- `failed_upload` + +### 分集 09_官网提示词.md + +官网附件槽位必须写成已上传图片名: + +```text +附件: +图1:@姜尚离_标准中景 +图2:@伯府狗洞_主视角宽幅 + +提示词: +参考图1锁定姜尚离的外貌、发型和服装,不参考图中背景;参考图2锁定伯府狗洞的空间结构、泥地材质和冷天光…… +``` + +### 分集 10_接口请求体.jsonl + +`image_url.url` 优先使用上传后的公网 URL;如果后续进入火山素材库,再替换为 `asset://...`: + +```json +{ + "type": "image_url", + "image_url": { + "url": "https://..." + }, + "role": "reference_image" +} +``` + +## 视频生成资源说明 + +nano-banana-2 只作为图片资产生成资源使用,不把它当作视频生成资源。视频生成继续使用 Seedance 2.0 请求体和官网提示词;如果项目另接 Veo、可灵或即梦视频 API,需要新增独立视频模型适配规则,不能复用图片上传脚本替代视频任务创建。 diff --git a/workflows/novel_to_seedance/references/image_model_provider_rules.md b/workflows/novel_to_seedance/references/image_model_provider_rules.md index e35f26d..5aa5f7b 100644 --- a/workflows/novel_to_seedance/references/image_model_provider_rules.md +++ b/workflows/novel_to_seedance/references/image_model_provider_rules.md @@ -25,6 +25,10 @@ nano-banana-2版 适合高密度中文设定板、中文标签、多图组合、局部编辑和一致性扩展。写法上保留“一张图内包含多个视角/模块”和“简洁中文文本标签说明”。人物中景、面部特写、侧光和极端情绪参考图应写成单图单状态,避免拼贴过多导致后续视频参考不稳定。 +本工作流中 nano-banana-2 不是只输出提示词的名称,而是默认图片执行资源。完成提示词后,必须生成 `image_generation_jobs.json`,调用图片模型出图,再调用图片上传接口得到公网 URL。具体执行见 `image_generation_upload_rules.md`。 + +视频生成不使用 nano-banana-2。视频资源继续走 Seedance 2.0 官网/API 请求体;如需接 Veo、可灵或即梦视频 API,应新增独立视频适配规则。 + ## MJ 版 适合美术概念探索、角色气质、服化道氛围和风格化写实参考图。正文仍输出中文,末尾可添加 `--ar 16:9 --style raw --v 7`,人物单状态参考图可用 `--ar 3:4 --style raw --v 7`。如果中文标签渲染不稳定,把“配有中文文本标签说明”改成“整洁设计稿标签区,文字后期添加”。 @@ -45,5 +49,8 @@ nano-banana-2版 图片模型:both 图片模型可选:mj,nano-banana-2 资产提示词格式:高密度中文设定板 +图片生成执行:true +图片上传执行:true +视频生成模型:seedance-2.0 ``` diff --git a/workflows/novel_to_seedance/references/post_upload_call_draft_rules.md b/workflows/novel_to_seedance/references/post_upload_call_draft_rules.md new file mode 100644 index 0000000..789361c --- /dev/null +++ b/workflows/novel_to_seedance/references/post_upload_call_draft_rules.md @@ -0,0 +1,119 @@ +# 上传后调用稿分支规则 + +## 触发时机 + +本分支只在图片生成和上传完成后执行。 + +前置条件: + +- `uploaded_assets.json` 已生成。 +- `08_附件清单.md` 已回填本地路径、公网 URL、官网上传名和状态。 +- `09_接口资产清单.md` 已回填 `内部引用`、`官网上传名`、`公网URL` 或 `火山 asset URI`、`参考职责`、`状态`。 + +如果图片还没上传,或 `09_接口资产清单.md` 里仍没有可用 URL,不得把分集 `10_接口请求体.jsonl` 标成可调用版本。 + +## 输入文件 + +AI 在本分支必须重新读取: + +- 全局 `09_接口资产清单.md`:资产映射的唯一可信来源。 +- 分集 `04_资产引用.md`:本集实际使用的角色、场景、道具。 +- 分集 `05_Seedance视频提示词.md`:镜头动作、对白、声音、首尾帧和负面约束。 + +`uploaded_assets.json` 只作为核对来源。正式生成分集调用稿时,以 `09_接口资产清单.md` 为准。 + +## 输出文件 + +本分支覆盖或新建每集: + +- `09_官网提示词.md` +- `10_接口请求体.jsonl` + +这两个文件必须在上传完成后重新生成。不能沿用上传前只包含内部 `@角色/@场景` 的草稿。 + +## 官网提示词生成规则 + +`09_官网提示词.md` 每个片段必须包含: + +- 上传素材表:图号、官网上传名、内部引用、公网 URL/asset URI、参考职责、状态。 +- 官网 Prompt:开头使用 `@官网上传名`,不是内部 `@角色名`。 +- 参考职责:写清参考图 1/2/3 分别锁定什么,不参考什么。 + +未上传成功的资产只能出现在素材表里,状态写清 `pending_upload / generated_uploaded_url_unparsed / failed_upload`,不能写进官网 Prompt 开头的 `@` 列表。 + +## JSONL 生成规则 + +`10_接口请求体.jsonl` 每行一个片段任务。 + +每行必须包含: + +- `episode` +- `shot_id` +- `model` +- `content` +- `parameters.duration` +- `parameters.resolution` +- `parameters.aspect_ratio` +- `callback_url` + +`content` 必须是多模态数组: + +1. 先放本片段可用的参考图片: + +```json +{ + "type": "image_url", + "image_url": { + "url": "https://example.com/asset.jpg" + }, + "role": "reference_image", + "name": "Image1", + "source_ref": "@角色名", + "reference_duty": "锁定角色外貌、发型和服装,不参考原图背景和姿势" +} +``` + +2. 再放文本提示词: + +```json +{ + "type": "text", + "text": "参考图1锁定角色外貌、发型和服装,不参考背景或姿势;参考图2锁定场景空间和光线。中景,角色推门进入,10秒,9:16..." +} +``` + +## 禁止项 + +- 不得把 `@角色名`、`@官网上传名`、本地路径或 `pending_upload` 写进 `image_url.url`。 +- 不得使用 `https://replace-with-uploaded-url`、`https://example.com/...` 这类占位 URL 作为最终 JSONL。 +- 不得只有文本 `content`,却在文本里写“参考 @角色”。只要本片段需要角色/场景一致性,就必须传真实图片 URL。 +- 不得让文本里的“参考图1/图2/图3”和 `content` 里的图片顺序不一致。 + +## 缺失资产处理 + +如果某个片段需要的资产未上传成功: + +- 不要把该资产写入 `content[].image_url`。 +- 在同一 JSON 行增加 `asset_warnings`。 +- `asset_warnings` 写明 `internalRef`、当前状态和缺失原因。 + +示例: + +```json +"asset_warnings": [ + { + "internalRef": "@场景名", + "status": "pending_upload", + "reason": "09_接口资产清单.md 中没有可用公网 URL 或 asset:// URI" + } +] +``` + +## 出口检查 + +本分支完成后必须检查: + +- 每行 JSON 都能被解析。 +- 所有 `content[].image_url.image_url.url` 都是 `http://`、`https://` 或 `asset://`。 +- 没有 `pending_upload`、本地路径、占位 URL 出现在 `image_url.url`。 +- 每个分集 `04_资产引用.md` 中的核心角色/场景,要么已经进入 JSONL 图片输入,要么出现在 `asset_warnings`。 diff --git a/workflows/novel_to_seedance/scripts/generate_upload_assets.mjs b/workflows/novel_to_seedance/scripts/generate_upload_assets.mjs new file mode 100644 index 0000000..c876177 --- /dev/null +++ b/workflows/novel_to_seedance/scripts/generate_upload_assets.mjs @@ -0,0 +1,208 @@ +#!/usr/bin/env node + +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import path from "node:path"; + +const DEFAULT_UPLOAD_URL = + "https://lms.laitool.cn/lms/FileUpload/FileUpload/01564ca6c907eab817058e5356bb40b57cd3f3171adbbc9ea49be0de448cc68e"; + +function argValue(name, fallback = undefined) { + const index = process.argv.indexOf(name); + if (index === -1 || index + 1 >= process.argv.length) return fallback; + return process.argv[index + 1]; +} + +function requireArg(name) { + const value = argValue(name); + if (!value) { + throw new Error(`Missing required argument: ${name}`); + } + return value; +} + +async function readJson(filePath) { + return JSON.parse(await readFile(filePath, "utf8")); +} + +function sanitizeName(value) { + return String(value) + .trim() + .replace(/[\\/:*?"<>|]+/g, "_") + .replace(/\s+/g, "_") + .slice(0, 120); +} + +function extensionFromMime(contentType) { + if (contentType === "image/png") return ".png"; + if (contentType === "image/webp") return ".webp"; + return ".jpg"; +} + +function extractInlineImage(response) { + const candidates = response?.candidates ?? []; + for (const candidate of candidates) { + const parts = candidate?.content?.parts ?? []; + for (const part of parts) { + const inlineData = part.inlineData ?? part.inline_data; + if (inlineData?.data) { + return { + base64: inlineData.data, + contentType: inlineData.mimeType ?? inlineData.mime_type ?? "image/jpeg", + }; + } + } + } + throw new Error("Image model response did not contain inline image data."); +} + +function extractUploadUrl(resultText) { + try { + const json = JSON.parse(resultText); + return ( + json.url ?? + json.fileUrl ?? + json.fileURL ?? + json.data?.url ?? + json.data?.fileUrl ?? + json.data?.fileURL ?? + json.data?.path ?? + json.result?.url ?? + json.result?.fileUrl ?? + "" + ); + } catch { + const match = resultText.match(/https?:\/\/[^\s"'<>]+/); + return match?.[0] ?? ""; + } +} + +async function generateImage({ prompt, model, apiKey, aspectRatio, contentType }) { + const endpoint = `https://magic666.top/v1beta/models/${encodeURIComponent( + model, + )}:generateContent?key=${encodeURIComponent(apiKey)}`; + + + const response = await fetch(endpoint, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + contents: [{ role: "user", parts: [{ text: prompt }] }], + generationConfig: { + responseModalities: ["IMAGE"], + ...(aspectRatio ? { aspectRatio } : {}), + }, + }), + }); + + const responseText = await response.text(); + if (!response.ok) { + throw new Error(`Image generation failed: ${response.status} ${responseText}`); + } + + const image = extractInlineImage(JSON.parse(responseText)); + return { + base64: image.base64, + contentType: contentType ?? image.contentType, + }; +} + +async function uploadImage({ base64, fileName, contentType, uploadUrl }) { + const originalSize = Buffer.byteLength(Buffer.from(base64, "base64")).toString(); + const response = await fetch(uploadUrl, { + method: "POST", + headers: { "Content-Type": "application/json" }, + redirect: "follow", + body: JSON.stringify({ + file: base64, + fileName, + contentType, + metadata: { + uploadTime: new Date().toISOString(), + originalSize, + }, + }), + }); + + const resultText = await response.text(); + if (!response.ok) { + throw new Error(`Image upload failed: ${response.status} ${resultText}`); + } + + return { + raw: resultText, + url: extractUploadUrl(resultText), + }; +} + +async function main() { + const jobsPath = requireArg("--jobs"); + const outDir = path.resolve(argValue("--out-dir", "outputs/generated_assets")); + const manifestPath = path.resolve(argValue("--manifest", path.join(outDir, "uploaded_assets.json"))); + // const apiKey = process.env.GEMINI_API_KEY ?? process.env.GOOGLE_API_KEY; + + const apiKey = "sk-QLEqdjgAE457aFsxmvUHdoj0EnyBcFaX23I4DxCJ8bgnrJNw" + const model = argValue("--model", process.env.NANO_BANANA_MODEL ?? "nano-banana-2"); + const uploadUrl = argValue("--upload-url", process.env.IMAGE_UPLOAD_URL ?? DEFAULT_UPLOAD_URL); + + if (!apiKey) { + throw new Error("Set GEMINI_API_KEY or GOOGLE_API_KEY before running image generation."); + } + const jobs = await readJson(jobsPath); + if (!Array.isArray(jobs)) { + throw new Error("Jobs file must be a JSON array."); + } + + await mkdir(outDir, { recursive: true }); + const results = []; + + for (const [index, job] of jobs.entries()) { + if (!job.prompt) { + throw new Error(`Job ${index + 1} is missing prompt.`); + } + + const generated = await generateImage({ + prompt: job.prompt, + model: "gemini-3-pro-image-preview", + apiKey, + aspectRatio: job.aspectRatio, + contentType: job.contentType, + }); + + const fileName = + job.fileName ?? + `${String(index + 1).padStart(3, "0")}_${sanitizeName(job.internalRef ?? job.name ?? "asset")}${extensionFromMime( + generated.contentType, + )}`; + const localPath = path.join(outDir, fileName); + await writeFile(localPath, Buffer.from(generated.base64, "base64")); + + const uploaded = await uploadImage({ + base64: generated.base64, + fileName, + contentType: generated.contentType, + uploadUrl, + }); + + results.push({ + internalRef: job.internalRef ?? "", + type: job.type ?? "", + promptName: job.promptName ?? job.name ?? "", + fileName, + localPath, + contentType: generated.contentType, + publicUrl: uploaded.url, + uploadRawResponse: uploaded.raw, + jimengUploadName: job.jimengUploadName ?? path.parse(fileName).name, + referenceDuty: job.referenceDuty ?? "", + status: uploaded.url ? "uploaded" : "uploaded_url_unparsed", + }); + } + + await writeFile(manifestPath, `${JSON.stringify(results, null, 2)}\n`, "utf8"); + console.log(`Wrote ${results.length} uploaded asset records to ${manifestPath}`); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/workflows/novel_to_seedance/templates/episode_09_official_site_prompts.md b/workflows/novel_to_seedance/templates/episode_09_official_site_prompts.md index 5b4adaf..4936457 100644 --- a/workflows/novel_to_seedance/templates/episode_09_official_site_prompts.md +++ b/workflows/novel_to_seedance/templates/episode_09_official_site_prompts.md @@ -4,6 +4,8 @@ 先在官网上传全局资产图片,并把素材命名为下方“官网上传名”。如果官网支持 `@` 图片/素材,在提示词中直接 `@官网上传名`。 +本文件只能引用已经在 `../../09_接口资产清单.md` 标记为 `generated_uploaded_ready` 的图片资产。未生成或未上传的图片不得写入本集官网 Prompt。 + 本文件只写本集视频生成用的官网提示词,不重新生成角色、场景、道具资产提示词。资产来源为: - `../../04_人物场景道具设计.md` @@ -17,6 +19,12 @@ | 官网上传名 | 内部引用 | 上传文件 | 参考职责 | | --- | --- | --- | --- | +要求: + +- `官网上传名` 必须与即梦素材库里的名字完全一致。 +- `上传文件` 必须对应已上传公网 URL 或导入即梦后的素材,不写本地路径占位。 +- Prompt 开头必须使用 `@官网上传名`,不能只写工作流内部 `@角色名`。 + ### 官网 Prompt ```text diff --git a/workflows/novel_to_seedance/templates/episode_10_api_payloads.jsonl b/workflows/novel_to_seedance/templates/episode_10_api_payloads.jsonl index 76d5910..6186463 100644 --- a/workflows/novel_to_seedance/templates/episode_10_api_payloads.jsonl +++ b/workflows/novel_to_seedance/templates/episode_10_api_payloads.jsonl @@ -1 +1 @@ -{"episode":"EP01_中文集名","shot_id":"EP01_S01","model":"doubao-seedance-2-0-pro","content":[{"type":"text","text":"参考图1锁定@角色名的外貌、发型和服装,不参考图中背景;参考图2锁定@场景名的空间结构、材质和光线;参考图3锁定@道具名的外观和尺寸。{景别/镜头类型},{主体 + 核心动作 + 场景},12s,9:16,电影级写实。首帧:{首帧状态}。镜头:{一个主要镜头运动或固定镜头;起点 -> 运动 -> 终点}。时间节拍:0-3s {动作1};3-7s {角色说出完整对白};7-12s {听者反应和动作结果}。对白/口型/表演:{写入本段由 Seedance 生成的完整对白;说明说话人、语气、语速、口型、停顿、听者反应、视线、呼吸、手部动作、身体重心变化}。声音:{Seedance 生成同期对白、VO、OS/环境音、音乐;如无则写无 VO、无背景音乐,只保留环境音}。环境:{灯光、材质、背景运动}。一致性:角色脸、服装、道具外观、场景空间保持参考图一致。尾帧:{最后0.5秒状态}。约束:不要字幕,不要把对白文字显示在画面里,不要水印,不要新增人物,不要变脸,不要夸张口型,不要口型机械开合,不要多余旁白,不要肢体畸变,不要闪烁。"},{"type":"image_url","image_url":{"url":"asset://replace-with-character-asset-id"},"role":"reference_image"},{"type":"image_url","image_url":{"url":"asset://replace-with-scene-asset-id"},"role":"reference_image"},{"type":"image_url","image_url":{"url":"asset://replace-with-prop-asset-id"},"role":"reference_image"}],"parameters":{"duration":12,"resolution":"720p","aspect_ratio":"9:16","return_last_frame":true},"callback_url":"https://your-domain.com/seedance/callback"} +{"episode":"EP01_中文集名","shot_id":"S01","model":"doubao-seedance-2-0-pro","content":[{"type":"image_url","image_url":{"url":"https://replace-with-uploaded-character-url"},"role":"reference_image","name":"Image1","source_ref":"@角色名","site_material_name":"角色名_标准中景","reference_duty":"锁定角色外貌、发型和服装,不参考原图背景和姿势"},{"type":"image_url","image_url":{"url":"https://replace-with-uploaded-scene-url"},"role":"reference_image","name":"Image2","source_ref":"@场景名","site_material_name":"场景名_主视角","reference_duty":"锁定场景空间、材质和光线方向,不改变角色身份"},{"type":"text","text":"参考图1锁定角色外貌、发型和服装,不参考图中背景或姿势;参考图2锁定场景空间、材质和光线方向。{景别/镜头类型},{主体 + 核心动作 + 场景},{时长}秒,{画幅},电影级写实。首帧:{首帧状态}。镜头:{镜头运动}。时间节拍:{动作顺序}。对白/口型/表演:{对白与表演}。声音:{环境音/对白/VO}。尾帧:{尾帧状态}。约束:不要字幕,不要水印,不要变脸。"}],"parameters":{"duration":12,"resolution":"720p","aspect_ratio":"9:16"},"callback_url":"https://your-domain.com/seedance/callback","asset_warnings":[]} diff --git a/workflows/novel_to_seedance/templates/global_05_image_asset_prompts.md b/workflows/novel_to_seedance/templates/global_05_image_asset_prompts.md index 8f0dae8..34f994a 100644 --- a/workflows/novel_to_seedance/templates/global_05_image_asset_prompts.md +++ b/workflows/novel_to_seedance/templates/global_05_image_asset_prompts.md @@ -2,6 +2,8 @@ 本文件集中输出本次输入的全部 MJ 版和 nano-banana-2 版图片资产提示词。分集目录不得再输出图片资产提示词,只能引用这里的资产。 +本文件不是图片资产链路的终点。完成提示词后,必须按 `references/image_generation_upload_rules.md` 生成 `image_generation_jobs.json`,执行 nano-banana-2 出图和上传,并把上传结果回填到 `08_附件清单.md` 与 `09_接口资产清单.md`。 + ## 语言与格式要求 - 统一管理全剧资产,不按集拆分。 @@ -144,6 +146,45 @@ - 分集 `04_资产引用.md` 只列出引用名,不复制本文件提示词。 - 分集 `05_Seedance视频提示词.md` 只通过 `@角色/@场景/@道具` 引用全局资产。 +- 分集 `09_官网提示词.md` 只能使用已上传图片的即梦官网素材名。 +- 分集 `10_接口请求体.jsonl` 只能使用上传后的公网 URL 或 `asset://` URI,不能使用本地路径。 + +## nano-banana-2 执行任务清单 + +完成本文件后,必须把可用于视频引用的 nano-banana-2 提示词整理为 `image_generation_jobs.json`。示例: + +```json +[ + { + "internalRef": "@角色名", + "type": "character", + "promptName": "标准中景参考图", + "fileName": "character_mid_reference.jpg", + "jimengUploadName": "角色名_标准中景", + "aspectRatio": "3:4", + "contentType": "image/jpeg", + "referenceDuty": "锁定角色脸型、发型、固定识别点和阶段服装,不参考背景和姿势", + "prompt": "粘贴最终 nano-banana-2 版标准中景参考图提示词" + }, + { + "internalRef": "@场景名", + "type": "scene", + "promptName": "主视角宽幅参考图", + "fileName": "scene_main_wide_reference.jpg", + "jimengUploadName": "场景名_主视角宽幅", + "aspectRatio": "16:9", + "contentType": "image/jpeg", + "referenceDuty": "锁定场景空间结构、固定锚点、材质和光线方向,不参考临时氛围变化", + "prompt": "粘贴最终 nano-banana-2 版主视角宽幅参考图提示词" + } +] +``` + +执行命令: + +```bash +node workflows/novel_to_seedance/scripts/generate_upload_assets.mjs --jobs outputs/novel_video_runs/{输入项目名}/image_generation_jobs.json --out-dir outputs/novel_video_runs/{输入项目名}/assets/generated --manifest outputs/novel_video_runs/{输入项目名}/uploaded_assets.json +``` ## 道具参考图 diff --git a/workflows/novel_to_seedance/templates/global_06_jimeng_asset_prompts.md b/workflows/novel_to_seedance/templates/global_06_jimeng_asset_prompts.md index 7caf169..c29b06f 100644 --- a/workflows/novel_to_seedance/templates/global_06_jimeng_asset_prompts.md +++ b/workflows/novel_to_seedance/templates/global_06_jimeng_asset_prompts.md @@ -2,6 +2,8 @@ 本文件集中输出本次输入的全部即梦安全版资产提示词。分集目录不得再输出即梦资产提示词,只能在官网提示词中列出需要上传的全局资产附件。 +即梦官网视频提示词不能只写内部 `@角色名`。必须先完成 nano-banana-2 出图和图片上传,把上传后的图片按 `09_接口资产清单.md` 中的“官网上传名”导入即梦,再在分集 `09_官网提示词.md` 中使用 `@官网上传名`。 + ## 即梦写法原则 - 使用中文为主,表达更温和。 @@ -101,6 +103,18 @@ 参考图1作为{角色}的外貌、发型和服装;参考图2作为{场景}的空间和光线。{视频动作描述}。自然克制的真实演员表演,不要夸张表情,不要文字,不要水印。 ``` +## 上传后即梦可调用格式 + +图片上传完成后,分集官网提示词必须使用以下格式: + +```text +@角色名_标准中景 @场景名_主视角宽幅 +参考图1锁定{角色}的外貌、发型、固定识别点和阶段服装,不参考图中背景或姿势;参考图2锁定{场景}的空间结构、固定锚点、材质和光线方向。 +{视频动作描述}。自然克制的真实演员表演,不要夸张表情,不要文字,不要水印。 +``` + +如果即梦官网不支持直接用 URL `@` 引用,则先把 `09_接口资产清单.md` 的公网 URL 下载或导入为官网素材,并保持素材名与“官网上传名”完全一致。 + ## 场景资产 - 风险等级: diff --git a/workflows/novel_to_seedance/templates/global_08_attachment_manifest.md b/workflows/novel_to_seedance/templates/global_08_attachment_manifest.md index dde4408..e891ae9 100644 --- a/workflows/novel_to_seedance/templates/global_08_attachment_manifest.md +++ b/workflows/novel_to_seedance/templates/global_08_attachment_manifest.md @@ -4,15 +4,26 @@ 本文件用于把工作流内部 `@角色/@场景/@道具` 映射到实际图片文件或 URL。网页端需要手动上传这些附件;API 端需要把这些文件 URL 放入请求体。 +图片必须先由 nano-banana-2 生成并上传。未上传前不得标记为可用于即梦或 API。 + ## 资产总表 -| 内部引用 | 类型 | 推荐文件名 | 本地路径/URL | 参考职责 | 首次使用集数 | -| --- | --- | --- | --- | --- | --- | +| 内部引用 | 类型 | 推荐文件名 | 即梦官网上传名 | 本地路径 | 公网URL | 参考职责 | 首次使用集数 | 状态 | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | ## 每集附件映射 ### EP01 -| 片段 | 官网附件槽位 | 上传文件 | API标签 | 内部引用 | 参考职责 | +| 片段 | 官网附件槽位 | 即梦官网上传名 | 公网URL/API标签 | 内部引用 | 参考职责 | | --- | --- | --- | --- | --- | --- | -| 1 | 图1 | | @Image1 | | 锁定外貌/服装/道具/场景,不参考无关背景或姿势 | +| 1 | 图1 | @角色名_标准中景 | https://... | @角色名 | 锁定外貌/服装/道具/场景,不参考无关背景或姿势 | + +## 状态规则 + +- `pending_generation`:已有提示词,未出图。 +- `pending_upload`:已有本地图片,未上传。 +- `generated_uploaded_ready`:已生成、已上传、已有公网 URL,可用于即梦和 API。 +- `generated_uploaded_url_unparsed`:上传成功但响应未解析出 URL,需人工回填。 +- `failed_generation`:图片生成失败。 +- `failed_upload`:图片上传失败。 diff --git a/workflows/novel_to_seedance/templates/global_09_api_asset_manifest.md b/workflows/novel_to_seedance/templates/global_09_api_asset_manifest.md index cc24844..daa6021 100644 --- a/workflows/novel_to_seedance/templates/global_09_api_asset_manifest.md +++ b/workflows/novel_to_seedance/templates/global_09_api_asset_manifest.md @@ -4,6 +4,8 @@ 本文件把内部引用映射到 API 可用的 URL 或 `asset://` URI,同时记录官网上传后的 `@素材名`。 +公网 URL 必须来自图片上传接口返回结果。不能把本地路径、提示词文件路径或未上传图片写入 API URL。 + ## 资产总表 | 内部引用 | 类型 | 推荐文件名 | 官网上传名 | 本地路径 | 公网URL | 火山 asset URI | 参考职责 | 状态 | @@ -12,6 +14,30 @@ ## 使用规则 - 官网:用 `@官网上传名`。 -- API:用 `公网URL` 或 `火山 asset URI`。 +- API:优先用上传后的 `公网URL`;如果后续进入火山素材库,再替换为 `火山 asset URI`。 - 本地路径不能直接进入 API。 - 每条任务必须在文本里写清参考职责,避免图片背景、角色姿势、道具用途被错误继承。 + +## 图片生成与上传记录 + +运行 `scripts/generate_upload_assets.mjs` 后,把 `uploaded_assets.json` 中的记录回填到本表: + +| 字段 | 回填位置 | +| --- | --- | +| `internalRef` | 内部引用 | +| `type` | 类型 | +| `fileName` | 推荐文件名 | +| `jimengUploadName` | 官网上传名 | +| `localPath` | 本地路径 | +| `publicUrl` | 公网URL | +| `referenceDuty` | 参考职责 | +| `status` | 状态 | + +允许的状态: + +- `pending_generation` +- `pending_upload` +- `generated_uploaded_ready` +- `generated_uploaded_url_unparsed` +- `failed_generation` +- `failed_upload`