@remixmate/cli 0.12.7 → 0.12.8
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/dist/manifest.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"version": "0.12.
|
|
4
|
-
"generatedAt": "2026-10-
|
|
3
|
+
"version": "0.12.8",
|
|
4
|
+
"generatedAt": "2026-10-20T23:46:18.482Z",
|
|
5
5
|
"skills": [
|
|
6
6
|
{
|
|
7
7
|
"id": "doc-parse",
|
|
@@ -592,7 +592,38 @@
|
|
|
592
592
|
},
|
|
593
593
|
"background_preset": {
|
|
594
594
|
"type": "string",
|
|
595
|
-
"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`
|
|
595
|
+
"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` need the user's file: pass it as background_asset (which also sets the preset)."
|
|
596
|
+
},
|
|
597
|
+
"sequence_preset": {
|
|
598
|
+
"type": "string",
|
|
599
|
+
"description": "Image-sequence style preset, one of the template's customPayloadSchema.sequence.preset values (image-to-video: vlog / news / poster / cinematic / fast-cut). Written to customPayload.sequence.preset; the preset sets transitions, motion, text layout and pacing (fast-cut shortens every image to 1.5s, and the scene duration follows). **Must** pass when the user picked a style preset: the replicate card shows `风格预设: 快剪(fast-cut)` → pass `fast-cut`; `none` keeps the template defaults. Without it the choice is silently dropped. Only for templates declaring capabilities.payloadDefaults.sequence; unsupported values are rejected."
|
|
600
|
+
},
|
|
601
|
+
"sequence_fit": {
|
|
602
|
+
"type": "string",
|
|
603
|
+
"description": "How image-sequence media fill the frame, one of customPayloadSchema.sequence.fit (cover = fill the frame and crop, contain = show the whole picture over a blurred backdrop). Written to customPayload.sequence.fit and beats the preset's own fit. **Must** pass when the user picked it: `显示方式: 完整显示(contain)` → pass `contain`; `auto` keeps the template / preset default. Only for templates declaring capabilities.payloadDefaults.sequence."
|
|
604
|
+
},
|
|
605
|
+
"background_asset": {
|
|
606
|
+
"type": "string",
|
|
607
|
+
"description": "URL of the user's background image or video (the replicate card's 背景素材 / 背景图片 asset line → its URL). Added as an existing asset and referenced from every scene's customPayload.background (assetRef for an image, videoAssetRef for a video; .mp4/.mov/.webm/.m4v count as video); the background preset becomes image / video, so background_preset can be omitted. **Must** pass when the user gave a background file — without it a preset of image / video renders the template's plain default. Requires template_id; rejected when the template has no background of that kind."
|
|
608
|
+
},
|
|
609
|
+
"payload": {
|
|
610
|
+
"type": "array",
|
|
611
|
+
"items": {
|
|
612
|
+
"type": "string"
|
|
613
|
+
},
|
|
614
|
+
"description": "Template-specific card choices as `KEY=VALUE`, written to every scene's customPayload and locked so the dsl_json you save cannot overwrite them. KEY is a dotted path into the template's customPayloadSchema (e.g. `grade=四年级`, `watermarkText=WorkBuddy 实战课`, `preset=course`, `carousel.fit=cover`, `templateData.revealMode=progressive`, `cardStyle.championFx=false`); VALUE is the id inside the card line's parentheses, or the text verbatim for free-text fields. Booleans take true/false, numbers are range-checked, enums are checked — an undeclared key or value is rejected with the allowed list. **Must** pass every card line that the system prompt maps to a payload key; without it the choice is silently dropped. Requires template_id."
|
|
615
|
+
},
|
|
616
|
+
"motion": {
|
|
617
|
+
"type": "string",
|
|
618
|
+
"enum": [
|
|
619
|
+
"static",
|
|
620
|
+
"kenburns-in",
|
|
621
|
+
"kenburns-out",
|
|
622
|
+
"pan-left",
|
|
623
|
+
"pan-right",
|
|
624
|
+
"float"
|
|
625
|
+
],
|
|
626
|
+
"description": "Camera motion for every scene (scenes[].animationHints.motion), replacing the skeleton's alternating push-in / pull-out. **Must** pass when the user picked one: `运镜效果: 缓慢推近(kenburns-in)` → `kenburns-in`."
|
|
596
627
|
},
|
|
597
628
|
"font_id": {
|
|
598
629
|
"type": "string",
|
|
@@ -639,7 +670,7 @@
|
|
|
639
670
|
"items": {
|
|
640
671
|
"type": "string"
|
|
641
672
|
},
|
|
642
|
-
"description": "Media URLs for the template's image/video carousel. When provided together with a template_id whose capabilities.payloadStyle=carousel-caption, these URLs are placed into customPayload.sequence.items when payloadDefaults.sequence is declared, otherwise customPayload.carousel.items, as existing assets — NO AI image generation is triggered. **Must** pass when the user explicitly provides image/video URLs for such a template. Each element is a full URL string."
|
|
673
|
+
"description": "Media URLs for the template's image/video carousel. When provided together with a template_id whose capabilities.payloadStyle=carousel-caption, these URLs are placed into customPayload.sequence.items when payloadDefaults.sequence is declared, otherwise customPayload.carousel.items, as existing assets — NO AI image generation is triggered. **Must** pass when the user explicitly provides image/video URLs for such a template. Each element is a full URL string. Image sequences (payloadDefaults.sequence) accept video URLs too (.mp4/.mov/.webm): gen_script probes each clip's real length, marks it type=video and plays it whole and muted; an unreadable video is rejected."
|
|
643
674
|
},
|
|
644
675
|
"scene_images": {
|
|
645
676
|
"type": "array",
|
|
@@ -653,7 +684,7 @@
|
|
|
653
684
|
"items": {
|
|
654
685
|
"type": "string"
|
|
655
686
|
},
|
|
656
|
-
"description": "On-screen caption lines for templates that declare a caption area (capabilities.payloadDefaults.caption). For image sequences (payloadDefaults.sequence), lines map in order to sequence.items[].caption, with at most one line per image. Otherwise each element is one line. Line-count limits and whether **emphasis** is parsed are declared by the template — read its llmHint, which gen_script prints in full on stderr. **Must** pass when the user explicitly provides bullet-point text / bottom copy. **Also must pass — written by you — when the user did NOT provide any copy but the template is typewriter-driven** (capabilities.durationStrategy=fit-caption): such templates have no narration, so these lines are both the video's content and the thing that decides its duration. Draft them from the material you researched (repo README, page screenshots, the topic). Leaving this empty for a fit-caption template is rejected: nothing auto-generates caption text, and an empty caption renders a titled carousel with no words at all."
|
|
687
|
+
"description": "On-screen caption lines for templates that declare a caption area (capabilities.payloadDefaults.caption). For image sequences (payloadDefaults.sequence), lines map in order to sequence.items[].caption, with at most one line per image. A replicate asset line's `(… desc: <text>)` suffix is the user's caption for that image: pass it here in the same order (use an empty string for images without one). Otherwise each element is one line. Line-count limits and whether **emphasis** is parsed are declared by the template — read its llmHint, which gen_script prints in full on stderr. **Must** pass when the user explicitly provides bullet-point text / bottom copy. **Also must pass — written by you — when the user did NOT provide any copy but the template is typewriter-driven** (capabilities.durationStrategy=fit-caption): such templates have no narration, so these lines are both the video's content and the thing that decides its duration. Draft them from the material you researched (repo README, page screenshots, the topic). Leaving this empty for a fit-caption template is rejected: nothing auto-generates caption text, and an empty caption renders a titled carousel with no words at all."
|
|
657
688
|
},
|
|
658
689
|
"stub_image_url": {
|
|
659
690
|
"type": "string",
|
|
@@ -693,6 +724,11 @@
|
|
|
693
724
|
"speed",
|
|
694
725
|
"variant",
|
|
695
726
|
"background_preset",
|
|
727
|
+
"sequence_preset",
|
|
728
|
+
"sequence_fit",
|
|
729
|
+
"background_asset",
|
|
730
|
+
"payload",
|
|
731
|
+
"motion",
|
|
696
732
|
"font_id",
|
|
697
733
|
"font_name",
|
|
698
734
|
"list_fonts",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remixmate/cli",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.8",
|
|
4
4
|
"description": "AI media generation skills for Claude Code / Codex — 12 skills covering image, video, voice, digital human, web screenshot, web recording, script, template registry, rendering, Jianying export, and video deconstruction.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -253,8 +253,9 @@ When the user selects a `carousel-caption` template (or any template whose `asse
|
|
|
253
253
|
- `durationStrategy: fit-caption` → **caption lines are mandatory.** The template has no narration and the typewriter copy is both the content and the clock: it is what the video says *and* what decides how long it runs. **If the user did not supply the copy, write it yourself** from the material you researched (repo README, page screenshots, the topic) and pass it. `gen_script.py` hard-fails on an empty caption for these templates.
|
|
254
254
|
- `durationStrategy: fit-images` → purely visual, captions genuinely optional; duration comes from the image count.
|
|
255
255
|
3. **Never call gen_script for a `carousel-caption` template without `--carousel-items`.** If the user picked such a template but provided no media, ask them for the image/video URLs first — do not generate an empty carousel.
|
|
256
|
-
4.
|
|
257
|
-
5.
|
|
256
|
+
4. Image sequences (`payloadDefaults.sequence`, e.g. image-to-video) take videos as well: pass the clip URL in `--carousel-items` like an image. gen_script probes its real length and writes `type: "video"` + `sourceDurationSec`, so the scene is timed by the clip, not as a 3-second still. A replicate asset line ` (…, desc: <text>)` carries the user's caption for that item — pass it as the matching `--caption-lines` entry (empty string for items without one).
|
|
257
|
+
5. Do **not** route these user-provided images through `gen-image`; they are existing assets and go straight into the carousel.
|
|
258
|
+
6. Nothing downstream fills these in for you. `gen_script.py` routes on `capabilities.payloadStyle` alone — omitting the flags does **not** fall back to a generic path that generates images or writes copy; it assembles an empty carousel / empty caption. There is no auto-generation of caption text anywhere in the pipeline.
|
|
258
259
|
|
|
259
260
|
### Command example
|
|
260
261
|
|
|
@@ -480,7 +481,12 @@ python3 <SkillDir>/scripts/gen_script.py \
|
|
|
480
481
|
| `--voice-id` | Narration voice id. | resolved from template + language fallback |
|
|
481
482
|
| `--speed` | Narration speech rate (0.5–2.0), written to `global.narration.speed` and applied when `prepare_video_assets` calls gen-voice. Pass it whenever the user picked a speed (the replicate form's 语速 field sends one). Above ~1.3 subtitle alignment drifts and the delivery turns mechanical — shorten the script instead. | `1.0` |
|
|
482
483
|
| `--variant` | Template visual variant id (a key of the template's `variants`; `template-registry --list-templates` prints them as `variantIds`). Written to `meta.templateVariant`, which takes precedence over the fuzzy `--style` match, and the variant's own `imageStyleGuide` is what the skeleton's image prompts / `negativePrompt` are built from (pass it to `gen_script` rather than setting `meta.templateVariant` afterwards, or the prompts keep the template's default look). Pass it whenever the user picked a style — the replicate card shows it as `名字(id)`, e.g. `视觉风格: 紫色科技(tech-purple)`; pass the id in the parentheses. Requires `--template-id`; unknown ids are rejected. | template default |
|
|
483
|
-
| `--background-preset` | Scene background preset, one of the template's `customPayloadSchema.background.preset` values. Written to every scene's `customPayload.background.preset` (other keys of the template's default background are kept). Pass it whenever the user picked a background, e.g. `背景样式: 纯色(solid)` → `solid`. `image` / `video`
|
|
484
|
+
| `--background-preset` | Scene background preset, one of the template's `customPayloadSchema.background.preset` values. Written to every scene's `customPayload.background.preset` (other keys of the template's default background are kept). Pass it whenever the user picked a background, e.g. `背景样式: 纯色(solid)` → `solid`. `image` / `video` need the user's file: pass `--background-asset` (which sets the preset itself). Requires `--template-id`; unsupported values are rejected. | template default |
|
|
485
|
+
| `--sequence-preset` | Image-sequence style preset, one of the template's `customPayloadSchema.sequence.preset` values (image-to-video: `vlog` / `news` / `poster` / `cinematic` / `fast-cut`). Written to `customPayload.sequence.preset`; the template's declared defaults are then left out so they do not shadow the preset, and the scene duration uses the preset's pacing (`durationModel.sequence.presets`). Pass it whenever the user picked a preset, e.g. `风格预设: 快剪(fast-cut)` → `fast-cut`; `none` = template defaults. | template defaults |
|
|
486
|
+
| `--sequence-fit` | Image-sequence fill mode, one of `customPayloadSchema.sequence.fit` (`cover` fill and crop / `contain` whole picture over a blurred backdrop), written to `customPayload.sequence.fit`; beats the preset's fit. `auto` = template / preset default. | template / preset default |
|
|
487
|
+
| `--background-asset` | URL of the user's background image or video (`.mp4/.mov/.webm/.m4v` → video). Added as existing asset `asset-background`; every scene's `customPayload.background` gets `preset: image` + `assetRef`, or `preset: video` + `videoAssetRef`. Conflicts with a different `--background-preset`; rejected when the template's background enum lacks that kind. | none |
|
|
488
|
+
| `--payload KEY=VALUE` | Template-specific card choice, repeatable. `KEY` is a dotted path in `customPayloadSchema` (or `capabilities.frameLevelKeys`), e.g. `grade=四年级`, `watermarkText=…`, `preset=course`, `carousel.fit=cover`, `caption.charsPerSec=20`, `templateData.revealMode=progressive`. Value is converted to the declared type (true/false, numbers) and checked against enum / range; undeclared keys are rejected. Written to every scene with sibling keys kept, applied before fit-caption duration estimation. | template default |
|
|
489
|
+
| `--motion` | Camera motion for every scene (`animationHints.motion`): `static` / `kenburns-in` / `kenburns-out` / `pan-left` / `pan-right` / `float`. | skeleton alternates in / out |
|
|
484
490
|
| `--font-id` | Font library key (`font.uniq_id`), written to `global.font.fontId`; ab-render resolves and installs the real font at render time. Pass it whenever the user picked a font — the replicate card shows it as `名字(id)`, e.g. `字体: <名字>(<uniqId>)`; pass the id in the parentheses. Never pass a CSS family or the display name: an unknown key falls back to the template font with no error. | template font |
|
|
485
491
|
| `--font-name` | Display name of the picked font → `global.font.fontName` (display only, never used for rendering). Pass it with `--font-id`. | — |
|
|
486
492
|
| `--list-fonts` | Print the live font library (`/font/page`: id, name, lang, category, license) and exit; no script is generated and `--topic` is not needed. The ids it prints are the only valid `--font-id` values — use it when the user asks which fonts exist or names a font without its id. | — |
|
|
@@ -497,6 +503,8 @@ python3 <SkillDir>/scripts/gen_script.py \
|
|
|
497
503
|
| `--scene-images` | Repeatable. One scene's visual for a **`visual-overlay`** template, mapped **positionally**. Each entry is a URL (that existing asset) or `ai` / `ai:<prompt>` (that scene goes to gen-image, optionally with the user's own prompt) — so an AI scene can sit anywhere in the order. Scenes past the last entry still get AI images. Scene count is raised (never lowered) to fit the entries when `--scenes` is absent. **Required** whenever the user supplies images for such a template — omitting it silently ignores every image they gave. | — |
|
|
498
504
|
| `--caption-lines` | Repeatable. On-screen caption line for `carousel-caption` templates → `customPayload.caption.lines`. Line limits and `**emphasis**` support are declared per template (see its `llmHint`). **Required** for `durationStrategy: fit-caption` templates — write the lines yourself if the user did not supply them. Optional only for `fit-images` templates. | — |
|
|
499
505
|
|
|
506
|
+
Values written by `--background-preset`, `--background-asset`, `--payload` and `--motion` are listed in `meta.lockedScenePaths` (scene-relative paths such as `customPayload.carousel.fit`, `animationHints.motion`). When the agent later saves its own `dsl_json` in merge mode, ab-agent restores those paths from this skeleton after merging, so a model-written `carousel` / `templateData` / `background` object cannot wipe a card choice. Do not hand-write these keys in `dsl_json`; pass the flag.
|
|
507
|
+
|
|
500
508
|
## DSL generation principles
|
|
501
509
|
|
|
502
510
|
Apply the following principles when producing the DSL:
|
|
@@ -1089,15 +1089,63 @@ def _estimate_fit_images_duration(sequence: dict, model: dict | None, fps: int =
|
|
|
1089
1089
|
rule_hold = next((tier["holdSec"] for tier in tiers if count <= tier["maxItems"]), None)
|
|
1090
1090
|
if rule_hold is None:
|
|
1091
1091
|
raise ValueError("fit-images requires durationModel.sequence.holdSecByCount covering the image count")
|
|
1092
|
-
|
|
1093
|
-
|
|
1092
|
+
|
|
1093
|
+
|
|
1094
|
+
preset_timing = (timing.get("presets") or {}).get(sequence.get("preset")) or {}
|
|
1095
|
+
base_hold = sequence.get("defaultHoldSec", preset_timing.get("defaultHoldSec", rule_hold))
|
|
1096
|
+
transition_ms = sequence.get("transitionMs", preset_timing.get("transitionMs", timing.get("transitionMs")))
|
|
1094
1097
|
margin = timing.get("minHoldMarginFrames")
|
|
1095
1098
|
if transition_ms is None or margin is None:
|
|
1096
1099
|
raise ValueError("fit-images requires sequence transitionMs and durationModel.sequence.minHoldMarginFrames")
|
|
1097
1100
|
|
|
1098
1101
|
min_frames = max(2, math.floor(transition_ms / 1000 * fps + 0.5)) + margin
|
|
1099
|
-
|
|
1100
|
-
|
|
1102
|
+
|
|
1103
|
+
def item_frames(item: dict) -> int:
|
|
1104
|
+
if item.get("type") == "video":
|
|
1105
|
+
|
|
1106
|
+
return max(1, math.floor(item.get("holdSec", item["sourceDurationSec"]) * fps + 0.5))
|
|
1107
|
+
return max(min_frames, math.floor(item.get("holdSec", base_hold) * fps + 0.5))
|
|
1108
|
+
|
|
1109
|
+
return sum(item_frames(item) for item in sequence["items"]) / fps
|
|
1110
|
+
|
|
1111
|
+
|
|
1112
|
+
def _probe_sequence_video_duration(url: str) -> float | None:
|
|
1113
|
+
|
|
1114
|
+
|
|
1115
|
+
|
|
1116
|
+
|
|
1117
|
+
|
|
1118
|
+
probe_dir = os.path.join(os.path.dirname(__file__), "..", "..", "render-video", "scripts")
|
|
1119
|
+
if os.path.isdir(probe_dir) and os.path.abspath(probe_dir) not in sys.path:
|
|
1120
|
+
sys.path.insert(0, os.path.abspath(probe_dir))
|
|
1121
|
+
try:
|
|
1122
|
+
from _video_probe import probe_video_duration
|
|
1123
|
+
except ImportError:
|
|
1124
|
+
return None
|
|
1125
|
+
return probe_video_duration(url)
|
|
1126
|
+
|
|
1127
|
+
|
|
1128
|
+
def _sequence_choice(sequence_schema: dict, key: str, value: str | None, flag: str,
|
|
1129
|
+
template_id: str, passthrough: str) -> str | None:
|
|
1130
|
+
|
|
1131
|
+
|
|
1132
|
+
|
|
1133
|
+
|
|
1134
|
+
|
|
1135
|
+
if not value or value == passthrough:
|
|
1136
|
+
return None
|
|
1137
|
+
prop = (sequence_schema.get("properties") or {}).get(key) or {}
|
|
1138
|
+
allowed = [str(v) for v in prop.get("enum") or []]
|
|
1139
|
+
if value not in allowed:
|
|
1140
|
+
raise ValueError(
|
|
1141
|
+
f"{flag} {value!r} is not supported by template {template_id!r}; "
|
|
1142
|
+
f"choose one of: {', '.join(allowed + [passthrough]) if allowed else '(this template has no such option)'}"
|
|
1143
|
+
)
|
|
1144
|
+
return value
|
|
1145
|
+
|
|
1146
|
+
|
|
1147
|
+
|
|
1148
|
+
PayloadWrites = list[tuple[list[str], object]]
|
|
1101
1149
|
|
|
1102
1150
|
|
|
1103
1151
|
def _build_carousel_caption_dsl(
|
|
@@ -1121,6 +1169,10 @@ def _build_carousel_caption_dsl(
|
|
|
1121
1169
|
payload_defaults: dict,
|
|
1122
1170
|
duration_strategy: str | None,
|
|
1123
1171
|
duration_model: dict | None,
|
|
1172
|
+
sequence_schema: dict | None = None,
|
|
1173
|
+
sequence_preset: str | None = None,
|
|
1174
|
+
sequence_fit: str | None = None,
|
|
1175
|
+
payload_writes: PayloadWrites | None = None,
|
|
1124
1176
|
) -> dict:
|
|
1125
1177
|
|
|
1126
1178
|
|
|
@@ -1134,16 +1186,30 @@ def _build_carousel_caption_dsl(
|
|
|
1134
1186
|
raise ValueError("Image sequence needs --carousel-items; caption text alone cannot supply images")
|
|
1135
1187
|
if sequence_mode and len(caption_lines) > len(carousel_items):
|
|
1136
1188
|
raise ValueError("Image sequence has more --caption-lines than images; use at most one caption per image")
|
|
1189
|
+
if not sequence_mode and (sequence_preset not in (None, "none") or sequence_fit not in (None, "auto")):
|
|
1190
|
+
raise ValueError(f"--sequence-preset / --sequence-fit only apply to image-sequence templates; {template_id!r} is not one")
|
|
1191
|
+
if sequence_mode:
|
|
1192
|
+
sequence_preset = _sequence_choice(sequence_schema or {}, "preset", sequence_preset,
|
|
1193
|
+
"--sequence-preset", template_id, "none")
|
|
1194
|
+
sequence_fit = _sequence_choice(sequence_schema or {}, "fit", sequence_fit,
|
|
1195
|
+
"--sequence-fit", template_id, "auto")
|
|
1137
1196
|
|
|
1138
1197
|
|
|
1139
1198
|
assets = []
|
|
1140
1199
|
carousel_asset_ids = []
|
|
1200
|
+
video_seconds: dict[str, float] = {}
|
|
1141
1201
|
for idx, url in enumerate(carousel_items):
|
|
1142
1202
|
asset_id = f"carousel-img-{idx + 1:02d}"
|
|
1143
1203
|
|
|
1144
1204
|
is_video = urlsplit(url).path.lower().endswith((".mp4", ".mov", ".webm", ".avi"))
|
|
1145
1205
|
if sequence_mode and is_video:
|
|
1146
|
-
|
|
1206
|
+
seconds = _probe_sequence_video_duration(url)
|
|
1207
|
+
if not seconds or seconds <= 0:
|
|
1208
|
+
raise ValueError(
|
|
1209
|
+
f"Cannot read the duration of video {url!r}; an image sequence needs it to time the clip. "
|
|
1210
|
+
f"Check the URL is reachable, or leave this video out."
|
|
1211
|
+
)
|
|
1212
|
+
video_seconds[asset_id] = round(float(seconds), 3)
|
|
1147
1213
|
assets.append({
|
|
1148
1214
|
"assetId": asset_id,
|
|
1149
1215
|
"type": "video" if is_video else "image",
|
|
@@ -1214,15 +1280,25 @@ def _build_carousel_caption_dsl(
|
|
|
1214
1280
|
|
|
1215
1281
|
|
|
1216
1282
|
if sequence_mode:
|
|
1217
|
-
|
|
1283
|
+
|
|
1284
|
+
|
|
1285
|
+
|
|
1286
|
+
sequence_config = {"preset": sequence_preset} if sequence_preset else defaults["sequence"]
|
|
1287
|
+
if sequence_fit:
|
|
1288
|
+
sequence_config["fit"] = sequence_fit
|
|
1218
1289
|
sequence_config["items"] = [
|
|
1219
1290
|
{"assetRef": asset_id,
|
|
1220
|
-
**({"
|
|
1221
|
-
|
|
1291
|
+
**({"type": "video", "sourceDurationSec": video_seconds[asset_id]} if kind == "video" else {}),
|
|
1292
|
+
**({"caption": caption_lines[index]} if index < len(caption_lines) and caption_lines[index] else {})}
|
|
1293
|
+
for index, (asset_id, kind) in enumerate(carousel_asset_ids)
|
|
1222
1294
|
]
|
|
1223
1295
|
custom_payload = {"sequence": sequence_config}
|
|
1224
1296
|
|
|
1225
1297
|
|
|
1298
|
+
|
|
1299
|
+
_write_payload(custom_payload, payload_writes or [])
|
|
1300
|
+
|
|
1301
|
+
|
|
1226
1302
|
narration_text = _localize_label(output_language, "narration_opening",
|
|
1227
1303
|
label="", topic=_clip(topic, _PLACEHOLDER_TOPIC_CHARS), duration=duration)
|
|
1228
1304
|
|
|
@@ -1472,12 +1548,29 @@ def _template_selection(
|
|
|
1472
1548
|
return selection
|
|
1473
1549
|
|
|
1474
1550
|
|
|
1551
|
+
|
|
1552
|
+
|
|
1553
|
+
|
|
1554
|
+
|
|
1555
|
+
|
|
1556
|
+
LOCKED_SCENE_PATHS_KEY = "lockedScenePaths"
|
|
1557
|
+
BACKGROUND_ASSET_ID = "asset-background"
|
|
1558
|
+
_VIDEO_EXTENSIONS = (".mp4", ".mov", ".webm", ".avi", ".m4v")
|
|
1559
|
+
|
|
1560
|
+
|
|
1561
|
+
def _lock_scene_path(dsl: dict, path: str) -> None:
|
|
1562
|
+
locks = dsl.setdefault("meta", {}).setdefault(LOCKED_SCENE_PATHS_KEY, [])
|
|
1563
|
+
if path not in locks:
|
|
1564
|
+
locks.append(path)
|
|
1565
|
+
|
|
1566
|
+
|
|
1475
1567
|
def _apply_look(
|
|
1476
1568
|
dsl: dict,
|
|
1477
1569
|
template_config: dict | None,
|
|
1478
1570
|
template_id: str | None,
|
|
1479
1571
|
variant: str | None,
|
|
1480
1572
|
background_preset: str | None,
|
|
1573
|
+
background_asset: str | None = None,
|
|
1481
1574
|
) -> None:
|
|
1482
1575
|
|
|
1483
1576
|
|
|
@@ -1491,10 +1584,17 @@ def _apply_look(
|
|
|
1491
1584
|
|
|
1492
1585
|
|
|
1493
1586
|
|
|
1494
|
-
|
|
1587
|
+
|
|
1588
|
+
|
|
1589
|
+
|
|
1590
|
+
|
|
1591
|
+
if not variant and not background_preset and not background_asset:
|
|
1495
1592
|
return
|
|
1496
1593
|
if not template_id or template_config is None:
|
|
1497
|
-
raise ValueError(
|
|
1594
|
+
raise ValueError(
|
|
1595
|
+
"--variant / --background-preset / --background-asset need --template-id: "
|
|
1596
|
+
"the allowed values come from the template."
|
|
1597
|
+
)
|
|
1498
1598
|
|
|
1499
1599
|
if variant:
|
|
1500
1600
|
variants = list((template_config.get("variants") or {}).keys())
|
|
@@ -1505,6 +1605,22 @@ def _apply_look(
|
|
|
1505
1605
|
)
|
|
1506
1606
|
dsl.setdefault("meta", {})["templateVariant"] = variant
|
|
1507
1607
|
|
|
1608
|
+
asset_url = (background_asset or "").strip()
|
|
1609
|
+
if asset_url:
|
|
1610
|
+
kind = "video" if urlsplit(asset_url).path.lower().endswith(_VIDEO_EXTENSIONS) else "image"
|
|
1611
|
+
if background_preset and background_preset != kind:
|
|
1612
|
+
raise ValueError(
|
|
1613
|
+
f"--background-preset {background_preset!r} conflicts with --background-asset, which is "
|
|
1614
|
+
f"{'a video' if kind == 'video' else 'an image'}; drop --background-preset or pass {kind!r}."
|
|
1615
|
+
)
|
|
1616
|
+
if kind not in _template_background_presets(template_config):
|
|
1617
|
+
raise ValueError(
|
|
1618
|
+
f"template {template_id!r} has no {kind} background, so --background-asset cannot be "
|
|
1619
|
+
f"{'a video' if kind == 'video' else 'an image'}; "
|
|
1620
|
+
f"its backgrounds are: {', '.join(_template_background_presets(template_config)) or '(none)'}"
|
|
1621
|
+
)
|
|
1622
|
+
background_preset = kind
|
|
1623
|
+
|
|
1508
1624
|
if background_preset:
|
|
1509
1625
|
presets = _template_background_presets(template_config)
|
|
1510
1626
|
if background_preset not in presets:
|
|
@@ -1512,19 +1628,175 @@ def _apply_look(
|
|
|
1512
1628
|
f"--background-preset {background_preset!r} is not supported by template {template_id!r}; "
|
|
1513
1629
|
f"choose one of: {', '.join(presets) or '(this template has no background option)'}"
|
|
1514
1630
|
)
|
|
1631
|
+
|
|
1632
|
+
ref_key = "videoAssetRef" if background_preset == "video" else "assetRef"
|
|
1515
1633
|
for scene in dsl.get("scenes", []):
|
|
1516
1634
|
payload = scene.setdefault("customPayload", {})
|
|
1517
1635
|
existing = payload.get("background") if isinstance(payload.get("background"), dict) else {}
|
|
1518
|
-
|
|
1519
|
-
|
|
1636
|
+
background = {**existing, "preset": background_preset}
|
|
1637
|
+
if asset_url:
|
|
1638
|
+
background[ref_key] = BACKGROUND_ASSET_ID
|
|
1639
|
+
payload["background"] = background
|
|
1640
|
+
_lock_scene_path(dsl, "customPayload.background.preset")
|
|
1641
|
+
if asset_url:
|
|
1642
|
+
dsl.setdefault("assets", []).append({
|
|
1643
|
+
"assetId": BACKGROUND_ASSET_ID,
|
|
1644
|
+
"type": background_preset,
|
|
1645
|
+
"source": "existing",
|
|
1646
|
+
"status": "generated",
|
|
1647
|
+
"url": asset_url,
|
|
1648
|
+
})
|
|
1649
|
+
_lock_scene_path(dsl, f"customPayload.background.{ref_key}")
|
|
1650
|
+
elif background_preset in ("image", "video"):
|
|
1520
1651
|
print(
|
|
1521
|
-
f"ℹ️ background preset {background_preset!r} needs a user asset:
|
|
1522
|
-
f"
|
|
1523
|
-
f"otherwise the template falls back to its default background.",
|
|
1652
|
+
f"ℹ️ background preset {background_preset!r} needs a user asset: pass --background-asset "
|
|
1653
|
+
f"<url>, otherwise the template falls back to its default background.",
|
|
1524
1654
|
file=sys.stderr,
|
|
1525
1655
|
)
|
|
1526
1656
|
|
|
1527
1657
|
|
|
1658
|
+
_TRUE_WORDS = ("true", "1", "yes", "on", "开", "是", "显示")
|
|
1659
|
+
_FALSE_WORDS = ("false", "0", "no", "off", "关", "否", "隐藏")
|
|
1660
|
+
|
|
1661
|
+
|
|
1662
|
+
def _payload_node(template_config: dict, parts: list[str], spec: str) -> dict:
|
|
1663
|
+
|
|
1664
|
+
|
|
1665
|
+
|
|
1666
|
+
|
|
1667
|
+
|
|
1668
|
+
|
|
1669
|
+
|
|
1670
|
+
schema = template_config.get("customPayloadSchema") or {}
|
|
1671
|
+
frame = ((template_config.get("capabilities") or {}).get("frameLevelKeys") or {}).get("keys") or []
|
|
1672
|
+
head = parts[0]
|
|
1673
|
+
if isinstance(schema, dict) and isinstance(schema.get(head), dict):
|
|
1674
|
+
node = schema[head]
|
|
1675
|
+
elif head in frame:
|
|
1676
|
+
node = {}
|
|
1677
|
+
else:
|
|
1678
|
+
declared = sorted(set(k for k in schema if not k.startswith("_")) | set(frame))
|
|
1679
|
+
raise ValueError(
|
|
1680
|
+
f"--payload {spec!r}: {head!r} is not a customPayload key of this template; "
|
|
1681
|
+
f"declared keys: {', '.join(declared) or '(none)'}"
|
|
1682
|
+
)
|
|
1683
|
+
for depth, part in enumerate(parts[1:], start=1):
|
|
1684
|
+
props = node.get("properties")
|
|
1685
|
+
if isinstance(props, dict) and isinstance(props.get(part), dict):
|
|
1686
|
+
node = props[part]
|
|
1687
|
+
elif isinstance(props, dict) and props and node.get("additionalProperties") is not True:
|
|
1688
|
+
raise ValueError(
|
|
1689
|
+
f"--payload {spec!r}: {'.'.join(parts[:depth])} has no {part!r}; "
|
|
1690
|
+
f"declared: {', '.join(sorted(props))}"
|
|
1691
|
+
)
|
|
1692
|
+
else:
|
|
1693
|
+
node = {}
|
|
1694
|
+
return node
|
|
1695
|
+
|
|
1696
|
+
|
|
1697
|
+
def _coerce_payload_value(raw: str, node: dict, spec: str):
|
|
1698
|
+
|
|
1699
|
+
declared = node.get("type")
|
|
1700
|
+
types = [declared] if isinstance(declared, str) else list(declared or [])
|
|
1701
|
+
if types and not set(types) & {"string", "boolean", "number", "integer"}:
|
|
1702
|
+
raise ValueError(
|
|
1703
|
+
f"--payload {spec!r}: this key holds {'/'.join(types)}, not a single value; "
|
|
1704
|
+
f"set one of its fields instead (KEY.FIELD=VALUE)."
|
|
1705
|
+
)
|
|
1706
|
+
value: object = raw
|
|
1707
|
+
if "boolean" in types:
|
|
1708
|
+
word = raw.strip().lower()
|
|
1709
|
+
if word in _TRUE_WORDS:
|
|
1710
|
+
value = True
|
|
1711
|
+
elif word in _FALSE_WORDS:
|
|
1712
|
+
value = False
|
|
1713
|
+
elif "string" not in types:
|
|
1714
|
+
raise ValueError(f"--payload {spec!r}: expected true or false")
|
|
1715
|
+
if value is raw and ("number" in types or "integer" in types):
|
|
1716
|
+
try:
|
|
1717
|
+
number = float(raw)
|
|
1718
|
+
except ValueError:
|
|
1719
|
+
if "string" not in types:
|
|
1720
|
+
raise ValueError(f"--payload {spec!r}: expected a number") from None
|
|
1721
|
+
else:
|
|
1722
|
+
if "integer" in types and "number" not in types:
|
|
1723
|
+
if not number.is_integer():
|
|
1724
|
+
raise ValueError(f"--payload {spec!r}: expected a whole number")
|
|
1725
|
+
value = int(number)
|
|
1726
|
+
else:
|
|
1727
|
+
value = int(number) if number.is_integer() else number
|
|
1728
|
+
lo, hi = node.get("minimum"), node.get("maximum")
|
|
1729
|
+
if (lo is not None and value < lo) or (hi is not None and value > hi):
|
|
1730
|
+
raise ValueError(f"--payload {spec!r}: must be within {lo}..{hi}")
|
|
1731
|
+
enum = node.get("enum")
|
|
1732
|
+
if isinstance(enum, list) and value not in enum:
|
|
1733
|
+
raise ValueError(f"--payload {spec!r}: choose one of: {', '.join(str(v) for v in enum)}")
|
|
1734
|
+
return value
|
|
1735
|
+
|
|
1736
|
+
|
|
1737
|
+
def _apply_scene_payload(
|
|
1738
|
+
dsl: dict,
|
|
1739
|
+
template_config: dict | None,
|
|
1740
|
+
template_id: str | None,
|
|
1741
|
+
payload_specs: list[str] | None,
|
|
1742
|
+
motion: str | None,
|
|
1743
|
+
) -> None:
|
|
1744
|
+
|
|
1745
|
+
|
|
1746
|
+
|
|
1747
|
+
|
|
1748
|
+
|
|
1749
|
+
|
|
1750
|
+
|
|
1751
|
+
if not payload_specs and not motion:
|
|
1752
|
+
return
|
|
1753
|
+
if not template_id or template_config is None:
|
|
1754
|
+
raise ValueError("--payload / --motion need --template-id: the allowed keys come from the template.")
|
|
1755
|
+
|
|
1756
|
+
writes = _parse_payload_specs(template_config, payload_specs)
|
|
1757
|
+
if motion:
|
|
1758
|
+
allowed = ("static", "kenburns-in", "kenburns-out", "pan-left", "pan-right", "float")
|
|
1759
|
+
if motion not in allowed:
|
|
1760
|
+
raise ValueError(f"--motion {motion!r}: choose one of: {', '.join(allowed)}")
|
|
1761
|
+
|
|
1762
|
+
for scene in dsl.get("scenes", []):
|
|
1763
|
+
_write_payload(scene.setdefault("customPayload", {}), writes)
|
|
1764
|
+
if motion:
|
|
1765
|
+
hints = scene.get("animationHints") if isinstance(scene.get("animationHints"), dict) else {}
|
|
1766
|
+
scene["animationHints"] = {**hints, "motion": motion}
|
|
1767
|
+
for parts, _ in writes:
|
|
1768
|
+
_lock_scene_path(dsl, "customPayload." + ".".join(parts))
|
|
1769
|
+
if motion:
|
|
1770
|
+
_lock_scene_path(dsl, "animationHints.motion")
|
|
1771
|
+
|
|
1772
|
+
|
|
1773
|
+
def _parse_payload_specs(template_config: dict, payload_specs: list[str] | None) -> PayloadWrites:
|
|
1774
|
+
|
|
1775
|
+
writes: PayloadWrites = []
|
|
1776
|
+
for spec in payload_specs or []:
|
|
1777
|
+
key, sep, raw = spec.partition("=")
|
|
1778
|
+
parts = [p.strip() for p in key.split(".")]
|
|
1779
|
+
if not sep or not all(parts):
|
|
1780
|
+
raise ValueError(f"--payload {spec!r}: expected KEY=VALUE, e.g. grade=四年级 or carousel.fit=cover")
|
|
1781
|
+
raw = raw.strip()
|
|
1782
|
+
if not raw:
|
|
1783
|
+
raise ValueError(f"--payload {spec!r}: empty value; leave the flag out to keep the template default")
|
|
1784
|
+
node = _payload_node(template_config, parts, spec)
|
|
1785
|
+
writes.append((parts, _coerce_payload_value(raw, node, spec)))
|
|
1786
|
+
return writes
|
|
1787
|
+
|
|
1788
|
+
|
|
1789
|
+
def _write_payload(custom_payload: dict, writes: PayloadWrites) -> None:
|
|
1790
|
+
|
|
1791
|
+
for parts, value in writes:
|
|
1792
|
+
node = custom_payload
|
|
1793
|
+
for part in parts[:-1]:
|
|
1794
|
+
if not isinstance(node.get(part), dict):
|
|
1795
|
+
node[part] = {}
|
|
1796
|
+
node = node[part]
|
|
1797
|
+
node[parts[-1]] = value
|
|
1798
|
+
|
|
1799
|
+
|
|
1528
1800
|
def build_dsl(
|
|
1529
1801
|
topic: str,
|
|
1530
1802
|
duration: int,
|
|
@@ -1549,6 +1821,9 @@ def build_dsl(
|
|
|
1549
1821
|
item_count: int | None = None,
|
|
1550
1822
|
source_document_url: str | None = None,
|
|
1551
1823
|
variant: str | None = None,
|
|
1824
|
+
sequence_preset: str | None = None,
|
|
1825
|
+
sequence_fit: str | None = None,
|
|
1826
|
+
payload_writes: PayloadWrites | None = None,
|
|
1552
1827
|
) -> dict:
|
|
1553
1828
|
|
|
1554
1829
|
|
|
@@ -1686,10 +1961,17 @@ def build_dsl(
|
|
|
1686
1961
|
payload_defaults=contract["payload_defaults"],
|
|
1687
1962
|
duration_strategy=contract["duration_strategy"],
|
|
1688
1963
|
duration_model=contract["duration_model"],
|
|
1964
|
+
sequence_schema=((template_config or {}).get("customPayloadSchema") or {}).get("sequence"),
|
|
1965
|
+
sequence_preset=sequence_preset,
|
|
1966
|
+
sequence_fit=sequence_fit,
|
|
1967
|
+
payload_writes=payload_writes,
|
|
1689
1968
|
)
|
|
1690
1969
|
_enforce_supported_duration(carousel_dsl, template_config, template_id)
|
|
1691
1970
|
return carousel_dsl
|
|
1692
1971
|
|
|
1972
|
+
if sequence_preset not in (None, "none") or sequence_fit not in (None, "auto"):
|
|
1973
|
+
raise ValueError(f"--sequence-preset / --sequence-fit only apply to image-sequence templates; {template_id!r} is not one")
|
|
1974
|
+
|
|
1693
1975
|
|
|
1694
1976
|
|
|
1695
1977
|
|
|
@@ -2150,6 +2432,38 @@ Examples:
|
|
|
2150
2432
|
"Written to every scene's customPayload.background.preset. When the user picked a background in "
|
|
2151
2433
|
"the replicate card (e.g. `纯色(solid)`), pass that id. Requires --template-id.",
|
|
2152
2434
|
)
|
|
2435
|
+
parser.add_argument(
|
|
2436
|
+
"--sequence-preset", default=None,
|
|
2437
|
+
help="Image-sequence style preset (one of the template's customPayloadSchema.sequence.preset enum, "
|
|
2438
|
+
"e.g. vlog / news / poster / cinematic / fast-cut), written to customPayload.sequence.preset; "
|
|
2439
|
+
"`none` keeps the template defaults. When the user picked a style preset in the replicate card "
|
|
2440
|
+
"(e.g. `快剪(fast-cut)`), pass that id. Only for templates declaring payloadDefaults.sequence.",
|
|
2441
|
+
)
|
|
2442
|
+
parser.add_argument(
|
|
2443
|
+
"--sequence-fit", default=None,
|
|
2444
|
+
help="How image-sequence media fill the frame (customPayloadSchema.sequence.fit enum: cover = fill and crop, "
|
|
2445
|
+
"contain = show whole with a blurred backdrop), written to customPayload.sequence.fit; `auto` keeps "
|
|
2446
|
+
"the template / preset default. When the user picked it in the replicate card, pass that id.",
|
|
2447
|
+
)
|
|
2448
|
+
parser.add_argument(
|
|
2449
|
+
"--background-asset", default=None,
|
|
2450
|
+
help="URL of the user's background image or video (.mp4/.mov/.webm → video). Added as an existing "
|
|
2451
|
+
"asset and referenced from every scene's customPayload.background (assetRef for an image, "
|
|
2452
|
+
"videoAssetRef for a video); the preset becomes image / video. Requires --template-id and a "
|
|
2453
|
+
"template whose background enum offers that kind.",
|
|
2454
|
+
)
|
|
2455
|
+
parser.add_argument(
|
|
2456
|
+
"--payload", action="append", default=None, metavar="KEY=VALUE",
|
|
2457
|
+
help="Set a template customPayload field on every scene, e.g. --payload grade=四年级 "
|
|
2458
|
+
"--payload carousel.fit=cover --payload templateData.revealMode=progressive. KEY is a dotted path "
|
|
2459
|
+
"checked against the template's customPayloadSchema; VALUE is converted to its declared type "
|
|
2460
|
+
"(true/false, numbers) and checked against its enum / range. Repeatable. Requires --template-id.",
|
|
2461
|
+
)
|
|
2462
|
+
parser.add_argument(
|
|
2463
|
+
"--motion", default=None,
|
|
2464
|
+
help="Camera motion for every scene (animationHints.motion): static / kenburns-in / kenburns-out / "
|
|
2465
|
+
"pan-left / pan-right / float. Replaces the skeleton's alternating default.",
|
|
2466
|
+
)
|
|
2153
2467
|
parser.add_argument("--allow-digital-human", action="store_true", help="Allow digital-human assets")
|
|
2154
2468
|
parser.add_argument("--allow-ai-video", action="store_true", help="Allow AI-generated video assets")
|
|
2155
2469
|
parser.add_argument("--template-id", default=None, help="Template id. The template owns outputLanguage and may declare a defaultVoiceId.")
|
|
@@ -2353,6 +2667,16 @@ Examples:
|
|
|
2353
2667
|
file=sys.stderr,
|
|
2354
2668
|
)
|
|
2355
2669
|
|
|
2670
|
+
try:
|
|
2671
|
+
|
|
2672
|
+
|
|
2673
|
+
payload_writes = (
|
|
2674
|
+
_parse_payload_specs(_load_template_config(args.template_id) or {}, args.payload)
|
|
2675
|
+
if args.payload and args.template_id else []
|
|
2676
|
+
)
|
|
2677
|
+
except ValueError as exc:
|
|
2678
|
+
parser.error(str(exc))
|
|
2679
|
+
|
|
2356
2680
|
try:
|
|
2357
2681
|
dsl = build_dsl(
|
|
2358
2682
|
topic=args.topic,
|
|
@@ -2378,6 +2702,9 @@ Examples:
|
|
|
2378
2702
|
item_count=args.item_count,
|
|
2379
2703
|
source_document_url=args.source_document_url,
|
|
2380
2704
|
variant=args.variant,
|
|
2705
|
+
sequence_preset=args.sequence_preset,
|
|
2706
|
+
sequence_fit=args.sequence_fit,
|
|
2707
|
+
payload_writes=payload_writes,
|
|
2381
2708
|
)
|
|
2382
2709
|
except ValueError as exc:
|
|
2383
2710
|
|
|
@@ -2386,13 +2713,16 @@ Examples:
|
|
|
2386
2713
|
parser.error(str(exc))
|
|
2387
2714
|
|
|
2388
2715
|
try:
|
|
2716
|
+
look_config = _load_template_config(args.template_id) if args.template_id else None
|
|
2389
2717
|
_apply_look(
|
|
2390
2718
|
dsl,
|
|
2391
|
-
|
|
2719
|
+
look_config,
|
|
2392
2720
|
args.template_id,
|
|
2393
2721
|
args.variant,
|
|
2394
2722
|
args.background_preset,
|
|
2723
|
+
args.background_asset,
|
|
2395
2724
|
)
|
|
2725
|
+
_apply_scene_payload(dsl, look_config, args.template_id, args.payload, args.motion)
|
|
2396
2726
|
except ValueError as exc:
|
|
2397
2727
|
parser.error(str(exc))
|
|
2398
2728
|
|
|
@@ -54,7 +54,38 @@
|
|
|
54
54
|
},
|
|
55
55
|
"background_preset": {
|
|
56
56
|
"type": "string",
|
|
57
|
-
"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`
|
|
57
|
+
"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` need the user's file: pass it as background_asset (which also sets the preset)."
|
|
58
|
+
},
|
|
59
|
+
"sequence_preset": {
|
|
60
|
+
"type": "string",
|
|
61
|
+
"description": "Image-sequence style preset, one of the template's customPayloadSchema.sequence.preset values (image-to-video: vlog / news / poster / cinematic / fast-cut). Written to customPayload.sequence.preset; the preset sets transitions, motion, text layout and pacing (fast-cut shortens every image to 1.5s, and the scene duration follows). **Must** pass when the user picked a style preset: the replicate card shows `风格预设: 快剪(fast-cut)` → pass `fast-cut`; `none` keeps the template defaults. Without it the choice is silently dropped. Only for templates declaring capabilities.payloadDefaults.sequence; unsupported values are rejected."
|
|
62
|
+
},
|
|
63
|
+
"sequence_fit": {
|
|
64
|
+
"type": "string",
|
|
65
|
+
"description": "How image-sequence media fill the frame, one of customPayloadSchema.sequence.fit (cover = fill the frame and crop, contain = show the whole picture over a blurred backdrop). Written to customPayload.sequence.fit and beats the preset's own fit. **Must** pass when the user picked it: `显示方式: 完整显示(contain)` → pass `contain`; `auto` keeps the template / preset default. Only for templates declaring capabilities.payloadDefaults.sequence."
|
|
66
|
+
},
|
|
67
|
+
"background_asset": {
|
|
68
|
+
"type": "string",
|
|
69
|
+
"description": "URL of the user's background image or video (the replicate card's 背景素材 / 背景图片 asset line → its URL). Added as an existing asset and referenced from every scene's customPayload.background (assetRef for an image, videoAssetRef for a video; .mp4/.mov/.webm/.m4v count as video); the background preset becomes image / video, so background_preset can be omitted. **Must** pass when the user gave a background file — without it a preset of image / video renders the template's plain default. Requires template_id; rejected when the template has no background of that kind."
|
|
70
|
+
},
|
|
71
|
+
"payload": {
|
|
72
|
+
"type": "array",
|
|
73
|
+
"items": {
|
|
74
|
+
"type": "string"
|
|
75
|
+
},
|
|
76
|
+
"description": "Template-specific card choices as `KEY=VALUE`, written to every scene's customPayload and locked so the dsl_json you save cannot overwrite them. KEY is a dotted path into the template's customPayloadSchema (e.g. `grade=四年级`, `watermarkText=WorkBuddy 实战课`, `preset=course`, `carousel.fit=cover`, `templateData.revealMode=progressive`, `cardStyle.championFx=false`); VALUE is the id inside the card line's parentheses, or the text verbatim for free-text fields. Booleans take true/false, numbers are range-checked, enums are checked — an undeclared key or value is rejected with the allowed list. **Must** pass every card line that the system prompt maps to a payload key; without it the choice is silently dropped. Requires template_id."
|
|
77
|
+
},
|
|
78
|
+
"motion": {
|
|
79
|
+
"type": "string",
|
|
80
|
+
"enum": [
|
|
81
|
+
"static",
|
|
82
|
+
"kenburns-in",
|
|
83
|
+
"kenburns-out",
|
|
84
|
+
"pan-left",
|
|
85
|
+
"pan-right",
|
|
86
|
+
"float"
|
|
87
|
+
],
|
|
88
|
+
"description": "Camera motion for every scene (scenes[].animationHints.motion), replacing the skeleton's alternating push-in / pull-out. **Must** pass when the user picked one: `运镜效果: 缓慢推近(kenburns-in)` → `kenburns-in`."
|
|
58
89
|
},
|
|
59
90
|
"font_id": {
|
|
60
91
|
"type": "string",
|
|
@@ -70,12 +101,18 @@
|
|
|
70
101
|
},
|
|
71
102
|
"template_source": {
|
|
72
103
|
"type": "string",
|
|
73
|
-
"enum": [
|
|
104
|
+
"enum": [
|
|
105
|
+
"user-explicit",
|
|
106
|
+
"user-confirmed",
|
|
107
|
+
"auto"
|
|
108
|
+
],
|
|
74
109
|
"description": "How template_id was chosen; recorded at meta.templateSelection.source. **Always pass it with template_id.** `user-explicit`: the user named this template — its id or name, a pasted template detail-page prompt or link, or it was settled earlier in this conversation. `user-confirmed`: the user did not name one, you offered candidates and the user picked or approved this one in the chat. `auto`: you picked it yourself and the user has not confirmed — gen_script refuses, so instead of calling it, show the candidates and end the turn. Never mark a template user-confirmed that the user has not seen and approved."
|
|
75
110
|
},
|
|
76
111
|
"template_candidates": {
|
|
77
112
|
"type": "array",
|
|
78
|
-
"items": {
|
|
113
|
+
"items": {
|
|
114
|
+
"type": "string"
|
|
115
|
+
},
|
|
79
116
|
"description": "The template ids you offered the user, recommended first; recorded at meta.templateSelection.candidates. Pass with template_source=user-confirmed — template_id must then be one of them (a template the user named outside the shortlist is user-explicit)."
|
|
80
117
|
},
|
|
81
118
|
"source_document_url": {
|
|
@@ -95,7 +132,7 @@
|
|
|
95
132
|
"items": {
|
|
96
133
|
"type": "string"
|
|
97
134
|
},
|
|
98
|
-
"description": "Media URLs for the template's image/video carousel. When provided together with a template_id whose capabilities.payloadStyle=carousel-caption, these URLs are placed into customPayload.sequence.items when payloadDefaults.sequence is declared, otherwise customPayload.carousel.items, as existing assets — NO AI image generation is triggered. **Must** pass when the user explicitly provides image/video URLs for such a template. Each element is a full URL string."
|
|
135
|
+
"description": "Media URLs for the template's image/video carousel. When provided together with a template_id whose capabilities.payloadStyle=carousel-caption, these URLs are placed into customPayload.sequence.items when payloadDefaults.sequence is declared, otherwise customPayload.carousel.items, as existing assets — NO AI image generation is triggered. **Must** pass when the user explicitly provides image/video URLs for such a template. Each element is a full URL string. Image sequences (payloadDefaults.sequence) accept video URLs too (.mp4/.mov/.webm): gen_script probes each clip's real length, marks it type=video and plays it whole and muted; an unreadable video is rejected."
|
|
99
136
|
},
|
|
100
137
|
"scene_images": {
|
|
101
138
|
"type": "array",
|
|
@@ -109,7 +146,7 @@
|
|
|
109
146
|
"items": {
|
|
110
147
|
"type": "string"
|
|
111
148
|
},
|
|
112
|
-
"description": "On-screen caption lines for templates that declare a caption area (capabilities.payloadDefaults.caption). For image sequences (payloadDefaults.sequence), lines map in order to sequence.items[].caption, with at most one line per image. Otherwise each element is one line. Line-count limits and whether **emphasis** is parsed are declared by the template — read its llmHint, which gen_script prints in full on stderr. **Must** pass when the user explicitly provides bullet-point text / bottom copy. **Also must pass — written by you — when the user did NOT provide any copy but the template is typewriter-driven** (capabilities.durationStrategy=fit-caption): such templates have no narration, so these lines are both the video's content and the thing that decides its duration. Draft them from the material you researched (repo README, page screenshots, the topic). Leaving this empty for a fit-caption template is rejected: nothing auto-generates caption text, and an empty caption renders a titled carousel with no words at all."
|
|
149
|
+
"description": "On-screen caption lines for templates that declare a caption area (capabilities.payloadDefaults.caption). For image sequences (payloadDefaults.sequence), lines map in order to sequence.items[].caption, with at most one line per image. A replicate asset line's `(… desc: <text>)` suffix is the user's caption for that image: pass it here in the same order (use an empty string for images without one). Otherwise each element is one line. Line-count limits and whether **emphasis** is parsed are declared by the template — read its llmHint, which gen_script prints in full on stderr. **Must** pass when the user explicitly provides bullet-point text / bottom copy. **Also must pass — written by you — when the user did NOT provide any copy but the template is typewriter-driven** (capabilities.durationStrategy=fit-caption): such templates have no narration, so these lines are both the video's content and the thing that decides its duration. Draft them from the material you researched (repo README, page screenshots, the topic). Leaving this empty for a fit-caption template is rejected: nothing auto-generates caption text, and an empty caption renders a titled carousel with no words at all."
|
|
113
150
|
},
|
|
114
151
|
"stub_image_url": {
|
|
115
152
|
"type": "string",
|
|
@@ -149,6 +186,11 @@
|
|
|
149
186
|
"speed",
|
|
150
187
|
"variant",
|
|
151
188
|
"background_preset",
|
|
189
|
+
"sequence_preset",
|
|
190
|
+
"sequence_fit",
|
|
191
|
+
"background_asset",
|
|
192
|
+
"payload",
|
|
193
|
+
"motion",
|
|
152
194
|
"font_id",
|
|
153
195
|
"font_name",
|
|
154
196
|
"list_fonts",
|
|
@@ -55,6 +55,11 @@
|
|
|
55
55
|
},
|
|
56
56
|
"audience": { "type": "string" },
|
|
57
57
|
"description": { "type": "string" },
|
|
58
|
+
"lockedScenePaths": {
|
|
59
|
+
"type": "array",
|
|
60
|
+
"items": { "type": "string" },
|
|
61
|
+
"description": "gen_script 用 CLI 参数(--payload / --background-asset / --background-preset / --motion)写进每个场景的值,按场景相对路径记录(如 customPayload.carousel.fit、animationHints.motion)。merge 模式合并模型提交的 DSL 后,ab-agent 按这张表把骨架里的值写回,模型写的同名对象盖不掉用户在参数卡里的选择。"
|
|
62
|
+
},
|
|
58
63
|
"templateSelection": {
|
|
59
64
|
"type": "object",
|
|
60
65
|
"description": "模板是怎么定下来的(gen_script --template-source / --template-candidates 写入)。user-explicit = 用户点名;user-confirmed = agent 推荐、用户确认。未经确认的 auto 选择不会出现在这里:gen_script 直接拒绝生成。",
|