@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/dist/co-azure-tts.es.js +158 -0
- package/dist/co-azure-tts.umd.js +7 -7
- package/package.json +1 -1
- package/src/TextToSpeech.ts +192 -0
package/package.json
CHANGED
package/src/TextToSpeech.ts
CHANGED
|
@@ -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
|
}
|