NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name Universal Subtitle Sniffer
// @namespace https://openuserjs.org/users/barretlee
// @version 0.6
// @description 自动嗅探字幕并提取到console,若无字幕则用Web Speech API兜底识别(支持语言配置)
// @author Barret Lee <barret.china@gmail.com>
// @match *://www.bilibili.com/*
// @match *://www.youku.com/*
// @match *://v.qq.com/*
// @match *://www.youtube.com/*
// @match *://*.iqiyi.com/*
// @match *://www.netflix.com/*
// @match *://www.disneyplus.com/*
// @match *://tv.apple.com/*
// @match *://www.primevideo.com/*
// @match *://www.xiaoyuzhoufm.com/*
// @match *://play.max.com/*
// @grant GM_registerMenuCommand
// @grant GM_setValue
// @grant GM_getValue
// @license MIT
// ==/UserScript==
(function () {
'use strict';
let config = GM_getValue("subtitle_config", {
keywords: ["subtitle", "timedtext", "aisubtitle", "srt"], // URL关键字
debug: true,
lang: "zh-CN"
});
let subtitles = [];
let subtitleFound = false;
// ---------- 菜单配置 ----------
GM_registerMenuCommand("配置字幕关键字", () => {
const input = prompt("请输入URL中的关键字,逗号分隔", config.keywords.join(","));
if (input) {
config.keywords = input.split(",").map(s => s.trim());
GM_setValue("subtitle_config", config);
alert("配置已保存: " + config.keywords.join(", "));
}
});
GM_registerMenuCommand("配置语音识别语言", () => {
const input = prompt("请输入语言代码 (如 zh-CN, en-US, ja-JP)", config.lang);
if (input) {
config.lang = input.trim();
GM_setValue("subtitle_config", config);
alert("语音识别语言已保存: " + config.lang);
}
});
function tryExtract(data, url) {
let subs = [];
if (data.body && Array.isArray(data.body)) { // B站
subs = data.body.map(x => ({
from: x.from,
to: x.to,
content: x.content
}));
}
else if (data.events && Array.isArray(data.events)) { // YouTube
subs = data.events.flatMap(e =>
(e.segs || []).map(s => ({
from: e.tStartMs / 1000,
to: (e.tStartMs + e.dDurationMs) / 1000,
content: s.utf8
}))
);
}
else if (Array.isArray(data)) { // 通用
data.forEach(x => {
if (x.content) subs.push({
from: x.from || 0,
to: x.to || 0,
content: x.content
});
});
}
if (subs.length > 0) {
subtitleFound = true;
subtitles.push(...subs);
console.log("%c字幕来源", "background: #0066cc; color: #fff; padding:2px 4px; border-radius:2px;");
console.log(url);
console.log("%c字幕新增", "background: #0066cc; color: #fff; padding:2px 4px; border-radius:2px;");
console.log(subs);
console.log("%c当前字幕总数组", "background: #0066cc; color: #fff; padding:2px 4px; border-radius:2px;");
console.log(subtitles);
console.log("%c字幕新增", "background: #0066cc; color: #fff; padding:2px 4px; border-radius:2px;");
console.log(subs.map(v => v.content).join(','));
}
}
// ---------- fetch/XHR 拦截 ----------
const origFetch = window.fetch;
window.fetch = async function (...args) {
const res = await origFetch.apply(this, args);
const url = args[0].toString();
if (config.keywords.some(k => url.includes(k))) {
res.clone().json().then(d => tryExtract(d, url)).catch(() => {});
}
return res;
};
const origOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function (...args) {
this._url = args[1];
return origOpen.apply(this, args);
};
const origSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.send = function (...args) {
this.addEventListener("load", function () {
const url = this._url || "";
if (config.keywords.some(k => url.includes(k))) {
try {
const d = JSON.parse(this.responseText);
tryExtract(d, url);
}
catch (e) {}
}
});
return origSend.apply(this, args);
};
// ---------- Web Speech API ----------
function startSpeechRecognition() {
if (!("webkitSpeechRecognition" in window) && !("SpeechRecognition" in window)) {
console.warn("浏览器不支持 Web Speech API");
return;
}
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
const recognition = new SpeechRecognition();
recognition.continuous = true;
recognition.interimResults = true;
recognition.lang = config.lang || "zh-CN"; // 使用配置的语言
let lastTranscript = "";
let regSubs = [];
// ---------- 创建悬浮层 ----------
let overlay = document.createElement("div");
overlay.id = "speech-overlay";
overlay.style.position = "fixed";
overlay.style.top = "10px";
overlay.style.right = "10px";
overlay.style.maxWidth = "40%";
overlay.style.maxHeight = "30%";
overlay.style.overflowY = "auto";
overlay.style.background = "rgba(0,0,0,0.6)";
overlay.style.color = "#fff";
overlay.style.fontSize = "12px";
overlay.style.lineHeight = "1.4";
overlay.style.padding = "8px 12px";
overlay.style.borderRadius = "6px";
overlay.style.zIndex = 999999;
overlay.style.pointerEvents = "none"; // 不影响点击网页
document.body.appendChild(overlay);
recognition.onresult = function (event) {
// 取最后一次结果
const transcript = event.results[event.results.length - 1][0].transcript.trim();
if (transcript) {
overlay.innerText += "\n" + transcript; // 累加显示
overlay.scrollTop = overlay.scrollHeight; // 滚动到底部
}
};
recognition.onerror = e => console.error("识别错误:", e);
recognition.start();
console.log("语音识别已启动,语言:", recognition.lang);
}
// ---------- 定时兜底 ----------
// 这里有点 bug,先注释了,感兴趣的朋友自己实现下。
// setTimeout(() => {
// if (!subtitleFound) {
// const media = document.querySelector("video") || document.querySelector("audio");
// if (media) {
// console.log(
// "%c未发现字幕接口,尝试语音识别 (" + media.tagName.toLowerCase() + ")...",
// "background: #0066cc; color: #fff; padding:2px 4px; border-radius:2px;"
// );
// startSpeechRecognition();
// }
// }
// }, 5000);
})();