@creativeorange/azure-text-to-speech 2.1.1 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@creativeorange/azure-text-to-speech",
3
- "version": "2.1.1",
3
+ "version": "2.2.0",
4
4
  "main": "dist/co-azure-tts.umd.js",
5
5
  "browser": "dist/co-azure-tts.es.js",
6
6
  "scripts": {
@@ -36,6 +36,9 @@ export class TextToSpeech {
36
36
  wordBoundaryOffset: number = 0;
37
37
  prevTextOffset: number = 0;
38
38
  url: string = '';
39
+ prefetchedAudio: Map<string, any> = new Map();
40
+ prefetchPromises: Map<string, Promise<any>> = new Map();
41
+ activePrefetchedAudioUrl: string = '';
39
42
 
40
43
  constructor(key: string, region: string, voice: string, rate: number = 0, pitch: number = 0, url: string = '') {
41
44
  this.key = key;
@@ -52,18 +55,21 @@ export class TextToSpeech {
52
55
 
53
56
  setVoice(voice: string) {
54
57
  this.voice = voice;
58
+ this.clearPrefetchedAudio();
55
59
 
56
60
  return this;
57
61
  }
58
62
 
59
63
  setRate(rate: number) {
60
64
  this.rate = rate;
65
+ this.clearPrefetchedAudio();
61
66
 
62
67
  return this;
63
68
  }
64
69
 
65
70
  setPitch(pitch: number) {
66
71
  this.pitch = pitch;
72
+ this.clearPrefetchedAudio();
67
73
 
68
74
  return this;
69
75
  }
@@ -237,6 +243,10 @@ export class TextToSpeech {
237
243
  if (this.player !== undefined) {
238
244
  this.player.pause();
239
245
  }
246
+ if (this.activePrefetchedAudioUrl !== '') {
247
+ URL.revokeObjectURL(this.activePrefetchedAudioUrl);
248
+ this.activePrefetchedAudioUrl = '';
249
+ }
240
250
  this.player = undefined;
241
251
  this.highlightDiv = undefined;
242
252
  this.prevTextOffset = 0;
@@ -262,6 +272,11 @@ export class TextToSpeech {
262
272
 
263
273
  if (this.clickedNode.hasAttribute('co-tts.next')) {
264
274
  const nextNode = document.getElementById(this.clickedNode.getAttribute('co-tts.next'));
275
+
276
+ if (nextNode && await this.playPrefetchedNode(nextNode)) {
277
+ return;
278
+ }
279
+
265
280
  if (nextNode && nextNode.attributes.getNamedItem('co-tts.text')) {
266
281
  this.handleWithoutClick(nextNode, nextNode.attributes.getNamedItem('co-tts.text'));
267
282
  } else if (nextNode) {
@@ -276,6 +291,8 @@ export class TextToSpeech {
276
291
  document.dispatchEvent(new CustomEvent('COAzureTTSStartedPlaying', {}));
277
292
  };
278
293
 
294
+ this.prefetchNextNode(this.clickedNode);
295
+
279
296
  this.synthesizer.speakSsmlAsync(this.buildSSML(this.textToRead),
280
297
  () => {
281
298
  this.synthesizer.close();
@@ -287,6 +304,181 @@ export class TextToSpeech {
287
304
  });
288
305
  }
289
306
 
307
+ getNodeText(node: any, attr?: Attr | null) {
308
+ if (attr && attr.value !== '') {
309
+ return attr.value;
310
+ }
311
+
312
+ if (node.hasAttribute('co-tts.text') && node.getAttribute('co-tts.text') !== '') {
313
+ return node.getAttribute('co-tts.text') ?? '';
314
+ }
315
+
316
+ return node.innerText;
317
+ }
318
+
319
+ getPrefetchKey(node: any, text: string) {
320
+ return [
321
+ node.id,
322
+ text,
323
+ this.voice,
324
+ this.rate,
325
+ this.pitch,
326
+ this.url,
327
+ ].join('|');
328
+ }
329
+
330
+ clearPrefetchedAudio() {
331
+ this.prefetchedAudio.forEach((prefetch) => {
332
+ if (prefetch.url) {
333
+ URL.revokeObjectURL(prefetch.url);
334
+ }
335
+ });
336
+
337
+ this.prefetchedAudio.clear();
338
+ this.prefetchPromises.clear();
339
+ }
340
+
341
+ async prefetchNextNode(node: any) {
342
+ if (!node || !node.hasAttribute('co-tts.next')) {
343
+ return;
344
+ }
345
+
346
+ const nextNode = document.getElementById(node.getAttribute('co-tts.next'));
347
+
348
+ if (!nextNode) {
349
+ return;
350
+ }
351
+
352
+ const attr = nextNode.attributes.getNamedItem('co-tts.text') ?? nextNode.attributes.getNamedItem('co-tts');
353
+ const text = this.getNodeText(nextNode, attr);
354
+
355
+ if (text === '') {
356
+ return;
357
+ }
358
+
359
+ const key = this.getPrefetchKey(nextNode, text);
360
+
361
+ if (this.prefetchedAudio.has(key) || this.prefetchPromises.has(key)) {
362
+ return;
363
+ }
364
+
365
+ const speechConfig = SpeechConfig.fromSubscription(this.key, this.region);
366
+ speechConfig.speechSynthesisVoiceName = `Microsoft Server Speech Text to Speech Voice (${this.voice})`;
367
+ speechConfig.speechSynthesisOutputFormat = SpeechSynthesisOutputFormat.Audio24Khz160KBitRateMonoMp3;
368
+
369
+ const synthesizer = new SpeechSynthesizer(speechConfig, null);
370
+ const wordBoundryList: any[] = [];
371
+
372
+ synthesizer.wordBoundary = (s: any, e: any) => {
373
+ wordBoundryList.push(e);
374
+ };
375
+
376
+ const prefetchPromise = new Promise((resolve) => {
377
+ synthesizer.speakSsmlAsync(this.buildSSML(text),
378
+ (result: any) => {
379
+ synthesizer.close();
380
+
381
+ if (!result?.audioData) {
382
+ resolve(null);
383
+
384
+ return;
385
+ }
386
+
387
+ const blob = new Blob([result.audioData], {type: 'audio/mpeg'});
388
+ const url = URL.createObjectURL(blob);
389
+ const prefetch = {
390
+ key,
391
+ nodeId: nextNode.id,
392
+ text,
393
+ url,
394
+ wordBoundryList,
395
+ };
396
+
397
+ this.prefetchedAudio.set(key, prefetch);
398
+ resolve(prefetch);
399
+ },
400
+ () => {
401
+ synthesizer.close();
402
+ resolve(null);
403
+ });
404
+ }).finally(() => {
405
+ this.prefetchPromises.delete(key);
406
+ });
407
+
408
+ this.prefetchPromises.set(key, prefetchPromise);
409
+ }
410
+
411
+ async playPrefetchedNode(node: any) {
412
+ const attr = node.attributes.getNamedItem('co-tts.text') ?? node.attributes.getNamedItem('co-tts');
413
+ const text = this.getNodeText(node, attr);
414
+ const key = this.getPrefetchKey(node, text);
415
+ const prefetch = this.prefetchedAudio.get(key) ?? await this.prefetchPromises.get(key);
416
+
417
+ if (!prefetch) {
418
+ return false;
419
+ }
420
+
421
+ this.prefetchedAudio.delete(key);
422
+ this.clickedNode = node;
423
+ this.textToRead = prefetch.text;
424
+ this.wordBoundryList = prefetch.wordBoundryList;
425
+ this.wordEncounters = [];
426
+ this.previousWordBoundary = undefined;
427
+ this.prevTextOffset = 0;
428
+ this.currentWord = '';
429
+ this.currentOffset = 0;
430
+ this.wordBoundaryOffset = 0;
431
+
432
+ if (node.hasAttribute('co-tts.highlight')) {
433
+ if (node.attributes.getNamedItem('co-tts.highlight')?.value !== '') {
434
+ const newReferenceDiv = document.getElementById(node.attributes.getNamedItem('co-tts.highlight').value);
435
+
436
+ this.highlightDiv = newReferenceDiv;
437
+ if (newReferenceDiv !== null) {
438
+ this.originalHighlightDivInnerHTML = newReferenceDiv.innerHTML;
439
+ }
440
+ } else {
441
+ this.highlightDiv = node;
442
+ this.originalHighlightDivInnerHTML = node.innerHTML;
443
+ }
444
+ }
445
+
446
+ await this.createInterval();
447
+
448
+ const audio = new Audio(prefetch.url);
449
+ this.activePrefetchedAudioUrl = prefetch.url;
450
+ this.player = audio;
451
+
452
+ audio.addEventListener('play', () => {
453
+ document.dispatchEvent(new CustomEvent('COAzureTTSStartedPlaying', {}));
454
+ }, {once: true});
455
+
456
+ audio.addEventListener('ended', async () => {
457
+ this.stopPlayer();
458
+
459
+ if (this.clickedNode.hasAttribute('co-tts.next')) {
460
+ const nextNode = document.getElementById(this.clickedNode.getAttribute('co-tts.next'));
461
+
462
+ if (nextNode && await this.playPrefetchedNode(nextNode)) {
463
+ return;
464
+ }
465
+
466
+ if (nextNode && nextNode.attributes.getNamedItem('co-tts.text')) {
467
+ this.handleWithoutClick(nextNode, nextNode.attributes.getNamedItem('co-tts.text'));
468
+ } else if (nextNode) {
469
+ nextNode.dispatchEvent(new Event('click'));
470
+ }
471
+ } else {
472
+ document.dispatchEvent(new CustomEvent('COAzureTTSFinishedPlaying', {}));
473
+ }
474
+ }, {once: true});
475
+
476
+ this.prefetchNextNode(node);
477
+ await audio.play();
478
+
479
+ return true;
480
+ }
481
+
290
482
  async clearInterval() {
291
483
  clearInterval(this.interval);
292
484
  }