# H3 长视频分段合成方法（2026-09-14 实测跑通）

> 目标：把 H3 的单次短片（≤15.08s）拆成多段生成、再自动合并成长视频。
> 实测：3 段 × 5s → **15.5s / 768×1344 / 24fps / 372 帧**，全程本地 0 积分。

---

## 一、核心机制（官方 `docs/long-video.md`）

- **单段生成上限**：5s ~ **15.083s**（362 帧 @24fps）。长视频**不是**把单次上限调大，而是**分段按顺序生成再合并**。
- **项目上限** 1000 段（>4 小时）；跨项目继续合并还能更长。
- **区分两类时长限制**：362 帧只适用于 H3 **新生成**的段；用户上传的参考视频/音频仍受「每段 ≤15s、各自合计 ≤15s」约束。

### 四种续接方式（`continuation`）

| 方式 | 作用 | 消耗参考槽 | 备注 |
|---|---|---|---|
| `none` | 首段独立生成 | — | 每项目第一段 |
| `tail_frame` | 服务端从上一段**尾帧**提取，作为本段起始帧 | 1 个像素槽 | **必须配 FL2VA profile** |
| `previous_video` | 上一段的**永久视频**作为参考 | 1 个像素槽 | Ref2VA 可用 |
| `motion_context` | 复用上一段 H3 **音视频 latent** | **不消耗** | 仅 API spec（`project create`）支持，**acceptance manifest 不支持**；相邻段尺寸必须**完全一致**；保存前自动裁掉重复头部 |

Motion Context 参数：`video_frames` ∈ {5,22,39,56}（默认 22）、`audio_frames` 0–240（默认 24）。
它与 Base 断点续采互斥，不能同时开。

---

## 二、三步操作

### 1) 上传素材，拿 32 位 asset_id
```bash
h3ctl asset upload <文件> --output json        # asset 按内容哈希复用，重复上传返回同一 id
# 返回 data.uploads[0].result.asset.asset_id
```

### 2) 写 manifest（严格 v1 schema，拒绝未知字段）
完整示例（3 段，每段 5s，9:16）：
```json
{
  "version": 1,
  "project": {
    "title": "Armored warrior identity migration - 3 segment long video",
    "segments": [
      {
        "continuation": "none",
        "request": {
          "prompt": "<六段式提示词，见下方「提示词硬约束」>",
          "parameters": {"aspect_ratio": "9:16", "duration": 5, "steps": 4,
                         "lora_strength": 0.75, "seed": 20260916, "mode": "auto"},
          "profile_id": "minimax-h3-ref2va",
          "profile_version": "1.3",
          "profile_digest": "cda098452f5b138ad604289d6a4786ecef57b7f43ab12a3f3581f4eb224c2fe4",
          "references": [
            {"asset_id": "<角色图id>", "role": "identity"},
            {"asset_id": "<源片段id>", "role": "reference"}
          ]
        }
      },
      { "continuation": "previous_video", "request": { "...同上，换 seed/prompt/denoise..." } },
      { "continuation": "previous_video", "request": { "...第三段..." } }
    ]
  },
  "acceptance": {"width": 768, "height": 1344, "expect_audio": true,
                 "duration_tolerance": 0.5, "output_name": "out.mp4"}
}
```

**schema 硬性约束**：
- `segments` **最少 3 段**、最多 1000（少于此数直接校验失败）
- `continuation` 只能 `none` / `tail_frame` / `previous_video`
- `references[].role` ∈ `first_frame` / `last_frame` / `identity` / `style` / `composition` / `reference`（+ voice 类）
- `acceptance.expect_audio` 必须为 `true`（const）
- `duration` 最大 `15.083333333333334`，不要写更大的近似值
- `acceptance` 的 H3 尺寸：**16:9 = 1344×768；9:16 = 768×1344**
- `profile_id/version/digest` 必须取自当前 `/api/capabilities`

### 3) 跑（两种等价路径）
```bash
# A. 一键成片（推荐）
h3ctl video compose --spec manifest.json --to final.mp4 --timeout 0

# B. 分步（可断点恢复；连接断开不影响服务端项目）
h3ctl project create --spec project-only.json     # 只要 project 对象
h3ctl project run    <PROJECT_ID>
h3ctl project wait   <PROJECT_ID> --timeout 0
h3ctl project merge  <PROJECT_ID>
h3ctl project download <PROJECT_ID> --to final.mp4
```

离线自检（不联网、不提交）：
```bash
python3 -m scripts.long_video --manifest manifest.json --output-dir out --dry-run
```

---

## 三、本次踩到的 3 个坑（都在 `create` 阶段暴露）

### 坑 1：派生引用项目**禁止字面媒体标签** ⭐ 最关键
```
{"code":"unstable_reference_tag",
 "message":"derived-reference prompts cannot contain literal <Picture N>, <Video N>, or <Audio N> tags;
            use a stable @{asset-id} alias"}
```
长视频/迁移项目里，源视频是**运行时派生资产**，标签要到每段提交时才分配，所以提示词里**不能预写** `<Picture 1>` / `<Video 1>` / `<Audio 1>`：
- 持久图片 → 用 **`@{asset-id}`** 别名（如 `@{66bdff...}`）
- 源视频 → 写成中性的 **"the source video reference"**（执行器会注入正确绑定）
- `<Subject N>` **可以保留**（它是内容标签，不是媒体标签）

> 这也解释了为什么 `h3ctl generate video`（直接生成）能用 `<Picture 1>`，而走 project 的路径全都卡在 `create`。

### 坑 2：`tail_frame` 需要 FL2VA profile
```
{"code":"continuation_profile","message":"tail_frame requires an FL2VA profile"}
```
尾帧续接本质是"首帧驱动"，必须用 `minimax-h3-fl2va`（version 1.2）。若想全程用 `minimax-h3-ref2va`，第二段起改用 `previous_video`。

### 坑 3：dry-run 通过 ≠ 服务端通过
`python3 -m scripts.long_video --dry-run` 只做 manifest 校验；服务端的**语义校验**（标签、profile/续接匹配、尺寸一致等）更严。
**建议**：正式跑之前先手动 POST 一次看错误：
```bash
curl -s -X POST http://127.0.0.1:6020/api/video-projects \
  -H 'Content-Type: application/json' -d @project-only.json
```

---

## 四、本次实测数据

| 项 | 值 |
|---|---|
| 段数 / 每段 | 3 段 × 5s（`none` / `previous_video` / `previous_video`）|
| profile | `minimax-h3-ref2va` v1.3 |
| 输出 | **768×1344 / 24fps / 372 帧 / 15.5s** |
| 总耗时 | **约 37 分钟**（21:15 → 21:52，含每段重新加载 13GB 模型）|
| 显存峰值 | 14480 MiB / 16311 MiB（余量很小）|
| 成本 | **0 积分**（本地）|

**注意**：三段串行比单段线性叠加更慢（每段都要重载模型 + 续接段要额外吃上一段视频作参考）。

---

## 五、待调优：身份漂移

本次成品**最后一段的后半部分身份漂移回了原人物**（出现原女孩的脸/白背心）。可能原因与对策：

1. **段2 `denoise 0.7` + references 同时挂源片段** → 源身份渗回。
   → 续接段建议**去掉原源片段 reference**，只保留 `identity` 角色图 + 由 `previous_video` 提供动作。
2. 续接段可提高角色图权重（目前 `ref_image_size: match`），或换用 `motion_context`（latent 级续接，不经过像素参考）。
3. 逐段复核：先用 `project run --segment <ID>` 单段重跑，确认后再 merge —— 避免整条重来。

---

## 六、相关文档

- `docs/long-video.md`（分段编排与验收）
- `docs/motion-context-long-video.md`（Motion Context 安装与项目合同）
- `scripts/long_video/example-manifest.json` / `manifest.schema.json`
- `skills/h3-character-migration/SKILL.md`（含"运行时 prompt 不得预写媒体标签"这条硬约束）

---

## 七、实战经验：project 编排不稳时的替代路径（2026-09-14 实测跑通）

做 15.7s 长视频时 `project run/wait/merge` **连续失败两次**，改用**单段独立生成 + ffmpeg 拼接**一次成功。

### 7.1 project 流程的两个坑

**坑 A：提交超时误判（"假失败"）**
段状态 = `failed / generation submission failed / prompt_id: None`，但 ComfyUI 日志明确写着
`Prompt executed in 473.55 seconds`，产物也已落盘。
根因在 `server/app.py:423`：
```python
except Exception:
    job.update({"status": "failed", "message": "generation submission failed", "error_code": "internal_error"})
```
即 `comfy_tasks.schedule()` 在**等待 ComfyUI 的提交响应**时抛异常 → 判失败；而 ComfyUI 其实收到并跑完了。
- **触发条件**：ComfyUI 刚重启，首次要加载 13GB 模型，`POST /prompt` 响应太慢。
- **对策**：先跑一个短任务**预热**（让模型驻留显存），再提交长任务。
- **兜底**：成品仍可从 `ComfyUI/output/h3-studio/videos/<job_id>_00001_.mp4` **手动捞回**（本次段 0 就是这么救回来的）。

**坑 B：链路脆弱**
project 里各段由服务端串成一条链，中间任一段失败整条链就断；而本机每段失败常伴随一次 GPU 挂死，重试成本很高。

### 7.2 替代路径（本机推荐）

```bash
# ① 每段独立生成：同角色图 + 固定 seed 保证身份一致；段与段之间留冷却时间
h3ctl generate video --mode rv2v --prompt-file P.txt \
  --ref hero_turnaround.jpg --source-video segN_480x864.mp4 \
  --duration 5.17 --aspect-ratio 9:16 --steps 4 --seed <固定值> \
  --wait --wait-timeout 40m --download segN_out.mp4

# ② 无损直拼（各段编码参数一致时可 -c copy）
cat > concat.txt <<'EOF'
file 'seg1_out.mp4'
file 'seg2_out.mp4'
file 'seg3_out.mp4'
EOF
ffmpeg -f concat -safe 0 -i concat.txt -c copy final.mp4
```

- **优点**：单段失败只需重跑那一段；不受服务端编排与续接约束；冷却节奏可精确控制。
- **代价**：段间没有 latent / 尾帧级续接，衔接一致性依赖**同角色图 + 固定 seed + 相邻源片段本身连贯**。
- **实测**：3 段（源各 5.3s）→ 拼接 **15.5s / 423 帧 / 768×1344**，形象与动作全程一致，换段处无漂移。

### 7.3 本机负载红线

2026-09-14 一晚发生 **4 次硬件级故障**（19:43 整机断电、19:57 / 22:35 / 04:43 三次 GPU 掉总线 GSP 挂死），
全部发生在 H3 高负载下（显存峰值 14.2~14.5GB / 16.3GB），SageAttention 全程关闭、nvrtc 无报错。
**建议**：单段任务之间留冷却；避免满档 362 帧（实测直接 OOM）；优先排查 PSU / 散热 / PCIe 链路。

### 7.4 ⚠️ 时长坑：`--duration` 会被**向上对齐**到 17k+5 帧网格

**踩坑实录**：三段各传 `--duration 5.17`（期望 124 帧 = 5.17s），H3 实际生成 **141 帧 = 5.875s**
（向上对齐到下一个合法网格值）。三段拼接 = **423 帧 = 17.625s**，而源内容只有 15.7s，凭空多出约 2 秒。
我当时只看了 `nb_frames=423` 却没换算成时长，误报成 15.5s。

**关键事实**：
- H3 输出帧数**只能是 `17k+5`**：5, 22, 39, 56, 73, 90, 107, **124**, **141**, 158, 175, 192 … 362
- `--duration` 不是精确值，会**向上取到最近合法帧数**
- 对应的精确时长：124 帧 = 5.167s，141 帧 = 5.875s，362 帧 = 15.083s

**规矩**：
1. **拼接前必须用 `ffprobe` 核对实际 `nb_frames` / `duration`**，绝不用"理论每段时长 × 段数"估算；
2. 需要精确总时长时，拼接前把每段**裁到目标帧数**：
   ```bash
   ffmpeg -y -i seg.mp4 -t <秒> -c:v libx264 -crf 18 -c:a aac seg_trim.mp4
   ```
3. 本次修正：每段裁到 **124 帧**（5.1667s）→ 3×124 = **372 帧 = 15.533s** ✅

### 7.5 ⚠️ 音轨坑：`generate video --mode rv2v` **不会复制源音轨**

**踩坑**：用 rv2v 分段生成后，成品只剩提示词 `overall_soundscape` 里描述的**生成环境音**，
源视频的**原唱 / 原声被丢掉了**。该模式的 workflow evidence 明确写着
`unsupported_audio_modes: ["source","mute"]` —— 它只能生成音频，无法沿用源音轨。

| 命令 | 音频行为 |
|---|---|
| `h3ctl video migrate-character` | 默认 `--audio copy-source`，**直接复用源音轨** ✅ |
| `h3ctl generate video --mode rv2v` | 只能生成音频（`source` / `mute` 不支持）❌ |

**修法：按段贴回源音轨（保证音画同步）**
```bash
# 每段：自己的视频（已裁到目标帧数） + 源视频对应时间段的音频
ffmpeg -y -i seg0.mp4 -ss 0    -t 5.1667 -i source.mp4 -c:v copy -map 0:v:0 -map 1:a:0 -shortest a0.mp4
ffmpeg -y -i seg2.mp4 -ss 5.2  -t 5.1667 -i source.mp4 -c:v copy -map 0:v:0 -map 1:a:0 -shortest a2.mp4
ffmpeg -y -i seg3.mp4 -ss 10.4 -t 5.1667 -i source.mp4 -c:v copy -map 0:v:0 -map 1:a:0 -shortest a3.mp4
# 再拼接
ffmpeg -y -f concat -safe 0 -i concat.txt -c copy final.mp4
```
**校验贴回是否成功**：用响度对比，二者接近即说明拿到的是源音轨
```bash
ffmpeg -i source.mp4    -af volumedetect -f null - 2>&1 | grep mean_volume
ffmpeg -i final.mp4     -af volumedetect -f null - 2>&1 | grep mean_volume
```
（本次实测：源 `mean −7.7 dB / max 0.0 dB`，成品 `mean −7.9 dB / max 0.0 dB` → 一致 ✅）
