@remixmate/cli 0.9.31 → 0.9.32

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "version": "0.9.31",
4
- "generatedAt": "2026-09-14T15:38:04.716Z",
3
+ "version": "0.9.32",
4
+ "generatedAt": "2026-09-15T01:33:27.056Z",
5
5
  "skills": [
6
6
  {
7
7
  "id": "export-jianying",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remixmate/cli",
3
- "version": "0.9.31",
3
+ "version": "0.9.32",
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",
@@ -55,6 +55,7 @@ The intent is to avoid the failure mode where "the generic DSL looks compatible
55
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
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
57
  3. Read its `customPayloadSchema`: every template-specific field lives there, including the enum of legal `customPayload.slideId` values for multi-layout templates.
58
+ 3b. If the template ships `slideSchemas` (multi-layout templates do), **author `templateData` straight from `slideSchemas[slideId]`** — it is the only place the per-layout field names exist. Do not infer them: the same concept is named `name` in one layout, `label` in another and `era` in a third, and a wrong name is dropped in silence (see below).
58
59
  4. Combine that with the template's `slotMapping`, `requiredProps`, `optionalProps`, `propExtractors`, `assetRequirements`, `supportedAspectRatios`, `supportedDurations`, `constraints`, and `scenePatterns`.
59
60
  5. Generate the DSL in the template's native shape — not the generic DSL shape.
60
61
  6. Make sure the DSL explicitly contains every template-specific field, e.g. `templateData.words`, `customPayload.slideId`, `visuals.avatar.assetRef`.
@@ -69,6 +70,14 @@ When a template's `customPayloadSchema.slideId.enum` holds more than one value (
69
70
 
70
71
  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.
71
72
 
73
+ ### Multi-layout templates: field names are per-layout, never guessable
74
+
75
+ Picking the layout is only half of it. Each layout reads its own field names out of `templateData`, and a field the layout does not read is **dropped without a word** — that slot renders empty, or shows the component's built-in placeholder text, and the render still reports success.
76
+
77
+ This has shipped broken videos: a 7-scene deck wrote `items[].title` where `html-slide`'s `feature-grid` reads `name`, `items[].description` where `timeline-axis` reads `detail`, and `title` / `subtitle` / `ctaText` where `closing-cta` reads `headline` / `actionLine` / `ctaLabel` / `ctaUrl`. Five of seven scenes rendered as empty cards and placeholder copy — after paying for TTS and the render.
78
+
79
+ So: **copy the field names out of `slideSchemas[slideId]`.** The DSL validator now rejects unknown field names before any asset is generated and tells you the declared ones, but that check only fires for templates that ship `slideSchemas` — reading the contract is still the primary move, not the fallback.
80
+
72
81
  ### On-screen text: the rules live in the template, not here
73
82
 
74
83
  How the on-screen text layers should be *written* — whether the headline is a hook or a product name, how many subheadline lines survive, whether `**emphasis**` is parsed, where a link is allowed to appear — is a property of each template's layout, and its single source of truth is that template's `llmHint` in `template.json`. This skill deliberately does not restate any of it: a copy here would drift from the registry, and the registry is what actually renders.
@@ -476,4 +485,4 @@ prefix and version — not a short alias:
476
485
  |------|---------|
477
486
  | `gen_script.py` | Core script — produces the Video DSL JSON from a topic. |
478
487
 
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.
488
+ The authoritative per-template contract is the registry's `template.json` (`llmHint` + `customPayloadSchema` + `slideSchemas` + `slotMapping`) — everything you need to author against is in there, so read it rather than inferring field names from the template's name or from another template. `template_registry list_examples=true` lists local reference DSLs when any exist, but that directory does not ship — see *DSL schema* above.
@@ -119,7 +119,7 @@ def _summarize(tpl: dict) -> dict:
119
119
  """The "choose a template" view: identity + selection criteria only.
120
120
 
121
121
  Deliberately excludes the authoring contract (llmHint in full,
122
- customPayloadSchema, slotMapping, compositions, variants' style bodies) —
122
+ customPayloadSchema, slideSchemas, slotMapping, compositions, variants' style bodies) —
123
123
  that is what ``--template-id`` returns, one template at a time.
124
124
  """
125
125
  variants = tpl.get("variants")
@@ -154,8 +154,8 @@ def _summarize(tpl: dict) -> dict:
154
154
  def _detail_hint(template_id: str = "<templateId>") -> str:
155
155
  return (
156
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."
157
+ "template's full definition (llmHint / customPayloadSchema / slideSchemas / "
158
+ "slotMapping / variants) — that is the per-template authoring contract."
159
159
  )
160
160
 
161
161
 
@@ -258,6 +258,7 @@ def main() -> None:
258
258
  "contract that does ship is the registry's template.json:\n"
259
259
  " • llmHint — how this template's on-screen text and layouts must be authored\n"
260
260
  " • customPayloadSchema — every template-specific field, incl. the legal slideId values\n"
261
+ " • slideSchemas — for multi-layout templates: what templateData each slideId eats\n"
261
262
  " • slotMapping — propExtractors / requiredProps / optionalProps\n"
262
263
  " Read it with `--template-id <id> --json-output` (one template's full "
263
264
  "definition). Run `--list-templates` first if you need the ids;\n"
@@ -394,7 +395,7 @@ def main() -> None:
394
395
  first_id = str(visible[0].get("templateId", "<templateId>"))
395
396
  print(
396
397
  f"Full definition of one template: --template-id {first_id} --json-output "
397
- "(llmHint / customPayloadSchema / slotMapping / variants)",
398
+ "(llmHint / customPayloadSchema / slideSchemas / slotMapping / variants)",
398
399
  )
399
400
 
400
401
 
@@ -20,6 +20,7 @@ DSL 校验器 - 对 Video DSL v1alpha1 做结构校验与标准化。
20
20
  处理的模板时**不需要再改 remixmate 代码**。
21
21
  """
22
22
 
23
+ import difflib
23
24
  import re
24
25
  from typing import Iterable, Optional
25
26
 
@@ -32,6 +33,22 @@ VALID_LAYOUTS = {"full-visual", "split-left-right", "split-top-bottom", "picture
32
33
  VALID_ASSET_TYPES = {"image", "video", "audio", "avatar", "subtitle", "bgm"}
33
34
  VALID_ASSET_SOURCES = {"existing", "gen-image", "gen-video", "gen-voice", "gen-digital-human"}
34
35
 
36
+ # 由 Composition 消费、而非 slide 组件消费的字段。前八个与模板侧
37
+ # utils/slidePayload.ts 的 FRAME_LEVEL_KEYS 同源 ——「整包态」binder 会把它们混进
38
+ # templateData,出现在那里是正当的,不该被当成未声明字段。highlightMap 同理(模板
39
+ # 一般在 slideSchemas._shared 里声明它,这里兜底没声明的情况)。
40
+ _SLIDE_FRAME_LEVEL_KEYS = frozenset({
41
+ "slideId",
42
+ "titleText",
43
+ "subtitleText",
44
+ "watermarkText",
45
+ "narrationAssetId",
46
+ "narrationSrc",
47
+ "background",
48
+ "templateData",
49
+ "highlightMap",
50
+ })
51
+
35
52
  # CJK 汉字范围(简繁通用)+ 常见中文标点
36
53
  _CJK_PATTERN = re.compile(r"[一-鿿㐀-䶿＀-￯ -〿]")
37
54
 
@@ -438,6 +455,137 @@ def _check_slide_id_chosen(dsl: dict) -> list[ValidationError]:
438
455
  return errors
439
456
 
440
457
 
458
+ def _check_slide_payload_fields(dsl: dict) -> list[ValidationError]:
459
+ """逐版式校验 ``customPayload.templateData`` 的字段名。
460
+
461
+ 为什么需要这道门禁:多版式模板里「slideId 决定 templateData 吃哪些字段」这条
462
+ 契约,此前只存在于模板的 React 源码里 —— 而写 DSL 的 agent 只能看到 registry
463
+ 下发的 ``template.json``。字段名猜错(把 ``name`` 写成 ``title``、把 ``detail``
464
+ 写成 ``description``)时组件读到 undefined,要么渲染出内置的占位文案、要么整块
465
+ 空白,**渲染成功、零日志、退出码 0**。真实案例:一份 7 场景的 DSL 里 5 个场景
466
+ 中招,TTS 和渲染的钱全花完才在成片里看出来。
467
+
468
+ 契约现在写在 ``template.json`` 的 ``slideSchemas`` 里(键 = slideId,值 = 该版式
469
+ 的 JSON Schema),由 template-library 的 ``check:slide-schemas`` 与组件源码双向
470
+ 对账。本函数按它核对每个场景,**未声明的字段名带 "did you mean" 提示** ——
471
+ 收到这条错误的多半正是那个猜错了名字的 agent,直接把正确的名字给它。
472
+
473
+ 没有声明 ``slideSchemas`` 的模板(全部单版式模板)一律跳过,不产生噪音。
474
+ """
475
+ tpl = _get_template_cfg(_pick_template_id(dsl))
476
+ if not tpl:
477
+ return []
478
+ schemas = tpl.get("slideSchemas")
479
+ if not isinstance(schemas, dict):
480
+ return []
481
+
482
+ tid = tpl.get("templateId", "<unknown>")
483
+ shared = ((schemas.get("_shared") or {}).get("properties") or {})
484
+ known_slides = [k for k in schemas if not k.startswith("_")]
485
+
486
+ errors: list[ValidationError] = []
487
+
488
+ for idx, scene in enumerate(dsl.get("scenes", []) or []):
489
+ sid = scene.get("id", f"scenes[{idx}]")
490
+ custom = scene.get("customPayload") or {}
491
+ if not isinstance(custom, dict):
492
+ continue
493
+ data = custom.get("templateData")
494
+ if not isinstance(data, dict):
495
+ data = scene.get("templateData")
496
+ if not isinstance(data, dict):
497
+ continue
498
+
499
+ slide_id = custom.get("slideId") or data.get("slideId")
500
+ if not isinstance(slide_id, str) or not slide_id:
501
+ continue # 「没选版式」由 _check_slide_id_chosen 负责报
502
+
503
+ schema = schemas.get(slide_id)
504
+ if not isinstance(schema, dict):
505
+ # 版式名写错 —— 同样静默回落到 DefaultSlide,和没写一样致命。
506
+ errors.append(ValidationError(
507
+ f"scenes[{idx}].customPayload.slideId",
508
+ f"scene '{sid}' uses slideId '{slide_id}', which template '{tid}' does not "
509
+ "register — the frame silently falls back to a single centred title.\n"
510
+ f" Available layouts: {', '.join(sorted(known_slides))}",
511
+ ))
512
+ continue
513
+
514
+ base = f"scenes[{idx}].customPayload.templateData"
515
+ errors += _check_payload_object(
516
+ data,
517
+ schema,
518
+ path=base,
519
+ scene=sid,
520
+ extra_allowed=shared,
521
+ # frame 级字段由 Composition 消费,「整包态」binder 会把它们混进
522
+ # templateData —— 出现在这里是正当的,不算未声明字段。
523
+ skip_keys=_SLIDE_FRAME_LEVEL_KEYS,
524
+ )
525
+
526
+ # 卡片数组:用户真正会写错的地方在这一层(items[].title vs items[].name)。
527
+ for key, sub in (schema.get("properties") or {}).items():
528
+ if not isinstance(sub, dict) or sub.get("type") != "array":
529
+ continue
530
+ item_schema = sub.get("items")
531
+ if not isinstance(item_schema, dict) or not item_schema.get("properties"):
532
+ continue
533
+ value = data.get(key)
534
+ if not isinstance(value, list):
535
+ continue
536
+ for i, item in enumerate(value):
537
+ if not isinstance(item, dict):
538
+ continue
539
+ errors += _check_payload_object(
540
+ item, item_schema, path=f"{base}.{key}[{i}]", scene=sid,
541
+ )
542
+
543
+ return errors
544
+
545
+
546
+ def _check_payload_object(
547
+ data: dict,
548
+ schema: dict,
549
+ *,
550
+ path: str,
551
+ scene: str,
552
+ extra_allowed: Optional[dict] = None,
553
+ skip_keys: frozenset = frozenset(),
554
+ ) -> list[ValidationError]:
555
+ """一层对象的「未声明字段 + 缺必填字段」核对。数组元素与顶层共用这一份。"""
556
+ props = schema.get("properties")
557
+ if not isinstance(props, dict):
558
+ return []
559
+ allowed = set(props) | set(extra_allowed or {})
560
+
561
+ errors: list[ValidationError] = []
562
+
563
+ for key in data:
564
+ if key in allowed or key in skip_keys:
565
+ continue
566
+ hint = difflib.get_close_matches(key, sorted(allowed), n=1, cutoff=0.6)
567
+ suggestion = (
568
+ f" Did you mean '{hint[0]}'?" if hint
569
+ else f" Declared fields: {', '.join(sorted(allowed))}."
570
+ )
571
+ errors.append(ValidationError(
572
+ f"{path}.{key}",
573
+ f"scene '{scene}': the slide does not read '{key}' — the value is silently "
574
+ f"dropped and that slot renders empty (or shows the component's placeholder)."
575
+ f"{suggestion}",
576
+ ))
577
+
578
+ for key in schema.get("required") or []:
579
+ if data.get(key) is None:
580
+ errors.append(ValidationError(
581
+ f"{path}.{key}",
582
+ f"scene '{scene}': required field '{key}' is missing — without it the slide "
583
+ "renders its built-in placeholder instead of your content.",
584
+ ))
585
+
586
+ return errors
587
+
588
+
441
589
  def _check_narration_language_strict(dsl: dict) -> list[ValidationError]:
442
590
  """完整版:模板若声明 ``capabilities.narrationLanguageStrict`` 则强约束旁白语言。
443
591
 
@@ -525,6 +673,9 @@ def validate_integrity(dsl: dict) -> list[ValidationError]:
525
673
  # render-video 与 prepare_video_assets 都走这条 —— 而"没选版式"正是要在烧掉渲染
526
674
  # 积分之前拦住的东西。单版式模板不受影响,见 _check_slide_id_chosen。
527
675
  errors += _check_slide_id_chosen(dsl)
676
+ # 同理:字段名写错同样是「渲染成功但画面是空壳」,必须在烧掉 TTS / 渲染积分
677
+ # 之前拦住。只对声明了 slideSchemas 的模板生效,见 _check_slide_payload_fields。
678
+ errors += _check_slide_payload_fields(dsl)
528
679
  return errors
529
680
 
530
681
 
@@ -553,6 +704,7 @@ def validate_dsl(dsl: dict) -> list[ValidationError]:
553
704
  errors += _check_narration_items_count(dsl)
554
705
  errors += _check_narration_language_strict(dsl)
555
706
  errors += _check_slide_id_chosen(dsl)
707
+ errors += _check_slide_payload_fields(dsl)
556
708
  return errors
557
709
 
558
710