Greasy Fork

Greasy Fork is available in English.

YouTube字幕文本转语音TTS(适用于沉浸式翻译)

将YouTube上的沉浸式翻译中文字幕转换为语音播放,支持更改音色和根据视频倍速调整语音速度

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

// ==UserScript==
// @name         YouTube字幕文本转语音TTS(适用于沉浸式翻译)
// @namespace    http://tampermonkey.net/
// @version      1.8
// @description  将YouTube上的沉浸式翻译中文字幕转换为语音播放,支持更改音色和根据视频倍速调整语音速度
// @author       Sean2333
// @match        https://www.youtube.com/*
// @grant        none
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    let lastCaptionText = '';
    const synth = window.speechSynthesis;
    let selectedVoice = null;
    let pendingText = null; // 存储等待朗读的文本
    let isWaitingToSpeak = false; // 是否正在等待朗读

    function loadVoices() {
        return new Promise(function(resolve) {
            let voices = synth.getVoices();
            if (voices.length !== 0) {
                resolve(voices);
            } else {
                synth.onvoiceschanged = function() {
                    voices = synth.getVoices();
                    resolve(voices);
                };
            }
        });
    }

    function selectVoice() {
        loadVoices().then(function(voices) {
            selectedVoice = voices.find(voice => voice.name === 'Microsoft Xiaoxiao Online (Natural) - Chinese (Mainland)');
            if (!selectedVoice) {
                selectedVoice = voices.find(voice => voice.lang === 'zh-CN');
            }
            console.log('已选择的语音:', selectedVoice ? selectedVoice.name : '未找到合适的语音');
        });
    }

    function speakText(text, isNewCaption = false) {
        const video = document.querySelector('video');

        // 如果有新字幕出现且当前正在朗读
        if (isNewCaption && synth.speaking) {
            console.log('新字幕出现,但当前语音未完成');
            pendingText = text; // 保存新字幕文本
            if (video && !video.paused) {
                video.pause(); // 暂停视频
                isWaitingToSpeak = true;
                console.log('视频已暂停,等待当前语音完成');
            }
            return;
        }

        console.log('准备朗读文本:', text);
        if (synth.speaking) {
            console.log('正在停止当前语音播放');
            synth.cancel();
        }

        if (text) {
            const utterance = new SpeechSynthesisUtterance(text);
            utterance.lang = 'zh-CN';

            if (selectedVoice) {
                utterance.voice = selectedVoice;
            }

            if (video) {
                utterance.rate = video.playbackRate;
                console.log('设置语音速率为:', utterance.rate);
            } else {
                utterance.rate = 1;
            }

            // 语音结束时的处理
            utterance.onend = () => {
                console.log('当前语音播放完成');

                // 如果有等待的文本,播放它
                if (pendingText) {
                    console.log('播放等待的文本');
                    const nextText = pendingText;
                    pendingText = null;
                    speakText(nextText);
                }
                // 如果视频是因为等待语音而暂停的,则恢复播放
                else if (isWaitingToSpeak && video && video.paused) {
                    isWaitingToSpeak = false;
                    video.play();
                    console.log('所有语音播放完成,视频继续播放');
                }
            };

            // 语音出错时的处理
            utterance.onerror = () => {
                console.error('语音播放出错');
                if (isWaitingToSpeak && video && video.paused) {
                    isWaitingToSpeak = false;
                    video.play();
                    console.log('语音播放出错,视频继续播放');
                }
                pendingText = null; // 清除等待的文本
            };

            synth.speak(utterance);
            console.log('开始朗读');
        } else {
            console.log('文本为空,跳过朗读');
        }
    }

    function getCaptionText() {
        const immersiveCaptionWindow = document.querySelector('#immersive-translate-caption-window');
        if (immersiveCaptionWindow && immersiveCaptionWindow.shadowRoot) {
            const captionContainer = immersiveCaptionWindow.shadowRoot.querySelector('div > div > div');
            if (captionContainer) {
                const targetCaptions = captionContainer.querySelectorAll('.target-cue');
                let captionText = '';
                targetCaptions.forEach(span => {
                    captionText += span.textContent + ' ';
                });
                captionText = captionText.trim();
                return captionText;
            }
        }
        return '';
    }

    function setupCaptionObserver() {
        function waitForCaptionContainer() {
            const immersiveCaptionWindow = document.querySelector('#immersive-translate-caption-window');
            if (immersiveCaptionWindow && immersiveCaptionWindow.shadowRoot) {
                const captionContainer = immersiveCaptionWindow.shadowRoot.querySelector('div > div > div');
                if (captionContainer) {
                    console.log('找到字幕容器,开始监听变化');

                    const observer = new MutationObserver((mutations) => {
                        if (mutations.some(mutation => mutation.type === 'childList')) {
                            const currentText = getCaptionText();
                            if (currentText && currentText !== lastCaptionText) {
                                lastCaptionText = currentText;
                                speakText(currentText, true); // 标记这是新字幕
                            }
                        }
                    });

                    const config = {
                        childList: true,
                        subtree: true
                    };

                    observer.observe(captionContainer, config);

                    const initialText = getCaptionText();
                    if (initialText) {
                        lastCaptionText = initialText;
                        speakText(initialText, true);
                    }
                } else {
                    setTimeout(waitForCaptionContainer, 1000);
                }
            } else {
                setTimeout(waitForCaptionContainer, 1000);
            }
        }

        waitForCaptionContainer();
    }

    window.addEventListener('load', function() {
        console.log('脚本已启动,等待视频播放');
        selectVoice();
        setupCaptionObserver();
    });

})();