🔗 管线 → 无限画布 同步机制全景

runner.py 如何在每个 Phase 完成后自动同步资产 + 结构化参数到无限画布

一、整体数据流:从 Phase 执行到画布节点

① Phase 模块执行
p03_script_audit.py 等 17 个 phase,各自调用 delegate_task 完成 expert 工作
result = {"phase": "p03_script_audit", "outputs": {...}, "gate": None}
▼
② runner.py — on_phase_complete 回调
每个 phase 完成后,runner 调用 cfg.on_phase_complete(episode_id, phase_id, result)
runner.py L895-897
▼
③ CanvasSyncSubscriber.on_phase_complete()
核心同步引擎 (2696行):从 result 中提取资产 → 映射结构化字段 → 构建画布图 → POST 到画布 API
canvas_sync.py L716-886
▼ 内部 7 步
④ Canvas API (save-v2)
POST /api/canvas/v2/save-v2 → 写入 React Flow 节点 → 前端渲染

二、同步引擎内部 7 步(on_phase_complete)

1
Load Canvas → 规范化图

load_canvas() → normalize_loaded_graph() — 加载当前画布所有节点+边,确保结构合法

2
创建 Zone(椭圆容器节点)

ensure_zone_node(graph, "p03") + ensure_zone_chain(graph) — 每个 phase 一个椭圆,按 P01→P02→…→P14 链式排列

3
创建 Phase 摘要节点

_build_phase_node() → upsert_node(graph, "n-p03_script_audit", data) — 摘要节点放在 zone 内,包含描述/分数/审核状态

4
提取资产子节点(核心)

_extract_artifacts() — 根据 _PHASE_OUTPUT_MAP 从 result["outputs"] 中提取每个资产项,透传全部字段

5
结构化字段映射

_build_artifact_node() → _apply_schema_mappings() — snake_case→camelCase、中文 enum→英文 enum、派生字段

6
跨 Phase 引用链接

_add_reference_links() — 如果资产标记了 consumes: ["p07/S01_front"],自动画一条上游依赖箭头

7
Sanitize + Save

sanitize_v2_graph(graph) — 清理所有字段确保通过 v2 zod schema → save_canvas(graph) → POST /api/canvas/v2/save-v2

三、Phase 输出 → 画布节点类型映射表

_PHASE_OUTPUT_MAP + _PHASE_TYPE_MAP 定义了每个 Phase 的哪些 output key 会变成画布节点,以及节点的 canvas type

Phase Output Key 节点标签 Canvas Type 展开子节点?
P01topic_kernel选题核script✅
hook_design钩子设计script✅
retention_engineering留存工程script—
P03script剧本script✅
audit_report审计报告script✅
P04character_bible角色设定集asset✅
character_assets角色资产asset✅
P07scene_images / images场景图asset—
scene_multi_angle场景多角度asset—
style_vector风格设定script—
color_intent色彩意图script—
P09shot_list分镜列表storyboard—
P10voice_clips语音片段audio—
P11video_clips视频片段video—

四、结构化参数映射:snake_case → 画布 zod schema

_apply_schema_mappings() (canvas_sync.py L299-459) 负责把 Phase 产出的原始字段翻译成画布前端 canvasAssetSchema.ts 能识别的字段

P06/P09 分镜 → storyboard 节点

camera_movement: "缓慢推进"
→
cameraMovement: "zoom_in"
framing: "中景"
→
framing: "medium"
composition: "三分法"
→
composition: "rule_of_thirds"
pacing: "快"
→
pacing: "fast"
axis_line: "左→右"
→
axisLine: "L2R"
ltx_prompt: "camera push in..."
→
ltxPrompt: "camera push in..."
shot_type: "CU" + duration_sec: 3.5
→
shot_type + duration_sec (zod 必填!)

P04 角色 → asset 节点

role: "社畜主角"
→
archetype: "protagonist" (派生)
age_variants: ["young_adult", "teen"]
→
ageRange: "young_adult" (取首项)

P01/P02/P03 剧本 → script 节点

hook_type: "情感钩"
→
hookType: "emotional_hook"
hook_strength: 0.8
→
hookIntensity: 4 (×5 四舍五入)
narrative_structure: "Man in a Hole"
→
mcmahonArc: "Man_in_a_Hole"

P10 音频 → audio 节点

audio_type: "对话"
→
audioType: "dialogue"
engine: "cosyvoice"
→
engine: "cosyvoice"
emotion: "愤怒"
→
emotion: "angry"

P11 视频 → video 节点

duration_sec: 3.5
→
duration: 3.5
murch_grade: 7.5
→
murchGrade: "excellent" (≥7→excellent, ≥5→pass...)
resolution: "1280x704"
→
resolution: "1280x704"

五、关键组件分工

runner.py

kais-movie-pipeline/pipeline/runner.py

编排器。每个 phase 完成后调用 on_phase_complete(episode_id, phase_id, result)。如果 CanvasClientError(HTTP 4xx)→ 停管线(Kai 原则:不掩盖问题)

canvas_sync.py

plugins/kais_aigc/canvas_sync.py (2696行)

同步引擎。7 步流程:load → zone → summary → artifacts → field mapping → reference links → sanitize + save

canvas_graph.py

plugins/kais_aigc/canvas_graph.py

图操作。zone 位置计算、节点 upsert、边创建、v2 schema sanitize、V4 语义布局(characterId/sceneId 分组)

canvas.py

plugins/kais_aigc/canvas.py

HTTP 客户端。load_canvas() POST load-v2 → save_canvas() POST save-v2。处理 project_id + episodes_id

_manifest.py

kais-movie-pipeline/pipeline/phases/_manifest.py

资产清单。每个 phase 写 manifest.json 到 OSS 目录,记录 nodes + edges + params。文档即画布 — manifest IS the topology

_canvas_helper.py

kais-movie-pipeline/pipeline/phases/_canvas_helper.py

自动连线。当 sync_canvas=None 时从环境变量 KAIS_CANVAS_URL + KAIS_CANVAS_PROJECT_ID 自动构建 sync 函数

六、一个分镜参数的完整旅程(端到端示例)

从 screenplay agent 输出到画布前端显示

以 camera_movement: "缓慢推进" 为例

1
Expert 输出(subagent JSON)

{"shot_id": "S01-01", "camera_movement": "缓慢推进", "framing": "中景", "duration_sec": 3.5, ...}

2
Phase 模块解析 → asset_bus

p09_shot_breakdown.py 把 delegate_task 返回的 JSON 解析后写入 asset_bus["shot_list"]

3
_extract_artifacts() 透传全部字段

关键设计(L1838-1858):不是白名单提取,而是透传 artifact dict 的所有字段。camera_movement、shot_type、duration_sec 等全部保留

4
_build_artifact_node() 构建 node.data

再次透传全部字段到 data dict(L2107-2111),然后调 _apply_schema_mappings()

5
_apply_schema_mappings() 字段翻译

P09 分支:_SNAKE_CAMEL 映射表把 camera_movement → 查 _CAMERA_MOVEMENT_MAP["缓慢推进"] → data["cameraMovement"] = "zoom_in"

6
sanitize_v2_graph() 清理

确保所有字段值符合 canvas v2 zod schema(枚举值合法、必填字段存在、类型正确)

7
save_canvas() → POST /api/canvas/v2/save-v2

整个 graph 序列化为 JSON POST 到画布后端。前端 React Flow 读取 data.cameraMovement 显示在 StructuredFieldPanel

七、Manifest 系统 — 文档即画布

manifest.json(每 Phase 一份)

/oss/{project_id}/p{NN}/manifest.json

记录该 phase 的全部资产节点 + 拓扑边。每个 node 携带 params 结构化参数和 consumes 上游依赖。canvas_sync 可直接读 manifest 构建画布

asset-manifest.jsonl(追加日志)

workdir/.pipeline-assets/asset-manifest.jsonl

每次写文件操作追加一行日志,记录 path, phase, engine, input_refs, hash。用于审计追溯——哪个 phase 用什么引擎生成了什么文件

双层设计:manifest.json 是结构化拓扑(给画布消费),asset-manifest.jsonl 是操作日志(给审计消费)。两者独立,互不依赖。

八、两种激活路径

路径 触发方式 同步入口 适用场景
CLI 路径 python -m pipeline.runner --canvas-url ... --canvas-project-id N register_canvas_sync() → cfg.on_phase_complete 正式管线运行(runner.py 全自动)
Agent 路径 Hermes agent 手动执行 phase / ad-hoc 调用 sync_phase_result() 或 auto_canvas_sync()(env var 触发) 调试、重跑单个 phase、agent 驱动执行

环境变量激活(零配置)

设置 KAIS_CANVAS_URL + KAIS_CANVAS_PROJECT_ID + KAIS_CANVAS_EPISODES_ID 后,即使 agent 手动调 phase(不走 runner.py),auto_canvas_sync() 也会自动构建同步函数并执行。无需在代码中显式传入 sync_canvas。

九、失败处理策略

错误类型行为原因
CanvasClientError
HTTP 4xx(数据有问题)
停管线 Kai 原则:同步失败就停,不掩盖问题。通常是 zod schema 校验失败(缺 shot_type/duration_sec 等)
ConnectError / Timeout
网络层错误
降级容忍(不阻塞管线) 画布不可达不应阻断视频生产。错误记录到 sync-errors.jsonl
write_manifest ValueError
缺必填 params
停管线 _validate_node_params() 检查每个 node type 的必填字段。video 必须有 shot_id/engine/duration_sec/resolution

核心源码: canvas_sync.py (2696L) · canvas_graph.py · canvas.py · _manifest.py (272L) · _canvas_helper.py (40L)
HTTP API: POST /api/canvas/v2/load-v2 · POST /api/canvas/v2/save-v2
前端 schema: canvasAssetSchema.ts (zod) · StructuredFieldPanel.tsx