@remixmate/cli 0.9.50 → 0.9.52

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/README.md +14 -26
  2. package/dist/manifest.json +27 -14
  3. package/package.json +10 -13
  4. package/skills/doc-parse/SKILL.md +6 -6
  5. package/skills/doc-parse/scripts/extract_via_render.py +3 -2
  6. package/skills/doc-parse/skill.json +1 -1
  7. package/skills/gen-script/SKILL.md +19 -18
  8. package/skills/gen-script/scripts/gen_script.py +170 -38
  9. package/skills/gen-script/skill.json +14 -4
  10. package/skills/gen-script/version.json +1 -1
  11. package/skills/prepare-video-assets/SKILL.md +3 -3
  12. package/skills/prepare-video-assets/skill.json +1 -1
  13. package/skills/prepare-video-assets/version.json +1 -1
  14. package/skills/render-video/scripts/render_video.py +53 -42
  15. package/skills/render-video/version.json +1 -1
  16. package/skills/template-registry/README.md +4 -15
  17. package/skills/template-registry/SKILL.md +20 -52
  18. package/skills/template-registry/scripts/countdown_spec.py +56 -6
  19. package/skills/template-registry/scripts/list_templates.py +133 -8
  20. package/skills/template-registry/scripts/registry_loader.py +3 -4
  21. package/skills/template-registry/skill.json +2 -2
  22. package/skills/template-registry/version.json +1 -1
  23. package/skills/template-registry/video_dsl/README.md +10 -53
  24. package/skills/template-registry/video_dsl/runtime/__init__.py +0 -1
  25. package/skills/template-registry/video_dsl/runtime/dsl_validator.py +108 -35
  26. package/skills/template-registry/video_dsl/runtime/narration_budget.py +129 -0
  27. package/skills/template-registry/video_dsl/schema/video-dsl-v1alpha1.json +1 -1
  28. package/skills/template-registry/scripts/check_contracts.py +0 -179
  29. package/skills/template-registry/scripts/sync_registry.py +0 -44
  30. package/skills/template-registry/scripts/template_paths.py +0 -72
  31. package/skills/template-registry/scripts/verify_props_contract.py +0 -273
  32. package/skills/template-registry/video_dsl/runtime/template_binder.py +0 -87
@@ -36,7 +36,7 @@ if os.path.isdir(_TEMPLATE_DSL_DIR):
36
36
  sys.path.insert(0, os.path.abspath(os.path.join(_TEMPLATE_DSL_DIR, "..")))
37
37
 
38
38
  # Shared Python lib lives under skills/template-registry/scripts/ — the de-facto
39
- # cross-skill module location (registry_loader, template_paths, etc.). See
39
+ # cross-skill module location (registry_loader, countdown_spec, etc.). See
40
40
  # AGENTS.md for the convention.
41
41
  _SHARED_SCRIPTS_DIR = os.path.join(
42
42
  os.path.dirname(__file__), "..", "..", "template-registry", "scripts"
@@ -54,6 +54,7 @@ try:
54
54
  resolve_count as resolve_countdown_count,
55
55
  resolve_countdown_spec,
56
56
  scene_payload as countdown_scene_payload,
57
+ stated_counts as countdown_stated_counts,
57
58
  )
58
59
  _HAS_COUNTDOWN = True
59
60
  except ImportError:
@@ -72,6 +73,14 @@ except ImportError:
72
73
  _HAS_ENHANCER = False
73
74
  SKELETON_PREFIXES = ("[骨架待填充]", "[skeleton placeholder]")
74
75
 
76
+ # 旁白字数预算:写进骨架占位旁白,让模型写脚本时就知道这一场放得下多少字。
77
+ # 守护式导入同上;缺失时占位文案退回不带预算的旧格式。
78
+ try:
79
+ from video_dsl.runtime import narration_budget as _nb
80
+ _HAS_BUDGET = True
81
+ except ImportError:
82
+ _HAS_BUDGET = False
83
+
75
84
  # 场景数上限的唯一实现在 dsl_validator(它同时是门禁方)。守护式导入的理由同上:
76
85
  # 共享目录可能不在(单测 / 裁剪安装),那时退回历史常量 20,行为与改动前一致。
77
86
  try:
@@ -109,7 +118,8 @@ except ImportError:
109
118
  VALID_RATIOS = ["16:9", "9:16", "1:1", "4:3", "3:4", "21:9"]
110
119
  VALID_PURPOSES = ["opening", "point", "example", "explanation", "highlight", "cta", "ending"]
111
120
 
112
- CHARS_PER_SECOND = 5.0 # 与 timeline_compiler.py 保持一致(中文约 5 字/秒)
121
+ # 规划用语速 / 行间停顿 / 场景尾巴统一在 video_dsl.runtime.narration_budget。
122
+ # 这里原有的 CHARS_PER_SECOND = 5.0 从未参与计算(只出现在注释里),已删。
113
123
 
114
124
  # ── Language contract (mirrors apps/ab-skill/src/handlers/template-meta.ts) ──
115
125
  # template.json may declare:
@@ -229,20 +239,25 @@ def _localize_label(language, key, **fmt):
229
239
  "label_point": {"zh": "内容要点 {n}", "en": "Point {n}"},
230
240
  "label_cta": {"zh": "结尾 CTA", "en": "Closing CTA"},
231
241
  "narration_opening": {
232
- "zh": "[骨架待填充] 关于{topic}的开场 Hook({duration}s)",
233
- "en": "[skeleton placeholder] Opening hook about {topic} ({duration}s)",
242
+ "zh": "[骨架待填充] 关于{topic}的开场 Hook({duration}s{budget})",
243
+ "en": "[skeleton placeholder] Opening hook about {topic} ({duration}s{budget})",
234
244
  },
235
245
  "narration_point": {
236
- "zh": "[骨架待填充] {label}:关于{topic}的一个要点({duration}s)",
237
- "en": "[skeleton placeholder] {label}: a point about {topic} ({duration}s)",
246
+ "zh": "[骨架待填充] {label}:关于{topic}的一个要点({duration}s{budget})",
247
+ "en": "[skeleton placeholder] {label}: a point about {topic} ({duration}s{budget})",
238
248
  },
239
249
  "narration_cta": {
240
- "zh": "[骨架待填充] 关于{topic}的 CTA 结尾({duration}s)",
241
- "en": "[skeleton placeholder] Closing CTA about {topic} ({duration}s)",
250
+ "zh": "[骨架待填充] 关于{topic}的 CTA 结尾({duration}s{budget})",
251
+ "en": "[skeleton placeholder] Closing CTA about {topic} ({duration}s{budget})",
252
+ },
253
+ # {budget} 由 _budget_suffix 填:「,整段≤32 字;分行每多一行约少 4 字」。
254
+ "budget_suffix": {
255
+ "zh": ",整段≤{chars} 字;分行旁白每多一行约少 {per_line} 字",
256
+ "en": ", ≤{chars} words as one block; ~{per_line} fewer per extra narration line",
242
257
  },
243
258
  "narration_default": {
244
- "zh": "[骨架待填充] 关于{topic}的内容({duration}s)",
245
- "en": "[skeleton placeholder] Content about {topic} ({duration}s)",
259
+ "zh": "[骨架待填充] 关于{topic}的内容({duration}s{budget})",
260
+ "en": "[skeleton placeholder] Content about {topic} ({duration}s{budget})",
246
261
  },
247
262
  # NOTE: 这里曾有 "point_description"("关于{topic}的要点内容")与
248
263
  # "more_about"("更多关于{topic}的内容")两条,用来给 no-visual 场景拼画面
@@ -256,6 +271,9 @@ def _localize_label(language, key, **fmt):
256
271
  }
257
272
  entry = table.get(key, {})
258
273
  template = entry.get(language) or entry.get(DEFAULT_LANGUAGE) or ""
274
+ # 字数预算只有按场景规划的路径才算得出来(轮播 / 按字幕定时长的模板没有
275
+ # 「每场秒数」这个前提),没给就是不带预算的旧格式。
276
+ fmt.setdefault("budget", "")
259
277
  return template.format(**fmt)
260
278
 
261
279
 
@@ -362,17 +380,47 @@ def plan_scenes(
362
380
  return scenes
363
381
 
364
382
 
365
- def estimate_narration_text(label: str, purpose: str, duration: int, topic: str, language: str = DEFAULT_LANGUAGE) -> str:
383
+ def _budget_suffix(duration: int, language: str, speed: float, tail_pad_sec: float | None) -> str:
384
+ """占位旁白里的字数预算。拿不到预算模块时返回空串(退回旧格式)。
385
+
386
+ 为什么写进占位文案而不是单独字段:模型填旁白时一定会读到这一句,而骨架多一个
387
+ 字段就得在校验白名单、TTS payload 两头都处理,漏一头就是新的静默问题。
388
+ """
389
+ if not _HAS_BUDGET:
390
+ return ""
391
+ tail = _nb.DEFAULT_TAIL_PAD_SEC if tail_pad_sec is None else tail_pad_sec
392
+ lang = "zh" if language == "zh" else "en"
393
+ chars = _nb.speech_budget(duration, speed, lang, lines=1, tail_pad_sec=tail)
394
+ per_line = max(1, round(_nb.LINE_PAUSE_SEC * _nb.speech_rate(lang, speed)))
395
+ return _localize_label(language, "budget_suffix", chars=chars, per_line=per_line)
396
+
397
+
398
+ def estimate_narration_text(
399
+ label: str,
400
+ purpose: str,
401
+ duration: int,
402
+ topic: str,
403
+ language: str = DEFAULT_LANGUAGE,
404
+ speed: float = 1.0,
405
+ tail_pad_sec: float | None = None,
406
+ ) -> str:
366
407
  """Generate a short skeleton narration hint for a scene.
367
408
 
368
409
  This returns a *skeleton only* — the agent is expected to replace this
369
410
  text with real narration before submitting DSL to prepare_video_assets. We
370
411
  deliberately return a short, clearly-placeholder-looking string rather
371
- than padding to duration × CHARS_PER_SECOND, because padding with a
412
+ than padding to the scene's length, because padding with a
372
413
  repeating filler sentence historically caused the skeleton to be
373
414
  submitted verbatim to TTS (audio narration mismatch bug).
415
+
416
+ It does carry the scene's narration *budget* (see narration_budget): the
417
+ one number the agent needs while writing, and never had — a 2026-09-27
418
+ session wrote 45–58 chars into 8s scenes and shipped 117s for a 75s target.
374
419
  """
375
- fmt = {"label": label, "topic": topic, "duration": duration}
420
+ fmt = {
421
+ "label": label, "topic": topic, "duration": duration,
422
+ "budget": _budget_suffix(duration, language, speed, tail_pad_sec),
423
+ }
376
424
  if purpose == "opening":
377
425
  return _localize_label(language, "narration_opening", **fmt)
378
426
  if purpose == "point":
@@ -1267,31 +1315,18 @@ def _plan_countdown_scenes(spec: dict, count: int, duration: float, headline: st
1267
1315
  ]
1268
1316
 
1269
1317
 
1270
- def _warn_on_count_mismatch(headline: str, subheadline: str, count: int) -> None:
1318
+ def _warn_on_count_mismatch(spec: dict, headline: str, subheadline: str, count: int) -> None:
1271
1319
  """画面文字里写的数量与实际节拍数不一致时出声提醒(不阻断、不改写)。
1272
1320
 
1273
- **刻意只 warn 不改写。** 这里曾经有一对 ``_rank_copy`` / ``_rank_title``,是
1274
- github-repo-rank 的 ``payload.ts`` 里 ``formatRankCopy`` / ``formatRankTitle``
1275
- 的逐行 Python 移植 —— 同样的三个正则,在两个仓库、两种语言里各存一份。那是本文件
1276
- 在 ``_echo_template_llm_hint`` 里写明要避免的事:**CLI 出机制,模板出内容**。标题
1277
- 该怎么写、数量该以什么措辞出现(``Top 5`` / ``前 5 名`` / ``5 个项目``)是模板的
1278
- 版式事实,真源是模板自己。
1279
-
1280
- 去掉那份移植不会让成片出错:模板在渲染时本来就会同步(github-repo-rank 在
1281
- ``Opening.tsx`` / ``Cover.tsx`` 里做),而且它**必须**保留这个能力 —— 手写 DSL、
1282
- 历史计划、ab-web 改过的标题都不经过 gen_script。CLI 这一份自始至终只影响用户在
1283
- 确认脚本那一步看到的文字,为它复制一套会漂移的正则不值得。
1321
+ **刻意只 warn 不改写。** 标题该怎么写、数量以什么措辞出现是模板的版式事实,
1322
+ 模板在渲染时本来就会同步;手写 DSL、历史计划、ab-web 改过的标题都不经过
1323
+ gen_script,所以这里替它改既不必要,也会静默改掉调用方给的文案。
1284
1324
 
1285
- 换成告警的另一个好处:静默改写调用方给的文案本身就不妥(同 ``_warn_on_url_text_layer``
1286
- 的分寸)。把数量告诉它,让它自己把文案写对,比替它改更不容易出意外。
1287
-
1288
- 这里认的措辞(``Top N`` / ``N 个项目`` / ``前 N 名``)只用来**发现不一致**,
1289
- 不用来生成文案,所以它不构成一份需要跟模板同步的版式规则。
1325
+ 认哪些措辞由模板的 ``countdown.countPatterns`` 声明(见 countdown_spec),
1326
+ CLI 不内置任何词表;没声明的模板不做这项检查。
1290
1327
  """
1291
- pattern = r"(?<![a-zA-Z0-9_])top\s*([0-9]+)(?![a-zA-Z0-9_])|(?:^|[^0-9])([0-9]+)\s*(?:个(?=\s*(?:开源项目|项目|仓库|工具))|(?=\s*名))"
1292
1328
  for flag, value in (("--headline", headline), ("--subheadline", subheadline)):
1293
- stated = {int(g) for match in re.finditer(pattern, value or "", flags=re.I)
1294
- for g in match.groups() if g is not None}
1329
+ stated = countdown_stated_counts(spec, value)
1295
1330
  if stated and count not in stated:
1296
1331
  print(
1297
1332
  f"⚠️ {flag} says {sorted(stated)} item(s) but --item-count is {count}. "
@@ -1305,6 +1340,73 @@ def _warn_on_count_mismatch(headline: str, subheadline: str, count: int) -> None
1305
1340
  )
1306
1341
 
1307
1342
 
1343
+ def _template_background_presets(template_config: dict | None) -> list[str]:
1344
+ """模板在 ``customPayloadSchema.background.preset`` 声明的背景取值(没声明 = 空表)。
1345
+
1346
+ 背景词汇是**模板自己的**:同样叫「网格」,不同模板的取值名各不相同,默认值也
1347
+ 各不相同。所以这里只读声明,不在 CLI 里维护任何取值表。
1348
+ """
1349
+ schema = (template_config or {}).get("customPayloadSchema") or {}
1350
+ background = schema.get("background") if isinstance(schema, dict) else None
1351
+ props = background.get("properties") if isinstance(background, dict) else None
1352
+ preset = props.get("preset") if isinstance(props, dict) else None
1353
+ enum = preset.get("enum") if isinstance(preset, dict) else None
1354
+ return [str(v) for v in enum] if isinstance(enum, list) else []
1355
+
1356
+
1357
+ def _apply_look(
1358
+ dsl: dict,
1359
+ template_config: dict | None,
1360
+ template_id: str | None,
1361
+ variant: str | None,
1362
+ background_preset: str | None,
1363
+ ) -> None:
1364
+ """把用户选的视觉风格 / 背景样式写进 DSL(就地改)。
1365
+
1366
+ 这两个值以前没有任何一跳能落进管线:复刻弹窗让用户选了,gen_script 却只写模板
1367
+ 默认背景,merge 模式又只替换旁白——成片照出、不报错,用户只觉得「好像没生效」。
1368
+
1369
+ - ``--variant`` → ``meta.templateVariant``:match_template 把它当**显式**选择,
1370
+ 优先于 meta.style 的模糊匹配,最终成为 inputProps.variantId;
1371
+ - ``--background-preset`` → 每个场景 ``customPayload.background.preset``:
1372
+ customPayload 原样透传给模板,模板读的正是这个字段。保留模板默认背景里的其它键。
1373
+
1374
+ 取值不在模板声明里就直接退出:静默丢掉用户的选择,正是这里要修的那个 bug。
1375
+ """
1376
+ if not variant and not background_preset:
1377
+ return
1378
+ if not template_id or template_config is None:
1379
+ raise ValueError("--variant / --background-preset need --template-id: the allowed values come from the template.")
1380
+
1381
+ if variant:
1382
+ variants = list((template_config.get("variants") or {}).keys())
1383
+ if variant not in variants:
1384
+ raise ValueError(
1385
+ f"--variant {variant!r} is not a variant of template {template_id!r}; "
1386
+ f"choose one of: {', '.join(variants) or '(this template declares no variants)'}"
1387
+ )
1388
+ dsl.setdefault("meta", {})["templateVariant"] = variant
1389
+
1390
+ if background_preset:
1391
+ presets = _template_background_presets(template_config)
1392
+ if background_preset not in presets:
1393
+ raise ValueError(
1394
+ f"--background-preset {background_preset!r} is not supported by template {template_id!r}; "
1395
+ f"choose one of: {', '.join(presets) or '(this template has no background option)'}"
1396
+ )
1397
+ for scene in dsl.get("scenes", []):
1398
+ payload = scene.setdefault("customPayload", {})
1399
+ existing = payload.get("background") if isinstance(payload.get("background"), dict) else {}
1400
+ payload["background"] = {**existing, "preset": background_preset}
1401
+ if background_preset in ("image", "video"):
1402
+ print(
1403
+ f"ℹ️ background preset {background_preset!r} needs a user asset: set "
1404
+ f"customPayload.background.assetRef on each scene to that asset's id, "
1405
+ f"otherwise the template falls back to its default background.",
1406
+ file=sys.stderr,
1407
+ )
1408
+
1409
+
1308
1410
  def build_dsl(
1309
1411
  topic: str,
1310
1412
  duration: int,
@@ -1393,7 +1495,7 @@ def build_dsl(
1393
1495
  _echo_template_llm_hint(template_config, template_id)
1394
1496
  _warn_on_url_text_layer(effective_headline, effective_subheadline)
1395
1497
  if beat_count is not None:
1396
- _warn_on_count_mismatch(effective_headline, effective_subheadline, beat_count)
1498
+ _warn_on_count_mismatch(countdown, effective_headline, effective_subheadline, beat_count)
1397
1499
 
1398
1500
  # ── carousel-caption 模板(单场景、图/视频轮播 + 打字机)单独装配并直接返回 ──
1399
1501
  if payload_style == "carousel-caption":
@@ -1522,6 +1624,7 @@ def build_dsl(
1522
1624
  image_compose = _HAS_ENHANCER and is_compose_mode(image_style_guide)
1523
1625
  image_ratio = image_style_guide.get("ratio") or ratio
1524
1626
 
1627
+ tail_pad_sec = _nb.template_tail_pad_sec(template_config) if _HAS_BUDGET else None
1525
1628
  for idx, plan in enumerate(scene_plans):
1526
1629
  scene_id = f"scene-{idx + 1:02d}"
1527
1630
  # 视觉素材的 assetId 命名遵循类型前缀:image → img-,video → video-
@@ -1531,7 +1634,10 @@ def build_dsl(
1531
1634
  visual_asset_id = f"img-{scene_id}"
1532
1635
  narration_asset_id = f"narration-{scene_id}" if needs_narration else None
1533
1636
  narration_text = (
1534
- estimate_narration_text(plan["label"], plan["purpose"], plan["duration"], topic, language=output_language)
1637
+ estimate_narration_text(
1638
+ plan["label"], plan["purpose"], plan["duration"], topic, language=output_language,
1639
+ speed=narration_speed, tail_pad_sec=tail_pad_sec,
1640
+ )
1535
1641
  if needs_narration else ""
1536
1642
  )
1537
1643
 
@@ -1817,7 +1923,9 @@ def build_dsl(
1817
1923
  f"is only a placeholder.\n"
1818
1924
  f" Write the full DSL yourself from the user's material (scenes, assets, visuals, "
1819
1925
  f"customPayload) and submit all of it to prepare_video_assets — it is used as-is, "
1820
- f"not merged onto this skeleton. Keep meta.templateId.",
1926
+ f"not merged onto this skeleton. Keep meta.templateId, and copy meta.templateVariant and "
1927
+ f"each scene's customPayload.background from this skeleton when present (they carry the "
1928
+ f"user's --variant / --background-preset choice).",
1821
1929
  file=sys.stderr,
1822
1930
  )
1823
1931
 
@@ -1846,7 +1954,7 @@ Examples:
1846
1954
  parser.add_argument(
1847
1955
  "--source-document-url", default=None,
1848
1956
  help="Source document URL (.pptx) for templates that build their per-scene payload from a "
1849
- "document (capabilities.payloadHydration, e.g. ppt-to-video). Recorded at "
1957
+ "document (capabilities.payloadHydration). Recorded at "
1850
1958
  "meta.sourceDocument; prepare_video_assets then fetches the layout itself and fills "
1851
1959
  "every scene's customPayload. Pass the same URL you gave doc_parse. Without it such a "
1852
1960
  "template's DSL is rejected downstream.",
@@ -1875,6 +1983,19 @@ Examples:
1875
1983
  ),
1876
1984
  )
1877
1985
  parser.add_argument("--font-name", default=None, help="Human-readable font name. Logged and stored for display only; never used for rendering.")
1986
+ parser.add_argument(
1987
+ "--variant", default=None,
1988
+ help="Template visual variant id (a key of the template's `variants`, listed as variantIds by "
1989
+ "template-registry --list-templates). Written to meta.templateVariant, which overrides the "
1990
+ "fuzzy --style match. When the user picked a style in the replicate card (shown as "
1991
+ "`名字(id)`, e.g. `紫色科技(tech-purple)`), pass that id. Requires --template-id.",
1992
+ )
1993
+ parser.add_argument(
1994
+ "--background-preset", default=None,
1995
+ help="Scene background preset (one of the template's customPayloadSchema.background.preset enum). "
1996
+ "Written to every scene's customPayload.background.preset. When the user picked a background in "
1997
+ "the replicate card (e.g. `纯色(solid)`), pass that id. Requires --template-id.",
1998
+ )
1878
1999
  parser.add_argument("--allow-digital-human", action="store_true", help="Allow digital-human assets")
1879
2000
  parser.add_argument("--allow-ai-video", action="store_true", help="Allow AI-generated video assets")
1880
2001
  parser.add_argument("--template-id", default=None, help="Template id. The template owns outputLanguage and may declare a defaultVoiceId.")
@@ -1920,7 +2041,7 @@ Examples:
1920
2041
  "--scene-images",
1921
2042
  action="append",
1922
2043
  default=None,
1923
- help="One scene's visual for a visual-overlay template (image-slide etc.). "
2044
+ help="One scene's visual for a visual-overlay template (capabilities.payloadStyle). "
1924
2045
  "Can be repeated: --scene-images url1 --scene-images ai --scene-images url2. "
1925
2046
  "Each entry is either a URL (use that existing asset) or the literal 'ai' / "
1926
2047
  "'ai:<prompt>' (leave this scene to gen-image, optionally with the user's prompt). "
@@ -2073,6 +2194,17 @@ Examples:
2073
2194
  # as a message instead of a traceback.
2074
2195
  parser.error(str(exc))
2075
2196
 
2197
+ try:
2198
+ _apply_look(
2199
+ dsl,
2200
+ _load_template_config(args.template_id) if args.template_id else None,
2201
+ args.template_id,
2202
+ args.variant,
2203
+ args.background_preset,
2204
+ )
2205
+ except ValueError as exc:
2206
+ parser.error(str(exc))
2207
+
2076
2208
  # Post-process for --skip-asset-generation: rewrite all gen-voice / gen-digital-human
2077
2209
  # assets to be already-generated stubs. Image / video are already covered by the
2078
2210
  # stub_image_url / stub_video_url params threaded through build_dsl above.
@@ -34,7 +34,7 @@
34
34
  },
35
35
  "scenes": {
36
36
  "type": "number",
37
- "description": "Scene count. Only applies to templates with capabilities.sceneStrategy=arc (the default); single / fixed / countdown templates own their own count. Bounded below by 3 and above by the template's capabilities.maxScenes (default 20) — a clamp is reported on stderr and is NOT an error, so always read the real count back from the returned DSL (scenes[].length) before describing the script to the user. For a template that maps one scene per source page (e.g. ppt-to-video), pass the page count you saw in doc_parse's summary."
37
+ "description": "Scene count. Only applies to templates with capabilities.sceneStrategy=arc (the default); single / fixed / countdown templates own their own count. Bounded below by 3 and above by the template's capabilities.maxScenes (default 20) — a clamp is reported on stderr and is NOT an error, so always read the real count back from the returned DSL (scenes[].length) before describing the script to the user. For a template that maps one scene per source page (capabilities.payloadHydration), pass the page count you saw in doc_parse's summary."
38
38
  },
39
39
  "voice_id": {
40
40
  "type": "string",
@@ -44,13 +44,21 @@
44
44
  "type": "number",
45
45
  "description": "Narration speech rate, 0.5-2.0 (default 1.0). Stored at global.narration.speed and applied when prepare_video_assets synthesizes the voice-over. **Must** pass when the user picked a speech rate — a template's 语速 / speed parameter arrives as a multiplier like 1.1, and without this flag it is silently dropped and the narration comes back at 1.0. Above ~1.3 subtitle alignment drifts and the delivery sounds mechanical; suggest shortening the script instead of raising it further."
46
46
  },
47
+ "variant": {
48
+ "type": "string",
49
+ "description": "Template visual variant id (a key of the template's variants; template_registry lists them as variantIds). Written to meta.templateVariant, which beats the fuzzy `style` match. **Must** pass when the user picked a style: the replicate card shows it as `名字(id)` — e.g. `视觉风格: 紫色科技(tech-purple)` or `板面颜色: 黑板(black-board)` → pass the id in the parentheses. Without it the choice is silently dropped. Requires template_id; an id the template does not declare is rejected."
50
+ },
51
+ "background_preset": {
52
+ "type": "string",
53
+ "description": "Scene background preset, one of the template's customPayloadSchema.background.preset values. Written to every scene's customPayload.background.preset. **Must** pass when the user picked a background: the replicate card shows `背景样式: 纯色(solid)` → pass `solid`. Without it the scene keeps the template default. Requires template_id; unsupported values are rejected. `image` / `video` also need customPayload.background.assetRef on each scene."
54
+ },
47
55
  "template_id": {
48
56
  "type": "string",
49
- "description": "Template id (e.g. html-slide). The template owns outputLanguage and may also declare a defaultVoiceId; both flow into the produced DSL."
57
+ "description": "Template id (from template_registry). The template owns outputLanguage and may also declare a defaultVoiceId; both flow into the produced DSL."
50
58
  },
51
59
  "source_document_url": {
52
60
  "type": "string",
53
- "description": "Source document URL (.pptx) for a template that builds its per-scene picture from that document (capabilities.payloadHydration — today: ppt-to-video). **Must** pass it for such a template: it is recorded at meta.sourceDocument, and prepare_video_assets then fetches the layout itself and fills every scene's customPayload.slide. Pass the same URL you gave doc_parse. You are NOT expected to author or even read that layout data — one page is hundreds of KB, so it never travels through the conversation; without this reference the DSL is rejected downstream after you have already written all the narration."
61
+ "description": "Source document URL (.pptx) for a template that builds its per-scene picture from that document (capabilities.payloadHydration). **Must** pass it for such a template: it is recorded at meta.sourceDocument, and prepare_video_assets then fetches the layout itself and fills the customPayload field the template declares (payloadHydration.payloadField) on every scene. Pass the same URL you gave doc_parse. You are NOT expected to author or even read that layout data — one page is hundreds of KB, so it never travels through the conversation; without this reference the DSL is rejected downstream after you have already written all the narration."
54
62
  },
55
63
  "headline": {
56
64
  "type": "string",
@@ -72,7 +80,7 @@
72
80
  "items": {
73
81
  "type": "string"
74
82
  },
75
- "description": "Per-scene visuals for a visual-overlay template (image-slide etc.) — the counterpart of carousel_items for templates that have real scenes. Each element is EITHER a full URL (use that existing asset for the scene, no gen-image call) OR the literal string 'ai' / 'ai:<prompt>' (leave that scene to gen-image; the text after the colon, when present, is the user's own image prompt for it). Mapping is POSITIONAL: element i is scene i, so an AI scene can sit anywhere in the order — pass ['url1','ai','url2'] verbatim when the user arranged it that way. Scenes past the last element still get an AI-generated image, so 2 images still produce a full video. When `scenes` is not set the scene count is raised (never lowered) to fit the elements, so nothing the user arranged is dropped and handing over fewer images does not shrink the video. **Must** pass when the user supplies images for such a template — otherwise their images are ignored entirely and every scene is AI-generated. Do NOT pass for carousel-caption templates; those take carousel_items."
83
+ "description": "Per-scene visuals for a visual-overlay template (capabilities.payloadStyle=visual-overlay) — the counterpart of carousel_items for templates that have real scenes. Each element is EITHER a full URL (use that existing asset for the scene, no gen-image call) OR the literal string 'ai' / 'ai:<prompt>' (leave that scene to gen-image; the text after the colon, when present, is the user's own image prompt for it). Mapping is POSITIONAL: element i is scene i, so an AI scene can sit anywhere in the order — pass ['url1','ai','url2'] verbatim when the user arranged it that way. Scenes past the last element still get an AI-generated image, so 2 images still produce a full video. When `scenes` is not set the scene count is raised (never lowered) to fit the elements, so nothing the user arranged is dropped and handing over fewer images does not shrink the video. **Must** pass when the user supplies images for such a template — otherwise their images are ignored entirely and every scene is AI-generated. Do NOT pass for carousel-caption templates; those take carousel_items."
76
84
  },
77
85
  "caption_lines": {
78
86
  "type": "array",
@@ -117,6 +125,8 @@
117
125
  "scenes",
118
126
  "voice_id",
119
127
  "speed",
128
+ "variant",
129
+ "background_preset",
120
130
  "template_id",
121
131
  "source_document_url",
122
132
  "headline",
@@ -2,6 +2,6 @@
2
2
  "skillName": "gen-script",
3
3
  "repoName": "agent-skill-media-maker",
4
4
  "skillId": "474",
5
- "version": "V16",
5
+ "version": "V18",
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
  }
@@ -73,7 +73,7 @@ The asset resolver invokes the following skills based on `AssetRef` declarations
73
73
  ```bash
74
74
  python3 <SkillDir>/scripts/prepare_video_assets.py \
75
75
  --dsl-json '<DSL_JSON_STRING>' \
76
- --template-id html-slide
76
+ --template-id <templateId>
77
77
  ```
78
78
 
79
79
  - `<DSL_JSON_STRING>` is the JSON the agent received from `gen_script` — pass inline; **do not** write it to disk.
@@ -86,7 +86,7 @@ python3 <SkillDir>/scripts/prepare_video_assets.py \
86
86
  ```bash
87
87
  python3 <SkillDir>/scripts/prepare_video_assets.py \
88
88
  --dsl my-video.dsl.json \
89
- --template-id html-slide
89
+ --template-id <templateId>
90
90
  ```
91
91
 
92
92
  `--template-id` automatically calls template-registry internally; or pass `--binding my-video.binding.json` if you have a pre-generated binding.
@@ -162,7 +162,7 @@ During dev / debug the user may want to exercise the pipeline without burning ge
162
162
  ```bash
163
163
  python3 <SkillDir>/scripts/prepare_video_assets.py \
164
164
  --dsl-json '<DSL_JSON_STRING>' \
165
- --template-id picture-book-en \
165
+ --template-id <templateId> \
166
166
  --stub-image-url "https://cdn.example.com/placeholder.jpg"
167
167
  ```
168
168
 
@@ -35,7 +35,7 @@
35
35
  },
36
36
  "source_document_url": {
37
37
  "type": "string",
38
- "description": "Source document URL (.pptx) for a template that builds its per-scene picture from that document (capabilities.payloadHydration — today: ppt-to-video). Normally unnecessary: gen_script already recorded it at meta.sourceDocument when you passed source_document_url there, and the cached skeleton carries it through. Pass it here only to recover when the skeleton lacks the reference — it saves a full gen_script round trip. The layout data is fetched here and written into every scene's customPayload.slide; you neither author nor see it."
38
+ "description": "Source document URL (.pptx) for a template that builds its per-scene picture from that document (capabilities.payloadHydration). Normally unnecessary: gen_script already recorded it at meta.sourceDocument when you passed source_document_url there, and the cached skeleton carries it through. Pass it here only to recover when the skeleton lacks the reference — it saves a full gen_script round trip. The layout data is fetched here and written into the customPayload field the template declares (payloadHydration.payloadField) on every scene; you neither author nor see it."
39
39
  },
40
40
  "binding_json": {
41
41
  "type": "string",
@@ -2,6 +2,6 @@
2
2
  "skillName": "prepare-video-assets",
3
3
  "repoName": "agent-skill-media-maker",
4
4
  "skillId": "476",
5
- "version": "V3",
5
+ "version": "V4",
6
6
  "skillDescription": "Asset-preparation skill: resolves and generates every asset (image / audio / video) referenced by a Video DSL, persists a RenderPlan to the database, and returns a `job_id` for the subsequent `render_video` call.\n\nUse this skill as soon as the user mentions any of these intents:\n- Generate / prepare video assets, resolve assets, render-ready\n- \"Make me a video about X\" (the agent calls gen_script → prepare_video_assets → render_video)\n- Regenerate one asset (image / audio) for a specific scene\n\nNext step: after the user confirms the resolved assets, call `render_video` with the `job_id` returned by this skill.\n\n⚠️ Stop-and-confirm gate: this skill runs only after the user has confirmed the script, and after it returns you must show the resolved assets and wait for the user's explicit confirmation. Never call `render_video` in the same turn."
7
7
  }