mcp-vision
This server does not currently have a deployable release on Glama. Follow the repository's setup instructions to install and configure it for your MCP client, including any required credentials and local dependencies.
mcp-vision
让 AI 看视频。 纯 Python 的 MCP 服务器,不依赖 Node 运行时。
仅供学习研究使用。代码以 MIT 发布。
仓库结构参照 dsh-video-vision 的分层重写:
保留同样的 workspace / video / audio / subtitles 四层与工作区目录布局,
实现全部换成 Python —— ffmpeg 解码、numpy 计算、onnxruntime 跑 PP-OCRv4,
没有浏览器、没有 Node 进程、没有 Electron。
输入本地文件或 B 站链接,输出模型能直接消费的东西:带时间戳的帧、近似正方形的网格故事板、 音频电平与静音区间、画面 OCR 与折叠后的字幕轨。全部在本机完成。
设计原则
纯 Python:stdlib + numpy + onnxruntime(可选)+ ffmpeg/ffprobe + yt-dlp。无 Node、无 Pillow、无 ImageMagick。
自含:结构照搬,代码重写,不 import 任何外部仓库(见「致谢」)。
MCP 优先:stdio + HTTP 双入口,工具 schema 与实现一一对应。
MIT。
Related MCP server: avv-mcp
依赖
Python 3.10+(开发在 3.14)
ffmpeg/ffprobe(PATH 或配置)numpyonnxruntime(可选,OCR 用)sherpa-onnx(可选,转写用)
模型不随仓库分发,路径全部走环境变量:
变量 | 用途 |
| PP-OCRv4 三个文件 |
| SenseVoice + silero VAD |
没配就只缺对应能力,抽帧和电平分析不受影响。详见 INSTALL.md。
yt-dlp(可选,链接下载)
快速开始
sh check.sh
python3 mcp_server.py # stdio
python3 mcp_server.py --http 8765sh check.sh --test 跑一遍体检 + 不需要模型的回归测试(L/R 分层出图)。
基准脚本在 scripts/(需要模型或运行中的服务),回归测试在 tests/。
依赖、模型下载、客户端配置见 INSTALL.md; 使用范围与责任见其中的「免责声明」。
工具
四个大工具,按职责分层:
workspace:open(打开/下载)/listvideo:给窗口 + 张数,抽帧拼成一页网格;quantity=1时不出页,直接给单帧原图(可crop)quantity抽帧数(默认 15,上限 30;1 = 单帧);quality像素上限:low=512² medium=1024² high=1536² max=2048²(拼页时管整页,单帧时管裁剪后那张)
audio:按view出图 ——level(20ms RMS 电平)/spectrum(FFT 幅度谱)/spectrogram(时频图)/peak(峰值频率轨迹)/wave(原始波形),all一次全给subtitles:mode=ocr识字幕(band可指范围)/mode=stt语音转写 (silero-vad 自动断句 + SenseVoice,模型路径由环境变量指定)
音频图一律 1024×1024,上半 L、下半 R。读数口径见「音频图的读数口径」。
视频固定在工作区 workspace/<文件夹名>/。
video / audio / subtitles 默认作用于最近打开的那个,可用 video_id 传文件夹名指定。
所有产物落到 workspace/<标题>/{video,audio,subtitles}/,可直接用文件系统管理(ls/rm/cp),
不再用不透明的 .mvv-cache。
OCR
PP-OCRv4(det + rec)在 numpy + onnxruntime 上从零实现,不依赖 OpenCV / Pillow / pyclipper / ImageMagick。 读图用 ffmpeg 解 raw RGB;连通域、CTC 解码都是纯 Python。
pkg install python-onnxruntime
# 先按 INSTALL.md 3.1 把 PP-OCRv4 三个文件放进 models/(约 16MB)
python3 ocr.py out/ocr_test.png # 单图加速器:优先
XnnpackExecutionProvider,可回落CPU;Android 上另有NnapiExecutionProvider调参:
DET_MAX_SIDE(默认 720)、DET_TH(默认 0.2)、MVV_ORT_THREADS实测:880x320 三行中英混排,1.0s 识别完毕,置信度 0.94–0.98
取窗:先选时间戳,再选看多久
所有耗时动作(video / audio / subtitles)共用一套取窗:
参数 | 含义 |
| 从哪个时间戳开始,默认 0(frame 里是第一帧的时刻) |
| 看多久;不传则用该工具的默认值 |
各工具的上限:audio 15 秒(不传默认 10 秒)、video / subtitles 900 秒(15 分钟)。
超过上限会被夹住,并在返回里带 note 说明夹到了哪里。长视频靠 time 翻页,
不会一次跑到底。
{"mode":"stt","time":1200,"seconds":600} // 从 20 分开始,转 10 分钟音频图的读数口径
五张图共用一套标定,跨窗口、跨文件都能横向比:
项 | 口径 |
FFT 点数 | 4096;44.1k 下分辨率 10.77 Hz,窗长 92.9 ms |
幅度 | 满幅正弦 = 1(`2· |
峰值频率 | 对数域抛物线插值,幅度再除以汉宁窗在偏移处的增益(扇贝损失) |
电平 | 20 ms 不重叠帧的 RMS,单位 dBFS |
纵轴 / 色标范围 | 顶端 = 峰值向上取整到 10 的整数次幂,底端退 3~6 个十进位档 |
时频图峰值格 | 每列单独补扇贝损失,纯音不论落在 bin 哪个位置都读准 |
频域图横轴对数 10 Hz–Nyquist,时频图 20 Hz–Nyquist,波形取的就是那 4096 点。
已知偏差(有意保留):时频图的噪声底比读数低 1.76 dB,这是纯音标定 + 汉宁窗 ENBW=1.5 的必然结果;全局减 1.76 dB 会让纯音偏低,所以不动。
输出不裁剪
_dump() 默认不截断。之前 8000 字符上限会把 97 段字幕砍成 55 段还不显眼,
现在整份返回;确实要塞小响应时才显式传 limit。
字幕段统一是 {"from": "MM:SS", "to": …, "text": …},OCR 与语音转写同形,
折叠后整份返回;要 SRT 直接拿三个字段拼,工具本身不落盘。
时间只到秒。 毫秒对定位没用——没人会拿 00:23.417 去切帧——
但每段两个时间各写 4 个字符,98 段字幕就是 784 字符(约 7% 的响应)。
OCR 的采样步长本来就是 1.0s,毫秒位恒为 000,留着等于伪造精度。
两条路能拿到的东西不一样
|
| |
读的是 | 画面像素 | 声音波形 |
| 屏幕上的字 | 嘴里说的话 |
| 有(OCR 置信度) | 无 |
| 没有 | 有 |
| 没有 | 有 |
| 没有 | 有 |
| 无 | 有 |
声学三项只有 stt 有,这不是没实现,是原理上取不到——
OCR 只看像素,画面里没有「情感」和「有没有 BGM」这两样信息。
响应里用 acoustic_fields: true/false 直接标明,省得调用方反复试。
所以:
想知道 BGM / 语气 / 语种 → 必须走
mode=stt,即使字幕已经在画面上了想知道 画面上的字(歌词、烧录字幕、弹幕)→ 走
mode=ocr两样都要 → 调两次。这类片子通常有口播,先跑 stt 拿语义,再用 ocr 核对画面原文
语音转写在 stt 模式下会带上 SenseVoice 同一次前向算出的三项(有才写,默认值省略):
字段 | 取值 | 说明 |
|
|
|
|
|
|
|
| 这首歌里出现 |
顶层另给一个 summary 统计(如 {"event": {"BGM": 4}})。
event=BGM 说明那段里音乐压过了人声,转写掉字多半就是这个原因——
但注意它是模型对整段音频的判断,不是声道分离的结论。
性能
实测环境:Android aarch64,Python 3.14,onnxruntime 1.31.1。
线程数:这机器 2 线程最优。det 推理 1 线程 0.182s → 2 线程 0.117s → 4 线程 0.125s → 8 线程 0.149s(小核争用)。
默认已设为 2,可用 MVV_ORT_THREADS 调。
加速器:CPU 实测最快,XNNPACK 反而更慢,NNAPI 初始化贵。默认 cpu,可用 MVV_ORT_PROVIDERS 改。
字幕带 + 跳帧(subtitles 的 mode=ocr)是最有效的一刀。
30 秒测试片(15 条字幕,每条 2 秒):
方案 | 采样 | 总帧数 | OCR 次数 | 耗时 |
全帧逐帧 | 1.0s | 30 | 30 | 8.10s |
字幕带+跳帧 | 1.0s | 30 | 15 | 2.20s |
原理:只裁一条字幕带(不是整个下半屏),字幕没变的帧直接复用上次结果。 带高由自动检测给出,1280×720 上实测 84–129px,占整幅 12–18%。
字幕带是怎么定的(mode=ocr 且 band=auto 时):
两段式:先只 OCR 下方 40%(多数片子的字幕在那儿,只花 0.4 屏);找不到够像的簇, 再补扫上半屏。两段共用同一批已解码的帧,整幅只扫一遍、不重复解码
簇按纵向重叠聚类,并限高 25%:重叠是传递的,一屏小字能从顶串到底, 串成一个 0.0–0.8 的「簇」会让选带彻底跑偏(《areyoulost》实测栽过一次)
8 个采样点按时长均分;窗口 ≤45 秒走一次解码取帧,更长的逐点 seek(成本与片长无关)
先筛掉不像字幕的簇:最宽 <12%(单个字母/图标)、纵向串成柱 >25%(几个无关的框连成一片)
簇打分 = 行高中位数^1.5 × 宽度 × 位置权重 × 出现次数;位置权重给底部(1.0→2.2)和 顶部(1.0→1.6)加分,画面中部压到 0.6
纵向间隔 ≤2% 的簇并成一条带(并完不超过画面 1/5)
边距按实测行高给(行高的 0.6 倍,至少 0.02):两行字幕会整块上移,固定边距会切掉一行
常驻水印:同一串字(去标点、大小写归一)在簇内占比 ≥50% 判为静态标,得分 ×0.25
一点字都没检出就判「无字幕」,直接返回空;
band=auto且 0 段时返回hint
注意:耗时由「字幕条数」决定,不由采样密度决定。 同一测试片:
采样 | 总帧数 | OCR 次数 | 耗时 |
0.5s | 60 | 15 | 2.33s |
1.0s(默认) | 30 | 15 | 2.29s |
1.5s | 20 | 15 | 2.24s |
采样越密只多出便宜的跳帧比对。默认 1.0s 是为了省临时数据量,而非省时间:
视频长度 | step=0.5s | step=1.0s | step=1.5s |
30 分钟 | 691 MB | 346 MB | 230 MB |
1 小时 | 1382 MB | 691 MB | 461 MB |
2 小时 | 2765 MB | 1382 MB | 922 MB |
因此实现上不落盘:ffmpeg 用管道(-f rawvideo -)流出带帧,逐帧读、逐帧判、逐帧丢,
磁盘占用恒为零,任意长度视频都不爆盘。
关于自量化(结论:死路,已验证):
权重 int8 量化(QDQ)与真·动态量化(DynamicQuantizeLinear + QLinearConv,62 个卷积)
两条路都跑通了——需自建 onnx_pb2,因为 Termux 装不了 onnx 包
(cp314t+manylinux 轮子对不上 android/Bionic)。
但 ORT 的 ARM int8 卷积和 fp32 NEON 一样快:推理 0.101s vs 0.101s,提速 1.00x,
还多出 62×3 个节点、加载慢 3 倍、精度下降(per-tensor,mean diff 0.064)。
量化工具与产物已删除,结论记录在此。
实测踩坑记录
起初两支片子:动画 MV BV1Rx54znE7k(328s,720p,有歌词字幕)、 实拍 vlog(320s,白色泡沫满屏)。后来又加了《areyoulost 降调》(字幕贴顶) 和《南充 0 分作文》(整屏手写稿)两支。按发现顺序记:
1. 跳帧在动画背景上失效
原按「带内像素绝对差」判重,背景一动就超阈值,跳过仅 7/328 (2%)。 改为「灰度 > 223 的高亮文字掩码差」:字幕未变 ≤0.15%,变了 ≥0.86%,跳过率升到 160/328 (49%)。 (220 与 223 在这几支片子上结果完全一致,取 223 只是留点余量。)
2. 不能把「短 cue」当噪声合并(重要)
该视频的歌词会把每句尾短语重复演唱一遍,字幕轨因此有独立短 cue:
小弦切切如私语 → 如私语、大珠小珠落玉盘 → 落玉盘。
最初误判为「OCR 残句」和「逐字擦除动画」并做了后缀合并——两次都错了。 查证过程:
密集抽帧(10fps):75.50 还是完整 7 字,75.60 直接变成居中 3 字,硬切
剩余 3 字重新居中,不是右侧残留 → 是新 cue,不是擦除
SenseVoice 转写:
…如私语,如私语。—— 唱了两遍,证实是真实重复
修正后 fold_segments 只做三件事:丢空帧、同文延长时间、编辑距离合并
同一 cue 的 OCR 变体。「更短的纯后缀」一律视为新 cue 保留。
3. ASR 预处理反直觉
试过降速/加音量/带通/降噪,全部让识别更差:
处理 | 结果 |
原始 |
|
0.8x + 1.2x 音量 |
|
0.7x + 2x + 带通+降噪 |
|
降速破坏音高与时长特征,带通削掉辅音高频,对唱腔 + BGM 反而有害。
4. 字幕带会切掉第二行
320s、实拍、白色泡沫满屏。0–60 秒只出 4 段,片头标题整句丢失。
聚类打印显示这片的字幕有两档高度:大字在 0.822–0.900,矮的一行低到 0.902–0.950。 检测只取最高分那一簇,带定成 0.8022–0.92,而 t=0 那行在 0.907–0.944—— 下边缘从字中间切过去,OCR 读不出来。
改为「纵向间隔 ≤2% 的簇并成一条带」(上限画面 1/5)后,带变 0.8022–0.97, 段数 4 → 15,片头标题和「说搞就搞」都回来了;《琵琶行》仍是 19 段,无回归。
代价:条子从 84px 涨到 129px,条内变化更容易越过 skip_th,
同一窗口 OCR 19 → 24 次、8.2 → 9.9 秒。skip_th 提到 1.5 可回到 21 次且段数不变,
但 3.0 会丢掉 5 段,没敢默认动。
5. 网格凑整:要 12 帧会回 15 帧(已修)
早期版本为了让整页接近方形,按 cols×rows 抽帧,quantity=12 实际返回 15 帧
(排成 3×5,填 12 格空 3 格)。现在要几张给几张,网格只决定排版,最后一排可以留空。
同时把空格惩罚从 0.05 提到 0.5:只要有无空格的方案,就不为了更方形去留黑格。 起因是 4 帧裁剪成字幕带时选了 1×5——页面比例 0.98 确实比 1×4 的 1.23 更方, 但白占一格。现在 4 帧稳定走 1×4。
6. 整屏都是字的片子:检测得出来,但读的仍是页面碎片
《南充 0 分作文》整屏是手写作答纸,底部压一行白色旁白。下方 40% 全是作文正文, 自动检测给出 23 段(改全帧后 37 段),条条是作文不是字幕。
当时试过四条按「像素/几何」筛的路子,全部不成立:
判据 | 为什么不成立 |
提 | 纸面比字幕还亮(≥250 的像素占 15–18%,字幕行只有 6%) |
按行高筛 | 作文行和字幕行高度同量级 |
亮度极性(白字黑边 vs 黑字白底) | 反了,纸底更亮 |
时间变化(字幕变、纸不动) | 纸在滚动,t=10 vs t=16 整屏每段差 22–31 |
后来换成「结构」判据才成立:不看像素,看文字怎么分布。
字幕不管换多少句,都挤在同一条(偶尔两行)里;整页文字会在多个高度各自成行。 于是数「可用簇」(宽度 ≥12%、高度 ≤25% 的纵向簇)的个数和跨度:
片子 | 可用簇 | 纵向跨度 | 判定 |
《琵琶行》歌词 | 1 | 0.00 | 字幕 |
泡沫实拍 | 3 | 0.00 | 字幕 |
幻灯片(dsh-storyboard) | 6 | 0.50 | 整页 |
作文纸《南充 0 分》 | 12 | 0.94 | 整页 |
判据是 簇数 ≥ 4 且 跨度 ≥ 0.45。命中时不扣数据,而是把 page_like 和 hint
一起返回——它确实读到了字,只是读的是页面的一个横条。
关键改动:以前下半屏命中 count≥2 且 w≥0.2 就直接收工,从不看上半屏。
幻灯片的表格行恰好满足这个门槛,于是永远发现不了「整屏都是字」。
现在命中后仍要扫上半屏验证,多付约 1.7 秒。
对这类片子,mode=stt 才是正解:口播转出来是连贯成句的,
比从页面碎片里拼要准得多。hint 也照这个顺序给建议。
状态
v0.4 已通:workspace(open / list)、video、
audio(level / spectrum / spectrogram / peak / wave)、subtitles(ocr / stt)。
转写链路:ffmpeg 直出 16k 单声道 → silero-vad 自动断句(阈值/静音/最长段交给模型, 不写死时间网格)→ 逐段 SenseVoice 解码 → 丢弃纯语气词片段。
模型全部按需加载,启动时不碰任何推理库:
时刻 | 常驻内存 |
进程启动 | ~36 MB |
首次 OCR(只建检测模型) | ~68 MB |
首次框出文字(才建识别模型) | ~90 MB |
转写(VAD 切出第一段语音才建 SenseVoice) | ~340 MB |
纯静音视频转写 0.17 秒结束,不加载 229 MB 的识别模型。
未做:B 站元数据/评论/弹幕、镜头切分、多站登录态。
致谢
思路参考了以下三个项目。代码没有复用,全部按 Python 重写,运行时也不依赖它们。
video-understanding-kit — 把视频拆成
analyze(整片)和frame(单帧)两层的做法,以及网格故事板的排版思路。dsh-video-vision — 链接规范化(
lib/ytdlp.js的normalizeSource)、按时间点取原帧。dsh-bili-watch — B 站下载与登录态(cookies)处理的经验。
也感谢 ffmpeg、yt-dlp、numpy、onnxruntime 的作者。
模型与依赖:
PP-OCRv4,来自 PaddleOCR(Apache-2.0)
SenseVoice,来自 FunAudioLLM / FunASR。 代码 MIT;模型权重是另一套:FunASR Model Open Source License v1.1,须保留模型名与署名
Silero VAD(MIT)、sherpa-onnx(Apache-2.0)
许可证
定位:仅供学习研究使用。
本仓库代码:MIT,见 LICENSE。© 2026 Huangjiang-a11y
LICENSE 只覆盖本仓库的代码。 模型权重不随仓库分发,遵循各自的上游许可; ffmpeg 是以子进程调用的外部程序(不链接、不捆绑); 经由本工具处理的音视频与字幕,版权归原权利人。
各组件许可、ffmpeg 的分发注意事项、模型署名的要求,见 THIRD_PARTY.md。
This server cannot be deployed
Maintenance
Related MCP Connectors
Extract YouTube transcripts, search what was said, and read on-screen frames with cited timestamps.
Transform video, audio and images, and generate media from prompts. FFmpeg, captions, models.
Fetch transcripts, subtitles, chapters, metadata and frames from YouTube and 10+ video platforms
Multimodal video analysis MCP — transcription, vision, and OCR for any video URL.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceEnables AI assistants to watch YouTube videos by extracting frames at scene changes and visual references, pairing each frame with the exact words spoken at that timestamp. Provides dense frame-transcript interleaving for any model.31 npm5MIT
- AlicenseNot gradedqualityCmaintenanceEnables AI agents to download, convert, and visually analyze videos by providing timestamped frames and locally transcribed speech, all without API keys.MIT
- AlicenseAqualityAmaintenanceEnables AI clients to watch local video files or YouTube/Bilibili and other supported URLs, receiving timestamped transcripts, subtitles, searchable text and keyframe contact sheets. Runs fully offline with local speech recognition and bundled ffmpeg, so no API key or cloud upload is needed.674 PyPI13MIT
- FlicenseNot gradedqualityBmaintenanceEnables AI clients to watch and hear videos from local files or YouTube, Vimeo, and Loom links by returning timestamped key frames, a transcript, a contact sheet, and a combined timeline, with all processing done locally.-