您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
在视频播放页面右下角添加悬浮按钮,可自动点击AI字幕按钮、智能切换到“字幕列表”标签页、并一键提取所有字幕文本。
// ==UserScript== // @name Bilibili - AI 小助手字幕提取器 // @namespace http://tampermonkey.net/ // @version 1.5 // @description 在视频播放页面右下角添加悬浮按钮,可自动点击AI字幕按钮、智能切换到“字幕列表”标签页、并一键提取所有字幕文本。 // @author Fine // @license MIT // @match *://*.bilibili.com/video/* // @icon https://www.bilibili.com/favicon.ico // @grant GM_addStyle // @grant GM_setClipboard // ==/UserScript== (function() { 'use strict'; // --- 配置区域 --- const SUBTITLE_CONFIGS = [ { name: 'Bilibili', host: 'bilibili.com', triggerSelector: '.video-ai-assistant', panelSelector: 'div[class*="_InteractWrapper_"]', // 改为更精确的标签页按钮选择器 tabButtonSelector: 'div[class*="_TabItem_"]', tabSelectorText: '字幕列表', containerSelector: 'div[class*="_Subtitles_"]', textSelector: 'span[class*="_Text_"]', }, // ... 其他网站配置 ]; // --- 脚本核心逻辑 --- /** * 高可靠性模拟点击函数。 * @param {Element} element - 需要点击的 DOM 元素。 */ function simulateClick(element) { if (!element) return; // 依次尝试多种事件,以兼容不同框架的事件监听 const events = [ new MouseEvent('pointerdown', { bubbles: true, cancelable: true }), new MouseEvent('mousedown', { bubbles: true, cancelable: true }), new MouseEvent('pointerup', { bubbles: true, cancelable: true }), new MouseEvent('mouseup', { bubbles: true, cancelable: true }), new MouseEvent('click', { bubbles: true, cancelable: true }) ]; events.forEach(event => element.dispatchEvent(event)); } function waitForElement(selector, timeout = 10000) { return new Promise((resolve, reject) => { const element = document.querySelector(selector); if (element) { resolve(element); return; } const observer = new MutationObserver((mutations, obs) => { const foundElement = document.querySelector(selector); if (foundElement) { obs.disconnect(); resolve(foundElement); } }); observer.observe(document.body, { childList: true, subtree: true }); setTimeout(() => { observer.disconnect(); reject(new Error(`等待元素超时: "${selector}" 在 ${timeout}ms 内未出现。`)); }, timeout); }); } async function performExtraction(uiElements) { const { outputTextarea, startButton, copyButton } = uiElements; startButton.disabled = true; copyButton.disabled = true; outputTextarea.value = "正在初始化..."; try { const currentHost = window.location.hostname; const config = SUBTITLE_CONFIGS.find(c => currentHost.includes(c.host)); if (!config) throw new Error("当前网站不受支持。"); // 步骤 1: 点击主触发按钮 const triggerButton = document.querySelector(config.triggerSelector); if (triggerButton) { outputTextarea.value = `[1/4] 找到主按钮,正在模拟点击...`; simulateClick(triggerButton); } else { console.warn(`未找到主触发按钮 "${config.triggerSelector}",将直接继续。`); } // 步骤 2: 等待AI面板容器出现 outputTextarea.value = `[2/4] 等待AI面板 "${config.panelSelector}" 加载...`; const panel = await waitForElement(config.panelSelector); // 步骤 3: 查找并点击 "字幕列表" 标签页 (已优化) outputTextarea.value = `[3/4] 面板已加载,查找并点击 "${config.tabSelectorText}" 标签页...`; // 查找所有可能的标签按钮 const tabButtons = panel.querySelectorAll(config.tabButtonSelector); let targetTab = null; for (const button of tabButtons) { if (button.textContent.includes(config.tabSelectorText)) { targetTab = button; break; } } if (targetTab) { // 使用高可靠性点击 simulateClick(targetTab); } else { throw new Error(`在AI面板中未能找到包含文本 "${config.tabSelectorText}" 的标签页按钮。`); } // 步骤 4: 等待最终的字幕容器出现 outputTextarea.value = `[4/4] 等待字幕容器 "${config.containerSelector}" 加载...`; const container = await waitForElement(config.containerSelector, 5000); outputTextarea.value = "容器已找到,正在提取文本..."; // 步骤 5: 提取所有字幕文本 const textElements = container.querySelectorAll(config.textSelector); if (textElements.length === 0) { throw new Error(`在容器中未能找到任何字幕文本。\n行内文本选择器:\n"${config.textSelector}"`); } const subtitles = Array.from(textElements) .map(el => el.textContent.trim()) .filter(text => text) .join('\n'); outputTextarea.value = subtitles; copyButton.disabled = false; } catch (error) { outputTextarea.value = `错误:\n${error.message}\n\n请确认页面结构未改变或刷新页面重试。`; console.error("字幕提取脚本错误:", error); } finally { startButton.disabled = false; } } // UI 创建和初始化 function init() { // --- UI 代码 (与上一版相同) --- GM_addStyle(` #subtitle-extractor-trigger { position: fixed; bottom: 20px; right: 20px; z-index: 99999; background-color: #00a1d6; color: white; padding: 10px 15px; border-radius: 8px; cursor: pointer; font-size: 14px; box-shadow: 0 4px 10px rgba(0,0,0,0.3); transition: all 0.2s ease-in-out; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; } #subtitle-extractor-trigger:hover { background-color: #00b5e5; transform: translateY(-2px); } #subtitle-extractor-modal { position: fixed; z-index: 100000; left: 0; top: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.6); display: none; align-items: center; justify-content: center; } #subtitle-extractor-modal .modal-content { background-color: #2c2c2c; color: #e0e0e0; padding: 20px; border-radius: 12px; width: 90%; max-width: 600px; height: 80%; max-height: 700px; display: flex; flex-direction: column; box-shadow: 0 5px 20px rgba(0,0,0,0.5); } #subtitle-extractor-modal .modal-header { display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #444; padding-bottom: 15px; margin-bottom: 15px; } #subtitle-extractor-modal .close-button { color: #aaa; font-size: 28px; font-weight: bold; cursor: pointer; } #subtitle-extractor-modal .close-button:hover { color: white; } #subtitle-extractor-modal .modal-body { flex-grow: 1; display: flex; } #subtitle-extractor-modal #subtitle-output { width: 100%; height: 100%; background: #1a1a1a; color: #e0e0e0; border: 1px solid #444; border-radius: 8px; padding: 15px; font-size: 14px; line-height: 1.6; resize: none; font-family: "Consolas", "Monaco", monospace; } #subtitle-extractor-modal .modal-footer { padding-top: 15px; text-align: right; } #subtitle-extractor-modal .modal-btn { padding: 10px 20px; border: 1px solid #555; background-color: #3e3e3e; color: white; border-radius: 6px; cursor: pointer; transition: all 0.2s ease; margin-left: 10px; } #subtitle-extractor-modal .modal-btn:hover:not(:disabled) { background-color: #555; } #subtitle-extractor-modal .modal-btn.primary { background-color: #00a1d6; border-color: #00a1d6; } #subtitle-extractor-modal .modal-btn.primary:hover:not(:disabled) { background-color: #00b5e5; } #subtitle-extractor-modal .modal-btn:disabled { opacity: 0.5; cursor: not-allowed; } `); const triggerButton = document.createElement('div'); triggerButton.textContent = '提取字幕'; triggerButton.id = 'subtitle-extractor-trigger'; document.body.appendChild(triggerButton); const modalContainer = document.createElement('div'); modalContainer.id = 'subtitle-extractor-modal'; modalContainer.innerHTML = ` <div class="modal-content"> <div class="modal-header"><h2>提取的字幕</h2><span class="close-button">×</span></div> <div class="modal-body"><textarea id="subtitle-output" readonly placeholder="点击“开始提取”按钮来启动..."></textarea></div> <div class="modal-footer"> <button id="copy-subtitle-btn" class="modal-btn" disabled>复制全部</button> <button id="start-extraction-btn" class="modal-btn primary">开始提取</button> </div> </div>`; document.body.appendChild(modalContainer); const uiElements = { modalContainer, closeButton: modalContainer.querySelector('.close-button'), copyButton: document.getElementById('copy-subtitle-btn'), startButton: document.getElementById('start-extraction-btn'), outputTextarea: document.getElementById('subtitle-output'), }; triggerButton.addEventListener('click', () => { uiElements.modalContainer.style.display = 'flex'; }); uiElements.closeButton.addEventListener('click', () => { uiElements.modalContainer.style.display = 'none'; }); modalContainer.addEventListener('click', (event) => { if (event.target === modalContainer) uiElements.modalContainer.style.display = 'none'; }); uiElements.startButton.addEventListener('click', () => performExtraction(uiElements)); uiElements.copyButton.addEventListener('click', () => { if (!uiElements.outputTextarea.value) return; GM_setClipboard(uiElements.outputTextarea.value); const originalText = uiElements.copyButton.textContent; uiElements.copyButton.textContent = '已复制!'; setTimeout(() => { uiElements.copyButton.textContent = originalText; }, 2000); }); } window.addEventListener('load', init, false); })();