├── CNAME ├── robots.txt ├── img.png ├── BilibiliAiSkip ├── icons │ ├── icon128.png │ ├── icon48.png │ ├── icon48_blue.png │ ├── icon48_red_0.png │ ├── icon48_red_1.png │ ├── icon48_red_2.png │ └── icon48_red_3.png ├── prompt.txt ├── manifest.json ├── background.js ├── popup.js ├── popup.html └── content.js ├── BilibiliAiSkip_Firefox ├── icons │ ├── icon128.png │ ├── icon48.png │ ├── icon48_blue.png │ ├── icon48_red_0.png │ ├── icon48_red_1.png │ ├── icon48_red_2.png │ └── icon48_red_3.png ├── prompt.txt ├── manifest.json ├── popup.js ├── background.js ├── popup.html └── content.js ├── script └── bilijump.sgmodule ├── README.md └── index.html /CNAME: -------------------------------------------------------------------------------- 1 | oooo.uno -------------------------------------------------------------------------------- /robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingmeng1/bilijump-ai/HEAD/img.png -------------------------------------------------------------------------------- /BilibiliAiSkip/icons/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingmeng1/bilijump-ai/HEAD/BilibiliAiSkip/icons/icon128.png -------------------------------------------------------------------------------- /BilibiliAiSkip/icons/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingmeng1/bilijump-ai/HEAD/BilibiliAiSkip/icons/icon48.png -------------------------------------------------------------------------------- /BilibiliAiSkip/icons/icon48_blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingmeng1/bilijump-ai/HEAD/BilibiliAiSkip/icons/icon48_blue.png -------------------------------------------------------------------------------- /BilibiliAiSkip/icons/icon48_red_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingmeng1/bilijump-ai/HEAD/BilibiliAiSkip/icons/icon48_red_0.png -------------------------------------------------------------------------------- /BilibiliAiSkip/icons/icon48_red_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingmeng1/bilijump-ai/HEAD/BilibiliAiSkip/icons/icon48_red_1.png -------------------------------------------------------------------------------- /BilibiliAiSkip/icons/icon48_red_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingmeng1/bilijump-ai/HEAD/BilibiliAiSkip/icons/icon48_red_2.png -------------------------------------------------------------------------------- /BilibiliAiSkip/icons/icon48_red_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingmeng1/bilijump-ai/HEAD/BilibiliAiSkip/icons/icon48_red_3.png -------------------------------------------------------------------------------- /BilibiliAiSkip_Firefox/icons/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingmeng1/bilijump-ai/HEAD/BilibiliAiSkip_Firefox/icons/icon128.png -------------------------------------------------------------------------------- /BilibiliAiSkip_Firefox/icons/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingmeng1/bilijump-ai/HEAD/BilibiliAiSkip_Firefox/icons/icon48.png -------------------------------------------------------------------------------- /BilibiliAiSkip_Firefox/icons/icon48_blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingmeng1/bilijump-ai/HEAD/BilibiliAiSkip_Firefox/icons/icon48_blue.png -------------------------------------------------------------------------------- /BilibiliAiSkip_Firefox/icons/icon48_red_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingmeng1/bilijump-ai/HEAD/BilibiliAiSkip_Firefox/icons/icon48_red_0.png -------------------------------------------------------------------------------- /BilibiliAiSkip_Firefox/icons/icon48_red_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingmeng1/bilijump-ai/HEAD/BilibiliAiSkip_Firefox/icons/icon48_red_1.png -------------------------------------------------------------------------------- /BilibiliAiSkip_Firefox/icons/icon48_red_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingmeng1/bilijump-ai/HEAD/BilibiliAiSkip_Firefox/icons/icon48_red_2.png -------------------------------------------------------------------------------- /BilibiliAiSkip_Firefox/icons/icon48_red_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingmeng1/bilijump-ai/HEAD/BilibiliAiSkip_Firefox/icons/icon48_red_3.png -------------------------------------------------------------------------------- /script/bilijump.sgmodule: -------------------------------------------------------------------------------- 1 | #!name=Bilibili AI Skip 2 | #!desc=使用 AI 跳过 Bilibili 视频植入广告。 3 | #!category=Enhancement 4 | 5 | [Script] 6 | bilijump-surge = type=http-request,pattern=^https:\/\/(?:grpc\.biliapi\.net|app\.bilibili\.com)\/bilibili\.app\.playerunite\.v1\.Player\/PlayViewUnite$,requires-body=1,binary-body-mode=1,max-size=-1,engine=webview,script-path=https://raw.githubusercontent.com/qingmeng1/bilijump-ai/refs/heads/main/script/bilijump-surge.bundle.js 7 | bilijump-surge-dm = type=http-request,pattern=^https:\/\/(?:grpc\.biliapi\.net|app\.bilibili\.com)\/bilibili\.community\.service\.dm\.v1\.DM\/DmSegMobile$,requires-body=1,binary-body-mode=1,max-size=-1,engine=webview,script-path=https://raw.githubusercontent.com/qingmeng1/bilijump-ai/refs/heads/main/script/bilijump-surge.bundle.js 8 | 9 | [MITM] 10 | hostname = %APPEND% grpc.biliapi.net, app.bilibili.com, api.bilibili.com -------------------------------------------------------------------------------- /BilibiliAiSkip/prompt.txt: -------------------------------------------------------------------------------- 1 | 请按照以下步骤分析提供的字幕内容(包含标题、时间轴和文本),识别可能存在的广告段落: 2 | 3 | 1. **基础特征扫描** 4 | 5 | * 关键词检测:查找以下类型词汇 6 | √ 直接推广类(赞助、促销、限时优惠) 7 | √ 联系信息类(官网、二维码、400电话) 8 | √ 品牌标识类("点击下方链接"、"关注公众号") 9 | * 结构特征分析: 10 | √ 固定开头/结尾模板(如"本节目由...赞助") 11 | √ 重复出现的品牌名称(≥3次非常规提及) 12 | √ 区分测评类视频,测评不识别为广告(≤1一个品牌或产品) 13 | 14 | 2. **上下文关联分析** 15 | 16 | * 判断内容与前后文的相关性 17 | * 检测突兀的产品功能介绍(如突然插入设备参数说明) 18 | * 注意软性植入(主持人非自然口播提及产品) 19 | 20 | 3. **时空特征识别** 21 | 22 | * 高频广告位置标记(片头15秒/片中转场处) 23 | * 异常时长段落(超过常规字幕时长的独立内容块) 24 | 25 | 26 | **附加要求:** 27 | 28 | * 区分赞助声明与实质广告内容 29 | * 非中文内容需要先翻译为中文再识别 30 | * 返回的产品名称和广告内容不能太长 31 | * 正确识别时间轴与字幕内容的顺序,时间轴在字幕内容上方,示例:397.17 --> 399.24\n这就要打开美团外卖app了 32 | * 如果多段广告的开始与结束时间范围重叠了就合并为一条 33 | * 如果广告时间从单段字幕中间部分开始,以单段时长/文字占比作为开始时间,例如:"447.34 --> 458.29\n只能说呢啊美国人眼里也是有活的啊,顺带一说呢,更有活的是火凤燎原第二季,每周四哔哩哔哩独家热播,感兴趣的同学们可以追番观看啊。", 字幕内容长度为 63,广告开始为第 17 个字符,公式为 (17/63)*(458.29-447.23)+447.23=450.214,开始时间为450.214 34 | * 如果广告片段少于 15 秒,忽略该段广告 35 | 36 | 请把产品名称与广告内容精简后严格以这样的json的格式返回: 37 | {"ads": [{"start_time": "335.88","end_time": "425.34","product_name": "产品名称","ad_content": "广告内容"},"msg": "识别到广告"]} -------------------------------------------------------------------------------- /BilibiliAiSkip_Firefox/prompt.txt: -------------------------------------------------------------------------------- 1 | 请按照以下步骤分析提供的字幕内容(包含标题、时间轴和文本),识别可能存在的广告段落: 2 | 3 | 1. **基础特征扫描** 4 | 5 | * 关键词检测:查找以下类型词汇 6 | √ 直接推广类(赞助、促销、限时优惠) 7 | √ 联系信息类(官网、二维码、400电话) 8 | √ 品牌标识类("点击下方链接"、"关注公众号") 9 | * 结构特征分析: 10 | √ 固定开头/结尾模板(如"本节目由...赞助") 11 | √ 重复出现的品牌名称(≥3次非常规提及) 12 | √ 区分测评类视频,测评不识别为广告(≤1一个品牌或产品) 13 | 14 | 2. **上下文关联分析** 15 | 16 | * 判断内容与前后文的相关性 17 | * 检测突兀的产品功能介绍(如突然插入设备参数说明) 18 | * 注意软性植入(主持人非自然口播提及产品) 19 | 20 | 3. **时空特征识别** 21 | 22 | * 高频广告位置标记(片头15秒/片中转场处) 23 | * 异常时长段落(超过常规字幕时长的独立内容块) 24 | 25 | 26 | **附加要求:** 27 | 28 | * 区分赞助声明与实质广告内容 29 | * 非中文内容需要先翻译为中文再识别 30 | * 返回的产品名称和广告内容不能太长 31 | * 正确识别时间轴与字幕内容的顺序,时间轴在字幕内容上方,示例:397.17 --> 399.24\n这就要打开美团外卖app了 32 | * 如果多段广告的开始与结束时间范围重叠了就合并为一条 33 | * 如果广告时间从单段字幕中间部分开始,以单段时长/文字占比作为开始时间,例如:"447.34 --> 458.29\n只能说呢啊美国人眼里也是有活的啊,顺带一说呢,更有活的是火凤燎原第二季,每周四哔哩哔哩独家热播,感兴趣的同学们可以追番观看啊。", 字幕内容长度为 63,广告开始为第 17 个字符,公式为 (17/63)*(458.29-447.23)+447.23=450.214,开始时间为450.214 34 | * 如果广告片段少于 15 秒,忽略该段广告 35 | 36 | 请把产品名称与广告内容精简后严格以这样的json的格式返回: 37 | {"ads": [{"start_time": "335.88","end_time": "425.34","product_name": "产品名称","ad_content": "广告内容"},"msg": "识别到广告"]} -------------------------------------------------------------------------------- /BilibiliAiSkip/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "Bilibili AI Skip", 4 | "version": "2.3.22", 5 | "description": "一个使用 AI 自动跳过 Bilibili 视频植入广告的 Chrome 扩展程序。", 6 | "host_permissions": [ 7 | "*://dashscope.aliyuncs.com/*", 8 | "*://api.cloudflare.com/*", 9 | "*://api.bilibili.com/*" 10 | ], 11 | "permissions": [ 12 | "storage" 13 | ], 14 | "content_scripts": [ 15 | { 16 | "matches": [ 17 | "*://*.bilibili.com/video/*", 18 | "*://*.bilibili.com/list/watchlater*" 19 | ], 20 | "js": ["content.js"], 21 | "run_at": "document_end" 22 | } 23 | ], 24 | "background": { 25 | "service_worker": "background.js" 26 | }, 27 | "action": { 28 | "default_popup": "popup.html", 29 | "default_icon": { 30 | "48": "icons/icon48_red_3.png", 31 | "128": "icons/icon128.png" 32 | } 33 | }, 34 | "icons": { 35 | "48": "icons/icon48.png", 36 | "128": "icons/icon128.png" 37 | }, 38 | "web_accessible_resources": [ 39 | { 40 | "resources": ["icons/icon48*.png"], 41 | "matches": [""] 42 | } 43 | ], 44 | "homepage_url": "https://oooo.uno" 45 | } -------------------------------------------------------------------------------- /BilibiliAiSkip_Firefox/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "Bilibili AI Skip", 4 | "version": "2.3.22", 5 | "description": "一个使用 AI 自动跳过 Bilibili 视频植入广告的 Chrome 扩展程序。", 6 | "host_permissions": [ 7 | "*://dashscope.aliyuncs.com/*", 8 | "*://api.cloudflare.com/*", 9 | "*://api.bilibili.com/*" 10 | ], 11 | "permissions": [ 12 | "storage" 13 | ], 14 | "content_scripts": [ 15 | { 16 | "matches": [ 17 | "*://*.bilibili.com/video/*", 18 | "*://*.bilibili.com/list/watchlater*" 19 | ], 20 | "js": ["content.js"], 21 | "run_at": "document_end" 22 | } 23 | ], 24 | "background": { 25 | "scripts": ["background.js"] 26 | }, 27 | "action": { 28 | "default_popup": "popup.html", 29 | "default_icon": { 30 | "48": "icons/icon48_red_3.png", 31 | "128": "icons/icon128.png" 32 | } 33 | }, 34 | "icons": { 35 | "48": "icons/icon48.png", 36 | "128": "icons/icon128.png" 37 | }, 38 | "web_accessible_resources": [ 39 | { 40 | "resources": ["icons/icon48*.png"], 41 | "matches": [""] 42 | } 43 | ], 44 | "homepage_url": "https://oooo.uno", 45 | "browser_specific_settings": { 46 | "gecko": { 47 | "id": "bilibili-ai-skip@oooo.uno" 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /BilibiliAiSkip/background.js: -------------------------------------------------------------------------------- 1 | chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { 2 | if (message.action === "fetchDashScope") { 3 | fetch(message.url, { 4 | method: message.method || "POST", 5 | headers: { 6 | "Authorization": `Bearer ${message.apiKey}`, 7 | "Content-Type": "application/json", 8 | "X-DashScope-Async": "enable" 9 | }, 10 | body: JSON.stringify(message.body) 11 | }) 12 | .then(response => response.json()) 13 | .then(data => sendResponse({ success: true, data })) 14 | .catch(error => sendResponse({ success: false, error: error.message })); 15 | return true; 16 | } 17 | if (message.action === "dbQuery") { 18 | fetch(message.url, { 19 | method: message.method || "POST", 20 | headers: { 21 | "Authorization": `Bearer ${message.cfApiKey}`, 22 | "Content-Type": "application/json" 23 | }, 24 | body: JSON.stringify(message.body) 25 | }) 26 | .then(response => response.json()) 27 | .then(data => sendResponse({ success: true, data })) 28 | .catch(error => sendResponse({ success: false, error: error.message })); 29 | return true; 30 | } 31 | if (message.action === "kvQuery") { 32 | getConfig(message.k) 33 | .then(data => sendResponse({ success: true, data: data })) 34 | .catch(error => sendResponse({ success: false, error: error })); 35 | return true; 36 | } 37 | }); 38 | 39 | async function initConfig() { 40 | const [config, banModels] = await Promise.all([ 41 | getConfig('bilijump-config'), 42 | getConfig('bilijump-ai-ban-model') 43 | ]); 44 | 45 | await chrome.storage.sync.set({ 46 | config: config ?? { 47 | "aliApiKey": "", 48 | "aliApiURL": "https://dashscope.aliyuncs.com/api/v1/services/audio/asr/transcription", 49 | "aliTaskURL": "https://dashscope.aliyuncs.com/api/v1/tasks/", 50 | "apiKey": "", 51 | "apiModel": "", 52 | "apiURL": "", 53 | "audioEnabled": true, 54 | "autoAudio": false, 55 | "autoJump": false, 56 | "cfApiKey": "Dmlpe9TkvsvBCE0N-FkqeRkN5ANCyHTnUSnAtGCH", 57 | "cfApiURL": "https://api.cloudflare.com/client/v4/accounts/34c49ed8e1d2bd41c330fb65de4c5890/d1/database/c1ad567a-2375-49b4-83e2-d1de52a0902f/query", 58 | "enabled": true 59 | }, 60 | banModels: banModels ?? ["glm-4-flash"] 61 | }); 62 | } 63 | 64 | async function getConfig(k) { 65 | let url = `https://api.cloudflare.com/client/v4/accounts/34c49ed8e1d2bd41c330fb65de4c5890/storage/kv/namespaces/1c7e51cc9ae546748c5afc55df196f85/values/${encodeURIComponent(k)}`; 66 | for (let i = 0; i < 3; i++) { 67 | try { 68 | const response = await fetch(url, { headers: { "Authorization": `Bearer Dmlpe9TkvsvBCE0N-FkqeRkN5ANCyHTnUSnAtGCH`, "Content-Type": "application/json"} }); 69 | if (response.ok) { 70 | return await response.json(); 71 | } 72 | } catch { 73 | // Ignore errors to allow for retry 74 | } 75 | } 76 | return null; 77 | } 78 | 79 | async function loadPrompt() { 80 | const promptURL = chrome.runtime.getURL('prompt.txt'); 81 | const response = await fetch(promptURL); 82 | while(!response.ok) { 83 | response = await fetch(promptURL); 84 | } 85 | const promptText = await response.text(); 86 | await chrome.storage.sync.set({ prompt: promptText }); 87 | } 88 | 89 | chrome.runtime.onInstalled.addListener(async () => { 90 | let uid = await chrome.storage.sync.get('uid'); 91 | if(!uid?.uid) { 92 | chrome.storage.sync.set({uid: crypto.randomUUID()}); 93 | } 94 | await initConfig(); 95 | await loadPrompt(); 96 | }); 97 | 98 | chrome.runtime.onStartup.addListener(async () => { 99 | await initConfig(); 100 | }); 101 | 102 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | > [!TIP] 4 | > 5 | > 1.如果本项目对您有帮助,请给项目 Star,谢谢 6 | > 2.推荐使用手动跳过,AI 可能识别错误 7 | > 3.语音解析需注册阿里云,每月免费:10小时/月,超出:0.288元/小时 8 | > 4.推荐使用手动控制语音解析,防止浪费免费额度 9 | > 5.推荐使用 **Gemini / grok / gpt-4.1** 质量相对其他会好很多(**gemini-2.5-flash** 便宜又好用) 10 | 11 |
12 | logo 13 |
14 | 15 | # Bilibili AI Skip 16 | 17 | 一个使用 AI 自动跳过 Bilibili 视频植入广告的 Chrome 扩展程序。 18 | 19 | ## 项目简介 20 | 21 | **Bilibili AI Skip** 是一个 Chrome 扩展程序,旨在通过 AI 识别并自动跳过 Bilibili 视频中植入的广告。它支持字幕和音频分析,能够精准定位广告的开始和结束时间,并提供手动或自动跳过广告的功能。用户可以通过设置 API 密钥和模型来自定义 AI 分析行为。 22 | 23 | [![Visit Official Website](https://img.shields.io/badge/Official%20Website-Visit%20Now-8E44AD?style=plastic&logo=globe&logoColor=white&labelColor=00CED1)](https://oooo.uno) 24 | [![Get it on Chrome Web Store](https://img.shields.io/badge/Chrome%20Web%20Store-Get%20Now-1E90FF?style=plastic&logo=google-chrome&logoColor=white&labelColor=FF69B4)](https://chromewebstore.google.com/detail/lkhedimikicklpjmldabifgkhchnjjan) 25 | [![Get it on Firefox Web Store](https://img.shields.io/badge/Firefox%20Web%20Store-Get%20Now-1E90FF?style=plastic&logo=firefox&logoColor=white&labelColor=FF9500)](https://addons.mozilla.org/zh-CN/firefox/addon/bilibili-ai-skip) 26 | 27 | 28 | ## 新增脚本 [script](script/) 29 | * **增加 [surge 模块](script/bilijump.sgmodule)**: 增加 surge 模块,ios 端使用番剧的跳过 OPED 功能,ipad 端使用弹幕功能,点击弹幕跳过;在 surge 中导入模块,开启 MitM 功能即可启用,目前仅支持云端数据;模块修改自 [Sparkle](https://github.com/kokoryh/Sparkle) 30 | 31 | ## v2.3.20 版本更新 32 | * **修复 bug**: 修复 bug,上架 Firefox 商店。 33 | 34 | ## v2.3.10 版本更新 35 | * **增加 UP主/标签 过滤**: 设置页面中设置需要过滤的UP主或标签,匹配规则的视频则不进行广告识别。 36 | 37 | ### 过往更新回顾 38 | 39 | * **v2.3.9**:增加AI重新识别;修改icon。 40 | * **v2.3.8**:增加人工纠错功能;键盘快捷键。 41 | * **v2.3.6**:修复了一些已知的 bug,并优化了 AI 的提示(prompt),以提升其性能和理解能力。 42 | * **v2.3.5**:视频进度条中的广告部分现已高亮为橙色,使广告区间更加清晰可见。 43 | * **v2.3.4**:增加了一个内置的免费 API 选项(由作者付费维护,不定期更新),并添加了多个官方及优质第三方 AI API 的选项。 44 | * **v2.3.3**:上架 Chrome 应用商店。 45 | 46 | --- 47 | ## 功能特性 48 | 49 | * ​**自动跳过广告**​:通过 AI 分析视频字幕或音频,识别广告并自动跳过。 50 | * ​**手动跳过选项**​:如果未启用自动跳过,扩展会在广告时段显示弹窗,允许用户手动跳过。 51 | * ​**字幕和音频分析**​: 52 | * 优先使用视频字幕进行广告识别。 53 | * 如果没有字幕,可选择使用音频分析(需用户授权)。 54 | * ​**云端数据支持**​:通过 Cloudflare API 查询已缓存的广告数据,提升效率。 55 | * **人工纠错机制**:用户可以修正 AI 识别的广告片段,共同维护一个共享的广告时间点数据库。 56 | * ​**自定义设置**​: 57 | * 支持设置 AI 的 API 密钥、URL、模型与 Aliyun API 密钥。 58 | * 可启用/禁用音频分析和自动跳过功能。 59 | * ​**用户友好界面**​:提供直观的设置页面和实时弹窗提示。 60 | 61 | ## 使用方法 62 | 63 | 1. ​**启用扩展**​: 64 | * 在设置页面中,确保“启用扩展”选项已勾选。 65 | 2. ​**设置自动跳过**​: 66 | * 勾选“自动跳过广告”以启用自动跳过功能。 (**推荐手动,AI 可能识别错误**) 67 | * 如果未勾选,广告时段会显示弹窗,包含跳过按钮和倒计时。 68 | 3. ​**音频分析**​: 69 | * 如果视频没有字幕,扩展会提示是否启用音频分析。(**推荐手动,避免浪费免费额度**) 70 | * 音频分析需要等待约 1 分钟,分析完成后会自动识别广告。 71 | 4. ​**查看状态**​: 72 | * 扩展会在视频播放器中显示弹窗,提示当前状态(如“AI 分析中...”或“广告已跳过”)。 73 | 5. **人工纠错**: 74 | * 如果广告被遗漏或识别不准确,可以点击播放器控制栏中的“纠错”按钮,手动定义或调整广告片段。 75 | 76 | ## 技术细节 77 | 78 | * ​**广告识别**​: 79 | * 使用 AI 模型分析字幕或音频内容,识别广告的开始和结束时间、产品名称及广告内容。 80 | * 20 秒以上的广告识别,并扩展到上下文相关内容。 81 | * ​**音频处理**​: 82 | * 通过阿里云 API 进行音频转录(使用 **paraformer-v2** 模型,**免费额度:10小时/月,超出:0.288元/小时**)。 83 | * 支持的语言:中文(含粤语等各种方言)、英文、日语、韩语、德语、法语、俄语。 84 | * ​**数据存储**​: 85 | * 使用 Cloudflare API 存储和查询广告数据,避免重复分析。 86 | * ​**前端界面**​: 87 | * 设置页面使用 HTML 和 CSS 构建,提供直观的开关和输入框。 88 | * 视频页面中的弹窗使用动态样式,支持鼠标悬停效果。 89 | 90 | ## 依赖 91 | 92 | * ​**Chrome 浏览器**​:需要支持 Manifest V3。 93 | * ​**API 密钥**​: 94 | * AI 的 API 密钥(用于广告识别)。 95 | * 阿里云 API 密钥(用于音频分析)。 96 | 97 | ## 常见问题 98 | 99 | * **为什么需要 API 密钥?** 100 | * API 密钥用于调用 AI 模型和阿里云的音频转录服务,以进行广告识别和音频分析。 101 | * **音频分析为什么需要等待?** 102 | * 音频分析需要将视频音频上传并处理,通常需要 1 分钟左右,具体时间取决于视频长度和服务器响应速度。 103 | * **扩展无法识别广告怎么办?** 104 | * 确保 API 密钥和 URL 配置正确。 105 | * 检查视频是否有字幕或音频可用。 106 | * 尝试重新加载页面或联系开发者反馈问题。 107 | 108 | ## 许可证 109 | 110 | 本项目采用 MIT 许可 111 | -------------------------------------------------------------------------------- /BilibiliAiSkip/popup.js: -------------------------------------------------------------------------------- 1 | document.addEventListener('DOMContentLoaded', async () => { 2 | const keys = ['autoJump', 'enabled', 'tagFilter', 'apiKey', 'apiURL', 'apiModel', 'audioEnabled', 'autoAudio', 'aliApiKey']; 3 | 4 | let aiconfig = await new Promise((resolve, reject) => { 5 | chrome.runtime.sendMessage({ 6 | action: "kvQuery", 7 | k: "bilijump-ai-api-config" 8 | }, response => { 9 | if (response.success) { 10 | resolve(response?.data); 11 | } else { 12 | styleLog("Background fetch error: " + response.error); 13 | reject(new Error(response.error)); 14 | } 15 | }); 16 | }); 17 | console.log(aiconfig); 18 | document.getElementById('free').textContent = aiconfig?.apiDesc; 19 | 20 | chrome.storage.sync.get(keys, result => { 21 | const apply = defaults => keys.forEach(k => { 22 | const el = document.getElementById(k); 23 | const val = result[k] ?? defaults[k] ?? (el.type === 'checkbox' ? false : ''); 24 | el[el.type === 'checkbox' ? 'checked' : 'value'] = val; 25 | el.addEventListener(el.type === 'checkbox' ? 'change' : 'input', save); 26 | }); 27 | chrome.storage.sync.get('config', result => { 28 | apply(result?.config || {}); 29 | }); 30 | }); 31 | 32 | const save = debounce(() => { 33 | const settings = Object.fromEntries(keys.map(k => { 34 | const el = document.getElementById(k); 35 | return [k, el.type === 'checkbox' ? el.checked : el.value.trim()]; 36 | })); 37 | 38 | chrome.storage.sync.set(settings, () => { 39 | chrome.action.setIcon({path: settings.enabled?'icons/icon48_red_3.png':'icons/icon48_blue.png'}); 40 | const s = document.getElementById('status'); 41 | s.textContent = 'Saved'; 42 | s.classList.add('show'); 43 | setTimeout(() => (s.classList.remove('show'), s.textContent = ''), 1000); 44 | }); 45 | }, 300); 46 | 47 | const apiURLInput = document.getElementById('apiURL'); 48 | const apiURLDropdown = document.getElementById('apiURLDropdown'); 49 | const dropdownOptions = apiURLDropdown.querySelectorAll('.dropdown-option'); 50 | 51 | apiURLInput.addEventListener('click', (e) => { 52 | apiURLDropdown.style.display = 'block'; 53 | e.stopPropagation(); 54 | }); 55 | 56 | dropdownOptions.forEach(option => { 57 | option.addEventListener('click', () => { 58 | apiURLInput.value = option.getAttribute('data-value'); 59 | apiURLDropdown.style.display = 'none'; 60 | save(); 61 | }); 62 | }); 63 | 64 | document.addEventListener('click', (e) => { 65 | if (!apiURLInput.contains(e.target) && !apiURLDropdown.contains(e.target)) { 66 | apiURLDropdown.style.display = 'none'; 67 | } 68 | }); 69 | 70 | apiURLInput.addEventListener('input', save); 71 | 72 | dropdownOptions.forEach(option => { 73 | option.addEventListener('click', () => { 74 | const selectedValue = option.getAttribute('data-value'); 75 | apiURLInput.value = selectedValue; 76 | apiURLDropdown.style.display = 'none'; 77 | 78 | if (option.id.trim() === 'free') { 79 | document.getElementById('apiKey').value = aiconfig?.apiKey; 80 | document.getElementById('apiURL').value = aiconfig?.apiURL; 81 | document.getElementById('apiModel').value = aiconfig?.apiModel; 82 | }else { 83 | document.getElementById('apiKey').value = ''; 84 | document.getElementById('apiModel').value = ''; 85 | } 86 | save(); 87 | }); 88 | }); 89 | }); 90 | 91 | const debounce = (fn, wait) => { 92 | let t; 93 | return (...args) => (clearTimeout(t), t = setTimeout(() => fn(...args), wait)); 94 | }; -------------------------------------------------------------------------------- /BilibiliAiSkip_Firefox/popup.js: -------------------------------------------------------------------------------- 1 | document.addEventListener('DOMContentLoaded', async () => { 2 | const keys = ['autoJump', 'enabled', 'tagFilter', 'apiKey', 'apiURL', 'apiModel', 'audioEnabled', 'autoAudio', 'aliApiKey']; 3 | 4 | let aiconfig = await new Promise((resolve, reject) => { 5 | chrome.runtime.sendMessage({ 6 | action: "kvQuery", 7 | k: "bilijump-ai-api-config" 8 | }, response => { 9 | if (response.success) { 10 | resolve(response?.data); 11 | } else { 12 | styleLog("Background fetch error: " + response.error); 13 | reject(new Error(response.error)); 14 | } 15 | }); 16 | }); 17 | console.log(aiconfig); 18 | document.getElementById('free').textContent = aiconfig?.apiDesc; 19 | 20 | chrome.storage.sync.get(keys, result => { 21 | const apply = defaults => keys.forEach(k => { 22 | const el = document.getElementById(k); 23 | const val = result[k] ?? defaults[k] ?? (el.type === 'checkbox' ? false : ''); 24 | el[el.type === 'checkbox' ? 'checked' : 'value'] = val; 25 | el.addEventListener(el.type === 'checkbox' ? 'change' : 'input', save); 26 | }); 27 | chrome.storage.sync.get('config', result => { 28 | apply(result?.config || {}); 29 | }); 30 | }); 31 | 32 | const save = debounce(() => { 33 | const settings = Object.fromEntries(keys.map(k => { 34 | const el = document.getElementById(k); 35 | return [k, el.type === 'checkbox' ? el.checked : el.value.trim()]; 36 | })); 37 | 38 | chrome.storage.sync.set(settings, () => { 39 | chrome.action.setIcon({path: settings.enabled?'icons/icon48_red_3.png':'icons/icon48_blue.png'}); 40 | const s = document.getElementById('status'); 41 | s.textContent = 'Saved'; 42 | s.classList.add('show'); 43 | setTimeout(() => (s.classList.remove('show'), s.textContent = ''), 1000); 44 | }); 45 | }, 300); 46 | 47 | const apiURLInput = document.getElementById('apiURL'); 48 | const apiURLDropdown = document.getElementById('apiURLDropdown'); 49 | const dropdownOptions = apiURLDropdown.querySelectorAll('.dropdown-option'); 50 | 51 | apiURLInput.addEventListener('click', (e) => { 52 | apiURLDropdown.style.display = 'block'; 53 | e.stopPropagation(); 54 | }); 55 | 56 | dropdownOptions.forEach(option => { 57 | option.addEventListener('click', () => { 58 | apiURLInput.value = option.getAttribute('data-value'); 59 | apiURLDropdown.style.display = 'none'; 60 | save(); 61 | }); 62 | }); 63 | 64 | document.addEventListener('click', (e) => { 65 | if (!apiURLInput.contains(e.target) && !apiURLDropdown.contains(e.target)) { 66 | apiURLDropdown.style.display = 'none'; 67 | } 68 | }); 69 | 70 | apiURLInput.addEventListener('input', save); 71 | 72 | dropdownOptions.forEach(option => { 73 | option.addEventListener('click', () => { 74 | const selectedValue = option.getAttribute('data-value'); 75 | apiURLInput.value = selectedValue; 76 | apiURLDropdown.style.display = 'none'; 77 | 78 | if (option.id.trim() === 'free') { 79 | document.getElementById('apiKey').value = defaultSettings?.apiKey; 80 | document.getElementById('apiURL').value = defaultSettings?.apiURL; 81 | document.getElementById('apiModel').value = defaultSettings?.apiModel; 82 | }else { 83 | document.getElementById('apiKey').value = ''; 84 | document.getElementById('apiModel').value = ''; 85 | } 86 | save(); 87 | }); 88 | }); 89 | }); 90 | 91 | const debounce = (fn, wait) => { 92 | let t; 93 | return (...args) => (clearTimeout(t), t = setTimeout(() => fn(...args), wait)); 94 | }; -------------------------------------------------------------------------------- /BilibiliAiSkip_Firefox/background.js: -------------------------------------------------------------------------------- 1 | chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { 2 | if (message.action === "fetchDashScope") { 3 | fetch(message.url, { 4 | method: message.method || "POST", 5 | headers: { 6 | "Authorization": `Bearer ${message.apiKey}`, 7 | "Content-Type": "application/json", 8 | "X-DashScope-Async": "enable" 9 | }, 10 | body: JSON.stringify(message.body) 11 | }) 12 | .then(response => response.json()) 13 | .then(data => sendResponse({ success: true, data })) 14 | .catch(error => sendResponse({ success: false, error: error.message })); 15 | return true; 16 | } 17 | if (message.action === "dbQuery") { 18 | fetch(message.url, { 19 | method: message.method || "POST", 20 | headers: { 21 | "Authorization": `Bearer ${message.cfApiKey}`, 22 | "Content-Type": "application/json" 23 | }, 24 | body: JSON.stringify(message.body) 25 | }) 26 | .then(response => response.json()) 27 | .then(data => sendResponse({ success: true, data })) 28 | .catch(error => sendResponse({ success: false, error: error.message })); 29 | return true; 30 | } 31 | if (message.action === "kvQuery") { 32 | getConfig(message.k) 33 | .then(data => sendResponse({ success: true, data: data })) 34 | .catch(error => sendResponse({ success: false, error: error })); 35 | return true; 36 | } 37 | if (message.action === "fetchTranscription") { 38 | fetch(message.url) 39 | .then(response => response.text()) 40 | .then(data => sendResponse({ success: true, data })) 41 | .catch(error => sendResponse({ success: false, error: error.message })); 42 | return true; 43 | } 44 | }); 45 | 46 | async function initConfig() { 47 | const [config, banModels] = await Promise.all([ 48 | getConfig('bilijump-config'), 49 | getConfig('bilijump-ai-ban-model') 50 | ]); 51 | 52 | await chrome.storage.sync.set({ 53 | config: config ?? { 54 | "aliApiKey": "", 55 | "aliApiURL": "https://dashscope.aliyuncs.com/api/v1/services/audio/asr/transcription", 56 | "aliTaskURL": "https://dashscope.aliyuncs.com/api/v1/tasks/", 57 | "apiKey": "", 58 | "apiModel": "", 59 | "apiURL": "", 60 | "audioEnabled": true, 61 | "autoAudio": false, 62 | "autoJump": false, 63 | "cfApiKey": "Dmlpe9TkvsvBCE0N-FkqeRkN5ANCyHTnUSnAtGCH", 64 | "cfApiURL": "https://api.cloudflare.com/client/v4/accounts/34c49ed8e1d2bd41c330fb65de4c5890/d1/database/c1ad567a-2375-49b4-83e2-d1de52a0902f/query", 65 | "enabled": true 66 | }, 67 | banModels: banModels ?? ["glm-4-flash"] 68 | }); 69 | } 70 | 71 | async function getConfig(k) { 72 | let url = `https://api.cloudflare.com/client/v4/accounts/34c49ed8e1d2bd41c330fb65de4c5890/storage/kv/namespaces/1c7e51cc9ae546748c5afc55df196f85/values/${encodeURIComponent(k)}`; 73 | for (let i = 0; i < 3; i++) { 74 | try { 75 | const response = await fetch(url, { headers: { "Authorization": `Bearer Dmlpe9TkvsvBCE0N-FkqeRkN5ANCyHTnUSnAtGCH`, "Content-Type": "application/json"} }); 76 | if (response.ok) { 77 | return await response.json(); 78 | } 79 | } catch { 80 | // Ignore errors to allow for retry 81 | } 82 | } 83 | return null; 84 | } 85 | 86 | 87 | async function loadPrompt() { 88 | const promptURL = chrome.runtime.getURL('prompt.txt'); 89 | const response = await fetch(promptURL); 90 | while(!response.ok) { 91 | response = await fetch(promptURL); 92 | } 93 | const promptText = await response.text(); 94 | await chrome.storage.sync.set({ prompt: promptText }); 95 | } 96 | 97 | chrome.runtime.onInstalled.addListener(async () => { 98 | let uid = await chrome.storage.sync.get('uid'); 99 | if(!uid?.uid) { 100 | chrome.storage.sync.set({uid: crypto.randomUUID()}); 101 | } 102 | await initConfig(); 103 | await loadPrompt(); 104 | }); 105 | 106 | chrome.runtime.onStartup.addListener(async () => { 107 | await initConfig(); 108 | }); 109 | 110 | -------------------------------------------------------------------------------- /BilibiliAiSkip/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Bilibili AI Skip 5 | 6 | 256 | 296 | 297 | 298 |

Bilibili AI Skip

299 |
300 | 307 |
308 |
309 | 316 |
317 |
318 | 319 |
320 | 321 |
322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 |
331 |
332 | (e.g., https://www.openai.com/v1/chat/completions) 333 |
334 |
335 | 336 | 337 | (e.g., sk-xxxxxx...) 338 |
339 |
340 | 341 | 342 | (e.g., gpt-4o-mini, deepseek-v3...) 343 |
344 |
345 | 346 | 347 | (匹配则不进行识别,逗号(英)分割) 348 |
349 |
350 | 357 |
358 |
359 | 366 | (启用后,若无字幕将在播放 45 秒后自动分析) 367 |
368 |
369 | 370 | 371 | (用于 Paraformer 语音识别,例如: sk-xxxxxx...) 372 |
373 |
374 | 375 | 376 | -------------------------------------------------------------------------------- /BilibiliAiSkip_Firefox/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Bilibili AI Skip 5 | 6 | 256 | 296 | 297 | 298 |

Bilibili AI Skip

299 |
300 | 307 |
308 |
309 | 316 |
317 |
318 | 319 |
320 | 321 |
322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 |
331 |
332 | (e.g., https://www.openai.com/v1/chat/completions) 333 |
334 |
335 | 336 | 337 | (e.g., sk-xxxxxx...) 338 |
339 |
340 | 341 | 342 | (e.g., gpt-4o-mini, deepseek-v3...) 343 |
344 |
345 | 346 | 347 | (匹配则不进行识别,逗号(英)分割) 348 |
349 |
350 | 357 |
358 |
359 | 366 | (启用后,若无字幕将在播放 45 秒后自动分析) 367 |
368 |
369 | 370 | 371 | (用于 Paraformer 语音识别,例如: sk-xxxxxx...) 372 |
373 |
374 | 375 | 376 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Bilibili AI Skip 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 42 | 374 | 375 | 376 |
377 |

Bilibili AI Skip

378 |

使用 AI 技术智能跳过 Bilibili 视频广告,提升你的观看体验

379 | 前往 Chrome 应用商店 380 | 前往 Firefox 应用商店 381 |
382 | 383 |
384 |
385 |

核心功能

386 |
387 |
388 |

自动跳过广告

389 |

通过 AI 分析视频字幕或音频,精准识别广告并自动跳过,节省你的时间。

390 |
391 |
392 |

字幕与音频分析

393 |

支持字幕优先分析,无字幕时可启用音频分析,全面覆盖各种视频场景。

394 |
395 |
396 |

云端数据共享

397 |

通过 Cloudflare API 查询已缓存的广告数据,提升效率,减少资源浪费。

398 |
399 |
400 |

自定义设置

401 |

支持设置 API 密钥、模型和跳过模式,满足不同用户的需求。

402 |
403 |
404 |
405 | 406 |
407 |

常见问题

408 |
409 |

为什么需要设置 API 密钥?

410 |

API 密钥用于调用 OpenAI 和阿里云的 AI 服务,以进行广告识别和音频分析。如果未设置密钥,扩展将无法正常工作。请在设置页面输入有效的 API KeyAPI URL

411 |
412 |
413 |

音频分析为什么需要等待?

414 |

音频分析需要将视频音频上传到阿里云服务器并进行转录,通常需要 1 分钟左右。分析时间取决于视频长度和服务器响应速度,请耐心等待。

415 |
416 |
417 |

扩展无法识别广告怎么办?

418 |

请检查以下几点:
419 | - 确保 API KeyAPI URL 配置正确。
420 | - 确认视频是否有字幕或音频可用。
421 | - 尝试刷新页面或重新加载扩展。
422 | 如果问题仍未解决,请通过 GitHub 提交问题反馈。 423 |

424 |
425 |
426 |

如何启用自动跳过功能?

427 |

在扩展的设置页面中,勾选“自动跳过广告”选项即可启用。启用后,扩展会在识别到广告时自动跳到广告结束时间点,无需手动操作。

428 |
429 |
430 | 431 |
432 |

联系我们

433 |

434 | 有任何问题或建议?欢迎通过以下方式联系我们:
435 | GitHub | 436 | 发送邮件 437 |

438 |
439 |
440 | 441 |
442 |

© 2025 Bilibili AI Skip. 由 qingmeng1 开发。保留所有权利。

443 |
444 | 445 | 492 | 493 | 494 | -------------------------------------------------------------------------------- /BilibiliAiSkip/content.js: -------------------------------------------------------------------------------- 1 | let settings; 2 | let banModels; 3 | 4 | const configKeys = ['autoJump','enabled','tagFilter','apiKey','apiURL','apiModel','audioEnabled','autoAudio','aliApiKey']; 5 | let popups = { audioCheck: null, task: null, ai: null, ads: [], others: []}, now_cid; 6 | 7 | (async function() { 8 | chrome.storage.sync.get('config', result => { 9 | settings = result.config; 10 | }); 11 | chrome.storage.sync.get('banModels', result => { 12 | banModels = result.banModels; 13 | }); 14 | 15 | chrome.storage.sync.get(configKeys, res => { 16 | configKeys.forEach(k => settings[k] = res[k] ?? settings[k]); 17 | startAdSkipping(); 18 | }); 19 | 20 | chrome.storage.onChanged.addListener(changes => 21 | Object.entries(changes).forEach(([k, v]) => { 22 | if (!configKeys.includes(k)) return; 23 | settings[k] = v.newValue; 24 | k === 'enabled' && (v.newValue ? startAdSkipping() : location.reload()); 25 | }) 26 | ); 27 | 28 | activeLog(); 29 | 30 | let bid = '', pid = '', intervals = []; 31 | function startAdSkipping() { 32 | if (!settings.enabled) return; 33 | 34 | showPopup(`AI skip start.`); 35 | //showPopup(`自动跳过:${settings.autoJump}`); 36 | //showPopup(`音频分析:${settings.audioEnabled}`); 37 | setInterval(async function(){ 38 | let bvid = window.location.pathname.split('/')[2], pvid = new URLSearchParams(window.location.search).get('p'); 39 | if(bvid == 'watchlater') bvid = new URLSearchParams(window.location.search).get('bvid'); 40 | if(bid !== bvid || pid !== pvid){ 41 | bid = bvid, pid = pvid; 42 | 43 | const tagFilter = settings.tagFilter || ""; 44 | const filterTags = tagFilter.split(',').map(tag => tag.trim().toLowerCase()).filter(tag => tag !== ""); 45 | if (filterTags.length > 0) { 46 | const tagElements = document.querySelectorAll('.tag-panel .tag .ordinary-tag a'); 47 | const author = document.querySelector('meta[name="author"]')?.getAttribute('content'); 48 | const tags = Array.from(tagElements).map(element => element.innerHTML.toLowerCase()); 49 | if (author) tags.push(author); 50 | if (filterTags.some(tag => tags.some(pageTag => pageTag.includes(tag)))) { 51 | popups.others.push(showPopup(`过滤列表,跳过`)); 52 | return; 53 | } 54 | } 55 | 56 | while (intervals.length) clearInterval(intervals.shift()); 57 | while (popups.others.length) popups.others.shift()?.remove(); 58 | while (popups.ads.length) popups.ads.shift()?.remove(); 59 | document.getElementById('bilibili-ai-skip-correct')?.remove(); 60 | 61 | closePopup(popups.audioCheck); 62 | closePopup(popups.task); 63 | closePopup(popups.ai); 64 | 65 | let video = document.querySelector('video'); 66 | while(!video?.duration) { 67 | await new Promise(resolve => setTimeout(resolve, 1000)); 68 | video = document.querySelector('video'); 69 | } 70 | popups.others.push(showPopup(`视频长度:${Math.ceil(video.duration)}s`)); 71 | if((video?.duration || 0) < 120) { 72 | styleLog(video.duration); 73 | popups.others.push(showPopup('短视频,无需分析广告')); 74 | return; 75 | } 76 | try { 77 | let adsData = await adRecognition(bvid,pvid); 78 | for(let i = 1; i < 3 && adsData == null; i++) { 79 | styleLog(adsData); 80 | popups.others.push(showPopup('Re-fetch AD data.')); 81 | adsData = await adRecognition(bvid,pvid); 82 | } 83 | styleLog(`广告数据: ` + JSON.stringify(adsData)); 84 | 85 | new Promise(async resolve => { 86 | let curr_progress = document.querySelectorAll('.bpx-player-progress-schedule-current'); 87 | while(!curr_progress || curr_progress?.length == 0) { 88 | await new Promise(resolve => setTimeout(resolve, 1000)); 89 | curr_progress = document.querySelectorAll('.bpx-player-progress-schedule-current'); 90 | } 91 | for (var p = 0; p < curr_progress.length; p++) { 92 | curr_progress[p].style.backgroundColor = '#13c58ae6'; 93 | curr_progress[p].style.zIndex = '99'; 94 | } 95 | }); 96 | 97 | if(adsData && adsData.ads.length > 0) { 98 | let progress = document.getElementsByClassName('bpx-player-progress-schedule'); 99 | while(!progress || progress?.length == 0) { 100 | await new Promise(resolve => setTimeout(resolve, 1000)); 101 | progress = document.getElementsByClassName('bpx-player-progress-schedule'); 102 | } 103 | let segment_progress = document.getElementsByClassName('bpx-player-progress-schedule-segment'); 104 | let player_progress = document.getElementsByClassName('bpx-player-progress'); 105 | let shadow_progress = document.getElementsByClassName('bpx-player-shadow-progress-schedule-wrap'); 106 | 107 | for (let i = 0; i < adsData.ads.length; i++) { 108 | let TARGET_TIME = adsData.ads[i].start_time, SKIP_TO_TIME = adsData.ads[i].end_time, product_name = adsData.ads[i].product_name, ad_content = adsData.ads[i].ad_content; 109 | intervals[i] = setInterval(skipVideoAD, 1000); 110 | popups.others.push(showPopup(`广告时间:${getTime(TARGET_TIME)} --> ${getTime(SKIP_TO_TIME)}`)); 111 | popups.others.push(showPopup(`产品名称:${product_name}`)); 112 | //showPopup(`广告内容:${ad_content}`); 113 | new Promise(async resolve => { 114 | if(segment_progress.length > 0) { 115 | var ad_progress = document.createElement('div'); 116 | ad_progress.className = 'bpx-player-progress-schedule-current'; 117 | ad_progress.style.transform = `translate(${(TARGET_TIME/video.duration)*100}%, 0%) 118 | scaleX(${(SKIP_TO_TIME-TARGET_TIME)/video.duration})`; 119 | ad_progress.style.backgroundColor = '#df9938'; 120 | ad_progress.style.zIndex = 'auto'; 121 | player_progress?.[0]?.appendChild(ad_progress); 122 | shadow_progress?.[0]?.appendChild(ad_progress.cloneNode(true)); 123 | }else { 124 | for (var p = 0; p < progress.length; p++) { 125 | var ad_progress = document.createElement('div'); 126 | ad_progress.className = 'bpx-player-progress-schedule-current'; 127 | ad_progress.style.transform = `translate(${(TARGET_TIME/video.duration)*100}%, 0%) 128 | scaleX(${(SKIP_TO_TIME-TARGET_TIME)/video.duration})`; 129 | ad_progress.style.backgroundColor = '#df9938'; 130 | progress[p].appendChild(ad_progress); 131 | } 132 | } 133 | }); 134 | function skipVideoAD() { 135 | let video = document.querySelector('video'); 136 | if (!video) { 137 | showPopup('未找到视频组件.'); 138 | return; 139 | } 140 | let currentTime = video.currentTime; 141 | if (currentTime > TARGET_TIME && currentTime < SKIP_TO_TIME) { 142 | if(settings.autoJump){ 143 | video.currentTime = SKIP_TO_TIME; 144 | popups.others.push(showPopup('广告已跳过.')); 145 | updateTimes(now_cid, SKIP_TO_TIME - TARGET_TIME); 146 | clearInterval(intervals[i]); 147 | } else { 148 | if(popups.ads[i]) { 149 | document.querySelector('#skip-button').innerHTML = Math.ceil(SKIP_TO_TIME - currentTime); 150 | return; 151 | } 152 | const playerContainer = document.querySelector('.bpx-player-container'); 153 | if (!playerContainer) { 154 | styleLog('Player container not found.'); 155 | return; 156 | } 157 | 158 | playerContainer.style.position = 'relative'; 159 | 160 | var popup = document.createElement('div'); 161 | popup.innerHTML = ` 162 |
163 |
164 |
165 |
广告 · ${product_name}(按 k 跳过)
166 |
167 |
168 | 169 |
170 |
171 |
${ad_content}
172 |
173 |
174 |
175 |
176 | `; 177 | //popup.title = "按k或点击倒计时跳过"; 178 | popup.style.position = 'absolute'; 179 | popup.style.bottom = '90px'; 180 | popup.style.right = '30px'; 181 | popup.style.width = '300px'; 182 | popup.style.padding = '15px'; 183 | popup.style.background = 'linear-gradient(135deg, rgba(0, 0, 0, 0.3), rgba(50, 50, 50, 0.5))'; 184 | popup.style.color = '#fff'; 185 | popup.style.borderRadius = '8px'; 186 | popup.style.boxShadow = '0 4px 10px rgba(0, 0, 0, 0.3)'; 187 | popup.style.zIndex = '50'; 188 | popup.style.fontFamily = 'Arial, sans-serif'; 189 | popup.style.lineHeight = '1.5'; 190 | popup.style.overflow = 'hidden'; 191 | popup.style.transition = 'background 0.3s ease'; 192 | 193 | var closeButton = document.createElement('span'); 194 | closeButton.innerHTML = '×'; 195 | closeButton.style.position = 'absolute'; 196 | closeButton.style.top = '10px'; 197 | closeButton.style.right = '15px'; 198 | closeButton.style.cursor = 'pointer'; 199 | closeButton.style.fontSize = '18px'; 200 | closeButton.style.color = '#fff'; 201 | popup.appendChild(closeButton); 202 | 203 | popup.addEventListener('mouseenter', function() { 204 | popup.style.background = 'linear-gradient(135deg, rgba(0, 0, 0, 0.8), rgba(50, 50, 50, 1))'; 205 | }); 206 | 207 | popup.addEventListener('mouseleave', function() { 208 | popup.style.background = 'linear-gradient(135deg, rgba(0, 0, 0, 0.3), rgba(50, 50, 50, 0.5))'; 209 | }); 210 | 211 | playerContainer.appendChild(popup); 212 | 213 | closeButton.addEventListener('click', () => { 214 | clearInterval(intervals[i]); 215 | popups.ads[i].remove(); 216 | }); 217 | popup.querySelector('#skip-button').addEventListener('click', () => { 218 | popup.style.background = 'linear-gradient(135deg, rgba(0, 0, 0, 0.3), rgba(50, 50, 50, 0.3))'; 219 | popups.ads[i].remove(); 220 | popups.ads[i] = undefined; 221 | video.currentTime = SKIP_TO_TIME; 222 | popups.others.push(showPopup('广告已跳过.')); 223 | updateTimes(now_cid, SKIP_TO_TIME - TARGET_TIME); 224 | }); 225 | 226 | popups.ads[i] = popup; 227 | } 228 | }else if(popups.ads[i]) { 229 | popups.ads[i].remove(); 230 | popups.ads[i] = undefined; 231 | if(window.location.pathname.split('/')[2] !== bvid || new URLSearchParams(window.location.search).get('p') !== pvid) { 232 | clearInterval(intervals[i]); 233 | return; 234 | } 235 | } 236 | } 237 | } 238 | } else { 239 | popups.others.push(showPopup(adsData?.msg || "无有效数据")); 240 | } 241 | } catch (error) { 242 | console.info('Failed to fetch ad time:', error); 243 | } 244 | } 245 | }, 1000); 246 | 247 | document.addEventListener('keydown', (event) => { 248 | if (event.key.toLowerCase() === 'k') { 249 | for (let i = 0; i < popups.ads.length; i++) { 250 | const adPopup = popups.ads[i]; 251 | if (adPopup && document.body.contains(adPopup)) { 252 | const skipButton = adPopup.querySelector('#skip-button'); 253 | if (skipButton) { 254 | skipButton.click(); 255 | break; 256 | } 257 | } 258 | } 259 | } else if (popups.audioCheck && document.body.contains(popups.audioCheck)) { 260 | if (event.key.toLowerCase() === 'y') { 261 | const yesButton = popups.audioCheck.querySelector('#yes-button'); 262 | if (yesButton) { 263 | yesButton.click(); 264 | } 265 | } else if (event.key.toLowerCase() === 'n') { 266 | const noButton = popups.audioCheck.querySelector('#no-button'); 267 | if (noButton) { 268 | noButton.click(); 269 | } 270 | } 271 | } 272 | }); 273 | } 274 | 275 | chrome.storage.onChanged.addListener((changes, namespace) => { 276 | for (const [key, { newValue }] of Object.entries(changes)) { 277 | settings[key] = newValue; 278 | if (key === 'enabled') { 279 | newValue ? startAdSkipping() : location.reload(); 280 | } 281 | } 282 | }); 283 | })(); 284 | 285 | async function adRecognition(bvid,pvid) { 286 | 287 | try { 288 | let resultS = await chrome.storage.local.get('subtitle'); 289 | 290 | let response = await fetch(`https://api.bilibili.com/x/web-interface/view?bvid=${bvid}`, {credentials: "include"}); 291 | const videoData = await response.json(), aid = videoData.data.aid, cid = videoData.data.pages?.[pvid?pvid-1:pvid]?.cid || videoData.data.cid, title = videoData.data.title; 292 | now_cid = cid; 293 | 294 | //showPopup(`视频ID: ${bvid}`); 295 | //showPopup(`CID: ${cid}`); 296 | styleLog(`PID: ${pvid}`); 297 | 298 | //todo 获取云端数据 299 | let dbResults = await new Promise((resolve, reject) => { 300 | chrome.runtime.sendMessage({ 301 | action: "dbQuery", 302 | url: settings.cfApiURL, 303 | method: "POST", 304 | cfApiKey: settings.cfApiKey, 305 | body: {sql: "SELECT data,model FROM bilijump WHERE cid = ? LIMIT 1;", params: [cid]} 306 | }, response => { 307 | if (response.success) { 308 | resolve(response?.data?.result?.[0]?.results?.[0]); 309 | } else { 310 | styleLog("Background fetch error: " + response.error); 311 | reject(new Error(response.error)); 312 | } 313 | }); 314 | }); 315 | let tempAds = JSON.parse(dbResults?.data || "{}"); 316 | if(dbResults?.data && tempAds?.ads && tempAds?.msg) { 317 | popups.others.push(showPopup(`使用云端数据, 模型: ${dbResults?.model}.`)); 318 | correctButton(cid, tempAds); 319 | return tempAds; 320 | } 321 | 322 | for(const key of ["apiKey", "apiURL", "apiModel"]) { 323 | if(!settings[key]) { 324 | popups.others.push(showPopup(`Please set ${key} in extension settings`)); 325 | return {ads:[], msg:`Please set ${key}`}; 326 | } 327 | } 328 | 329 | if (banModels.includes(settings.apiModel)) { 330 | showPopup(`禁用 ${settings.apiModel} 模型,效果太差`); 331 | return {ads:[], msg: `请使用其他模型`}; 332 | } 333 | 334 | response = await fetch(`https://api.bilibili.com/x/player/wbi/v2?aid=${aid}&cid=${cid}`, { 335 | credentials: "include" 336 | }); 337 | const playerData = await response.json(), subtitleUrl = playerData.data?.subtitle?.subtitles?.[0]?.subtitle_url; 338 | 339 | 340 | let subtitle = "", type; 341 | if (subtitleUrl) { 342 | type = '字幕'; 343 | popups.others.push(showPopup("使用字幕分析.")); 344 | response = await fetch(`https:${subtitleUrl}`); 345 | const subtitleData = await response.json(); 346 | 347 | subtitleData.body.forEach(item => { 348 | subtitle += `${item.from} --> ${item.to}\n${item.content}\n`; 349 | }); 350 | }else if(settings.audioEnabled) { 351 | type = '音频'; 352 | if (resultS?.subtitle?.hasOwnProperty(cid)) { 353 | subtitle = resultS.subtitle[cid]; 354 | popups.others.push(showPopup("使用音频缓存.")); 355 | }else { 356 | if(!settings["aliApiKey"]) { 357 | showPopup(`Please set aliApiKey in extension settings`); 358 | return {ads:[], msg:"Please set aliApiKey"}; 359 | } 360 | if (settings.autoAudio) { 361 | popups.others.push(showPopup("01:00 后解锁音频分析.")); 362 | while(document.querySelector('video').currentTime < 45) { 363 | if(window.location.pathname.split('/')[2] !== bvid || new URLSearchParams(window.location.search).get('p') !== pvid) { 364 | return {ads:[], msg:"上下文已切换."}; 365 | } 366 | await new Promise(resolve => setTimeout(resolve, 1000)); 367 | } 368 | //await new Promise(resolve => setTimeout(resolve, document.querySelector('video').currentTime < 60 ? (60 - document.querySelector('video').currentTime) * 1000 : 0)); 369 | } else if(!await checkPopup()) { 370 | return {ads:[], msg:"用户拒绝音频分析, 识别结束."}; 371 | } 372 | 373 | popups.others.push(showPopup("使用音频分析.")); 374 | response = await fetch(`https://api.bilibili.com/x/player/wbi/playurl?bvid=${bvid}&cid=${cid}&qn=0&fnver=0&fnval=80&fourk=1`, { 375 | credentials: "include" 376 | }); 377 | const playerData = await response.json(), audioUrl = playerData?.data?.dash?.audio?.[0]?.base_url; 378 | 379 | if(!audioUrl) { 380 | return {ads:[], msg:"获取不到音频文件."}; 381 | } 382 | popups.others.push(showPopup("提交音频文件.")); 383 | styleLog("audioUrl: " + audioUrl); 384 | const taskId = await submitTranscriptionTask("https://bili.oooo.uno?url="+encodeURIComponent(audioUrl)); 385 | styleLog("Task submitted successfully, Task ID: " + taskId); 386 | 387 | popups.others.push(showPopup("等待音频分析结果.")); 388 | const results = await waitForTaskCompletion(taskId); 389 | 390 | for (const result of results) { 391 | if (result.subtask_status === "SUCCEEDED") { 392 | const transcription = await fetchTranscription(result.transcription_url); 393 | subtitle = generateSubtitle(transcription); 394 | 395 | resultS = await chrome.storage.local.get('subtitle'); 396 | const updatedSubtitles = { 397 | ...(resultS.subtitle || {}), 398 | [cid]: subtitle 399 | }; 400 | await chrome.storage.local.set({subtitle: updatedSubtitles}); 401 | //styleLog("Subtitle content:\n", subtitle); 402 | } else { 403 | styleLog(`Subtask failed for file ${result.file_url}, status: ${result.subtask_status}`); 404 | if(result.code === "SUCCESS_WITH_NO_VALID_FRAGMENT") { 405 | return {ads:[], msg:"音频无有效片段."}; 406 | } else { 407 | return {ads:[], msg:`音频解析失败:${result.message}`}; 408 | } 409 | } 410 | } 411 | } 412 | } 413 | 414 | if (!subtitle) { 415 | return {ads:[], msg:"无解析内容."}; 416 | } 417 | 418 | subtitle = `标题: ${title} 419 | 420 | 内容: 421 | 422 | ` + subtitle; 423 | 424 | popups.ai = showPopup("AI 分析中...",1); 425 | popups.others.push(popups.ai); 426 | 427 | let data , aiResponse , total_tokens, resultAD; 428 | for(let i = 0; i < 3 && (!resultAD?.ads || !resultAD?.msg); i++) { 429 | data = await callOpenAI(subtitle), aiResponse = data?.choices?.[0]?.message?.content, total_tokens = data?.usage?.total_tokens; 430 | 431 | if (!aiResponse) { 432 | showPopup("AI 解析失败."); 433 | showPopup('Re-fetch AI.'); 434 | continue; 435 | } 436 | 437 | const jsonMatch = aiResponse.match(/```json([\s\S]*?)```/); 438 | if (jsonMatch && jsonMatch[1]) { 439 | const jsonContent = jsonMatch[1].trim(); 440 | resultAD = JSON.parse(jsonContent); 441 | }else { 442 | try { 443 | resultAD = JSON.parse(aiResponse); 444 | } catch (error) { 445 | styleLog(`CID: ${cid}, error: ${error}, resp: ${aiResponse}`); 446 | showPopup("AI 分析结果获取失败. "); 447 | showPopup("AI 解析失败."); 448 | continue; 449 | } 450 | } 451 | } 452 | closePopup(popups.ai); 453 | 454 | if (!resultAD || !resultAD?.ads || !resultAD?.msg) { 455 | return {ads:[], msg:"对象结构解析失败."}; 456 | } 457 | 458 | styleLog(`CID: ${cid}, data: ${JSON.stringify(resultAD)}`); 459 | chrome.runtime.sendMessage({ 460 | action: "dbQuery", url: settings.cfApiURL, method: "POST", cfApiKey: settings.cfApiKey, 461 | body: { 462 | sql: `INSERT INTO bilijump (aid, bid, cid, data, subtitle, title, type, model, tokens) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(cid) DO UPDATE SET data = excluded.data;`, 463 | params: [aid, bvid, cid, JSON.stringify(resultAD), subtitle, title, type, settings.apiModel, total_tokens] 464 | } 465 | }); 466 | 467 | correctButton(cid, resultAD); 468 | resultS = await chrome.storage.local.get('subtitle'); 469 | if (resultS?.subtitle?.hasOwnProperty(cid)) { 470 | delete resultS.subtitle[cid]; 471 | await chrome.storage.local.set({ subtitle: resultS.subtitle }); 472 | } 473 | return resultAD; 474 | } catch (error) { 475 | showPopup("Error: " + JSON.stringify(error)); 476 | if(popups.audioCheck) closePopup(popups.audioCheck); 477 | if(popups.task) closePopup(popups.task); 478 | if(popups.ai) closePopup(popups.ai); 479 | return null; 480 | } 481 | } 482 | 483 | async function callOpenAI(subtitle) { 484 | const storageData = await chrome.storage.sync.get('prompt'); 485 | const requestData = { 486 | model: settings.apiModel, 487 | max_tokens: 8192, 488 | messages: [ {role: "system", content: storageData.prompt}, 489 | {role: "user", content: subtitle}]}; 490 | 491 | let response; 492 | for (var i = 0; i < 3;) { 493 | try { 494 | response = await fetch(settings.apiURL, { 495 | method: "POST", 496 | headers: {"Content-Type": "application/json", "Authorization": `Bearer ${settings.apiKey}`}, 497 | body: JSON.stringify(requestData) 498 | }); 499 | i = 3; 500 | } catch (error) { 501 | i++; 502 | } 503 | } 504 | 505 | const data = await response.json(); 506 | if (data.error?.message) { 507 | showPopup("API error: " + data.error.message); 508 | return ""; 509 | } 510 | 511 | if (!data.choices?.length) { 512 | showPopup("未收到有效响应."); 513 | return ""; 514 | } 515 | return data; 516 | } 517 | 518 | let popupCount = 0; 519 | 520 | function closePopup(popup) { 521 | if(popup) { 522 | popup.remove(); 523 | for (const key in popups) { 524 | if (key === 'ads') continue; 525 | if (popups[key] === popup) { 526 | popups[key] = null; 527 | break; 528 | } 529 | } 530 | popups.ads = popups.ads.filter(item => item !== popup); 531 | popupCount--; 532 | adjustPopupPositions(); 533 | } 534 | } 535 | 536 | async function checkPopup() { 537 | const userChoice = await new Promise((resolve) => { 538 | const popup = document.createElement('div'); 539 | popup.innerHTML = ` 540 |
541 |
没有可识别字幕
542 |
是否启用音频分析? 音频分析大约需要一分钟。
543 |
544 | 545 | 546 |
547 |
548 | `; 549 | popup.style.position = 'absolute'; 550 | popup.style.bottom = '90px'; 551 | popup.style.right = '30px'; 552 | popup.style.width = '300px'; 553 | popup.style.padding = '15px'; 554 | popup.style.background = 'linear-gradient(135deg, rgba(0, 0, 0, 0.3), rgba(50, 50, 50, 0.5))'; 555 | popup.style.color = '#fff'; 556 | popup.style.borderRadius = '8px'; 557 | popup.style.boxShadow = '0 4px 10px rgba(0, 0, 0, 0.3)'; 558 | popup.style.zIndex = '1000'; 559 | popup.style.fontFamily = 'Arial, sans-serif'; 560 | popup.style.lineHeight = '1.5'; 561 | 562 | popup.addEventListener('mouseenter', function() { 563 | popup.style.background = 'linear-gradient(135deg, rgba(0, 0, 0, 0.5), rgba(50, 50, 50, 7))'; 564 | }); 565 | popup.addEventListener('mouseleave', function() { 566 | popup.style.background = 'linear-gradient(135deg, rgba(0, 0, 0, 0.3), rgba(50, 50, 50, 0.5))'; 567 | }); 568 | 569 | const playerContainer = document.querySelector('.bpx-player-container'); 570 | if (playerContainer) { 571 | playerContainer.appendChild(popup); 572 | } else { 573 | console.info('Player container not found.'); 574 | resolve(false); 575 | return; 576 | } 577 | 578 | popup.querySelector('#yes-button').addEventListener('click', () => { 579 | popup.remove(); 580 | resolve(true); 581 | }); 582 | popup.querySelector('#no-button').addEventListener('click', () => { 583 | popup.remove(); 584 | resolve(false); 585 | }); 586 | popups.audioCheck = popup; 587 | //popups.others.push(popups.audioCheck); 588 | }) 589 | return userChoice; 590 | } 591 | 592 | function showPopup(msg,persist) { 593 | styleLog(msg); 594 | var popup = document.createElement('div'); 595 | popup.innerText = msg; 596 | popup.style.position = 'absolute'; 597 | popup.style.bottom = `${80 + popupCount * 60}px`; 598 | popup.style.right = '10px'; 599 | popup.style.padding = '10px'; 600 | popup.style.background = 'linear-gradient(135deg, rgba(0, 0, 0, 0.3), rgba(50, 50, 50, 0.5))'; 601 | popup.style.color = '#fff'; 602 | popup.style.borderRadius = '5px'; 603 | popup.style.zIndex = '1000'; 604 | popup.classList.add('popup'); 605 | 606 | var playerContainer = document.querySelector('.bpx-player-container'); 607 | if (playerContainer) { 608 | playerContainer.appendChild(popup); 609 | popupCount++; 610 | 611 | adjustPopupPositions(); 612 | if (!persist) { 613 | setTimeout(function() { 614 | closePopup(popup); 615 | }, 7000); 616 | } 617 | } else { 618 | console.info('Player container not found.'); 619 | } 620 | return popup; 621 | } 622 | 623 | function getVideoElement() { 624 | return document.querySelector('video'); 625 | } 626 | 627 | function secondsToTime(seconds, forceHours = false) { 628 | if (seconds === null || seconds === undefined || isNaN(seconds)) return '--:--'; 629 | seconds = Math.floor(seconds); 630 | const hrs = Math.floor(seconds / 3600); 631 | const mins = Math.floor((seconds % 3600) / 60); 632 | const secs = seconds % 60; 633 | if (hrs > 0 || forceHours) { 634 | return `${hrs.toString().padStart(2, '0')}:${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; 635 | } 636 | return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; 637 | } 638 | 639 | let isSettingTime = false; 640 | let targetSegmentElement = null; 641 | let targetTimeType = null; 642 | let progressClickListener = null; 643 | let activeTimeSettingButton = null; 644 | 645 | function showCorrectionPopup(cid, currentAdsData) { 646 | const existingPopup = document.getElementById('bilibili-ai-skip-correction-popup'); 647 | if (existingPopup) { 648 | closePopup(existingPopup); 649 | return; 650 | } 651 | 652 | const cancelTimeSelectionMode = () => { 653 | if (!isSettingTime) return; 654 | 655 | const progressBar = document.querySelector('.bpx-player-progress-wrap'); 656 | const timeSelectionStatus = document.getElementById('time-selection-status'); 657 | 658 | if (progressBar && progressClickListener) { 659 | progressBar.removeEventListener('click', progressClickListener); 660 | progressBar.style.cursor = ''; 661 | styleLog("Bilibili AI Skip: Removed progress bar click listener."); 662 | } 663 | 664 | if (activeTimeSettingButton) { 665 | const isStartButton = activeTimeSettingButton.classList.contains('set-start-time'); 666 | activeTimeSettingButton.textContent = isStartButton ? 'Set Start' : 'Set End'; 667 | styleLog(`Bilibili AI Skip: Reset button text for ${isStartButton ? 'start' : 'end'} time.`); 668 | } 669 | 670 | if (timeSelectionStatus) { 671 | timeSelectionStatus.textContent = ''; 672 | timeSelectionStatus.style.display = 'none'; 673 | styleLog("Bilibili AI Skip: Cleared and hid time selection status."); 674 | } 675 | 676 | isSettingTime = false; 677 | targetSegmentElement = null; 678 | targetTimeType = null; 679 | progressClickListener = null; 680 | activeTimeSettingButton = null; 681 | styleLog("Bilibili AI Skip: Time selection mode cancelled."); 682 | }; 683 | 684 | const popup = document.createElement('div'); 685 | popup.id = 'bilibili-ai-skip-correction-popup'; 686 | popup.innerHTML = ` 687 |
688 |
689 | 人工纠错 690 | 691 |
692 | 693 |
694 |
695 | 696 |
697 |
698 | 699 | 700 | 701 | 702 |
703 |
704 | `; 705 | popup.style.position = 'absolute'; 706 | popup.style.bottom = '120px'; 707 | popup.style.right = '30px'; 708 | popup.style.width = '420px'; 709 | popup.style.padding = '20px'; 710 | popup.style.background = 'linear-gradient(135deg, rgba(0, 0, 0, 0.6), rgba(50, 50, 50, 0.8))'; 711 | popup.style.color = '#fff'; 712 | popup.style.borderRadius = '12px'; 713 | popup.style.boxShadow = '0 6px 12px rgba(0, 0, 0, 0.4)'; 714 | popup.style.zIndex = '50'; 715 | popup.style.fontFamily = '"Arial", sans-serif'; 716 | popup.style.lineHeight = '1.6'; 717 | popup.style.backdropFilter = 'blur(8px)'; 718 | 719 | 720 | const playerContainer = document.querySelector('.bpx-player-container'); 721 | if (!playerContainer) { 722 | showPopup('错误:无法找到播放器容器'); 723 | return; 724 | } 725 | playerContainer.appendChild(popup); 726 | if (popups?.others) { 727 | popups.others.push(popup); 728 | } else { 729 | console.warn("Bilibili AI Skip: popups.others array not found. Popup management might be affected."); 730 | } 731 | 732 | const adSegmentsContainer = popup.querySelector('#ad-segments'); 733 | const emptyStateDiv = popup.querySelector('#empty-state'); 734 | const timeSelectionStatus = popup.querySelector('#time-selection-status'); 735 | 736 | const enterTimeSelectionMode = (segmentElement, timeType, buttonElement) => { 737 | if (isSettingTime && activeTimeSettingButton === buttonElement) { 738 | styleLog(`Bilibili AI Skip: Re-clicked the active setting button. Cancelling selection.`); 739 | cancelTimeSelectionMode(); 740 | return; 741 | } 742 | 743 | if (isSettingTime) { 744 | styleLog("Bilibili AI Skip: Switching time selection target. Cancelling previous mode."); 745 | cancelTimeSelectionMode(); 746 | } 747 | 748 | const video = getVideoElement(); 749 | const progressBar = document.querySelector('.bpx-player-progress-wrap'); 750 | 751 | if (!video || !progressBar) { 752 | showPopup('错误:无法找到视频或进度条'); 753 | styleLog("Bilibili AI Skip: Video or progress bar element not found for time setting."); 754 | return; 755 | } 756 | 757 | isSettingTime = true; 758 | targetSegmentElement = segmentElement; 759 | targetTimeType = timeType; 760 | activeTimeSettingButton = buttonElement; 761 | 762 | buttonElement.textContent = 'Setting...'; 763 | timeSelectionStatus.textContent = `请点击播放器进度条以设置${timeType === 'start' ? '开始' : '结束'}时间`; 764 | timeSelectionStatus.style.display = 'block'; 765 | progressBar.style.cursor = 'crosshair'; 766 | progressClickListener = (event) => { 767 | const rect = progressBar.getBoundingClientRect(); 768 | const offsetX = event.clientX - rect.left; 769 | const barWidth = progressBar.offsetWidth; 770 | const duration = video.duration; 771 | 772 | if (!isNaN(duration) && duration > 0 && barWidth > 0) { 773 | const clickedTime = Math.max(0, Math.min(duration, (offsetX / barWidth) * duration)); 774 | const timeDisplaySpan = segmentElement.querySelector(timeType === 'start' ? '.start-time-display' : '.end-time-display'); 775 | if (timeDisplaySpan) { 776 | timeDisplaySpan.textContent = secondsToTime(clickedTime); 777 | segmentElement.setAttribute(`data-${timeType}-seconds`, clickedTime.toFixed(3)); 778 | console.log(`Bilibili AI Skip: Set ${timeType} time to ${clickedTime.toFixed(3)}s (${secondsToTime(clickedTime)})`); 779 | if(timeType === 'start') { 780 | segmentElement.dataset.sortTime = clickedTime.toFixed(3); 781 | } 782 | } else { 783 | console.info("Bilibili AI Skip: Time display span not found for", timeType); 784 | } 785 | video.currentTime = clickedTime; 786 | cancelTimeSelectionMode(); 787 | 788 | } else { 789 | console.warn("Bilibili AI Skip: Could not calculate time. Duration or bar width invalid.", {duration, barWidth}); 790 | showPopup("无法获取时间,请确保视频已加载"); 791 | } 792 | }; 793 | 794 | progressBar.addEventListener('click', progressClickListener); 795 | }; 796 | 797 | function addAdSegment(ad = { start_time: null, end_time: null, product_name: '', ad_content: '' }) { 798 | const segment = document.createElement('div'); 799 | segment.className = 'ad-segment'; 800 | segment.style.marginBottom = '15px'; 801 | segment.style.padding = '15px'; 802 | segment.style.background = 'rgba(255, 255, 255, 0.1)'; 803 | segment.style.borderRadius = '8px'; 804 | segment.style.border = '1px solid rgba(255, 255, 255, 0.2)'; 805 | segment.style.position = 'relative'; 806 | 807 | const startTimeSeconds = (ad.start_time !== null && !isNaN(parseFloat(ad.start_time))) ? parseFloat(ad.start_time) : null; 808 | const endTimeSeconds = (ad.end_time !== null && !isNaN(parseFloat(ad.end_time))) ? parseFloat(ad.end_time) : null; 809 | 810 | segment.setAttribute('data-start-seconds', startTimeSeconds !== null ? startTimeSeconds.toFixed(3) : ''); 811 | segment.setAttribute('data-end-seconds', endTimeSeconds !== null ? endTimeSeconds.toFixed(3) : ''); 812 | segment.dataset.sortTime = startTimeSeconds !== null ? startTimeSeconds.toFixed(3) : Infinity; 813 | 814 | segment.innerHTML = ` 815 | 816 |
817 |
818 | 819 | ${secondsToTime(startTimeSeconds)} 820 | 821 |
822 |
823 |
824 | 825 | ${secondsToTime(endTimeSeconds)} 826 | 827 |
828 |
829 |
830 |
831 |
832 | 833 | 834 | 835 | 836 | `; 837 | 838 | const updateMiniProgress = () => { 839 | const video = getVideoElement(); 840 | if (!video || !video.duration || isNaN(video.duration) || video.duration <= 0) return; 841 | const duration = video.duration; 842 | const startAttr = segment.getAttribute('data-start-seconds'); 843 | const endAttr = segment.getAttribute('data-end-seconds'); 844 | const indicator = segment.querySelector('.mini-progress-indicator'); 845 | 846 | if (!indicator) return; 847 | 848 | const start = (startAttr && !isNaN(parseFloat(startAttr))) ? parseFloat(startAttr) : null; 849 | const end = (endAttr && !isNaN(parseFloat(endAttr))) ? parseFloat(endAttr) : null; 850 | 851 | if (start !== null && end !== null && end > start) { 852 | const leftPercent = (start / duration) * 100; 853 | const widthPercent = ((end - start) / duration) * 100; 854 | indicator.style.left = `${Math.max(0, leftPercent)}%`; 855 | indicator.style.width = `${Math.min(100 - leftPercent, widthPercent)}%`; 856 | } else { 857 | indicator.style.left = '0%'; 858 | indicator.style.width = '0%'; 859 | } 860 | }; 861 | 862 | updateMiniProgress(); 863 | const observer = new MutationObserver(mutations => { 864 | if (mutations.some(m => m.attributeName === 'data-start-seconds' || m.attributeName === 'data-end-seconds')) { 865 | updateMiniProgress(); 866 | } 867 | }); 868 | observer.observe(segment, { 869 | attributes: true, attributeFilter: ['data-start-seconds', 'data-end-seconds'] 870 | }); 871 | const videoElem = getVideoElement(); 872 | if (videoElem) { 873 | if (videoElem.readyState >= 1) { 874 | updateMiniProgress(); 875 | } else { 876 | videoElem.addEventListener('loadedmetadata', updateMiniProgress, {once: true}); 877 | } 878 | } 879 | 880 | const sortSegmentsVisually = () => { 881 | const segments = Array.from(adSegmentsContainer.children).filter(el => el.classList.contains('ad-segment')); 882 | segments.sort((a, b) => { 883 | const timeA = parseFloat(a.dataset.sortTime ?? Infinity); 884 | const timeB = parseFloat(b.dataset.sortTime ?? Infinity); 885 | return timeA - timeB; 886 | }); 887 | segments.forEach(seg => adSegmentsContainer.appendChild(seg)); 888 | } 889 | 890 | adSegmentsContainer.appendChild(segment); 891 | sortSegmentsVisually(); 892 | 893 | segment.querySelector('.remove-segment').addEventListener('click', () => { 894 | if(targetSegmentElement === segment) { 895 | cancelTimeSelectionMode(); 896 | } 897 | segment.remove(); 898 | observer.disconnect(); 899 | const videoElem = getVideoElement(); 900 | if (videoElem) videoElem.removeEventListener('loadedmetadata', updateMiniProgress); 901 | checkEmptyState(); 902 | }); 903 | 904 | const setStartButton = segment.querySelector('.set-start-time'); 905 | setStartButton.addEventListener('click', () => { 906 | enterTimeSelectionMode(segment, 'start', setStartButton); 907 | }); 908 | 909 | const setEndButton = segment.querySelector('.set-end-time'); 910 | setEndButton.addEventListener('click', () => { 911 | enterTimeSelectionMode(segment, 'end', setEndButton); 912 | }); 913 | 914 | checkEmptyState(); 915 | } 916 | 917 | function checkEmptyState() { 918 | const segmentCount = adSegmentsContainer.children.length; 919 | emptyStateDiv.style.display = segmentCount === 0 ? 'block' : 'none'; 920 | } 921 | 922 | const initialAds = (currentAdsData && currentAdsData.ads ? currentAdsData.ads : []) 923 | .map(ad => ({ 924 | start_time: !isNaN(parseFloat(ad.start_time)) ? parseFloat(ad.start_time) : null, 925 | end_time: !isNaN(parseFloat(ad.end_time)) ? parseFloat(ad.end_time) : null, 926 | product_name: ad.product_name, 927 | ad_content: ad.ad_content 928 | })); 929 | initialAds.forEach(ad => addAdSegment(ad)); 930 | checkEmptyState(); 931 | 932 | popup.querySelector('#add-segment').addEventListener('click', () => { 933 | addAdSegment(); 934 | }); 935 | 936 | popup.querySelector('#close-correction-popup').addEventListener('click', () => { 937 | cancelTimeSelectionMode(); 938 | closePopup(popup); 939 | }); 940 | 941 | async function submitCorrection(adsData, message, model) { 942 | cancelTimeSelectionMode(); 943 | if (typeof settings === 'undefined' || !settings.cfApiURL || !settings.cfApiKey) { 944 | showPopup("提交失败:无法访问配置"); 945 | return; 946 | } 947 | 948 | if (JSON.stringify(adsData) == JSON.stringify(currentAdsData)) { 949 | const submitButton = popup.querySelector('#submit-button'); 950 | if(submitButton) submitButton.disabled = false; submitButton.textContent = '提交'; 951 | showPopup("数据无变化"); 952 | return; 953 | } 954 | 955 | try { 956 | await new Promise((resolve, reject) => { 957 | chrome.runtime.sendMessage({ 958 | action: "dbQuery", 959 | url: settings.cfApiURL, 960 | method: "POST", 961 | cfApiKey: settings.cfApiKey, 962 | body: { 963 | sql: `UPDATE bilijump SET data = ?, model = ? WHERE cid = ?;`, 964 | params: [JSON.stringify(adsData), model, cid] 965 | } 966 | }, response => { 967 | if (chrome.runtime.lastError) { 968 | console.info("Bilibili AI Skip: chrome.runtime.lastError:", chrome.runtime.lastError.message); 969 | reject(new Error(chrome.runtime.lastError.message || 'Unknown background script error')); 970 | } else if (response && response.success) { 971 | styleLog('Bilibili AI Skip: Database update successful for correction.'); 972 | resolve(); 973 | } else { 974 | const errorMsg = (response && response.error) ? response.error : 'Unknown background error'; 975 | console.info("Bilibili AI Skip: Correction submission error -", errorMsg); 976 | reject(new Error(errorMsg)); 977 | } 978 | }); 979 | }); 980 | 981 | showPopup(message); 982 | styleLog('Bilibili AI Skip: Correction submitted successfully.'); 983 | closePopup(popup); 984 | showPopup("页面将在3秒后刷新..."); 985 | setTimeout(() => { location.reload(); }, 3000); 986 | } catch (error) { 987 | showPopup('提交失败:' + error.message); 988 | console.info('Bilibili AI Skip: Submission failed:', error); 989 | } 990 | } 991 | 992 | popup.querySelector('#ai-re-recog').addEventListener('click', async () => { 993 | if (window.confirm("您确定使用 AI 重新识别广告吗?这将覆盖之前的记录。")) { 994 | let bvid = window.location.pathname.split('/')[2], pvid = new URLSearchParams(window.location.search).get('p'); 995 | let response = await fetch(`https://api.bilibili.com/x/web-interface/view?bvid=${bvid}`, {credentials: "include"}); 996 | const videoData = await response.json(), aid = videoData.data.aid, cid = videoData.data.pages?.[pvid?pvid-1:pvid]?.cid || videoData.data.cid; 997 | 998 | let dbResults = await new Promise((resolve, reject) => { 999 | chrome.runtime.sendMessage({ 1000 | action: "dbQuery", 1001 | url: settings.cfApiURL, 1002 | method: "POST", 1003 | cfApiKey: settings.cfApiKey, 1004 | body: {sql: "SELECT subtitle FROM bilijump WHERE cid = ? LIMIT 1;", params: [cid]} 1005 | }, response => { 1006 | if (response.success) { 1007 | resolve(response?.data?.result?.[0]?.results?.[0]); 1008 | } else { 1009 | styleLog("Background fetch error: " + response.error); 1010 | reject(new Error(response.error)); 1011 | } 1012 | }); 1013 | }); 1014 | 1015 | if(dbResults?.subtitle) { 1016 | for(const key of ["apiKey", "apiURL", "apiModel"]) { 1017 | if(!settings[key]) { 1018 | popups.others.push(showPopup(`Please set ${key} in extension settings`)); 1019 | return {ads:[], msg:`Please set ${key}`}; 1020 | } 1021 | } 1022 | 1023 | if (banModels.includes(settings.apiModel)) { 1024 | showPopup(`禁用 ${settings.apiModel} 模型,效果太差`); 1025 | return {ads:[], msg: `请使用其他模型`}; 1026 | } 1027 | 1028 | popups.ai = showPopup(`使用 ${settings.apiModel} 重新分析中...`,1); 1029 | popups.others.push(popups.ai); 1030 | 1031 | let data = await callOpenAI(dbResults.subtitle), aiResponse = data?.choices?.[0]?.message?.content, total_tokens = data?.usage?.total_tokens; 1032 | for(let i = 1; i < 3 && !aiResponse; i++) { 1033 | showPopup('Re-fetch AI.'); 1034 | aiResponse = await callOpenAI(dbResults.subtitle); 1035 | } 1036 | closePopup(popups.ai); 1037 | 1038 | 1039 | if (!aiResponse) { 1040 | showPopup("AI 解析失败."); 1041 | return 1042 | } 1043 | 1044 | const jsonMatch = aiResponse.match(/```json([\s\S]*?)```/); 1045 | let resultAD; 1046 | if (jsonMatch && jsonMatch[1]) { 1047 | const jsonContent = jsonMatch[1].trim(); 1048 | resultAD = JSON.parse(jsonContent); 1049 | }else { 1050 | try { 1051 | resultAD = JSON.parse(aiResponse); 1052 | } catch (error) { 1053 | showPopup("AAI 分析结果获取失败. " + error); 1054 | return 1055 | } 1056 | } 1057 | 1058 | submitCorrection(resultAD, '重新识别已完成.', settings.apiModel); 1059 | } 1060 | } else { 1061 | showPopup('取消提交'); 1062 | } 1063 | }); 1064 | 1065 | popup.querySelector('#confirm-no-ads').addEventListener('click', () => { 1066 | if (window.confirm("您确定这个视频没有广告内容吗?这将覆盖之前的记录。")) { 1067 | const noAdsData = { 1068 | ads: [], 1069 | msg: "未识别到广告" 1070 | }; 1071 | submitCorrection(noAdsData, '已提交', 'artificial'); 1072 | } else { 1073 | showPopup('取消提交'); 1074 | } 1075 | }); 1076 | 1077 | popup.querySelector('#submit-button').addEventListener('click', async () => { 1078 | cancelTimeSelectionMode(); 1079 | 1080 | const segments = adSegmentsContainer.querySelectorAll('.ad-segment'); 1081 | const ads = []; 1082 | let validationError = null; 1083 | let firstErrorElement = null; 1084 | 1085 | for (const segment of segments) { 1086 | const startTimeStr = segment.getAttribute('data-start-seconds'); 1087 | const endTimeStr = segment.getAttribute('data-end-seconds'); 1088 | const productNameInput = segment.querySelector('.product-name'); 1089 | const adContentTextarea = segment.querySelector('.ad-content-textarea'); 1090 | const startTimeDisplay = segment.querySelector('.start-time-display'); 1091 | const endTimeDisplay = segment.querySelector('.end-time-display'); 1092 | 1093 | const productName = productNameInput.value.trim(); 1094 | const adContent = adContentTextarea.value.trim(); 1095 | 1096 | startTimeDisplay.style.borderColor = 'transparent'; 1097 | endTimeDisplay.style.borderColor = 'transparent'; 1098 | productNameInput.style.borderColor = ''; 1099 | adContentTextarea.style.borderColor = ''; 1100 | 1101 | const startTime = (startTimeStr && !isNaN(parseFloat(startTimeStr))) ? parseFloat(startTimeStr) : null; 1102 | const endTime = (endTimeStr && !isNaN(parseFloat(endTimeStr))) ? parseFloat(endTimeStr) : null; 1103 | 1104 | if (startTime === null) { 1105 | validationError = '存在未设置的开始时间'; 1106 | startTimeDisplay.style.borderColor = 'red'; 1107 | if (!firstErrorElement) firstErrorElement = segment.querySelector('.set-start-time'); 1108 | break; 1109 | } 1110 | if (endTime === null) { 1111 | validationError = '存在未设置的结束时间'; 1112 | endTimeDisplay.style.borderColor = 'red'; 1113 | if (!firstErrorElement) firstErrorElement = segment.querySelector('.set-end-time'); 1114 | break; 1115 | } 1116 | if (startTime >= endTime) { 1117 | validationError = `开始时间 (${secondsToTime(startTime)}) 必须早于结束时间 (${secondsToTime(endTime)})`; 1118 | startTimeDisplay.style.borderColor = 'red'; 1119 | endTimeDisplay.style.borderColor = 'red'; 1120 | if (!firstErrorElement) firstErrorElement = startTimeDisplay; 1121 | break; 1122 | } 1123 | 1124 | if (ads.length > 0) { 1125 | const prevAd = ads[ads.length - 1]; 1126 | const prevEndTime = parseFloat(prevAd.end_time); 1127 | if (startTime < prevEndTime) { 1128 | validationError = `片段重叠:开始时间 (${secondsToTime(startTime)}) 早于上一个片段的结束时间 (${secondsToTime(prevEndTime)})`; 1129 | startTimeDisplay.style.borderColor = 'orange'; 1130 | const prevSegment = segments[ads.length -1]; 1131 | if(prevSegment) prevSegment.querySelector('.end-time-display').style.borderColor = 'orange'; 1132 | if (!firstErrorElement) firstErrorElement = startTimeDisplay; 1133 | break; 1134 | } 1135 | } 1136 | 1137 | if (!productName) { 1138 | validationError = '产品名称不能为空'; 1139 | productNameInput.style.borderColor = 'red'; 1140 | if (!firstErrorElement) firstErrorElement = productNameInput; 1141 | break; 1142 | } 1143 | if (!adContent) { 1144 | validationError = '广告内容不能为空'; 1145 | adContentTextarea.style.borderColor = 'red'; 1146 | if (!firstErrorElement) firstErrorElement = adContentTextarea; 1147 | break; 1148 | } 1149 | 1150 | ads.push({ 1151 | start_time: startTime.toFixed(2), 1152 | end_time: endTime.toFixed(2), 1153 | product_name: productName, 1154 | ad_content: adContent 1155 | }); 1156 | } 1157 | 1158 | if (validationError) { 1159 | showPopup('提交失败:' + validationError); 1160 | styleLog('Bilibili AI Skip: Validation failed -' + validationError); 1161 | if (firstErrorElement && typeof firstErrorElement.focus === 'function') { 1162 | firstErrorElement.focus(); 1163 | } 1164 | if(firstErrorElement) { 1165 | const errorSegment = firstErrorElement.closest('.ad-segment'); 1166 | if(errorSegment) { 1167 | errorSegment.style.transition = 'outline 0.1s ease-in-out'; 1168 | errorSegment.style.outline = '2px solid red'; 1169 | setTimeout(() => { errorSegment.style.outline = 'none'; }, 2000); 1170 | } 1171 | } 1172 | return; 1173 | } 1174 | 1175 | ads.sort((a, b) => parseFloat(a.start_time) - parseFloat(b.start_time)); 1176 | 1177 | const mergedAds = []; 1178 | let currentAd = null; 1179 | const mergeThreshold = 1.0; 1180 | 1181 | for (const ad of ads) { 1182 | const adStart = parseFloat(ad.start_time); 1183 | const adEnd = parseFloat(ad.end_time); 1184 | if (!currentAd) { 1185 | currentAd = { ...ad, start_time: adStart, end_time: adEnd }; 1186 | } else if (adStart <= currentAd.end_time + mergeThreshold) { 1187 | currentAd.end_time = Math.max(currentAd.end_time, adEnd); 1188 | currentAd.product_name = `${currentAd.product_name} | ${ad.product_name}`; 1189 | currentAd.ad_content = `${currentAd.ad_content}\n---\n${ad.ad_content}`; 1190 | } else { 1191 | mergedAds.push({ 1192 | ...currentAd, 1193 | start_time: currentAd.start_time.toFixed(2), 1194 | end_time: currentAd.end_time.toFixed(2) 1195 | }); 1196 | currentAd = { ...ad, start_time: adStart, end_time: adEnd }; 1197 | } 1198 | } 1199 | if (currentAd) { 1200 | mergedAds.push({ 1201 | ...currentAd, 1202 | start_time: currentAd.start_time.toFixed(2), 1203 | end_time: currentAd.end_time.toFixed(2) 1204 | }); 1205 | } 1206 | 1207 | const correctedAdsData = { 1208 | ads: mergedAds, 1209 | msg: mergedAds.length > 0 ? "识别到广告" : "未识别到广告" 1210 | }; 1211 | 1212 | const submitButton = popup.querySelector('#submit-button'); 1213 | if(submitButton) submitButton.disabled = true; submitButton.textContent = '提交中...'; 1214 | 1215 | try { 1216 | if(window.confirm("确定提交?这将覆盖之前的记录。")) { 1217 | await submitCorrection(correctedAdsData, '纠错提交成功!', 'artificial'); 1218 | }else { 1219 | showPopup('取消提交'); 1220 | if(submitButton) submitButton.disabled = false; submitButton.textContent = '提交'; 1221 | } 1222 | } catch (error) { 1223 | if(submitButton) submitButton.disabled = false; submitButton.textContent = '提交'; 1224 | } 1225 | }); 1226 | } 1227 | 1228 | const adjustPopupPositions = () => { 1229 | document.querySelectorAll('.popup').forEach((el, i, arr) => { 1230 | el.style.bottom = `${100 + (arr.length - i - 1) * (el.offsetHeight + 10)}px`; 1231 | }); 1232 | }; 1233 | 1234 | const getTime = (seconds) => { 1235 | const pad = n => n.toString().padStart(2, '0'); 1236 | return [ 1237 | Math.floor(seconds / 3600), 1238 | Math.floor(seconds % 3600 / 60), 1239 | Math.floor(seconds % 60) 1240 | ].map(pad).join(':'); 1241 | }; 1242 | 1243 | async function submitTranscriptionTask(audioURL) { 1244 | const requestBody = { 1245 | model: "paraformer-v2", 1246 | input: { file_urls: [audioURL] }, 1247 | parameters: { channel_id: [0], language_hints: ["zh", "en", "ja", "yue", "ko", "de", "fr", "ru"] } 1248 | }; 1249 | return new Promise((resolve, reject) => { 1250 | chrome.runtime.sendMessage({ 1251 | action: "fetchDashScope", 1252 | url: settings.aliApiURL, 1253 | method: "POST", 1254 | apiKey: settings.aliApiKey, 1255 | body: requestBody 1256 | }, response => { 1257 | if (response.success) { 1258 | resolve(response.data.output.task_id); 1259 | } else { 1260 | styleLog("Background fetch error: " + JSON.stringify(response.error)); 1261 | reject(new Error(response.error)); 1262 | } 1263 | }); 1264 | }); 1265 | } 1266 | 1267 | async function waitForTaskCompletion(taskId) { 1268 | while (true) { 1269 | try { 1270 | const response = await new Promise((resolve, reject) => { 1271 | chrome.runtime.sendMessage({ 1272 | action: "fetchDashScope", 1273 | url: `${settings.aliTaskURL}${taskId}`, 1274 | method: "GET", 1275 | apiKey: settings.aliApiKey 1276 | }, response => { 1277 | if (response.success) { 1278 | resolve(response.data); 1279 | } else { 1280 | styleLog("Background fetch error: " + JSON.stringify(response.error)); 1281 | reject(new Error(response.error)); 1282 | } 1283 | }); 1284 | }); 1285 | 1286 | styleLog("Task status: " + response?.output?.task_status); 1287 | 1288 | switch (response.output.task_status) { 1289 | case "SUCCEEDED": 1290 | showPopup("音频解析成功."); 1291 | closePopup(popups.task); 1292 | return response.output.results; 1293 | case "FAILED": 1294 | showPopup("音频解析失败."); 1295 | closePopup(popups.task); 1296 | //throw new Error(`Task failed: ${response.error?.message || "Unknown error"}`); 1297 | return response.output.results; 1298 | case "RUNNING": 1299 | case "PENDING": 1300 | if (!popups.task) { 1301 | popups.task = showPopup("音频解析中...", 1); 1302 | popups.others.push(popups.task); 1303 | } 1304 | await new Promise(resolve => setTimeout(resolve, 5000)); 1305 | break; 1306 | default: 1307 | showPopup("音频解析遇到未知错误."); 1308 | throw new Error(`Unknown task status: ${response.output.task_status}`); 1309 | } 1310 | } catch (error) { 1311 | styleLog("Error checking task status: " + JSON.stringify(error)); 1312 | throw error; 1313 | } 1314 | } 1315 | } 1316 | 1317 | async function fetchTranscription(transcriptionURL) { 1318 | try { 1319 | const response = await fetch(transcriptionURL); 1320 | if (!response.ok) { 1321 | throw new Error(`Failed to fetch transcription: ${response.status}`); 1322 | } 1323 | return await response.text(); 1324 | } catch (error) { 1325 | styleLog("Error fetching transcription: " + JSON.stringify(error)); 1326 | throw error; 1327 | } 1328 | } 1329 | 1330 | function generateSubtitle(transcription) { 1331 | const transcripts = JSON.parse(transcription).transcripts[0].sentences; 1332 | let subtitle = ""; 1333 | 1334 | transcripts.forEach(sentence => { 1335 | const from = (sentence.begin_time / 1000).toFixed(2); 1336 | const to = (sentence.end_time / 1000).toFixed(2); 1337 | subtitle += `${from} --> ${to}\n${sentence.text}\n`; 1338 | }); 1339 | 1340 | return subtitle; 1341 | } 1342 | 1343 | function updateTimes(cid, skip_time) { 1344 | chrome.runtime.sendMessage({ 1345 | action: "dbQuery", url: settings.cfApiURL, method: "POST", cfApiKey: settings.cfApiKey, 1346 | body: { 1347 | sql: `UPDATE bilijump SET times = times + 1, skip_time = skip_time + ? WHERE cid = ?;`, 1348 | params: [Math.ceil(skip_time), cid] 1349 | } 1350 | }); 1351 | } 1352 | 1353 | function correctButton(cid, data) { 1354 | const adLength = data.ads.length; 1355 | const adTime = data.ads.reduce((sum, ad) => sum + (parseFloat(ad.end_time) - parseFloat(ad.start_time)), 0); 1356 | const iconUse = Math.max(adLength < 3 ? adLength : 3, adTime == 0 ? 0 : adTime <= 45 ? 1 : adTime <= 90 ? 2 : 3); 1357 | 1358 | let playerRight = document.querySelector('.bpx-player-control-bottom-right'); 1359 | var correct = document.createElement('div'); 1360 | correct.innerHTML = `
icon纠错
`; 1361 | correct.id = 'bilibili-ai-skip-correct'; 1362 | correct.style.width = 'auto'; 1363 | correct.style.height = '22px'; 1364 | correct.style.marginRight = '20px'; 1365 | correct.addEventListener('click', () => showCorrectionPopup(cid, data)); 1366 | playerRight.prepend(correct); 1367 | } 1368 | 1369 | async function activeLog() { 1370 | let uid = await chrome.storage.sync.get('uid'); 1371 | chrome.runtime.sendMessage({ 1372 | action: "dbQuery", url: settings.cfApiURL, method: "POST", cfApiKey: settings.cfApiKey, 1373 | body: { 1374 | sql: `INSERT INTO bilijump_active(uid) VALUES(?);`, 1375 | params: [uid.uid] 1376 | } 1377 | }); 1378 | } 1379 | 1380 | function styleLog(msg) { 1381 | console.info("%c Bilibili AI Skip %c " + msg + " ", "padding: 2px 6px; border-radius: 3px 0 0 3px; color: #fff; background: #a19cef; font-weight: bold;", "padding: 2px 6px; border-radius: 0 3px 3px 0; color: #fff; background: #FF6699; font-weight: bold;") 1382 | } -------------------------------------------------------------------------------- /BilibiliAiSkip_Firefox/content.js: -------------------------------------------------------------------------------- 1 | let settings; 2 | let banModels; 3 | 4 | const configKeys = ['autoJump','enabled','tagFilter','apiKey','apiURL','apiModel','audioEnabled','autoAudio','aliApiKey']; 5 | let popups = { audioCheck: null, task: null, ai: null, ads: [], others: []}, now_cid; 6 | 7 | (async function() { 8 | chrome.storage.sync.get('config', result => { 9 | settings = result.config; 10 | }); 11 | chrome.storage.sync.get('banModels', result => { 12 | banModels = result.banModels; 13 | }); 14 | 15 | chrome.storage.sync.get(configKeys, res => { 16 | configKeys.forEach(k => settings[k] = res[k] ?? settings[k]); 17 | startAdSkipping(); 18 | }); 19 | 20 | chrome.storage.onChanged.addListener(changes => 21 | Object.entries(changes).forEach(([k, v]) => { 22 | if (!configKeys.includes(k)) return; 23 | settings[k] = v.newValue; 24 | k === 'enabled' && (v.newValue ? startAdSkipping() : location.reload()); 25 | }) 26 | ); 27 | 28 | activeLog(); 29 | 30 | let bid = '', pid = '', intervals = []; 31 | function startAdSkipping() { 32 | if (!settings.enabled) return; 33 | 34 | showPopup(`AI skip start.`); 35 | //showPopup(`自动跳过:${settings.autoJump}`); 36 | //showPopup(`音频分析:${settings.audioEnabled}`); 37 | setInterval(async function(){ 38 | let bvid = window.location.pathname.split('/')[2], pvid = new URLSearchParams(window.location.search).get('p'); 39 | if(bvid == 'watchlater') bvid = new URLSearchParams(window.location.search).get('bvid'); 40 | if(bid !== bvid || pid !== pvid){ 41 | bid = bvid, pid = pvid; 42 | 43 | const tagFilter = settings.tagFilter || ""; 44 | const filterTags = tagFilter.split(',').map(tag => tag.trim().toLowerCase()).filter(tag => tag !== ""); 45 | if (filterTags.length > 0) { 46 | const tagElements = document.querySelectorAll('.tag-panel .tag .ordinary-tag a'); 47 | const author = document.querySelector('meta[name="author"]')?.getAttribute('content'); 48 | const tags = Array.from(tagElements).map(element => element.innerHTML.toLowerCase()); 49 | if (author) tags.push(author); 50 | if (filterTags.some(tag => tags.some(pageTag => pageTag.includes(tag)))) { 51 | popups.others.push(showPopup(`过滤列表,跳过`)); 52 | return; 53 | } 54 | } 55 | 56 | while (intervals.length) clearInterval(intervals.shift()); 57 | while (popups.others.length) popups.others.shift()?.remove(); 58 | while (popups.ads.length) popups.ads.shift()?.remove(); 59 | document.getElementById('bilibili-ai-skip-correct')?.remove(); 60 | 61 | closePopup(popups.audioCheck); 62 | closePopup(popups.task); 63 | closePopup(popups.ai); 64 | 65 | let video = document.querySelector('video'); 66 | while(!video?.duration) { 67 | await new Promise(resolve => setTimeout(resolve, 1000)); 68 | video = document.querySelector('video'); 69 | } 70 | popups.others.push(showPopup(`视频长度:${Math.ceil(video.duration)}s`)); 71 | if((video?.duration || 0) < 120) { 72 | styleLog(video.duration); 73 | popups.others.push(showPopup('短视频,无需分析广告')); 74 | return; 75 | } 76 | try { 77 | let adsData = await adRecognition(bvid,pvid); 78 | for(let i = 1; i < 3 && adsData == null; i++) { 79 | styleLog(adsData); 80 | popups.others.push(showPopup('Re-fetch AD data.')); 81 | adsData = await adRecognition(bvid,pvid); 82 | } 83 | styleLog(`广告数据: ` + JSON.stringify(adsData)); 84 | 85 | new Promise(async resolve => { 86 | let curr_progress = document.querySelectorAll('.bpx-player-progress-schedule-current'); 87 | while(!curr_progress || curr_progress?.length == 0) { 88 | await new Promise(resolve => setTimeout(resolve, 1000)); 89 | curr_progress = document.querySelectorAll('.bpx-player-progress-schedule-current'); 90 | } 91 | for (var p = 0; p < curr_progress.length; p++) { 92 | curr_progress[p].style.backgroundColor = '#13c58ae6'; 93 | curr_progress[p].style.zIndex = '99'; 94 | } 95 | }); 96 | 97 | if(adsData && adsData.ads.length > 0) { 98 | let progress = document.getElementsByClassName('bpx-player-progress-schedule'); 99 | while(!progress || progress?.length == 0) { 100 | await new Promise(resolve => setTimeout(resolve, 1000)); 101 | progress = document.getElementsByClassName('bpx-player-progress-schedule'); 102 | } 103 | let segment_progress = document.getElementsByClassName('bpx-player-progress-schedule-segment'); 104 | let player_progress = document.getElementsByClassName('bpx-player-progress'); 105 | let shadow_progress = document.getElementsByClassName('bpx-player-shadow-progress-schedule-wrap'); 106 | 107 | for (let i = 0; i < adsData.ads.length; i++) { 108 | let TARGET_TIME = adsData.ads[i].start_time, SKIP_TO_TIME = adsData.ads[i].end_time, product_name = adsData.ads[i].product_name, ad_content = adsData.ads[i].ad_content; 109 | intervals[i] = setInterval(skipVideoAD, 1000); 110 | popups.others.push(showPopup(`广告时间:${getTime(TARGET_TIME)} --> ${getTime(SKIP_TO_TIME)}`)); 111 | popups.others.push(showPopup(`产品名称:${product_name}`)); 112 | //showPopup(`广告内容:${ad_content}`); 113 | new Promise(async resolve => { 114 | if(segment_progress.length > 0) { 115 | var ad_progress = document.createElement('div'); 116 | ad_progress.className = 'bpx-player-progress-schedule-current'; 117 | ad_progress.style.transform = `translate(${(TARGET_TIME/video.duration)*100}%, 0%) 118 | scaleX(${(SKIP_TO_TIME-TARGET_TIME)/video.duration})`; 119 | ad_progress.style.backgroundColor = '#df9938'; 120 | ad_progress.style.zIndex = 'auto'; 121 | player_progress?.[0]?.appendChild(ad_progress); 122 | shadow_progress?.[0]?.appendChild(ad_progress.cloneNode(true)); 123 | }else { 124 | for (var p = 0; p < progress.length; p++) { 125 | var ad_progress = document.createElement('div'); 126 | ad_progress.className = 'bpx-player-progress-schedule-current'; 127 | ad_progress.style.transform = `translate(${(TARGET_TIME/video.duration)*100}%, 0%) 128 | scaleX(${(SKIP_TO_TIME-TARGET_TIME)/video.duration})`; 129 | ad_progress.style.backgroundColor = '#df9938'; 130 | progress[p].appendChild(ad_progress); 131 | } 132 | } 133 | }); 134 | function skipVideoAD() { 135 | let video = document.querySelector('video'); 136 | if (!video) { 137 | showPopup('未找到视频组件.'); 138 | return; 139 | } 140 | let currentTime = video.currentTime; 141 | if (currentTime > TARGET_TIME && currentTime < SKIP_TO_TIME) { 142 | if(settings.autoJump){ 143 | video.currentTime = SKIP_TO_TIME; 144 | popups.others.push(showPopup('广告已跳过.')); 145 | updateTimes(now_cid, SKIP_TO_TIME - TARGET_TIME); 146 | clearInterval(intervals[i]); 147 | } else { 148 | if(popups.ads[i]) { 149 | document.querySelector('#skip-button').innerHTML = Math.ceil(SKIP_TO_TIME - currentTime); 150 | return; 151 | } 152 | const playerContainer = document.querySelector('.bpx-player-container'); 153 | if (!playerContainer) { 154 | styleLog('Player container not found.'); 155 | return; 156 | } 157 | 158 | playerContainer.style.position = 'relative'; 159 | 160 | var popup = document.createElement('div'); 161 | popup.innerHTML = ` 162 |
163 |
164 |
165 |
广告 · ${product_name}(按 k 跳过)
166 |
167 |
168 | 169 |
170 |
171 |
${ad_content}
172 |
173 |
174 |
175 |
176 | `; 177 | //popup.title = "按k或点击倒计时跳过"; 178 | popup.style.position = 'absolute'; 179 | popup.style.bottom = '90px'; 180 | popup.style.right = '30px'; 181 | popup.style.width = '300px'; 182 | popup.style.padding = '15px'; 183 | popup.style.background = 'linear-gradient(135deg, rgba(0, 0, 0, 0.3), rgba(50, 50, 50, 0.5))'; 184 | popup.style.color = '#fff'; 185 | popup.style.borderRadius = '8px'; 186 | popup.style.boxShadow = '0 4px 10px rgba(0, 0, 0, 0.3)'; 187 | popup.style.zIndex = '50'; 188 | popup.style.fontFamily = 'Arial, sans-serif'; 189 | popup.style.lineHeight = '1.5'; 190 | popup.style.overflow = 'hidden'; 191 | popup.style.transition = 'background 0.3s ease'; 192 | 193 | var closeButton = document.createElement('span'); 194 | closeButton.innerHTML = '×'; 195 | closeButton.style.position = 'absolute'; 196 | closeButton.style.top = '10px'; 197 | closeButton.style.right = '15px'; 198 | closeButton.style.cursor = 'pointer'; 199 | closeButton.style.fontSize = '18px'; 200 | closeButton.style.color = '#fff'; 201 | popup.appendChild(closeButton); 202 | 203 | popup.addEventListener('mouseenter', function() { 204 | popup.style.background = 'linear-gradient(135deg, rgba(0, 0, 0, 0.8), rgba(50, 50, 50, 1))'; 205 | }); 206 | 207 | popup.addEventListener('mouseleave', function() { 208 | popup.style.background = 'linear-gradient(135deg, rgba(0, 0, 0, 0.3), rgba(50, 50, 50, 0.5))'; 209 | }); 210 | 211 | playerContainer.appendChild(popup); 212 | 213 | closeButton.addEventListener('click', () => { 214 | clearInterval(intervals[i]); 215 | popups.ads[i].remove(); 216 | }); 217 | popup.querySelector('#skip-button').addEventListener('click', () => { 218 | popup.style.background = 'linear-gradient(135deg, rgba(0, 0, 0, 0.3), rgba(50, 50, 50, 0.3))'; 219 | popups.ads[i].remove(); 220 | popups.ads[i] = undefined; 221 | video.currentTime = SKIP_TO_TIME; 222 | popups.others.push(showPopup('广告已跳过.')); 223 | updateTimes(now_cid, SKIP_TO_TIME - TARGET_TIME); 224 | }); 225 | 226 | popups.ads[i] = popup; 227 | } 228 | }else if(popups.ads[i]) { 229 | popups.ads[i].remove(); 230 | popups.ads[i] = undefined; 231 | if(window.location.pathname.split('/')[2] !== bvid || new URLSearchParams(window.location.search).get('p') !== pvid) { 232 | clearInterval(intervals[i]); 233 | return; 234 | } 235 | } 236 | } 237 | } 238 | } else { 239 | popups.others.push(showPopup(adsData?.msg || "无有效数据")); 240 | } 241 | } catch (error) { 242 | console.info('Failed to fetch ad time:', error); 243 | } 244 | } 245 | }, 1000); 246 | 247 | document.addEventListener('keydown', (event) => { 248 | if (event.key.toLowerCase() === 'k') { 249 | for (let i = 0; i < popups.ads.length; i++) { 250 | const adPopup = popups.ads[i]; 251 | if (adPopup && document.body.contains(adPopup)) { 252 | const skipButton = adPopup.querySelector('#skip-button'); 253 | if (skipButton) { 254 | skipButton.click(); 255 | break; 256 | } 257 | } 258 | } 259 | } else if (popups.audioCheck && document.body.contains(popups.audioCheck)) { 260 | if (event.key.toLowerCase() === 'y') { 261 | const yesButton = popups.audioCheck.querySelector('#yes-button'); 262 | if (yesButton) { 263 | yesButton.click(); 264 | } 265 | } else if (event.key.toLowerCase() === 'n') { 266 | const noButton = popups.audioCheck.querySelector('#no-button'); 267 | if (noButton) { 268 | noButton.click(); 269 | } 270 | } 271 | } 272 | }); 273 | } 274 | 275 | chrome.storage.onChanged.addListener((changes, namespace) => { 276 | for (const [key, { newValue }] of Object.entries(changes)) { 277 | settings[key] = newValue; 278 | if (key === 'enabled') { 279 | newValue ? startAdSkipping() : location.reload(); 280 | } 281 | } 282 | }); 283 | })(); 284 | 285 | async function adRecognition(bvid,pvid) { 286 | 287 | try { 288 | let resultS = await chrome.storage.local.get('subtitle'); 289 | 290 | let response = await fetch(`https://api.bilibili.com/x/web-interface/view?bvid=${bvid}`, {credentials: "include"}); 291 | const videoData = await response.json(), aid = videoData.data.aid, cid = videoData.data.pages?.[pvid?pvid-1:pvid]?.cid || videoData.data.cid, title = videoData.data.title; 292 | now_cid = cid; 293 | 294 | //showPopup(`视频ID: ${bvid}`); 295 | //showPopup(`CID: ${cid}`); 296 | styleLog(`PID: ${pvid}`); 297 | 298 | //todo 获取云端数据 299 | let dbResults = await new Promise((resolve, reject) => { 300 | chrome.runtime.sendMessage({ 301 | action: "dbQuery", 302 | url: settings.cfApiURL, 303 | method: "POST", 304 | cfApiKey: settings.cfApiKey, 305 | body: {sql: "SELECT data,model FROM bilijump WHERE cid = ? LIMIT 1;", params: [cid]} 306 | }, response => { 307 | if (response.success) { 308 | resolve(response?.data?.result?.[0]?.results?.[0]); 309 | } else { 310 | styleLog("Background fetch error: " + response.error); 311 | reject(new Error(response.error)); 312 | } 313 | }); 314 | }); 315 | 316 | let tempAds = JSON.parse(dbResults?.data || "{}"); 317 | if(dbResults?.data && tempAds?.ads && tempAds?.msg) { 318 | popups.others.push(showPopup(`使用云端数据, 模型: ${dbResults?.model}.`)); 319 | correctButton(cid, tempAds); 320 | return tempAds; 321 | } 322 | 323 | for(const key of ["apiKey", "apiURL", "apiModel"]) { 324 | if(!settings[key]) { 325 | popups.others.push(showPopup(`Please set ${key} in extension settings`)); 326 | return {ads:[], msg:`Please set ${key}`}; 327 | } 328 | } 329 | 330 | if (banModels.includes(settings.apiModel)) { 331 | showPopup(`禁用 ${settings.apiModel} 模型,效果太差`); 332 | return {ads:[], msg: `请使用其他模型`}; 333 | } 334 | 335 | response = await fetch(`https://api.bilibili.com/x/player/wbi/v2?aid=${aid}&cid=${cid}`, { 336 | credentials: "include" 337 | }); 338 | const playerData = await response.json(), subtitleUrl = playerData.data?.subtitle?.subtitles?.[0]?.subtitle_url; 339 | 340 | 341 | let subtitle = "", type; 342 | if (subtitleUrl) { 343 | type = '字幕'; 344 | popups.others.push(showPopup("使用字幕分析.")); 345 | response = await fetch(`https:${subtitleUrl}`); 346 | const subtitleData = await response.json(); 347 | 348 | subtitleData.body.forEach(item => { 349 | subtitle += `${item.from} --> ${item.to}\n${item.content}\n`; 350 | }); 351 | }else if(settings.audioEnabled) { 352 | type = '音频'; 353 | if (resultS?.subtitle?.hasOwnProperty(cid)) { 354 | subtitle = resultS.subtitle[cid]; 355 | popups.others.push(showPopup("使用音频缓存.")); 356 | }else { 357 | if(!settings["aliApiKey"]) { 358 | showPopup(`Please set aliApiKey in extension settings`); 359 | return {ads:[], msg:"Please set aliApiKey"}; 360 | } 361 | if (settings.autoAudio) { 362 | popups.others.push(showPopup("01:00 后解锁音频分析.")); 363 | while(document.querySelector('video').currentTime < 45) { 364 | if(window.location.pathname.split('/')[2] !== bvid || new URLSearchParams(window.location.search).get('p') !== pvid) { 365 | return {ads:[], msg:"上下文已切换."}; 366 | } 367 | await new Promise(resolve => setTimeout(resolve, 1000)); 368 | } 369 | //await new Promise(resolve => setTimeout(resolve, document.querySelector('video').currentTime < 60 ? (60 - document.querySelector('video').currentTime) * 1000 : 0)); 370 | } else if(!await checkPopup()) { 371 | return {ads:[], msg:"用户拒绝音频分析, 识别结束."}; 372 | } 373 | 374 | popups.others.push(showPopup("使用音频分析.")); 375 | response = await fetch(`https://api.bilibili.com/x/player/wbi/playurl?bvid=${bvid}&cid=${cid}&qn=0&fnver=0&fnval=80&fourk=1`, { 376 | credentials: "include" 377 | }); 378 | const playerData = await response.json(), audioUrl = playerData?.data?.dash?.audio?.[0]?.base_url; 379 | 380 | if(!audioUrl) { 381 | return {ads:[], msg:"获取不到音频文件."}; 382 | } 383 | popups.others.push(showPopup("提交音频文件.")); 384 | styleLog("audioUrl: " + audioUrl); 385 | const taskId = await submitTranscriptionTask("https://bili.oooo.uno?url="+encodeURIComponent(audioUrl)); 386 | styleLog("Task submitted successfully, Task ID: " + taskId); 387 | 388 | popups.others.push(showPopup("等待音频分析结果.")); 389 | const results = await waitForTaskCompletion(taskId); 390 | 391 | for (const result of results) { 392 | if (result.subtask_status === "SUCCEEDED") { 393 | const transcription = await fetchTranscription(result.transcription_url); 394 | subtitle = generateSubtitle(transcription); 395 | 396 | resultS = await chrome.storage.local.get('subtitle'); 397 | const updatedSubtitles = { 398 | ...(resultS.subtitle || {}), 399 | [cid]: subtitle 400 | }; 401 | await chrome.storage.local.set({subtitle: updatedSubtitles}); 402 | //styleLog("Subtitle content:\n", subtitle); 403 | } else { 404 | styleLog(`Subtask failed for file ${result.file_url}, status: ${result.subtask_status}`); 405 | if(result.code === "SUCCESS_WITH_NO_VALID_FRAGMENT") { 406 | return {ads:[], msg:"音频无有效片段."}; 407 | } else { 408 | return {ads:[], msg:`音频解析失败:${result.message}`}; 409 | } 410 | } 411 | } 412 | } 413 | } 414 | 415 | if (!subtitle) { 416 | return {ads:[], msg:"无解析内容."}; 417 | } 418 | 419 | subtitle = `标题: ${title} 420 | 421 | 内容: 422 | 423 | ` + subtitle; 424 | 425 | popups.ai = showPopup("AI 分析中...",1); 426 | popups.others.push(popups.ai); 427 | 428 | let data , aiResponse , total_tokens, resultAD; 429 | for(let i = 0; i < 3 && (!resultAD?.ads || !resultAD?.msg); i++) { 430 | data = await callOpenAI(subtitle), aiResponse = data?.choices?.[0]?.message?.content, total_tokens = data?.usage?.total_tokens; 431 | 432 | if (!aiResponse) { 433 | showPopup("AI 解析失败."); 434 | showPopup('Re-fetch AI.'); 435 | continue; 436 | } 437 | 438 | const jsonMatch = aiResponse.match(/```json([\s\S]*?)```/); 439 | if (jsonMatch && jsonMatch[1]) { 440 | const jsonContent = jsonMatch[1].trim(); 441 | resultAD = JSON.parse(jsonContent); 442 | }else { 443 | try { 444 | resultAD = JSON.parse(aiResponse); 445 | } catch (error) { 446 | styleLog(`CID: ${cid}, error: ${error}, resp: ${aiResponse}`); 447 | showPopup("AI 分析结果获取失败. "); 448 | showPopup('Re-fetch AI.'); 449 | continue; 450 | } 451 | } 452 | } 453 | closePopup(popups.ai); 454 | 455 | if (!resultAD || !resultAD?.ads || !resultAD?.msg) { 456 | return {ads:[], msg:"对象结构解析失败."}; 457 | } 458 | 459 | styleLog(`CID: ${cid}, data: ${JSON.stringify(resultAD)}`); 460 | chrome.runtime.sendMessage({ 461 | action: "dbQuery", url: settings.cfApiURL, method: "POST", cfApiKey: settings.cfApiKey, 462 | body: { 463 | sql: `INSERT INTO bilijump (aid, bid, cid, data, subtitle, title, type, model, tokens) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(cid) DO UPDATE SET data = excluded.data;`, 464 | params: [aid, bvid, cid, JSON.stringify(resultAD), subtitle, title, type, settings.apiModel, total_tokens] 465 | } 466 | }); 467 | 468 | correctButton(cid, resultAD); 469 | resultS = await chrome.storage.local.get('subtitle'); 470 | if (resultS?.subtitle?.hasOwnProperty(cid)) { 471 | delete resultS.subtitle[cid]; 472 | await chrome.storage.local.set({ subtitle: resultS.subtitle }); 473 | } 474 | return resultAD; 475 | } catch (error) { 476 | showPopup("Error: " + JSON.stringify(error)); 477 | if(popups.audioCheck) closePopup(popups.audioCheck); 478 | if(popups.task) closePopup(popups.task); 479 | if(popups.ai) closePopup(popups.ai); 480 | return null; 481 | } 482 | } 483 | 484 | async function callOpenAI(subtitle) { 485 | const storageData = await chrome.storage.sync.get('prompt'); 486 | const requestData = { 487 | model: settings.apiModel, 488 | max_tokens: 8192 * 4, 489 | messages: [ {role: "system", content: storageData.prompt}, 490 | {role: "user", content: subtitle}]}; 491 | 492 | let response; 493 | for (var i = 0; i < 3;) { 494 | try { 495 | response = await fetch(settings.apiURL, { 496 | method: "POST", 497 | headers: {"Content-Type": "application/json", "Authorization": `Bearer ${settings.apiKey}`}, 498 | body: JSON.stringify(requestData) 499 | }); 500 | i = 3; 501 | } catch (error) { 502 | i++; 503 | } 504 | } 505 | 506 | const data = await response.json(); 507 | if (data.error?.message) { 508 | showPopup("API error: " + data.error.message); 509 | return ""; 510 | } 511 | 512 | if (!data.choices?.length) { 513 | showPopup("未收到有效响应."); 514 | return ""; 515 | } 516 | return data; 517 | } 518 | 519 | let popupCount = 0; 520 | 521 | function closePopup(popup) { 522 | if(popup) { 523 | popup.remove(); 524 | for (const key in popups) { 525 | if (key === 'ads') continue; 526 | if (popups[key] === popup) { 527 | popups[key] = null; 528 | break; 529 | } 530 | } 531 | popups.ads = popups.ads.filter(item => item !== popup); 532 | popupCount--; 533 | adjustPopupPositions(); 534 | } 535 | } 536 | 537 | async function checkPopup() { 538 | const userChoice = await new Promise((resolve) => { 539 | const popup = document.createElement('div'); 540 | popup.innerHTML = ` 541 |
542 |
没有可识别字幕
543 |
是否启用音频分析? 音频分析大约需要一分钟。
544 |
545 | 546 | 547 |
548 |
549 | `; 550 | popup.style.position = 'absolute'; 551 | popup.style.bottom = '90px'; 552 | popup.style.right = '30px'; 553 | popup.style.width = '300px'; 554 | popup.style.padding = '15px'; 555 | popup.style.background = 'linear-gradient(135deg, rgba(0, 0, 0, 0.3), rgba(50, 50, 50, 0.5))'; 556 | popup.style.color = '#fff'; 557 | popup.style.borderRadius = '8px'; 558 | popup.style.boxShadow = '0 4px 10px rgba(0, 0, 0, 0.3)'; 559 | popup.style.zIndex = '1000'; 560 | popup.style.fontFamily = 'Arial, sans-serif'; 561 | popup.style.lineHeight = '1.5'; 562 | 563 | popup.addEventListener('mouseenter', function() { 564 | popup.style.background = 'linear-gradient(135deg, rgba(0, 0, 0, 0.5), rgba(50, 50, 50, 7))'; 565 | }); 566 | popup.addEventListener('mouseleave', function() { 567 | popup.style.background = 'linear-gradient(135deg, rgba(0, 0, 0, 0.3), rgba(50, 50, 50, 0.5))'; 568 | }); 569 | 570 | const playerContainer = document.querySelector('.bpx-player-container'); 571 | if (playerContainer) { 572 | playerContainer.appendChild(popup); 573 | } else { 574 | console.info('Player container not found.'); 575 | resolve(false); 576 | return; 577 | } 578 | 579 | popup.querySelector('#yes-button').addEventListener('click', () => { 580 | popup.remove(); 581 | resolve(true); 582 | }); 583 | popup.querySelector('#no-button').addEventListener('click', () => { 584 | popup.remove(); 585 | resolve(false); 586 | }); 587 | popups.audioCheck = popup; 588 | //popups.others.push(popups.audioCheck); 589 | }) 590 | return userChoice; 591 | } 592 | 593 | function showPopup(msg,persist) { 594 | styleLog(msg); 595 | var popup = document.createElement('div'); 596 | popup.innerText = msg; 597 | popup.style.position = 'absolute'; 598 | popup.style.bottom = `${80 + popupCount * 60}px`; 599 | popup.style.right = '10px'; 600 | popup.style.padding = '10px'; 601 | popup.style.background = 'linear-gradient(135deg, rgba(0, 0, 0, 0.3), rgba(50, 50, 50, 0.5))'; 602 | popup.style.color = '#fff'; 603 | popup.style.borderRadius = '5px'; 604 | popup.style.zIndex = '1000'; 605 | popup.classList.add('popup'); 606 | 607 | var playerContainer = document.querySelector('.bpx-player-container'); 608 | if (playerContainer) { 609 | playerContainer.appendChild(popup); 610 | popupCount++; 611 | 612 | adjustPopupPositions(); 613 | if (!persist) { 614 | setTimeout(function() { 615 | closePopup(popup); 616 | }, 7000); 617 | } 618 | } else { 619 | console.info('Player container not found.'); 620 | } 621 | return popup; 622 | } 623 | 624 | function getVideoElement() { 625 | return document.querySelector('video'); 626 | } 627 | 628 | function secondsToTime(seconds, forceHours = false) { 629 | if (seconds === null || seconds === undefined || isNaN(seconds)) return '--:--'; 630 | seconds = Math.floor(seconds); 631 | const hrs = Math.floor(seconds / 3600); 632 | const mins = Math.floor((seconds % 3600) / 60); 633 | const secs = seconds % 60; 634 | if (hrs > 0 || forceHours) { 635 | return `${hrs.toString().padStart(2, '0')}:${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; 636 | } 637 | return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; 638 | } 639 | 640 | let isSettingTime = false; 641 | let targetSegmentElement = null; 642 | let targetTimeType = null; 643 | let progressClickListener = null; 644 | let activeTimeSettingButton = null; 645 | 646 | function showCorrectionPopup(cid, currentAdsData) { 647 | const existingPopup = document.getElementById('bilibili-ai-skip-correction-popup'); 648 | if (existingPopup) { 649 | closePopup(existingPopup); 650 | return; 651 | } 652 | 653 | const cancelTimeSelectionMode = () => { 654 | if (!isSettingTime) return; 655 | 656 | const progressBar = document.querySelector('.bpx-player-progress-wrap'); 657 | const timeSelectionStatus = document.getElementById('time-selection-status'); 658 | 659 | if (progressBar && progressClickListener) { 660 | progressBar.removeEventListener('click', progressClickListener); 661 | progressBar.style.cursor = ''; 662 | styleLog("Bilibili AI Skip: Removed progress bar click listener."); 663 | } 664 | 665 | if (activeTimeSettingButton) { 666 | const isStartButton = activeTimeSettingButton.classList.contains('set-start-time'); 667 | activeTimeSettingButton.textContent = isStartButton ? 'Set Start' : 'Set End'; 668 | styleLog(`Bilibili AI Skip: Reset button text for ${isStartButton ? 'start' : 'end'} time.`); 669 | } 670 | 671 | if (timeSelectionStatus) { 672 | timeSelectionStatus.textContent = ''; 673 | timeSelectionStatus.style.display = 'none'; 674 | styleLog("Bilibili AI Skip: Cleared and hid time selection status."); 675 | } 676 | 677 | isSettingTime = false; 678 | targetSegmentElement = null; 679 | targetTimeType = null; 680 | progressClickListener = null; 681 | activeTimeSettingButton = null; 682 | styleLog("Bilibili AI Skip: Time selection mode cancelled."); 683 | }; 684 | 685 | const popup = document.createElement('div'); 686 | popup.id = 'bilibili-ai-skip-correction-popup'; 687 | popup.innerHTML = ` 688 |
689 |
690 | 人工纠错 691 | 692 |
693 | 694 |
695 |
696 | 697 |
698 |
699 | 700 | 701 | 702 | 703 |
704 |
705 | `; 706 | popup.style.position = 'absolute'; 707 | popup.style.bottom = '120px'; 708 | popup.style.right = '30px'; 709 | popup.style.width = '420px'; 710 | popup.style.padding = '20px'; 711 | popup.style.background = 'linear-gradient(135deg, rgba(0, 0, 0, 0.6), rgba(50, 50, 50, 0.8))'; 712 | popup.style.color = '#fff'; 713 | popup.style.borderRadius = '12px'; 714 | popup.style.boxShadow = '0 6px 12px rgba(0, 0, 0, 0.4)'; 715 | popup.style.zIndex = '50'; 716 | popup.style.fontFamily = '"Arial", sans-serif'; 717 | popup.style.lineHeight = '1.6'; 718 | popup.style.backdropFilter = 'blur(8px)'; 719 | 720 | 721 | const playerContainer = document.querySelector('.bpx-player-container'); 722 | if (!playerContainer) { 723 | showPopup('错误:无法找到播放器容器'); 724 | return; 725 | } 726 | playerContainer.appendChild(popup); 727 | if (popups?.others) { 728 | popups.others.push(popup); 729 | } else { 730 | console.warn("Bilibili AI Skip: popups.others array not found. Popup management might be affected."); 731 | } 732 | 733 | const adSegmentsContainer = popup.querySelector('#ad-segments'); 734 | const emptyStateDiv = popup.querySelector('#empty-state'); 735 | const timeSelectionStatus = popup.querySelector('#time-selection-status'); 736 | 737 | const enterTimeSelectionMode = (segmentElement, timeType, buttonElement) => { 738 | if (isSettingTime && activeTimeSettingButton === buttonElement) { 739 | styleLog(`Bilibili AI Skip: Re-clicked the active setting button. Cancelling selection.`); 740 | cancelTimeSelectionMode(); 741 | return; 742 | } 743 | 744 | if (isSettingTime) { 745 | styleLog("Bilibili AI Skip: Switching time selection target. Cancelling previous mode."); 746 | cancelTimeSelectionMode(); 747 | } 748 | 749 | const video = getVideoElement(); 750 | const progressBar = document.querySelector('.bpx-player-progress-wrap'); 751 | 752 | if (!video || !progressBar) { 753 | showPopup('错误:无法找到视频或进度条'); 754 | styleLog("Bilibili AI Skip: Video or progress bar element not found for time setting."); 755 | return; 756 | } 757 | 758 | isSettingTime = true; 759 | targetSegmentElement = segmentElement; 760 | targetTimeType = timeType; 761 | activeTimeSettingButton = buttonElement; 762 | 763 | buttonElement.textContent = 'Setting...'; 764 | timeSelectionStatus.textContent = `请点击播放器进度条以设置${timeType === 'start' ? '开始' : '结束'}时间`; 765 | timeSelectionStatus.style.display = 'block'; 766 | progressBar.style.cursor = 'crosshair'; 767 | progressClickListener = (event) => { 768 | const rect = progressBar.getBoundingClientRect(); 769 | const offsetX = event.clientX - rect.left; 770 | const barWidth = progressBar.offsetWidth; 771 | const duration = video.duration; 772 | 773 | if (!isNaN(duration) && duration > 0 && barWidth > 0) { 774 | const clickedTime = Math.max(0, Math.min(duration, (offsetX / barWidth) * duration)); 775 | const timeDisplaySpan = segmentElement.querySelector(timeType === 'start' ? '.start-time-display' : '.end-time-display'); 776 | if (timeDisplaySpan) { 777 | timeDisplaySpan.textContent = secondsToTime(clickedTime); 778 | segmentElement.setAttribute(`data-${timeType}-seconds`, clickedTime.toFixed(3)); 779 | console.log(`Bilibili AI Skip: Set ${timeType} time to ${clickedTime.toFixed(3)}s (${secondsToTime(clickedTime)})`); 780 | if(timeType === 'start') { 781 | segmentElement.dataset.sortTime = clickedTime.toFixed(3); 782 | } 783 | } else { 784 | console.info("Bilibili AI Skip: Time display span not found for", timeType); 785 | } 786 | video.currentTime = clickedTime; 787 | cancelTimeSelectionMode(); 788 | 789 | } else { 790 | console.warn("Bilibili AI Skip: Could not calculate time. Duration or bar width invalid.", {duration, barWidth}); 791 | showPopup("无法获取时间,请确保视频已加载"); 792 | } 793 | }; 794 | 795 | progressBar.addEventListener('click', progressClickListener); 796 | }; 797 | 798 | function addAdSegment(ad = { start_time: null, end_time: null, product_name: '', ad_content: '' }) { 799 | const segment = document.createElement('div'); 800 | segment.className = 'ad-segment'; 801 | segment.style.marginBottom = '15px'; 802 | segment.style.padding = '15px'; 803 | segment.style.background = 'rgba(255, 255, 255, 0.1)'; 804 | segment.style.borderRadius = '8px'; 805 | segment.style.border = '1px solid rgba(255, 255, 255, 0.2)'; 806 | segment.style.position = 'relative'; 807 | 808 | const startTimeSeconds = (ad.start_time !== null && !isNaN(parseFloat(ad.start_time))) ? parseFloat(ad.start_time) : null; 809 | const endTimeSeconds = (ad.end_time !== null && !isNaN(parseFloat(ad.end_time))) ? parseFloat(ad.end_time) : null; 810 | 811 | segment.setAttribute('data-start-seconds', startTimeSeconds !== null ? startTimeSeconds.toFixed(3) : ''); 812 | segment.setAttribute('data-end-seconds', endTimeSeconds !== null ? endTimeSeconds.toFixed(3) : ''); 813 | segment.dataset.sortTime = startTimeSeconds !== null ? startTimeSeconds.toFixed(3) : Infinity; 814 | 815 | segment.innerHTML = ` 816 | 817 |
818 |
819 | 820 | ${secondsToTime(startTimeSeconds)} 821 | 822 |
823 |
824 |
825 | 826 | ${secondsToTime(endTimeSeconds)} 827 | 828 |
829 |
830 |
831 |
832 |
833 | 834 | 835 | 836 | 837 | `; 838 | 839 | const updateMiniProgress = () => { 840 | const video = getVideoElement(); 841 | if (!video || !video.duration || isNaN(video.duration) || video.duration <= 0) return; 842 | const duration = video.duration; 843 | const startAttr = segment.getAttribute('data-start-seconds'); 844 | const endAttr = segment.getAttribute('data-end-seconds'); 845 | const indicator = segment.querySelector('.mini-progress-indicator'); 846 | 847 | if (!indicator) return; 848 | 849 | const start = (startAttr && !isNaN(parseFloat(startAttr))) ? parseFloat(startAttr) : null; 850 | const end = (endAttr && !isNaN(parseFloat(endAttr))) ? parseFloat(endAttr) : null; 851 | 852 | if (start !== null && end !== null && end > start) { 853 | const leftPercent = (start / duration) * 100; 854 | const widthPercent = ((end - start) / duration) * 100; 855 | indicator.style.left = `${Math.max(0, leftPercent)}%`; 856 | indicator.style.width = `${Math.min(100 - leftPercent, widthPercent)}%`; 857 | } else { 858 | indicator.style.left = '0%'; 859 | indicator.style.width = '0%'; 860 | } 861 | }; 862 | 863 | updateMiniProgress(); 864 | const observer = new MutationObserver(mutations => { 865 | if (mutations.some(m => m.attributeName === 'data-start-seconds' || m.attributeName === 'data-end-seconds')) { 866 | updateMiniProgress(); 867 | } 868 | }); 869 | observer.observe(segment, { 870 | attributes: true, attributeFilter: ['data-start-seconds', 'data-end-seconds'] 871 | }); 872 | const videoElem = getVideoElement(); 873 | if (videoElem) { 874 | if (videoElem.readyState >= 1) { 875 | updateMiniProgress(); 876 | } else { 877 | videoElem.addEventListener('loadedmetadata', updateMiniProgress, {once: true}); 878 | } 879 | } 880 | 881 | const sortSegmentsVisually = () => { 882 | const segments = Array.from(adSegmentsContainer.children).filter(el => el.classList.contains('ad-segment')); 883 | segments.sort((a, b) => { 884 | const timeA = parseFloat(a.dataset.sortTime ?? Infinity); 885 | const timeB = parseFloat(b.dataset.sortTime ?? Infinity); 886 | return timeA - timeB; 887 | }); 888 | segments.forEach(seg => adSegmentsContainer.appendChild(seg)); 889 | } 890 | 891 | adSegmentsContainer.appendChild(segment); 892 | sortSegmentsVisually(); 893 | 894 | segment.querySelector('.remove-segment').addEventListener('click', () => { 895 | if(targetSegmentElement === segment) { 896 | cancelTimeSelectionMode(); 897 | } 898 | segment.remove(); 899 | observer.disconnect(); 900 | const videoElem = getVideoElement(); 901 | if (videoElem) videoElem.removeEventListener('loadedmetadata', updateMiniProgress); 902 | checkEmptyState(); 903 | }); 904 | 905 | const setStartButton = segment.querySelector('.set-start-time'); 906 | setStartButton.addEventListener('click', () => { 907 | enterTimeSelectionMode(segment, 'start', setStartButton); 908 | }); 909 | 910 | const setEndButton = segment.querySelector('.set-end-time'); 911 | setEndButton.addEventListener('click', () => { 912 | enterTimeSelectionMode(segment, 'end', setEndButton); 913 | }); 914 | 915 | checkEmptyState(); 916 | } 917 | 918 | function checkEmptyState() { 919 | const segmentCount = adSegmentsContainer.children.length; 920 | emptyStateDiv.style.display = segmentCount === 0 ? 'block' : 'none'; 921 | } 922 | 923 | const initialAds = (currentAdsData && currentAdsData.ads ? currentAdsData.ads : []) 924 | .map(ad => ({ 925 | start_time: !isNaN(parseFloat(ad.start_time)) ? parseFloat(ad.start_time) : null, 926 | end_time: !isNaN(parseFloat(ad.end_time)) ? parseFloat(ad.end_time) : null, 927 | product_name: ad.product_name, 928 | ad_content: ad.ad_content 929 | })); 930 | initialAds.forEach(ad => addAdSegment(ad)); 931 | checkEmptyState(); 932 | 933 | popup.querySelector('#add-segment').addEventListener('click', () => { 934 | addAdSegment(); 935 | }); 936 | 937 | popup.querySelector('#close-correction-popup').addEventListener('click', () => { 938 | cancelTimeSelectionMode(); 939 | closePopup(popup); 940 | }); 941 | 942 | async function submitCorrection(adsData, message, model) { 943 | cancelTimeSelectionMode(); 944 | if (typeof settings === 'undefined' || !settings.cfApiURL || !settings.cfApiKey) { 945 | showPopup("提交失败:无法访问配置"); 946 | return; 947 | } 948 | 949 | if (JSON.stringify(adsData) == JSON.stringify(currentAdsData)) { 950 | const submitButton = popup.querySelector('#submit-button'); 951 | if(submitButton) submitButton.disabled = false; submitButton.textContent = '提交'; 952 | showPopup("数据无变化"); 953 | return; 954 | } 955 | 956 | try { 957 | await new Promise((resolve, reject) => { 958 | chrome.runtime.sendMessage({ 959 | action: "dbQuery", 960 | url: settings.cfApiURL, 961 | method: "POST", 962 | cfApiKey: settings.cfApiKey, 963 | body: { 964 | sql: `UPDATE bilijump SET data = ?, model = ? WHERE cid = ?;`, 965 | params: [JSON.stringify(adsData), model, cid] 966 | } 967 | }, response => { 968 | if (chrome.runtime.lastError) { 969 | console.info("Bilibili AI Skip: chrome.runtime.lastError:", chrome.runtime.lastError.message); 970 | reject(new Error(chrome.runtime.lastError.message || 'Unknown background script error')); 971 | } else if (response && response.success) { 972 | styleLog('Bilibili AI Skip: Database update successful for correction.'); 973 | resolve(); 974 | } else { 975 | const errorMsg = (response && response.error) ? response.error : 'Unknown background error'; 976 | console.info("Bilibili AI Skip: Correction submission error -", errorMsg); 977 | reject(new Error(errorMsg)); 978 | } 979 | }); 980 | }); 981 | 982 | showPopup(message); 983 | styleLog('Bilibili AI Skip: Correction submitted successfully.'); 984 | closePopup(popup); 985 | showPopup("页面将在3秒后刷新..."); 986 | setTimeout(() => { location.reload(); }, 3000); 987 | } catch (error) { 988 | showPopup('提交失败:' + error.message); 989 | console.info('Bilibili AI Skip: Submission failed:', error); 990 | } 991 | } 992 | 993 | popup.querySelector('#ai-re-recog').addEventListener('click', async () => { 994 | if (window.confirm("您确定使用 AI 重新识别广告吗?这将覆盖之前的记录。")) { 995 | let bvid = window.location.pathname.split('/')[2], pvid = new URLSearchParams(window.location.search).get('p'); 996 | let response = await fetch(`https://api.bilibili.com/x/web-interface/view?bvid=${bvid}`, {credentials: "include"}); 997 | const videoData = await response.json(), aid = videoData.data.aid, cid = videoData.data.pages?.[pvid?pvid-1:pvid]?.cid || videoData.data.cid; 998 | 999 | let dbResults = await new Promise((resolve, reject) => { 1000 | chrome.runtime.sendMessage({ 1001 | action: "dbQuery", 1002 | url: settings.cfApiURL, 1003 | method: "POST", 1004 | cfApiKey: settings.cfApiKey, 1005 | body: {sql: "SELECT subtitle FROM bilijump WHERE cid = ? LIMIT 1;", params: [cid]} 1006 | }, response => { 1007 | if (response.success) { 1008 | resolve(response?.data?.result?.[0]?.results?.[0]); 1009 | } else { 1010 | styleLog("Background fetch error: " + response.error); 1011 | reject(new Error(response.error)); 1012 | } 1013 | }); 1014 | }); 1015 | 1016 | if(dbResults?.subtitle) { 1017 | for(const key of ["apiKey", "apiURL", "apiModel"]) { 1018 | if(!settings[key]) { 1019 | popups.others.push(showPopup(`Please set ${key} in extension settings`)); 1020 | return {ads:[], msg:`Please set ${key}`}; 1021 | } 1022 | } 1023 | 1024 | if (banModels.includes(settings.apiModel)) { 1025 | showPopup(`禁用 ${settings.apiModel} 模型,效果太差`); 1026 | return {ads:[], msg: `请使用其他模型`}; 1027 | } 1028 | 1029 | popups.ai = showPopup(`使用 ${settings.apiModel} 重新分析中...`,1); 1030 | popups.others.push(popups.ai); 1031 | 1032 | let data = await callOpenAI(dbResults.subtitle), aiResponse = data?.choices?.[0]?.message?.content, total_tokens = data?.usage?.total_tokens; 1033 | for(let i = 1; i < 3 && !aiResponse; i++) { 1034 | showPopup('Re-fetch AI.'); 1035 | aiResponse = await callOpenAI(dbResults.subtitle); 1036 | } 1037 | closePopup(popups.ai); 1038 | 1039 | 1040 | if (!aiResponse) { 1041 | showPopup("AI 解析失败."); 1042 | return 1043 | } 1044 | 1045 | const jsonMatch = aiResponse.match(/```json([\s\S]*?)```/); 1046 | let resultAD; 1047 | if (jsonMatch && jsonMatch[1]) { 1048 | const jsonContent = jsonMatch[1].trim(); 1049 | resultAD = JSON.parse(jsonContent); 1050 | }else { 1051 | try { 1052 | resultAD = JSON.parse(aiResponse); 1053 | } catch (error) { 1054 | showPopup("AAI 分析结果获取失败. " + error); 1055 | return 1056 | } 1057 | } 1058 | 1059 | submitCorrection(resultAD, '重新识别已完成.', settings.apiModel); 1060 | } 1061 | } else { 1062 | showPopup('取消提交'); 1063 | } 1064 | }); 1065 | 1066 | popup.querySelector('#confirm-no-ads').addEventListener('click', () => { 1067 | if (window.confirm("您确定这个视频没有广告内容吗?这将覆盖之前的记录。")) { 1068 | const noAdsData = { 1069 | ads: [], 1070 | msg: "未识别到广告" 1071 | }; 1072 | submitCorrection(noAdsData, '已提交', 'artificial'); 1073 | } else { 1074 | showPopup('取消提交'); 1075 | } 1076 | }); 1077 | 1078 | popup.querySelector('#submit-button').addEventListener('click', async () => { 1079 | cancelTimeSelectionMode(); 1080 | 1081 | const segments = adSegmentsContainer.querySelectorAll('.ad-segment'); 1082 | const ads = []; 1083 | let validationError = null; 1084 | let firstErrorElement = null; 1085 | 1086 | for (const segment of segments) { 1087 | const startTimeStr = segment.getAttribute('data-start-seconds'); 1088 | const endTimeStr = segment.getAttribute('data-end-seconds'); 1089 | const productNameInput = segment.querySelector('.product-name'); 1090 | const adContentTextarea = segment.querySelector('.ad-content-textarea'); 1091 | const startTimeDisplay = segment.querySelector('.start-time-display'); 1092 | const endTimeDisplay = segment.querySelector('.end-time-display'); 1093 | 1094 | const productName = productNameInput.value.trim(); 1095 | const adContent = adContentTextarea.value.trim(); 1096 | 1097 | startTimeDisplay.style.borderColor = 'transparent'; 1098 | endTimeDisplay.style.borderColor = 'transparent'; 1099 | productNameInput.style.borderColor = ''; 1100 | adContentTextarea.style.borderColor = ''; 1101 | 1102 | const startTime = (startTimeStr && !isNaN(parseFloat(startTimeStr))) ? parseFloat(startTimeStr) : null; 1103 | const endTime = (endTimeStr && !isNaN(parseFloat(endTimeStr))) ? parseFloat(endTimeStr) : null; 1104 | 1105 | if (startTime === null) { 1106 | validationError = '存在未设置的开始时间'; 1107 | startTimeDisplay.style.borderColor = 'red'; 1108 | if (!firstErrorElement) firstErrorElement = segment.querySelector('.set-start-time'); 1109 | break; 1110 | } 1111 | if (endTime === null) { 1112 | validationError = '存在未设置的结束时间'; 1113 | endTimeDisplay.style.borderColor = 'red'; 1114 | if (!firstErrorElement) firstErrorElement = segment.querySelector('.set-end-time'); 1115 | break; 1116 | } 1117 | if (startTime >= endTime) { 1118 | validationError = `开始时间 (${secondsToTime(startTime)}) 必须早于结束时间 (${secondsToTime(endTime)})`; 1119 | startTimeDisplay.style.borderColor = 'red'; 1120 | endTimeDisplay.style.borderColor = 'red'; 1121 | if (!firstErrorElement) firstErrorElement = startTimeDisplay; 1122 | break; 1123 | } 1124 | 1125 | if (ads.length > 0) { 1126 | const prevAd = ads[ads.length - 1]; 1127 | const prevEndTime = parseFloat(prevAd.end_time); 1128 | if (startTime < prevEndTime) { 1129 | validationError = `片段重叠:开始时间 (${secondsToTime(startTime)}) 早于上一个片段的结束时间 (${secondsToTime(prevEndTime)})`; 1130 | startTimeDisplay.style.borderColor = 'orange'; 1131 | const prevSegment = segments[ads.length -1]; 1132 | if(prevSegment) prevSegment.querySelector('.end-time-display').style.borderColor = 'orange'; 1133 | if (!firstErrorElement) firstErrorElement = startTimeDisplay; 1134 | break; 1135 | } 1136 | } 1137 | 1138 | if (!productName) { 1139 | validationError = '产品名称不能为空'; 1140 | productNameInput.style.borderColor = 'red'; 1141 | if (!firstErrorElement) firstErrorElement = productNameInput; 1142 | break; 1143 | } 1144 | if (!adContent) { 1145 | validationError = '广告内容不能为空'; 1146 | adContentTextarea.style.borderColor = 'red'; 1147 | if (!firstErrorElement) firstErrorElement = adContentTextarea; 1148 | break; 1149 | } 1150 | 1151 | ads.push({ 1152 | start_time: startTime.toFixed(2), 1153 | end_time: endTime.toFixed(2), 1154 | product_name: productName, 1155 | ad_content: adContent 1156 | }); 1157 | } 1158 | 1159 | if (validationError) { 1160 | showPopup('提交失败:' + validationError); 1161 | styleLog('Bilibili AI Skip: Validation failed -' + validationError); 1162 | if (firstErrorElement && typeof firstErrorElement.focus === 'function') { 1163 | firstErrorElement.focus(); 1164 | } 1165 | if(firstErrorElement) { 1166 | const errorSegment = firstErrorElement.closest('.ad-segment'); 1167 | if(errorSegment) { 1168 | errorSegment.style.transition = 'outline 0.1s ease-in-out'; 1169 | errorSegment.style.outline = '2px solid red'; 1170 | setTimeout(() => { errorSegment.style.outline = 'none'; }, 2000); 1171 | } 1172 | } 1173 | return; 1174 | } 1175 | 1176 | ads.sort((a, b) => parseFloat(a.start_time) - parseFloat(b.start_time)); 1177 | 1178 | const mergedAds = []; 1179 | let currentAd = null; 1180 | const mergeThreshold = 1.0; 1181 | 1182 | for (const ad of ads) { 1183 | const adStart = parseFloat(ad.start_time); 1184 | const adEnd = parseFloat(ad.end_time); 1185 | if (!currentAd) { 1186 | currentAd = { ...ad, start_time: adStart, end_time: adEnd }; 1187 | } else if (adStart <= currentAd.end_time + mergeThreshold) { 1188 | currentAd.end_time = Math.max(currentAd.end_time, adEnd); 1189 | currentAd.product_name = `${currentAd.product_name} | ${ad.product_name}`; 1190 | currentAd.ad_content = `${currentAd.ad_content}\n---\n${ad.ad_content}`; 1191 | } else { 1192 | mergedAds.push({ 1193 | ...currentAd, 1194 | start_time: currentAd.start_time.toFixed(2), 1195 | end_time: currentAd.end_time.toFixed(2) 1196 | }); 1197 | currentAd = { ...ad, start_time: adStart, end_time: adEnd }; 1198 | } 1199 | } 1200 | if (currentAd) { 1201 | mergedAds.push({ 1202 | ...currentAd, 1203 | start_time: currentAd.start_time.toFixed(2), 1204 | end_time: currentAd.end_time.toFixed(2) 1205 | }); 1206 | } 1207 | 1208 | const correctedAdsData = { 1209 | ads: mergedAds, 1210 | msg: mergedAds.length > 0 ? "识别到广告" : "未识别到广告" 1211 | }; 1212 | 1213 | const submitButton = popup.querySelector('#submit-button'); 1214 | if(submitButton) submitButton.disabled = true; submitButton.textContent = '提交中...'; 1215 | 1216 | try { 1217 | if(window.confirm("确定提交?这将覆盖之前的记录。")) { 1218 | await submitCorrection(correctedAdsData, '纠错提交成功!', 'artificial'); 1219 | }else { 1220 | showPopup('取消提交'); 1221 | if(submitButton) submitButton.disabled = false; submitButton.textContent = '提交'; 1222 | } 1223 | } catch (error) { 1224 | if(submitButton) submitButton.disabled = false; submitButton.textContent = '提交'; 1225 | } 1226 | }); 1227 | } 1228 | 1229 | const adjustPopupPositions = () => { 1230 | document.querySelectorAll('.popup').forEach((el, i, arr) => { 1231 | el.style.bottom = `${100 + (arr.length - i - 1) * (el.offsetHeight + 10)}px`; 1232 | }); 1233 | }; 1234 | 1235 | const getTime = (seconds) => { 1236 | const pad = n => n.toString().padStart(2, '0'); 1237 | return [ 1238 | Math.floor(seconds / 3600), 1239 | Math.floor(seconds % 3600 / 60), 1240 | Math.floor(seconds % 60) 1241 | ].map(pad).join(':'); 1242 | }; 1243 | 1244 | async function submitTranscriptionTask(audioURL) { 1245 | const requestBody = { 1246 | model: "paraformer-v2", 1247 | input: { file_urls: [audioURL] }, 1248 | parameters: { channel_id: [0], language_hints: ["zh", "en", "ja", "yue", "ko", "de", "fr", "ru"] } 1249 | }; 1250 | return new Promise((resolve, reject) => { 1251 | chrome.runtime.sendMessage({ 1252 | action: "fetchDashScope", 1253 | url: settings.aliApiURL, 1254 | method: "POST", 1255 | apiKey: settings.aliApiKey, 1256 | body: requestBody 1257 | }, response => { 1258 | if (response.success) { 1259 | resolve(response.data.output.task_id); 1260 | } else { 1261 | styleLog("Background fetch error: " + JSON.stringify(response.error)); 1262 | reject(new Error(response.error)); 1263 | } 1264 | }); 1265 | }); 1266 | } 1267 | 1268 | async function waitForTaskCompletion(taskId) { 1269 | while (true) { 1270 | try { 1271 | const response = await new Promise((resolve, reject) => { 1272 | chrome.runtime.sendMessage({ 1273 | action: "fetchDashScope", 1274 | url: `${settings.aliTaskURL}${taskId}`, 1275 | method: "GET", 1276 | apiKey: settings.aliApiKey 1277 | }, response => { 1278 | if (response.success) { 1279 | resolve(response.data); 1280 | } else { 1281 | styleLog("Background fetch error: " + JSON.stringify(response.error)); 1282 | reject(new Error(response.error)); 1283 | } 1284 | }); 1285 | }); 1286 | 1287 | styleLog("Task status: " + response?.output?.task_status); 1288 | 1289 | switch (response.output.task_status) { 1290 | case "SUCCEEDED": 1291 | showPopup("音频解析成功."); 1292 | closePopup(popups.task); 1293 | return response.output.results; 1294 | case "FAILED": 1295 | showPopup("音频解析失败."); 1296 | closePopup(popups.task); 1297 | //throw new Error(`Task failed: ${response.error?.message || "Unknown error"}`); 1298 | return response.output.results; 1299 | case "RUNNING": 1300 | case "PENDING": 1301 | if (!popups.task) { 1302 | popups.task = showPopup("音频解析中...", 1); 1303 | popups.others.push(popups.task); 1304 | } 1305 | await new Promise(resolve => setTimeout(resolve, 5000)); 1306 | break; 1307 | default: 1308 | showPopup("音频解析遇到未知错误."); 1309 | throw new Error(`Unknown task status: ${response.output.task_status}`); 1310 | } 1311 | } catch (error) { 1312 | styleLog("Error checking task status: " + JSON.stringify(error)); 1313 | throw error; 1314 | } 1315 | } 1316 | } 1317 | 1318 | async function fetchTranscription(transcriptionURL) { 1319 | try { 1320 | const response = await new Promise((resolve, reject) => { 1321 | chrome.runtime.sendMessage( 1322 | { action: "fetchTranscription", url: transcriptionURL }, 1323 | (response) => { 1324 | if (chrome.runtime.lastError) { 1325 | return reject(new Error(chrome.runtime.lastError.message)); 1326 | } 1327 | if (response && response.success) { 1328 | resolve(response.data); 1329 | } else { 1330 | styleLog("Background fetch error: " + JSON.stringify(response?.error)); 1331 | reject(new Error(response?.error || "Unknown error fetching transcription")); 1332 | } 1333 | } 1334 | ); 1335 | }); 1336 | return response; 1337 | } catch (error) { 1338 | styleLog("Error fetching transcription: " + JSON.stringify(error)); 1339 | throw error; 1340 | } 1341 | } 1342 | 1343 | function generateSubtitle(transcription) { 1344 | const transcripts = JSON.parse(transcription).transcripts[0].sentences; 1345 | let subtitle = ""; 1346 | 1347 | transcripts.forEach(sentence => { 1348 | const from = (sentence.begin_time / 1000).toFixed(2); 1349 | const to = (sentence.end_time / 1000).toFixed(2); 1350 | subtitle += `${from} --> ${to}\n${sentence.text}\n`; 1351 | }); 1352 | 1353 | return subtitle; 1354 | } 1355 | 1356 | function updateTimes(cid, skip_time) { 1357 | chrome.runtime.sendMessage({ 1358 | action: "dbQuery", url: settings.cfApiURL, method: "POST", cfApiKey: settings.cfApiKey, 1359 | body: { 1360 | sql: `UPDATE bilijump SET times = times + 1, skip_time = skip_time + ? WHERE cid = ?;`, 1361 | params: [Math.ceil(skip_time), cid] 1362 | } 1363 | }); 1364 | } 1365 | 1366 | function correctButton(cid, data) { 1367 | const adLength = data.ads.length; 1368 | const adTime = data.ads.reduce((sum, ad) => sum + (parseFloat(ad.end_time) - parseFloat(ad.start_time)), 0); 1369 | const iconUse = Math.max(adLength < 3 ? adLength : 3, adTime == 0 ? 0 : adTime <= 45 ? 1 : adTime <= 90 ? 2 : 3); 1370 | 1371 | let playerRight = document.querySelector('.bpx-player-control-bottom-right'); 1372 | var correct = document.createElement('div'); 1373 | correct.innerHTML = `
icon纠错
`; 1374 | correct.id = 'bilibili-ai-skip-correct'; 1375 | correct.style.width = 'auto'; 1376 | correct.style.height = '22px'; 1377 | correct.style.marginRight = '20px'; 1378 | correct.addEventListener('click', () => showCorrectionPopup(cid, data)); 1379 | playerRight.prepend(correct); 1380 | } 1381 | 1382 | async function activeLog() { 1383 | let uid = await chrome.storage.sync.get('uid'); 1384 | chrome.runtime.sendMessage({ 1385 | action: "dbQuery", url: settings.cfApiURL, method: "POST", cfApiKey: settings.cfApiKey, 1386 | body: { 1387 | sql: `INSERT INTO bilijump_active(uid) VALUES(?);`, 1388 | params: [uid.uid] 1389 | } 1390 | }); 1391 | } 1392 | 1393 | function styleLog(msg) { 1394 | console.info("%c Bilibili AI Skip %c " + msg + " ", "padding: 2px 6px; border-radius: 3px 0 0 3px; color: #fff; background: #a19cef; font-weight: bold;", "padding: 2px 6px; border-radius: 0 3px 3px 0; color: #fff; background: #FF6699; font-weight: bold;") 1395 | } --------------------------------------------------------------------------------