@dsh-gis/dsh-video-studio-skill 0.8.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 (54) hide show
  1. package/CHANGELOG.md +582 -0
  2. package/README.md +287 -304
  3. package/THIRD_PARTY_NOTICES.md +1 -1
  4. package/package.json +11 -10
  5. package/schemas/design.schema.json +722 -0
  6. package/schemas/film.schema.json +171 -15
  7. package/schemas/proposal.schema.json +133 -0
  8. package/skills/video-studio/SKILL.md +1316 -1066
  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 +12 -9
  12. package/skills/video-studio/assets/scenes.py +2207 -1190
  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 +2 -2
  20. package/skills/video-studio/references/scene-grammar.md +311 -0
  21. package/skills/video-studio/references/sfx-design.md +1 -1
  22. package/skills/video-studio/references/style-packs.md +107 -168
  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 +104 -33
  28. package/skills/video-studio/scripts/filmkit.py +153 -78
  29. package/skills/video-studio/scripts/grammar.py +619 -0
  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 +505 -142
  37. package/skills/video-studio/scripts/scorekit.py +50 -1748
  38. package/skills/video-studio/scripts/selfcheck.py +851 -115
  39. package/skills/video-studio/scripts/stagewatch.py +21 -5
  40. package/skills/video-studio/scripts/stylekit.py +293 -570
  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 +9 -1
  45. package/tools/estimate.py +14 -8
  46. package/tools/golden_frames.py +56 -11
  47. package/tools/silhouette_qa.py +1263 -0
  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-craft.png +0 -0
  53. package/docs/styles/sheet-night.png +0 -0
  54. 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,142 +315,158 @@ 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
-
318
+ # ============================================================================
319
+ # 四、风格包(已删除:v1.0.1 起不提供任何模板)
320
+ # ============================================================================
321
+ #
322
+ # 这里曾有 STYLES:一个 style id = 主题 + 配乐 + 旁白 + 节奏的整套模板,
323
+ # 外加 pick_style 按关键词/指纹**自动挑一套**。那是本技能最像"模板库"的部分。
324
+ #
325
+ # 现在没有任何 style id:一个设计从哪来只有一条路——work/design.json,
326
+ # 由 propose.py 按 brief 现场采样、人确认后冻结。
327
+ #
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 套现成配色"。
426
338
  MINOR_KEYS = ("Dm", "Am", "Em", "Cm")
427
339
  MAJOR_KEYS = ("C", "G", "D", "F", "A", "Bb")
428
340
 
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
- ALL_STYLE_IDS = (LEGACY_STYLE,) + STYLE_IDS
535
-
536
341
 
342
+ # ---- 兼容壳:模板表删掉后,老调用点不该 AttributeError ----
343
+ # 这些函数在 v1.0.1 之前用于"按 style id 取模板"。现在没有模板,它们**必须明确报错**,
344
+ # 而不是返回一个假的默认值(那等于偷偷恢复模板)。
537
345
  def style_ids(include_legacy=True):
538
- return list(ALL_STYLE_IDS) if include_legacy else list(STYLE_IDS)
346
+ return []
539
347
 
540
348
 
541
349
  def get(style_id):
542
- """取风格包;未知 id 报错并列出可用值(不静默回落,v0.4 的教训)。"""
543
- sid = str(style_id or "").strip()
544
- if sid not in STYLES:
545
- raise KeyError("未知风格 %r;可用:%s" % (style_id, " / ".join(ALL_STYLE_IDS)))
546
- return STYLES[sid]
350
+ raise KeyError(
351
+ "没有风格 %r:v1.0.1 起本技能不提供内置风格(模板已删除)。\n"
352
+ " 每一版外观都必须现场生成:python propose.py --brief \"题材\" --script film.json\n"
353
+ " 再用 --confirm 冻结 design.json" % (style_id,))
547
354
 
548
355
 
549
356
  def theme_of(style_id):
550
- """取风格对应的主题字典(元组颜色原样返回,给 PIL 直接下标)。"""
551
- return THEMES[get(style_id)["theme"]]
357
+ raise KeyError(
358
+ "没有主题 %r:v1.0.1 起没有内置主题表(模板已删除)。\n"
359
+ " 主题由 designkit 从 (色相,饱和,明度) 连续空间现场构造,见 work/design.json"
360
+ % (style_id,))
552
361
 
553
362
 
554
363
  def is_legacy(style_id):
555
- return str(style_id or LEGACY_STYLE) == LEGACY_STYLE
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")
369
+
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 = {}
556
384
 
557
385
 
386
+ def visual_language(lang=None):
387
+ """兼容壳:v1.0.1 起没有命名视觉语言,传什么返回什么;传 None 返回空壳。
388
+
389
+ 保留这个函数只是为了让 `plan["language"] = ST.visual_language(...)` 这类老调用点
390
+ 不至于 AttributeError —— 真正的语言包由 designkit 提供。"""
391
+ return lang if isinstance(lang, dict) else {}
392
+
393
+
394
+ def visual_lint_language(vl):
395
+ """结构签名体检:对**任意** language 字典,不要求它属于某个命名风格。
396
+
397
+ 这是从 v0.9 的 visual_lint(style_id) 里抽出来的核心——原来它先查
398
+ VISUAL_LANGUAGES[style_id] 再体检,模板删掉后没有 style_id 可查了,
399
+ 但仍然需要"这套语言参数越界没有"的检查(否则转场 1 秒、push 0.3 这类
400
+ 会让整片观感崩掉的错误要等渲完 300 帧才发现)。
401
+ """
402
+ bad = []
403
+ if not isinstance(vl, dict) or not vl:
404
+ return ["视觉语言包缺失或不是字典"]
405
+ mo = vl.get("motion") or {}
406
+ mf = vl.get("motif") or {} # 注意:mo = motion,mf = motif,别混用
407
+ tr = vl.get("transition") or {}
408
+ ty = vl.get("typography") or {}
409
+ if str(mf.get("kind")) not in VISUAL_MOTIFS:
410
+ bad.append("motif.kind=%r 不在 %s 里" % (mf.get("kind"), " / ".join(VISUAL_MOTIFS)))
411
+ if str(tr.get("kind")) not in VISUAL_TRANSITIONS:
412
+ bad.append("transition.kind=%r 不在 %s 里"
413
+ % (tr.get("kind"), " / ".join(VISUAL_TRANSITIONS)))
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:
419
+ # 段界 veil 超过 0.6s 会吃掉 3–6s 节奏下限,也会把两段内容糊在一起
420
+ bad.append("transition.dur=%r 必须在 0~0.60s(段界 veil 上限)" % tr.get("dur"))
421
+ if str(ty.get("number")) not in NUMBER_STYLES:
422
+ bad.append("typography.number=%r 不在 %s 里"
423
+ % (ty.get("number"), " / ".join(NUMBER_STYLES)))
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:
445
+ bad.append("motif.density=%r 必须在 0~1" % mf.get("density"))
446
+ elif dens <= 0.0 and str(mf.get("kind")) != "minimal":
447
+ bad.append("motif.density=0 只允许用于 minimal(否则等于没有装饰语言)")
448
+ enter = mo.get("enter") or (0.45, 0.85)
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:
455
+ bad.append("motion.enter=%r 必须是 (起, 止) 且 0 < 起 <= 止 <= 1.6 秒" % (enter,))
456
+ return bad
457
+
458
+
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 {})
465
+
466
+
467
+ def visual_lint_all():
468
+ """v1.0.1 起没有内置语言可逐一检查,保留空实现只为兼容。"""
469
+ return {}
558
470
  # ============================================================================
559
471
  # 五、内容指纹与自动选风格
560
472
  # ============================================================================
@@ -599,67 +511,15 @@ def fingerprint(script):
599
511
  }
600
512
 
601
513
 
602
- def _kind_profile(script):
603
- """kind 分布画像:用于给风格打分(题材相近的片子 kind 结构也像)。"""
604
- kinds = [str(ln.get("kind") or "body") for ln in script.get("lines") or []]
605
- n = max(1, len(kinds))
606
- return {
607
- "hook": kinds.count("hook") / n,
608
- "body": kinds.count("body") / n,
609
- "outro": kinds.count("outro") / n,
610
- "items": sum(1 for ln in script.get("lines") or [] if ln.get("items")) / n,
611
- "avg_len": sum(len(str(ln.get("text") or "")) for ln in script.get("lines") or []) / n,
612
- }
613
-
614
-
615
- def score_styles(script):
616
- """给所有风格打分,返回 [(style_id, score, hits)],按分数降序(同分按 STYLE_IDS 顺序)。"""
617
- text = _text_of(script)
618
- prof = _kind_profile(script)
619
- fp = fingerprint(script)
620
- rows = []
621
- for sid in STYLE_IDS:
622
- th = THEMES[STYLES[sid]["theme"]]
623
- hits = []
624
- score = 0.0
625
- for kw in th["keywords"]:
626
- c = text.count(str(kw))
627
- if c:
628
- score += 3.0 * c
629
- hits.append(str(kw))
630
- if prof["items"] >= 0.5:
631
- score += 1.5 if sid in ("tech-deep", "magazine", "vivid") else 0.0
632
- if prof["avg_len"] >= 28:
633
- score += 1.0 if sid in ("cinema", "paper", "mono") else 0.0
634
- if prof["hook"] > 0 and prof["outro"] > 0:
635
- score += 0.5 if sid in ("tech-deep", "vivid", "epic") else 0.0
636
- # 指纹破平:必须把**风格 id** 混进哈希,否则这一项对所有行是同一个常数,
637
- # 等于没破平(v0.8 深审实测:旧写法在"AI 报告"上 tech-deep / magazine 同分 12.0,
638
- # 永远按 STYLE_IDS 顺序选 tech-deep)。幅度上限 0.0006 << 最小真实分差 0.5,
639
- # 只在精确同分时起作用,且不改变"改错别字不换风格"(指纹只吃标题)。
640
- score += (_digest("tiebreak", fp["style"], sid) % 7) * 0.0001
641
- rows.append((sid, round(score, 4), hits))
642
- rows.sort(key=lambda r: (-r[1], STYLE_IDS.index(r[0])))
643
- return rows
644
-
645
-
646
- def pick_style(script):
647
- """内容驱动选风格。返回 (style_id, reason[list[str], 给用户看的理由])。"""
648
- rows = score_styles(script)
649
- best_id, best_score, best_hits = rows[0]
650
- fp = fingerprint(script)
651
- if best_score >= 1.0:
652
- if best_hits:
653
- reason = ["关键词命中:%s" % "、".join(dict.fromkeys(best_hits))[:80]]
654
- else:
655
- # 分数可以只由结构加分(items / 长句 / hook+outro)凑到 1.0 以上,
656
- # 这时旧文案会写"关键词命中:"后面空一截 —— 等于向用户报告没发生的事。
657
- reason = ["无关键词命中,按题材结构加分选中(items / 长句 / hook+outro)"]
658
- else:
659
- pool = list(STYLE_IDS)
660
- best_id = pool[int(fp["tempo"], 16) % len(pool)]
661
- reason = ["无关键词命中,按内容指纹落桶(%s)" % fp["tempo"][:8]]
662
- 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 现场采样。
663
523
 
664
524
 
665
525
  # ============================================================================
@@ -691,182 +551,72 @@ def _as_bool(v, default=False):
691
551
  return bool(v)
692
552
 
693
553
 
694
- def resolve_script_style(script):
695
- """解析整份剧本的最终风格计划(纯函数、确定性、可直接 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
+ }
696
568
 
697
- 优先级(从高到低):
698
- 1. script["style"] 全片锁定(不是 "auto" 就生效)
699
- 2. script["visual"]["theme"] 等价于锁 style(主题与风格 1:1)
700
- 3. 自动:关键词 + 指纹
701
- 配乐 BPM / 调性 / 情绪、旁白声线映射、节奏都能被 film.json 单独覆盖。
702
- """
703
- fp = fingerprint(script)
704
- vis = script.get("visual") if isinstance(script.get("visual"), dict) else {}
705
- mus = script.get("music") if isinstance(script.get("music"), dict) else {}
706
- voce = script.get("voice") if isinstance(script.get("voice"), dict) else {}
707
-
708
- has_style_field = "style" in script
709
- raw = script.get("style")
710
- raw_s = str(raw).strip().lower() if raw is not None else ""
711
- if not has_style_field and not vis.get("theme") and not mus.get("style"):
712
- # 老项目(film.json 里根本没有 style 字段)-> legacy:配乐逐样本一致,v0.4 原路径
713
- sid, source, reason = LEGACY_STYLE, "legacy", ["film.json 没有 style 字段,走 v0.4 兼容路径"]
714
- elif raw_s in ("", "auto", "default"):
715
- if vis.get("theme"):
716
- sid = str(vis["theme"])
717
- source = "visual.theme"
718
- reason = ["film.json visual.theme 指定"]
719
- else:
720
- sid, reason = pick_style(script)
721
- source = "auto"
722
- else:
723
- sid = str(raw).strip()
724
- source = "explicit"
725
- reason = ["film.json style 指定"]
726
- get(sid) # 未知 id 在这里就炸,不要等到渲染 300 帧后
727
- pack = STYLES[sid]
728
-
729
- # ---- 配乐:风格 / BPM / 调性 / 情绪 ----
730
- music_style = str(mus.get("style") or "").strip().lower()
731
- if music_style in ("", "auto", "default"):
732
- music_style = sid if not is_legacy(sid) else LEGACY_STYLE
733
- mpack = pack["music"]
734
- ladder = tuple(mpack["bpm_ladder"])
735
- bpm_raw = mus.get("bpm", script.get("bpm"))
736
- if isinstance(bpm_raw, str) and bpm_raw.strip().lower() == "auto":
737
- bpm_raw = "auto"
738
- if bpm_raw in (None, "", "auto"):
739
- if is_legacy(sid):
740
- bpm, bpm_source = 120.0, "legacy" # v0.4 的缺省值,必须是 120
741
- else:
742
- bpm = float(_pick_from_pool(ladder, fp["tempo"], salt="bpm:" + sid))
743
- bpm_source = "style_ladder"
744
- else:
745
- bpm, bpm_source = float(bpm_raw), "explicit"
746
-
747
- key_raw = mus.get("key")
748
- if key_raw in (None, "", "auto"):
749
- if is_legacy(sid):
750
- key, key_source = "Dm", "legacy" # v0.4 的缺省值,必须是 Dm
751
- else:
752
- key = _pick_from_pool(mpack["keys"], fp["tempo"], salt="key:" + sid) or "Dm"
753
- key_source = "style_pool"
754
- else:
755
- key, key_source = str(key_raw), "explicit"
756
569
 
757
- mood = str(mus.get("mood") or "").strip().lower()
758
- if mood in ("", "auto", "default"):
759
- mood, mood_source = str(mpack.get("mood") or "steady"), "style"
760
- else:
761
- 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)
762
576
 
763
- seed_raw = mus.get("seed", script.get("seed", 7))
764
- try:
765
- seed = abs(int(seed_raw if seed_raw is not None else 7))
766
- except (TypeError, ValueError):
767
- seed = 7
768
- seed_from_content = _as_bool(mus.get("seed_from_content"), default=(not is_legacy(sid)))
769
- if seed_from_content:
770
- seed_effective = (seed * 1000003 + int(fp["full"], 16)) % (2 ** 31 - 1)
771
- else:
772
- seed_effective = seed
773
-
774
- # ---- 旁白:声线映射 + 策略 ----
775
- style_voice = pack["voice"]
776
- voice_map = {}
777
- if not is_legacy(sid):
778
- for kind in ("hook", "body", "quote", "outro"):
779
- dv = style_voice.get(kind)
780
- if dv:
781
- voice_map[kind] = dv
782
- explicit_map = voce.get("map") if isinstance(voce.get("map"), dict) else {}
783
- voice_map.update({str(k): str(v) for k, v in explicit_map.items() if v})
784
- policy = {
785
- "auto": _as_bool(voce.get("auto"), default=(not is_legacy(sid))),
786
- "jitter": float(voce.get("jitter", 0.0 if is_legacy(sid) else 0.35)),
787
- "max_distinct": int(voce.get("max_distinct", 2 if is_legacy(sid) else 3)),
788
- "rate_range": (-15, 12),
789
- "pitch_range": (-8, 8),
790
- "cps_band": (3.6, 5.6),
791
- }
792
- if isinstance(voce.get("rate_range"), (list, tuple)) and len(voce["rate_range"]) == 2:
793
- policy["rate_range"] = (int(voce["rate_range"][0]), int(voce["rate_range"][1]))
794
- if isinstance(voce.get("cps_band"), (list, tuple)) and len(voce["cps_band"]) == 2:
795
- policy["cps_band"] = (float(voce["cps_band"][0]), float(voce["cps_band"][1]))
796
-
797
- # ---- 节奏 ----
798
- sp = pack["pacing"]
799
- pacing = {
800
- "lead": float(script.get("lead", sp["lead"])),
801
- "tail": float(script.get("tail", sp["tail"])),
802
- "min_dur": float(script.get("min_dur", sp["min_dur"])),
803
- "hold": float(script.get("hold", sp["hold"])),
804
- "item_stagger": float(THEMES[pack["theme"]]["item_stagger"]),
805
- }
806
- explicit_pace = script.get("pacing") if isinstance(script.get("pacing"), dict) else {}
807
- for k in ("lead", "tail", "min_dur", "hold", "item_stagger"):
808
- if explicit_pace.get(k) is not None:
809
- pacing[k] = float(explicit_pace[k])
810
-
811
- # ---- 版式轮换:按 chapter 轮换骨架(同一章节内不变,避免每句都跳) ----
812
- layouts = list(pack["layouts"]) or ["left-stack"]
813
- chapters = []
814
- for ln in script.get("lines") or []:
815
- ch = str(ln.get("chapter") or "")
816
- if ch not in chapters:
817
- chapters.append(ch)
818
- explicit_layout = str(vis.get("layout") or "").strip()
819
- if explicit_layout:
820
- layout(explicit_layout)
821
- chap_layout = {ch: explicit_layout for ch in chapters}
822
- else:
823
- chap_layout = {ch: layouts[i % len(layouts)] for i, ch in enumerate(chapters)}
824
-
825
- # theme_overrides 走带校验的合并(颜色支持 "#rrggbb",非法值在这里就报错,
826
- # 不要等到 scenes.py 渲到第 300 帧才 int("#") 崩——见 apply_theme_overrides)
827
- theme = apply_theme_overrides(THEMES[pack["theme"]], vis.get("theme_overrides"))
828
- # visual.layout 是"显式换整片版式":旧的实现只把它写进 chapter_layout,而
829
- # PIL 路线渲染读的是 theme["layout"]、web 路线读 layout_default —— 两条路线都不看
830
- # chapter_layout,于是这条文档化的覆盖等于没写(v0.8 深审实测:visual.layout=
831
- # split-right 的帧与默认帧 sha256 完全相同)。这里补写 theme["layout"] 与 layout_default,
832
- # 让两条路线都跟着走;theme_overrides 更具体,它显式写了 layout 时以它为准。
833
- if explicit_layout and "layout" not in (vis.get("theme_overrides") or {}):
834
- theme["layout"] = explicit_layout
835
- if explicit_layout:
836
- chap_layout = {ch: str(theme["layout"]) for ch in chapters}
837
577
 
838
- return {
839
- "version": 1,
840
- "generator": "stylekit 0.5.0",
841
- "style": sid,
842
- "style_name": pack["name"],
843
- "style_source": source,
844
- "style_reason": list(reason),
845
- "legacy": bool(is_legacy(sid)),
846
- "fingerprint": fp,
847
- "theme_id": pack["theme"],
848
- "theme_label": THEMES[pack["theme"]]["label"],
849
- # 主题字典随计划一起返回:scenes.py 的 _scene_theme 优先用它(含 theme_overrides),
850
- # 也方便 render.py 直接传给 draw_subtitle/write_ass,不必再按 id 查一次。
851
- "theme": theme,
852
- "layout_default": str(theme["layout"]) if explicit_layout else layouts[0],
853
- "chapter_layout": chap_layout,
854
- "music": {
855
- "style": music_style,
856
- "bpm": bpm, "bpm_source": bpm_source,
857
- "key": key, "key_source": key_source,
858
- "mood": mood, "mood_source": mood_source,
859
- "seed": int(seed), "seed_effective": int(seed_effective),
860
- "seed_from_content": bool(seed_from_content),
861
- },
862
- "voice": {
863
- "map": voice_map,
864
- "default": voce.get("default"),
865
- "policy": policy,
866
- },
867
- "pacing": pacing,
868
- }
578
+ _AUTO_WARNED = []
579
+
869
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)
870
620
 
871
621
  # ============================================================================
872
622
  # 七、门禁:主题体检(对比度 / 明暗 / 结构)
@@ -971,71 +721,44 @@ def blend_theme(a, b, p):
971
721
 
972
722
 
973
723
  # ============================================================================
974
- # 九、CLI:看风格、看主题、跑门禁
724
+ # 九、CLI:v1.0.1 起没有可列/可看的风格(模板已删)
975
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
+ # 一并删掉(留着就是死代码,下一个人会以为它还能用)。
976
732
 
977
733
 
978
- def _describe(sid):
979
- pack = STYLES[sid]
980
- th = THEMES[pack["theme"]]
981
- lines = [
982
- "[%s] %s (theme=%s / layouts=%s)" % (sid, pack["name"], pack["theme"],
983
- ",".join(pack["layouts"])),
984
- " tags : %s" % "、".join(pack["tags"]),
985
- " keywords : %s" % "、".join(th["keywords"]),
986
- " music : %s 调池=%s BPM 梯度=%s mood=%s"
987
- % (pack["music"]["scale"], ",".join(pack["music"]["keys"]),
988
- ",".join(str(b) for b in pack["music"]["bpm_ladder"]), pack["music"]["mood"]),
989
- " voice : %s" % ", ".join("%s=%s" % (k, v) for k, v in pack["voice"].items() if v),
990
- " pacing : lead=%.2f tail=%.2f min_dur=%.1f"
991
- % (pack["pacing"]["lead"], pack["pacing"]["tail"], pack["pacing"]["min_dur"]),
992
- " palette : bg=%s bg2=%s ink=%s accent=%s"
993
- % (th["bg"], th["bg2"], th["ink"], th["accent"]),
994
- # 与 theme_lint 同口径:bg/bg2 两端取最差(旧版打中点值,会掩盖端点不达标)
995
- " contrast : ink %.2f / muted %.2f / dim %.2f / accent %.2f(bg/bg2 最差端)"
996
- % (bg_contrast(th, "ink"), bg_contrast(th, "muted"),
997
- bg_contrast(th, "dim"), bg_contrast(th, "accent")),
998
- " font : family=%s display=%spx hud=%s"
999
- % (th["family"], th["scale"]["display"][0], th["hud"]),
1000
- ]
1001
- bad = theme_lint(th)
1002
- lines.append(" lint : %s" % ("PASS" if not bad else "FAIL -> " + ";".join(bad)))
1003
- 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)
1004
743
 
1005
744
 
1006
745
  def main(argv=None):
1007
- ap = argparse.ArgumentParser(description="风格包:列出 / 查看 / 体检")
1008
- ap.add_argument("--list", action="store_true", help="列出所有风格")
1009
- ap.add_argument("--show", metavar="ID", help="查看某个风格的完整字段")
1010
- 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="(已移除)批量体检内置主题")
1011
751
  ap.add_argument("--json", action="store_true", help="配合 --show 输出 JSON")
1012
752
  a = ap.parse_args(argv)
1013
753
  try:
1014
754
  sys.stdout.reconfigure(encoding="utf-8", errors="replace")
1015
755
  except Exception: # noqa: BLE001
1016
756
  pass
1017
- if a.show:
1018
- # 未知 id 以前是 KeyError traceback,现在给可用清单 + exit 2(与 render.py 的口径一致)
1019
- if a.show not in STYLES:
1020
- print("[未知风格] %r;可用:%s" % (a.show, " / ".join(ALL_STYLE_IDS)), file=sys.stderr)
1021
- return 2
1022
- if a.json:
1023
- import json
1024
- print(json.dumps({"style": STYLES[a.show],
1025
- "theme": THEMES[STYLES[a.show]["theme"]]},
1026
- ensure_ascii=False, indent=1, default=str))
1027
- else:
1028
- print(_describe(a.show))
1029
- return 0
1030
- if a.lint:
1031
- bad = lint_all()
1032
- for sid in STYLE_IDS:
1033
- print(("FAIL " if sid in bad else "PASS ") + sid
1034
- + ("" if sid not in bad else " " + ";".join(bad[sid])))
1035
- return 1 if bad else 0
1036
- for sid in ALL_STYLE_IDS:
1037
- print(_describe(sid))
1038
- 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()
1039
762
  return 0
1040
763
 
1041
764