Youtube 双语字幕版

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

目前為 2024-12-29 提交的版本,檢視 最新版本

// ==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://gf.qytechs.cn/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();
})();

QingJ © 2025

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