把视频/音频转成纯文本的 MCP 服务。给一个媒体 URL,返回转写文本 —— 就这一件事,不含任何业务逻辑。
用 SenseVoice(阿里 FunASR)做中文识别,ffmpeg 抽音频, 全程本地推理,不调任何云 API。Apple 芯片自动走 MPS(Metal)加速。
MCP 是请求/响应模型,客户端超时通常只有 30~120s,而一条视频转写要几分钟到几十分钟。 所以这里用异步 submit / poll:
submit_transcribe(media_url) -> {"job_id": "..."} # 秒回,后台排队跑
get_transcribe_result(job_id) -> {"status": "running"} # 轮询
-> {"status": "done", "text": "...", "asr_seconds": 42.3}
任何 MCP 客户端都能用(Claude Desktop、自建流水线…)。
| 工具 | 说明 |
|---|---|
submit_transcribe(media_url, title?, engine?) |
提交任务,立即返回 job_id |
get_transcribe_result(job_id) |
查状态/取结果 |
cancel_transcribe(job_id) |
取消尚未开跑的任务 |
transcribe_status() |
服务状态:引擎/设备/队列积压(连通性检查) |
media_url 支持 http(s)://(可配 API key 鉴权)与 file:// 本地路径。
brew install ffmpeg # Ubuntu: apt install ffmpeg
python3.12 -m venv venv # 需要 Python ≥3.10
./venv/bin/pip install -r requirements.txt
cp config.toml.example config.toml # 按需改首次运行会自动下载 SenseVoice 模型(约 1GB)。离线环境请预先下载好模型缓存。
推荐:一键脚本(自动经 launchd 管理,并智能处理端口占用):
./start.sh # 启动/重启:端口被本程序占用→停旧重启;被别的程序占用→报错不动它
./stop.sh # 停止(只停本程序,别的程序不动)
./status.sh # 查看运行状态(launchd 托管 / 端口 / 是否本程序)start.sh 逻辑:端口空闲→启动;被本程序占用→bootout+kill 旧实例后重启;被其它程序占用→打印占用它的 PID/命令并退出(绝不误杀)。装了 launchd(见下)就走 launchd(开机自启+崩溃自愈),否则后台直接跑。
手动前台跑(调试用;跑前先 ./stop.sh 释放端口):
./venv/bin/python server.py # 127.0.0.1:8010
./venv/bin/python server.py --host 0.0.0.0 --preload # 对局域网开放 + 启动即加载模型--preload 让模型在启动时加载,首个任务不必等模型初始化。
cp deploy/com.weft.video-transcript.plist ~/Library/LaunchAgents/ # plist 里路径按本机改
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.weft.video-transcript.plist # 加载+启动
launchctl list | grep video-transcript # 查看 PID / 上次退出码(左列是数字=在跑)
tail -f ~/Library/Logs/video-transcript.log # 看日志
launchctl bootout gui/$(id -u)/com.weft.video-transcript # 停用装好后日常直接用 ./start.sh / ./stop.sh 即可(内部就走上面这些 launchctl)。
KeepAlive 会在进程崩溃/被杀后自动拉起(ThrottleInterval=30 秒退避)。两个坑:
- plist 里要显式设
PATH包含/opt/homebrew/bin——launchd 默认 PATH 不含它,否则找不到 ffmpeg。 - 日志路径别放
~/Documents/下:macOS TCC(文稿文件夹隐私保护)会让后台 launchd 打不开该目录下的文件写日志 → job 每次启动立即 退出 78 且不写任何日志(手动跑却正常)。故日志放~/Library/Logs/。
config.toml(或环境变量 VT_*,优先级更高):
| 项 | 默认 | 说明 |
|---|---|---|
host / port |
127.0.0.1 / 8010 |
监听地址 |
weft_key |
空 | 拉取媒体时带的 X-Weft-Key |
engine |
sensevoice |
ASR 引擎 |
concurrency |
1 |
同时转写几条 |
device |
auto |
auto/mps/cpu/cuda:0 |
job_timeout_s |
3600 |
单条最长处理时间 |
engines/ 是可插拔的:实现 load() / transcribe(wav_path) / info(),再在
engines/__init__.py 的 _REGISTRY 登记一行即可。已预留 whisper_cpp
(Apple 芯片上 Metal 极快、无 Python/torch 依赖)与 faster-whisper 的位置。
| 指标 | 实测 |
|---|---|
| 模型加载(一次性) | ~43s → 所以服务常驻、--preload 启动即加载 |
| 转写实时率 | 0.041x(140.5s 音频 → 5.7s) |
| 折算 | 1 小时音频 ≈ 2.4 分钟 |
短音频(<10s)因固定开销 RTF 会差很多(实测 0.23x),长音频才是真实水平。
- macOS 系统代理会拦 127.0.0.1:MCP 客户端(httpx)默认
trust_env=True,本机连本机会被代理 劫持成 502。跑客户端时设NO_PROXY=127.0.0.1,localhost,或在自己的客户端里用trust_env=False。 (Linux 服务器一般没这问题。) - 不要把模块命名成
queue.py:会 shadow 标准库queue,依赖链里任何import queue都会拿到你的模块。 本项目用的是jobqueue.py。
- 仅供内网/本机使用,不要直接暴露到公网(没有内建鉴权,鉴权应由反代或调用方网络边界负责)。
- 转写是 CPU/GPU 密集型;跑在与其它服务同机时建议
concurrency=1并配合nice降优先级。 - 请遵守内容来源平台的服务条款与版权规定,仅将本工具用于你有权处理的内容。