@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.
- package/README.md +14 -26
- package/dist/manifest.json +27 -14
- package/package.json +10 -13
- package/skills/doc-parse/SKILL.md +6 -6
- package/skills/doc-parse/scripts/extract_via_render.py +3 -2
- package/skills/doc-parse/skill.json +1 -1
- package/skills/gen-script/SKILL.md +19 -18
- package/skills/gen-script/scripts/gen_script.py +170 -38
- package/skills/gen-script/skill.json +14 -4
- package/skills/gen-script/version.json +1 -1
- package/skills/prepare-video-assets/SKILL.md +3 -3
- package/skills/prepare-video-assets/skill.json +1 -1
- package/skills/prepare-video-assets/version.json +1 -1
- package/skills/render-video/scripts/render_video.py +53 -42
- package/skills/render-video/version.json +1 -1
- package/skills/template-registry/README.md +4 -15
- package/skills/template-registry/SKILL.md +20 -52
- package/skills/template-registry/scripts/countdown_spec.py +56 -6
- package/skills/template-registry/scripts/list_templates.py +133 -8
- package/skills/template-registry/scripts/registry_loader.py +3 -4
- package/skills/template-registry/skill.json +2 -2
- package/skills/template-registry/version.json +1 -1
- package/skills/template-registry/video_dsl/README.md +10 -53
- package/skills/template-registry/video_dsl/runtime/__init__.py +0 -1
- package/skills/template-registry/video_dsl/runtime/dsl_validator.py +108 -35
- package/skills/template-registry/video_dsl/runtime/narration_budget.py +129 -0
- package/skills/template-registry/video_dsl/schema/video-dsl-v1alpha1.json +1 -1
- package/skills/template-registry/scripts/check_contracts.py +0 -179
- package/skills/template-registry/scripts/sync_registry.py +0 -44
- package/skills/template-registry/scripts/template_paths.py +0 -72
- package/skills/template-registry/scripts/verify_props_contract.py +0 -273
- package/skills/template-registry/video_dsl/runtime/template_binder.py +0 -87
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
编译时间线,最终调用 Remotion 渲染引擎输出视频。
|
|
8
8
|
|
|
9
9
|
用法:
|
|
10
|
-
python render_video.py --dsl video.dsl.json --template-id
|
|
11
|
-
python render_video.py --dsl video.dsl.json --template-id
|
|
10
|
+
python render_video.py --dsl video.dsl.json --template-id <templateId>
|
|
11
|
+
python render_video.py --dsl video.dsl.json --template-id <templateId> --resolve-only
|
|
12
12
|
python render_video.py --render-plan video.render-plan.json
|
|
13
13
|
|
|
14
14
|
环境变量:
|
|
@@ -53,7 +53,7 @@ _SHARED_LIB_DIR = os.path.join(os.path.dirname(__file__), "..", "..", "template-
|
|
|
53
53
|
# Single sys.path setup for both cross-skill import surfaces of the shared lib:
|
|
54
54
|
# - <shared> for `video_dsl.runtime.*`
|
|
55
55
|
# - <shared>/scripts for `registry_loader`, `match_template`,
|
|
56
|
-
# `render_job_client`, `
|
|
56
|
+
# `render_job_client`, `countdown_spec`
|
|
57
57
|
# Each deeper function used to repeat its own sys.path.insert; consolidating
|
|
58
58
|
# here keeps the module's import side-effects in one place and matches
|
|
59
59
|
# Python's "set up sys.path once at module top" idiom.
|
|
@@ -516,7 +516,7 @@ def _hydrate_dsl_payload(
|
|
|
516
516
|
) -> None:
|
|
517
517
|
"""按模板声明把源文件的每一页填进 ``customPayload``(就地改 dsl)。
|
|
518
518
|
|
|
519
|
-
只对声明了 ``capabilities.payloadHydration``
|
|
519
|
+
只对声明了 ``capabilities.payloadHydration`` 的模板生效;
|
|
520
520
|
其余模板这里是零成本 no-op —— 声明驱动,不写 ``if template_id == X``(AGENTS.md)。
|
|
521
521
|
|
|
522
522
|
失败一律 exit 2 并把「下一步做什么」打全:这条路上所有失败形态的共同点是
|
|
@@ -573,9 +573,12 @@ def _compose_image_prompts(dsl: dict, template_id: str | None) -> None:
|
|
|
573
573
|
LogPrint(f" {line}", file=sys.stderr)
|
|
574
574
|
|
|
575
575
|
|
|
576
|
-
def _warn_step_sentence_mismatch(entry: dict, sentence_indices: list[int]) -> None:
|
|
576
|
+
def _warn_step_sentence_mismatch(entry: dict, sentence_indices: list[int], step_field: Optional[str]) -> None:
|
|
577
577
|
"""动作条数与旁白句数不等时出声。
|
|
578
578
|
|
|
579
|
+
动作数组放在哪个字段由模板的 ``capabilities.stepAlignment.payloadField`` 声明
|
|
580
|
+
(``step_field``);没声明的模板不检查。
|
|
581
|
+
|
|
579
582
|
为什么值得单独打一条:这一类缺陷**渲得出来**。它不报错、不退非零、renderJob
|
|
580
583
|
completed、漏斗记 rendered —— 2026-09-23 线上那条鸡兔同笼成片就是这样交付的,
|
|
581
584
|
10 步板面配 10 句旁白,听起来该一一对应,实际板面在音频中点就走完了。
|
|
@@ -585,8 +588,9 @@ def _warn_step_sentence_mismatch(entry: dict, sentence_indices: list[int]) -> No
|
|
|
585
588
|
真正需要拦的是"作者以为一一对应、实际不是",而那个判断只有作者做得了 ——
|
|
586
589
|
所以这里给的是两个数字,不是一个结论。
|
|
587
590
|
"""
|
|
588
|
-
|
|
589
|
-
|
|
591
|
+
if not step_field:
|
|
592
|
+
return
|
|
593
|
+
steps = (entry.get("props") or {}).get(step_field)
|
|
590
594
|
if not isinstance(steps, list) or not steps:
|
|
591
595
|
return
|
|
592
596
|
sentences = (sentence_indices[-1] + 1) if sentence_indices else 0
|
|
@@ -1360,7 +1364,12 @@ def resolve_assets(render_plan: dict, private_token: str, max_retries: int, time
|
|
|
1360
1364
|
return render_plan
|
|
1361
1365
|
|
|
1362
1366
|
|
|
1363
|
-
def _auto_highlight_map(
|
|
1367
|
+
def _auto_highlight_map(
|
|
1368
|
+
entry: dict,
|
|
1369
|
+
narration_asset: Optional[dict],
|
|
1370
|
+
render_plan: dict,
|
|
1371
|
+
line_sync: dict[str, str],
|
|
1372
|
+
) -> None:
|
|
1364
1373
|
"""Auto-fill ``templateData.highlightMap`` for any template that uses
|
|
1365
1374
|
structured ``{intro, items, outro}`` narration and ships per-line cards.
|
|
1366
1375
|
|
|
@@ -1371,6 +1380,10 @@ def _auto_highlight_map(entry: dict, narration_asset: Optional[dict], render_pla
|
|
|
1371
1380
|
this auto-fill. Adding a new template that follows the same pattern needs
|
|
1372
1381
|
NO change to this function.
|
|
1373
1382
|
|
|
1383
|
+
``line_sync`` is the template's ``lineSyncSlides`` declaration (slideId →
|
|
1384
|
+
card-array key). It caps the map at the card count so trailing outro lines
|
|
1385
|
+
don't point past the last card; slides it doesn't list get no cap.
|
|
1386
|
+
|
|
1374
1387
|
Skipped silently when:
|
|
1375
1388
|
- narration asset has no narrationLineCount (structured form wasn't used)
|
|
1376
1389
|
- author already supplied a highlightMap (explicit wins)
|
|
@@ -1405,23 +1418,13 @@ def _auto_highlight_map(entry: dict, narration_asset: Optional[dict], render_pla
|
|
|
1405
1418
|
intro_lines = int(narration_asset.get("narrationIntroLines") or 0)
|
|
1406
1419
|
item_count = line_count - intro_lines
|
|
1407
1420
|
# outro counts toward line_count too; cap item_count at the authored
|
|
1408
|
-
#
|
|
1409
|
-
# indices.
|
|
1410
|
-
#
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
# 顺序也有意义:取第一个命中的键,新键一律追加在末尾。
|
|
1416
|
-
# template-library 的 `pnpm validate:contracts` 现在会双向核对这两份表。
|
|
1417
|
-
items_key = next(
|
|
1418
|
-
(
|
|
1419
|
-
k
|
|
1420
|
-
for k in ("concepts", "pillars", "eras", "items", "steps", "annotations")
|
|
1421
|
-
if isinstance(tdata.get(k), list)
|
|
1422
|
-
),
|
|
1423
|
-
None,
|
|
1424
|
-
)
|
|
1421
|
+
# card-array length so trailing outro lines don't spill into card
|
|
1422
|
+
# indices. 数组键读模板的 lineSyncSlides 声明(按 slideId),CLI 不保留键表副本
|
|
1423
|
+
# —— 以前这里有一份模板 CARD_ARRAY_KEYS 的手抄,已经漂移过两次。
|
|
1424
|
+
slide_id = props.get("slideId") or tdata.get("slideId")
|
|
1425
|
+
items_key = line_sync.get(slide_id) if isinstance(slide_id, str) else None
|
|
1426
|
+
if items_key and not isinstance(tdata.get(items_key), list):
|
|
1427
|
+
items_key = None
|
|
1425
1428
|
if items_key:
|
|
1426
1429
|
item_count = min(item_count, len(tdata[items_key]))
|
|
1427
1430
|
|
|
@@ -1440,21 +1443,25 @@ def _auto_highlight_map(entry: dict, narration_asset: Optional[dict], render_pla
|
|
|
1440
1443
|
})
|
|
1441
1444
|
|
|
1442
1445
|
|
|
1443
|
-
def
|
|
1446
|
+
def _timing_template(template_id: str) -> dict:
|
|
1447
|
+
"""排轴阶段要读的模板声明;取不到就返回空 dict,走全局默认,不因为这个挡渲染。"""
|
|
1448
|
+
if not template_id:
|
|
1449
|
+
return {}
|
|
1450
|
+
try:
|
|
1451
|
+
from registry_loader import get_template # type: ignore
|
|
1452
|
+
return get_template(template_id, include_all_statuses=True) or {}
|
|
1453
|
+
except Exception: # noqa: BLE001
|
|
1454
|
+
return {}
|
|
1455
|
+
|
|
1456
|
+
|
|
1457
|
+
def _template_default_tail_pad_frames(tpl: dict, fps: int) -> int | None:
|
|
1444
1458
|
"""模板声明的旁白尾巴默认值(``capabilities.defaultTailPadSec``),换算成帧。
|
|
1445
1459
|
|
|
1446
1460
|
场景自己的 ``customPayload.tailPadSec`` 仍然优先;这里只替换全局 1.5s 兜底。
|
|
1447
1461
|
为什么要模板级:逐场景设置靠模型照着 llmHint 抄,实际经常漏 —— 2026-09-24
|
|
1448
1462
|
线上《狼来了》绘本每页结尾 1.9s 空白,86s 里 15s 没有声音。
|
|
1449
1463
|
"""
|
|
1450
|
-
|
|
1451
|
-
return None
|
|
1452
|
-
try:
|
|
1453
|
-
from registry_loader import get_template # type: ignore
|
|
1454
|
-
tpl = get_template(template_id, include_all_statuses=True)
|
|
1455
|
-
except Exception: # noqa: BLE001 - 取不到模板就走全局默认,不因为这个挡渲染
|
|
1456
|
-
return None
|
|
1457
|
-
value = ((tpl or {}).get("capabilities") or {}).get("defaultTailPadSec")
|
|
1464
|
+
value = (tpl.get("capabilities") or {}).get("defaultTailPadSec")
|
|
1458
1465
|
if isinstance(value, bool) or not isinstance(value, (int, float)) or value < 0:
|
|
1459
1466
|
return None
|
|
1460
1467
|
return int(round(float(value) * fps))
|
|
@@ -1469,7 +1476,11 @@ def adjust_timeline_to_audio(render_plan: dict) -> dict:
|
|
|
1469
1476
|
assets_map = {a["assetId"]: a for a in render_plan["resolvedAssets"]}
|
|
1470
1477
|
current_frame = 0
|
|
1471
1478
|
adjusted = 0
|
|
1472
|
-
|
|
1479
|
+
from video_dsl.runtime.dsl_validator import line_sync_slides, step_alignment_field
|
|
1480
|
+
timing_tpl = _timing_template(render_plan.get("templateId", ""))
|
|
1481
|
+
line_sync = line_sync_slides(timing_tpl)
|
|
1482
|
+
step_field = step_alignment_field(timing_tpl)
|
|
1483
|
+
default_tail_pad = _template_default_tail_pad_frames(timing_tpl, fps)
|
|
1473
1484
|
if default_tail_pad is None:
|
|
1474
1485
|
default_tail_pad = int(fps * 1.5)
|
|
1475
1486
|
|
|
@@ -1533,7 +1544,7 @@ def adjust_timeline_to_audio(render_plan: dict) -> dict:
|
|
|
1533
1544
|
}
|
|
1534
1545
|
for i, s in enumerate(tts_subs)
|
|
1535
1546
|
]
|
|
1536
|
-
_warn_step_sentence_mismatch(entry, sentence_indices)
|
|
1547
|
+
_warn_step_sentence_mismatch(entry, sentence_indices, step_field)
|
|
1537
1548
|
else:
|
|
1538
1549
|
# No TTS timestamps — re-distribute existing subtitle
|
|
1539
1550
|
# lines across the REAL audio duration, by character
|
|
@@ -1605,7 +1616,7 @@ def adjust_timeline_to_audio(render_plan: dict) -> dict:
|
|
|
1605
1616
|
cursor += seg_frames
|
|
1606
1617
|
entry["subtitleSegments"] = new_subs
|
|
1607
1618
|
|
|
1608
|
-
_auto_highlight_map(entry, narration_asset, render_plan)
|
|
1619
|
+
_auto_highlight_map(entry, narration_asset, render_plan, line_sync)
|
|
1609
1620
|
|
|
1610
1621
|
# Re-anchor subtitles for scenes that weren't handled in the narration block above.
|
|
1611
1622
|
# This covers scenes without narration or with invalid duration.
|
|
@@ -2317,8 +2328,8 @@ def main():
|
|
|
2317
2328
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
2318
2329
|
epilog="""
|
|
2319
2330
|
Examples:
|
|
2320
|
-
python render_video.py --dsl video.dsl.json --template-id
|
|
2321
|
-
python render_video.py --dsl video.dsl.json --template-id
|
|
2331
|
+
python render_video.py --dsl video.dsl.json --template-id <templateId>
|
|
2332
|
+
python render_video.py --dsl video.dsl.json --template-id <templateId> --resolve-only
|
|
2322
2333
|
python render_video.py --render-plan video.render-plan.json
|
|
2323
2334
|
""",
|
|
2324
2335
|
)
|
|
@@ -2328,7 +2339,7 @@ Examples:
|
|
|
2328
2339
|
parser.add_argument(
|
|
2329
2340
|
"--source-document-url", default=None,
|
|
2330
2341
|
help="Source document URL (.pptx) for templates that build their per-scene payload from a "
|
|
2331
|
-
"document (capabilities.payloadHydration
|
|
2342
|
+
"document (capabilities.payloadHydration). Overrides "
|
|
2332
2343
|
"meta.sourceDocument in the DSL. The layout data is fetched here and written into "
|
|
2333
2344
|
"every scene's customPayload — it is deliberately never routed through the model.",
|
|
2334
2345
|
)
|
|
@@ -2522,8 +2533,8 @@ Examples:
|
|
|
2522
2533
|
|
|
2523
2534
|
# ── 按引用注入模板声明的大 payload(必须在校验之前)─────────────
|
|
2524
2535
|
#
|
|
2525
|
-
#
|
|
2526
|
-
#
|
|
2536
|
+
# 按引用注入的模板画面数据(payloadHydration.payloadField 指定的字段:一整页
|
|
2537
|
+
# 的节点树、坐标、配色、beats)只有一个合法产出方 —— ab-render 的 /extract layout 层,
|
|
2527
2538
|
# 而它体积大到不能过模型(一页几百 KB,23 页好几 MB)。所以 DSL 里只带一个
|
|
2528
2539
|
# 引用(meta.sourceDocument),在这里把全量取回来填进每个场景。
|
|
2529
2540
|
#
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
"skillName": "render-video",
|
|
3
3
|
"repoName": "agent-skill-media-maker",
|
|
4
4
|
"skillId": "473",
|
|
5
|
-
"version": "
|
|
5
|
+
"version": "V31",
|
|
6
6
|
"skillDescription": "Final-render skill: loads a persisted RenderPlan by `job_id` and drives the Remotion engine to produce the final video.\n\nUse this skill as soon as the user mentions any of these intents (after assets are already prepared):\n- Render the video, composite the video, export the video\n- Turn the prepared assets into the final clip\n- Render with Remotion\n\nPrerequisite: assets must already be generated via `prepare_video_assets`. This skill never resolves or regenerates assets — pass it a `job_id` from a previous `prepare_video_assets` call.\n\n⚠️ Stop-and-confirm gate: never call this skill until the user has explicitly confirmed the assets prepared by `prepare_video_assets`. If those assets were prepared in the current turn and the user has not replied since, stop and ask instead of rendering."
|
|
7
7
|
}
|
|
@@ -17,7 +17,7 @@ HTTP 拉取带磁盘缓存(TTL + ETag/304);瞬时故障时降级复用同
|
|
|
17
17
|
|
|
18
18
|
ab-api 响应包装格式 `{code, msg, data}` 由 `registry_loader._fetch_http` 透明拆封;静态 JSON endpoint 也支持(直接返回 registry 文档)。
|
|
19
19
|
|
|
20
|
-
> 早期文档曾描述本 skill 通过 `@ab-templates/metadata` npm 包 / monorepo 文件消费 registry —— 这些本地来源已移除,运行时只走 ab-api HTTP
|
|
20
|
+
> 早期文档曾描述本 skill 通过 `@ab-templates/metadata` npm 包 / monorepo 文件消费 registry —— 这些本地来源已移除,运行时只走 ab-api HTTP。配套的 monorepo-only 维护脚本(`check_contracts.py` / `sync_registry.py`)也已随之删除,契约对账改由 template-library 的 `generateManifest` / `validate:contracts` 在上游负责。
|
|
21
21
|
|
|
22
22
|
## 新增模板工作流
|
|
23
23
|
|
|
@@ -30,17 +30,8 @@ ab-api 响应包装格式 `{code, msg, data}` 由 `registry_loader._fetch_http`
|
|
|
30
30
|
|
|
31
31
|
## 契约一致性
|
|
32
32
|
|
|
33
|
-
template-library
|
|
34
|
-
|
|
35
|
-
- 每个 `template.json` 中引用的 `compositionId` 都存在于 remotion-renderer 的 `manifest.ts`
|
|
36
|
-
- manifest 中的每个 compositionId 都被某个 `template.json` 引用
|
|
37
|
-
- 所有 `template.json` 通过 JSON Schema 校验
|
|
38
|
-
|
|
39
|
-
本地可运行 `check_contracts.py` 做跨仓库一致性检查(从 monorepo `<repo>/remotion-renderer/src/core/compositions/manifest.ts` 读 manifest):
|
|
40
|
-
|
|
41
|
-
```bash
|
|
42
|
-
python3 <SkillDir>/scripts/check_contracts.py
|
|
43
|
-
```
|
|
33
|
+
模板契约(schema、compositionId、slideSchemas、lineSyncSlides 等)由 template-library
|
|
34
|
+
仓库自己的 `pnpm check:all` 校验。本仓不保留任何模板级校验或模板数据副本。
|
|
44
35
|
|
|
45
36
|
## 共享 Python 模块
|
|
46
37
|
|
|
@@ -48,12 +39,10 @@ python3 <SkillDir>/scripts/check_contracts.py
|
|
|
48
39
|
|
|
49
40
|
| 模块 | 谁在用 | 作用 |
|
|
50
41
|
|---|---|---|
|
|
51
|
-
| `template_paths.py` | registry_loader / template_binder / gen-script / check_contracts | 统一计算 monorepo / template-library 路径,代替 4 个 caller 各自数 `..` |
|
|
52
42
|
| `registry_loader.py` | template-registry / match_template / render-video / gen-script / dsl_validator | 加载 registry(HTTP + cache + file) |
|
|
53
43
|
| `match_template.py` | render-video(`auto_bind_template`) | DSL → TemplateBinding 算法 |
|
|
54
44
|
| `render_job_client.py` | render-video / export-jianying | ab-api renderJob HTTP 客户端 |
|
|
55
|
-
| `
|
|
56
|
-
| `sync_registry.py` | 维护脚本 | 把 registry 摘要落盘供 LLM prompt 使用 |
|
|
45
|
+
| `countdown_spec.py` | gen-script / render-video | 解析 `capabilities.countdown` 声明 |
|
|
57
46
|
|
|
58
47
|
sibling skill 通过 `sys.path.insert(0, "<…>/template-registry/scripts")` 然后直接 `import` 模块名,模式见 `render_video.py` 顶部的"Single sys.path setup"段。
|
|
59
48
|
|
|
@@ -25,23 +25,7 @@ Stores every video-template definition, loads a template by **template-id**, and
|
|
|
25
25
|
|
|
26
26
|
Template metadata is authored in the standalone **template-library** monorepo, published to ab-api, and served to the CLI over HTTP. At runtime template-registry loads the registry from a single source — the ab-api endpoint (see the env table below) — which returns every template definition (full slotMapping, compositions, etc.) including the caller's private/tenant templates.
|
|
27
27
|
|
|
28
|
-
>
|
|
29
|
-
|
|
30
|
-
```
|
|
31
|
-
template-library/packages/
|
|
32
|
-
├── templates/src/
|
|
33
|
-
│ ├── html-slide/ # Knowledge-board template
|
|
34
|
-
│ │ ├── Composition.tsx
|
|
35
|
-
│ │ └── template.json
|
|
36
|
-
│ ├── image-slide/ # Basic image + narration template (includes the tech variant)
|
|
37
|
-
│ │ └── ...
|
|
38
|
-
│ ├── picture-book-en/ # English picture-book video template
|
|
39
|
-
│ │ └── ...
|
|
40
|
-
│ └── screen-walkthrough/ # Screen-recording walkthrough template
|
|
41
|
-
│ └── ...
|
|
42
|
-
└── metadata/
|
|
43
|
-
└── registry.json # Aggregated registry (every template.json merged)
|
|
44
|
-
```
|
|
28
|
+
> Each template's `template.json` (authored in template-library, aggregated into `registry.json` there) is the single source of truth for everything template-specific: `llmHint`, `capabilities`, `customPayloadSchema`, `slideSchemas`, `slotMapping`. This skill and the rest of the CLI only implement the generic mechanisms those declarations drive — they never name individual templates, so read the template's own definition (see *Steps*) instead of looking for per-template rules here.
|
|
45
29
|
|
|
46
30
|
## Agent behavior: image URL handling
|
|
47
31
|
|
|
@@ -77,7 +61,8 @@ the registry is loaded **only** from the ab-api HTTP endpoint — explicit `VIDE
|
|
|
77
61
|
> 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
62
|
|
|
79
63
|
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. **Read that template's contract**: run `remixmate template-registry --template-id <id
|
|
64
|
+
2. **Read that template's contract**: run `remixmate template-registry --template-id <id>` 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. Read it **once per conversation** — it does not change between turns.
|
|
65
|
+
2b. **Multi-layout templates: expand only the layouts you use.** For templates that ship `slideSchemas`, step 2 returns a per-layout **menu** (description, field names, card-list key / item fields / `maxItems`, and a `narration` note on layouts that must split narration into `items`). Choose layouts and draft the script from the menu; then, right before writing `templateData`, call once more with `--slide-id <id>` for every chosen layout (repeatable) to get their exact field schemas.
|
|
81
66
|
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
67
|
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.
|
|
83
68
|
|
|
@@ -89,20 +74,6 @@ the registry is loaded **only** from the ab-api HTTP endpoint — explicit `VIDE
|
|
|
89
74
|
remixmate template-registry --list-templates
|
|
90
75
|
```
|
|
91
76
|
|
|
92
|
-
### Sync the registry cache (optional, used for offline / LLM prompt)
|
|
93
|
-
|
|
94
|
-
```bash
|
|
95
|
-
python3 <SkillDir>/scripts/sync_registry.py
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
`sync_registry.py` and `check_contracts.py` are **maintenance helpers** for template-library, not skill entry points. Keep invoking them directly via `python3`.
|
|
99
|
-
|
|
100
|
-
### Verify template / Remotion contract consistency
|
|
101
|
-
|
|
102
|
-
```bash
|
|
103
|
-
python3 <SkillDir>/scripts/check_contracts.py
|
|
104
|
-
```
|
|
105
|
-
|
|
106
77
|
### Build a binding locally (debug only, not part of the render pipeline)
|
|
107
78
|
|
|
108
79
|
```python
|
|
@@ -111,7 +82,7 @@ sys.path.insert(0, "<SkillDir>/scripts")
|
|
|
111
82
|
from match_template import build_binding, load_full_template, load_registry
|
|
112
83
|
|
|
113
84
|
dsl = json.load(open("my-video.dsl.json"))
|
|
114
|
-
template = next(t for t in load_registry() if t["templateId"] == "
|
|
85
|
+
template = next(t for t in load_registry() if t["templateId"] == "<templateId>")
|
|
115
86
|
binding = build_binding(load_full_template(template), dsl)
|
|
116
87
|
print(json.dumps(binding, ensure_ascii=False, indent=2))
|
|
117
88
|
```
|
|
@@ -121,13 +92,14 @@ print(json.dumps(binding, ensure_ascii=False, indent=2))
|
|
|
121
92
|
| Flag | Description | Default |
|
|
122
93
|
|------|-------------|---------|
|
|
123
94
|
| `--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
|
|
95
|
+
| `--template-id <id>` | Print that template's **full definition** — llmHint in full, `customPayloadSchema`, `slotMapping`, `compositions`, `variants` — as compact JSON. Multi-layout templates return `slideSchemas` as a menu. Repeatable. | — |
|
|
96
|
+
| `--slide-id <slideId>` | With `--template-id`: expand these layouts' full `slideSchemas` (repeatable; `all` = every layout, large). Unknown ids fail with the legal list. | menu |
|
|
125
97
|
| `--full` | List mode: emit full definitions instead of summaries. Refuses when the result would exceed 60 K characters. | off |
|
|
126
98
|
| `--filter-tag` | Keep only templates whose `styleTags` match this substring (case-insensitive). | none |
|
|
127
99
|
| `--filter-aspect` | Keep only templates declaring this aspect ratio (e.g. `9:16`). | none |
|
|
128
100
|
| `--filter-language` | Keep only templates whose `contentLanguage` includes this code (`zh`/`en`); language-agnostic templates always show. | none |
|
|
129
101
|
| `--include-beta` | Also show `status: beta` templates (same effect as `ENABLE_BETA_TEMPLATES=1`). | off |
|
|
130
|
-
| `--json-output` |
|
|
102
|
+
| `--json-output` | List mode: emit `{ "templates": [...] }` summaries (or full definitions with `--full`) instead of the table. Detail mode (`--template-id`) is always compact JSON. | off |
|
|
131
103
|
|
|
132
104
|
### List vs. detail
|
|
133
105
|
|
|
@@ -135,9 +107,12 @@ A registry row is 4–17 KB of JSON, so dumping every full definition at once ov
|
|
|
135
107
|
|
|
136
108
|
```bash
|
|
137
109
|
remixmate template-registry --list-templates # choose
|
|
138
|
-
remixmate template-registry --template-id
|
|
110
|
+
remixmate template-registry --template-id <templateId> # then read its contract (slide menu)
|
|
111
|
+
remixmate template-registry --template-id <templateId> --slide-id key-points --slide-id timeline # exact schemas of the chosen layouts
|
|
139
112
|
```
|
|
140
113
|
|
|
114
|
+
Detail mode used to pretty-print, which nearly doubled the size (html-slide: 37 K → 65 K characters) and pushed the plain call past the agent's tool-result cap — the result was spilled to a file the agent cannot open, so it re-called with other flags, and re-read the whole contract on the next turn too (session review 2026-09-27). Output is now always compact, and `slideSchemas` — two thirds of a multi-layout template — is a menu until you name the layouts.
|
|
115
|
+
|
|
141
116
|
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.
|
|
142
117
|
|
|
143
118
|
## Props extraction rules
|
|
@@ -151,7 +126,7 @@ This mirrors the list/detail split described in `scripts/registry_loader.py` (P1
|
|
|
151
126
|
- Two forms are supported: `{ from: "textLayers", role: "..." }` matches a textLayer by role; `{ from: "a.b.c" }` reads a value from the scene by dotted path.
|
|
152
127
|
2. **`customPayload` full-field pass-through (no allowlist check)**
|
|
153
128
|
- First `scene.customPayload.templateData.*` is injected; then `scene.customPayload.*` (except `templateData` itself). The latter overrides same-named values from the former, but neither can override fields already produced by propExtractors.
|
|
154
|
-
- This is the channel for templates
|
|
129
|
+
- This is the channel for templates that define their own sub-schema (multi-layout templates pass `slideId`, their per-layout `templateData`, `highlightMap`, etc.).
|
|
155
130
|
- `scene.templateData` (without the `customPayload` wrapper) is also recognized as a `templateData` source — equivalent shorthand.
|
|
156
131
|
- **Note**: `requiredProps` / `optionalProps` are currently **just template-schema documentation**; they trigger no implicit pass-through. A field reaches `props` either through a propExtractor or through `customPayload`.
|
|
157
132
|
|
|
@@ -165,11 +140,11 @@ This mirrors the list/detail split described in `scripts/registry_loader.py` (P1
|
|
|
165
140
|
|
|
166
141
|
**Common pitfall**: writing the same field in both `propExtractors` and `customPayload` — when they collide, `propExtractors` wins. Keep each field in exactly one place. `build_binding` now **detects this**: any prop name that is both declared in `propExtractors` and hit by `customPayload` pass-through (top-level, or `templateData` when the extractor reads from elsewhere) is recorded on the binding row as `dualChannelProps` and printed as a stderr warning; `test-template-pipeline.py` surfaces it as a non-fatal authoring warning. A field placed in its extractor's own declared source (e.g. extractor `from:"templateData.slideId"` + value in `templateData.slideId`) is **not** flagged — that is the legitimate source, not a double-write.
|
|
167
142
|
|
|
168
|
-
>
|
|
143
|
+
> Convention: **`slideId` flows through `customPayload` top-level pass-through**; templates do not declare it in `propExtractors`. An earlier template did both, the extractor read `templateData.slideId` while every DSL wrote `customPayload.slideId`, and every scene silently fell back to the generic layout.
|
|
169
144
|
|
|
170
|
-
## Narration for multi-card scenes (
|
|
145
|
+
## Narration for multi-card scenes (`lineSyncSlides`)
|
|
171
146
|
|
|
172
|
-
|
|
147
|
+
A template that highlights cards one by one in step with the narration declares which layouts do so in `lineSyncSlides` (`slideId` → the `templateData` array holding the cards). For those layouts you **must** use structured narration, one line per card, to avoid highlight/audio drift:
|
|
173
148
|
|
|
174
149
|
```jsonc
|
|
175
150
|
"audio": {
|
|
@@ -189,13 +164,13 @@ When `html-slide` selects `demo-concept-overview` / `feature-grid` / `timeline`
|
|
|
189
164
|
```
|
|
190
165
|
|
|
191
166
|
Rules:
|
|
192
|
-
- `items.length` must equal
|
|
193
|
-
- Do not write `templateData.highlightMap`; `render_video.py` builds it automatically from the per-line timestamps that TTS returns.
|
|
167
|
+
- `items.length` must equal the length of `customPayload.templateData[lineSyncSlides[slideId]]`, otherwise `dsl_validator` rejects it.
|
|
168
|
+
- Do not write `templateData.highlightMap`; `render_video.py` builds it automatically from the per-line timestamps that TTS returns (capped at the declared card array's length). Template-specific exceptions (e.g. how an `outro` line should map) are in the template's `llmHint`.
|
|
194
169
|
- If the author explicitly provides a `highlightMap`, the system keeps it as-is.
|
|
195
170
|
- The legacy flat `narration.text` form still works but the author then has to keep `highlightMap` in sync with the subtitle segmentation (error-prone, not recommended).
|
|
196
171
|
- `narration.languageBoost` (optional, also accepted at `global.narration`) pins the TTS language hint (`auto` / `Chinese` / `English` / …). Leave it unset unless auto-detection gets it wrong — the service applies its own default, and a scene-level value overrides the global one, exactly like `speed`.
|
|
197
172
|
|
|
198
|
-
## Image sources
|
|
173
|
+
## Image sources
|
|
199
174
|
|
|
200
175
|
Image assets support two source forms:
|
|
201
176
|
|
|
@@ -232,16 +207,9 @@ Image assets support two source forms:
|
|
|
232
207
|
- Local paths (`./image.png`, `/tmp/...`) are forbidden; HTTPS URLs only.
|
|
233
208
|
- Both forms can be mixed inside the same DSL — each asset decides independently.
|
|
234
209
|
|
|
235
|
-
##
|
|
236
|
-
|
|
237
|
-
With `picture-book-en`, **narration reads English only, never Chinese**:
|
|
210
|
+
## Template-declared narration and text-layer constraints
|
|
238
211
|
|
|
239
|
-
-
|
|
240
|
-
- The Chinese translation appears only on screen via `textLayers[role=subheadline].content` (mapped to `chineseTitle` / `chineseText`); it never enters TTS.
|
|
241
|
-
- Layout is fixed:
|
|
242
|
-
- **Cover**: top half is the title area (English `englishTitle` on top in a large round-bold font; Chinese `chineseTitle` below in a smaller font); bottom half is the cover image / video.
|
|
243
|
-
- **Content page**: top half is the text area (English `englishText` on top in a large round-bold font; Chinese `chineseText` below); bottom half is the illustration.
|
|
244
|
-
- `textLayers` role mapping is mandatory: `role=headline` carries the English text, `role=subheadline` carries the Chinese text.
|
|
212
|
+
Some templates constrain what narration or on-screen text may contain — e.g. `capabilities.narrationLanguageStrict` (narration and its `gen-voice` text must be in one language) and `capabilities.textLayerRules` (which `textLayers` roles are required, and in which language). `dsl_validator` enforces the declarations; the template's `llmHint` explains how to write for them.
|
|
245
213
|
|
|
246
214
|
## Error handling
|
|
247
215
|
|
|
@@ -29,10 +29,12 @@ The strategy carries the shape; the template carries every concrete value:
|
|
|
29
29
|
"sceneStrategy": "countdown",
|
|
30
30
|
"countdown": {
|
|
31
31
|
"countProp": "topN", // where the count goes in customPayload
|
|
32
|
-
"itemProp": "item", //
|
|
33
|
-
"indexProp": "rank", //
|
|
32
|
+
"itemProp": "item", // where one beat's item goes
|
|
33
|
+
"indexProp": "rank", // the index field inside that item
|
|
34
34
|
"legacyCountProps": ["totalItems"],
|
|
35
|
-
"openingSec": 2.5
|
|
35
|
+
"openingSec": 2.5, // optional, default DEFAULT_OPENING_SEC
|
|
36
|
+
"countPatterns": ["..."] // optional: regexes that find a count stated
|
|
37
|
+
// in on-screen copy (group 1 = the number)
|
|
36
38
|
}
|
|
37
39
|
}
|
|
38
40
|
|
|
@@ -50,6 +52,8 @@ the count would fall back to "however many cards are present", and the
|
|
|
50
52
|
|
|
51
53
|
from __future__ import annotations
|
|
52
54
|
|
|
55
|
+
import re
|
|
56
|
+
|
|
53
57
|
#: Opening budget when the template declares none. A generic "an opening is a
|
|
54
58
|
#: beat, not a chapter" value — **not** any one template's measurement. It used
|
|
55
59
|
#: to be 3 hardcoded in gen_script, taken from github-repo-rank's reference film
|
|
@@ -88,10 +92,23 @@ def resolve_countdown_spec(template: dict | None) -> dict | None:
|
|
|
88
92
|
raise CountdownError(
|
|
89
93
|
f"Template '{template_id}' declares capabilities.sceneStrategy=countdown but no "
|
|
90
94
|
"capabilities.countdown.countProp. Declare which customPayload field carries the "
|
|
91
|
-
"item count
|
|
95
|
+
"item count."
|
|
92
96
|
)
|
|
93
97
|
count_prop = count_prop.strip()
|
|
94
98
|
|
|
99
|
+
# itemProp / indexProp 同样必须声明:缺省值只能是某个具体模板的字段名,
|
|
100
|
+
# 那等于把那个模板写进 CLI。
|
|
101
|
+
names: dict[str, str] = {}
|
|
102
|
+
for key in ("itemProp", "indexProp"):
|
|
103
|
+
value = cfg.get(key)
|
|
104
|
+
if not isinstance(value, str) or not value.strip():
|
|
105
|
+
raise CountdownError(
|
|
106
|
+
f"Template '{template_id}' declares capabilities.sceneStrategy=countdown but no "
|
|
107
|
+
f"capabilities.countdown.{key}. Declare the customPayload field name — the CLI "
|
|
108
|
+
"does not guess one."
|
|
109
|
+
)
|
|
110
|
+
names[key] = value.strip()
|
|
111
|
+
|
|
95
112
|
schema = (template or {}).get("customPayloadSchema")
|
|
96
113
|
bounds = schema.get(count_prop) if isinstance(schema, dict) else None
|
|
97
114
|
if not isinstance(bounds, dict):
|
|
@@ -119,6 +136,22 @@ def resolve_countdown_spec(template: dict | None) -> dict | None:
|
|
|
119
136
|
legacy = cfg.get("legacyCountProps")
|
|
120
137
|
legacy = [p for p in legacy if isinstance(p, str) and p.strip()] if isinstance(legacy, list) else []
|
|
121
138
|
|
|
139
|
+
patterns = []
|
|
140
|
+
for raw in cfg.get("countPatterns") or []:
|
|
141
|
+
try:
|
|
142
|
+
compiled = re.compile(raw, re.I) if isinstance(raw, str) else None
|
|
143
|
+
except re.error as exc:
|
|
144
|
+
raise CountdownError(
|
|
145
|
+
f"Template '{template_id}': countdown.countPatterns entry {raw!r} is not a valid "
|
|
146
|
+
f"regex ({exc})."
|
|
147
|
+
) from None
|
|
148
|
+
if compiled is None or compiled.groups < 1:
|
|
149
|
+
raise CountdownError(
|
|
150
|
+
f"Template '{template_id}': countdown.countPatterns entry {raw!r} must be a regex "
|
|
151
|
+
"string with a capturing group for the number."
|
|
152
|
+
)
|
|
153
|
+
patterns.append(compiled)
|
|
154
|
+
|
|
122
155
|
opening_sec = cfg.get("openingSec")
|
|
123
156
|
if not (type(opening_sec) in (int, float) and opening_sec > 0):
|
|
124
157
|
opening_sec = DEFAULT_OPENING_SEC
|
|
@@ -127,8 +160,9 @@ def resolve_countdown_spec(template: dict | None) -> dict | None:
|
|
|
127
160
|
"template_id": template_id,
|
|
128
161
|
"count_prop": count_prop,
|
|
129
162
|
"legacy_count_props": legacy,
|
|
130
|
-
"item_prop":
|
|
131
|
-
"index_prop":
|
|
163
|
+
"item_prop": names["itemProp"],
|
|
164
|
+
"index_prop": names["indexProp"],
|
|
165
|
+
"count_patterns": patterns,
|
|
132
166
|
"minimum": minimum,
|
|
133
167
|
"maximum": maximum,
|
|
134
168
|
"default": default,
|
|
@@ -136,6 +170,22 @@ def resolve_countdown_spec(template: dict | None) -> dict | None:
|
|
|
136
170
|
}
|
|
137
171
|
|
|
138
172
|
|
|
173
|
+
def stated_counts(spec: dict, text: str) -> set[int]:
|
|
174
|
+
"""Counts that on-screen copy states, found with the template's ``countPatterns``.
|
|
175
|
+
|
|
176
|
+
How a count is worded (``Top 5`` / ``前 5 名`` / ``5 个技巧``) is the
|
|
177
|
+
template's copy convention, so the patterns are declared there; a template
|
|
178
|
+
that declares none gets no check.
|
|
179
|
+
"""
|
|
180
|
+
found: set[int] = set()
|
|
181
|
+
for pattern in spec.get("count_patterns") or []:
|
|
182
|
+
for match in pattern.finditer(text or ""):
|
|
183
|
+
for group in match.groups():
|
|
184
|
+
if group is not None and group.isdigit():
|
|
185
|
+
found.add(int(group))
|
|
186
|
+
return found
|
|
187
|
+
|
|
188
|
+
|
|
139
189
|
def resolve_count(spec: dict, requested: int | None, scene_count: int | None) -> int:
|
|
140
190
|
"""Validate a caller-supplied item count against the template's own bounds.
|
|
141
191
|
|