@remixmate/cli 0.9.72 → 0.9.73
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/LICENSE +45 -21
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/argv.js +1 -51
- package/dist/auth/auth-lock.js +1 -100
- package/dist/auth/auto-login.js +7 -75
- package/dist/auth/commands.js +40 -290
- package/dist/auth/credential-store.js +1 -143
- package/dist/auth/device-flow-runner.js +13 -110
- package/dist/auth/device-flow.js +1 -132
- package/dist/auth/ensure.js +1 -62
- package/dist/auth/environment.js +1 -69
- package/dist/auth/pending-store.js +1 -45
- package/dist/auth/resolve.js +1 -44
- package/dist/billing.js +3 -76
- package/dist/capabilities.js +1 -75
- package/dist/cli.js +39 -211
- package/dist/diagnostics.js +2 -70
- package/dist/doctor.js +9 -188
- package/dist/errors.js +1 -33
- package/dist/exec.js +4 -47
- package/dist/handlers/gen-digital-human.js +33 -264
- package/dist/handlers/gen-image.js +9 -113
- package/dist/handlers/gen-post.js +15 -192
- package/dist/handlers/gen-video.js +7 -185
- package/dist/handlers/gen-voice.js +19 -133
- package/dist/handlers/index.js +1 -20
- package/dist/handlers/shared.js +1 -66
- package/dist/handlers/template-init.js +1 -21
- package/dist/http.js +3 -155
- package/dist/index.js +1 -8
- package/dist/install.js +40 -448
- package/dist/manifest.json +44 -3
- package/dist/progress.js +2 -15
- package/dist/project/commands.js +34 -156
- package/dist/project/host.js +1 -151
- package/dist/project/resolve.js +1 -65
- package/dist/project/store.js +2 -97
- package/dist/project/take.js +1 -106
- package/dist/project/trace.js +5 -278
- package/dist/registry.js +3 -70
- package/dist/runner.js +7 -225
- package/dist/skill-schema.js +1 -194
- package/dist/template/args.js +1 -69
- package/dist/template/check.js +2 -30
- package/dist/template/commands.js +34 -163
- package/dist/template/constants.js +1 -46
- package/dist/template/init.js +5 -133
- package/dist/template/list.js +1 -46
- package/dist/template/preview.js +7 -318
- package/dist/template/proc.js +1 -43
- package/dist/template/project.js +6 -161
- package/dist/template/publish.js +5 -185
- package/dist/template/render-client.js +2 -79
- package/dist/template/upgrade.js +4 -123
- package/dist/text.js +1 -15
- package/package.json +2 -35
- package/skills/doc-parse/scripts/extract_via_render.py +32 -32
- package/skills/export-jianying/scripts/gen_jianying_draft.py +188 -188
- package/skills/gen-script/SKILL.md +23 -1
- package/skills/gen-script/scripts/gen_script.py +644 -551
- package/skills/gen-script/skill.json +12 -0
- package/skills/gen-script/version.json +1 -1
- package/skills/prepare-video-assets/scripts/prepare_video_assets.py +21 -21
- package/skills/render-video/scripts/_chrome_vendor.py +17 -17
- package/skills/render-video/scripts/_digital_human_segments.py +34 -34
- package/skills/render-video/scripts/_video_probe.py +33 -33
- package/skills/render-video/scripts/_vod_polling.py +13 -13
- package/skills/render-video/scripts/remote_renderer_client.py +42 -42
- package/skills/render-video/scripts/render_video.py +639 -639
- package/skills/render-video/scripts/required_props.py +37 -37
- package/skills/render-video/scripts/upload_video.py +35 -35
- package/skills/render-video/version.json +1 -1
- package/skills/template-registry/SKILL.md +16 -0
- package/skills/template-registry/scripts/countdown_spec.py +92 -92
- package/skills/template-registry/scripts/doc_extract.py +40 -40
- package/skills/template-registry/scripts/list_templates.py +303 -191
- package/skills/template-registry/scripts/log_diagnostics.py +1 -1
- package/skills/template-registry/scripts/match_template.py +87 -87
- package/skills/template-registry/scripts/payload_hydrator.py +89 -89
- package/skills/template-registry/scripts/registry_loader.py +159 -159
- package/skills/template-registry/scripts/render_job_client.py +75 -75
- package/skills/template-registry/scripts/showcase_loader.py +44 -44
- package/skills/template-registry/skill.json +4 -1
- package/skills/template-registry/version.json +1 -1
- package/skills/template-registry/video_dsl/runtime/__init__.py +8 -8
- package/skills/template-registry/video_dsl/runtime/dsl_validator.py +329 -329
- package/skills/template-registry/video_dsl/runtime/narration_budget.py +36 -36
- package/skills/template-registry/video_dsl/runtime/payload_contract.py +7 -7
- package/skills/template-registry/video_dsl/runtime/prompt_enhancer.py +98 -98
- package/skills/template-registry/video_dsl/runtime/timeline_compiler.py +127 -127
- package/skills/template-registry/video_dsl/schema/video-dsl-v1alpha1.json +14 -1
- package/skills/video-parser/scripts/analyze_video.py +173 -173
- package/skills/video-parser/scripts/deconstruct_video.py +91 -91
- package/skills/video-parser/scripts/parse_via_render.py +21 -21
- package/skills/web-screenshot/scripts/_media_screenshot/__init__.py +4 -4
- package/skills/web-screenshot/scripts/_media_screenshot/bootstrap.py +7 -7
- package/skills/web-screenshot/scripts/_media_screenshot/browser.py +4 -4
- package/skills/web-screenshot/scripts/_media_screenshot/cli_args.py +4 -4
- package/skills/web-screenshot/scripts/_media_screenshot/js_loader.py +2 -2
- package/skills/web-screenshot/scripts/_media_screenshot/overlay.py +1 -1
- package/skills/web-screenshot/scripts/_media_screenshot/reader.py +40 -40
- package/skills/web-screenshot/scripts/_media_screenshot/recording.py +11 -11
- package/skills/web-screenshot/scripts/_media_screenshot/scenes/__init__.py +8 -8
- package/skills/web-screenshot/scripts/_media_screenshot/scenes/arrow.py +2 -2
- package/skills/web-screenshot/scripts/_media_screenshot/scenes/caption.py +1 -1
- package/skills/web-screenshot/scripts/_media_screenshot/scenes/click.py +1 -1
- package/skills/web-screenshot/scripts/_media_screenshot/scenes/code_lines.py +5 -5
- package/skills/web-screenshot/scripts/_media_screenshot/scenes/focus.py +1 -1
- package/skills/web-screenshot/scripts/_media_screenshot/scenes/highlight.py +3 -3
- package/skills/web-screenshot/scripts/_media_screenshot/scenes/hover.py +1 -1
- package/skills/web-screenshot/scripts/_media_screenshot/scenes/redact.py +1 -1
- package/skills/web-screenshot/scripts/_media_screenshot/scenes/scroll.py +1 -1
- package/skills/web-screenshot/scripts/_media_screenshot/scenes/sequence.py +1 -1
- package/skills/web-screenshot/scripts/_media_screenshot/scenes/title_card.py +1 -1
- package/skills/web-screenshot/scripts/_media_screenshot/scenes/type.py +1 -1
- package/skills/web-screenshot/scripts/_media_screenshot/scenes/wait.py +1 -1
- package/skills/web-screenshot/scripts/_media_screenshot/scenes/zoom.py +2 -2
- package/skills/web-screenshot/scripts/_media_screenshot/screenshot.py +15 -15
- package/skills/web-screenshot/scripts/_media_screenshot/storyboard.py +2 -2
- package/skills/web-screenshot/scripts/_media_screenshot/template.py +11 -11
- package/skills/web-screenshot/scripts/_media_screenshot/trim.py +10 -10
- package/skills/web-screenshot/scripts/_media_screenshot/urlguard.py +22 -22
- package/skills/web-screenshot/scripts/_media_upload.py +33 -33
- package/skills/web-screenshot/scripts/read_page.py +15 -15
- package/skills/web-screenshot/scripts/record.py +26 -26
- package/skills/web-screenshot/scripts/screenshot.py +5 -5
- package/dist/argv.d.ts +0 -15
- package/dist/auth/auth-lock.d.ts +0 -26
- package/dist/auth/auto-login.d.ts +0 -20
- package/dist/auth/commands.d.ts +0 -8
- package/dist/auth/credential-store.d.ts +0 -60
- package/dist/auth/device-flow-runner.d.ts +0 -58
- package/dist/auth/device-flow.d.ts +0 -52
- package/dist/auth/ensure.d.ts +0 -55
- package/dist/auth/environment.d.ts +0 -38
- package/dist/auth/pending-store.d.ts +0 -33
- package/dist/auth/resolve.d.ts +0 -30
- package/dist/billing.d.ts +0 -44
- package/dist/capabilities.d.ts +0 -63
- package/dist/cli.d.ts +0 -13
- package/dist/diagnostics.d.ts +0 -24
- package/dist/doctor.d.ts +0 -22
- package/dist/errors.d.ts +0 -29
- package/dist/exec.d.ts +0 -18
- package/dist/handlers/gen-digital-human.d.ts +0 -17
- package/dist/handlers/gen-image.d.ts +0 -20
- package/dist/handlers/gen-post.d.ts +0 -22
- package/dist/handlers/gen-video.d.ts +0 -19
- package/dist/handlers/gen-voice.d.ts +0 -26
- package/dist/handlers/index.d.ts +0 -20
- package/dist/handlers/shared.d.ts +0 -26
- package/dist/handlers/template-init.d.ts +0 -10
- package/dist/http.d.ts +0 -72
- package/dist/index.d.ts +0 -9
- package/dist/install.d.ts +0 -71
- package/dist/progress.d.ts +0 -23
- package/dist/project/commands.d.ts +0 -19
- package/dist/project/host.d.ts +0 -91
- package/dist/project/resolve.d.ts +0 -43
- package/dist/project/store.d.ts +0 -31
- package/dist/project/take.d.ts +0 -50
- package/dist/project/trace.d.ts +0 -98
- package/dist/registry.d.ts +0 -30
- package/dist/runner.d.ts +0 -39
- package/dist/skill-schema.d.ts +0 -138
- package/dist/template/args.d.ts +0 -25
- package/dist/template/check.d.ts +0 -23
- package/dist/template/commands.d.ts +0 -25
- package/dist/template/constants.d.ts +0 -39
- package/dist/template/init.d.ts +0 -28
- package/dist/template/list.d.ts +0 -23
- package/dist/template/preview.d.ts +0 -60
- package/dist/template/proc.d.ts +0 -26
- package/dist/template/project.d.ts +0 -47
- package/dist/template/publish.d.ts +0 -43
- package/dist/template/render-client.d.ts +0 -39
- package/dist/template/upgrade.d.ts +0 -44
- package/dist/text.d.ts +0 -8
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
Video script generator — turns a topic into Video DSL v1alpha1 JSON.
|
|
4
2
|
|
|
5
|
-
Default behavior:
|
|
6
|
-
Produces a structured Video DSL JSON to stdout based on the topic, duration,
|
|
7
|
-
and other constraints.
|
|
8
3
|
|
|
9
|
-
Usage:
|
|
10
|
-
python gen_script.py --topic "3 AI hacks that double study efficiency" --duration 30 --ratio "9:16"
|
|
11
|
-
python gen_script.py --validate --input my-video.dsl.json
|
|
12
4
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
17
|
|
|
18
18
|
import argparse
|
|
19
19
|
import json
|
|
@@ -30,26 +30,26 @@ SCHEMA_PATH = os.environ.get(
|
|
|
30
30
|
"skills/template-registry/video_dsl/schema/video-dsl-v1alpha1.json",
|
|
31
31
|
)
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
|
|
34
34
|
_TEMPLATE_DSL_DIR = os.path.join(os.path.dirname(__file__), "..", "..", "template-registry", "video_dsl")
|
|
35
35
|
if os.path.isdir(_TEMPLATE_DSL_DIR):
|
|
36
36
|
sys.path.insert(0, os.path.abspath(os.path.join(_TEMPLATE_DSL_DIR, "..")))
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
41
41
|
_SHARED_SCRIPTS_DIR = os.path.join(
|
|
42
42
|
os.path.dirname(__file__), "..", "..", "template-registry", "scripts"
|
|
43
43
|
)
|
|
44
44
|
if os.path.isdir(_SHARED_SCRIPTS_DIR):
|
|
45
45
|
sys.path.insert(0, os.path.abspath(_SHARED_SCRIPTS_DIR))
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
|
|
51
51
|
try:
|
|
52
|
-
from countdown_spec import (
|
|
52
|
+
from countdown_spec import (
|
|
53
53
|
opening_sec as countdown_opening_sec,
|
|
54
54
|
resolve_count as resolve_countdown_count,
|
|
55
55
|
resolve_countdown_spec,
|
|
@@ -73,16 +73,16 @@ except ImportError:
|
|
|
73
73
|
_HAS_ENHANCER = False
|
|
74
74
|
SKELETON_PREFIXES = ("[骨架待填充]", "[skeleton placeholder]")
|
|
75
75
|
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
|
|
77
|
+
|
|
78
78
|
try:
|
|
79
79
|
from video_dsl.runtime import narration_budget as _nb
|
|
80
80
|
_HAS_BUDGET = True
|
|
81
81
|
except ImportError:
|
|
82
82
|
_HAS_BUDGET = False
|
|
83
83
|
|
|
84
|
-
|
|
85
|
-
|
|
84
|
+
|
|
85
|
+
|
|
86
86
|
try:
|
|
87
87
|
from video_dsl.runtime.dsl_validator import (
|
|
88
88
|
DEFAULT_MAX_SCENES,
|
|
@@ -91,13 +91,13 @@ try:
|
|
|
91
91
|
except ImportError:
|
|
92
92
|
DEFAULT_MAX_SCENES = 20
|
|
93
93
|
|
|
94
|
-
def max_scenes_for_template(template_config):
|
|
94
|
+
def max_scenes_for_template(template_config):
|
|
95
95
|
return DEFAULT_MAX_SCENES
|
|
96
96
|
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
|
|
98
|
+
|
|
99
99
|
try:
|
|
100
|
-
from payload_hydrator import hydration_spec
|
|
100
|
+
from payload_hydrator import hydration_spec
|
|
101
101
|
_HAS_HYDRATOR = True
|
|
102
102
|
except ImportError:
|
|
103
103
|
_HAS_HYDRATOR = False
|
|
@@ -112,58 +112,58 @@ try:
|
|
|
112
112
|
except ImportError:
|
|
113
113
|
_HAS_REGISTRY_LOADER = False
|
|
114
114
|
|
|
115
|
-
class TemplateStatusGatedError(RuntimeError):
|
|
116
|
-
|
|
115
|
+
class TemplateStatusGatedError(RuntimeError):
|
|
116
|
+
pass
|
|
117
117
|
|
|
118
118
|
VALID_RATIOS = ["16:9", "9:16", "1:1", "4:3", "3:4", "21:9"]
|
|
119
119
|
VALID_PURPOSES = ["opening", "point", "example", "explanation", "highlight", "cta", "ending"]
|
|
120
120
|
|
|
121
|
-
# 规划用语速 / 行间停顿 / 场景尾巴统一在 video_dsl.runtime.narration_budget。
|
|
122
|
-
# 这里原有的 CHARS_PER_SECOND = 5.0 从未参与计算(只出现在注释里),已删。
|
|
123
121
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
|
|
129
129
|
|
|
130
130
|
DEFAULT_LANGUAGE = "zh"
|
|
131
131
|
LANGUAGE_TO_META = {"en": "en-US", "zh": "zh-CN"}
|
|
132
132
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
|
|
136
136
|
FALLBACK_VOICE = {
|
|
137
137
|
"zh": "Chinese (Mandarin)_Male_Announcer",
|
|
138
|
-
|
|
138
|
+
|
|
139
139
|
"en": "english-male-friendly-01",
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
|
|
149
149
|
DEFAULT_NARRATION_SPEED = 1.0
|
|
150
150
|
SPEED_MIN = 0.5
|
|
151
151
|
SPEED_MAX = 2.0
|
|
152
152
|
|
|
153
153
|
|
|
154
154
|
def _resolve_narration_speed(speed):
|
|
155
|
-
|
|
155
|
+
|
|
156
156
|
if speed is None:
|
|
157
157
|
return DEFAULT_NARRATION_SPEED
|
|
158
158
|
return float(speed)
|
|
159
159
|
|
|
160
160
|
|
|
161
161
|
def _resolve_template_language_meta(template_config):
|
|
162
|
-
"""Tolerant reader for outputLanguage / defaultVoiceId on a template dict.
|
|
163
162
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
|
|
167
167
|
if not isinstance(template_config, dict):
|
|
168
168
|
return DEFAULT_LANGUAGE, None
|
|
169
169
|
raw_lang = template_config.get("outputLanguage")
|
|
@@ -185,10 +185,10 @@ def _resolve_template_language_meta(template_config):
|
|
|
185
185
|
|
|
186
186
|
|
|
187
187
|
def _resolve_voice_id(cli_voice_id, template_default_voice_id, language):
|
|
188
|
-
"""Mirror of voice-resolver.ts resolveVoiceId. Total function.
|
|
189
188
|
|
|
190
|
-
|
|
191
|
-
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
|
|
192
192
|
if isinstance(cli_voice_id, str) and cli_voice_id.strip():
|
|
193
193
|
return cli_voice_id.strip()
|
|
194
194
|
if isinstance(template_default_voice_id, str) and template_default_voice_id.strip():
|
|
@@ -197,12 +197,12 @@ def _resolve_voice_id(cli_voice_id, template_default_voice_id, language):
|
|
|
197
197
|
|
|
198
198
|
|
|
199
199
|
def _check_narration_language(narrations, expected):
|
|
200
|
-
"""Non-blocking sanity check: warn when narration looks like the wrong language.
|
|
201
200
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
|
|
206
206
|
cjk_re = __import__("re").compile(r"[\u4e00-\u9fff]")
|
|
207
207
|
latin_re = __import__("re").compile(r"[A-Za-z]")
|
|
208
208
|
for text in narrations:
|
|
@@ -227,13 +227,13 @@ def _check_narration_language(narrations, expected):
|
|
|
227
227
|
|
|
228
228
|
|
|
229
229
|
def _localize_label(language, key, **fmt):
|
|
230
|
-
"""Tiny localization helper for the on-screen / narration default strings.
|
|
231
230
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
|
|
237
237
|
table = {
|
|
238
238
|
"label_opening": {"zh": "开场 Hook", "en": "Opening hook"},
|
|
239
239
|
"label_point": {"zh": "内容要点 {n}", "en": "Point {n}"},
|
|
@@ -250,7 +250,7 @@ def _localize_label(language, key, **fmt):
|
|
|
250
250
|
"zh": "[骨架待填充] 关于{topic}的 CTA 结尾({duration}s{budget})",
|
|
251
251
|
"en": "[skeleton placeholder] Closing CTA about {topic} ({duration}s{budget})",
|
|
252
252
|
},
|
|
253
|
-
|
|
253
|
+
|
|
254
254
|
"budget_suffix": {
|
|
255
255
|
"zh": ",整段≤{chars} 字;分行旁白每多一行约少 {per_line} 字",
|
|
256
256
|
"en": ", ≤{chars} words as one block; ~{per_line} fewer per extra narration line",
|
|
@@ -259,10 +259,10 @@ def _localize_label(language, key, **fmt):
|
|
|
259
259
|
"zh": "[骨架待填充] 关于{topic}的内容({duration}s{budget})",
|
|
260
260
|
"en": "[skeleton placeholder] Content about {topic} ({duration}s{budget})",
|
|
261
261
|
},
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
|
|
266
266
|
"follow_us": {"zh": "关注我们", "en": "Follow us"},
|
|
267
267
|
"video_description": {
|
|
268
268
|
"zh": "关于「{topic}」的{duration}秒短视频",
|
|
@@ -271,8 +271,8 @@ def _localize_label(language, key, **fmt):
|
|
|
271
271
|
}
|
|
272
272
|
entry = table.get(key, {})
|
|
273
273
|
template = entry.get(language) or entry.get(DEFAULT_LANGUAGE) or ""
|
|
274
|
-
|
|
275
|
-
|
|
274
|
+
|
|
275
|
+
|
|
276
276
|
fmt.setdefault("budget", "")
|
|
277
277
|
return template.format(**fmt)
|
|
278
278
|
|
|
@@ -285,17 +285,17 @@ def load_schema():
|
|
|
285
285
|
|
|
286
286
|
|
|
287
287
|
def validate_dsl(dsl: dict) -> list:
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
from video_dsl.runtime.dsl_validator import (
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
from video_dsl.runtime.dsl_validator import (
|
|
299
299
|
validate_structural,
|
|
300
300
|
errors_as_strings,
|
|
301
301
|
)
|
|
@@ -310,14 +310,14 @@ def plan_scenes(
|
|
|
310
310
|
language: str = DEFAULT_LANGUAGE,
|
|
311
311
|
max_scenes: int = DEFAULT_MAX_SCENES,
|
|
312
312
|
) -> list:
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
|
|
321
321
|
requested = scene_count
|
|
322
322
|
if scene_count is None:
|
|
323
323
|
if duration <= 15:
|
|
@@ -352,10 +352,10 @@ def plan_scenes(
|
|
|
352
352
|
opening_duration = max(3, round(duration * 0.12))
|
|
353
353
|
ending_duration = max(3, round(duration * 0.12))
|
|
354
354
|
content_total = duration - opening_duration - ending_duration
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
|
|
359
359
|
base_content, extra_content = divmod(max(content_total, 0), content_scenes)
|
|
360
360
|
|
|
361
361
|
scenes.append({
|
|
@@ -381,11 +381,11 @@ def plan_scenes(
|
|
|
381
381
|
|
|
382
382
|
|
|
383
383
|
def _budget_suffix(duration: int, language: str, speed: float, tail_pad_sec: float | None) -> str:
|
|
384
|
-
"""占位旁白里的字数预算。拿不到预算模块时返回空串(退回旧格式)。
|
|
385
384
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
|
|
389
389
|
if not _HAS_BUDGET:
|
|
390
390
|
return ""
|
|
391
391
|
tail = _nb.DEFAULT_TAIL_PAD_SEC if tail_pad_sec is None else tail_pad_sec
|
|
@@ -404,19 +404,19 @@ def estimate_narration_text(
|
|
|
404
404
|
speed: float = 1.0,
|
|
405
405
|
tail_pad_sec: float | None = None,
|
|
406
406
|
) -> str:
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
|
|
420
420
|
fmt = {
|
|
421
421
|
"label": label, "topic": _clip(topic, _PLACEHOLDER_TOPIC_CHARS), "duration": duration,
|
|
422
422
|
"budget": _budget_suffix(duration, language, speed, tail_pad_sec),
|
|
@@ -430,17 +430,17 @@ def estimate_narration_text(
|
|
|
430
430
|
return _localize_label(language, "narration_default", **fmt)
|
|
431
431
|
|
|
432
432
|
|
|
433
|
-
|
|
433
|
+
|
|
434
434
|
NARRATION_SKELETON_MARKER = SKELETON_PREFIXES[0]
|
|
435
435
|
|
|
436
436
|
|
|
437
437
|
def generate_image_prompt(
|
|
438
438
|
purpose: str, topic: str, style: str, narration_text: str = "", style_guide: dict | None = None
|
|
439
439
|
) -> dict:
|
|
440
|
-
"""Generate image prompt for a scene.
|
|
441
440
|
|
|
442
|
-
|
|
443
|
-
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
|
|
444
444
|
if _HAS_ENHANCER:
|
|
445
445
|
return build_enhanced_prompt(
|
|
446
446
|
purpose=purpose,
|
|
@@ -450,7 +450,7 @@ def generate_image_prompt(
|
|
|
450
450
|
style_guide=style_guide,
|
|
451
451
|
)
|
|
452
452
|
|
|
453
|
-
|
|
453
|
+
|
|
454
454
|
style_suffix = f",{style}风格" if style else ""
|
|
455
455
|
prompts = {
|
|
456
456
|
"opening": f"{topic}主题概念图,吸引眼球的视觉效果{style_suffix},高清大图",
|
|
@@ -461,18 +461,18 @@ def generate_image_prompt(
|
|
|
461
461
|
|
|
462
462
|
|
|
463
463
|
def _parse_ai_slot(slot: str) -> tuple[bool, str]:
|
|
464
|
-
"""解析 --scene-images 的一项:是不是 AI 段,以及用户给的提示词。
|
|
465
464
|
|
|
466
|
-
返回 ``(is_ai, prompt)``:
|
|
467
465
|
|
|
468
|
-
- ``"https://…/a.png"`` → ``(False, "")`` 用已有素材
|
|
469
|
-
- ``"ai"`` → ``(True, "")`` 这一段交给 gen-image,提示词由模板推
|
|
470
|
-
- ``"ai: 深色调机房"`` → ``(True, "深色调机房")``
|
|
471
466
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
|
|
476
476
|
text = (slot or "").strip()
|
|
477
477
|
if text.lower() == "ai":
|
|
478
478
|
return True, ""
|
|
@@ -482,23 +482,23 @@ def _parse_ai_slot(slot: str) -> tuple[bool, str]:
|
|
|
482
482
|
|
|
483
483
|
|
|
484
484
|
def _load_template_config(template_id: str) -> dict | None:
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
|
|
502
502
|
if _HAS_REGISTRY_LOADER:
|
|
503
503
|
try:
|
|
504
504
|
return _get_template(template_id)
|
|
@@ -510,7 +510,7 @@ def _load_template_config(template_id: str) -> dict | None:
|
|
|
510
510
|
file=sys.stderr,
|
|
511
511
|
)
|
|
512
512
|
|
|
513
|
-
|
|
513
|
+
|
|
514
514
|
legacy_dir = os.path.join(
|
|
515
515
|
os.path.dirname(__file__), "..", "..", "template-registry", "video_dsl", "templates"
|
|
516
516
|
)
|
|
@@ -522,11 +522,11 @@ def _load_template_config(template_id: str) -> dict | None:
|
|
|
522
522
|
|
|
523
523
|
|
|
524
524
|
def _template_supported_ratio(template_config: dict | None) -> str | None:
|
|
525
|
-
"""Return template 的首选宽高比。
|
|
526
525
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
|
|
530
530
|
if not template_config:
|
|
531
531
|
return None
|
|
532
532
|
ratios = template_config.get("supportedAspectRatios") or []
|
|
@@ -537,15 +537,15 @@ def _template_supported_ratio(template_config: dict | None) -> str | None:
|
|
|
537
537
|
|
|
538
538
|
|
|
539
539
|
def _template_primary_visual_type(template_config: dict | None) -> str:
|
|
540
|
-
"""从模板的 assetRequirements 推导每个 scene 的主要视觉素材类型。
|
|
541
540
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
|
|
547
547
|
if not template_config:
|
|
548
|
-
return "image"
|
|
548
|
+
return "image"
|
|
549
549
|
requirements = template_config.get("assetRequirements") or []
|
|
550
550
|
if "image" in requirements:
|
|
551
551
|
return "image"
|
|
@@ -555,22 +555,22 @@ def _template_primary_visual_type(template_config: dict | None) -> str:
|
|
|
555
555
|
|
|
556
556
|
|
|
557
557
|
def _template_needs_narration(template_config: dict | None) -> bool:
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
|
|
574
574
|
caps = (template_config or {}).get("capabilities")
|
|
575
575
|
if not isinstance(caps, dict):
|
|
576
576
|
return True
|
|
@@ -578,46 +578,46 @@ def _template_needs_narration(template_config: dict | None) -> bool:
|
|
|
578
578
|
|
|
579
579
|
|
|
580
580
|
def _template_needs_image(template_config: dict | None) -> bool:
|
|
581
|
-
"""Check if a template requires image assets.
|
|
582
581
|
|
|
583
|
-
|
|
584
|
-
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
|
|
585
585
|
if template_config is None:
|
|
586
|
-
return True
|
|
586
|
+
return True
|
|
587
587
|
requirements = template_config.get("assetRequirements", [])
|
|
588
588
|
return "image" in requirements
|
|
589
589
|
|
|
590
590
|
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
|
|
613
613
|
|
|
614
614
|
|
|
615
615
|
def _resolve_countdown(template_config: dict | None) -> dict:
|
|
616
|
-
"""countdown 声明解析;共享模块缺失时明确报错而不是静默换一种形状。
|
|
617
616
|
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
|
|
621
621
|
if not _HAS_COUNTDOWN:
|
|
622
622
|
raise ValueError(
|
|
623
623
|
"capabilities.sceneStrategy=countdown needs the shared countdown_spec module "
|
|
@@ -627,12 +627,12 @@ def _resolve_countdown(template_config: dict | None) -> dict:
|
|
|
627
627
|
|
|
628
628
|
|
|
629
629
|
def _resolve_contract(template_config: dict | None) -> dict:
|
|
630
|
-
"""把 template.json 的 capabilities 归一化成一份装配 contract。
|
|
631
630
|
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
|
|
636
636
|
caps = (template_config or {}).get("capabilities")
|
|
637
637
|
caps = caps if isinstance(caps, dict) else {}
|
|
638
638
|
|
|
@@ -654,7 +654,7 @@ def _resolve_contract(template_config: dict | None) -> dict:
|
|
|
654
654
|
|
|
655
655
|
fixed_scenes = caps.get("fixedScenes")
|
|
656
656
|
if not isinstance(fixed_scenes, list) or not fixed_scenes:
|
|
657
|
-
|
|
657
|
+
|
|
658
658
|
fixed_scenes = [{"purpose": "opening"}] if scene_strategy in ("single", "fixed") else []
|
|
659
659
|
|
|
660
660
|
payload_defaults = caps.get("payloadDefaults")
|
|
@@ -664,10 +664,10 @@ def _resolve_contract(template_config: dict | None) -> dict:
|
|
|
664
664
|
"needs_narration": needs_narration,
|
|
665
665
|
"scene_strategy": scene_strategy,
|
|
666
666
|
"fixed_scenes": fixed_scenes,
|
|
667
|
-
|
|
667
|
+
|
|
668
668
|
"max_scenes": max_scenes_for_template(template_config),
|
|
669
|
-
|
|
670
|
-
|
|
669
|
+
|
|
670
|
+
|
|
671
671
|
"countdown": _resolve_countdown(template_config) if scene_strategy == "countdown" else None,
|
|
672
672
|
"payload_style": payload_style,
|
|
673
673
|
"duration_strategy": duration_strategy,
|
|
@@ -676,21 +676,21 @@ def _resolve_contract(template_config: dict | None) -> dict:
|
|
|
676
676
|
"primary_visual_type": primary_visual_type,
|
|
677
677
|
"default_layout": caps.get("defaultLayout"),
|
|
678
678
|
"authored_scenes": caps.get("authoredScenes") is True,
|
|
679
|
-
|
|
679
|
+
|
|
680
680
|
"text_layer_rules": caps.get("textLayerRules") if isinstance(caps.get("textLayerRules"), dict) else None,
|
|
681
|
-
|
|
681
|
+
|
|
682
682
|
"arc_closing_purpose": caps.get("arcClosingPurpose") if isinstance(caps.get("arcClosingPurpose"), str) else None,
|
|
683
|
-
|
|
683
|
+
|
|
684
684
|
"default_transition": caps.get("defaultTransition") if isinstance(caps.get("defaultTransition"), str) else None,
|
|
685
685
|
}
|
|
686
686
|
|
|
687
687
|
|
|
688
688
|
def _slot_reads_visual(template_config: dict | None, purpose: str) -> bool:
|
|
689
|
-
"""这个用途的版式会不会显示背景素材(slotMapping 里有没有读 visuals.background.assetRef)。
|
|
690
689
|
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
|
|
694
694
|
mapping = (template_config or {}).get("slotMapping")
|
|
695
695
|
if not isinstance(mapping, dict) or not mapping:
|
|
696
696
|
return True
|
|
@@ -709,12 +709,12 @@ def _slot_reads_visual(template_config: dict | None, purpose: str) -> bool:
|
|
|
709
709
|
|
|
710
710
|
|
|
711
711
|
def _placeholder_text_layers(rules: dict) -> list[dict]:
|
|
712
|
-
"""模板声明了 capabilities.textLayerRules 时,骨架的文字层 = 每个必填 role 一条占位。
|
|
713
712
|
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
|
|
718
718
|
language = rules.get("language") if isinstance(rules.get("language"), dict) else {}
|
|
719
719
|
hints = {"en": "English text for this page", "zh": "本页中文译文"}
|
|
720
720
|
layers = []
|
|
@@ -728,9 +728,9 @@ def _placeholder_text_layers(rules: dict) -> list[dict]:
|
|
|
728
728
|
return layers
|
|
729
729
|
|
|
730
730
|
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
|
|
734
734
|
_TITLE_MAX_CHARS = 60
|
|
735
735
|
_PLACEHOLDER_TOPIC_CHARS = 30
|
|
736
736
|
|
|
@@ -741,48 +741,48 @@ def _clip(text: str, limit: int) -> str:
|
|
|
741
741
|
|
|
742
742
|
|
|
743
743
|
def _video_title(headline: str | None, topic: str) -> str:
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
|
|
753
|
+
|
|
754
754
|
return _clip((headline or "").replace("**", ""), _TITLE_MAX_CHARS) or _clip(topic, _TITLE_MAX_CHARS)
|
|
755
755
|
|
|
756
756
|
|
|
757
757
|
def _normalize_linebreaks(value: str | None) -> str:
|
|
758
|
-
"""把画面文字里的**字面量** ``\\n`` 折成真换行。
|
|
759
758
|
|
|
760
|
-
``textLayers[].content`` 用真换行分行,但调用方是在拼 shell 命令行的模型:
|
|
761
|
-
``--subheadline 'A\\nB'`` 在 POSIX 单引号里传进来的是**反斜杠 + n 两个字符**,
|
|
762
|
-
不是换行。不折的话它会原样渲进画面(副标题上真的印着一个 ``\\n``),而这既不
|
|
763
|
-
报错也不出现在确认摘要里 —— 属于"只有渲完看片才发现"的那类坑。
|
|
764
759
|
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
760
|
+
|
|
761
|
+
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
|
|
765
|
+
|
|
766
|
+
|
|
767
|
+
|
|
768
768
|
return (value or "").replace("\\n", "\n").strip()
|
|
769
769
|
|
|
770
770
|
|
|
771
771
|
def _echo_template_llm_hint(template_config: dict | None, template_id: str | None) -> None:
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
772
|
+
|
|
773
|
+
|
|
774
|
+
|
|
775
|
+
|
|
776
|
+
|
|
777
|
+
|
|
778
|
+
|
|
779
|
+
|
|
780
|
+
|
|
781
|
+
|
|
782
|
+
|
|
783
|
+
|
|
784
|
+
|
|
785
|
+
|
|
786
786
|
hint = (template_config or {}).get("llmHint")
|
|
787
787
|
if isinstance(hint, str) and hint.strip():
|
|
788
788
|
print(
|
|
@@ -793,16 +793,16 @@ def _echo_template_llm_hint(template_config: dict | None, template_id: str | Non
|
|
|
793
793
|
|
|
794
794
|
|
|
795
795
|
def _warn_on_url_text_layer(headline: str, subheadline: str) -> None:
|
|
796
|
-
"""画面文字整条是一个裸 URL 时出声提醒(不阻断)。
|
|
797
796
|
|
|
798
|
-
这是 DSL 层的事实、不是某个模板的版式:``textLayers`` 是**画在画面上**的字,
|
|
799
|
-
而观众没法点视频里的链接、也很少有人会照着念一串 URL 抄下来。链接的正确去处
|
|
800
|
-
因模板而异(打字机行、结尾 CTA、简介),所以这里只说"这不该出现在画面标题里",
|
|
801
|
-
不替模板规定它该去哪 —— 那句话由模板的 llmHint 自己讲。
|
|
802
797
|
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
798
|
+
|
|
799
|
+
|
|
800
|
+
|
|
801
|
+
|
|
802
|
+
|
|
803
|
+
|
|
804
|
+
|
|
805
|
+
|
|
806
806
|
for flag, value in (("--headline", headline), ("--subheadline", subheadline)):
|
|
807
807
|
lines = [ln.strip() for ln in (value or "").split("\n") if ln.strip()]
|
|
808
808
|
if lines and all(
|
|
@@ -822,16 +822,16 @@ def _warn_on_url_text_layer(headline: str, subheadline: str) -> None:
|
|
|
822
822
|
def _enforce_supported_duration(
|
|
823
823
|
dsl: dict, template_config: dict | None, template_id: str | None
|
|
824
824
|
) -> None:
|
|
825
|
-
"""校验成片总时长落在模板自己声明的 ``supportedDurations`` 区间内。
|
|
826
825
|
|
|
827
|
-
这条约束此前只在 template-library 的 CI(``check-dsl-examples.mjs``)对仓库里的
|
|
828
|
-
示例 DSL 生效,运行时链路(gen_script → prepare_video_assets → render_video)没有
|
|
829
|
-
任何一环校验它。于是一个声明了 ``supportedDurations.min`` 的模板照样可以静默产出
|
|
830
|
-
低于下限的成片 —— 模板自己说了"我不是为这么短设计的",却没人拦。这里在 DSL 出厂前补上同一道栅栏。
|
|
831
826
|
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
827
|
+
|
|
828
|
+
|
|
829
|
+
|
|
830
|
+
|
|
831
|
+
|
|
832
|
+
|
|
833
|
+
|
|
834
|
+
|
|
835
835
|
sd = (template_config or {}).get("supportedDurations")
|
|
836
836
|
if not isinstance(sd, dict):
|
|
837
837
|
return
|
|
@@ -870,16 +870,16 @@ def _record_source_document(
|
|
|
870
870
|
template_id: str | None,
|
|
871
871
|
source_document_url: str | None,
|
|
872
872
|
) -> None:
|
|
873
|
-
"""把源文件引用写进 ``meta.sourceDocument``,供渲染管线按引用取数。
|
|
874
873
|
|
|
875
|
-
这个键必须由**骨架**携带,不能指望模型后面补:ab-agent 的合并层
|
|
876
|
-
(``applyNarrationToSkeleton``)以骨架为基底,只从模型提交的 dsl_json 里搬
|
|
877
|
-
narration 与 customPayload,``meta`` / ``global`` 归骨架所有。模型往 dsl_json 里写
|
|
878
|
-
``meta.sourceDocument`` 会被静默丢掉。
|
|
879
874
|
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
875
|
+
|
|
876
|
+
|
|
877
|
+
|
|
878
|
+
|
|
879
|
+
|
|
880
|
+
|
|
881
|
+
|
|
882
|
+
|
|
883
883
|
if source_document_url:
|
|
884
884
|
dsl["meta"]["sourceDocument"] = {"url": source_document_url}
|
|
885
885
|
return
|
|
@@ -888,7 +888,7 @@ def _record_source_document(
|
|
|
888
888
|
return
|
|
889
889
|
try:
|
|
890
890
|
spec = hydration_spec(template_config)
|
|
891
|
-
except Exception:
|
|
891
|
+
except Exception:
|
|
892
892
|
return
|
|
893
893
|
if not spec:
|
|
894
894
|
return
|
|
@@ -907,15 +907,15 @@ def _record_source_document(
|
|
|
907
907
|
def _plan_contract_scenes(
|
|
908
908
|
contract: dict, topic: str, duration: int, scene_count: int | None, language: str
|
|
909
909
|
) -> list:
|
|
910
|
-
"""按 contract.scene_strategy 规划场景列表。
|
|
911
910
|
|
|
912
|
-
- arc : 沿用 plan_scenes 叙事弧(opening → point* → cta,最少 3 段)。
|
|
913
|
-
- single : 单场景(默认 opening),整段时长归一个场景。
|
|
914
|
-
- fixed : 按 capabilities.fixedScenes 顺序展开,时长均分。
|
|
915
911
|
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
912
|
+
|
|
913
|
+
|
|
914
|
+
|
|
915
|
+
|
|
916
|
+
|
|
917
|
+
|
|
918
|
+
|
|
919
919
|
if contract["scene_strategy"] == "arc":
|
|
920
920
|
plans = plan_scenes(
|
|
921
921
|
topic, duration, scene_count, False,
|
|
@@ -929,8 +929,8 @@ def _plan_contract_scenes(
|
|
|
929
929
|
|
|
930
930
|
fixed = contract["fixed_scenes"] or [{"purpose": "opening"}]
|
|
931
931
|
n = len(fixed)
|
|
932
|
-
|
|
933
|
-
|
|
932
|
+
|
|
933
|
+
|
|
934
934
|
if scene_count is not None and scene_count != n:
|
|
935
935
|
print(
|
|
936
936
|
f"⚠️ requested {scene_count} scene(s) but this template's sceneStrategy="
|
|
@@ -954,24 +954,24 @@ def _plan_contract_scenes(
|
|
|
954
954
|
|
|
955
955
|
|
|
956
956
|
def _plain_len(text: str) -> int:
|
|
957
|
-
|
|
957
|
+
|
|
958
958
|
return len((text or "").replace("**", ""))
|
|
959
959
|
|
|
960
960
|
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
961
|
+
|
|
962
|
+
|
|
963
|
+
|
|
964
|
+
|
|
965
|
+
|
|
966
|
+
|
|
967
|
+
|
|
968
|
+
|
|
969
|
+
|
|
970
|
+
|
|
971
971
|
|
|
972
972
|
|
|
973
973
|
def _estimate_caption_natural_sec(caption: dict, model: dict) -> float:
|
|
974
|
-
|
|
974
|
+
|
|
975
975
|
max_lines = model.get("maxLines")
|
|
976
976
|
lines = caption.get("lines") or []
|
|
977
977
|
if isinstance(max_lines, int) and max_lines > 0:
|
|
@@ -995,7 +995,7 @@ def _estimate_caption_natural_sec(caption: dict, model: dict) -> float:
|
|
|
995
995
|
|
|
996
996
|
|
|
997
997
|
def _estimate_carousel_min_sec(carousel: dict, model: dict) -> float:
|
|
998
|
-
|
|
998
|
+
|
|
999
999
|
items = carousel.get("items") or []
|
|
1000
1000
|
n = len(items)
|
|
1001
1001
|
if n == 0:
|
|
@@ -1018,13 +1018,13 @@ def _estimate_carousel_min_sec(carousel: dict, model: dict) -> float:
|
|
|
1018
1018
|
def _estimate_fit_caption_duration(
|
|
1019
1019
|
custom_payload: dict, duration_model: dict | None, template_id: str | None
|
|
1020
1020
|
) -> float:
|
|
1021
|
-
"""fit-caption 策略:按打字机 + 轮播 + 标题入场推算建议时长(秒)。
|
|
1022
1021
|
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1022
|
+
|
|
1023
|
+
|
|
1024
|
+
|
|
1025
|
+
|
|
1026
|
+
|
|
1027
|
+
|
|
1028
1028
|
if not isinstance(duration_model, dict) or not duration_model:
|
|
1029
1029
|
print(
|
|
1030
1030
|
f"⚠️ template '{template_id}' declares durationStrategy=fit-caption but no "
|
|
@@ -1048,11 +1048,11 @@ def _estimate_fit_caption_duration(
|
|
|
1048
1048
|
|
|
1049
1049
|
|
|
1050
1050
|
def _estimate_fit_images_duration(sequence: dict, model: dict | None, fps: int = 30) -> float:
|
|
1051
|
-
"""Apply template-declared image timing, in frames just like the renderer.
|
|
1052
1051
|
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1052
|
+
|
|
1053
|
+
|
|
1054
|
+
|
|
1055
|
+
|
|
1056
1056
|
timing = (model or {}).get("sequence", {})
|
|
1057
1057
|
tiers = timing.get("holdSecByCount", [])
|
|
1058
1058
|
count = len(sequence["items"])
|
|
@@ -1064,7 +1064,7 @@ def _estimate_fit_images_duration(sequence: dict, model: dict | None, fps: int =
|
|
|
1064
1064
|
margin = timing.get("minHoldMarginFrames")
|
|
1065
1065
|
if transition_ms is None or margin is None:
|
|
1066
1066
|
raise ValueError("fit-images requires sequence transitionMs and durationModel.sequence.minHoldMarginFrames")
|
|
1067
|
-
|
|
1067
|
+
|
|
1068
1068
|
min_frames = max(2, math.floor(transition_ms / 1000 * fps + 0.5)) + margin
|
|
1069
1069
|
return sum(max(min_frames, math.floor(item.get("holdSec", base_hold) * fps + 0.5))
|
|
1070
1070
|
for item in sequence["items"]) / fps
|
|
@@ -1092,12 +1092,12 @@ def _build_carousel_caption_dsl(
|
|
|
1092
1092
|
duration_strategy: str | None,
|
|
1093
1093
|
duration_model: dict | None,
|
|
1094
1094
|
) -> dict:
|
|
1095
|
-
"""carousel-caption 装配(单场景、图/视频轮播 + 打字机文字)。
|
|
1096
1095
|
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1096
|
+
|
|
1097
|
+
|
|
1098
|
+
|
|
1099
|
+
|
|
1100
|
+
|
|
1101
1101
|
defaults = deepcopy(payload_defaults or {})
|
|
1102
1102
|
sequence_mode = isinstance(defaults.get("sequence"), dict)
|
|
1103
1103
|
if sequence_mode and not carousel_items:
|
|
@@ -1105,12 +1105,12 @@ def _build_carousel_caption_dsl(
|
|
|
1105
1105
|
if sequence_mode and len(caption_lines) > len(carousel_items):
|
|
1106
1106
|
raise ValueError("Image sequence has more --caption-lines than images; use at most one caption per image")
|
|
1107
1107
|
|
|
1108
|
-
|
|
1108
|
+
|
|
1109
1109
|
assets = []
|
|
1110
1110
|
carousel_asset_ids = []
|
|
1111
1111
|
for idx, url in enumerate(carousel_items):
|
|
1112
1112
|
asset_id = f"carousel-img-{idx + 1:02d}"
|
|
1113
|
-
|
|
1113
|
+
|
|
1114
1114
|
is_video = urlsplit(url).path.lower().endswith((".mp4", ".mov", ".webm", ".avi"))
|
|
1115
1115
|
if sequence_mode and is_video:
|
|
1116
1116
|
raise ValueError("Image sequence requires image URLs in --carousel-items, not video URLs")
|
|
@@ -1123,7 +1123,7 @@ def _build_carousel_caption_dsl(
|
|
|
1123
1123
|
})
|
|
1124
1124
|
carousel_asset_ids.append((asset_id, "video" if is_video else "image"))
|
|
1125
1125
|
|
|
1126
|
-
|
|
1126
|
+
|
|
1127
1127
|
narration_asset_id = "narration-scene-01"
|
|
1128
1128
|
if narration_enabled:
|
|
1129
1129
|
assets.append({
|
|
@@ -1136,7 +1136,7 @@ def _build_carousel_caption_dsl(
|
|
|
1136
1136
|
},
|
|
1137
1137
|
})
|
|
1138
1138
|
|
|
1139
|
-
|
|
1139
|
+
|
|
1140
1140
|
carousel_defaults = defaults.get("carousel", {})
|
|
1141
1141
|
carousel_config = {
|
|
1142
1142
|
"items": [],
|
|
@@ -1153,25 +1153,25 @@ def _build_carousel_caption_dsl(
|
|
|
1153
1153
|
item["muted"] = True
|
|
1154
1154
|
carousel_config["items"].append(item)
|
|
1155
1155
|
|
|
1156
|
-
|
|
1156
|
+
|
|
1157
1157
|
caption_defaults = defaults.get("caption", {})
|
|
1158
1158
|
caption_config = {
|
|
1159
1159
|
"lines": caption_lines if caption_lines else [],
|
|
1160
1160
|
**caption_defaults,
|
|
1161
1161
|
}
|
|
1162
|
-
|
|
1162
|
+
|
|
1163
1163
|
if caption_lines:
|
|
1164
1164
|
caption_config["lines"] = caption_lines
|
|
1165
1165
|
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1166
|
+
|
|
1167
|
+
|
|
1168
|
+
|
|
1169
|
+
|
|
1170
|
+
|
|
1171
|
+
|
|
1172
|
+
|
|
1173
|
+
|
|
1174
|
+
|
|
1175
1175
|
custom_payload = {
|
|
1176
1176
|
"carousel": carousel_config,
|
|
1177
1177
|
"caption": caption_config,
|
|
@@ -1180,9 +1180,9 @@ def _build_carousel_caption_dsl(
|
|
|
1180
1180
|
if defaults.get(key):
|
|
1181
1181
|
custom_payload[key] = defaults[key]
|
|
1182
1182
|
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1183
|
+
|
|
1184
|
+
|
|
1185
|
+
|
|
1186
1186
|
if sequence_mode:
|
|
1187
1187
|
sequence_config = defaults["sequence"]
|
|
1188
1188
|
sequence_config["items"] = [
|
|
@@ -1192,14 +1192,14 @@ def _build_carousel_caption_dsl(
|
|
|
1192
1192
|
]
|
|
1193
1193
|
custom_payload = {"sequence": sequence_config}
|
|
1194
1194
|
|
|
1195
|
-
|
|
1195
|
+
|
|
1196
1196
|
narration_text = _localize_label(output_language, "narration_opening",
|
|
1197
1197
|
label="", topic=_clip(topic, _PLACEHOLDER_TOPIC_CHARS), duration=duration)
|
|
1198
1198
|
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1199
|
+
|
|
1200
|
+
|
|
1201
|
+
|
|
1202
|
+
|
|
1203
1203
|
effective_duration = duration
|
|
1204
1204
|
if duration_strategy == "fit-images":
|
|
1205
1205
|
if not sequence_mode:
|
|
@@ -1215,7 +1215,7 @@ def _build_carousel_caption_dsl(
|
|
|
1215
1215
|
file=sys.stderr,
|
|
1216
1216
|
)
|
|
1217
1217
|
|
|
1218
|
-
|
|
1218
|
+
|
|
1219
1219
|
text_layers = [
|
|
1220
1220
|
{"role": "headline", "content": headline, "animation": "slide-up"},
|
|
1221
1221
|
]
|
|
@@ -1239,7 +1239,7 @@ def _build_carousel_caption_dsl(
|
|
|
1239
1239
|
},
|
|
1240
1240
|
}
|
|
1241
1241
|
|
|
1242
|
-
|
|
1242
|
+
|
|
1243
1243
|
effective_headline = headline or topic
|
|
1244
1244
|
effective_subheadline = subheadline or ""
|
|
1245
1245
|
|
|
@@ -1261,8 +1261,8 @@ def _build_carousel_caption_dsl(
|
|
|
1261
1261
|
"aspectRatio": ratio,
|
|
1262
1262
|
"resolution": resolution,
|
|
1263
1263
|
"fps": 30,
|
|
1264
|
-
|
|
1265
|
-
|
|
1264
|
+
|
|
1265
|
+
|
|
1266
1266
|
"subtitle": {"enabled": narration_enabled, "style": "bottom"},
|
|
1267
1267
|
**({"narration": {"voiceId": resolved_voice_id, "speed": narration_speed}} if narration_enabled else {}),
|
|
1268
1268
|
**({"font": {"fontId": font_id, **({"fontName": font_name} if font_name else {})}} if font_id else {}),
|
|
@@ -1276,26 +1276,26 @@ def _build_carousel_caption_dsl(
|
|
|
1276
1276
|
return dsl
|
|
1277
1277
|
|
|
1278
1278
|
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1279
|
+
|
|
1280
|
+
|
|
1281
|
+
|
|
1282
|
+
|
|
1283
|
+
|
|
1284
|
+
|
|
1285
1285
|
SLIDE_ID_SENTINEL = "__CHOOSE_SLIDE__"
|
|
1286
1286
|
|
|
1287
1287
|
|
|
1288
1288
|
def _is_multi_slide_template(template_config: dict | None) -> bool:
|
|
1289
|
-
"""模板是否注册了多个可选版式 —— 由模板自己声明,不看模板 id。
|
|
1290
1289
|
|
|
1291
|
-
判据是 ``customPayloadSchema.slideId.enum`` 有没有超过一个取值:有得挑才谈得上
|
|
1292
|
-
「该挑哪个」。dsl_validator 的 ``_check_slide_id_chosen`` 用的是同一条判据,两侧
|
|
1293
|
-
因此不会对同一个模板给出相反的结论。
|
|
1294
1290
|
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1291
|
+
|
|
1292
|
+
|
|
1293
|
+
|
|
1294
|
+
|
|
1295
|
+
|
|
1296
|
+
|
|
1297
|
+
|
|
1298
|
+
|
|
1299
1299
|
schema = (template_config or {}).get("customPayloadSchema")
|
|
1300
1300
|
if not isinstance(schema, dict):
|
|
1301
1301
|
return False
|
|
@@ -1305,34 +1305,34 @@ def _is_multi_slide_template(template_config: dict | None) -> bool:
|
|
|
1305
1305
|
|
|
1306
1306
|
|
|
1307
1307
|
def _build_custom_payload(template_config: dict | None) -> dict:
|
|
1308
|
-
"""骨架阶段的 customPayload —— **不猜版式、不拼画面文案**。
|
|
1309
1308
|
|
|
1310
|
-
这个函数曾经按 purpose 写死两种 slideId(opening / cta → demo-concept-overview,
|
|
1311
|
-
其余 → demo-single-concept),于是 16 种注册版式里只有 2 种会被用到,且与内容
|
|
1312
|
-
形态完全无关 —— 线上「不论选题版式永远一样」就是这么来的。它还往
|
|
1313
|
-
templateData.description 里拼 "关于{topic}的要点内容",topic 长度不受控,画面
|
|
1314
|
-
溢出是必然的。
|
|
1315
1309
|
|
|
1316
|
-
现在分两档:
|
|
1317
1310
|
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1311
|
+
|
|
1312
|
+
|
|
1313
|
+
|
|
1314
|
+
|
|
1315
|
+
|
|
1316
|
+
|
|
1317
|
+
|
|
1318
|
+
|
|
1319
|
+
|
|
1320
|
+
|
|
1321
|
+
|
|
1322
1322
|
if _is_multi_slide_template(template_config):
|
|
1323
1323
|
return {"slideId": SLIDE_ID_SENTINEL, "templateData": {}}
|
|
1324
1324
|
return {}
|
|
1325
1325
|
|
|
1326
1326
|
|
|
1327
1327
|
def _plan_countdown_scenes(spec: dict, count: int, duration: float, headline: str) -> list:
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1328
|
+
|
|
1329
|
+
|
|
1330
|
+
|
|
1331
|
+
|
|
1332
|
+
|
|
1333
|
+
|
|
1334
|
+
|
|
1335
|
+
|
|
1336
1336
|
head_sec = countdown_opening_sec(spec, count, duration)
|
|
1337
1337
|
beat_sec = (duration - head_sec) / count
|
|
1338
1338
|
return [{"purpose": "opening", "duration": head_sec, "label": headline}] + [
|
|
@@ -1342,15 +1342,15 @@ def _plan_countdown_scenes(spec: dict, count: int, duration: float, headline: st
|
|
|
1342
1342
|
|
|
1343
1343
|
|
|
1344
1344
|
def _warn_on_count_mismatch(spec: dict, headline: str, subheadline: str, count: int) -> None:
|
|
1345
|
-
"""画面文字里写的数量与实际节拍数不一致时出声提醒(不阻断、不改写)。
|
|
1346
1345
|
|
|
1347
|
-
**刻意只 warn 不改写。** 标题该怎么写、数量以什么措辞出现是模板的版式事实,
|
|
1348
|
-
模板在渲染时本来就会同步;手写 DSL、历史计划、ab-web 改过的标题都不经过
|
|
1349
|
-
gen_script,所以这里替它改既不必要,也会静默改掉调用方给的文案。
|
|
1350
1346
|
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1347
|
+
|
|
1348
|
+
|
|
1349
|
+
|
|
1350
|
+
|
|
1351
|
+
|
|
1352
|
+
|
|
1353
|
+
|
|
1354
1354
|
for flag, value in (("--headline", headline), ("--subheadline", subheadline)):
|
|
1355
1355
|
stated = countdown_stated_counts(spec, value)
|
|
1356
1356
|
if stated and count not in stated:
|
|
@@ -1367,11 +1367,11 @@ def _warn_on_count_mismatch(spec: dict, headline: str, subheadline: str, count:
|
|
|
1367
1367
|
|
|
1368
1368
|
|
|
1369
1369
|
def _template_background_presets(template_config: dict | None) -> list[str]:
|
|
1370
|
-
"""模板在 ``customPayloadSchema.background.preset`` 声明的背景取值(没声明 = 空表)。
|
|
1371
1370
|
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1371
|
+
|
|
1372
|
+
|
|
1373
|
+
|
|
1374
|
+
|
|
1375
1375
|
schema = (template_config or {}).get("customPayloadSchema") or {}
|
|
1376
1376
|
background = schema.get("background") if isinstance(schema, dict) else None
|
|
1377
1377
|
props = background.get("properties") if isinstance(background, dict) else None
|
|
@@ -1380,6 +1380,68 @@ def _template_background_presets(template_config: dict | None) -> list[str]:
|
|
|
1380
1380
|
return [str(v) for v in enum] if isinstance(enum, list) else []
|
|
1381
1381
|
|
|
1382
1382
|
|
|
1383
|
+
|
|
1384
|
+
|
|
1385
|
+
|
|
1386
|
+
|
|
1387
|
+
|
|
1388
|
+
|
|
1389
|
+
|
|
1390
|
+
TEMPLATE_SOURCES = ("user-explicit", "user-confirmed", "auto")
|
|
1391
|
+
CONFIRMED_TEMPLATE_SOURCES = ("user-explicit", "user-confirmed")
|
|
1392
|
+
|
|
1393
|
+
|
|
1394
|
+
class TemplateSelectionError(ValueError):
|
|
1395
|
+
pass
|
|
1396
|
+
|
|
1397
|
+
|
|
1398
|
+
def _template_selection(
|
|
1399
|
+
template_id: str | None,
|
|
1400
|
+
source: str | None,
|
|
1401
|
+
candidates: list[str] | None,
|
|
1402
|
+
) -> dict | None:
|
|
1403
|
+
|
|
1404
|
+
|
|
1405
|
+
|
|
1406
|
+
|
|
1407
|
+
|
|
1408
|
+
cands: list[str] = []
|
|
1409
|
+
for c in candidates or []:
|
|
1410
|
+
c = (c or "").strip()
|
|
1411
|
+
if c and c not in cands:
|
|
1412
|
+
cands.append(c)
|
|
1413
|
+
|
|
1414
|
+
if not source:
|
|
1415
|
+
if cands:
|
|
1416
|
+
raise TemplateSelectionError("--template-candidates needs --template-source.")
|
|
1417
|
+
return None
|
|
1418
|
+
if not template_id:
|
|
1419
|
+
raise TemplateSelectionError("--template-source needs --template-id: it records how that template was chosen.")
|
|
1420
|
+
if source == "auto":
|
|
1421
|
+
listed = ", ".join(cands) if cands else template_id
|
|
1422
|
+
raise TemplateSelectionError(
|
|
1423
|
+
f"template {template_id!r} was picked by the agent and the user has not confirmed it yet.\n"
|
|
1424
|
+
f" Stop here and ask the user to choose: list the candidates ({listed}) with one line on why "
|
|
1425
|
+
f"each fits and its detail-page link (template_registry pageUrl), recommended one first. End the turn.\n"
|
|
1426
|
+
f" After the user picks one, call gen_script again with --template-source user-confirmed "
|
|
1427
|
+
f"(and the same --template-candidates)."
|
|
1428
|
+
)
|
|
1429
|
+
if source not in CONFIRMED_TEMPLATE_SOURCES:
|
|
1430
|
+
raise TemplateSelectionError(
|
|
1431
|
+
f"--template-source must be one of {', '.join(TEMPLATE_SOURCES)} (got {source!r})."
|
|
1432
|
+
)
|
|
1433
|
+
if source == "user-confirmed" and cands and template_id not in cands:
|
|
1434
|
+
raise TemplateSelectionError(
|
|
1435
|
+
f"--template-id {template_id!r} is not among --template-candidates ({', '.join(cands)}). "
|
|
1436
|
+
f"If the user named a template outside the shortlist, that is --template-source user-explicit."
|
|
1437
|
+
)
|
|
1438
|
+
|
|
1439
|
+
selection: dict = {"source": source}
|
|
1440
|
+
if cands:
|
|
1441
|
+
selection["candidates"] = cands
|
|
1442
|
+
return selection
|
|
1443
|
+
|
|
1444
|
+
|
|
1383
1445
|
def _apply_look(
|
|
1384
1446
|
dsl: dict,
|
|
1385
1447
|
template_config: dict | None,
|
|
@@ -1387,18 +1449,18 @@ def _apply_look(
|
|
|
1387
1449
|
variant: str | None,
|
|
1388
1450
|
background_preset: str | None,
|
|
1389
1451
|
) -> None:
|
|
1390
|
-
"""把用户选的视觉风格 / 背景样式写进 DSL(就地改)。
|
|
1391
1452
|
|
|
1392
|
-
这两个值以前没有任何一跳能落进管线:复刻弹窗让用户选了,gen_script 却只写模板
|
|
1393
|
-
默认背景,merge 模式又只替换旁白——成片照出、不报错,用户只觉得「好像没生效」。
|
|
1394
1453
|
|
|
1395
|
-
- ``--variant`` → ``meta.templateVariant``:match_template 把它当**显式**选择,
|
|
1396
|
-
优先于 meta.style 的模糊匹配,最终成为 inputProps.variantId;
|
|
1397
|
-
- ``--background-preset`` → 每个场景 ``customPayload.background.preset``:
|
|
1398
|
-
customPayload 原样透传给模板,模板读的正是这个字段。保留模板默认背景里的其它键。
|
|
1399
1454
|
|
|
1400
|
-
|
|
1401
|
-
|
|
1455
|
+
|
|
1456
|
+
|
|
1457
|
+
|
|
1458
|
+
|
|
1459
|
+
|
|
1460
|
+
|
|
1461
|
+
|
|
1462
|
+
|
|
1463
|
+
|
|
1402
1464
|
if not variant and not background_preset:
|
|
1403
1465
|
return
|
|
1404
1466
|
if not template_id or template_config is None:
|
|
@@ -1457,34 +1519,34 @@ def build_dsl(
|
|
|
1457
1519
|
item_count: int | None = None,
|
|
1458
1520
|
source_document_url: str | None = None,
|
|
1459
1521
|
) -> dict:
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1522
|
+
|
|
1523
|
+
|
|
1524
|
+
|
|
1525
|
+
|
|
1526
|
+
|
|
1527
|
+
|
|
1528
|
+
|
|
1529
|
+
|
|
1530
|
+
|
|
1531
|
+
|
|
1532
|
+
|
|
1533
|
+
|
|
1534
|
+
|
|
1535
|
+
|
|
1536
|
+
|
|
1537
|
+
|
|
1538
|
+
|
|
1539
|
+
|
|
1540
|
+
|
|
1541
|
+
|
|
1542
|
+
|
|
1543
|
+
|
|
1544
|
+
|
|
1545
|
+
|
|
1546
|
+
|
|
1547
|
+
|
|
1548
|
+
|
|
1549
|
+
|
|
1488
1550
|
template_config = _load_template_config(template_id) if template_id else None
|
|
1489
1551
|
contract = _resolve_contract(template_config)
|
|
1490
1552
|
countdown = contract["countdown"]
|
|
@@ -1496,15 +1558,15 @@ def build_dsl(
|
|
|
1496
1558
|
|
|
1497
1559
|
primary_visual_type = contract["primary_visual_type"]
|
|
1498
1560
|
payload_style = contract["payload_style"]
|
|
1499
|
-
|
|
1561
|
+
|
|
1500
1562
|
has_visual = payload_style == "visual-overlay"
|
|
1501
|
-
|
|
1502
|
-
|
|
1563
|
+
|
|
1564
|
+
|
|
1503
1565
|
needs_narration = contract["needs_narration"]
|
|
1504
1566
|
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1567
|
+
|
|
1568
|
+
|
|
1569
|
+
|
|
1508
1570
|
output_language, template_default_voice_id = _resolve_template_language_meta(template_config)
|
|
1509
1571
|
resolved_voice_id = _resolve_voice_id(voice_id, template_default_voice_id, output_language)
|
|
1510
1572
|
narration_speed = _resolve_narration_speed(speed)
|
|
@@ -1515,7 +1577,7 @@ def build_dsl(
|
|
|
1515
1577
|
file=sys.stderr,
|
|
1516
1578
|
)
|
|
1517
1579
|
|
|
1518
|
-
|
|
1580
|
+
|
|
1519
1581
|
effective_headline = _normalize_linebreaks(headline) or topic
|
|
1520
1582
|
effective_subheadline = _normalize_linebreaks(subheadline)
|
|
1521
1583
|
_echo_template_llm_hint(template_config, template_id)
|
|
@@ -1523,16 +1585,16 @@ def build_dsl(
|
|
|
1523
1585
|
if beat_count is not None:
|
|
1524
1586
|
_warn_on_count_mismatch(countdown, effective_headline, effective_subheadline, beat_count)
|
|
1525
1587
|
|
|
1526
|
-
|
|
1588
|
+
|
|
1527
1589
|
if payload_style == "carousel-caption":
|
|
1528
1590
|
print(
|
|
1529
1591
|
f"ℹ️ contract: payloadStyle=carousel-caption for template {template_id}",
|
|
1530
1592
|
file=sys.stderr,
|
|
1531
1593
|
)
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1594
|
+
|
|
1595
|
+
|
|
1596
|
+
|
|
1597
|
+
|
|
1536
1598
|
if not (carousel_items or []) and not (caption_lines or []):
|
|
1537
1599
|
print(
|
|
1538
1600
|
"❌ carousel-caption template "
|
|
@@ -1548,13 +1610,13 @@ def build_dsl(
|
|
|
1548
1610
|
file=sys.stderr,
|
|
1549
1611
|
)
|
|
1550
1612
|
sys.exit(1)
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1613
|
+
|
|
1614
|
+
|
|
1615
|
+
|
|
1616
|
+
|
|
1617
|
+
|
|
1618
|
+
|
|
1619
|
+
|
|
1558
1620
|
if contract["duration_strategy"] == "fit-caption" and not (caption_lines or []):
|
|
1559
1621
|
print(
|
|
1560
1622
|
"❌ template "
|
|
@@ -1597,11 +1659,11 @@ def build_dsl(
|
|
|
1597
1659
|
_enforce_supported_duration(carousel_dsl, template_config, template_id)
|
|
1598
1660
|
return carousel_dsl
|
|
1599
1661
|
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1662
|
+
|
|
1663
|
+
|
|
1664
|
+
|
|
1665
|
+
|
|
1666
|
+
|
|
1605
1667
|
provided_slots = [s.strip() for s in (scene_images or []) if s and s.strip()]
|
|
1606
1668
|
|
|
1607
1669
|
if beat_count is not None:
|
|
@@ -1609,14 +1671,14 @@ def build_dsl(
|
|
|
1609
1671
|
else:
|
|
1610
1672
|
scene_plans = _plan_contract_scenes(contract, topic, duration, scene_count, output_language)
|
|
1611
1673
|
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1674
|
+
|
|
1675
|
+
|
|
1676
|
+
|
|
1677
|
+
|
|
1678
|
+
|
|
1679
|
+
|
|
1680
|
+
|
|
1681
|
+
|
|
1620
1682
|
if (
|
|
1621
1683
|
provided_slots
|
|
1622
1684
|
and scene_count is None
|
|
@@ -1629,8 +1691,8 @@ def build_dsl(
|
|
|
1629
1691
|
contract, topic, duration, len(provided_slots), output_language
|
|
1630
1692
|
)
|
|
1631
1693
|
|
|
1632
|
-
|
|
1633
|
-
|
|
1694
|
+
|
|
1695
|
+
|
|
1634
1696
|
if provided_slots and len(provided_slots) > len(scene_plans):
|
|
1635
1697
|
print(
|
|
1636
1698
|
f"\u26a0\ufe0f {len(provided_slots)} scene-image entries provided but the template "
|
|
@@ -1643,9 +1705,9 @@ def build_dsl(
|
|
|
1643
1705
|
assets = []
|
|
1644
1706
|
scenes = []
|
|
1645
1707
|
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1708
|
+
|
|
1709
|
+
|
|
1710
|
+
|
|
1649
1711
|
image_style_guide = resolve_style_guide(template_config) if _HAS_ENHANCER else {}
|
|
1650
1712
|
image_compose = _HAS_ENHANCER and is_compose_mode(image_style_guide)
|
|
1651
1713
|
image_ratio = image_style_guide.get("ratio") or ratio
|
|
@@ -1653,7 +1715,7 @@ def build_dsl(
|
|
|
1653
1715
|
tail_pad_sec = _nb.template_tail_pad_sec(template_config) if _HAS_BUDGET else None
|
|
1654
1716
|
for idx, plan in enumerate(scene_plans):
|
|
1655
1717
|
scene_id = f"scene-{idx + 1:02d}"
|
|
1656
|
-
|
|
1718
|
+
|
|
1657
1719
|
if primary_visual_type == "video":
|
|
1658
1720
|
visual_asset_id = f"video-{scene_id}"
|
|
1659
1721
|
else:
|
|
@@ -1667,16 +1729,16 @@ def build_dsl(
|
|
|
1667
1729
|
if needs_narration else ""
|
|
1668
1730
|
)
|
|
1669
1731
|
|
|
1670
|
-
|
|
1732
|
+
|
|
1671
1733
|
scene_has_visual = has_visual and _slot_reads_visual(template_config, plan["purpose"])
|
|
1672
1734
|
|
|
1673
|
-
|
|
1735
|
+
|
|
1674
1736
|
if scene_has_visual and primary_visual_type == "image":
|
|
1675
1737
|
slot = provided_slots[idx] if idx < len(provided_slots) else ""
|
|
1676
1738
|
slot_is_ai, slot_prompt = _parse_ai_slot(slot)
|
|
1677
1739
|
if slot and not slot_is_ai:
|
|
1678
|
-
|
|
1679
|
-
|
|
1740
|
+
|
|
1741
|
+
|
|
1680
1742
|
assets.append({
|
|
1681
1743
|
"assetId": visual_asset_id,
|
|
1682
1744
|
"type": "image",
|
|
@@ -1684,9 +1746,9 @@ def build_dsl(
|
|
|
1684
1746
|
"status": "generated",
|
|
1685
1747
|
"url": slot,
|
|
1686
1748
|
})
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1749
|
+
|
|
1750
|
+
|
|
1751
|
+
|
|
1690
1752
|
elif stub_image_url:
|
|
1691
1753
|
assets.append({
|
|
1692
1754
|
"assetId": visual_asset_id,
|
|
@@ -1704,10 +1766,10 @@ def build_dsl(
|
|
|
1704
1766
|
narration_text=narration_text, style_guide=image_style_guide,
|
|
1705
1767
|
)
|
|
1706
1768
|
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1769
|
+
|
|
1770
|
+
|
|
1771
|
+
|
|
1772
|
+
|
|
1711
1773
|
if slot_prompt:
|
|
1712
1774
|
image_result = {**image_result, "prompt": slot_prompt}
|
|
1713
1775
|
|
|
@@ -1738,16 +1800,16 @@ def build_dsl(
|
|
|
1738
1800
|
"url": stub_video_url,
|
|
1739
1801
|
})
|
|
1740
1802
|
else:
|
|
1741
|
-
|
|
1742
|
-
|
|
1803
|
+
|
|
1804
|
+
|
|
1743
1805
|
image_result = generate_image_prompt(plan["purpose"], topic, style, narration_text=narration_text)
|
|
1744
1806
|
video_prompt = image_result["prompt"]
|
|
1745
|
-
|
|
1807
|
+
|
|
1746
1808
|
video_duration = max(4, min(int(plan["duration"]), 15))
|
|
1747
1809
|
vid_payload = {
|
|
1748
1810
|
"prompt": video_prompt,
|
|
1749
|
-
|
|
1750
|
-
|
|
1811
|
+
|
|
1812
|
+
|
|
1751
1813
|
"model": os.environ.get("DEFAULT_VIDEO_MODEL", "seedance"),
|
|
1752
1814
|
"ratio": ratio,
|
|
1753
1815
|
"duration": video_duration,
|
|
@@ -1760,10 +1822,10 @@ def build_dsl(
|
|
|
1760
1822
|
"payload": vid_payload,
|
|
1761
1823
|
})
|
|
1762
1824
|
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1825
|
+
|
|
1826
|
+
|
|
1827
|
+
|
|
1828
|
+
|
|
1767
1829
|
if needs_narration:
|
|
1768
1830
|
assets.append({
|
|
1769
1831
|
"assetId": narration_asset_id,
|
|
@@ -1788,9 +1850,9 @@ def build_dsl(
|
|
|
1788
1850
|
text_layers = [
|
|
1789
1851
|
{"role": "badge", "content": plan["label"], "animation": "slide-up"},
|
|
1790
1852
|
]
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1853
|
+
|
|
1854
|
+
|
|
1855
|
+
|
|
1794
1856
|
text_layers.insert(0, {"role": "headline", "content": effective_headline, "animation": "fade-in"})
|
|
1795
1857
|
if effective_subheadline:
|
|
1796
1858
|
text_layers.append({"role": "subheadline", "content": effective_subheadline, "animation": "fade-in"})
|
|
@@ -1820,37 +1882,37 @@ def build_dsl(
|
|
|
1820
1882
|
"narration": {
|
|
1821
1883
|
"text": narration_text,
|
|
1822
1884
|
"assetRef": narration_asset_id,
|
|
1823
|
-
|
|
1885
|
+
|
|
1824
1886
|
"needsFill": True,
|
|
1825
1887
|
},
|
|
1826
1888
|
}
|
|
1827
1889
|
scenes.append(scene)
|
|
1828
1890
|
else:
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1891
|
+
|
|
1892
|
+
|
|
1893
|
+
|
|
1894
|
+
|
|
1895
|
+
|
|
1896
|
+
|
|
1897
|
+
|
|
1898
|
+
|
|
1899
|
+
|
|
1900
|
+
|
|
1901
|
+
|
|
1902
|
+
|
|
1903
|
+
|
|
1904
|
+
|
|
1905
|
+
|
|
1906
|
+
|
|
1907
|
+
|
|
1846
1908
|
_layout_cfg = (template_config or {}).get("capabilities") or {}
|
|
1847
1909
|
layout = _layout_cfg.get("defaultLayout") or template_id or "text-overlay"
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1910
|
+
|
|
1911
|
+
|
|
1912
|
+
|
|
1913
|
+
|
|
1914
|
+
|
|
1915
|
+
|
|
1854
1916
|
text_layers = []
|
|
1855
1917
|
if plan["purpose"] == "opening":
|
|
1856
1918
|
text_layers = [
|
|
@@ -1874,8 +1936,8 @@ def build_dsl(
|
|
|
1874
1936
|
|
|
1875
1937
|
custom_payload = _build_custom_payload(template_config)
|
|
1876
1938
|
if beat_count is not None:
|
|
1877
|
-
|
|
1878
|
-
|
|
1939
|
+
|
|
1940
|
+
|
|
1879
1941
|
custom_payload.update(
|
|
1880
1942
|
countdown_scene_payload(countdown, beat_count, plan.get("countdownIndex"))
|
|
1881
1943
|
)
|
|
@@ -1887,8 +1949,8 @@ def build_dsl(
|
|
|
1887
1949
|
"layout": layout,
|
|
1888
1950
|
"textLayers": text_layers,
|
|
1889
1951
|
}
|
|
1890
|
-
|
|
1891
|
-
|
|
1952
|
+
|
|
1953
|
+
|
|
1892
1954
|
if custom_payload:
|
|
1893
1955
|
scene["customPayload"] = custom_payload
|
|
1894
1956
|
if needs_narration:
|
|
@@ -1896,7 +1958,7 @@ def build_dsl(
|
|
|
1896
1958
|
"narration": {
|
|
1897
1959
|
"text": narration_text,
|
|
1898
1960
|
"assetRef": narration_asset_id,
|
|
1899
|
-
|
|
1961
|
+
|
|
1900
1962
|
"needsFill": True,
|
|
1901
1963
|
},
|
|
1902
1964
|
}
|
|
@@ -1914,15 +1976,15 @@ def build_dsl(
|
|
|
1914
1976
|
"outputLanguage": output_language,
|
|
1915
1977
|
"style": style or "",
|
|
1916
1978
|
"description": _localize_label(output_language, "video_description", topic=topic, duration=duration),
|
|
1917
|
-
|
|
1979
|
+
|
|
1918
1980
|
**({"characterSheet": ""} if image_compose and has_visual else {}),
|
|
1919
1981
|
},
|
|
1920
1982
|
"global": {
|
|
1921
1983
|
"aspectRatio": ratio,
|
|
1922
1984
|
"resolution": resolution,
|
|
1923
1985
|
"fps": 30,
|
|
1924
|
-
|
|
1925
|
-
|
|
1986
|
+
|
|
1987
|
+
|
|
1926
1988
|
"subtitle": {"enabled": needs_narration, "style": "bottom"},
|
|
1927
1989
|
**({"narration": {"voiceId": resolved_voice_id, "speed": narration_speed}} if needs_narration else {}),
|
|
1928
1990
|
**({"font": {"fontId": font_id, **({"fontName": font_name} if font_name else {})}} if font_id else {}),
|
|
@@ -1930,7 +1992,7 @@ def build_dsl(
|
|
|
1930
1992
|
},
|
|
1931
1993
|
"assets": assets,
|
|
1932
1994
|
"scenes": scenes,
|
|
1933
|
-
|
|
1995
|
+
|
|
1934
1996
|
"transitions": {"default": contract["default_transition"] or "fade", "duration": 0.5},
|
|
1935
1997
|
}
|
|
1936
1998
|
|
|
@@ -1940,9 +2002,9 @@ def build_dsl(
|
|
|
1940
2002
|
_record_source_document(dsl, template_config, template_id, source_document_url)
|
|
1941
2003
|
|
|
1942
2004
|
if contract["authored_scenes"]:
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
2005
|
+
|
|
2006
|
+
|
|
2007
|
+
|
|
1946
2008
|
dsl["meta"]["authoredScenes"] = True
|
|
1947
2009
|
print(
|
|
1948
2010
|
f"ℹ️ template '{template_id}' declares capabilities.authoredScenes: the scene list below "
|
|
@@ -2025,6 +2087,19 @@ Examples:
|
|
|
2025
2087
|
parser.add_argument("--allow-digital-human", action="store_true", help="Allow digital-human assets")
|
|
2026
2088
|
parser.add_argument("--allow-ai-video", action="store_true", help="Allow AI-generated video assets")
|
|
2027
2089
|
parser.add_argument("--template-id", default=None, help="Template id. The template owns outputLanguage and may declare a defaultVoiceId.")
|
|
2090
|
+
parser.add_argument(
|
|
2091
|
+
"--template-source", default=None, choices=TEMPLATE_SOURCES,
|
|
2092
|
+
help="How --template-id was chosen, recorded at meta.templateSelection.source. "
|
|
2093
|
+
"user-explicit: the user named this template (id, name, detail-page prompt, or an earlier turn). "
|
|
2094
|
+
"user-confirmed: you recommended it and the user confirmed in the chat. "
|
|
2095
|
+
"auto: you picked it and the user has not confirmed — gen_script refuses and tells you to ask first. "
|
|
2096
|
+
"Requires --template-id.",
|
|
2097
|
+
)
|
|
2098
|
+
parser.add_argument(
|
|
2099
|
+
"--template-candidates", action="append", default=None,
|
|
2100
|
+
help="Template ids you offered the user (repeatable), recommended first. Recorded at "
|
|
2101
|
+
"meta.templateSelection.candidates. With user-confirmed, --template-id must be one of them.",
|
|
2102
|
+
)
|
|
2028
2103
|
parser.add_argument("--validate", action="store_true", help="Validate-only mode: only validate the input DSL")
|
|
2029
2104
|
parser.add_argument("--input", "-i", help="Input DSL file path (required for validate mode)")
|
|
2030
2105
|
parser.add_argument(
|
|
@@ -2125,10 +2200,25 @@ Examples:
|
|
|
2125
2200
|
)
|
|
2126
2201
|
sys.exit(1)
|
|
2127
2202
|
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2203
|
+
|
|
2204
|
+
try:
|
|
2205
|
+
template_selection = _template_selection(
|
|
2206
|
+
args.template_id, args.template_source, args.template_candidates,
|
|
2207
|
+
)
|
|
2208
|
+
except TemplateSelectionError as exc:
|
|
2209
|
+
print(f"❌ {exc}", file=sys.stderr)
|
|
2210
|
+
sys.exit(1)
|
|
2211
|
+
if args.template_id and template_selection is None:
|
|
2212
|
+
print(
|
|
2213
|
+
"ℹ️ --template-source not given: how this template was chosen is not recorded. "
|
|
2214
|
+
"If the user did not name it, stop and let them confirm it before writing the script.",
|
|
2215
|
+
file=sys.stderr,
|
|
2216
|
+
)
|
|
2217
|
+
|
|
2218
|
+
|
|
2219
|
+
|
|
2220
|
+
|
|
2221
|
+
|
|
2132
2222
|
resolved_ratio = args.ratio
|
|
2133
2223
|
if resolved_ratio is None:
|
|
2134
2224
|
if args.template_id:
|
|
@@ -2158,8 +2248,8 @@ Examples:
|
|
|
2158
2248
|
if args.speed is not None:
|
|
2159
2249
|
print(f" narration speed: {args.speed}x", file=sys.stderr)
|
|
2160
2250
|
|
|
2161
|
-
|
|
2162
|
-
|
|
2251
|
+
|
|
2252
|
+
|
|
2163
2253
|
stub_image_url = args.stub_image_url
|
|
2164
2254
|
stub_video_url = args.stub_video_url
|
|
2165
2255
|
if stub_image_url is None:
|
|
@@ -2173,11 +2263,11 @@ Examples:
|
|
|
2173
2263
|
print(f"⚠️ STUB_VIDEO_URL env var detected ({env_v}); using it as the video stub. Prefer passing --stub-video-url explicitly, or unset the env var.", file=sys.stderr)
|
|
2174
2264
|
stub_video_url = env_v
|
|
2175
2265
|
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2266
|
+
|
|
2267
|
+
|
|
2268
|
+
|
|
2269
|
+
|
|
2270
|
+
|
|
2181
2271
|
if args.skip_asset_generation:
|
|
2182
2272
|
if not stub_image_url:
|
|
2183
2273
|
stub_image_url = "https://placeholder.local/stub.png"
|
|
@@ -2215,9 +2305,9 @@ Examples:
|
|
|
2215
2305
|
source_document_url=args.source_document_url,
|
|
2216
2306
|
)
|
|
2217
2307
|
except ValueError as exc:
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2308
|
+
|
|
2309
|
+
|
|
2310
|
+
|
|
2221
2311
|
parser.error(str(exc))
|
|
2222
2312
|
|
|
2223
2313
|
try:
|
|
@@ -2231,19 +2321,22 @@ Examples:
|
|
|
2231
2321
|
except ValueError as exc:
|
|
2232
2322
|
parser.error(str(exc))
|
|
2233
2323
|
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2324
|
+
if template_selection:
|
|
2325
|
+
dsl.setdefault("meta", {})["templateSelection"] = template_selection
|
|
2326
|
+
|
|
2327
|
+
|
|
2328
|
+
|
|
2329
|
+
|
|
2237
2330
|
if args.skip_asset_generation:
|
|
2238
2331
|
_AUDIO_STUB = "https://placeholder.local/stub.mp3"
|
|
2239
|
-
_VIDEO_STUB = stub_video_url
|
|
2332
|
+
_VIDEO_STUB = stub_video_url
|
|
2240
2333
|
for asset in dsl.get("assets", []):
|
|
2241
2334
|
src = asset.get("source")
|
|
2242
2335
|
if src in ("gen-voice",):
|
|
2243
2336
|
asset["source"] = "existing"
|
|
2244
2337
|
asset["status"] = "generated"
|
|
2245
2338
|
asset["url"] = _AUDIO_STUB
|
|
2246
|
-
|
|
2339
|
+
|
|
2247
2340
|
elif src in ("gen-digital-human",):
|
|
2248
2341
|
asset["source"] = "existing"
|
|
2249
2342
|
asset["status"] = "generated"
|
|
@@ -2256,7 +2349,7 @@ Examples:
|
|
|
2256
2349
|
print(f" - {err}", file=sys.stderr)
|
|
2257
2350
|
sys.exit(1)
|
|
2258
2351
|
|
|
2259
|
-
|
|
2352
|
+
|
|
2260
2353
|
expected_language = dsl.get("meta", {}).get("outputLanguage", DEFAULT_LANGUAGE)
|
|
2261
2354
|
if expected_language in ("en", "zh"):
|
|
2262
2355
|
narrations_for_check = [
|
|
@@ -2279,8 +2372,8 @@ if __name__ == "__main__":
|
|
|
2279
2372
|
try:
|
|
2280
2373
|
main()
|
|
2281
2374
|
except TemplateStatusGatedError as exc:
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2375
|
+
|
|
2376
|
+
|
|
2377
|
+
|
|
2285
2378
|
print(f"❌ {exc}", file=sys.stderr)
|
|
2286
2379
|
sys.exit(1)
|