@remixmate/cli 0.12.9 → 0.13.1
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 +7 -2
- package/README.zh-CN.md +1 -1
- package/dist/manifest.json +20 -10
- package/dist/template/preview.js +7 -7
- package/dist/template/publish.js +8 -8
- package/dist/template/scene-props.js +1 -0
- package/dist/template/still-cases.js +1 -1
- package/package.json +1 -1
- package/skills/export-jianying/scripts/gen_jianying_draft.py +38 -33
- package/skills/export-jianying/version.json +1 -1
- package/skills/gen-script/SKILL.md +15 -14
- package/skills/gen-script/scripts/gen_script.py +855 -568
- package/skills/gen-script/skill.json +14 -8
- package/skills/gen-script/version.json +1 -1
- package/skills/render-video/scripts/_video_probe.py +0 -1
- package/skills/render-video/scripts/remote_renderer_client.py +9 -7
- package/skills/render-video/scripts/render_video.py +553 -391
- package/skills/render-video/version.json +1 -1
- package/skills/template-registry/SKILL.md +1 -1
- package/skills/template-registry/scripts/match_template.py +2 -2
- package/skills/template-registry/scripts/payload_hydrator.py +9 -1
- package/skills/template-registry/scripts/render_job_client.py +4 -0
- package/skills/template-registry/version.json +1 -1
- package/skills/template-registry/video_dsl/runtime/dsl_validator.py +50 -24
- package/skills/template-registry/video_dsl/schema/render-plan-v1alpha1.json +7 -1
- package/skills/video-parser/scripts/analyze_video.py +2 -0
- package/skills/video-parser/version.json +1 -1
|
@@ -23,6 +23,7 @@ import os
|
|
|
23
23
|
import sys
|
|
24
24
|
import uuid
|
|
25
25
|
from copy import deepcopy
|
|
26
|
+
from dataclasses import dataclass
|
|
26
27
|
from urllib.parse import urlsplit
|
|
27
28
|
|
|
28
29
|
SCHEMA_PATH = os.environ.get(
|
|
@@ -581,6 +582,10 @@ def _template_primary_visual_type(template_config: dict | None) -> str:
|
|
|
581
582
|
return "none"
|
|
582
583
|
|
|
583
584
|
|
|
585
|
+
|
|
586
|
+
NARRATION_MODES = ("auto", "on", "off")
|
|
587
|
+
|
|
588
|
+
|
|
584
589
|
def _template_needs_narration(template_config: dict | None) -> bool:
|
|
585
590
|
|
|
586
591
|
|
|
@@ -1045,6 +1050,29 @@ def _estimate_carousel_min_sec(carousel: dict, model: dict) -> float:
|
|
|
1045
1050
|
return total - (n - 1) * transition_sec
|
|
1046
1051
|
|
|
1047
1052
|
|
|
1053
|
+
|
|
1054
|
+
|
|
1055
|
+
|
|
1056
|
+
|
|
1057
|
+
|
|
1058
|
+
|
|
1059
|
+
|
|
1060
|
+
_DURATION_KINDS = ("typewriter", "slideshow", "frame-sequence")
|
|
1061
|
+
_LEGACY_DURATION_KINDS = {"caption": "typewriter", "carousel": "slideshow", "sequence": "frame-sequence"}
|
|
1062
|
+
|
|
1063
|
+
|
|
1064
|
+
def _duration_components(duration_model: dict | None, kind: str) -> list[tuple[str, dict]]:
|
|
1065
|
+
|
|
1066
|
+
out = []
|
|
1067
|
+
for key, comp in (duration_model or {}).items():
|
|
1068
|
+
if not isinstance(comp, dict):
|
|
1069
|
+
continue
|
|
1070
|
+
comp_kind = comp.get("kind") or _LEGACY_DURATION_KINDS.get(key)
|
|
1071
|
+
if comp_kind == kind:
|
|
1072
|
+
out.append((key, comp))
|
|
1073
|
+
return out
|
|
1074
|
+
|
|
1075
|
+
|
|
1048
1076
|
def _estimate_fit_caption_duration(
|
|
1049
1077
|
custom_payload: dict, duration_model: dict | None, template_id: str | None
|
|
1050
1078
|
) -> float:
|
|
@@ -1060,35 +1088,39 @@ def _estimate_fit_caption_duration(
|
|
|
1060
1088
|
f"⚠️ template '{template_id}' declares durationStrategy=fit-caption but no "
|
|
1061
1089
|
"capabilities.durationModel, so gen_script cannot compute the fitted "
|
|
1062
1090
|
"duration and falls back to the requested --duration.\n"
|
|
1063
|
-
" Fix in the template (not here): add capabilities.durationModel with
|
|
1064
|
-
"
|
|
1065
|
-
"
|
|
1066
|
-
"
|
|
1091
|
+
" Fix in the template (not here): add capabilities.durationModel with "
|
|
1092
|
+
"headlineIntroSec / tailPaddingSec plus one component per paced payload field, "
|
|
1093
|
+
"keyed by that field: {kind: typewriter, charsPerSec, lineGapMs, startDelayMs, "
|
|
1094
|
+
"tailHoldSec, maxLines} or {kind: slideshow, transitionMs, defaultHoldSec, "
|
|
1095
|
+
"minPerImageSec, minPerVideoSec}.",
|
|
1067
1096
|
file=sys.stderr,
|
|
1068
1097
|
)
|
|
1069
1098
|
return 0.0
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
custom_payload.get(
|
|
1075
|
-
)
|
|
1076
|
-
headline_intro = duration_model.get("headlineIntroSec", 0)
|
|
1077
|
-
return max(headline_intro, caption_sec, carousel_sec) + duration_model.get("tailPaddingSec", 0)
|
|
1099
|
+
parts = [duration_model.get("headlineIntroSec", 0)]
|
|
1100
|
+
for key, comp in _duration_components(duration_model, "typewriter"):
|
|
1101
|
+
parts.append(_estimate_caption_natural_sec(custom_payload.get(key) or {}, comp))
|
|
1102
|
+
for key, comp in _duration_components(duration_model, "slideshow"):
|
|
1103
|
+
parts.append(_estimate_carousel_min_sec(custom_payload.get(key) or {}, comp))
|
|
1104
|
+
return max(parts) + duration_model.get("tailPaddingSec", 0)
|
|
1078
1105
|
|
|
1079
1106
|
|
|
1080
|
-
def _estimate_fit_images_duration(
|
|
1107
|
+
def _estimate_fit_images_duration(custom_payload: dict, model: dict | None, fps: int = 30) -> float:
|
|
1081
1108
|
|
|
1082
1109
|
|
|
1083
1110
|
|
|
1084
1111
|
|
|
1085
1112
|
|
|
1086
|
-
|
|
1113
|
+
|
|
1114
|
+
comps = _duration_components(model, "frame-sequence")
|
|
1115
|
+
if not comps:
|
|
1116
|
+
raise ValueError("fit-images requires a durationModel component of kind frame-sequence")
|
|
1117
|
+
key, timing = comps[0]
|
|
1118
|
+
sequence = custom_payload.get(key) or {}
|
|
1087
1119
|
tiers = timing.get("holdSecByCount", [])
|
|
1088
|
-
count = len(sequence
|
|
1120
|
+
count = len(sequence.get("items") or [])
|
|
1089
1121
|
rule_hold = next((tier["holdSec"] for tier in tiers if count <= tier["maxItems"]), None)
|
|
1090
1122
|
if rule_hold is None:
|
|
1091
|
-
raise ValueError("fit-images requires durationModel.
|
|
1123
|
+
raise ValueError(f"fit-images requires durationModel.{key}.holdSecByCount covering the item count")
|
|
1092
1124
|
|
|
1093
1125
|
|
|
1094
1126
|
preset_timing = (timing.get("presets") or {}).get(sequence.get("preset")) or {}
|
|
@@ -1096,7 +1128,7 @@ def _estimate_fit_images_duration(sequence: dict, model: dict | None, fps: int =
|
|
|
1096
1128
|
transition_ms = sequence.get("transitionMs", preset_timing.get("transitionMs", timing.get("transitionMs")))
|
|
1097
1129
|
margin = timing.get("minHoldMarginFrames")
|
|
1098
1130
|
if transition_ms is None or margin is None:
|
|
1099
|
-
raise ValueError("fit-images requires
|
|
1131
|
+
raise ValueError(f"fit-images requires {key}.transitionMs and durationModel.{key}.minHoldMarginFrames")
|
|
1100
1132
|
|
|
1101
1133
|
min_frames = max(2, math.floor(transition_ms / 1000 * fps + 0.5)) + margin
|
|
1102
1134
|
|
|
@@ -1125,8 +1157,8 @@ def _probe_sequence_video_duration(url: str) -> float | None:
|
|
|
1125
1157
|
return probe_video_duration(url)
|
|
1126
1158
|
|
|
1127
1159
|
|
|
1128
|
-
def
|
|
1129
|
-
|
|
1160
|
+
def _media_option_choice(option_schema: dict, value: str | None, flag: str,
|
|
1161
|
+
template_id: str, passthrough: str) -> str | None:
|
|
1130
1162
|
|
|
1131
1163
|
|
|
1132
1164
|
|
|
@@ -1134,8 +1166,7 @@ def _sequence_choice(sequence_schema: dict, key: str, value: str | None, flag: s
|
|
|
1134
1166
|
|
|
1135
1167
|
if not value or value == passthrough:
|
|
1136
1168
|
return None
|
|
1137
|
-
|
|
1138
|
-
allowed = [str(v) for v in prop.get("enum") or []]
|
|
1169
|
+
allowed = [str(v) for v in (option_schema or {}).get("enum") or []]
|
|
1139
1170
|
if value not in allowed:
|
|
1140
1171
|
raise ValueError(
|
|
1141
1172
|
f"{flag} {value!r} is not supported by template {template_id!r}; "
|
|
@@ -1145,6 +1176,71 @@ def _sequence_choice(sequence_schema: dict, key: str, value: str | None, flag: s
|
|
|
1145
1176
|
|
|
1146
1177
|
|
|
1147
1178
|
|
|
1179
|
+
|
|
1180
|
+
|
|
1181
|
+
|
|
1182
|
+
|
|
1183
|
+
|
|
1184
|
+
|
|
1185
|
+
|
|
1186
|
+
|
|
1187
|
+
|
|
1188
|
+
|
|
1189
|
+
|
|
1190
|
+
|
|
1191
|
+
|
|
1192
|
+
|
|
1193
|
+
|
|
1194
|
+
|
|
1195
|
+
_MEDIA_OPTION_FLAGS = {"preset": "--sequence-preset", "fit": "--sequence-fit"}
|
|
1196
|
+
_LEGACY_MEDIA_BINDINGS = {
|
|
1197
|
+
"sequence": {
|
|
1198
|
+
"itemsPath": "sequence.items",
|
|
1199
|
+
"videoItem": {"type": "video"},
|
|
1200
|
+
"videoDurationKey": "sourceDurationSec",
|
|
1201
|
+
"captions": {"perItemKey": "caption"},
|
|
1202
|
+
"requireItems": True,
|
|
1203
|
+
"options": {"preset": {"none": "none", "replacesDefaults": True}, "fit": {"none": "auto"}},
|
|
1204
|
+
},
|
|
1205
|
+
"carousel": {
|
|
1206
|
+
"itemsPath": "carousel.items",
|
|
1207
|
+
"item": {"entrance": "random", "exit": "random"},
|
|
1208
|
+
"videoItem": {"kind": "video", "muted": True},
|
|
1209
|
+
"captions": {"linesPath": "caption.lines"},
|
|
1210
|
+
},
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
|
|
1214
|
+
def _resolve_media_binding(template_config: dict | None) -> dict:
|
|
1215
|
+
caps = (template_config or {}).get("capabilities")
|
|
1216
|
+
caps = caps if isinstance(caps, dict) else {}
|
|
1217
|
+
binding = caps.get("mediaBinding")
|
|
1218
|
+
if isinstance(binding, dict) and isinstance(binding.get("itemsPath"), str) and binding["itemsPath"]:
|
|
1219
|
+
return deepcopy(binding)
|
|
1220
|
+
defaults = caps.get("payloadDefaults") if isinstance(caps.get("payloadDefaults"), dict) else {}
|
|
1221
|
+
legacy = "sequence" if isinstance(defaults.get("sequence"), dict) else "carousel"
|
|
1222
|
+
return deepcopy(_LEGACY_MEDIA_BINDINGS[legacy])
|
|
1223
|
+
|
|
1224
|
+
|
|
1225
|
+
def _schema_at(template_config: dict | None, parts: list[str]) -> dict:
|
|
1226
|
+
|
|
1227
|
+
node = (template_config or {}).get("customPayloadSchema") or {}
|
|
1228
|
+
for i, part in enumerate(parts):
|
|
1229
|
+
if not isinstance(node, dict):
|
|
1230
|
+
return {}
|
|
1231
|
+
node = node.get(part) if i == 0 else (node.get("properties") or {}).get(part)
|
|
1232
|
+
return node if isinstance(node, dict) else {}
|
|
1233
|
+
|
|
1234
|
+
|
|
1235
|
+
def _get_path(obj: dict, parts: list[str]):
|
|
1236
|
+
for part in parts:
|
|
1237
|
+
if not isinstance(obj, dict):
|
|
1238
|
+
return None
|
|
1239
|
+
obj = obj.get(part)
|
|
1240
|
+
return obj
|
|
1241
|
+
|
|
1242
|
+
|
|
1243
|
+
|
|
1148
1244
|
PayloadWrites = list[tuple[list[str], object]]
|
|
1149
1245
|
|
|
1150
1246
|
|
|
@@ -1169,7 +1265,8 @@ def _build_carousel_caption_dsl(
|
|
|
1169
1265
|
payload_defaults: dict,
|
|
1170
1266
|
duration_strategy: str | None,
|
|
1171
1267
|
duration_model: dict | None,
|
|
1172
|
-
|
|
1268
|
+
media_binding: dict,
|
|
1269
|
+
template_config: dict | None = None,
|
|
1173
1270
|
sequence_preset: str | None = None,
|
|
1174
1271
|
sequence_fit: str | None = None,
|
|
1175
1272
|
payload_writes: PayloadWrites | None = None,
|
|
@@ -1180,33 +1277,51 @@ def _build_carousel_caption_dsl(
|
|
|
1180
1277
|
|
|
1181
1278
|
|
|
1182
1279
|
|
|
1280
|
+
|
|
1183
1281
|
defaults = deepcopy(payload_defaults or {})
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1282
|
+
binding = media_binding
|
|
1283
|
+
items_parts = binding["itemsPath"].split(".")
|
|
1284
|
+
container_parts = items_parts[:-1]
|
|
1285
|
+
captions = binding.get("captions") if isinstance(binding.get("captions"), dict) else {}
|
|
1286
|
+
per_item_caption = captions.get("perItemKey")
|
|
1287
|
+
lines_parts = captions["linesPath"].split(".") if isinstance(captions.get("linesPath"), str) else None
|
|
1288
|
+
options = binding.get("options") if isinstance(binding.get("options"), dict) else {}
|
|
1289
|
+
|
|
1290
|
+
if binding.get("requireItems") and not carousel_items:
|
|
1291
|
+
raise ValueError("This template is driven by media items; pass --carousel-items (caption text alone cannot supply them)")
|
|
1292
|
+
if per_item_caption and len(caption_lines) > len(carousel_items):
|
|
1293
|
+
raise ValueError("More --caption-lines than --carousel-items; this template takes at most one caption per item")
|
|
1294
|
+
|
|
1295
|
+
|
|
1296
|
+
|
|
1297
|
+
picked: dict[str, str] = {}
|
|
1298
|
+
for key, value in (("preset", sequence_preset), ("fit", sequence_fit)):
|
|
1299
|
+
flag = _MEDIA_OPTION_FLAGS[key]
|
|
1300
|
+
opt = options.get(key)
|
|
1301
|
+
none_value = opt.get("none") if isinstance(opt, dict) else ("none" if key == "preset" else "auto")
|
|
1302
|
+
if not isinstance(opt, dict) or not container_parts:
|
|
1303
|
+
if value not in (None, none_value, "none", "auto"):
|
|
1304
|
+
raise ValueError(f"{flag} is not an option of template {template_id!r} (no mediaBinding.options.{key})")
|
|
1305
|
+
continue
|
|
1306
|
+
choice = _media_option_choice(_schema_at(template_config, container_parts + [key]),
|
|
1307
|
+
value, flag, template_id, none_value)
|
|
1308
|
+
if choice:
|
|
1309
|
+
picked[key] = choice
|
|
1196
1310
|
|
|
1197
1311
|
|
|
1312
|
+
duration_key = binding.get("videoDurationKey")
|
|
1198
1313
|
assets = []
|
|
1199
|
-
|
|
1314
|
+
media_assets = []
|
|
1200
1315
|
video_seconds: dict[str, float] = {}
|
|
1201
1316
|
for idx, url in enumerate(carousel_items):
|
|
1202
1317
|
asset_id = f"carousel-img-{idx + 1:02d}"
|
|
1203
1318
|
|
|
1204
1319
|
is_video = urlsplit(url).path.lower().endswith((".mp4", ".mov", ".webm", ".avi"))
|
|
1205
|
-
if
|
|
1320
|
+
if duration_key and is_video:
|
|
1206
1321
|
seconds = _probe_sequence_video_duration(url)
|
|
1207
1322
|
if not seconds or seconds <= 0:
|
|
1208
1323
|
raise ValueError(
|
|
1209
|
-
f"Cannot read the duration of video {url!r};
|
|
1324
|
+
f"Cannot read the duration of video {url!r}; this template needs it to time the clip. "
|
|
1210
1325
|
f"Check the URL is reachable, or leave this video out."
|
|
1211
1326
|
)
|
|
1212
1327
|
video_seconds[asset_id] = round(float(seconds), 3)
|
|
@@ -1217,7 +1332,7 @@ def _build_carousel_caption_dsl(
|
|
|
1217
1332
|
"status": "generated",
|
|
1218
1333
|
"url": url,
|
|
1219
1334
|
})
|
|
1220
|
-
|
|
1335
|
+
media_assets.append((asset_id, "video" if is_video else "image"))
|
|
1221
1336
|
|
|
1222
1337
|
|
|
1223
1338
|
narration_asset_id = "narration-scene-01"
|
|
@@ -1233,84 +1348,50 @@ def _build_carousel_caption_dsl(
|
|
|
1233
1348
|
})
|
|
1234
1349
|
|
|
1235
1350
|
|
|
1236
|
-
carousel_defaults = defaults.get("carousel", {})
|
|
1237
|
-
carousel_config = {
|
|
1238
|
-
"items": [],
|
|
1239
|
-
**carousel_defaults,
|
|
1240
|
-
}
|
|
1241
|
-
for asset_id, kind in carousel_asset_ids:
|
|
1242
|
-
item = {
|
|
1243
|
-
"assetRef": asset_id,
|
|
1244
|
-
"entrance": carousel_defaults.get("defaultEntrance", "random"),
|
|
1245
|
-
"exit": carousel_defaults.get("defaultExit", "random"),
|
|
1246
|
-
}
|
|
1247
|
-
if kind == "video":
|
|
1248
|
-
item["kind"] = "video"
|
|
1249
|
-
item["muted"] = True
|
|
1250
|
-
carousel_config["items"].append(item)
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
caption_defaults = defaults.get("caption", {})
|
|
1254
|
-
caption_config = {
|
|
1255
|
-
"lines": caption_lines if caption_lines else [],
|
|
1256
|
-
**caption_defaults,
|
|
1257
|
-
}
|
|
1258
|
-
|
|
1259
|
-
if caption_lines:
|
|
1260
|
-
caption_config["lines"] = caption_lines
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
custom_payload = {
|
|
1272
|
-
"carousel": carousel_config,
|
|
1273
|
-
"caption": caption_config,
|
|
1274
|
-
}
|
|
1275
|
-
for key in ("background", "headlineStyle"):
|
|
1276
|
-
if defaults.get(key):
|
|
1277
|
-
custom_payload[key] = defaults[key]
|
|
1278
|
-
|
|
1279
1351
|
|
|
1280
1352
|
|
|
1281
1353
|
|
|
1282
|
-
if sequence_mode:
|
|
1283
1354
|
|
|
1284
1355
|
|
|
1356
|
+
custom_payload = defaults
|
|
1357
|
+
if container_parts:
|
|
1358
|
+
container = custom_payload
|
|
1359
|
+
for part in container_parts:
|
|
1360
|
+
if not isinstance(container.get(part), dict):
|
|
1361
|
+
container[part] = {}
|
|
1362
|
+
container = container[part]
|
|
1363
|
+
if any(isinstance(o, dict) and o.get("replacesDefaults") and k in picked for k, o in options.items()):
|
|
1364
|
+
container.clear()
|
|
1365
|
+
container.update(picked)
|
|
1366
|
+
else:
|
|
1367
|
+
container = custom_payload
|
|
1368
|
+
|
|
1369
|
+
item_extra = binding.get("item") if isinstance(binding.get("item"), dict) else {}
|
|
1370
|
+
video_extra = binding.get("videoItem") if isinstance(binding.get("videoItem"), dict) else {}
|
|
1371
|
+
container[items_parts[-1]] = [
|
|
1372
|
+
{"assetRef": asset_id,
|
|
1373
|
+
**item_extra,
|
|
1374
|
+
**(video_extra if kind == "video" else {}),
|
|
1375
|
+
**({duration_key: video_seconds[asset_id]} if duration_key and kind == "video" else {}),
|
|
1376
|
+
**({per_item_caption: caption_lines[index]}
|
|
1377
|
+
if per_item_caption and index < len(caption_lines) and caption_lines[index] else {})}
|
|
1378
|
+
for index, (asset_id, kind) in enumerate(media_assets)
|
|
1379
|
+
]
|
|
1380
|
+
if lines_parts:
|
|
1285
1381
|
|
|
1286
|
-
|
|
1287
|
-
if sequence_fit:
|
|
1288
|
-
sequence_config["fit"] = sequence_fit
|
|
1289
|
-
sequence_config["items"] = [
|
|
1290
|
-
{"assetRef": asset_id,
|
|
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)
|
|
1294
|
-
]
|
|
1295
|
-
custom_payload = {"sequence": sequence_config}
|
|
1382
|
+
_write_payload(custom_payload, [(lines_parts, list(caption_lines))])
|
|
1296
1383
|
|
|
1297
1384
|
|
|
1298
1385
|
|
|
1299
1386
|
_write_payload(custom_payload, payload_writes or [])
|
|
1300
1387
|
|
|
1301
1388
|
|
|
1302
|
-
narration_text = _localize_label(output_language, "narration_opening",
|
|
1303
|
-
label="", topic=_clip(topic, _PLACEHOLDER_TOPIC_CHARS), duration=duration)
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
1389
|
|
|
1307
1390
|
|
|
1308
1391
|
|
|
1309
1392
|
effective_duration = duration
|
|
1310
1393
|
if duration_strategy == "fit-images":
|
|
1311
|
-
|
|
1312
|
-
raise ValueError("fit-images requires capabilities.payloadDefaults.sequence")
|
|
1313
|
-
effective_duration = _estimate_fit_images_duration(custom_payload["sequence"], duration_model)
|
|
1394
|
+
effective_duration = _estimate_fit_images_duration(custom_payload, duration_model)
|
|
1314
1395
|
if duration_strategy == "fit-caption":
|
|
1315
1396
|
est = _estimate_fit_caption_duration(custom_payload, duration_model, template_id)
|
|
1316
1397
|
if est > 0:
|
|
@@ -1322,6 +1403,33 @@ def _build_carousel_caption_dsl(
|
|
|
1322
1403
|
)
|
|
1323
1404
|
|
|
1324
1405
|
|
|
1406
|
+
|
|
1407
|
+
|
|
1408
|
+
narration_text = _localize_label(
|
|
1409
|
+
output_language, "narration_opening", label="", topic=_clip(topic, _PLACEHOLDER_TOPIC_CHARS),
|
|
1410
|
+
duration=effective_duration,
|
|
1411
|
+
budget=_budget_suffix(effective_duration, output_language, narration_speed, None),
|
|
1412
|
+
)
|
|
1413
|
+
narration_locks: list[str] = []
|
|
1414
|
+
if narration_enabled and duration_strategy == "fit-images" and _schema_at(template_config, ["minDurationSec"]):
|
|
1415
|
+
|
|
1416
|
+
|
|
1417
|
+
|
|
1418
|
+
custom_payload["minDurationSec"] = effective_duration
|
|
1419
|
+
narration_locks.append("customPayload.minDurationSec")
|
|
1420
|
+
|
|
1421
|
+
|
|
1422
|
+
|
|
1423
|
+
|
|
1424
|
+
pacing_path = container_parts + ["pacing"]
|
|
1425
|
+
pacing_schema = _schema_at(template_config, pacing_path)
|
|
1426
|
+
explicit_pacing = any(parts == pacing_path for parts, _ in (payload_writes or []))
|
|
1427
|
+
if (container_parts and not video_seconds and "fit-scene" in (pacing_schema.get("enum") or [])
|
|
1428
|
+
and not explicit_pacing):
|
|
1429
|
+
container["pacing"] = "fit-scene"
|
|
1430
|
+
narration_locks.append("customPayload." + ".".join(container_parts + ["pacing"]))
|
|
1431
|
+
|
|
1432
|
+
|
|
1325
1433
|
text_layers = [
|
|
1326
1434
|
{"role": "headline", "content": headline, "animation": "slide-up"},
|
|
1327
1435
|
]
|
|
@@ -1378,6 +1486,8 @@ def _build_carousel_caption_dsl(
|
|
|
1378
1486
|
"scenes": [scene],
|
|
1379
1487
|
"transitions": {"default": "fade", "duration": 0.5},
|
|
1380
1488
|
}
|
|
1489
|
+
for path in narration_locks:
|
|
1490
|
+
_lock_scene_path(dsl, path)
|
|
1381
1491
|
|
|
1382
1492
|
return dsl
|
|
1383
1493
|
|
|
@@ -1785,7 +1895,7 @@ def _parse_payload_specs(template_config: dict, payload_specs: list[str] | None)
|
|
|
1785
1895
|
key, sep, raw = spec.partition("=")
|
|
1786
1896
|
parts = [p.strip() for p in key.split(".")]
|
|
1787
1897
|
if not sep or not all(parts):
|
|
1788
|
-
raise ValueError(f"--payload {spec!r}: expected KEY=VALUE, e.g.
|
|
1898
|
+
raise ValueError(f"--payload {spec!r}: expected KEY=VALUE, e.g. <field>=<value> or <object>.<field>=<value>")
|
|
1789
1899
|
raw = raw.strip()
|
|
1790
1900
|
if not raw:
|
|
1791
1901
|
raise ValueError(f"--payload {spec!r}: empty value; leave the flag out to keep the template default")
|
|
@@ -1901,130 +2011,64 @@ def _apply_avatar(
|
|
|
1901
2011
|
_lock_scene_path(dsl, "visuals.avatar.assetRef")
|
|
1902
2012
|
|
|
1903
2013
|
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
duration: int,
|
|
1907
|
-
style: str,
|
|
1908
|
-
ratio: str,
|
|
1909
|
-
resolution: str,
|
|
1910
|
-
voice_id: str,
|
|
1911
|
-
font_id: str | None,
|
|
1912
|
-
font_name: str | None,
|
|
1913
|
-
scene_count: int | None,
|
|
1914
|
-
allow_digital_human: bool,
|
|
1915
|
-
allow_ai_video: bool,
|
|
1916
|
-
speed: float | None = None,
|
|
1917
|
-
template_id: str | None = None,
|
|
1918
|
-
stub_image_url: str = "",
|
|
1919
|
-
stub_video_url: str = "",
|
|
1920
|
-
headline: str | None = None,
|
|
1921
|
-
subheadline: str | None = None,
|
|
1922
|
-
carousel_items: list[str] | None = None,
|
|
1923
|
-
caption_lines: list[str] | None = None,
|
|
1924
|
-
scene_images: list[str] | None = None,
|
|
1925
|
-
item_count: int | None = None,
|
|
1926
|
-
source_document_url: str | None = None,
|
|
1927
|
-
variant: str | None = None,
|
|
1928
|
-
sequence_preset: str | None = None,
|
|
1929
|
-
sequence_fit: str | None = None,
|
|
1930
|
-
payload_writes: PayloadWrites | None = None,
|
|
1931
|
-
countdown_ending: bool = False,
|
|
1932
|
-
) -> dict:
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
2014
|
+
@dataclass(frozen=True)
|
|
2015
|
+
class _SkeletonContext:
|
|
1947
2016
|
|
|
1948
2017
|
|
|
1949
2018
|
|
|
1950
2019
|
|
|
1951
2020
|
|
|
2021
|
+
template_config: dict | None
|
|
2022
|
+
template_id: str | None
|
|
2023
|
+
contract: dict
|
|
2024
|
+
topic: str
|
|
2025
|
+
style: str
|
|
2026
|
+
ratio: str
|
|
2027
|
+
output_language: str
|
|
2028
|
+
primary_visual_type: str
|
|
2029
|
+
has_visual: bool
|
|
2030
|
+
needs_narration: bool
|
|
2031
|
+
headline: str
|
|
2032
|
+
subheadline: str
|
|
2033
|
+
resolved_voice_id: str
|
|
2034
|
+
narration_speed: float
|
|
2035
|
+
tail_pad_sec: float | None
|
|
2036
|
+
image_style_guide: dict
|
|
2037
|
+
image_compose: bool
|
|
2038
|
+
image_ratio: str
|
|
2039
|
+
stub_image_url: str
|
|
2040
|
+
stub_video_url: str
|
|
2041
|
+
shared_background: bool
|
|
2042
|
+
countdown: dict | None
|
|
2043
|
+
beat_count: int | None
|
|
1952
2044
|
|
|
1953
2045
|
|
|
2046
|
+
def _require_carousel_content(
|
|
2047
|
+
template_id: str | None,
|
|
2048
|
+
contract: dict,
|
|
2049
|
+
carousel_items: list[str] | None,
|
|
2050
|
+
caption_lines: list[str] | None,
|
|
2051
|
+
) -> None:
|
|
1954
2052
|
|
|
1955
2053
|
|
|
1956
2054
|
|
|
1957
2055
|
|
|
1958
2056
|
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
template_config = _load_template_config(template_id) if template_id else None
|
|
1962
|
-
contract = _resolve_contract(template_config)
|
|
1963
|
-
countdown = contract["countdown"]
|
|
1964
|
-
if countdown is None and item_count is not None:
|
|
1965
|
-
raise ValueError(
|
|
1966
|
-
"--item-count requires a template declaring capabilities.sceneStrategy=countdown"
|
|
1967
|
-
)
|
|
1968
|
-
beat_count = resolve_countdown_count(countdown, item_count, scene_count) if countdown else None
|
|
1969
|
-
|
|
1970
|
-
countdown_ending = bool(countdown_ending and countdown)
|
|
1971
|
-
if countdown_ending and not _template_has_ending_slot(template_config):
|
|
1972
|
-
raise ValueError(f"--ending-payload: template {template_id!r} has no ending slot")
|
|
1973
|
-
|
|
1974
|
-
primary_visual_type = contract["primary_visual_type"]
|
|
1975
|
-
payload_style = contract["payload_style"]
|
|
1976
|
-
|
|
1977
|
-
has_visual = payload_style == "visual-overlay"
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
needs_narration = contract["needs_narration"]
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
output_language, template_default_voice_id = _resolve_template_language_meta(template_config)
|
|
1986
|
-
resolved_voice_id = _resolve_voice_id(voice_id, template_default_voice_id, output_language)
|
|
1987
|
-
narration_speed = _resolve_narration_speed(speed)
|
|
1988
|
-
if resolved_voice_id != voice_id:
|
|
1989
|
-
print(
|
|
1990
|
-
f"ℹ️ voice resolution: cli={voice_id!r} → resolved={resolved_voice_id!r} "
|
|
1991
|
-
f"(templateDefault={template_default_voice_id!r}, language={output_language})",
|
|
1992
|
-
file=sys.stderr,
|
|
1993
|
-
)
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
effective_headline = _normalize_linebreaks(headline) or topic
|
|
1997
|
-
effective_subheadline = _normalize_linebreaks(subheadline)
|
|
1998
|
-
_echo_template_llm_hint(template_config, template_id)
|
|
1999
|
-
_warn_on_url_text_layer(effective_headline, effective_subheadline)
|
|
2000
|
-
if beat_count is not None:
|
|
2001
|
-
_warn_on_count_mismatch(countdown, effective_headline, effective_subheadline, beat_count)
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
if payload_style == "carousel-caption":
|
|
2057
|
+
if not (carousel_items or []) and not (caption_lines or []):
|
|
2005
2058
|
print(
|
|
2006
|
-
|
|
2059
|
+
"❌ carousel-caption template "
|
|
2060
|
+
f"'{template_id}' needs visual or text content, but received neither "
|
|
2061
|
+
"--carousel-items nor --caption-lines.\n"
|
|
2062
|
+
" This template renders a carousel of user-supplied media; with no items "
|
|
2063
|
+
"it produces a black, minimum-length (2s) clip.\n"
|
|
2064
|
+
" Fix: pass the user's image/video URLs via --carousel-items "
|
|
2065
|
+
"(repeat the flag per item), e.g.\n"
|
|
2066
|
+
" gen_script.py --topic <topic> --template-id "
|
|
2067
|
+
f"{template_id} --carousel-items <url1> --carousel-items <url2> ...\n"
|
|
2068
|
+
" Optionally add --caption-lines '<text>' for on-screen typewriter captions.",
|
|
2007
2069
|
file=sys.stderr,
|
|
2008
2070
|
)
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
if not (carousel_items or []) and not (caption_lines or []):
|
|
2014
|
-
print(
|
|
2015
|
-
"❌ carousel-caption template "
|
|
2016
|
-
f"'{template_id}' needs visual or text content, but received neither "
|
|
2017
|
-
"--carousel-items nor --caption-lines.\n"
|
|
2018
|
-
" This template renders a carousel of user-supplied media; with no items "
|
|
2019
|
-
"it produces a black, minimum-length (2s) clip.\n"
|
|
2020
|
-
" Fix: pass the user's image/video URLs via --carousel-items "
|
|
2021
|
-
"(repeat the flag per item), e.g.\n"
|
|
2022
|
-
" gen_script.py --topic <topic> --template-id "
|
|
2023
|
-
f"{template_id} --carousel-items <url1> --carousel-items <url2> ...\n"
|
|
2024
|
-
" Optionally add --caption-lines '<text>' for on-screen typewriter captions.",
|
|
2025
|
-
file=sys.stderr,
|
|
2026
|
-
)
|
|
2027
|
-
sys.exit(1)
|
|
2071
|
+
sys.exit(1)
|
|
2028
2072
|
|
|
2029
2073
|
|
|
2030
2074
|
|
|
@@ -2032,65 +2076,43 @@ def build_dsl(
|
|
|
2032
2076
|
|
|
2033
2077
|
|
|
2034
2078
|
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
)
|
|
2052
|
-
sys.exit(1)
|
|
2053
|
-
carousel_dsl = _build_carousel_caption_dsl(
|
|
2054
|
-
template_id=template_id,
|
|
2055
|
-
topic=topic,
|
|
2056
|
-
headline=effective_headline,
|
|
2057
|
-
subheadline=effective_subheadline,
|
|
2058
|
-
carousel_items=carousel_items or [],
|
|
2059
|
-
caption_lines=caption_lines or [],
|
|
2060
|
-
duration=duration,
|
|
2061
|
-
style=style,
|
|
2062
|
-
ratio=ratio,
|
|
2063
|
-
resolution=resolution,
|
|
2064
|
-
output_language=output_language,
|
|
2065
|
-
resolved_voice_id=resolved_voice_id,
|
|
2066
|
-
narration_speed=narration_speed,
|
|
2067
|
-
font_id=font_id,
|
|
2068
|
-
font_name=font_name,
|
|
2069
|
-
narration_enabled=needs_narration,
|
|
2070
|
-
payload_defaults=contract["payload_defaults"],
|
|
2071
|
-
duration_strategy=contract["duration_strategy"],
|
|
2072
|
-
duration_model=contract["duration_model"],
|
|
2073
|
-
sequence_schema=((template_config or {}).get("customPayloadSchema") or {}).get("sequence"),
|
|
2074
|
-
sequence_preset=sequence_preset,
|
|
2075
|
-
sequence_fit=sequence_fit,
|
|
2076
|
-
payload_writes=payload_writes,
|
|
2079
|
+
if contract["duration_strategy"] == "fit-caption" and not (caption_lines or []):
|
|
2080
|
+
print(
|
|
2081
|
+
"❌ template "
|
|
2082
|
+
f"'{template_id}' is typewriter-driven (capabilities.durationStrategy="
|
|
2083
|
+
"fit-caption), but --caption-lines is empty.\n"
|
|
2084
|
+
" For this template the bottom typewriter copy IS the content: it carries "
|
|
2085
|
+
"the message and it decides the video length. With no lines the render "
|
|
2086
|
+
"collapses to the carousel floor (~5s for 2 images) and shows no text at all.\n"
|
|
2087
|
+
" Fix: pass one --caption-lines '<text>' per line. If the user did not "
|
|
2088
|
+
"supply the copy, WRITE IT YOURSELF from the material you researched "
|
|
2089
|
+
"(repo README, page screenshots, the user's topic) and pass it — do not leave "
|
|
2090
|
+
"it empty, and do not describe lines you never passed.\n"
|
|
2091
|
+
" gen_script.py --topic <topic> --template-id "
|
|
2092
|
+
f"{template_id} --carousel-items <url> "
|
|
2093
|
+
"--caption-lines '<line 1>' --caption-lines '<line 2>' ...",
|
|
2094
|
+
file=sys.stderr,
|
|
2077
2095
|
)
|
|
2078
|
-
|
|
2079
|
-
return carousel_dsl
|
|
2080
|
-
|
|
2081
|
-
if sequence_preset not in (None, "none") or sequence_fit not in (None, "auto"):
|
|
2082
|
-
raise ValueError(f"--sequence-preset / --sequence-fit only apply to image-sequence templates; {template_id!r} is not one")
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2096
|
+
sys.exit(1)
|
|
2087
2097
|
|
|
2088
2098
|
|
|
2089
|
-
|
|
2099
|
+
def _plan_skeleton_scenes(
|
|
2100
|
+
contract: dict,
|
|
2101
|
+
countdown: dict | None,
|
|
2102
|
+
beat_count: int | None,
|
|
2103
|
+
topic: str,
|
|
2104
|
+
duration: int,
|
|
2105
|
+
scene_count: int | None,
|
|
2106
|
+
output_language: str,
|
|
2107
|
+
headline: str,
|
|
2108
|
+
countdown_ending: bool,
|
|
2109
|
+
provided_slots: list[str],
|
|
2110
|
+
has_visual: bool,
|
|
2111
|
+
) -> list[dict]:
|
|
2090
2112
|
|
|
2091
2113
|
if beat_count is not None:
|
|
2092
2114
|
scene_plans = _plan_countdown_scenes(
|
|
2093
|
-
countdown, beat_count, duration,
|
|
2115
|
+
countdown, beat_count, duration, headline, ending=countdown_ending
|
|
2094
2116
|
)
|
|
2095
2117
|
else:
|
|
2096
2118
|
scene_plans = _plan_contract_scenes(contract, topic, duration, scene_count, output_language)
|
|
@@ -2107,7 +2129,7 @@ def build_dsl(
|
|
|
2107
2129
|
provided_slots
|
|
2108
2130
|
and scene_count is None
|
|
2109
2131
|
and contract["scene_strategy"] == "arc"
|
|
2110
|
-
and primary_visual_type == "image"
|
|
2132
|
+
and contract["primary_visual_type"] == "image"
|
|
2111
2133
|
and has_visual
|
|
2112
2134
|
and len(provided_slots) > len(scene_plans)
|
|
2113
2135
|
):
|
|
@@ -2119,236 +2141,212 @@ def build_dsl(
|
|
|
2119
2141
|
|
|
2120
2142
|
if provided_slots and len(provided_slots) > len(scene_plans):
|
|
2121
2143
|
print(
|
|
2122
|
-
f"
|
|
2144
|
+
f"⚠️ {len(provided_slots)} scene-image entries provided but the template "
|
|
2123
2145
|
f"plans only {len(scene_plans)} scene(s); the extra "
|
|
2124
2146
|
f"{len(provided_slots) - len(scene_plans)} will not be used. "
|
|
2125
2147
|
f"Raise --scenes or pick a template with more scenes.",
|
|
2126
2148
|
file=sys.stderr,
|
|
2127
2149
|
)
|
|
2128
|
-
|
|
2129
|
-
assets = []
|
|
2130
|
-
scenes = []
|
|
2150
|
+
return scene_plans
|
|
2131
2151
|
|
|
2132
2152
|
|
|
2153
|
+
def _visual_asset_id(ctx: _SkeletonContext, scene_id: str) -> str:
|
|
2133
2154
|
|
|
2155
|
+
if ctx.primary_visual_type == "video":
|
|
2156
|
+
return f"video-{scene_id}"
|
|
2157
|
+
if ctx.shared_background:
|
|
2158
|
+
return "img-background"
|
|
2159
|
+
return f"img-{scene_id}"
|
|
2134
2160
|
|
|
2135
2161
|
|
|
2162
|
+
def _plan_visual_asset(
|
|
2163
|
+
ctx: _SkeletonContext, plan: dict, asset_id: str, slot: str, narration_text: str,
|
|
2164
|
+
) -> dict | None:
|
|
2136
2165
|
|
|
2166
|
+
if ctx.primary_visual_type == "image":
|
|
2167
|
+
slot_is_ai, slot_prompt = _parse_ai_slot(slot)
|
|
2168
|
+
if slot and not slot_is_ai:
|
|
2137
2169
|
|
|
2138
|
-
image_style_guide = (
|
|
2139
|
-
resolve_style_guide(template_config, variant or "") if _HAS_ENHANCER else {}
|
|
2140
|
-
)
|
|
2141
|
-
image_compose = _HAS_ENHANCER and is_compose_mode(image_style_guide)
|
|
2142
|
-
image_ratio = image_style_guide.get("ratio") or ratio
|
|
2143
2170
|
|
|
2144
|
-
|
|
2171
|
+
return {
|
|
2172
|
+
"assetId": asset_id,
|
|
2173
|
+
"type": "image",
|
|
2174
|
+
"source": "existing",
|
|
2175
|
+
"status": "generated",
|
|
2176
|
+
"url": slot,
|
|
2177
|
+
}
|
|
2145
2178
|
|
|
2146
2179
|
|
|
2147
2180
|
|
|
2181
|
+
if ctx.stub_image_url:
|
|
2182
|
+
return {
|
|
2183
|
+
"assetId": asset_id,
|
|
2184
|
+
"type": "image",
|
|
2185
|
+
"source": "existing",
|
|
2186
|
+
"status": "generated",
|
|
2187
|
+
"url": ctx.stub_image_url,
|
|
2188
|
+
}
|
|
2189
|
+
if ctx.image_compose:
|
|
2190
|
+
image_result = {"prompt": COMPOSE_SUBJECT_PLACEHOLDER}
|
|
2191
|
+
elif ctx.shared_background:
|
|
2148
2192
|
|
|
2149
|
-
shared_background = (
|
|
2150
|
-
contract["background_strategy"] == "shared"
|
|
2151
|
-
and primary_visual_type == "image"
|
|
2152
|
-
and not provided_slots
|
|
2153
|
-
)
|
|
2154
|
-
shared_background_planned = False
|
|
2155
|
-
for idx, plan in enumerate(scene_plans):
|
|
2156
|
-
scene_id = f"scene-{idx + 1:02d}"
|
|
2157
2193
|
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
visual_asset_id = "img-background"
|
|
2194
|
+
image_result = generate_image_prompt(
|
|
2195
|
+
"point", ctx.topic, ctx.style, narration_text="", style_guide=ctx.image_style_guide,
|
|
2196
|
+
)
|
|
2162
2197
|
else:
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
estimate_narration_text(
|
|
2167
|
-
plan["label"], plan["purpose"], plan["duration"], topic, language=output_language,
|
|
2168
|
-
speed=narration_speed, tail_pad_sec=tail_pad_sec,
|
|
2198
|
+
image_result = generate_image_prompt(
|
|
2199
|
+
plan["purpose"], ctx.topic, ctx.style,
|
|
2200
|
+
narration_text=narration_text, style_guide=ctx.image_style_guide,
|
|
2169
2201
|
)
|
|
2170
|
-
if needs_narration else ""
|
|
2171
|
-
)
|
|
2172
|
-
|
|
2173
2202
|
|
|
2174
|
-
scene_has_visual = has_visual and _slot_reads_visual(template_config, plan["purpose"])
|
|
2175
2203
|
|
|
2176
2204
|
|
|
2177
|
-
if scene_has_visual and shared_background and shared_background_planned:
|
|
2178
|
-
pass
|
|
2179
|
-
elif scene_has_visual and primary_visual_type == "image":
|
|
2180
|
-
if shared_background:
|
|
2181
|
-
shared_background_planned = True
|
|
2182
|
-
slot = provided_slots[idx] if idx < len(provided_slots) else ""
|
|
2183
|
-
slot_is_ai, slot_prompt = _parse_ai_slot(slot)
|
|
2184
|
-
if slot and not slot_is_ai:
|
|
2185
2205
|
|
|
2186
2206
|
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
"type": "image",
|
|
2190
|
-
"source": "existing",
|
|
2191
|
-
"status": "generated",
|
|
2192
|
-
"url": slot,
|
|
2193
|
-
})
|
|
2207
|
+
if slot_prompt:
|
|
2208
|
+
image_result = {**image_result, "prompt": slot_prompt}
|
|
2194
2209
|
|
|
2210
|
+
img_payload = {
|
|
2211
|
+
"prompt": image_result["prompt"],
|
|
2212
|
+
"model": os.environ.get("DEFAULT_IMAGE_MODEL", "doubao/doubao-seedream-5-0-260128"),
|
|
2213
|
+
"ratio": ctx.image_ratio,
|
|
2214
|
+
}
|
|
2215
|
+
if "negativePrompt" in image_result:
|
|
2216
|
+
img_payload["negativePrompt"] = image_result["negativePrompt"]
|
|
2217
|
+
if "guidanceScale" in image_result:
|
|
2218
|
+
img_payload["guidanceScale"] = image_result["guidanceScale"]
|
|
2219
|
+
return {
|
|
2220
|
+
"assetId": asset_id,
|
|
2221
|
+
"type": "image",
|
|
2222
|
+
"source": "gen-image",
|
|
2223
|
+
"status": "planned",
|
|
2224
|
+
"payload": img_payload,
|
|
2225
|
+
}
|
|
2195
2226
|
|
|
2227
|
+
if ctx.primary_visual_type == "video":
|
|
2228
|
+
if ctx.stub_video_url:
|
|
2229
|
+
return {
|
|
2230
|
+
"assetId": asset_id,
|
|
2231
|
+
"type": "video",
|
|
2232
|
+
"source": "existing",
|
|
2233
|
+
"status": "generated",
|
|
2234
|
+
"url": ctx.stub_video_url,
|
|
2235
|
+
}
|
|
2196
2236
|
|
|
2197
|
-
elif stub_image_url:
|
|
2198
|
-
assets.append({
|
|
2199
|
-
"assetId": visual_asset_id,
|
|
2200
|
-
"type": "image",
|
|
2201
|
-
"source": "existing",
|
|
2202
|
-
"status": "generated",
|
|
2203
|
-
"url": stub_image_url,
|
|
2204
|
-
})
|
|
2205
|
-
else:
|
|
2206
|
-
if image_compose:
|
|
2207
|
-
image_result = {"prompt": COMPOSE_SUBJECT_PLACEHOLDER}
|
|
2208
|
-
elif shared_background:
|
|
2209
2237
|
|
|
2238
|
+
image_result = generate_image_prompt(plan["purpose"], ctx.topic, ctx.style, narration_text=narration_text)
|
|
2239
|
+
return {
|
|
2240
|
+
"assetId": asset_id,
|
|
2241
|
+
"type": "video",
|
|
2242
|
+
"source": "gen-video",
|
|
2243
|
+
"status": "planned",
|
|
2244
|
+
"payload": {
|
|
2245
|
+
"prompt": image_result["prompt"],
|
|
2210
2246
|
|
|
2211
|
-
image_result = generate_image_prompt(
|
|
2212
|
-
"point", topic, style, narration_text="", style_guide=image_style_guide,
|
|
2213
|
-
)
|
|
2214
|
-
else:
|
|
2215
|
-
image_result = generate_image_prompt(
|
|
2216
|
-
plan["purpose"], topic, style,
|
|
2217
|
-
narration_text=narration_text, style_guide=image_style_guide,
|
|
2218
|
-
)
|
|
2219
2247
|
|
|
2248
|
+
"model": os.environ.get("DEFAULT_VIDEO_MODEL", "seedance"),
|
|
2249
|
+
"ratio": ctx.ratio,
|
|
2220
2250
|
|
|
2251
|
+
"duration": max(4, min(int(plan["duration"]), 15)),
|
|
2252
|
+
},
|
|
2253
|
+
}
|
|
2254
|
+
return None
|
|
2221
2255
|
|
|
2222
2256
|
|
|
2257
|
+
def _visual_text_layers(ctx: _SkeletonContext, plan: dict) -> list[dict]:
|
|
2223
2258
|
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
assets.append({
|
|
2238
|
-
"assetId": visual_asset_id,
|
|
2239
|
-
"type": "image",
|
|
2240
|
-
"source": "gen-image",
|
|
2241
|
-
"status": "planned",
|
|
2242
|
-
"payload": img_payload,
|
|
2243
|
-
})
|
|
2244
|
-
elif scene_has_visual and primary_visual_type == "video":
|
|
2245
|
-
if stub_video_url:
|
|
2246
|
-
assets.append({
|
|
2247
|
-
"assetId": visual_asset_id,
|
|
2248
|
-
"type": "video",
|
|
2249
|
-
"source": "existing",
|
|
2250
|
-
"status": "generated",
|
|
2251
|
-
"url": stub_video_url,
|
|
2252
|
-
})
|
|
2253
|
-
else:
|
|
2259
|
+
if ctx.contract["text_layer_rules"]:
|
|
2260
|
+
return _placeholder_text_layers(ctx.contract["text_layer_rules"])
|
|
2261
|
+
text_layers = []
|
|
2262
|
+
if plan["purpose"] == "opening":
|
|
2263
|
+
text_layers = [
|
|
2264
|
+
{"role": "headline", "content": ctx.headline, "animation": "slide-up"},
|
|
2265
|
+
]
|
|
2266
|
+
if ctx.subheadline:
|
|
2267
|
+
text_layers.append({"role": "subheadline", "content": ctx.subheadline, "animation": "fade-in"})
|
|
2268
|
+
elif plan["purpose"] == "point":
|
|
2269
|
+
text_layers = [
|
|
2270
|
+
{"role": "badge", "content": plan["label"], "animation": "slide-up"},
|
|
2271
|
+
]
|
|
2254
2272
|
|
|
2255
2273
|
|
|
2256
|
-
image_result = generate_image_prompt(plan["purpose"], topic, style, narration_text=narration_text)
|
|
2257
|
-
video_prompt = image_result["prompt"]
|
|
2258
2274
|
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2275
|
+
text_layers.insert(0, {"role": "headline", "content": ctx.headline, "animation": "fade-in"})
|
|
2276
|
+
if ctx.subheadline:
|
|
2277
|
+
text_layers.append({"role": "subheadline", "content": ctx.subheadline, "animation": "fade-in"})
|
|
2278
|
+
elif plan["purpose"] == "cta":
|
|
2279
|
+
text_layers = [
|
|
2280
|
+
{"role": "headline", "content": _localize_label(ctx.output_language, "follow_us"), "animation": "fade-in"},
|
|
2281
|
+
]
|
|
2282
|
+
if ctx.subheadline:
|
|
2283
|
+
text_layers.append({"role": "subheadline", "content": ctx.subheadline, "animation": "fade-in"})
|
|
2284
|
+
return text_layers
|
|
2262
2285
|
|
|
2263
2286
|
|
|
2264
|
-
|
|
2265
|
-
"ratio": ratio,
|
|
2266
|
-
"duration": video_duration,
|
|
2267
|
-
}
|
|
2268
|
-
assets.append({
|
|
2269
|
-
"assetId": visual_asset_id,
|
|
2270
|
-
"type": "video",
|
|
2271
|
-
"source": "gen-video",
|
|
2272
|
-
"status": "planned",
|
|
2273
|
-
"payload": vid_payload,
|
|
2274
|
-
})
|
|
2287
|
+
def _payload_text_layers(ctx: _SkeletonContext, plan: dict) -> list[dict]:
|
|
2275
2288
|
|
|
2276
2289
|
|
|
2277
2290
|
|
|
2278
2291
|
|
|
2279
2292
|
|
|
2280
|
-
if needs_narration:
|
|
2281
|
-
assets.append({
|
|
2282
|
-
"assetId": narration_asset_id,
|
|
2283
|
-
"type": "audio",
|
|
2284
|
-
"source": "gen-voice",
|
|
2285
|
-
"status": "planned",
|
|
2286
|
-
"payload": {
|
|
2287
|
-
"voiceId": resolved_voice_id,
|
|
2288
|
-
},
|
|
2289
|
-
})
|
|
2290
2293
|
|
|
2291
|
-
if scene_has_visual:
|
|
2292
|
-
layout = "text-overlay" if plan["purpose"] in ("opening", "cta") else "full-visual"
|
|
2293
|
-
text_layers = []
|
|
2294
|
-
if plan["purpose"] == "opening":
|
|
2295
|
-
text_layers = [
|
|
2296
|
-
{"role": "headline", "content": effective_headline, "animation": "slide-up"},
|
|
2297
|
-
]
|
|
2298
|
-
if effective_subheadline:
|
|
2299
|
-
text_layers.append({"role": "subheadline", "content": effective_subheadline, "animation": "fade-in"})
|
|
2300
|
-
elif plan["purpose"] == "point":
|
|
2301
|
-
text_layers = [
|
|
2302
|
-
{"role": "badge", "content": plan["label"], "animation": "slide-up"},
|
|
2303
|
-
]
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
text_layers.insert(0, {"role": "headline", "content": effective_headline, "animation": "fade-in"})
|
|
2308
|
-
if effective_subheadline:
|
|
2309
|
-
text_layers.append({"role": "subheadline", "content": effective_subheadline, "animation": "fade-in"})
|
|
2310
|
-
elif plan["purpose"] == "cta":
|
|
2311
|
-
text_layers = [
|
|
2312
|
-
{"role": "headline", "content": _localize_label(output_language, "follow_us"), "animation": "fade-in"},
|
|
2313
|
-
]
|
|
2314
|
-
if effective_subheadline:
|
|
2315
|
-
text_layers.append({"role": "subheadline", "content": effective_subheadline, "animation": "fade-in"})
|
|
2316
|
-
if contract["text_layer_rules"]:
|
|
2317
|
-
text_layers = _placeholder_text_layers(contract["text_layer_rules"])
|
|
2318
|
-
|
|
2319
|
-
scene = {
|
|
2320
|
-
"id": scene_id,
|
|
2321
|
-
"purpose": plan["purpose"],
|
|
2322
|
-
"duration": plan["duration"],
|
|
2323
|
-
"layout": layout,
|
|
2324
|
-
"visuals": {"background": {"assetRef": visual_asset_id}},
|
|
2325
|
-
"textLayers": text_layers,
|
|
2326
|
-
"animationHints": {
|
|
2327
|
-
"entrance": "fade",
|
|
2328
|
-
"motion": "kenburns-in" if idx % 2 == 0 else "kenburns-out",
|
|
2329
|
-
},
|
|
2330
|
-
}
|
|
2331
|
-
if needs_narration:
|
|
2332
|
-
scene["audio"] = {
|
|
2333
|
-
"narration": {
|
|
2334
|
-
"text": narration_text,
|
|
2335
|
-
"assetRef": narration_asset_id,
|
|
2336
|
-
|
|
2337
|
-
"needsFill": True,
|
|
2338
|
-
},
|
|
2339
|
-
}
|
|
2340
|
-
scenes.append(scene)
|
|
2341
|
-
else:
|
|
2342
2294
|
|
|
2343
2295
|
|
|
2296
|
+
if ctx.contract["text_layer_rules"]:
|
|
2297
|
+
return _placeholder_text_layers(ctx.contract["text_layer_rules"])
|
|
2298
|
+
text_layers = []
|
|
2299
|
+
if plan["purpose"] == "opening":
|
|
2300
|
+
text_layers = [
|
|
2301
|
+
{"role": "headline", "content": ctx.headline, "animation": "fade-in"},
|
|
2302
|
+
]
|
|
2303
|
+
if ctx.subheadline:
|
|
2304
|
+
text_layers.append({"role": "subheadline", "content": ctx.subheadline, "animation": "fade-in"})
|
|
2305
|
+
elif plan["purpose"] == "point":
|
|
2306
|
+
text_layers = [
|
|
2307
|
+
{"role": "headline", "content": plan["label"], "animation": "slide-up"},
|
|
2308
|
+
]
|
|
2309
|
+
elif plan["purpose"] == "cta":
|
|
2310
|
+
text_layers = [
|
|
2311
|
+
{"role": "headline", "content": _localize_label(ctx.output_language, "follow_us"), "animation": "zoom-in"},
|
|
2312
|
+
]
|
|
2313
|
+
if ctx.subheadline:
|
|
2314
|
+
text_layers.append({"role": "subheadline", "content": ctx.subheadline, "animation": "fade-in"})
|
|
2315
|
+
return text_layers
|
|
2344
2316
|
|
|
2345
2317
|
|
|
2318
|
+
def _attach_narration(scene: dict, narration_text: str, narration_asset_id: str | None) -> dict:
|
|
2346
2319
|
|
|
2320
|
+
if narration_asset_id:
|
|
2321
|
+
scene["audio"] = {
|
|
2322
|
+
"narration": {
|
|
2323
|
+
"text": narration_text,
|
|
2324
|
+
"assetRef": narration_asset_id,
|
|
2347
2325
|
|
|
2326
|
+
"needsFill": True,
|
|
2327
|
+
},
|
|
2328
|
+
}
|
|
2329
|
+
return scene
|
|
2348
2330
|
|
|
2349
2331
|
|
|
2332
|
+
def _visual_scene(
|
|
2333
|
+
ctx: _SkeletonContext, idx: int, scene_id: str, plan: dict, visual_asset_id: str,
|
|
2334
|
+
) -> dict:
|
|
2335
|
+
return {
|
|
2336
|
+
"id": scene_id,
|
|
2337
|
+
"purpose": plan["purpose"],
|
|
2338
|
+
"duration": plan["duration"],
|
|
2339
|
+
"layout": "text-overlay" if plan["purpose"] in ("opening", "cta") else "full-visual",
|
|
2340
|
+
"visuals": {"background": {"assetRef": visual_asset_id}},
|
|
2341
|
+
"textLayers": _visual_text_layers(ctx, plan),
|
|
2342
|
+
"animationHints": {
|
|
2343
|
+
"entrance": "fade",
|
|
2344
|
+
"motion": "kenburns-in" if idx % 2 == 0 else "kenburns-out",
|
|
2345
|
+
},
|
|
2346
|
+
}
|
|
2350
2347
|
|
|
2351
2348
|
|
|
2349
|
+
def _payload_scene(ctx: _SkeletonContext, scene_id: str, plan: dict) -> dict:
|
|
2352
2350
|
|
|
2353
2351
|
|
|
2354
2352
|
|
|
@@ -2356,97 +2354,336 @@ def build_dsl(
|
|
|
2356
2354
|
|
|
2357
2355
|
|
|
2358
2356
|
|
|
2359
|
-
_layout_cfg = (template_config or {}).get("capabilities") or {}
|
|
2360
|
-
layout = _layout_cfg.get("defaultLayout") or template_id or "text-overlay"
|
|
2361
2357
|
|
|
2362
2358
|
|
|
2363
2359
|
|
|
2364
2360
|
|
|
2365
2361
|
|
|
2366
2362
|
|
|
2367
|
-
text_layers = []
|
|
2368
|
-
if plan["purpose"] == "opening":
|
|
2369
|
-
text_layers = [
|
|
2370
|
-
{"role": "headline", "content": effective_headline, "animation": "fade-in"},
|
|
2371
|
-
]
|
|
2372
|
-
if effective_subheadline:
|
|
2373
|
-
text_layers.append({"role": "subheadline", "content": effective_subheadline, "animation": "fade-in"})
|
|
2374
|
-
elif plan["purpose"] == "point":
|
|
2375
|
-
text_layers = [
|
|
2376
|
-
{"role": "headline", "content": plan["label"], "animation": "slide-up"},
|
|
2377
|
-
]
|
|
2378
|
-
elif plan["purpose"] == "cta":
|
|
2379
|
-
text_layers = [
|
|
2380
|
-
{"role": "headline", "content": _localize_label(output_language, "follow_us"), "animation": "zoom-in"},
|
|
2381
|
-
]
|
|
2382
|
-
if effective_subheadline:
|
|
2383
|
-
text_layers.append({"role": "subheadline", "content": effective_subheadline, "animation": "fade-in"})
|
|
2384
2363
|
|
|
2385
|
-
if contract["text_layer_rules"]:
|
|
2386
|
-
text_layers = _placeholder_text_layers(contract["text_layer_rules"])
|
|
2387
2364
|
|
|
2388
|
-
custom_payload = _build_custom_payload(template_config)
|
|
2389
|
-
if beat_count is not None:
|
|
2390
2365
|
|
|
2391
2366
|
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2367
|
+
layout_cfg = (ctx.template_config or {}).get("capabilities") or {}
|
|
2368
|
+
scene = {
|
|
2369
|
+
"id": scene_id,
|
|
2370
|
+
"purpose": plan["purpose"],
|
|
2371
|
+
"duration": plan["duration"],
|
|
2372
|
+
"layout": layout_cfg.get("defaultLayout") or ctx.template_id or "text-overlay",
|
|
2373
|
+
"textLayers": _payload_text_layers(ctx, plan),
|
|
2374
|
+
}
|
|
2375
|
+
custom_payload = _build_custom_payload(ctx.template_config)
|
|
2376
|
+
if ctx.beat_count is not None:
|
|
2395
2377
|
|
|
2396
|
-
scene = {
|
|
2397
|
-
"id": scene_id,
|
|
2398
|
-
"purpose": plan["purpose"],
|
|
2399
|
-
"duration": plan["duration"],
|
|
2400
|
-
"layout": layout,
|
|
2401
|
-
"textLayers": text_layers,
|
|
2402
|
-
}
|
|
2403
2378
|
|
|
2379
|
+
custom_payload.update(
|
|
2380
|
+
countdown_scene_payload(ctx.countdown, ctx.beat_count, plan.get("countdownIndex"))
|
|
2381
|
+
)
|
|
2404
2382
|
|
|
2405
|
-
if custom_payload:
|
|
2406
|
-
scene["customPayload"] = custom_payload
|
|
2407
|
-
if needs_narration:
|
|
2408
|
-
scene["audio"] = {
|
|
2409
|
-
"narration": {
|
|
2410
|
-
"text": narration_text,
|
|
2411
|
-
"assetRef": narration_asset_id,
|
|
2412
2383
|
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
scenes.append(scene)
|
|
2384
|
+
if custom_payload:
|
|
2385
|
+
scene["customPayload"] = custom_payload
|
|
2386
|
+
return scene
|
|
2417
2387
|
|
|
2418
|
-
|
|
2388
|
+
|
|
2389
|
+
def _skeleton_document(
|
|
2390
|
+
ctx: _SkeletonContext,
|
|
2391
|
+
assets: list[dict],
|
|
2392
|
+
scenes: list[dict],
|
|
2393
|
+
*,
|
|
2394
|
+
headline_given: bool,
|
|
2395
|
+
duration: int,
|
|
2396
|
+
resolution: str,
|
|
2397
|
+
font_id: str | None,
|
|
2398
|
+
font_name: str | None,
|
|
2399
|
+
) -> dict:
|
|
2400
|
+
|
|
2401
|
+
return {
|
|
2419
2402
|
"version": "v1alpha1",
|
|
2420
2403
|
"meta": {
|
|
2421
|
-
"title": _video_title(
|
|
2422
|
-
"topic": topic,
|
|
2423
|
-
"headline":
|
|
2424
|
-
"subheadline":
|
|
2404
|
+
"title": _video_title(ctx.headline if headline_given else None, ctx.topic),
|
|
2405
|
+
"topic": ctx.topic,
|
|
2406
|
+
"headline": ctx.headline,
|
|
2407
|
+
"subheadline": ctx.subheadline,
|
|
2425
2408
|
"targetDuration": duration,
|
|
2426
|
-
"language": LANGUAGE_TO_META[output_language],
|
|
2427
|
-
"outputLanguage": output_language,
|
|
2428
|
-
"style": style or "",
|
|
2429
|
-
"description": _localize_label(output_language, "video_description", topic=topic, duration=duration),
|
|
2409
|
+
"language": LANGUAGE_TO_META[ctx.output_language],
|
|
2410
|
+
"outputLanguage": ctx.output_language,
|
|
2411
|
+
"style": ctx.style or "",
|
|
2412
|
+
"description": _localize_label(ctx.output_language, "video_description", topic=ctx.topic, duration=duration),
|
|
2430
2413
|
|
|
2431
|
-
**({"characterSheet": ""} if image_compose and has_visual else {}),
|
|
2414
|
+
**({"characterSheet": ""} if ctx.image_compose and ctx.has_visual else {}),
|
|
2432
2415
|
},
|
|
2433
2416
|
"global": {
|
|
2434
|
-
"aspectRatio": ratio,
|
|
2417
|
+
"aspectRatio": ctx.ratio,
|
|
2435
2418
|
"resolution": resolution,
|
|
2436
2419
|
"fps": 30,
|
|
2437
2420
|
|
|
2438
2421
|
|
|
2439
|
-
"subtitle": {"enabled": needs_narration, "style": "bottom"},
|
|
2440
|
-
**({"narration": {"voiceId": resolved_voice_id, "speed": narration_speed}} if needs_narration else {}),
|
|
2422
|
+
"subtitle": {"enabled": ctx.needs_narration, "style": "bottom"},
|
|
2423
|
+
**({"narration": {"voiceId": ctx.resolved_voice_id, "speed": ctx.narration_speed}} if ctx.needs_narration else {}),
|
|
2441
2424
|
**({"font": {"fontId": font_id, **({"fontName": font_name} if font_name else {})}} if font_id else {}),
|
|
2442
2425
|
"bgm": {"enabled": True, "volume": 0.12},
|
|
2443
2426
|
},
|
|
2444
2427
|
"assets": assets,
|
|
2445
2428
|
"scenes": scenes,
|
|
2446
2429
|
|
|
2447
|
-
"transitions": {"default": contract["default_transition"] or "fade", "duration": 0.5},
|
|
2430
|
+
"transitions": {"default": ctx.contract["default_transition"] or "fade", "duration": 0.5},
|
|
2448
2431
|
}
|
|
2449
2432
|
|
|
2433
|
+
|
|
2434
|
+
def build_dsl(
|
|
2435
|
+
topic: str,
|
|
2436
|
+
duration: int,
|
|
2437
|
+
style: str,
|
|
2438
|
+
ratio: str,
|
|
2439
|
+
resolution: str,
|
|
2440
|
+
voice_id: str,
|
|
2441
|
+
font_id: str | None,
|
|
2442
|
+
font_name: str | None,
|
|
2443
|
+
scene_count: int | None,
|
|
2444
|
+
allow_digital_human: bool,
|
|
2445
|
+
allow_ai_video: bool,
|
|
2446
|
+
speed: float | None = None,
|
|
2447
|
+
template_id: str | None = None,
|
|
2448
|
+
stub_image_url: str = "",
|
|
2449
|
+
stub_video_url: str = "",
|
|
2450
|
+
headline: str | None = None,
|
|
2451
|
+
subheadline: str | None = None,
|
|
2452
|
+
carousel_items: list[str] | None = None,
|
|
2453
|
+
caption_lines: list[str] | None = None,
|
|
2454
|
+
scene_images: list[str] | None = None,
|
|
2455
|
+
item_count: int | None = None,
|
|
2456
|
+
source_document_url: str | None = None,
|
|
2457
|
+
variant: str | None = None,
|
|
2458
|
+
sequence_preset: str | None = None,
|
|
2459
|
+
sequence_fit: str | None = None,
|
|
2460
|
+
payload_writes: PayloadWrites | None = None,
|
|
2461
|
+
countdown_ending: bool = False,
|
|
2462
|
+
narration: str = "auto",
|
|
2463
|
+
) -> dict:
|
|
2464
|
+
|
|
2465
|
+
|
|
2466
|
+
|
|
2467
|
+
|
|
2468
|
+
|
|
2469
|
+
|
|
2470
|
+
|
|
2471
|
+
|
|
2472
|
+
|
|
2473
|
+
|
|
2474
|
+
|
|
2475
|
+
|
|
2476
|
+
|
|
2477
|
+
|
|
2478
|
+
|
|
2479
|
+
|
|
2480
|
+
|
|
2481
|
+
|
|
2482
|
+
|
|
2483
|
+
|
|
2484
|
+
|
|
2485
|
+
|
|
2486
|
+
|
|
2487
|
+
|
|
2488
|
+
|
|
2489
|
+
|
|
2490
|
+
|
|
2491
|
+
|
|
2492
|
+
template_config = _load_template_config(template_id) if template_id else None
|
|
2493
|
+
contract = _resolve_contract(template_config)
|
|
2494
|
+
|
|
2495
|
+
|
|
2496
|
+
if narration not in NARRATION_MODES:
|
|
2497
|
+
raise ValueError(f"--narration must be one of {', '.join(NARRATION_MODES)}, got {narration!r}")
|
|
2498
|
+
if narration != "auto":
|
|
2499
|
+
contract["needs_narration"] = narration == "on"
|
|
2500
|
+
countdown = contract["countdown"]
|
|
2501
|
+
if countdown is None and item_count is not None:
|
|
2502
|
+
raise ValueError(
|
|
2503
|
+
"--item-count requires a template declaring capabilities.sceneStrategy=countdown"
|
|
2504
|
+
)
|
|
2505
|
+
beat_count = resolve_countdown_count(countdown, item_count, scene_count) if countdown else None
|
|
2506
|
+
|
|
2507
|
+
countdown_ending = bool(countdown_ending and countdown)
|
|
2508
|
+
if countdown_ending and not _template_has_ending_slot(template_config):
|
|
2509
|
+
raise ValueError(f"--ending-payload: template {template_id!r} has no ending slot")
|
|
2510
|
+
|
|
2511
|
+
primary_visual_type = contract["primary_visual_type"]
|
|
2512
|
+
payload_style = contract["payload_style"]
|
|
2513
|
+
|
|
2514
|
+
has_visual = payload_style == "visual-overlay"
|
|
2515
|
+
|
|
2516
|
+
|
|
2517
|
+
needs_narration = contract["needs_narration"]
|
|
2518
|
+
|
|
2519
|
+
|
|
2520
|
+
|
|
2521
|
+
|
|
2522
|
+
output_language, template_default_voice_id = _resolve_template_language_meta(template_config)
|
|
2523
|
+
resolved_voice_id = _resolve_voice_id(voice_id, template_default_voice_id, output_language)
|
|
2524
|
+
narration_speed = _resolve_narration_speed(speed)
|
|
2525
|
+
if resolved_voice_id != voice_id:
|
|
2526
|
+
print(
|
|
2527
|
+
f"ℹ️ voice resolution: cli={voice_id!r} → resolved={resolved_voice_id!r} "
|
|
2528
|
+
f"(templateDefault={template_default_voice_id!r}, language={output_language})",
|
|
2529
|
+
file=sys.stderr,
|
|
2530
|
+
)
|
|
2531
|
+
|
|
2532
|
+
|
|
2533
|
+
effective_headline = _normalize_linebreaks(headline) or topic
|
|
2534
|
+
effective_subheadline = _normalize_linebreaks(subheadline)
|
|
2535
|
+
_echo_template_llm_hint(template_config, template_id)
|
|
2536
|
+
_warn_on_url_text_layer(effective_headline, effective_subheadline)
|
|
2537
|
+
if beat_count is not None:
|
|
2538
|
+
_warn_on_count_mismatch(countdown, effective_headline, effective_subheadline, beat_count)
|
|
2539
|
+
|
|
2540
|
+
|
|
2541
|
+
if payload_style == "carousel-caption":
|
|
2542
|
+
print(
|
|
2543
|
+
f"ℹ️ contract: payloadStyle=carousel-caption for template {template_id}",
|
|
2544
|
+
file=sys.stderr,
|
|
2545
|
+
)
|
|
2546
|
+
_require_carousel_content(template_id, contract, carousel_items, caption_lines)
|
|
2547
|
+
carousel_dsl = _build_carousel_caption_dsl(
|
|
2548
|
+
template_id=template_id,
|
|
2549
|
+
topic=topic,
|
|
2550
|
+
headline=effective_headline,
|
|
2551
|
+
subheadline=effective_subheadline,
|
|
2552
|
+
carousel_items=carousel_items or [],
|
|
2553
|
+
caption_lines=caption_lines or [],
|
|
2554
|
+
duration=duration,
|
|
2555
|
+
style=style,
|
|
2556
|
+
ratio=ratio,
|
|
2557
|
+
resolution=resolution,
|
|
2558
|
+
output_language=output_language,
|
|
2559
|
+
resolved_voice_id=resolved_voice_id,
|
|
2560
|
+
narration_speed=narration_speed,
|
|
2561
|
+
font_id=font_id,
|
|
2562
|
+
font_name=font_name,
|
|
2563
|
+
narration_enabled=needs_narration,
|
|
2564
|
+
payload_defaults=contract["payload_defaults"],
|
|
2565
|
+
duration_strategy=contract["duration_strategy"],
|
|
2566
|
+
duration_model=contract["duration_model"],
|
|
2567
|
+
media_binding=_resolve_media_binding(template_config),
|
|
2568
|
+
template_config=template_config,
|
|
2569
|
+
sequence_preset=sequence_preset,
|
|
2570
|
+
sequence_fit=sequence_fit,
|
|
2571
|
+
payload_writes=payload_writes,
|
|
2572
|
+
)
|
|
2573
|
+
_enforce_supported_duration(carousel_dsl, template_config, template_id)
|
|
2574
|
+
return carousel_dsl
|
|
2575
|
+
|
|
2576
|
+
if sequence_preset not in (None, "none") or sequence_fit not in (None, "auto"):
|
|
2577
|
+
raise ValueError(f"--sequence-preset / --sequence-fit only apply to templates declaring mediaBinding.options; {template_id!r} does not")
|
|
2578
|
+
|
|
2579
|
+
|
|
2580
|
+
|
|
2581
|
+
|
|
2582
|
+
|
|
2583
|
+
|
|
2584
|
+
provided_slots = [s.strip() for s in (scene_images or []) if s and s.strip()]
|
|
2585
|
+
scene_plans = _plan_skeleton_scenes(
|
|
2586
|
+
contract, countdown, beat_count, topic, duration, scene_count, output_language,
|
|
2587
|
+
effective_headline, countdown_ending, provided_slots, has_visual,
|
|
2588
|
+
)
|
|
2589
|
+
|
|
2590
|
+
|
|
2591
|
+
|
|
2592
|
+
|
|
2593
|
+
|
|
2594
|
+
|
|
2595
|
+
|
|
2596
|
+
image_style_guide = (
|
|
2597
|
+
resolve_style_guide(template_config, variant or "") if _HAS_ENHANCER else {}
|
|
2598
|
+
)
|
|
2599
|
+
ctx = _SkeletonContext(
|
|
2600
|
+
template_config=template_config,
|
|
2601
|
+
template_id=template_id,
|
|
2602
|
+
contract=contract,
|
|
2603
|
+
topic=topic,
|
|
2604
|
+
style=style,
|
|
2605
|
+
ratio=ratio,
|
|
2606
|
+
output_language=output_language,
|
|
2607
|
+
primary_visual_type=primary_visual_type,
|
|
2608
|
+
has_visual=has_visual,
|
|
2609
|
+
needs_narration=needs_narration,
|
|
2610
|
+
headline=effective_headline,
|
|
2611
|
+
subheadline=effective_subheadline,
|
|
2612
|
+
resolved_voice_id=resolved_voice_id,
|
|
2613
|
+
narration_speed=narration_speed,
|
|
2614
|
+
tail_pad_sec=_nb.template_tail_pad_sec(template_config) if _HAS_BUDGET else None,
|
|
2615
|
+
image_style_guide=image_style_guide,
|
|
2616
|
+
image_compose=_HAS_ENHANCER and is_compose_mode(image_style_guide),
|
|
2617
|
+
image_ratio=image_style_guide.get("ratio") or ratio,
|
|
2618
|
+
stub_image_url=stub_image_url,
|
|
2619
|
+
stub_video_url=stub_video_url,
|
|
2620
|
+
|
|
2621
|
+
|
|
2622
|
+
|
|
2623
|
+
|
|
2624
|
+
shared_background=(
|
|
2625
|
+
contract["background_strategy"] == "shared"
|
|
2626
|
+
and primary_visual_type == "image"
|
|
2627
|
+
and not provided_slots
|
|
2628
|
+
),
|
|
2629
|
+
countdown=countdown,
|
|
2630
|
+
beat_count=beat_count,
|
|
2631
|
+
)
|
|
2632
|
+
|
|
2633
|
+
assets = []
|
|
2634
|
+
scenes = []
|
|
2635
|
+
shared_background_planned = False
|
|
2636
|
+
for idx, plan in enumerate(scene_plans):
|
|
2637
|
+
scene_id = f"scene-{idx + 1:02d}"
|
|
2638
|
+
visual_asset_id = _visual_asset_id(ctx, scene_id)
|
|
2639
|
+
narration_asset_id = f"narration-{scene_id}" if needs_narration else None
|
|
2640
|
+
narration_text = (
|
|
2641
|
+
estimate_narration_text(
|
|
2642
|
+
plan["label"], plan["purpose"], plan["duration"], topic, language=output_language,
|
|
2643
|
+
speed=narration_speed, tail_pad_sec=ctx.tail_pad_sec,
|
|
2644
|
+
)
|
|
2645
|
+
if needs_narration else ""
|
|
2646
|
+
)
|
|
2647
|
+
|
|
2648
|
+
|
|
2649
|
+
scene_has_visual = has_visual and _slot_reads_visual(template_config, plan["purpose"])
|
|
2650
|
+
|
|
2651
|
+
|
|
2652
|
+
|
|
2653
|
+
if scene_has_visual and not (ctx.shared_background and shared_background_planned):
|
|
2654
|
+
shared_background_planned = ctx.shared_background
|
|
2655
|
+
slot = provided_slots[idx] if idx < len(provided_slots) else ""
|
|
2656
|
+
visual_asset = _plan_visual_asset(ctx, plan, visual_asset_id, slot, narration_text)
|
|
2657
|
+
if visual_asset:
|
|
2658
|
+
assets.append(visual_asset)
|
|
2659
|
+
|
|
2660
|
+
|
|
2661
|
+
|
|
2662
|
+
|
|
2663
|
+
|
|
2664
|
+
if needs_narration:
|
|
2665
|
+
assets.append({
|
|
2666
|
+
"assetId": narration_asset_id,
|
|
2667
|
+
"type": "audio",
|
|
2668
|
+
"source": "gen-voice",
|
|
2669
|
+
"status": "planned",
|
|
2670
|
+
"payload": {
|
|
2671
|
+
"voiceId": resolved_voice_id,
|
|
2672
|
+
},
|
|
2673
|
+
})
|
|
2674
|
+
|
|
2675
|
+
scene = (
|
|
2676
|
+
_visual_scene(ctx, idx, scene_id, plan, visual_asset_id)
|
|
2677
|
+
if scene_has_visual else _payload_scene(ctx, scene_id, plan)
|
|
2678
|
+
)
|
|
2679
|
+
scenes.append(_attach_narration(scene, narration_text, narration_asset_id))
|
|
2680
|
+
|
|
2681
|
+
dsl = _skeleton_document(
|
|
2682
|
+
ctx, assets, scenes,
|
|
2683
|
+
headline_given=bool(headline), duration=duration, resolution=resolution,
|
|
2684
|
+
font_id=font_id, font_name=font_name,
|
|
2685
|
+
)
|
|
2686
|
+
|
|
2450
2687
|
if template_id:
|
|
2451
2688
|
dsl["meta"]["templateId"] = template_id
|
|
2452
2689
|
|
|
@@ -2473,7 +2710,7 @@ def build_dsl(
|
|
|
2473
2710
|
return dsl
|
|
2474
2711
|
|
|
2475
2712
|
|
|
2476
|
-
def
|
|
2713
|
+
def _build_arg_parser() -> argparse.ArgumentParser:
|
|
2477
2714
|
parser = argparse.ArgumentParser(
|
|
2478
2715
|
description="Video script generator — produces Video DSL v1alpha1 JSON",
|
|
2479
2716
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
@@ -2541,20 +2778,27 @@ Examples:
|
|
|
2541
2778
|
"--background-preset", default=None,
|
|
2542
2779
|
help="Scene background preset (one of the template's customPayloadSchema.background.preset enum). "
|
|
2543
2780
|
"Written to every scene's customPayload.background.preset. When the user picked a background in "
|
|
2544
|
-
"the replicate card (
|
|
2781
|
+
"the replicate card (`<名称>(<id>)`), pass that id. Requires --template-id.",
|
|
2545
2782
|
)
|
|
2546
2783
|
parser.add_argument(
|
|
2547
2784
|
"--sequence-preset", default=None,
|
|
2548
|
-
help="
|
|
2549
|
-
"
|
|
2550
|
-
"
|
|
2551
|
-
"(
|
|
2785
|
+
help="Style preset for templates declaring capabilities.mediaBinding.options.preset (one of that "
|
|
2786
|
+
"option's enum in customPayloadSchema), written next to the template's media list; `none` keeps "
|
|
2787
|
+
"the template defaults. When the user picked a style preset in the replicate card "
|
|
2788
|
+
"(`<名称>(<id>)`), pass that id.",
|
|
2789
|
+
)
|
|
2790
|
+
parser.add_argument(
|
|
2791
|
+
"--narration", default="auto", choices=NARRATION_MODES,
|
|
2792
|
+
help="Narration on / off, overriding the template's capabilities.needsNarration (auto = the template "
|
|
2793
|
+
"decides). Templates where narration is optional default to none; pass `on` when the user picked "
|
|
2794
|
+
"narration in the replicate card (`旁白: <名称>(narrated)`), so the skeleton gets narration slots, "
|
|
2795
|
+
"a gen-voice asset and subtitles. `off` drops narration from a narrated template.",
|
|
2552
2796
|
)
|
|
2553
2797
|
parser.add_argument(
|
|
2554
2798
|
"--sequence-fit", default=None,
|
|
2555
|
-
help="
|
|
2556
|
-
"
|
|
2557
|
-
"
|
|
2799
|
+
help="Media fill mode for templates declaring capabilities.mediaBinding.options.fit (one of that "
|
|
2800
|
+
"option's enum in customPayloadSchema); `auto` keeps the template / preset default. When the user "
|
|
2801
|
+
"picked it in the replicate card, pass that id.",
|
|
2558
2802
|
)
|
|
2559
2803
|
parser.add_argument(
|
|
2560
2804
|
"--background-asset", default=None,
|
|
@@ -2565,8 +2809,8 @@ Examples:
|
|
|
2565
2809
|
)
|
|
2566
2810
|
parser.add_argument(
|
|
2567
2811
|
"--payload", action="append", default=None, metavar="KEY=VALUE",
|
|
2568
|
-
help="Set a template customPayload field on every scene, e.g. --payload
|
|
2569
|
-
"--payload
|
|
2812
|
+
help="Set a template customPayload field on every scene, e.g. --payload <field>=<value> "
|
|
2813
|
+
"--payload <object>.<field>=<value>. KEY is a dotted path "
|
|
2570
2814
|
"checked against the template's customPayloadSchema; VALUE is converted to its declared type "
|
|
2571
2815
|
"(true/false, numbers) and checked against its enum / range. Repeatable. Requires --template-id.",
|
|
2572
2816
|
)
|
|
@@ -2577,7 +2821,7 @@ Examples:
|
|
|
2577
2821
|
)
|
|
2578
2822
|
parser.add_argument("--allow-digital-human", action="store_true", help="Allow digital-human assets")
|
|
2579
2823
|
parser.add_argument("--avatar-id", default=None, help="Digital-human avatar id (dh_xxx) for templates declaring capabilities.digitalHuman: adds one gen-digital-human asset referenced by every scene's visuals.avatar.assetRef (one continuous presenter driven by the narration). Placement stays the template default unless set with --payload digitalHuman.<field>=...")
|
|
2580
|
-
parser.add_argument("--ending-payload", action="append", default=None, metavar="KEY=VALUE", help="Like --payload, but written only to the ending scene (e.g.
|
|
2824
|
+
parser.add_argument("--ending-payload", action="append", default=None, metavar="KEY=VALUE", help="Like --payload, but written only to the ending scene (e.g. <object>.<field>=<closing line>). On countdown templates it also appends the ending scene after #1. Repeatable.")
|
|
2581
2825
|
parser.add_argument("--allow-ai-video", action="store_true", help="Allow AI-generated video assets")
|
|
2582
2826
|
parser.add_argument("--template-id", default=None, help="Template id. The template owns outputLanguage and may declare a defaultVoiceId.")
|
|
2583
2827
|
parser.add_argument(
|
|
@@ -2624,12 +2868,11 @@ Examples:
|
|
|
2624
2868
|
"--carousel-items",
|
|
2625
2869
|
action="append",
|
|
2626
2870
|
default=None,
|
|
2627
|
-
help="Media URL for the template's
|
|
2871
|
+
help="Media URL for the template's image/video list. Can be repeated: "
|
|
2628
2872
|
"--carousel-items url1 --carousel-items url2. "
|
|
2629
2873
|
"For templates declaring capabilities.payloadStyle=carousel-caption, "
|
|
2630
|
-
"these URLs are placed directly into "
|
|
2631
|
-
"
|
|
2632
|
-
"customPayload.carousel.items, as existing assets without AI image generation.",
|
|
2874
|
+
"these URLs are placed directly into the list named by capabilities.mediaBinding.itemsPath, "
|
|
2875
|
+
"as existing assets without AI image generation.",
|
|
2633
2876
|
)
|
|
2634
2877
|
parser.add_argument(
|
|
2635
2878
|
"--scene-images",
|
|
@@ -2652,40 +2895,38 @@ Examples:
|
|
|
2652
2895
|
help="On-screen caption line. Can be repeated: "
|
|
2653
2896
|
"--caption-lines 'line1' --caption-lines 'line2'. "
|
|
2654
2897
|
"For templates declaring capabilities.payloadStyle=carousel-caption, "
|
|
2655
|
-
"these
|
|
2656
|
-
"
|
|
2898
|
+
"these go where capabilities.mediaBinding.captions says (one text block, or one caption "
|
|
2899
|
+
"per media item in order). "
|
|
2657
2900
|
"Line count limits and whether **emphasis** is parsed are declared per "
|
|
2658
2901
|
"template — see the chosen template's llmHint.",
|
|
2659
2902
|
)
|
|
2903
|
+
return parser
|
|
2660
2904
|
|
|
2661
|
-
args = parser.parse_args()
|
|
2662
2905
|
|
|
2906
|
+
def _run_validate(input_path: str | None) -> None:
|
|
2663
2907
|
|
|
2908
|
+
if not input_path:
|
|
2909
|
+
print("❌ validate mode requires --input", file=sys.stderr)
|
|
2910
|
+
sys.exit(1)
|
|
2911
|
+
if not os.path.exists(input_path):
|
|
2912
|
+
print(f"❌ file not found: {input_path}", file=sys.stderr)
|
|
2913
|
+
sys.exit(1)
|
|
2914
|
+
with open(input_path, "r", encoding="utf-8") as f:
|
|
2915
|
+
dsl = json.load(f)
|
|
2916
|
+
errors, warnings = validate_authored_dsl(dsl)
|
|
2917
|
+
for w in warnings:
|
|
2918
|
+
print(f"⚠️ {w}", file=sys.stderr)
|
|
2919
|
+
if errors:
|
|
2920
|
+
print("❌ DSL validation failed:", file=sys.stderr)
|
|
2921
|
+
for err in errors:
|
|
2922
|
+
print(f" - {err}", file=sys.stderr)
|
|
2923
|
+
sys.exit(1)
|
|
2924
|
+
else:
|
|
2925
|
+
print("✅ DSL validation passed")
|
|
2926
|
+
return
|
|
2664
2927
|
|
|
2665
|
-
if args.list_fonts:
|
|
2666
|
-
from font_catalog import print_font_catalog
|
|
2667
|
-
sys.exit(print_font_catalog())
|
|
2668
2928
|
|
|
2669
|
-
|
|
2670
|
-
if not args.input:
|
|
2671
|
-
print("❌ validate mode requires --input", file=sys.stderr)
|
|
2672
|
-
sys.exit(1)
|
|
2673
|
-
if not os.path.exists(args.input):
|
|
2674
|
-
print(f"❌ file not found: {args.input}", file=sys.stderr)
|
|
2675
|
-
sys.exit(1)
|
|
2676
|
-
with open(args.input, "r", encoding="utf-8") as f:
|
|
2677
|
-
dsl = json.load(f)
|
|
2678
|
-
errors, warnings = validate_authored_dsl(dsl)
|
|
2679
|
-
for w in warnings:
|
|
2680
|
-
print(f"⚠️ {w}", file=sys.stderr)
|
|
2681
|
-
if errors:
|
|
2682
|
-
print("❌ DSL validation failed:", file=sys.stderr)
|
|
2683
|
-
for err in errors:
|
|
2684
|
-
print(f" - {err}", file=sys.stderr)
|
|
2685
|
-
sys.exit(1)
|
|
2686
|
-
else:
|
|
2687
|
-
print("✅ DSL validation passed")
|
|
2688
|
-
return
|
|
2929
|
+
def _check_generation_args(args, parser: argparse.ArgumentParser) -> None:
|
|
2689
2930
|
|
|
2690
2931
|
if not args.topic:
|
|
2691
2932
|
print("❌ please provide --topic", file=sys.stderr)
|
|
@@ -2702,6 +2943,9 @@ Examples:
|
|
|
2702
2943
|
sys.exit(1)
|
|
2703
2944
|
|
|
2704
2945
|
|
|
2946
|
+
def _resolve_template_selection(args) -> dict | None:
|
|
2947
|
+
|
|
2948
|
+
|
|
2705
2949
|
try:
|
|
2706
2950
|
template_selection = _template_selection(
|
|
2707
2951
|
args.template_id, args.template_source, args.template_candidates,
|
|
@@ -2715,6 +2959,10 @@ Examples:
|
|
|
2715
2959
|
"If the user did not name it, stop and let them confirm it before writing the script.",
|
|
2716
2960
|
file=sys.stderr,
|
|
2717
2961
|
)
|
|
2962
|
+
return template_selection
|
|
2963
|
+
|
|
2964
|
+
|
|
2965
|
+
def _resolve_ratio(args) -> str:
|
|
2718
2966
|
|
|
2719
2967
|
|
|
2720
2968
|
|
|
@@ -2740,14 +2988,10 @@ Examples:
|
|
|
2740
2988
|
)
|
|
2741
2989
|
else:
|
|
2742
2990
|
resolved_ratio = "16:9"
|
|
2743
|
-
|
|
2991
|
+
return resolved_ratio
|
|
2744
2992
|
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
print(f" duration: {args.duration}s", file=sys.stderr)
|
|
2748
|
-
print(f" ratio: {args.ratio}", file=sys.stderr)
|
|
2749
|
-
if args.speed is not None:
|
|
2750
|
-
print(f" narration speed: {args.speed}x", file=sys.stderr)
|
|
2993
|
+
|
|
2994
|
+
def _resolve_stub_urls(args) -> tuple[str, str]:
|
|
2751
2995
|
|
|
2752
2996
|
|
|
2753
2997
|
|
|
@@ -2779,6 +3023,10 @@ Examples:
|
|
|
2779
3023
|
f"(image={stub_image_url}, video={stub_video_url}, audio=https://placeholder.local/stub.mp3)",
|
|
2780
3024
|
file=sys.stderr,
|
|
2781
3025
|
)
|
|
3026
|
+
return stub_image_url, stub_video_url
|
|
3027
|
+
|
|
3028
|
+
|
|
3029
|
+
def _parse_payload_writes(args, parser: argparse.ArgumentParser) -> list:
|
|
2782
3030
|
|
|
2783
3031
|
try:
|
|
2784
3032
|
|
|
@@ -2789,6 +3037,66 @@ Examples:
|
|
|
2789
3037
|
)
|
|
2790
3038
|
except ValueError as exc:
|
|
2791
3039
|
parser.error(str(exc))
|
|
3040
|
+
return payload_writes
|
|
3041
|
+
|
|
3042
|
+
|
|
3043
|
+
def _stub_generated_assets(dsl: dict, stub_video_url: str) -> None:
|
|
3044
|
+
|
|
3045
|
+
|
|
3046
|
+
|
|
3047
|
+
_AUDIO_STUB = "https://placeholder.local/stub.mp3"
|
|
3048
|
+
_VIDEO_STUB = stub_video_url
|
|
3049
|
+
for asset in dsl.get("assets", []):
|
|
3050
|
+
src = asset.get("source")
|
|
3051
|
+
if src in ("gen-voice",):
|
|
3052
|
+
asset["source"] = "existing"
|
|
3053
|
+
asset["status"] = "generated"
|
|
3054
|
+
asset["url"] = _AUDIO_STUB
|
|
3055
|
+
|
|
3056
|
+
elif src in ("gen-digital-human",):
|
|
3057
|
+
asset["source"] = "existing"
|
|
3058
|
+
asset["status"] = "generated"
|
|
3059
|
+
asset["url"] = _VIDEO_STUB
|
|
3060
|
+
|
|
3061
|
+
|
|
3062
|
+
def _check_output_language(dsl: dict) -> None:
|
|
3063
|
+
|
|
3064
|
+
expected_language = dsl.get("meta", {}).get("outputLanguage", DEFAULT_LANGUAGE)
|
|
3065
|
+
if expected_language in ("en", "zh"):
|
|
3066
|
+
narrations_for_check = [
|
|
3067
|
+
((scene.get("audio") or {}).get("narration") or {}).get("text", "")
|
|
3068
|
+
for scene in dsl.get("scenes", [])
|
|
3069
|
+
]
|
|
3070
|
+
_check_narration_language(narrations_for_check, expected_language)
|
|
3071
|
+
|
|
3072
|
+
|
|
3073
|
+
def main():
|
|
3074
|
+
parser = _build_arg_parser()
|
|
3075
|
+
args = parser.parse_args()
|
|
3076
|
+
|
|
3077
|
+
|
|
3078
|
+
|
|
3079
|
+
if args.list_fonts:
|
|
3080
|
+
from font_catalog import print_font_catalog
|
|
3081
|
+
sys.exit(print_font_catalog())
|
|
3082
|
+
|
|
3083
|
+
if args.validate:
|
|
3084
|
+
_run_validate(args.input)
|
|
3085
|
+
return
|
|
3086
|
+
|
|
3087
|
+
_check_generation_args(args, parser)
|
|
3088
|
+
template_selection = _resolve_template_selection(args)
|
|
3089
|
+
args.ratio = _resolve_ratio(args)
|
|
3090
|
+
|
|
3091
|
+
print(f"📝 generating video script...", file=sys.stderr)
|
|
3092
|
+
print(f" topic: {args.topic}", file=sys.stderr)
|
|
3093
|
+
print(f" duration: {args.duration}s", file=sys.stderr)
|
|
3094
|
+
print(f" ratio: {args.ratio}", file=sys.stderr)
|
|
3095
|
+
if args.speed is not None:
|
|
3096
|
+
print(f" narration speed: {args.speed}x", file=sys.stderr)
|
|
3097
|
+
|
|
3098
|
+
stub_image_url, stub_video_url = _resolve_stub_urls(args)
|
|
3099
|
+
payload_writes = _parse_payload_writes(args, parser)
|
|
2792
3100
|
|
|
2793
3101
|
try:
|
|
2794
3102
|
dsl = build_dsl(
|
|
@@ -2819,6 +3127,7 @@ Examples:
|
|
|
2819
3127
|
sequence_fit=args.sequence_fit,
|
|
2820
3128
|
payload_writes=payload_writes,
|
|
2821
3129
|
countdown_ending=bool(args.ending_payload),
|
|
3130
|
+
narration=args.narration,
|
|
2822
3131
|
)
|
|
2823
3132
|
except ValueError as exc:
|
|
2824
3133
|
|
|
@@ -2845,23 +3154,8 @@ Examples:
|
|
|
2845
3154
|
if template_selection:
|
|
2846
3155
|
dsl.setdefault("meta", {})["templateSelection"] = template_selection
|
|
2847
3156
|
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
3157
|
if args.skip_asset_generation:
|
|
2852
|
-
|
|
2853
|
-
_VIDEO_STUB = stub_video_url
|
|
2854
|
-
for asset in dsl.get("assets", []):
|
|
2855
|
-
src = asset.get("source")
|
|
2856
|
-
if src in ("gen-voice",):
|
|
2857
|
-
asset["source"] = "existing"
|
|
2858
|
-
asset["status"] = "generated"
|
|
2859
|
-
asset["url"] = _AUDIO_STUB
|
|
2860
|
-
|
|
2861
|
-
elif src in ("gen-digital-human",):
|
|
2862
|
-
asset["source"] = "existing"
|
|
2863
|
-
asset["status"] = "generated"
|
|
2864
|
-
asset["url"] = _VIDEO_STUB
|
|
3158
|
+
_stub_generated_assets(dsl, stub_video_url)
|
|
2865
3159
|
|
|
2866
3160
|
errors = validate_dsl(dsl)
|
|
2867
3161
|
if errors:
|
|
@@ -2870,14 +3164,7 @@ Examples:
|
|
|
2870
3164
|
print(f" - {err}", file=sys.stderr)
|
|
2871
3165
|
sys.exit(1)
|
|
2872
3166
|
|
|
2873
|
-
|
|
2874
|
-
expected_language = dsl.get("meta", {}).get("outputLanguage", DEFAULT_LANGUAGE)
|
|
2875
|
-
if expected_language in ("en", "zh"):
|
|
2876
|
-
narrations_for_check = [
|
|
2877
|
-
((scene.get("audio") or {}).get("narration") or {}).get("text", "")
|
|
2878
|
-
for scene in dsl.get("scenes", [])
|
|
2879
|
-
]
|
|
2880
|
-
_check_narration_language(narrations_for_check, expected_language)
|
|
3167
|
+
_check_output_language(dsl)
|
|
2881
3168
|
|
|
2882
3169
|
output_json = json.dumps(dsl, ensure_ascii=False, indent=2)
|
|
2883
3170
|
print(output_json)
|