// ==UserScript== // @name Youtube 双语字幕版 // @version 1.3.0 // @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 GM_registerMenuCommand // @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 DEFAULT_LANG = 'zh'; // 默认设置为中文 let TARGET_LANG = DEFAULT_LANG; // 获取用户选择的翻译目标语言 function getUserSelectedLang() { const userLang = localStorage.getItem('dualSubTargetLang'); return userLang || DEFAULT_LANG; // 如果未设置,则使用默认语言 } // 保存用户选择的翻译目标语言 function setUserSelectedLang(lang) { localStorage.setItem('dualSubTargetLang', lang); TARGET_LANG = lang; } // 添加设置选项(Via浏览器支持脚本交互) function addSettingsMenu() { if (typeof GM_registerMenuCommand === 'function') { GM_registerMenuCommand('设置翻译语言', async () => { const userInput = prompt('请输入目标语言的ISO 639-1代码(例如:zh 中文, en 英文, ja 日语):', TARGET_LANG); if (userInput) { setUserSelectedLang(userInput.trim()); alert(`翻译目标语言已设置为:${userInput.trim()}`); } }); } } // 初始化目标语言 TARGET_LANG = getUserSelectedLang(); addSettingsMenu(); 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 = Array.from({ length: s1.length + 1 }, (_, i) => Array(s2.length + 1).fill(0).map((_, j) => (i === 0 ? j : i))); for (let i = 1; i <= s1.length; i++) { for (let j = 1; j <= s2.length; j++) { matrix[i][j] = (s1[i - 1] === s2[j - 1]) ? matrix[i - 1][j - 1] : 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 = [...set1].filter(x => set2.has(x)).length; const union = new Set([...set1, ...set2]).size; return intersection / union; } // 计算综合相似度 function calculateSimilarity(s1, s2) { const maxLength = Math.max(s1.length, s2.length); const levenshteinSimilarity = 1 - (levenshteinDistance(s1, s2) / maxLength); const jaccardSim = jaccardSimilarity(s1, s2); 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); const translatedMap = new Map(translatedEvents.map(event => [event.tStartMs, event])); // 使用 Map 存储翻译事件 for (let i = 0; i < mergedSubs.events.length; i++) { const defaultEvent = mergedSubs.events[i]; if (!defaultEvent.segs) continue; // 查找时间最接近的翻译字幕事件 const translatedEvent = [...translatedMap.keys()].reduce((closest, tStartMs) => { return (Math.abs(tStartMs - defaultEvent.tStartMs) < Math.abs(closest - defaultEvent.tStartMs)) ? tStartMs : closest; }, Infinity); const eventToMerge = translatedMap.get(translatedEvent); if (eventToMerge) { const defaultText = defaultEvent.segs.map(seg => seg.utf8).join(''); const translatedText = eventToMerge.segs.map(seg => seg.utf8).join(''); // 计算时间重叠 const timeOverlap = Math.min(defaultEvent.tStartMs + defaultEvent.dDurationMs, eventToMerge.tStartMs + eventToMerge.dDurationMs) - Math.max(defaultEvent.tStartMs, eventToMerge.tStartMs); if (timeOverlap > 0) { // 计算综合相似度 const similarity = calculateSimilarity(defaultText, translatedText); if (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(); })();