@creativeorange/azure-text-to-speech 2.1.1 → 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.
@@ -8457,6 +8457,10 @@ class TextToSpeech {
8457
8457
  __publicField(this, "wordBoundaryOffset", 0);
8458
8458
  __publicField(this, "prevTextOffset", 0);
8459
8459
  __publicField(this, "url", "");
8460
+ __publicField(this, "prefetchedAudio", /* @__PURE__ */ new Map());
8461
+ __publicField(this, "prefetchPromises", /* @__PURE__ */ new Map());
8462
+ __publicField(this, "activePrefetchedAudioUrl", "");
8463
+ __publicField(this, "playbackSegments", []);
8460
8464
  this.key = key;
8461
8465
  this.region = region;
8462
8466
  this.voice = voice;
@@ -8469,14 +8473,17 @@ class TextToSpeech {
8469
8473
  }
8470
8474
  setVoice(voice) {
8471
8475
  this.voice = voice;
8476
+ this.clearPrefetchedAudio();
8472
8477
  return this;
8473
8478
  }
8474
8479
  setRate(rate) {
8475
8480
  this.rate = rate;
8481
+ this.clearPrefetchedAudio();
8476
8482
  return this;
8477
8483
  }
8478
8484
  setPitch(pitch) {
8479
8485
  this.pitch = pitch;
8486
+ this.clearPrefetchedAudio();
8480
8487
  return this;
8481
8488
  }
8482
8489
  async registerBindings(node) {
@@ -8624,9 +8631,15 @@ class TextToSpeech {
8624
8631
  this.originalHighlightDivInnerHTML = "";
8625
8632
  this.wordBoundryList = [];
8626
8633
  this.wordEncounters = [];
8634
+ this.resetPlaybackSegments();
8635
+ this.playbackSegments = [];
8627
8636
  if (this.player !== void 0) {
8628
8637
  this.player.pause();
8629
8638
  }
8639
+ if (this.activePrefetchedAudioUrl !== "") {
8640
+ URL.revokeObjectURL(this.activePrefetchedAudioUrl);
8641
+ this.activePrefetchedAudioUrl = "";
8642
+ }
8630
8643
  this.player = void 0;
8631
8644
  this.highlightDiv = void 0;
8632
8645
  this.prevTextOffset = 0;
@@ -8641,10 +8654,25 @@ class TextToSpeech {
8641
8654
  this.synthesizer.wordBoundary = (s, e) => {
8642
8655
  this.wordBoundryList.push(e);
8643
8656
  };
8657
+ const playbackChain = this.collectPlaybackChain(this.clickedNode);
8658
+ const isChainedPlayback = playbackChain.length > 1;
8659
+ if (isChainedPlayback) {
8660
+ this.preparePlaybackChain(playbackChain);
8661
+ } else {
8662
+ this.playbackSegments = [];
8663
+ }
8644
8664
  this.player.onAudioEnd = async () => {
8665
+ const wasChainedPlayback = this.playbackSegments.length > 0;
8645
8666
  this.stopPlayer();
8667
+ if (wasChainedPlayback) {
8668
+ document.dispatchEvent(new CustomEvent("COAzureTTSFinishedPlaying", {}));
8669
+ return;
8670
+ }
8646
8671
  if (this.clickedNode.hasAttribute("co-tts.next")) {
8647
8672
  const nextNode = document.getElementById(this.clickedNode.getAttribute("co-tts.next"));
8673
+ if (nextNode && await this.playPrefetchedNode(nextNode)) {
8674
+ return;
8675
+ }
8648
8676
  if (nextNode && nextNode.attributes.getNamedItem("co-tts.text")) {
8649
8677
  this.handleWithoutClick(nextNode, nextNode.attributes.getNamedItem("co-tts.text"));
8650
8678
  } else if (nextNode) {
@@ -8657,6 +8685,9 @@ class TextToSpeech {
8657
8685
  this.player.onAudioStart = async () => {
8658
8686
  document.dispatchEvent(new CustomEvent("COAzureTTSStartedPlaying", {}));
8659
8687
  };
8688
+ if (!isChainedPlayback) {
8689
+ this.prefetchNextNode(this.clickedNode);
8690
+ }
8660
8691
  this.synthesizer.speakSsmlAsync(
8661
8692
  this.buildSSML(this.textToRead),
8662
8693
  () => {
@@ -8669,6 +8700,238 @@ class TextToSpeech {
8669
8700
  }
8670
8701
  );
8671
8702
  }
8703
+ collectPlaybackChain(node) {
8704
+ var _a, _b;
8705
+ const chain = [];
8706
+ const seenNodeIds = /* @__PURE__ */ new Set();
8707
+ let currentNode = node;
8708
+ let offset = 0;
8709
+ while (currentNode && !seenNodeIds.has(currentNode.id)) {
8710
+ seenNodeIds.add(currentNode.id);
8711
+ const attr = (_a = currentNode.attributes.getNamedItem("co-tts.text")) != null ? _a : currentNode.attributes.getNamedItem("co-tts");
8712
+ const text = this.getNodeText(currentNode, attr);
8713
+ if (text === "") {
8714
+ break;
8715
+ }
8716
+ const highlightDiv = this.getHighlightDivForNode(currentNode);
8717
+ chain.push({
8718
+ node: currentNode,
8719
+ text,
8720
+ start: offset,
8721
+ end: offset + text.length,
8722
+ highlightDiv,
8723
+ originalHighlightDivInnerHTML: (_b = highlightDiv == null ? void 0 : highlightDiv.innerHTML) != null ? _b : ""
8724
+ });
8725
+ offset += text.length + 2;
8726
+ if (!currentNode.hasAttribute("co-tts.next")) {
8727
+ break;
8728
+ }
8729
+ currentNode = document.getElementById(currentNode.getAttribute("co-tts.next"));
8730
+ }
8731
+ return chain;
8732
+ }
8733
+ preparePlaybackChain(chain) {
8734
+ this.playbackSegments = chain;
8735
+ this.textToRead = chain.map((segment) => segment.text).join(". ");
8736
+ this.highlightDiv = void 0;
8737
+ this.originalHighlightDivInnerHTML = "";
8738
+ this.wordEncounters = [];
8739
+ this.previousWordBoundary = void 0;
8740
+ this.prevTextOffset = 0;
8741
+ this.currentWord = "";
8742
+ this.currentOffset = 0;
8743
+ this.wordBoundaryOffset = 0;
8744
+ }
8745
+ getHighlightDivForNode(node) {
8746
+ var _a;
8747
+ if (!node.hasAttribute("co-tts.highlight")) {
8748
+ return void 0;
8749
+ }
8750
+ if (((_a = node.attributes.getNamedItem("co-tts.highlight")) == null ? void 0 : _a.value) !== "") {
8751
+ return document.getElementById(node.attributes.getNamedItem("co-tts.highlight").value);
8752
+ }
8753
+ return node;
8754
+ }
8755
+ resetPlaybackSegments() {
8756
+ this.playbackSegments.forEach((segment) => {
8757
+ if (segment.highlightDiv) {
8758
+ segment.highlightDiv.innerHTML = segment.originalHighlightDivInnerHTML;
8759
+ }
8760
+ });
8761
+ }
8762
+ updateChainedHighlight(wordBoundary) {
8763
+ var _a;
8764
+ if (~[".", ",", "!", "?", "*", "(", ")", "&", "\\", "/", "^", "[", "]", "<", ">", ":"].indexOf(wordBoundary.text)) {
8765
+ wordBoundary = (_a = this.previousWordBoundary) != null ? _a : void 0;
8766
+ }
8767
+ if (!wordBoundary) {
8768
+ this.resetPlaybackSegments();
8769
+ return;
8770
+ }
8771
+ const segment = this.playbackSegments.find((candidate) => wordBoundary.textOffset >= candidate.start && wordBoundary.textOffset < candidate.end);
8772
+ this.resetPlaybackSegments();
8773
+ if (!(segment == null ? void 0 : segment.highlightDiv)) {
8774
+ this.previousWordBoundary = wordBoundary;
8775
+ return;
8776
+ }
8777
+ const relativeTextOffset = wordBoundary.textOffset - segment.start;
8778
+ const currentOffset = this.getPosition(segment.originalHighlightDivInnerHTML, wordBoundary.text, relativeTextOffset);
8779
+ if (currentOffset === Number.MAX_SAFE_INTEGER) {
8780
+ this.previousWordBoundary = wordBoundary;
8781
+ return;
8782
+ }
8783
+ const startOfString = segment.originalHighlightDivInnerHTML.substring(0, currentOffset);
8784
+ const endOffset = currentOffset + wordBoundary.wordLength;
8785
+ const endOfString = segment.originalHighlightDivInnerHTML.substring(endOffset);
8786
+ segment.highlightDiv.innerHTML = `
8787
+ ${startOfString}<mark class='co-tts-highlight'>${wordBoundary.text}</mark>${endOfString}
8788
+ `;
8789
+ this.previousWordBoundary = wordBoundary;
8790
+ }
8791
+ getNodeText(node, attr) {
8792
+ var _a;
8793
+ if (attr && attr.value !== "") {
8794
+ return attr.value;
8795
+ }
8796
+ if (node.hasAttribute("co-tts.text") && node.getAttribute("co-tts.text") !== "") {
8797
+ return (_a = node.getAttribute("co-tts.text")) != null ? _a : "";
8798
+ }
8799
+ return node.innerText;
8800
+ }
8801
+ getPrefetchKey(node, text) {
8802
+ return [
8803
+ node.id,
8804
+ text,
8805
+ this.voice,
8806
+ this.rate,
8807
+ this.pitch,
8808
+ this.url
8809
+ ].join("|");
8810
+ }
8811
+ clearPrefetchedAudio() {
8812
+ this.prefetchedAudio.forEach((prefetch) => {
8813
+ if (prefetch.url) {
8814
+ URL.revokeObjectURL(prefetch.url);
8815
+ }
8816
+ });
8817
+ this.prefetchedAudio.clear();
8818
+ this.prefetchPromises.clear();
8819
+ }
8820
+ async prefetchNextNode(node) {
8821
+ var _a;
8822
+ if (!node || !node.hasAttribute("co-tts.next")) {
8823
+ return;
8824
+ }
8825
+ const nextNode = document.getElementById(node.getAttribute("co-tts.next"));
8826
+ if (!nextNode) {
8827
+ return;
8828
+ }
8829
+ const attr = (_a = nextNode.attributes.getNamedItem("co-tts.text")) != null ? _a : nextNode.attributes.getNamedItem("co-tts");
8830
+ const text = this.getNodeText(nextNode, attr);
8831
+ if (text === "") {
8832
+ return;
8833
+ }
8834
+ const key = this.getPrefetchKey(nextNode, text);
8835
+ if (this.prefetchedAudio.has(key) || this.prefetchPromises.has(key)) {
8836
+ return;
8837
+ }
8838
+ const speechConfig = SpeechConfig.fromSubscription(this.key, this.region);
8839
+ speechConfig.speechSynthesisVoiceName = `Microsoft Server Speech Text to Speech Voice (${this.voice})`;
8840
+ speechConfig.speechSynthesisOutputFormat = SpeechSynthesisOutputFormat.Audio24Khz160KBitRateMonoMp3;
8841
+ const synthesizer = new SpeechSynthesizer(speechConfig, null);
8842
+ const wordBoundryList = [];
8843
+ synthesizer.wordBoundary = (s, e) => {
8844
+ wordBoundryList.push(e);
8845
+ };
8846
+ const prefetchPromise = new Promise((resolve) => {
8847
+ synthesizer.speakSsmlAsync(
8848
+ this.buildSSML(text),
8849
+ (result) => {
8850
+ synthesizer.close();
8851
+ if (!(result == null ? void 0 : result.audioData)) {
8852
+ resolve(null);
8853
+ return;
8854
+ }
8855
+ const blob = new Blob([result.audioData], { type: "audio/mpeg" });
8856
+ const url = URL.createObjectURL(blob);
8857
+ const prefetch = {
8858
+ key,
8859
+ nodeId: nextNode.id,
8860
+ text,
8861
+ url,
8862
+ wordBoundryList
8863
+ };
8864
+ this.prefetchedAudio.set(key, prefetch);
8865
+ resolve(prefetch);
8866
+ },
8867
+ () => {
8868
+ synthesizer.close();
8869
+ resolve(null);
8870
+ }
8871
+ );
8872
+ }).finally(() => {
8873
+ this.prefetchPromises.delete(key);
8874
+ });
8875
+ this.prefetchPromises.set(key, prefetchPromise);
8876
+ }
8877
+ async playPrefetchedNode(node) {
8878
+ var _a, _b, _c;
8879
+ const attr = (_a = node.attributes.getNamedItem("co-tts.text")) != null ? _a : node.attributes.getNamedItem("co-tts");
8880
+ const text = this.getNodeText(node, attr);
8881
+ const key = this.getPrefetchKey(node, text);
8882
+ const prefetch = (_b = this.prefetchedAudio.get(key)) != null ? _b : await this.prefetchPromises.get(key);
8883
+ if (!prefetch) {
8884
+ return false;
8885
+ }
8886
+ this.prefetchedAudio.delete(key);
8887
+ this.clickedNode = node;
8888
+ this.textToRead = prefetch.text;
8889
+ this.wordBoundryList = prefetch.wordBoundryList;
8890
+ this.wordEncounters = [];
8891
+ this.previousWordBoundary = void 0;
8892
+ this.prevTextOffset = 0;
8893
+ this.currentWord = "";
8894
+ this.currentOffset = 0;
8895
+ this.wordBoundaryOffset = 0;
8896
+ if (node.hasAttribute("co-tts.highlight")) {
8897
+ if (((_c = node.attributes.getNamedItem("co-tts.highlight")) == null ? void 0 : _c.value) !== "") {
8898
+ const newReferenceDiv = document.getElementById(node.attributes.getNamedItem("co-tts.highlight").value);
8899
+ this.highlightDiv = newReferenceDiv;
8900
+ if (newReferenceDiv !== null) {
8901
+ this.originalHighlightDivInnerHTML = newReferenceDiv.innerHTML;
8902
+ }
8903
+ } else {
8904
+ this.highlightDiv = node;
8905
+ this.originalHighlightDivInnerHTML = node.innerHTML;
8906
+ }
8907
+ }
8908
+ await this.createInterval();
8909
+ const audio = new Audio(prefetch.url);
8910
+ this.activePrefetchedAudioUrl = prefetch.url;
8911
+ this.player = audio;
8912
+ audio.addEventListener("play", () => {
8913
+ document.dispatchEvent(new CustomEvent("COAzureTTSStartedPlaying", {}));
8914
+ }, { once: true });
8915
+ audio.addEventListener("ended", async () => {
8916
+ this.stopPlayer();
8917
+ if (this.clickedNode.hasAttribute("co-tts.next")) {
8918
+ const nextNode = document.getElementById(this.clickedNode.getAttribute("co-tts.next"));
8919
+ if (nextNode && await this.playPrefetchedNode(nextNode)) {
8920
+ return;
8921
+ }
8922
+ if (nextNode && nextNode.attributes.getNamedItem("co-tts.text")) {
8923
+ this.handleWithoutClick(nextNode, nextNode.attributes.getNamedItem("co-tts.text"));
8924
+ } else if (nextNode) {
8925
+ nextNode.dispatchEvent(new Event("click"));
8926
+ }
8927
+ } else {
8928
+ document.dispatchEvent(new CustomEvent("COAzureTTSFinishedPlaying", {}));
8929
+ }
8930
+ }, { once: true });
8931
+ this.prefetchNextNode(node);
8932
+ await audio.play();
8933
+ return true;
8934
+ }
8672
8935
  async clearInterval() {
8673
8936
  clearInterval(this.interval);
8674
8937
  }
@@ -8686,6 +8949,10 @@ class TextToSpeech {
8686
8949
  }
8687
8950
  }
8688
8951
  if (wordBoundary !== void 0) {
8952
+ if (this.playbackSegments.length > 0) {
8953
+ this.updateChainedHighlight(wordBoundary);
8954
+ return;
8955
+ }
8689
8956
  if (~[".", ",", "!", "?", "*", "(", ")", "&", "\\", "/", "^", "[", "]", "<", ">", ":"].indexOf(wordBoundary.text)) {
8690
8957
  wordBoundary = (_a = this.previousWordBoundary) != null ? _a : void 0;
8691
8958
  }