@remixmate/cli 0.9.6 → 0.9.7

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.
Files changed (40) hide show
  1. package/README.md +23 -0
  2. package/dist/capabilities.d.ts +56 -0
  3. package/dist/capabilities.js +75 -0
  4. package/dist/handlers/gen-digital-human.js +7 -7
  5. package/dist/handlers/gen-image.d.ts +12 -6
  6. package/dist/handlers/gen-image.js +35 -47
  7. package/dist/handlers/gen-video.d.ts +7 -8
  8. package/dist/handlers/gen-video.js +41 -65
  9. package/dist/handlers/gen-voice.d.ts +1 -1
  10. package/dist/handlers/gen-voice.js +9 -7
  11. package/dist/http.d.ts +2 -2
  12. package/dist/http.js +3 -3
  13. package/dist/manifest.json +2 -2
  14. package/package.json +7 -1
  15. package/skills/export-jianying/SKILL.md +1 -1
  16. package/skills/export-jianying/scripts/gen_jianying_draft.py +2 -2
  17. package/skills/gen-digital-human/SKILL.md +1 -1
  18. package/skills/gen-image/SKILL.md +1 -1
  19. package/skills/gen-video/SKILL.md +1 -1
  20. package/skills/gen-voice/SKILL.md +3 -3
  21. package/skills/gen-voice/version.json +1 -1
  22. package/skills/prepare-video-assets/SKILL.md +2 -2
  23. package/skills/render-video/SKILL.md +4 -4
  24. package/skills/render-video/scripts/_video_probe.py +4 -1
  25. package/skills/render-video/scripts/_vod_polling.py +1 -1
  26. package/skills/render-video/scripts/remote_renderer_client.py +3 -3
  27. package/skills/render-video/scripts/render_video.py +6 -1
  28. package/skills/render-video/scripts/upload_video.py +3 -3
  29. package/skills/template-registry/README.md +1 -1
  30. package/skills/template-registry/SKILL.md +3 -3
  31. package/skills/template-registry/scripts/check_contracts.py +8 -25
  32. package/skills/template-registry/scripts/match_template.py +100 -28
  33. package/skills/template-registry/scripts/registry_loader.py +3 -3
  34. package/skills/template-registry/scripts/render_job_client.py +2 -2
  35. package/skills/template-registry/scripts/verify_props_contract.py +273 -0
  36. package/skills/template-registry/video_dsl/README.md +0 -1
  37. package/skills/video-parser/SKILL.md +1 -1
  38. package/skills/video-parser/scripts/deconstruct_video.py +2 -2
  39. package/skills/video-parser/scripts/parse_via_render.py +2 -2
  40. package/skills/template-registry/video_dsl/schema/template-definition-v1alpha1.json +0 -247
@@ -0,0 +1,273 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ 脆弱点 6②:propExtractors / requiredProps ↔ 组件实际消费 props 的**双向对账**。
4
+
5
+ 不同于纯静态「consumed - providable」对账(噪声过大:分不清可选/必填、textLayers
6
+ 派生、同名变量污染),本校验用**三个无误报的稳健信号**做对账,且复用 render-video
7
+ 的真实 `build_binding`(而非重写绑定逻辑),保证与运行时一致:
8
+
9
+ A. requiredProps ⊆ produced
10
+ 用真实 dsl-example.json 跑 build_binding,断言每个 slot 声明的 requiredProps
11
+ 都真的进了 props。缺失 = 渲染前会回退通用布局(脆弱点 2 的 slideId 事故类)。
12
+ —— ERROR
13
+
14
+ B. requiredProps ⊆ typeFields
15
+ slot 声明 required 的 prop,组件 props 类型(entry.props as <T>,<T> 定义在
16
+ types.ts)里必须有该字段;否则 = 契约自相矛盾(声明必填却无处接收)。
17
+ —— ERROR
18
+
19
+ C. propExtractors keys ⊆ typeFields
20
+ 提取器产出的每个 prop,目标组件类型必须声明;否则 = 死字段 / 漂移(提取器辛苦
21
+ 算出来却没人读,或类型重命名后提取器没跟)。
22
+ —— WARN
23
+
24
+ 为什么这三个无误报:它们只用「类型声明(含 ?可选信息)」+「requiredProps 显式契约」
25
+ +「真实绑定产出」,完全不猜组件源码里的 `props.x` 成员访问(那才是噪声源,且 tsc
26
+ 已覆盖「读了类型里没有的字段」)。
27
+
28
+ 与 test-template-pipeline.py 的分工:后者通过 ab-api registry 跑 build_binding,已
29
+ 覆盖 A(missingRequiredProps),但**离线 CI 跑不了**(registry_loader 需 ab-api);
30
+ 本模块直接从磁盘读 template.json + types.ts,**无需 ab-api**,且额外提供类型级的
31
+ B/C 对账(别处都没有)。由 scripts/test-props-contract.py 作为 CI 入口驱动。
32
+
33
+ 库接口:
34
+ verify_template(template_dir) -> {name, errors[], warnings[], info{}}
35
+ """
36
+
37
+ from __future__ import annotations
38
+
39
+ import json
40
+ import os
41
+ import re
42
+ import sys
43
+
44
+ # 复用真实绑定逻辑(同目录 sibling 模块)。registry_loader 无 import 期副作用,
45
+ # 不触发 ab-api,故可安全 import。
46
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
47
+ from match_template import build_binding # noqa: E402
48
+
49
+
50
+ CAST_RE = re.compile(r"entry\.props\s+as\s+([A-Za-z_$][\w$]*)")
51
+
52
+
53
+ def parse_interface_fields(types_src: str) -> dict:
54
+ """从 types.ts 解析 `export interface Name { ... }` → {Name: {field: optional_bool}}.
55
+
56
+ 只解析顶层 interface 的顶层成员(足够覆盖模板 props 类型——它们都是扁平接口)。
57
+ 嵌套对象类型按整体跳过(不影响顶层字段名收集)。
58
+ """
59
+ result: dict = {}
60
+ i = 0
61
+ n = len(types_src)
62
+ iface_re = re.compile(r"export\s+interface\s+([A-Za-z_$][\w$]*)\s*\{")
63
+ for m in iface_re.finditer(types_src):
64
+ name = m.group(1)
65
+ # 从 `{` 起做花括号配平,截取 body
66
+ start = m.end() - 1
67
+ depth = 0
68
+ j = start
69
+ while j < n:
70
+ c = types_src[j]
71
+ if c == "{":
72
+ depth += 1
73
+ elif c == "}":
74
+ depth -= 1
75
+ if depth == 0:
76
+ break
77
+ j += 1
78
+ body = types_src[start + 1 : j]
79
+ result[name] = _parse_members(body)
80
+ return result
81
+
82
+
83
+ def _parse_members(body: str) -> dict:
84
+ """解析接口 body 顶层成员名 + 是否可选。嵌套 { } 整体跳过。"""
85
+ fields: dict = {}
86
+ i = 0
87
+ n = len(body)
88
+ # 移除注释,避免把注释里的标识符当成员
89
+ body = re.sub(r"/\*.*?\*/", "", body, flags=re.S)
90
+ body = re.sub(r"//[^\n]*", "", body)
91
+ n = len(body)
92
+ # 按顶层切分:在 depth==0 处遇到 `;` 或 `\n` 视为成员边界
93
+ member_re = re.compile(r"([A-Za-z_$][\w$]*)\s*(\??)\s*:")
94
+ depth = 0
95
+ seg_start = 0
96
+ segments = []
97
+ for k, c in enumerate(body):
98
+ if c in "{([":
99
+ depth += 1
100
+ elif c in "})]":
101
+ depth -= 1
102
+ elif c in ";\n" and depth == 0:
103
+ segments.append(body[seg_start:k])
104
+ seg_start = k + 1
105
+ segments.append(body[seg_start:])
106
+ for seg in segments:
107
+ seg = seg.strip()
108
+ if not seg:
109
+ continue
110
+ mm = member_re.match(seg)
111
+ if mm:
112
+ fields[mm.group(1)] = mm.group(2) == "?"
113
+ return fields
114
+
115
+
116
+ def resolve_slot_def(slot_mapping: dict, purpose: str) -> dict:
117
+ slot_def = slot_mapping.get(purpose) or slot_mapping.get("default") or {}
118
+ visited = set()
119
+ while isinstance(slot_def, dict) and "$ref" in slot_def:
120
+ ref = slot_def["$ref"]
121
+ if ref in visited:
122
+ break
123
+ visited.add(ref)
124
+ slot_def = slot_mapping.get(ref) or {}
125
+ return slot_def
126
+
127
+
128
+ def verify_template(tdir: str) -> dict:
129
+ """对单个模板目录做 A/B/C 对账,返回 {errors:[], warnings:[], info:{}}。"""
130
+ name = os.path.basename(tdir.rstrip("/"))
131
+ errors: list = []
132
+ warnings: list = []
133
+ info: dict = {}
134
+
135
+ tpl_path = os.path.join(tdir, "template.json")
136
+ with open(tpl_path, "r", encoding="utf-8") as f:
137
+ tpl = json.load(f)
138
+
139
+ slot_mapping = tpl.get("slotMapping", {}) or {}
140
+ compositions = tpl.get("compositions", []) or []
141
+
142
+ # compositionId → componentFile
143
+ comp_file = {c.get("compositionId"): c.get("componentFile") for c in compositions}
144
+
145
+ # componentFile → cast 类型名(entry.props as <T>)
146
+ file_cast: dict = {}
147
+ for cf in set(comp_file.values()):
148
+ if not cf:
149
+ continue
150
+ src_path = os.path.normpath(os.path.join(tdir, cf))
151
+ if not os.path.exists(src_path):
152
+ warnings.append(f"组件文件不存在:{cf}")
153
+ continue
154
+ with open(src_path, "r", encoding="utf-8") as f:
155
+ src = f.read()
156
+ casts = CAST_RE.findall(src)
157
+ file_cast[cf] = casts[0] if casts else None
158
+ if not casts:
159
+ warnings.append(f"{cf} 未找到 `entry.props as <Type>`,跳过类型对账")
160
+
161
+ # 收集 types.ts(含同目录所有 .ts 里的 interface,稳妥起见全扫)
162
+ type_fields: dict = {}
163
+ for root, _dirs, files in os.walk(tdir):
164
+ for fn in files:
165
+ if fn.endswith(".ts") and not fn.endswith(".d.ts"):
166
+ try:
167
+ with open(os.path.join(root, fn), "r", encoding="utf-8") as f:
168
+ type_fields.update(parse_interface_fields(f.read()))
169
+ except Exception:
170
+ pass
171
+
172
+ # compositionId → 类型字段集合
173
+ def fields_for_comp(comp_id: str):
174
+ cf = comp_file.get(comp_id)
175
+ cast = file_cast.get(cf)
176
+ if cast and cast in type_fields:
177
+ return set(type_fields[cast].keys()), cast
178
+ return None, cast
179
+
180
+ # ---- B & C:基于 slot 声明(不依赖 dsl-example)----
181
+ seen_slots = set()
182
+ for purpose in list(slot_mapping.keys()):
183
+ sd = resolve_slot_def(slot_mapping, purpose)
184
+ comp_id = sd.get("compositionId")
185
+ if not comp_id or comp_id in seen_slots:
186
+ continue
187
+ seen_slots.add(comp_id)
188
+ tfields, cast = fields_for_comp(comp_id)
189
+ required = sd.get("requiredProps") or []
190
+ extractor_keys = list((sd.get("propExtractors") or {}).keys())
191
+
192
+ if tfields is None:
193
+ if required or extractor_keys:
194
+ warnings.append(
195
+ f"slot→{comp_id}: 无法解析组件 props 类型({cast}),B/C 跳过"
196
+ )
197
+ continue
198
+
199
+ # B: requiredProps ⊆ typeFields
200
+ for rp in required:
201
+ if rp not in tfields:
202
+ errors.append(
203
+ f"[B] slot→{comp_id}: requiredProps 含 '{rp}',但组件类型 {cast} 未声明该字段"
204
+ )
205
+ # C: propExtractors keys ⊆ typeFields
206
+ for ek in extractor_keys:
207
+ if ek not in tfields:
208
+ warnings.append(
209
+ f"[C] slot→{comp_id}: propExtractors 产出 '{ek}',但组件类型 {cast} 未声明 → 死字段/漂移"
210
+ )
211
+
212
+ # ---- A:用真实 dsl-example 跑 build_binding,requiredProps ⊆ produced ----
213
+ ex_path = os.path.join(tdir, "dsl-example.json")
214
+ if os.path.exists(ex_path):
215
+ with open(ex_path, "r", encoding="utf-8") as f:
216
+ dsl = json.load(f)
217
+ binding = build_binding(tpl, dsl)
218
+ miss_rows = [b for b in binding.get("bindings", []) if b.get("missingRequiredProps")]
219
+ for b in miss_rows:
220
+ errors.append(
221
+ f"[A] scene={b['sceneId']} slot={b['slotId']}: "
222
+ f"requiredProps 缺失 {b['missingRequiredProps']}(dsl-example 跑真实绑定后仍未产出)"
223
+ )
224
+ info["scenes_bound"] = len(binding.get("bindings", []))
225
+ else:
226
+ warnings.append("缺 dsl-example.json,A(运行时绑定对账)跳过")
227
+
228
+ return {"name": name, "errors": errors, "warnings": warnings, "info": info}
229
+
230
+
231
+ def verify_all(base: str) -> int:
232
+ """对 base/*/template.json 逐个对账,打印报告,返回 error 总数。"""
233
+ dirs = sorted(
234
+ os.path.join(base, d)
235
+ for d in os.listdir(base)
236
+ if os.path.isdir(os.path.join(base, d))
237
+ and os.path.exists(os.path.join(base, d, "template.json"))
238
+ )
239
+ if not dirs:
240
+ print(f"未发现模板({base}/*/template.json)", file=sys.stderr)
241
+ return 0
242
+
243
+ total_err = 0
244
+ total_warn = 0
245
+ for tdir in dirs:
246
+ r = verify_template(tdir)
247
+ status = "FAIL" if r["errors"] else ("WARN" if r["warnings"] else "PASS")
248
+ scenes = r["info"].get("scenes_bound", "-")
249
+ print(f"\n=== {r['name']} [{status}] (scenes_bound={scenes}) ===")
250
+ for e in r["errors"]:
251
+ print(f" ERROR {e}")
252
+ for w in r["warnings"]:
253
+ print(f" WARN {w}")
254
+ if not r["errors"] and not r["warnings"]:
255
+ print(" ✓ A/B/C 全部通过")
256
+ total_err += len(r["errors"])
257
+ total_warn += len(r["warnings"])
258
+
259
+ print(f"\n—— 汇总:{len(dirs)} 模板,{total_err} error,{total_warn} warning ——")
260
+ return total_err
261
+
262
+
263
+ if __name__ == "__main__":
264
+ import argparse
265
+
266
+ ap = argparse.ArgumentParser(description="脆弱点 6② props 双向对账(A/B/C)")
267
+ ap.add_argument("--templates-dir", required=True, help="template-library/packages/templates/src")
268
+ args = ap.parse_args()
269
+ base = os.path.abspath(args.templates_dir)
270
+ if not os.path.isdir(base):
271
+ print(f"templates-dir 不存在: {base}", file=sys.stderr)
272
+ raise SystemExit(2)
273
+ raise SystemExit(1 if verify_all(base) else 0)
@@ -8,7 +8,6 @@
8
8
  video_dsl/
9
9
  ├── schema/ # DSL Schema 定义
10
10
  │ ├── video-dsl-v1alpha1.json # Video DSL JSON Schema
11
- │ ├── template-definition-v1alpha1.json # 模板定义 Schema
12
11
  │ ├── template-binding-v1alpha1.json # 模板绑定 Schema
13
12
  │ ├── render-plan-v1alpha1.json # RenderPlan Schema
14
13
  │ └── examples/ # 各模板的 DSL + Binding 示例
@@ -30,7 +30,7 @@ No skill-local env file — the executing process inherits the system environmen
30
30
 
31
31
  | Env var | Description | Default |
32
32
  |---------|-------------|---------|
33
- | `RENDER_API_URL` | ab-render service base URL (e.g. `http://localhost:3000`). Required. | (none) |
33
+ | `RENDER_API_URL` | ab-render service base URL (e.g. `https://api-render.remixmate.com`). Required. | (none) |
34
34
  | `PRIV_TOKEN` | Tianyan token, sent as `X-Priv-Token`. | (none) |
35
35
  | `CONVERSATION_ID` | Optional, sent as `x-conversation-id` for file association. | (none) |
36
36
 
@@ -13,7 +13,7 @@
13
13
  python deconstruct_video.py --url "https://example.com/video.mp4" --json-output
14
14
 
15
15
  环境变量:
16
- MM_API_BASE_URL - 后端 API 地址(默认: http://localhost:3001/api)
16
+ MM_API_BASE_URL - 后端 API 地址(默认: https://api-agent.remixmate.com/api)
17
17
  PRIV_TOKEN - PrivToken 认证令牌
18
18
  """
19
19
 
@@ -27,7 +27,7 @@ import time
27
27
  import urllib.error
28
28
  import urllib.request
29
29
 
30
- API_BASE_URL = os.environ.get("MM_API_BASE_URL", "http://localhost:3001/api")
30
+ API_BASE_URL = os.environ.get("MM_API_BASE_URL", "https://api-agent.remixmate.com/api")
31
31
 
32
32
  PRIVATE_TOKEN = "" # 在 main() 中通过 resolve_token() 初始化
33
33
  SKILL_NAME = "video-parser"
@@ -7,7 +7,7 @@
7
7
  POST $RENDER_API_URL/parseStatus → { status, progress, result?, error? }
8
8
 
9
9
  必要环境变量(由 ab-agent 自动注入):
10
- RENDER_API_URL ab-render 服务地址,例如 http://localhost:3000
10
+ RENDER_API_URL ab-render 服务地址(默认 https://api-render.remixmate.com;指向本地/staging 时覆盖)
11
11
  PRIV_TOKEN ab-api 私有 token
12
12
 
13
13
  可选环境变量:
@@ -24,7 +24,7 @@ import urllib.request
24
24
 
25
25
  # ─── 环境变量 ─────────────────────────────────────────────────────────────────
26
26
 
27
- RENDER_API_URL = os.environ.get("RENDER_API_URL", "").rstrip("/")
27
+ RENDER_API_URL = os.environ.get("RENDER_API_URL", "https://api-render.remixmate.com").rstrip("/")
28
28
  PRIV_TOKEN = os.environ.get("PRIV_TOKEN", "")
29
29
  CONVERSATION_ID = os.environ.get("CONVERSATION_ID", "")
30
30
 
@@ -1,247 +0,0 @@
1
- {
2
- "$schema": "http://json-schema.org/draft-07/schema#",
3
- "$id": "video.dsl/template-definition/v1alpha1",
4
- "title": "Template Definition v1alpha1",
5
- "description": "视频模板定义规范,描述模板的匹配条件、slot 绑定规则、默认样式和扩展配置。",
6
- "type": "object",
7
- "required": ["templateId", "name", "version", "slotMapping", "remotionEntry"],
8
- "properties": {
9
- "templateId": {
10
- "type": "string",
11
- "pattern": "^[a-z][a-z0-9-]*$",
12
- "description": "模板唯一标识,kebab-case"
13
- },
14
- "name": {
15
- "type": "string",
16
- "description": "模板显示名称"
17
- },
18
- "description": {
19
- "type": "string",
20
- "description": "模板用途说明"
21
- },
22
- "version": {
23
- "type": "string",
24
- "pattern": "^\\d+\\.\\d+\\.\\d+$",
25
- "description": "语义化版本号"
26
- },
27
- "supportedAspectRatios": {
28
- "type": "array",
29
- "items": {
30
- "type": "string",
31
- "enum": ["16:9", "9:16", "1:1", "4:3", "3:4", "21:9"]
32
- },
33
- "minItems": 1,
34
- "description": "支持的宽高比列表"
35
- },
36
- "supportedDurations": {
37
- "type": "object",
38
- "properties": {
39
- "min": { "type": "number", "minimum": 0, "description": "最短时长(秒)" },
40
- "max": { "type": "number", "minimum": 1, "description": "最长时长(秒)" }
41
- },
42
- "required": ["min", "max"]
43
- },
44
- "styleTags": {
45
- "type": "array",
46
- "items": { "type": "string" },
47
- "description": "风格标签,用于匹配 DSL meta.style"
48
- },
49
- "scenePatterns": {
50
- "type": "array",
51
- "items": {
52
- "type": "string",
53
- "enum": ["opening", "intro", "point", "example", "explanation", "transition", "highlight", "cta", "ending", "default"]
54
- },
55
- "description": "模板支持的场景 purpose 类型"
56
- },
57
- "assetRequirements": {
58
- "type": "array",
59
- "items": {
60
- "type": "string",
61
- "enum": ["image", "video", "audio", "avatar", "subtitle", "bgm"]
62
- },
63
- "description": "模板需要的素材类型"
64
- },
65
- "slotMapping": {
66
- "type": "object",
67
- "description": "purpose → slot 绑定规则映射,必须包含 default。值可以是完整 SlotDefinition 或 {\"$ref\": \"<purpose>\"} 别名引用另一个 purpose 的定义。",
68
- "additionalProperties": {
69
- "oneOf": [
70
- { "$ref": "#/definitions/SlotDefinition" },
71
- { "$ref": "#/definitions/SlotRef" }
72
- ]
73
- },
74
- "required": ["default"]
75
- },
76
- "defaultTypography": { "$ref": "#/definitions/Typography" },
77
- "defaultMotionPreset": {
78
- "type": "string",
79
- "enum": ["minimal", "smooth", "energetic", "cinematic"],
80
- "default": "smooth"
81
- },
82
- "defaultColorScheme": {
83
- "type": "array",
84
- "items": { "type": "string" },
85
- "description": "默认配色方案 HEX 值列表"
86
- },
87
- "remotionEntry": {
88
- "type": "object",
89
- "description": "按宽高比映射 Remotion 根 Composition ID",
90
- "additionalProperties": { "type": "string" }
91
- },
92
- "themeConfig": {
93
- "type": "object",
94
- "description": "模板专属主题配置,透传给 Remotion Composition",
95
- "additionalProperties": true
96
- },
97
- "imageStyleGuide": {
98
- "$ref": "#/definitions/ImageStyleGuide",
99
- "description": "图片生成视觉风格指南,用于增强自动生成的 image prompt"
100
- },
101
- "constraints": {
102
- "type": "object",
103
- "description": "模板级约束条件",
104
- "additionalProperties": true
105
- },
106
- "extensions": {
107
- "type": "object",
108
- "description": "模板专属扩展类型声明(如 slideBlockTypes)",
109
- "additionalProperties": true
110
- },
111
- "compositionMap": {
112
- "type": "object",
113
- "description": "(已废弃)旧版 purpose → compositionId 映射,请迁移到 slotMapping",
114
- "additionalProperties": { "type": "string" }
115
- },
116
- "slotSchema": {
117
- "type": "object",
118
- "description": "(已废弃)旧版 slot 描述,请迁移到 slotMapping",
119
- "additionalProperties": true
120
- }
121
- },
122
- "definitions": {
123
- "SlotRef": {
124
- "type": "object",
125
- "description": "引用另一个 purpose 的 SlotDefinition,消除重复定义",
126
- "required": ["$ref"],
127
- "properties": {
128
- "$ref": {
129
- "type": "string",
130
- "description": "目标 purpose 名称,如 \"point\" 或 \"cta\""
131
- }
132
- },
133
- "additionalProperties": false
134
- },
135
- "SlotDefinition": {
136
- "type": "object",
137
- "required": ["slotId", "compositionId"],
138
- "properties": {
139
- "slotId": {
140
- "type": "string",
141
- "description": "模板 slot 标识"
142
- },
143
- "compositionId": {
144
- "type": "string",
145
- "description": "对应 Remotion Composition ID"
146
- },
147
- "description": {
148
- "type": "string"
149
- },
150
- "propExtractors": {
151
- "type": "object",
152
- "description": "props 提取规则:propName → 提取配置",
153
- "additionalProperties": { "$ref": "#/definitions/PropExtractor" }
154
- },
155
- "requiredProps": {
156
- "type": "array",
157
- "items": { "type": "string" },
158
- "description": "必需的 props"
159
- },
160
- "optionalProps": {
161
- "type": "array",
162
- "items": { "type": "string" },
163
- "description": "可选的 props"
164
- }
165
- }
166
- },
167
- "PropExtractor": {
168
- "oneOf": [
169
- {
170
- "type": "object",
171
- "description": "从 textLayers 按 role 提取",
172
- "required": ["from", "role"],
173
- "properties": {
174
- "from": { "type": "string", "const": "textLayers" },
175
- "role": { "type": "string" }
176
- }
177
- },
178
- {
179
- "type": "object",
180
- "description": "从 scene 按点分路径提取",
181
- "required": ["from"],
182
- "properties": {
183
- "from": { "type": "string", "description": "点分路径,如 visuals.background.assetRef" }
184
- }
185
- }
186
- ]
187
- },
188
- "Typography": {
189
- "type": "object",
190
- "properties": {
191
- "titleFont": { "type": "string" },
192
- "bodyFont": { "type": "string" },
193
- "titleSize": { "type": "integer" },
194
- "bodySize": { "type": "integer" },
195
- "primaryColor": { "type": "string" },
196
- "secondaryColor": { "type": "string" }
197
- }
198
- },
199
- "ImageStyleGuide": {
200
- "type": "object",
201
- "description": "图片生成视觉风格指南",
202
- "properties": {
203
- "baseStyle": {
204
- "type": "string",
205
- "description": "基础视觉风格描述,追加到所有 prompt 末尾"
206
- },
207
- "colorDirective": {
208
- "type": "string",
209
- "description": "色彩方向指令(文字描述,非 HEX)"
210
- },
211
- "negativePrompt": {
212
- "type": "string",
213
- "description": "全局负向提示词,描述不希望出现的元素"
214
- },
215
- "guidanceScale": {
216
- "type": "number",
217
- "description": "推荐引导系数(如 7.5),控制生成图片对 prompt 的遵循程度"
218
- },
219
- "purposeOverrides": {
220
- "type": "object",
221
- "description": "按场景 purpose 覆盖的风格配置",
222
- "additionalProperties": {
223
- "$ref": "#/definitions/PurposeStyleOverride"
224
- }
225
- }
226
- }
227
- },
228
- "PurposeStyleOverride": {
229
- "type": "object",
230
- "description": "针对特定 purpose 的风格覆盖",
231
- "properties": {
232
- "promptTemplate": {
233
- "type": "string",
234
- "description": "prompt 模板,支持 {narration_summary} {topic} {style_suffix} {scene_title} 变量"
235
- },
236
- "styleModifier": {
237
- "type": "string",
238
- "description": "追加的风格修饰符"
239
- },
240
- "negativePrompt": {
241
- "type": "string",
242
- "description": "覆盖全局的负向提示词"
243
- }
244
- }
245
- }
246
- }
247
- }