@creativeorange/azure-text-to-speech 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
package/package.json CHANGED
@@ -1,21 +1,23 @@
1
1
  {
2
2
  "name": "@creativeorange/azure-text-to-speech",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "main": "dist/co-azure-tts.umd.js",
5
5
  "browser": "dist/co-azure-tts.es.js",
6
6
  "scripts": {
7
- "serve-vite": "vite",
8
- "build-vite": "vite build --watch",
9
- "preview-vite": "vite preview"
7
+ "build": "vite build --watch"
10
8
  },
11
9
  "license": "MIT",
12
10
  "author": "Edsardio",
13
11
  "devDependencies": {
12
+ "@originjs/vite-plugin-commonjs": "^1.0.1",
13
+ "@typescript-eslint/eslint-plugin": "^5.33.0",
14
14
  "build-esm": "^4.2.2",
15
+ "eslint": "^8.21.0",
16
+ "eslint-config-google": "^0.14.0",
15
17
  "microsoft-cognitiveservices-speech-sdk": "^1.22.0",
16
18
  "typescript": "^4.7.4",
17
- "vite-plugin-env-compatible": "^1.1.1",
18
19
  "vite": "^2.7.2",
19
- "@originjs/vite-plugin-commonjs": "^1.0.1"
20
+ "vite-plugin-env-compatible": "^1.1.1",
21
+ "vite-plugin-eslint": "^1.7.0"
20
22
  }
21
- }
23
+ }
package/src/main.ts CHANGED
@@ -1,7 +1,12 @@
1
- import { SpeakerAudioDestination, AudioConfig, OutputFormat, Recognizer, SpeechConfig, SpeechRecognizer, SpeechSynthesizer } from 'microsoft-cognitiveservices-speech-sdk';
1
+ import {
2
+ SpeakerAudioDestination,
3
+ AudioConfig,
4
+ SpeechConfig,
5
+ SpeechSynthesizer,
6
+ SpeechSynthesisOutputFormat,
7
+ } from 'microsoft-cognitiveservices-speech-sdk';
2
8
 
3
9
  export default class TextToSpeech {
4
-
5
10
  key: string;
6
11
  region: string;
7
12
  voice: string;
@@ -20,55 +25,32 @@ export default class TextToSpeech {
20
25
 
21
26
  previousWordBoundary: any;
22
27
 
28
+ interval: any;
29
+
30
+ wordEncounters: number[] = [];
31
+ originalHighlightDivInnerHTML: string = '';
32
+ currentWord: string = '';
33
+ currentOffset: number = 0;
34
+
23
35
 
24
- constructor(key: string, region: string, voice: string = 'male') {
36
+ constructor(key: string, region: string, voice: string) {
25
37
  this.key = key;
26
38
  this.region = region;
27
39
  this.voice = voice;
28
40
  }
29
41
 
30
42
  async start() {
31
- setInterval(() => {
32
- if (this.player !== undefined && this.highlightDiv) {
33
- const currentTime = this.player.currentTime;
34
- let wordBoundary;
35
- for (const e of this.wordBoundryList) {
36
- if (currentTime * 1000 > e.audioOffset / 10000) {
37
- wordBoundary = e;
38
- } else {
39
- break;
40
- }
41
- }
42
- if (~['.', ',', '!', '?'].indexOf(wordBoundary.text)) {
43
- wordBoundary = this.previousWordBoundary ?? undefined;
44
- }
45
-
46
- if (wordBoundary !== undefined) {
47
- this.previousWordBoundary = wordBoundary;
48
- this.highlightDiv.innerHTML = this.textToRead.substr(0, wordBoundary.textOffset) +
49
- "<span class='co-tts-highlight'>" + wordBoundary.text + "</span>" +
50
- this.textToRead.substr(wordBoundary.textOffset + wordBoundary.wordLength);
51
- } else {
52
- this.highlightDiv.innerHTML = this.textToRead;
53
- }
54
- }
55
- }, 50);
56
-
57
43
  await this.registerBindings(document);
58
44
  }
59
45
 
60
- async synthesis() {
61
-
62
- }
63
-
64
46
  async registerBindings(node: any) {
65
- let nodes = node.childNodes;
47
+ const nodes = node.childNodes;
66
48
  for (let i = 0; i < nodes.length; i++) {
67
49
  if (!nodes[i]) {
68
50
  continue;
69
51
  }
70
52
 
71
- let currentNode = nodes[i];
53
+ const currentNode = nodes[i];
72
54
 
73
55
  if (currentNode.attributes) {
74
56
  if (currentNode.attributes.getNamedItem('co-tts.id')) {
@@ -95,8 +77,9 @@ export default class TextToSpeech {
95
77
  async handleIdModifier(node: any, attr: Attr) {
96
78
  node.addEventListener('click', async (_: any) => {
97
79
  this.stopPlayer();
80
+ await this.createInterval();
98
81
  this.clickedNode = node;
99
- let referenceDiv = document.getElementById(attr.value);
82
+ const referenceDiv = document.getElementById(attr.value);
100
83
 
101
84
  if (!referenceDiv) {
102
85
  return;
@@ -105,11 +88,12 @@ export default class TextToSpeech {
105
88
  if (referenceDiv.hasAttribute('co-tts.text') && referenceDiv.getAttribute('co-tts.text') !== '') {
106
89
  this.textToRead = referenceDiv.getAttribute('co-tts.text') ?? '';
107
90
  } else {
108
- this.textToRead = referenceDiv.innerText;
91
+ this.textToRead = referenceDiv.innerHTML;
109
92
  }
110
93
 
111
94
  if (referenceDiv.hasAttribute('co-tts.highlight')) {
112
95
  this.highlightDiv = referenceDiv;
96
+ this.originalHighlightDivInnerHTML = referenceDiv.innerHTML;
113
97
  }
114
98
 
115
99
  this.startSynthesizer(node, attr);
@@ -119,26 +103,29 @@ export default class TextToSpeech {
119
103
  async handleAjaxModifier(node: any, attr: Attr) {
120
104
  node.addEventListener('click', async (_: any) => {
121
105
  this.stopPlayer();
106
+ await this.createInterval();
122
107
  this.clickedNode = node;
123
- let response = await fetch(attr.value, {
108
+ const response = await fetch(attr.value, {
124
109
  method: `GET`,
125
110
  });
126
111
 
127
112
  this.textToRead = await response.text();
128
113
 
129
114
  this.startSynthesizer(node, attr);
130
- })
115
+ });
131
116
  }
132
117
 
133
118
  async handleDefault(node: any, attr: Attr) {
134
119
  node.addEventListener('click', async (_: any) => {
135
120
  this.stopPlayer();
121
+ await this.createInterval();
136
122
  this.clickedNode = node;
137
123
  if (node.hasAttribute('co-tts.highlight')) {
138
124
  this.highlightDiv = node;
125
+ this.originalHighlightDivInnerHTML = node.innerHTML;
139
126
  }
140
- if (attr.value === "") {
141
- this.textToRead = node.innerText;
127
+ if (attr.value === '') {
128
+ this.textToRead = node.innerHTML;
142
129
  } else {
143
130
  this.textToRead = attr.value;
144
131
  }
@@ -147,31 +134,57 @@ export default class TextToSpeech {
147
134
  });
148
135
  }
149
136
 
137
+ async handleWithoutClick(node: any, attr: Attr) {
138
+ this.stopPlayer();
139
+ await this.createInterval();
140
+ this.clickedNode = node;
141
+ if (node.hasAttribute('co-tts.highlight')) {
142
+ this.highlightDiv = node;
143
+ this.originalHighlightDivInnerHTML = node.innerHTML;
144
+ }
145
+ if (attr.value === '') {
146
+ this.textToRead = node.innerHTML;
147
+ } else {
148
+ this.textToRead = attr.value;
149
+ }
150
+
151
+ this.startSynthesizer(node, attr);
152
+ }
153
+
150
154
  async handleStopModifier(node: any, attr: Attr) {
151
155
  node.addEventListener('click', async (_: any) => {
152
156
  await this.stopPlayer();
157
+ document.dispatchEvent(new CustomEvent('COAzureTTSStoppedPlaying', {}));
153
158
  });
154
159
  }
155
160
 
156
161
  async handlePauseModifier(node: any, attr: Attr) {
157
162
  node.addEventListener('click', async (_: any) => {
163
+ await this.clearInterval();
158
164
  await this.player.pause();
165
+ document.dispatchEvent(new CustomEvent('COAzureTTSPausedPlaying', {}));
159
166
  });
160
167
  }
161
168
 
162
169
  async handleResumeModifier(node: any, attr: Attr) {
163
170
  node.addEventListener('click', async (_: any) => {
171
+ await this.createInterval();
164
172
  await this.player.resume();
173
+ document.dispatchEvent(new CustomEvent('COAzureTTSResumedPlaying', {}));
165
174
  });
166
175
  }
167
176
 
168
177
  async stopPlayer() {
178
+ await this.clearInterval();
169
179
  if (this.highlightDiv !== undefined) {
170
- this.highlightDiv.innerHTML = this.textToRead;
180
+ this.highlightDiv.innerHTML = this.originalHighlightDivInnerHTML;
171
181
  }
172
-
182
+
173
183
  this.textToRead = '';
184
+ this.currentWord = '';
185
+ this.originalHighlightDivInnerHTML = '';
174
186
  this.wordBoundryList = [];
187
+ this.wordEncounters = [];
175
188
  if (this.player !== undefined) {
176
189
  this.player.pause();
177
190
  }
@@ -179,15 +192,11 @@ export default class TextToSpeech {
179
192
  this.highlightDiv = undefined;
180
193
  }
181
194
 
182
- async startSynthesizer (node: any, attr: Attr) {
195
+ async startSynthesizer(node: any, attr: Attr) {
183
196
  this.speechConfig = SpeechConfig.fromSubscription(this.key, this.region);
184
197
 
185
- if (this.voice === 'female') {
186
- this.speechConfig.speechSynthesisVoiceName = 'Microsoft Server Speech Text to Speech Voice (nl-NL, ColetteNeural)';
187
- } else {
188
- this.speechConfig.speechSynthesisVoiceName = 'Microsoft Server Speech Text to Speech Voice (nl-NL, MaartenNeural)';
189
- }
190
- this.speechConfig.speechSynthesisOutputFormat = 8;
198
+ this.speechConfig.speechSynthesisVoiceName = `Microsoft Server Speech Text to Speech Voice (${this.voice})`;
199
+ this.speechConfig.speechSynthesisOutputFormat = SpeechSynthesisOutputFormat.Audio24Khz160KBitRateMonoMp3;
191
200
 
192
201
  this.player = new SpeakerAudioDestination();
193
202
 
@@ -198,17 +207,25 @@ export default class TextToSpeech {
198
207
  this.wordBoundryList.push(e);
199
208
  };
200
209
 
201
- this.player.onAudioEnd = () => {
210
+ this.player.onAudioEnd = async () => {
202
211
  this.stopPlayer();
203
-
212
+
204
213
  if (this.clickedNode.hasAttribute('co-tts.next')) {
205
- let nextNode = document.getElementById(this.clickedNode.getAttribute('co-tts.next'));
206
- if (nextNode) {
214
+ const nextNode = document.getElementById(this.clickedNode.getAttribute('co-tts.next'));
215
+ if (nextNode && nextNode.attributes.getNamedItem('co-tts.text')) {
216
+ this.handleWithoutClick(nextNode, nextNode.attributes.getNamedItem('co-tts.text'));
217
+ } else if (nextNode) {
207
218
  nextNode.dispatchEvent(new Event('click'));
208
219
  }
220
+ } else {
221
+ document.dispatchEvent(new CustomEvent('COAzureTTSFinishedPlaying', {}));
209
222
  }
210
223
  };
211
-
224
+
225
+ this.player.onAudioStart = async () => {
226
+ document.dispatchEvent(new CustomEvent('COAzureTTSStartedPlaying', {}));
227
+ };
228
+
212
229
  this.synthesizer.speakTextAsync(this.textToRead,
213
230
  () => {
214
231
  this.synthesizer.close();
@@ -220,4 +237,61 @@ export default class TextToSpeech {
220
237
  });
221
238
  }
222
239
 
223
- }
240
+ async clearInterval() {
241
+ clearInterval(this.interval);
242
+ }
243
+
244
+ async createInterval() {
245
+ this.interval = setInterval(() => {
246
+ if (this.player !== undefined && this.highlightDiv) {
247
+ const currentTime = this.player.currentTime;
248
+ let wordBoundary;
249
+ for (const e of this.wordBoundryList) {
250
+ if (currentTime * 1000 > e.audioOffset / 10000) {
251
+ wordBoundary = e;
252
+ } else {
253
+ break;
254
+ }
255
+ }
256
+
257
+ if (wordBoundary !== undefined) {
258
+ if (~['.', ',', '!', '?'].indexOf(wordBoundary.text)) {
259
+ wordBoundary = this.previousWordBoundary ?? undefined;
260
+ }
261
+
262
+ if (wordBoundary === undefined) {
263
+ this.highlightDiv.innerHTML = this.originalHighlightDivInnerHTML;
264
+ } else {
265
+ if (!this.wordEncounters[wordBoundary.text]) {
266
+ this.wordEncounters[wordBoundary.text] = 0;
267
+ }
268
+
269
+ if (this.currentWord !== wordBoundary.text) {
270
+ this.wordEncounters[wordBoundary.text]++;
271
+ console.log(this.wordEncounters);
272
+ this.currentOffset = this.getPosition(
273
+ this.originalHighlightDivInnerHTML,
274
+ wordBoundary.text,
275
+ this.wordEncounters[wordBoundary.text]
276
+ );
277
+ this.currentWord = wordBoundary.text;
278
+ }
279
+
280
+ this.previousWordBoundary = wordBoundary;
281
+ this.highlightDiv.innerHTML = this.originalHighlightDivInnerHTML.substring(0, this.currentOffset) +
282
+ '<mark class=\'co-tts-highlight\'>' + wordBoundary.text + '</mark>' +
283
+ this.originalHighlightDivInnerHTML.substring(this.currentOffset + wordBoundary.wordLength);
284
+ }
285
+ } else {
286
+ this.highlightDiv.innerHTML = this.originalHighlightDivInnerHTML;
287
+ }
288
+ }
289
+ }, 50);
290
+ }
291
+
292
+ getPosition(string: string, subString: string, index: number) {
293
+ const regex = new RegExp(`\\b${subString}\\b`, 'g');
294
+ console.log(string.split(regex, index).join(subString), regex, index);
295
+ return string.split(regex, index).join(subString).length;
296
+ }
297
+ }
package/test.xml ADDED
@@ -0,0 +1,9 @@
1
+ <speak version='1.0' xml:lang='nl-NL'>
2
+ <voice xml:lang='nl-NL' name='nl-NL-ColetteNeural'>
3
+ Tijdens deze opdracht
4
+ <lexeme>
5
+ <grapheme>Scotland MV</grapheme>
6
+ <alias>Scotland Media Wave</alias>
7
+ </lexeme>
8
+ </voice>
9
+ </speak>
package/vite.config.ts CHANGED
@@ -1,8 +1,14 @@
1
1
  import { defineConfig } from 'vite';
2
2
  import path from 'path';
3
+ import eslint from 'vite-plugin-eslint';
3
4
 
4
5
  // https://vitejs.dev/config/
5
6
  export default defineConfig({
7
+ plugins: [
8
+ eslint({
9
+ fix: true
10
+ })
11
+ ],
6
12
  build: {
7
13
  lib: {
8
14
  entry: './src/main.ts',