├── 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 |
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 |
--------------------------------------------------------------------------------