@remixmate/cli 0.9.29 → 0.9.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/manifest.json +16 -6
- package/package.json +1 -1
- package/skills/gen-script/SKILL.md +33 -19
- package/skills/gen-script/scripts/gen_script.py +66 -36
- package/skills/template-registry/SKILL.md +18 -4
- package/skills/template-registry/scripts/list_templates.py +181 -8
- package/skills/template-registry/skill.json +6 -4
- package/skills/template-registry/video_dsl/runtime/dsl_validator.py +63 -0
package/dist/manifest.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"version": "0.9.
|
|
4
|
-
"generatedAt": "2026-09-
|
|
3
|
+
"version": "0.9.31",
|
|
4
|
+
"generatedAt": "2026-09-14T15:38:04.716Z",
|
|
5
5
|
"skills": [
|
|
6
6
|
{
|
|
7
7
|
"id": "export-jianying",
|
|
@@ -833,7 +833,7 @@
|
|
|
833
833
|
"tier": "orchestration",
|
|
834
834
|
"category": "authoring",
|
|
835
835
|
"title": "Video Template Registry",
|
|
836
|
-
"summary": "List all available video templates (templateId / name / aspect ratio / style tags). Template-to-DSL binding is no longer exposed as a separate step — once prepare_video_assets receives a template_id it runs the full DSL→RenderPlan pipeline internally.",
|
|
836
|
+
"summary": "List all available video templates (templateId / name / aspect ratio / style tags), or read one template's full definition with template_id. Listing returns summaries; pass template_id to get that template's authoring contract (llmHint / customPayloadSchema / slotMapping / variants). Template-to-DSL binding is no longer exposed as a separate step — once prepare_video_assets receives a template_id it runs the full DSL→RenderPlan pipeline internally.",
|
|
837
837
|
"triggers": [
|
|
838
838
|
"View available templates / list every template"
|
|
839
839
|
],
|
|
@@ -853,11 +853,19 @@
|
|
|
853
853
|
"properties": {
|
|
854
854
|
"list_templates": {
|
|
855
855
|
"type": "boolean",
|
|
856
|
-
"description": "List available templates (default behavior; also implied when other flags are passed)."
|
|
856
|
+
"description": "List available templates as summaries — templateId / name / description / aspect ratios / language / status / styleTags / variantIds / key capabilities / a truncated llmHint (default behavior; also implied when other flags are passed)."
|
|
857
|
+
},
|
|
858
|
+
"template_id": {
|
|
859
|
+
"type": "string",
|
|
860
|
+
"description": "Return this template's FULL definition instead of the summary list: llmHint in full, customPayloadSchema (every template-specific field incl. the legal slideId values), slotMapping, compositions, variants. This is the per-template authoring contract — read it before writing DSL for that template."
|
|
861
|
+
},
|
|
862
|
+
"full": {
|
|
863
|
+
"type": "boolean",
|
|
864
|
+
"description": "List mode only: emit full definitions for every listed template instead of summaries. Rejected when the result would be too large for one tool result — prefer template_id, or narrow with the filter_* parameters."
|
|
857
865
|
},
|
|
858
866
|
"list_examples": {
|
|
859
867
|
"type": "boolean",
|
|
860
|
-
"description": "List the *.dsl.json / *.binding.json reference examples
|
|
868
|
+
"description": "List the *.dsl.json / *.binding.json reference examples under template-registry/video_dsl/schema/examples/, grouped by templateId. That directory is optional and is not part of the published package, so this is normally empty — to read a template's reference shape, use template_id instead (the registry's own contract)."
|
|
861
869
|
},
|
|
862
870
|
"filter_tag": {
|
|
863
871
|
"type": "string",
|
|
@@ -877,7 +885,7 @@
|
|
|
877
885
|
},
|
|
878
886
|
"json_output": {
|
|
879
887
|
"type": "boolean",
|
|
880
|
-
"description": "Emit a JSON result
|
|
888
|
+
"description": "Emit a JSON result instead of the human-readable table: { templates: [...] } — summaries, or full definitions when template_id / full is set — or { examples: [...] }."
|
|
881
889
|
}
|
|
882
890
|
},
|
|
883
891
|
"required": []
|
|
@@ -886,6 +894,8 @@
|
|
|
886
894
|
"primary": [],
|
|
887
895
|
"advanced": [
|
|
888
896
|
"list_templates",
|
|
897
|
+
"template_id",
|
|
898
|
+
"full",
|
|
889
899
|
"list_examples",
|
|
890
900
|
"filter_tag",
|
|
891
901
|
"filter_aspect",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remixmate/cli",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.31",
|
|
4
4
|
"description": "AI media generation skills for Claude Code / Codex — 12 skills covering image, video, voice, digital human, web screenshot, web recording, script, template registry, rendering, Jianying export, and video deconstruction.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -37,26 +37,37 @@ Turns a user-supplied **topic** into a **Video DSL v1alpha1** JSON describing wh
|
|
|
37
37
|
|
|
38
38
|
## DSL schema
|
|
39
39
|
|
|
40
|
-
The full schema is `template-registry` skill's `video_dsl/schema/video-dsl-v1alpha1.json
|
|
40
|
+
The full schema is `template-registry` skill's `video_dsl/schema/video-dsl-v1alpha1.json`.
|
|
41
|
+
|
|
42
|
+
**Per-template reference DSLs do not ship with this package.** `video_dsl/schema/examples/` is a local-only directory: it is absent from the published npm package (and has never existed in the repo), so `template_registry list_examples=true` normally returns nothing. Do not read "no examples found" as "this template is unsupported" — the authoritative per-template contract ships inside the registry's `template.json`, see below.
|
|
41
43
|
|
|
42
44
|
## Agent behavior: DSL generation when a template is selected
|
|
43
45
|
|
|
44
|
-
**When the user explicitly specifies a template id, the agent must first read
|
|
46
|
+
**When the user explicitly specifies a template id, the agent must first read that template's full definition from the registry, then generate the DSL in the template's native shape. The agent must not generate the DSL from scratch ignoring the template definition, and must not produce a generic DSL first and rely on `template-registry` to force-match the template later.**
|
|
45
47
|
|
|
46
|
-
The intent is to
|
|
48
|
+
The intent is to avoid the failure mode where "the generic DSL looks compatible on the surface, but is missing template-specific fields, has the wrong nesting, or binds incorrectly — only to fail later at binding or render time". Examples:
|
|
47
49
|
|
|
48
|
-
- A template may require fields the generic DSL never emits (a slide id, a word list, an avatar assetRef)
|
|
49
|
-
-
|
|
50
|
+
- A template may require fields the generic DSL never emits (a slide id, a word list, an avatar assetRef). `customPayloadSchema` is what declares them.
|
|
51
|
+
- Templates differ in `slotMapping` choices, asset-binding styles, and scene organization.
|
|
50
52
|
|
|
51
53
|
### Mandatory steps
|
|
52
54
|
|
|
53
|
-
1.
|
|
54
|
-
2. Read
|
|
55
|
-
3. Read
|
|
56
|
-
4. Combine
|
|
57
|
-
5. Generate the DSL
|
|
58
|
-
6. Make sure the DSL explicitly contains every template-specific field, e.g. `templateData.words`, `
|
|
59
|
-
7.
|
|
55
|
+
1. Read the requested template's full definition from the registry — `template_registry` with `template_id=<the id>` and `json_output=true` emits that one template whole. This is the shipping source of truth, and unlike the summary list it truncates nothing. Do **not** ask for every template's full definition (`list_templates=true` + `full=true`): the whole registry is far more JSON than one tool result can carry, and the call fails instead of returning the contract.
|
|
56
|
+
2. Read its `llmHint` end to end — that is where the template states how its on-screen text and its layouts must be authored.
|
|
57
|
+
3. Read its `customPayloadSchema`: every template-specific field lives there, including the enum of legal `customPayload.slideId` values for multi-layout templates.
|
|
58
|
+
4. Combine that with the template's `slotMapping`, `requiredProps`, `optionalProps`, `propExtractors`, `assetRequirements`, `supportedAspectRatios`, `supportedDurations`, `constraints`, and `scenePatterns`.
|
|
59
|
+
5. Generate the DSL in the template's native shape — not the generic DSL shape.
|
|
60
|
+
6. Make sure the DSL explicitly contains every template-specific field, e.g. `templateData.words`, `customPayload.slideId`, `visuals.avatar.assetRef`.
|
|
61
|
+
7. If `template_registry list_examples=true` does return files locally, read them as an extra sample — never as a substitute for step 1.
|
|
62
|
+
8. After generation, run the schema check and show the script summary to the user for confirmation.
|
|
63
|
+
|
|
64
|
+
### Multi-layout templates: pick a layout per scene
|
|
65
|
+
|
|
66
|
+
When a template's `customPayloadSchema.slideId.enum` holds more than one value (today `html-slide` and `html-slide-blackboard`, 16 layouts each), **every scene must name one**, chosen to fit that scene's information shape — a comparison, a timeline, a code walkthrough and a set of numbers are four different layouts.
|
|
67
|
+
|
|
68
|
+
`gen_script.py` cannot make that choice: it emits the placeholder `"slideId": "__CHOOSE_SLIDE__"`, and the DSL validator rejects any DSL that still carries it, listing the legal values in the error. Replace every placeholder, and do not reuse one layout for the whole video.
|
|
69
|
+
|
|
70
|
+
A missing `slideId` is the quietest failure in the pipeline: the renderer drops the entire `templateData` and draws a single centred title — no error, no log, exit code 0.
|
|
60
71
|
|
|
61
72
|
### On-screen text: the rules live in the template, not here
|
|
62
73
|
|
|
@@ -69,23 +80,26 @@ Two consequences for the agent:
|
|
|
69
80
|
|
|
70
81
|
### Hard constraints
|
|
71
82
|
|
|
72
|
-
- **Forbidden**: the user specified a template, but the agent generated the DSL without reading
|
|
73
|
-
- **Forbidden**:
|
|
83
|
+
- **Forbidden**: the user specified a template, but the agent generated the DSL without reading that template's full `template.json` (`llmHint` + `customPayloadSchema` included).
|
|
84
|
+
- **Forbidden**: leaving any `"__CHOOSE_SLIDE__"` placeholder in the DSL, or shipping scenes with no `customPayload.slideId` on a multi-layout template.
|
|
85
|
+
- **Forbidden**: reusing a single `slideId` across the whole video on a multi-layout template because it was the first one in the enum.
|
|
74
86
|
- **Forbidden**: the user specified a template, but the agent first generated a generic DSL and then passed `--template-id` to `template-registry` to force-bind it.
|
|
75
87
|
- **Forbidden**: continuing into binding / rendering despite knowing that template-specific fields, scene shapes, or binding details are missing.
|
|
76
88
|
- **Forbidden**: silently degrading to a generic DSL because the current script cannot support a template, leaving the failure to the downstream stage.
|
|
77
89
|
|
|
78
90
|
### When a template is not yet supported
|
|
79
91
|
|
|
80
|
-
|
|
92
|
+
"No examples found" is **not** this case — see the note under *DSL schema*; examples do not ship, and `template.json` is the contract.
|
|
93
|
+
|
|
94
|
+
This case is: the template's `customPayloadSchema` / `slotMapping` demands a shape `gen_script.py` cannot build and the agent cannot hand-assemble. Then say clearly that the template is not yet supported for auto-generation, and name the fields, structure or binding info that are missing. Possible next steps:
|
|
81
95
|
|
|
82
|
-
1. Ask the user to switch to a template
|
|
83
|
-
2.
|
|
96
|
+
1. Ask the user to switch to a template whose shape is already supported.
|
|
97
|
+
2. Hand-craft the required DSL structure from `customPayloadSchema` and `slotMapping`, then show it to the user for confirmation.
|
|
84
98
|
3. Stop the flow and wait for the user to decide, rather than emitting a DSL that "looks like it matches but cannot render".
|
|
85
99
|
|
|
86
100
|
### Design principle
|
|
87
101
|
|
|
88
|
-
When the user specifies a template, `gen-script`'s goal is no longer "produce a generally-compatible DSL" but "produce a template-native DSL
|
|
102
|
+
When the user specifies a template, `gen-script`'s goal is no longer "produce a generally-compatible DSL" but "produce a template-native DSL that satisfies that template's own declared contract".
|
|
89
103
|
|
|
90
104
|
## Authentication & environment
|
|
91
105
|
|
|
@@ -462,4 +476,4 @@ prefix and version — not a short alias:
|
|
|
462
476
|
|------|---------|
|
|
463
477
|
| `gen_script.py` | Core script — produces the Video DSL JSON from a topic. |
|
|
464
478
|
|
|
465
|
-
The authoritative
|
|
479
|
+
The authoritative per-template contract is the registry's `template.json` (`llmHint` + `customPayloadSchema` + `slotMapping`). `template_registry list_examples=true` lists local reference DSLs when any exist, but that directory does not ship — see *DSL schema* above.
|
|
@@ -196,12 +196,11 @@ def _localize_label(language, key, **fmt):
|
|
|
196
196
|
"zh": "[骨架待填充] 关于{topic}的内容({duration}s)",
|
|
197
197
|
"en": "[skeleton placeholder] Content about {topic} ({duration}s)",
|
|
198
198
|
},
|
|
199
|
-
"point_description"
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
199
|
+
# NOTE: 这里曾有 "point_description"("关于{topic}的要点内容")与
|
|
200
|
+
# "more_about"("更多关于{topic}的内容")两条,用来给 no-visual 场景拼画面
|
|
201
|
+
# 文案。两条都已删除:topic 长度不受控,拼进画面必然溢出,而正确的文案来源
|
|
202
|
+
# 是调用方给的 --headline / --subheadline。
|
|
203
203
|
"follow_us": {"zh": "关注我们", "en": "Follow us"},
|
|
204
|
-
"more_about": {"zh": "更多关于{topic}的内容", "en": "More about {topic}"},
|
|
205
204
|
"video_description": {
|
|
206
205
|
"zh": "关于「{topic}」的{duration}秒短视频",
|
|
207
206
|
"en": "A {duration}s short video about \"{topic}\"",
|
|
@@ -938,33 +937,52 @@ def _build_carousel_caption_dsl(
|
|
|
938
937
|
return dsl
|
|
939
938
|
|
|
940
939
|
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
940
|
+
# 「作者还没选版式」的显式标记。
|
|
941
|
+
#
|
|
942
|
+
# 为什么是哨兵而不是留空:slideId 缺失会静默回落到 DefaultSlide —— 渲染成功、零日志、
|
|
943
|
+
# 退出码 0,画面只剩一行居中标题,正是要消灭的失效形态。哨兵让下游能区分「作者没选」
|
|
944
|
+
# 与「作者选了但拼错」,两者的修法不同。dsl_validator 见到它会硬拒绝(同名常量在
|
|
945
|
+
# video_dsl/runtime/dsl_validator.py,两处字面量必须一致)。
|
|
946
|
+
SLIDE_ID_SENTINEL = "__CHOOSE_SLIDE__"
|
|
947
|
+
|
|
948
|
+
|
|
949
|
+
def _is_multi_slide_template(template_config: dict | None) -> bool:
|
|
950
|
+
"""模板是否注册了多个可选版式 —— 由模板自己声明,不看模板 id。
|
|
951
|
+
|
|
952
|
+
判据是 ``customPayloadSchema.slideId.enum`` 有没有超过一个取值:有得挑才谈得上
|
|
953
|
+
「该挑哪个」。dsl_validator 的 ``_check_slide_id_chosen`` 用的是同一条判据,两侧
|
|
954
|
+
因此不会对同一个模板给出相反的结论。
|
|
955
|
+
|
|
956
|
+
**刻意不写死模板 id**:哪个模板有几种版式是模板自己的事实,真源在它的
|
|
957
|
+
``template.json``;把名单抄进 CLI,模板改名 / 新增多版式模板都要跟着发一次 npm,
|
|
958
|
+
而漏发的表现是"这个模板又开始只出一种版式了"——正是本次要修的那个形态。
|
|
959
|
+
"""
|
|
960
|
+
schema = (template_config or {}).get("customPayloadSchema")
|
|
961
|
+
if not isinstance(schema, dict):
|
|
962
|
+
return False
|
|
963
|
+
slide_schema = schema.get("slideId")
|
|
964
|
+
enum = slide_schema.get("enum") if isinstance(slide_schema, dict) else None
|
|
965
|
+
return isinstance(enum, list) and len(enum) > 1
|
|
966
|
+
|
|
967
|
+
|
|
968
|
+
def _build_custom_payload(template_config: dict | None) -> dict:
|
|
969
|
+
"""骨架阶段的 customPayload —— **不猜版式、不拼画面文案**。
|
|
970
|
+
|
|
971
|
+
这个函数曾经按 purpose 写死两种 slideId(opening / cta → demo-concept-overview,
|
|
972
|
+
其余 → demo-single-concept),于是 16 种注册版式里只有 2 种会被用到,且与内容
|
|
973
|
+
形态完全无关 —— 线上「不论选题版式永远一样」就是这么来的。它还往
|
|
974
|
+
templateData.description 里拼 "关于{topic}的要点内容",topic 长度不受控,画面
|
|
975
|
+
溢出是必然的。
|
|
976
|
+
|
|
977
|
+
现在分两档:
|
|
978
|
+
|
|
979
|
+
- 多版式模板 → 只放哨兵,等 agent 按内容挑版式并填 templateData;
|
|
980
|
+
- 其余 no-visual 模板 → 空载荷。这些模板只有一种版式,没有可挑的东西,而注入
|
|
981
|
+
它们 schema 不认识的字段只会把真正要的 slide / statement / item 挤掉。
|
|
982
|
+
"""
|
|
983
|
+
if _is_multi_slide_template(template_config):
|
|
984
|
+
return {"slideId": SLIDE_ID_SENTINEL, "templateData": {}}
|
|
985
|
+
return {}
|
|
968
986
|
|
|
969
987
|
|
|
970
988
|
def build_dsl(
|
|
@@ -1346,11 +1364,19 @@ def build_dsl(
|
|
|
1346
1364
|
# (已核对),渲染走的是 slotMapping / compositionId。所以换值不改成片。
|
|
1347
1365
|
_layout_cfg = (template_config or {}).get("capabilities") or {}
|
|
1348
1366
|
layout = _layout_cfg.get("defaultLayout") or template_id or "text-overlay"
|
|
1367
|
+
# ⚠️ 画面文字用 effective_headline / effective_subheadline,与上面的
|
|
1368
|
+
# has_visual 分支同源。这两条分支长期分叉:visual 分支一直用的是
|
|
1369
|
+
# --headline,no-visual 分支却渲染裸 topic —— SKILL.md 写着"用户给了标题
|
|
1370
|
+
# 就传 --headline,否则 headline 会回落成长 topic 并撑破顶部文字层",
|
|
1371
|
+
# agent 照做了,这条分支没兑现。调用方把整篇提纲当 topic 传进来时,成片
|
|
1372
|
+
# 必然是一屏文字墙,而 meta.headline 里存的又是对的,只看 meta 查不出来。
|
|
1349
1373
|
text_layers = []
|
|
1350
1374
|
if plan["purpose"] == "opening":
|
|
1351
1375
|
text_layers = [
|
|
1352
|
-
{"role": "headline", "content":
|
|
1376
|
+
{"role": "headline", "content": effective_headline, "animation": "fade-in"},
|
|
1353
1377
|
]
|
|
1378
|
+
if effective_subheadline:
|
|
1379
|
+
text_layers.append({"role": "subheadline", "content": effective_subheadline, "animation": "fade-in"})
|
|
1354
1380
|
elif plan["purpose"] == "point":
|
|
1355
1381
|
text_layers = [
|
|
1356
1382
|
{"role": "headline", "content": plan["label"], "animation": "slide-up"},
|
|
@@ -1358,10 +1384,11 @@ def build_dsl(
|
|
|
1358
1384
|
elif plan["purpose"] == "cta":
|
|
1359
1385
|
text_layers = [
|
|
1360
1386
|
{"role": "headline", "content": _localize_label(output_language, "follow_us"), "animation": "zoom-in"},
|
|
1361
|
-
{"role": "subheadline", "content": _localize_label(output_language, "more_about", topic=topic), "animation": "fade-in"},
|
|
1362
1387
|
]
|
|
1388
|
+
if effective_subheadline:
|
|
1389
|
+
text_layers.append({"role": "subheadline", "content": effective_subheadline, "animation": "fade-in"})
|
|
1363
1390
|
|
|
1364
|
-
custom_payload = _build_custom_payload(
|
|
1391
|
+
custom_payload = _build_custom_payload(template_config)
|
|
1365
1392
|
|
|
1366
1393
|
scene = {
|
|
1367
1394
|
"id": scene_id,
|
|
@@ -1369,8 +1396,11 @@ def build_dsl(
|
|
|
1369
1396
|
"duration": plan["duration"],
|
|
1370
1397
|
"layout": layout,
|
|
1371
1398
|
"textLayers": text_layers,
|
|
1372
|
-
"customPayload": custom_payload,
|
|
1373
1399
|
}
|
|
1400
|
+
# 单版式模板的载荷是空 dict —— 写一个空 customPayload 只会让 agent 以为
|
|
1401
|
+
# 这里有个需要填的结构,干脆不写这个键。
|
|
1402
|
+
if custom_payload:
|
|
1403
|
+
scene["customPayload"] = custom_payload
|
|
1374
1404
|
if needs_narration:
|
|
1375
1405
|
scene["audio"] = {
|
|
1376
1406
|
"narration": {
|
|
@@ -77,8 +77,9 @@ the registry is loaded **only** from the ab-api HTTP endpoint — explicit `VIDE
|
|
|
77
77
|
> The Python binding logic that maps DSL → TemplateBinding lives in `scripts/match_template.py` but is **not exposed as a CLI** — it is only consumed as a Python library by `render-video`'s `render_video.py` via `import match_template`.
|
|
78
78
|
|
|
79
79
|
1. **List available templates**: run `remixmate template-registry --list-templates` to view the templates in the registry along with their supported aspect ratios / style tags, and decide which `templateId` to pick.
|
|
80
|
-
2. **
|
|
81
|
-
3. **
|
|
80
|
+
2. **Read that template's contract**: run `remixmate template-registry --template-id <id> --json-output` for the full definition (llmHint, `customPayloadSchema` with the legal `slideId` values, `slotMapping`, `variants`). The list view only carries a 200-char llmHint preview, which is not enough to author against.
|
|
81
|
+
3. **Write the DSL**: when generating the Video DSL, put the chosen `templateId` into `meta.templateId` (the canonical location). Use `meta.templateVariant` / `renderHints.templateVariant` to explicitly select a variant. The legacy `renderHints.templatePreference[0]` is still tolerated by `match_template.py` and `dsl_validator._pick_template_id` during transition, but new authors should write `meta.templateId`.
|
|
82
|
+
4. **Produce the TemplateBinding**: there is no standalone CLI for DSL → TemplateBinding; `prepare_video_assets` calls `match_template.build_binding(template, dsl)` inline during the asset-resolution pipeline and embeds the binding into the RenderPlan it hands to the renderer — no separate `*.binding.json` file is written.
|
|
82
83
|
|
|
83
84
|
> Design trade-off: collapsing the binding step into the asset-prep pipeline (no CLI, no on-disk artifact) avoids binding files drifting between the agent, the database, and the file system; any hand-edited `.binding.json` would never be consumed by the renderer anyway. For local debugging you can still `import match_template.build_binding` from Python.
|
|
84
85
|
|
|
@@ -119,12 +120,25 @@ print(json.dumps(binding, ensure_ascii=False, indent=2))
|
|
|
119
120
|
|
|
120
121
|
| Flag | Description | Default |
|
|
121
122
|
|------|-------------|---------|
|
|
122
|
-
| `--list-templates` | List every available template (
|
|
123
|
+
| `--list-templates` | List every available template as a **summary** (templateId / name / description / aspect ratios / language / status / styleTags / variantIds / the `capabilities` keys that drive authoring — `payloadStyle` / `needsNarration` / `durationStrategy` / `narrationDriver` — and llmHint truncated to 200 chars). | — |
|
|
124
|
+
| `--template-id <id>` | Print that template's **full definition** — llmHint in full, `customPayloadSchema`, `slotMapping`, `compositions`, `variants`. Repeatable. | — |
|
|
125
|
+
| `--full` | List mode: emit full definitions instead of summaries. Refuses when the result would exceed 60 K characters. | off |
|
|
123
126
|
| `--filter-tag` | Keep only templates whose `styleTags` match this substring (case-insensitive). | none |
|
|
124
127
|
| `--filter-aspect` | Keep only templates declaring this aspect ratio (e.g. `9:16`). | none |
|
|
125
128
|
| `--filter-language` | Keep only templates whose `contentLanguage` includes this code (`zh`/`en`); language-agnostic templates always show. | none |
|
|
126
129
|
| `--include-beta` | Also show `status: beta` templates (same effect as `ENABLE_BETA_TEMPLATES=1`). | off |
|
|
127
|
-
| `--json-output` | Emit `{ "templates": [...] }` instead of the table
|
|
130
|
+
| `--json-output` | Emit `{ "templates": [...] }` instead of the table — summaries, or full definitions under `--template-id` / `--full`. | off |
|
|
131
|
+
|
|
132
|
+
### List vs. detail
|
|
133
|
+
|
|
134
|
+
A registry row is 4–17 KB of JSON, so dumping every full definition at once overflows an LLM tool result (the caller sees "exceeds maximum allowed tokens" instead of the contract it asked for). The list verb therefore returns summaries — enough to *choose* a template — and `--template-id` returns the one definition you need to *author* for it:
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
remixmate template-registry --list-templates # choose
|
|
138
|
+
remixmate template-registry --template-id html-slide --json-output # then read its contract
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
This mirrors the list/detail split described in `scripts/registry_loader.py` (P1.2), realized CLI-side so it holds even while the backend still serves one merged registry payload.
|
|
128
142
|
|
|
129
143
|
## Props extraction rules
|
|
130
144
|
|
|
@@ -13,8 +13,32 @@ TS↔Python duplication; this is now the only place the list verb is realized.
|
|
|
13
13
|
The DSL → TemplateBinding logic stays in ``match_template.py`` and is invoked
|
|
14
14
|
in-process by ``render-video``; it is intentionally NOT exposed as a CLI verb.
|
|
15
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
|
+
|
|
16
35
|
Flags:
|
|
17
|
-
--list-templates (accepted; listing is the
|
|
36
|
+
--list-templates (accepted; listing is the default verb)
|
|
37
|
+
--template-id <id> print the FULL definition of this template
|
|
38
|
+
(repeatable); this is the detail verb
|
|
39
|
+
--full in list mode, emit full definitions instead
|
|
40
|
+
of summaries (size-guarded; prefer
|
|
41
|
+
--template-id)
|
|
18
42
|
--list-examples list *.dsl.json / *.binding.json reference
|
|
19
43
|
examples grouped by templateId (consumed
|
|
20
44
|
by downstream agents that want to read a
|
|
@@ -44,9 +68,19 @@ from registry_loader import ( # noqa: E402
|
|
|
44
68
|
EXIT_NOT_AUTHENTICATED,
|
|
45
69
|
RegistryAuthError,
|
|
46
70
|
RegistryUnreachableError,
|
|
71
|
+
get_template,
|
|
47
72
|
list_templates as load_visible_templates,
|
|
48
73
|
)
|
|
49
74
|
|
|
75
|
+
# Summary llmHint budget: enough to tell templates apart when choosing, far
|
|
76
|
+
# short of the multi-KB authoring contract (read that with --template-id).
|
|
77
|
+
_SUMMARY_HINT_CHARS = 200
|
|
78
|
+
|
|
79
|
+
# Upper bound for a multi-template full dump (--full). ~60 K chars ≈ 20 K
|
|
80
|
+
# tokens: still large, but inside a tool-result budget, and the message it
|
|
81
|
+
# fails with names the flag that replaces it.
|
|
82
|
+
_MAX_FULL_DUMP_CHARS = 60_000
|
|
83
|
+
|
|
50
84
|
|
|
51
85
|
def _matches(tpl: dict, tag: str | None, aspect: str | None, language: str | None) -> bool:
|
|
52
86
|
if tag:
|
|
@@ -74,6 +108,57 @@ def _status_of(tpl: dict) -> str:
|
|
|
74
108
|
return "stable"
|
|
75
109
|
|
|
76
110
|
|
|
111
|
+
def _truncate(text: object, limit: int) -> str | None:
|
|
112
|
+
if not isinstance(text, str) or not text.strip():
|
|
113
|
+
return None
|
|
114
|
+
text = text.strip()
|
|
115
|
+
return text if len(text) <= limit else text[: limit - 3] + "..."
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _summarize(tpl: dict) -> dict:
|
|
119
|
+
"""The "choose a template" view: identity + selection criteria only.
|
|
120
|
+
|
|
121
|
+
Deliberately excludes the authoring contract (llmHint in full,
|
|
122
|
+
customPayloadSchema, slotMapping, compositions, variants' style bodies) —
|
|
123
|
+
that is what ``--template-id`` returns, one template at a time.
|
|
124
|
+
"""
|
|
125
|
+
variants = tpl.get("variants")
|
|
126
|
+
# Only the scalar capability keys that change how a caller *drives* the
|
|
127
|
+
# template (narration vs typewriter caption, who decides duration). The
|
|
128
|
+
# rest of `capabilities` — payloadDefaults above all — is bulk that scales
|
|
129
|
+
# with the registry, so it stays in the detail view.
|
|
130
|
+
caps = tpl.get("capabilities") if isinstance(tpl.get("capabilities"), dict) else {}
|
|
131
|
+
caps_summary = {
|
|
132
|
+
k: caps[k]
|
|
133
|
+
for k in ("payloadStyle", "needsNarration", "durationStrategy", "narrationDriver")
|
|
134
|
+
if k in caps
|
|
135
|
+
}
|
|
136
|
+
summary = {
|
|
137
|
+
"templateId": tpl.get("templateId"),
|
|
138
|
+
"name": tpl.get("name"),
|
|
139
|
+
"description": tpl.get("description"),
|
|
140
|
+
"status": _status_of(tpl),
|
|
141
|
+
"supportedAspectRatios": tpl.get("supportedAspectRatios") or [],
|
|
142
|
+
"contentLanguage": tpl.get("contentLanguage") or [],
|
|
143
|
+
"styleTags": tpl.get("styleTags") or [],
|
|
144
|
+
"variantIds": sorted(variants.keys()) if isinstance(variants, dict) else [],
|
|
145
|
+
"capabilities": caps_summary,
|
|
146
|
+
"llmHintPreview": _truncate(tpl.get("llmHint"), _SUMMARY_HINT_CHARS),
|
|
147
|
+
}
|
|
148
|
+
hint = tpl.get("llmHint")
|
|
149
|
+
if isinstance(hint, str) and len(hint.strip()) > _SUMMARY_HINT_CHARS:
|
|
150
|
+
summary["llmHintTruncated"] = True
|
|
151
|
+
return summary
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _detail_hint(template_id: str = "<templateId>") -> str:
|
|
155
|
+
return (
|
|
156
|
+
f"Summaries only. Run --template-id {template_id} --json-output for one "
|
|
157
|
+
"template's full definition (llmHint / customPayloadSchema / slotMapping / "
|
|
158
|
+
"variants) — that is the per-template authoring contract."
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
|
|
77
162
|
# ----- Examples discovery (--list-examples) ---------------------------------
|
|
78
163
|
#
|
|
79
164
|
# Examples live under <SkillDir>/video_dsl/schema/examples/ as `*.dsl.json` and
|
|
@@ -129,7 +214,12 @@ def main() -> None:
|
|
|
129
214
|
description="List available video templates from the registry.",
|
|
130
215
|
)
|
|
131
216
|
ap.add_argument("--list-templates", action="store_true",
|
|
132
|
-
help="List every available template (the
|
|
217
|
+
help="List every available template (summaries; the default verb).")
|
|
218
|
+
ap.add_argument("--template-id", action="append", dest="template_ids", metavar="ID",
|
|
219
|
+
help="Print this template's FULL definition (repeatable).")
|
|
220
|
+
ap.add_argument("--full", action="store_true",
|
|
221
|
+
help="List mode: emit full definitions instead of summaries "
|
|
222
|
+
"(size-guarded; prefer --template-id).")
|
|
133
223
|
ap.add_argument("--list-examples", action="store_true",
|
|
134
224
|
help="List reference examples (*.dsl.json / *.binding.json) grouped by templateId.")
|
|
135
225
|
ap.add_argument("--filter-tag", help="Keep templates whose styleTags match this substring.")
|
|
@@ -151,10 +241,28 @@ def main() -> None:
|
|
|
151
241
|
print(json.dumps({"examples": examples}, ensure_ascii=False))
|
|
152
242
|
return
|
|
153
243
|
if not examples:
|
|
244
|
+
# 这**不是**异常状态,而是常态:该目录从未进过仓库,也就从未随包发布过。
|
|
245
|
+
#
|
|
246
|
+
# 旧文案("This package may have shipped without reference examples;
|
|
247
|
+
# check the cli source repo or upgrade @remixmate/cli")把常态说成了事故,
|
|
248
|
+
# 而 gen-script 的 SKILL.md 又把「先读这里的参考 DSL」列为强制第一步 ——
|
|
249
|
+
# 于是线上 agent 每次都在这里撞空,然后退回 gen_script 的骨架照抄,连模板
|
|
250
|
+
# 自己声明的版式都不去看。升级 CLI 修不了它,因为没有哪一版带过这些文件。
|
|
251
|
+
#
|
|
252
|
+
# 真正随包到达调用方的逐模板契约是 registry 里的 template.json,所以这里
|
|
253
|
+
# 直接把人指过去,而不是让它以为"该模板不受支持"。
|
|
154
254
|
print(
|
|
155
|
-
f"
|
|
156
|
-
"
|
|
157
|
-
"
|
|
255
|
+
f"ℹ️ No local reference DSLs under {_EXAMPLES_DIR} — that directory is "
|
|
256
|
+
"optional and is not part of the published package.\n"
|
|
257
|
+
" This does NOT mean the template is unsupported. The per-template "
|
|
258
|
+
"contract that does ship is the registry's template.json:\n"
|
|
259
|
+
" • llmHint — how this template's on-screen text and layouts must be authored\n"
|
|
260
|
+
" • customPayloadSchema — every template-specific field, incl. the legal slideId values\n"
|
|
261
|
+
" • slotMapping — propExtractors / requiredProps / optionalProps\n"
|
|
262
|
+
" Read it with `--template-id <id> --json-output` (one template's full "
|
|
263
|
+
"definition). Run `--list-templates` first if you need the ids;\n"
|
|
264
|
+
" that view is summaries only — pointing it at the whole registry "
|
|
265
|
+
"returns more JSON than a tool result can carry.",
|
|
158
266
|
)
|
|
159
267
|
return
|
|
160
268
|
print(f"\n{'Template ID':<28} Files")
|
|
@@ -185,13 +293,74 @@ def main() -> None:
|
|
|
185
293
|
print(f"❌ {exc}", file=sys.stderr)
|
|
186
294
|
sys.exit(1)
|
|
187
295
|
|
|
296
|
+
# ----- detail mode (--template-id) ---------------------------------------
|
|
297
|
+
#
|
|
298
|
+
# Explicit ids win over --filter-*: the caller already chose, and silently
|
|
299
|
+
# returning nothing because a stale filter excluded the pick would read as
|
|
300
|
+
# "template does not exist".
|
|
301
|
+
if args.template_ids:
|
|
302
|
+
by_id = {t.get("templateId"): t for t in templates}
|
|
303
|
+
picked: list[dict] = []
|
|
304
|
+
missing: list[str] = []
|
|
305
|
+
for tid in args.template_ids:
|
|
306
|
+
tpl = by_id.get(tid)
|
|
307
|
+
if tpl is None:
|
|
308
|
+
missing.append(tid)
|
|
309
|
+
else:
|
|
310
|
+
picked.append(tpl)
|
|
311
|
+
if missing:
|
|
312
|
+
for tid in missing:
|
|
313
|
+
# Distinguish "gated out by status" from "does not exist" —
|
|
314
|
+
# otherwise a beta template reads as a typo and the caller
|
|
315
|
+
# retries the id instead of passing --include-beta.
|
|
316
|
+
gated = get_template(tid, include_all_statuses=True)
|
|
317
|
+
if gated is not None:
|
|
318
|
+
print(
|
|
319
|
+
f"❌ Template '{tid}' exists but its status is "
|
|
320
|
+
f"'{_status_of(gated)}' — pass --include-beta to read it.",
|
|
321
|
+
file=sys.stderr,
|
|
322
|
+
)
|
|
323
|
+
else:
|
|
324
|
+
print(f"❌ Unknown template id: '{tid}'", file=sys.stderr)
|
|
325
|
+
known = ", ".join(sorted(str(t.get("templateId")) for t in templates))
|
|
326
|
+
print(f" Available ids: {known}", file=sys.stderr)
|
|
327
|
+
sys.exit(1)
|
|
328
|
+
if args.json_output:
|
|
329
|
+
print(json.dumps({"templates": picked}, ensure_ascii=False))
|
|
330
|
+
else:
|
|
331
|
+
# Pretty-printed rather than tabular: the whole point of detail mode
|
|
332
|
+
# is the nested contract (customPayloadSchema / slotMapping), which
|
|
333
|
+
# a table cannot show.
|
|
334
|
+
print(json.dumps({"templates": picked}, ensure_ascii=False, indent=2))
|
|
335
|
+
return
|
|
336
|
+
|
|
188
337
|
visible = [
|
|
189
338
|
t for t in templates
|
|
190
339
|
if _matches(t, args.filter_tag, args.filter_aspect, args.filter_language)
|
|
191
340
|
]
|
|
192
341
|
|
|
193
342
|
if args.json_output:
|
|
194
|
-
|
|
343
|
+
if args.full:
|
|
344
|
+
payload = json.dumps({"templates": visible}, ensure_ascii=False)
|
|
345
|
+
if len(payload) > _MAX_FULL_DUMP_CHARS and len(visible) > 1:
|
|
346
|
+
print(
|
|
347
|
+
f"❌ Full definitions for {len(visible)} template(s) are "
|
|
348
|
+
f"{len(payload):,} characters — past the {_MAX_FULL_DUMP_CHARS:,}-char "
|
|
349
|
+
"cap, and past what a tool result can carry.\n"
|
|
350
|
+
" Read one template at a time with `--template-id <id> --json-output`, "
|
|
351
|
+
"or narrow with --filter-tag / --filter-aspect / --filter-language.",
|
|
352
|
+
file=sys.stderr,
|
|
353
|
+
)
|
|
354
|
+
sys.exit(1)
|
|
355
|
+
print(payload)
|
|
356
|
+
return
|
|
357
|
+
print(json.dumps(
|
|
358
|
+
{
|
|
359
|
+
"templates": [_summarize(t) for t in visible],
|
|
360
|
+
"detailHint": _detail_hint(),
|
|
361
|
+
},
|
|
362
|
+
ensure_ascii=False,
|
|
363
|
+
))
|
|
195
364
|
return
|
|
196
365
|
|
|
197
366
|
if not visible:
|
|
@@ -217,12 +386,16 @@ def main() -> None:
|
|
|
217
386
|
status = _status_of(tpl).ljust(8)
|
|
218
387
|
tags = ", ".join((tpl.get("styleTags") or [])[:5])
|
|
219
388
|
print(f"{tid} {name} {ratios} {lang} {status} {tags}")
|
|
220
|
-
hint = tpl.get("llmHint")
|
|
389
|
+
hint = _truncate(tpl.get("llmHint"), _SUMMARY_HINT_CHARS)
|
|
221
390
|
if hint:
|
|
222
|
-
hint = hint if len(hint) <= 200 else hint[:197] + "..."
|
|
223
391
|
print(f"{' ' * 22} ↳ {hint}")
|
|
224
392
|
|
|
225
393
|
print(f"\n{len(visible)} template(s) shown")
|
|
394
|
+
first_id = str(visible[0].get("templateId", "<templateId>"))
|
|
395
|
+
print(
|
|
396
|
+
f"Full definition of one template: --template-id {first_id} --json-output "
|
|
397
|
+
"(llmHint / customPayloadSchema / slotMapping / variants)",
|
|
398
|
+
)
|
|
226
399
|
|
|
227
400
|
|
|
228
401
|
if __name__ == "__main__":
|
|
@@ -4,20 +4,22 @@
|
|
|
4
4
|
"tier": "orchestration",
|
|
5
5
|
"category": "authoring",
|
|
6
6
|
"title": "Video Template Registry",
|
|
7
|
-
"description": "List all available video templates (templateId / name / aspect ratio / style tags). Template-to-DSL binding is no longer exposed as a separate step — once prepare_video_assets receives a template_id it runs the full DSL→RenderPlan pipeline internally.",
|
|
7
|
+
"description": "List all available video templates (templateId / name / aspect ratio / style tags), or read one template's full definition with template_id. Listing returns summaries; pass template_id to get that template's authoring contract (llmHint / customPayloadSchema / slotMapping / variants). Template-to-DSL binding is no longer exposed as a separate step — once prepare_video_assets receives a template_id it runs the full DSL→RenderPlan pipeline internally.",
|
|
8
8
|
"auth": "required",
|
|
9
9
|
"envVars": ["PRIV_TOKEN", "VIDEO_TEMPLATE_REGISTRY_URL", "MM_API_BASE_URL", "VIDEO_TEMPLATE_REGISTRY_HTTP_METHOD"],
|
|
10
10
|
"entry": { "type": "python", "scriptPath": "scripts/list_templates.py" },
|
|
11
11
|
"parameters": {
|
|
12
12
|
"type": "object",
|
|
13
13
|
"properties": {
|
|
14
|
-
"list_templates": { "type": "boolean", "description": "List available templates (default behavior; also implied when other flags are passed)." },
|
|
15
|
-
"
|
|
14
|
+
"list_templates": { "type": "boolean", "description": "List available templates as summaries — templateId / name / description / aspect ratios / language / status / styleTags / variantIds / key capabilities / a truncated llmHint (default behavior; also implied when other flags are passed)." },
|
|
15
|
+
"template_id": { "type": "string", "description": "Return this template's FULL definition instead of the summary list: llmHint in full, customPayloadSchema (every template-specific field incl. the legal slideId values), slotMapping, compositions, variants. This is the per-template authoring contract — read it before writing DSL for that template." },
|
|
16
|
+
"full": { "type": "boolean", "description": "List mode only: emit full definitions for every listed template instead of summaries. Rejected when the result would be too large for one tool result — prefer template_id, or narrow with the filter_* parameters." },
|
|
17
|
+
"list_examples": { "type": "boolean", "description": "List the *.dsl.json / *.binding.json reference examples under template-registry/video_dsl/schema/examples/, grouped by templateId. That directory is optional and is not part of the published package, so this is normally empty — to read a template's reference shape, use template_id instead (the registry's own contract)." },
|
|
16
18
|
"filter_tag": { "type": "string", "description": "Show only templates whose styleTags contain (or are contained in) this string. Case-insensitive. E.g. 'tech' / '科普' / 'walkthrough'." },
|
|
17
19
|
"filter_aspect": { "type": "string", "description": "Show only templates that declare this aspect ratio. E.g. '9:16' / '16:9' / '1:1'." },
|
|
18
20
|
"filter_language": { "type": "string", "description": "Show only templates whose contentLanguage includes this code ('zh' or 'en'). Templates with no declared language are always shown (treated as language-agnostic)." },
|
|
19
21
|
"include_beta": { "type": "boolean", "description": "Also list templates with status='beta'. Default off (only 'stable' shows). The ENABLE_BETA_TEMPLATES env var has the same effect process-wide." },
|
|
20
|
-
"json_output": { "type": "boolean", "description": "Emit a JSON result
|
|
22
|
+
"json_output": { "type": "boolean", "description": "Emit a JSON result instead of the human-readable table: { templates: [...] } — summaries, or full definitions when template_id / full is set — or { examples: [...] }." }
|
|
21
23
|
},
|
|
22
24
|
"required": []
|
|
23
25
|
}
|
|
@@ -380,6 +380,64 @@ def _check_narration_items_count(dsl: dict) -> list[ValidationError]:
|
|
|
380
380
|
return errors
|
|
381
381
|
|
|
382
382
|
|
|
383
|
+
# gen_script 写进骨架的「作者还没选版式」哨兵。
|
|
384
|
+
#
|
|
385
|
+
# 两处必须是同一个字面量,而 gen-script 与 template-registry 是两个独立 skill 目录、
|
|
386
|
+
# 没有共享模块可以 import,所以这里复制一份。改动需同步 gen_script.py 的
|
|
387
|
+
# ``SLIDE_ID_SENTINEL``(那边的注释也指回这里)。
|
|
388
|
+
SLIDE_ID_SENTINEL = "__CHOOSE_SLIDE__"
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def _check_slide_id_chosen(dsl: dict) -> list[ValidationError]:
|
|
392
|
+
"""多版式模板的每个场景都必须显式选一个 ``customPayload.slideId``。
|
|
393
|
+
|
|
394
|
+
为什么需要这道门禁:slideId 缺失(或还是哨兵)时,渲染端会静默回落到
|
|
395
|
+
``DefaultSlide`` —— 渲染成功、零日志、退出码 0,画面只剩一行居中标题。这是整条
|
|
396
|
+
管线里最安静的失效方式,看起来"像是模板本来就长这样",只有付完渲染的钱才会发现。
|
|
397
|
+
|
|
398
|
+
**判定「是不是多版式模板」不写死模板 id**,而是看模板自己声明的
|
|
399
|
+
``customPayloadSchema.slideId.enum`` 有没有超过一个取值 —— 有得挑才要求挑。
|
|
400
|
+
单版式模板(枚举只有一项或压根没声明)不拦:那里没有可选的东西,报错只会变成
|
|
401
|
+
修不掉的常驻噪音。
|
|
402
|
+
|
|
403
|
+
报错文案直接把该模板全部可选 slideId 列出来,因为收到这条错误的调用方多半
|
|
404
|
+
正是那个"不知道有哪些版式"的 agent。
|
|
405
|
+
"""
|
|
406
|
+
tpl = _get_template_cfg(_pick_template_id(dsl))
|
|
407
|
+
if not tpl:
|
|
408
|
+
return []
|
|
409
|
+
schema = tpl.get("customPayloadSchema")
|
|
410
|
+
if not isinstance(schema, dict):
|
|
411
|
+
return []
|
|
412
|
+
slide_schema = schema.get("slideId")
|
|
413
|
+
enum = slide_schema.get("enum") if isinstance(slide_schema, dict) else None
|
|
414
|
+
if not isinstance(enum, list) or len(enum) <= 1:
|
|
415
|
+
return []
|
|
416
|
+
|
|
417
|
+
tid = tpl.get("templateId", "<unknown>")
|
|
418
|
+
choices = ", ".join(str(x) for x in enum)
|
|
419
|
+
errors: list[ValidationError] = []
|
|
420
|
+
for idx, scene in enumerate(dsl.get("scenes", []) or []):
|
|
421
|
+
sid = scene.get("id", f"scenes[{idx}]")
|
|
422
|
+
custom = scene.get("customPayload") or {}
|
|
423
|
+
slide_id = custom.get("slideId") or (custom.get("templateData") or {}).get("slideId")
|
|
424
|
+
if isinstance(slide_id, str) and slide_id and slide_id != SLIDE_ID_SENTINEL:
|
|
425
|
+
continue
|
|
426
|
+
reason = (
|
|
427
|
+
"still holds the gen_script placeholder"
|
|
428
|
+
if slide_id == SLIDE_ID_SENTINEL
|
|
429
|
+
else "has no slideId"
|
|
430
|
+
)
|
|
431
|
+
errors.append(ValidationError(
|
|
432
|
+
f"scenes[{idx}].customPayload.slideId",
|
|
433
|
+
f"scene '{sid}' {reason} — template '{tid}' registers several layouts and "
|
|
434
|
+
"one must be chosen per scene, otherwise the whole templateData is dropped "
|
|
435
|
+
"and the frame silently renders a single centred title.\n"
|
|
436
|
+
f" Pick the one that matches this scene's information shape: {choices}",
|
|
437
|
+
))
|
|
438
|
+
return errors
|
|
439
|
+
|
|
440
|
+
|
|
383
441
|
def _check_narration_language_strict(dsl: dict) -> list[ValidationError]:
|
|
384
442
|
"""完整版:模板若声明 ``capabilities.narrationLanguageStrict`` 则强约束旁白语言。
|
|
385
443
|
|
|
@@ -463,6 +521,10 @@ def validate_integrity(dsl: dict) -> list[ValidationError]:
|
|
|
463
521
|
errors += _check_asset_duplicates(dsl)
|
|
464
522
|
errors += _check_scene_asset_refs(dsl)
|
|
465
523
|
errors += _check_narration_items_count(dsl)
|
|
524
|
+
# 新增(非历史规则):多版式模板必须逐场景选定 slideId。放进 integrity 集合是因为
|
|
525
|
+
# render-video 与 prepare_video_assets 都走这条 —— 而"没选版式"正是要在烧掉渲染
|
|
526
|
+
# 积分之前拦住的东西。单版式模板不受影响,见 _check_slide_id_chosen。
|
|
527
|
+
errors += _check_slide_id_chosen(dsl)
|
|
466
528
|
return errors
|
|
467
529
|
|
|
468
530
|
|
|
@@ -490,6 +552,7 @@ def validate_dsl(dsl: dict) -> list[ValidationError]:
|
|
|
490
552
|
errors += _check_assetbindings_deprecation(dsl)
|
|
491
553
|
errors += _check_narration_items_count(dsl)
|
|
492
554
|
errors += _check_narration_language_strict(dsl)
|
|
555
|
+
errors += _check_slide_id_chosen(dsl)
|
|
493
556
|
return errors
|
|
494
557
|
|
|
495
558
|
|