├── README.txt ├── ext ├── icons │ ├── icon128.png │ ├── icon16.png │ ├── icon19.png │ └── icon48.png ├── js │ └── background.js ├── manifest.json └── src │ ├── inject │ └── injector.js │ └── options │ ├── index.html │ └── options.js └── slack-webspeech-snippet.js /README.txt: -------------------------------------------------------------------------------- 1 | Use javascript snippet: (updated 2020-06-02) 2 | 3 | Copy code from slack-webspeech-snippet.js into Chrome's javascript snippet and execute it on slack web page for some channel. Or put this in Tampermonkey userscript if you want recognition to start automatically. 4 | 5 | OR 6 | 7 | Use Chrome Extension: (obsolete, not updated) 8 | 9 | Open chrome://extensions/, check Developer mode, click Load unpacked extension..., select directory 'ext'. 10 | Click on extension's Options link if you want to change default settings. Go to Slack web page for channel or chat. (https://....slack.com/messages/...) Click on extension's button on the right of the address bar to toggle speech recognition and TTS (optional). 11 | 12 | Extension status is displayed on the right of channel title. 13 | 14 | TODO: chrome extension (create icons, publish to chrome web store) 15 | 16 | 17 | -------------------------------------------------------------------------------- /ext/icons/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crackleware/slack-webspeech/c8fd008606656f9af36e24bd5175254293d6240c/ext/icons/icon128.png -------------------------------------------------------------------------------- /ext/icons/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crackleware/slack-webspeech/c8fd008606656f9af36e24bd5175254293d6240c/ext/icons/icon16.png -------------------------------------------------------------------------------- /ext/icons/icon19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crackleware/slack-webspeech/c8fd008606656f9af36e24bd5175254293d6240c/ext/icons/icon19.png -------------------------------------------------------------------------------- /ext/icons/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crackleware/slack-webspeech/c8fd008606656f9af36e24bd5175254293d6240c/ext/icons/icon48.png -------------------------------------------------------------------------------- /ext/js/background.js: -------------------------------------------------------------------------------- 1 | 2 | // When the extension is installed or upgraded ... 3 | chrome.runtime.onInstalled.addListener(function() { 4 | // Replace all rules ... 5 | chrome.declarativeContent.onPageChanged.removeRules(undefined, function() { 6 | // With a new rule ... 7 | chrome.declarativeContent.onPageChanged.addRules([ 8 | { 9 | // That fires when a page's URL contains ... 10 | conditions: [ 11 | new chrome.declarativeContent.PageStateMatcher({ 12 | pageUrl: { urlContains: '.slack.com/messages/', schemes: ['https'] }, 13 | }) 14 | ], 15 | // And shows the extension's page action. 16 | actions: [ new chrome.declarativeContent.ShowPageAction() ] 17 | } 18 | ]); 19 | }); 20 | }); 21 | 22 | 23 | chrome.pageAction.onClicked.addListener(function (tab) { 24 | chrome.tabs.executeScript(null, {"file": "src/inject/injector.js"}); 25 | }); 26 | 27 | -------------------------------------------------------------------------------- /ext/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Slack-Webspeech", 3 | "version": "0.0.1", 4 | "manifest_version": 2, 5 | "description": "Send Slack messages by speaking. Browser speaks received messages using TTS.", 6 | "homepage_url": "https://github.com/crackleware/slack-webspeech", 7 | "icons": { 8 | "16": "icons/icon16.png", 9 | "48": "icons/icon48.png", 10 | "128": "icons/icon128.png" 11 | }, 12 | "background": { 13 | "scripts": ["js/background.js"], 14 | "persistent": false 15 | }, 16 | "options_page": "src/options/index.html", 17 | "page_action": { 18 | "default_icon": "icons/icon19.png", 19 | "default_title": "Toggle Slack Webspeech" 20 | }, 21 | "permissions": [ 22 | "activeTab", 23 | "declarativeContent", 24 | "storage" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /ext/src/inject/injector.js: -------------------------------------------------------------------------------- 1 | chrome.extension.sendMessage({}, function(response) { 2 | var readyStateCheckInterval = setInterval(function() { 3 | if (document.readyState === "complete") { 4 | clearInterval(readyStateCheckInterval); 5 | 6 | chrome.storage.sync.get({ 7 | lang: 'en-US', 8 | enableTTS: false, 9 | voiceName: 'Google US English', 10 | prependUsername: false 11 | }, function(items) { 12 | console.log('storage items:', items); 13 | var s = document.createElement('script'); 14 | s.textContent = '('+toggleSlackWebspeechInPage+')('+JSON.stringify(items)+')'; 15 | (document.head || document.documentElement).appendChild(s); 16 | }); 17 | } 18 | }, 10); 19 | }); 20 | 21 | 22 | function toggleSlackWebspeechInPage(opts) { 23 | function log() { 24 | var a = Array.prototype.slice.call(arguments, 0); 25 | console.log.apply(console, ['SlackWebspeech'].concat(a)); 26 | } 27 | 28 | var sws = window.slackWebspeech; 29 | if (sws) { 30 | log('stoping old'); 31 | if (sws.flushDelayInterval != undefined) { 32 | clearInterval(sws.flushDelayInterval); 33 | } 34 | $('#msgs_div')[0].removeEventListener("DOMNodeInserted", sws.onNodeInserted, false); 35 | 36 | if (sws.enableTTS) { 37 | sws.voiceSynth.cancel(); 38 | } 39 | 40 | sws.start_recognition = function () { }; 41 | sws.stop_recognition(); 42 | 43 | $('#slack-webspeech-status').remove(); 44 | 45 | delete window.slackWebspeech; 46 | 47 | return; 48 | } 49 | 50 | log('starting new', opts); 51 | sws = window.slackWebspeech = {}; 52 | 53 | sws.lang = opts.lang; 54 | sws.enableTTS = opts.enableTTS; 55 | sws.voiceName = opts.voiceName; 56 | sws.prependUsername = opts.prependUsername; 57 | 58 | sws.flushDelay = 2000; // ms 59 | 60 | 61 | $('.messages_header .channel_title').after( 62 | '
') 63 | $('#slack-webspeech-status pre').text('Slack Webspeech enabled'); 64 | 65 | sws.recognition = new webkitSpeechRecognition(); 66 | sws.recognition.continuous = true; 67 | sws.recognition.interimResults = true; 68 | if (sws.lang) sws.recognition.lang = sws.lang; 69 | 70 | sws.final_transcript = ''; 71 | 72 | sws.recognition_keep_running = true; 73 | 74 | sws.send_message = function() { 75 | log('send_message:', sws.final_transcript); 76 | $('#message-input')[0].value = sws.final_transcript; TS.view.submit(); // slack integration 77 | sws.final_transcript = ''; 78 | sws.update_status('', sws.final_transcript); 79 | } 80 | 81 | sws.update_status = function(interim_transcript, final_transcript) { 82 | $('#slack-webspeech-status pre').html( 83 | 'interim: ' + interim_transcript.trim() + '\n' + 84 | ' final: ' + final_transcript.trim() + ''); 85 | } 86 | 87 | sws.recognition.onstart = function() { 88 | log('webspeech onstart'); 89 | } 90 | sws.recognition.onresult = function(event) { 91 | //log('webspeech onresult', event); 92 | 93 | sws.lasttime = sws.time(); 94 | 95 | var interim_transcript = ''; 96 | 97 | for (var i = event.resultIndex; i < event.results.length; ++i) { 98 | if (event.results[i].isFinal) { 99 | sws.final_transcript += event.results[i][0].transcript; 100 | } else { 101 | interim_transcript += event.results[i][0].transcript; 102 | } 103 | } 104 | log('final_transcript:', sws.final_transcript); 105 | log('interim_transcript:', interim_transcript); 106 | 107 | sws.update_status(interim_transcript, sws.final_transcript); 108 | 109 | if (0) { 110 | var send = false; 111 | ['.', '!', '?'].forEach(function (c) { 112 | if (sws.final_transcript.endsWith(c)) { 113 | send = true; 114 | } 115 | }); 116 | if (send) sws.send_message(); 117 | } 118 | } 119 | sws.recognition.onerror = function(event) { 120 | log('webspeech onerror', event); 121 | } 122 | sws.recognition.onend = function() { 123 | log('webspeech onend'); 124 | if (sws.recognition_keep_running) { 125 | setTimeout(function () { sws.recognition.start(); }, 10); 126 | } 127 | } 128 | 129 | sws.time = function() { return (new Date()).getTime(); } 130 | sws.lasttime = sws.time(); 131 | 132 | if (sws.flushDelay != null) { 133 | sws.flushDelayInterval = setInterval(function () { 134 | var t = sws.time(); 135 | if (t - sws.lasttime > sws.flushDelay) { 136 | sws.lasttime = t; 137 | if (sws.final_transcript) sws.send_message(); 138 | } 139 | }, 200); 140 | } 141 | 142 | //setInterval(function () { console.log(sws.recognition_keep_running); }, 500); 143 | 144 | sws.start_recognition = function() { 145 | sws.recognition_keep_running = true; 146 | sws.recognition.start(); 147 | } 148 | 149 | sws.stop_recognition = function() { 150 | sws.recognition_keep_running = false; 151 | sws.recognition.stop(); 152 | } 153 | 154 | if (sws.enableTTS) { // TTS 155 | sws.voiceSynth = window.speechSynthesis; 156 | 157 | sws.voice = null; 158 | // voices needs some time to be ready 159 | sws.voiceSynth.onvoiceschanged = function() { 160 | if (sws.voice) return; 161 | var voices = ''; 162 | sws.voiceSynth.getVoices().forEach(function (v) { 163 | voices += v.name+' ('+v.lang+'), '; 164 | if (v.name == sws.voiceName) { 165 | sws.voice = v; 166 | } 167 | }); 168 | log('found voices:', voices); 169 | if (sws.voice) { 170 | log('selected voice:', [sws.voice.name, sws.voice.lang]); 171 | $('#slack-webspeech-status pre').text('Found TTS voice.'); 172 | } 173 | }; 174 | 175 | sws.speak_message = function(msg) { 176 | log('speak_message:', msg); 177 | var ut = new SpeechSynthesisUtterance(msg); 178 | ut.voice = sws.voice; 179 | if (sws.lang) ut.lang = sws.lang; 180 | sws.stop_recognition(); 181 | ut.onend = function() { 182 | sws.start_recognition(); 183 | }; 184 | sws.voiceSynth.speak(ut); 185 | } 186 | 187 | // find my slack username 188 | sws.my_slack_username = $('#team_header_user_name').text(); 189 | log('my_slack_username:', sws.my_slack_username); 190 | 191 | // monitor for new messages 192 | sws.onNodeInserted = function (ev) { 193 | if (ev.target.tagName == 'TS-MESSAGE') { 194 | //log(ev.target); 195 | var el = $(ev.target); 196 | var username = el.find('.message_content_header_left a').first().text(); 197 | //if (el.attr('data-member-id') != my_slack_id) { 198 | if (username != sws.my_slack_username) { 199 | var el_body = el.find('.message_body').first(); 200 | var text = ''; 201 | if (sws.prependUsername) text += username + 'says, '; 202 | text += el_body.text(); 203 | sws.speak_message(text); 204 | } 205 | } 206 | }; 207 | $('#msgs_div')[0].addEventListener("DOMNodeInserted", sws.onNodeInserted, false); 208 | } 209 | 210 | sws.start_recognition(); 211 | 212 | return; 213 | } 214 | 215 | 216 | -------------------------------------------------------------------------------- /ext/src/options/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Slack-Webspeech Options 5 | 8 | 9 | 10 | 11 |
12 |
Language:
13 |
Enable Text-To-Speech
14 |
Voice name:
15 |
Prepend sender's username for TTS (... says, ...)
16 |
17 | 18 |
19 | 20 |
21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /ext/src/options/options.js: -------------------------------------------------------------------------------- 1 | function save_options() { 2 | var lang = document.getElementById('lang').value; 3 | var enableTTS = document.getElementById('enableTTS').checked; 4 | var voiceName = document.getElementById('voiceName').value; 5 | var prependUsername = document.getElementById('prependUsername').checked; 6 | chrome.storage.sync.set({ 7 | lang: lang, 8 | enableTTS: enableTTS, 9 | voiceName: voiceName, 10 | prependUsername: prependUsername 11 | }, function() { 12 | var status = document.getElementById('status'); 13 | status.textContent = 'Options saved.'; 14 | setTimeout(function() { 15 | status.textContent = ''; 16 | }, 750); 17 | }); 18 | } 19 | 20 | function restore_options() { 21 | chrome.storage.sync.get({ 22 | lang: 'en-US', 23 | enableTTS: false, 24 | voiceName: 'Google US English', 25 | prependUsername: true 26 | }, function(items) { 27 | document.getElementById('lang').value = items.lang; 28 | document.getElementById('enableTTS').checked = items.enableTTS; 29 | document.getElementById('voiceName').value = items.voiceName; 30 | document.getElementById('prependUsername').value = items.prependUsername; 31 | }); 32 | } 33 | 34 | document.addEventListener('DOMContentLoaded', restore_options); 35 | document.getElementById('save').addEventListener('click', save_options); 36 | -------------------------------------------------------------------------------- /slack-webspeech-snippet.js: -------------------------------------------------------------------------------- 1 | slack_webspeech_lang = null; 2 | if (0) slack_webspeech_lang = 'sr-SP'; 3 | 4 | var recognition = new webkitSpeechRecognition(); 5 | recognition.continuous = true; 6 | recognition.interimResults = true; 7 | if (slack_webspeech_lang) recognition.lang = slack_webspeech_lang; 8 | 9 | final_transcript = ''; 10 | 11 | recognition_keep_running = true; 12 | 13 | function send_message() { 14 | console.log('send_message:', final_transcript); 15 | document.querySelector('.ql-editor').innerText = final_transcript; 16 | setTimeout(() => { 17 | document.querySelector('div.ql-buttons > button.c-texty_input__button--send').click(); 18 | }, 50); 19 | final_transcript = ''; 20 | } 21 | 22 | recognition.onstart = function() { 23 | console.log('webspeech onstart'); 24 | } 25 | recognition.onresult = function(event) { 26 | // console.log('webspeech onresult', event); 27 | 28 | var interim_transcript = ''; 29 | 30 | for (var i = event.resultIndex; i < event.results.length; ++i) { 31 | if (event.results[i].isFinal) { 32 | final_transcript += event.results[i][0].transcript; 33 | lasttime = time(); 34 | } else { 35 | interim_transcript += event.results[i][0].transcript; 36 | } 37 | } 38 | console.log('final_transcript:', final_transcript); 39 | console.log('interim_transcript:', interim_transcript); 40 | 41 | if (0) { 42 | var send = false; 43 | ['.', '!', '?'].forEach(function (c) { 44 | if (final_transcript.endsWith(c)) { 45 | send = true; 46 | } 47 | }); 48 | if (send) send_message(); 49 | } 50 | } 51 | recognition.onerror = function(event) { 52 | console.log('webspeech onerror', event); 53 | } 54 | recognition.onend = function() { 55 | console.log('webspeech onend'); 56 | if (recognition_keep_running) { 57 | setTimeout(function () { recognition.start(); }, 50); 58 | } 59 | } 60 | 61 | function time() { return (new Date()).getTime(); } 62 | lasttime = time(); 63 | 64 | if (1) { 65 | setInterval(function () { 66 | var t = time(); 67 | if (t - lasttime > 2000) { 68 | lasttime = t; 69 | if (final_transcript) send_message(); 70 | } 71 | }, 500); 72 | } 73 | 74 | function start_recognition() { 75 | recognition_keep_running = true; 76 | recognition.start(); 77 | } 78 | 79 | function stop_recognition() { 80 | recognition_keep_running = false; 81 | recognition.stop(); 82 | } 83 | 84 | if (1) { // TTS 85 | var voiceSynth = window.speechSynthesis; 86 | 87 | var googleVoice = null; 88 | // voices needs some time to be ready 89 | voiceSynth.getVoices().forEach(function (v) { 90 | console.log('searching voice:', [v.name, v.lang]); 91 | if (1 && slack_webspeech_lang == null && v.name == 'Google US English') { 92 | console.log('selecting voice:', [v.name, v.lang]); 93 | googleVoice = v; 94 | } 95 | }); 96 | 97 | function speak_message(msg) { 98 | console.log('speak_message:', msg); 99 | if (0) return; 100 | var ut = new SpeechSynthesisUtterance(msg); 101 | ut.voice = googleVoice; 102 | if (slack_webspeech_lang) ut.lang = slack_webspeech_lang; 103 | stop_recognition(); 104 | ut.onend = function() { 105 | start_recognition(); 106 | }; 107 | voiceSynth.speak(ut); 108 | } 109 | 110 | // monitor for new messages 111 | var last_el_id = null; 112 | var is_my_msg = true; 113 | document.querySelector(".c-message_list > div.c-scrollbar__hider > div > div").addEventListener("DOMNodeInserted", function (ev) { 114 | var el = ev.target; 115 | if (el && el.classList.contains('c-virtual_list__item')) { 116 | console.log(el); 117 | console.log(el.innerText); 118 | if (el.getAttribute('aria-expanded') == "false") { 119 | var id = el.getAttribute('id'); 120 | if (id.search('x') == -1) { 121 | var el_id = parseFloat(id); 122 | if (el_id == null || (el_id != NaN && (last_el_id == null || el_id > last_el_id))) { 123 | last_el_id = el_id; 124 | var el2 = el.querySelector("div.c-message_kit__gutter__right"); 125 | if (el2) { 126 | var el3 = el2.querySelector('.c-message__sender'); 127 | if (el3) { 128 | is_my_msg = el3.style.color == 'rgb(223, 61, 192)'; 129 | } 130 | console.log('is_my_msg:', is_my_msg); 131 | if (!is_my_msg) { 132 | speak_message(el2.innerText); 133 | } 134 | } 135 | } 136 | } 137 | } 138 | } 139 | }, false); 140 | } 141 | 142 | if (1) { start_recognition(); } 143 | 144 | true 145 | 146 | --------------------------------------------------------------------------------