@dsh-gis/dsh-video-studio-skill 1.0.2 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +49 -0
- package/README.md +84 -108
- package/THIRD_PARTY_NOTICES.md +2 -2
- package/package.json +1 -1
- package/schemas/design.schema.json +5 -5
- package/schemas/film.schema.json +31 -31
- package/skills/video-studio/SKILL.md +168 -268
- package/skills/video-studio/assets/film.json +2 -3
- package/skills/video-studio/assets/golden_frames.json +11 -11
- package/skills/video-studio/assets/scenes.py +92 -49
- package/skills/video-studio/references/{style-packs.md → appearance.md} +17 -15
- package/skills/video-studio/references/audio-variation.md +73 -96
- package/skills/video-studio/references/audio.md +40 -44
- package/skills/video-studio/references/change-impact.md +2 -2
- package/skills/video-studio/references/{v08-quality.md → delivery-gates.md} +9 -11
- package/skills/video-studio/references/frame-qa.md +47 -15
- package/skills/video-studio/references/pipeline.md +4 -4
- package/skills/video-studio/references/pitfalls.md +22 -3
- package/skills/video-studio/references/proposal.md +19 -36
- package/skills/video-studio/references/quality-gates.md +11 -11
- package/skills/video-studio/references/scene-authoring.md +2 -2
- package/skills/video-studio/references/scene-grammar.md +17 -24
- package/skills/video-studio/references/sfx-design.md +7 -5
- package/skills/video-studio/references/web-engine.md +7 -7
- package/skills/video-studio/references/word-timeline.md +7 -7
- package/skills/video-studio/requirements.txt +9 -13
- package/skills/video-studio/scripts/designkit.py +84 -89
- package/skills/video-studio/scripts/filmcheck.py +19 -19
- package/skills/video-studio/scripts/filmkit.py +2949 -2948
- package/skills/video-studio/scripts/gates.py +4 -4
- package/skills/video-studio/scripts/grammar.py +10 -10
- package/skills/video-studio/scripts/musicgen.py +3 -4
- package/skills/video-studio/scripts/preflight.py +5 -5
- package/skills/video-studio/scripts/previewkit.py +11 -12
- package/skills/video-studio/scripts/propose.py +11 -11
- package/skills/video-studio/scripts/qa_frames.py +34 -35
- package/skills/video-studio/scripts/qaasr.py +26 -7
- package/skills/video-studio/scripts/qavmaf.py +19 -19
- package/skills/video-studio/scripts/render.py +137 -141
- package/skills/video-studio/scripts/scorekit.py +21 -23
- package/skills/video-studio/scripts/selfcheck.py +208 -135
- package/skills/video-studio/scripts/sfxkit.py +27 -26
- package/skills/video-studio/scripts/stagewatch.py +4 -4
- package/skills/video-studio/scripts/stylekit.py +77 -282
- package/skills/video-studio/scripts/voicekit.py +43 -44
- package/skills/video-studio/scripts/webengine.py +62 -64
- package/tools/audio_similarity.py +12 -12
- package/tools/ci_check.py +16 -16
- package/tools/estimate.py +24 -25
- package/tools/golden_frames.py +17 -18
- package/tools/silhouette_qa.py +13 -14
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 词级时间轴与词锚点
|
|
2
2
|
|
|
3
3
|
> 本页是 `work/words.json` 的**契约**、`cue at` 词锚点的**语义**,以及四个在真实跑里踩过的坑。
|
|
4
4
|
> 设计前提一句话:**句子时长仍以 TTS 实测时长为权威**;词级时间戳只是附加值,
|
|
@@ -28,8 +28,8 @@ timeline.json 的 scenes[].words_abs (句内词表平移到绝对时间)
|
|
|
28
28
|
| 防呆 | 为什么 |
|
|
29
29
|
|---|---|
|
|
30
30
|
| 显式 `boundary="WordBoundary"` + `stream()` | 用 `save()` 会把词事件丢掉;不传 `boundary` 时默认 `SentenceBoundary`,**一条词事件都收不到**(实测 edge-tts 7.3.1 / zh-CN:词事件数 = 0) |
|
|
31
|
-
| 音频先写 `.part` 再 `os.replace` |
|
|
32
|
-
| 词表独立缓存键 `tts_words_key` | 并进 `tts_key_v2`
|
|
31
|
+
| 音频先写 `.part` 再 `os.replace` | 直接 `open(目标, "wb")` 时,一次断网/超时就把**已缓存的好音频截成 0 字节**;先写 `.part` 再替换,失败只丢本次新音频,旧缓存还在 |
|
|
32
|
+
| 词表独立缓存键 `tts_words_key` | 并进 `tts_key_v2` 会让**全部已有音频缓存一次性失效**(必须整片重新联网合成)。分开记 -> 已有音频照旧复用,只在"这次显式要词级"时补采一次 |
|
|
33
33
|
|
|
34
34
|
### 1.1 `work/words.json` 契约
|
|
35
35
|
|
|
@@ -68,9 +68,9 @@ G10 会因此记 `skipped` 或 `fail`(看是全部为空还是个别句为空
|
|
|
68
68
|
| `words_summary(words, dur=None)` | 出 `n / covered / gaps / tail / mono / start`(G10 判据全在这里算) | — |
|
|
69
69
|
| `sub_words_at(scenes, t)` | 当前上屏字幕对应的 `words_abs`,没有就 `None` | 无词级 -> `None`(调用方回退整句) |
|
|
70
70
|
| `wrap_by_words(text, f, max_w, words)` | 按真实词边界折行;词表与原文对不上(有尾差/切不开)就返回 `None` | `None` -> `fit_sub_lines` 回退字符折行 |
|
|
71
|
-
| `fit_sub_lines(..., words=)` / `draw_subtitle(..., words=)` | 排版入口,多一个可选参数 | `words=None`
|
|
71
|
+
| `fit_sub_lines(..., words=)` / `draw_subtitle(..., words=)` | 排版入口,多一个可选参数 | `words=None` 时排版与不带词级时逐像素一致 |
|
|
72
72
|
| `build_timeline(..., fps=)` | 给每段补 `words_abs`(句内词表平移到绝对时间) | 该句没有 `words` 就没有 `words_abs` |
|
|
73
|
-
| `cue_time(at, scenes=, total=)` | 解析 `at
|
|
73
|
+
| `cue_time(at, scenes=, total=)` | 解析 `at`(含 `l03:w2` 词锚点) | 见 §3 |
|
|
74
74
|
|
|
75
75
|
---
|
|
76
76
|
|
|
@@ -88,7 +88,7 @@ G10 会因此记 `skipped` 或 `fail`(看是全部为空还是个别句为空
|
|
|
88
88
|
`[WARN] cue at='l03:w99':l03 只有 13 个词,w99 不存在,退化为句偏移`,然后按 `l03+偏移` 处理。
|
|
89
89
|
—— 所以**改完台词一定要看 stdout 有没有这条 WARN**,否则你会以为自己精确打到了某个词。
|
|
90
90
|
- 场景 id 本身不存在时才 `raise ValueError`(先跑 layout,或者改用 `abs:秒`)。
|
|
91
|
-
-
|
|
91
|
+
- `abs:12.5` / `12.5` / `l03` / `l03+0.35` 这几种写法不含词锚点,行为与不带词级时**完全一致**。
|
|
92
92
|
|
|
93
93
|
---
|
|
94
94
|
|
|
@@ -141,7 +141,7 @@ python render.py ... --stage qa # 看 QC-REPORT.md 的 G10 行
|
|
|
141
141
|
## 6. 关掉它
|
|
142
142
|
|
|
143
143
|
- 单片:`film.json` 写 `"words": false` -> `--stage tts` 不再收词事件,G10 `skipped`(写明"本片未启用"),
|
|
144
|
-
|
|
144
|
+
字幕退回整句折行。**不要词级能力就写这一行。**
|
|
145
145
|
- 只跳过门禁不跳过能力:没有开关——G10 就是词级数据的体检,没数据就 skipped,不会 fail。
|
|
146
146
|
- 词事件拿不到(服务端变了):不阻断出片,G10 `skipped` / `fail` 二选一看覆盖面,
|
|
147
147
|
meta 里 `words_ok=false`、`words_events=0` 是排查入口。
|
|
@@ -24,32 +24,28 @@ opencv-python-headless>=4.8
|
|
|
24
24
|
# 装完还要下载浏览器:python -m playwright install chromium
|
|
25
25
|
# playwright>=1.40
|
|
26
26
|
|
|
27
|
-
# ---- 可选:
|
|
27
|
+
# ---- 可选:ASR 回读门禁(qaasr.py)----
|
|
28
28
|
# 把成片音轨转回文字与剧本对账,抓"TTS 缓存串了 / 混音丢了句子 / 音轨是旧的"。
|
|
29
29
|
# 缺了会依次降级:faster-whisper -> ffmpeg 的 whisper 滤镜(需 ggml 模型)-> skipped。
|
|
30
30
|
# 实测(CPU-only, int8):small 模型加载 2.0–3.3 s、转写 RTF≈0.17;29 s 模板片约 6 s。
|
|
31
31
|
# faster-whisper>=1.0
|
|
32
32
|
|
|
33
|
-
# ----
|
|
34
|
-
# scripts/gates.py 门禁账本 / QC 报告 /
|
|
35
|
-
# scripts/stagewatch.py
|
|
36
|
-
# scripts/filmcheck.py film.json
|
|
37
|
-
# tools/estimate.py 开渲前成本预演(可选)
|
|
38
|
-
# 若你从旧版本升级,记得把 gates.py / stagewatch.py 一起拷到工作目录,
|
|
39
|
-
# 否则 render.py 会 ModuleNotFoundError。
|
|
40
|
-
|
|
41
|
-
# ---- v0.8.0 新增文件(同样不需要 pip 包;升级记得一起拷)----
|
|
33
|
+
# ---- 纯标准库模块(**不需要**任何 pip 包,也不用拷到工程目录)----
|
|
34
|
+
# scripts/gates.py 门禁账本 / QC 报告 / 人工签核(render.py 直接 import)
|
|
35
|
+
# scripts/stagewatch.py 阶段新鲜度(render.py 直接 import)
|
|
36
|
+
# scripts/filmcheck.py film.json 契约校验(ci_check 第 12 项会调)
|
|
42
37
|
# scripts/preflight.py 渲染前资源闸门(可选:缺了就记 skipped,门禁不冒充通过)
|
|
43
38
|
# scripts/qavmaf.py 全片 VMAF/SSIM(可选:缺了就 skipped)
|
|
44
39
|
# scripts/qaasr.py ASR 回读(可选:缺了就 skipped)
|
|
45
|
-
# tools/
|
|
46
|
-
#
|
|
40
|
+
# tools/estimate.py 开渲前成本预演(仓库工具)
|
|
41
|
+
# tools/golden_frames.py 金帧回归(仓库工具)
|
|
42
|
+
# 三个可选门禁都**不是** render.py 的硬依赖(用了 _try_module),缺了只让对应门禁
|
|
47
43
|
# 记 skipped,不会 ModuleNotFoundError;但 qa 报告里会明确写"本次没查"。
|
|
48
44
|
|
|
49
45
|
# ---- 不需要 ----
|
|
50
46
|
# moviepy / pydub / soundfile / librosa / manim:本技能一行都不 import。
|
|
51
47
|
# ffmpeg 不是 pip 包:系统 PATH 里要有(含 libx264 / aac / loudnorm),
|
|
52
48
|
# 或用环境变量 FILMKIT_FFMPEG 指向可执行文件。
|
|
53
|
-
#
|
|
49
|
+
# 探测是 fail-closed 的:每个候选真跑 `-version`,跑不起来的会被跳过
|
|
54
50
|
# (防的是"存在但缺 xfade 的旧版 ffmpeg 被静默选中")。`FILMKIT_FFMPEG_STRICT=1`
|
|
55
51
|
# 可把"找不到可用 ffmpeg"从回退裸命令改成直接报错。
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
|
-
"""designkit.py — 设计 DSL
|
|
2
|
+
"""designkit.py — 设计 DSL:把"这一版长什么样"写成可校验、可采样的 JSON。
|
|
3
3
|
|
|
4
4
|
设计原则(与 stylekit 一致的纪律)
|
|
5
5
|
----------------------------------
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
2. 确定性:只用 sha1 派生,禁 random / time / hash()(str hash 每进程带随机盐)。
|
|
9
9
|
同一份 brief 永远采样出同一组候选。
|
|
10
10
|
3. fail-closed:设计缺字段、枚举越界、对比度不达标 -> 立刻报错,**绝不静默回落默认值**
|
|
11
|
-
(静默回落 =
|
|
11
|
+
(静默回落 = 所有片子又长得一样)。
|
|
12
12
|
|
|
13
13
|
design.json 的形状(schema=1)
|
|
14
14
|
-----------------------------
|
|
@@ -46,13 +46,13 @@ design.json 的形状(schema=1)
|
|
|
46
46
|
|
|
47
47
|
配色不是"随便取色":语义色由对比度求解得到
|
|
48
48
|
-----------------------------------------
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
实测:纯 HSV 随机取色的 theme_lint 通过率只有 21/60 = 35%,不可用;
|
|
50
|
+
按"色相 + (s,v) 网格求解,取最接近设计意图且满足对比度的色"可达 60/60 = 100%。
|
|
51
51
|
网格是 s,v ∈ {0,0.1,...,1.0} 的 11×11;可行解按 |Δs|+|Δv| 取最小(并列取先遇到者,确定性)。
|
|
52
52
|
7 个语义色的对比度门槛直接取 stylekit.LINT_LIMITS(与 theme_lint 同一份)。
|
|
53
53
|
|
|
54
|
-
注意:**设计里存的是求解后的 11
|
|
55
|
-
|
|
54
|
+
注意:**设计里存的是求解后的 11 个颜色**,不是配色名——这样 design.json 是自足的,
|
|
55
|
+
换机器/换版本不会因为"哪儿没有那个名字"而漂移;词 -> 色相锚点只用于采样期的求解过程。
|
|
56
56
|
|
|
57
57
|
对外接口
|
|
58
58
|
--------
|
|
@@ -62,7 +62,7 @@ design.json 的形状(schema=1)
|
|
|
62
62
|
design_fingerprint(design) -> str
|
|
63
63
|
design_distance(a, b) -> dict # 用于"伪多方案"门禁
|
|
64
64
|
|
|
65
|
-
brief.must_have / avoid
|
|
65
|
+
brief.must_have / avoid 是真过滤
|
|
66
66
|
----------------------------------------
|
|
67
67
|
词表见 _CONSTRAINT_WORDS:配色 / 母题 / 转场 / 情绪 / 声线,中文按子串认、英文按整词认。
|
|
68
68
|
must_have 是 AND(候选不许违反任何一条),avoid 是否定(违反一条就出局)。
|
|
@@ -114,13 +114,13 @@ FPSES = (24, 25, 30, 50, 60)
|
|
|
114
114
|
HOLD_POLICIES = ("none", "readable", "long")
|
|
115
115
|
SFX_DENSITIES = ("none", "sparse", "normal", "rich")
|
|
116
116
|
|
|
117
|
-
#
|
|
117
|
+
# 配色:**没有命名家族**。
|
|
118
118
|
#
|
|
119
119
|
# 决定这件事的依据很直白:本技能只提供"生成视频的技术",不提供模板。既然没有模板,
|
|
120
120
|
# 就不该存在"深蓝 / 近黑金 / 冷薄荷…"这样一份**可选清单** —— 有清单就意味着
|
|
121
|
-
#
|
|
121
|
+
# 用户的片子只能在少数几个预设外观里挑,下一个永远不存在。
|
|
122
122
|
#
|
|
123
|
-
#
|
|
123
|
+
# 模型是**连续色相空间**:
|
|
124
124
|
# · 底色 = (色相 h ∈ [0,360), 饱和度 s, 明度 v) 三个连续量 -> 直接算出 bg/bg2/surface;
|
|
125
125
|
# · 其余 8 个语义色(ink/muted/dim/accent/accent2/good/bad/rule)由对比度网格求解;
|
|
126
126
|
# · 全部通过 theme_lint 才算数(对比度不足就重采,绝不悄悄放行)。
|
|
@@ -144,13 +144,13 @@ def _palette_from_hue(hue, sat, val, dark):
|
|
|
144
144
|
return bg, bg2, surface
|
|
145
145
|
|
|
146
146
|
|
|
147
|
-
# 没有"配色族"
|
|
147
|
+
# 没有"配色族"这个概念:这个空 dict 只是为了让按名遍历的调用点
|
|
148
148
|
# (`for name in PALETTE_FAMILIES` 之类)不 AttributeError —— 它必须保持为空:
|
|
149
149
|
# 一旦有人往里加东西,就等于偷偷恢复了一份"可选外观清单"。
|
|
150
150
|
PALETTE_FAMILIES = {}
|
|
151
151
|
|
|
152
152
|
# 但 brief 里仍然会有人说"要深蓝 / 近黑 / 薄荷"。这些词要能生效,否则用户的
|
|
153
|
-
#
|
|
153
|
+
# 自然语言要求就落空了。
|
|
154
154
|
# 做法:词 -> **色相锚点 + 明暗**,不是"族名 -> 一套现成外观"。锚点只是采样的起点,
|
|
155
155
|
# 色相仍然是连续量(可在锚点附近 ±HUE_WINDOW 内浮动)。
|
|
156
156
|
_PALETTE_ANCHOR = {
|
|
@@ -165,10 +165,9 @@ _PALETTE_ANCHOR = {
|
|
|
165
165
|
}
|
|
166
166
|
_HUE_WINDOW = 26.0 # 锚点附近 ±26°(再宽就不像"深蓝"了,再窄等于恢复成枚举)
|
|
167
167
|
# 多个候选之间的"底色必须看得出不一样"的色相最小间距(度)。
|
|
168
|
-
# 为什么是 12°
|
|
169
|
-
#
|
|
170
|
-
#
|
|
171
|
-
# 在 n=3 时大半概率采样失败。
|
|
168
|
+
# 为什么是 12° 而不是分桶:12° 是并排看图时人眼能稳定分辨的色相下限附近
|
|
169
|
+
# (再小就成了同一片底色的细微偏色)。约束收窄时可选色相窗口只有 52° 宽,
|
|
170
|
+
# 12° 间距仍能放下 3–4 个候选,不会像"24° 分桶"那样在 n=3 时大半概率采样失败。
|
|
172
171
|
_PALETTE_KEEP_GAP = 12.0
|
|
173
172
|
|
|
174
173
|
|
|
@@ -194,9 +193,8 @@ _GOOD_HUE = 135.0
|
|
|
194
193
|
_BAD_HUE = 5.0
|
|
195
194
|
|
|
196
195
|
# 情绪 -> BPM / 层门限。注意这里**没有"情绪 -> 曲风"的映射**:
|
|
197
|
-
#
|
|
198
|
-
#
|
|
199
|
-
# 情绪只决定 BPM 与织体门限,不再指向任何现成风格。
|
|
196
|
+
# 把情绪映射到一套配乐风格表,等于把"配乐模板"伪装成参数。
|
|
197
|
+
# 配乐由 musicgen 现场编排,情绪只决定 BPM 与织体门限,不指向任何现成风格。
|
|
200
198
|
_MOOD_BPM = {
|
|
201
199
|
"calm": 78, "reflective": 86, "hopeful": 92, "steady": 104,
|
|
202
200
|
"driving": 124, "tense": 98, "uplifting": 118,
|
|
@@ -219,10 +217,10 @@ _TRANS_WORD = {
|
|
|
219
217
|
# 声线池:全部是可被 static 校验的 zh-CN-XxxNeural(本模块不 import voicekit——
|
|
220
218
|
# 那会拉起 edge_tts。格式只做正则,不做"这个 id 是否真的存在"的联网校验)。
|
|
221
219
|
#
|
|
222
|
-
#
|
|
220
|
+
# **只从"有响度补偿数据的音色"里采样**。
|
|
223
221
|
# 为什么:voicekit 的响度补偿是一张按 voice id 的数据表(VOICE_LOUDNESS / assets/voice_loudness.json);
|
|
224
222
|
# 采样器如果产出表里没有的音色,那行的 gain_db 会回落 0 —— 等于"这个候选的旁白电平没校准"。
|
|
225
|
-
#
|
|
223
|
+
# 实测:5 个题材 × 3 候选共产出 7 个音色,其中 **4 个没有补偿数据**。
|
|
226
224
|
# 两处各存一份清单必然漂移,所以这里直接读共享 JSON(不 import voicekit)。
|
|
227
225
|
_VOICE_LOUDNESS_PATH = os.path.normpath(
|
|
228
226
|
os.path.join(HERE, os.pardir, "assets", "voice_loudness.json"))
|
|
@@ -287,7 +285,7 @@ _STRUCT_KEYS = ("segments", "beats", "hold_policy", "target_sec")
|
|
|
287
285
|
def _theme_field_keys():
|
|
288
286
|
"""主题字段清单:从 _theme 构造器现场造一份样板再取键。
|
|
289
287
|
|
|
290
|
-
|
|
288
|
+
为什么不从某套内置主题取:内置主题表已经不存在,没有"参照物"。
|
|
291
289
|
也不手抄一份常量 —— 手抄的清单一定会在某次改构造器时漂移,
|
|
292
290
|
然后表现成"某个颜色字段取不到"这种半夜才能查出来的 KeyError。
|
|
293
291
|
"""
|
|
@@ -308,16 +306,16 @@ def _theme_field_keys():
|
|
|
308
306
|
|
|
309
307
|
|
|
310
308
|
# 模块加载时立刻算出来:它只是把构造器跑一遍取键名,没有副作用。
|
|
311
|
-
#
|
|
309
|
+
# (占位成空元组而忘了回填,所有 design 都会报"主题字段集合不一致"——
|
|
312
310
|
# 断言本身是对的,错在参照物是空的。)
|
|
313
311
|
_THEME_FIELD_KEYS = _theme_field_keys()
|
|
314
312
|
|
|
315
313
|
|
|
316
314
|
_VISUAL_KEYS = ("palette", "family", "typography", "motif", "transition", "motion",
|
|
317
315
|
"bias", "layout", "layout_cycle", "hud", "size", "fps", "subtitle")
|
|
318
|
-
#
|
|
319
|
-
#
|
|
320
|
-
#
|
|
316
|
+
# 字幕参数:字号 / 位置 / 背板 / 圆角若全写死在渲染器里,
|
|
317
|
+
# **任何两条片子**的字幕带都一模一样 —— 恰好是原则 5 说的"大面积长时间重复"。
|
|
318
|
+
# 这些参数进设计 DSL(可选,缺省 = 最朴素的字幕带,不写 subtitle 的 design.json 逐像素不变)。
|
|
321
319
|
_SUBTITLE_DEFAULTS = {
|
|
322
320
|
"size": 46, "y": None, "align": "center", "plate_alpha": 170,
|
|
323
321
|
"radius": 10, "family": None, "color": None, "max_lines": 2,
|
|
@@ -404,7 +402,7 @@ def _slug(text):
|
|
|
404
402
|
|
|
405
403
|
|
|
406
404
|
# ============================================================================
|
|
407
|
-
#
|
|
405
|
+
# 三、配色求解:色相锚点 + (s,v) 网格(实测唯一 100% 通过的做法)
|
|
408
406
|
# ============================================================================
|
|
409
407
|
|
|
410
408
|
|
|
@@ -430,14 +428,14 @@ def _solve_role(hue, s0, v0, need, refs):
|
|
|
430
428
|
|
|
431
429
|
|
|
432
430
|
def _solve_palette(family, hue_shift=0.0, target_shift=0.0):
|
|
433
|
-
"""
|
|
431
|
+
"""一个配色锚点 -> 11 个颜色(语义色全部求解;无解直接报错,不回落)。
|
|
434
432
|
|
|
435
|
-
hue_shift / target_shift
|
|
433
|
+
hue_shift / target_shift 是采样器给不同候选留的小扰动(确定性,幅度很小),
|
|
436
434
|
让"同族不同设计"也有色相差异;扰动不会改变可行性(网格里始终有 v=0/v=1 的端点)。
|
|
437
435
|
"""
|
|
438
|
-
# 只接受 (色相, 饱和度, 明度, 暗色)
|
|
439
|
-
#
|
|
440
|
-
# 所以"
|
|
436
|
+
# 只接受 (色相, 饱和度, 明度, 暗色),不接受命名族("deep-blue" 之类):
|
|
437
|
+
# design.json 里存的是**算好的 RGB**,从不存名字,
|
|
438
|
+
# 所以"design 带名字"这件事根本不存在——留着分支只会在别处误导人。
|
|
441
439
|
if not isinstance(family, (tuple, list)) or len(family) < 4:
|
|
442
440
|
raise ValueError("配色必须是 (色相, 饱和度, 明度, 暗色) 四元组;收到 %r" % (family,))
|
|
443
441
|
hue, sat, val = float(family[0]), float(family[1]), float(family[2])
|
|
@@ -505,7 +503,7 @@ def _safe_bias(bias):
|
|
|
505
503
|
|
|
506
504
|
|
|
507
505
|
def _theme_id(design_id):
|
|
508
|
-
# 设计 id 已经是 "design-xxxxxxxx"
|
|
506
|
+
# 设计 id 已经是 "design-xxxxxxxx";这里只做字符规整,不叠前缀。
|
|
509
507
|
return _slug(design_id)
|
|
510
508
|
|
|
511
509
|
|
|
@@ -554,7 +552,7 @@ def _theme_of_design(design):
|
|
|
554
552
|
dict(style=vis["hud"]),
|
|
555
553
|
qa,
|
|
556
554
|
)
|
|
557
|
-
#
|
|
555
|
+
# 主题的字段清单:没有内置主题可参照,从构造器现场造一份样板取键。
|
|
558
556
|
# 为什么还要求"字段完全一致":scenes.py 按固定键名取色,少一个键就是 KeyError,
|
|
559
557
|
# 多一个键则说明构造器与消费端脱节 —— 两边必须严丝合缝。
|
|
560
558
|
ref = set(_THEME_FIELD_KEYS)
|
|
@@ -599,7 +597,7 @@ _VISUAL_PROBE_ID = "__designkit_probe__"
|
|
|
599
597
|
|
|
600
598
|
|
|
601
599
|
def _visual_lint_language(lang):
|
|
602
|
-
"""对任意 language
|
|
600
|
+
"""对任意 language 字典做体检(直接调纯函数,不往 stylekit 的表里临时注册)。"""
|
|
603
601
|
return ST.visual_lint_language(lang)
|
|
604
602
|
|
|
605
603
|
|
|
@@ -718,14 +716,14 @@ def _validate_music(mu, bad):
|
|
|
718
716
|
mode = mu.get("mode")
|
|
719
717
|
if mode not in ("params", "file", "off"):
|
|
720
718
|
bad.append("audio.music.mode=%r 必须是 params / file / off" % (mode,))
|
|
721
|
-
#
|
|
719
|
+
# **不要求 style**,也不限定成任何内置名。
|
|
722
720
|
# 配乐由 musicgen 现场编排,它吃的是 bpm/key/scale/mood/seed + 可选的
|
|
723
|
-
# progressions/motif/drums/voices —— 没有"曲风"
|
|
724
|
-
#
|
|
725
|
-
# 避免"
|
|
721
|
+
# progressions/motif/drums/voices —— 没有"曲风"这个概念。
|
|
722
|
+
# design.json 里残留的 style 字段仍被接受(不校验取值),但不参与渲染,
|
|
723
|
+
# 避免"带了这个键的设计全部报错"。
|
|
726
724
|
_need(mu, ("scale", "bpm", "key", "mood", "seed"), "audio.music", bad)
|
|
727
725
|
if "style" in mu and not isinstance(mu["style"], str):
|
|
728
|
-
bad.append("audio.music.style
|
|
726
|
+
bad.append("audio.music.style 若存在必须是字符串(不参与渲染)")
|
|
729
727
|
if "scale" in mu:
|
|
730
728
|
_enum_of(mu["scale"], SCALES, "audio.music.scale", bad)
|
|
731
729
|
if "mood" in mu:
|
|
@@ -944,9 +942,9 @@ def validate_design(design):
|
|
|
944
942
|
elif vis["fps"] not in FPSES:
|
|
945
943
|
bad.append("visual.fps=%r 不在 %s 里" % (vis["fps"], FPSES))
|
|
946
944
|
|
|
947
|
-
#
|
|
948
|
-
# 不写 = 由 design_to_plan
|
|
949
|
-
#
|
|
945
|
+
# 版式轮换:片内按章节轮换版式,防"全片一张版式"。
|
|
946
|
+
# 不写 = 由 design_to_plan 按设计指纹自动派生一套轮换(plan 里没有 layout_cycle
|
|
947
|
+
# 时 grammar 仍走 layout_default,与不写这个字段等价)。
|
|
950
948
|
if "layout_cycle" in vis and vis["layout_cycle"] is not None:
|
|
951
949
|
cyc = vis["layout_cycle"]
|
|
952
950
|
if not isinstance(cyc, (list, tuple)) or not cyc:
|
|
@@ -960,7 +958,7 @@ def validate_design(design):
|
|
|
960
958
|
if len(set(str(x) for x in cyc)) < 2:
|
|
961
959
|
bad.append("visual.layout_cycle 至少要有 2 种不同版式(只有一种等于不轮换)")
|
|
962
960
|
|
|
963
|
-
#
|
|
961
|
+
# 字幕样式:可选;不写 = 最朴素字幕带的默认值(逐像素不变)。
|
|
964
962
|
sub = vis.get("subtitle")
|
|
965
963
|
if sub is not None:
|
|
966
964
|
if not isinstance(sub, dict):
|
|
@@ -1113,18 +1111,18 @@ def design_to_plan(design):
|
|
|
1113
1111
|
"theme": th,
|
|
1114
1112
|
"language": lang,
|
|
1115
1113
|
"layout_default": vis["layout"],
|
|
1116
|
-
#
|
|
1117
|
-
#
|
|
1114
|
+
# 片内版式轮换:grammar 在 chapter_layout 为空时按章节轮换这个序列。
|
|
1115
|
+
# 没有这个键(设计里不写 layout_cycle)时 grammar 走 layout_default。
|
|
1118
1116
|
"layout_cycle": list(vis.get("layout_cycle") or []),
|
|
1119
1117
|
"chapter_layout": {},
|
|
1120
|
-
#
|
|
1118
|
+
# 字幕样式:render/filmkit 只读这一份,不各自写默认值。
|
|
1121
1119
|
"subtitle": subtitle_of(design),
|
|
1122
1120
|
# 目标时长(可能为 null):只用于"实测 vs 目标"提示,不改变 TTS 决定的真实时长。
|
|
1123
1121
|
"target_sec": (None if design.get("structure", {}).get("target_sec") is None
|
|
1124
1122
|
else float(design["structure"]["target_sec"])),
|
|
1125
1123
|
"music": {
|
|
1126
|
-
#
|
|
1127
|
-
# 没有"曲风表"
|
|
1124
|
+
# 没有 "style" 这个键 —— 配乐由 musicgen 现场编排,
|
|
1125
|
+
# 没有"曲风表"可指向。设计里残留的 style 字段被忽略。
|
|
1128
1126
|
"scale": mu["scale"],
|
|
1129
1127
|
"bpm": float(mu["bpm"]),
|
|
1130
1128
|
"bpm_source": "design",
|
|
@@ -1148,11 +1146,11 @@ def design_to_plan(design):
|
|
|
1148
1146
|
"cps_band": (3.6, 5.6),
|
|
1149
1147
|
},
|
|
1150
1148
|
},
|
|
1151
|
-
# ----
|
|
1152
|
-
# design.audio.music.gain_db / duck_db 与 audio.sfx.*
|
|
1153
|
-
#
|
|
1154
|
-
# 仍出 8
|
|
1155
|
-
#
|
|
1149
|
+
# ---- 声音参数 ----
|
|
1150
|
+
# design.audio.music.gain_db / duck_db 与 audio.sfx.* **必须真的被读**:
|
|
1151
|
+
# 只校验不读,用户改了 design.json 里的音量/音效开关,渲染结果一模一样(实测
|
|
1152
|
+
# enabled=false 仍出 8 条音效)。所以原样交给 render,由它填进 script 的
|
|
1153
|
+
# music/sfx 缺省位(film.json 显式写了就仍以 film.json 为准)。
|
|
1156
1154
|
"audio_overrides": {
|
|
1157
1155
|
"music": {k: float(mu[k]) for k in ("gain_db", "duck_db") if mu.get(k) is not None},
|
|
1158
1156
|
"sfx": {k: (bool(au["sfx"][k]) if k == "enabled" else
|
|
@@ -1232,11 +1230,10 @@ def _voice_key(design):
|
|
|
1232
1230
|
def _music_key(design):
|
|
1233
1231
|
"""design -> 配乐身份("调式/速度档/情绪"复合键)。
|
|
1234
1232
|
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
现在用参数化配乐真正决定听感的三件事做签名:调式 + BPM 档(±6 归一)+ 情绪。
|
|
1233
|
+
不能读 audio.music.style:那是"配乐风格表"的字段,采样器不产出它,读了这一维
|
|
1234
|
+
就**恒为 None**,两个候选永远"配乐相同",SKILL §0.5 承诺的"候选至少在含配乐
|
|
1235
|
+
在内的 2 个维度上不同"会实际少一维(也是重采样失败率偏高的原因之一)。
|
|
1236
|
+
签名用参数化配乐真正决定听感的三件事:调式 + BPM 档(±6 归一)+ 情绪。
|
|
1240
1237
|
"""
|
|
1241
1238
|
mu = _dig(design, ("audio", "music")) or {}
|
|
1242
1239
|
if not isinstance(mu, dict):
|
|
@@ -1356,7 +1353,7 @@ _CONSTRAINT_WORDS = (
|
|
|
1356
1353
|
)
|
|
1357
1354
|
|
|
1358
1355
|
_CATEGORY_VALUES = {
|
|
1359
|
-
# 配色没有"可选值清单"
|
|
1356
|
+
# 配色没有"可选值清单"(连续色相空间),这里只保留空元组以兼容约束解析路径:
|
|
1360
1357
|
# brief 里写"深蓝"这类词会走 _CONSTRAINT_WORDS -> 被解释成色相区间(见 _resolve_constraints)。
|
|
1361
1358
|
"palette": (),
|
|
1362
1359
|
"motif": tuple(MOTIFS),
|
|
@@ -1434,7 +1431,7 @@ def _resolve_constraints(brief):
|
|
|
1434
1431
|
|
|
1435
1432
|
must {类别: (取值, 命中的词, 原句)}
|
|
1436
1433
|
avoid {类别: [(取值, 命中的词, 原句), ...]}
|
|
1437
|
-
domain {类别: 允许的取值元组;None =
|
|
1434
|
+
domain {类别: 允许的取值元组;None = 这一类没被约束(保持完整采样域)}
|
|
1438
1435
|
unmatched 一句都没认出来的原句(按出现顺序去重)——交给顶层提示用户"这条没生效"
|
|
1439
1436
|
"""
|
|
1440
1437
|
must_raw = [str(x) for x in (brief.get("must_have") or [])]
|
|
@@ -1538,10 +1535,10 @@ def _voices_for(genders):
|
|
|
1538
1535
|
|
|
1539
1536
|
|
|
1540
1537
|
def _palette_family_of(design):
|
|
1541
|
-
"""design ->
|
|
1538
|
+
"""design -> 配色分桶名(反查 bg 的色相与明暗;认不出返回 None)。
|
|
1542
1539
|
|
|
1543
1540
|
为什么反查而不是在 design 里多存一个 family 字段:design.json 要保持"自足",
|
|
1544
|
-
|
|
1541
|
+
配色一旦存了桶名,改桶就会让既有设计漂移;底色是采样时原样写进去的,反查无歧义。
|
|
1545
1542
|
"""
|
|
1546
1543
|
pal = _dig(design, ("visual", "palette"))
|
|
1547
1544
|
if not isinstance(pal, dict):
|
|
@@ -1675,7 +1672,7 @@ def _voice_gender_of(vid):
|
|
|
1675
1672
|
|
|
1676
1673
|
|
|
1677
1674
|
def _voice_gender(vid):
|
|
1678
|
-
"""pitch 文案用的单字:女 /
|
|
1675
|
+
"""pitch 文案用的单字:女 / 男(认不出的按男声写,输出口径固定)。"""
|
|
1679
1676
|
return "女" if _voice_gender_of(vid) == "female" else "男"
|
|
1680
1677
|
|
|
1681
1678
|
|
|
@@ -1692,7 +1689,7 @@ def _make_design(brief, index, seed, attempt, domain=None, unmatched=()):
|
|
|
1692
1689
|
cb = _brief_canon(brief)
|
|
1693
1690
|
ctext = _canonical(cb)
|
|
1694
1691
|
# nonce 是"这一次生成本身"的身份:默认每次调用新生成,传入 seed 时固定。
|
|
1695
|
-
# 它必须参与哈希,否则同一 brief
|
|
1692
|
+
# 它必须参与哈希,否则同一 brief 的第二次生成与第一次逐位相同。
|
|
1696
1693
|
nonce = brief.get("_nonce") if isinstance(brief, dict) else None
|
|
1697
1694
|
tag = "%s|%s" % ("" if seed is None else str(seed), nonce or "")
|
|
1698
1695
|
|
|
@@ -1700,7 +1697,7 @@ def _make_design(brief, index, seed, attempt, domain=None, unmatched=()):
|
|
|
1700
1697
|
return _digest(salt, ctext, tag, index, attempt)
|
|
1701
1698
|
|
|
1702
1699
|
dom = domain or {}
|
|
1703
|
-
# 配色 =
|
|
1700
|
+
# 配色 = 连续色相空间里的一个点(不是若干命名外观之一)。
|
|
1704
1701
|
# 色相按黄金角 137.5° 步进:相邻候选的色相必不接近(比固定步长更难撞色),
|
|
1705
1702
|
# 且整体分布均匀 —— 这就是"每条片底色都不同"的来源。
|
|
1706
1703
|
dark = h("dark") % 100 < 62 # 约 62% 暗色片(贴合"工程技术片"气质)
|
|
@@ -1748,10 +1745,10 @@ def _make_design(brief, index, seed, attempt, domain=None, unmatched=()):
|
|
|
1748
1745
|
align = ALIGNS[(h("align") + index) % len(ALIGNS)]
|
|
1749
1746
|
font_family = FONT_FAMILIES[(h("family") + index) % len(FONT_FAMILIES)]
|
|
1750
1747
|
|
|
1751
|
-
# ----
|
|
1752
|
-
#
|
|
1748
|
+
# ---- 片内版式轮换(原则 5)----
|
|
1749
|
+
# visual.layout 是单值 → grammar 会把它当"全片默认",
|
|
1753
1750
|
# 于是 8 段片子 8 屏都是同一个骨架(左标题右卡片 / 居中大字 / 右标题左卡片 里挑一个),
|
|
1754
|
-
# 观众看到的是"长时间重复"
|
|
1751
|
+
# 观众看到的是"长时间重复"。所以默认派生 2–3 种版式的轮换序列,
|
|
1755
1752
|
# 由 grammar 按**章节**轮换(同一章内稳定,换章才换版式)。
|
|
1756
1753
|
# 用户仍可在 design.visual.layout_cycle 里显式指定(会覆盖这里)。
|
|
1757
1754
|
_others = [x for x in LAYOUTS if x != layout]
|
|
@@ -1760,9 +1757,9 @@ def _make_design(brief, index, seed, attempt, domain=None, unmatched=()):
|
|
|
1760
1757
|
cycle_len = 2 if h("layout_n") % 2 == 0 else 3
|
|
1761
1758
|
layout_cycle = [layout] + _others[:max(0, cycle_len - 1)]
|
|
1762
1759
|
|
|
1763
|
-
# ----
|
|
1764
|
-
#
|
|
1765
|
-
# 是"跨片同质化"
|
|
1760
|
+
# ---- 字幕样式(原则 2/5)----
|
|
1761
|
+
# 字幕字号/位置/背板/圆角若是渲染器里的模块常量,任何两条片子字幕都一模一样,
|
|
1762
|
+
# 是"跨片同质化"里最稳定的一条重复。所以进设计(可选字段,写进 design.json 可改)。
|
|
1766
1763
|
# 取值范围都留在"好看"的区间内:字号 40–56、背板透明度 96–208、圆角 2–24。
|
|
1767
1764
|
sub_size = 40 + (h("subsize") % 5) * 4 # 40..56
|
|
1768
1765
|
sub_align = "center" if h("subalign") % 4 else "left" # 多数居中,偶尔左对齐
|
|
@@ -1880,10 +1877,10 @@ def _make_design(brief, index, seed, attempt, domain=None, unmatched=()):
|
|
|
1880
1877
|
def sample_designs(brief, n=3, seed=None):
|
|
1881
1878
|
"""现场采样 n 个两两可区分的 design(brief 的 must_have/avoid 在这里硬过滤)。
|
|
1882
1879
|
|
|
1883
|
-
**默认"每次都不一样"
|
|
1880
|
+
**默认"每次都不一样"**:
|
|
1884
1881
|
- 不传 seed -> 每次调用都生成一个**新的 nonce**,同一 brief 跑两次得到不同设计;
|
|
1885
1882
|
- 传 seed -> 固定 nonce,逐字节可复现(调试、回归、写文档示例时用)。
|
|
1886
|
-
|
|
1883
|
+
默认必须是"每次都不一样":若默认"同一 brief 永远同一结果",实测两次独立进程
|
|
1887
1884
|
产出的指纹完全相同(31f64edd34e502f9 / 52d385953835379e)——那等于把"没有模板"
|
|
1888
1885
|
做成了"模板藏在哈希里",用户要新片子只能改 brief 措辞。这不满足"每条视频都不一样"。
|
|
1889
1886
|
|
|
@@ -1938,16 +1935,15 @@ def sample_designs(brief, n=3, seed=None):
|
|
|
1938
1935
|
# 底色必须和已接受候选**拉开色相**。为什么单独管这一条:
|
|
1939
1936
|
# design_distance 会在"母题/转场/缓动/编号"上找到足够的差异,
|
|
1940
1937
|
# 于是两个候选可能共用同一片底色却仍被判为"可区分"。
|
|
1941
|
-
#
|
|
1938
|
+
# 实测样例:A=近黑金·电影遮幅 / B=近黑金·细线页眉 —— 并排图里第一眼
|
|
1942
1939
|
# 就是同一个片子的两种排法,用户不会认为那是两个方案。
|
|
1943
1940
|
# 底色是整屏第一眼信息,必须先分叉。
|
|
1944
1941
|
#
|
|
1945
|
-
#
|
|
1946
|
-
#
|
|
1947
|
-
#
|
|
1948
|
-
#
|
|
1949
|
-
#
|
|
1950
|
-
# 约束不再和判重打架,候选数量也不再受"分桶数"限制。
|
|
1942
|
+
# 判重直接比**底色色相距离**,不按 24° 分桶:有 must_have 配色约束时,
|
|
1943
|
+
# 可选色相被压到 ±26° 窗口里,24° 分桶只剩 2–3 档,n=3 要求 3 个不同族 →
|
|
1944
|
+
# 重采样 50 次成功率约一半;"暖纸色+女声"这类常见组合实测 20/20 全失败
|
|
1945
|
+
# (brief 本身完全合理)。比色相距离则窗口内只要彼此 ≥ _PALETTE_KEEP_GAP 度
|
|
1946
|
+
# 就算两片不同底,约束不与判重打架,候选数量也不受"分桶数"限制。
|
|
1951
1947
|
if out:
|
|
1952
1948
|
hue = _design_bg_hue(cand)
|
|
1953
1949
|
clash = None
|
|
@@ -1962,7 +1958,7 @@ def sample_designs(brief, n=3, seed=None):
|
|
|
1962
1958
|
if gap < _PALETTE_KEEP_GAP:
|
|
1963
1959
|
clash = (o["id"], gap)
|
|
1964
1960
|
break
|
|
1965
|
-
#
|
|
1961
|
+
# 分档判重:brief 把配色约束到窗口里时(如"深蓝"= 210°±26°),
|
|
1966
1962
|
# 52° 的窗口放不下"3 个两两差 12°"的底色 —— 硬要求会让 n=3 有近一半概率
|
|
1967
1963
|
# 采样失败(实测 19/40)。所以分两档:
|
|
1968
1964
|
# · 色相差 < 6°:无论别的维度多不同,并排看都是同一片底色 -> 必须换;
|
|
@@ -2011,9 +2007,9 @@ def _setup_stdio():
|
|
|
2011
2007
|
def _palette_rows(n=8):
|
|
2012
2008
|
"""演示用:从连续色相空间均匀取 n 个点,各自的解出结果。
|
|
2013
2009
|
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2010
|
+
配色是连续色相空间,没有"族"可列,但**还是得让人看见配色能长什么样**——
|
|
2011
|
+
所以均匀采样给你看,而不是给一份可选清单。这不是模板:打印出来的每个点
|
|
2012
|
+
都可以再调(h/s/v 都是连续量)。
|
|
2017
2013
|
"""
|
|
2018
2014
|
rows = []
|
|
2019
2015
|
for i in range(int(n)):
|
|
@@ -2052,10 +2048,9 @@ def main(argv=None):
|
|
|
2052
2048
|
a = ap.parse_args(argv)
|
|
2053
2049
|
|
|
2054
2050
|
if a.list_palettes:
|
|
2055
|
-
#
|
|
2056
|
-
#
|
|
2057
|
-
#
|
|
2058
|
-
# 现在按行里真实存在的键打印,并说明"这不是可选清单"。
|
|
2051
|
+
# 按 _palette_rows() 真实返回的键(hue/dark/ink_hex/...)打印,不要按老键名
|
|
2052
|
+
# (family/bg/accent_hue)取:键名对不上会 KeyError,而 --json 分支不读这些键、
|
|
2053
|
+
# 照样正常,自检容易漏掉。同时说明"这不是可选清单"。
|
|
2059
2054
|
rows = _palette_rows()
|
|
2060
2055
|
if a.json:
|
|
2061
2056
|
print(json.dumps(rows, ensure_ascii=False, indent=1))
|