ShotSeek:我们如何在 DGX Spark 上把长视频搜索做成可交付的镜头定位
ShotSeek 参赛开发记录
做 ShotSeek 之前,我们先把手头几个相关项目翻了一遍。OpenBMB 相关工程里已经有视频处理和镜头边界的积累,NVIDIA VSS 的界面与 Agent 思路也能借鉴,StepFun 则提供视频理解和语音识别。最初的功能清单很长:自动剪片、多集管理、人脸实名、数字人,甚至直接控制 DaVinci。
比赛时间不长,这些功能不可能同时做好。最后留下的目标只有一个:用户说一句话,系统在长视频里找到对应镜头,跳到正确位置,说明为什么是这里,再把时间码交给剪辑软件。
后面的取舍都按这个目标来。
第一步:删功能
开始时我们没有画首页,也没有急着拆一堆 Agent。我们先写了“不做什么”,然后把结果格式定下来。一次命中至少要有入点、出点、关键帧、对白或画面证据,还要能导出。只返回一句“女主在仓库发现了尸体”不算完成,因为剪辑师仍然不知道该从哪一帧开始用。
项目最初借用了 OpenBMB 的技术底子,但没有把旧流水线整套搬过来。比赛版只准备复用镜头切点、shot-first、证据整理、边界审计和导出这些已经验证过的零件。新的 StepFun 接口、Scene 数据结构和 Agent 编排重新接,免得时间都花在整理旧工程依赖上。
第二步:验证 StepFun 接口
第一轮接口测试就卡住了。
Step Plan 的 Chat Completions 可以调用 step-3.7-flash,但标准 Files API 返回过 HTTP 402,Step Plan 当时能用的 SSE ASR 又没有说话人字段。官方建议先试 1—2 秒超短视频,并把 stream 设为 false。我们照做了,也验证了视频理解和时间偏移,但这仍然不能算完整通过,因为“能调一个模型”和“标准 Files、视频理解、异步 ASR 全部走通”是两回事。
所以 7 月 16 日的结果被保留为 partial。报告里明确写着 files_api_upload 和 speaker_info 没过,没有用本地假数据把它涂绿。
额度恢复后,7 月 17 日重新做了 Fresh Files 上传和异步 ASR。固定运行 20260717T031522.134383Z 最后得到 23 条视觉事件、7 个带说话人和毫秒时间戳的分句,以及 30 条统一证据,M0 的 15 项检查才全部通过。后面的离线 Fixture 也只允许从这次通过的真实运行脱敏生成。
第三步:统一时间
视频模型能描述画面,但它给出的往往是片段内部时间。原片被切成窗口以后,还有窗口起点、重叠区、帧率和镜头切点。直接把模型写出的 92 秒显示给用户,放回完整节目里很可能已经错了十几分钟。
我们最后同时保存毫秒和帧号。模型结果先从片段局部时间换算到原片全局时间,再吸附到真实 Shot Grid。播放器和接口用毫秒,边界审计和 EDL 用帧。页面上看到的时码只是最后一层格式化结果,不再是数据库里的事实。
把时间基定下来以后,播放器、Scene 和 EDL 才第一次对上同一个入点。
第四步:拆旧代码
原来的工程能力很多,整个迁过来反而很难解释。我们把可复用部分拆小:镜头检测负责给出 shot,shot-first 负责把语义区间校到镜头边界,boundary audit 检查入点和出点,evidence 统一画面与对白来源,exporters 负责交付。
StepFun 返回的任何事件都先当候选,不直接写成最终 Scene。字段缺失、负时长、越界、重叠和低置信都会在本地处理,原始响应保留,标准化结果另存。这样出了问题还能回答:是模型没看见、时间换算错了,还是边界校准错了。
第五步:处理“第一次”
最早的搜索基线很简单,SQLite FTS 召回相关 Scene。但很快就遇到几种完全不同的查询:精确对白、视觉动作、人物加地点,以及“第一次”“在某件事之前”这样的时间关系。
“找到女主第一次发现尸体”不能先拿普通 Top 3,再从里面挑时间最早的一个。那样会漏掉排名较低但时间更早的候选。现在的做法是先宽召回,再做人物和证据过滤,最后对全部有效候选执行序数运算。带引号的对白优先走 ASR,物体和动作优先看视觉证据,模糊剧情才需要混合召回。
M1 用 15 条查询把这条链路锁住。数字不大,它的作用是防止后面的 Agent 改坏已经能工作的 Search Core。
第六步:接 Planner 和 Verifier
做到 M2 时,我们才真正加入 Query Planner 和 Verifier。Planner 负责把自然语言拆成实体、动作、对白和时间约束;Retriever 给出候选;Verifier 检查直接证据、反证和边界。每一步都记录输入、输出、候选数量和淘汰原因。
这个设计没有 Agent 互相讨论的动画。执行轨迹更像一张办案记录:最初召回多少,角色过滤后剩多少,哪条证据支持,为什么最终选择这一段。
黄金集的 40 条查询可以稳定回归,但我们从一开始就把它叫 regression dataset,没有把满分写成“通用准确率”。
第七步:做 Runtime
走完前六步时,很多操作还要跑脚本。到了 Runtime 阶段,入口才变成 Upload → Job → Progress → Result。
Runtime 增加了持久化任务状态、SSE 进度、取消、阶段重试和重启恢复。媒体阶段限制并发,网络调用可以并行;浏览器通过 HTTP Range 播放代理视频,搜索结果点一下就能跳转,不必先把整部长片下载完。
做到这里,上传、处理、搜索、看证据和导出终于能在浏览器里连着走完。
第八步:收拾界面
讨论界面时,有一句反馈我们记得很清楚:“更商业一点”不是加收费入口,而是更懂得详略。
早期方案很容易把原始 JSON、GPU 指标、每路证据和 Agent 日志都放在首屏,因为这些最能证明技术工作量。但真正使用时,第一眼只需要播放器、搜索框、最佳结果、时间码和跳转按钮。Visual、Dialogue、Boundary、Trace 这些信息被收进 Evidence Drawer,需要解释时再展开。
导出也没有停在一个 JSON 按钮。最后保留了 JSON、SRT、XML 和 CMX3600 EDL,而且它们和播放器共用同一套 Scene 时间基,避免页面显示一个入点,交付文件又出现另一个入点。
第九步:跑长视频和独立集
36 分 58 秒素材最终处理到 READY,生成 216 个 Scene,视觉和 ASR 都是 LIVE,总用时 1784.916 秒,RTF 约 0.805。这次运行至少确认了 Runtime 能连续处理一段接近 37 分钟的素材。
随后跑 2 分 26 秒的独立素材时,任务却在媒体探测阶段失败。视频是 3650 帧、25fps,视频流正好 146.000 秒;AAC padding 把 MP4 容器写成了 146.048 秒。旧代码拿容器总时长和视频帧数比较,于是把一个正常 CFR 文件判成异常。
修复没有简单放宽所有阈值,而是改用视频流时长和帧数校验,同时保留对真实不一致的拒绝。相同素材之后零重试跑到 READY,144.770 秒生成 18 个 Scene。
同一阶段的独立评测结果如下:
| 数据集 | R@1 | R@3 |
|---|---|---|
| 黄金回归 | 100.0% | 100.0% |
| Development v1 | 100.0% | 100.0% |
| Holdout v1 | 66.7% | 66.7% |
| Longform v1 | 73.3% | 73.3% |
| Holdout v2 首次运行 | 16.7% | 16.7% |
Holdout v2 只返回两个结果,两个都正确,但 llama、animal、goat-like animal 的指代变化和复合动作被严格规则漏掉了。报告状态因此一直保留为 MIXED - GENERALIZATION GATES NOT MET。我们没有按结果修改冻结查询,也没有把失败样本挪出评测集。
第十步:封版
封版前主要在做提交材料、干净环境复现、演示脚本和备份。到这个阶段,再换模型、重构 Scene 或增加自动剪辑,带来的风险已经比收益大。
参赛版本冻结在 competition-demo-v1。之后只补了一项和搜索能力无关的运维增强:shotseek doctor。它默认离线,只读检查 Python、FFmpeg、NVIDIA、磁盘、端口、Runtime、SQLite 和前端资源;只有显式使用 deep 或 live 才会做真实 NVENC 编码或一次低成本 StepFun 文本请求。第一次写 Deep 探针时用了 128×72,GB10 的 NVENC 直接报帧尺寸低于支持下限,改成 640×360 后才通过。因此 Deep 检查保留真实编码,不再只看能力列表里有没有 h264_nvenc。
现在这套系统能把一次模型输出变成可以追溯、校时、播放和导出的结果,但跨实体叫法和复杂动作仍会漏检。这个问题留在评测报告里,没有在提交前临时改查询或挪样本。
比赛版先停在这里。如果之后继续,计划从一批全新素材的 Holdout v3 开始,先看这些漏检能不能真正降下来。
更多推荐



所有评论(0)