@remixmate/cli 0.9.71 → 0.9.73

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/LICENSE +45 -21
  2. package/README.md +1 -1
  3. package/README.zh-CN.md +1 -1
  4. package/dist/argv.js +1 -51
  5. package/dist/auth/auth-lock.js +1 -100
  6. package/dist/auth/auto-login.js +7 -75
  7. package/dist/auth/commands.js +40 -290
  8. package/dist/auth/credential-store.js +1 -143
  9. package/dist/auth/device-flow-runner.js +13 -110
  10. package/dist/auth/device-flow.js +1 -132
  11. package/dist/auth/ensure.js +1 -62
  12. package/dist/auth/environment.js +1 -69
  13. package/dist/auth/pending-store.js +1 -45
  14. package/dist/auth/resolve.js +1 -44
  15. package/dist/billing.js +3 -76
  16. package/dist/capabilities.js +1 -75
  17. package/dist/cli.js +39 -211
  18. package/dist/diagnostics.js +2 -70
  19. package/dist/doctor.js +9 -188
  20. package/dist/errors.js +1 -33
  21. package/dist/exec.js +4 -47
  22. package/dist/handlers/gen-digital-human.js +33 -264
  23. package/dist/handlers/gen-image.js +9 -113
  24. package/dist/handlers/gen-post.js +15 -188
  25. package/dist/handlers/gen-video.js +7 -185
  26. package/dist/handlers/gen-voice.js +19 -133
  27. package/dist/handlers/index.js +1 -20
  28. package/dist/handlers/shared.js +1 -66
  29. package/dist/handlers/template-init.js +1 -21
  30. package/dist/http.js +3 -155
  31. package/dist/index.js +1 -8
  32. package/dist/install.js +40 -448
  33. package/dist/manifest.json +53 -13
  34. package/dist/progress.js +2 -15
  35. package/dist/project/commands.js +34 -156
  36. package/dist/project/host.js +1 -151
  37. package/dist/project/resolve.js +1 -65
  38. package/dist/project/store.js +2 -97
  39. package/dist/project/take.js +1 -106
  40. package/dist/project/trace.js +5 -278
  41. package/dist/registry.js +3 -70
  42. package/dist/runner.js +7 -225
  43. package/dist/skill-schema.js +1 -194
  44. package/dist/template/args.js +1 -69
  45. package/dist/template/check.js +2 -30
  46. package/dist/template/commands.js +34 -163
  47. package/dist/template/constants.js +1 -46
  48. package/dist/template/init.js +5 -133
  49. package/dist/template/list.js +1 -46
  50. package/dist/template/preview.js +7 -318
  51. package/dist/template/proc.js +1 -43
  52. package/dist/template/project.js +6 -161
  53. package/dist/template/publish.js +5 -185
  54. package/dist/template/render-client.js +2 -79
  55. package/dist/template/upgrade.js +4 -123
  56. package/dist/text.js +1 -15
  57. package/package.json +2 -35
  58. package/skills/doc-parse/scripts/extract_via_render.py +32 -32
  59. package/skills/export-jianying/scripts/gen_jianying_draft.py +188 -188
  60. package/skills/gen-post/SKILL.md +23 -13
  61. package/skills/gen-post/skill.json +9 -10
  62. package/skills/gen-script/SKILL.md +23 -1
  63. package/skills/gen-script/scripts/gen_script.py +644 -551
  64. package/skills/gen-script/skill.json +12 -0
  65. package/skills/gen-script/version.json +1 -1
  66. package/skills/prepare-video-assets/scripts/prepare_video_assets.py +21 -21
  67. package/skills/render-video/scripts/_chrome_vendor.py +17 -17
  68. package/skills/render-video/scripts/_digital_human_segments.py +34 -34
  69. package/skills/render-video/scripts/_video_probe.py +33 -33
  70. package/skills/render-video/scripts/_vod_polling.py +13 -13
  71. package/skills/render-video/scripts/remote_renderer_client.py +42 -42
  72. package/skills/render-video/scripts/render_video.py +639 -639
  73. package/skills/render-video/scripts/required_props.py +37 -37
  74. package/skills/render-video/scripts/upload_video.py +35 -35
  75. package/skills/render-video/version.json +1 -1
  76. package/skills/template-registry/SKILL.md +16 -0
  77. package/skills/template-registry/scripts/countdown_spec.py +92 -92
  78. package/skills/template-registry/scripts/doc_extract.py +40 -40
  79. package/skills/template-registry/scripts/list_templates.py +303 -191
  80. package/skills/template-registry/scripts/log_diagnostics.py +1 -1
  81. package/skills/template-registry/scripts/match_template.py +87 -87
  82. package/skills/template-registry/scripts/payload_hydrator.py +89 -89
  83. package/skills/template-registry/scripts/registry_loader.py +159 -159
  84. package/skills/template-registry/scripts/render_job_client.py +75 -75
  85. package/skills/template-registry/scripts/showcase_loader.py +44 -44
  86. package/skills/template-registry/skill.json +4 -1
  87. package/skills/template-registry/version.json +1 -1
  88. package/skills/template-registry/video_dsl/runtime/__init__.py +8 -8
  89. package/skills/template-registry/video_dsl/runtime/dsl_validator.py +329 -329
  90. package/skills/template-registry/video_dsl/runtime/narration_budget.py +36 -36
  91. package/skills/template-registry/video_dsl/runtime/payload_contract.py +7 -7
  92. package/skills/template-registry/video_dsl/runtime/prompt_enhancer.py +98 -98
  93. package/skills/template-registry/video_dsl/runtime/timeline_compiler.py +127 -127
  94. package/skills/template-registry/video_dsl/schema/video-dsl-v1alpha1.json +14 -1
  95. package/skills/video-parser/scripts/analyze_video.py +173 -173
  96. package/skills/video-parser/scripts/deconstruct_video.py +91 -91
  97. package/skills/video-parser/scripts/parse_via_render.py +21 -21
  98. package/skills/web-screenshot/scripts/_media_screenshot/__init__.py +4 -4
  99. package/skills/web-screenshot/scripts/_media_screenshot/bootstrap.py +7 -7
  100. package/skills/web-screenshot/scripts/_media_screenshot/browser.py +4 -4
  101. package/skills/web-screenshot/scripts/_media_screenshot/cli_args.py +4 -4
  102. package/skills/web-screenshot/scripts/_media_screenshot/js_loader.py +2 -2
  103. package/skills/web-screenshot/scripts/_media_screenshot/overlay.py +1 -1
  104. package/skills/web-screenshot/scripts/_media_screenshot/reader.py +40 -40
  105. package/skills/web-screenshot/scripts/_media_screenshot/recording.py +11 -11
  106. package/skills/web-screenshot/scripts/_media_screenshot/scenes/__init__.py +8 -8
  107. package/skills/web-screenshot/scripts/_media_screenshot/scenes/arrow.py +2 -2
  108. package/skills/web-screenshot/scripts/_media_screenshot/scenes/caption.py +1 -1
  109. package/skills/web-screenshot/scripts/_media_screenshot/scenes/click.py +1 -1
  110. package/skills/web-screenshot/scripts/_media_screenshot/scenes/code_lines.py +5 -5
  111. package/skills/web-screenshot/scripts/_media_screenshot/scenes/focus.py +1 -1
  112. package/skills/web-screenshot/scripts/_media_screenshot/scenes/highlight.py +3 -3
  113. package/skills/web-screenshot/scripts/_media_screenshot/scenes/hover.py +1 -1
  114. package/skills/web-screenshot/scripts/_media_screenshot/scenes/redact.py +1 -1
  115. package/skills/web-screenshot/scripts/_media_screenshot/scenes/scroll.py +1 -1
  116. package/skills/web-screenshot/scripts/_media_screenshot/scenes/sequence.py +1 -1
  117. package/skills/web-screenshot/scripts/_media_screenshot/scenes/title_card.py +1 -1
  118. package/skills/web-screenshot/scripts/_media_screenshot/scenes/type.py +1 -1
  119. package/skills/web-screenshot/scripts/_media_screenshot/scenes/wait.py +1 -1
  120. package/skills/web-screenshot/scripts/_media_screenshot/scenes/zoom.py +2 -2
  121. package/skills/web-screenshot/scripts/_media_screenshot/screenshot.py +15 -15
  122. package/skills/web-screenshot/scripts/_media_screenshot/storyboard.py +2 -2
  123. package/skills/web-screenshot/scripts/_media_screenshot/template.py +11 -11
  124. package/skills/web-screenshot/scripts/_media_screenshot/trim.py +10 -10
  125. package/skills/web-screenshot/scripts/_media_screenshot/urlguard.py +22 -22
  126. package/skills/web-screenshot/scripts/_media_upload.py +33 -33
  127. package/skills/web-screenshot/scripts/read_page.py +15 -15
  128. package/skills/web-screenshot/scripts/record.py +26 -26
  129. package/skills/web-screenshot/scripts/screenshot.py +5 -5
  130. package/dist/argv.d.ts +0 -15
  131. package/dist/auth/auth-lock.d.ts +0 -26
  132. package/dist/auth/auto-login.d.ts +0 -20
  133. package/dist/auth/commands.d.ts +0 -8
  134. package/dist/auth/credential-store.d.ts +0 -60
  135. package/dist/auth/device-flow-runner.d.ts +0 -58
  136. package/dist/auth/device-flow.d.ts +0 -52
  137. package/dist/auth/ensure.d.ts +0 -55
  138. package/dist/auth/environment.d.ts +0 -38
  139. package/dist/auth/pending-store.d.ts +0 -33
  140. package/dist/auth/resolve.d.ts +0 -30
  141. package/dist/billing.d.ts +0 -44
  142. package/dist/capabilities.d.ts +0 -63
  143. package/dist/cli.d.ts +0 -13
  144. package/dist/diagnostics.d.ts +0 -24
  145. package/dist/doctor.d.ts +0 -22
  146. package/dist/errors.d.ts +0 -29
  147. package/dist/exec.d.ts +0 -18
  148. package/dist/handlers/gen-digital-human.d.ts +0 -17
  149. package/dist/handlers/gen-image.d.ts +0 -20
  150. package/dist/handlers/gen-post.d.ts +0 -21
  151. package/dist/handlers/gen-video.d.ts +0 -19
  152. package/dist/handlers/gen-voice.d.ts +0 -26
  153. package/dist/handlers/index.d.ts +0 -20
  154. package/dist/handlers/shared.d.ts +0 -26
  155. package/dist/handlers/template-init.d.ts +0 -10
  156. package/dist/http.d.ts +0 -72
  157. package/dist/index.d.ts +0 -9
  158. package/dist/install.d.ts +0 -71
  159. package/dist/progress.d.ts +0 -23
  160. package/dist/project/commands.d.ts +0 -19
  161. package/dist/project/host.d.ts +0 -91
  162. package/dist/project/resolve.d.ts +0 -43
  163. package/dist/project/store.d.ts +0 -31
  164. package/dist/project/take.d.ts +0 -50
  165. package/dist/project/trace.d.ts +0 -98
  166. package/dist/registry.d.ts +0 -30
  167. package/dist/runner.d.ts +0 -39
  168. package/dist/skill-schema.d.ts +0 -138
  169. package/dist/template/args.d.ts +0 -25
  170. package/dist/template/check.d.ts +0 -23
  171. package/dist/template/commands.d.ts +0 -25
  172. package/dist/template/constants.d.ts +0 -39
  173. package/dist/template/init.d.ts +0 -28
  174. package/dist/template/list.d.ts +0 -23
  175. package/dist/template/preview.d.ts +0 -60
  176. package/dist/template/proc.d.ts +0 -26
  177. package/dist/template/project.d.ts +0 -47
  178. package/dist/template/publish.d.ts +0 -43
  179. package/dist/template/render-client.d.ts +0 -39
  180. package/dist/template/upgrade.d.ts +0 -44
  181. package/dist/text.d.ts +0 -8
@@ -1,4 +1,4 @@
1
- """Portable stdlib-only context and opt-in stderr diagnostics; never writes stdout."""
1
+
2
2
  import json
3
3
  import os
4
4
  import re
@@ -1,39 +1,39 @@
1
- """
2
- template-registry 库模块 — DSL → TemplateBinding 的纯函数实现。
3
1
 
4
- 仅作为 Python library 被 render-video 的 render_video.py / gen-script 的
5
- gen_script.py 通过 `sys.path.insert` 后 `import match_template` 引入;
6
- 对外不暴露 binding CLI。template-registry skill 的 `--list-templates` 入口由
7
- sibling 的 `list_templates.py` 实现(它复用 registry_loader 的统一加载 +
8
- 状态门控),两者共享同一份 registry 读取逻辑,无 TS 侧重复实现。
9
2
 
10
- 提供的公共接口:
11
- load_registry() → list[dict] 模板列表
12
- load_full_template(template) → dict 模板完整定义(registry 自带,原样返回)
13
- build_binding(template, dsl) → dict DSL → TemplateBinding
14
- """
3
+
4
+
5
+
6
+
7
+
8
+
9
+
10
+
11
+
12
+
13
+
14
+
15
15
 
16
16
  from registry_loader import load_registry_data
17
17
  import sys
18
18
 
19
19
 
20
20
  def load_registry() -> list:
21
- """Load template list via the unified registry loader."""
21
+
22
22
  return load_registry_data().get("templates", [])
23
23
 
24
24
 
25
25
  def load_full_template(template: dict) -> dict:
26
- """Load the full template definition.
27
26
 
28
- Since the metadata registry.json now contains the complete template
29
- definition (including slotMapping, compositions, etc.), this simply
30
- returns the template as-is.
31
- """
27
+
28
+
29
+
30
+
31
+
32
32
  return template
33
33
 
34
34
 
35
35
  def _resolve_slot_def(slot_mapping: dict, purpose: str) -> dict:
36
- """Resolve a slotMapping entry, following $ref chains (max depth 10)."""
36
+
37
37
  slot_def = slot_mapping.get(purpose) or slot_mapping.get("default") or {}
38
38
  visited: set = set()
39
39
  while "$ref" in slot_def:
@@ -46,15 +46,15 @@ def _resolve_slot_def(slot_mapping: dict, purpose: str) -> dict:
46
46
 
47
47
 
48
48
  def _extract_prop(scene: dict, extractor: dict):
49
- """Extract a single prop value from a scene using an extractor definition.
50
49
 
51
- Supports two extractor patterns:
52
- { "from": "textLayers", "role": "headline" } – search textLayers by role
53
- { "from": "visuals.background.assetRef" } – dotted-path traversal
54
- """
50
+
51
+
52
+
53
+
54
+
55
55
  source = extractor.get("from", "")
56
56
 
57
- # textLayers extractor
57
+
58
58
  if source == "textLayers":
59
59
  role = extractor.get("role", "")
60
60
  for tl in scene.get("textLayers") or []:
@@ -62,7 +62,7 @@ def _extract_prop(scene: dict, extractor: dict):
62
62
  return tl.get("content")
63
63
  return None
64
64
 
65
- # Dotted-path extractor
65
+
66
66
  val = scene
67
67
  for part in source.split("."):
68
68
  if not isinstance(val, dict):
@@ -72,7 +72,7 @@ def _extract_prop(scene: dict, extractor: dict):
72
72
 
73
73
 
74
74
  def build_binding(template: dict, dsl: dict) -> dict:
75
- """Build a TemplateBinding from template and DSL."""
75
+
76
76
  bindings = []
77
77
  slot_mapping = template.get("slotMapping", {})
78
78
 
@@ -84,41 +84,41 @@ def build_binding(template: dict, dsl: dict) -> dict:
84
84
  comp_id = slot_def.get("compositionId", "GenericScene")
85
85
  slot_id = slot_def.get("slotId", f"{purpose}-slot")
86
86
 
87
- # 用模板的 propExtractors 提取 props
87
+
88
88
  props = {}
89
89
  for prop_name, extractor in slot_def.get("propExtractors", {}).items():
90
90
  val = _extract_prop(scene, extractor)
91
91
  if val is not None:
92
92
  props[prop_name] = val
93
93
 
94
- # P1.3: props 提取仅保留两条路径——
95
- # (1) 显式 propExtractors(上面的 _extract_prop 调用)
96
- # (4) customPayload 全字段透传(顶层 + templateData)— 模板内部 schema
97
- # 去掉了原来的"白名单同名隐式透传"两层 fallback,因为它们让"字段写在哪"
98
- # 这个问题变得不可预测:同一字段写在 optionalProps + propExtractors
99
- # 里时优先级会冲突,让 AI 反复出错。
100
- #
101
- # 顶层透传是必要的:DSL 示例(hermes-agent-purple / vibe-coding 等)以
102
- # `customPayload.slideId` 顶层声明 slide id,而 html-slide 模板的
103
- # `propExtractors.slideId` 只声明从 templateData.slideId 取值。如果不
104
- # 同时透传顶层,slideId 就会缺失,html-slide 的 `point` slot 把
105
- # slideId 设为 requiredProps,缺失会导致 React 端回退到通用「标题 +
106
- # 字幕」兜底样式,所有场景看起来都是同一个布局。
94
+
95
+
96
+
97
+
98
+
99
+
100
+
101
+
102
+
103
+
104
+
105
+
106
+
107
107
  custom = scene.get("customPayload") or {}
108
108
  template_data = custom.get("templateData") or scene.get("templateData") or {}
109
109
 
110
- # 脆弱点 3:双通道冲突检测。同一字段名既声明在 slot 的 propExtractors,
111
- # 又被 customPayload 的全字段透传命中时,来源二义:propExtractors 永远赢
112
- # (下面 `k not in props` 透传),但如果 extractor 返回 None、值又恰好在
113
- # payload 里,作者会误以为"是 payload 生效",反之亦然。html-slide 的
114
- # slideId 事故正是此类——extractor 读 `templateData.slideId`,DSL 却把值
115
- # 写在 `customPayload.slideId` 顶层。
116
- #
117
- # 仅标记**真正的位置冲突**,避开误报:当 extractor 的 `from` 恰好指向
118
- # `templateData.<同名>` 时,templateData 里出现该字段是它的**法定来源**,
119
- # 不算"写错位置",跳过。其余命中(顶层透传、或 templateData 透传但 extractor
120
- # 从别处取值)才算冲突。只记录 + warning,不改透传行为;CI 的
121
- # test-template-pipeline.py 据此浮出去重提示。
110
+
111
+
112
+
113
+
114
+
115
+
116
+
117
+
118
+
119
+
120
+
121
+
122
122
  extractors = slot_def.get("propExtractors", {}) or {}
123
123
  top_keys = {k for k in custom.keys() if k != "templateData"}
124
124
  dual_channel = []
@@ -130,7 +130,7 @@ def build_binding(template: dict, dsl: dict) -> dict:
130
130
  dual_channel.append(k)
131
131
  dual_channel = sorted(dual_channel)
132
132
 
133
- # 先注入 templateData.* 再注入顶层 customPayload.*,让顶层覆盖同名值
133
+
134
134
  for k, v in template_data.items():
135
135
  if k not in props:
136
136
  props[k] = v
@@ -140,16 +140,16 @@ def build_binding(template: dict, dsl: dict) -> dict:
140
140
  if k not in props:
141
141
  props[k] = v
142
142
 
143
- # animationHints 可能为 null
143
+
144
144
  anim = scene.get("animationHints") or {}
145
145
  entrance = anim.get("entrance", "fade")
146
146
  transition_type = entrance if entrance != "none" else "cut"
147
147
 
148
- # requiredProps 完整性:slotMapping 声明的 requiredProps 必须都已进 props。
149
- # 这里只**记录**缺失(不抛错,保持渲染行为不变);CI 的
150
- # test-template-pipeline.py 据此断言「requiredProps 齐全」并 fail loud,
151
- # 避免历史上「声明了 requiredProps 却静默缺失 → React 端回退通用布局」的事故
152
- # (见 template-json-design.md 脆弱点 2)。是否升级为渲染前硬失败属后续决策。
148
+
149
+
150
+
151
+
152
+
153
153
  required_props = slot_def.get("requiredProps") or []
154
154
  missing_required = [p for p in required_props if p not in props]
155
155
 
@@ -174,33 +174,33 @@ def build_binding(template: dict, dsl: dict) -> dict:
174
174
  )
175
175
  bindings.append(binding_row)
176
176
 
177
- # P1.2: 不再做 screen-walkthrough 专属的截断 / 兜底。模板的 propExtractors
178
- # 已声明 titleText ← textLayers[role=headline]、projectName ← textLayers[role=subheadline],
179
- # gen_script 在每个场景都填入了 headline / subheadline,因此 props 由提取器
180
- # 直接产出,不再需要后处理。
177
+
178
+
179
+
180
+
181
181
 
182
182
  typography = template.get("defaultTypography", {})
183
183
  motion = template.get("defaultMotionPreset", "smooth")
184
184
  colors = template.get("defaultColorScheme", [])
185
185
 
186
- # 用户选中的字体(DSL 的 global.font)。这里只是把不透明 key 折进 typography,
187
- # 真正的解析(uniqId → 排印家族名 + 字重文件)在 ab-render 侧完成。
188
- #
189
- # 优先级见 ab-platform/docs/product/creation/font-selection-design.md §13.3:
190
- # 用户选择 > variant.defaultFontId(预留档位,当前恒为空)
191
- # > template.defaultTypography.bodyFont(现状裸 family 名)> 模板 theme 字栈
192
- # 下面的 variant 合并在此之后执行,所以 fontId 单独在合并后再写回,
193
- # 避免被 variant 的 defaultTypography 覆盖掉。
186
+
187
+
188
+
189
+
190
+
191
+
192
+
193
+
194
194
  user_font_id = ((dsl.get("global", {}) or {}).get("font", {}) or {}).get("fontId")
195
195
 
196
- # --- variant selection(确定性)---
197
- # 优先级:
198
- # 1) DSL 显式声明 — meta.templateVariant / meta.variant / renderHints.templateVariant
199
- # 2) meta.style 与 variant.styleTags 的**分级**匹配(精确 > 整段包含 > token 子串)
200
- # 旧实现按 dict 遍历顺序「命中第一个」,多义时结果不稳定;现改为:
201
- # - 按 variant id 排序遍历(消除顺序依赖);
202
- # - 取匹配等级最高(tier 数最小)的 variant;
203
- # - 同一最高等级有多个候选 → 记歧义、取排序首个,并把理由写进 variantReason。
196
+
197
+
198
+
199
+
200
+
201
+
202
+
203
+
204
204
  variant_id = None
205
205
  variant_reason = None
206
206
  meta = dsl.get("meta", {}) or {}
@@ -218,7 +218,7 @@ def build_binding(template: dict, dsl: dict) -> dict:
218
218
  elif variants:
219
219
  raw_style = (meta.get("style") or "").strip().lower()
220
220
  if raw_style:
221
- # token 化:整段 + 分隔符拆分 + 单中文字符级
221
+
222
222
  s = raw_style
223
223
  for sep in [",", ",", ";", ";", " ", "/", "·", "、"]:
224
224
  s = s.replace(sep, ",")
@@ -232,10 +232,10 @@ def build_binding(template: dict, dsl: dict) -> dict:
232
232
  if "\u4e00" <= ch <= "\u9fff":
233
233
  tokens.add(ch)
234
234
 
235
- # 为每个 variant 算最佳匹配等级(1 最强)
236
- # tier 1: 某 style token 与某 tag 完全相等
237
- # tier 2: 整段 raw_style 与某 tag 互为子串(整体短语级)
238
- # tier 3: 某 token 与某 tag 互为子串(细粒度,最宽松)
235
+
236
+
237
+
238
+
239
239
  by_tier: dict = {}
240
240
  for vid in sorted(variants.keys()):
241
241
  vtags = [str(t).strip().lower() for t in (variants[vid].get("styleTags") or []) if str(t).strip()]
@@ -251,7 +251,7 @@ def build_binding(template: dict, dsl: dict) -> dict:
251
251
 
252
252
  if by_tier:
253
253
  best_tier = min(by_tier.keys())
254
- winners = by_tier[best_tier] # 已按 vid 排序
254
+ winners = by_tier[best_tier]
255
255
  variant_id = winners[0]
256
256
  if len(winners) > 1:
257
257
  variant_reason = (
@@ -278,7 +278,7 @@ def build_binding(template: dict, dsl: dict) -> dict:
278
278
  if vcolors:
279
279
  colors = vcolors
280
280
 
281
- # 用户的字体选择压过 variant 默认值(§13.3 的优先级第一档),所以写在合并之后。
281
+
282
282
  if isinstance(user_font_id, str) and user_font_id.strip():
283
283
  typography = {**typography, "fontId": user_font_id.strip()}
284
284
 
@@ -1,41 +1,41 @@
1
1
  #!/usr/bin/env python3
2
- """Fill a template-declared ``customPayload`` field from a source document, by reference.
3
-
4
- 问题(2026-09-23 线上 conv_Sesw2g8oM81 的根因)
5
- ---------------------------------------------
6
- ``ppt-to-video`` 要求每个场景都有 ``customPayload.slide`` —— 一整页 PPT 的节点树、
7
- 坐标、配色、beats。这份数据只有一个合法产出方(ab-render 的 ``/extract`` layout 层),
8
- 而它**体积大到不能过模型**:一页带内嵌图标可达几百 KB,23 页就是好几 MB。
9
-
10
- 于是模板的 llmHint 写着「把返回的 slides[i].slide 整个填进 customPayload.slide」,
11
- doc-parse 的 SKILL.md 写着「feed slides[i] straight into customPayload.slide」,而实际
12
- 链路是:doc_parse 把全量写进本地文件 → 模型没有任何文件读取工具 → 拿不到 →
13
- prepare_video_assets 报 ``customPayload.slide (required)`` ×N。模型无论怎么改都过不了。
14
-
15
- 解法
16
- ----
17
- **不让这份数据过模型**。DSL 里只留一个引用(``meta.sourceDocument``),skill 侧在
18
- 校验之前自己去把全量取回来填进每个场景。模型负责的是它真正能负责的东西:旁白、
19
- 场景与页的对应关系、以及 mode 的下压。
20
-
21
- 这与 ``extractCustomPayloadOverrides`` / ``extractNarrationOverrides`` 是同一类教训的
22
- 第三次:**合并层丢掉了数据,而下游报的错指向模型**。前两次丢的是 slideId 和 items。
23
-
24
- 模板怎么声明
25
- -----------
26
- ``template.json`` 的 ``capabilities.payloadHydration``::
27
-
28
- "payloadHydration": {
29
- "payloadField": "slide", # 要填的 customPayload 字段名
30
- "source": "doc-extract", # 数据来源(目前只有这一个)
31
- "docKind": "pptx",
32
- "fields": "content,layout", # 传给 /extract 的层
33
- "indexField": "slideIndex" # 场景→页的映射键(写在 customPayload 里)
34
- }
35
2
 
36
- 不写 ``if template_id == "ppt-to-video"``(AGENTS.md 明令禁止):任何模板声明了这一段
37
- 就自动获得同样的行为。
38
- """
3
+
4
+
5
+
6
+
7
+
8
+
9
+
10
+
11
+
12
+
13
+
14
+
15
+
16
+
17
+
18
+
19
+
20
+
21
+
22
+
23
+
24
+
25
+
26
+
27
+
28
+
29
+
30
+
31
+
32
+
33
+
34
+
35
+
36
+
37
+
38
+
39
39
 
40
40
  from __future__ import annotations
41
41
 
@@ -45,7 +45,7 @@ from typing import Callable, Optional
45
45
 
46
46
  sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
47
47
 
48
- from doc_extract import ( # noqa: E402
48
+ from doc_extract import (
49
49
  DocExtractError,
50
50
  extract_document,
51
51
  slides_by_index,
@@ -57,29 +57,29 @@ DEFAULT_INDEX_FIELD = "slideIndex"
57
57
 
58
58
 
59
59
  def declared_subfields(template_config: Optional[dict], payload_field: str) -> Optional[set[str]]:
60
- """模板为 ``customPayload.<payload_field>`` 声明的子字段名集合。
61
60
 
62
- 注入必须按这个集合**投影**,不能把抽取器返回的整个对象原样塞进去 ——
63
- 两边的字段集根本不是一回事:
64
61
 
65
- 抽取器返回(每页) slideIndex / title / text / rawNotes / verdict / coverage /
66
- flags / nodes / beats / mode / modeReason / ambientIds /
67
- background / recommendedSec / canvas …
68
- 模板声明(slide) nodes / beats / mode / modeReason / ambientIds /
69
- background / recommendedSec / canvas
70
62
 
71
- 前者多出来的那些是**内容层与抽取器自己的记账字段**(备注原文、质检结论、覆盖率),
72
- 渲染端一个都不读。而 ``dsl_validator._check_custom_payload_fields`` 会逐字段核对
73
- 声明表,遇到没声明的就报「the slide does not read 'x' — the value is silently
74
- dropped」并整单拒绝。2026-09-23 线上 conv_StrnauS7TaZNUQV4BfLX 就是这样:
75
- 注入明明成功(23/23 页),却因为多了 5 个字段被校验整单拒掉,而且**重试必然复现**。
76
63
 
77
- 为什么用投影而不是写个黑名单:抽取器以后再加字段(verdict / coverage 就是后加的)
78
- 黑名单就会再漏一次。声明表是渲染端真正读的那一份,按它投影才不会二次失效。
79
64
 
80
- 声明表读不到时返回 None = 不投影。这与校验层的行为一致:没有
81
- ``customPayloadSchema`` 时它也不查字段名,所以不投影不会引入新的失败。
82
- """
65
+
66
+
67
+
68
+
69
+
70
+
71
+
72
+
73
+
74
+
75
+
76
+
77
+
78
+
79
+
80
+
81
+
82
+
83
83
  if not template_config:
84
84
  return None
85
85
  cps = template_config.get("customPayloadSchema")
@@ -95,21 +95,21 @@ def declared_subfields(template_config: Optional[dict], payload_field: str) -> O
95
95
 
96
96
 
97
97
  class HydrationError(RuntimeError):
98
- """注入失败。message 必须直接告诉调用方下一步做什么。
98
+ pass
99
+
100
+
101
+
102
+
99
103
 
100
- 这个类存在的全部意义是替换掉 ``customPayload.slide (required)`` ×20 那种报错 ——
101
- 它说的是"某个字段没填",而真相是"这个字段的数据源没有通路",两者要采取的行动
102
- 完全不同,而模型只看得到前者。
103
- """
104
104
 
105
105
 
106
106
  def hydration_spec(template_config: Optional[dict]) -> Optional[dict]:
107
- """读 ``capabilities.payloadHydration``;未声明 / 声明无效返回 None。
108
107
 
109
- 宽容读取:一个写坏的 template.json 不该让所有模板的渲染一起失败。但
110
- ``source`` 认不出来时要报错而不是静默跳过 —— 静默跳过会退回到「必填字段缺失」
111
- 那条死路,也就是这个模块本来要修掉的东西。
112
- """
108
+
109
+
110
+
111
+
112
+
113
113
  caps = (template_config or {}).get("capabilities")
114
114
  if not isinstance(caps, dict):
115
115
  return None
@@ -140,16 +140,16 @@ def hydration_spec(template_config: Optional[dict]) -> Optional[dict]:
140
140
 
141
141
 
142
142
  def source_document_ref(dsl: dict, override_url: Optional[str] = None) -> Optional[dict]:
143
- """取 DSL 里的源文件引用。``override_url`` 优先(CLI 显式传入时)。
144
143
 
145
- 位置是 ``meta.sourceDocument`` 而不是场景级:一条片子对一份源文件,写在场景里
146
- 会得到 23 份同样的 URL,还得处理它们互相矛盾的情况。
147
144
 
148
- 为什么是 ``meta``:ab-agent 的合并层(``applyNarrationToSkeleton``)以 gen_script
149
- 骨架为基底,只从模型提交的 DSL 里搬 narration 与 customPayload,``meta`` / ``global``
150
- 归骨架所有。所以写在 meta 的引用能活着穿过合并层,而模型自己往 dsl_json 里写
151
- ``meta.sourceDocument`` 会被丢掉 —— 引用必须在 gen_script 那一步就进骨架。
152
- """
145
+
146
+
147
+
148
+
149
+
150
+
151
+
152
+
153
153
  if override_url:
154
154
  return {"url": override_url}
155
155
  meta = dsl.get("meta")
@@ -178,16 +178,16 @@ def hydrate_dsl(
178
178
  override_url: Optional[str] = None,
179
179
  log: Optional[Callable[[str], None]] = None,
180
180
  ) -> list[str]:
181
- """把源文件的每一页填进对应场景的 ``customPayload.<payloadField>``。
182
181
 
183
- 返回一份给人看的报告行列表(调用方自己决定打到哪)。什么都没做时返回空列表。
184
182
 
185
- 映射规则:场景的 ``customPayload.<indexField>`` 是 1-based 页号,缺省按场景顺序
186
- 取第 i 页。显式页号优先 —— 用户可能只讲其中几页,或者把两页并成一个场景。
187
183
 
188
- 已有值不覆盖:场景里已经有 ``customPayload.slide`` 时跳过。直接调 CLI 的调试流程
189
- 会手动塞一份进去,把它冲掉等于让调试者的输入人间蒸发。
190
- """
184
+
185
+
186
+
187
+
188
+
189
+
190
+
191
191
  emit = log or (lambda _msg: None)
192
192
 
193
193
  spec = hydration_spec(template_config)
@@ -244,13 +244,13 @@ def hydrate_dsl(
244
244
  filled = 0
245
245
  missing: list[str] = []
246
246
 
247
- # 按模板声明投影。抽取结果里混着内容层与抽取器记账字段,渲染端一个都不读,
248
- # 而校验层见到未声明字段会整单拒绝(见 declared_subfields 的说明)。
247
+
248
+
249
249
  allowed = declared_subfields(template_config, field)
250
250
  dropped: set[str] = set()
251
251
 
252
- # 页号只有在模板**声明**了它的时候才写回去。没声明就是未知字段,写了会被校验
253
- # 拒掉 —— 这正是上一版的第二个错:既投影不足,又多写了一个 customPayload.slideIndex。
252
+
253
+
254
254
  index_is_declared = index_field in (declared_subfields(template_config, index_field) or ())
255
255
  if not index_is_declared:
256
256
  cps = (template_config or {}).get("customPayloadSchema")
@@ -271,7 +271,7 @@ def hydrate_dsl(
271
271
  else:
272
272
  dropped |= {k for k in page if k not in allowed}
273
273
  payload[field] = {k: v for k, v in page.items() if k in allowed}
274
- # 留下"这一场用的是第几页"的痕迹 —— 仅当模板声明了这个字段。
274
+
275
275
  if index_is_declared:
276
276
  payload.setdefault(index_field, page_no)
277
277
  filled += 1
@@ -283,15 +283,15 @@ def hydrate_dsl(
283
283
  f"{source_meta.get('name', 'the source document')}"
284
284
  )
285
285
  if dropped:
286
- # 说清丢了什么:这些是内容层/记账字段,渲染端本来就不读。写出来是为了
287
- # 抽取器哪天把一个**该读**的字段也归到这里时,有人看得见。
286
+
287
+
288
288
  report.append(
289
289
  f" (已按模板声明投影,忽略 {len(dropped)} 个渲染端不读的字段:"
290
290
  f"{', '.join(sorted(dropped)[:8])})"
291
291
  )
292
292
 
293
293
  if missing:
294
- # 页号对不上不是警告而是错误:少一页 = 成片里一个空白场景,而它渲得出来。
294
+
295
295
  raise HydrationError(
296
296
  f"{len(missing)} scene(s) point at a page that the source document does not have "
297
297
  f"({'; '.join(missing[:5])}"
@@ -300,7 +300,7 @@ def hydrate_dsl(
300
300
  f"or fix customPayload.{index_field} on those scenes."
301
301
  )
302
302
 
303
- # 降级记录要顺着传上来 —— 画面上少一块,这里不说就没人知道为什么。
303
+
304
304
  flagged = [
305
305
  f"page {no}: {'; '.join(page.get('flags') or [])}"
306
306
  for no, page in sorted(pages.items())