Skip to content

Instantly share code, notes, and snippets.

@jkp
Last active June 25, 2023 14:46
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jkp/3139dcd9256e72e899811f2d84f59aed to your computer and use it in GitHub Desktop.
Save jkp/3139dcd9256e72e899811f2d84f59aed to your computer and use it in GitHub Desktop.
Text-to-speech Bookmarklet
(function() {
var script = document.createElement('script');
script.setAttribute('src', 'https://bit.ly/426oCvn');
script.onload = function() {
textToSpeech('YOURKEYHERE', getRandomVoice());
};
document.body.appendChild(script);
})();
const readabilityLibraryUrl = "https://tinyurl.com/yfdzwf96"
const speechLibraryUrl = 'https://tinyurl.com/44vzwvdc';
const loginUrl = 'https://uksouth.api.cognitive.microsoft.com/sts/v1.0/issuetoken';
const batchSize = 5;
function addScript(src) {
return new Promise(function(resolve, reject) {
var script = document.createElement('script');
script.setAttribute('src', src);
script.onload = resolve;
script.onerror = reject;
document.body.appendChild(script);
});
}
function getRandomVoice() {
const voices = [
'en-GB-AbbiNeural',
'en-GB-AlfieNeural',
'en-GB-BellaNeura',
'en-GB-ElliotNeural',
'en-GB-EthanNeural',
'en-GB-HollieNeural',
'en-GB-LibbyNeural',
'en-GB-MaisieNeural',
'en-GB-NoahNeural',
'en-GB-OliverNeural',
'en-GB-OliviaNeural',
'en-GB-RyanNeural1',
'en-GB-SoniaNeural1',
'en-GB-ThomasNeural'
];
const randomIndex = Math.floor(Math.random() * voices.length);
return voices[randomIndex];
}
function getAuthorizationToken(apiKey) {
return new Promise(function(resolve, reject) {
const request = new XMLHttpRequest();
request.open('POST', loginUrl);
request.setRequestHeader('Content-Type', 'application/json');
request.setRequestHeader('Ocp-Apim-Subscription-Key', apiKey)
request.onload = () => {
if (request.status == 200) {
resolve(request.responseText);
} else {
reject("File not Found");
}
}
request.send();
});
}
async function textToSpeech(apiKey, voiceName, text) {
const allResults = await Promise.all([
getAuthorizationToken(apiKey),
addScript(speechLibraryUrl),
addScript(readabilityLibraryUrl)
]);
class AudioCallback extends SpeechSDK.PushAudioOutputStreamCallback {
constructor(sourceBuffer) {
super();
this.buffersReceived = 0;
this.pendingBuffers = [];
this.sourceBuffer = sourceBuffer;
this.timer = null;
var that = this;
this.sourceBuffer.addEventListener('updateend', function (_) {
that.processPendingData();
});
}
write(dataBuffer) {
console.log('Received data');
this.pendingBuffers.push(dataBuffer);
this.processPendingData();
}
processPendingData() {
if (this.pendingBuffers.length && !this.sourceBuffer.updating) {
try {
this.sourceBuffer.appendBuffer(this.pendingBuffers[0]);
this.pendingBuffers.shift();
} catch (error) {
if (error instanceof DOMException && error.name === 'QuotaExceededError') {
//console.log('Buffers full: delaying processing');
if (!this.timer) {
this.timer = window.setTimeout(() => {
this.processPendingData();
this.timer = null;
}, 1000);
}
} else {
throw error;
}
}
}
}
}
const authorizationToken = allResults[0];
var text = text || readability.grabArticle().innerText.replace(/\.([A-Za-z])/g, ".\n\n$1");
const body = document.querySelector('body');
body.innerHTML = '';
var mediaSource = new MediaSource();
const objectURL = URL.createObjectURL(mediaSource);
var audioPlayer = document.createElement('audio');
audioPlayer.controls = true;
audioPlayer.src = objectURL;
body.appendChild(audioPlayer);
await new Promise(function(resolve, reject) {
function _sourceOpened() {
mediaSource.removeEventListener('sourceopen', _sourceOpened);
resolve();
}
mediaSource.addEventListener('sourceopen', _sourceOpened);
});
var sourceBuffer = mediaSource.addSourceBuffer('audio/mpeg');
var audioCallback = new AudioCallback(sourceBuffer);
var audioConfig = SpeechSDK.AudioConfig.fromStreamOutput(audioCallback);
var serviceRegion = "uksouth";
var speechConfig = SpeechSDK.SpeechConfig.fromAuthorizationToken(authorizationToken, serviceRegion);
speechConfig.speechSynthesisOutputFormat = SpeechSDK.SpeechSynthesisOutputFormat.Audio48Khz192KBitRateMonoMp3;
speechConfig.speechSynthesisVoiceName = voiceName;
var synthesizer = new SpeechSDK.SpeechSynthesizer(speechConfig, audioConfig);
//console.log(text);
audioPlayer.play();
paragraphs = text.split('\n').filter(e => e);
while (paragraphs.length) {
var text = "";
var noParas = Math.min(batchSize, paragraphs.length);
for (var i = 0; i < noParas; i++) {
text += paragraphs.shift() + '\n';
}
console.log(text);
await new Promise(function(resolve, reject) {
synthesizer.speakTextAsync(text, resolve, reject);
});
}
console.log('Synthesis finished');
mediaSource.endOfStream();
synthesizer.close();
}
@jkp
Copy link
Author

jkp commented Apr 9, 2021

A quick and dirty bookmarklet that translates article content into an mp3 file using the Microsoft Azure text-to-speech APIs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment