@creativeorange/azure-text-to-speech 3.0.0 → 3.0.2

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