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

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.2.0",
3
+ "version": "2.2.1",
4
4
  "main": "dist/co-azure-tts.umd.js",
5
5
  "browser": "dist/co-azure-tts.es.js",
6
6
  "scripts": {
@@ -39,6 +39,7 @@ export class TextToSpeech {
39
39
  prefetchedAudio: Map<string, any> = new Map();
40
40
  prefetchPromises: Map<string, Promise<any>> = new Map();
41
41
  activePrefetchedAudioUrl: string = '';
42
+ playbackSegments: any[] = [];
42
43
 
43
44
  constructor(key: string, region: string, voice: string, rate: number = 0, pitch: number = 0, url: string = '') {
44
45
  this.key = key;
@@ -240,6 +241,8 @@ export class TextToSpeech {
240
241
  this.originalHighlightDivInnerHTML = '';
241
242
  this.wordBoundryList = [];
242
243
  this.wordEncounters = [];
244
+ this.resetPlaybackSegments();
245
+ this.playbackSegments = [];
243
246
  if (this.player !== undefined) {
244
247
  this.player.pause();
245
248
  }
@@ -267,9 +270,25 @@ export class TextToSpeech {
267
270
  this.wordBoundryList.push(e);
268
271
  };
269
272
 
273
+ const playbackChain = this.collectPlaybackChain(this.clickedNode);
274
+ const isChainedPlayback = playbackChain.length > 1;
275
+
276
+ if (isChainedPlayback) {
277
+ this.preparePlaybackChain(playbackChain);
278
+ } else {
279
+ this.playbackSegments = [];
280
+ }
281
+
270
282
  this.player.onAudioEnd = async () => {
283
+ const wasChainedPlayback = this.playbackSegments.length > 0;
271
284
  this.stopPlayer();
272
285
 
286
+ if (wasChainedPlayback) {
287
+ document.dispatchEvent(new CustomEvent('COAzureTTSFinishedPlaying', {}));
288
+
289
+ return;
290
+ }
291
+
273
292
  if (this.clickedNode.hasAttribute('co-tts.next')) {
274
293
  const nextNode = document.getElementById(this.clickedNode.getAttribute('co-tts.next'));
275
294
 
@@ -291,7 +310,9 @@ export class TextToSpeech {
291
310
  document.dispatchEvent(new CustomEvent('COAzureTTSStartedPlaying', {}));
292
311
  };
293
312
 
294
- this.prefetchNextNode(this.clickedNode);
313
+ if (!isChainedPlayback) {
314
+ this.prefetchNextNode(this.clickedNode);
315
+ }
295
316
 
296
317
  this.synthesizer.speakSsmlAsync(this.buildSSML(this.textToRead),
297
318
  () => {
@@ -304,6 +325,120 @@ export class TextToSpeech {
304
325
  });
305
326
  }
306
327
 
328
+ collectPlaybackChain(node: any) {
329
+ const chain = [];
330
+ const seenNodeIds = new Set<string>();
331
+ let currentNode = node;
332
+ let offset = 0;
333
+
334
+ while (currentNode && !seenNodeIds.has(currentNode.id)) {
335
+ seenNodeIds.add(currentNode.id);
336
+
337
+ const attr = currentNode.attributes.getNamedItem('co-tts.text') ?? currentNode.attributes.getNamedItem('co-tts');
338
+ const text = this.getNodeText(currentNode, attr);
339
+
340
+ if (text === '') {
341
+ break;
342
+ }
343
+
344
+ const highlightDiv = this.getHighlightDivForNode(currentNode);
345
+
346
+ chain.push({
347
+ node: currentNode,
348
+ text,
349
+ start: offset,
350
+ end: offset + text.length,
351
+ highlightDiv,
352
+ originalHighlightDivInnerHTML: highlightDiv?.innerHTML ?? '',
353
+ });
354
+
355
+ offset += text.length + 2;
356
+
357
+ if (!currentNode.hasAttribute('co-tts.next')) {
358
+ break;
359
+ }
360
+
361
+ currentNode = document.getElementById(currentNode.getAttribute('co-tts.next'));
362
+ }
363
+
364
+ return chain;
365
+ }
366
+
367
+ preparePlaybackChain(chain: any[]) {
368
+ this.playbackSegments = chain;
369
+ this.textToRead = chain.map((segment) => segment.text).join('. ');
370
+ this.highlightDiv = undefined;
371
+ this.originalHighlightDivInnerHTML = '';
372
+ this.wordEncounters = [];
373
+ this.previousWordBoundary = undefined;
374
+ this.prevTextOffset = 0;
375
+ this.currentWord = '';
376
+ this.currentOffset = 0;
377
+ this.wordBoundaryOffset = 0;
378
+ }
379
+
380
+ getHighlightDivForNode(node: any) {
381
+ if (!node.hasAttribute('co-tts.highlight')) {
382
+ return undefined;
383
+ }
384
+
385
+ if (node.attributes.getNamedItem('co-tts.highlight')?.value !== '') {
386
+ return document.getElementById(node.attributes.getNamedItem('co-tts.highlight').value);
387
+ }
388
+
389
+ return node;
390
+ }
391
+
392
+ resetPlaybackSegments() {
393
+ this.playbackSegments.forEach((segment) => {
394
+ if (segment.highlightDiv) {
395
+ segment.highlightDiv.innerHTML = segment.originalHighlightDivInnerHTML;
396
+ }
397
+ });
398
+ }
399
+
400
+ updateChainedHighlight(wordBoundary: any) {
401
+ if (~['.', ',', '!', '?', '*', '(', ')', '&', '\\', '/', '^', '[', ']', '<', '>', ':'].indexOf(wordBoundary.text)) {
402
+ wordBoundary = this.previousWordBoundary ?? undefined;
403
+ }
404
+
405
+ if (!wordBoundary) {
406
+ this.resetPlaybackSegments();
407
+
408
+ return;
409
+ }
410
+
411
+ const segment = this.playbackSegments.find((candidate) => (
412
+ wordBoundary.textOffset >= candidate.start && wordBoundary.textOffset < candidate.end
413
+ ));
414
+
415
+ this.resetPlaybackSegments();
416
+
417
+ if (!segment?.highlightDiv) {
418
+ this.previousWordBoundary = wordBoundary;
419
+
420
+ return;
421
+ }
422
+
423
+ const relativeTextOffset = wordBoundary.textOffset - segment.start;
424
+ const currentOffset = this.getPosition(segment.originalHighlightDivInnerHTML, wordBoundary.text, relativeTextOffset);
425
+
426
+ if (currentOffset === Number.MAX_SAFE_INTEGER) {
427
+ this.previousWordBoundary = wordBoundary;
428
+
429
+ return;
430
+ }
431
+
432
+ const startOfString = segment.originalHighlightDivInnerHTML.substring(0, currentOffset);
433
+ const endOffset = currentOffset + wordBoundary.wordLength;
434
+ const endOfString = segment.originalHighlightDivInnerHTML.substring(endOffset);
435
+
436
+ segment.highlightDiv.innerHTML = `
437
+ ${startOfString}<mark class='co-tts-highlight'>${wordBoundary.text}</mark>${endOfString}
438
+ `;
439
+ this.previousWordBoundary = wordBoundary;
440
+ }
441
+
307
442
  getNodeText(node: any, attr?: Attr | null) {
308
443
  if (attr && attr.value !== '') {
309
444
  return attr.value;
@@ -497,6 +632,12 @@ export class TextToSpeech {
497
632
  }
498
633
 
499
634
  if (wordBoundary !== undefined) {
635
+ if (this.playbackSegments.length > 0) {
636
+ this.updateChainedHighlight(wordBoundary);
637
+
638
+ return;
639
+ }
640
+
500
641
  if (~['.', ',', '!', '?', '*', '(', ')', '&', '\\', '/', '^', '[', ']', '<', '>', ':']
501
642
  .indexOf(wordBoundary.text)) {
502
643
  wordBoundary = this.previousWordBoundary ?? undefined;