关于 rendercheck
rendercheck 是用于检查生成音频、视频和图像中可测缺陷的 Python 库与命令行工具,适用于媒体生成和构建流水线,并返回判定报告而非生成媒体。
主要功能
- 01音频语速与响度断言
- 02时长、静音与轨道检查
- 03字幕时序与说话者检查
- 04基于视觉规则的图像检查
- 05提供 JSON 报告和退出码
适用场景
- 01在 pytest 中验收生成的节目
- 02在 CI 中发现截断的渲染文件
- 03发布前检查音频与字幕
- 04检查生成幻灯片的布局问题
MCP 相关
MCP 服务向 MCP 客户端提供媒体检查工具,包括返回判定与测量值的 `check_media`。除 `looks_ok` 外,大多数检查均为确定性检查且无需密钥。
配置
本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。
工具
2收录的工具列表 · 更新于 2 周前
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| path | string | 是 | 要检查的媒体文件。指定目录时会检查其中的内容。 |
| preset | enum | 否 | 此文件目标用途对应的响度目标。apple | atsc | ebu | netflix | podcast | spotify | tiktok | web | youtube |
| rubric | array | 否 | 静帧必须满足的条件,例如 'the title fits on one line'。启用视觉检查,该检查需要 API key。 |
| script | string | 否 | 旁白文本,或转录文本、.vtt、.srt 文件的路径。启用语速检查。 |
| strict | boolean | 否 | 将无法运行的检查计为失败。 |
| captions | string | 否 | 用于与音频比对的 .vtt 或 .srt 文件。如果文件与媒体文件同名且位于同一目录,则会自动查找。 |
| presenter | string | 否 | 预期的旁白讲述者。启用说话人检查,该检查还需要 known_names。 |
| expect_fps | number | 否 | 渲染所要求的帧率。还会检测可变帧率,它是导致音画不同步的常见原因。 |
| known_names | array | 否 | 所有实际可能被选为讲述者的人员。必须与 presenter 一起提供:如果没有此项,场景中的角色说“I'm Rosa”会导致每个讲故事的脚本都触发检查。 |
| expect_width | number | 否 | 渲染所要求的画面宽度,单位为像素。 |
| expect_height | number | 否 | 渲染所要求的画面高度,单位为像素。 |
| expect_channels | number | 否 | 交付规格要求的声道数:1 表示单声道,2 表示立体声。 |
| expected_seconds | number | 否 | 渲染预期的时长。 |
| expect_sample_rate | number | 否 | 交付规格要求的音频采样率,单位为 Hz。 |
详细介绍
rendercheck
生成媒体中最糟糕的 Bug 不会抛出异常。
“音频经常会截掉最后一句话 […],尽管 API 返回成功,也没有任何错误信号”
— 一位开发者在 OpenAI 论坛 上的发言, 2026 年 4 月,描述生产环境中的输出

如果你用模型生成语音或视频——TTS、语音 Agent、播客、虚拟形象、AI 视频——测试能捕获那些从未发生的异常,却捕获不了每分钟朗读 300 个单词的旁白、比其对应画面低 18 dB 的音轨、只渲染了 42% 时长却被缓存为成功的片段、早于音频三秒描述内容的字幕,或完全缺少音轨的文件。
在 2026 年,检测这些问题的最先进方法仍然是有人听一遍输出。这确实有效,但成本比你整个流水线中的其他所有环节加起来都高。
rendercheck 会让这些问题抛出异常。
from rendercheck import assert_pace, assert_loudness, looks_ok
assert_pace("episode-12.mp3", "episode-12.vtt")
assert_loudness("episode-12.mp3")
looks_ok("slide-14.png", ["the title fits on one line"])
纯粹的断言函数。没有框架、运行器或服务。它们会抛出 AssertionError,因此可直接用于 pytest、CI 或五行脚本。19 项检查中有 18 项不依赖任何项,也不会发起网络请求——只要你有 ffmpeg,就可以开始使用。
快速开始
你需要让 ffmpeg 位于 PATH 中(brew install ffmpeg、apt-get install ffmpeg 或 winget install ffmpeg)。然后:
pip install rendercheck
rendercheck demo
或者将文件放入**在线体验区**——运行相同的检查,使用编译为 WebAssembly 的 ffmpeg,不会上传任何内容。
demo 会合成 10 个有缺陷的文件,并对它们运行实际检查,让你无需拥有有问题的渲染文件也能看到效果。以下是原样输出的前两项(共 8 项):
Narration too fast
A voice picked to match a presenter's face read English at machine-gun speed. Valid audio, correct timing, perfectly in sync.
$ rendercheck check machine-gun.wav --script narration.vtt
FAIL pace narration pace 300 WPM exceeds 245 (300 words in 60.0s) -- this reads as machine-gun delivery and listeners cannot follow it: machine-gun.wav
PASS loudness -16.1 LUFS
PASS dead air 0.0 s silence
PASS truncation 8.9 dB of fall-off at the end
PASS clipping 0 samples at 0 dBFS
Levels that don't match
Synthesised narration landed 18 dB under the footage it was cut against. Nobody noticed until viewers rode the volume knob.
$ rendercheck check too-quiet.wav
SKIP pace no --script given
FAIL loudness -34.2 LUFS is 18.2 dB quieter than the -16 target -- it will sound inaudible next to correctly-levelled audio cut alongside it: too-quiet.wav
PASS dead air 0.0 s silence
PASS truncation 8.8 dB of fall-off at the end
PASS clipping 0 samples at 0 dBFS
……以及在 0.3.0 中新增的两项之一:
Captions against the wrong clock
A concatenation added three seconds of pre-roll after the captions were written. Both files are perfectly valid on their own.
$ rendercheck check late-captions.wav
SKIP pace no --script given
PASS loudness -16.0 LUFS
PASS dead air 0.0 s silence
PASS truncation 74.3 dB of fall-off at the end
PASS clipping 0 samples at 0 dBFS
FAIL captions late-captions.vtt runs 3.0s late against late-captions.wav, past the 0.75s limit -- every line arrives at the wrong moment, and both files are individually valid so nothing else catches it
然后将它用于你自己的输出:
rendercheck check episode-12.mp3 --script episode-12.vtt --preset podcast
只要有任何检查失败,退出代码就是 1;如果无法测量任何内容,退出代码也是 1,因为什么都没检查的运行不能算通过。路径拼错时退出代码为 2。--json 会为任何语言的流水线提供相同的报告,而 --strict 也会拒绝不完整的运行。
这个文件要放到哪里?
“音量应该多大?”没有唯一答案——这完全取决于文件的最终用途,而且每个平台公布的数值都不一样。--preset 将这些数值转化为构建流程可以强制执行的规则:
$ rendercheck presets
preset target tol peak source
youtube -14L 1.0dB -1.0TP YouTube normalises playback to -14 LUFS
spotify -14L 1.0dB -1.0TP Spotify, including podcasts, at -14 LUFS
tiktok -14L 1.5dB -1.0TP TikTok and Instagram, measured rather than published
podcast -16L 1.0dB -1.0TP AES71 / Apple Podcasts: -16 LUFS stereo, -19 mono
apple -16L 1.0dB -1.0TP Apple Music Sound Check, -16 LUFS
web -16L 2.0dB -- spoken-word web video -- rendercheck's own defaults
ebu -23L 1.0dB -1.0TP EBU R128, European broadcast
atsc -24L 2.0dB -2.0TP ATSC A/85, North American broadcast
netflix -27L 2.0dB -2.0TP Netflix delivery, dialog-gated
这些数值没有一个是我们制定的。我们的贡献在于,--preset ebu 是评审者一眼就能看懂的明确选择,而 --target-lufs -23 则是一个后来者不敢改动的神秘数字。设定上限的预设还会启用真峰值检查,以捕获本地测量正常、上传后却失真的母带。web 仅用于命名内置默认值,因此它不指定任何数值,行为与完全不传预设完全相同。
项目级设置可以放在 rendercheck.toml(或 pyproject.toml 中的 [tool.rendercheck])里,这样 CI 步骤就不必把 8 个标志挤在一行:
preset = "podcast"
max_silence = 5.0
手动输入的标志优先于配置文件,而配置文件优先于内置默认值。
在 pytest 中,它们就是普通断言——无需插件或 fixtures:
@pytest.mark.parametrize("episode", EPISODES)
def test_episode_is_shippable(episode):
assert_pace(episode.audio, episode.vtt)
assert_loudness(episode.audio)
assert_no_dead_air(episode.audio)
“这不就是四十行的 pyloudnorm 吗?”
对于十九项检查中的一项,差不多是这样。这些测量都不新鲜,声称它们有新意并不诚实:
| 测量项 | 已有工具 |
|---|---|
| 综合响度、真峰值 | pyloudnorm、ffmpeg 的 loudnorm |
| 静音检测 | pydub.silence、ffmpeg 的 silencedetect |
| 时长、流布局、帧率 | ffprobe |
| 黑帧、画面冻结 | ffmpeg 的 blackdetect、freezedetect |
| 字幕↔音频偏移 | ffsubsync——它会校正偏移 |
| 容器和编解码器合规性 | MediaConch——基于策略、通过/失败判断,可从 CLI 使用 |
| 说话人身份 | resemblyzer |
| 视频质量指标 | VMAF、ffmpeg-quality-metrics |
其中大多数工具只是把一个数值交给研究人员。已经能作为门禁的两个工具,检查的则是另一回事:MediaConch 检查文件是否符合容器策略,这是保存方面的问题,而不是感知方面的问题——文件可以通过 MediaConch 的每一条规则,却仍然以 300 WPM 的语速进行旁白。ffsubsync 会欣然重新对齐原本没错的字幕,因为它并不判断字幕是否需要对齐。
真正缺少的,也是这个工具提供的:
- 来自缺陷的阈值,而不是来自论文。245 WPM,是因为真实配音曾以 280 WPM 录制并交付。−16 LUFS,是因为旁白音量只有 −34 LUFS,而画面素材为 −13 LUFS。
- 说明人会注意到什么的消息。“−34.0 LUFS”只是一个读数。“比 −16 的目标低 18.0 dB——与剪辑在一起、音量正确的音频相比,它会听不见”才是一条缺陷报告。
- 基础设施出错时放行,出现缺陷时拦截,这样它就能用于 CI,而不会因为自身的问题变成构建失败的原因。
- 退出码和一个可对目录执行的命令,而不是一个 Notebook。
与 LLM 评估工具相比,区别在于结构,而不是覆盖范围。promptfoo、 DeepEval 和 RAGAS 都很出色,但它们都做不到这一点:它们的测试用例是一个字符串。你无法添加断言,因为没有地方放文件。用它们检查脚本;用这个工具检查脚本生成的成品。
如果你已经在运行广播级 QC——Interra BATON、Telestream Vidchecker、QCTools——那么二十年来你基本都已经拥有了这些功能。它只是还没进入你的 git hooks。
在你的流水线中
GitHub Actions——安装 ffmpeg,并在发现缺陷时使构建失败:
- uses: rogermsc/rendercheck@v0
with:
files: out/
preset: podcast
strict: "true"
Node、Remotion,以及任何在构建步骤中进行渲染的工具:
npx rendercheck check out/
Docker,如果你不想使用任何 Python 工具链——镜像中已经有 ffmpeg:
docker run --rm -v "$PWD:/work" ghcr.io/rogermsc/rendercheck check /work/out.mp4
编码 Agent,通过 MCP 使用。刚写完渲染流水线并运行它的 Agent,无法判断生成的文件质量如何;其他媒体 MCP 服务会剪辑和转码,因此给它的是更多媒体,而不是答案:
claude mcp add rendercheck -- rendercheck mcp
它已列入 MCP registry,名称为 io.github.rogermsc/rendercheck,因此能读取 registry 的客户端无需知道它的位置即可安装。无需密钥——除 looks ok 之外的所有检查都是确定性的。
check_media 会为每项检查返回一个判定及测量值,因此模型可以根据“−34 LUFS,比目标低 18 dB”采取行动,而不必面对一个它无法听见的文件。
promptfoo——它的断言全都针对字符串,因此评估可以确认旁白脚本,却无法告诉你音频的情况。examples/promptfoo/ 用三十行代码补上了这一部分:
assert:
- type: python
value: file://rendercheck_assert.py:get_assert
其他任何场景——通过 stdout 输出 --json,每个文件对应一个对象,并返回退出码。
六项检查,六起事故
每个默认值都是由实际发布的缺陷确定的阈值,而不是为了对称而选的数字。 以下输出均为原文。每个阈值都是一个参数——调整方法请参见参考文档。
旁白语速过快。 为匹配演示者面部而选的声音,以约 280 WPM 的语速朗读英文。音频有效、时长正确、同步完美。只是听起来像机关枪。
assert_pace("episode-12.mp3", "episode-12.vtt", max_wpm=245)
narration pace 300 WPM exceeds 245 (300 words in 60.0s) -- this reads as
machine-gun delivery and listeners cannot follow it: episode-12.mp3
音量不匹配。 合成旁白的响度为 −34 LUFS,与响度为 −13 的视频片段拼接在一起。同一个文件,中间出现了 20 dB 的音量跃变。直到观众花了 45 分钟不停调节音量旋钮,才有人注意到。
assert_loudness("episode-12.mp3", target_lufs=-16, tol=2.0)
-34.0 LUFS is 18.0 dB quieter than the -16 target -- it will sound inaudible
next to correctly-levelled audio cut alongside it: episode-12.mp3
被缓存为成功的截断渲染。 编码失败生成的片段只有预期时长的一小部分,但流水线却将其缓存为成功。重试只会重新运行那些报错的任务——而这些任务并没有报错。
assert_duration("segment-07.mp4", expected_seconds=24.0)
segment-07.mp4 is 10.0s -- 42% of the expected 24.0s. A render this short is a
silent encode failure, not a short take; re-render rather than retry
中间有空洞。 合成过程发生瞬时且无声的故障,导致文件中间出现一段空白。时长正确,平均响度正确。只有中间部分出现了空洞。
assert_no_dead_air("episode-12.mp3", max_silence=3.0)
6.2s of silence starting at 0:41 exceeds the 3s limit -- a gap this long
mid-file is a dropped segment, not a pause (2 found in total): episode-12.mp3
说话的人错了。 脚本中写着“我是 Jordan”,但系统分配的却是 Alex。整个模块都以错误的面孔和错误的声音渲染。其他所有关卡都通过了。
assert_speaker(script, expected="Alex", known_names=["Alex", "Jordan", "Sam"])
the script introduces the presenter as "Jordan" but Alex is assigned -- the
rendered avatar would introduce itself with someone else's name. Fix whichever
is wrong: the assigned presenter, or the name in the script
known_names 名单是必需的,也是关键所在:没有它,场景中的角色说出*“我叫 Rosa,是一名护士”*时,每个讲故事的脚本都会触发检查。只有属于实际可能被选中的人物的姓名,才算是对说话者身份的声明。
只能通过视觉发现的问题。 溢出的标题、彼此碰撞的徽标、只填了一半的画布、被裁到字幕中间的人物。这些内容都在渲染时没有报错。
looks_ok("slide-14.png", ["the title fits on one line", "no text is clipped"])
[major] slide-14.png: the title wraps to three lines and overlaps the logo in
the top-right corner -- failed rubric item: 'the title fits on one line'
这是唯一需要密钥的检查:pip install "rendercheck[vision]"。
还有十三项,针对其他人反复报告的缺陷
上面六项来自同一条流水线。接下来的这些则来自阅读其他人的错误报告——同样的投诉,轮番指向每一家提供商:
| 检查项 | 缺陷 |
|---|---|
assert_no_truncation | 语音在句子中途停止,但 API 返回成功。这是生成音频中报告最多的缺陷;以文件自身的平均音量为基准进行测量,因此无论内容音量高低都适用。 |
assert_has_sound | 返回的片段没有声音——升频步骤丢弃了音轨,混流指向了错误的流,或合成过程写入了全零数据。 |
assert_no_clipping | 增益级将波形推到了满量程以上。辅音出现爆裂声,之后再调低音量也无法消除。 |
assert_true_peak | 本地测量正常,上传后却失真。响度和峰值是两个不同的问题:有损编解码器会重建样本之间的波形,并在波形越界处造成削波。 |
assert_no_black_frames | 生成的视频截断成黑屏,却没有报错:时长正确,容器有效,最后三分之一没有内容。 |
assert_not_frozen | 画面停止运动。每一帧都存在,但每一帧都是同一帧。 |
assert_captions_aligned | 字幕按一个时钟编写,音频却按另一个时钟渲染。每一行都在错误的时刻出现,而两个文件单独看都完美无缺。 |
assert_streams_aligned | 声音和画面覆盖的时长不一致——混流时某个输入耗尽,或拼接时首个片段的时间安排有误。 |
assert_format | 渲染结果悄悄降到了 720p、帧率不正确,或采用了可变帧率,而下游流水线假定使用恒定帧率。 |
assert_not_blank | 图像生成器失败并返回空画布——DALL·E、Stable Diffusion、Qwen、Gemini 和 Krita 都报告过这种情况,而且表现始终相同:没有错误,没有警告,尺寸正确,画布上却空无一物。它能检测任何纯色画布,不只是黑色画布;blackdetect 检测不到其他纯色画布。 |
assert_loudness_range | 文件没有一个合适的统一音量设置:为了听清安静段落而调大音量,响亮段落又会吓人。这与 assert_loudness 问的问题不同,后者只关注音量中间值。 |
assert_audio_format | 要求立体声却交付了单声道,或规格要求 48 kHz 却交付了 44.1 kHz——之后由链路中碰巧使用的转换器进行重采样。 |
assert_lip_sync | 说话人嘴部动作与语音不同步。容器正确,流有效,时长一致,assert_streams_aligned 也通过了——因为它读取的是容器时间信息,而这项检查读取的是内容。默认关闭,而且拒绝判断的次数多于给出判断的次数:参见 它不检查的内容 下方的说明。 |
空白检查是不需要密钥的一项检查。 在它出现之前,静态图像必须先经过视觉层级检查,这个工具才会对它作出任何判断,因此 rendercheck check slide.png 无法测量任何内容,并以非零状态退出。它读取的是亮度分布上下端之间的跨度,而不是最小值与最大值之间的跨度,这正是它可靠的原因:包含单个杂散伪影的空白帧,用最小值/最大值计算时会覆盖整个范围,但在这里仍会被判定为空白。
字幕检查是唯一没有同类功能的检查。 ffsubsync 及类似工具会校正漂移;在线验证器只会单独检查 .srt 文件——例如字幕条目重叠、阅读速度和空行。两者都不会检查这个字幕文件是否与这段音频匹配,而这正是构建流程唯一需要回答的问题。它通过匹配语音形态与字幕条目的形态来工作,并分别报告固定偏移和漂移:偏移是整体偏离正确位置,而漂移无法通过单次整体移动来修正。
这些检查项在无法测量时都会跳过,而不是通过——没有视频流、没有可供对齐的静音结构,或容器中没有每个流的时长。对 .wav 执行 blackdetect 不会报告任何内容,否则这可能会被误读成“检查过了,一切正常”。这与下面的回归问题是同一个陷阱,也是所有检查项都跳过时运行仍以非零状态退出的原因。
在真实文件上发现的问题
这不是合成基准测试。我们将它用于一个大规模渲染旁白视频的生产流水线输出:
| 结果 | |
|---|---|
| 已知质量合格的课程 | 正常——162 WPM,−14.1 LUFS,无无声片段 |
| 响度修复合入后重新渲染的内容 | 以 −14.2 LUFS 通过 |
| 修复前渲染的 4 集内容 | 以 −19.4 至 −21.3 LUFS 未通过 |
它恰好在修复合入处划出了分界线;这些文件从未被告知任何信息,但所得结论与人们几个月前就已得出的结论一致。
这张表只是一个轶事,而且明确标注了这一点。 媒体文件是私有的,因此没人——包括作者本人——能够重新运行测试。你可以核验的数据见 docs/calibration.md,由 bench/bench.py 生成:
python bench/bench.py sync # no network, no corpus, about a minute
该脚本会向已知答案的媒体中注入已知程度的缺陷,并统计每项检查实际报告了什么;未作出判断的情况也计入分母,每个零值旁都会打印三法则界限。本库中每个有实测依据的阈值,都源自这里。
两项承诺
基础设施出问题时会默认放行。 没有 ffmpeg、没有密钥、没有网络、无法测量 → 发出警告并放行。如果一个门禁因自身故障而阻塞你的流水线,它一周内就会被删掉,之后便再也起不到保护作用。缺陷默认拒绝;检查器默认放行。(文件缺失是你拼错了路径,不属于基础设施问题——这种情况仍会引发异常。)
绝不把沉默误认为成功。 无法运行的检查会明确说明,显示为 rendercheck.Skipped 警告,也会出现在 CLI 输出中。一次空运行绝不会被误读为检查正常。
我们为做到第二点,前后努力了两次。
这个库的第一个版本会对完全没有音轨的文件返回 PASS——没有可供分析的内容时,silencedetect 不会报告任何内容,于是被误读成“未发现静音”。如今,这是测试套件中最醒目的失败案例,回归测试也以它命名。它教会我们的原则适用于所有其他情况:测量后发现结果不正确,就默认拒绝;无法测量,就默认放行。
随后,在以这个名称发布之前,我们根据工具自身的设计初衷对其进行了审计,又发现了另外 7 个问题。每个问题都会在尚未确认检查成功时报告成功:
- 全部跳过的运行以
0退出。运行器上没有 ffmpeg,构建便显示为绿色。 - 文件路径拼错时以
0退出,与上文两段前的承诺相矛盾。 --script路径拼错时,文件内容被当成旁白——只有一个词——并给出一个关于音频的自信错误判定:1 WPM is below 110。- 未提供
--known-names时,--presenter会将人员名单默认设为指定的主持人,导致说话人检查在结构上不可能触发。即便脚本中提到了其他人,它也会输出PASS。 looks_ok会把任何异常都归咎于缺少 API 密钥,因此即使 SDK 不兼容也会永远放行,还会让 Bedrock 用户追查错误方向。- ffprobe 故障会在消息中泄露原始 Python 列表。
- 没有 ffmpeg 时,测试套件会在收集阶段崩溃,而不是跳过测试。
这 7 个问题都已修复,每项修复都有一个缺少该修复就会失败的测试;退出码如今也有明确约定:0 表示已测量且正常,1 表示存在缺陷或未测量任何内容,2 表示路径不存在。详情见变更日志。
一个能发现静默失败的工具,其价值完全取决于它是否能如实报告这些失败。
不检查的内容
明确说明这一点,因为 QA 工具要是暗示自己覆盖的范围超出实际能力,那还不如没有:
- 达到实际所需容差的唇形同步。
assert_lip_sync会将画面运动与语音包络进行相关分析,在没有人脸检测、也没有模型的情况下,能发现嘴部动作与声音相差近半秒的大多数情况。但它做不到两件事。它无法检测出感知上真正重要的误差:EBU R37 规定,音频领先 +40 ms、滞后 −60 ms 时即可察觉;而它只能检测出不到三分之一的 200 ms 误差。此外,它会对大约三分之二的文件不作判断,因为这些文件的相关峰值不够明显,无法据此读出数值。它能发现损坏的流水线,但不能证明流水线正常;要做到这一点,仍需要模型(SyncNet 等)。 - 视频的感知质量。 不支持 PSNR、SSIM 或 VMAF——这些指标需要参考编码进行对比,而生成的媒体没有参考编码。
- 旁白是否正确——这里只检查朗读速度,以及字幕是否与时间同步。这里没有任何功能会转录音频,因此,即使声音读错了稿子,只要语速合理,也能通过所有检查。这项检查需要用到 Whisper,而相对于其他部分,它引入的依赖过于庞大。事实依据和准确性又是另一个问题,LLM 评估工具已经能很好地覆盖这一点。
- 语音清晰度。 音量大不等于清晰;音轨即使电平正确,仍可能含糊不清。
- 音乐、混音或任何非语音内容。 默认设置是为口语设计的。
- 你没有编写的评分规则。
looks_ok只会检查你要求它检查的内容。
更多信息
MIT。
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。