// ==UserScript==
// @name Text-to-Speech Reader
// @namespace http://tampermonkey.net/
// @version 1.3
// @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) {
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: 'tts-1',
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'
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);
}
} 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="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);
}
function saveConfig() {
const baseUrl = document.getElementById('baseUrl').value;
const apiKey = document.getElementById('apiKey').value;
const voice = document.getElementById('voice').value;
GM_setValue('baseUrl', baseUrl);
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';
}
GM_registerMenuCommand('Configure TTS Settings', openModal);
// Register menu command to clear cache
GM_registerMenuCommand('Clear TTS Cache', clearCache);
})();