Skip to main content
Glama
wjxn13

dsh-video-evidence

by wjxn13

dsh-video-evidence

兼容的 Harness 版本

  • 已验证支持:DeepSeek Harness 0.1.0-rc.6(通过 @deepseek-ai/dsh-mcp-client,跨 profile 可用)。

  • 最新版 0.1.2-alpha.3:适配计划进行中,暂无确切完成时间。升级前请先备份你的 DSH 安装。

DeepSeek Harness (dsh) 的 MCP 插件:按问题检索 B 站视频,并定位到真正讲到该内容的片段(带时间戳、可直接回跳)。

不是"搜到视频"就结束,而是回答"这个问题的答案在视频的第几分钟"。

用户:Spring Boot 项目怎么创建?

→ 1. https://www.bilibili.com/video/BV17m411d7Ny?t=0
     环境准备、JDK 版本选择 …
→ 2. https://www.bilibili.com/video/BV17m411d7Ny?t=60
     打开 IDEA,新建 Project,选择 Spring Initializr …
→ 3. https://www.bilibili.com/video/BV17m411d7Ny?t=90
     填写 Group / Artifact,勾选依赖,Finish …

Related MCP server: video_agent

为什么做这个

市面上的"跨模态检索 / 智能搜索"插件,绝大多数做的是网页和文本这一层:给一段查询,返回一堆链接和摘要。这一层已经很挤了,重复造轮子没有意义。

真正还没有被好好解决的,是视频内部的片段级定位

  • 搜索引擎只能告诉你"这个视频可能相关",不会告诉你相关的是哪 30 秒;

  • 视频平台自带的字幕搜索依赖 UP 主上传字幕,覆盖率和质量都靠运气;

  • 对教程、课程录像、会议发言这类内容,用户真正要的从来不是"整片",而是"讲到 X 的那一小段"。

本插件补的就是这一段:公开视频 → 本地转写 → 片段级检索 → 可回跳的时间戳证据。全部本地完成,不依赖任何付费 API 或第三方检索服务。


工作流程

        ┌──────────────────────── dsh (MCP client) ────────────────────────┐
        │  mcp__video_evidence__video_search  /  __video_cache_stats       │
        └───────────────────────────────┬──────────────────────────────────┘
                                        │ stdio / JSON-RPC
        ┌───────────────────────────────▼──────────────────────────────────┐
        │  server.js   (Node 常驻进程,只做协议转发)                       │
        │   · 双分帧解析:NDJSON ⇄ Content-Length                           │
        │   · 收到 tools/call 才 spawn Python worker                        │
        └───────────────────────────────┬──────────────────────────────────┘
                                        │ spawn(按需,用完即退)
        ┌───────────────────────────────▼──────────────────────────────────┐
        │  worker.py → server.py                                           │
        │   1. bili.py      B 站 wbi 签名搜索 → 取 cid → 取 360P 流地址      │
        │   2. asr.py       ffmpeg 拉流转 16k 单声道 wav                     │
        │                   faster-whisper (base/int8, CPU) 转写            │
        │   3. cache.py     SQLite 按 BV 号缓存转写结果(TTL 30 天)          │
        │   4. evidence.py  中文 BM25(单字 + 二元组)打分                    │
        │                   45s 滑窗 / 30s 步长切块 → 排序取 top-k           │
        └──────────────────────────────────────────────────────────────────┘

为什么是 Node 常驻 + Python worker: dsh 的 dsh-mcp-client spawn 之后会立刻发 initialize,跟 Python 常驻进程握手不稳定(实测 4 次启动、0 条消息收到)。Node 冷启动足够快,而真正的检索/ASR 留在 Python 侧,只在工具被真正调用时才拉起,空闲时不占资源。


安装

前置依赖

依赖

说明

Node.js

18+,只用到内置模块,无需 npm install

Python

3.9+

ffmpeg

必须在 PATH 中,或用 FFMPEG_BIN 指定绝对路径

faster-whisper

pip install -r requirements.txt

git clone https://github.com/wjxn13/dsh-video-evidence.git
cd dsh-video-evidence

# Windows
python -m venv .venv
.venv\Scripts\pip install -r requirements.txt

# macOS / Linux
python3 -m venv .venv
# 注意:server.js 里写死了 .venv/Scripts/python.exe(Windows 布局)。
# 非 Windows 需要把 server.js 中 PY 的路径改成 .venv/bin/python。
.venv/bin/pip install -r requirements.txt

首次调用 video_search 时,faster-whisper 会自动下载 base 模型到 models/base/(约 145MB)。也可以手动放置,或用 ASR_MODEL_DIR 指向已有模型目录。

接入 dsh

在用户级 ~/.dsh/cordis.patch.yml 里追加(路径改成你自己的):

- insert:
    - id: mcp-video-evidence
      name: "@deepseek-ai/dsh-mcp-client"
      config:
        serverName: video_evidence        # ← 决定工具名前缀:mcp__video_evidence__*
        transport: stdio
        command: "C:/path/to/node.exe"
        args:
          - "C:/path/to/dsh-video-evidence/server.js"
        cwd: "C:/path/to/dsh-video-evidence"
        env:
          PYTHONIOENCODING: "utf-8"
          PYTHONUNBUFFERED: "1"
        toolCallTimeoutMs: 240000

几个关键点:

  • name 要填 @deepseek-ai/dsh-mcp-client,这是 dsh 的 MCP loader 包名,不是随便起的名字。

  • serverName 决定工具名前缀mcp__<serverName>__<toolName>,所以这里是 mcp__video_evidence__video_search

  • command 用 node 的绝对路径。dsh 会清理子进程环境,不能依赖 PATH。

  • cwd 要指向插件目录server.js 依赖它定位 .venvworker.py

  • toolCallTimeoutMs 给足(这里 240 秒)。首次查询要拉流 + 转写,默认的短超时会让工具在结果出来前被判失败。

然后重启 dsh——配置只在启动时加载。用 dsh --dump-config --profile web 可以确认条目有没有被正确合并进去。

⚠️ id 不能和已有条目重复。 cordis loader 不允许同 id 覆盖,重复会直接让 dsh 启动崩溃(报 duplicate loader entry)。改配置前先备份 cordis.patch.yml


工具

参数

类型

默认值

说明

query

string

必填

要检索的问题或关键词

max_videos

integer

3

最多处理几个候选视频(1–5)

max_clips

integer

5

返回几个最相关片段(1–10)

audio_seconds

integer

300

每个视频最多转写前多少秒(60–1800)

budget_seconds

integer

170

总时间预算,超时就返回已经拿到的结果

video_cache_stats

无参数。返回已缓存转写的视频数量。


性能实测

本机环境:Windows 11 / Ryzen 7 6800H / RTX 3050 Ti(纯 CPU 推理,不用显卡),faster-whisper base + int8

项目

实测

转写速度

45 秒音频 ≈ 2.3 秒(约 20 倍实时)

10 分钟视频

≈ 30 秒

命中缓存后

几乎瞬时(只查 SQLite + BM25)

也就是说,一次交互式提问(转写前 90–180 秒)通常在几秒内返回。


环境变量

变量

默认值

说明

FFMPEG_BIN

PATH 里的 ffmpeg

ffmpeg 可执行文件路径。不设则自动从 PATH 查找,找不到才需要显式指定

ASR_MODEL_DIR

<插件目录>/models/base

faster-whisper 模型目录

BILI_SESSDATA

B 站 SESSDATA Cookie。不填也能用(搜索/取流正常),填了可以尝试走官方字幕通道

VDB_CACHE

<插件目录>/cache.sqlite3

转写缓存数据库路径

VDB_CACHE_TTL

2592000(30 天)

缓存有效期(秒)

VDB_AUDIO_SEC

300

默认单视频转写时长上限

VDB_BUDGET_SEC

170

默认总时间预算

VDB_MAX_VIDEOS

3

默认候选视频数

VDB_WINDOW / VDB_STRIDE

45 / 30

滑窗切块的窗口长度与步长(秒)

VDB_WORKER_TIMEOUT

220000

Python worker 超时(毫秒)


已知限制

  • 只覆盖 B 站公开视频,且只转写前 audio_seconds 秒(默认 5 分钟)。长视频靠后的内容需要调大该参数,耗时线性增长。

  • 匿名状态下拿不到官方字幕subtitles 接口返回 0 条),所以一律走本地 ASR,不依赖 UP 主有没有上传字幕。

  • BM25 是词面匹配,不做语义向量检索。换个说法提问可能命中不到,这是有意的取舍——本地 CPU 上跑 embedding 模型会显著拖慢交互式响应。

  • server.js 里 Python 路径写死为 .venv/Scripts/python.exe(Windows 布局),非 Windows 需要手动改一处。


排错

工具注册不上去(dsh 里看不到 mcp__video_evidence__*

这是最坑的一类问题,现象是:配置明明正确(dsh --dump-config --profile web 能看到条目),重启也没用,日志里反复刷 server ready → stdin closed → server ready

根因:同一个 dsh 实例里存在两套 MCP 分帧格式。

  • dsh 自带的 dsh-mcp-client 发的是 NDJSON(裸 JSON + \n没有 Content-Length

  • argo / memorix 这类用标准 MCP SDK 的走的是 Content-Length: N\r\n\r\n{json}

只认 Content-Length 的解析器收 NDJSON 时永远凑不出一条完整消息 → 不回 initialize → dsh 超时关 stdin → 重拉,死循环。

本插件的 server.js 已经用 clientFraming 闩锁 + nextMessage() 做了双格式兼容,响应按客户端格式对称回写。如果你要 fork 改写协议层,这两段不要动

配套的两个必要细节:

  1. initialize 必须回显客户端发来的 protocolVersion,不能硬编码。MCP SDK 会校验服务端返回的版本是否在自身支持列表内,硬编码新版会让较老 SDK 判定协商失败并直接关闭 stdin。

  2. ping / resources/list / prompts/list 要返回空结果而不是 -32601 客户端探测能力时收到 method not found 会判定连接不健康,进而断连重连。

怎么定位: 在 stdin 的 data 回调里打第一条原始数据:

if (!gotAny) {
  gotAny = true;
  log("FIRST stdin data: raw=" + JSON.stringify(d.toString("utf8").slice(0, 400)));
}

看到 {"method":"initialize",...} 后面直接跟 \n、没有 Content-Length,就确认是 NDJSON。

B 站搜索返回空(HTTP 200、code:0、但结果为空)

wbi 签名的坑:urllib.parse.urlencode 默认用 quote_plus,空格编码成 +;B 站服务端按 %20 重算签名,校验不通过就静默拒绝。

# 必须用 quote(空格 -> %20),不能用默认的 quote_plus(空格 -> +)
q = urllib.parse.urlencode(p, quote_via=urllib.parse.quote)

bili.py 里已经修好,并且加了非 wbi 的 legacy 接口作为兜底。


目录结构

dsh-video-evidence/
├── server.js      # Node 常驻 MCP server(协议层:双分帧 + 方法分发)
├── worker.py      # Python worker 入口(CLI,读 JSON 参数、打 JSON 结果)
├── server.py      # 业务编排:搜索 → 取流 → 转写 → 检索
├── bili.py        # B 站 API 客户端(wbi 签名、搜索、cid、playurl)
├── asr.py         # ffmpeg 拉流 + faster-whisper 转写
├── evidence.py    # 中文 BM25 打分 + 滑窗切块
├── cache.py       # SQLite 转写缓存(按 BV 号,TTL 30 天)
└── requirements.txt

License

MIT

Related MCP Connectors

Related MCP Servers