Greasy Fork

Greasy Fork is available in English.

Youtube 双语字幕版

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

当前为 2024-12-29 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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           http://greasyfork.icu/users/1210499
// @icon https://www.youtube.com/s/desktop/b9bfb983/img/favicon_32x32.png
// ==/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();
})();