// ==UserScript==
// @name 🤖ChatGPT 朗读助手
// @namespace http://tampermonkey.net/
// @version 1.0.1
// @description 在ChatGPT原生网页中添加朗读功能的脚本,可以让你听到聊天记录的文字内容。
// @author OpenAI - ChatGPT
// @match https://chat.openai.com/*
// @license GNU GPLv3
// ==/UserScript==
(function () {
'use strict';
const buttonStyles = {
shared: {
borderColor: 'rgba(86,88,105,var(--tw-border-opacity))',
fontSize: '.875rem',
lineHeight: '1.25rem',
padding: '0.5rem 0.75rem',
borderRadius: '0.25rem',
marginLeft: '0.25rem',
position: 'relative',
bottom: '5px',
},
light: {
backgroundColor: 'white',
color: 'black',
},
dark: {
backgroundColor: 'rgba(52,53,65,var(--tw-bg-opacity))',
color: 'rgba(217,217,227,var(--tw-text-opacity))',
},
};
const selectorStyles = {
shared: {
marginLeft: '0.25rem',
position: 'relative',
bottom: '5px',
fontSize: '.875rem',
padding: '0.1rem 0.5rem',
borderRadius: '0.25rem',
},
light: {
backgroundColor: 'white',
color: 'black',
},
dark: {
backgroundColor: 'rgba(52,53,65,var(--tw-bg-opacity))',
color: 'rgba(217,217,227,var(--tw-text-opacity))',
},
};
function isDarkMode() {
return document.documentElement.classList.contains('dark');
}
function createButton() {
const button = document.createElement('button');
button.innerHTML = '朗读';
button.title = '朗读';
button.setAttribute('data-chatgpt', '');
Object.assign(button.style, buttonStyles.shared, isDarkMode() ? buttonStyles.dark : buttonStyles.light);
return button;
}
function createLanguageSelector() {
const select = document.createElement('select');
select.setAttribute('data-chatgpt', '');
Object.assign(select.style, selectorStyles.shared, isDarkMode() ? selectorStyles.dark : selectorStyles.light);
const voices = window.speechSynthesis.getVoices().filter(voice => voice.name.includes('Microsoft'));
const voiceGroups = voices.reduce((groups, voice) => {
const language = voice.lang.split('-')[0];
if (!groups[language]) {
groups[language] = [];
}
groups[language].push(voice);
return groups;
}, {});
Object.keys(voiceGroups).forEach((language) => {
const optgroup = document.createElement('optgroup');
optgroup.label = language;
voiceGroups[language].forEach((voice) => {
const option = document.createElement('option');
option.value = voice.name;
option.text = voice.name;
optgroup.appendChild(option);
});
select.appendChild(optgroup);
});
select.value = 'Microsoft Xiaoxiao Online (Natural) - Chinese (Mainland)';
return select;
}
function addButtonAndSelector() {
const elements = document.querySelectorAll('.markdown.prose');
elements.forEach((elm) => {
if (elm.nextElementSibling?.nodeName ==='SELECT') return;
const button = createButton();
const languageSelector = createLanguageSelector();
button.addEventListener('mouseenter', () => {
button.style.backgroundColor = buttonStyles.hover.backgroundColor;
});
button.addEventListener('mouseleave', () => {
button.style.backgroundColor = buttonStyles.normal.backgroundColor;
});
button.addEventListener('click', () => {
if (button.classList.contains('playing')) {
window.speechSynthesis.cancel();
button.innerHTML = '朗读';
button.classList.remove('playing');
button.disabled = false;
return;
}
button.classList.add('playing');
button.innerHTML = '生成中请稍等...';
button.disabled = true;
const msg = new SpeechSynthesisUtterance(elm.textContent);
msg.rate = 0.825;
msg.addEventListener('boundary', (event) => {
const currentWord = elm.textContent.slice(event.charIndex, event.charIndex + event.charLength);
button.innerHTML = `朗读中: ${currentWord}`;
button.disabled = false;
});
msg.addEventListener('end', () => {
button.innerHTML = '朗读';
button.classList.remove('playing');
button.disabled = false;
});
msg.voice = speechSynthesis.getVoices().find(voice => voice.name === languageSelector.value);
msg.onerror = (errorEvent) => {
if (errorEvent.error === 'interrupted') {
return;
}
const errorMsg = `发生错误: ${errorEvent.error}`;
button.innerHTML = `发生错误: ${errorEvent.error}`;
button.classList.remove('playing');
button.disabled = false;
};
window.speechSynthesis.speak(msg);
});
elm.parentNode.insertBefore(languageSelector, elm.nextSibling);
elm.parentNode.insertBefore(button, languageSelector.nextSibling);
});
}
function updateButtonAndSelectorStyles() {
const buttons = document.querySelectorAll('button[data-chatgpt]');
const selectors = document.querySelectorAll('select[data-chatgpt]');
buttons.forEach((button) => {
Object.assign(button.style, buttonStyles.shared, isDarkMode() ? buttonStyles.dark : buttonStyles.light);
});
selectors.forEach((select) => {
Object.assign(select.style, selectorStyles.shared, isDarkMode() ? selectorStyles.dark : selectorStyles.light);
});
}
window.addEventListener('keydown', (event) => {
if (event.key === 'Escape') {
window.speechSynthesis.cancel();
const buttons = document.querySelectorAll('button.playing');
buttons.forEach((button) => {
button.innerHTML = '朗读';
button.classList.remove('playing');
});
}
});
window.speechSynthesis.onvoiceschanged = () => {
addButtonAndSelector();
};
setInterval(() => {
addButtonAndSelector();
}, 2000);
const darkModeObserver = new MutationObserver(updateButtonAndSelectorStyles);
darkModeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
})();