V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
爱意满满的作品展示区。
hh54188
V2EX  ›  分享创造

哈哈,发现一个用 AI 跳过 B 站带货广告的办法

  •  
  •   hh54188 · 8 小时 8 分钟前 · 1883 次点击

    使用教程、源码、下载地址都在这里:https://v2think.com/ad-killer


    这件事的技术含量不高,但是它很有趣。这是我制作的一款插件,插件的下载地址、源码以及使用方式在这里。如果你是在微信公众号上阅读到的这篇文章,请点击左下角的阅读原文获取。

    同时它也开源的,其解决问题的核心思路也是本文想要分享的。希望它能给你带来更多实用 AI 解决问题的灵感。

    Showcase

    这是视频演示

    以下是图文讲解

    简单来说插件的广告识别分为两个步骤,首先如果它发现该视频具备广告识别的条件,在浏览器周围会出现浅蓝色光晕,代表它正在思考

    image.png

    一旦识别完成之后,光晕消失。在进度条上出现广告时间区间

    image.png

    在播放极端即将进入广告区间时,播放器周围会出现五彩光晕进行提示,表示即将跳过该广告区间

    image.png

    核心原理

    简单来说通过字幕

    网站在加载每个视频时都需要请求一个路径为api.bilibili.com/x/player/wbi/v2的接口,我不知道这个接口是做什么的,但是在这个接口中会返回字幕的“元信息”。之所以称之为元信息,是因为它不包含字幕本身,而是包含围绕字幕的有关其他信息,例如该字幕属于哪国语言,该字幕是否由 AI 生成,以及最重要的请求字幕用的 URL

    注意,接口只会在登录的情况下才会返回字幕信息。同时注意字幕的 URL 中是带有 token 的,所以你无法一直持有并访问它,因为 token 需要被更新。

    image.png

    我通常会选取中文字幕,并且找到其中的 URL ,然后发送请求,得到的字幕结果内容如下:

    image.png

    理论上我认为把这段 JSON 发给 AI ,它就可以识别出其中的广告。但是为了确保准确性,以及节省 token ,这里我们需要将其传化为更为简单的结构,转化的函数如下:

    export function convertSubtitleObjToStr(subtitles: subtitle[]): string {
        return subtitles.map((sub: subtitle) => {
            const { from, to, content } = sub
            const subtitleStr: SubtitleString = `[${from}-${to}]:${content}`;
            return subtitleStr;
        }).join(';')
    }
    

    转化后的结果如下:

    [0.82-2.06]:哇来了哇;[2.06-5.48]哦让我们去看看这个今天就吃一桶螺蛳粉了;[5.48-9.41]哇这哇哦终于吃螺蛳粉了;
    

    在将字幕转化为指定格式之后,需要将其发送至 AI ,送使用的提示词( prompt )如下:

    接下我会分享给你一段视频字幕,该段字幕由多个字幕语句组成。 每一句字幕包含三部分内容,分别是起始时间,结束时间,以及字幕内容,格式如下:[{起始时间}-{结束时间}]:{字幕内容}。语句之间由分号(;)隔开。 帮助我分析其中哪些是与视频无关的广告内容,给出其中连续广告内容起始时间和终止时间。我可能还会分享给你视频的标题以及视频的描述,用于辅助你判断广告内容 如果存在广告内容,请将广告的起止时间返回给我,返回格式为:{startTime: number, endTime: number} 如果不存在广告内容,返回 null 字幕内容如下:

    因为我需要在网页播放器的时间轴上精确的渲染出来广告时间段,所以在上述提示词中我明确告诉 AI 将广告的起止时间以 JSON 的格式返回。

    继续优化

    以上能够覆盖 90%的情况了,但是在代码的实现过程中还是有必要解决一下边缘情况。

    首先在提示词中指定返回的 JSON 格式并不靠谱,于是我选择在调用 SDK 的代码中以代码的方式指定返回的 JSON schema

    const responseSchema = {
        type: 'OBJECT',
        properties: {
            startTime: {type: 'number', nullable: false},
            endTime: {type: 'number', nullable: false},
        },
        required: ['startTime', 'endTime'],
    };
    
    const response = await geminiClient.models.generateContent({
        model: aiModel,
        config: {
            responseJsonSchema: responseSchema,
            responseMimeType: "application/json",
            httpOptions: {
                timeout: 1000 * 60,
            }
        },
        contents: ''
    });
    

    其次我们还可以通过向 AI 提供更丰富的信息来协助他,例如视频的标题和描述,所以的我最终喂给 AI 的最终提示词中实际上是包含视频的标题以及描述的:

    接下我会分享给你一段视频字幕,该段字幕由多个字幕语句组成

    ……

    字幕内容如下: xxxx

    视频标题如下: xxxx

    视频描述如下: xxx

    最后我发现对于短视频 AI 通过字幕判断广告的并不理想,所以默认我不对五分钟以内的视频进行判断。

    技术细节

    也许你对实现过程中的一些技术细节感兴趣,这里我把我能够想到的都分享出来。

    如何找到字幕接口的

    搜索,这是最简单的方式。

    例如在上面的视频中我发现视频字幕中出现了“螺蛳粉”这三个字,于是我就在 Chrome 的开发者工具中全局搜索“螺蛳粉”(你需要再视频一开始加载时就打开开发者工具),结果如下

    image.png

    很明搜索列表的第一个结果就是字幕接口的返回,右边就可以找到该返回所对应的请求 URL 是什么。有了这个 aisubtitle.hdslb.com 关键词之后我就顺藤摸瓜找到返回这个域名的接口

    image.png

    如何拿到视频的标题、描述以及时长信息

    事实上我不用等待页面完全渲染完毕之后从 DOM 中截取,视频的各种基本信息已经被提前被存储到了全局变量window.__INITIAL_STATE__

    image.png

    通过页面源代码不难看出,该变量早就由后端渲染在了页面上

    image.png

    事实上还有一种较为复杂的场景是播放列表。在这个场景中上一个视频播放完毕之后会自动播放下一个视频。不用担心,此时的window.__INITIAL_STATE__变量内的数据也会得到更新

    如何拦截请求并得到其中的字幕加载 URL

    前端想要发送 API 请求,无非只有两个手段 1 )通过 XMLHttpRequest ; 2 )通过 Fetch API 。经过测试不难发现 Bilibili 网站试用的是前者,于是我选择通过 monkey patch 方式将它“黑”掉,替换成我的实现方法以便我监控每一个发出的请求,以及获取到它们的返回:

      const originalOpen = XMLHttpRequest.prototype.open;
      const originalSend = XMLHttpRequest.prototype.send;
    
      XMLHttpRequest.prototype.open = function(method: string, url: string | URL, ...args: any[]) {
        // @ts-ignore
        this._url = url.toString();
        // @ts-ignore
        return originalOpen.call(this, method, url, ...args);
      };
    
      XMLHttpRequest.prototype.send = function(...args: any[]) {
        // @ts-ignore
        const url = this._url;
    
        if (window.location.pathname.startsWith('/video/') && url && url.includes('api.bilibili.com/x/player/wbi/v2')) {
          console.log('📺 ✔️ Detected player API request');
    

    为什么只考虑了 Gemini ?

    因为兼容每一种大模型都需要时间成本,而我的精力实在有限(欢迎给源代码提 pull request )。我唯二非常想集成的大模型是 OpenAI ,苦于他们不接受国内信用卡,亲测 Google Pay 也无效。

    集成不同模型最高效的办法是用某个兼容超强的 SDK ,例如 Vercel 提供的AI SDK,又或者某个 Agent 框架例如Mastra,可问题在于虽然多数框架支持的编程语言是 TypeScript ,但框架并支持在浏览器端运行。

    事实上我还考虑过使用 Chrome 内置的本地模型——没错,目前 Chrome 支持用户下载一个 Gemini Nano 模型安装在本地(大约 2G 左右),但是该模型判断广告的效果非常差,于是隐藏了该功能。你还可以在我的源码中找到该部分代码

    未来

    如果每个人都安装了这个插件的话,可想而知同一个热门视频的字幕会被发送给 AI 很多次——这其实是一种浪费。理想情况下,只要我们中的任何一个人拿到了 AI 返回的结果那么它接可以与其他人共同分享。

    这才是我最想做的事情:把每个人得到的不同视频的分析结果上传到云端共享,这样一来不仅用户体验可以大大提升,个体花费的时间和金钱也可以节省不少。

    25 条回复    2026-01-27 19:41:08 +08:00
    craftsmanship
        1
    craftsmanship  
       8 小时 3 分钟前 via Android
    这个可以有👍
    zhouweiluan
        2
    zhouweiluan  
       7 小时 53 分钟前
    牛逼思路
    w3
        3
    w3  
       7 小时 18 分钟前
    NB !我的选择是基本不看 B 站。
    HeyWeGo
        4
    HeyWeGo  
       7 小时 17 分钟前
    投放和屏蔽广告的双方互相对抗 ing
    TrackBack
        5
    TrackBack  
       7 小时 16 分钟前
    有一个古法众包手动标注的插件:
    https://github.com/hanydd/BilibiliSponsorBlock

    曾经有人用了 AI 提交标注的广告片段,但是因为太不准被大家踩爆了
    不知道 lz 的这个准确的咋样
    panda188
        6
    panda188  
       7 小时 14 分钟前 via Android
    token 消耗爆表🙃
    listenerri
        7
    listenerri  
       6 小时 55 分钟前
    有趣,感谢分析
    dule
        8
    dule  
       6 小时 31 分钟前
    思路不错
    hugsky
        9
    hugsky  
       6 小时 29 分钟前
    https://bsbsb.top/ 空降助手
    dule
        10
    dule  
       6 小时 27 分钟前
    @dule 要是有专门的模型能部署本地或者闲置的服务器上不必消耗 token 就更妙了,广告方一般是固定的那些商家,通过维护关键字匹配是不是也可以,只是视频对应的前后进度区间不好获取,这种不明确嵌入式的广告观感真的还不如油管这种开会员能明确跳过的体验感好的,另外 op 还要考虑 b 站会不会律师函警告啥的
    my101du
        11
    my101du  
       5 小时 49 分钟前
    好像这是违法的。。。

    我还想过开发过在网页版播放爱优腾/油管视频的时候,检测开头广告,自动静音,覆盖一个全屏强制背单词/提肛的。
    自己用吧,不如花点钱买个会员了。 分享出去吧,要坐穿牢底了。
    zhouqian
        12
    zhouqian  
       5 小时 17 分钟前
    这个思路确实可以!
    peasant
        13
    peasant  
       5 小时 1 分钟前
    @my101du 这和你说的应该不是一个东西,你说的是平台投放的广告,OP 跳过的是 UP 主在自己视频里插的带货广告。
    Cheons
        14
    Cheons  
       4 小时 59 分钟前 via Android
    pilipala 里的 sponsor block 挺好用的。

    看了下就是上面说过的 空降助手
    urlk
        15
    urlk  
       4 小时 53 分钟前
    把功能整合进空降助手, 实现人工+AI 双通道数据采集, 通过共享助手平台数据共享 , 完美
    whoosy
        16
    whoosy  
       4 小时 50 分钟前
    其实还可以结合弹幕来识别
    xu11111111
        17
    xu11111111  
       4 小时 44 分钟前
    tomclancy
        18
    tomclancy  
       4 小时 42 分钟前
    最好恐怕是不看了
    现在叔叔说没广告实际基本都是广告,也就百大级别接宣发少一些
    中小创作者基本没多少创作收入
    ala2008
        19
    ala2008  
       4 小时 30 分钟前
    token 消耗怎么样
    fengkookoo2
        20
    fengkookoo2  
       3 小时 47 分钟前
    只能网页端用
    goodryb
        21
    goodryb  
       2 小时 47 分钟前
    如果做成带服务端,那效果估计会更好,不过隐私这块是个问题
    shuiduoduo
        22
    shuiduoduo  
       2 小时 33 分钟前 via iPhone
    loon 的站插件
    iorilu
        23
    iorilu  
       1 小时 47 分钟前
    蛮厉害的

    不过 b 站也没别的法子弄钱, up 主自力更生搞点广告看看也还行把
    shewhen
        24
    shewhen  
       59 分钟前 via iPhone
    样式也肥肠帅气!支持⬆️
    wl9739
        25
    wl9739  
       32 分钟前
    在不远的未来,地球不再缺少算力的时候,这个方案可以铺开来
    关于   ·   帮助文档   ·   自助推广系统   ·   博客   ·   API   ·   FAQ   ·   Solana   ·   3258 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 23ms · UTC 12:13 · PVG 20:13 · LAX 04:13 · JFK 07:13
    ♥ Do have faith in what you're doing.