dsh-motion 0.4.1 → 0.4.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.
- package/docs/performance-budget.md +2 -2
- package/lib/ops.js +19 -11
- package/package.json +1 -1
- package/styles/3b1b/theme.py +7 -8
- package/styles/README.md +1 -1
- package/styles/blueprint/theme.py +7 -8
- package/styles/dataviz/theme.py +7 -8
- package/styles/swiss-motion/theme.py +7 -8
- package/styles/whiteboard/theme.py +7 -8
|
@@ -45,8 +45,8 @@
|
|
|
45
45
|
|
|
46
46
|
已完成:
|
|
47
47
|
|
|
48
|
-
- [x] `
|
|
49
|
-
- [x] post 强制 CFR
|
|
48
|
+
- [x] 格式契约:`apply_config()` 只设背景色,不再覆盖分辨率/帧率;`motion_render` 的 `width/height/fps` 与 CLI 的 `-r/--fps` 生效;
|
|
49
|
+
- [x] CFR:post 强制 CFR;所有成片按 `nb_read_frames` 对比期望帧数,少帧时用 `tpad + CFR` 补齐并复验;
|
|
50
50
|
- [x] storyboard 顶部 caption 条加提示,避免误判“裁切”。
|
|
51
51
|
|
|
52
52
|
明确不做:
|
package/lib/ops.js
CHANGED
|
@@ -106,7 +106,7 @@ export async function renderScene(config, args) {
|
|
|
106
106
|
|
|
107
107
|
// post/ffmpeg 可能因 tmix/丢帧把时长压短;用 CFR 补齐并校验,避免"30s 变 27.4s"。
|
|
108
108
|
if (summary.durationSec) {
|
|
109
|
-
const fixed = await
|
|
109
|
+
const fixed = await ensureCfr(config, summary, fps)
|
|
110
110
|
if (fixed) summary.normalized = true
|
|
111
111
|
}
|
|
112
112
|
|
|
@@ -114,22 +114,25 @@ export async function renderScene(config, args) {
|
|
|
114
114
|
return summary
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
/**
|
|
118
|
-
async function
|
|
117
|
+
/** 成片时长或帧数不对(VFR 少帧)时,用 tpad + CFR 修正;返回是否修过。 */
|
|
118
|
+
async function ensureCfr(config, summary, fps) {
|
|
119
119
|
const expected = Number(summary.durationSec)
|
|
120
120
|
if (!Number.isFinite(expected) || expected <= 0) return false
|
|
121
121
|
const file = summary.output
|
|
122
122
|
const before = await probeOutput(config, file)
|
|
123
|
-
const
|
|
124
|
-
|
|
123
|
+
const expectedFrames = Math.round(expected * fps)
|
|
124
|
+
const frameGap = Number.isFinite(before.frames) ? Math.abs(before.frames - expectedFrames) : 0
|
|
125
|
+
const durationGap = Math.abs(expected - before.durationSec)
|
|
126
|
+
if (frameGap <= 1 && durationGap <= 0.08) return false
|
|
125
127
|
|
|
128
|
+
const delta = expected - before.durationSec
|
|
126
129
|
const tmp = `${file}.cfr.mp4`
|
|
127
130
|
const chain = []
|
|
128
131
|
if (delta > 0.05) chain.push(`tpad=stop_mode=clone:stop_duration=${delta.toFixed(3)}`)
|
|
129
132
|
chain.push(`fps=${fps}`)
|
|
130
133
|
const args = [
|
|
131
134
|
'-y', '-loglevel', 'error', '-i', file,
|
|
132
|
-
'-vf', chain.join(','), '-fps_mode', 'cfr',
|
|
135
|
+
'-vf', chain.join(','), '-fps_mode', 'cfr', '-r', String(fps),
|
|
133
136
|
'-c:v', 'libx264', '-crf', '18', '-pix_fmt', 'yuv420p', '-an',
|
|
134
137
|
]
|
|
135
138
|
if (delta < -0.05) args.push('-t', expected.toFixed(3))
|
|
@@ -137,8 +140,9 @@ async function ensureDuration(config, summary, fps) {
|
|
|
137
140
|
await exec(binaryOf(config, 'ffmpeg'), args, { timeout: 900_000, maxBuffer: 8 * 1024 * 1024 })
|
|
138
141
|
|
|
139
142
|
const after = await probeOutput(config, tmp)
|
|
140
|
-
|
|
141
|
-
|
|
143
|
+
const afterGap = Number.isFinite(after.frames) ? Math.abs(after.frames - expectedFrames) : 0
|
|
144
|
+
if (afterGap > 1 || Math.abs(after.durationSec - expected) > 0.05) {
|
|
145
|
+
throw new Error(`CFR 修正后仍不对:期望 ${expectedFrames} 帧 / ${expected}s,实际 ${after.frames} 帧 / ${after.durationSec}s(${tmp})`)
|
|
142
146
|
}
|
|
143
147
|
renameSync(tmp, file)
|
|
144
148
|
Object.assign(summary, after)
|
|
@@ -348,10 +352,10 @@ export async function probeVideo(config, args) {
|
|
|
348
352
|
/** 用 ffprobe 重新读成片参数(post 之后帧率、时长、体积都可能与中间产物不同)。 */
|
|
349
353
|
export async function probeOutput(config, file) {
|
|
350
354
|
const { stdout } = await exec(binaryOf(config, 'ffprobe'), [
|
|
351
|
-
'-v', 'error', '-select_streams', 'v:0',
|
|
352
|
-
'-show_entries', 'stream=width,height,r_frame_rate:format=duration,size',
|
|
355
|
+
'-v', 'error', '-count_frames', '-select_streams', 'v:0',
|
|
356
|
+
'-show_entries', 'stream=width,height,r_frame_rate,avg_frame_rate,nb_read_frames:format=duration,size',
|
|
353
357
|
'-of', 'json', file,
|
|
354
|
-
], { timeout:
|
|
358
|
+
], { timeout: 120_000, maxBuffer: 4 * 1024 * 1024 })
|
|
355
359
|
const data = JSON.parse(stdout)
|
|
356
360
|
const stream = data.streams?.[0] ?? {}
|
|
357
361
|
const [num, den = '1'] = String(stream.r_frame_rate ?? '0/1').split('/')
|
|
@@ -363,6 +367,10 @@ export async function probeOutput(config, file) {
|
|
|
363
367
|
width: stream.width ?? null,
|
|
364
368
|
height: stream.height ?? null,
|
|
365
369
|
fps: Number.isFinite(rate) && rate > 0 ? Math.round(rate * 100) / 100 : null,
|
|
370
|
+
avgFps: Number.isFinite(Number(stream.avg_frame_rate?.split?.('/')[0]))
|
|
371
|
+
? Math.round((Number(stream.avg_frame_rate.split('/')[0]) / Number(stream.avg_frame_rate.split('/')[1] ?? 1)) * 100) / 100
|
|
372
|
+
: null,
|
|
373
|
+
frames: Number.isFinite(Number(stream.nb_read_frames)) ? Number(stream.nb_read_frames) : null,
|
|
366
374
|
megabytes: Number.isFinite(size) ? Math.round(size / 1e5) / 10 : null,
|
|
367
375
|
}
|
|
368
376
|
}
|
package/package.json
CHANGED
package/styles/3b1b/theme.py
CHANGED
|
@@ -13,9 +13,6 @@ import manimgx as m
|
|
|
13
13
|
|
|
14
14
|
TOKENS = json.loads(Path(__file__).with_name("tokens.json").read_text(encoding="utf-8"))
|
|
15
15
|
|
|
16
|
-
# inspect 在 import 时读 config.frame_rate;必须放在模块层,不能只在 construct 里。
|
|
17
|
-
m.config.frame_rate = int(TOKENS["motion"]["fps"])
|
|
18
|
-
|
|
19
16
|
BG = TOKENS["colors"]["bg"]
|
|
20
17
|
TEXT = TOKENS["colors"]["text"]
|
|
21
18
|
LABEL = TOKENS["colors"]["label"]
|
|
@@ -30,11 +27,13 @@ AXIS = TOKENS["colors"]["axis"]
|
|
|
30
27
|
LAYOUT = TOKENS["layout"]
|
|
31
28
|
|
|
32
29
|
|
|
33
|
-
def apply_config(
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
30
|
+
def apply_config(background: str = BG) -> None:
|
|
31
|
+
"""只设背景色;分辨率/帧率由 CLI(-r / --fps)或调用方决定。
|
|
32
|
+
|
|
33
|
+
不要在场景里写死 1920x1080/30:那会覆盖 motion_render 的 width/height/fps,
|
|
34
|
+
也是 1280x720 崩溃与 --fps 60 静默变 5s 的根因。
|
|
35
|
+
"""
|
|
36
|
+
m.config.background_color = background
|
|
38
37
|
|
|
39
38
|
|
|
40
39
|
def backdrop() -> m.Rectangle:
|
package/styles/README.md
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
- `STYLE.md` 是**改编**,必须在上游基础上写清楚差异;不复制 demo 代码;
|
|
19
19
|
- 颜色、字体、时间表、音频以 `tokens.json` 为准;版面坐标与组件配方写在 `STYLE.md`;
|
|
20
20
|
- 中文文本不做字符级 `ReplacementTransform` / `Transform`(会出字形碎片);用 Fade/Write/Indicate,换词用 FadeOut+FadeIn;
|
|
21
|
-
-
|
|
21
|
+
- **不要在场景/`theme.py` 里写死 1920×1080 / fps**:`apply_config()` 只设背景色;分辨率与帧率交给 `motion_render` 的 `width/height/fps` 或 CLI 的 `-r/--fps`。写死会覆盖 CLI,导致 `-r 1280x720` 崩溃、`--fps 60` 静默变 5s;
|
|
22
22
|
- 路由:`styles/registry.json` 是元数据,`node scripts/route-style.mjs "<题材>"` 打分;点名优先,低置信度/并列先问用户;
|
|
23
23
|
- 新片在自己的项目目录写 `DESIGN.md`,头部 `style: <slug>` + `style_version: 1.0.0`;
|
|
24
24
|
- 参考与示例统一用 HyperFrames 的 [examples](https://hyperframes.heygen.com/examples) / [catalog](https://hyperframes.heygen.com/catalog),不在仓库里放样片(`showcase/` 只放可复现的演示)。
|
|
@@ -15,9 +15,6 @@ import manimgx as m
|
|
|
15
15
|
|
|
16
16
|
TOKENS = json.loads(Path(__file__).with_name("tokens.json").read_text(encoding="utf-8"))
|
|
17
17
|
|
|
18
|
-
# inspect 在 import 时读 config.frame_rate;必须放在模块层,不能只在 construct 里。
|
|
19
|
-
m.config.frame_rate = int(TOKENS["motion"]["fps"])
|
|
20
|
-
|
|
21
18
|
TABLE = TOKENS["colors"]["table"]
|
|
22
19
|
PAPER = TOKENS["colors"]["paper"]
|
|
23
20
|
PAPER_LIGHT = TOKENS["colors"]["paper_light"]
|
|
@@ -27,11 +24,13 @@ STAMP = TOKENS["colors"]["stamp"]
|
|
|
27
24
|
LAYOUT = TOKENS["layout"]
|
|
28
25
|
|
|
29
26
|
|
|
30
|
-
def apply_config(
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
27
|
+
def apply_config(background: str = TABLE) -> None:
|
|
28
|
+
"""只设背景色;分辨率/帧率由 CLI(-r / --fps)或调用方决定。
|
|
29
|
+
|
|
30
|
+
不要在场景里写死 1920x1080/30:那会覆盖 motion_render 的 width/height/fps,
|
|
31
|
+
也是 1280x720 崩溃与 --fps 60 静默变 5s 的根因。
|
|
32
|
+
"""
|
|
33
|
+
m.config.background_color = background
|
|
35
34
|
|
|
36
35
|
|
|
37
36
|
def backdrop(seed: int = 11) -> m.ImageMobject:
|
package/styles/dataviz/theme.py
CHANGED
|
@@ -13,9 +13,6 @@ import manimgx as m
|
|
|
13
13
|
|
|
14
14
|
TOKENS = json.loads(Path(__file__).with_name("tokens.json").read_text(encoding="utf-8"))
|
|
15
15
|
|
|
16
|
-
# inspect 在 import 时读 config.frame_rate;必须放在模块层,不能只在 construct 里。
|
|
17
|
-
m.config.frame_rate = int(TOKENS["motion"]["fps"])
|
|
18
|
-
|
|
19
16
|
PAPER = TOKENS["colors"]["paper"]
|
|
20
17
|
PAPER_SHADOW = TOKENS["colors"]["paper_shadow"]
|
|
21
18
|
INK = TOKENS["colors"]["ink"]
|
|
@@ -28,11 +25,13 @@ MONO = TOKENS["typography"]["mono"]["family"]
|
|
|
28
25
|
LAYOUT = TOKENS["layout"]
|
|
29
26
|
|
|
30
27
|
|
|
31
|
-
def apply_config(
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
28
|
+
def apply_config(background: str = PAPER) -> None:
|
|
29
|
+
"""只设背景色;分辨率/帧率由 CLI(-r / --fps)或调用方决定。
|
|
30
|
+
|
|
31
|
+
不要在场景里写死 1920x1080/30:那会覆盖 motion_render 的 width/height/fps,
|
|
32
|
+
也是 1280x720 崩溃与 --fps 60 静默变 5s 的根因。
|
|
33
|
+
"""
|
|
34
|
+
m.config.background_color = background
|
|
36
35
|
|
|
37
36
|
|
|
38
37
|
def backdrop() -> m.Rectangle:
|
|
@@ -13,9 +13,6 @@ import manimgx as m
|
|
|
13
13
|
|
|
14
14
|
TOKENS = json.loads(Path(__file__).with_name("tokens.json").read_text(encoding="utf-8"))
|
|
15
15
|
|
|
16
|
-
# inspect 在 import 时读 config.frame_rate;必须放在模块层,不能只在 construct 里。
|
|
17
|
-
m.config.frame_rate = int(TOKENS["motion"]["fps"])
|
|
18
|
-
|
|
19
16
|
GROUND = TOKENS["colors"]["ground"]
|
|
20
17
|
PAPER = TOKENS["colors"]["paper"]
|
|
21
18
|
INK = TOKENS["colors"]["ink"]
|
|
@@ -27,11 +24,13 @@ MOTION = TOKENS["motion"]
|
|
|
27
24
|
LAYOUT = TOKENS["layout"]
|
|
28
25
|
|
|
29
26
|
|
|
30
|
-
def apply_config(
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
27
|
+
def apply_config(background: str = GROUND) -> None:
|
|
28
|
+
"""只设背景色;分辨率/帧率由 CLI(-r / --fps)或调用方决定。
|
|
29
|
+
|
|
30
|
+
不要在场景里写死 1920x1080/30:那会覆盖 motion_render 的 width/height/fps,
|
|
31
|
+
也是 1280x720 崩溃与 --fps 60 静默变 5s 的根因。
|
|
32
|
+
"""
|
|
33
|
+
m.config.background_color = background
|
|
35
34
|
|
|
36
35
|
|
|
37
36
|
def backdrop() -> m.Rectangle:
|
|
@@ -15,9 +15,6 @@ import manimgx as m
|
|
|
15
15
|
|
|
16
16
|
TOKENS = json.loads(Path(__file__).with_name("tokens.json").read_text(encoding="utf-8"))
|
|
17
17
|
|
|
18
|
-
# inspect 在 import 时读 config.frame_rate;必须放在模块层,不能只在 construct 里。
|
|
19
|
-
m.config.frame_rate = int(TOKENS["motion"]["fps"])
|
|
20
|
-
|
|
21
18
|
BOARD = TOKENS["colors"]["board"]
|
|
22
19
|
BOARD_EDGE = TOKENS["colors"]["board_edge"]
|
|
23
20
|
INK = TOKENS["colors"]["ink"]
|
|
@@ -28,11 +25,13 @@ GHOST = TOKENS["colors"]["ghost"]
|
|
|
28
25
|
LAYOUT = TOKENS["layout"]
|
|
29
26
|
|
|
30
27
|
|
|
31
|
-
def apply_config(
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
28
|
+
def apply_config(background: str = BOARD) -> None:
|
|
29
|
+
"""只设背景色;分辨率/帧率由 CLI(-r / --fps)或调用方决定。
|
|
30
|
+
|
|
31
|
+
不要在场景里写死 1920x1080/30:那会覆盖 motion_render 的 width/height/fps,
|
|
32
|
+
也是 1280x720 崩溃与 --fps 60 静默变 5s 的根因。
|
|
33
|
+
"""
|
|
34
|
+
m.config.background_color = background
|
|
36
35
|
|
|
37
36
|
|
|
38
37
|
def backdrop(seed: int = 5) -> m.ImageMobject:
|