@remixmate/cli 0.9.49 → 0.9.51
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.
- package/README.md +14 -26
- package/dist/manifest.json +18 -8
- package/package.json +9 -13
- package/skills/doc-parse/SKILL.md +6 -6
- package/skills/doc-parse/scripts/extract_via_render.py +3 -2
- package/skills/doc-parse/skill.json +1 -1
- package/skills/gen-script/SKILL.md +13 -11
- package/skills/gen-script/scripts/gen_script.py +106 -25
- package/skills/gen-script/skill.json +14 -4
- package/skills/gen-script/version.json +1 -1
- package/skills/prepare-video-assets/SKILL.md +3 -3
- package/skills/prepare-video-assets/skill.json +1 -1
- package/skills/prepare-video-assets/version.json +1 -1
- package/skills/render-video/scripts/render_video.py +77 -44
- package/skills/render-video/version.json +1 -1
- package/skills/template-registry/README.md +3 -14
- package/skills/template-registry/SKILL.md +12 -49
- package/skills/template-registry/scripts/countdown_spec.py +56 -6
- package/skills/template-registry/scripts/registry_loader.py +3 -4
- package/skills/template-registry/version.json +1 -1
- package/skills/template-registry/video_dsl/README.md +10 -53
- package/skills/template-registry/video_dsl/runtime/__init__.py +0 -1
- package/skills/template-registry/video_dsl/runtime/dsl_validator.py +44 -35
- package/skills/template-registry/video_dsl/schema/video-dsl-v1alpha1.json +1 -1
- package/skills/template-registry/scripts/check_contracts.py +0 -179
- package/skills/template-registry/scripts/sync_registry.py +0 -44
- package/skills/template-registry/scripts/template_paths.py +0 -72
- package/skills/template-registry/scripts/verify_props_contract.py +0 -273
- package/skills/template-registry/video_dsl/runtime/template_binder.py +0 -87
|
@@ -29,10 +29,12 @@ The strategy carries the shape; the template carries every concrete value:
|
|
|
29
29
|
"sceneStrategy": "countdown",
|
|
30
30
|
"countdown": {
|
|
31
31
|
"countProp": "topN", // where the count goes in customPayload
|
|
32
|
-
"itemProp": "item", //
|
|
33
|
-
"indexProp": "rank", //
|
|
32
|
+
"itemProp": "item", // where one beat's item goes
|
|
33
|
+
"indexProp": "rank", // the index field inside that item
|
|
34
34
|
"legacyCountProps": ["totalItems"],
|
|
35
|
-
"openingSec": 2.5
|
|
35
|
+
"openingSec": 2.5, // optional, default DEFAULT_OPENING_SEC
|
|
36
|
+
"countPatterns": ["..."] // optional: regexes that find a count stated
|
|
37
|
+
// in on-screen copy (group 1 = the number)
|
|
36
38
|
}
|
|
37
39
|
}
|
|
38
40
|
|
|
@@ -50,6 +52,8 @@ the count would fall back to "however many cards are present", and the
|
|
|
50
52
|
|
|
51
53
|
from __future__ import annotations
|
|
52
54
|
|
|
55
|
+
import re
|
|
56
|
+
|
|
53
57
|
#: Opening budget when the template declares none. A generic "an opening is a
|
|
54
58
|
#: beat, not a chapter" value — **not** any one template's measurement. It used
|
|
55
59
|
#: to be 3 hardcoded in gen_script, taken from github-repo-rank's reference film
|
|
@@ -88,10 +92,23 @@ def resolve_countdown_spec(template: dict | None) -> dict | None:
|
|
|
88
92
|
raise CountdownError(
|
|
89
93
|
f"Template '{template_id}' declares capabilities.sceneStrategy=countdown but no "
|
|
90
94
|
"capabilities.countdown.countProp. Declare which customPayload field carries the "
|
|
91
|
-
"item count
|
|
95
|
+
"item count."
|
|
92
96
|
)
|
|
93
97
|
count_prop = count_prop.strip()
|
|
94
98
|
|
|
99
|
+
# itemProp / indexProp 同样必须声明:缺省值只能是某个具体模板的字段名,
|
|
100
|
+
# 那等于把那个模板写进 CLI。
|
|
101
|
+
names: dict[str, str] = {}
|
|
102
|
+
for key in ("itemProp", "indexProp"):
|
|
103
|
+
value = cfg.get(key)
|
|
104
|
+
if not isinstance(value, str) or not value.strip():
|
|
105
|
+
raise CountdownError(
|
|
106
|
+
f"Template '{template_id}' declares capabilities.sceneStrategy=countdown but no "
|
|
107
|
+
f"capabilities.countdown.{key}. Declare the customPayload field name — the CLI "
|
|
108
|
+
"does not guess one."
|
|
109
|
+
)
|
|
110
|
+
names[key] = value.strip()
|
|
111
|
+
|
|
95
112
|
schema = (template or {}).get("customPayloadSchema")
|
|
96
113
|
bounds = schema.get(count_prop) if isinstance(schema, dict) else None
|
|
97
114
|
if not isinstance(bounds, dict):
|
|
@@ -119,6 +136,22 @@ def resolve_countdown_spec(template: dict | None) -> dict | None:
|
|
|
119
136
|
legacy = cfg.get("legacyCountProps")
|
|
120
137
|
legacy = [p for p in legacy if isinstance(p, str) and p.strip()] if isinstance(legacy, list) else []
|
|
121
138
|
|
|
139
|
+
patterns = []
|
|
140
|
+
for raw in cfg.get("countPatterns") or []:
|
|
141
|
+
try:
|
|
142
|
+
compiled = re.compile(raw, re.I) if isinstance(raw, str) else None
|
|
143
|
+
except re.error as exc:
|
|
144
|
+
raise CountdownError(
|
|
145
|
+
f"Template '{template_id}': countdown.countPatterns entry {raw!r} is not a valid "
|
|
146
|
+
f"regex ({exc})."
|
|
147
|
+
) from None
|
|
148
|
+
if compiled is None or compiled.groups < 1:
|
|
149
|
+
raise CountdownError(
|
|
150
|
+
f"Template '{template_id}': countdown.countPatterns entry {raw!r} must be a regex "
|
|
151
|
+
"string with a capturing group for the number."
|
|
152
|
+
)
|
|
153
|
+
patterns.append(compiled)
|
|
154
|
+
|
|
122
155
|
opening_sec = cfg.get("openingSec")
|
|
123
156
|
if not (type(opening_sec) in (int, float) and opening_sec > 0):
|
|
124
157
|
opening_sec = DEFAULT_OPENING_SEC
|
|
@@ -127,8 +160,9 @@ def resolve_countdown_spec(template: dict | None) -> dict | None:
|
|
|
127
160
|
"template_id": template_id,
|
|
128
161
|
"count_prop": count_prop,
|
|
129
162
|
"legacy_count_props": legacy,
|
|
130
|
-
"item_prop":
|
|
131
|
-
"index_prop":
|
|
163
|
+
"item_prop": names["itemProp"],
|
|
164
|
+
"index_prop": names["indexProp"],
|
|
165
|
+
"count_patterns": patterns,
|
|
132
166
|
"minimum": minimum,
|
|
133
167
|
"maximum": maximum,
|
|
134
168
|
"default": default,
|
|
@@ -136,6 +170,22 @@ def resolve_countdown_spec(template: dict | None) -> dict | None:
|
|
|
136
170
|
}
|
|
137
171
|
|
|
138
172
|
|
|
173
|
+
def stated_counts(spec: dict, text: str) -> set[int]:
|
|
174
|
+
"""Counts that on-screen copy states, found with the template's ``countPatterns``.
|
|
175
|
+
|
|
176
|
+
How a count is worded (``Top 5`` / ``前 5 名`` / ``5 个技巧``) is the
|
|
177
|
+
template's copy convention, so the patterns are declared there; a template
|
|
178
|
+
that declares none gets no check.
|
|
179
|
+
"""
|
|
180
|
+
found: set[int] = set()
|
|
181
|
+
for pattern in spec.get("count_patterns") or []:
|
|
182
|
+
for match in pattern.finditer(text or ""):
|
|
183
|
+
for group in match.groups():
|
|
184
|
+
if group is not None and group.isdigit():
|
|
185
|
+
found.add(int(group))
|
|
186
|
+
return found
|
|
187
|
+
|
|
188
|
+
|
|
139
189
|
def resolve_count(spec: dict, requested: int | None, scene_count: int | None) -> int:
|
|
140
190
|
"""Validate a caller-supplied item count against the template's own bounds.
|
|
141
191
|
|
|
@@ -22,9 +22,8 @@
|
|
|
22
22
|
鉴权:只读 PRIV_TOKEN 环境变量。凭证库 / 钥匙串 / 设备授权流由 CLI 的 Node 侧
|
|
23
23
|
唯一鉴权入口负责(src/auth/ensure.ts),并在 spawn 时注入本进程环境。
|
|
24
24
|
|
|
25
|
-
monorepo /
|
|
26
|
-
|
|
27
|
-
那是 monorepo-only 工具,不是运行时数据源。
|
|
25
|
+
monorepo / 显式本地文件等其它来源已**移除**。模板仓库自己的维护工具住在
|
|
26
|
+
template-library,不经过本模块。
|
|
28
27
|
|
|
29
28
|
P1.2 — list/detail split (待后端就绪):
|
|
30
29
|
100+ 模板时 ``GET /api/template/list`` 应该只返摘要(id / name / aspects /
|
|
@@ -379,7 +378,7 @@ def load_registry_indexed(
|
|
|
379
378
|
"""Return registry as ``{templateId: template_dict}``.
|
|
380
379
|
|
|
381
380
|
Filters by the process-wide status gate unless ``include_all_statuses``
|
|
382
|
-
is set (
|
|
381
|
+
is set (e.g. render-video reading an already-chosen template). Templates
|
|
383
382
|
missing ``status`` are treated as ``stable`` for backwards compatibility.
|
|
384
383
|
|
|
385
384
|
``ttl_seconds`` / ``http_timeout`` are accepted for API symmetry with
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
"skillName": "template-registry",
|
|
3
3
|
"repoName": "agent-skill-media-maker",
|
|
4
4
|
"skillId": "475",
|
|
5
|
-
"version": "
|
|
5
|
+
"version": "V23",
|
|
6
6
|
"skillDescription": "Video-template registry skill. Stores every video-template definition and lists the available templates (templateId / name / aspect ratio / style tags).\n\nUse this skill as soon as the user mentions any of these intents:\n- View available templates / list every template\n\nNote: DSL→TemplateBinding is no longer a separate exposed step — once prepare_video_assets receives a template_id it builds the binding internally."
|
|
7
7
|
}
|
|
@@ -9,27 +9,17 @@ video_dsl/
|
|
|
9
9
|
├── schema/ # DSL Schema 定义
|
|
10
10
|
│ ├── video-dsl-v1alpha1.json # Video DSL JSON Schema
|
|
11
11
|
│ ├── template-binding-v1alpha1.json # 模板绑定 Schema
|
|
12
|
-
│
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
├── runtime/ # DSL 运行时工具
|
|
19
|
-
│ ├── dsl_validator.py # DSL Schema 校验
|
|
20
|
-
│ ├── template_binder.py # Scene → Slot 绑定(从 @ab-templates/metadata 读取模板)
|
|
21
|
-
│ ├── timeline_compiler.py # 时间线编译(帧号、字幕切片、旁白分段)
|
|
22
|
-
│ ├── asset_adapter.py # 素材适配器
|
|
23
|
-
│ └── prompt_enhancer.py # Prompt 增强
|
|
24
|
-
└── library/ # 可复用组件库(预留)
|
|
25
|
-
├── blocks/ # 功能块(如 CTA、数字人、全屏背景)
|
|
26
|
-
├── components/ # 基础组件(音频、文本、背景、装饰)
|
|
27
|
-
├── scenes/ # 场景模板(开场钩子、知识点、绘本页)
|
|
28
|
-
└── videos/ # 完整视频模板(英文绘本等)
|
|
12
|
+
│ └── render-plan-v1alpha1.json # RenderPlan Schema
|
|
13
|
+
└── runtime/ # DSL 运行时工具(通用机制,不认识任何具体模板)
|
|
14
|
+
├── dsl_validator.py # DSL 校验(读模板声明做契约检查)
|
|
15
|
+
├── payload_contract.py # customPayload 必填字段检查
|
|
16
|
+
├── timeline_compiler.py # 时间线编译(帧号、字幕切片、旁白分段)
|
|
17
|
+
└── prompt_enhancer.py # Prompt 增强
|
|
29
18
|
```
|
|
30
19
|
|
|
31
|
-
> **注意**:模板元数据(template.json / registry.json
|
|
32
|
-
>
|
|
20
|
+
> **注意**:模板元数据(template.json / registry.json)住在独立的 `template-library` 仓库,
|
|
21
|
+
> 运行时经 ab-api 下发,由 `scripts/registry_loader.py` 读取;DSL → TemplateBinding 在 `scripts/match_template.py`。
|
|
22
|
+
> 逐模板的契约与写法只在各自的 `template.json` 里,本目录不列模板、不带模板示例。
|
|
33
23
|
|
|
34
24
|
## 核心概念
|
|
35
25
|
|
|
@@ -51,46 +41,13 @@ video_dsl/
|
|
|
51
41
|
[render-video] → RenderPlan → MP4 / 剪映草稿
|
|
52
42
|
```
|
|
53
43
|
|
|
54
|
-
## 模板列表
|
|
55
|
-
|
|
56
|
-
| 模板 ID | 名称 | 适用场景 |
|
|
57
|
-
|---------|------|----------|
|
|
58
|
-
| `image-slide` | 图片幻灯片 | 通用知识分享、产品介绍(含 tech 科技风变体) |
|
|
59
|
-
| `picture-book-en` | 英文绘本视频 | 儿童英语启蒙、双语绘本故事 |
|
|
60
|
-
| `image-slideshow` | 图片轮播视频 | 知识分享、种草笔记、技巧总结 |
|
|
61
|
-
| `knowledge-slides` | 知识幻灯片 | 方法论拆解、教程讲解、知识科普 |
|
|
62
|
-
| `html-slide` | 知识看板 | 知识科普、概念讲解、白板风格内容 |
|
|
63
|
-
|
|
64
44
|
## runtime 模块说明
|
|
65
45
|
|
|
66
46
|
| 模块 | 职责 | 被谁调用 |
|
|
67
47
|
|------|------|----------|
|
|
68
48
|
| `dsl_validator.py` | DSL JSON Schema 校验与参数标准化 | gen-script、render-video |
|
|
69
|
-
| `template_binder.py` | 将 DSL scene 映射到模板 slot,生成 TemplateBinding(从 @ab-templates/metadata 读取模板定义) | template-registry |
|
|
70
49
|
| `timeline_compiler.py` | 帧号计算、字幕切片(`split_subtitle`)、旁白分段(`segment_narration`) | render-video(跨 skill import) |
|
|
71
|
-
| `
|
|
50
|
+
| `payload_contract.py` | customPayload 必填字段 / 计划素材检查 | dsl_validator、render-video |
|
|
72
51
|
| `prompt_enhancer.py` | 图片 prompt 增强 | render-video |
|
|
73
52
|
|
|
74
53
|
**注意**:`timeline_compiler.py` 被 `render-video/scripts/render_video.py` 直接 import,修改函数签名需两个 skill 同步测试。
|
|
75
|
-
|
|
76
|
-
## library 组件库(预留)
|
|
77
|
-
|
|
78
|
-
`library/` 目录采用四层分级设计,用于存放可复用的 DSL 片段:
|
|
79
|
-
|
|
80
|
-
- **components/**:最小粒度的基础组件(音频、文本、背景、头像、装饰、媒体、叠加层)
|
|
81
|
-
- **blocks/**:由多个 component 组合而成的功能块(CTA 按钮、数字人区域、全屏背景图、标题区、产品卡片、配音条、双语字幕)
|
|
82
|
-
- **scenes/**:完整的场景模板(开场钩子、知识点讲解、绘本封面/内页/结尾、CTA 结尾、数字人口播)
|
|
83
|
-
- **videos/**:完整的视频模板(英文绘本等)
|
|
84
|
-
|
|
85
|
-
组件库当前为目录骨架,后续可填充具体的 JSON 片段供 Agent 和 gen_script.py 复用。
|
|
86
|
-
|
|
87
|
-
## 示例文件
|
|
88
|
-
|
|
89
|
-
`schema/examples/` 下包含各模板的完整 DSL + Binding 示例,是 Agent 生成 DSL 时的权威参考:
|
|
90
|
-
|
|
91
|
-
- `ai-learning-tips.*` — image-slide 模板示例
|
|
92
|
-
- `html-slide-demo.*` — html-slide 模板示例
|
|
93
|
-
- `knowledge-slides-demo.*` — knowledge-slides 模板示例
|
|
94
|
-
- `animal-friends-picturebook.*` — picture-book-en 模板示例
|
|
95
|
-
|
|
96
|
-
Agent 在为指定模板生成 DSL 时,必须先读取对应示例,按模板原生要求生成。
|
|
@@ -6,6 +6,5 @@ and gen-script). Public modules:
|
|
|
6
6
|
|
|
7
7
|
- timeline_compiler : DSL → Remotion frame-axis timeline + subtitle splits
|
|
8
8
|
- prompt_enhancer : LLM image-prompt enrichment used by gen-script
|
|
9
|
-
- template_binder : registry summary writer (sync_registry helper)
|
|
10
9
|
- dsl_validator : DSL validation (structural / integrity / full presets)
|
|
11
10
|
"""
|
|
@@ -31,7 +31,7 @@ VALID_PURPOSES = {"opening", "intro", "point", "example", "explanation", "transi
|
|
|
31
31
|
VALID_LAYOUTS = {"full-visual", "split-left-right", "split-top-bottom", "picture-in-picture", "text-overlay", "avatar-with-bg", "kenburns"}
|
|
32
32
|
|
|
33
33
|
# 场景数上限的兜底值。叙事弧类模板的合理上界——20 段以上的短视频必然过碎。
|
|
34
|
-
# "一页素材一个场景"的模板(
|
|
34
|
+
# "一页素材一个场景"的模板(capabilities.payloadHydration)场景数由源文件页数决定,应当用
|
|
35
35
|
# ``capabilities.maxScenes`` 抬高;这个常量只是没声明时的默认。
|
|
36
36
|
DEFAULT_MAX_SCENES = 20
|
|
37
37
|
|
|
@@ -61,21 +61,17 @@ def max_scenes_for_template(template_config: Optional[dict]) -> int:
|
|
|
61
61
|
VALID_ASSET_TYPES = {"image", "video", "audio", "avatar", "subtitle", "bgm"}
|
|
62
62
|
VALID_ASSET_SOURCES = {"existing", "gen-image", "gen-video", "gen-voice", "gen-digital-human"}
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
"
|
|
72
|
-
"
|
|
73
|
-
"
|
|
74
|
-
|
|
75
|
-
"background",
|
|
76
|
-
"templateData",
|
|
77
|
-
"highlightMap",
|
|
78
|
-
})
|
|
64
|
+
def _frame_level_keys(tpl: dict) -> frozenset:
|
|
65
|
+
"""模板声明的 frame 级字段(``capabilities.frameLevelKeys.keys``)。
|
|
66
|
+
|
|
67
|
+
这些字段由 Composition 消费、不属于任何 slide 载荷;「整包态」binder 会把它们
|
|
68
|
+
混进 templateData,出现在那里是正当的,不该被当成未声明字段。名单是模板的事实,
|
|
69
|
+
CLI 不保留副本 —— 没声明的模板就是空集。``highlightMap`` 这类跨版式字段走
|
|
70
|
+
``slideSchemas._shared``,不在这里。
|
|
71
|
+
"""
|
|
72
|
+
spec = (tpl.get("capabilities") or {}).get("frameLevelKeys")
|
|
73
|
+
keys = spec.get("keys") if isinstance(spec, dict) else None
|
|
74
|
+
return frozenset(k for k in keys if isinstance(k, str)) if isinstance(keys, list) else frozenset()
|
|
79
75
|
|
|
80
76
|
# CJK 汉字范围(简繁通用)+ 常见中文标点
|
|
81
77
|
_CJK_PATTERN = re.compile(r"[一-鿿㐀-䶿- -〿]")
|
|
@@ -144,11 +140,32 @@ def _line_sync_slides_for_template(template_id: Optional[str]) -> dict[str, str]
|
|
|
144
140
|
O(1) lookup via ``registry_loader.get_template``; safe to call once per
|
|
145
141
|
scene at 100+ templates without scanning the full list.
|
|
146
142
|
"""
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
143
|
+
return line_sync_slides(_get_template_cfg(template_id))
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def line_sync_slides(tpl: Optional[dict]) -> dict[str, str]:
|
|
147
|
+
"""模板的 ``lineSyncSlides`` 声明:slideId → templateData 里卡片数组的键。
|
|
148
|
+
|
|
149
|
+
校验层(narration.items 数对齐)与 render-video(自动 highlightMap 的上限裁剪)
|
|
150
|
+
读的是同一份声明,CLI 不保留任何卡片键表的副本。
|
|
151
|
+
"""
|
|
152
|
+
spec = (tpl or {}).get("lineSyncSlides") or {}
|
|
153
|
+
return {k: v for k, v in spec.items() if not k.startswith("_") and isinstance(v, str)}
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def step_alignment_field(tpl: Optional[dict]) -> Optional[str]:
|
|
157
|
+
"""模板 ``capabilities.stepAlignment`` 声明的动作数组字段;未声明返回 None。
|
|
158
|
+
|
|
159
|
+
只认 ``alignTo`` 缺省或 ``narration-sentences``(第 i 个动作 = 第 i 句旁白)。
|
|
160
|
+
"""
|
|
161
|
+
caps = (tpl or {}).get("capabilities")
|
|
162
|
+
spec = caps.get("stepAlignment") if isinstance(caps, dict) else None
|
|
163
|
+
if not isinstance(spec, dict):
|
|
164
|
+
return None
|
|
165
|
+
if spec.get("alignTo") not in (None, "narration-sentences"):
|
|
166
|
+
return None
|
|
167
|
+
field = spec.get("payloadField")
|
|
168
|
+
return field if isinstance(field, str) and field else None
|
|
152
169
|
|
|
153
170
|
|
|
154
171
|
def _get_template_cfg(template_id: Optional[str]) -> Optional[dict]:
|
|
@@ -280,7 +297,7 @@ def _check_scenes_max(dsl: dict) -> list[ValidationError]:
|
|
|
280
297
|
|
|
281
298
|
上限由模板的 ``capabilities.maxScenes`` 声明,缺省 ``DEFAULT_MAX_SCENES``(20)。
|
|
282
299
|
20 这个数字是给"叙事弧"类模板定的——一条 60 秒短视频排到 20 段已经过碎。但
|
|
283
|
-
"一页素材一个场景"的模板(
|
|
300
|
+
"一页素材一个场景"的模板(逐页讲解一份源文件)的场景数由源文件页数决定,
|
|
284
301
|
跟叙事节奏无关:2026-09-23 线上一份 23 页 PPT 就是在这里被砍到 20 页,
|
|
285
302
|
而且砍得毫无声响,直到下游报 ``customPayload.slide (required)`` ×20 才暴露。
|
|
286
303
|
所以上限改成模板自己声明,并且这里的报错要说清它是谁定的。
|
|
@@ -576,8 +593,7 @@ def _check_slide_cross_field(dsl: dict) -> list[ValidationError]:
|
|
|
576
593
|
schemas = tpl.get("slideSchemas")
|
|
577
594
|
if not isinstance(schemas, dict):
|
|
578
595
|
return []
|
|
579
|
-
line_sync =
|
|
580
|
-
if isinstance(v, str)}
|
|
596
|
+
line_sync = line_sync_slides(tpl)
|
|
581
597
|
|
|
582
598
|
errors: list[ValidationError] = []
|
|
583
599
|
|
|
@@ -673,8 +689,7 @@ def _check_line_sync_narration(dsl: dict) -> list[ValidationError]:
|
|
|
673
689
|
tpl = _get_template_cfg(_pick_template_id(dsl))
|
|
674
690
|
if not tpl:
|
|
675
691
|
return []
|
|
676
|
-
line_sync =
|
|
677
|
-
if isinstance(v, str)}
|
|
692
|
+
line_sync = line_sync_slides(tpl)
|
|
678
693
|
if not line_sync:
|
|
679
694
|
return []
|
|
680
695
|
schemas = tpl.get("slideSchemas") if isinstance(tpl.get("slideSchemas"), dict) else {}
|
|
@@ -751,14 +766,8 @@ def _check_step_narration_alignment(dsl: dict) -> list[ValidationError]:
|
|
|
751
766
|
tpl = _get_template_cfg(_pick_template_id(dsl))
|
|
752
767
|
if not tpl:
|
|
753
768
|
return []
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
if not isinstance(spec, dict):
|
|
757
|
-
return []
|
|
758
|
-
field = spec.get("payloadField")
|
|
759
|
-
if not isinstance(field, str) or not field:
|
|
760
|
-
return []
|
|
761
|
-
if spec.get("alignTo") not in (None, "narration-sentences"):
|
|
769
|
+
field = step_alignment_field(tpl)
|
|
770
|
+
if not field:
|
|
762
771
|
return []
|
|
763
772
|
|
|
764
773
|
try:
|
|
@@ -877,7 +886,7 @@ def _check_slide_payload_fields(dsl: dict) -> list[ValidationError]:
|
|
|
877
886
|
extra_allowed=shared,
|
|
878
887
|
# frame 级字段由 Composition 消费,「整包态」binder 会把它们混进
|
|
879
888
|
# templateData —— 出现在这里是正当的,不算未声明字段。
|
|
880
|
-
skip_keys=
|
|
889
|
+
skip_keys=_frame_level_keys(tpl),
|
|
881
890
|
)
|
|
882
891
|
|
|
883
892
|
return errors
|
|
@@ -243,7 +243,7 @@
|
|
|
243
243
|
"items": {
|
|
244
244
|
"type": "array",
|
|
245
245
|
"items": { "type": "string" },
|
|
246
|
-
"description": "每行一个卡片的叙述句。长度必须等于 templateData
|
|
246
|
+
"description": "每行一个卡片的叙述句。长度必须等于 templateData 里对应数组的长度(数组键由模板的 lineSyncSlides[slideId] 声明)"
|
|
247
247
|
},
|
|
248
248
|
"outro": { "type": "string", "description": "可选:收尾句(此期间无卡片高亮)" },
|
|
249
249
|
"assetRef": { "type": "string", "description": "旁白音频 assetId" },
|
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Cross-module contract checker: template-library ↔ remotion-renderer.
|
|
3
|
-
|
|
4
|
-
Verifies that every `compositionId` referenced from the @ab-templates/metadata
|
|
5
|
-
registry.json is:
|
|
6
|
-
1. Registered in `<monorepo-root>/remotion-renderer/src/core/compositions/manifest.ts`
|
|
7
|
-
2. Mapped there to the SAME `templateId` as the template entry's
|
|
8
|
-
top-level `templateId` field
|
|
9
|
-
|
|
10
|
-
Exit non-zero with a useful diff if the two sides disagree.
|
|
11
|
-
|
|
12
|
-
Usage:
|
|
13
|
-
python3 scripts/check_contracts.py
|
|
14
|
-
"""
|
|
15
|
-
from __future__ import annotations
|
|
16
|
-
|
|
17
|
-
import json
|
|
18
|
-
import os
|
|
19
|
-
import re
|
|
20
|
-
import sys
|
|
21
|
-
from typing import Iterator
|
|
22
|
-
|
|
23
|
-
from template_paths import monorepo_registry_path, monorepo_root
|
|
24
|
-
|
|
25
|
-
_REGISTRY_PATH = monorepo_registry_path()
|
|
26
|
-
_MANIFEST_PATH = os.path.abspath(
|
|
27
|
-
os.path.join(
|
|
28
|
-
monorepo_root(),
|
|
29
|
-
"remotion-renderer",
|
|
30
|
-
"src",
|
|
31
|
-
"core",
|
|
32
|
-
"compositions",
|
|
33
|
-
"manifest.ts",
|
|
34
|
-
)
|
|
35
|
-
)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
# Matches a single manifest row:
|
|
39
|
-
# SomeCompositionId: { component: Foo, templateId: "t-id", slot: "opening" },
|
|
40
|
-
_MANIFEST_ROW_RE = re.compile(
|
|
41
|
-
r'^\s*(?P<cid>[A-Za-z0-9_]+)\s*:\s*\{\s*'
|
|
42
|
-
r'component\s*:\s*[A-Za-z0-9_]+\s*,\s*'
|
|
43
|
-
r'templateId\s*:\s*"(?P<tid>[^"]+)"\s*,\s*'
|
|
44
|
-
r'slot\s*:\s*"(?P<slot>[^"]+)"\s*\}\s*,?\s*$'
|
|
45
|
-
)
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def parse_manifest(path: str) -> dict[str, dict[str, str]]:
|
|
49
|
-
"""Parse manifest.ts and return {compositionId: {templateId, slot}}."""
|
|
50
|
-
if not os.path.isfile(path):
|
|
51
|
-
raise FileNotFoundError(f"manifest.ts not found at {path}")
|
|
52
|
-
|
|
53
|
-
with open(path, "r", encoding="utf-8") as f:
|
|
54
|
-
text = f.read()
|
|
55
|
-
|
|
56
|
-
start_marker = "// MANIFEST_ENTRIES:START"
|
|
57
|
-
end_marker = "// MANIFEST_ENTRIES:END"
|
|
58
|
-
start = text.find(start_marker)
|
|
59
|
-
end = text.find(end_marker)
|
|
60
|
-
if start == -1 or end == -1 or end < start:
|
|
61
|
-
raise ValueError(
|
|
62
|
-
f"manifest.ts is missing {start_marker} / {end_marker} markers"
|
|
63
|
-
)
|
|
64
|
-
|
|
65
|
-
block = text[start:end]
|
|
66
|
-
entries: dict[str, dict[str, str]] = {}
|
|
67
|
-
for line in block.splitlines():
|
|
68
|
-
m = _MANIFEST_ROW_RE.match(line)
|
|
69
|
-
if not m:
|
|
70
|
-
continue
|
|
71
|
-
entries[m.group("cid")] = {
|
|
72
|
-
"templateId": m.group("tid"),
|
|
73
|
-
"slot": m.group("slot"),
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if not entries:
|
|
77
|
-
raise ValueError(
|
|
78
|
-
f"No manifest entries parsed from {path}. Did the format change?"
|
|
79
|
-
)
|
|
80
|
-
return entries
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
def _iter_composition_ids(node: object) -> Iterator[str]:
|
|
84
|
-
"""Walk an arbitrary JSON tree and yield every `compositionId` value."""
|
|
85
|
-
if isinstance(node, dict):
|
|
86
|
-
cid = node.get("compositionId")
|
|
87
|
-
if isinstance(cid, str):
|
|
88
|
-
yield cid
|
|
89
|
-
for v in node.values():
|
|
90
|
-
yield from _iter_composition_ids(v)
|
|
91
|
-
elif isinstance(node, list):
|
|
92
|
-
for v in node:
|
|
93
|
-
yield from _iter_composition_ids(v)
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
def collect_template_references(registry_path: str) -> list[dict[str, str]]:
|
|
97
|
-
"""Return a list of {templateId, compositionId, file} for every reference in the registry."""
|
|
98
|
-
refs: list[dict[str, str]] = []
|
|
99
|
-
if not os.path.isfile(registry_path):
|
|
100
|
-
print(f"WARN: registry.json not found at {registry_path}", file=sys.stderr)
|
|
101
|
-
return refs
|
|
102
|
-
|
|
103
|
-
with open(registry_path, "r", encoding="utf-8") as f:
|
|
104
|
-
data = json.load(f)
|
|
105
|
-
|
|
106
|
-
for tpl in data.get("templates", []):
|
|
107
|
-
tid = tpl.get("templateId")
|
|
108
|
-
if not tid:
|
|
109
|
-
continue
|
|
110
|
-
for cid in _iter_composition_ids(tpl):
|
|
111
|
-
refs.append({"templateId": tid, "compositionId": cid, "file": registry_path})
|
|
112
|
-
return refs
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
def main() -> int:
|
|
116
|
-
try:
|
|
117
|
-
manifest = parse_manifest(_MANIFEST_PATH)
|
|
118
|
-
except (FileNotFoundError, ValueError) as e:
|
|
119
|
-
print(f"FAIL: {e}", file=sys.stderr)
|
|
120
|
-
return 2
|
|
121
|
-
|
|
122
|
-
refs = collect_template_references(_REGISTRY_PATH)
|
|
123
|
-
|
|
124
|
-
errors: list[str] = []
|
|
125
|
-
seen: set[str] = set()
|
|
126
|
-
|
|
127
|
-
for ref in refs:
|
|
128
|
-
cid = ref["compositionId"]
|
|
129
|
-
tid = ref["templateId"]
|
|
130
|
-
rel = os.path.relpath(ref["file"], monorepo_root())
|
|
131
|
-
seen.add(cid)
|
|
132
|
-
|
|
133
|
-
entry = manifest.get(cid)
|
|
134
|
-
if entry is None:
|
|
135
|
-
errors.append(
|
|
136
|
-
f"{rel}: compositionId '{cid}' is referenced but not in manifest.ts"
|
|
137
|
-
)
|
|
138
|
-
continue
|
|
139
|
-
if entry["templateId"] != tid:
|
|
140
|
-
errors.append(
|
|
141
|
-
f"{rel}: compositionId '{cid}' is registered under templateId "
|
|
142
|
-
f"'{entry['templateId']}' in manifest.ts, but template.json declares "
|
|
143
|
-
f"templateId '{tid}'"
|
|
144
|
-
)
|
|
145
|
-
|
|
146
|
-
# Warn (non-fatal) about manifest entries never referenced by any template.
|
|
147
|
-
unused = sorted(set(manifest.keys()) - seen)
|
|
148
|
-
for cid in unused:
|
|
149
|
-
print(
|
|
150
|
-
f"WARN: manifest.ts entry '{cid}' is not referenced by any "
|
|
151
|
-
f"template.json (templateId={manifest[cid]['templateId']})",
|
|
152
|
-
file=sys.stderr,
|
|
153
|
-
)
|
|
154
|
-
|
|
155
|
-
# 命名空间唯一性说明:compositionId 是否全库唯一,由 template-library 的
|
|
156
|
-
# generateManifest 在生成 manifest.ts 时保障(跨模板撞名直接抛
|
|
157
|
-
# DuplicateCompositionIdError、CI 失败)。manifest.ts 因此按构造即唯一,
|
|
158
|
-
# 本脚本读取的已是去重结果,无需再校验唯一性。
|
|
159
|
-
#
|
|
160
|
-
# (已移除早期 P1.1 的 `<templateId>/` 前缀 warning:权威 schema 的
|
|
161
|
-
# compositionId pattern 为 PascalCase `^[A-Z][A-Za-z0-9]*$`,本就禁止 `/`,
|
|
162
|
-
# 前缀建议与 schema 矛盾且非必需——唯一性已由上游硬保障。)
|
|
163
|
-
|
|
164
|
-
if errors:
|
|
165
|
-
print("FAIL: contract check found inconsistencies:", file=sys.stderr)
|
|
166
|
-
for e in errors:
|
|
167
|
-
print(f" - {e}", file=sys.stderr)
|
|
168
|
-
return 1
|
|
169
|
-
|
|
170
|
-
print(
|
|
171
|
-
f"OK: {len(refs)} compositionId references across "
|
|
172
|
-
f"{len({r['templateId'] for r in refs})} templates match manifest.ts "
|
|
173
|
-
f"({len(manifest)} entries)."
|
|
174
|
-
)
|
|
175
|
-
return 0
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
if __name__ == "__main__":
|
|
179
|
-
sys.exit(main())
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Write a local registry-cache.json from @ab-templates/metadata.
|
|
3
|
-
|
|
4
|
-
Usage:
|
|
5
|
-
python3 scripts/sync_registry.py # write cache
|
|
6
|
-
python3 scripts/sync_registry.py --registry <path> # use custom registry source
|
|
7
|
-
|
|
8
|
-
The cache is a lightweight summary for LLM prompts and offline use.
|
|
9
|
-
The authoritative source is template-library/packages/metadata/registry.json.
|
|
10
|
-
"""
|
|
11
|
-
from __future__ import annotations
|
|
12
|
-
|
|
13
|
-
import argparse
|
|
14
|
-
import sys
|
|
15
|
-
import os
|
|
16
|
-
|
|
17
|
-
# Make video_dsl.runtime importable regardless of CWD.
|
|
18
|
-
_SKILL_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
|
19
|
-
sys.path.insert(0, _SKILL_DIR)
|
|
20
|
-
|
|
21
|
-
from video_dsl.runtime.template_binder import sync_registry # noqa: E402
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def main() -> int:
|
|
25
|
-
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
|
26
|
-
parser.add_argument(
|
|
27
|
-
"--registry",
|
|
28
|
-
default=None,
|
|
29
|
-
help="Path to @ab-templates/metadata registry.json (auto-detected if omitted)",
|
|
30
|
-
)
|
|
31
|
-
parser.add_argument(
|
|
32
|
-
"--output",
|
|
33
|
-
default=None,
|
|
34
|
-
help="Output path for the cache file (default: video_dsl/registry-cache.json)",
|
|
35
|
-
)
|
|
36
|
-
args = parser.parse_args()
|
|
37
|
-
|
|
38
|
-
out = sync_registry(output_path=args.output, registry_path=args.registry)
|
|
39
|
-
print(f"wrote {out}")
|
|
40
|
-
return 0
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if __name__ == "__main__":
|
|
44
|
-
sys.exit(main())
|