Youtube 双语字幕版

YouTube双语字幕,任何语言翻译成中文或用户选择的目标语言。支持YouTube翻译和谷歌翻译,自动切换。支持移动端和桌面端,适配Via浏览器。

  1. // ==UserScript==
  2. // @name Youtube 双语字幕版
  3. // @version 1.4.0
  4. // @author LR
  5. // @license MIT
  6. // @description YouTube双语字幕,任何语言翻译成中文或用户选择的目标语言。支持YouTube翻译和谷歌翻译,自动切换。支持移动端和桌面端,适配Via浏览器。
  7. // @match *://www.youtube.com/*
  8. // @match *://m.youtube.com/*
  9. // @require https://unpkg.com/ajax-hook@latest/dist/ajaxhook.min.js
  10. // @grant GM_registerMenuCommand
  11. // @run-at document-start
  12. // @namespace https://gf.qytechs.cn/users/1210499
  13. // @icon https://www.youtube.com/s/desktop/b9bfb983/img/favicon_32x32.png
  14. // ==/UserScript==
  15.  
  16. (function () {
  17. 'use strict';
  18.  
  19. // 设置
  20. const DEFAULT_LANG = 'zh';
  21. let TARGET_LANG = DEFAULT_LANG;
  22. const DEFAULT_TRANS_SERVICE = 'youtube';
  23. let TRANS_SERVICE = DEFAULT_TRANS_SERVICE;
  24. let LAST_FAILED_SERVICE = null; // 记录上次失败的服务
  25.  
  26. // 获取用户设置
  27. function getUserSettings() {
  28. return {
  29. lang: localStorage.getItem('dualSubTargetLang') || DEFAULT_LANG,
  30. service: localStorage.getItem('dualSubTransService') || DEFAULT_TRANS_SERVICE
  31. };
  32. }
  33.  
  34. // 保存用户设置
  35. function saveUserSettings(lang, service) {
  36. localStorage.setItem('dualSubTargetLang', lang);
  37. localStorage.setItem('dualSubTransService', service);
  38. TARGET_LANG = lang;
  39. TRANS_SERVICE = service;
  40. }
  41.  
  42. // 添加设置菜单
  43. function addSettingsMenu() {
  44. if (typeof GM_registerMenuCommand === 'function') {
  45. GM_registerMenuCommand('设置翻译语言', async () => {
  46. const userInput = prompt('请输入目标语言的ISO 639-1代码(例如:zh 中文, en 英文, ja 日语):', TARGET_LANG);
  47. if (userInput) {
  48. saveUserSettings(userInput.trim(), TRANS_SERVICE);
  49. alert(`翻译目标语言已设置为:${userInput.trim()}`);
  50. }
  51. });
  52.  
  53. GM_registerMenuCommand('选择翻译引擎', async () => {
  54. const userInput = prompt('请选择翻译引擎(输入数字):\n1. YouTube 翻译\n2. Google 翻译', TRANS_SERVICE === 'youtube' ? '1' : '2');
  55. if (userInput) {
  56. const service = userInput.trim() === '1' ? 'youtube' : 'google';
  57. saveUserSettings(TARGET_LANG, service);
  58. LAST_FAILED_SERVICE = null; // 重置失败记录
  59. alert(`翻译引擎已设置为:${service === 'youtube' ? 'YouTube 翻译' : 'Google 翻译'}`);
  60. }
  61. });
  62. }
  63. }
  64.  
  65. // 初始化设置
  66. const settings = getUserSettings();
  67. TARGET_LANG = settings.lang;
  68. TRANS_SERVICE = settings.service;
  69. addSettingsMenu();
  70.  
  71. // 谷歌翻译API
  72. async function googleTranslate(text) {
  73. try {
  74. const response = await fetch(`https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=${TARGET_LANG}&dt=t&q=${encodeURIComponent(text)}`);
  75. if (!response.ok) throw new Error('Google API请求失败');
  76. const data = await response.json();
  77. return data[0][0][0];
  78. } catch (error) {
  79. console.error('Google翻译失败:', error);
  80. return null; // 返回null表示翻译失败
  81. }
  82. }
  83.  
  84. async function enableDualSubtitles() {
  85. // 获取翻译后的字幕数据
  86. async function fetchTranslatedSubtitles(url, preferredService = TRANS_SERVICE) {
  87. // 如果上次失败的服务和当前首选服务相同,自动切换到另一个服务
  88. if (LAST_FAILED_SERVICE === preferredService) {
  89. preferredService = preferredService === 'youtube' ? 'google' : 'youtube';
  90. console.log(`上次${LAST_FAILED_SERVICE}翻译失败,自动切换到${preferredService}`);
  91. }
  92.  
  93. async function tryYouTubeTranslation() {
  94. const cleanUrl = url.replace(/(^|[&?])tlang=[^&]*/g, '') + `&tlang=${TARGET_LANG}&translate_h00ked`;
  95. try {
  96. const response = await fetch(cleanUrl, { method: 'GET' });
  97. if (!response.ok) throw new Error('YouTube翻译请求失败');
  98. const data = await response.json();
  99. // 验证返回的数据是否有效
  100. if (!data.events || data.events.length === 0) throw new Error('YouTube返回的字幕数据无效');
  101. return data;
  102. } catch (error) {
  103. console.error('YouTube翻译失败:', error);
  104. LAST_FAILED_SERVICE = 'youtube';
  105. return null;
  106. }
  107. }
  108.  
  109. async function tryGoogleTranslation() {
  110. try {
  111. const response = await fetch(url, { method: 'GET' });
  112. if (!response.ok) throw new Error('获取原字幕失败');
  113. const data = await response.json();
  114. const translatedData = JSON.parse(JSON.stringify(data));
  115.  
  116. // 批量收集需要翻译的文本
  117. const textToTranslate = translatedData.events
  118. .filter(event => event.segs)
  119. .map(event => ({
  120. text: event.segs.map(seg => seg.utf8).join('').trim(),
  121. event: event
  122. }))
  123. .filter(item => item.text);
  124.  
  125. // 批量翻译
  126. const results = await Promise.all(
  127. textToTranslate.map(async ({ text, event }) => {
  128. const translatedText = await googleTranslate(text);
  129. if (translatedText === null) throw new Error('Google翻译失败');
  130. return { event, translatedText };
  131. })
  132. );
  133.  
  134. // 更新翻译结果
  135. results.forEach(({ event, translatedText }) => {
  136. event.segs = [{ utf8: translatedText }];
  137. });
  138.  
  139. return translatedData;
  140. } catch (error) {
  141. console.error('谷歌翻译失败:', error);
  142. LAST_FAILED_SERVICE = 'google';
  143. return null;
  144. }
  145. }
  146.  
  147. // 尝试首选服务
  148. let result = null;
  149. if (preferredService === 'youtube') {
  150. result = await tryYouTubeTranslation();
  151. if (!result) {
  152. console.log('YouTube翻译失败,尝试使用谷歌翻译');
  153. result = await tryGoogleTranslation();
  154. }
  155. } else {
  156. result = await tryGoogleTranslation();
  157. if (!result) {
  158. console.log('谷歌翻译失败,尝试使用YouTube翻译');
  159. result = await tryYouTubeTranslation();
  160. }
  161. }
  162.  
  163. return result;
  164. }
  165.  
  166. // 编辑距离计算
  167. function levenshteinDistance(s1, s2) {
  168. if (s1.length === 0) return s2.length;
  169. if (s2.length === 0) return s1.length;
  170.  
  171. const matrix = Array.from({ length: s1.length + 1 }, (_, i) => Array(s2.length + 1).fill(0).map((_, j) => (i === 0 ? j : i)));
  172.  
  173. for (let i = 1; i <= s1.length; i++) {
  174. for (let j = 1; j <= s2.length; j++) {
  175. matrix[i][j] = (s1[i - 1] === s2[j - 1])
  176. ? matrix[i - 1][j - 1]
  177. : Math.min(
  178. matrix[i - 1][j - 1] + 1,
  179. matrix[i][j - 1] + 1,
  180. matrix[i - 1][j] + 1
  181. );
  182. }
  183. }
  184.  
  185. return matrix[s1.length][s2.length];
  186. }
  187.  
  188. // Jaccard相似度计算
  189. function jaccardSimilarity(str1, str2) {
  190. const set1 = new Set(str1.split(''));
  191. const set2 = new Set(str2.split(''));
  192. const intersection = [...set1].filter(x => set2.has(x)).length;
  193. const union = new Set([...set1, ...set2]).size;
  194. return intersection / union;
  195. }
  196.  
  197. // 相似度计算
  198. function calculateSimilarity(s1, s2) {
  199. const maxLength = Math.max(s1.length, s2.length);
  200. const levenshteinSimilarity = 1 - (levenshteinDistance(s1, s2) / maxLength);
  201. const jaccardSim = jaccardSimilarity(s1, s2);
  202. return (levenshteinSimilarity * 0.7) + (jaccardSim * 0.3);
  203. }
  204.  
  205. // 合并字幕
  206. function mergeSubtitles(defaultSubs, translatedSubs) {
  207. const mergedSubs = JSON.parse(JSON.stringify(defaultSubs));
  208. const translatedEvents = translatedSubs.events.filter(event => event.segs);
  209. const translatedMap = new Map(translatedEvents.map(event => [event.tStartMs, event]));
  210.  
  211. for (let i = 0; i < mergedSubs.events.length; i++) {
  212. const defaultEvent = mergedSubs.events[i];
  213. if (!defaultEvent.segs) continue;
  214.  
  215. const translatedEvent = [...translatedMap.keys()].reduce((closest, tStartMs) => {
  216. return (Math.abs(tStartMs - defaultEvent.tStartMs) < Math.abs(closest - defaultEvent.tStartMs)) ? tStartMs : closest;
  217. }, Infinity);
  218.  
  219. const eventToMerge = translatedMap.get(translatedEvent);
  220. if (eventToMerge) {
  221. const defaultText = defaultEvent.segs.map(seg => seg.utf8).join('').trim();
  222. const translatedText = eventToMerge.segs.map(seg => seg.utf8).join('').trim();
  223.  
  224. const timeOverlap = Math.min(defaultEvent.tStartMs + defaultEvent.dDurationMs, eventToMerge.tStartMs + eventToMerge.dDurationMs) - Math.max(defaultEvent.tStartMs, eventToMerge.tStartMs);
  225. if (timeOverlap > 0) {
  226. const similarity = calculateSimilarity(defaultText, translatedText);
  227. // 使用0.6容错率
  228. if (similarity < 0.6) {
  229. defaultEvent.segs = [{
  230. utf8: `${defaultText}\n${translatedText}`,
  231. tStartMs: defaultEvent.tStartMs,
  232. dDurationMs: defaultEvent.dDurationMs
  233. }];
  234. }
  235. }
  236. }
  237. }
  238.  
  239. return JSON.stringify(mergedSubs);
  240. }
  241.  
  242. // ajax-hook代理
  243. ah.proxy({
  244. onResponse: async (response, handler) => {
  245. if (response.config.url.includes('/api/timedtext') && !response.config.url.includes('&translate_h00ked')) {
  246. try {
  247. const defaultSubs = JSON.parse(response.response);
  248. const translatedSubs = await fetchTranslatedSubtitles(response.config.url);
  249. if (translatedSubs) {
  250. response.response = mergeSubtitles(defaultSubs, translatedSubs);
  251. }
  252. } catch (error) {
  253. console.error("处理字幕时出错:", error);
  254. }
  255. }
  256. handler.resolve(response);
  257. }
  258. });
  259. }
  260.  
  261. enableDualSubtitles();
  262. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址