@dsh-gis/dsh-video-studio-skill 0.9.1 → 1.0.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 (55) hide show
  1. package/CHANGELOG.md +424 -0
  2. package/README.md +287 -327
  3. package/THIRD_PARTY_NOTICES.md +1 -1
  4. package/package.json +9 -10
  5. package/schemas/design.schema.json +722 -0
  6. package/schemas/film.schema.json +4 -15
  7. package/schemas/proposal.schema.json +133 -0
  8. package/skills/video-studio/SKILL.md +1316 -1123
  9. package/skills/video-studio/assets/design_fixture.json +141 -0
  10. package/skills/video-studio/assets/film.json +1 -2
  11. package/skills/video-studio/assets/golden_frames.json +11 -8
  12. package/skills/video-studio/assets/scenes.py +67 -22
  13. package/skills/video-studio/assets/voice_loudness.json +16 -0
  14. package/skills/video-studio/references/audio-variation.md +15 -11
  15. package/skills/video-studio/references/audio.md +36 -34
  16. package/skills/video-studio/references/change-impact.md +3 -3
  17. package/skills/video-studio/references/frame-qa.md +2 -2
  18. package/skills/video-studio/references/proposal.md +177 -0
  19. package/skills/video-studio/references/quality-gates.md +1 -1
  20. package/skills/video-studio/references/scene-grammar.md +28 -23
  21. package/skills/video-studio/references/sfx-design.md +1 -1
  22. package/skills/video-studio/references/style-packs.md +95 -257
  23. package/skills/video-studio/references/v08-quality.md +8 -8
  24. package/skills/video-studio/references/web-engine.md +1 -1
  25. package/skills/video-studio/references/word-timeline.md +2 -2
  26. package/skills/video-studio/scripts/designkit.py +2120 -0
  27. package/skills/video-studio/scripts/filmcheck.py +7 -33
  28. package/skills/video-studio/scripts/filmkit.py +153 -78
  29. package/skills/video-studio/scripts/grammar.py +67 -7
  30. package/skills/video-studio/scripts/musicgen.py +2043 -0
  31. package/skills/video-studio/scripts/preflight.py +10 -3
  32. package/skills/video-studio/scripts/previewkit.py +849 -0
  33. package/skills/video-studio/scripts/propose.py +825 -0
  34. package/skills/video-studio/scripts/qa_frames.py +45 -10
  35. package/skills/video-studio/scripts/qavmaf.py +77 -11
  36. package/skills/video-studio/scripts/render.py +472 -154
  37. package/skills/video-studio/scripts/scorekit.py +50 -1748
  38. package/skills/video-studio/scripts/selfcheck.py +658 -131
  39. package/skills/video-studio/scripts/stagewatch.py +21 -5
  40. package/skills/video-studio/scripts/stylekit.py +265 -741
  41. package/skills/video-studio/scripts/voicekit.py +502 -380
  42. package/skills/video-studio/scripts/webengine.py +15 -14
  43. package/tools/audio_similarity.py +57 -30
  44. package/tools/ci_check.py +7 -1
  45. package/tools/estimate.py +14 -8
  46. package/tools/golden_frames.py +56 -11
  47. package/tools/silhouette_qa.py +521 -168
  48. package/docs/demo-pil-sheet.png +0 -0
  49. package/docs/styles/demo-results.json +0 -76
  50. package/docs/styles/sheet-8-styles.png +0 -0
  51. package/docs/styles/sheet-ai.png +0 -0
  52. package/docs/styles/sheet-before-after.png +0 -0
  53. package/docs/styles/sheet-craft.png +0 -0
  54. package/docs/styles/sheet-night.png +0 -0
  55. package/skills/video-studio/assets/score_demo.json +0 -307
@@ -45,6 +45,50 @@ import sys
45
45
  # ============================================================================
46
46
 
47
47
 
48
+ def hsv_to_rgb(h, s, v):
49
+ """HSV(0..360, 0..1, 0..1) -> (r,g,b) 整数 0..255。
50
+
51
+ 技术原语:让"配色"可以是一个**连续空间**而不是一份命名清单。
52
+ 有了它,第 9 种外观 = 把色相转到 200°,不需要有人先写一个模板文件。
53
+ """
54
+ h = float(h) % 360.0
55
+ s = max(0.0, min(1.0, float(s)))
56
+ v = max(0.0, min(1.0, float(v)))
57
+ c = v * s
58
+ x = c * (1.0 - abs((h / 60.0) % 2.0 - 1.0))
59
+ m = v - c
60
+ if h < 60:
61
+ rgb = (c, x, 0.0)
62
+ elif h < 120:
63
+ rgb = (x, c, 0.0)
64
+ elif h < 180:
65
+ rgb = (0.0, c, x)
66
+ elif h < 240:
67
+ rgb = (0.0, x, c)
68
+ elif h < 300:
69
+ rgb = (x, 0.0, c)
70
+ else:
71
+ rgb = (c, 0.0, x)
72
+ return tuple(int(round(max(0.0, min(1.0, ch + m)) * 255.0)) for ch in rgb)
73
+
74
+
75
+ def rgb_to_hsv(rgb):
76
+ """(r,g,b) 0..255 -> (h 0..360, s 0..1, v 0..1)。与 hsv_to_rgb 互逆。"""
77
+ r, g, b = (max(0.0, min(1.0, float(c) / 255.0)) for c in tuple(rgb)[:3])
78
+ mx, mn = max(r, g, b), min(r, g, b)
79
+ d = mx - mn
80
+ if d < 1e-12:
81
+ return 0.0, 0.0, mx
82
+ if mx == r:
83
+ h = (60.0 * ((g - b) / d)) % 360.0
84
+ elif mx == g:
85
+ h = 60.0 * ((b - r) / d) + 120.0
86
+ else:
87
+ h = 60.0 * ((r - g) / d) + 240.0
88
+ s = 0.0 if mx < 1e-12 else d / mx
89
+ return h, s, mx
90
+
91
+
48
92
  def _srgb_to_lin(v):
49
93
  """sRGB 通道(0..255)-> 线性光。"""
50
94
  c = float(v) / 255.0
@@ -206,172 +250,24 @@ def _theme(style_id, label, keywords, palette, material, typ, layout, motion, hu
206
250
  }
207
251
 
208
252
 
209
- THEMES = {}
210
-
211
- THEMES["tech-deep"] = _theme(
212
- "tech-deep", "科技深蓝",
213
- ("AI", "算法", "代码", "模型", "系统", "数据", "架构", "开源", "编程", "大模型"),
214
- dict(bg=(7, 14, 26), bg2=(17, 33, 54), surface=(26, 42, 64),
215
- ink=(240, 245, 252), muted=(150, 174, 200), dim=(120, 142, 168),
216
- accent=(243, 132, 60), accent2=(86, 170, 232),
217
- good=(108, 178, 110), bad=(226, 96, 92), rule=(255, 255, 255)),
218
- dict(vignette=0.40, vignette_tint=(0, 0, 0), grain=5.0, grain_freq=0.31,
219
- texture="dots", texture_alpha=0.045),
220
- dict(family="sans", scale={"display": (96, "bold"), "heading": (64, "bold"),
221
- "sub": (38, "light"), "body": (34, "reg"),
222
- "kicker": (28, "bold"), "chip": (27, "reg"),
223
- "brand": (23, "reg")}),
224
- dict(layout="left-stack",
225
- card={"radius": 14, "fill_a": 16, "outline_a": 28, "pad": 34, "h": 96, "gap": 118}),
226
- dict(ease="out3", entrance_dx=0, entrance_dy=26, push=0.04, item_stagger=0.32, transition="black"),
227
- dict(style="chip-left"),
228
- dict(luma_max=0.030, flicker_thresh=6.0, dark=True),
229
- )
230
-
231
- THEMES["magazine"] = _theme(
232
- "magazine", "杂志暖白",
233
- ("报告", "复盘", "年度", "总结", "研究", "分析", "调研", "白皮书", "趋势", "发布"),
234
- dict(bg=(250, 246, 238), bg2=(236, 228, 214), surface=(255, 252, 246),
235
- ink=(28, 26, 24), muted=(88, 82, 74), dim=(112, 104, 94),
236
- accent=(176, 48, 40), accent2=(38, 86, 128),
237
- good=(30, 110, 72), bad=(160, 36, 32), rule=(28, 26, 24)),
238
- dict(vignette=0.10, vignette_tint=(214, 204, 186), grain=3.0, grain_freq=0.27,
239
- texture="paper", texture_alpha=0.030),
240
- dict(family="serif", scale={"display": (92, "bold"), "heading": (60, "bold"),
241
- "sub": (36, "light"), "body": (33, "reg"),
242
- "kicker": (27, "bold"), "chip": (26, "reg"),
243
- "brand": (22, "reg")}, line_gap=1.34, tracking=2.0),
244
- dict(layout="split-right",
245
- card={"radius": 8, "fill_a": 26, "outline_a": 40, "pad": 30, "h": 92, "gap": 112},
246
- safe_bias=-8),
247
- dict(ease="out5", entrance_dx=18, entrance_dy=20, push=0.06, item_stagger=0.38, transition="wash"),
248
- dict(style="corner-line"),
249
- dict(luma_max=0.045, flicker_thresh=6.0, dark=False),
250
- )
251
-
252
- THEMES["paper"] = _theme(
253
- "paper", "手绘纸感",
254
- ("手工", "自然", "历史", "传统", "木", "田野", "匠", "古", "茶", "手艺", "山河"),
255
- dict(bg=(240, 232, 212), bg2=(222, 210, 186), surface=(249, 244, 232),
256
- ink=(46, 38, 30), muted=(92, 80, 64), dim=(112, 98, 80),
257
- accent=(42, 72, 116), accent2=(124, 72, 32),
258
- good=(48, 98, 60), bad=(150, 48, 40), rule=(46, 38, 30)),
259
- dict(vignette=0.14, vignette_tint=(198, 184, 158), grain=9.0, grain_freq=0.22,
260
- texture="paper", texture_alpha=0.055),
261
- dict(family="kai", scale={"display": (92, "bold"), "heading": (60, "bold"),
262
- "sub": (36, "light"), "body": (33, "reg"),
263
- "kicker": (27, "bold"), "chip": (26, "reg"),
264
- "brand": (22, "reg")}, line_gap=1.36, tracking=1.0),
265
- dict(layout="left-stack",
266
- card={"radius": 4, "fill_a": 30, "outline_a": 52, "pad": 32, "h": 94, "gap": 116}),
267
- dict(ease="inOut3", entrance_dx=0, entrance_dy=18, push=0.03, item_stagger=0.42, transition="wash"),
268
- dict(style="corner-line"),
269
- dict(luma_max=0.045, flicker_thresh=6.0, dark=False),
270
- )
271
-
272
- THEMES["vivid"] = _theme(
273
- "vivid", "活力撞色",
274
- ("活动", "上新", "招募", "挑战", "年轻", "潮流", "种草", "福利", "开箱", "限量"),
275
- dict(bg=(20, 18, 34), bg2=(38, 26, 58), surface=(46, 34, 70),
276
- ink=(255, 252, 246), muted=(206, 196, 224), dim=(168, 158, 190),
277
- accent=(255, 214, 64), accent2=(120, 224, 255),
278
- good=(120, 230, 150), bad=(255, 120, 150), rule=(255, 255, 255)),
279
- dict(vignette=0.24, vignette_tint=(12, 10, 22), grain=4.0, grain_freq=0.35,
280
- texture="grid", texture_alpha=0.060),
281
- dict(family="sans", scale={"display": (104, "bold"), "heading": (68, "bold"),
282
- "sub": (39, "reg"), "body": (35, "reg"),
283
- "kicker": (30, "bold"), "chip": (27, "reg"),
284
- "brand": (23, "reg")}, line_gap=1.26, tracking=1.0),
285
- dict(layout="center-stack",
286
- card={"radius": 22, "fill_a": 30, "outline_a": 56, "pad": 32, "h": 100, "gap": 122}),
287
- dict(ease="outBack", entrance_dx=0, entrance_dy=34, push=0.10, item_stagger=0.26, transition="black"),
288
- dict(style="chip-left"),
289
- dict(luma_max=0.030, flicker_thresh=8.0, dark=True),
290
- )
291
-
292
- THEMES["mono"] = _theme(
293
- "mono", "极简黑白",
294
- ("观点", "为什么", "本质", "真相", "极简", "反思", "边界", "定义", "取舍"),
295
- dict(bg=(246, 246, 244), bg2=(222, 222, 220), surface=(255, 255, 255),
296
- ink=(16, 16, 16), muted=(68, 68, 68), dim=(104, 104, 104),
297
- accent=(16, 16, 16), accent2=(80, 80, 80),
298
- good=(38, 92, 52), bad=(150, 36, 36), rule=(16, 16, 16)),
299
- dict(vignette=0.06, vignette_tint=(214, 214, 212), grain=0.0, grain_freq=0.30,
300
- texture="none", texture_alpha=0.0),
301
- dict(family="sans", scale={"display": (104, "bold"), "heading": (66, "bold"),
302
- "sub": (36, "light"), "body": (33, "reg"),
303
- "kicker": (26, "bold"), "chip": (25, "reg"),
304
- "brand": (21, "reg")}, line_gap=1.40, tracking=6.0),
305
- dict(layout="center-stack",
306
- card={"radius": 0, "fill_a": 0, "outline_a": 70, "pad": 28, "h": 90, "gap": 108}),
307
- dict(ease="inOut3", entrance_dx=0, entrance_dy=12, push=0.02, item_stagger=0.55, transition="none"),
308
- dict(style="corner-line"),
309
- dict(luma_max=0.045, flicker_thresh=6.0, dark=False),
310
- )
311
-
312
- THEMES["cinema"] = _theme(
313
- "cinema", "电影青橙",
314
- ("故事", "记忆", "城市", "深夜", "旅行", "纪实", "情绪", "时代", "变迁", "命运"),
315
- dict(bg=(8, 14, 16), bg2=(20, 32, 34), surface=(26, 40, 42),
316
- ink=(238, 234, 226), muted=(164, 176, 170), dim=(128, 140, 136),
317
- accent=(232, 126, 58), accent2=(104, 196, 190),
318
- good=(116, 196, 140), bad=(214, 92, 84), rule=(255, 255, 255)),
319
- dict(vignette=0.55, vignette_tint=(0, 0, 0), grain=6.0, grain_freq=0.29,
320
- texture="scanlines", texture_alpha=0.035),
321
- dict(family="sans", scale={"display": (94, "bold"), "heading": (58, "bold"),
322
- "sub": (36, "light"), "body": (33, "reg"),
323
- "kicker": (27, "bold"), "chip": (26, "reg"),
324
- "brand": (22, "reg")}, line_gap=1.38, tracking=3.0),
325
- dict(layout="center-stack",
326
- card={"radius": 2, "fill_a": 14, "outline_a": 30, "pad": 30, "h": 92, "gap": 114}),
327
- dict(ease="inOut3", entrance_dx=0, entrance_dy=16, push=0.12, item_stagger=0.46, transition="black"),
328
- dict(style="corner-line"),
329
- dict(luma_max=0.020, flicker_thresh=6.0, dark=True),
330
- )
331
-
332
- THEMES["lo-fi"] = _theme(
333
- "lo-fi", "暖灰薄荷",
334
- ("深夜", "学习", "效率", "专注", "写作", "整理", "笔记", "书", "日常", "房间"),
335
- # 冷调灰绿:和 paper(暖牛皮纸)在色相上分开,不靠"亮度差不多"硬撑
336
- dict(bg=(232, 238, 236), bg2=(210, 220, 216), surface=(244, 248, 246),
337
- ink=(44, 52, 50), muted=(84, 98, 94), dim=(112, 126, 122),
338
- accent=(20, 86, 84), accent2=(104, 72, 96),
339
- good=(40, 100, 72), bad=(146, 52, 50), rule=(44, 52, 50)),
340
- dict(vignette=0.12, vignette_tint=(206, 200, 190), grain=7.0, grain_freq=0.24,
341
- texture="paper", texture_alpha=0.048),
342
- dict(family="sans", scale={"display": (86, "bold"), "heading": (58, "bold"),
343
- "sub": (35, "light"), "body": (32, "reg"),
344
- "kicker": (26, "bold"), "chip": (25, "reg"),
345
- "brand": (21, "reg")}, line_gap=1.36, tracking=1.0),
346
- dict(layout="left-stack",
347
- card={"radius": 18, "fill_a": 40, "outline_a": 26, "pad": 34, "h": 98, "gap": 120},
348
- safe_bias=6),
349
- dict(ease="out3", entrance_dx=0, entrance_dy=22, push=0.05, item_stagger=0.44, transition="wash"),
350
- dict(style="chip-left"),
351
- dict(luma_max=0.045, flicker_thresh=6.0, dark=False),
352
- )
353
-
354
- THEMES["epic"] = _theme(
355
- "epic", "深黑鎏金",
356
- ("史诗", "爆发", "冲击", "巅峰", "决战", "荣耀", "震撼", "传奇", "极限", "突破"),
357
- dict(bg=(9, 9, 12), bg2=(24, 20, 16), surface=(32, 28, 22),
358
- ink=(248, 242, 228), muted=(186, 176, 158), dim=(142, 134, 120),
359
- accent=(222, 178, 84), accent2=(206, 116, 92),
360
- good=(140, 200, 140), bad=(222, 96, 84), rule=(255, 255, 255)),
361
- dict(vignette=0.62, vignette_tint=(0, 0, 0), grain=5.0, grain_freq=0.26,
362
- texture="scanlines", texture_alpha=0.028),
363
- dict(family="serif", scale={"display": (108, "bold"), "heading": (70, "bold"),
364
- "sub": (38, "light"), "body": (34, "reg"),
365
- "kicker": (29, "bold"), "chip": (27, "reg"),
366
- "brand": (23, "reg")}, line_gap=1.32, tracking=4.0),
367
- dict(layout="center-stack",
368
- card={"radius": 6, "fill_a": 20, "outline_a": 44, "pad": 32, "h": 98, "gap": 120}),
369
- dict(ease="out5", entrance_dx=0, entrance_dy=24, push=0.14, item_stagger=0.34, transition="black"),
370
- dict(style="corner-line"),
371
- dict(luma_max=0.018, flicker_thresh=6.0, dark=True),
372
- )
373
-
374
253
  # ============================================================================
254
+ # 二、主题(已删除:v1.0.1 起本技能不提供任何模板)
255
+ # ============================================================================
256
+ #
257
+ # 这里曾有 8 套命名主题(THEMES:科技深蓝 / 近黑金 / 冷薄荷 / 暖砂金 / 石青灰 /
258
+ # 深林绿 / 暖纸白 / 墨纸白)。它们是**模板**:用户只能在这 8 种外观里选,
259
+ # 第 9 种永远不会出现。
260
+ #
261
+ # 现在主题由 designkit 从连续空间现场构造:
262
+ # · 底色 = (色相 h∈[0,360), 饱和 s, 明度 v) 连续取值,经 _palette_from_hue 算出三段底色;
263
+ # · 其余 8 个语义色由对比度网格求解(每套都必须过 theme_lint);
264
+ # · 产出仍是同形的 35 字段主题字典,scenes.py 不需要知道它从哪来。
265
+ #
266
+ # 保留的**技术原语**(这些不是模板,是造东西的工具):
267
+ # luminance / contrast / midpoint / _rgb / _color_of / apply_theme_overrides /
268
+ # _theme(构造器) / hsv_to_rgb / rgb_to_hsv / LINT_LIMITS / theme_lint / is_dark /
269
+ # blend_theme / family_compatible / LAYOUTS(几何参数,不是外观目录)
270
+ THEMES = {}
375
271
  # 三、版式骨架(LAYOUTS):3 种,scenes.py 按名字取几何参数
376
272
  # ============================================================================
377
273
  #
@@ -419,272 +315,93 @@ def layout(name):
419
315
  # music 字段只放「策展层」(BPM 梯度 / 调池 / 情绪 / 音色组偏好),
420
316
  # 具体的和弦进行、鼓型、乐器合成在 scorekit 里(合成层)。两边用同一个 style id 对齐。
421
317
 
422
- STYLE_IDS = ("tech-deep", "magazine", "paper", "vivid", "mono", "cinema", "lo-fi", "epic")
423
- DEFAULT_STYLE = "tech-deep"
424
- LEGACY_STYLE = "legacy" # 显式老行为:v0.4 的画风 + 配乐 + 单一旁白
425
-
426
- MINOR_KEYS = ("Dm", "Am", "Em", "Cm")
427
- MAJOR_KEYS = ("C", "G", "D", "F", "A", "Bb")
428
-
429
- STYLES = {
430
- "tech-deep": {
431
- "name": "科技深蓝",
432
- "tags": ("AI", "工程", "数据", "科普"),
433
- "theme": "tech-deep",
434
- "layouts": ("left-stack", "split-right"),
435
- "music": {"keys": ("Dm", "Am", "Em", "Cm"), "scale": "minor",
436
- "bpm_ladder": (100, 108, 116, 124), "mood": "steady"},
437
- "voice": {"body": "narrator_male", "hook": "young_male",
438
- "quote": "narrator_female", "outro": "narrator_male"},
439
- "pacing": {"lead": 0.58, "tail": 0.88, "min_dur": 3.0, "hold": 0.0},
440
- },
441
- "magazine": {
442
- "name": "杂志暖白",
443
- "tags": ("报告", "复盘", "研究", "出版物"),
444
- "theme": "magazine",
445
- "layouts": ("left-stack", "split-right"),
446
- "music": {"keys": ("C", "G", "D", "F"), "scale": "major",
447
- "bpm_ladder": (88, 92, 96, 100), "mood": "reflective"},
448
- "voice": {"body": "narrator_female", "hook": "narrator_female",
449
- "quote": "authority_male", "outro": "narrator_female"},
450
- "pacing": {"lead": 0.64, "tail": 0.94, "min_dur": 3.2, "hold": 0.2},
451
- },
452
- "paper": {
453
- "name": "手绘纸感",
454
- "tags": ("手工", "自然", "历史", "人文"),
455
- "theme": "paper",
456
- "layouts": ("left-stack", "center-stack"),
457
- "music": {"keys": ("G", "D", "C"), "scale": "major",
458
- "bpm_ladder": (84, 88, 92), "mood": "hopeful"},
459
- "voice": {"body": "warm_female", "hook": "narrator_female",
460
- "quote": "narrator_male", "outro": "warm_female"},
461
- "pacing": {"lead": 0.68, "tail": 1.00, "min_dur": 3.4, "hold": 0.3},
462
- },
463
- "vivid": {
464
- "name": "活力撞色",
465
- "tags": ("活动", "营销", "年轻", "上新"),
466
- "theme": "vivid",
467
- "layouts": ("center-stack", "left-stack"),
468
- "music": {"keys": ("D", "G", "A"), "scale": "major",
469
- "bpm_ladder": (120, 126, 132), "mood": "uplifting"},
470
- "voice": {"body": "narrator_female", "hook": "bright_female",
471
- "quote": "young_male", "outro": "bright_female"},
472
- "pacing": {"lead": 0.46, "tail": 0.76, "min_dur": 2.8, "hold": 0.0},
473
- },
474
- "mono": {
475
- "name": "极简黑白",
476
- "tags": ("观点", "演讲", "思辨", "极简"),
477
- "theme": "mono",
478
- "layouts": ("center-stack",),
479
- "music": {"keys": ("C", "F", "Am"), "scale": "major",
480
- "bpm_ladder": (60, 64, 68), "mood": "calm"},
481
- "voice": {"body": "narrator_male", "hook": "authority_male",
482
- "quote": "narrator_female", "outro": "authority_male"},
483
- "pacing": {"lead": 0.86, "tail": 1.24, "min_dur": 4.0, "hold": 0.6},
484
- },
485
- "cinema": {
486
- "name": "电影青橙",
487
- "tags": ("纪录", "叙事", "散文", "城市"),
488
- "theme": "cinema",
489
- "layouts": ("center-stack", "left-stack"),
490
- "music": {"keys": ("Dm", "Am"), "scale": "minor",
491
- "bpm_ladder": (76, 80, 84), "mood": "reflective"},
492
- "voice": {"body": "narrator_male", "hook": "authority_male",
493
- "quote": "narrator_female", "outro": "narrator_male"},
494
- "pacing": {"lead": 0.76, "tail": 1.10, "min_dur": 3.6, "hold": 0.4},
495
- },
496
- "lo-fi": {
497
- "name": "暖灰薄荷",
498
- "tags": ("日常", "学习", "专注", "深夜"),
499
- "theme": "lo-fi",
500
- "layouts": ("left-stack", "split-right"),
501
- "music": {"keys": ("C", "F", "G"), "scale": "major",
502
- "bpm_ladder": (72, 78, 84), "mood": "calm"},
503
- "voice": {"body": "narrator_female", "hook": "warm_female",
504
- "quote": "narrator_male", "outro": "narrator_female"},
505
- "pacing": {"lead": 0.72, "tail": 1.06, "min_dur": 3.4, "hold": 0.4},
506
- },
507
- "epic": {
508
- "name": "深黑鎏金",
509
- "tags": ("史诗", "宣传", "高潮", "荣耀"),
510
- "theme": "epic",
511
- "layouts": ("center-stack",),
512
- "music": {"keys": ("Cm", "Dm"), "scale": "minor",
513
- "bpm_ladder": (88, 92, 96), "mood": "driving"},
514
- "voice": {"body": "authority_male", "hook": "narrator_male",
515
- "quote": "narrator_female", "outro": "authority_male"},
516
- "pacing": {"lead": 0.62, "tail": 0.96, "min_dur": 3.2, "hold": 0.2},
517
- },
518
- }
519
-
520
- # 老行为(不写 style 时):画风 = v0.4 的深蓝橙,配乐 = v0.4 的 Dm/120/seed7,旁白 = script.voice
521
- STYLES[LEGACY_STYLE] = {
522
- "name": "v0.4 兼容",
523
- "tags": ("兼容", "老项目"),
524
- "theme": "tech-deep",
525
- "layouts": ("left-stack",),
526
- "music": {"keys": ("Dm", "Am", "Cm", "Em"), "scale": "minor",
527
- "bpm_ladder": (120,), "mood": "steady", "legacy": True},
528
- "voice": {"body": None, "hook": None, "quote": None, "outro": None},
529
- # 必须是 render.py v0.4 的函数默认值(lead=0.55 / tail=0.85 / min_dur=3.0),
530
- # 否则"没写 lead 字段的老剧本"会悄悄换掉节奏 —— legacy 的含义就是一个字节都不变。
531
- "pacing": {"lead": 0.55, "tail": 0.85, "min_dur": 3.0, "hold": 0.0},
532
- }
533
-
534
318
  # ============================================================================
535
- # 四、视觉语言包(VISUAL_LANGUAGES):v0.9
319
+ # 四、风格包(已删除:v1.0.1 起不提供任何模板)
536
320
  # ============================================================================
537
321
  #
538
- # 为什么还要一层
539
- # -------------
540
- # v0.5 的 THEMES 只管"皮肤":配色、字体族、纹理、颗粒、字距、缓动、HUD、3 种版式几何。
541
- # 8 套风格共用同一批版式与同一套装饰(开场放射线 + 收尾光点环),所以换风格只是换色,
542
- # 构图骨架(剪影)没变 —— 这正是"每个视频都差不多"的根因。
322
+ # 这里曾有 STYLES:一个 style id = 主题 + 配乐 + 旁白 + 节奏的整套模板,
323
+ # 外加 pick_style 按关键词/指纹**自动挑一套**。那是本技能最像"模板库"的部分。
543
324
  #
544
- # 这一层补的是**结构签名**,四组:
545
- # motion 入场怎么进、镜头怎么推、呼吸元素多快
546
- # motif 这套风格独有的装饰图形语言(直角括角 / 手绘内框 / letterbox / 信号条 …)
547
- # transition 段与段之间怎么过(dip / wash / 纸卷 / 斜擦 / 硬切 / 放射环)
548
- # typography 字距、行高、对齐、编号写法、display:body 字号比
325
+ # 现在没有任何 style id:一个设计从哪来只有一条路——work/design.json,
326
+ # 由 propose.py 按 brief 现场采样、人确认后冻结。
549
327
  #
550
- # 设计原则(与 THEMES 一致)
551
- # 1. 纯数据:不 import PIL / filmkit,scenes.py 反过来读我,成环就炸。
552
- # 2. 只追加不改顺序:新风格往 STYLE_IDS 末尾追加,老风格的指纹映射不漂。
553
- # 3. 每条签名都必须"能被剪影看出差别":只改颜色/字重的项不算签名。
554
- # 4. 全部受 PIL 预算约束:装饰要么预烘焙成静态层,要么是 O(几) 次 ImageDraw 调用;
555
- # 禁每帧全屏 GaussianBlur(实测 30–60ms)与全屏双线性 resize(29ms),预算 25ms。
556
- #
557
- # 剪影判据(tools/silhouette_qa.py 用的口径,也是 P1 的验收标准)
558
- # 同一段内容在两种风格下渲 3 帧 → 灰度 → 以四角亮度中位数为基准三值化 →
559
- # 比较 前景占比 F / 前景质心 (Cx,Cy) / 四边覆盖率 B / 左右对称度 S。
560
- # 五项里至少一项达标才算"这套风格真的不一样":
561
- # ΔF ≥ 6pp、|ΔCx| ≥ 8pp、|ΔCy| ≥ 8pp、ΔB ≥ 14pp、ΔS ≥ 0.20
328
+ # 保留的:LEGACY_STYLE 常量(只为了让"写 style=legacy"能被明确拒绝并给迁移指引,
329
+ # 而不是抛 AttributeError)。
330
+ STYLE_IDS = ()
331
+ DEFAULT_STYLE = None
332
+ LEGACY_STYLE = "legacy" # 只为让老剧本被**明确拒绝**时能报出可读的迁移信息
333
+ STYLES = {}
334
+
335
+ # ---- 调式池:技术原语,不是模板 ----
336
+ # 项目只支持中文口播常见的 4 个小调主音与 6 个大调主音。列出来是**约束**
337
+ # (musicgen 与 designkit 都按它校验"调性是否可实现"),不是"给你 10 套现成配色"。
338
+ MINOR_KEYS = ("Dm", "Am", "Em", "Cm")
339
+ MAJOR_KEYS = ("C", "G", "D", "F", "A", "Bb")
562
340
 
563
- VISUAL_MOTIFS = ("brackets", "hairlines", "handdrawn", "slabs",
564
- "minimal", "letterbox", "signal", "rays")
565
- VISUAL_TRANSITIONS = ("dip", "wash", "paper", "wipe", "cut", "iris")
566
- NUMBER_STYLES = ("dim", "serif", "cn", "block", "roman", "timecode", "dot", "gilt")
567
341
 
568
- VISUAL_LANGUAGES = {}
342
+ # ---- 兼容壳:模板表删掉后,老调用点不该 AttributeError ----
343
+ # 这些函数在 v1.0.1 之前用于"按 style id 取模板"。现在没有模板,它们**必须明确报错**,
344
+ # 而不是返回一个假的默认值(那等于偷偷恢复模板)。
345
+ def style_ids(include_legacy=True):
346
+ return []
569
347
 
570
- VISUAL_LANGUAGES["tech-deep"] = {
571
- "motion": {"ease": "out3", "enter": (0.45, 0.85), "push": 0.04, "breathe_hz": 0.85},
572
- "motif": {"kind": "brackets", "density": 1.0,
573
- "detail": {"corner": 24, "rule_step": 120, "scan": True}},
574
- "transition": {"kind": "dip", "dur": 0.28},
575
- "typography": {"tracking": 0.0, "line_gap": 1.30, "align": "left",
576
- "number": "dim", "ratio": 1.50},
577
- "bias": {"x": 0, "y": 0, "inset": 0},
578
- "layout_cycle": ("left-stack", "split-right", "center-stack"),
579
- }
580
348
 
581
- VISUAL_LANGUAGES["magazine"] = {
582
- "motion": {"ease": "out5", "enter": (0.60, 1.10), "push": 0.001, "breathe_hz": 0.50},
583
- "motif": {"kind": "hairlines", "density": 0.85,
584
- "detail": {"header_rule": True, "column_rule": True, "pageno": True}},
585
- "transition": {"kind": "wash", "dur": 0.35},
586
- "typography": {"tracking": 2.0, "line_gap": 1.34, "align": "right",
587
- "number": "serif", "ratio": 1.53},
588
- "bias": {"x": 60, "y": -20, "inset": 0},
589
- "layout_cycle": ("split-right", "left-stack", "center-stack"),
590
- }
349
+ def get(style_id):
350
+ raise KeyError(
351
+ "没有风格 %r:v1.0.1 起本技能不提供内置风格(模板已删除)。\n"
352
+ " 每一版外观都必须现场生成:python propose.py --brief \"题材\" --script film.json\n"
353
+ " 再用 --confirm 冻结 design.json" % (style_id,))
591
354
 
592
- VISUAL_LANGUAGES["paper"] = {
593
- "motion": {"ease": "inOut3", "enter": (0.55, 1.00), "push": 0.03, "breathe_hz": 0.42},
594
- "motif": {"kind": "handdrawn", "density": 1.0,
595
- "detail": {"wobble": 3.0, "seal": True, "tape": True}},
596
- "transition": {"kind": "paper", "dur": 0.35},
597
- "typography": {"tracking": 1.0, "line_gap": 1.36, "align": "left",
598
- "number": "cn", "ratio": 1.53},
599
- # 重心偏移(px):paper 是"手账"气质——内容整体压在左下、右侧留出大片空白给框与胶带;
600
- # 这不是配色能表达的差异,是构图差异(剪影 Cx/Cy 直接反映它)。
601
- "bias": {"x": -150, "y": 80, "inset": 40},
602
- "layout_cycle": ("left-stack", "center-stack", "split-right"),
603
- }
604
355
 
605
- VISUAL_LANGUAGES["vivid"] = {
606
- "motion": {"ease": "outBack", "enter": (0.35, 0.70), "push": 0.10, "breathe_hz": 1.10},
607
- "motif": {"kind": "slabs", "density": 1.0,
608
- "detail": {"bar_h": 24, "slant": 18, "sticker": True}},
609
- "transition": {"kind": "wipe", "dur": 0.20},
610
- "typography": {"tracking": 1.0, "line_gap": 1.26, "align": "center",
611
- "number": "block", "ratio": 2.97},
612
- "bias": {"x": 0, "y": 40, "inset": 0},
613
- "layout_cycle": ("center-stack", "left-stack", "split-right"),
614
- }
356
+ def theme_of(style_id):
357
+ raise KeyError(
358
+ "没有主题 %r:v1.0.1 起没有内置主题表(模板已删除)。\n"
359
+ " 主题由 designkit 从 (色相,饱和,明度) 连续空间现场构造,见 work/design.json"
360
+ % (style_id,))
615
361
 
616
- VISUAL_LANGUAGES["mono"] = {
617
- "motion": {"ease": "inOut3", "enter": (0.50, 0.90), "push": 0.02, "breathe_hz": 0.35},
618
- "motif": {"kind": "minimal", "density": 0.55,
619
- "detail": {"rule_h": 2, "box_radius": 0}},
620
- "transition": {"kind": "cut", "dur": 0.0},
621
- "typography": {"tracking": 6.0, "line_gap": 1.40, "align": "center",
622
- "number": "roman", "ratio": 3.15},
623
- "bias": {"x": 0, "y": 0, "inset": 0},
624
- "layout_cycle": ("center-stack", "center-stack", "center-stack"),
625
- }
626
362
 
627
- VISUAL_LANGUAGES["cinema"] = {
628
- "motion": {"ease": "inOut3", "enter": (0.70, 1.20), "push": 0.12, "breathe_hz": 0.30},
629
- "motif": {"kind": "letterbox", "density": 1.0,
630
- "detail": {"top": 40, "bottom": 44, "flares": True}},
631
- "transition": {"kind": "dip", "dur": 0.45},
632
- "typography": {"tracking": 3.0, "line_gap": 1.38, "align": "center",
633
- "number": "timecode", "ratio": 1.62},
634
- "bias": {"x": 0, "y": 30, "inset": 0},
635
- "layout_cycle": ("center-stack", "left-stack", "split-right"),
636
- }
363
+ def is_legacy(style_id):
364
+ return str(style_id or "") == LEGACY_STYLE
365
+ VISUAL_MOTIFS = ("brackets", "hairlines", "handdrawn", "slabs",
366
+ "minimal", "letterbox", "signal", "rays")
367
+ VISUAL_TRANSITIONS = ("dip", "wash", "paper", "wipe", "cut", "iris")
368
+ NUMBER_STYLES = ("dim", "serif", "cn", "block", "roman", "timecode", "dot", "gilt")
637
369
 
638
- VISUAL_LANGUAGES["lo-fi"] = {
639
- "motion": {"ease": "out3", "enter": (0.45, 0.85), "push": 0.05, "breathe_hz": 0.60},
640
- "motif": {"kind": "signal", "density": 0.90,
641
- "detail": {"bars": 12, "radius": 18, "note": True}},
642
- "transition": {"kind": "wash", "dur": 0.30},
643
- "typography": {"tracking": 1.0, "line_gap": 1.36, "align": "left",
644
- "number": "dot", "ratio": 1.48},
645
- # lo-fi 是"桌面便签"气质:整体右移、内容偏上,底部留给信号条与时间戳。
646
- "bias": {"x": 60, "y": -70, "inset": 0},
647
- "layout_cycle": ("left-stack", "split-right", "center-stack"),
648
- }
370
+ # ============================================================================
371
+ # 五、视觉语言包(已删除:v1.0.1 起不提供任何模板)
372
+ # ============================================================================
373
+ #
374
+ # 这里曾有 VISUAL_LANGUAGES:8 套命名结构签名(母题/转场/字距/缓动/重心/layout_cycle)。
375
+ # 它们是另一种模板——换风格时用户只能在这 8 组里挑。
376
+ #
377
+ # 现在视觉语言由 designkit._language_of(design) 从设计字段直接拼出来(同形五组),
378
+ # 再交给 _visual_lint_language 做同一条体检。
379
+ #
380
+ # 保留的:VISUAL_MOTIFS / VISUAL_TRANSITIONS / NUMBER_STYLES(**绘制原语的名字**,
381
+ # scenes.py 按名字分发到具体画法;这是"能画什么",不是"长什么样")、
382
+ # visual_lint_language(lang)(对任意 language 字典做体检的纯函数)。
383
+ VISUAL_LANGUAGES = {}
649
384
 
650
- VISUAL_LANGUAGES["epic"] = {
651
- "motion": {"ease": "out5", "enter": (0.70, 1.20), "push": 0.14, "breathe_hz": 0.28},
652
- "motif": {"kind": "rays", "density": 1.0,
653
- "detail": {"rays": 18, "rule_gap": 26, "frame": True}},
654
- "transition": {"kind": "iris", "dur": 0.40},
655
- "typography": {"tracking": 4.0, "line_gap": 1.32, "align": "center",
656
- "number": "gilt", "ratio": 3.18},
657
- "bias": {"x": 0, "y": -30, "inset": 0},
658
- "layout_cycle": ("center-stack", "center-stack", "center-stack"),
659
- }
660
385
 
661
- # legacy:v0.4 行为,没有视觉语言(scenes.py 走 _LEGACY_DRAWERS,不会读到这张表)
662
- VISUAL_LANGUAGES[LEGACY_STYLE] = {
663
- "motion": {"ease": "out3", "enter": (0.45, 0.85), "push": 0.04, "breathe_hz": 0.85},
664
- "motif": {"kind": "brackets", "density": 1.0, "detail": {}},
665
- "transition": {"kind": "cut", "dur": 0.0},
666
- "typography": {"tracking": 0.0, "line_gap": 1.30, "align": "left",
667
- "number": "dim", "ratio": 1.50},
668
- "layout_cycle": ("left-stack",),
669
- }
386
+ def visual_language(lang=None):
387
+ """兼容壳:v1.0.1 起没有命名视觉语言,传什么返回什么;传 None 返回空壳。
670
388
 
389
+ 保留这个函数只是为了让 `plan["language"] = ST.visual_language(...)` 这类老调用点
390
+ 不至于 AttributeError —— 真正的语言包由 designkit 提供。"""
391
+ return lang if isinstance(lang, dict) else {}
671
392
 
672
- def visual_language(style_id):
673
- """取风格的结构签名;未知 id 退到默认风格(不让拼写错误炸掉渲染)。"""
674
- sid = str(style_id or "").strip()
675
- got = VISUAL_LANGUAGES.get(sid)
676
- if got is not None:
677
- return got
678
- return VISUAL_LANGUAGES[DEFAULT_STYLE]
679
393
 
394
+ def visual_lint_language(vl):
395
+ """结构签名体检:对**任意** language 字典,不要求它属于某个命名风格。
680
396
 
681
- def visual_lint(style_id):
682
- """结构签名体检(--stage style 就拦,不等到渲完 300 帧)。返回问题列表,空 = 过。"""
397
+ 这是从 v0.9 的 visual_lint(style_id) 里抽出来的核心——原来它先查
398
+ VISUAL_LANGUAGES[style_id] 再体检,模板删掉后没有 style_id 可查了,
399
+ 但仍然需要"这套语言参数越界没有"的检查(否则转场 1 秒、push 0.3 这类
400
+ 会让整片观感崩掉的错误要等渲完 300 帧才发现)。
401
+ """
683
402
  bad = []
684
- sid = str(style_id or "")
685
- if sid not in VISUAL_LANGUAGES:
686
- return ["风格 %r 没有视觉语言包" % sid]
687
- vl = VISUAL_LANGUAGES[sid]
403
+ if not isinstance(vl, dict) or not vl:
404
+ return ["视觉语言包缺失或不是字典"]
688
405
  mo = vl.get("motion") or {}
689
406
  mf = vl.get("motif") or {} # 注意:mo = motion,mf = motif,别混用
690
407
  tr = vl.get("transition") or {}
@@ -694,66 +411,62 @@ def visual_lint(style_id):
694
411
  if str(tr.get("kind")) not in VISUAL_TRANSITIONS:
695
412
  bad.append("transition.kind=%r 不在 %s 里"
696
413
  % (tr.get("kind"), " / ".join(VISUAL_TRANSITIONS)))
697
- if not 0.0 <= float(tr.get("dur") or 0.0) <= 0.60:
414
+ try:
415
+ _d = float(tr.get("dur") or 0.0)
416
+ except (TypeError, ValueError):
417
+ _d = -1.0
418
+ if not 0.0 <= _d <= 0.60:
698
419
  # 段界 veil 超过 0.6s 会吃掉 3–6s 节奏下限,也会把两段内容糊在一起
699
420
  bad.append("transition.dur=%r 必须在 0~0.60s(段界 veil 上限)" % tr.get("dur"))
700
421
  if str(ty.get("number")) not in NUMBER_STYLES:
701
422
  bad.append("typography.number=%r 不在 %s 里"
702
423
  % (ty.get("number"), " / ".join(NUMBER_STYLES)))
703
- if not 1.0 <= float(ty.get("ratio") or 0.0) <= 3.6:
704
- bad.append("typography.ratio=%r 必须在 1.0~3.6(display:body 字号比)" % ty.get("ratio"))
705
- if not 0.0 <= float(ty.get("tracking") or 0.0) <= 8.0:
706
- bad.append("typography.tracking=%r 必须在 0~8px" % ty.get("tracking"))
707
- if not 1.15 <= float(ty.get("line_gap") or 0.0) <= 1.55:
708
- bad.append("typography.line_gap=%r 必须在 1.15~1.55" % ty.get("line_gap"))
709
- if not 0.0 <= float(mf.get("density") or 0.0) <= 1.0:
424
+ for key, lo, hi in (("ratio", 1.0, 3.6), ("tracking", 0.0, 8.0),
425
+ ("line_gap", 1.15, 1.55), ("push", 0.0, 0.16)):
426
+ try:
427
+ v = float((ty if key != "push" else mo).get(key) or 0.0)
428
+ except (TypeError, ValueError):
429
+ bad.append("typography/motion.%s=%r 不是数字" % (key, (ty if key != "push" else mo).get(key)))
430
+ continue
431
+ if key == "ratio" and not lo <= v <= hi:
432
+ bad.append("typography.ratio=%r 必须在 1.0~3.6(display:body 字号比)" % v)
433
+ elif key == "tracking" and not lo <= v <= hi:
434
+ bad.append("typography.tracking=%r 必须在 0~8px" % v)
435
+ elif key == "line_gap" and not lo <= v <= hi:
436
+ bad.append("typography.line_gap=%r 必须在 1.15~1.55" % v)
437
+ elif key == "push" and not lo <= v <= hi:
438
+ # push > 0.16 时背景层放大后内容会被推出 SAFE(_bg_plate 只裁不缩)
439
+ bad.append("motion.push=%r 必须在 0~0.16(超过会把内容推出安全区)" % v)
440
+ try:
441
+ dens = float(mf.get("density") or 0.0)
442
+ except (TypeError, ValueError):
443
+ dens = -1.0
444
+ if not 0.0 <= dens <= 1.0:
710
445
  bad.append("motif.density=%r 必须在 0~1" % mf.get("density"))
711
- if float(mf.get("density") or 0.0) <= 0.0 and str(mf.get("kind")) != "minimal":
446
+ elif dens <= 0.0 and str(mf.get("kind")) != "minimal":
712
447
  bad.append("motif.density=0 只允许用于 minimal(否则等于没有装饰语言)")
713
448
  enter = mo.get("enter") or (0.45, 0.85)
714
- if (not isinstance(enter, (list, tuple)) or len(enter) != 2
715
- or not 0.0 < float(enter[0]) <= float(enter[1]) <= 1.6):
449
+ try:
450
+ ok_enter = (isinstance(enter, (list, tuple)) and len(enter) == 2
451
+ and 0.0 < float(enter[0]) <= float(enter[1]) <= 1.6)
452
+ except (TypeError, ValueError):
453
+ ok_enter = False
454
+ if not ok_enter:
716
455
  bad.append("motion.enter=%r 必须是 (起, 止) 且 0 < 起 <= 止 <= 1.6 秒" % (enter,))
717
- if not 0.0 <= float(mo.get("push") or 0.0) <= 0.16:
718
- # push > 0.16 时背景层放大后内容会被推出 SAFE(_bg_plate 只裁不缩)
719
- bad.append("motion.push=%r 必须在 0~0.16(超过会把内容推出安全区)" % mo.get("push"))
720
456
  return bad
721
457
 
722
458
 
723
- def visual_lint_all():
724
- """全部内置风格的视觉语言体检,返回 {style_id: [问题]}(只有有问题的才在里面)。"""
725
- out = {}
726
- for sid in STYLE_IDS:
727
- bad = visual_lint(sid)
728
- if bad:
729
- out[sid] = bad
730
- return out
731
-
732
-
733
- ALL_STYLE_IDS = (LEGACY_STYLE,) + STYLE_IDS
734
-
735
-
736
- def style_ids(include_legacy=True):
737
- return list(ALL_STYLE_IDS) if include_legacy else list(STYLE_IDS)
738
-
739
-
740
- def get(style_id):
741
- """取风格包;未知 id 报错并列出可用值(不静默回落,v0.4 的教训)。"""
742
- sid = str(style_id or "").strip()
743
- if sid not in STYLES:
744
- raise KeyError("未知风格 %r;可用:%s" % (style_id, " / ".join(ALL_STYLE_IDS)))
745
- return STYLES[sid]
746
-
747
-
748
- def theme_of(style_id):
749
- """取风格对应的主题字典(元组颜色原样返回,给 PIL 直接下标)。"""
750
- return THEMES[get(style_id)["theme"]]
751
-
752
-
753
- def is_legacy(style_id):
754
- return str(style_id or LEGACY_STYLE) == LEGACY_STYLE
459
+ def visual_lint(lang=None):
460
+ """对**任意** language 字典做体检(不再是"某个 style id 的体检")。"""
461
+ if isinstance(lang, str):
462
+ # 老的 visual_lint("cinema") 调用:没有命名语言了,明确报错而不是静默通过
463
+ return ["没有命名视觉语言 %r(v1.0.1 起视觉语言由 design 现场生成)" % lang]
464
+ return visual_lint_language(lang or {})
755
465
 
756
466
 
467
+ def visual_lint_all():
468
+ """v1.0.1 起没有内置语言可逐一检查,保留空实现只为兼容。"""
469
+ return {}
757
470
  # ============================================================================
758
471
  # 五、内容指纹与自动选风格
759
472
  # ============================================================================
@@ -798,67 +511,15 @@ def fingerprint(script):
798
511
  }
799
512
 
800
513
 
801
- def _kind_profile(script):
802
- """kind 分布画像:用于给风格打分(题材相近的片子 kind 结构也像)。"""
803
- kinds = [str(ln.get("kind") or "body") for ln in script.get("lines") or []]
804
- n = max(1, len(kinds))
805
- return {
806
- "hook": kinds.count("hook") / n,
807
- "body": kinds.count("body") / n,
808
- "outro": kinds.count("outro") / n,
809
- "items": sum(1 for ln in script.get("lines") or [] if ln.get("items")) / n,
810
- "avg_len": sum(len(str(ln.get("text") or "")) for ln in script.get("lines") or []) / n,
811
- }
812
-
813
-
814
- def score_styles(script):
815
- """给所有风格打分,返回 [(style_id, score, hits)],按分数降序(同分按 STYLE_IDS 顺序)。"""
816
- text = _text_of(script)
817
- prof = _kind_profile(script)
818
- fp = fingerprint(script)
819
- rows = []
820
- for sid in STYLE_IDS:
821
- th = THEMES[STYLES[sid]["theme"]]
822
- hits = []
823
- score = 0.0
824
- for kw in th["keywords"]:
825
- c = text.count(str(kw))
826
- if c:
827
- score += 3.0 * c
828
- hits.append(str(kw))
829
- if prof["items"] >= 0.5:
830
- score += 1.5 if sid in ("tech-deep", "magazine", "vivid") else 0.0
831
- if prof["avg_len"] >= 28:
832
- score += 1.0 if sid in ("cinema", "paper", "mono") else 0.0
833
- if prof["hook"] > 0 and prof["outro"] > 0:
834
- score += 0.5 if sid in ("tech-deep", "vivid", "epic") else 0.0
835
- # 指纹破平:必须把**风格 id** 混进哈希,否则这一项对所有行是同一个常数,
836
- # 等于没破平(v0.8 深审实测:旧写法在"AI 报告"上 tech-deep / magazine 同分 12.0,
837
- # 永远按 STYLE_IDS 顺序选 tech-deep)。幅度上限 0.0006 << 最小真实分差 0.5,
838
- # 只在精确同分时起作用,且不改变"改错别字不换风格"(指纹只吃标题)。
839
- score += (_digest("tiebreak", fp["style"], sid) % 7) * 0.0001
840
- rows.append((sid, round(score, 4), hits))
841
- rows.sort(key=lambda r: (-r[1], STYLE_IDS.index(r[0])))
842
- return rows
843
-
844
-
845
- def pick_style(script):
846
- """内容驱动选风格。返回 (style_id, reason[list[str], 给用户看的理由])。"""
847
- rows = score_styles(script)
848
- best_id, best_score, best_hits = rows[0]
849
- fp = fingerprint(script)
850
- if best_score >= 1.0:
851
- if best_hits:
852
- reason = ["关键词命中:%s" % "、".join(dict.fromkeys(best_hits))[:80]]
853
- else:
854
- # 分数可以只由结构加分(items / 长句 / hook+outro)凑到 1.0 以上,
855
- # 这时旧文案会写"关键词命中:"后面空一截 —— 等于向用户报告没发生的事。
856
- reason = ["无关键词命中,按题材结构加分选中(items / 长句 / hook+outro)"]
857
- else:
858
- pool = list(STYLE_IDS)
859
- best_id = pool[int(fp["tempo"], 16) % len(pool)]
860
- reason = ["无关键词命中,按内容指纹落桶(%s)" % fp["tempo"][:8]]
861
- return best_id, reason
514
+ # —— 自动选风格(score_styles / pick_style / _kind_profile)已删除 ——
515
+ #
516
+ # 这三个函数是"机器替你挑一套内置风格"的实现:按关键词给 8 套 STYLES 打分、同分用指纹破平。
517
+ # v1.0.1 把 STYLES/THEMES 清空后它们必然崩(`THEMES[STYLES[sid]["theme"]]` -> KeyError),
518
+ # 而且留着也没有意义 —— 没有"可选集合"就没有"选"这件事。
519
+ #
520
+ # 为什么必须**删掉**而不是留着报错:留下一个"看起来还能调用"的入口,
521
+ # 下一个维护者会以为它是活的,调通了才发现整个语义已经不成立。
522
+ # 现在的唯一路径是 propose.py 现场采样。
862
523
 
863
524
 
864
525
  # ============================================================================
@@ -890,182 +551,72 @@ def _as_bool(v, default=False):
890
551
  return bool(v)
891
552
 
892
553
 
893
- def resolve_script_style(script):
894
- """解析整份剧本的最终风格计划(纯函数、确定性、可直接 json.dump)。
554
+ # persona 名 -> edge voice id 的**兼容映射**(v1.0 / D3)。
555
+ # 这不是"身份表":它不带语速/音高/响度补偿,只是把老 STYLES 表里的字符串翻译成
556
+ # voicekit 现在要求的 edge voice id。真正的声音参数在 design.audio.voice 里。
557
+ _PERSONA_VOICE = {
558
+ "narrator_male": "zh-CN-YunyangNeural",
559
+ "narrator_female": "zh-CN-XiaoxiaoNeural",
560
+ "warm_female": "zh-TW-HsiaoChenNeural",
561
+ "bright_female": "zh-CN-XiaoyiNeural",
562
+ "authority_male": "zh-CN-YunjianNeural",
563
+ "young_male": "zh-CN-YunxiNeural",
564
+ "teen_male": "zh-CN-YunxiaNeural",
565
+ "dialect_ne_female": "zh-CN-liaoning-XiaobeiNeural",
566
+ "dialect_sx_female": "zh-CN-shaanxi-XiaoniNeural",
567
+ }
895
568
 
896
- 优先级(从高到低):
897
- 1. script["style"] 全片锁定(不是 "auto" 就生效)
898
- 2. script["visual"]["theme"] 等价于锁 style(主题与风格 1:1)
899
- 3. 自动:关键词 + 指纹
900
- 配乐 BPM / 调性 / 情绪、旁白声线映射、节奏都能被 film.json 单独覆盖。
901
- """
902
- fp = fingerprint(script)
903
- vis = script.get("visual") if isinstance(script.get("visual"), dict) else {}
904
- mus = script.get("music") if isinstance(script.get("music"), dict) else {}
905
- voce = script.get("voice") if isinstance(script.get("voice"), dict) else {}
906
-
907
- has_style_field = "style" in script
908
- raw = script.get("style")
909
- raw_s = str(raw).strip().lower() if raw is not None else ""
910
- if not has_style_field and not vis.get("theme") and not mus.get("style"):
911
- # 老项目(film.json 里根本没有 style 字段)-> legacy:配乐逐样本一致,v0.4 原路径
912
- sid, source, reason = LEGACY_STYLE, "legacy", ["film.json 没有 style 字段,走 v0.4 兼容路径"]
913
- elif raw_s in ("", "auto", "default"):
914
- if vis.get("theme"):
915
- sid = str(vis["theme"])
916
- source = "visual.theme"
917
- reason = ["film.json visual.theme 指定"]
918
- else:
919
- sid, reason = pick_style(script)
920
- source = "auto"
921
- else:
922
- sid = str(raw).strip()
923
- source = "explicit"
924
- reason = ["film.json style 指定"]
925
- get(sid) # 未知 id 在这里就炸,不要等到渲染 300 帧后
926
- pack = STYLES[sid]
927
-
928
- # ---- 配乐:风格 / BPM / 调性 / 情绪 ----
929
- music_style = str(mus.get("style") or "").strip().lower()
930
- if music_style in ("", "auto", "default"):
931
- music_style = sid if not is_legacy(sid) else LEGACY_STYLE
932
- mpack = pack["music"]
933
- ladder = tuple(mpack["bpm_ladder"])
934
- bpm_raw = mus.get("bpm", script.get("bpm"))
935
- if isinstance(bpm_raw, str) and bpm_raw.strip().lower() == "auto":
936
- bpm_raw = "auto"
937
- if bpm_raw in (None, "", "auto"):
938
- if is_legacy(sid):
939
- bpm, bpm_source = 120.0, "legacy" # v0.4 的缺省值,必须是 120
940
- else:
941
- bpm = float(_pick_from_pool(ladder, fp["tempo"], salt="bpm:" + sid))
942
- bpm_source = "style_ladder"
943
- else:
944
- bpm, bpm_source = float(bpm_raw), "explicit"
945
-
946
- key_raw = mus.get("key")
947
- if key_raw in (None, "", "auto"):
948
- if is_legacy(sid):
949
- key, key_source = "Dm", "legacy" # v0.4 的缺省值,必须是 Dm
950
- else:
951
- key = _pick_from_pool(mpack["keys"], fp["tempo"], salt="key:" + sid) or "Dm"
952
- key_source = "style_pool"
953
- else:
954
- key, key_source = str(key_raw), "explicit"
955
569
 
956
- mood = str(mus.get("mood") or "").strip().lower()
957
- if mood in ("", "auto", "default"):
958
- mood, mood_source = str(mpack.get("mood") or "steady"), "style"
959
- else:
960
- mood_source = "explicit"
570
+ def _persona_to_voice_id(name):
571
+ """老 persona 名 -> edge voice id;已经是 voice id 就原样返回。"""
572
+ s = str(name or "").strip()
573
+ if not s:
574
+ return s
575
+ return _PERSONA_VOICE.get(s, s)
961
576
 
962
- seed_raw = mus.get("seed", script.get("seed", 7))
963
- try:
964
- seed = abs(int(seed_raw if seed_raw is not None else 7))
965
- except (TypeError, ValueError):
966
- seed = 7
967
- seed_from_content = _as_bool(mus.get("seed_from_content"), default=(not is_legacy(sid)))
968
- if seed_from_content:
969
- seed_effective = (seed * 1000003 + int(fp["full"], 16)) % (2 ** 31 - 1)
970
- else:
971
- seed_effective = seed
972
-
973
- # ---- 旁白:声线映射 + 策略 ----
974
- style_voice = pack["voice"]
975
- voice_map = {}
976
- if not is_legacy(sid):
977
- for kind in ("hook", "body", "quote", "outro"):
978
- dv = style_voice.get(kind)
979
- if dv:
980
- voice_map[kind] = dv
981
- explicit_map = voce.get("map") if isinstance(voce.get("map"), dict) else {}
982
- voice_map.update({str(k): str(v) for k, v in explicit_map.items() if v})
983
- policy = {
984
- "auto": _as_bool(voce.get("auto"), default=(not is_legacy(sid))),
985
- "jitter": float(voce.get("jitter", 0.0 if is_legacy(sid) else 0.35)),
986
- "max_distinct": int(voce.get("max_distinct", 2 if is_legacy(sid) else 3)),
987
- "rate_range": (-15, 12),
988
- "pitch_range": (-8, 8),
989
- "cps_band": (3.6, 5.6),
990
- }
991
- if isinstance(voce.get("rate_range"), (list, tuple)) and len(voce["rate_range"]) == 2:
992
- policy["rate_range"] = (int(voce["rate_range"][0]), int(voce["rate_range"][1]))
993
- if isinstance(voce.get("cps_band"), (list, tuple)) and len(voce["cps_band"]) == 2:
994
- policy["cps_band"] = (float(voce["cps_band"][0]), float(voce["cps_band"][1]))
995
-
996
- # ---- 节奏 ----
997
- sp = pack["pacing"]
998
- pacing = {
999
- "lead": float(script.get("lead", sp["lead"])),
1000
- "tail": float(script.get("tail", sp["tail"])),
1001
- "min_dur": float(script.get("min_dur", sp["min_dur"])),
1002
- "hold": float(script.get("hold", sp["hold"])),
1003
- "item_stagger": float(THEMES[pack["theme"]]["item_stagger"]),
1004
- }
1005
- explicit_pace = script.get("pacing") if isinstance(script.get("pacing"), dict) else {}
1006
- for k in ("lead", "tail", "min_dur", "hold", "item_stagger"):
1007
- if explicit_pace.get(k) is not None:
1008
- pacing[k] = float(explicit_pace[k])
1009
-
1010
- # ---- 版式轮换:按 chapter 轮换骨架(同一章节内不变,避免每句都跳) ----
1011
- layouts = list(pack["layouts"]) or ["left-stack"]
1012
- chapters = []
1013
- for ln in script.get("lines") or []:
1014
- ch = str(ln.get("chapter") or "")
1015
- if ch not in chapters:
1016
- chapters.append(ch)
1017
- explicit_layout = str(vis.get("layout") or "").strip()
1018
- if explicit_layout:
1019
- layout(explicit_layout)
1020
- chap_layout = {ch: explicit_layout for ch in chapters}
1021
- else:
1022
- chap_layout = {ch: layouts[i % len(layouts)] for i, ch in enumerate(chapters)}
1023
-
1024
- # theme_overrides 走带校验的合并(颜色支持 "#rrggbb",非法值在这里就报错,
1025
- # 不要等到 scenes.py 渲到第 300 帧才 int("#") 崩——见 apply_theme_overrides)
1026
- theme = apply_theme_overrides(THEMES[pack["theme"]], vis.get("theme_overrides"))
1027
- # visual.layout 是"显式换整片版式":旧的实现只把它写进 chapter_layout,而
1028
- # PIL 路线渲染读的是 theme["layout"]、web 路线读 layout_default —— 两条路线都不看
1029
- # chapter_layout,于是这条文档化的覆盖等于没写(v0.8 深审实测:visual.layout=
1030
- # split-right 的帧与默认帧 sha256 完全相同)。这里补写 theme["layout"] 与 layout_default,
1031
- # 让两条路线都跟着走;theme_overrides 更具体,它显式写了 layout 时以它为准。
1032
- if explicit_layout and "layout" not in (vis.get("theme_overrides") or {}):
1033
- theme["layout"] = explicit_layout
1034
- if explicit_layout:
1035
- chap_layout = {ch: str(theme["layout"]) for ch in chapters}
1036
577
 
1037
- return {
1038
- "version": 1,
1039
- "generator": "stylekit 0.5.0",
1040
- "style": sid,
1041
- "style_name": pack["name"],
1042
- "style_source": source,
1043
- "style_reason": list(reason),
1044
- "legacy": bool(is_legacy(sid)),
1045
- "fingerprint": fp,
1046
- "theme_id": pack["theme"],
1047
- "theme_label": THEMES[pack["theme"]]["label"],
1048
- # 主题字典随计划一起返回:scenes.py 的 _scene_theme 优先用它(含 theme_overrides),
1049
- # 也方便 render.py 直接传给 draw_subtitle/write_ass,不必再按 id 查一次。
1050
- "theme": theme,
1051
- "layout_default": str(theme["layout"]) if explicit_layout else layouts[0],
1052
- "chapter_layout": chap_layout,
1053
- "music": {
1054
- "style": music_style,
1055
- "bpm": bpm, "bpm_source": bpm_source,
1056
- "key": key, "key_source": key_source,
1057
- "mood": mood, "mood_source": mood_source,
1058
- "seed": int(seed), "seed_effective": int(seed_effective),
1059
- "seed_from_content": bool(seed_from_content),
1060
- },
1061
- "voice": {
1062
- "map": voice_map,
1063
- "default": voce.get("default"),
1064
- "policy": policy,
1065
- },
1066
- "pacing": pacing,
1067
- }
578
+ _AUTO_WARNED = []
579
+
1068
580
 
581
+ def _warn_auto_style_once():
582
+ """每天每进程只喊一次:自动选型已经不该再用了。"""
583
+ if _AUTO_WARNED:
584
+ return
585
+ _AUTO_WARNED.append(1)
586
+ import sys as _sys
587
+ print("[stylekit][deprecated] style=auto 的自动选型已停用(v1.0 / D3)。"
588
+ "\n 现在的主路径是 work/design.json:先 python propose.py --brief \"...\" 出方案,"
589
+ "\n 再 python propose.py --confirm --candidate A 冻结设计。"
590
+ "\n 想以出厂预设为起点:python propose.py --preset tech-deep ...",
591
+ file=_sys.stderr)
592
+
593
+
594
+ def resolve_script_style(script):
595
+ """已删除:v1.0.1 起本技能没有内置风格表,这里只剩「明确报错 + 迁移指引」。
596
+
597
+ 历史:v1.0.1 之前本函数按 8 套内置 STYLES 解析出一份风格计划
598
+ (主题 + 配乐 + 声线 + 节奏 + 版式轮换)。模板删掉后它已经没有可解析的对象——
599
+ 但 v1.0.1 只删了表,没删这条函数,于是留下三个死分支:
600
+ `pick_style` / `STYLES[sid]` / `THEMES[...]`,任何一个被调用都是 NameError/KeyError,
601
+ 连 `style="auto"` 都直接崩(v1.0.2 实测)。
602
+
603
+ 为什么保留函数而不是删掉:`filmkit.style_plan` / `scenes.py` / `preflight` /
604
+ `qavmaf` 里还有「拿不到 plan 时回退到这里」的调用点。保留一个明确报错的入口,
605
+ 那些点会得到一句能照着做的指引;删掉函数只会得到 AttributeError。
606
+ 真正的计划只有两个来源:designkit.design_to_plan(work/design.json)或
607
+ render.resolve_style(已解析好的 script["_style"])。
608
+ """
609
+ raw = script.get("style") if isinstance(script, dict) else None
610
+ extra = ("(收到 style=%r)" % (raw,)) if raw not in (None, "") else ""
611
+ raise ValueError(
612
+ "这个剧本没有可解析的设计%s:v1.0.1 起本技能不提供任何内置风格/模板,\n"
613
+ " 「写 style 走内置外观」与「不写 style 走老外观」两条路都已删除。\n"
614
+ " 正确做法(两步):\n"
615
+ " 1) python propose.py --brief \"题材/要求\" --script <film.json> --n 3\n"
616
+ " 2) python propose.py --confirm --candidate A # 冻结成 work/design.json\n"
617
+ " 然后 python render.py --script <film.json> --stage all\n"
618
+ " (设计也可以手写 work/design.json,字段契约见 schemas/design.schema.json)"
619
+ % extra)
1069
620
 
1070
621
  # ============================================================================
1071
622
  # 七、门禁:主题体检(对比度 / 明暗 / 结构)
@@ -1170,71 +721,44 @@ def blend_theme(a, b, p):
1170
721
 
1171
722
 
1172
723
  # ============================================================================
1173
- # 九、CLI:看风格、看主题、跑门禁
724
+ # 九、CLI:v1.0.1 起没有可列/可看的风格(模板已删)
1174
725
  # ============================================================================
726
+ #
727
+ # 这个 CLI 以前有 --list / --show / --lint 三个动作,列的是内置 STYLES 表。
728
+ # v1.0.1 删表时漏掉了这里,于是:--list/--show 直接 NameError: ALL_STYLE_IDS,
729
+ # --lint 在空表上一条都不检查却 exit 0 —— 后者是最坏的一种:**假通过**。
730
+ # 本版把三个动作改成「明确说明能力已迁移 + 非零退出码」,并把 lint_all / _describe
731
+ # 一并删掉(留着就是死代码,下一个人会以为它还能用)。
1175
732
 
1176
733
 
1177
- def _describe(sid):
1178
- pack = STYLES[sid]
1179
- th = THEMES[pack["theme"]]
1180
- lines = [
1181
- "[%s] %s (theme=%s / layouts=%s)" % (sid, pack["name"], pack["theme"],
1182
- ",".join(pack["layouts"])),
1183
- " tags : %s" % "、".join(pack["tags"]),
1184
- " keywords : %s" % "、".join(th["keywords"]),
1185
- " music : %s 调池=%s BPM 梯度=%s mood=%s"
1186
- % (pack["music"]["scale"], ",".join(pack["music"]["keys"]),
1187
- ",".join(str(b) for b in pack["music"]["bpm_ladder"]), pack["music"]["mood"]),
1188
- " voice : %s" % ", ".join("%s=%s" % (k, v) for k, v in pack["voice"].items() if v),
1189
- " pacing : lead=%.2f tail=%.2f min_dur=%.1f"
1190
- % (pack["pacing"]["lead"], pack["pacing"]["tail"], pack["pacing"]["min_dur"]),
1191
- " palette : bg=%s bg2=%s ink=%s accent=%s"
1192
- % (th["bg"], th["bg2"], th["ink"], th["accent"]),
1193
- # 与 theme_lint 同口径:bg/bg2 两端取最差(旧版打中点值,会掩盖端点不达标)
1194
- " contrast : ink %.2f / muted %.2f / dim %.2f / accent %.2f(bg/bg2 最差端)"
1195
- % (bg_contrast(th, "ink"), bg_contrast(th, "muted"),
1196
- bg_contrast(th, "dim"), bg_contrast(th, "accent")),
1197
- " font : family=%s display=%spx hud=%s"
1198
- % (th["family"], th["scale"]["display"][0], th["hud"]),
1199
- ]
1200
- bad = theme_lint(th)
1201
- lines.append(" lint : %s" % ("PASS" if not bad else "FAIL -> " + ";".join(bad)))
1202
- return "\n".join(lines)
734
+ def _explain_removed(what):
735
+ print("[stylekit] %s 已随内置风格表一起删除(v1.0.1:本技能不提供任何模板)。" % what,
736
+ file=sys.stderr)
737
+ print(" 现在没有「可选外观清单」,每一版外观都是现场生成的:", file=sys.stderr)
738
+ print(" python propose.py --brief \"题材/要求\" --script <film.json> --n 3", file=sys.stderr)
739
+ print(" python propose.py --confirm --candidate A # 冻结 work/design.json", file=sys.stderr)
740
+ print(" 想看某个设计长什么样 / 是否合规:", file=sys.stderr)
741
+ print(" python designkit.py --lint <design.json>", file=sys.stderr)
742
+ print(" python propose.py --show A", file=sys.stderr)
1203
743
 
1204
744
 
1205
745
  def main(argv=None):
1206
- ap = argparse.ArgumentParser(description="风格包:列出 / 查看 / 体检")
1207
- ap.add_argument("--list", action="store_true", help="列出所有风格")
1208
- ap.add_argument("--show", metavar="ID", help="查看某个风格的完整字段")
1209
- ap.add_argument("--lint", action="store_true", help="全部内置主题走对比度门禁")
746
+ ap = argparse.ArgumentParser(
747
+ description="风格包工具(v1.0.1 起无内置风格:本命令只解释迁移,不再列外观清单)")
748
+ ap.add_argument("--list", action="store_true", help="(已移除)列出内置风格")
749
+ ap.add_argument("--show", metavar="ID", help="(已移除)查看某个内置风格")
750
+ ap.add_argument("--lint", action="store_true", help="(已移除)批量体检内置主题")
1210
751
  ap.add_argument("--json", action="store_true", help="配合 --show 输出 JSON")
1211
752
  a = ap.parse_args(argv)
1212
753
  try:
1213
754
  sys.stdout.reconfigure(encoding="utf-8", errors="replace")
1214
755
  except Exception: # noqa: BLE001
1215
756
  pass
1216
- if a.show:
1217
- # 未知 id 以前是 KeyError traceback,现在给可用清单 + exit 2(与 render.py 的口径一致)
1218
- if a.show not in STYLES:
1219
- print("[未知风格] %r;可用:%s" % (a.show, " / ".join(ALL_STYLE_IDS)), file=sys.stderr)
1220
- return 2
1221
- if a.json:
1222
- import json
1223
- print(json.dumps({"style": STYLES[a.show],
1224
- "theme": THEMES[STYLES[a.show]["theme"]]},
1225
- ensure_ascii=False, indent=1, default=str))
1226
- else:
1227
- print(_describe(a.show))
1228
- return 0
1229
- if a.lint:
1230
- bad = lint_all()
1231
- for sid in STYLE_IDS:
1232
- print(("FAIL " if sid in bad else "PASS ") + sid
1233
- + ("" if sid not in bad else " " + ";".join(bad[sid])))
1234
- return 1 if bad else 0
1235
- for sid in ALL_STYLE_IDS:
1236
- print(_describe(sid))
1237
- print("")
757
+ if a.show or a.list or a.lint:
758
+ _explain_removed("--list / --show / --lint")
759
+ # 退出码 2:与 render.py 的「未知取值」口径一致(不是 1 的「跑失败」)。
760
+ return 2
761
+ ap.print_help()
1238
762
  return 0
1239
763
 
1240
764