// ==UserScript==
// @name Text-to-Speech Reader
// @namespace http://tampermonkey.net/
// @version 1.4
// @description Read selected text using OpenAI TTS API
// @author https://linux.do/u/snaily
// @match *://*/*
// @grant GM_xmlhttpRequest
// @grant GM_registerMenuCommand
// @grant GM_addStyle
// @grant GM_setValue
// @grant GM_getValue
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// Add a button to the page for reading selected text
const button = document.createElement('button');
button.innerText = 'Read Aloud';
button.style.position = 'absolute';
button.style.width = 'auto';
button.style.zIndex = '1000';
button.style.display = 'none'; // Initially hidden
button.style.backgroundColor = '#007BFF'; // Blue background
button.style.color = '#FFFFFF'; // White text
button.style.border = 'none';
button.style.borderRadius = '5px';
button.style.padding = '10px 20px';
button.style.boxShadow = '0 2px 5px rgba(0, 0, 0, 0.2)';
button.style.cursor = 'pointer';
button.style.fontSize = '14px';
button.style.fontFamily = 'Arial, sans-serif';
document.body.appendChild(button);
// Function to get selected text
function getSelectedText() {
let text = '';
if (window.getSelection) {
text = window.getSelection().toString();
} else if (document.selection && document.selection.type != 'Control') {
text = document.selection.createRange().text;
}
console.log('Selected Text:', text); // Debugging line
return text;
}
// Function to call OpenAI TTS API
function callOpenAITTS(text, baseUrl, apiKey, voice, model) {
const cachedAudioUrl = getCachedAudio(text);
if (cachedAudioUrl) {
console.log('Using cached audio');
playAudio(cachedAudioUrl);
resetButton();
return;
}
const url = `${baseUrl}/v1/audio/speech`;
console.log('Calling OpenAI TTS API with text:', text);
GM_xmlhttpRequest({
method: 'POST',
url: url,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`
},
data: JSON.stringify({
model: model,
input: text,
voice: voice
}),
responseType: 'arraybuffer',
onload: function(response) {
if (response.status === 200) {
console.log('API call successful'); // Debugging line
const audioBlob = new Blob([response.response], { type: 'audio/mpeg' });
const audioUrl = URL.createObjectURL(audioBlob);
playAudio(audioUrl);
cacheAudio(text, audioUrl);
} else {
console.error('Error:', response.statusText);
}
// Reset button after request is complete
resetButton();
},
onerror: function(error) {
console.error('Request failed', error);
// Reset button after request is complete
resetButton();
}
});
}
// Function to play audio
function playAudio(url) {
const audio = new Audio(url);
audio.play();
}
// Function to use browser's built-in TTS
function speakText(text) {
const utterance = new SpeechSynthesisUtterance(text);
speechSynthesis.speak(utterance);
}
// Function to set button to loading state
function setLoadingState() {
button.disabled = true;
button.innerText = 'Loading...';
button.style.backgroundColor = '#6c757d'; // Grey background
button.style.cursor = 'not-allowed';
}
// Function to reset button to original state
function resetButton() {
button.disabled = false;
button.innerText = 'Read Aloud';
button.style.backgroundColor = '#007BFF'; // Blue background
button.style.cursor = 'pointer';
}
// Helper function to get cached audio URL
function getCachedAudio(text) {
const cache = GM_getValue('cache', {});
const item = cache[text];
if (item) {
const now = new Date().getTime();
const weekInMillis = 7 * 24 * 60 * 60 * 1000; // One day in milliseconds
if (now - item.timestamp < weekInMillis) {
return item.audioUrl;
} else {
delete cache[text]; // Remove expired cache item
GM_setValue('cache', cache);
}
}
return null;
}
// Helper function to cache audio URL
function cacheAudio(text, audioUrl) {
const cache = GM_getValue('cache', {});
cache[text] = {
audioUrl: audioUrl,
timestamp: new Date().getTime()
};
GM_setValue('cache', cache);
}
// Function to clear cache
function clearCache() {
GM_setValue('cache', {});
alert('Cache cleared successfully.');
}
// Event listener for button click
button.addEventListener('click', () => {
const selectedText = getSelectedText();
if (selectedText) {
let apiKey = GM_getValue('apiKey', null);
let baseUrl = GM_getValue('baseUrl', null);
let voice = GM_getValue('voice', 'onyx'); // Default to 'onyx'
let model = GM_getValue('model', 'tts-1'); // Default to 'tts-1'
if (!baseUrl) {
alert('Please set the base URL for the TTS API in the Tampermonkey menu.');
return;
}
if (!apiKey) {
alert('Please set the API key for the TTS API in the Tampermonkey menu.');
return;
}
setLoadingState(); // Set button to loading state
if (window.location.hostname === 'github.com') {
speakText(selectedText);
resetButton(); // Reset button immediately for built-in TTS
}else {
callOpenAITTS(selectedText, baseUrl, apiKey, voice, model);
}
} else {
alert('Please select some text to read aloud.');
}
});
// Show the button near the selected text
document.addEventListener('mouseup', (event) => {
// Check if the mouseup event is triggered by the button itself
if (event.target === button) {
return;
}
const selectedText = getSelectedText();
if (selectedText) {
const mouseX = event.pageX;
const mouseY = event.pageY;
button.style.left = `${mouseX + 10}px`;
button.style.top = `${mouseY + 10}px`;
button.style.display = 'block';
} else {
button.style.display = 'none';
}
});
// Initialize UI components
function initModal() {
const modalHTML = `
<div id="configModal" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); display: none; justify-content: center; align-items: center; z-index: 10000;">
<div style="background: white; padding: 20px; border-radius: 10px; width: 300px;">
<h2>Configure TTS Settings</h2>
<label for="baseUrl">Base URL:</label>
<input type="text" id="baseUrl" value="${GM_getValue('baseUrl', 'https://api.openai.com')}" style="width: 100%;">
<label for="apiKey">API Key:</label>
<input type="text" id="apiKey" value="${GM_getValue('apiKey', '')}" style="width: 100%;">
<label for="model">Model:</label>
<select id="model" style="width: 100%;">
<option value="tts-1">tts-1</option>
<option value="tts-hailuo">tts-hailuo</option>
</select>
<label for="voice">Voice:</label>
<select id="voice" style="width: 100%;">
<option value="alloy">Alloy</option>
<option value="echo">Echo</option>
<option value="fable">Fable</option>
<option value="onyx">Onyx</option>
<option value="nova">Nova</option>
<option value="shimmer">Shimmer</option>
</select>
<button id="saveConfig" style="margin-top: 10px; width: 100%; padding: 10px; background-color: #007BFF; color: white; border: none; border-radius: 5px;">Save</button>
<button id="cancelConfig" style="margin-top: 10px; width: 100%; padding: 10px; background-color: grey; color: white; border: none; border-radius: 5px;">Cancel</button>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', modalHTML);
document.getElementById('saveConfig').addEventListener('click', saveConfig);
document.getElementById('cancelConfig').addEventListener('click', closeModal);
document.getElementById('model').addEventListener('change', updateVoiceOptions);
}
function updateVoiceOptions() {
// 获取select元素
var modelSelect = document.getElementById('model');
var voiceSelect = document.getElementById('voice');
if (modelSelect.value === 'tts-hailuo') {
// 清空voiceSelect
voiceSelect.innerHTML = `
<option value="male-botong">思远</option>
<option value="Podcast_girl">心悦</option>
<option value="boyan_new_hailuo">子轩</option>
<option value="female-shaonv">灵儿</option>
<option value="YaeMiko_hailuo">语嫣</option>
<option value="xiaoyi_mix_hailuo">少泽</option>
<option value="xiaomo_sft">芷溪</option>
<option value="cove_test2_hailuo">浩翔(英文)</option>
<option value="scarlett_hailuo">雅涵(英文)</option>
<option value="Leishen2_hailuo">雷电将军</option>
<option value="Zhongli_hailuo">钟离</option>
<option value="Paimeng_hailuo">派蒙</option>
<option value="keli_hailuo">可莉</option>
<option value="Hutao_hailuo">胡桃</option>
<option value="Xionger_hailuo">熊二</option>
<option value="Haimian_hailuo">海绵宝宝</option>
<option value="Robot_hunter_hailuo">变形金刚</option>
<option value="Linzhiling_hailuo">小玲玲</option>
<option value="huafei_hailuo">拽妃</option>
<option value="lingfeng_hailuo">东北er</option>
<option value="male_dongbei_hailuo">老铁</option>
<option value="Beijing_hailuo">北京er</option>
<option value="JayChou_hailuo">JayChou</option>
<option value="Daniel_hailuo">潇然</option>
<option value="Bingjiao_zongcai_hailuo">沉韵</option>
<option value="female-yaoyao-hd">瑶瑶</option>
<option value="murong_sft">晨曦</option>
<option value="shangshen_sft">沐珊</option>
<option value="kongchen_sft">祁辰</option>
<option value="shenteng2_hailuo">夏洛特</option>
<option value="Guodegang_hailuo">郭嘚嘚</option>
<option value="yueyue_hailuo">小月月</option>
`;
} else {
// 恢复默认选项
voiceSelect.innerHTML = `
<option value="alloy">Alloy</option>
<option value="echo">Echo</option>
<option value="fable">Fable</option>
<option value="onyx">Onyx</option>
<option value="nova">Nova</option>
<option value="shimmer">Shimmer</option>
`;
}
}
function saveConfig() {
const baseUrl = document.getElementById('baseUrl').value;
const model = document.getElementById('model').value;
const apiKey = document.getElementById('apiKey').value;
const voice = document.getElementById('voice').value;
GM_setValue('baseUrl', baseUrl);
GM_setValue('model', model);
GM_setValue('apiKey', apiKey);
GM_setValue('voice', voice);
alert('Settings saved successfully.');
closeModal();
}
function closeModal() {
document.getElementById('configModal').style.display = 'none';
}
function openModal() {
if (!document.getElementById('configModal')) {
initModal();
}
document.getElementById('configModal').style.display = 'flex';
// Set the current values from the cache
document.getElementById('baseUrl').value = GM_getValue('baseUrl', '');
document.getElementById('apiKey').value = GM_getValue('apiKey', '');
document.getElementById('model').value = GM_getValue('model', 'tts-1');
updateVoiceOptions(); // Ensure voice options are updated based on the model
document.getElementById('voice').value = GM_getValue('voice', 'onyx');
}
GM_registerMenuCommand('Configure TTS Settings', openModal);
// Register menu command to clear cache
GM_registerMenuCommand('Clear TTS Cache', clearCache);
})();