# Spec-2：建树批量并行入口

- **日期**: 2026-07-11
- **状态**: 已批准（用户确认两层参数推荐方案）
- **系列**: Spec-1/2/3 三件套之一，见 [2026-07-11-agent-runtime-fixes-design.md]、[2026-07-11-question-gen-v2-design.md]

## 1. 问题

TRM5 只有单视频建树（`app/tree/video_builder.py`，内部 Semaphore(16) 限 VLM/LLM 调用）与修复/迁移工具，**没有多视频批量构建入口**——批量建树只能视频间串行，非 API 阶段（ffmpeg 帧提取、图像编码、IO）与 API 阶段无法跨视频重叠，太慢。

## 2. 并发语义调研结论（项目惯例）

| 位置 | 并行单元 | 惯例 |
|------|---------|------|
| `app/harness/inference.py` | 题目 | 一个 `asyncio.Semaphore` + `gather`，任务级 |
| `tools/repair_trees.py` | 视频 | 视频级 Semaphore + gather + progress.json + 熔断阈值随并发缩放 |
| `app/tree/video_builder.py` | API 调用 | Semaphore 作为参数在协程链中显式传递 |

建树是唯一任务内部本身有大并发的场景 → **视频级与 API 级信号量必须分开**，否则 16×16=256 API 并发打爆端点与熔断器。

## 3. 设计

### 入口形态（遵循项目结构规范）

- `tools/build_trees.py`：独立工具（不被其他模块 import），复刻 `repair_trees.py` 的编排模式
- `scripts/build_trees.sh`：自包含实验记录，写死参数、零参数复现（GPU 卡号除外）

### 两层并发参数

```mermaid
graph LR
    subgraph tools/build_trees.py
        V[视频级 Semaphore<br/>video_concurrency=16] --> B1[VideoTreeBuilder 视频A]
        V --> B2[VideoTreeBuilder 视频B]
        V --> B3[...]
    end
    B1 --> API[全局共享 Semaphore<br/>api_concurrency=16]
    B2 --> API
    B3 --> API
    API --> E[VLM/LLM 端点]
```

| 参数 | 默认 | 语义 |
|------|------|------|
| `--video-concurrency` | 16 | 同时在建的视频数；吞吐提升来自非 API 阶段跨视频重叠 |
| `--api-concurrency` | 16 | 全局在途 VLM/LLM 调用上限，跨所有视频共享**一个** Semaphore 实例——端点压力与今天单视频建树完全一致 |

熔断阈值按 repair_trees 惯例缩放：`max(cfg_threshold, api_concurrency * 2)`。

**配置归属（D7 规则，Codex 审查补充）**：

| 参数 | 归属 | 理由 |
|------|------|------|
| `api_concurrency` | 工程配置 `.env`（`TREE_BUILD_API_CONCURRENCY=16`） | 端点保护参数，少变、随部署环境定 |
| `video_concurrency` | sh 脚本写死（默认 16）+ CLI 单次覆盖 | 单机吞吐参数，随硬件观测调整，不进科研 YAML（不会被实验扫动） |

### builder 改动（唯一的存量修改）

两处（Codex 审查修正后）：

1. **公开异步入口**：现有 `build()` 是同步壳（内部 `asyncio.run(self._build_async(...))`），在异步批量编排里调用会触发"事件循环嵌套"运行时错误。将 `_build_async` 提升为公开 `build_async()` 供批量工具调用；同步 `build()` 保留原样（内部改为调 `build_async`），单视频调用方零影响。
2. **Semaphore 注入**：内部 Semaphore 改为构造器可选参数（不传则自建，行为零变化）。builder 内部协程链本就显式传递 Semaphore，改动面极小。

### 断点续跑

- 视频级：`progress.json`（复用 repair_trees 的 `save_progress` 模式）；tree.json 存在且完整性校验通过的视频自动跳过
- 视频内：现有段级恢复（核心算法 #3）不动

### 输入输出

- 输入：`--videos-dir`（视频文件 + 可选同名 SRT）
- 输出：`store/videos/<video_id>/tree.json`；帧持久化沿用现有 cache 机制

## 4. 风险与观测

- 16 路并行 ffmpeg/cv2 解码可能压满 CPU/磁盘 → 实现时输出速率日志（视频/分钟，复刻 repair_trees），观测后再调 video_concurrency
- 日志遵循"禁止缓存、立即输出"（CLAUDE.md §2.1）

## 5. 不做什么（YAGNI）

- 不做分布式/多机；不做动态并发自适应
- 不改单视频建树算法（核心算法 #1/#2/#3 保真，仅信号量注入）

## 6. 验证

1. 单测：Semaphore 注入后单视频行为不变（默认自建路径）
2. 集成：3-4 个短视频小批量构建，验证跨视频并行、progress 跳过、全局 API 信号量生效（遥测里在途调用数 ≤ api_concurrency）
3. 中断-恢复测试：构建中 Ctrl+C 后重跑，已完成视频跳过、未完成视频从段级断点续跑

## 7. 被否方案

- **单一视频级 Semaphore（repair_trees 原样照搬）**：建树内部并发大，总 API 并发 = 视频数 × 内部并发，不可控
- **仅共享全局 API Semaphore、视频数不限**：任意多视频同时提帧会压垮磁盘 IO/CPU
