@remixmate/cli 0.9.72 → 0.9.74

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 (179) 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 -192
  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 +44 -3
  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-script/SKILL.md +23 -1
  61. package/skills/gen-script/scripts/gen_script.py +644 -551
  62. package/skills/gen-script/skill.json +12 -0
  63. package/skills/gen-script/version.json +1 -1
  64. package/skills/prepare-video-assets/scripts/prepare_video_assets.py +21 -21
  65. package/skills/render-video/scripts/_chrome_vendor.py +17 -17
  66. package/skills/render-video/scripts/_digital_human_segments.py +34 -34
  67. package/skills/render-video/scripts/_video_probe.py +33 -33
  68. package/skills/render-video/scripts/_vod_polling.py +13 -13
  69. package/skills/render-video/scripts/remote_renderer_client.py +42 -42
  70. package/skills/render-video/scripts/render_video.py +639 -639
  71. package/skills/render-video/scripts/required_props.py +37 -37
  72. package/skills/render-video/scripts/upload_video.py +35 -35
  73. package/skills/render-video/version.json +1 -1
  74. package/skills/template-registry/SKILL.md +16 -0
  75. package/skills/template-registry/scripts/countdown_spec.py +92 -92
  76. package/skills/template-registry/scripts/doc_extract.py +40 -40
  77. package/skills/template-registry/scripts/list_templates.py +303 -191
  78. package/skills/template-registry/scripts/log_diagnostics.py +1 -1
  79. package/skills/template-registry/scripts/match_template.py +87 -87
  80. package/skills/template-registry/scripts/payload_hydrator.py +89 -89
  81. package/skills/template-registry/scripts/registry_loader.py +159 -159
  82. package/skills/template-registry/scripts/render_job_client.py +75 -75
  83. package/skills/template-registry/scripts/showcase_loader.py +44 -44
  84. package/skills/template-registry/skill.json +4 -1
  85. package/skills/template-registry/version.json +1 -1
  86. package/skills/template-registry/video_dsl/runtime/__init__.py +8 -8
  87. package/skills/template-registry/video_dsl/runtime/dsl_validator.py +329 -329
  88. package/skills/template-registry/video_dsl/runtime/narration_budget.py +36 -36
  89. package/skills/template-registry/video_dsl/runtime/payload_contract.py +7 -7
  90. package/skills/template-registry/video_dsl/runtime/prompt_enhancer.py +98 -98
  91. package/skills/template-registry/video_dsl/runtime/timeline_compiler.py +127 -127
  92. package/skills/template-registry/video_dsl/schema/video-dsl-v1alpha1.json +14 -1
  93. package/skills/video-parser/scripts/analyze_video.py +173 -173
  94. package/skills/video-parser/scripts/deconstruct_video.py +91 -91
  95. package/skills/video-parser/scripts/parse_via_render.py +21 -21
  96. package/skills/web-screenshot/scripts/_media_screenshot/__init__.py +4 -4
  97. package/skills/web-screenshot/scripts/_media_screenshot/bootstrap.py +7 -7
  98. package/skills/web-screenshot/scripts/_media_screenshot/browser.py +4 -4
  99. package/skills/web-screenshot/scripts/_media_screenshot/cli_args.py +4 -4
  100. package/skills/web-screenshot/scripts/_media_screenshot/js_loader.py +2 -2
  101. package/skills/web-screenshot/scripts/_media_screenshot/overlay.py +1 -1
  102. package/skills/web-screenshot/scripts/_media_screenshot/reader.py +40 -40
  103. package/skills/web-screenshot/scripts/_media_screenshot/recording.py +11 -11
  104. package/skills/web-screenshot/scripts/_media_screenshot/scenes/__init__.py +8 -8
  105. package/skills/web-screenshot/scripts/_media_screenshot/scenes/arrow.py +2 -2
  106. package/skills/web-screenshot/scripts/_media_screenshot/scenes/caption.py +1 -1
  107. package/skills/web-screenshot/scripts/_media_screenshot/scenes/click.py +1 -1
  108. package/skills/web-screenshot/scripts/_media_screenshot/scenes/code_lines.py +5 -5
  109. package/skills/web-screenshot/scripts/_media_screenshot/scenes/focus.py +1 -1
  110. package/skills/web-screenshot/scripts/_media_screenshot/scenes/highlight.py +3 -3
  111. package/skills/web-screenshot/scripts/_media_screenshot/scenes/hover.py +1 -1
  112. package/skills/web-screenshot/scripts/_media_screenshot/scenes/redact.py +1 -1
  113. package/skills/web-screenshot/scripts/_media_screenshot/scenes/scroll.py +1 -1
  114. package/skills/web-screenshot/scripts/_media_screenshot/scenes/sequence.py +1 -1
  115. package/skills/web-screenshot/scripts/_media_screenshot/scenes/title_card.py +1 -1
  116. package/skills/web-screenshot/scripts/_media_screenshot/scenes/type.py +1 -1
  117. package/skills/web-screenshot/scripts/_media_screenshot/scenes/wait.py +1 -1
  118. package/skills/web-screenshot/scripts/_media_screenshot/scenes/zoom.py +2 -2
  119. package/skills/web-screenshot/scripts/_media_screenshot/screenshot.py +15 -15
  120. package/skills/web-screenshot/scripts/_media_screenshot/storyboard.py +2 -2
  121. package/skills/web-screenshot/scripts/_media_screenshot/template.py +11 -11
  122. package/skills/web-screenshot/scripts/_media_screenshot/trim.py +10 -10
  123. package/skills/web-screenshot/scripts/_media_screenshot/urlguard.py +22 -22
  124. package/skills/web-screenshot/scripts/_media_upload.py +33 -33
  125. package/skills/web-screenshot/scripts/read_page.py +15 -15
  126. package/skills/web-screenshot/scripts/record.py +26 -26
  127. package/skills/web-screenshot/scripts/screenshot.py +5 -5
  128. package/dist/argv.d.ts +0 -15
  129. package/dist/auth/auth-lock.d.ts +0 -26
  130. package/dist/auth/auto-login.d.ts +0 -20
  131. package/dist/auth/commands.d.ts +0 -8
  132. package/dist/auth/credential-store.d.ts +0 -60
  133. package/dist/auth/device-flow-runner.d.ts +0 -58
  134. package/dist/auth/device-flow.d.ts +0 -52
  135. package/dist/auth/ensure.d.ts +0 -55
  136. package/dist/auth/environment.d.ts +0 -38
  137. package/dist/auth/pending-store.d.ts +0 -33
  138. package/dist/auth/resolve.d.ts +0 -30
  139. package/dist/billing.d.ts +0 -44
  140. package/dist/capabilities.d.ts +0 -63
  141. package/dist/cli.d.ts +0 -13
  142. package/dist/diagnostics.d.ts +0 -24
  143. package/dist/doctor.d.ts +0 -22
  144. package/dist/errors.d.ts +0 -29
  145. package/dist/exec.d.ts +0 -18
  146. package/dist/handlers/gen-digital-human.d.ts +0 -17
  147. package/dist/handlers/gen-image.d.ts +0 -20
  148. package/dist/handlers/gen-post.d.ts +0 -22
  149. package/dist/handlers/gen-video.d.ts +0 -19
  150. package/dist/handlers/gen-voice.d.ts +0 -26
  151. package/dist/handlers/index.d.ts +0 -20
  152. package/dist/handlers/shared.d.ts +0 -26
  153. package/dist/handlers/template-init.d.ts +0 -10
  154. package/dist/http.d.ts +0 -72
  155. package/dist/index.d.ts +0 -9
  156. package/dist/install.d.ts +0 -71
  157. package/dist/progress.d.ts +0 -23
  158. package/dist/project/commands.d.ts +0 -19
  159. package/dist/project/host.d.ts +0 -91
  160. package/dist/project/resolve.d.ts +0 -43
  161. package/dist/project/store.d.ts +0 -31
  162. package/dist/project/take.d.ts +0 -50
  163. package/dist/project/trace.d.ts +0 -98
  164. package/dist/registry.d.ts +0 -30
  165. package/dist/runner.d.ts +0 -39
  166. package/dist/skill-schema.d.ts +0 -138
  167. package/dist/template/args.d.ts +0 -25
  168. package/dist/template/check.d.ts +0 -23
  169. package/dist/template/commands.d.ts +0 -25
  170. package/dist/template/constants.d.ts +0 -39
  171. package/dist/template/init.d.ts +0 -28
  172. package/dist/template/list.d.ts +0 -23
  173. package/dist/template/preview.d.ts +0 -60
  174. package/dist/template/proc.d.ts +0 -26
  175. package/dist/template/project.d.ts +0 -47
  176. package/dist/template/publish.d.ts +0 -43
  177. package/dist/template/render-client.d.ts +0 -39
  178. package/dist/template/upgrade.d.ts +0 -44
  179. package/dist/text.d.ts +0 -8
@@ -1,9 +1,9 @@
1
- """Validate template-required scene props before asset generation or rendering.
2
1
 
3
- Binding records missingRequiredProps for diagnostics, but a production caller
4
- must reject those scenes instead of paying for audio and rendering placeholders.
5
- Existing RenderPlans need the same check because they bypass binding entirely.
6
- """
2
+
3
+
4
+
5
+
6
+
7
7
 
8
8
  from __future__ import annotations
9
9
 
@@ -11,14 +11,14 @@ import math
11
11
  import sys
12
12
  from pathlib import Path
13
13
 
14
- # Shared cross-skill lib (see AGENTS.md). Inserted here rather than relying on
15
- # render_video's module-top setup because this module is imported directly by
16
- # tests and by prepare-video-assets.
14
+
15
+
16
+
17
17
  sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "template-registry" / "scripts"))
18
18
 
19
- from countdown_spec import resolve_countdown_spec # noqa: E402
19
+ from countdown_spec import resolve_countdown_spec
20
20
  sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "template-registry"))
21
- from video_dsl.runtime.payload_contract import required_payload_errors # noqa: E402
21
+ from video_dsl.runtime.payload_contract import required_payload_errors
22
22
 
23
23
 
24
24
  def _payload_prop(props: dict, key: str):
@@ -35,11 +35,11 @@ def _payload_prop(props: dict, key: str):
35
35
 
36
36
 
37
37
  def _required_value_errors(value, schema: dict, path: str) -> list[str]:
38
- """Check required values only, using the template's declared shape and bounds.
39
38
 
40
- This is not a full JSON Schema validator: optional fields and undeclared
41
- extensions are left to the existing DSL validators and template runtime.
42
- """
39
+
40
+
41
+
42
+
43
43
  if value is None or (isinstance(value, str) and not value.strip()):
44
44
  return [path]
45
45
  kind = schema.get("type")
@@ -67,10 +67,10 @@ def _required_value_errors(value, schema: dict, path: str) -> list[str]:
67
67
 
68
68
 
69
69
  def validate_required_props(template: dict, rows: list[dict]) -> None:
70
- """Raise with scene IDs and field names only; never include scene content."""
70
+
71
71
  required_by_composition: dict[str, set[str]] = {}
72
- # References reuse a concrete slot's composition, so only concrete slots
73
- # contribute requirements here. Union if multiple slots share a component.
72
+
73
+
74
74
  for slot in (template.get("slotMapping") or {}).values():
75
75
  if not isinstance(slot, dict) or not slot.get("compositionId"):
76
76
  continue
@@ -102,29 +102,29 @@ def validate_required_props(template: dict, rows: list[dict]) -> None:
102
102
 
103
103
 
104
104
  def _index_of(row: dict, spec: dict):
105
- """A beat's countdown index, or None when the row carries no usable item.
106
-
107
- Returning None rather than indexing straight into the payload keeps a scene
108
- with a missing/malformed item on the countdown error path (which names the
109
- scene and says what to fix) instead of an AttributeError from deep inside the
110
- validator. Reachable whenever a countdown template's point slot does not list
111
- its item prop in requiredProps, so the earlier per-scene check lets it through.
112
- """
105
+
106
+
107
+
108
+
109
+
110
+
111
+
112
+
113
113
  item = _payload_prop(row.get("props") or {}, spec["item_prop"])
114
114
  return item.get(spec["index_prop"]) if isinstance(item, dict) else None
115
115
 
116
116
 
117
117
  def _validate_countdown(template: dict, rows: list[dict]) -> None:
118
- """Check a countdown plan's shape before anything is paid for.
119
-
120
- Every prop name here comes from the template's own `capabilities.countdown`
121
- declaration, so this function holds no knowledge of ranking, GitHub, or any
122
- template id. That matters beyond tidiness: the names used to be hardcoded,
123
- which meant a second countdown template calling its count `itemCount` would
124
- find no counts at all, fall back to "however many beats are present", and
125
- reduce the scene-count check to comparing that number against itself —
126
- validation silently gone, with nothing on screen or in the logs to say so.
127
- """
118
+
119
+
120
+
121
+
122
+
123
+
124
+
125
+
126
+
127
+
128
128
  spec = resolve_countdown_spec(template)
129
129
  if spec is None:
130
130
  return
@@ -134,7 +134,7 @@ def _validate_countdown(template: dict, rows: list[dict]) -> None:
134
134
  ending_id = (slots.get("ending") or {}).get("compositionId")
135
135
  points = [row for row in rows if row.get("compositionId") == point_id]
136
136
  count_props = [spec["count_prop"], *spec["legacy_count_props"]]
137
- # Keep old plans usable: infer their count when no explicit count was saved.
137
+
138
138
  declared = []
139
139
  for row in rows:
140
140
  props = row.get("props") or {}
@@ -163,7 +163,7 @@ def _validate_countdown(template: dict, rows: list[dict]) -> None:
163
163
  )
164
164
  if not rows or rows[0].get("compositionId") != opening_id or sum(row.get("compositionId") == opening_id for row in rows) != 1:
165
165
  raise ValueError("Countdown requires exactly one opening before the beats")
166
- # A separate ending may hold the final beat, but cannot add another index.
166
+
167
167
  endings = [row for row in rows if row.get("compositionId") == ending_id]
168
168
  if len(endings) > 1 or (endings and (rows[-1] is not endings[0] or _index_of(endings[0], spec) != 1)):
169
169
  raise ValueError("Countdown ending may only hold the #1 scene after the countdown")
@@ -1,26 +1,26 @@
1
1
  #!/usr/bin/env python3
2
- """
3
- 视频上传工具 - 将本地 MP4 文件通过 multipart/form-data 上传到 ab-api,
4
- 获取可用 URL(直传阿里云 OSS,同步返回)。
5
2
 
6
- 默认行为:
7
- 读取本地文件,以 multipart/form-data 形式 POST 到
8
- `<MM_API_BASE_URL>/file/uploadMediaFile`,解析响应 JSON,输出 fileUrl。
9
3
 
10
- 用法:
11
- python3 upload_video.py --file output/video.mp4 --role final_video
12
- python3 upload_video.py --file output/video.mp4 --role final_video --title "我的视频"
13
- python3 upload_video.py --file output/video.mp4 --role final_video --json-output
14
4
 
15
- 环境变量 PRIV_TOKEN 必填(接口在 ab-api 私有组,产物落素材表并归到调用者名下)。
16
5
 
17
- 环境变量:
18
- MM_API_BASE_URL - API 根地址(默认 MM_API_BASE_URL 环境变量或 https://api-agent.remixmate.ai/api)
19
6
 
20
- 注意:
21
- - `uploadMediaFile` 接口视频大小限制 ≤ 50MB,音频 ≤ 15MB
22
- - 进度信息输出到 stderr,最终 URL 输出到 stdout(最后一行)
23
- """
7
+
8
+
9
+
10
+
11
+
12
+
13
+
14
+
15
+
16
+
17
+
18
+
19
+
20
+
21
+
22
+
23
+
24
24
 
25
25
  import argparse
26
26
  import http.client
@@ -42,11 +42,11 @@ def _guess_content_type(file_path: str) -> str:
42
42
 
43
43
 
44
44
  def _build_multipart_body(file_path: str, fields: dict) -> (bytes, str):
45
- """
46
- 手动构建 multipart/form-data body。fields 里值为空的字段不发。
47
45
 
48
- 返回 (body_bytes, content_type_header)。
49
- """
46
+
47
+
48
+
49
+
50
50
  boundary = "----MediaMakerBoundary" + uuid.uuid4().hex
51
51
  crlf = b"\r\n"
52
52
  parts = []
@@ -59,7 +59,7 @@ def _build_multipart_body(file_path: str, fields: dict) -> (bytes, str):
59
59
  parts.append(b"")
60
60
  parts.append(str(value).encode("utf-8"))
61
61
 
62
- # file 字段
62
+
63
63
  file_name = os.path.basename(file_path)
64
64
  content_type = _guess_content_type(file_path)
65
65
  with open(file_path, "rb") as f:
@@ -76,7 +76,7 @@ def _build_multipart_body(file_path: str, fields: dict) -> (bytes, str):
76
76
  parts.append(b"")
77
77
  parts.append(file_data)
78
78
 
79
- # 结尾 boundary
79
+
80
80
  parts.append(("--" + boundary + "--").encode("utf-8"))
81
81
  parts.append(b"")
82
82
 
@@ -94,15 +94,15 @@ def upload_media_file(
94
94
  role: str,
95
95
  retention: str = None,
96
96
  ) -> dict:
97
- """
98
- 将本地媒体文件以 multipart/form-data 形式上传到 ab-api,并落素材表。
99
97
 
100
- role 必填:素材语义角色(final_video / dh_drive_audio / source_audio ...),由调用方
101
- 按「这是什么」声明——ab-api 不再按文件类型猜。retention 可选(intermediate / output / user)。
102
98
 
103
- 返回服务端 data 字段,形如 {"fileUrl": "https://...", "fileId": 123, "fileUid": "file_xxx"}。
104
- 上传失败会抛出异常。
105
- """
99
+
100
+
101
+
102
+
103
+
104
+
105
+
106
106
  if not role:
107
107
  raise ValueError("upload_media_file: role is required")
108
108
  if not os.path.exists(file_path):
@@ -118,7 +118,7 @@ def upload_media_file(
118
118
  body, content_type = _build_multipart_body(
119
119
  file_path, {"title": title, "role": role, "retention": retention}
120
120
  )
121
- # 该接口在 ab-api 私有组:产物要落素材表、归到调用者名下,必须带 PrivToken。
121
+
122
122
  headers = {
123
123
  "Content-Type": content_type,
124
124
  "Content-Length": str(len(body)),
@@ -129,8 +129,8 @@ def upload_media_file(
129
129
  if conversation_id:
130
130
  headers["x-conversation-id"] = conversation_id
131
131
 
132
- # Use http.client directly to avoid urllib RemoteDisconnected issues
133
- # with servers that return 'connection: close'
132
+
133
+
134
134
  parsed = urllib.parse.urlparse(url)
135
135
  host = parsed.hostname
136
136
  port = parsed.port
@@ -164,7 +164,7 @@ def upload_media_file(
164
164
  except json.JSONDecodeError as e:
165
165
  raise RuntimeError(f"failed to parse response: {resp_body}") from e
166
166
 
167
- # ab-api shared response: { code: 0, data: {...}, msg: "..." }
167
+
168
168
  if isinstance(result, dict) and result.get("code") not in (0, 200):
169
169
  raise RuntimeError(f"server returned an error: code={result.get('code')}, msg={result.get('msg')}")
170
170
 
@@ -217,7 +217,7 @@ def main():
217
217
  if args.json_output:
218
218
  print(json.dumps(data, ensure_ascii=False))
219
219
  else:
220
- # 最终 URL 输出到 stdout(最后一行),方便其他脚本捕获
220
+
221
221
  print(data["fileUrl"])
222
222
 
223
223
 
@@ -2,6 +2,6 @@
2
2
  "skillName": "render-video",
3
3
  "repoName": "agent-skill-media-maker",
4
4
  "skillId": "473",
5
- "version": "V36",
5
+ "version": "V37",
6
6
  "skillDescription": "Final-render skill: loads a persisted RenderPlan by `job_id` and drives the Remotion engine to produce the final video.\n\nUse this skill as soon as the user mentions any of these intents (after assets are already prepared):\n- Render the video, composite the video, export the video\n- Turn the prepared assets into the final clip\n- Render with Remotion\n\nPrerequisite: assets must already be generated via `prepare_video_assets`. This skill never resolves or regenerates assets — pass it a `job_id` from a previous `prepare_video_assets` call.\n\n⚠️ Stop-and-confirm gate: never call this skill until the user has explicitly confirmed the assets prepared by `prepare_video_assets`. If those assets were prepared in the current turn and the user has not replied since, stop and ask instead of rendering."
7
7
  }
@@ -86,6 +86,19 @@ When the user asks to see / browse / pick templates, they decide by *watching* s
86
86
  - No sample info (older backend, or a template with no samples yet) is not an error: show the template without it.
87
87
  - The detail page's prompt is meant to be pasted back into this chat; treat it like any user request.
88
88
 
89
+ ### Recommend templates when the user named none
90
+
91
+ A video request without a template is a template choice the user has not made yet — not one for the agent to make silently. Shortlist with the list verb's hard constraints — they read each template's declarations, so a template that cannot serve the request is dropped with a stated reason (JSON `excluded[{templateId, reasons}]`, or an `Excluded by the current filters` block in the table):
92
+
93
+ ```bash
94
+ remixmate template-registry --json-output --filter-aspect 9:16 --filter-language zh \
95
+ --user-media yes # the user handed over images/videos (no = they did not)
96
+ # --source-doc pptx # the user gave a .pptx (none = drop templates that need one)
97
+ # --narration no # the user wants no voice-over
98
+ ```
99
+
100
+ Set only the constraints the request actually pins down; an unset one does not filter. Then rank what is left against `llmHint` / `description` / `styleTags`, and use `notFor` (a template's own list of requests it is the wrong pick for) to separate close siblings. Use coarse levels (recommended / also fits) and show 2–3 in the format above with a one-line reason each. Then end the turn and let the user pick; `gen_script` takes the answer as `--template-source user-confirmed` plus `--template-candidates`. The full rule is in the `gen-script` skill, "Agent behavior: template selection comes before the script".
101
+
89
102
  ### Build a binding locally (debug only, not part of the render pipeline)
90
103
 
91
104
  ```python
@@ -110,6 +123,9 @@ print(json.dumps(binding, ensure_ascii=False, indent=2))
110
123
  | `--filter-tag` | Keep only templates whose `styleTags` match this substring (case-insensitive). | none |
111
124
  | `--filter-aspect` | Keep only templates declaring this aspect ratio (e.g. `9:16`). | none |
112
125
  | `--filter-language` | Keep only templates whose `contentLanguage` includes this code (`zh`/`en`); language-agnostic templates always show. | none |
126
+ | `--user-media yes\|no` | Shortlist by the template's media shape (`capabilities.payloadStyle`, else derived from `assetRequirements`): `yes` drops templates with no place for the user's own images/videos, `no` drops templates that need them. | none |
127
+ | `--source-doc <kind>\|none` | Shortlist by `capabilities.payloadHydration.docKind`: a kind keeps only templates built from such a document; `none` drops them. | none |
128
+ | `--narration yes\|no` | Shortlist by `capabilities.needsNarration` (default true). | none |
113
129
  | `--include-beta` | Also show `status: beta` templates (same effect as `ENABLE_BETA_TEMPLATES=1`). | off |
114
130
  | `--json-output` | List mode: emit `{ "templates": [...] }` summaries (or full definitions with `--full`) instead of the table. Detail mode (`--template-id`) is always compact JSON. Summaries carry `pageUrl` (web detail page) and `showcases: { count, pageUrl, items }`; detail mode carries top-level `pageUrls` and `showcases` maps. Each is absent when the backend does not serve it. | off |
115
131
  | `--open <id>` | Print the template's web detail page URL (samples, layouts) and open the browser when run from an interactive terminal. Exits 1 when the backend has no link for it. Cannot be combined with `--template-id`. | — |
@@ -1,83 +1,83 @@
1
- """Shared resolver for the ``countdown`` scene strategy.
2
-
3
- A countdown is the "one opening, then N beats of the same kind, counting down"
4
- shape: Top-N lists, "5 个技巧", "7 个常见错误". It sits on the same axis as
5
- ``arc`` / ``single`` / ``fixed`` because it is a *shape*, not a subject — which
6
- is the whole reason it is not called "ranking" and holds no GitHub-specific
7
- knowledge.
8
-
9
- None of the existing strategies could express it: ``arc`` clamps to at least 3
10
- scenes, always appends a ``cta``, and numbers its beats upward; ``fixed`` needs a
11
- static scene list and so cannot take a count at all.
12
-
13
- ─── Why this module is shared rather than copied ──────────────────────────────
14
- Two skills need the same answer from the same declaration:
15
-
16
- * ``gen-script`` — to lay out the scenes and write the count into each payload
17
- * ``render-video`` — to validate a plan (scene count, countdown completeness)
18
- before any asset is paid for
19
-
20
- Copying the resolution into both is how the previous iteration of this feature
21
- went wrong in the other direction (a Python port of the template's copy rules
22
- living next to the real ones). ``skills/template-registry/scripts/`` is the
23
- sanctioned home for cross-skill Python — see AGENTS.md.
24
-
25
- ─── What is declared where ───────────────────────────────────────────────────
26
- The strategy carries the shape; the template carries every concrete value:
27
-
28
- "capabilities": {
29
- "sceneStrategy": "countdown",
30
- "countdown": {
31
- "countProp": "topN", // where the count goes in customPayload
32
- "itemProp": "item", // where one beat's item goes
33
- "indexProp": "rank", // the index field inside that item
34
- "legacyCountProps": ["totalItems"],
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)
38
- }
39
- }
40
1
 
41
- Bounds are deliberately **not** repeated here: they are read from
42
- ``customPayloadSchema[countProp]``'s ``minimum`` / ``maximum`` / ``default``,
43
- which every template already has to author for its own payload contract. Having
44
- one number in two places is how they drift.
45
2
 
46
- Declaring the prop *names* is what keeps this module honest. They used to be
47
- hardcoded, which meant a second countdown template naming its count
48
- ``itemCount`` would silently lose validation entirely: nothing would be found,
49
- the count would fall back to "however many cards are present", and the
50
- "scene count matches the count" check would compare that number to itself.
51
- """
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
+
40
+
41
+
42
+
43
+
44
+
45
+
46
+
47
+
48
+
49
+
50
+
51
+
52
52
 
53
53
  from __future__ import annotations
54
54
 
55
55
  import re
56
56
 
57
- #: Opening budget when the template declares none. A generic "an opening is a
58
- #: beat, not a chapter" value — **not** any one template's measurement. It used
59
- #: to be 3 hardcoded in gen_script, taken from github-repo-rank's reference film
60
- #: (2.43s), which put a single template's fact in the CLI.
57
+
58
+
59
+
60
+
61
61
  DEFAULT_OPENING_SEC = 3
62
62
 
63
63
 
64
64
  class CountdownError(ValueError):
65
- """Raised for an unusable countdown declaration.
65
+ pass
66
+
67
+
68
+
69
+
66
70
 
67
- A subclass so callers can keep treating it as the ValueError they already
68
- handle, while a reader can still see that an authoring mistake and a bad
69
- ``--item-count`` are two different things.
70
- """
71
71
 
72
72
 
73
73
  def resolve_countdown_spec(template: dict | None) -> dict | None:
74
- """Resolve a template's countdown declaration, or None if it is not one.
75
74
 
76
- Raises CountdownError when ``sceneStrategy=countdown`` is declared but the
77
- rest of the contract is missing. Failing loudly here is the point: every
78
- downstream check reads these names, so an incomplete declaration must not
79
- degrade into "no countdown rules apply".
80
- """
75
+
76
+
77
+
78
+
79
+
80
+
81
81
  caps = (template or {}).get("capabilities")
82
82
  caps = caps if isinstance(caps, dict) else {}
83
83
  if caps.get("sceneStrategy") != "countdown":
@@ -96,8 +96,8 @@ def resolve_countdown_spec(template: dict | None) -> dict | None:
96
96
  )
97
97
  count_prop = count_prop.strip()
98
98
 
99
- # itemProp / indexProp 同样必须声明:缺省值只能是某个具体模板的字段名,
100
- # 那等于把那个模板写进 CLI。
99
+
100
+
101
101
  names: dict[str, str] = {}
102
102
  for key in ("itemProp", "indexProp"):
103
103
  value = cfg.get(key)
@@ -171,12 +171,12 @@ def resolve_countdown_spec(template: dict | None) -> dict | None:
171
171
 
172
172
 
173
173
  def stated_counts(spec: dict, text: str) -> set[int]:
174
- """Counts that on-screen copy states, found with the template's ``countPatterns``.
175
174
 
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
- """
175
+
176
+
177
+
178
+
179
+
180
180
  found: set[int] = set()
181
181
  for pattern in spec.get("count_patterns") or []:
182
182
  for match in pattern.finditer(text or ""):
@@ -187,14 +187,14 @@ def stated_counts(spec: dict, text: str) -> set[int]:
187
187
 
188
188
 
189
189
  def resolve_count(spec: dict, requested: int | None, scene_count: int | None) -> int:
190
- """Validate a caller-supplied item count against the template's own bounds.
191
-
192
- ``requested`` is the number of content beats the caller asked for — the same
193
- number the user picked. It is deliberately *not* derived from ``scene_count``:
194
- a caller passing the user's 5 as a total scene count would produce 4 cards
195
- with a self-consistent ``count=4`` written everywhere, which no validator can
196
- catch and the user never sees. So the two are cross-checked instead.
197
- """
190
+
191
+
192
+
193
+
194
+
195
+
196
+
197
+
198
198
  count = spec["default"] if requested is None else requested
199
199
  if type(count) is not int or not spec["minimum"] <= count <= spec["maximum"]:
200
200
  raise ValueError(
@@ -210,26 +210,26 @@ def resolve_count(spec: dict, requested: int | None, scene_count: int | None) ->
210
210
 
211
211
 
212
212
  def opening_sec(spec: dict, count: int, duration: float) -> float:
213
- """Seconds for the single opening scene.
214
213
 
215
- ``openingSec`` is an upper bound, not a fixed value: on a short video it
216
- yields to an even split so a long opening budget cannot starve the beats.
217
214
 
218
- Worth knowing how provisional this is — ``render_video``'s
219
- ``adjust_timeline_to_audio`` overwrites every scene's duration with the real
220
- narration length once TTS exists. This number shapes the script the user
221
- reviews and the duration/cost estimate, not the finished cut.
222
- """
215
+
216
+
217
+
218
+
219
+
220
+
221
+
222
+
223
223
  return min(spec["opening_sec"], duration / (count + 1))
224
224
 
225
225
 
226
226
  def scene_payload(spec: dict, count: int, index: int | None) -> dict:
227
- """The countdown part of one scene's customPayload, under declared names.
228
227
 
229
- ``index`` is None for the opening. For a beat only the index is filled:
230
- inventing the rest of the item would put fabricated data on screen, so the
231
- caller is expected to fill it with real content before assets are prepared.
232
- """
228
+
229
+
230
+
231
+
232
+
233
233
  payload: dict = {spec["count_prop"]: count}
234
234
  for prop in spec["legacy_count_props"]:
235
235
  payload[prop] = count