@remixmate/cli 0.12.5 → 0.12.7

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.12.5",
4
- "generatedAt": "2026-10-18T12:54:02.484Z",
3
+ "version": "0.12.7",
4
+ "generatedAt": "2026-10-20T11:33:13.520Z",
5
5
  "skills": [
6
6
  {
7
7
  "id": "doc-parse",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remixmate/cli",
3
- "version": "0.12.5",
3
+ "version": "0.12.7",
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",
@@ -78,7 +78,7 @@ The intent is to avoid the failure mode where "the generic DSL looks compatible
78
78
  4. Combine that with the template's `slotMapping`, `requiredProps`, `optionalProps`, `propExtractors`, `assetRequirements`, `supportedAspectRatios`, `supportedDurations`, `constraints`, and `scenePatterns`.
79
79
  5. Generate the DSL in the template's native shape — not the generic DSL shape.
80
80
  6. Make sure the DSL explicitly contains every template-specific field, e.g. `templateData.words`, `customPayload.slideId`, `visuals.avatar.assetRef`.
81
- 7. After generation, run the schema check and show the script summary to the user for confirmation.
81
+ 7. After generation, run `gen_script.py --validate -i <dsl.json>` and fix every error it reports, then show the script summary to the user for confirmation. It checks the template contract, not just the DSL shape: undeclared `customPayload` field names, values outside a declared `enum` (e.g. `layout: "bogus"`, `style.preset: "nwes"`), and every `assetRef` / `*AssetRef` (`logoAssetRef`, `avatarAssetRef` …) pointing at an asset that `assets[]` does not declare. These are the same checks render-video runs before spending credits; catching them here saves a confirmed script from being rejected later.
82
82
 
83
83
  ### Document-driven templates: pass the source URL, don't author the payload
84
84
 
@@ -123,7 +123,7 @@ Picking the layout is only half of it. Each layout reads its own field names out
123
123
 
124
124
  This has shipped broken videos: a 7-scene deck guessed generic names (`title`, `description`, `ctaText`) where the chosen layouts read different ones. Five of seven scenes rendered as empty cards and placeholder copy — after paying for TTS and the render. Which names each layout reads is the template's fact; the template's `llmHint` calls out the usual traps.
125
125
 
126
- 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.
126
+ So: **copy the field names out of `slideSchemas[slideId]`.** The DSL validator (`gen_script.py --validate`, and again in render-video) rejects unknown field names and out-of-enum values before any asset is generated and tells you the declared ones — per layout from `slideSchemas` for multi-layout templates, from `customPayloadSchema` for every other template. Reading the contract is still the primary move, not the fallback: the validator checks names and enums, not whether the content fits the layout.
127
127
 
128
128
  ### On-screen text: the rules live in the template, not here
129
129
 
@@ -283,6 +283,12 @@ Without this flag the user's images are **ignored entirely** and every scene get
283
283
  AI-generated picture. That failure is quiet: the video renders fine, it just isn't made of
284
284
  the material the user handed you.
285
285
 
286
+ **One shared background.** A template that declares `capabilities.backgroundStrategy: "shared"`
287
+ (e.g. an audiogram whose whole video is one "show poster") gets a skeleton with a single
288
+ `img-background` asset that every scene references, instead of one image per scene. Keep it
289
+ that way; switch to another image only where the template's `llmHint` says you may (such as a
290
+ new chapter). `--scene-images` still maps the user's own images one per scene.
291
+
286
292
  ### The mapping rule (positional, then AI fills the rest)
287
293
 
288
294
  Each entry is one scene's visual, and it is either a **URL** (use that existing asset) or the
@@ -473,7 +479,7 @@ python3 <SkillDir>/scripts/gen_script.py \
473
479
  | `--scenes` | Scene count. | auto-planned |
474
480
  | `--voice-id` | Narration voice id. | resolved from template + language fallback |
475
481
  | `--speed` | Narration speech rate (0.5–2.0), written to `global.narration.speed` and applied when `prepare_video_assets` calls gen-voice. Pass it whenever the user picked a speed (the replicate form's 语速 field sends one). Above ~1.3 subtitle alignment drifts and the delivery turns mechanical — shorten the script instead. | `1.0` |
476
- | `--variant` | Template visual variant id (a key of the template's `variants`; `template-registry --list-templates` prints them as `variantIds`). Written to `meta.templateVariant`, which takes precedence over the fuzzy `--style` match. Pass it whenever the user picked a style — the replicate card shows it as `名字(id)`, e.g. `视觉风格: 紫色科技(tech-purple)`; pass the id in the parentheses. Requires `--template-id`; unknown ids are rejected. | template default |
482
+ | `--variant` | Template visual variant id (a key of the template's `variants`; `template-registry --list-templates` prints them as `variantIds`). Written to `meta.templateVariant`, which takes precedence over the fuzzy `--style` match, and the variant's own `imageStyleGuide` is what the skeleton's image prompts / `negativePrompt` are built from (pass it to `gen_script` rather than setting `meta.templateVariant` afterwards, or the prompts keep the template's default look). Pass it whenever the user picked a style — the replicate card shows it as `名字(id)`, e.g. `视觉风格: 紫色科技(tech-purple)`; pass the id in the parentheses. Requires `--template-id`; unknown ids are rejected. | template default |
477
483
  | `--background-preset` | Scene background preset, one of the template's `customPayloadSchema.background.preset` values. Written to every scene's `customPayload.background.preset` (other keys of the template's default background are kept). Pass it whenever the user picked a background, e.g. `背景样式: 纯色(solid)` → `solid`. `image` / `video` also need `customPayload.background.assetRef`. Requires `--template-id`; unsupported values are rejected. | template default |
478
484
  | `--font-id` | Font library key (`font.uniq_id`), written to `global.font.fontId`; ab-render resolves and installs the real font at render time. Pass it whenever the user picked a font — the replicate card shows it as `名字(id)`, e.g. `字体: <名字>(<uniqId>)`; pass the id in the parentheses. Never pass a CSS family or the display name: an unknown key falls back to the template font with no error. | template font |
479
485
  | `--font-name` | Display name of the picked font → `global.font.fontName` (display only, never used for rendering). Pass it with `--font-id`. | — |
@@ -482,7 +488,7 @@ python3 <SkillDir>/scripts/gen_script.py \
482
488
  | `--template-candidates` | Repeatable. Template ids offered to the user, recommended first → `meta.templateSelection.candidates`. With `user-confirmed`, `--template-id` must be one of them. | — |
483
489
  | `--allow-digital-human` | Whether to allow digital-human assets. | off |
484
490
  | `--allow-ai-video` | Whether to allow AI-generated video assets. | off |
485
- | `--validate` | Validate-only mode: only validate the input DSL. | — |
491
+ | `--validate` | Validate-only mode: validate the input DSL against the DSL schema **and** the template contract (customPayload field names, enum values, asset references). Warnings are printed but do not fail. | — |
486
492
  | `--input` | Input DSL file path (required in validate mode). | — |
487
493
  | `-o` / `--output` | Output DSL file path. | stdout |
488
494
  | `--stub-image-url` | Test mode: every image AssetRef is written as existing + generated + this URL, no prompt (env: `STUB_IMAGE_URL`). | — |
@@ -302,6 +302,33 @@ def validate_dsl(dsl: dict) -> list:
302
302
  return errors_as_strings(validate_structural(dsl))
303
303
 
304
304
 
305
+ def validate_authored_dsl(dsl: dict) -> tuple[list, list]:
306
+
307
+
308
+
309
+
310
+
311
+
312
+
313
+
314
+
315
+ from video_dsl.runtime.dsl_validator import (
316
+ validate_integrity,
317
+ validate_structural,
318
+ errors_as_strings,
319
+ )
320
+ found, seen = [], set()
321
+
322
+ for e in validate_structural(dsl) + validate_integrity(dsl):
323
+ key = (e.path, e.message)
324
+ if key not in seen:
325
+ seen.add(key)
326
+ found.append(e)
327
+ errors = [e for e in found if getattr(e, "severity", "error") == "error"]
328
+ warnings = [e for e in found if getattr(e, "severity", "error") != "error"]
329
+ return errors_as_strings(errors), errors_as_strings(warnings)
330
+
331
+
305
332
  def plan_scenes(
306
333
  topic: str,
307
334
  duration: int,
@@ -609,6 +636,8 @@ def _template_needs_image(template_config: dict | None) -> bool:
609
636
 
610
637
 
611
638
 
639
+
640
+
612
641
 
613
642
 
614
643
 
@@ -674,6 +703,7 @@ def _resolve_contract(template_config: dict | None) -> dict:
674
703
  "duration_model": caps.get("durationModel"),
675
704
  "payload_defaults": payload_defaults,
676
705
  "primary_visual_type": primary_visual_type,
706
+ "background_strategy": "shared" if caps.get("backgroundStrategy") == "shared" else "per-scene",
677
707
  "default_layout": caps.get("defaultLayout"),
678
708
  "authored_scenes": caps.get("authoredScenes") is True,
679
709
 
@@ -1518,6 +1548,7 @@ def build_dsl(
1518
1548
  scene_images: list[str] | None = None,
1519
1549
  item_count: int | None = None,
1520
1550
  source_document_url: str | None = None,
1551
+ variant: str | None = None,
1521
1552
  ) -> dict:
1522
1553
 
1523
1554
 
@@ -1708,16 +1739,33 @@ def build_dsl(
1708
1739
 
1709
1740
 
1710
1741
 
1711
- image_style_guide = resolve_style_guide(template_config) if _HAS_ENHANCER else {}
1742
+
1743
+
1744
+
1745
+ image_style_guide = (
1746
+ resolve_style_guide(template_config, variant or "") if _HAS_ENHANCER else {}
1747
+ )
1712
1748
  image_compose = _HAS_ENHANCER and is_compose_mode(image_style_guide)
1713
1749
  image_ratio = image_style_guide.get("ratio") or ratio
1714
1750
 
1715
1751
  tail_pad_sec = _nb.template_tail_pad_sec(template_config) if _HAS_BUDGET else None
1752
+
1753
+
1754
+
1755
+
1756
+ shared_background = (
1757
+ contract["background_strategy"] == "shared"
1758
+ and primary_visual_type == "image"
1759
+ and not provided_slots
1760
+ )
1761
+ shared_background_planned = False
1716
1762
  for idx, plan in enumerate(scene_plans):
1717
1763
  scene_id = f"scene-{idx + 1:02d}"
1718
1764
 
1719
1765
  if primary_visual_type == "video":
1720
1766
  visual_asset_id = f"video-{scene_id}"
1767
+ elif shared_background:
1768
+ visual_asset_id = "img-background"
1721
1769
  else:
1722
1770
  visual_asset_id = f"img-{scene_id}"
1723
1771
  narration_asset_id = f"narration-{scene_id}" if needs_narration else None
@@ -1733,7 +1781,11 @@ def build_dsl(
1733
1781
  scene_has_visual = has_visual and _slot_reads_visual(template_config, plan["purpose"])
1734
1782
 
1735
1783
 
1736
- if scene_has_visual and primary_visual_type == "image":
1784
+ if scene_has_visual and shared_background and shared_background_planned:
1785
+ pass
1786
+ elif scene_has_visual and primary_visual_type == "image":
1787
+ if shared_background:
1788
+ shared_background_planned = True
1737
1789
  slot = provided_slots[idx] if idx < len(provided_slots) else ""
1738
1790
  slot_is_ai, slot_prompt = _parse_ai_slot(slot)
1739
1791
  if slot and not slot_is_ai:
@@ -1760,6 +1812,12 @@ def build_dsl(
1760
1812
  else:
1761
1813
  if image_compose:
1762
1814
  image_result = {"prompt": COMPOSE_SUBJECT_PLACEHOLDER}
1815
+ elif shared_background:
1816
+
1817
+
1818
+ image_result = generate_image_prompt(
1819
+ "point", topic, style, narration_text="", style_guide=image_style_guide,
1820
+ )
1763
1821
  else:
1764
1822
  image_result = generate_image_prompt(
1765
1823
  plan["purpose"], topic, style,
@@ -2082,7 +2140,8 @@ Examples:
2082
2140
  "--variant", default=None,
2083
2141
  help="Template visual variant id (a key of the template's `variants`, listed as variantIds by "
2084
2142
  "template-registry --list-templates). Written to meta.templateVariant, which overrides the "
2085
- "fuzzy --style match. When the user picked a style in the replicate card (shown as "
2143
+ "fuzzy --style match, and the variant's imageStyleGuide drives the skeleton's image "
2144
+ "prompts. When the user picked a style in the replicate card (shown as "
2086
2145
  "`名字(id)`, e.g. `紫色科技(tech-purple)`), pass that id. Requires --template-id.",
2087
2146
  )
2088
2147
  parser.add_argument(
@@ -2189,7 +2248,9 @@ Examples:
2189
2248
  sys.exit(1)
2190
2249
  with open(args.input, "r", encoding="utf-8") as f:
2191
2250
  dsl = json.load(f)
2192
- errors = validate_dsl(dsl)
2251
+ errors, warnings = validate_authored_dsl(dsl)
2252
+ for w in warnings:
2253
+ print(f"⚠️ {w}", file=sys.stderr)
2193
2254
  if errors:
2194
2255
  print("❌ DSL validation failed:", file=sys.stderr)
2195
2256
  for err in errors:
@@ -2316,6 +2377,7 @@ Examples:
2316
2377
  scene_images=args.scene_images,
2317
2378
  item_count=args.item_count,
2318
2379
  source_document_url=args.source_document_url,
2380
+ variant=args.variant,
2319
2381
  )
2320
2382
  except ValueError as exc:
2321
2383
 
@@ -2,6 +2,6 @@
2
2
  "skillName": "gen-script",
3
3
  "repoName": "agent-skill-media-maker",
4
4
  "skillId": "474",
5
- "version": "V21",
5
+ "version": "V22",
6
6
  "skillDescription": "Video-script generation skill. Turns a user-supplied topic into a structured Video DSL (JSON) that describes the full video — scene structure, asset requirements, and narrative flow.\n\nUse this skill as soon as the user mentions any of these intents:\n- Write a video script, generate a video script, plan a video, write storyboards\n- Create a short video, plan video content, generate a Video DSL\n- Turn a topic into a video structure / video content plan\n\nEven when the user does not say \"generate the DSL\", use this skill whenever they want to turn a topic into a structured video plan.\n\n⚠️ Stop-and-confirm gate: after this skill returns a DSL, show the full script and wait for the user's explicit confirmation. Never call `prepare_video_assets` in the same turn."
7
7
  }
@@ -779,6 +779,12 @@ def _probe_video_duration(url: str):
779
779
  return probe_video_duration(url)
780
780
 
781
781
 
782
+ def _probe_video_meta(url: str) -> dict:
783
+
784
+ from _video_probe import probe_video_meta
785
+ return probe_video_meta(url)
786
+
787
+
782
788
  def build_render_plan(dsl: dict, binding: dict) -> dict:
783
789
 
784
790
  fps = dsl.get("global", {}).get("fps", 30)
@@ -868,11 +874,35 @@ def build_render_plan(dsl: dict, binding: dict) -> dict:
868
874
 
869
875
 
870
876
 
877
+
878
+
879
+
880
+
881
+
882
+
883
+
884
+ meta_by_url: dict = {}
871
885
  for aid, asset in assets_by_id.items():
872
- if asset["type"] == "video" and asset["source"] == "existing" and not asset.get("duration") and asset.get("url"):
873
- probed = _probe_video_duration(asset["url"])
874
- if probed:
875
- asset["duration"] = probed
886
+ url = asset.get("url")
887
+ if asset["type"] != "video" or asset["source"] != "existing" or not url:
888
+ continue
889
+ need_size = not (asset.get("width") and asset.get("height"))
890
+ need_duration = not asset.get("duration")
891
+ if not (need_size or need_duration):
892
+ continue
893
+ meta = meta_by_url.get(url)
894
+ if meta is None and need_size:
895
+ meta = meta_by_url[url] = _probe_video_meta(url)
896
+ meta = meta or {}
897
+ if need_size and meta.get("width") and meta.get("height"):
898
+ asset["width"], asset["height"] = meta["width"], meta["height"]
899
+ if need_duration:
900
+ if meta.get("duration_ms"):
901
+ asset["duration"] = meta["duration_ms"] / 1000.0
902
+ else:
903
+ probed = _probe_video_duration(url)
904
+ if probed:
905
+ asset["duration"] = probed
876
906
 
877
907
  timeline = []
878
908
  current_frame = 0
@@ -365,6 +365,11 @@ def _allowed_statuses() -> set[str]:
365
365
 
366
366
 
367
367
 
368
+
369
+
370
+
371
+
372
+
368
373
  allowed = {"stable"}
369
374
  if _truthy_env(_BETA_GATE_ENV):
370
375
  allowed.add("beta")
@@ -2,6 +2,6 @@
2
2
  "skillName": "template-registry",
3
3
  "repoName": "agent-skill-media-maker",
4
4
  "skillId": "475",
5
- "version": "V28",
5
+ "version": "V29",
6
6
  "skillDescription": "Video-template registry skill. Stores every video-template definition and lists the available templates (templateId / name / aspect ratio / style tags).\n\nUse this skill as soon as the user mentions any of these intents:\n- View available templates / list every template\n\nNote: DSL→TemplateBinding is no longer a separate exposed step — once prepare_video_assets receives a template_id it builds the binding internally."
7
7
  }
@@ -1014,6 +1014,10 @@ def _walk_payload(
1014
1014
 
1015
1015
 
1016
1016
 
1017
+
1018
+
1019
+
1020
+
1017
1021
  errors = _check_payload_object(
1018
1022
  data,
1019
1023
  schema,
@@ -1086,6 +1090,10 @@ def _check_payload_object(
1086
1090
  "the slide renders its built-in placeholder instead of your content.",
1087
1091
  ))
1088
1092
 
1093
+ for key, sub in merged_props.items():
1094
+ if key in data and isinstance(sub, dict):
1095
+ errors += _enum_errors(data[key], sub, path=f"{path}.{key}", scene=scene, key=key)
1096
+
1089
1097
 
1090
1098
 
1091
1099
  for key, sub in props.items():
@@ -1104,6 +1112,41 @@ def _check_payload_object(
1104
1112
  return errors
1105
1113
 
1106
1114
 
1115
+ def _enum_errors(
1116
+ value: object, schema: dict, *, path: str, scene: str, key: str
1117
+ ) -> list[ValidationError]:
1118
+
1119
+
1120
+
1121
+
1122
+
1123
+ enum = schema.get("enum")
1124
+ if isinstance(enum, list) and enum:
1125
+ if value is None or isinstance(value, (dict, list)) or value in enum:
1126
+ return []
1127
+ hint = ""
1128
+ if isinstance(value, str):
1129
+ hit = difflib.get_close_matches(
1130
+ value, [e for e in enum if isinstance(e, str)], n=1, cutoff=0.6
1131
+ )
1132
+ if hit:
1133
+ hint = f" Did you mean '{hit[0]}'?"
1134
+ allowed = ", ".join(repr(e) for e in enum)
1135
+ return [ValidationError(
1136
+ path,
1137
+ f"scene '{scene}': '{key}' is {value!r}, which is not one of [{allowed}] — the "
1138
+ "template ignores an unknown value and falls back to its default, so the look "
1139
+ "you asked for silently never appears." + hint,
1140
+ )]
1141
+ items = schema.get("items")
1142
+ if schema.get("type") == "array" and isinstance(items, dict) and isinstance(value, list):
1143
+ errors: list[ValidationError] = []
1144
+ for i, item in enumerate(value):
1145
+ errors += _enum_errors(item, items, path=f"{path}[{i}]", scene=scene, key=f"{key}[{i}]")
1146
+ return errors
1147
+ return []
1148
+
1149
+
1107
1150
  def _is_blank(value: object) -> bool:
1108
1151
 
1109
1152
 
@@ -59,13 +59,22 @@ def required_payload_errors(template: dict, props: dict) -> list[str]:
59
59
 
60
60
  def asset_references(value, path: str = "", *, bound_props: bool = False):
61
61
 
62
+
63
+
64
+
65
+
66
+
62
67
  if isinstance(value, list):
63
68
  for index, item in enumerate(value):
64
69
  yield from asset_references(item, f"{path}[{index}]", bound_props=bound_props)
65
70
  elif isinstance(value, dict):
66
71
  for key, child in value.items():
67
72
  child_path = f"{path}.{key}" if path else key
68
- if key == "assetRef" or (bound_props and key.endswith("AssetId")):
73
+ if (
74
+ key == "assetRef"
75
+ or key.endswith("AssetRef")
76
+ or (bound_props and key.endswith("AssetId"))
77
+ ):
69
78
  if isinstance(child, str) and child.strip():
70
79
  yield child_path, child
71
80
  else:
@@ -462,7 +462,9 @@ def compose_dsl_image_prompts(dsl: dict, template: Optional[dict]) -> list[str]:
462
462
 
463
463
 
464
464
  meta = dsl.get("meta") or {}
465
- variant_id = str(meta.get("variantId") or "")
465
+
466
+
467
+ variant_id = str(meta.get("templateVariant") or meta.get("variantId") or "")
466
468
  if not is_compose_mode(resolve_style_guide(template, variant_id)):
467
469
  return []
468
470