# 工单 L1：蓝图多 shot 时间线编排 previs_timeline.py

**任务性质**：管线能力实施批（非修 bug 批）。交付 = 新模块 `scripts/previs_timeline.py` + 单测 + 交付文档。
**派发定位**：架构司机（glm-5.3，deep design + 实施）。

## 0. 基线与环境

- worktree `/data/workspace/kais-hermes-skills-wt/previs-timeline`，分支 `wt/previs-timeline`，基线 `feat/kmc-h3-blockcache` @ `b7f5fe01`。
- 你只提交到 `wt/previs-timeline` 分支，禁 push、禁碰主仓其他 worktree。
- 开工第一步：`git log --oneline -5` 认环境；`git merge-base --is-ancestor dc95d65f HEAD` 断言依赖在位。

## 1. 背景事实（operator 实测锚点，直接采用勿重新推导）

1. **camera_motion.py 真源**（`skills/kais-blender/kais-blender-layout/scripts/camera_motion.py`，421 行）：
   - `CAMERA_MOVES` 12-move 不可变枚举（static/pan/tilt/dolly_in/dolly_out/tracking/crane_up/crane_down/handheld/steadicam/zoom_in/zoom_out）
   - `move_duration_frames(move, fps) -> int`：speed 档换算真值（static=0，显式 duration_frames 优先）；static 的展示时长语义由调用方定（参考 render_showcase_12move.py 的 `or 48` 静持惯例）
   - `generate_camera_motion_lines(shot_name, move, fps, *, cam_name) -> list[str]`：生成 bpy 代码行，**调用方须 exec 并注入 `{"__name__":"__main__","bpy":bpy}`**
   - `_validate_move(move)` 已挡非法输入
2. **showcase_scene.py**（同目录）：`build_showcase_scene()` / `make_showcase_camera(shot_name, *, loc, tgt_loc, lens)` / `animate_walker_walk(f0, f1)`——统一场景纪律：多卡对比一律此起景，禁驱动自建场景（0912 Kai fix 定谳，SKILL.md 有案）。
3. **render_showcase_12move.py**（同目录）：单 Blender 进程连渲先例——逐 move `make_showcase_camera` + exec 运镜块 + `bpy.ops.render.render(animation=True)`。**时长=move_duration_frames 真值防截断**（0912 实录：硬编码 frame_end=48 把 slow 144f 截断，自审抓出）。
4. **蓝图 move 字段**：蓝图 `camera.shots[]` 每镜头可挂 `move` 字段（SKILL.md §智能运镜），但**当前 layout_engine.py 只出静帧/单 shot，多 shot 连续时间线不存在**——本批交付它。
5. Blender 环境命令：`blender --background --factory-startup --python <script>`（4.5.0 本机实测可用，WORKBENCH CPU 渲染）。
6. pytest 收集现状：`skills/kais-blender/kais-blender-layout/tests/` 目录**不存在**——你负责创建；基线 0 collected。

## 2. 任务

### C1. `scripts/previs_timeline.py` — 多 shot 时间线编排模块

核心 API（签名可微调，语义不可变）：

```python
def build_timeline(shot_plan: list[dict], fps: int = 24) -> dict:
    """shot_plan = [{"name": "s001", "move": {...12-move之一...},
                     "duration_frames": 96(可选,缺省走 speed 换算)},
                    ...]
    返回 timeline = {"shots": [...含 start_frame/end_frame...],
                     "total_frames": N, "fps": fps}"""

def render_timeline(timeline: dict, *, out_dir: str, scene_builder=None) -> list[str]:
    """单 Blender 进程顺序渲染全部 shot → 每shot一个 mp4 + 拼接成片。
    scene_builder: 可选场景构建回调(缺省= showcase_scene.build_showcase_scene)。
    per-shot: 建镜(make_showcase_camera) → exec 运镜块(绝对帧域!见 C2) → 渲染。
    拼接: ffmpeg concat demuxer(re-encode, 统一参数)。"""
```

### C2. 绝对帧域映射（本批核心设计点）

`generate_camera_motion_lines` 生成块内部用 `frame_start/frame_end` 相对语义（`_f0 = bpy.context.scene.frame_start`）。时间线里每 shot 必须落在**全局连续帧域**（shot2 从 shot1 结束帧+1 开始）。两种实现路线，你实读源码后择一并写明理由：
- 路线 A：每 shot 渲染时把 `scene.frame_start/frame_end` 临时设为该 shot 的全局区间，运镜块零改动；
- 路线 B：改写生成块帧变量为绝对帧（需 post-process 生成行，侵入 camera_motion.py——**禁改 camera_motion.py**，本批红线）。
选 A 是默认预期（零侵入）。steadicam 的 `animate_walker_walk(f0,f1)` 同样吃全局帧域。

### C3. 转场策略（最小可用）

v1 不做花式转场：shot 间硬切（concat 直接拼）。但在 `build_timeline` 支持 shot 级 `transition: {"type": "cut"}` 字段占位（校验只接受 "cut"，其他值 ValueError 提示未来扩展）。

### C4. 单测（新目录 `tests/`，参照 kais-movie-pipeline 测试风格）

不依赖 Blender 可跑（import bpy 会炸——模块顶部 bpy import 须放进函数内或 try 帧检测）。覆盖：
1. `build_timeline` 帧域计算：3 shot 各 96f → start/end 连续、total=288；
2. static shot `or 48` 静持语义；
3. 显式 duration_frames 优先；
4. 非法 move 抛 ValueError（复用 camera_motion 校验）；
5. transition 非法值拒绝；
6. 12-move 全枚举过 build_timeline（静态校验层）。

### C5. 冒烟驱动 `scripts/smoke_previs_timeline.py`

Blender headless 跑 3 shot（static 24f + dolly_in 48f + pan 36f，共 108f）：产 3 mp4 + 拼接片，打印逐 shot 帧域与产物路径。验证命令：`timeout 240 blender --background --factory-startup --python scripts/smoke_previs_timeline.py`——**发车前 operator 未实测此命令（新脚本），你须自跑成功并把输出写进交付文档**。

### C6. 交付文档 `docs/previs-timeline-DELIVERY.md`

改动清单（文件×函数×行数）、C2 路线决策与理由、冒烟输出实录、ffmpeg 拼接参数、已知边界（v1 不做交叉溶解等）。

## 3. 红线

1. **逐文件白名单**：只可新增 `scripts/previs_timeline.py`、`scripts/smoke_previs_timeline.py`、`tests/test_previs_timeline.py`、`docs/previs-timeline-DELIVERY.md`；**其余任何文件一律不动**（尤其 camera_motion.py / showcase_scene.py / render_showcase_12move.py 只读引用，禁"顺手重构"——0912 司机越界回滚姊妹批成果实录在案）。
2. 禁 GPU 渲染任务（本批 WORKBENCH CPU only）、禁跑 KMC 管线、禁 push。
3. Blender 只在冒烟驱动内调用；单测禁依赖 Blender 可执行。
4. 冒烟产物落 `/tmp/`，不入 commit。

## 4. 验证命令（operator 将原样复跑）

```bash
cd /data/workspace/kais-hermes-skills-wt/previs-timeline
python3 -m pytest skills/kais-blender/kais-blender-layout/tests/test_previs_timeline.py -q   # 全绿
python3 -m py_compile skills/kais-blender/kais-blender-layout/scripts/previs_timeline.py
timeout 240 blender --background --factory-startup --python skills/kais-blender/kais-blender-layout/scripts/smoke_previs_timeline.py 2>&1 | tail -20  # 3 mp4 + 拼接片 + 逐shot帧域打印
git diff b7f5fe01..HEAD --stat   # 恰好 4 个新文件，零越界
```
