@creativeorange/azure-text-to-speech 2.2.3 → 3.0.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.
@@ -1,745 +0,0 @@
1
- import {
2
- SpeakerAudioDestination,
3
- AudioConfig,
4
- SpeechConfig,
5
- SpeechSynthesizer,
6
- SpeechSynthesisOutputFormat,
7
- } from 'microsoft-cognitiveservices-speech-sdk';
8
-
9
- export class TextToSpeech {
10
- key: string;
11
- region: string;
12
- voice: string;
13
- rate: number;
14
- pitch: number;
15
-
16
- textToRead: string = '';
17
-
18
- wordBoundryList: any[] = [];
19
-
20
- clickedNode: any;
21
- highlightDiv: any;
22
-
23
- speechConfig: any;
24
- audioConfig: any;
25
- player: any;
26
- synthesizer: any;
27
-
28
- previousWordBoundary: any;
29
-
30
- interval: any;
31
-
32
- wordEncounters: number[] = [];
33
- originalHighlightDivInnerHTML: string = '';
34
- currentWord: string = '';
35
- currentOffset: number = 0;
36
- wordBoundaryOffset: number = 0;
37
- playbackTextOffsetBase: number | undefined;
38
- prevTextOffset: number = 0;
39
- url: string = '';
40
- prefetchedAudio: Map<string, any> = new Map();
41
- prefetchPromises: Map<string, Promise<any>> = new Map();
42
- activePrefetchedAudioUrl: string = '';
43
- playbackSegments: any[] = [];
44
-
45
- constructor(key: string, region: string, voice: string, rate: number = 0, pitch: number = 0, url: string = '') {
46
- this.key = key;
47
- this.region = region;
48
- this.voice = voice;
49
- this.rate = rate;
50
- this.pitch = pitch;
51
- this.url = url;
52
- }
53
-
54
- async start() {
55
- await this.registerBindings(document);
56
- }
57
-
58
- setVoice(voice: string) {
59
- this.voice = voice;
60
- this.clearPrefetchedAudio();
61
-
62
- return this;
63
- }
64
-
65
- setRate(rate: number) {
66
- this.rate = rate;
67
- this.clearPrefetchedAudio();
68
-
69
- return this;
70
- }
71
-
72
- setPitch(pitch: number) {
73
- this.pitch = pitch;
74
- this.clearPrefetchedAudio();
75
-
76
- return this;
77
- }
78
-
79
- async registerBindings(node: any) {
80
- const nodes = node.childNodes;
81
- for (let i = 0; i < nodes.length; i++) {
82
- if (!nodes[i]) {
83
- continue;
84
- }
85
-
86
- const currentNode = nodes[i];
87
-
88
- if (currentNode.attributes) {
89
- if (currentNode.attributes.getNamedItem('co-tts.id')) {
90
- await this.handleIdModifier(currentNode, currentNode.attributes.getNamedItem('co-tts.id'));
91
- } else if (currentNode.attributes.getNamedItem('co-tts.ajax')) {
92
- await this.handleAjaxModifier(currentNode, currentNode.attributes.getNamedItem('co-tts.ajax'));
93
- } else if (currentNode.attributes.getNamedItem('co-tts')) {
94
- await this.handleDefault(currentNode, currentNode.attributes.getNamedItem('co-tts'));
95
- } else if (currentNode.attributes.getNamedItem('co-tts.stop')) {
96
- await this.handleStopModifier(currentNode, currentNode.attributes.getNamedItem('co-tts.stop'));
97
- } else if (currentNode.attributes.getNamedItem('co-tts.resume')) {
98
- await this.handleResumeModifier(currentNode, currentNode.attributes.getNamedItem('co-tts.resume'));
99
- } else if (currentNode.attributes.getNamedItem('co-tts.pause')) {
100
- await this.handlePauseModifier(currentNode, currentNode.attributes.getNamedItem('co-tts.pause'));
101
- }
102
- }
103
-
104
- if (currentNode.childNodes.length > 0) {
105
- await this.registerBindings(currentNode);
106
- }
107
- }
108
- }
109
-
110
- async handleIdModifier(node: any, attr: Attr) {
111
- node.addEventListener('click', async (_: any) => {
112
- this.stopPlayer();
113
- await this.createInterval();
114
- const referenceDiv = document.getElementById(attr.value);
115
- this.clickedNode = referenceDiv;
116
-
117
- if (!referenceDiv) {
118
- return;
119
- }
120
-
121
- if (referenceDiv.hasAttribute('co-tts.text') && referenceDiv.getAttribute('co-tts.text') !== '') {
122
- this.textToRead = referenceDiv.getAttribute('co-tts.text') ?? '';
123
- } else {
124
- this.textToRead = referenceDiv.innerText;
125
- }
126
-
127
- if (referenceDiv.hasAttribute('co-tts.highlight')) {
128
- if (referenceDiv.attributes.getNamedItem('co-tts.highlight')?.value !== '') {
129
- const newReferenceDiv =
130
- document.getElementById(referenceDiv.attributes.getNamedItem('co-tts.highlight').value);
131
-
132
- this.highlightDiv = newReferenceDiv;
133
- this.originalHighlightDivInnerHTML = newReferenceDiv.innerHTML;
134
- } else {
135
- this.highlightDiv = referenceDiv;
136
- this.originalHighlightDivInnerHTML = referenceDiv.innerHTML;
137
- }
138
- }
139
-
140
- this.startSynthesizer(node, attr);
141
- });
142
- }
143
-
144
- async handleAjaxModifier(node: any, attr: Attr) {
145
- node.addEventListener('click', async (_: any) => {
146
- this.stopPlayer();
147
- await this.createInterval();
148
- this.clickedNode = node;
149
- const response = await fetch(attr.value, {
150
- method: `GET`,
151
- });
152
-
153
- this.textToRead = await response.text();
154
-
155
- this.startSynthesizer(node, attr);
156
- });
157
- }
158
-
159
- async handleDefault(node: any, attr: Attr) {
160
- node.addEventListener('click', async (_: any) => {
161
- this.stopPlayer();
162
- await this.createInterval();
163
- this.clickedNode = node;
164
- if (node.hasAttribute('co-tts.highlight')) {
165
- if (node.attributes.getNamedItem('co-tts.highlight')?.value !== '') {
166
- const newReferenceDiv = document.getElementById(node.attributes.getNamedItem('co-tts.highlight').value);
167
-
168
- this.highlightDiv = newReferenceDiv;
169
- this.originalHighlightDivInnerHTML = newReferenceDiv.innerHTML;
170
- } else {
171
- this.highlightDiv = node;
172
- this.originalHighlightDivInnerHTML = node.innerHTML;
173
- }
174
- }
175
- if (attr.value === '') {
176
- this.textToRead = node.innerText;
177
- } else {
178
- this.textToRead = attr.value;
179
- }
180
-
181
- this.startSynthesizer(node, attr);
182
- });
183
- }
184
-
185
- async handleWithoutClick(node: any, attr: Attr) {
186
- this.stopPlayer();
187
- await this.createInterval();
188
- this.clickedNode = node;
189
- if (node.hasAttribute('co-tts.highlight')) {
190
- if (node.attributes.getNamedItem('co-tts.highlight')?.value !== '') {
191
- const newReferenceDiv = document.getElementById(node.attributes.getNamedItem('co-tts.highlight').value);
192
-
193
- this.highlightDiv = newReferenceDiv;
194
- if (newReferenceDiv !== null) {
195
- this.originalHighlightDivInnerHTML = newReferenceDiv.innerHTML;
196
- }
197
- } else {
198
- this.highlightDiv = node;
199
- this.originalHighlightDivInnerHTML = node.innerHTML;
200
- }
201
- }
202
- if (attr.value === '') {
203
- this.textToRead = node.innerText;
204
- } else {
205
- this.textToRead = attr.value;
206
- }
207
-
208
- this.startSynthesizer(node, attr);
209
- }
210
-
211
- async handleStopModifier(node: any, attr: Attr) {
212
- node.addEventListener('click', async (_: any) => {
213
- await this.stopPlayer();
214
- document.dispatchEvent(new CustomEvent('COAzureTTSStoppedPlaying', {}));
215
- });
216
- }
217
-
218
- async handlePauseModifier(node: any, attr: Attr) {
219
- node.addEventListener('click', async (_: any) => {
220
- await this.clearInterval();
221
- await this.player.pause();
222
- document.dispatchEvent(new CustomEvent('COAzureTTSPausedPlaying', {}));
223
- });
224
- }
225
-
226
- async handleResumeModifier(node: any, attr: Attr) {
227
- node.addEventListener('click', async (_: any) => {
228
- await this.createInterval();
229
- await this.player.resume();
230
- document.dispatchEvent(new CustomEvent('COAzureTTSResumedPlaying', {}));
231
- });
232
- }
233
-
234
- async stopPlayer() {
235
- await this.clearInterval();
236
- if (this.highlightDiv !== undefined) {
237
- this.highlightDiv.innerHTML = this.originalHighlightDivInnerHTML;
238
- }
239
-
240
- this.textToRead = '';
241
- this.currentWord = '';
242
- this.originalHighlightDivInnerHTML = '';
243
- this.wordBoundryList = [];
244
- this.wordEncounters = [];
245
- this.resetPlaybackSegments();
246
- this.playbackSegments = [];
247
- if (this.player !== undefined) {
248
- this.player.pause();
249
- }
250
- if (this.activePrefetchedAudioUrl !== '') {
251
- URL.revokeObjectURL(this.activePrefetchedAudioUrl);
252
- this.activePrefetchedAudioUrl = '';
253
- }
254
- this.player = undefined;
255
- this.highlightDiv = undefined;
256
- this.prevTextOffset = 0;
257
- this.playbackTextOffsetBase = undefined;
258
- }
259
-
260
- async startSynthesizer(node: any, attr: Attr) {
261
- this.speechConfig = SpeechConfig.fromSubscription(this.key, this.region);
262
-
263
- this.speechConfig.speechSynthesisVoiceName = `Microsoft Server Speech Text to Speech Voice (${this.voice})`;
264
- this.speechConfig.speechSynthesisOutputFormat = SpeechSynthesisOutputFormat.Audio24Khz160KBitRateMonoMp3;
265
-
266
- this.player = new SpeakerAudioDestination();
267
-
268
- this.audioConfig = AudioConfig.fromSpeakerOutput(this.player);
269
- this.synthesizer = new SpeechSynthesizer(this.speechConfig, this.audioConfig);
270
-
271
- this.synthesizer.wordBoundary = (s: any, e: any) => {
272
- this.wordBoundryList.push(e);
273
- };
274
-
275
- const playbackChain = this.collectPlaybackChain(this.clickedNode);
276
- const isChainedPlayback = playbackChain.length > 1;
277
-
278
- if (isChainedPlayback) {
279
- this.preparePlaybackChain(playbackChain);
280
- } else {
281
- this.playbackSegments = [];
282
- }
283
-
284
- this.player.onAudioEnd = async () => {
285
- const wasChainedPlayback = this.playbackSegments.length > 0;
286
- this.stopPlayer();
287
-
288
- if (wasChainedPlayback) {
289
- document.dispatchEvent(new CustomEvent('COAzureTTSFinishedPlaying', {}));
290
-
291
- return;
292
- }
293
-
294
- if (this.clickedNode.hasAttribute('co-tts.next')) {
295
- const nextNode = document.getElementById(this.clickedNode.getAttribute('co-tts.next'));
296
-
297
- if (nextNode && await this.playPrefetchedNode(nextNode)) {
298
- return;
299
- }
300
-
301
- if (nextNode && nextNode.attributes.getNamedItem('co-tts.text')) {
302
- this.handleWithoutClick(nextNode, nextNode.attributes.getNamedItem('co-tts.text'));
303
- } else if (nextNode) {
304
- nextNode.dispatchEvent(new Event('click'));
305
- }
306
- } else {
307
- document.dispatchEvent(new CustomEvent('COAzureTTSFinishedPlaying', {}));
308
- }
309
- };
310
-
311
- this.player.onAudioStart = async () => {
312
- document.dispatchEvent(new CustomEvent('COAzureTTSStartedPlaying', {}));
313
- };
314
-
315
- if (!isChainedPlayback) {
316
- this.prefetchNextNode(this.clickedNode);
317
- }
318
-
319
- this.synthesizer.speakSsmlAsync(this.buildSSML(this.textToRead),
320
- () => {
321
- this.synthesizer.close();
322
- this.synthesizer = undefined;
323
- },
324
- () => {
325
- this.synthesizer.close();
326
- this.synthesizer = undefined;
327
- });
328
- }
329
-
330
- collectPlaybackChain(node: any) {
331
- const chain = [];
332
- const seenNodeIds = new Set<string>();
333
- let currentNode = node;
334
- let offset = 0;
335
-
336
- while (currentNode && !seenNodeIds.has(currentNode.id)) {
337
- seenNodeIds.add(currentNode.id);
338
-
339
- const attr = currentNode.attributes.getNamedItem('co-tts.text') ?? currentNode.attributes.getNamedItem('co-tts');
340
- const text = this.getNodeText(currentNode, attr);
341
-
342
- if (text === '') {
343
- break;
344
- }
345
-
346
- const highlightDiv = this.getHighlightDivForNode(currentNode);
347
-
348
- chain.push({
349
- node: currentNode,
350
- text,
351
- start: offset,
352
- end: offset + text.length,
353
- highlightDiv,
354
- originalHighlightDivInnerHTML: highlightDiv?.innerHTML ?? '',
355
- });
356
-
357
- offset += text.length + 1;
358
-
359
- if (!currentNode.hasAttribute('co-tts.next')) {
360
- break;
361
- }
362
-
363
- currentNode = document.getElementById(currentNode.getAttribute('co-tts.next'));
364
- }
365
-
366
- return chain;
367
- }
368
-
369
- preparePlaybackChain(chain: any[]) {
370
- this.playbackSegments = chain;
371
- this.textToRead = chain.map((segment) => segment.text).join(' ');
372
- this.highlightDiv = undefined;
373
- this.originalHighlightDivInnerHTML = '';
374
- this.wordEncounters = [];
375
- this.previousWordBoundary = undefined;
376
- this.prevTextOffset = 0;
377
- this.playbackTextOffsetBase = undefined;
378
- this.currentWord = '';
379
- this.currentOffset = 0;
380
- this.wordBoundaryOffset = 0;
381
- }
382
-
383
- getHighlightDivForNode(node: any) {
384
- if (!node.hasAttribute('co-tts.highlight')) {
385
- return undefined;
386
- }
387
-
388
- if (node.attributes.getNamedItem('co-tts.highlight')?.value !== '') {
389
- return document.getElementById(node.attributes.getNamedItem('co-tts.highlight').value);
390
- }
391
-
392
- return node;
393
- }
394
-
395
- resetPlaybackSegments() {
396
- this.playbackSegments.forEach((segment) => {
397
- if (segment.highlightDiv) {
398
- segment.highlightDiv.innerHTML = segment.originalHighlightDivInnerHTML;
399
- }
400
- });
401
- }
402
-
403
- updateChainedHighlight(wordBoundary: any) {
404
- if (~['.', ',', '!', '?', '*', '(', ')', '&', '\\', '/', '^', '[', ']', '<', '>', ':'].indexOf(wordBoundary.text)) {
405
- wordBoundary = this.previousWordBoundary ?? undefined;
406
- }
407
-
408
- if (!wordBoundary) {
409
- this.resetPlaybackSegments();
410
-
411
- return;
412
- }
413
-
414
- if (this.playbackTextOffsetBase === undefined) {
415
- this.playbackTextOffsetBase = wordBoundary.textOffset;
416
- }
417
-
418
- const normalizedTextOffset = Math.max(0, wordBoundary.textOffset - this.playbackTextOffsetBase);
419
- const segment = this.playbackSegments.find((candidate) => (
420
- normalizedTextOffset >= candidate.start && normalizedTextOffset < candidate.end
421
- ));
422
-
423
- this.resetPlaybackSegments();
424
-
425
- if (!segment?.highlightDiv) {
426
- this.previousWordBoundary = wordBoundary;
427
-
428
- return;
429
- }
430
-
431
- const relativeTextOffset = normalizedTextOffset - segment.start;
432
- const currentOffset = this.getPosition(segment.originalHighlightDivInnerHTML, wordBoundary.text, relativeTextOffset);
433
-
434
- if (currentOffset === Number.MAX_SAFE_INTEGER) {
435
- this.previousWordBoundary = wordBoundary;
436
-
437
- return;
438
- }
439
-
440
- const startOfString = segment.originalHighlightDivInnerHTML.substring(0, currentOffset);
441
- const endOffset = currentOffset + wordBoundary.wordLength;
442
- const endOfString = segment.originalHighlightDivInnerHTML.substring(endOffset);
443
-
444
- segment.highlightDiv.innerHTML = `
445
- ${startOfString}<mark class='co-tts-highlight'>${wordBoundary.text}</mark>${endOfString}
446
- `;
447
- this.previousWordBoundary = wordBoundary;
448
- }
449
-
450
- getNodeText(node: any, attr?: Attr | null) {
451
- if (attr && attr.value !== '') {
452
- return attr.value;
453
- }
454
-
455
- if (node.hasAttribute('co-tts.text') && node.getAttribute('co-tts.text') !== '') {
456
- return node.getAttribute('co-tts.text') ?? '';
457
- }
458
-
459
- return node.innerText;
460
- }
461
-
462
- getPrefetchKey(node: any, text: string) {
463
- return [
464
- node.id,
465
- text,
466
- this.voice,
467
- this.rate,
468
- this.pitch,
469
- this.url,
470
- ].join('|');
471
- }
472
-
473
- clearPrefetchedAudio() {
474
- this.prefetchedAudio.forEach((prefetch) => {
475
- if (prefetch.url) {
476
- URL.revokeObjectURL(prefetch.url);
477
- }
478
- });
479
-
480
- this.prefetchedAudio.clear();
481
- this.prefetchPromises.clear();
482
- }
483
-
484
- async prefetchNextNode(node: any) {
485
- if (!node || !node.hasAttribute('co-tts.next')) {
486
- return;
487
- }
488
-
489
- const nextNode = document.getElementById(node.getAttribute('co-tts.next'));
490
-
491
- if (!nextNode) {
492
- return;
493
- }
494
-
495
- const attr = nextNode.attributes.getNamedItem('co-tts.text') ?? nextNode.attributes.getNamedItem('co-tts');
496
- const text = this.getNodeText(nextNode, attr);
497
-
498
- if (text === '') {
499
- return;
500
- }
501
-
502
- const key = this.getPrefetchKey(nextNode, text);
503
-
504
- if (this.prefetchedAudio.has(key) || this.prefetchPromises.has(key)) {
505
- return;
506
- }
507
-
508
- const speechConfig = SpeechConfig.fromSubscription(this.key, this.region);
509
- speechConfig.speechSynthesisVoiceName = `Microsoft Server Speech Text to Speech Voice (${this.voice})`;
510
- speechConfig.speechSynthesisOutputFormat = SpeechSynthesisOutputFormat.Audio24Khz160KBitRateMonoMp3;
511
-
512
- const synthesizer = new SpeechSynthesizer(speechConfig, null);
513
- const wordBoundryList: any[] = [];
514
-
515
- synthesizer.wordBoundary = (s: any, e: any) => {
516
- wordBoundryList.push(e);
517
- };
518
-
519
- const prefetchPromise = new Promise((resolve) => {
520
- synthesizer.speakSsmlAsync(this.buildSSML(text),
521
- (result: any) => {
522
- synthesizer.close();
523
-
524
- if (!result?.audioData) {
525
- resolve(null);
526
-
527
- return;
528
- }
529
-
530
- const blob = new Blob([result.audioData], {type: 'audio/mpeg'});
531
- const url = URL.createObjectURL(blob);
532
- const prefetch = {
533
- key,
534
- nodeId: nextNode.id,
535
- text,
536
- url,
537
- wordBoundryList,
538
- };
539
-
540
- this.prefetchedAudio.set(key, prefetch);
541
- resolve(prefetch);
542
- },
543
- () => {
544
- synthesizer.close();
545
- resolve(null);
546
- });
547
- }).finally(() => {
548
- this.prefetchPromises.delete(key);
549
- });
550
-
551
- this.prefetchPromises.set(key, prefetchPromise);
552
- }
553
-
554
- async playPrefetchedNode(node: any) {
555
- const attr = node.attributes.getNamedItem('co-tts.text') ?? node.attributes.getNamedItem('co-tts');
556
- const text = this.getNodeText(node, attr);
557
- const key = this.getPrefetchKey(node, text);
558
- const prefetch = this.prefetchedAudio.get(key) ?? await this.prefetchPromises.get(key);
559
-
560
- if (!prefetch) {
561
- return false;
562
- }
563
-
564
- this.prefetchedAudio.delete(key);
565
- this.clickedNode = node;
566
- this.textToRead = prefetch.text;
567
- this.wordBoundryList = prefetch.wordBoundryList;
568
- this.wordEncounters = [];
569
- this.previousWordBoundary = undefined;
570
- this.prevTextOffset = 0;
571
- this.currentWord = '';
572
- this.currentOffset = 0;
573
- this.wordBoundaryOffset = 0;
574
-
575
- if (node.hasAttribute('co-tts.highlight')) {
576
- if (node.attributes.getNamedItem('co-tts.highlight')?.value !== '') {
577
- const newReferenceDiv = document.getElementById(node.attributes.getNamedItem('co-tts.highlight').value);
578
-
579
- this.highlightDiv = newReferenceDiv;
580
- if (newReferenceDiv !== null) {
581
- this.originalHighlightDivInnerHTML = newReferenceDiv.innerHTML;
582
- }
583
- } else {
584
- this.highlightDiv = node;
585
- this.originalHighlightDivInnerHTML = node.innerHTML;
586
- }
587
- }
588
-
589
- await this.createInterval();
590
-
591
- const audio = new Audio(prefetch.url);
592
- this.activePrefetchedAudioUrl = prefetch.url;
593
- this.player = audio;
594
-
595
- audio.addEventListener('play', () => {
596
- document.dispatchEvent(new CustomEvent('COAzureTTSStartedPlaying', {}));
597
- }, {once: true});
598
-
599
- audio.addEventListener('ended', async () => {
600
- this.stopPlayer();
601
-
602
- if (this.clickedNode.hasAttribute('co-tts.next')) {
603
- const nextNode = document.getElementById(this.clickedNode.getAttribute('co-tts.next'));
604
-
605
- if (nextNode && await this.playPrefetchedNode(nextNode)) {
606
- return;
607
- }
608
-
609
- if (nextNode && nextNode.attributes.getNamedItem('co-tts.text')) {
610
- this.handleWithoutClick(nextNode, nextNode.attributes.getNamedItem('co-tts.text'));
611
- } else if (nextNode) {
612
- nextNode.dispatchEvent(new Event('click'));
613
- }
614
- } else {
615
- document.dispatchEvent(new CustomEvent('COAzureTTSFinishedPlaying', {}));
616
- }
617
- }, {once: true});
618
-
619
- this.prefetchNextNode(node);
620
- await audio.play();
621
-
622
- return true;
623
- }
624
-
625
- async clearInterval() {
626
- clearInterval(this.interval);
627
- }
628
-
629
- async createInterval() {
630
- this.interval = setInterval(() => {
631
- if (this.player !== undefined && (this.highlightDiv || this.playbackSegments.length > 0)) {
632
- const currentTime = this.player.currentTime;
633
- let wordBoundary;
634
- for (const e of this.wordBoundryList) {
635
- if (currentTime * 1000 > e.audioOffset / 10000) {
636
- wordBoundary = e;
637
- } else {
638
- break;
639
- }
640
- }
641
-
642
- if (wordBoundary !== undefined) {
643
- if (this.playbackSegments.length > 0) {
644
- this.updateChainedHighlight(wordBoundary);
645
-
646
- return;
647
- }
648
-
649
- if (~['.', ',', '!', '?', '*', '(', ')', '&', '\\', '/', '^', '[', ']', '<', '>', ':']
650
- .indexOf(wordBoundary.text)) {
651
- wordBoundary = this.previousWordBoundary ?? undefined;
652
- }
653
-
654
- if (wordBoundary === undefined || this.prevTextOffset > wordBoundary.prevTextOffset) {
655
- this.highlightDiv.innerHTML = this.originalHighlightDivInnerHTML;
656
- } else {
657
- if (!this.wordEncounters[wordBoundary.text]) {
658
- this.wordEncounters[wordBoundary.text] = 0;
659
- }
660
- this.prevTextOffset = wordBoundary.prevTextOffset;
661
-
662
- if (this.currentWord !== wordBoundary.text || this.wordBoundaryOffset !== wordBoundary.textOffset) {
663
- this.currentOffset = this.getPosition(
664
- this.originalHighlightDivInnerHTML,
665
- wordBoundary.text,
666
- wordBoundary.textOffset
667
- );
668
- this.wordEncounters[wordBoundary.text] = this.currentOffset + wordBoundary.wordLength;
669
- this.currentWord = wordBoundary.text;
670
- this.wordBoundaryOffset = wordBoundary.textOffset;
671
- }
672
-
673
- if (this.currentOffset === Number.MAX_SAFE_INTEGER) {
674
- this.highlightDiv.innerHTML = this.originalHighlightDivInnerHTML;
675
- } else {
676
- this.previousWordBoundary = wordBoundary;
677
- const startOfString = this.originalHighlightDivInnerHTML.substring(0, this.currentOffset);
678
- const endOffset = this.currentOffset + wordBoundary.wordLength;
679
- const endOfString = this.originalHighlightDivInnerHTML.substring(endOffset);
680
- this.highlightDiv.innerHTML = `
681
- ${startOfString}<mark class='co-tts-highlight'>${wordBoundary.text}</mark>${endOfString}
682
- `;
683
- }
684
- }
685
- } else if (this.playbackSegments.length > 0) {
686
- this.resetPlaybackSegments();
687
- } else {
688
- this.highlightDiv.innerHTML = this.originalHighlightDivInnerHTML;
689
- }
690
- }
691
- }, 50);
692
- }
693
-
694
- getPosition(string: string, subString: string, textOffset: number) {
695
- let visibleOffset = 0;
696
- let insideTag = false;
697
-
698
- for (let htmlOffset = 0; htmlOffset < string.length; htmlOffset++) {
699
- const character = string[htmlOffset];
700
-
701
- if (character === '<') {
702
- insideTag = true;
703
- }
704
-
705
- if (!insideTag) {
706
- if (visibleOffset === textOffset) {
707
- return htmlOffset;
708
- }
709
-
710
- visibleOffset++;
711
- }
712
-
713
- if (character === '>') {
714
- insideTag = false;
715
- }
716
- }
717
-
718
- return string.indexOf(subString);
719
- }
720
-
721
- buildSSML(text: string) {
722
- let ssml = `<speak xmlns="http://www.w3.org/2001/10/synthesis"
723
- xmlns:mstts="http://www.w3.org/2001/mstts"
724
- xmlns:emo="http://www.w3.org/2009/10/emotionml"
725
- version="1.0"
726
- xml:lang="en-US">
727
- <voice name="${this.voice}">`;
728
-
729
- if (this.url !== '') {
730
- ssml += `<lexicon uri="${this.url}"/>`;
731
- }
732
-
733
- ssml += `<prosody rate="${this.rate}%" pitch="${this.pitch}%">
734
- ${this.convertHtmlEntities(text)}
735
- </prosody></voice></speak>`;
736
- return ssml;
737
- }
738
-
739
- convertHtmlEntities(input: string) {
740
- const p = document.createElement(`p`);
741
- p.textContent = input;
742
-
743
- return p.innerHTML;
744
- }
745
- }