@remixmate/cli 0.9.50 → 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.
Files changed (29) hide show
  1. package/README.md +14 -26
  2. package/dist/manifest.json +18 -8
  3. package/package.json +9 -13
  4. package/skills/doc-parse/SKILL.md +6 -6
  5. package/skills/doc-parse/scripts/extract_via_render.py +3 -2
  6. package/skills/doc-parse/skill.json +1 -1
  7. package/skills/gen-script/SKILL.md +13 -11
  8. package/skills/gen-script/scripts/gen_script.py +106 -25
  9. package/skills/gen-script/skill.json +14 -4
  10. package/skills/gen-script/version.json +1 -1
  11. package/skills/prepare-video-assets/SKILL.md +3 -3
  12. package/skills/prepare-video-assets/skill.json +1 -1
  13. package/skills/prepare-video-assets/version.json +1 -1
  14. package/skills/render-video/scripts/render_video.py +53 -42
  15. package/skills/render-video/version.json +1 -1
  16. package/skills/template-registry/README.md +3 -14
  17. package/skills/template-registry/SKILL.md +12 -49
  18. package/skills/template-registry/scripts/countdown_spec.py +56 -6
  19. package/skills/template-registry/scripts/registry_loader.py +3 -4
  20. package/skills/template-registry/version.json +1 -1
  21. package/skills/template-registry/video_dsl/README.md +10 -53
  22. package/skills/template-registry/video_dsl/runtime/__init__.py +0 -1
  23. package/skills/template-registry/video_dsl/runtime/dsl_validator.py +44 -35
  24. package/skills/template-registry/video_dsl/schema/video-dsl-v1alpha1.json +1 -1
  25. package/skills/template-registry/scripts/check_contracts.py +0 -179
  26. package/skills/template-registry/scripts/sync_registry.py +0 -44
  27. package/skills/template-registry/scripts/template_paths.py +0 -72
  28. package/skills/template-registry/scripts/verify_props_contract.py +0 -273
  29. package/skills/template-registry/video_dsl/runtime/template_binder.py +0 -87
@@ -2,6 +2,6 @@
2
2
  "skillName": "template-registry",
3
3
  "repoName": "agent-skill-media-maker",
4
4
  "skillId": "475",
5
- "version": "V22",
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
- │ ├── render-plan-v1alpha1.json # RenderPlan Schema
13
- │ └── examples/ # 各模板的 DSL + Binding 示例
14
- │ ├── ai-learning-tips.dsl.json
15
- │ ├── ai-learning-tips.binding.json
16
- │ ├── html-slide-demo.dsl.json
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)已迁移至独立的 `template-library` 仓库,
32
- > 通过 `@ab-templates/metadata` 包发布。`template_binder.py` 直接读取该包产出的 `registry.json`。
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
- | `asset_adapter.py` | 素材格式适配 | render-video |
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
- # "一页素材一个场景"的模板(ppt-to-video)场景数由源文件页数决定,应当用
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
- # 由 Composition 消费、而非 slide 组件消费的字段。前八个与模板侧
65
- # utils/slidePayload.ts 的 FRAME_LEVEL_KEYS 同源 ——「整包态」binder 会把它们混进
66
- # templateData,出现在那里是正当的,不该被当成未声明字段。highlightMap 同理(模板
67
- # 一般在 slideSchemas._shared 里声明它,这里兜底没声明的情况)。
68
- _SLIDE_FRAME_LEVEL_KEYS = frozenset({
69
- "slideId",
70
- "titleText",
71
- "subtitleText",
72
- "watermarkText",
73
- "narrationAssetId",
74
- "narrationSrc",
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
- tpl = _get_template_cfg(template_id)
148
- if not tpl:
149
- return {}
150
- spec = tpl.get("lineSyncSlides") or {}
151
- return {k: v for k, v in spec.items() if isinstance(v, str)}
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
- "一页素材一个场景"的模板(ppt-to-video 逐页讲解)的场景数由源文件页数决定,
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 = {k: v for k, v in (tpl.get("lineSyncSlides") or {}).items()
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 = {k: v for k, v in (tpl.get("lineSyncSlides") or {}).items()
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
- caps = tpl.get("capabilities")
755
- spec = caps.get("stepAlignment") if isinstance(caps, dict) else None
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=_SLIDE_FRAME_LEVEL_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 里对应数组(concepts/pillars/eras/items)的长度"
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())
@@ -1,72 +0,0 @@
1
- """Centralized monorepo path resolution for template-library artifacts.
2
-
3
- Why this module exists
4
- ----------------------
5
- Before this file landed, every caller that needed to reach into the
6
- monorepo's ``template-library/`` from inside ``apps/ab-skill/`` had to count
7
- ``..`` segments by hand. The depth differs per call site:
8
-
9
- skills/template-registry/scripts/registry_loader.py → 5 × ".."
10
- skills/template-registry/scripts/check_contracts.py → 3 × ".."
11
- skills/template-registry/video_dsl/runtime/template_binder → 6 × ".."
12
- skills/gen-script/scripts/gen_script.py → 5 × ".."
13
- skills/render-video/scripts/render_video.py → 4 × ".."
14
-
15
- Five copies, five chances for an off-by-one bug, and any future directory
16
- move silently breaks one site without touching the others. This module
17
- computes the monorepo root **once** from its own location and exposes
18
- helpers so callers no longer count.
19
-
20
- All sibling skills (including the ``list_templates.py`` CLI entry, via
21
- ``registry_loader``) go through these helpers, so the monorepo-root
22
- computation lives in exactly one place.
23
- """
24
- from __future__ import annotations
25
-
26
- import os
27
- from typing import Optional
28
-
29
- __all__ = ["monorepo_root", "monorepo_registry_path", "monorepo_template_src_dir"]
30
-
31
- # This file lives at:
32
- # <repo>/apps/ab-skill/skills/template-registry/scripts/template_paths.py
33
- # So the monorepo root is 5 levels up.
34
- _HERE = os.path.dirname(os.path.abspath(__file__))
35
- _REPO_ROOT = os.path.normpath(os.path.join(_HERE, "..", "..", "..", "..", ".."))
36
-
37
-
38
- def monorepo_root() -> str:
39
- """Absolute path to the ab-platform monorepo root.
40
-
41
- Always returns a string; the caller is responsible for checking
42
- ``os.path.isdir(...)`` if the root may genuinely be absent (e.g. when
43
- the skill is consumed standalone via the npm package).
44
- """
45
- return _REPO_ROOT
46
-
47
-
48
- def monorepo_registry_path() -> str:
49
- """Path to ``template-library/packages/metadata/registry.json``.
50
-
51
- Always returns the computed path even when the file does not exist —
52
- so callers can keep their existing ``os.path.exists(...)`` / try-load
53
- branches unchanged. Mirrors the historical behavior of the 4 in-tree
54
- helpers it replaces.
55
- """
56
- return os.path.join(_REPO_ROOT, "template-library", "packages", "metadata", "registry.json")
57
-
58
-
59
- def monorepo_template_src_dir(template_id: str) -> Optional[str]:
60
- """Locate a template's source directory under ``template-library``.
61
-
62
- Returns the absolute path **only when the directory exists**; returns
63
- ``None`` for the standalone / npm-consumer case where there is no
64
- monorepo to reach into. Matches ``gen_script._locate_template_dir``'s
65
- historical contract (callers test the result for None).
66
- """
67
- if not template_id:
68
- return None
69
- tpl_dir = os.path.join(
70
- _REPO_ROOT, "template-library", "packages", "templates", "src", template_id
71
- )
72
- return tpl_dir if os.path.isdir(tpl_dir) else None