# AICG3D 长时批量生成 · 经验与教训

> 来源：2026-09-28 ~ 09-29，《一直很安静》竖屏 MV 项目（45 镜 AI 生成 + 1 镜 ffmpeg 尾卡，连续运行约 24 小时）
> 任务量：45 个分镜，单镜约 28 分钟，总产出含重跑
> 记录人：DSH 会话（含用户多次指正）
> 结论一句话：**这次真正的坑只有两个——一个是我"优化"出来的（模型不卸载），一个是我写错的（提示词注入 bug）；其余都是参考图卫生与提示词写法问题。**

---

## 〇、最关键的 5 条（如果只读这一段）

| # | 教训 | 代价 |
|---|---|---|
| 1 | **不要"优化"工作流的内存管理**。把 `cleanup_after_run` 从工作流原值「卸载模型」改成「不处理」，导致 35GB 模型在 30GB 内存机器上跨镜累积，**当天整机硬挂 4 次** | 4 次硬挂 + 数小时重跑 |
| 2 | **生成的提示词必须与实际输入对账**。`build_prompt` 把「场景描述文本」当成「场景图列表」用，**每个提示词被注入 25~60 条引用不存在图片的 `<Picture N>` 声明** | 三人镜连续失效，排查数轮 |
| 3 | **参考图必须"净化"到单一主体、无文字**。整张三视图→拼贴；带「背面/BACK」标签的裁图→标签渗进画面 | 2 镜重跑 |
| 4 | **负面提示词会反向诱导**。写「不要悬浮的人脸」→ 脸变得更大；写「不要拼贴」→ 更容易拼贴 | 1 镜白跑 2 轮 |
| 5 | **长跑必须配自愈监督脚本**（探活→取证→拉起→断点续跑），而不是人盯着 | 否则 6 小时 timeout 后无人接管 |

---

## 一、机器稳定性：最贵的教训

### 1.1 事件经过

当天 GPU 机（RTX 5060 Ti 16GB / 30GB RAM）**硬挂 4 次**：02:26、05:04、11:33、约 16:10。症状均为：ping 100% 丢包、SSH `No route to host`、ARP `INCOMPLETE`，**且不触发 `kernel.panic` 自动重启，需人工断电**。

### 1.2 根因：我自己改的一个参数

AICG3D 工作流（`AICG3D-13-渲染器二采高清放大-可用版.json`）里，两个渲染节点的 `cleanup_after_run` 原值是 **「卸载模型」**。

我为了"避免每镜重新加载模型、提速"，把它改成了 **「不处理」**。后果：

```
该工作流要加载：UNET 20GB + 文本编码器 15GB = 35GB
宿主机内存：30GB
「不处理」→ 模型跨镜持续驻留不释放 → 内存压力累积 → 整机冻结
```

**时间线完全吻合**：

```
03:49-04:17  S01（用「卸载模型」）        → ✅ 成功
04:17        我改成「不处理」             ← 错误决定
04:18-04:50  S02                          → 勉强通过
04:50-05:04  S03                          → ❌ 硬挂（第 2 次）
11:29-11:33  S03 重跑                     → ❌ 硬挂（第 3 次，仅 3 分钟）
（改回「卸载模型」后）
12:16-次日   连续 24 小时+ / 45 镜        → ✅ 零崩溃，内存压力告警 0
```

### 1.3 排查中走过的弯路

**第 3 次硬挂后我一度断言**："内存压力是元凶，加了 `--disable-pinned-memory` 就能稳定跑完"。**这个结论被证伪**——第 3 次硬挂恰恰发生在该参数已生效、内存压力告警为 0 的情况下。

**教训**：不要把单次相关性当因果。当时唯一抓到的内核异常是内存压力，但**真正的原因（模型不卸载）我还没意识到**。正确说法应是"内存压力是一个可疑因素，但不足以解释全部"。

### 1.4 崩溃取证（这次第一次拿到现场）

运维记录里长期写着"这台机器没有崩溃捕获能力"。**这个结论是错的**：`/var/log/journal` 是持久化的，`journalctl --list-boots` 能列出历史启动，`journalctl -b -1` 能读上一次启动的完整内核日志。

**关键做法：在机器恢复的"第一时间"抓**（错过就被新启动的日志冲淡）。已封装为 `~/.tools/capture_after_recovery.sh`，抓 9 项：

```
1. uptime / who / last -x reboot（判断自动重启还是人工断电）
2. 上一次启动的完整内核日志 journalctl -k -b -1
3. 上次启动的 Xid|nvrm|gsp|fell off|pcie|aer|oom|hung_task|BUG:|panic
4. 当前启动的内核错误
5. journald 是否持久化（-b -1 是否可用）
6. nvidia-smi -q 的 PCIe 代际/降速、Retired/Remapped 页、温度、功耗上限
7. NVreg_DynamicPowerManagement 与 kernel.panic* 设置
8. ComfyUI 日志尾部（死亡时的最后动作）
9. ram_guard 痕迹与日志轮转情况
```

**实际抓到的东西**（第二次硬挂）：

```
04:19:31 systemd-journald: Under memory pressure, flushing caches.
04:56:30 systemd-journald: Under memory pressure, flushing caches.
05:04:39 ← 日志戛然而止：无 panic、无 Xid、无 OOM、无正常关机
```

**「日志直接断掉」= 内核冻结**，这就是"硬挂"的定义。

### 1.5 ComfyUI 启动参数的取舍

```bash
cd /home/zyw/ComfyUI && setsid nohup env \
  PYTORCH_CUDA_ALLOC_CONF=max_split_size_mb:512,expandable_segments:True \
  CUDA_VISIBLE_DEVICES=0 OMP_NUM_THREADS=4 \
  venv/bin/python main.py --listen 0.0.0.0 --port 8189 \
  --lowvram --force-fp16 --reserve-vram 1.0 --fast-disk \
  --cache-lru 2 --disable-pinned-memory --preview-method none \
  > /home/zyw/comfyui_8189_opt.log 2>&1 < /dev/null &
```

- `--disable-pinned-memory`：**必须有**。ComfyUI 默认会按公式 pin 住约 27GB「不可换页」宿主内存，而机器只有 30GB（09-15 取证报告的结论）
- `--cache-lru 2`：降缓存占用
- `--fast-disk`：偏向磁盘后备、少占非 pinned RAM

---

## 二、提示词生成的代码缺陷（影响全部 45 镜）

### 2.1 缺陷本体

```python
# 调用点
chars, scene, sdesc, motion, sound = SHOTS[sid]
prompt = build_prompt(sid, chars, sdesc, motion, sound)
#                                ^^^^^ 第 3 个位置传的是「场景描述文本」
```
```python
def build_prompt(sid, chars, scene, motion, sound):
    ...
    for j in range(len(scene)):        # scene 这里是字符串，len() = 文本字数
        defs.append(f"<Picture {len(chars)+j+1}> 是 [镜头1] 的环境与光照依据。")
```

### 2.2 后果

场景描述有多少字，就往提示词里塞多少条「环境依据」图片声明。实测**每个提示词被注入 25~60 条引用不存在的图片**，而实际只传了 2~4 张图。

模型的反应：把参考图当作**独立元素去拼贴**，而不是组合进同一个场景。表现为三人镜「**某个角色整个消失**」或「三人被生硬拼贴、比例悬殊」。

### 2.3 修法

```python
def build_prompt(sid, chars, scene_imgs, sdesc, motion, sound):
    """scene_imgs = 场景图路径列表；sdesc = 场景描述文本（两者必须分开）"""
```

**验证方法（应固化为习惯）**：生成提示词后，**数一遍 `<Picture N>` 声明的条数，必须等于实际传入的图片数**。

```python
# 自检
decl = [l for l in prompt.split('\n') if l.startswith('<Picture')]
assert len([d for d in decl if '环境依据' in d]) == len(scene_imgs)
```

---

## 三、参考图卫生（模型会照抄素材里的一切）

### 3.1 三类事故

| 事故 | 素材问题 | 现象 |
|---|---|---|
| **拼贴** | 用整张**三视图**（含正面/侧面/背面）作参考 | 画面出现「大幅正面像里嵌着缩小背影」 |
| **标签渗出** | 裁三视图时把「**背面 / BACK**」中英文标签一起裁进去 | 成片画面上方出现「背面 / BACK」文字 |
| **主体冲突** | 用**正面**参考图，但镜头要求「走离、只给背影」 | 天空出现**巨大悬浮人脸** |

### 3.2 规律

> **参考图里的任何文字、标注、多余主体，模型都可能原样复制到成片里。**
> **参考图的视角必须与镜头的机位一致。**

### 3.3 处置规范

1. **一人一图，只保留单一主体**：从三视图裁出「单人正面全身」「单人侧面全身」「单人背面全身」分别存档
   ```
   人物参考/正面单图/<角色>_front.jpg
   人物参考/侧面单图/<角色>_side.jpg     ← 记得裁掉顶部 8% 的「侧面/SIDE」标签
   人物参考/背影单图/<角色>_back.jpg     ← 同样裁掉「背面/BACK」标签
   ```
2. **按镜头机位选图**：
   - 正面/中景 → 正面图
   - **走离、背对镜头 → 不要用正面图**（会给天空补脸）。改用**侧面图 + 把镜头改成「正侧面横向跟拍」**，给模型一个合法的角度呈现身份
   - 转身走离 → 背面图，并在提示词里留出「侧后方、带到侧脸轮廓」的出口
3. **裁图后必须人眼验收**：确认无标签、无邻图残影、无印章

---

## 四、提示词写法（负面句会反向诱导）

### 4.1 实测证据

| 我写的 | 结果 |
|---|---|
| 「只给背影」+ 加「**不要出现悬浮的人脸、人像叠影、或天空中的面孔**」 | 浮脸**变得更大更清晰** |
| 「画面中只出现这一个实例，**不要出现第二个身影、不要拼贴、不要镜像**」 | 拼贴概率似乎上升 |
| 「画面只见纸张的**背面**」（想规避文字） | 模型仍渲染纸张正面并生成乱码汉字 |

### 4.2 规范

- **只写"要什么"，不写"不要什么"**。需要排除的内容，用**正向的空间描述**替代：
  - ❌「不要出现悬浮的人脸」 → ✅「镜头自始至终只拍她的侧影」（并配合侧面参考图）
  - ❌「不要出现文字」 → ✅「画面只见纸张的背面与木柱纹理」（但注意这条对"告示"类词无效，见下）
- **`STYLE` 里的通用否定（不要文字/水印/现代元素）保留即可**，但对**具体名词**（告示、符文、牌匾）无效

### 4.3 某些词必然生成文字，只能绕开或后期处理

「告示 / 符文 / 牌匾 / 匾额」→ 模型**一定**生成汉字，且几乎都是乱码。

三种处置（按优先级）：
1. **取用无文字的时间段**：S08 前 1 秒红纸有乱码，1.2 秒后纸被风吹落、柱面干净 → **合成时从 1.2s 起截取**
2. **后期叠加真实文字**（ffmpeg `drawtext`）——符合原定原则「文字后期叠加，不让图像模型生成」
3. **接受**：发光符咒中的伪文字在仙侠题材里属可接受表现手法（本片 S25 塔壁符文、S36 横幅小字即按此保留）

---

## 五、多角色镜头（≥3 个主体）

### 5.1 症状

三人 + 场景（4 张参考图）时，模型会把每个 `<Subject>` 当成要"展示"的独立元素**拼贴**，而不是组合进同一场景。

### 5.2 三处修法（组合使用）

1. **修掉 §2 的注入 bug**（这是主因）
2. **把场景图排到 `<Picture 1>`**，人物参考依次排后 —— 先给场景锚点
   ```python
   def scene_first(sid, chars, scene):
       return len(chars) >= 3 and len(scene) >= 1
   ```
   注意：`subject_definitions` / `retention_analysis` 里的 `<Picture N>` 编号与 `<Subject N>` 编号要同步重排
3. **在 `detailed_description` 里显式点名每个人物的位置**
   ```
   画面中三个人物同时出现在同一镜头里：
   <Subject 2>（少年剑客）与 <Subject 3>（蓝衣少女）并肩走在画面前景、身体微微相靠；
   <Subject 1>（紫衣少女）走在他们身后数步、位于画面左侧的中景处，处于浅景深之外、略微虚焦。
   三人的身高比例一致。
   ```

**实测**：只做 1+2 就能让 S16/S25 通过；S12/S13 加上 3 后也通过。

### 5.3 角色列表必须与动作设计一致

S19 的动作设计**只描述了狐妖女**，但我误把林月如也放进了 `chars` → **林月如喧宾夺主，狐妖女被挡**。

> **镜头里不该出现的人，绝不能放进 `chars`。**

**建议固化为审计**（本次就是这么抓出来的）：

```python
for sid in SHOT_ORDER:
    chars, scene, sd, mo, so = SHOTS[sid]
    hit = []
    if '紫衣少女' in mo: hit.append('L')
    if '少年' in mo or '剑客' in mo: hit.append('X')
    if '蓝衣少女' in mo: hit.append('Z')
    if chars and not hit: print(f'{sid} ⚠️ 传了角色但动作里没提到')
    if not chars and hit: print(f'{sid} ⚠️ 没传角色但动作里提到了')
```
（注意：用「三人」「两名主角」这类泛称时会误报，需人工复核）

---

## 六、工程化：让长跑不需要人盯

### 6.1 自愈监督脚本

`~/.tools/mv_aicg3d_supervisor.sh` 的循环：

```
每 60s 一轮：
  探活 ping
    ├─ 不可达 → 标记 WAS_DOWN=1，等 60s 重试
    └─ 可达
         ├─ WAS_DOWN=1 → **第一时间跑 capture_after_recovery.sh 取证**
         ├─ ComfyUI 未就绪 → 自动拉起（含正确参数）
         └─ 断点续跑 mv_aicg3d.py
```

**本次实测有效两次**：

```
05:03:21  批次被 6h timeout 杀掉（rc=124）
05:04:21  监督脚本第 93 轮：已完成 19/45 → 自动重启批次
05:04:22  按顺序跳过已完成，补跑 S15

11:04:21  批次被 6h timeout 杀掉（rc=124）
11:05:21  监督脚本第 94 轮 → 自动重启
11:05:22  补跑 S24（用去标签版参考图）

17:05     同样机制补跑 S34
```

> **配合 `timeout 21600`（6 小时）使用**：批次定期被杀重启，既避免单次运行过长，又**自动捡起所有缺失镜头**——这成了本次的自愈核心。

### 6.2 断点续传

启动时先 `ssh` 列一遍 GPU 机产出目录，**已有的直接下载跳过，不重复提交**：

```python
def scan_remote_existing():
    """扫 GPU 机 output/video 里已有的 AICG3D_Pass2_<镜号>_*.mp4，返回 {镜号: 文件名}"""
    ...
```

**注意**：若要**重跑某镜**，必须同时删除
① 本机产出 ② 运行日志里的记录 ③ **GPU 机上的产出文件** —— 否则续传扫描会把它重新下载回来、跳过重跑。

### 6.3 产出下载的两个坑

**坑 1：AICG3D 的 `SaveVideo` 在 `/history` 里把结果放在 `images` 键**，不是 `videos`/`gifs`：

```python
for key in ("images", "videos", "gifs", "animated"):
    for v in (out.get(key) or []):
        if isinstance(v, dict) and v.get("filename"):
            files.append(v)
```

**坑 2：等待上限太小会漏文件**。S24 生成超过 40 分钟，超出 `wait_and_fetch(pid, timeout_min=40)`，批次记成 `FAIL 1files`，**只下载了 Pass1、Pass2 留在 GPU 上**。

> 修法：上限提到 **90 分钟**；并校验**下载文件数是否等于预期的 SaveVideo 节点数**（本工作流是 2）。

### 6.4 重跑单镜的正确姿势

```
1. 确认 ComfyUI 队列为空（等间隔期，不要在采样中途动手）
2. 移走本机产出到 问题记录/
3. 从 运行日志.tsv 删掉该镜记录
4. 删掉 GPU 机上的产出文件
5. kill 批次进程（监督脚本会在下一轮自动重启并补跑该镜）
   ⚠️ 一定按 PID kill，不要用 pkill 模式
```

**不要清空 ComfyUI 队列**——那会强制 35GB 模型重新加载，制造内存/IO 尖峰（这正是第 3 次硬挂的可疑诱因）。

---

## 七、操作纪律：我在本次犯的错

| # | 错误 | 后果 | 正确做法 |
|---|---|---|---|
| 1 | 为"提速"改 `cleanup_after_run` | 4 次硬挂 | **不要动工作流已调好的内存管理参数** |
| 2 | 过早宣布根因（"内存压力+缺 pinned 参数"） | 结论被第 3 次硬挂证伪，需公开更正 | 区分"相关"与"因果"；标注置信度 |
| 3 | 为换参考图**清空 ComfyUI 队列** | 强制模型重载，疑为硬挂诱因 | 改动一律等**批次间隔期** |
| 4 | `pkill -f "mv_aicg3d_superviso[r].sh"` | **匹配到自己的命令行、把自己杀了**，新旧监督脚本一起死 | 按 **PID** kill；或确保模式不出现在自己的命令行里 |
| 5 | 一开始只挂了一个 SaveVideo | 用户要求"一采也要落盘"后才补 | 先对齐工作流的完整节点结构（工作流有 2 个 SaveVideo） |
| 6 | 为修缺陷**多次重启批次** | 拖长了单镜完成时间 | **先攒够问题，再一次性处理** |
| 7 | 漏了 `--disable-pinned-memory` | 前两次硬挂时该参数缺失 | 启动参数对照运维记录的修复项逐条核对 |

---

## 八、可复用的参数与命令

### 8.1 生成参数（本次实测可用）

| 项 | 值 |
|---|---|
| 工作流 | `AICG3D-13-渲染器二采高清放大-可用版.json` |
| 模式 | `reference`（REF2VA） |
| 分辨率 | `768P` / `9:16` / 768×1344 → Pass2 ×1.2 → **928×1664** |
| 帧率 | 24 fps |
| 时长 | 5 秒 |
| Pass1 | `res_multistep` + `simple`，**3 步**，`denoise 1.0` |
| Pass2 | `res_multistep` + `simple`，**8 步**，`second_pass_denoise 0.5` |
| 潜空间放大 | `minimax_h3_latent_upscaler_3d_fp16.safetensors`，`upscale_scale 1.2` |
| `cleanup_after_run` | **卸载模型**（两个节点都要） |
| LoRA | `minimax/taomate_h3_3step_comfy.safetensors`，strength 1.0 |
| 模型 | UNET `minimax_h3_{fl2va,ref2va}_pruned_int8_convrot` / 文本编码器 `qwen3vl_32b_minimax_h3_nvfp4_awq` |
| 单镜耗时 | 约 **28 分钟** |

### 8.2 关键 API 端点

```
POST /prompt          提交工作流（API 图）
GET  /history/<pid>   查结果 → outputs[<node>].images[] ← **注意是 images**
POST /upload/image    上传参考图
GET  /view?filename=&subfolder=&type=output   下载产出
POST /interrupt       中断当前任务
POST /queue {"clear":true}   清空队列（⚠️ 慎用，见 §6.4）
GET  /system_stats    探活
GET  /object_info/<节点>  查节点输入规格
```

### 8.3 合成（ffmpeg）

```bash
# 逐镜：统一 1080x1920 + 按目标时长裁切（S08 从 1.2s 起）
ffmpeg -y -nostdin -i "$f" \
  -vf "scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,fps=30,\
trim=start=${off},setpts=PTS-STARTPTS,tpad=stop_mode=clone:stop_duration=20" \
  -t "$dur" -an -c:v libx264 -crf 16 -preset veryfast -pix_fmt yuv420p "$out"

# 拼接
ffmpeg -y -f concat -safe 0 -i concat.txt -c copy v_noa.mp4
# 混音乐
ffmpeg -y -i v_noa.mp4 -i "$SONG" -map 0:v:0 -map 1:a:0 -c:v copy -c:a aac -b:a 192k -shortest v_music.mp4
# 烧字幕
ffmpeg -y -i v_music.mp4 -vf "subtitles=字幕.srt:force_style='FontName=Source Han Serif SC,\
FontSize=17,PrimaryColour=&H00FFFFFF,OutlineColour=&H80000000,BorderStyle=1,Outline=2,Shadow=1,MarginV=150'" \
  -c:v libx264 -crf 17 -preset medium -c:a copy 成片.mp4
```

> ⚠️ **循环里的 ffmpeg 必须加 `< /dev/null`**（或 `-nostdin`），否则它会吃掉 `while read` 的 stdin，导致循环只处理前几条。

---

## 九、方法论反思

### 9.1 校验优先于生成

本次所有"跑很多轮才修好"的事故，**根源都是没有在下游对账上游**：

- 提示词 → 没数 `<Picture>` 条数（§2）
- 参考图 → 没验收裁图结果（§3）
- 产出 → 没核对文件数（§6.3）

**建议固化为 checklist**：

```
提交前：提示词里的 <Picture N> 条数 == 传入图片数？
        参考图无文字/标签/邻图？视角与镜头机位一致？
        chars 里的每个人都在动作设计里出现了？
提交后：SaveVideo 节点数 == 下载到的文件数？
        画面里有没有不该有的元素（文字/浮脸/多余人）？
```

### 9.2 批量复核优于逐镜盯

45 镜如果逐镜细看，成本极高。**有效做法是"分组抽帧做对比图"**：

```bash
# 每镜取 3 个时间点拼一行，多镜拼成一张总览
ffmpeg -pattern_type glob -i "$tmp/*.jpg" -filter_complex "tile=3x2:padding=5" -frames:v 1 复核.jpg
```

本次靠这个方法在半小时内复核完全部 45 镜，抓出 4 处缺陷。

### 9.3 缺陷清单要"活"

`待修复清单.md` 的格式（**问题 / 根因 / 修法 / 状态**）在整个过程中反复被引用，是有价值的。特别是：

- **已解决的也要留着**（避免重复踩同一个坑）
- **"观察不改"的也要记**（说明是有意识接受的，不是漏看）
- **要写清"为什么这么改"**，而不只是"改了什么"

### 9.4 承认并更正自己的结论

我在会话中一次把"内存压力"当成确定结论向用户断言，随后被证伪。**在取证报告里专设了「重要修正」一节**，写明原结论、被证伪的证据、以及新的假设。

> 这比悄悄改口更有价值——后来者能看到"这个假设已经被排除过"。

---

## 十、附：本次产出

| 内容 | 路径 |
|---|---|
| 成片 | `dl-hub/62-仙剑1MV素材/MV制作/MV-一直很安静-竖屏成片.mp4` |
| 完成报告 | `dl-hub/62-仙剑1MV素材/MV制作/竖屏MV制作完成报告.md` |
| 缺陷清单（含 8 处修复全过程） | `dl-hub/62-仙剑1MV素材/MV制作/AICG3D-竖屏分镜/待修复清单.md` |
| 缺陷证据与坏产物存档 | `dl-hub/62-仙剑1MV素材/MV制作/AICG3D-竖屏分镜/问题记录/` |
| 崩溃取证报告（含更正） | `dl-hub/06-ComfyUI-H3运维记录/崩溃取证/2026-09-28-首次崩溃取证报告.md` |
| 崩溃原始现场 | `dl-hub/06-ComfyUI-H3运维记录/崩溃取证/recovery_20260928_*.txt` |
| 监督脚本 | `~/.tools/mv_aicg3d_supervisor.sh` |
| 取证脚本 | `~/.tools/capture_after_recovery.sh` |
| 逐镜生成脚本 | `~/.tools/mv_aicg3d.py` |
| 合成脚本 | `~/.tools/mv_assemble_final.sh` |
