@remixmate/cli 0.9.72 → 0.9.73

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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,82 +1,82 @@
1
1
  #!/usr/bin/env python3
2
- """
3
- template-registry CLI — list available video templates.
4
-
5
- Single source of truth: registry loading, caching and stable/beta status
6
- gating all live in ``registry_loader.py`` (the same module ``render-video``
7
- and ``gen-script`` import in-process). This entry point only adds the CLI
8
- surface — flag parsing, tag/aspect/language filtering, and table/JSON
9
- rendering — that used to be re-implemented in TypeScript
10
- (``src/handlers/template-bind.ts``). Deleting that handler removes the
11
- TS↔Python duplication; this is now the only place the list verb is realized.
12
-
13
- The DSL → TemplateBinding logic stays in ``match_template.py`` and is invoked
14
- in-process by ``render-video``; it is intentionally NOT exposed as a CLI verb.
15
-
16
- List vs. detail (why ``--json-output`` no longer dumps everything):
17
- A registry row is 4–17 KB of JSON (customPayloadSchema + slotMapping +
18
- variants dominate), so ``--list-templates --json-output`` over a real
19
- registry produced a six-figure-character blob. Every caller of this CLI is
20
- an LLM tool call, and that blob overflowed the tool-result budget outright —
21
- the agent got an "exceeds maximum allowed tokens" error instead of the
22
- template definition it asked for, i.e. the mode that existed *to* read a
23
- template's contract was the one mode that could never be read.
24
-
25
- So the list verb emits **summaries** (identity + how to choose: name,
26
- description, aspects, language, status, tags, variant ids, the capability
27
- keys that drive authoring, a truncated llmHint) and ``--template-id <id>``
28
- emits the **full definition** of the one
29
- template the agent picked. That is the shape ``registry_loader``'s P1.2 note
30
- already anticipated, realized CLI-side so it works before the backend
31
- list/detail endpoints exist. ``--full`` still dumps whole definitions for
32
- programmatic callers, guarded by a size check so it can't silently recreate
33
- the overflow.
34
-
35
- Slide menu (why --template-id no longer returns every slideSchemas body):
36
- slideSchemas is two thirds of a multi-layout template (html-slide: 24.6 K of
37
- 37.2 K chars, 26 layouts), while one video uses 5–9 layouts. Returning all of
38
- it every time pushed the detail view past the agent's tool-result size cap
39
- (the result got spilled to a file the agent cannot open, so it re-called
40
- with other flags), and the agent re-read the whole thing on the next turn
41
- anyway. So detail mode returns a per-layout *menu* — description, field
42
- names, list key / item fields / maxItems, and whether narration must be
43
- split into items — which is enough to choose layouts and draft a script;
44
- ``--slide-id`` then returns the exact schemas of the chosen layouts for
45
- writing ``templateData``. Registry consumers inside the pipeline (validator,
46
- render) read the registry directly and are unaffected.
47
-
48
- Flags:
49
- --list-templates (accepted; listing is the default verb)
50
- --template-id <id> print the FULL definition of this template
51
- (repeatable); this is the detail verb.
52
- Multi-layout templates come back with a
53
- slideSchemas *menu* (see "Slide menu" below)
54
- --slide-id <slideId|all> detail mode: expand these layouts' full
55
- templateData schemas (repeatable)
56
- --full in list mode, emit full definitions instead
57
- of summaries (size-guarded; prefer
58
- --template-id)
59
- --list-examples list *.dsl.json / *.binding.json reference
60
- examples grouped by templateId (consumed
61
- by downstream agents that want to read a
62
- template's reference shape before producing
63
- new DSL)
64
- --filter-tag <substr> keep templates whose styleTags match
65
- --filter-aspect <ratio> keep templates supporting this aspect ratio
66
- --filter-language <zh|en|...> keep templates tagged with this content lang
67
- (templates with no contentLanguage are kept)
68
- --include-beta also show beta templates (else stable-only)
69
- --json-output emit {"templates": [...]} or {"examples": [...]}
70
- instead of a table
71
- --open <id> print the template's web detail page URL
72
- (samples, layouts); also opens the browser
73
- when stdout is an interactive terminal
74
-
75
- Links & samples: list and detail views add each template's web detail-page
76
- link (registry document-level ``pageUrls``, falling back to the samples
77
- endpoint) and sample count (showcase_loader.py) when ab-api serves them; when
78
- it does not, the views are unchanged apart from one stderr warning.
79
- """
2
+
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
+
53
+
54
+
55
+
56
+
57
+
58
+
59
+
60
+
61
+
62
+
63
+
64
+
65
+
66
+
67
+
68
+
69
+
70
+
71
+
72
+
73
+
74
+
75
+
76
+
77
+
78
+
79
+
80
80
 
81
81
  from __future__ import annotations
82
82
 
@@ -86,9 +86,9 @@ import os
86
86
  import sys
87
87
  from pathlib import Path
88
88
 
89
- # When run as `python3 <skillDir>/scripts/list_templates.py`, this script's own
90
- # directory is sys.path[0], so the sibling shared modules import directly.
91
- from registry_loader import ( # noqa: E402
89
+
90
+
91
+ from registry_loader import (
92
92
  EXIT_BACKEND_UNREACHABLE,
93
93
  EXIT_NOT_AUTHENTICATED,
94
94
  RegistryAuthError,
@@ -97,40 +97,109 @@ from registry_loader import ( # noqa: E402
97
97
  list_templates as load_visible_templates,
98
98
  registry_page_urls,
99
99
  )
100
- from showcase_loader import load_showcases, summarize_group # noqa: E402
100
+ from showcase_loader import load_showcases, summarize_group
101
+
102
+
101
103
 
102
- # Samples per template in list views (table count + JSON items). The detail
103
- # view (--template-id) returns up to _DETAIL_SHOWCASES.
104
104
  _LIST_SHOWCASES = 3
105
105
  _DETAIL_SHOWCASES = 20
106
106
 
107
- # Summary llmHint budget: enough to tell templates apart when choosing, far
108
- # short of the multi-KB authoring contract (read that with --template-id).
107
+
108
+
109
109
  _SUMMARY_HINT_CHARS = 200
110
110
 
111
- # Upper bound for a multi-template full dump (--full). ~60 K chars ≈ 20 K
112
- # tokens: still large, but inside a tool-result budget, and the message it
113
- # fails with names the flag that replaces it.
111
+
112
+
113
+
114
114
  _MAX_FULL_DUMP_CHARS = 60_000
115
115
 
116
116
 
117
- def _matches(tpl: dict, tag: str | None, aspect: str | None, language: str | None) -> bool:
117
+ _PAYLOAD_STYLES = ("visual-overlay", "slide", "carousel-caption")
118
+
119
+
120
+ def _caps(tpl: dict) -> dict:
121
+ caps = tpl.get("capabilities")
122
+ return caps if isinstance(caps, dict) else {}
123
+
124
+
125
+ def _effective_payload_style(tpl: dict) -> str:
126
+
127
+
128
+ declared = _caps(tpl).get("payloadStyle")
129
+ if declared in _PAYLOAD_STYLES:
130
+ return declared
131
+ reqs = tpl.get("assetRequirements") or []
132
+ return "visual-overlay" if ("image" in reqs or "video" in reqs) else "slide"
133
+
134
+
135
+ def _source_doc_kind(tpl: dict) -> str | None:
136
+ hydration = _caps(tpl).get("payloadHydration")
137
+ if isinstance(hydration, dict):
138
+ kind = hydration.get("docKind")
139
+ return str(kind).lower() if kind else "any"
140
+ return None
141
+
142
+
143
+ def _exclusion_reasons(
144
+ tpl: dict,
145
+ *,
146
+ tag: str | None = None,
147
+ aspect: str | None = None,
148
+ language: str | None = None,
149
+ user_media: str | None = None,
150
+ source_doc: str | None = None,
151
+ narration: str | None = None,
152
+ ) -> list[str]:
153
+
154
+
155
+
156
+
157
+
158
+
159
+ reasons: list[str] = []
118
160
  if tag:
119
161
  needle = tag.lower()
120
162
  tags = [str(t).lower() for t in (tpl.get("styleTags") or [])]
121
163
  if not any(needle in t or t in needle for t in tags):
122
- return False
164
+ reasons.append(f"no styleTag matches {tag!r}")
123
165
  if aspect:
124
166
  want = aspect.lower()
125
167
  ratios = [str(r).lower() for r in (tpl.get("supportedAspectRatios") or [])]
126
168
  if want not in ratios:
127
- return False
169
+ reasons.append(f"aspect ratio {aspect} not supported ({', '.join(ratios) or 'none declared'})")
128
170
  if language:
129
- # Unknown language (absent/empty) is never excluded by a language filter.
171
+
130
172
  langs = [str(l).lower() for l in (tpl.get("contentLanguage") or [])]
131
173
  if langs and language.lower() not in langs:
132
- return False
133
- return True
174
+ reasons.append(f"content language {language} not supported ({', '.join(langs)})")
175
+
176
+ style = _effective_payload_style(tpl)
177
+ if user_media == "yes" and style == "slide":
178
+ reasons.append("no place for the user's own images/videos (payloadStyle=slide)")
179
+ if user_media == "no" and style == "carousel-caption":
180
+ reasons.append("needs the user's own images/videos (payloadStyle=carousel-caption)")
181
+
182
+ doc_kind = _source_doc_kind(tpl)
183
+ if source_doc:
184
+ want_doc = source_doc.lower()
185
+ if want_doc == "none":
186
+ if doc_kind is not None:
187
+ reasons.append("builds every scene from a source document the user has not given (capabilities.payloadHydration)")
188
+ elif doc_kind is None:
189
+ reasons.append(f"does not build scenes from a {want_doc} document")
190
+ elif doc_kind not in ("any", want_doc):
191
+ reasons.append(f"builds scenes from a {doc_kind} document, not {want_doc}")
192
+
193
+ needs_narration = _caps(tpl).get("needsNarration", True) is not False
194
+ if narration == "yes" and not needs_narration:
195
+ reasons.append("has no narration (capabilities.needsNarration=false)")
196
+ if narration == "no" and needs_narration:
197
+ reasons.append("is narration-driven (needsNarration is not false)")
198
+ return reasons
199
+
200
+
201
+ def _matches(tpl: dict, tag: str | None, aspect: str | None, language: str | None) -> bool:
202
+ return not _exclusion_reasons(tpl, tag=tag, aspect=aspect, language=language)
134
203
 
135
204
 
136
205
  def _status_of(tpl: dict) -> str:
@@ -148,23 +217,25 @@ def _truncate(text: object, limit: int) -> str | None:
148
217
 
149
218
 
150
219
  def _summarize(tpl: dict) -> dict:
151
- """The "choose a template" view: identity + selection criteria only.
152
220
 
153
- Deliberately excludes the authoring contract (llmHint in full,
154
- customPayloadSchema, slideSchemas, slotMapping, compositions, variants' style bodies) —
155
- that is what ``--template-id`` returns, one template at a time.
156
- """
221
+
222
+
223
+
224
+
225
+
157
226
  variants = tpl.get("variants")
158
- # Only the scalar capability keys that change how a caller *drives* the
159
- # template (narration vs typewriter caption, who decides duration). The
160
- # rest of `capabilities` — payloadDefaults above all — is bulk that scales
161
- # with the registry, so it stays in the detail view.
162
- caps = tpl.get("capabilities") if isinstance(tpl.get("capabilities"), dict) else {}
227
+
228
+
229
+
230
+
231
+ caps = _caps(tpl)
163
232
  caps_summary = {
164
233
  k: caps[k]
165
- for k in ("payloadStyle", "needsNarration", "durationStrategy", "narrationDriver")
234
+ for k in ("payloadStyle", "needsNarration", "durationStrategy", "narrationDriver", "sceneStrategy")
166
235
  if k in caps
167
236
  }
237
+ if (doc_kind := _source_doc_kind(tpl)) is not None:
238
+ caps_summary["sourceDocKind"] = doc_kind
168
239
  summary = {
169
240
  "templateId": tpl.get("templateId"),
170
241
  "name": tpl.get("name"),
@@ -177,6 +248,10 @@ def _summarize(tpl: dict) -> dict:
177
248
  "capabilities": caps_summary,
178
249
  "llmHintPreview": _truncate(tpl.get("llmHint"), _SUMMARY_HINT_CHARS),
179
250
  }
251
+
252
+
253
+ if isinstance(tpl.get("notFor"), str) and tpl["notFor"].strip():
254
+ summary["notFor"] = tpl["notFor"].strip()
180
255
  hint = tpl.get("llmHint")
181
256
  if isinstance(hint, str) and len(hint.strip()) > _SUMMARY_HINT_CHARS:
182
257
  summary["llmHintTruncated"] = True
@@ -184,30 +259,30 @@ def _summarize(tpl: dict) -> dict:
184
259
 
185
260
 
186
261
  def _open_in_browser(url: str) -> None:
187
- """Best-effort browser open after the URL has already been printed.
188
262
 
189
- Only when stdout is a TTY (a human at a terminal) and REMIXMATE_NO_BROWSER
190
- is unset: agent hosts run commands through pipes / sandboxes, where the
191
- printed URL is what they relay to the user. Failures are swallowed.
192
- """
263
+
264
+
265
+
266
+
267
+
193
268
  if not sys.stdout.isatty() or os.environ.get("REMIXMATE_NO_BROWSER", "").strip():
194
269
  return
195
270
  try:
196
271
  import webbrowser
197
272
 
198
273
  webbrowser.open(url, new=2)
199
- except Exception: # noqa: BLE001
274
+ except Exception:
200
275
  pass
201
276
 
202
277
 
203
278
  def _page_url(template_id: object, page_urls: dict[str, str], groups: dict | None) -> str | None:
204
- """Web detail page link for one template.
205
279
 
206
- The registry's document-level ``pageUrls`` comes first: it rides on the call
207
- every list makes anyway, so the link survives the samples endpoint being
208
- down. The samples group's ``pageUrl`` is the fallback (same URL, built by
209
- the same backend helper).
210
- """
280
+
281
+
282
+
283
+
284
+
285
+
211
286
  if not isinstance(template_id, str):
212
287
  return None
213
288
  if page_urls.get(template_id):
@@ -219,9 +294,9 @@ def _page_url(template_id: object, page_urls: dict[str, str], groups: dict | Non
219
294
 
220
295
  def _report_unknown_ids(missing: list[str], templates: list[dict]) -> None:
221
296
  for tid in missing:
222
- # Distinguish "gated out by status" from "does not exist" —
223
- # otherwise a beta template reads as a typo and the caller
224
- # retries the id instead of passing --include-beta.
297
+
298
+
299
+
225
300
  gated = get_template(tid, include_all_statuses=True)
226
301
  if gated is not None:
227
302
  print(
@@ -243,7 +318,7 @@ def _detail_hint(template_id: str = "<templateId>") -> str:
243
318
  )
244
319
 
245
320
 
246
- # ----- Slide menu (--template-id without --slide-id) -------------------------
321
+
247
322
 
248
323
  _SLIDE_EXPAND_ALL = "all"
249
324
 
@@ -256,7 +331,7 @@ def _slide_schema_ids(tpl: dict) -> list[str]:
256
331
 
257
332
 
258
333
  def _menu_entry(schema: dict, line_sync_key: str | None) -> dict:
259
- """One layout's menu row: enough to pick it and draft a script, not to fill it."""
334
+
260
335
  props = schema.get("properties") if isinstance(schema.get("properties"), dict) else {}
261
336
  entry: dict = {}
262
337
  if schema.get("description"):
@@ -264,7 +339,7 @@ def _menu_entry(schema: dict, line_sync_key: str | None) -> dict:
264
339
  entry["fields"] = list(props.keys())
265
340
  if schema.get("required"):
266
341
  entry["required"] = schema["required"]
267
- # The card list: the line-sync key when declared, else the first array of objects.
342
+
268
343
  list_key = line_sync_key if line_sync_key in props else None
269
344
  if list_key is None:
270
345
  for k, v in props.items():
@@ -283,8 +358,8 @@ def _menu_entry(schema: dict, line_sync_key: str | None) -> dict:
283
358
  lst["maxItems"] = arr["maxItems"]
284
359
  entry["list"] = lst
285
360
  if line_sync_key:
286
- # The single most expensive miss in practice: authoring one narration
287
- # block for a layout that lights cards one by one (validator rejects it).
361
+
362
+
288
363
  entry["narration"] = (
289
364
  f"items — one audio.narration.items line per templateData.{line_sync_key} entry, no text"
290
365
  )
@@ -295,7 +370,7 @@ def _menu_entry(schema: dict, line_sync_key: str | None) -> dict:
295
370
 
296
371
 
297
372
  def _apply_slide_view(tpl: dict, slide_ids: list[str] | None) -> dict:
298
- """Return tpl with slideSchemas reduced to a menu, or to the requested layouts."""
373
+
299
374
  ss = tpl.get("slideSchemas")
300
375
  if not isinstance(ss, dict):
301
376
  return tpl
@@ -333,20 +408,20 @@ def _apply_slide_view(tpl: dict, slide_ids: list[str] | None) -> dict:
333
408
  return out
334
409
 
335
410
 
336
- # ----- Examples discovery (--list-examples) ---------------------------------
337
- #
338
- # Examples live under <SkillDir>/video_dsl/schema/examples/ as `*.dsl.json` and
339
- # `*.binding.json` files. Naming convention (per gen-script SKILL.md) is
340
- # `<templateId>.dsl.json` / `<templateId>.binding.json`, occasionally with
341
- # variant suffixes (`<templateId>.<variant>.dsl.json`).
342
- #
343
- # We do not parse the JSON content here (downstream agents read the raw files
344
- # they care about); we just enumerate paths so the agent doesn't have to guess
345
- # the directory layout. When the directory does not exist (e.g. inside a slim
346
- # package distribution), we return an empty list — non-fatal.
347
-
348
- # Resolve the examples directory relative to this script.
349
- # <skill>/scripts/list_templates.py → <skill>/video_dsl/schema/examples/
411
+
412
+
413
+
414
+
415
+
416
+
417
+
418
+
419
+
420
+
421
+
422
+
423
+
424
+
350
425
  _EXAMPLES_DIR = Path(__file__).resolve().parent.parent / "video_dsl" / "schema" / "examples"
351
426
 
352
427
 
@@ -358,7 +433,7 @@ def _collect_examples() -> list[dict]:
358
433
  if not entry.is_file():
359
434
  continue
360
435
  name = entry.name
361
- # Recognise *.dsl.json / *.binding.json
436
+
362
437
  if name.endswith(".dsl.json"):
363
438
  kind = "dsl"
364
439
  base = name[: -len(".dsl.json")]
@@ -367,7 +442,7 @@ def _collect_examples() -> list[dict]:
367
442
  base = name[: -len(".binding.json")]
368
443
  else:
369
444
  continue
370
- # Variant: 'foo.bar.dsl.json' → templateId='foo', variant='bar'
445
+
371
446
  if "." in base:
372
447
  template_id, variant = base.split(".", 1)
373
448
  else:
@@ -376,7 +451,7 @@ def _collect_examples() -> list[dict]:
376
451
  bucket["files"].append({
377
452
  "kind": kind,
378
453
  "variant": variant,
379
- "path": str(entry.relative_to(_EXAMPLES_DIR.parent.parent.parent)), # relative to skill dir
454
+ "path": str(entry.relative_to(_EXAMPLES_DIR.parent.parent.parent)),
380
455
  "absolutePath": str(entry),
381
456
  "sizeBytes": entry.stat().st_size,
382
457
  })
@@ -402,6 +477,14 @@ def main() -> None:
402
477
  ap.add_argument("--filter-tag", help="Keep templates whose styleTags match this substring.")
403
478
  ap.add_argument("--filter-aspect", help="Keep templates supporting this aspect ratio (e.g. 9:16).")
404
479
  ap.add_argument("--filter-language", help="Keep templates tagged with this content language (zh|en|...).")
480
+ ap.add_argument("--user-media", choices=("yes", "no"),
481
+ help="Shortlist: yes = the user handed over their own images/videos (drop templates "
482
+ "with nowhere to put them); no = they did not (drop templates that need them).")
483
+ ap.add_argument("--source-doc", metavar="KIND",
484
+ help="Shortlist: the user gave a source document of this kind (e.g. pptx) — keep only "
485
+ "templates that build scenes from one; 'none' drops those templates.")
486
+ ap.add_argument("--narration", choices=("yes", "no"),
487
+ help="Shortlist: yes = the video needs voice-over; no = the user wants none.")
405
488
  ap.add_argument("--include-beta", action="store_true", help="Also show beta templates (default: stable only).")
406
489
  ap.add_argument("--json-output", action="store_true", help='Emit JSON ({"templates":[...]} or {"examples":[...]}) instead of a table.')
407
490
  ap.add_argument("--open", dest="open_id", metavar="ID",
@@ -412,28 +495,28 @@ def main() -> None:
412
495
  if args.open_id and (args.template_ids or args.list_examples or args.list_templates):
413
496
  ap.error("--open cannot be combined with --template-id / --list-examples / --list-templates")
414
497
 
415
- # --include-beta flips registry_loader's documented status gate via env, so
416
- # the CLI and in-process callers share one gating implementation.
498
+
499
+
417
500
  if args.include_beta:
418
501
  os.environ["ENABLE_BETA_TEMPLATES"] = "1"
419
502
 
420
- # ----- examples mode -----------------------------------------------------
503
+
421
504
  if args.list_examples:
422
505
  examples = _collect_examples()
423
506
  if args.json_output:
424
507
  print(json.dumps({"examples": examples}, ensure_ascii=False))
425
508
  return
426
509
  if not examples:
427
- # 这**不是**异常状态,而是常态:该目录从未进过仓库,也就从未随包发布过。
428
- #
429
- # 旧文案("This package may have shipped without reference examples;
430
- # check the cli source repo or upgrade @remixmate/cli")把常态说成了事故,
431
- # 而 gen-script 的 SKILL.md 又把「先读这里的参考 DSL」列为强制第一步 ——
432
- # 于是线上 agent 每次都在这里撞空,然后退回 gen_script 的骨架照抄,连模板
433
- # 自己声明的版式都不去看。升级 CLI 修不了它,因为没有哪一版带过这些文件。
434
- #
435
- # 真正随包到达调用方的逐模板契约是 registry 里的 template.json,所以这里
436
- # 直接把人指过去,而不是让它以为"该模板不受支持"。
510
+
511
+
512
+
513
+
514
+
515
+
516
+
517
+
518
+
519
+
437
520
  print(
438
521
  f"ℹ️ No local reference DSLs under {_EXAMPLES_DIR} — that directory is "
439
522
  "optional and is not part of the published package.\n"
@@ -465,12 +548,12 @@ def main() -> None:
465
548
  try:
466
549
  templates = load_visible_templates()
467
550
  except RegistryAuthError as exc:
468
- # 退出码 4 = 需要授权(见 registry_loader.EXIT_NOT_AUTHENTICATED),宿主
469
- # 据此引导用户重新登录,而不是把它当成一次普通失败。
551
+
552
+
470
553
  print(f"❌ {exc}", file=sys.stderr)
471
554
  sys.exit(EXIT_NOT_AUTHENTICATED)
472
555
  except RegistryUnreachableError as exc:
473
- # 退出码 5 = 后端不可达;重试或修配置即可,不需要重新登录。
556
+
474
557
  print(f"❌ {exc}", file=sys.stderr)
475
558
  sys.exit(EXIT_BACKEND_UNREACHABLE)
476
559
  except RuntimeError as exc:
@@ -480,10 +563,10 @@ def main() -> None:
480
563
  by_id = {t.get("templateId"): t for t in templates}
481
564
  page_urls = registry_page_urls()
482
565
 
483
- # ----- open mode (--open) -------------------------------------------------
484
- #
485
- # The URL goes to stdout first, unconditionally: in Codex / Claude Code the
486
- # command runs through a pipe and the printed link is what the agent relays.
566
+
567
+
568
+
569
+
487
570
  if args.open_id:
488
571
  if args.open_id not in by_id:
489
572
  _report_unknown_ids([args.open_id], templates)
@@ -502,11 +585,11 @@ def main() -> None:
502
585
  _open_in_browser(page_url)
503
586
  return
504
587
 
505
- # ----- detail mode (--template-id) ---------------------------------------
506
- #
507
- # Explicit ids win over --filter-*: the caller already chose, and silently
508
- # returning nothing because a stale filter excluded the pick would read as
509
- # "template does not exist".
588
+
589
+
590
+
591
+
592
+
510
593
  if args.template_ids:
511
594
  picked: list[dict] = []
512
595
  missing: list[str] = []
@@ -536,13 +619,13 @@ def main() -> None:
536
619
  or s == _SLIDE_EXPAND_ALL] or None)
537
620
  for t in picked
538
621
  ]
539
- # Always compact, with or without --json-output. Every caller is an LLM
540
- # tool call: indent=2 nearly doubled the size (html-slide 37 K → 65 K
541
- # chars), which alone pushed the plain call past the tool-result cap and
542
- # got it spilled to a file the agent cannot read. Humans can `| jq`.
622
+
623
+
624
+
625
+
543
626
  detail: dict = {"templates": picked}
544
- # Samples and links sit beside the definitions, never inside them: the
545
- # definition is the template's authoring contract, these are web content.
627
+
628
+
546
629
  groups = load_showcases([str(t.get("templateId")) for t in picked], _DETAIL_SHOWCASES)
547
630
  links = {
548
631
  str(t.get("templateId")): url
@@ -558,10 +641,26 @@ def main() -> None:
558
641
  print(json.dumps(detail, ensure_ascii=False))
559
642
  return
560
643
 
561
- visible = [
562
- t for t in templates
563
- if _matches(t, args.filter_tag, args.filter_aspect, args.filter_language)
564
- ]
644
+ constraints = {
645
+ "tag": args.filter_tag,
646
+ "aspect": args.filter_aspect,
647
+ "language": args.filter_language,
648
+ "user_media": args.user_media,
649
+ "source_doc": args.source_doc,
650
+ "narration": args.narration,
651
+ }
652
+ filtering = any(v for v in constraints.values())
653
+ visible: list[dict] = []
654
+
655
+
656
+
657
+ excluded: list[dict] = []
658
+ for t in templates:
659
+ reasons = _exclusion_reasons(t, **constraints)
660
+ if reasons:
661
+ excluded.append({"templateId": t.get("templateId"), "name": t.get("name"), "reasons": reasons})
662
+ else:
663
+ visible.append(t)
565
664
 
566
665
  if args.json_output:
567
666
  if args.full:
@@ -584,25 +683,26 @@ def main() -> None:
584
683
  s = _summarize(t)
585
684
  if url := _page_url(t.get("templateId"), page_urls, groups):
586
685
  s["pageUrl"] = url
587
- # Absent (not null) when the endpoint is unavailable, so a consumer
588
- # never mistakes "unknown" for "zero samples".
686
+
687
+
589
688
  if groups is not None and t.get("templateId") in groups:
590
689
  s["showcases"] = summarize_group(groups[t["templateId"]], _LIST_SHOWCASES)
591
690
  summaries.append(s)
592
- print(json.dumps(
593
- {
594
- "templates": summaries,
595
- "detailHint": _detail_hint(),
596
- },
597
- ensure_ascii=False,
598
- ))
691
+ listing: dict = {
692
+ "templates": summaries,
693
+ "detailHint": _detail_hint(),
694
+ }
695
+ if filtering:
696
+ listing["excluded"] = excluded
697
+ print(json.dumps(listing, ensure_ascii=False))
599
698
  return
600
699
 
601
700
  if not visible:
602
701
  print(
603
702
  "⚠️ No templates match the current filters "
604
- "(check --include-beta or relax --filter-*).",
703
+ "(check --include-beta or relax --filter-* / --user-media / --source-doc / --narration).",
605
704
  )
705
+ _print_excluded(excluded)
606
706
  return
607
707
 
608
708
  groups = load_showcases([str(t.get("templateId")) for t in visible], _LIST_SHOWCASES)
@@ -627,8 +727,11 @@ def main() -> None:
627
727
  hint = _truncate(tpl.get("llmHint"), _SUMMARY_HINT_CHARS)
628
728
  if hint:
629
729
  print(f"{' ' * 22} ↳ {hint}")
630
- # Two lines, link on its own: an agent summarizing the table keeps a
631
- # labelled `detail:` line, a URL trailing a count reads as an aside.
730
+ not_for = tpl.get("notFor")
731
+ if isinstance(not_for, str) and not_for.strip():
732
+ print(f"{' ' * 22} ↳ not for: {not_for.strip()}")
733
+
734
+
632
735
  group = (groups or {}).get(tpl.get("templateId"))
633
736
  if group is not None:
634
737
  print(f"{' ' * 22} ↳ samples: {int(group.get('showcaseCount') or 0)}")
@@ -644,6 +747,15 @@ def main() -> None:
644
747
  )
645
748
  if any_link:
646
749
  print(f"Watch samples / layouts in the browser: open a detail link, or --open {first_id}")
750
+ _print_excluded(excluded)
751
+
752
+
753
+ def _print_excluded(excluded: list[dict]) -> None:
754
+ if not excluded:
755
+ return
756
+ print(f"\nExcluded by the current filters ({len(excluded)}):")
757
+ for item in excluded:
758
+ print(f" {item.get('templateId')}: {'; '.join(item.get('reasons') or [])}")
647
759
 
648
760
 
649
761
  if __name__ == "__main__":