您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
基于大语言模型检测B站视频中的植入广告
当前为
// ==UserScript== // @name VideoAdGuard // @namespace https://github.com/Warma10032/VideoAdGuard // @version 1.1.0 // @description 基于大语言模型检测B站视频中的植入广告 // @author Warma10032 // @match *://*.bilibili.com/video/* // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @connect api.bilibili.com // @connect open.bigmodel.cn // @run-at document-end // @license MIT // ==/UserScript== (function() { 'use strict'; // 存储设置的默认值 const DEFAULT_API_URL = 'https://open.bigmodel.cn/api/paas/v4/chat/completions'; const DEFAULT_MODEL = 'glm-4-flash'; // 工具类 - WBI 签名 const WbiUtils = { mixinKeyEncTab: [ 46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35, 27, 43, 5, 49, 33, 9, 42, 19, 29, 28, 14, 39, 12, 38, 41, 13, 37, 48, 7, 16, 24, 55, 40, 61, 26, 17, 0, 1, 60, 51, 30, 4, 22, 25, 54, 21, 56, 59, 6, 63, 57, 62, 11, 36, 20, 34, 44, 52 ], getMixinKey(orig) { return this.mixinKeyEncTab .map(i => orig[i]) .join('') .slice(0, 32); }, // 简化版的 MD5 函数 (需要外部库支持) md5(text) { // 简单实现,实际使用时可能需要引入外部库 let hash = 0; for (let i = 0; i < text.length; i++) { hash = ((hash << 5) - hash) + text.charCodeAt(i); hash |= 0; } return hash.toString(16); }, async getWbiKeys() { const wbiCache = GM_getValue('wbi_cache'); const today = new Date().setHours(0, 0, 0, 0); if (wbiCache && wbiCache.timestamp >= today) { return [wbiCache.img_key, wbiCache.sub_key]; } return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: 'https://api.bilibili.com/x/web-interface/nav', headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 'Referer': 'https://www.bilibili.com/' }, onload: (response) => { try { const data = JSON.parse(response.responseText); if (data.code !== 0) { reject(new Error(data.message)); return; } const imgUrl = data.data.wbi_img.img_url; const subUrl = data.data.wbi_img.sub_url; const imgKey = imgUrl.substring(imgUrl.lastIndexOf('/') + 1, imgUrl.lastIndexOf('.')); const subKey = subUrl.substring(subUrl.lastIndexOf('/') + 1, subUrl.lastIndexOf('.')); const cache = { img_key: imgKey, sub_key: subKey, timestamp: today }; GM_setValue('wbi_cache', cache); resolve([imgKey, subKey]); } catch (error) { reject(error); } }, onerror: reject }); }); }, async encWbi(params) { const [imgKey, subKey] = await this.getWbiKeys(); const mixinKey = this.getMixinKey(imgKey + subKey); const currTime = Math.floor(Date.now() / 1000); const newParams = { ...params, wts: currTime }; // 按照key排序 const query = Object.keys(newParams) .sort() .map(key => { // 过滤特殊字符 const value = newParams[key].toString() .replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]/g, '') .replace(/[&?:\/=]/g, ''); return `${key}=${encodeURIComponent(value)}`; }) .join('&'); const wbiSign = this.md5(query + mixinKey); return { ...newParams, w_rid: wbiSign }; } }; // B站服务类 const BilibiliService = { async fetchWithCookie(url, params = {}) { const queryString = new URLSearchParams(params).toString(); const fullUrl = `${url}?${queryString}`; console.log('[BilibiliService] Fetching URL:', fullUrl); return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: fullUrl, headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 'Referer': 'https://www.bilibili.com/' }, onload: (response) => { try { const data = JSON.parse(response.responseText); console.log('[BilibiliService] Response data:', data); if (data.code !== 0) { reject(new Error(data.message)); return; } resolve(data.data); } catch (error) { reject(error); } }, onerror: reject }); }); }, async getVideoInfo(bvid) { console.log('[BilibiliService] Getting video info for bvid:', bvid); const data = await this.fetchWithCookie( 'https://api.bilibili.com/x/web-interface/view', { bvid } ); console.log('[BilibiliService] Video info result:', data); return data; }, async getComments(bvid) { console.log('[BilibiliService] Getting comments for bvid:', bvid); const data = await this.fetchWithCookie( 'https://api.bilibili.com/x/v2/reply', { oid: bvid, type: 1 } ); console.log('[BilibiliService] Comments result:', data); return data; }, async getPlayerInfo(bvid, cid) { console.log('[BilibiliService] Getting player info for bvid:', bvid, 'cid:', cid); const params = { bvid, cid }; const signedParams = await WbiUtils.encWbi(params); const data = await this.fetchWithCookie( 'https://api.bilibili.com/x/player/wbi/v2', signedParams ); console.log('[BilibiliService] Player info result:', data); return data; }, async getCaptions(url) { console.log('[BilibiliService] Getting captions from URL:', url); return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: url, onload: (response) => { try { const data = JSON.parse(response.responseText); console.log('[BilibiliService] Captions result:', data); resolve(data); } catch (error) { reject(error); } }, onerror: reject }); }); } }; // AI 服务类 const AIService = { async analyze(videoInfo) { console.log('开始分析视频信息:', videoInfo); const apiKey = this.getApiKey(); if (!apiKey) { throw new Error('未设置API密钥'); } console.log('成功获取API密钥'); return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'POST', url: this.getApiUrl(), headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }, data: JSON.stringify({ model: this.getModel(), messages: [ { 'role': 'system', 'content': '你是一个敏感的视频观看者,能根据视频的连贯性改变和宣传推销类内容,找出视频中可能存在的植入广告。内容如果和主题相关,即使是推荐/评价也可能只是分享而不是广告,重点要看有没有提到通过视频博主可以受益的渠道进行购买。' }, { 'role': 'user', 'content': this.buildPrompt(videoInfo) } ], response_format: { 'type': 'json_object' }, temperature: 0.1, max_tokens: 1024 }), onload: (response) => { try { const data = JSON.parse(response.responseText); console.log('收到API响应:', data); resolve(JSON.parse(data.choices[0].message.content)); } catch (error) { reject(error); } }, onerror: reject }); }); }, buildPrompt(videoInfo) { console.log('构建提示词,输入数据:', videoInfo); const prompt = `视频的标题和置顶评论如下,可供参考判断是否有植入广告。如果置顶评论中有购买链接,则肯定有广告,同时可以根据置顶评论的内容锁定视频中广告的位置。 视频标题:${videoInfo.title} 置顶评论:${videoInfo.topComment || '无'} 下面我会给你这个视频的字幕字典,形式为 index: context. 请你完整地找出其中的植入广告,返回json格式的数据。内容如果和主题相关,即使是推荐/评价也可能只是分享而不是广告,重点要看有没有提到通过视频博主可以受益的渠道进行购买。一般来说视频是没有广告的,但也有小部分会植入一段广告,极少部分是多段广告。 字幕内容:${JSON.stringify(videoInfo.captions)} 先返回'exist': bool。true表示存在植入广告,false表示不存在植入广告。 再返回'index_lists': list[list[int]]。二维数组,行数表示广告的段数,一般来说视频是没有广告的,但也有小部分会植入一段广告,极少部分是多段广告。每一行是长度为2的数组[start, end],表示一段广告的开头结尾,start和end是字幕的index。`; return prompt; }, getApiUrl() { return GM_getValue('apiUrl', DEFAULT_API_URL); }, getApiKey() { return GM_getValue('apiKey', null); }, getModel() { return GM_getValue('model', DEFAULT_MODEL); } }; // 广告检测器类 const AdDetector = { adDetectionResult: null, adTimeRanges: [], async getCurrentBvid() { const match = window.location.pathname.match(/\/video\/(BV[\w]+)/); if (!match) throw new Error('未找到视频ID'); return match[1]; }, async analyze() { try { // 移除已存在的跳过按钮 const existingButton = document.querySelector('.skip-ad-button10032'); if (existingButton) { existingButton.remove(); } const bvid = await this.getCurrentBvid(); // 获取视频信息 const videoInfo = await BilibiliService.getVideoInfo(bvid); const comments = await BilibiliService.getComments(bvid); const playerInfo = await BilibiliService.getPlayerInfo(bvid, videoInfo.cid); // 获取字幕 if (!playerInfo.subtitle?.subtitles?.length) { console.log('无字幕'); this.adDetectionResult = '当前视频无字幕,无法检测'; return; } const captionsUrl = 'https:' + playerInfo.subtitle.subtitles[0].subtitle_url; const captionsData = await BilibiliService.getCaptions(captionsUrl); // 处理数据 const captions = {}; captionsData.body.forEach((caption, index) => { captions[index] = caption.content; }); // AI分析 const result = await AIService.analyze({ title: videoInfo.title, topComment: comments.upper?.top?.content?.message || null, captions }); if (result.exist) { console.log('检测到广告片段:', JSON.stringify(result.index_lists)); const second_lists = this.index2second(result.index_lists, captionsData.body); this.adTimeRanges = second_lists; this.adDetectionResult = `发现${second_lists.length}处广告:${ second_lists.map(([start, end]) => `${this.second2time(start)}~${this.second2time(end)}`).join(' | ') }`; // 注入跳过按钮 this.injectSkipButton(); // 显示通知 this.showNotification(this.adDetectionResult); } else { this.adDetectionResult = '无广告内容'; } } catch (error) { console.error('分析失败:', error); this.adDetectionResult = '分析失败:' + error.message; } }, index2second(indexLists, captions) { // 直接生成时间范围列表 const time_lists = indexLists.map(list => { const start = captions[list[0]]?.from || 0; const end = captions[list[list.length - 1]]?.to || 0; return [start, end]; }); return time_lists; }, second2time(seconds) { const hour = Math.floor(seconds / 3600); const min = Math.floor((seconds % 3600) / 60); const sec = Math.floor(seconds % 60); return `${hour > 0 ? hour + ':' : ''}${min.toString().padStart(2, '0')}:${sec.toString().padStart(2, '0')}`; }, injectSkipButton() { const player = document.querySelector('.bpx-player-control-bottom'); if (!player) return; const skipButton = document.createElement('button'); skipButton.className = 'skip-ad-button10032'; skipButton.textContent = '跳过广告'; skipButton.style.cssText = ` font-size: 14px; position: absolute; right: 20px; bottom: 100px; z-index: 999; padding: 4px 4px; color: #000000; font-weight: bold; background: rgba(255, 255, 255, 0.7); border: none; border-radius: 4px; cursor: pointer; `; player.appendChild(skipButton); // 监听视频播放时间 const video = document.querySelector('video'); if (!video) { console.error('未找到视频元素'); return; } // 点击跳过按钮 skipButton.addEventListener('click', () => { const currentTime = video.currentTime; console.log('当前时间:', currentTime); const adSegment = this.adTimeRanges.find(([start, end]) => currentTime >= start && currentTime < end ); if (adSegment) { video.currentTime = adSegment[1]; // 跳到广告段结束时间 console.log('跳转时间:', adSegment[1]); } }); }, showNotification(message) { const notification = document.createElement('div'); notification.style.cssText = ` position: fixed; top: 20px; right: 20px; background: rgba(0, 0, 0, 0.7); color: white; padding: 10px 15px; border-radius: 4px; z-index: 9999; max-width: 300px; `; notification.textContent = message; document.body.appendChild(notification); setTimeout(() => { notification.style.opacity = '0'; notification.style.transition = 'opacity 0.5s'; setTimeout(() => notification.remove(), 500); }, 5000); }, // 添加设置面板 addSettingsButton() { const settingsButton = document.createElement('button'); settingsButton.textContent = '⚙️'; settingsButton.style.cssText = ` position: fixed; bottom: 20px; right: 20px; width: 40px; height: 40px; border-radius: 50%; background: rgba(0, 0, 0, 0.7); color: white; border: none; font-size: 20px; cursor: pointer; z-index: 9999; `; document.body.appendChild(settingsButton); settingsButton.addEventListener('click', () => { this.showSettingsPanel(); }); }, showSettingsPanel() { // 移除已存在的面板 const existingPanel = document.querySelector('.vag-settings-panel'); if (existingPanel) { existingPanel.remove(); return; } const panel = document.createElement('div'); panel.className = 'vag-settings-panel'; panel.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.3); z-index: 10000; width: 300px; `; panel.innerHTML = ` <h3 style="margin-top: 0;">VideoAdGuard 设置</h3> <div style="margin-bottom: 15px;"> <label style="display: block; margin-bottom: 5px;">API地址:</label> <input type="text" id="vag-api-url" style="width: 100%; padding: 5px; box-sizing: border-box;" value="${AIService.getApiUrl()}"> </div> <div style="margin-bottom: 15px;"> <label style="display: block; margin-bottom: 5px;">API密钥:</label> <input type="password" id="vag-api-key" style="width: 100%; padding: 5px; box-sizing: border-box;" value="${AIService.getApiKey() || ''}"> </div> <div style="margin-bottom: 15px;"> <label style="display: block; margin-bottom: 5px;">模型名称:</label> <input type="text" id="vag-model" style="width: 100%; padding: 5px; box-sizing: border-box;" value="${AIService.getModel()}"> </div> <div style="display: flex; justify-content: space-between;"> <button id="vag-save" style="padding: 8px 15px; background: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer;"> 保存 </button> <button id="vag-cancel" style="padding: 8px 15px; background: #f44336; color: white; border: none; border-radius: 4px; cursor: pointer;"> 取消 </button> </div> `; document.body.appendChild(panel); // 保存按钮事件 document.getElementById('vag-save').addEventListener('click', () => { const apiUrl = document.getElementById('vag-api-url').value; const apiKey = document.getElementById('vag-api-key').value; const model = document.getElementById('vag-model').value; GM_setValue('apiUrl', apiUrl); GM_setValue('apiKey', apiKey); GM_setValue('model', model); this.showNotification('设置已保存'); panel.remove(); }); // 取消按钮事件 document.getElementById('vag-cancel').addEventListener('click', () => { panel.remove(); }); } }; // 初始化 function init() { // 页面加载完成后执行分析 AdDetector.analyze(); // 添加设置按钮 AdDetector.addSettingsButton(); // 添加 URL 变化监听 let lastUrl = location.href; new MutationObserver(() => { const url = location.href; if (url !== lastUrl) { lastUrl = url; console.log('URL changed:', url); AdDetector.analyze(); } }).observe(document, { subtree: true, childList: true }); // 监听 history 变化 window.addEventListener('popstate', () => { console.log('History changed:', location.href); AdDetector.analyze(); }); } // 等待页面加载完成 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址