@dsh-gis/dsh-video-studio-skill 0.4.0 → 0.7.2

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 (38) hide show
  1. package/README.md +93 -18
  2. package/docs/styles/demo-results.json +76 -0
  3. package/docs/styles/sheet-8-styles.png +0 -0
  4. package/docs/styles/sheet-ai.png +0 -0
  5. package/docs/styles/sheet-craft.png +0 -0
  6. package/docs/styles/sheet-night.png +0 -0
  7. package/index.js +20 -7
  8. package/package.json +13 -3
  9. package/schemas/film.schema.json +599 -0
  10. package/skills/video-studio/SKILL.md +252 -28
  11. package/skills/video-studio/assets/film.json +78 -24
  12. package/skills/video-studio/assets/scenes.py +1005 -42
  13. package/skills/video-studio/assets/web/engine.js +18 -1
  14. package/skills/video-studio/assets/web/film.js +29 -4
  15. package/skills/video-studio/assets/web/index.html +5 -0
  16. package/skills/video-studio/references/audio-variation.md +158 -0
  17. package/skills/video-studio/references/audio.md +96 -6
  18. package/skills/video-studio/references/change-impact.md +54 -0
  19. package/skills/video-studio/references/quality-gates.md +1 -1
  20. package/skills/video-studio/references/style-packs.md +178 -0
  21. package/skills/video-studio/references/web-engine.md +1 -1
  22. package/skills/video-studio/requirements.txt +13 -1
  23. package/skills/video-studio/scripts/filmcheck.py +725 -0
  24. package/skills/video-studio/scripts/filmkit.py +953 -76
  25. package/skills/video-studio/scripts/gates.py +509 -0
  26. package/skills/video-studio/scripts/qa_frames.py +109 -19
  27. package/skills/video-studio/scripts/render.py +942 -45
  28. package/skills/video-studio/scripts/scorekit.py +1887 -27
  29. package/skills/video-studio/scripts/selfcheck.py +648 -2
  30. package/skills/video-studio/scripts/sfxkit.py +132 -30
  31. package/skills/video-studio/scripts/stagewatch.py +405 -0
  32. package/skills/video-studio/scripts/stylekit.py +1012 -0
  33. package/skills/video-studio/scripts/voicekit.py +880 -0
  34. package/skills/video-studio/scripts/webengine.py +68 -14
  35. package/tools/audio_similarity.py +279 -0
  36. package/tools/ci_check.py +450 -0
  37. package/tools/estimate.py +657 -0
  38. package/tools/selftest_noscipy.py +70 -0
@@ -0,0 +1,599 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://github.com/znlgis/dsh-video-studio-skill/schemas/film.schema.json",
4
+ "title": "video-studio film.json(剧本契约)",
5
+ "description": "video-studio 的剧本契约(唯一内容真源)。这是 JSON Schema draft-07 的**子集**:\n只用到 type / enum / required / properties / additionalProperties / items / minimum / maximum / pattern。\n\n为什么不写死 additionalProperties:false:draft-07 子集里没有 patternProperties,\n表达不了“下划线开头的扩展键豁免”这条规则,硬写 false 会把 _comment 这类注释键判死。\n所以未知键**不在 schema 层判死**,而是由 skills/video-studio/scripts/filmcheck.py 报 WARN\n(并给出“是不是想写 X”的建议)——schema 负责文档与 IDE 补全,filmcheck 负责执行。\n\n与 render.py 的 load_script() 的关系:load_script 是权威硬校验(title/lines 必填、lines 非空、\n每条有 text、id 不重复、bpm >0 或 auto)。本 schema 是它的补强(枚举/范围/嵌套键),不与之矛盾;\n两者都允许的脚本才能跑通。范围类约束以本文件为准,跨字段约束(cue 的 at 引用、hold 与 timeline\n对账)只有 filmcheck.py 能查。",
6
+ "type": "object",
7
+ "required": [
8
+ "title",
9
+ "lines"
10
+ ],
11
+ "properties": {
12
+ "_comment": {
13
+ "type": "string",
14
+ "description": "下划线开头的键是扩展位:schema 与 filmcheck 都不检查,渲染器也不读(模板用它写迁移说明)"
15
+ },
16
+ "title": {
17
+ "type": "string",
18
+ "description": "片名(必填):顶栏常驻,也参与风格/配乐种子指纹"
19
+ },
20
+ "end_text": {
21
+ "type": "string",
22
+ "description": "片尾结束字(模板写“完”);scenes.py 用来画收尾大字,可留空"
23
+ },
24
+ "style": {
25
+ "type": "string",
26
+ "enum": [
27
+ "auto",
28
+ "tech-deep",
29
+ "magazine",
30
+ "paper",
31
+ "vivid",
32
+ "mono",
33
+ "cinema",
34
+ "lo-fi",
35
+ "epic"
36
+ ],
37
+ "description": "风格包 id:一套决定画风+配乐+旁白+节奏。“auto”= 按内容自动选;不写这个键 = v0.4 legacy 行为(逐样本兼容)。枚举真值来自 stylekit.STYLES,新增风格时先改那边再改这里"
38
+ },
39
+ "engine": {
40
+ "type": "string",
41
+ "enum": [
42
+ "pil",
43
+ "web"
44
+ ],
45
+ "description": "渲染引擎:pil = Pillow 逐帧(默认,40–80 fps);web = 无头浏览器逐帧(80–130 ms/帧,需要 playwright + assets/web/)"
46
+ },
47
+ "voice": {
48
+ "type": [
49
+ "string",
50
+ "object"
51
+ ],
52
+ "description": "字符串 = edge-tts voice id(如 zh-CN-YunxiNeural,legacy 单声线路径);对象 = v0.5 声线策略(map 按 kind 映射 persona、auto、jitter、max_distinct、rate_range、cps_band),此时忽略 rate/pitch 的单值覆盖语义",
53
+ "properties": {
54
+ "map": {
55
+ "type": "object",
56
+ "description": "按 kind 映射 persona:{\"hook\": \"young_male\", \"body\": \"narrator_male\", \"quote\": \"narrator_female\", \"outro\": \"narrator_male\"}(键取 lines[].kind 的 6 个值;值取 voicekit.PERSONA_IDS)"
57
+ },
58
+ "default": {
59
+ "type": "string",
60
+ "description": "没有映射时用的 persona 名(voicekit.PERSONA_IDS 之一)"
61
+ },
62
+ "auto": {
63
+ "type": "boolean",
64
+ "description": "是否按内容自动挑 persona(默认 true;false = 只用 map/default)"
65
+ },
66
+ "jitter": {
67
+ "type": "number",
68
+ "minimum": 0,
69
+ "description": "逐句 rate/pitch/lead/tail 的确定性抖动幅度(默认 0.35;legacy 默认 0)"
70
+ },
71
+ "max_distinct": {
72
+ "type": "integer",
73
+ "minimum": 1,
74
+ "description": "全片最多用几个不同 persona(默认 3;相邻句不换人是防广播剧的关键)"
75
+ },
76
+ "rate_range": {
77
+ "type": "array",
78
+ "description": "语速夹取区间(百分数,例如 [-15, 12])",
79
+ "items": { "type": "number" }
80
+ },
81
+ "cps_band": {
82
+ "type": "array",
83
+ "description": "可接受的每秒字数区间(默认 [3.6, 5.6])",
84
+ "items": { "type": "number" }
85
+ }
86
+ }
87
+ },
88
+ "rate": {
89
+ "type": "string",
90
+ "pattern": "^\\s*[+-]?\\d+\\s*%\\s*$",
91
+ "description": "语速,如 \"+6%\" / \"-2%\"(edge-tts 只接受整数);超过 +15% 会有机械感"
92
+ },
93
+ "pitch": {
94
+ "type": "string",
95
+ "pattern": "^\\s*[+-]?\\d+\\s*Hz\\s*$",
96
+ "description": "音高,如 \"+0Hz\" / \"-2Hz\""
97
+ },
98
+ "bpm": {
99
+ "type": [
100
+ "number",
101
+ "string"
102
+ ],
103
+ "minimum": 0,
104
+ "description": "配乐 BPM:数字必须 > 0(这里是 ≥0 的宽松下限,0 与负数由 filmcheck 报 error);\"auto\" = 按风格 BPM 梯度 + 内容指纹决定。music.bpm 优先级更高"
105
+ },
106
+ "seed": {
107
+ "type": "integer",
108
+ "minimum": 0,
109
+ "description": "配乐/纹理种子(缺省 7);music.seed 优先"
110
+ },
111
+ "crf": {
112
+ "type": "integer",
113
+ "minimum": 1,
114
+ "maximum": 51,
115
+ "description": "x264 CRF:17 更锐利(体积约 +30%),18 是 filmkit 默认;范围 1–51"
116
+ },
117
+ "preset": {
118
+ "type": "string",
119
+ "enum": [
120
+ "ultrafast",
121
+ "superfast",
122
+ "veryfast",
123
+ "faster",
124
+ "fast",
125
+ "medium",
126
+ "slow",
127
+ "slower",
128
+ "veryslow"
129
+ ],
130
+ "description": "x264 preset:medium 是实测基准(141 fps @crf17);slow 更小更慢,veryfast 只适合预览"
131
+ },
132
+ "lines": {
133
+ "type": "array",
134
+ "description": "台词数组(必填且非空):一句一个对象,顺序即播放顺序。id 缺省时 render.py 自动生成 l01、l02…;sfx.cues 的 at 用同一个 id 定位",
135
+ "items": {
136
+ "type": "object",
137
+ "required": [
138
+ "text"
139
+ ],
140
+ "additionalProperties": true,
141
+ "properties": {
142
+ "id": {
143
+ "type": "string",
144
+ "pattern": "^[A-Za-z0-9_.-]+$",
145
+ "description": "场景 id(TTS 文件与 cue 都靠它定位)。缺省 = l%02d;一旦写 sfx.cues 就显式给 id"
146
+ },
147
+ "kind": {
148
+ "type": "string",
149
+ "enum": [
150
+ "hook",
151
+ "body",
152
+ "quote",
153
+ "outro",
154
+ "emphasis",
155
+ "calm"
156
+ ],
157
+ "description": "段落类型:hook 开场 / body 正文 / quote 引用 / outro 收尾 / emphasis 强调 / calm 沉静。决定声线、语气、版式与配乐能量"
158
+ },
159
+ "text": {
160
+ "type": "string",
161
+ "description": "朗读文本(必填,不能为空):自然口语,TTS 念这一句"
162
+ },
163
+ "sub": {
164
+ "type": "string",
165
+ "description": "上屏字幕:短、一行优先(约 ≤24 个中文字);缺省沿用 text"
166
+ },
167
+ "big": {
168
+ "type": "string",
169
+ "description": "画面大字(标题/金句)"
170
+ },
171
+ "subtitle": {
172
+ "type": "string",
173
+ "description": "副标题(版式可用的第二行小字,可选)"
174
+ },
175
+ "chapter": {
176
+ "type": "string",
177
+ "description": "章节标签,如 \"02 · 怎么做到的\";同一章节内轮换版式,不会每句都跳"
178
+ },
179
+ "kicker": {
180
+ "type": "string",
181
+ "description": "小标签(如 \"PIPELINE\"),版式里的英文眉标"
182
+ },
183
+ "items": {
184
+ "type": "array",
185
+ "description": "要点列表(版式画成勾选卡片);items.py 只读 text/ok",
186
+ "items": {
187
+ "type": "object",
188
+ "required": [
189
+ "text"
190
+ ],
191
+ "additionalProperties": true,
192
+ "properties": {
193
+ "text": {
194
+ "type": "string",
195
+ "description": "要点文字"
196
+ },
197
+ "ok": {
198
+ "type": "boolean",
199
+ "description": "true = 勾(绿色),false = 叉(红色)"
200
+ }
201
+ }
202
+ }
203
+ },
204
+ "hold": {
205
+ "type": "number",
206
+ "minimum": 0,
207
+ "description": "语音结束后额外停留秒数(让观众看清);缺省用全局 hold / 风格 pacing"
208
+ },
209
+ "lead": {
210
+ "type": "number",
211
+ "minimum": 0,
212
+ "description": "句前留白秒数;覆盖全局 lead 与 voice_plan 的逐句 lead"
213
+ },
214
+ "tail": {
215
+ "type": "number",
216
+ "minimum": 0,
217
+ "description": "注意:**当前不生效**(保留字段)。filmkit.build_timeline 只读 tail_map,逐句句后留白请写顶层 tail_map,或交给 voicekit 的 voice_plan(st_layout 会把它的逐句 tail 并进 tail_map)。filmcheck 对写了 lines[].tail 的剧本会报 WARN 提醒。"
218
+ },
219
+ "persona": {
220
+ "type": "string",
221
+ "description": "显式声线 persona(voicekit.PERSONA_IDS:narrator_male / narrator_female / warm_female / bright_female / authority_male / young_male / teen_male / dialect_ne_female / dialect_sx_female)"
222
+ },
223
+ "tone": {
224
+ "type": "string",
225
+ "enum": [
226
+ "body",
227
+ "hook",
228
+ "quote",
229
+ "outro",
230
+ "emphasis",
231
+ "calm"
232
+ ],
233
+ "description": "显式语气(voicekit.VOICE_TONES 的键):覆盖 kind 规则"
234
+ },
235
+ "voice": {
236
+ "type": "string",
237
+ "description": "显式 edge-tts voice id(覆盖 persona 底子)"
238
+ }
239
+ }
240
+ }
241
+ },
242
+ "lead": {
243
+ "type": "number",
244
+ "minimum": 0,
245
+ "description": "全局句前留白(秒)。缺省取风格 pacing(v0.5 body 基准 0.62)"
246
+ },
247
+ "tail": {
248
+ "type": "number",
249
+ "minimum": 0,
250
+ "description": "全局句后留白(秒)。缺省取风格 pacing(v0.5 body 基准 0.92)"
251
+ },
252
+ "min_dur": {
253
+ "type": "number",
254
+ "minimum": 0,
255
+ "description": "每段最短时长(秒):语音太短时把画面撑住,缺省 3.0–4.0(按风格)"
256
+ },
257
+ "hold": {
258
+ "type": "number",
259
+ "minimum": 0,
260
+ "description": "全局停留秒数(语音结束后的额外停留),逐句 hold 优先"
261
+ },
262
+ "max_dur": {
263
+ "type": "number",
264
+ "minimum": 0,
265
+ "description": "每段最长时长(秒):超过就截断(会截断旁白,慎用)"
266
+ },
267
+ "bar": {
268
+ "type": "number",
269
+ "minimum": 0,
270
+ "description": "按小节对齐(秒):>0 时每段时长向上取整到整数倍。注意 render.py 目前未传这个值给 build_timeline(保留字段)"
271
+ },
272
+ "sub_max_hold": {
273
+ "type": [
274
+ "number",
275
+ "null"
276
+ ],
277
+ "description": "字幕最长停留秒数(null = 覆盖整句);拍的是“字幕跟读完”,不是“画面慢放”"
278
+ },
279
+ "trim_silence": {
280
+ "type": "boolean",
281
+ "description": "TTS 后是否裁掉首尾静音(默认 true;要保留 edge-tts 尾部 ~0.95 s 静音就设 false)"
282
+ },
283
+ "trim_db": {
284
+ "type": "number",
285
+ "description": "裁静音的门限 dBFS(默认 -42.0);太激进会把气声裁掉"
286
+ },
287
+ "tts_workers": {
288
+ "type": "integer",
289
+ "minimum": 1,
290
+ "description": "TTS 并发句数(默认 4;edge-tts 免费端点别开太大)"
291
+ },
292
+ "chunk_frames": {
293
+ "type": "integer",
294
+ "minimum": 0,
295
+ "description": "分块渲染的块大小(帧);0 = 按 frames/jobs/4 自动算。写死它会同时进分块计划指纹"
296
+ },
297
+ "reencode_video": {
298
+ "type": "boolean",
299
+ "description": "mux 时是否重编码视频(默认 false = -c:v copy,只改封装与音频)"
300
+ },
301
+ "voice_gain": {
302
+ "type": "number",
303
+ "description": "旁白总增益(混音侧线性倍数,默认 1.0)"
304
+ },
305
+ "whoosh": {
306
+ "type": "boolean",
307
+ "description": "v0.2 兼容:没有 cues.json 时每段开头一声 whoosh(默认 true)"
308
+ },
309
+ "whoosh_amp": {
310
+ "type": "number",
311
+ "minimum": 0,
312
+ "description": "旧版 whoosh 的幅度(默认 0.055–0.06)"
313
+ },
314
+ "tail_map": {
315
+ "type": "object",
316
+ "description": "按 lines id 覆盖句后留白:{\"l03\": 1.4}(逐句微调用)"
317
+ },
318
+ "pacing": {
319
+ "type": "object",
320
+ "additionalProperties": true,
321
+ "description": "节奏覆盖(优先于 lead/tail/min_dur/hold 字段,也优先于风格 pacing)",
322
+ "properties": {
323
+ "lead": {
324
+ "type": "number",
325
+ "minimum": 0,
326
+ "description": "句前留白"
327
+ },
328
+ "tail": {
329
+ "type": "number",
330
+ "minimum": 0,
331
+ "description": "句后留白"
332
+ },
333
+ "min_dur": {
334
+ "type": "number",
335
+ "minimum": 0,
336
+ "description": "最短段长"
337
+ },
338
+ "hold": {
339
+ "type": "number",
340
+ "minimum": 0,
341
+ "description": "全局停留"
342
+ },
343
+ "item_stagger": {
344
+ "type": "number",
345
+ "minimum": 0,
346
+ "description": "要点条目的入场步长(秒)"
347
+ }
348
+ }
349
+ },
350
+ "visual": {
351
+ "type": "object",
352
+ "additionalProperties": true,
353
+ "description": "视觉覆盖(v0.5):一般不用写,风格包已经决定;要锁主题/版式时才用",
354
+ "properties": {
355
+ "theme": {
356
+ "type": "string",
357
+ "description": "锁主题 id(等价于锁 style,主题与风格 1:1)"
358
+ },
359
+ "layout": {
360
+ "type": "string",
361
+ "description": "锁版式名(stylekit.LAYOUTS 的键,如 left-stack / center-stack / split-right)"
362
+ },
363
+ "theme_overrides": {
364
+ "type": "object",
365
+ "description": "主题字段覆盖:{\"accent\": \"#ff8a3d\"}(只接受主题里已存在的键)"
366
+ }
367
+ }
368
+ },
369
+ "music": {
370
+ "type": "object",
371
+ "additionalProperties": true,
372
+ "description": "配乐:scorekit 程序化生成 / v0.2 和弦床 / 外部 wav / 关掉。不写 mode 时走 v0.2 兼容链(有 bpm 就生成,没有就沿用)",
373
+ "properties": {
374
+ "enabled": {
375
+ "type": "boolean",
376
+ "description": "false = 本片不要配乐(等价于 mode=off)"
377
+ },
378
+ "mode": {
379
+ "type": "string",
380
+ "enum": [
381
+ "score",
382
+ "bed",
383
+ "file",
384
+ "off"
385
+ ],
386
+ "description": "score = scorekit 程序化配乐;bed = v0.2 和弦床;file = 用外部 work/music.wav;off = 不要配乐(不读也不删 music.wav)"
387
+ },
388
+ "style": {
389
+ "type": "string",
390
+ "description": "\"auto\" 或风格 id:配乐风格(缺省跟随全片 style)"
391
+ },
392
+ "mood": {
393
+ "type": "string",
394
+ "description": "\"auto\" 或情绪名(steady/reflective/hopeful/uplifting/calm/driving,按风格缺省)"
395
+ },
396
+ "bpm": {
397
+ "type": [
398
+ "number",
399
+ "string"
400
+ ],
401
+ "minimum": 0,
402
+ "description": "配乐 BPM:数字必须 > 0;\"auto\" = 风格梯度 + 指纹。优先级高于顶层 bpm"
403
+ },
404
+ "key": {
405
+ "type": "string",
406
+ "description": "调性:\"auto\" 或 C/G/D/F/A/Bb/Dm/Am/Em/Cm;缺省按风格调池选"
407
+ },
408
+ "seed": {
409
+ "type": "integer",
410
+ "minimum": 0,
411
+ "description": "配乐种子(缺省 7);同一 seed 逐采样一致"
412
+ },
413
+ "seed_from_content": {
414
+ "type": "boolean",
415
+ "description": "种子是否掺入内容指纹(默认 true:改台词换 BPM/调性,不换细节音色)"
416
+ },
417
+ "gain_db": {
418
+ "type": "number",
419
+ "description": "配乐总线增益 dB(v0.7.1 默认 -12:视频以旁白为主,背景必须明显低于人声;旧版 -1.5 会让背景与台词几乎等高)"
420
+ },
421
+ "duck_db": {
422
+ "type": "number",
423
+ "description": "音效响起时配乐让位深度 dB(v0.7 默认按风格 3.0–5.5)"
424
+ },
425
+ "voice_duck_db": {
426
+ "type": "number",
427
+ "description": "v0.7.1:旁白说话时配乐额外让位深度 dB(默认 6;0 = 关闭旁白侧链)"
428
+ },
429
+ "voice_duck_sfx_db": {
430
+ "type": "number",
431
+ "description": "v0.7.1:旁白说话时音效额外让位深度 dB(默认 3;0 = 关闭)"
432
+ },
433
+ "voice_thresh_db": {
434
+ "type": "number",
435
+ "description": "v0.7.1:语音活跃度阈值 dBFS(默认 -42;低于它算静音间隙,背景恢复)"
436
+ },
437
+ "voice_floor_db": {
438
+ "type": "number",
439
+ "description": "v0.7.1:语音包络的地板 dBFS(默认 -60;与阈值之间做 0~1 映射)"
440
+ },
441
+ "guard_snr_db": {
442
+ "type": "number",
443
+ "description": "v0.7.1:旁白清晰度兜底(默认 12)。混音后实测人声活跃区的人声/背景响度差,不足就整体再压背景;0 = 关闭"
444
+ },
445
+ "guard_cap_db": {
446
+ "type": "number",
447
+ "description": "v0.7.1:SNR 兜底最多补压多少 dB(默认 30,防止把背景压成忽有忽无;补压 >18 dB 会提示检查素材电平)"
448
+ },
449
+ "seg": {
450
+ "type": "number",
451
+ "minimum": 0,
452
+ "description": "v0.2 和弦床的段长秒数(默认 4.0)"
453
+ },
454
+ "amp": {
455
+ "type": "number",
456
+ "minimum": 0,
457
+ "description": "v0.2 和弦床幅度(默认 0.055)"
458
+ },
459
+ "gain": {
460
+ "type": "number",
461
+ "minimum": 0,
462
+ "description": "v0.2 和弦床增益(默认 0.9)"
463
+ },
464
+ "xfade": {
465
+ "type": "number",
466
+ "minimum": 0,
467
+ "description": "v0.2 和弦床交叉淡化秒数(默认 1.2)"
468
+ },
469
+ "chords": {
470
+ "type": "array",
471
+ "description": "v0.2 兼容:和弦频率表 [[220, 261.63, 329.63], ...];只有 mode=bed/兼容链用得到",
472
+ "items": {
473
+ "type": "array",
474
+ "items": {
475
+ "type": "number"
476
+ }
477
+ }
478
+ },
479
+ "sections": {
480
+ "type": "array",
481
+ "description": "手工分段:每项 {id, at 或 start, end, energy, chords, layers…};at 可写 \"l03\" 让分段跟着场景走",
482
+ "items": {
483
+ "type": "object",
484
+ "additionalProperties": true
485
+ }
486
+ }
487
+ }
488
+ },
489
+ "sfx": {
490
+ "type": "object",
491
+ "additionalProperties": true,
492
+ "description": "音效:按 cues 在绝对时刻落位(画面提示点驱动声音,不靠“对齐猜”)",
493
+ "properties": {
494
+ "enabled": {
495
+ "type": "boolean",
496
+ "description": "false = 不登记 film.json 的 cues(scenes.py 的 cues() 钩子仍会跑)"
497
+ },
498
+ "gain_db": {
499
+ "type": "number",
500
+ "description": "音效总线增益 dB(默认 0)"
501
+ },
502
+ "base_db": {
503
+ "type": "object",
504
+ "additionalProperties": true,
505
+ "description": "按配方名覆盖基础电平 dB:{\"whoosh\": -6}。render.py 的 st_sfx 原样透传给 filmkit.CueBus.build_audio(base_db=...),只对键名命中的配方生效(未知配方名不报错、也不生效)"
506
+ },
507
+ "mix_gain_db": {
508
+ "type": "number",
509
+ "description": "混音时音效总线额外增益 dB(默认 0)"
510
+ },
511
+ "content_seed": {
512
+ "type": "boolean",
513
+ "description": "v0.7:true(默认)= 每条 cue 的 seed 由 style/content_fp 派生(不同影片同一位置也不同);false = 回到 v0.6 的 seed_base+i 位置派生(老片逐样本兼容)"
514
+ },
515
+ "duck_transitions": {
516
+ "type": "boolean",
517
+ "description": "v0.7:true(默认)= 段落边界前 2.2s 内的 whoosh/riser/reverse_swell 自动 -3 dB,避免与配乐过门在同一频段撞车;false = 不处理"
518
+ },
519
+ "cues": {
520
+ "type": "array",
521
+ "description": "提示点数组:每条 {at, type, dur, gain, ...};未知参数原样透传给 sfxkit 配方(size/pan/seed/pitch…)",
522
+ "items": {
523
+ "type": "object",
524
+ "required": [
525
+ "at",
526
+ "type"
527
+ ],
528
+ "additionalProperties": true,
529
+ "properties": {
530
+ "at": {
531
+ "type": [
532
+ "number",
533
+ "string"
534
+ ],
535
+ "description": "时刻:数字 = 绝对秒;\"abs:12.5\" = 绝对秒;\"l03\" = 场景起点;\"l03+0.35\" / \"l03-0.2\" = 相对场景 start 的偏移。用 id 写法时该 id 必须存在于 lines"
536
+ },
537
+ "type": {
538
+ "type": "string",
539
+ "description": "sfxkit 配方名或别名:whoosh / riser / impact / ui_blip / ui_click / data_chirp / typing / wind / rain / glitch / reverse_swell …(别名 blip→ui_blip、click→ui_click、chirp→data_chirp、wind→wind 等)"
540
+ },
541
+ "dur": {
542
+ "type": "number",
543
+ "minimum": 0,
544
+ "description": "时长秒数(不写用配方默认)"
545
+ },
546
+ "gain": {
547
+ "type": "number",
548
+ "description": "该条增益 dB"
549
+ },
550
+ "size": {
551
+ "type": "number",
552
+ "description": "impact 的体量参数(1 = 默认)"
553
+ },
554
+ "pan": {
555
+ "type": "number",
556
+ "description": "声像 -1(左)~ +1(右)"
557
+ },
558
+ "seed": {
559
+ "type": "integer",
560
+ "description": "该条随机种子(同 seed 逐采样一致)"
561
+ },
562
+ "visual_at": {
563
+ "type": [
564
+ "number",
565
+ "string"
566
+ ],
567
+ "description": "可选锚点:画面上那个事件真正发生的秒数(数字或 \"abs:12.42\");qa_frames 用它查音画同步"
568
+ },
569
+ "visual_t": {
570
+ "type": [
571
+ "number",
572
+ "string"
573
+ ],
574
+ "description": "visual_at 的兼容写法(qa_frames 两个都认)"
575
+ }
576
+ }
577
+ }
578
+ }
579
+ }
580
+ },
581
+ "web": {
582
+ "type": "object",
583
+ "additionalProperties": true,
584
+ "description": "浏览器引擎(engine=web)专用配置;走 PIL 时整块可删",
585
+ "properties": {
586
+ "entry": {
587
+ "type": "string",
588
+ "description": "入口 HTML 路径(相对 film.json 或绝对路径);缺省 = <项目>/web/index.html"
589
+ },
590
+ "chunk_s": {
591
+ "type": "number",
592
+ "minimum": 0,
593
+ "description": "页面分块秒数(webengine 切块用,默认 30)"
594
+ }
595
+ }
596
+ }
597
+ },
598
+ "additionalProperties": true
599
+ }