Last active
June 25, 2023 14:46
-
-
Save jkp/3139dcd9256e72e899811f2d84f59aed to your computer and use it in GitHub Desktop.
Text-to-speech Bookmarklet
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(function() { | |
var script = document.createElement('script'); | |
script.setAttribute('src', 'https://bit.ly/426oCvn'); | |
script.onload = function() { | |
textToSpeech('YOURKEYHERE', getRandomVoice()); | |
}; | |
document.body.appendChild(script); | |
})(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
A quick and dirty bookmarklet that translates article content into an mp3 file using the Microsoft Azure text-to-speech APIs