@remixmate/cli 0.1.0

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 (91) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +166 -0
  3. package/dist/argv.d.ts +15 -0
  4. package/dist/argv.js +51 -0
  5. package/dist/cli.d.ts +13 -0
  6. package/dist/cli.js +83 -0
  7. package/dist/handlers/gen-image.d.ts +13 -0
  8. package/dist/handlers/gen-image.js +161 -0
  9. package/dist/handlers/gen-voice.d.ts +16 -0
  10. package/dist/handlers/gen-voice.js +110 -0
  11. package/dist/handlers/index.d.ts +13 -0
  12. package/dist/handlers/index.js +14 -0
  13. package/dist/handlers/template-bind.d.ts +15 -0
  14. package/dist/handlers/template-bind.js +149 -0
  15. package/dist/http.d.ts +52 -0
  16. package/dist/http.js +110 -0
  17. package/dist/index.d.ts +9 -0
  18. package/dist/index.js +8 -0
  19. package/dist/manifest.json +260 -0
  20. package/dist/progress.d.ts +23 -0
  21. package/dist/progress.js +15 -0
  22. package/dist/registry.d.ts +33 -0
  23. package/dist/registry.js +72 -0
  24. package/dist/runner.d.ts +17 -0
  25. package/dist/runner.js +63 -0
  26. package/package.json +54 -0
  27. package/skills/export-jianying/SKILL.md +251 -0
  28. package/skills/export-jianying/scripts/gen_jianying_draft.py +1099 -0
  29. package/skills/export-jianying/skill.json +24 -0
  30. package/skills/export-jianying/version.json +7 -0
  31. package/skills/gen-digital-human/SKILL.md +211 -0
  32. package/skills/gen-digital-human/scripts/gen_digital_human_video.py +574 -0
  33. package/skills/gen-digital-human/skill.json +23 -0
  34. package/skills/gen-digital-human/version.json +7 -0
  35. package/skills/gen-image/SKILL.md +130 -0
  36. package/skills/gen-image/skill.json +22 -0
  37. package/skills/gen-image/version.json +7 -0
  38. package/skills/gen-script/SKILL.md +283 -0
  39. package/skills/gen-script/scripts/gen_script.py +1208 -0
  40. package/skills/gen-script/skill.json +41 -0
  41. package/skills/gen-script/version.json +7 -0
  42. package/skills/gen-video/SKILL.md +170 -0
  43. package/skills/gen-video/scripts/gen_video.py +591 -0
  44. package/skills/gen-video/skill.json +24 -0
  45. package/skills/gen-video/version.json +7 -0
  46. package/skills/gen-voice/SKILL.md +104 -0
  47. package/skills/gen-voice/skill.json +21 -0
  48. package/skills/gen-voice/version.json +7 -0
  49. package/skills/prepare-video-assets/SKILL.md +192 -0
  50. package/skills/prepare-video-assets/scripts/prepare_video_assets.py +57 -0
  51. package/skills/prepare-video-assets/skill.json +23 -0
  52. package/skills/prepare-video-assets/version.json +7 -0
  53. package/skills/render-video/SKILL.md +187 -0
  54. package/skills/render-video/scripts/_chrome_vendor.py +305 -0
  55. package/skills/render-video/scripts/_video_probe.py +174 -0
  56. package/skills/render-video/scripts/_vod_polling.py +86 -0
  57. package/skills/render-video/scripts/remote_renderer_client.py +236 -0
  58. package/skills/render-video/scripts/render_video.py +2288 -0
  59. package/skills/render-video/scripts/upload_video.py +193 -0
  60. package/skills/render-video/skill.json +21 -0
  61. package/skills/render-video/version.json +7 -0
  62. package/skills/template-bind/README.md +63 -0
  63. package/skills/template-bind/SKILL.md +234 -0
  64. package/skills/template-bind/scripts/check_contracts.py +196 -0
  65. package/skills/template-bind/scripts/match_template.py +215 -0
  66. package/skills/template-bind/scripts/registry_loader.py +379 -0
  67. package/skills/template-bind/scripts/render_job_client.py +162 -0
  68. package/skills/template-bind/scripts/sync_registry.py +44 -0
  69. package/skills/template-bind/scripts/template_paths.py +72 -0
  70. package/skills/template-bind/skill.json +20 -0
  71. package/skills/template-bind/version.json +7 -0
  72. package/skills/template-bind/video_dsl/README.md +97 -0
  73. package/skills/template-bind/video_dsl/runtime/__init__.py +11 -0
  74. package/skills/template-bind/video_dsl/runtime/dsl_validator.py +495 -0
  75. package/skills/template-bind/video_dsl/runtime/prompt_enhancer.py +274 -0
  76. package/skills/template-bind/video_dsl/runtime/template_binder.py +87 -0
  77. package/skills/template-bind/video_dsl/runtime/timeline_compiler.py +312 -0
  78. package/skills/template-bind/video_dsl/schema/render-plan-v1alpha1.json +193 -0
  79. package/skills/template-bind/video_dsl/schema/template-binding-v1alpha1.json +122 -0
  80. package/skills/template-bind/video_dsl/schema/template-definition-v1alpha1.json +247 -0
  81. package/skills/template-bind/video_dsl/schema/video-dsl-v1alpha1.json +378 -0
  82. package/skills/video-parser/SKILL.md +238 -0
  83. package/skills/video-parser/scripts/analyze_video.py +1038 -0
  84. package/skills/video-parser/scripts/deconstruct_video.py +602 -0
  85. package/skills/video-parser/scripts/parse_via_render.py +148 -0
  86. package/skills/video-parser/skill.json +21 -0
  87. package/skills/video-parser/version.json +7 -0
  88. package/skills/web-capture/SKILL.md +183 -0
  89. package/skills/web-capture/scripts/web_capture.py +461 -0
  90. package/skills/web-capture/skill.json +29 -0
  91. package/skills/web-capture/version.json +7 -0
@@ -0,0 +1,196 @@
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
+ # P1.1 nudge (warning only): compositionId should be prefixed with
156
+ # `<templateId>/` so the 300-400 names we'll have at 100+ templates can't
157
+ # collide in the flat global namespace. The actual migration touches
158
+ # remotion-renderer/manifest.ts and every template.json, so we ship the
159
+ # warning first to surface the gap without breaking builds.
160
+ unprefixed: list[tuple[str, str]] = []
161
+ for ref in refs:
162
+ cid = ref["compositionId"]
163
+ tid = ref["templateId"]
164
+ expected_prefix = f"{tid}/"
165
+ # Allow either `<tid>/Cover` or `<tid>.Cover` (TS member access form).
166
+ if not (cid.startswith(expected_prefix) or cid.startswith(f"{tid}.")):
167
+ unprefixed.append((tid, cid))
168
+ if unprefixed:
169
+ print(
170
+ "WARN (P1.1): the following compositionIds are not prefixed with their "
171
+ "templateId — at 100+ templates this flat namespace will collide. "
172
+ "Migration plan: rename `<cid>` to `<templateId>/<cid>` in both "
173
+ "template.json and remotion-renderer/manifest.ts.",
174
+ file=sys.stderr,
175
+ )
176
+ for tid, cid in unprefixed[:10]:
177
+ print(f" - {tid}: '{cid}' → suggested '{tid}/{cid}'", file=sys.stderr)
178
+ if len(unprefixed) > 10:
179
+ print(f" ... ({len(unprefixed)} total)", file=sys.stderr)
180
+
181
+ if errors:
182
+ print("FAIL: contract check found inconsistencies:", file=sys.stderr)
183
+ for e in errors:
184
+ print(f" - {e}", file=sys.stderr)
185
+ return 1
186
+
187
+ print(
188
+ f"OK: {len(refs)} compositionId references across "
189
+ f"{len({r['templateId'] for r in refs})} templates match manifest.ts "
190
+ f"({len(manifest)} entries)."
191
+ )
192
+ return 0
193
+
194
+
195
+ if __name__ == "__main__":
196
+ sys.exit(main())
@@ -0,0 +1,215 @@
1
+ """
2
+ template-bind 库模块 — DSL → TemplateBinding 的纯函数实现。
3
+
4
+ 仅作为 Python library 被 render-video 的 render_video.py / gen-script 的
5
+ gen_script.py 通过 `sys.path.insert` 后 `import match_template` 引入;
6
+ 对外不再暴露 CLI(template-bind skill 已迁移到 ab-skill CLI 的 builtin
7
+ handler,由 src/handlers/template-bind.ts 处理 --list-templates)。
8
+
9
+ 提供的公共接口:
10
+ load_registry() → list[dict] 模板列表
11
+ load_full_template(template) → dict 模板完整定义(registry 自带,原样返回)
12
+ build_binding(template, dsl) → dict DSL → TemplateBinding
13
+ """
14
+
15
+ from registry_loader import load_registry_data
16
+
17
+
18
+ def load_registry() -> list:
19
+ """Load template list via the unified registry loader."""
20
+ return load_registry_data().get("templates", [])
21
+
22
+
23
+ def load_full_template(template: dict) -> dict:
24
+ """Load the full template definition.
25
+
26
+ Since the metadata registry.json now contains the complete template
27
+ definition (including slotMapping, compositions, etc.), this simply
28
+ returns the template as-is.
29
+ """
30
+ return template
31
+
32
+
33
+ def _resolve_slot_def(slot_mapping: dict, purpose: str) -> dict:
34
+ """Resolve a slotMapping entry, following $ref chains (max depth 10)."""
35
+ slot_def = slot_mapping.get(purpose) or slot_mapping.get("default") or {}
36
+ visited: set = set()
37
+ while "$ref" in slot_def:
38
+ ref = slot_def["$ref"]
39
+ if ref in visited:
40
+ break
41
+ visited.add(ref)
42
+ slot_def = slot_mapping.get(ref) or {}
43
+ return slot_def
44
+
45
+
46
+ def _extract_prop(scene: dict, extractor: dict):
47
+ """Extract a single prop value from a scene using an extractor definition.
48
+
49
+ Supports two extractor patterns:
50
+ { "from": "textLayers", "role": "headline" } – search textLayers by role
51
+ { "from": "visuals.background.assetRef" } – dotted-path traversal
52
+ """
53
+ source = extractor.get("from", "")
54
+
55
+ # textLayers extractor
56
+ if source == "textLayers":
57
+ role = extractor.get("role", "")
58
+ for tl in scene.get("textLayers") or []:
59
+ if tl.get("role") == role:
60
+ return tl.get("content")
61
+ return None
62
+
63
+ # Dotted-path extractor
64
+ val = scene
65
+ for part in source.split("."):
66
+ if not isinstance(val, dict):
67
+ return None
68
+ val = val.get(part)
69
+ return val
70
+
71
+
72
+ def build_binding(template: dict, dsl: dict) -> dict:
73
+ """Build a TemplateBinding from template and DSL."""
74
+ bindings = []
75
+ slot_mapping = template.get("slotMapping", {})
76
+
77
+ for idx, scene in enumerate(dsl.get("scenes", [])):
78
+ scene_id = scene.get("id") or scene.get("sceneId") or f"scene-{idx:03d}"
79
+ purpose = scene.get("purpose", "point")
80
+
81
+ slot_def = _resolve_slot_def(slot_mapping, purpose)
82
+ comp_id = slot_def.get("compositionId", "GenericScene")
83
+ slot_id = slot_def.get("slotId", f"{purpose}-slot")
84
+
85
+ # 用模板的 propExtractors 提取 props
86
+ props = {}
87
+ for prop_name, extractor in slot_def.get("propExtractors", {}).items():
88
+ val = _extract_prop(scene, extractor)
89
+ if val is not None:
90
+ props[prop_name] = val
91
+
92
+ # P1.3: props 提取仅保留两条路径——
93
+ # (1) 显式 propExtractors(上面的 _extract_prop 调用)
94
+ # (4) customPayload 全字段透传(顶层 + templateData)— 模板内部 schema
95
+ # 去掉了原来的"白名单同名隐式透传"两层 fallback,因为它们让"字段写在哪"
96
+ # 这个问题变得不可预测:同一字段写在 optionalProps + propExtractors
97
+ # 里时优先级会冲突,让 AI 反复出错。
98
+ #
99
+ # 顶层透传是必要的:DSL 示例(hermes-agent-purple / vibe-coding 等)以
100
+ # `customPayload.slideId` 顶层声明 slide id,而 html-slide 模板的
101
+ # `propExtractors.slideId` 只声明从 templateData.slideId 取值。如果不
102
+ # 同时透传顶层,slideId 就会缺失,html-slide 的 `point` slot 把
103
+ # slideId 设为 requiredProps,缺失会导致 React 端回退到通用「标题 +
104
+ # 字幕」兜底样式,所有场景看起来都是同一个布局。
105
+ custom = scene.get("customPayload") or {}
106
+ template_data = custom.get("templateData") or scene.get("templateData") or {}
107
+ # 先注入 templateData.* 再注入顶层 customPayload.*,让顶层覆盖同名值
108
+ for k, v in template_data.items():
109
+ if k not in props:
110
+ props[k] = v
111
+ for k, v in custom.items():
112
+ if k == "templateData":
113
+ continue
114
+ if k not in props:
115
+ props[k] = v
116
+
117
+ # animationHints 可能为 null
118
+ anim = scene.get("animationHints") or {}
119
+ entrance = anim.get("entrance", "fade")
120
+ transition_type = entrance if entrance != "none" else "cut"
121
+
122
+ bindings.append({
123
+ "sceneId": scene_id,
124
+ "slotId": slot_id,
125
+ "compositionId": comp_id,
126
+ "layoutVariant": scene.get("layout", "full-visual"),
127
+ "props": props,
128
+ "transitionIn": {"type": transition_type, "durationFrames": 15},
129
+ "transitionOut": {"type": "fade", "durationFrames": 15},
130
+ })
131
+
132
+ # P1.2: 不再做 screen-walkthrough 专属的截断 / 兜底。模板的 propExtractors
133
+ # 已声明 titleText ← textLayers[role=headline]、projectName ← textLayers[role=subheadline],
134
+ # gen_script 在每个场景都填入了 headline / subheadline,因此 props 由提取器
135
+ # 直接产出,不再需要后处理。
136
+
137
+ typography = template.get("defaultTypography", {})
138
+ motion = template.get("defaultMotionPreset", "smooth")
139
+ colors = template.get("defaultColorScheme", [])
140
+
141
+ # --- variant selection ---
142
+ # 优先级:
143
+ # 1) DSL 显式声明 — meta.templateVariant / renderHints.templateVariant
144
+ # 2) DSL meta.style / renderHints.brandColors 与 variant.styleTags 的子串/包含匹配
145
+ # 显式声明优先是因为:
146
+ # - 风格名(如 "紫色科技")在不同 variant 的 styleTags 里可能多义;
147
+ # - meta.style 经常是一整串短语而非单 tag,旧的"逐 tag 拼成空格 → 子串匹配"
148
+ # 会被 split 后的整段短语漏过(例如 "紫色科技" 不是 "紫色 科技 purple ..."
149
+ # 的子串)。显式 templateVariant 让作者可以一锤定音。
150
+ variant_id = None
151
+ meta = dsl.get("meta", {}) or {}
152
+ render_hints = dsl.get("renderHints", {}) or {}
153
+ explicit_variant = (
154
+ meta.get("templateVariant")
155
+ or meta.get("variant")
156
+ or render_hints.get("templateVariant")
157
+ )
158
+ variants = template.get("variants", {}) or {}
159
+
160
+ if explicit_variant and explicit_variant in variants:
161
+ variant_id = explicit_variant
162
+ else:
163
+ # Fallback: 把 meta.style 拆成更细的 token(按逗号 / 空白 / 中英标点 / 单字)
164
+ # 再到 variant.styleTags 里找任一包含或被包含关系。这样 "紫色科技" 既能命中
165
+ # ["紫色","科技"] 这种细粒度 tag,也能命中 ["紫色科技"] 这种整段 tag。
166
+ raw_style = (meta.get("style") or "").strip().lower()
167
+ if raw_style:
168
+ # 整段 + 逗号/分号/空白拆分 + 单中文字符级 token,统一去重
169
+ style_tokens = {raw_style}
170
+ for sep in [",", ",", ";", ";", " ", "/", "·"]:
171
+ raw_style = raw_style.replace(sep, ",")
172
+ for tok in raw_style.split(","):
173
+ tok = tok.strip()
174
+ if not tok:
175
+ continue
176
+ style_tokens.add(tok)
177
+ # 中文按字拆,命中如 "紫"/"色"/"科"/"技" 这种单字 tag 的场景
178
+ for ch in tok:
179
+ if "\u4e00" <= ch <= "\u9fff":
180
+ style_tokens.add(ch)
181
+ for vid, variant in variants.items():
182
+ vtags = [str(t).strip().lower() for t in variant.get("styleTags", [])]
183
+ # 任一 tag 与任一 style token 互为子串即视为匹配
184
+ if any(any(tok in tag or tag in tok for tag in vtags) for tok in style_tokens if tok):
185
+ variant_id = vid
186
+ break
187
+
188
+ if variant_id and variant_id in variants:
189
+ variant = variants[variant_id]
190
+ vtypo = variant.get("defaultTypography")
191
+ if vtypo:
192
+ typography = {**typography, **vtypo}
193
+ vmotion = variant.get("defaultMotionPreset")
194
+ if vmotion:
195
+ motion = vmotion
196
+ vcolors = variant.get("defaultColorScheme")
197
+ if vcolors:
198
+ colors = vcolors
199
+
200
+ result = {
201
+ "version": "v1alpha1",
202
+ "templateId": template.get("templateId", ""),
203
+ "templateName": template.get("name", ""),
204
+ "globalOverrides": {
205
+ "typography": typography,
206
+ "motionPreset": motion,
207
+ "colorScheme": colors,
208
+ },
209
+ "bindings": bindings,
210
+ }
211
+ if variant_id:
212
+ result["variantId"] = variant_id
213
+ return result
214
+
215
+