// ==UserScript== // @name Youtube 双语字幕版 // @version 1.2.1 // @author LR // @license MIT // @description YouTube双语字幕,任何语言翻译成中文。支持移动端和桌面端,适配Via浏览器。 // @match *://www.youtube.com/* // @match *://m.youtube.com/* // @require https://unpkg.com/ajax-hook@latest/dist/ajaxhook.min.js // @grant none // @run-at document-start // @namespace https://greasyfork.org/users/1210499 // @icon https://www.youtube.com/s/desktop/b9bfb983/img/favicon_32x32.png // @downloadURL none // ==/UserScript== (function () { 'use strict'; const TARGET_LANG = 'zh'; // 设置目标翻译语言为中文 async function enableDualSubtitles() { // 获取翻译后的字幕数据 async function fetchTranslatedSubtitles(url) { const cleanUrl = url.replace(/(^|[&?])tlang=[^&]*/g, '') + `&tlang=${TARGET_LANG}&translate_h00ked`; try { const response = await fetch(cleanUrl, { method: 'GET' }); if (!response.ok) { throw new Error(`Failed to fetch translated subtitles: ${response.status}`); } return await response.json(); } catch (error) { console.error(error); return null; } } // 计算编辑距离(Levenshtein距离) function levenshteinDistance(s1, s2) { if (s1.length === 0) return s2.length; if (s2.length === 0) return s1.length; const matrix = []; // 初始化矩阵 for (let i = 0; i <= s1.length; i++) { matrix[i] = [i]; } for (let j = 0; j <= s2.length; j++) { matrix[0][j] = j; } // 填充矩阵 for (let i = 1; i <= s1.length; i++) { for (let j = 1; j <= s2.length; j++) { if (s1[i - 1] === s2[j - 1]) { matrix[i][j] = matrix[i - 1][j - 1]; } else { matrix[i][j] = Math.min( matrix[i - 1][j - 1] + 1, // 替换 matrix[i][j - 1] + 1, // 插入 matrix[i - 1][j] + 1 // 删除 ); } } } return matrix[s1.length][s2.length]; } // 计算Jaccard相似度 function jaccardSimilarity(str1, str2) { const set1 = new Set(str1.split('')); const set2 = new Set(str2.split('')); const intersection = new Set([...set1].filter(x => set2.has(x))); const union = new Set([...set1, ...set2]); return intersection.size / union.size; } // 计算综合相似度 function calculateSimilarity(s1, s2) { const maxLength = Math.max(s1.length, s2.length); const levenshteinSimilarity = 1 - (levenshteinDistance(s1, s2) / maxLength); const jaccardSim = jaccardSimilarity(s1, s2); // 给予Levenshtein距离更高的权重 return (levenshteinSimilarity * 0.7) + (jaccardSim * 0.3); } function mergeSubtitles(defaultSubs, translatedSubs) { const mergedSubs = JSON.parse(JSON.stringify(defaultSubs)); const translatedEvents = translatedSubs.events.filter(event => event.segs); let tIndex = 0; // 翻译事件索引 for (let i = 0; i < mergedSubs.events.length; i++) { const defaultEvent = mergedSubs.events[i]; if (!defaultEvent.segs) continue; // 匹配时间最接近的翻译字幕事件 while (tIndex < translatedEvents.length && translatedEvents[tIndex].tStartMs < defaultEvent.tStartMs) { tIndex++; } const translatedEvent = translatedEvents[tIndex]; if (translatedEvent) { const defaultText = defaultEvent.segs.map(seg => seg.utf8).join(''); const translatedText = translatedEvent.segs.map(seg => seg.utf8).join(''); // 检查时间重叠 const timeOverlap = Math.min(defaultEvent.tStartMs + defaultEvent.dDurationMs, translatedEvent.tStartMs + translatedEvent.dDurationMs) - Math.max(defaultEvent.tStartMs, translatedEvent.tStartMs); const isTimeOverlapping = timeOverlap > 0; // 计算综合相似度 const similarity = calculateSimilarity(defaultText, translatedText); // 如果时间重叠且相似度较低,则合并字幕 if (isTimeOverlapping && similarity < 0.6) { defaultEvent.segs[0].utf8 = `${defaultText}\n${translatedText}`; defaultEvent.segs = [defaultEvent.segs[0]]; } } } return JSON.stringify(mergedSubs); } // 使用 ajax-hook 代理请求和响应,以获取并处理字幕数据 ah.proxy({ onResponse: async (response, handler) => { if (response.config.url.includes('/api/timedtext') && !response.config.url.includes('&translate_h00ked')) { try { const defaultSubs = JSON.parse(response.response); const translatedSubs = await fetchTranslatedSubtitles(response.config.url); if (translatedSubs) { response.response = mergeSubtitles(defaultSubs, translatedSubs); } } catch (error) { console.error("Error processing subtitles:", error); } } handler.resolve(response); } }); } enableDualSubtitles(); })();