@dsh-gis/dsh-video-studio-skill 0.6.0 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -16
- package/index.js +20 -7
- package/package.json +4 -2
- package/schemas/film.schema.json +75 -4
- package/skills/video-studio/SKILL.md +95 -23
- package/skills/video-studio/assets/film.json +7 -14
- package/skills/video-studio/assets/scenes.py +115 -10
- package/skills/video-studio/assets/web/engine.js +18 -1
- package/skills/video-studio/assets/web/film.js +29 -4
- package/skills/video-studio/assets/web/index.html +5 -0
- package/skills/video-studio/references/audio-variation.md +158 -0
- package/skills/video-studio/references/audio.md +67 -5
- package/skills/video-studio/references/change-impact.md +1 -1
- package/skills/video-studio/references/quality-gates.md +1 -1
- package/skills/video-studio/references/web-engine.md +1 -1
- package/skills/video-studio/requirements.txt +2 -1
- package/skills/video-studio/scripts/filmcheck.py +81 -12
- package/skills/video-studio/scripts/filmkit.py +271 -18
- package/skills/video-studio/scripts/gates.py +75 -12
- package/skills/video-studio/scripts/qa_frames.py +109 -19
- package/skills/video-studio/scripts/render.py +420 -51
- package/skills/video-studio/scripts/scorekit.py +289 -46
- package/skills/video-studio/scripts/selfcheck.py +302 -2
- package/skills/video-studio/scripts/sfxkit.py +132 -30
- package/skills/video-studio/scripts/stagewatch.py +141 -31
- package/skills/video-studio/scripts/stylekit.py +94 -6
- package/skills/video-studio/scripts/voicekit.py +8 -0
- package/skills/video-studio/scripts/webengine.py +68 -14
- package/tools/audio_similarity.py +279 -0
- package/tools/ci_check.py +145 -34
- package/tools/estimate.py +133 -54
- package/tools/selftest_noscipy.py +70 -0
package/tools/estimate.py
CHANGED
|
@@ -40,23 +40,46 @@ import unicodedata
|
|
|
40
40
|
from pathlib import Path
|
|
41
41
|
|
|
42
42
|
# ============================================================================
|
|
43
|
-
#
|
|
43
|
+
# 基准常量(v0.7.2 重新校准:换机请重测后回填,别把这里的数字当通用真理)
|
|
44
44
|
# ============================================================================
|
|
45
45
|
|
|
46
46
|
W, H, FPS = 1920, 1080, 30 # filmkit.W/H/FPS:本项目写死,film.json 改不了
|
|
47
|
-
PIL_FPS = (40.0, 80.0) # PIL
|
|
47
|
+
PIL_FPS = (40.0, 80.0) # PIL 逐帧渲染速度:中等复杂度 40–54 fps / 模板片 68–82 fps
|
|
48
|
+
PARALLEL_EFF = 0.5 # 分块并行的**实测效率**:2 进程只快 1.5×,不是 2×(见 CALIBRATION)
|
|
48
49
|
WEB_SEC_PER_FRAME = (0.080, 0.130) # web 引擎:Edge 无头 PNG 截图 80–130 ms/帧
|
|
49
50
|
WEB_STARTUP_SEC = 6.0 # 浏览器冷启动 + CDP 连接(12 s 片 28–30 s 含启动)
|
|
50
|
-
X264_FPS = 141.0 # libx264 -preset medium -crf 17 ≈ 4.7×
|
|
51
|
-
TTS_SEC_PER_LINE = 4.0
|
|
52
|
-
SCORE_REFS = ((
|
|
51
|
+
X264_FPS = 141.0 # libx264 -preset medium -crf 17 ≈ 4.7× 实时(仅 reencode_video 用)
|
|
52
|
+
TTS_SEC_PER_LINE = (2.0, 4.0) # edge-tts:每句每 worker 的墙钟(联网,波动大):实测 2.2 s,留到 4.0
|
|
53
|
+
SCORE_REFS = ((29.0, 3.6, 6.5), (58.0, 4.5, 6.5), (389.0, 8.0, 18.0))
|
|
54
|
+
# ^ (片长 s, 耗时低, 耗时高):前两个点是本机实测(29 s / 58 s 各 3 次),
|
|
55
|
+
# 第三个点是片长外推锚点(不是本机实测),只用来保证长片不会被系统性算小
|
|
53
56
|
SFX_SEC_PER_CUE = 0.20 # 单条短音效 合成+落位(24 配方单次 ≤1.5 s 是上限口径)
|
|
54
|
-
SFX_MIN_SEC = 0.
|
|
55
|
-
LAYOUT_BASE, LAYOUT_PER_LINE = 0.
|
|
56
|
-
CUES_BASE, CUES_PER_LINE = 0.
|
|
57
|
+
SFX_MIN_SEC = 0.8 # 实测 18 条 cue 的模板片 sfx 墙钟 0.8 s(含 ffmpeg 写盘)
|
|
58
|
+
LAYOUT_BASE, LAYOUT_PER_LINE = 0.05, 0.01 # 排轴 + srt/ass + 安全区/字形自查(实测 29 s 片 <0.15 s)
|
|
59
|
+
CUES_BASE, CUES_PER_LINE = 0.05, 0.005 # 提示点落盘 + 排序(实测 29 s 片 <0.15 s)
|
|
57
60
|
AUDIO_BASE, AUDIO_PER_SEC = 0.6, 0.012 # numpy 总线按采样点叠加 + 写 wav
|
|
58
|
-
|
|
59
|
-
|
|
61
|
+
# mux 默认是 -c:v copy + 两遍 loudnorm:实测 29 s 片 1.7–1.9 s,与帧数无关、跟片长弱相关。
|
|
62
|
+
MUX_BASE_SEC = 0.6 # 起 ffmpeg 两遍 + 封装
|
|
63
|
+
MUX_PER_SEC = 0.045 # 每片长秒(两遍 loudnorm 解码音频)
|
|
64
|
+
# QA:实测 29 s 片 6.2–6.8 s(含联系表 + 锐度参考帧重渲染 + 帧级深检)。原来的 1/30 把 qa 算了 6 倍小。
|
|
65
|
+
QA_BASE_SEC = (1.0, 3.0) # 固定开销:联系表 + 参考帧重渲染
|
|
66
|
+
QA_SEC_PER_SEC = (0.12, 0.25) # 随片长:帧级深检流式解码 + 响度曲线
|
|
67
|
+
UNMEASURED_BAND = 0.30 # 没有实测的阶段(layout/cues/sfx/audio)给 ±30% 的区间,别假装精确
|
|
68
|
+
|
|
69
|
+
# 校准出处:换成你自己的机器前,请按 SKILL.md §12 重测并回填——这些数字决定预演准不准。
|
|
70
|
+
CALIBRATION = dict(
|
|
71
|
+
machine=("AMD64 Family 26 Model 36 Stepping 0(24 逻辑核)/ Windows;"
|
|
72
|
+
"SKILL.md §12 记录的作者机是 Ryzen AI 9 HX 370 / 12C24T / Radeon 890M"
|
|
73
|
+
"(是否同一台未核实,只保证这些数字是在上述 24 逻辑核机器上测的)"),
|
|
74
|
+
measured_at="2026-11-10",
|
|
75
|
+
sdk="video-studio v0.7.1 scripts(ffmpeg 自带 x264)",
|
|
76
|
+
film="29.011 s / 870 帧 / 模板片(assets/film.json + assets/scenes.py)",
|
|
77
|
+
method="render.py --stage all --jobs 2,逐 stage 取 [time] 行墙钟;同片重复 2 次",
|
|
78
|
+
observed=dict(tts=2.2, layout=0.0, cues=0.0, score=6.3, sfx=0.8, audio=0.7,
|
|
79
|
+
frames=9.7, mux=1.9, qa=6.8, total=28.4),
|
|
80
|
+
extra="frames: 870 帧 jobs=1 14.37 s / jobs=2 9.53 s(并行效率 1.51×);"
|
|
81
|
+
" score 58 s 片 4.5–5.0 s; qa 6.2–6.8 s",
|
|
82
|
+
)
|
|
60
83
|
CPS = 4.2 # 每秒约 4.2 字(voicekit 校准 3.84–4.61 cps 的中位口径)
|
|
61
84
|
DEFAULT_LEAD, DEFAULT_TAIL, DEFAULT_MIN_DUR = 0.62, 0.92, 3.0 # v0.5 body 基准
|
|
62
85
|
CORES = 12 # HX 370:12C/24T;jobs > 12 不再线性加速
|
|
@@ -65,8 +88,8 @@ WAV_BYTES_PER_SEC = 48000 * 2 * 2 # 48 kHz 立体声 PCM16 = 192 KB/s
|
|
|
65
88
|
VIDEO_MBPS = (1.2, 3.0) # crf17 @1080p30 图文内容(暗底大字 0.5–1 Mbps 属正常)
|
|
66
89
|
AUDIO_MBPS = 0.192 # AAC 192 kbps
|
|
67
90
|
QA_DISK_BYTES = 12 * 1024 * 1024 # 联系表 + 静帧 + deep.png
|
|
68
|
-
BASELINE_NOTE = ("
|
|
69
|
-
"
|
|
91
|
+
BASELINE_NOTE = ("区间来自 v0.7.2 本机实测(机型/样本见下方 calibration;"
|
|
92
|
+
"SKILL.md §12 记录的作者机是 Ryzen AI 9 HX 370 / 12C24T);换机请先重测再采信")
|
|
70
93
|
|
|
71
94
|
STAGE_LABELS = {
|
|
72
95
|
"tts": "旁白 edge-tts",
|
|
@@ -245,25 +268,53 @@ def score_seconds(total):
|
|
|
245
268
|
return max(0.3, lo), max(0.5, hi)
|
|
246
269
|
|
|
247
270
|
|
|
248
|
-
def
|
|
249
|
-
"""
|
|
271
|
+
def _speedup(jobs):
|
|
272
|
+
"""分块并行的**有效加速比**:不是 jobs 倍。
|
|
250
273
|
|
|
251
|
-
|
|
252
|
-
|
|
274
|
+
实测(870 帧模板片):jobs=1 → 14.37 s、jobs=2 → 9.53 s,只有 1.51×。
|
|
275
|
+
原因很实在:父进程要 spawn 子进程、每段都要重新 import scenes.py / 解析主题、
|
|
276
|
+
落盘 mux 串行、磁盘写带宽争用。按 jobs 线性折算会持续低估长片(越长的片分块越多)。
|
|
277
|
+
这里用 1 + (jobs-1)*PARALLEL_EFF 的线性模型:比理论值保守,实测 2 进程刚好对上。
|
|
253
278
|
"""
|
|
254
279
|
jobs_eff = max(1, min(int(jobs), CORES))
|
|
280
|
+
return 1.0 + (jobs_eff - 1) * PARALLEL_EFF
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def frames_seconds(frames, jobs, engine, rerender):
|
|
284
|
+
"""frames 阶段墙钟(只算画面生成;不重编码时封装成本在 mux 行)。
|
|
285
|
+
|
|
286
|
+
PIL:帧数 ÷ 渲染 fps ÷ 有效加速比;web:启动开销 + 帧数 × 每帧截图耗时 ÷ 加速比。
|
|
287
|
+
render.py 的分块 worker 是边渲染边编码,所以这里的 fps 已经含了编码开销。
|
|
288
|
+
"""
|
|
289
|
+
sp = _speedup(jobs)
|
|
255
290
|
if engine == "web":
|
|
256
|
-
base = [WEB_STARTUP_SEC + frames * spf /
|
|
291
|
+
base = [WEB_STARTUP_SEC + frames * spf / sp for spf in WEB_SEC_PER_FRAME]
|
|
257
292
|
else:
|
|
258
|
-
base = [frames / rfps /
|
|
293
|
+
base = [frames / rfps / sp for rfps in PIL_FPS]
|
|
259
294
|
base = sorted(base)
|
|
260
295
|
return base[0] * rerender, base[-1] * rerender
|
|
261
296
|
|
|
262
297
|
|
|
263
|
-
def
|
|
264
|
-
"""
|
|
265
|
-
|
|
266
|
-
|
|
298
|
+
def mux_seconds_pair(frames, jobs, total, script):
|
|
299
|
+
"""mux 阶段区间:默认 -c:v copy 只跟片长有关;reencode_video=true 才跟帧数有关。"""
|
|
300
|
+
reencode = bool(script.get("reencode_video", False))
|
|
301
|
+
if reencode:
|
|
302
|
+
v = frames / X264_FPS / _speedup(jobs)
|
|
303
|
+
return v * 0.8, v * 1.6
|
|
304
|
+
base = MUX_BASE_SEC + MUX_PER_SEC * max(0.0, float(total))
|
|
305
|
+
return base * 0.75, base * 1.35
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def mux_seconds(frames, jobs, total, reencode=False):
|
|
309
|
+
"""mux 阶段墙钟。
|
|
310
|
+
|
|
311
|
+
默认 `-c:v copy` + 两遍 loudnorm:视频流不解码不重编码,成本 ≈ 起 ffmpeg + 按片长
|
|
312
|
+
解码音频跑两遍。实测 29 s 片 1.7–1.9 s。
|
|
313
|
+
reencode_video=true 时才要重编视频,那时才用 x264 速率并按有效加速比摊。
|
|
314
|
+
"""
|
|
315
|
+
if reencode:
|
|
316
|
+
return frames / X264_FPS / _speedup(jobs)
|
|
317
|
+
return MUX_BASE_SEC + MUX_PER_SEC * max(0.0, float(total))
|
|
267
318
|
|
|
268
319
|
|
|
269
320
|
def chunk_plan(frames, fps, jobs, explicit):
|
|
@@ -390,49 +441,63 @@ def build_report(script_path, script, args):
|
|
|
390
441
|
preset = script.get("preset", "medium")
|
|
391
442
|
|
|
392
443
|
# ---- 各阶段 ----
|
|
444
|
+
# 没有实测的阶段一律给区间(正负 UNMEASURED_BAND),别把经验公式输出当精确值。
|
|
445
|
+
def _band(v):
|
|
446
|
+
# 下界永远让它摸到 0:这些阶段实测常常 <0.1 s(render.py 的 [time] 行按 0.1 s 打印,
|
|
447
|
+
# 于是显示 0.0),下界写成正值会让"实测值落在区间外"这种假告警出现。
|
|
448
|
+
return min(0.0, v * (1.0 - UNMEASURED_BAND)), v * (1.0 + UNMEASURED_BAND)
|
|
393
449
|
workers = int(_num(script.get("tts_workers", 4), 4) or 4)
|
|
394
450
|
workers = max(1, min(workers, max(1, n)))
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
451
|
+
# TTS 按每句每 worker 摊:实测 4 句 / 4 workers = 2.2 s(串行口径 8.8 s)。
|
|
452
|
+
tts_lo = n * TTS_SEC_PER_LINE[0] / float(workers)
|
|
453
|
+
tts_hi = n * TTS_SEC_PER_LINE[1] / float(workers)
|
|
454
|
+
layout_lo, layout_hi = _band(LAYOUT_BASE + LAYOUT_PER_LINE * n)
|
|
455
|
+
cues_lo, cues_hi = _band(CUES_BASE + CUES_PER_LINE * n)
|
|
398
456
|
if score_on:
|
|
399
457
|
score_lo, score_hi = score_seconds(total)
|
|
400
458
|
else:
|
|
401
459
|
score_lo = score_hi = 0.0
|
|
402
|
-
|
|
403
|
-
|
|
460
|
+
sfx_lo, sfx_hi = _band(max(SFX_MIN_SEC, n_cues * SFX_SEC_PER_CUE)) if sfx_on else (0.0, 0.0)
|
|
461
|
+
audio_lo, audio_hi = _band(AUDIO_BASE + total * AUDIO_PER_SEC)
|
|
404
462
|
fr_lo, fr_hi = frames_seconds(frames, jobs_eff, engine, rerender)
|
|
405
|
-
|
|
406
|
-
|
|
463
|
+
mux_lo, mux_hi = mux_seconds_pair(frames, jobs_eff, total, script)
|
|
464
|
+
qa_lo = max(0.5, QA_BASE_SEC[0] + total * QA_SEC_PER_SEC[0])
|
|
465
|
+
qa_hi = max(qa_lo, QA_BASE_SEC[1] + total * QA_SEC_PER_SEC[1])
|
|
407
466
|
|
|
408
467
|
stages = [
|
|
409
468
|
dict(stage="tts", lo=tts_lo, hi=tts_hi, measured=True,
|
|
410
|
-
note=("%d 句
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
469
|
+
note=("%d 句 ÷ tts_workers=%d,每句每 worker %.1f–%.1f s(实测 2.2 s/4 句;"
|
|
470
|
+
"需联网,edge-tts 免费端点波动大)"
|
|
471
|
+
% (n, workers, TTS_SEC_PER_LINE[0], TTS_SEC_PER_LINE[1]))),
|
|
472
|
+
dict(stage="layout", lo=layout_lo, hi=layout_hi, measured=False,
|
|
473
|
+
note="排轴 + sub.srt/ass + 安全区/字形自查(经验模型,给 ±%d%% 区间)"
|
|
474
|
+
% int(UNMEASURED_BAND * 100)),
|
|
475
|
+
dict(stage="cues", lo=cues_lo, hi=cues_hi, measured=False,
|
|
476
|
+
note="%d 条 sfx.cues 落位 + 排序(经验模型,给 ±%d%% 区间)"
|
|
477
|
+
% (n_cues, int(UNMEASURED_BAND * 100))),
|
|
416
478
|
dict(stage="score", lo=score_lo, hi=score_hi, measured=bool(score_on),
|
|
417
|
-
note=("
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
479
|
+
note=("以实测点 29 s≈3.6–6.3 s / 58 s≈4.5–5.0 s 为锚(--stage all 里与其它阶段争 CPU 会更慢;389 s 只是外推锚点)"
|
|
480
|
+
if score_on else
|
|
481
|
+
("mode=%s:不跑 scorekit(%s)"
|
|
482
|
+
% (mode or "-", "外部配乐/和弦床/关掉配乐")))),
|
|
483
|
+
dict(stage="sfx", lo=sfx_lo, hi=sfx_hi, measured=False,
|
|
484
|
+
note=("%d 条音效(实测 18 条 0.8 s;单条上限口径 ≤1.5 s,长环境声 wind/rain 另计)"
|
|
485
|
+
% n_cues if sfx_on else
|
|
486
|
+
"没有 sfx.cues:跳过(whoosh 兼容路径除外)")),
|
|
487
|
+
dict(stage="audio", lo=audio_lo, hi=audio_hi, measured=False,
|
|
488
|
+
note="三总线混音 + 写 audio.wav(实测 29 s 片 0.7 s;模型 %.2f + %.3f×片长)"
|
|
425
489
|
% (AUDIO_BASE, AUDIO_PER_SEC)),
|
|
426
490
|
dict(stage="frames", lo=fr_lo, hi=fr_hi, measured=True,
|
|
427
|
-
note=("%s:%s,--jobs %d
|
|
491
|
+
note=("%s:%s,--jobs %d → 有效加速比 %.2f×(实测 2 进程 1.51×),"
|
|
492
|
+
"返工系数 %.2g;不重编码时封装成本在 mux 行"
|
|
428
493
|
% (engine, "PIL 40–80 fps" if engine == "pil"
|
|
429
|
-
else "web 80–130 ms/帧 + 启动 6 s", jobs_eff,
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
%
|
|
434
|
-
dict(stage="qa", lo=
|
|
435
|
-
note="
|
|
494
|
+
else "web 80–130 ms/帧 + 启动 6 s", jobs_eff,
|
|
495
|
+
_speedup(jobs_eff), rerender))),
|
|
496
|
+
dict(stage="mux", lo=mux_lo, hi=mux_hi, measured=True,
|
|
497
|
+
note=("-c:v copy + 两遍 loudnorm(实测 29 s 片 1.7–1.9 s,与帧数无关);"
|
|
498
|
+
"reencode_video=true 才走 x264 %.0f fps" % X264_FPS)),
|
|
499
|
+
dict(stage="qa", lo=qa_lo, hi=qa_hi, measured=True,
|
|
500
|
+
note=("联系表 + 锐度参考帧重渲染 + 帧级深检(实测 29 s 片 6.2–6.8 s,不是 1 s)")),
|
|
436
501
|
]
|
|
437
502
|
total_lo = sum(s["lo"] for s in stages)
|
|
438
503
|
total_hi = sum(s["hi"] for s in stages)
|
|
@@ -485,11 +550,16 @@ def build_report(script_path, script, args):
|
|
|
485
550
|
"分辨率 1920x1080@%d 由 filmkit.W/H/FPS 写死,film.json 改不了" % FPS,
|
|
486
551
|
"lead=%.2f / tail=%.2f / min_dur=%.1f(v0.5 body 基准;剧本写了就以剧本为准)"
|
|
487
552
|
% (DEFAULT_LEAD, DEFAULT_TAIL, DEFAULT_MIN_DUR),
|
|
488
|
-
"tts 耗时按 %d 句 × %.1f s
|
|
489
|
-
|
|
490
|
-
"
|
|
553
|
+
"tts 耗时按 %d 句 ÷ workers × %.1f–%.1f s(每句每 worker);实际受网络波动影响"
|
|
554
|
+
% (n, TTS_SEC_PER_LINE[0], TTS_SEC_PER_LINE[1]),
|
|
555
|
+
"frames 的并行不是 jobs 倍:有效加速比 = 1 + (jobs-1)×%.2f,封顶 %d 核"
|
|
556
|
+
% (PARALLEL_EFF, CORES),
|
|
557
|
+
"mux 默认 -c:v copy,只跟片长有关;reencode_video=true 才按帧数重编",
|
|
558
|
+
"layout/cues/audio/sfx 给 ±%d%% 经验区间(无单阶段实测)" % int(UNMEASURED_BAND * 100),
|
|
559
|
+
"基准来源见 calibration 字段(机型/日期/样本);换机请重测后回填顶部常量",
|
|
491
560
|
],
|
|
492
561
|
"baseline": BASELINE_NOTE,
|
|
562
|
+
"calibration": dict(CALIBRATION),
|
|
493
563
|
}
|
|
494
564
|
return report
|
|
495
565
|
|
|
@@ -545,6 +615,15 @@ def print_report(rep):
|
|
|
545
615
|
% (bn["stage"], bn["seconds_hi"], bn["share"] * 100, bn["advice"]))
|
|
546
616
|
print()
|
|
547
617
|
print(" 基准说明:%s" % rep["baseline"])
|
|
618
|
+
cal = rep.get("calibration") or {}
|
|
619
|
+
if cal:
|
|
620
|
+
print(" 校准出处(换机请重测后回填 tools/estimate.py 顶部常量):")
|
|
621
|
+
print(" 机型 %s" % cal.get("machine", "-"))
|
|
622
|
+
print(" 实测 %s · 样本 %s" % (cal.get("measured_at", "-"), cal.get("film", "-")))
|
|
623
|
+
print(" 方法 %s" % cal.get("method", "-"))
|
|
624
|
+
obs = cal.get("observed") or {}
|
|
625
|
+
if obs:
|
|
626
|
+
print(" 实测值 " + " · ".join("%s %.1fs" % (k, v) for k, v in obs.items()))
|
|
548
627
|
print(" 假设:")
|
|
549
628
|
for a in rep["assumptions"]:
|
|
550
629
|
print(" - %s" % a)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
r"""selftest_noscipy.py — 在"没有 scipy"的环境里跑可选模块的自检。
|
|
3
|
+
|
|
4
|
+
为什么需要它:scipy 在 requirements.txt 里是**可选**依赖,没装 scipy 的用户会走
|
|
5
|
+
"分块 FFT 卷积"兜底路径——那是他们真正运行的代码,必须在 CI 里被测到。
|
|
6
|
+
本地开发机装了 scipy,所以要用这个脚本把 scipy 屏蔽掉再跑。
|
|
7
|
+
|
|
8
|
+
为什么用子进程:每个模块都在干净解释器里跑,避免"上一个模块已经把 scipy 用上了"
|
|
9
|
+
这种互相污染;同时能保证 sys.modules['scipy'] = None 在 import 之前就生效。
|
|
10
|
+
|
|
11
|
+
用法:
|
|
12
|
+
python tools/selftest_noscipy.py # 默认 sfxkit scorekit
|
|
13
|
+
python tools/selftest_noscipy.py sfxkit # 只跑指定模块
|
|
14
|
+
|
|
15
|
+
退出码:任一模块自检失败即返回 1。
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import os
|
|
20
|
+
import subprocess
|
|
21
|
+
import sys
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
25
|
+
SCRIPTS = ROOT / "skills" / "video-studio" / "scripts"
|
|
26
|
+
|
|
27
|
+
# 子进程里先屏蔽 scipy,再 import 目标模块并跑它的 selftest()。
|
|
28
|
+
RUNNER = r"""
|
|
29
|
+
import sys
|
|
30
|
+
sys.modules["scipy"] = None # 模拟"没装 scipy":import scipy 抛 ImportError
|
|
31
|
+
sys.path.insert(0, sys.argv[1])
|
|
32
|
+
name = sys.argv[2]
|
|
33
|
+
mod = __import__(name)
|
|
34
|
+
|
|
35
|
+
# 自证屏蔽真的生效:sfxkit 模块级 _sig 必须是 None(它是全仓库唯一的 scipy 入口)。
|
|
36
|
+
sfx = getattr(mod, "S", None) if name != "sfxkit" else mod
|
|
37
|
+
sig = getattr(sfx, "_sig", None) if sfx is not None else None
|
|
38
|
+
if name in ("sfxkit", "scorekit") and sig is not None:
|
|
39
|
+
print("屏蔽失败:%s 仍然拿到了 scipy" % name, file=sys.stderr)
|
|
40
|
+
raise SystemExit(2)
|
|
41
|
+
|
|
42
|
+
raise SystemExit(mod.selftest())
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def main(argv=None) -> int:
|
|
47
|
+
args = list(argv if argv is not None else sys.argv[1:])
|
|
48
|
+
modules = args or ["sfxkit", "scorekit"]
|
|
49
|
+
bad = []
|
|
50
|
+
for name in modules:
|
|
51
|
+
print("=" * 70)
|
|
52
|
+
print("[无 scipy 兜底路径] %s --selftest" % name)
|
|
53
|
+
print("=" * 70, flush=True)
|
|
54
|
+
proc = subprocess.run(
|
|
55
|
+
[sys.executable, "-c", RUNNER, str(SCRIPTS), name],
|
|
56
|
+
cwd=str(SCRIPTS),
|
|
57
|
+
env={**os.environ, "PYTHONDONTWRITEBYTECODE": "1"},
|
|
58
|
+
)
|
|
59
|
+
if proc.returncode != 0:
|
|
60
|
+
bad.append("%s (exit %d)" % (name, proc.returncode))
|
|
61
|
+
print()
|
|
62
|
+
if bad:
|
|
63
|
+
print("无 scipy 兜底路径自检失败:%s" % ", ".join(bad))
|
|
64
|
+
return 1
|
|
65
|
+
print("无 scipy 兜底路径自检全部通过:%s" % ", ".join(modules))
|
|
66
|
+
return 0
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
if __name__ == "__main__":
|
|
70
|
+
sys.exit(main())
|