@creativeorange/azure-text-to-speech 2.2.3 → 3.0.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/README.md +496 -0
- package/dist/co-azure-tts.es.js +425 -166
- package/dist/co-azure-tts.umd.js +8 -8
- package/package.json +21 -3
- package/src/SpeechToText.ts +106 -40
- package/src/TextToSpeech.ts +230 -129
- package/src/authentication.ts +228 -0
- package/src/main.ts +7 -0
- package/.eslintrc.js +0 -30
- package/index.html +0 -578
- package/nuxt/plugins/azure-speech-to-text.client.js +0 -27
- package/nuxt/plugins/azure-text-to-speech.client.js +0 -28
- package/tsconfig.json +0 -21
- package/vite.config.ts +0 -26
package/src/TextToSpeech.ts
CHANGED
|
@@ -5,9 +5,21 @@ import {
|
|
|
5
5
|
SpeechSynthesizer,
|
|
6
6
|
SpeechSynthesisOutputFormat,
|
|
7
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
|
+
};
|
|
8
21
|
|
|
9
22
|
export class TextToSpeech {
|
|
10
|
-
key: string;
|
|
11
23
|
region: string;
|
|
12
24
|
voice: string;
|
|
13
25
|
rate: number;
|
|
@@ -42,13 +54,19 @@ export class TextToSpeech {
|
|
|
42
54
|
activePrefetchedAudioUrl: string = '';
|
|
43
55
|
playbackSegments: any[] = [];
|
|
44
56
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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 ?? '';
|
|
52
70
|
}
|
|
53
71
|
|
|
54
72
|
async start() {
|
|
@@ -121,7 +139,7 @@ export class TextToSpeech {
|
|
|
121
139
|
if (referenceDiv.hasAttribute('co-tts.text') && referenceDiv.getAttribute('co-tts.text') !== '') {
|
|
122
140
|
this.textToRead = referenceDiv.getAttribute('co-tts.text') ?? '';
|
|
123
141
|
} else {
|
|
124
|
-
this.textToRead = referenceDiv.innerText;
|
|
142
|
+
this.textToRead = referenceDiv.innerText ?? referenceDiv.textContent ?? '';
|
|
125
143
|
}
|
|
126
144
|
|
|
127
145
|
if (referenceDiv.hasAttribute('co-tts.highlight')) {
|
|
@@ -137,7 +155,7 @@ export class TextToSpeech {
|
|
|
137
155
|
}
|
|
138
156
|
}
|
|
139
157
|
|
|
140
|
-
this.startSynthesizer(node, attr);
|
|
158
|
+
await this.startSynthesizer(node, attr);
|
|
141
159
|
});
|
|
142
160
|
}
|
|
143
161
|
|
|
@@ -152,7 +170,7 @@ export class TextToSpeech {
|
|
|
152
170
|
|
|
153
171
|
this.textToRead = await response.text();
|
|
154
172
|
|
|
155
|
-
this.startSynthesizer(node, attr);
|
|
173
|
+
await this.startSynthesizer(node, attr);
|
|
156
174
|
});
|
|
157
175
|
}
|
|
158
176
|
|
|
@@ -173,12 +191,12 @@ export class TextToSpeech {
|
|
|
173
191
|
}
|
|
174
192
|
}
|
|
175
193
|
if (attr.value === '') {
|
|
176
|
-
this.textToRead = node.innerText;
|
|
194
|
+
this.textToRead = node.innerText ?? node.textContent ?? '';
|
|
177
195
|
} else {
|
|
178
196
|
this.textToRead = attr.value;
|
|
179
197
|
}
|
|
180
198
|
|
|
181
|
-
this.startSynthesizer(node, attr);
|
|
199
|
+
await this.startSynthesizer(node, attr);
|
|
182
200
|
});
|
|
183
201
|
}
|
|
184
202
|
|
|
@@ -200,12 +218,12 @@ export class TextToSpeech {
|
|
|
200
218
|
}
|
|
201
219
|
}
|
|
202
220
|
if (attr.value === '') {
|
|
203
|
-
this.textToRead = node.innerText;
|
|
221
|
+
this.textToRead = node.innerText ?? node.textContent ?? '';
|
|
204
222
|
} else {
|
|
205
223
|
this.textToRead = attr.value;
|
|
206
224
|
}
|
|
207
225
|
|
|
208
|
-
this.startSynthesizer(node, attr);
|
|
226
|
+
await this.startSynthesizer(node, attr);
|
|
209
227
|
}
|
|
210
228
|
|
|
211
229
|
async handleStopModifier(node: any, attr: Attr) {
|
|
@@ -251,80 +269,105 @@ export class TextToSpeech {
|
|
|
251
269
|
URL.revokeObjectURL(this.activePrefetchedAudioUrl);
|
|
252
270
|
this.activePrefetchedAudioUrl = '';
|
|
253
271
|
}
|
|
272
|
+
this.closeSynthesizer();
|
|
254
273
|
this.player = undefined;
|
|
274
|
+
this.audioConfig = undefined;
|
|
275
|
+
this.speechConfig = undefined;
|
|
255
276
|
this.highlightDiv = undefined;
|
|
256
277
|
this.prevTextOffset = 0;
|
|
257
278
|
this.playbackTextOffsetBase = undefined;
|
|
258
279
|
}
|
|
259
280
|
|
|
260
|
-
async
|
|
261
|
-
|
|
281
|
+
private async createSpeechConfig(): Promise<SpeechConfig> {
|
|
282
|
+
const authorization =
|
|
283
|
+
await this.authorizationManager.getAuthorization();
|
|
262
284
|
|
|
263
|
-
|
|
264
|
-
|
|
285
|
+
const speechConfig = SpeechConfig.fromAuthorizationToken(
|
|
286
|
+
authorization.token,
|
|
287
|
+
authorization.region,
|
|
288
|
+
);
|
|
265
289
|
|
|
266
|
-
|
|
290
|
+
speechConfig.speechSynthesisVoiceName =
|
|
291
|
+
`Microsoft Server Speech Text to Speech Voice (${this.voice})`;
|
|
267
292
|
|
|
268
|
-
|
|
269
|
-
|
|
293
|
+
speechConfig.speechSynthesisOutputFormat =
|
|
294
|
+
SpeechSynthesisOutputFormat.Audio24Khz160KBitRateMonoMp3;
|
|
270
295
|
|
|
271
|
-
this.
|
|
272
|
-
this.wordBoundryList.push(e);
|
|
273
|
-
};
|
|
296
|
+
this.region = authorization.region;
|
|
274
297
|
|
|
275
|
-
|
|
276
|
-
|
|
298
|
+
return speechConfig;
|
|
299
|
+
}
|
|
277
300
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
this.playbackSegments = [];
|
|
282
|
-
}
|
|
301
|
+
async startSynthesizer(node: any, attr: Attr) {
|
|
302
|
+
try {
|
|
303
|
+
this.speechConfig = await this.createSpeechConfig();
|
|
283
304
|
|
|
284
|
-
|
|
285
|
-
const wasChainedPlayback = this.playbackSegments.length > 0;
|
|
286
|
-
this.stopPlayer();
|
|
305
|
+
this.player = new SpeakerAudioDestination();
|
|
287
306
|
|
|
288
|
-
|
|
289
|
-
|
|
307
|
+
this.audioConfig = AudioConfig.fromSpeakerOutput(this.player);
|
|
308
|
+
this.synthesizer = new SpeechSynthesizer(this.speechConfig, this.audioConfig);
|
|
290
309
|
|
|
291
|
-
|
|
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 = [];
|
|
292
321
|
}
|
|
293
322
|
|
|
294
|
-
|
|
295
|
-
const
|
|
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', {}));
|
|
296
329
|
|
|
297
|
-
if (nextNode && await this.playPrefetchedNode(nextNode)) {
|
|
298
330
|
return;
|
|
299
331
|
}
|
|
300
332
|
|
|
301
|
-
if (
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
nextNode.
|
|
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', {}));
|
|
305
347
|
}
|
|
306
|
-
}
|
|
307
|
-
document.dispatchEvent(new CustomEvent('COAzureTTSFinishedPlaying', {}));
|
|
308
|
-
}
|
|
309
|
-
};
|
|
348
|
+
};
|
|
310
349
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
350
|
+
this.player.onAudioStart = async () => {
|
|
351
|
+
document.dispatchEvent(new CustomEvent('COAzureTTSStartedPlaying', {}));
|
|
352
|
+
};
|
|
314
353
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
354
|
+
if (!isChainedPlayback) {
|
|
355
|
+
this.prefetchNextNode(this.clickedNode);
|
|
356
|
+
}
|
|
318
357
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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
|
+
}
|
|
328
371
|
}
|
|
329
372
|
|
|
330
373
|
collectPlaybackChain(node: any) {
|
|
@@ -456,7 +499,7 @@ export class TextToSpeech {
|
|
|
456
499
|
return node.getAttribute('co-tts.text') ?? '';
|
|
457
500
|
}
|
|
458
501
|
|
|
459
|
-
return node.innerText;
|
|
502
|
+
return node.innerText ?? node.textContent ?? '';
|
|
460
503
|
}
|
|
461
504
|
|
|
462
505
|
getPrefetchKey(node: any, text: string) {
|
|
@@ -505,46 +548,57 @@ export class TextToSpeech {
|
|
|
505
548
|
return;
|
|
506
549
|
}
|
|
507
550
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
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
|
+
});
|
|
546
594
|
});
|
|
547
|
-
|
|
595
|
+
} catch (error) {
|
|
596
|
+
this.closeResource(synthesizer);
|
|
597
|
+
synthesizer = undefined;
|
|
598
|
+
this.dispatchError(error);
|
|
599
|
+
return null;
|
|
600
|
+
}
|
|
601
|
+
})().finally(() => {
|
|
548
602
|
this.prefetchPromises.delete(key);
|
|
549
603
|
});
|
|
550
604
|
|
|
@@ -588,38 +642,53 @@ export class TextToSpeech {
|
|
|
588
642
|
|
|
589
643
|
await this.createInterval();
|
|
590
644
|
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
645
|
+
try {
|
|
646
|
+
const audio = new Audio(prefetch.url);
|
|
647
|
+
this.activePrefetchedAudioUrl = prefetch.url;
|
|
648
|
+
this.player = audio;
|
|
594
649
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
650
|
+
audio.addEventListener('play', () => {
|
|
651
|
+
document.dispatchEvent(new CustomEvent('COAzureTTSStartedPlaying', {}));
|
|
652
|
+
}, {once: true});
|
|
598
653
|
|
|
599
|
-
|
|
600
|
-
|
|
654
|
+
audio.addEventListener('ended', async () => {
|
|
655
|
+
this.stopPlayer();
|
|
601
656
|
|
|
602
|
-
|
|
603
|
-
|
|
657
|
+
if (this.clickedNode.hasAttribute('co-tts.next')) {
|
|
658
|
+
const nextNode = document.getElementById(this.clickedNode.getAttribute('co-tts.next'));
|
|
604
659
|
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
660
|
+
if (nextNode && await this.playPrefetchedNode(nextNode)) {
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
608
663
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
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', {}));
|
|
613
671
|
}
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
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();
|
|
621
689
|
|
|
622
|
-
|
|
690
|
+
return false;
|
|
691
|
+
}
|
|
623
692
|
}
|
|
624
693
|
|
|
625
694
|
async clearInterval() {
|
|
@@ -742,4 +811,36 @@ export class TextToSpeech {
|
|
|
742
811
|
|
|
743
812
|
return p.innerHTML;
|
|
744
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
|
+
}
|
|
745
846
|
}
|