@remixmate/cli 0.9.49 → 0.9.51
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 +18 -8
- package/package.json +9 -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 +13 -11
- package/skills/gen-script/scripts/gen_script.py +106 -25
- 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 +77 -44
- package/skills/render-video/version.json +1 -1
- package/skills/template-registry/README.md +3 -14
- package/skills/template-registry/SKILL.md +12 -49
- package/skills/template-registry/scripts/countdown_spec.py +56 -6
- package/skills/template-registry/scripts/registry_loader.py +3 -4
- 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 +44 -35
- 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
|
@@ -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 (
|
|
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 (
|
|
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
|
|
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 (
|
|
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": "
|
|
5
|
+
"version": "V17",
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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": "
|
|
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
|
}
|
|
@@ -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.
|
|
@@ -1873,6 +1884,11 @@ def _persist_plan_checkpoint(render_plan: dict, job_id: Optional[int], private_t
|
|
|
1873
1884
|
LogPrint(f" ⚠️ RenderPlan checkpoint failed (jobId={job_id}): {exc}", file=sys.stderr)
|
|
1874
1885
|
|
|
1875
1886
|
|
|
1887
|
+
|
|
1888
|
+
def _builtin_dynamic_render_enabled() -> bool:
|
|
1889
|
+
"""内置模板是否也走 /renderDraft(Phase 3b 路由翻转开关,默认关)。"""
|
|
1890
|
+
return os.environ.get("REMIXMATE_BUILTIN_DYNAMIC_RENDER", "").strip().lower() in {"1", "true", "yes", "on"}
|
|
1891
|
+
|
|
1876
1892
|
def render_with_remote_api(
|
|
1877
1893
|
render_plan: dict,
|
|
1878
1894
|
output_path: str,
|
|
@@ -1941,6 +1957,13 @@ def render_with_remote_api(
|
|
|
1941
1957
|
status_path = "/renderStatus"
|
|
1942
1958
|
|
|
1943
1959
|
source_oss_key = None
|
|
1960
|
+
# 内置模板是否也走动态渲染(Phase 3b 路由翻转,ab-platform/docs/
|
|
1961
|
+
# template-authoring-page-review.md §7.2)。默认关:翻转依赖 ab-render 以服务身份取
|
|
1962
|
+
# 内置源码(DRAFT_BUILTIN_SOURCE_TOKEN ↔ ab-api ab_render.service_token),服务端没配好
|
|
1963
|
+
# 就打开,普通用户渲染内置模板会全部失败。服务端就绪后设 REMIXMATE_BUILTIN_DYNAMIC_RENDER=1;
|
|
1964
|
+
# 回退 = 取消该变量,内置模板回到 /render。
|
|
1965
|
+
builtin_dynamic = _builtin_dynamic_render_enabled()
|
|
1966
|
+
is_builtin_dynamic = False
|
|
1944
1967
|
template_id = render_plan.get("templateId", "")
|
|
1945
1968
|
if template_id:
|
|
1946
1969
|
try:
|
|
@@ -1950,8 +1973,12 @@ def render_with_remote_api(
|
|
|
1950
1973
|
# 是另一个模板的画面。
|
|
1951
1974
|
_meta = get_template(template_id, include_all_statuses=True)
|
|
1952
1975
|
if _meta and not _meta.get("isBuiltin"):
|
|
1953
|
-
|
|
1954
|
-
|
|
1976
|
+
source_oss_key = _meta.get("hasSource")
|
|
1977
|
+
elif _meta and builtin_dynamic:
|
|
1978
|
+
# 内置模板:客户端拿不到也不需要 sourceOssKey(源码保护),只要知道
|
|
1979
|
+
# 它在 registry 里;ab-render 按 templateKey 以服务身份取包。
|
|
1980
|
+
source_oss_key = True
|
|
1981
|
+
is_builtin_dynamic = True
|
|
1955
1982
|
except Exception:
|
|
1956
1983
|
source_oss_key = None
|
|
1957
1984
|
|
|
@@ -1970,6 +1997,10 @@ def render_with_remote_api(
|
|
|
1970
1997
|
# 不传则 ab-render 退回截首帧 / 不渲。
|
|
1971
1998
|
if cover_composition_id:
|
|
1972
1999
|
payload["coverCompositionId"] = cover_composition_id
|
|
2000
|
+
if is_builtin_dynamic:
|
|
2001
|
+
# 与预构建 /render 的 MainVideo 语义对齐:不把 entry.props 摊平到顶层
|
|
2002
|
+
# (摊平会让同名键覆盖平台注入的 typography / variantId 等)。
|
|
2003
|
+
payload["spreadEntryProps"] = False
|
|
1973
2004
|
render_path = "/renderDraft"
|
|
1974
2005
|
status_path = "/renderDraftStatus"
|
|
1975
2006
|
else:
|
|
@@ -1988,6 +2019,8 @@ def render_with_remote_api(
|
|
|
1988
2019
|
if pending_resume:
|
|
1989
2020
|
# 续跑:下面那堆"正在提交"的日志会误导人,跳过
|
|
1990
2021
|
LogPrint(f"🎬 Remote render task already exists, will resume polling", file=sys.stderr)
|
|
2022
|
+
elif is_builtin_dynamic:
|
|
2023
|
+
LogPrint(f" Builtin template (dynamic bundle): {template_id}", file=sys.stderr)
|
|
1991
2024
|
elif source_oss_key:
|
|
1992
2025
|
LogPrint(f" Private template (OSS dynamic bundle): {template_id}", file=sys.stderr)
|
|
1993
2026
|
else:
|
|
@@ -2295,8 +2328,8 @@ def main():
|
|
|
2295
2328
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
2296
2329
|
epilog="""
|
|
2297
2330
|
Examples:
|
|
2298
|
-
python render_video.py --dsl video.dsl.json --template-id
|
|
2299
|
-
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
|
|
2300
2333
|
python render_video.py --render-plan video.render-plan.json
|
|
2301
2334
|
""",
|
|
2302
2335
|
)
|
|
@@ -2306,7 +2339,7 @@ Examples:
|
|
|
2306
2339
|
parser.add_argument(
|
|
2307
2340
|
"--source-document-url", default=None,
|
|
2308
2341
|
help="Source document URL (.pptx) for templates that build their per-scene payload from a "
|
|
2309
|
-
"document (capabilities.payloadHydration
|
|
2342
|
+
"document (capabilities.payloadHydration). Overrides "
|
|
2310
2343
|
"meta.sourceDocument in the DSL. The layout data is fetched here and written into "
|
|
2311
2344
|
"every scene's customPayload — it is deliberately never routed through the model.",
|
|
2312
2345
|
)
|
|
@@ -2500,8 +2533,8 @@ Examples:
|
|
|
2500
2533
|
|
|
2501
2534
|
# ── 按引用注入模板声明的大 payload(必须在校验之前)─────────────
|
|
2502
2535
|
#
|
|
2503
|
-
#
|
|
2504
|
-
#
|
|
2536
|
+
# 按引用注入的模板画面数据(payloadHydration.payloadField 指定的字段:一整页
|
|
2537
|
+
# 的节点树、坐标、配色、beats)只有一个合法产出方 —— ab-render 的 /extract layout 层,
|
|
2505
2538
|
# 而它体积大到不能过模型(一页几百 KB,23 页好几 MB)。所以 DSL 里只带一个
|
|
2506
2539
|
# 引用(meta.sourceDocument),在这里把全量取回来填进每个场景。
|
|
2507
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
|
}
|
|
@@ -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
|
|
|
@@ -89,20 +73,6 @@ the registry is loaded **only** from the ab-api HTTP endpoint — explicit `VIDE
|
|
|
89
73
|
remixmate template-registry --list-templates
|
|
90
74
|
```
|
|
91
75
|
|
|
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
76
|
### Build a binding locally (debug only, not part of the render pipeline)
|
|
107
77
|
|
|
108
78
|
```python
|
|
@@ -111,7 +81,7 @@ sys.path.insert(0, "<SkillDir>/scripts")
|
|
|
111
81
|
from match_template import build_binding, load_full_template, load_registry
|
|
112
82
|
|
|
113
83
|
dsl = json.load(open("my-video.dsl.json"))
|
|
114
|
-
template = next(t for t in load_registry() if t["templateId"] == "
|
|
84
|
+
template = next(t for t in load_registry() if t["templateId"] == "<templateId>")
|
|
115
85
|
binding = build_binding(load_full_template(template), dsl)
|
|
116
86
|
print(json.dumps(binding, ensure_ascii=False, indent=2))
|
|
117
87
|
```
|
|
@@ -135,7 +105,7 @@ A registry row is 4–17 KB of JSON, so dumping every full definition at once ov
|
|
|
135
105
|
|
|
136
106
|
```bash
|
|
137
107
|
remixmate template-registry --list-templates # choose
|
|
138
|
-
remixmate template-registry --template-id
|
|
108
|
+
remixmate template-registry --template-id <templateId> --json-output # then read its contract
|
|
139
109
|
```
|
|
140
110
|
|
|
141
111
|
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.
|
|
@@ -151,7 +121,7 @@ This mirrors the list/detail split described in `scripts/registry_loader.py` (P1
|
|
|
151
121
|
- 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
122
|
2. **`customPayload` full-field pass-through (no allowlist check)**
|
|
153
123
|
- 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
|
|
124
|
+
- This is the channel for templates that define their own sub-schema (multi-layout templates pass `slideId`, their per-layout `templateData`, `highlightMap`, etc.).
|
|
155
125
|
- `scene.templateData` (without the `customPayload` wrapper) is also recognized as a `templateData` source — equivalent shorthand.
|
|
156
126
|
- **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
127
|
|
|
@@ -165,11 +135,11 @@ This mirrors the list/detail split described in `scripts/registry_loader.py` (P1
|
|
|
165
135
|
|
|
166
136
|
**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
137
|
|
|
168
|
-
>
|
|
138
|
+
> 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
139
|
|
|
170
|
-
## Narration for multi-card scenes (
|
|
140
|
+
## Narration for multi-card scenes (`lineSyncSlides`)
|
|
171
141
|
|
|
172
|
-
|
|
142
|
+
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
143
|
|
|
174
144
|
```jsonc
|
|
175
145
|
"audio": {
|
|
@@ -189,13 +159,13 @@ When `html-slide` selects `demo-concept-overview` / `feature-grid` / `timeline`
|
|
|
189
159
|
```
|
|
190
160
|
|
|
191
161
|
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.
|
|
162
|
+
- `items.length` must equal the length of `customPayload.templateData[lineSyncSlides[slideId]]`, otherwise `dsl_validator` rejects it.
|
|
163
|
+
- 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
164
|
- If the author explicitly provides a `highlightMap`, the system keeps it as-is.
|
|
195
165
|
- 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
166
|
- `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
167
|
|
|
198
|
-
## Image sources
|
|
168
|
+
## Image sources
|
|
199
169
|
|
|
200
170
|
Image assets support two source forms:
|
|
201
171
|
|
|
@@ -232,16 +202,9 @@ Image assets support two source forms:
|
|
|
232
202
|
- Local paths (`./image.png`, `/tmp/...`) are forbidden; HTTPS URLs only.
|
|
233
203
|
- Both forms can be mixed inside the same DSL — each asset decides independently.
|
|
234
204
|
|
|
235
|
-
##
|
|
236
|
-
|
|
237
|
-
With `picture-book-en`, **narration reads English only, never Chinese**:
|
|
205
|
+
## Template-declared narration and text-layer constraints
|
|
238
206
|
|
|
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.
|
|
207
|
+
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
208
|
|
|
246
209
|
## Error handling
|
|
247
210
|
|