@remixmate/cli 0.12.8 → 0.12.9

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.8",
4
- "generatedAt": "2026-10-20T23:46:18.482Z",
3
+ "version": "0.12.9",
4
+ "generatedAt": "2026-10-21T03:02:36.247Z",
5
5
  "skills": [
6
6
  {
7
7
  "id": "doc-parse",
@@ -625,6 +625,17 @@
625
625
  ],
626
626
  "description": "Camera motion for every scene (scenes[].animationHints.motion), replacing the skeleton's alternating push-in / pull-out. **Must** pass when the user picked one: `运镜效果: 缓慢推近(kenburns-in)` → `kenburns-in`."
627
627
  },
628
+ "ending_payload": {
629
+ "type": "array",
630
+ "items": {
631
+ "type": "string"
632
+ },
633
+ "description": "Like payload, but `KEY=VALUE` is written only to the ending scene and locked there (e.g. `recap.cta=关注我,每周更新开源热榜` for github-repo-rank's 片尾引导). On countdown templates (Top-N lists) it also appends the ending scene after #1 — fill that scene's narration (a closing line) and its customPayload.item with the #1 entry. Without it a countdown ends on the #1 card. Requires template_id."
634
+ },
635
+ "avatar_id": {
636
+ "type": "string",
637
+ "description": "Digital-human avatar id (`dh_xxx`, the replicate card's 数字人主讲 / 数字人讲师 line) for templates declaring capabilities.digitalHuman (image-slide, ppt-to-video, html-slide, …). Adds one gen-digital-human asset referenced by every scene's visuals.avatar.assetRef: one continuous presenter whose lips follow the narration. Placement stays the template default; adjust with payload `digitalHuman.<field>=…`. Copy the id verbatim, never invent one. Costs digital-human credits, so pass it only when the user picked an avatar. Requires template_id."
638
+ },
628
639
  "font_id": {
629
640
  "type": "string",
630
641
  "description": "Font library key (font.uniq_id) for the on-screen text and subtitles. Written to global.font.fontId; match_template folds it into the binding's typography and ab-render resolves and installs the real font before rendering. **Must** pass when the user picked a font: the replicate card shows it as `名字(id)`, e.g. `字体: <名字>(<uniqId>)` → pass the id in the parentheses. Pass the opaque key, never a CSS family name or the display name — those fall back to the template font with no error. When the user names a font without its id, call gen_script with list_fonts=true first and take the id from that list; do not invent ids. Without it the choice is silently dropped and the video renders in the template's own font. Omit to follow the template's font pairing."
@@ -728,6 +739,8 @@
728
739
  "sequence_fit",
729
740
  "background_asset",
730
741
  "payload",
742
+ "ending_payload",
743
+ "avatar_id",
731
744
  "motion",
732
745
  "font_id",
733
746
  "font_name",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remixmate/cli",
3
- "version": "0.12.8",
3
+ "version": "0.12.9",
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",
@@ -486,6 +486,8 @@ python3 <SkillDir>/scripts/gen_script.py \
486
486
  | `--sequence-fit` | Image-sequence fill mode, one of `customPayloadSchema.sequence.fit` (`cover` fill and crop / `contain` whole picture over a blurred backdrop), written to `customPayload.sequence.fit`; beats the preset's fit. `auto` = template / preset default. | template / preset default |
487
487
  | `--background-asset` | URL of the user's background image or video (`.mp4/.mov/.webm/.m4v` → video). Added as existing asset `asset-background`; every scene's `customPayload.background` gets `preset: image` + `assetRef`, or `preset: video` + `videoAssetRef`. Conflicts with a different `--background-preset`; rejected when the template's background enum lacks that kind. | none |
488
488
  | `--payload KEY=VALUE` | Template-specific card choice, repeatable. `KEY` is a dotted path in `customPayloadSchema` (or `capabilities.frameLevelKeys`), e.g. `grade=四年级`, `watermarkText=…`, `preset=course`, `carousel.fit=cover`, `caption.charsPerSec=20`, `templateData.revealMode=progressive`. Value is converted to the declared type (true/false, numbers) and checked against enum / range; undeclared keys are rejected. Written to every scene with sibling keys kept, applied before fit-caption duration estimation. | template default |
489
+ | `--ending-payload KEY=VALUE` | Same as `--payload`, but written only to the ending scene, e.g. `recap.cta=关注我,每周更新开源热榜`. On countdown templates it also appends an ending scene after #1 (it carries `item.rank=1`; fill its narration and `item`). Errors when the script has no ending scene. | none |
490
+ | `--avatar-id` | Digital-human avatar id (`dh_xxx`) for templates declaring `capabilities.digitalHuman`. Adds asset `asset-avatar` (`type: avatar`, `source: gen-digital-human`, `payload.avatarId`) and sets `visuals.avatar.assetRef` on every scene whose slot reads it — one continuous presenter driven by the narration. Placement: template default, or `--payload digitalHuman.<field>=…` (a photo-only presenter is `--payload digitalHuman.posterUrl=<url>` without this flag). | none |
489
491
  | `--motion` | Camera motion for every scene (`animationHints.motion`): `static` / `kenburns-in` / `kenburns-out` / `pan-left` / `pan-right` / `float`. | skeleton alternates in / out |
490
492
  | `--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 |
491
493
  | `--font-name` | Display name of the picked font → `global.font.fontName` (display only, never used for rendering). Pass it with `--font-id`. | — |
@@ -503,7 +505,7 @@ python3 <SkillDir>/scripts/gen_script.py \
503
505
  | `--scene-images` | Repeatable. One scene's visual for a **`visual-overlay`** template, mapped **positionally**. Each entry is a URL (that existing asset) or `ai` / `ai:<prompt>` (that scene goes to gen-image, optionally with the user's own prompt) — so an AI scene can sit anywhere in the order. Scenes past the last entry still get AI images. Scene count is raised (never lowered) to fit the entries when `--scenes` is absent. **Required** whenever the user supplies images for such a template — omitting it silently ignores every image they gave. | — |
504
506
  | `--caption-lines` | Repeatable. On-screen caption line for `carousel-caption` templates → `customPayload.caption.lines`. Line limits and `**emphasis**` support are declared per template (see its `llmHint`). **Required** for `durationStrategy: fit-caption` templates — write the lines yourself if the user did not supply them. Optional only for `fit-images` templates. | — |
505
507
 
506
- Values written by `--background-preset`, `--background-asset`, `--payload` and `--motion` are listed in `meta.lockedScenePaths` (scene-relative paths such as `customPayload.carousel.fit`, `animationHints.motion`). When the agent later saves its own `dsl_json` in merge mode, ab-agent restores those paths from this skeleton after merging, so a model-written `carousel` / `templateData` / `background` object cannot wipe a card choice. Do not hand-write these keys in `dsl_json`; pass the flag.
508
+ Values written by `--background-preset`, `--background-asset`, `--payload`, `--ending-payload`, `--avatar-id` and `--motion` are listed in `meta.lockedScenePaths` (scene-relative paths such as `customPayload.carousel.fit`, `animationHints.motion`). When the agent later saves its own `dsl_json` in merge mode, ab-agent restores those paths from this skeleton after merging, so a model-written `carousel` / `templateData` / `background` object cannot wipe a card choice. Do not hand-write these keys in `dsl_json`; pass the flag. Templates that author their own scenes (`meta.authoredScenes`, e.g. screen-walkthrough) are not merged: there ab-agent writes each locked value that the skeleton holds on every scene (with one value) onto every submitted scene, and adds the skeleton assets those values reference (`asset-background`, `asset-avatar`) when the submitted `assets` lacks them.
507
509
 
508
510
  ## DSL generation principles
509
511
 
@@ -1430,7 +1430,12 @@ def _build_custom_payload(template_config: dict | None) -> dict:
1430
1430
  return {}
1431
1431
 
1432
1432
 
1433
- def _plan_countdown_scenes(spec: dict, count: int, duration: float, headline: str) -> list:
1433
+ def _plan_countdown_scenes(
1434
+ spec: dict, count: int, duration: float, headline: str, ending: bool = False
1435
+ ) -> list:
1436
+
1437
+
1438
+
1434
1439
 
1435
1440
 
1436
1441
 
@@ -1440,11 +1445,14 @@ def _plan_countdown_scenes(spec: dict, count: int, duration: float, headline: st
1440
1445
 
1441
1446
 
1442
1447
  head_sec = countdown_opening_sec(spec, count, duration)
1443
- beat_sec = (duration - head_sec) / count
1444
- return [{"purpose": "opening", "duration": head_sec, "label": headline}] + [
1448
+ beat_sec = (duration - head_sec) / (count + (1 if ending else 0))
1449
+ plans = [{"purpose": "opening", "duration": head_sec, "label": headline}] + [
1445
1450
  {"purpose": "point", "duration": beat_sec, "label": f"#{index}", "countdownIndex": index}
1446
1451
  for index in range(count, 0, -1)
1447
1452
  ]
1453
+ if ending:
1454
+ plans.append({"purpose": "ending", "duration": beat_sec, "label": "#1", "countdownIndex": 1})
1455
+ return plans
1448
1456
 
1449
1457
 
1450
1458
  def _warn_on_count_mismatch(spec: dict, headline: str, subheadline: str, count: int) -> None:
@@ -1797,6 +1805,102 @@ def _write_payload(custom_payload: dict, writes: PayloadWrites) -> None:
1797
1805
  node[parts[-1]] = value
1798
1806
 
1799
1807
 
1808
+ def _template_has_ending_slot(template_config: dict | None) -> bool:
1809
+ return isinstance(((template_config or {}).get("slotMapping") or {}).get("ending"), dict)
1810
+
1811
+
1812
+ def _apply_ending_payload(
1813
+ dsl: dict,
1814
+ template_config: dict | None,
1815
+ template_id: str | None,
1816
+ payload_specs: list[str] | None,
1817
+ ) -> None:
1818
+
1819
+
1820
+
1821
+
1822
+
1823
+
1824
+
1825
+ if not payload_specs:
1826
+ return
1827
+ if not template_id or template_config is None:
1828
+ raise ValueError("--ending-payload needs --template-id: the allowed keys come from the template.")
1829
+ try:
1830
+ writes = _parse_payload_specs(template_config, payload_specs)
1831
+ except ValueError as exc:
1832
+ raise ValueError(str(exc).replace("--payload ", "--ending-payload ", 1)) from None
1833
+ endings = [s for s in dsl.get("scenes", []) if s.get("purpose") == "ending"]
1834
+ if not endings:
1835
+ raise ValueError(
1836
+ "--ending-payload: this script has no ending scene to write to "
1837
+ f"(scene purposes: {', '.join(str(s.get('purpose')) for s in dsl.get('scenes', []))})."
1838
+ )
1839
+ for scene in endings:
1840
+ _write_payload(scene.setdefault("customPayload", {}), writes)
1841
+ for parts, _ in writes:
1842
+ _lock_scene_path(dsl, "customPayload." + ".".join(parts))
1843
+
1844
+
1845
+ AVATAR_ASSET_ID = "asset-avatar"
1846
+
1847
+
1848
+ def _slot_extracts(template_config: dict | None, purpose: str, source: str) -> bool:
1849
+
1850
+ mapping = (template_config or {}).get("slotMapping")
1851
+ if not isinstance(mapping, dict):
1852
+ return False
1853
+ slot = mapping.get(purpose) or mapping.get("default") or {}
1854
+ seen: set = set()
1855
+ while isinstance(slot, dict) and "$ref" in slot and slot["$ref"] not in seen:
1856
+ seen.add(slot["$ref"])
1857
+ slot = mapping.get(slot["$ref"]) or {}
1858
+ extractors = slot.get("propExtractors") if isinstance(slot, dict) else None
1859
+ return isinstance(extractors, dict) and any(
1860
+ isinstance(ex, dict) and ex.get("from") == source for ex in extractors.values()
1861
+ )
1862
+
1863
+
1864
+ def _apply_avatar(
1865
+ dsl: dict,
1866
+ template_config: dict | None,
1867
+ template_id: str | None,
1868
+ avatar_id: str | None,
1869
+ ) -> None:
1870
+
1871
+
1872
+
1873
+
1874
+
1875
+
1876
+ if not avatar_id:
1877
+ return
1878
+ if not template_id or template_config is None:
1879
+ raise ValueError("--avatar-id needs --template-id: the template decides where the presenter shows.")
1880
+ support = ((template_config.get("capabilities") or {}).get("digitalHuman") or {}).get("support")
1881
+ if support not in ("optional", "required"):
1882
+ raise ValueError(f"--avatar-id: template {template_id!r} does not declare capabilities.digitalHuman")
1883
+ scenes = [
1884
+ s for s in dsl.get("scenes", [])
1885
+ if _slot_extracts(template_config, str(s.get("purpose")), "visuals.avatar.assetRef")
1886
+ ]
1887
+ if not scenes:
1888
+ raise ValueError(f"--avatar-id: no scene of template {template_id!r} reads visuals.avatar.assetRef")
1889
+ if not any((s.get("audio") or {}).get("narration") for s in scenes):
1890
+ raise ValueError("--avatar-id: the presenter is driven by the narration, and this script has none")
1891
+ for scene in scenes:
1892
+ visuals = scene.get("visuals") if isinstance(scene.get("visuals"), dict) else {}
1893
+ scene["visuals"] = {**visuals, "avatar": {"assetRef": AVATAR_ASSET_ID}}
1894
+ dsl.setdefault("assets", []).append({
1895
+ "assetId": AVATAR_ASSET_ID,
1896
+ "type": "avatar",
1897
+ "source": "gen-digital-human",
1898
+ "status": "planned",
1899
+ "payload": {"avatarId": avatar_id},
1900
+ })
1901
+ _lock_scene_path(dsl, "visuals.avatar.assetRef")
1902
+
1903
+
1800
1904
  def build_dsl(
1801
1905
  topic: str,
1802
1906
  duration: int,
@@ -1824,6 +1928,7 @@ def build_dsl(
1824
1928
  sequence_preset: str | None = None,
1825
1929
  sequence_fit: str | None = None,
1826
1930
  payload_writes: PayloadWrites | None = None,
1931
+ countdown_ending: bool = False,
1827
1932
  ) -> dict:
1828
1933
 
1829
1934
 
@@ -1862,6 +1967,10 @@ def build_dsl(
1862
1967
  )
1863
1968
  beat_count = resolve_countdown_count(countdown, item_count, scene_count) if countdown else None
1864
1969
 
1970
+ countdown_ending = bool(countdown_ending and countdown)
1971
+ if countdown_ending and not _template_has_ending_slot(template_config):
1972
+ raise ValueError(f"--ending-payload: template {template_id!r} has no ending slot")
1973
+
1865
1974
  primary_visual_type = contract["primary_visual_type"]
1866
1975
  payload_style = contract["payload_style"]
1867
1976
 
@@ -1980,7 +2089,9 @@ def build_dsl(
1980
2089
  provided_slots = [s.strip() for s in (scene_images or []) if s and s.strip()]
1981
2090
 
1982
2091
  if beat_count is not None:
1983
- scene_plans = _plan_countdown_scenes(countdown, beat_count, duration, effective_headline)
2092
+ scene_plans = _plan_countdown_scenes(
2093
+ countdown, beat_count, duration, effective_headline, ending=countdown_ending
2094
+ )
1984
2095
  else:
1985
2096
  scene_plans = _plan_contract_scenes(contract, topic, duration, scene_count, output_language)
1986
2097
 
@@ -2465,6 +2576,8 @@ Examples:
2465
2576
  "pan-left / pan-right / float. Replaces the skeleton's alternating default.",
2466
2577
  )
2467
2578
  parser.add_argument("--allow-digital-human", action="store_true", help="Allow digital-human assets")
2579
+ parser.add_argument("--avatar-id", default=None, help="Digital-human avatar id (dh_xxx) for templates declaring capabilities.digitalHuman: adds one gen-digital-human asset referenced by every scene's visuals.avatar.assetRef (one continuous presenter driven by the narration). Placement stays the template default unless set with --payload digitalHuman.<field>=...")
2580
+ parser.add_argument("--ending-payload", action="append", default=None, metavar="KEY=VALUE", help="Like --payload, but written only to the ending scene (e.g. recap.cta=关注我,每周更新开源热榜). On countdown templates it also appends the ending scene after #1. Repeatable.")
2468
2581
  parser.add_argument("--allow-ai-video", action="store_true", help="Allow AI-generated video assets")
2469
2582
  parser.add_argument("--template-id", default=None, help="Template id. The template owns outputLanguage and may declare a defaultVoiceId.")
2470
2583
  parser.add_argument(
@@ -2705,6 +2818,7 @@ Examples:
2705
2818
  sequence_preset=args.sequence_preset,
2706
2819
  sequence_fit=args.sequence_fit,
2707
2820
  payload_writes=payload_writes,
2821
+ countdown_ending=bool(args.ending_payload),
2708
2822
  )
2709
2823
  except ValueError as exc:
2710
2824
 
@@ -2723,6 +2837,8 @@ Examples:
2723
2837
  args.background_asset,
2724
2838
  )
2725
2839
  _apply_scene_payload(dsl, look_config, args.template_id, args.payload, args.motion)
2840
+ _apply_ending_payload(dsl, look_config, args.template_id, args.ending_payload)
2841
+ _apply_avatar(dsl, look_config, args.template_id, args.avatar_id)
2726
2842
  except ValueError as exc:
2727
2843
  parser.error(str(exc))
2728
2844
 
@@ -87,6 +87,17 @@
87
87
  ],
88
88
  "description": "Camera motion for every scene (scenes[].animationHints.motion), replacing the skeleton's alternating push-in / pull-out. **Must** pass when the user picked one: `运镜效果: 缓慢推近(kenburns-in)` → `kenburns-in`."
89
89
  },
90
+ "ending_payload": {
91
+ "type": "array",
92
+ "items": {
93
+ "type": "string"
94
+ },
95
+ "description": "Like payload, but `KEY=VALUE` is written only to the ending scene and locked there (e.g. `recap.cta=关注我,每周更新开源热榜` for github-repo-rank's 片尾引导). On countdown templates (Top-N lists) it also appends the ending scene after #1 — fill that scene's narration (a closing line) and its customPayload.item with the #1 entry. Without it a countdown ends on the #1 card. Requires template_id."
96
+ },
97
+ "avatar_id": {
98
+ "type": "string",
99
+ "description": "Digital-human avatar id (`dh_xxx`, the replicate card's 数字人主讲 / 数字人讲师 line) for templates declaring capabilities.digitalHuman (image-slide, ppt-to-video, html-slide, …). Adds one gen-digital-human asset referenced by every scene's visuals.avatar.assetRef: one continuous presenter whose lips follow the narration. Placement stays the template default; adjust with payload `digitalHuman.<field>=…`. Copy the id verbatim, never invent one. Costs digital-human credits, so pass it only when the user picked an avatar. Requires template_id."
100
+ },
90
101
  "font_id": {
91
102
  "type": "string",
92
103
  "description": "Font library key (font.uniq_id) for the on-screen text and subtitles. Written to global.font.fontId; match_template folds it into the binding's typography and ab-render resolves and installs the real font before rendering. **Must** pass when the user picked a font: the replicate card shows it as `名字(id)`, e.g. `字体: <名字>(<uniqId>)` → pass the id in the parentheses. Pass the opaque key, never a CSS family name or the display name — those fall back to the template font with no error. When the user names a font without its id, call gen_script with list_fonts=true first and take the id from that list; do not invent ids. Without it the choice is silently dropped and the video renders in the template's own font. Omit to follow the template's font pairing."
@@ -190,6 +201,8 @@
190
201
  "sequence_fit",
191
202
  "background_asset",
192
203
  "payload",
204
+ "ending_payload",
205
+ "avatar_id",
193
206
  "motion",
194
207
  "font_id",
195
208
  "font_name",