Skip to content

Instantly share code, notes, and snippets.

@lac5
Last active June 19, 2024 00:39
Show Gist options
  • Save lac5/1f5705400d1a5d8fbf72270f289d8d12 to your computer and use it in GitHub Desktop.
Save lac5/1f5705400d1a5d8fbf72270f289d8d12 to your computer and use it in GitHub Desktop.
Automatically reads messages in a chatroom page. (i.e. Picarto, Twitch, Youtube, etc.)
// ==UserScript==
// @name Auto TTS
// @namespace larryc5
// @version 0.1
// @description Automatically reads messages in a chatroom page. (i.e. Picarto, Twitch, Youtube, etc.)
// @author Larry Costigan <[email protected]>
// @match https://picarto.tv/chatpopout/*/public
// @require https://code.jquery.com/jquery-latest.min.js
// @grant GM_setValue
// @grant GM_getValue
// @downloadURL https://gist.githubusercontent.com/larryc5/1f5705400d1a5d8fbf72270f289d8d12/raw/autotts.user.js
// @updateURL https://gist.githubusercontent.com/larryc5/1f5705400d1a5d8fbf72270f289d8d12/raw/autotts.meta.js
// ==/UserScript==
// ==UserScript==
// @name Auto TTS
// @namespace larryc5
// @version 0.1
// @description Automatically reads messages in a chatroom page. (i.e. Picarto, Twitch, Youtube, etc.)
// @author Larry Costigan <[email protected]>
// @match https://picarto.tv/chatpopout/*/public
// @require https://code.jquery.com/jquery-latest.min.js
// @grant GM_setValue
// @grant GM_getValue
// @downloadURL https://gist.githubusercontent.com/larryc5/1f5705400d1a5d8fbf72270f289d8d12/raw/autotts.user.js
// @updateURL https://gist.githubusercontent.com/larryc5/1f5705400d1a5d8fbf72270f289d8d12/raw/autotts.meta.js
// ==/UserScript==
(function(window) {
'use strict';
var $ = window.jQuery;
var console = window.console;
var document = window.document;
var location = window.location;
var Math = window.Math;
var MutationObserver = window.MutationObserver;
var navigator = window.navigator;
var Promise = window.Promise;
var speechSynthesis = window.speechSynthesis;
var SpeechSynthesisUtterance = window.SpeechSynthesisUtterance;
var String = window.String;
var p = Promise.resolve();
var $volume = $('<input title="volume" type="range" min="0" max="1" step="0.001" />')
.val(GM_getValue('volume', 1))
.on('change', function() {
var volume = $volume.val();
GM_setValue('volume', volume);
$volume.next().text((volume * 100).toFixed(1) + '%');
});
var $containerSelector = $('<input title="container selector" type="text">')
.val(GM_getValue('containerSelector.'+ location.host, '.messageli'))
.on('change', function() {
GM_setValue('containerSelector.'+ location.host, $containerSelector.val());
});
var $textSelector = $('<input title="text selector" type="text">')
.val(GM_getValue('textSelector.'+ location.host, '.theMsg'))
.on('change', function() {
GM_setValue('textSelector.'+ location.host, $textSelector.val());
});
var $speakerSelector = $('<input title="speaker selector" type="text">')
.val(GM_getValue('speakerSelector.'+ location.host, '.msgUsername'))
.on('change', function() {
GM_setValue('speakerSelector.'+ location.host, $speakerSelector.val());
});
function speak(text, speaker) {
text = String(text || '').trim().toLowerCase().replace(/\s+/g, ' ');
if (!text) return;
speaker = speaker ? speaker.trim().toLowerCase().replace(/\s+/g, ' ') : text;
var voices = speechSynthesis.getVoices();
var userLang = String(navigator.language || navigator.userLanguage || '').slice(2).toLowerCase();
voices = voices.filter(function(voice) {
return voice.lang.slice(2).toLowerCase() === userLang;
});
if (!(voices && voices.length > 0)) {
console.error('No voices found.');
return;
}
var speakerVal = speaker.split('').reduce(function(a, b) {
a ^= b.charCodeAt(0);
a ^= a << 13;
a ^= a >> 17;
a ^= a << 5;
return a;
}, 0) >>> 0;
var voice = voices[speakerVal % voices.length];
var utterance = new SpeechSynthesisUtterance(text);
utterance.voice = voice;
utterance.volume = $volume.val();
utterance.pitch = Math.sin(speakerVal*2*Math.sqrt(2)) + 1;
utterance.rate = Math.sin(speakerVal*3*Math.sqrt(3))/3 + 1;
console.log('%s: "%s" { val = %o, voice = %o, pitch = %o, rate = %o }',
speaker,
text,
speakerVal,
speakerVal % voices.length,
utterance.pitch,
utterance.rate
);
p = p.then(function() {
return new Promise(function(resolve, reject) {
utterance.onend = resolve;
utterance.onerror = reject;
speechSynthesis.speak(utterance);
});
}).catch(console.error);
}
function speakMsg(node) {
var container, text, speaker;
node = $(node);
if (node.is(':visible')) {
container = node.closest($containerSelector.val());
text = container.find($textSelector.val()).last().clone();
speaker = container.find($speakerSelector.val()).last();
text.find('img[alt]').each(function() {
this.parentNode.replaceChild(document.createTextNode(this.getAttribute('alt')), this);
});
speak(text.text(), speaker.text());
}
}
$(function() {
$('<div style="'+
'position: absolute;'+
'z-index: 100000;'+
'top: 0;'+
'left: 0;'+
'text-align: left;'+
'"></div>')
.append($('<button type="button" data-show-settings="0">TTS&gt;</button>')
.on('click', function() {
var $this = $(this);
var showSettings = !$this.data('show-settings');
$this.data('show-settings', showSettings);
if (showSettings) {
$this.nextAll().show();
$this.text('TTS<');
} else {
$this.nextAll().hide();
$this.text('TTS>');
}
}))
.append($('<span />')
.append($volume)
.append(
'<span style="'+
'background-color: #FFF;'+
'color: #000;'+
'font-family: monospace;'+
'">'+ ($volume.val() * 100).toFixed(1) +'%</span>')
.hide())
.append($containerSelector.hide())
.append($speakerSelector.hide())
.append($textSelector.hide())
.appendTo('body');
setTimeout(function() {
new MutationObserver(function(mutations) {
try {
for (var i = 0; i < mutations.length; i++) {
for (var j = 0; j < mutations[i].addedNodes.length; j++) {
speakMsg(mutations[i].addedNodes[j]);
}
}
} catch (e) {
console.error(e);
}
}).observe(document.body, {
childList: true,
subtree: true
});
speak('ready');
}, 3000);
});
})(window);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment