# H3 Motion Context 链式续接引擎 → KAP 集成规划

> **文档性质**:架构设计 / 调研规划(不含实现代码)
> **日期**:2026-08-13
> **插件**:ComfyUI-H3-Motion-Context(NikoDemon80,容器 `comfyui-primary` 已安装验证)
> **目标**:让 MiniMax H3 视频片段实现"运动 + 音频跨剪辑无缝续接",突破单段 124 帧(5.2s)/362 帧(15s)时长限制。

---

## 0. 摘要(TL;DR)

- **Motion Context 是什么**:5 个 ComfyUI 节点,通过在 conditioning 层"钉住上一段视频尾部帧 + 尾部音频",让下一段采样从上一段结束处**真正续接**(同样的运动速度/方向 + 连续音频),而非"听起来像的新 take"。
- **续接传递方式**:`safetensors` latent 文件(承载视频+音频双流),上一段采样输出 → `SaveLatent` → 下一段 `LoadLatent` → `MotionContext.context_latent`。
- **关键事实(代码验证)**:两个运行时补丁打在 `MiniMaxH3` 模型内部(关键帧布局 + `extra_conds`),**与采样器无关**——T8 Dual-Clock 链路与原生 KSampler 链路都能用;且用 marker 门控,**对非链式图零影响**。
- **实测数据印证**(帧数推导自 `context_length=22` 默认值):
  - 2 段 1216×672 → 226 帧 = **9.42s** ✅(124 + (124−22))
  - 3 段 1344×768 → 328 帧 = **13.67s** ✅(124 + (124−22)×2)
- **核心设计结论**:
  1. 链式**必须直出 H3 原生音频**(skipFoley),否则 LTX Foley 每段独立重生成环境音会**破坏续接连续性**——这是音频跨剪辑连续的整个意义所在。
  2. **禁用低步数 Turbo LoRA**(T8 Turbo 4 步 / LightX2V)做链式:README 明确"turbo 以极少步数收敛,牺牲的正是最后几步负责的音频细节,会让声音变浊、画面变软"。生产链式应走 **T8 production 50 步** 或 **native-sage**。
  3. **分辨率全程必须一致**(latent 不可 resize,节点会拒绝并报出两段分辨率)。
  4. 每段独立渲染(ComfyUI 在 prompt 间卸载模型),**VRAM 不叠加**(单段 ~20GB @ 1344×768),但总耗时 = N × 单段耗时(串行,因为下段依赖上段 latent)。

---

## 1. 现状分析

### 1.1 KAP H3 generate.ts 三步管线(`src/routes/production/minimax-h3/generate.ts`)

`POST /api/production/minimax-h3/generate` 是统一管线入口,同步等待,把视频生成与音频替换串成一个请求:

| 步骤 | 内容 | 超时 |
|---|---|---|
| **Step 1** | H3 视频生成(按 `mode` 选 t2va/i2va/ref2va)→ 提交 ComfyUI → 轮询 → 下载 mp4(内嵌 H3 原生音频) | ≤45min(`pollComfyuiCompletion(..., 2_700_000)`) |
| **Step 2** | (skipFoley profile 跳过)replace-audio:LTX 2.3 Foley 环境音 + NAG + BGM 频谱检测 + 换 seed 重试(≤2 次) | 每次 ≤10min |
| **Step 3** | 最终合并:`mergeAudioAndVideo` 用 Foley 环境音(+可选 TTS 混音)替换 H3 原音轨,**保留 H3 原始分辨率视频流**(`-map 0:v:0`) | — |

- **profile 分支**:`preview`/`turbo`/`lightx2v-*` 设 `skipFoley=true` → Step 1 后直出 H3 原生视频交付(省 5–10min)。
- **采样链路三选一**(由 `native` / `lightx2vVariant` / 默认 T8 决定):
  - `buildH3WorkflowT8`(默认 + turbo):MiniMaxH3AudioConditioningT8(20) → DualClockSamplerT8(30) → RandomNoise(31)+BasicGuider(32)+SamplerCustomAdvanced(33) → AVDecodeT8(40)
  - `buildH3WorkflowNative`:MiniMaxH3ImageToVideo/ReferenceToVideo(20) → SigmaShift(21)[+TESpeed(35)] → KSampler(30) → VAEDecode(40)+VAEDecodeAudio(41)
  - `buildH3WorkflowLightX2V`:SigmaShift(14_shift) → LoraLoaderModelOnly(15) → KSamplerSelect+BasicScheduler+BasicGuider+SamplerCustomAdvanced(30-34)

### 1.2 既有节点 ID 约定(链式改造必须沿用)

三种工作流共享以下 ID 语义(链式注入新节点时需避开冲突):

| ID | 节点 | 说明 |
|---|---|---|
| 10 / 11 / 12 / 13 | CLIPLoader / VAELoader(video) / UNETLoader / VAELoader(audio) | 模型/VAE |
| 14 / 141 / 142… | LoadImage | i2va 首帧 / ref2va 参考图(LightX2V 用 `14_shift` 做 SigmaShift 避冲突) |
| 14_lora | LoraLoaderBypassModelOnly | T8 Turbo LoRA |
| 16 | MiniMaxH3ImageToVideo(无图) | 负面条件占位(native/LightX2V 用) |
| 20 | 条件节点 | T8: `MiniMaxH3AudioConditioningT8` → **[0]=CONDITIONING, [1]=LATENT**;native: `MiniMaxH3ImageToVideo`/`ReferenceToVideo` 同样 [0]=COND [1]=LATENT |
| 21 / 14_shift | MiniMaxH3SigmaShift | shift_video/shift_audio |
| 30 | DualClockSamplerT8 / KSampler | T8 采样器配置 / 原生采样 |
| 31/32/33 | RandomNoise/BasicGuider/SamplerCustomAdvanced | T8 + LightX2V 采样链 |
| 40 / 41 / 42 / 50 | 解码 / 音频解码 / CreateVideo / SaveVideo | 输出 |

**关键注入点**:`BasicGuider(32).conditioning` 当前接 `[20,0]`;原生链路 `KSampler(30).positive` 接 `[20,0]`。链式下段需把它改接 MotionContext 输出的修改后 conditioning。

### 1.3 H3VideoEngine(KMC)如何调用 KAP(`kais-hermes-skills/plugins/kais_aigc/video_engine.py`)

- `H3VideoEngine.render(shot, output_path, ...)` 是**单镜头**入口:`multipart/form-data` POST 到 `http://localhost:10588/api/production/minimax-h3/generate`(同步等待整条管线)。
- `_build_multipart` 把 `shot` dict 映射成字段:`projectId / prompt / mode(ref2va|i2va|t2va)/ width / height / length(n%17==5)/ filenamePrefix(=shot_id)/ profile(+turbo|native|steps)/ refImages[] / ttsAudio / image`。
- profile 解析(`_resolve_profile`):`turbo` / `native`(→ 映射 KAP `native-sage` + 显式 15 步) / `lightx2v` / `preview` / `production`,默认 `native`。
- 模式解析(`_resolve_mode`):`shot.mode > episode_config.h3_mode > "ref2va"`。
- 分辨率(`_resolve_resolution`):`landscape=1344×768` / `portrait=768×1344`(32 倍数)。
- 帧数(`_compute_expected_frames`):`shot.duration_sec` → n%17==5 snap,clamp [5,362](优先用 `pipeline.engine_adapters.get_adapter("h3")`,否则内联 snap 兜底)。
- 下载:`video_url` 形如 `/mnt/agents/output/xxx.mp4`(需 auth),改写成 `/oss/`(免 auth 静态挂载)后 stream 下载到 `output_path`。
- **P0-4 帧数硬门**:下载后 `_default_frame_validator` 用 ffprobe 校验 duration>0 且帧数 == 期望值(容差 ±1);不达标判 `degraded` 并删除坏片。
- D-09 degrade 契约:连接错误/超时/5xx/429 → 返回 `{"degraded":True,...}`(不抛);4xx → 抛 `VideoEngineError`。
- WR-01:`ThreadPoolExecutor(max_workers=3)` 共享一个 engine 实例,`threading.Lock` 串行化 POST。

### 1.4 当前单段限制(config.ts)

```
H3_CONSTANTS.MAX_FRAMES   = 362   // ~15s 训练上限
H3_CONSTANTS.MAX_DURATION = 15    // 秒
H3_CONSTANTS.FRAME_GRID   = 17, FRAME_OFFSET = 5   // 帧数 n%17==5
H3_CONSTANTS.FPS          = 24
H3_CONSTANTS.AUDIO_SAMPLE_RATE = 32000   // ⚠️ H3 音频 32kHz, 非 48k
H3_DEFAULTS.defaultLength = 124   // ~5.2s
```

单段最长 15s。**链式是突破该上限的唯一路径**(分段渲染 + latent 续接 + ffmpeg 拼接)。

---

## 2. Motion Context 节点拓扑

### 2.1 五节点规格(取自 ComfyUI `object_info` 实测,category=`conditioning/minimax`)

#### ① `MiniMaxH3MotionContext`(核心:钉住上段尾部)
| | 字段 | 类型 | 默认 |
|---|---|---|---|
| **必需入** | `conditioning` | CONDITIONING | —(本段条件节点 [0] 输出) |
| | `vae` | VAE | —(视频 VAE = node 11) |
| | `latent` | LATENT | —(本段空 latent = 条件节点 [1] 输出,用于读帧数) |
| | `context_length` | ENUM `["22","5","39","56"]` | `"22"` |
| | `audio_context_length` | INT [0,240] | `24` |
| **可选入** | `context_latent` | LATENT | 上段**采样输出** latent(首选,无解码损耗) |
| | `context_frames` | IMAGE | 上段解码帧(无 context_latent 时用,有损) |
| | `audio_vae` | VAE | 音频 VAE(node 13,仅 context_audio 路径需要) |
| | `context_audio` | AUDIO | 上段音频(无 context_latent 时用) |
| **出** | `conditioning` | CONDITIONING | 修改后(已注入钉住的尾部) |
| | `trim_frames` | INT | 需在解码后裁掉的钉住头长度(= context_length) |

> ⚠️ `context_latent` 必须是**上段采样器输出**(喂解码节点那个 latent),不是空 latent。它同时承载视频+音频双流,直接从 latent 切片,**省去 h264 解码→VAE 重编码的往返**(消除可见接缝,更快)。

#### ② `MiniMaxH3MotionContextTrim`(裁掉钉住的头部)
| | 字段 | 类型 | 默认 |
|---|---|---|---|
| **必需入** | `images` | IMAGE | 解码后图像(含钉住头) |
| | `trim_frames` | INT [0,4096] | `0`(接 MotionContext.trim_frames) |
| **可选入** | `audio` | AUDIO | 解码后音频(同段) |
| | `fps` | FLOAT | `24.0`(须与 CreateVideo 一致) |
| | `match_tail` | BOOL | `true`(让音频时长精确 = frames/fps,补偿 H3 音频网格 ~8ms 误差) |
| **出** | `images` / `audio` | IMAGE / AUDIO | 裁后(钉住头已移除) |

#### ③ `MiniMaxH3MotionContextSaveLatent`(保存本段 latent 供下段用)
| | 字段 | 类型 | 默认 |
|---|---|---|---|
| **必需入** | `latent` | LATENT | 本段采样器输出 |
| | `filename_prefix` | STRING | `"h3_context/clip"`(ComfyUI output 目录下) |
| | `clip_index` | INT [0,9999] | `0` |
| **出** | `latent_path` | STRING | 保存路径 |

> clip_index 语义:**这是链中的第几段**。`>0` → 固定槽位 `<prefix>_<idx:05d>.safetensors`(重 roll 覆盖自己的旧文件,不堆叠);`=0` → 自动 `<prefix>_<n>_.safetensors`(按运行计数,非按段)。

#### ④ `MiniMaxH3MotionContextLoadLatent`(加载上段 latent)
| | 字段 | 类型 | 默认 |
|---|---|---|---|
| **必需入** | `latent_path` | STRING | `"h3_context"`(文件夹或具体文件;相对路径相对 ComfyUI output 目录) |
| | `clip_index` | INT [0,9999] | `0` |
| **出** | `LATENT` | LATENT | 仅用于喂 `context_latent`,**勿接解码节点** |

> clip_index 语义:**续接自第几段**。`>0` → 精确槽位(跳过自动 `_` 文件,retry-safe);`=0` → 文件夹内最新(re-roll 会加载自己刚被拒的音频,**不可靠**);指向具体**文件**则忽略 index。
> **生成第 N 段时:Load `clip_index=N-1`,Save `clip_index=N`。**

#### ⑤ `MiniMaxH3MotionContextSeamProbe`(可选诊断,QA 用)
| | 字段 | 类型 | 默认 |
|---|---|---|---|
| **必需入** | `clip_b_untrimmed` | AUDIO | 本段 VAE 解码后**未裁剪**的音频(含钉住头) |
| | `trim_frames` | INT | 接 MotionContext.trim_frames |
| **可选入** | `clip_a_latent` | LATENT | 上段 AV latent(节点自行解码+尾匹配) |
| | `audio_vae` / `fps` / `window_ms`(50)/ `search_ms`(40) | — | — |
| **出** | `audio`(透传)/ `report`(STRING,接 Preview Text) | — | — |

> report 内容:跨钉住段的归一化互相关曲线(`mean corr`)、跟踪滞后 `lag_ms`、交付切口的电平阶跃(`level step`)。**仅测量,不影响渲染**。可用于自动 QA 门控。

### 2.2 运行时补丁机制(决定兼容性)

`__init__.py` 仅注册节点,**补丁在首次运行 MotionContext 节点时安装**(避免污染所有 H3 图):

- **`patch_layout.py`**:解除 ComfyUI 关键帧锚点"仅首/尾"的限制(`raise ValueError("only first/last keyframe anchors are supported")`),允许内部锚点。打在 `MiniMaxH3` 关键帧**布局**代码上。
- **`patch_payload.py`**:patch `MiniMaxH3.extra_conds`(comfy/model_base.py),让关键帧 latent 与 ref latent **共存**(默认 refs 分支会覆盖关键帧的 `cond_video_latents`)。

**两条关键性质(代码已验证)**:
1. **采样器无关**:补丁打在模型层(extra_conds / 布局),不碰采样器。→ **T8 DualClockSampler 与原生 KSampler 都能链式**。
2. **marker 门控**:`patch_payload` 用 `MC_KEY` marker 识别"本插件产生的关键帧",无 marker 的图**逐位与 stock 一致**(`patch_payload.py:74-98`)。→ 已上线的 H3 工作流不受任何影响。

### 2.3 链式 vs 单段拓扑差异

**单段(T8 链路,现状)**:
```
20 Conditioning ──[0]COND──► 32 BasicGuider ─┐
       └──[1]LATENT─────────────────────────► │ ─► 33 SamplerCustomAdvanced ─►[0]LATENT ─► 40 AVDecodeT8 ─► 42 CreateVideo ─► 50 SaveVideo
                                              │            ▲ noise/sampler/sigmas ◄── 30/31
                                              └─ model ◄───┘
```

**链式第 1 段**(无 context,等同单段 + 多一个 SaveLatent):
```
20 Conditioning ──[0]COND──► 32 BasicGuider
       └──[1]LATENT────────► 33 SamplerCustomAdvanced ─►[0]LATENT ─┬─► 40 AVDecodeT8 ─► 42 ─► 50 SaveVideo  (clip1 全长交付, 无 Trim)
                                                                  └─► 60 SaveLatent(clip_index=1)   ← 新增:为 clip2 存 latent
```

**链式第 N 段(N≥2)**(注入 Load + MotionContext + Trim + Save):
```
61 LoadLatent(clip_index=N-1) ─►[0]LATENT(context_latent)
                                          │
20 Conditioning ──[0]COND──┐              │
       └──[1]LATENT──┐     │              │
                     ▼     ▼              ▼
                   62 MotionContext(conditioning, vae=11, latent=20[1], context_length, audio_context_length, context_latent=61[0])
                            │     │
                  [0]COND───┘     └──[1]trim_frames
                     │                  │
                     ▼                  │(暂存, 喂 Trim)
                 32 BasicGuider ◄─ model ◄ 30        │
                     │                              │
                     ▼                              │
                 33 SamplerCustomAdvanced ─►[0]LATENT ─┬─► 40 AVDecodeT8 ─►[0]IMAGE ┐
                                                       │                [1]AUDIO ┘│
                                                       │                          ▼
                                                       │              63 MotionContextTrim(images, trim_frames=62[1], audio, fps=24, match_tail=true)
                                                       │                          │  [0]IMAGE  [1]AUDIO
                                                       │                          ▼
                                                       │                     42 CreateVideo ─► 50 SaveVideo  (clipN 交付 = length - trim_frames)
                                                       └─► 60 SaveLatent(clip_index=N)
```

**改动点仅 3 处**(相对单段):
1. `BasicGuider(32).conditioning`:`[20,0]` → `[62,0]`(原生链路则 `KSampler(30).positive` 同理)
2. 解码后插入 `Trim(63)`,`CreateVideo(42)` 改接 Trim 输出
3. 新增 `SaveLatent(60)`(每段)、`LoadLatent(61)`+`MotionContext(62)`(N≥2 段)
   `latent_image`(33)仍接 `[20,1]` **不变**;model 链(Turbo LoRA / SigmaShift)**不变**。

### 2.4 关键参数

| 参数 | 取值 | 含义 / 推荐 |
|---|---|---|
| `context_length` | `"22"`(默认)/ `"5"` / `"39"` / `"56"` | 承接的上段画面帧数。**只用这 4 个值**(它们恰是整数个 latent step)。`5` 勉强流畅、`22` 近乎无缝、`56` 每 render 浪费 2.3s 画面。**推荐 22**。 |
| `audio_context_length` | INT [0,240],默认 `24` | 钉住的尾部音频帧数,与画面窗口**末端对齐**(只控制声音回溯多远)。3 的倍数落在 40Hz 音频网格、24 的倍数是整秒。**推荐 24**(恰好 1 秒)。 |
| `trim_frames` | MotionContext 输出 | = context_length。解码后由 Trim 节点裁掉。`match_tail=true` 补偿音频网格 ~8ms 累积误差。 |
| `fps` | 24 | H3 原生帧率,Trim/CreateVideo 须一致。 |
| `match_tail` | true | 链式必须开(每段 ~8ms 误差在长链累积)。 |

---

## 3. 帧数与时长数学模型(已用实测数据验证)

设单段采样帧数 `L`(n%17==5,默认 124),`context_length = ctx`(默认 22),段数 `N`:

```
第 1 段交付帧数 = L                      (无 context, 不裁剪)
第 k 段交付帧数 = L − ctx        (k = 2..N, 钉住头被裁掉)
总交付帧数     = L + (L − ctx) × (N − 1)
总时长(秒)     = 总帧数 / 24
```

| N | L | ctx | 总帧数 | 时长 | 对应实测 |
|---|---|---|---|---|---|
| 2 | 124 | 22 | 226 | **9.42s** | 1216×672 2 段 ✅ |
| 3 | 124 | 22 | 328 | **13.67s** | 1344×768 3 段 ✅ |
| 4 | 124 | 22 | 430 | 17.92s | — |
| 6 | 124 | 22 | 634 | 26.42s | — |

**API 入参的 `length` 指"单段采样帧数 L"**(全程一致),交付时长由公式自动决定。KAP 应在校验后用公式预计算 `totalDuration` 回显给调用方。

---

## 4. 新 API Endpoint 设计

### 4.1 端点

```
POST /api/production/minimax-h3/chain      (multipart/form-data, 同步)
```

同步模式(与 generate.ts 一致):KAP 内部串行渲染 N 段 + ffmpeg 拼接,一次性返回最终 mp4。KMC 端 `render_chain` 只需一次 POST + 等待。

### 4.2 入参

**全局参数(全程一致)**:
| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
| `projectId` | number | —(必填) | |
| `width` / `height` | number | 1344 / 768 | 必须 32 倍数;**全程固定**(latent 不可 resize) |
| `length` | number | 124 | 单段采样帧数,n%17==5 自动对齐;全程固定 |
| `contextLength` | `"22"`/`"5"`/`"39"`/`"56"` | `"22"` | |
| `audioContextLength` | number | 24 | |
| `profile` | string | `"production"` | 推荐 `production`(T8 50 步)/ `native-sage`;**链式禁止 `turbo`/`lightx2v-*`**(音频退化,见 §6) |
| `filenamePrefix` | string | `h3_chain_<projectId>_<ts>` | 同时用作 latent 文件夹名(`h3_chain_<chainId>`)和输出前缀 |

**每段参数(数组,JSON 字符串 `clips`)**:
```jsonc
clips = [
  { "prompt": "...", "mode": "ref2va", "seed": 12345,
    "firstFrameIdx": null, "refImageIdxs": [0,1], "ttsAudioIdx": null },
  { "prompt": "...", "mode": "t2va",   "seed": 67890 },
  // ... 至少 2 段, 建议上限 6 段(总时长/VRAM 综合考虑)
]
```
- `prompt`(每段必填)、`mode`(`t2va`/`i2va`/`ref2va`)、`seed`(可选,默认随机)。
- 文件用全局数组 + 索引引用,避免 multipart 数组字段歧义:
  - `firstFrames`(File[],可选):`clips[i].firstFrameIdx` 索引(i2va 首帧)
  - `refImages`(File[],≤9/段):`clips[i].refImageIdxs` 索引(ref2va 参考图)
  - `ttsAudios`(File[],可选):`clips[i].ttsAudioIdx` 索引

> **校验**:`clips.length >= 2`;所有 `mode=i2va` 的段必须有 `firstFrameIdx`;`mode=ref2va` 的段必须有 ≥1 个 `refImageIdxs`;`profile` ∈ {production, native-sage, preview}。

### 4.3 返回

```jsonc
{
  "code": 200, "data": {
    "status": "completed",
    "chainId": "h3_chain_1_1723536000",
    "profile": "production",
    "contextLength": "22", "audioContextLength": 24,
    "width": 1344, "height": 768, "lengthPerClip": 124,
    "totalFrames": 328, "totalDurationSec": 13.67,
    "fps": 24, "audioSampleRate": 32000,
    "clips": [
      { "index": 1, "promptId": "...", "mode": "ref2va", "status": "completed",
        "frames": 124, "videoPath": ".../clip1.mp4", "latentSaved": true },
      { "index": 2, "promptId": "...", "mode": "t2va",   "status": "completed",
        "frames": 102, "videoPath": ".../clip2.mp4", "latentSaved": true },
      { "index": 3, "promptId": "...", "mode": "t2va",   "status": "completed",
        "frames": 102, "videoPath": ".../clip3.mp4", "latentSaved": true }
    ],
    "videoUrl":  "/mnt/agents/output/h3_chain_1_..._final.mp4",
    "videoPath": "/mnt/agents/output/h3_chain_1_..._final.mp4"
  }
}
```

### 4.4 错误处理 / degrade

- 任一段 ComfyUI 提交失败 / 轮询失败 → **整链失败**,返回 502 + `pipeline.clips[]`(已完成段标记 completed、失败段标记 failed + error),已渲染段 mp4 路径仍回传(可手动拼接 salvage)。
- 分辨率中途不一致:由 MotionContext 节点在 ComfyUI 端拒绝并报出两段分辨率 → KAP 捕获 node_errors 转成 502(KAP 入参已强制全程一致,理论上不会触发)。
- 超时:每段独立轮询,单段预算 × N(KAP 配置见 §5.4)。

---

## 5. KAP 实现方案

### 5.1 新文件 `src/routes/production/minimax-h3/chain.ts`

结构对齐 `generate.ts`:express router + multer(1GB)+ zod + 内联工作流构建 + 复用 `replace-audio.ts` 辅助函数。

```ts
import { /* 现有 config 常量 */ ..., H3_CHAIN } from "./config";
import { copyToContainer, pollComfyuiCompletion, downloadVideoFromOutputs } from "./replace-audio";
```

### 5.2 config.ts 新增常量 `H3_CHAIN`

```ts
export const H3_CHAIN = {
  minClips: 2,
  maxClips: 6,                    // 总时长 + VRAM 综合上限(6 段≈26s)
  defaultContextLength: "22",     // ENUM: "22"|"5"|"39"|"56"
  defaultAudioContextLength: 24,  // INT, 3 的倍数, 24=1 整秒
  // latent 文件夹前缀(ComfyUI output 目录下)
  latentPrefix: (chainId: string) => `h3_context/${chainId}`,
  // 节点 ID(避开既有 10-50 段)
  NODE: { saveLatent: "60", loadLatent: "61", motionContext: "62", trim: "63", seamProbe: "64" },
  // 单段轮询预算(毫秒)× 段数 = 总预算;链式强制高步数故按生产档估
  perClipPollMs: 2_700_000,       // 45min/段(与 generate Step1 一致;T8 production 实测 ~12-15min)
  // 链式允许的 profile(禁 turbo/lightx2v — 音频退化)
  allowedProfiles: ["production", "native-sage", "native", "preview"],
} as const;
```

### 5.3 工作流构建(基于 generate.ts 的 builder 改造)

**策略**:不重写,而是给现有 `buildH3WorkflowT8 / Native / LightX2V` 增加可选链式参数,或新增薄包装。推荐**新增专用 builder**(`buildH3ChainWorkflowClip`)以隔离复杂度:

```ts
interface H3ChainClipOpts extends H3GenOpts {
  clipIndex: number;            // 1..N
  chainId: string;              // latent 文件夹 + 输出前缀
  contextLength: "22"|"5"|"39"|"56";
  audioContextLength: number;   // 默认 24
  // profile 决定走 T8 / native / LightX2V(链式建议 T8 production 或 native-sage)
}
```

- **clip 1**(`clipIndex === 1`):复用单段 builder,仅追加 `SaveLatent(60)`:
  - `60.SaveLatent.latent` = 采样输出(T8: `[33,0]`;native/LightX2V: `[30,0]`/`[34,0]`)
  - `filename_prefix` = `H3_CHAIN.latentPrefix(chainId)`、`clip_index` = 1
  - 解码 → CreateVideo → SaveVideo(无 Trim,全长交付)
- **clip N≥2**:在单段 builder 基础上 3 处改动(见 §2.3):
  - 新增 `61 LoadLatent(latent_path=H3_CHAIN.latentPrefix(chainId), clip_index=N-1)`
  - 新增 `62 MotionContext(conditioning=[20,0], vae=[11,0], latent=[20,1], context_length, audio_context_length, context_latent=[61,0])`
  - `BasicGuider(32).conditioning` / `KSampler(30).positive` 改为 `[62,0]`
  - 解码输出接 `63 Trim(images=decode[0], trim_frames=[62,1], audio=decode[1], fps=24, match_tail=true)`
  - `CreateVideo(42)` 接 `[63,0]`/`[63,1]`
  - 追加 `60 SaveLatent(latent=采样输出, clip_index=N)`
- **可选 SeamProbe(64)**:QA profile 时插入,`report` 输出到 history(不接 SaveVideo,纯诊断)。

> Turbo LoRA:链式 profile 列表已禁 `turbo`/`lightx2v`,故 `useLora` 恒 false(T8 production 50 步不加载 LoRA)。LightX2V 8 步已实测噪点不可用(2026-08-13),禁止放开。

### 5.4 编排:串行渲染 + ffmpeg 拼接

```ts
// 伪码
const chainId = `h3_chain_${projectId}_${Date.now()}`;
const clipPaths: string[] = [];
for (let i = 0; i < clips.length; i++) {
  const clipIndex = i + 1;
  // 1. 上传本段文件(firstFrame/refImages)到容器
  // 2. 构建 wf = buildH3ChainWorkflowClip({ clipIndex, chainId, ... })
  // 3. POST /prompt → pollComfyuiCompletion(perClipPollMs)
  // 4. downloadVideoFromOutputs → clipPaths[i]
}
// 5. ffmpeg concat(见下) → finalOutputPath
```

**ffmpeg 拼接(关键:32kHz 音频)**——README 警告"H3 是 32kHz 不是 48k,流拷贝 concat 不能中途变采样率,硬编码 48000 会让长片尾部静音"。正确做法:

```bash
# 方案A:concat demuxer(要求各段编码/参数一致 —— H3 各段同参,可用)
printf "file '%s'\n" clip1.mp4 clip2.mp4 ... > list.txt
ffmpeg -y -f concat -safe 0 -i list.txt -c copy final.mp4
# ⚠️ 若段间时间戳/编码微差导致 -c copy 失败 → 回退重编码(读采样率不硬编码):
ffmpeg -y -f concat -safe 0 -i list.txt -c:v libx264 -crf 18 -c:a aac -ar 32k final.mp4
```

- `replace-audio.ts` 现有 `mergeAudioAndVideo` **不复用**(它假设单视频+音频流替换);链式需新增 `concatClips(clipPaths, outPath)` 辅助函数。
- **不做 Step 2/3 Foley**:链式直出 H3 原生音频(连续性要求,见 §0)。TTS 混音作为 v2 议题(逐段混音会破坏接缝)。

### 5.5 路由注册(`src/router.ts`)

```ts
import routeH3Chain from "./routes/production/minimax-h3/chain";
// ...
app.use("/api/production/minimax-h3/chain", routeH3Chain);
```

> 注意 `router.ts` 是**手动维护**(见 Kap 后端路由约定 memory:`core.ts` 自动生成会拉入无 default export 的 helper 致 tsc 失败)。新增 import + `app.use` 两行,更新顶部 `@routes-hash` 注释。

---

## 6. KMC video_engine.py 改造

### 6.1 新增 `H3VideoEngine.render_chain()`(或独立 `ChainRender` 协议)

`render()` 是**单镜头**语义(一个 shot → 一个 clip + 帧数门)。链式是**多镜头连续叙事**——建议新增方法而非改 `render()` 签名(保持单镜头路径不变):

```python
class H3VideoEngine(VideoEngine):
    CHAIN_PATH = "/api/production/minimax-h3/chain"

    def render_chain(
        self, *, shots: list[dict], output_path: str,
        context_length: str = "22", audio_context_length: int = 24,
        episode_config: dict | None = None,
    ) -> dict:
        """把一组连续 shots 渲染为一条链式续接视频。

        shots 必须同分辨率/form_factor(链式硬约束)。逐 shot 解析 prompt/
        mode/refImages/ttsAudio,组装 KAP chain 入参,单次 POST 同步等待,
        下载 final.mp4,跑帧数门(期望帧数 = 公式见 §3)。
        """
```

- `_build_chain_multipart`:全局字段(`projectId / width / height / length / contextLength / audioContextLength / profile / filenamePrefix`)+ `clips`(JSON:每段 `{prompt, mode, seed, firstFrameIdx, refImageIdxs, ttsAudioIdx}`)+ 文件数组(`firstFrames / refImages / ttsAudios`)。
- `length` 由首段 shot 的 `duration_sec` 经 `_compute_expected_frames` 解析(全程一致)。
- `profile`:**强制 `native-sage` 或 `production`**(链式禁 turbo/lightx2v);忽略 shot 的 `h3_profile` 若为禁用值并 WARN。
- **帧数门调整**:期望帧数 = `L + (L - ctx) × (N-1)`(§3 公式),非单段 `L`。下载 final.mp4 后校验。
- D-09 / WR-01 契约不变:`_post`/`_lock`/degrade 复用。

### 6.2 调用方接入(P11 / 管线)

- P11 当前按 shot 并行 fan-out(`ThreadPoolExecutor(max_workers=3)`,每 shot 一个 `render`)。**链式是串行的**(段间 latent 依赖),不能并入并行池。
- 新增管线语义:**"连续镜头组"**(同一场景/机位的多个 beat)→ 调 `render_chain`;独立镜头 → 调 `render`。判定可基于 shot 的 `chain_group` 字段或相邻 shot 的 scene_id + continuity 标记。
- 提示词须遵循 README 的"airlock"原则(见 §8):段 N+1 开头先**保持段 N 的结尾取景 ~2s**再切新内容,接缝测得比普通帧间剪辑更紧。

---

## 7. 限制与注意事项

| # | 限制 | 影响 / 对策 |
|---|---|---|
| 1 | **分辨率全程必须一致** | latent 不可 resize,节点拒绝并报两段分辨率。KAP 入参全局 width/height 校验。 |
| 2 | **禁所有加速 LoRA**(T8 Turbo 4步 / LightX2V 4步 / **LightX2V 8步**) | README §256 警告已验证。2026-08-13 实测 LightX2V 8步 LoRA + Motion Context 链式:技术兼容(锚定+修剪正常),但**画面出现明显噪点不可用**。Kai 判定:"八步LoRA是噪点"。**链式 profile 白名单只留 native-sage(15步) / production(50步)**。 |
| 3 | **必须直出 H3 原生音频**(skipFoley) | LTX Foley 每段独立重生成环境音 → 破坏跨段音频连续性(链式的核心价值)。chain.ts 不走 generate.ts Step 2/3。 |
| 4 | **VRAM 不叠加但单段高** | 每段独立渲染(ComfyUI prompt 间卸载),~20GB @ 1344×768。3090 24GB 单段可行(见 h3-oom memory:1216×672 更稳);但需注意容器内 IndexTTS/其他 session 的 GPU 竞争。 |
| 5 | **总耗时 = N × 单段**(串行) | 下段依赖上段 latent,无法并行。3 段 production ≈ 3×12-15min ≈ 40min。timeout 按段数倍增(`perClipPollMs × N`)。 |
| 6 | **32kHz 音频**(非 48k) | ffmpeg concat **不可硬编码 48000**(会让长片尾部静音)。读 clip 实际采样率,或用 `-c copy`。 |
| 7 | **链式音频质量递减** | README:长链音频高频退化,续接最好落在自然音乐过渡处。建议 N≤6。 |
| 8 | **提示词陷阱** | ① 模型把矛盾渲染成"并集"(段 N+1 描述不同人物排列 → 三者全出现);② "airlock":切新内容前先 hold 上段结尾取景 ~2s;③ held framing 须写入微动作(呼吸/视线),否则渲染成定格。 |
| 9 | **clip_index retry 语义** | Load 用 `clip_index=N-1`(精确槽,retry-safe);Save 用 `clip_index=N`(覆盖自己旧文件)。**勿用 0**(newest,re-roll 会加载被拒音频)。KAP 用 `chainId` 做文件夹隔离不同链。 |
| 10 | **ComfyUI H3 支持尚年轻** | 补丁依赖当前 ComfyUI 形态,每次启动自检;上游改动会"节点拒绝运行"而非静默坏渲染。需在 KAP 启动健康检查里探测 5 个 MC 节点是否注册。 |
| 11 | **补丁首次运行才安装** | 首次链式渲染前,建议 KAP 做一次"warmup"空跑或在容器启动后跑一次 MC 节点,避免首段渲染时才发现补丁失败。 |

---

## 8. 测试计划

### 8.1 单段 vs 链式对比(基线)
- 同 prompt、同 seed、同分辨率:① 现有 `/generate` 单段;② `/chain` 2 段(第 2 段 prompt = 第 1 段延续)。
- 对比:接缝处运动连贯性(光流/SSIM)、音频连续性(波形衔接 + SeamProbe `lag_ms`/`corr`)。

### 8.2 context_length 接缝质量
- 固定 2 段、`context_length` ∈ {5, 22, 39, 56},其余同参。
- 用 `MiniMaxH3MotionContextSeamProbe` 自动收 `report`:目标 `mean corr` 高、`lag_ms` 小、`level step` 平。
- 权衡:ctx 越大接缝越紧但每段浪费画面越多(56 浪费 2.3s/段)。

### 8.3 采样链路兼容性(关键)
- **T8 production 50 步**(主推) vs **native-sage** 链式:验证补丁对两条链路都生效(§2.2 代码已证 sampler-agnostic,需端到端确认)。
- **LightX2V 8 步**(探索):README 未直接禁 LightX2V,但属"低步数 LoRA",音频可能退化。跑 2 段 + SeamProbe audio corr,与 production 对比;若 corr 显著低则确认禁用。

### 8.4 分辨率/时长矩阵
- 1216×672(低 VRAM,稳)与 1344×768(上限)各跑 3 段;验证 §3 帧数公式(交付 328 帧=13.67s)。
- N=4/6 长链:验证音频递减 + 总时长 + ffmpeg concat 32kHz 正确性(听长片尾部是否静音)。

### 8.5 错误注入 / degrade
- 中途一段 ComfyUI 超时 → 验证整链 502 + 已完成段路径回传(salvage)。
- 分辨率不一致(构造)→ MotionContext 拒绝,KAP 转译 node_errors。
- 容器重启中途 → KAP 捕获并 degrade。

### 8.6 KMC 集成
- `H3VideoEngine.render_chain` 帧数门:期望 = 公式值(非单段 L),过门/不过门路径。
- 与 `render` 单镜头路径互不干扰(回归 P11 现有 fan-out)。

---

## 9. 实现里程碑(建议)

| 阶段 | 交付 | 依赖 |
|---|---|---|
| **M1** | config.ts 增 `H3_CHAIN`;chain.ts 骨架 + clip1+clip2 两段 T8 production 跑通(无 SeamProbe);ffmpeg concat;路由注册 | §5 |
| **M2** | 多段(N≤6)+ native-sage 链路 + 帧数公式回显;错误/degrade 路径 | §5.4, §8.4 |
| **M3** | SeamProbe 接入(QA report)+ context_length 对比测试 + LightX2V 8 步兼容性判定 | §8.2, §8.3 |
| **M4** | KMC `render_chain` + P11 "连续镜头组"语义接入 + 帧数门调整 | §6 |

---

## 附录 A:关键文件清单

| 文件 | 作用 |
|---|---|
| `src/routes/production/minimax-h3/config.ts` | 配置常量(新增 `H3_CHAIN`) |
| `src/routes/production/minimax-h3/generate.ts` | 统一管线入口(builder 复用模板) |
| `src/routes/production/minimax-h3/replace-audio.ts` | 复用 `copyToContainer`/`pollComfyuiCompletion`/`downloadVideoFromOutputs` |
| `src/routes/production/minimax-h3/chain.ts` | **新增**:链式 endpoint |
| `src/router.ts` | **改**:注册 `/api/production/minimax-h3/chain` |
| `kais-hermes-skills/plugins/kais_aigc/video_engine.py` | **改**:`H3VideoEngine.render_chain` |
| 容器 `comfyui-primary:/root/ComfyUI/custom_nodes/ComfyUI-H3-Motion-Context/` | 5 节点 + 2 补丁(已就位) |

## 附录 B:5 节点 class_type 名(ComfyUI API 用)

```
MiniMaxH3MotionContext              → conditioning(minimax)
MiniMaxH3MotionContextTrim          → conditioning(minimax)
MiniMaxH3MotionContextSaveLatent    → conditioning(minimax)
MiniMaxH3MotionContextLoadLatent    → conditioning(minimax)
MiniMaxH3MotionContextSeamProbe     → conditioning(minimax)
```
（已由 `GET :8188/object_info` 验证全部注册,comfyui-primary `Up (healthy)`。)
