@creativeorange/azure-text-to-speech 2.2.2 → 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.
@@ -8350,17 +8350,170 @@ class RestMessageAdapter {
8350
8350
  return Object.keys(params).map((k) => encodeURIComponent(k) + "=" + encodeURIComponent(params[k])).join("&");
8351
8351
  }
8352
8352
  }
8353
+ const DEFAULT_TOKEN_LIFETIME_MS = 8 * 60 * 1e3;
8354
+ function createSpeechAuthorizationProvider(options) {
8355
+ var _a;
8356
+ const hasTokenEndpoint = typeof options.tokenEndpoint === "string" && options.tokenEndpoint.trim() !== "";
8357
+ const hasProvider = typeof options.getAuthorizationToken === "function";
8358
+ if (hasTokenEndpoint && hasProvider) {
8359
+ throw new Error(
8360
+ "Provide either tokenEndpoint or getAuthorizationToken, not both."
8361
+ );
8362
+ }
8363
+ if (!hasTokenEndpoint && !hasProvider) {
8364
+ throw new Error(
8365
+ "A tokenEndpoint or getAuthorizationToken provider is required."
8366
+ );
8367
+ }
8368
+ if (hasProvider) {
8369
+ return options.getAuthorizationToken;
8370
+ }
8371
+ const tokenEndpoint = options.tokenEndpoint.trim();
8372
+ const requestOptions = (_a = options.tokenRequestOptions) != null ? _a : {};
8373
+ return async () => {
8374
+ var _a2, _b;
8375
+ let response;
8376
+ try {
8377
+ response = await fetch(tokenEndpoint, {
8378
+ method: "GET",
8379
+ credentials: (_a2 = requestOptions.credentials) != null ? _a2 : "same-origin",
8380
+ headers: {
8381
+ Accept: "application/json",
8382
+ ...(_b = requestOptions.headers) != null ? _b : {}
8383
+ }
8384
+ });
8385
+ } catch {
8386
+ throw createSafeError(
8387
+ "Could not reach the speech token endpoint.",
8388
+ "TOKEN_ENDPOINT_UNREACHABLE"
8389
+ );
8390
+ }
8391
+ if (!response.ok) {
8392
+ throw createSafeError(
8393
+ `Speech token endpoint returned HTTP ${response.status}.`,
8394
+ "TOKEN_ENDPOINT_HTTP_ERROR"
8395
+ );
8396
+ }
8397
+ let payload;
8398
+ try {
8399
+ payload = await response.json();
8400
+ } catch {
8401
+ throw createSafeError(
8402
+ "Speech token endpoint returned invalid JSON.",
8403
+ "TOKEN_ENDPOINT_INVALID_JSON"
8404
+ );
8405
+ }
8406
+ return validateSpeechAuthorization(payload);
8407
+ };
8408
+ }
8409
+ function validateSpeechAuthorization(value) {
8410
+ if (!value || typeof value !== "object") {
8411
+ throw createSafeError(
8412
+ "Speech authorization response must be an object.",
8413
+ "TOKEN_INVALID_RESPONSE"
8414
+ );
8415
+ }
8416
+ const candidate = value;
8417
+ const token = typeof candidate.token === "string" ? candidate.token.trim() : "";
8418
+ const region = typeof candidate.region === "string" ? candidate.region.trim() : "";
8419
+ if (token === "") {
8420
+ throw createSafeError(
8421
+ "Speech authorization token is missing or empty.",
8422
+ "TOKEN_EMPTY"
8423
+ );
8424
+ }
8425
+ if (region === "") {
8426
+ throw createSafeError(
8427
+ "Speech authorization region is missing or empty.",
8428
+ "TOKEN_REGION_MISSING"
8429
+ );
8430
+ }
8431
+ return { token, region };
8432
+ }
8433
+ function createSafeError(message, code) {
8434
+ const error = new Error(message);
8435
+ error.message = message;
8436
+ if (code) {
8437
+ error.code = code;
8438
+ }
8439
+ return error;
8440
+ }
8441
+ function toSafeErrorDetail(error) {
8442
+ if (error && typeof error === "object") {
8443
+ const candidate = error;
8444
+ const message = typeof candidate.message === "string" && candidate.message.trim() !== "" ? sanitizeErrorText(candidate.message) : "An unexpected speech error occurred.";
8445
+ const detail = { message };
8446
+ if (typeof candidate.code === "string" && candidate.code.trim() !== "") {
8447
+ detail.code = candidate.code;
8448
+ }
8449
+ return detail;
8450
+ }
8451
+ return {
8452
+ message: "An unexpected speech error occurred."
8453
+ };
8454
+ }
8455
+ function sanitizeErrorText(message) {
8456
+ return message.replace(/Bearer\s+\S+/gi, "Bearer [redacted]").replace(/token["']?\s*[:=]\s*["']?[^"',\s}]+/gi, "token=[redacted]");
8457
+ }
8458
+ class SpeechAuthorizationManager {
8459
+ constructor(options) {
8460
+ __publicField(this, "authorization");
8461
+ __publicField(this, "authorizationExpiresAt", 0);
8462
+ __publicField(this, "authorizationRequest");
8463
+ __publicField(this, "provider");
8464
+ __publicField(this, "tokenLifetimeMs");
8465
+ this.provider = createSpeechAuthorizationProvider(options);
8466
+ this.tokenLifetimeMs = options.tokenLifetimeMs && options.tokenLifetimeMs > 0 ? options.tokenLifetimeMs : DEFAULT_TOKEN_LIFETIME_MS;
8467
+ }
8468
+ async getAuthorization() {
8469
+ if (this.authorization && Date.now() < this.authorizationExpiresAt) {
8470
+ return this.authorization;
8471
+ }
8472
+ if (this.authorizationRequest) {
8473
+ return this.authorizationRequest;
8474
+ }
8475
+ this.authorizationRequest = this.fetchAuthorization().then((authorization) => {
8476
+ this.authorization = authorization;
8477
+ this.authorizationExpiresAt = Date.now() + this.tokenLifetimeMs;
8478
+ return authorization;
8479
+ }).finally(() => {
8480
+ this.authorizationRequest = void 0;
8481
+ });
8482
+ return this.authorizationRequest;
8483
+ }
8484
+ clearAuthorization() {
8485
+ this.authorization = void 0;
8486
+ this.authorizationExpiresAt = 0;
8487
+ this.authorizationRequest = void 0;
8488
+ }
8489
+ async fetchAuthorization() {
8490
+ try {
8491
+ const authorization = await this.provider();
8492
+ return validateSpeechAuthorization(authorization);
8493
+ } catch (error) {
8494
+ this.clearAuthorization();
8495
+ throw createSafeError(
8496
+ toSafeErrorDetail(error).message,
8497
+ error == null ? void 0 : error.code
8498
+ );
8499
+ }
8500
+ }
8501
+ }
8353
8502
  class SpeechToText {
8354
- constructor(key, region, sourceLanguage, targetLanguage = null) {
8355
- __publicField(this, "key");
8503
+ constructor(options) {
8356
8504
  __publicField(this, "region");
8357
8505
  __publicField(this, "sourceLanguage");
8358
8506
  __publicField(this, "targetLanguage");
8359
8507
  __publicField(this, "recognizer");
8360
- this.key = key;
8361
- this.region = region;
8362
- this.sourceLanguage = sourceLanguage;
8363
- this.targetLanguage = targetLanguage !== null ? targetLanguage : sourceLanguage;
8508
+ __publicField(this, "authorizationManager");
8509
+ var _a, _b, _c;
8510
+ if (!options || typeof options.sourceLanguage !== "string" || options.sourceLanguage.trim() === "") {
8511
+ throw new Error("A sourceLanguage is required.");
8512
+ }
8513
+ this.authorizationManager = new SpeechAuthorizationManager(options);
8514
+ this.region = (_b = (_a = options.region) == null ? void 0 : _a.trim()) != null ? _b : "";
8515
+ this.sourceLanguage = options.sourceLanguage;
8516
+ this.targetLanguage = (_c = options.targetLanguage) != null ? _c : options.sourceLanguage;
8364
8517
  }
8365
8518
  async start() {
8366
8519
  await this.registerBindings(document);
@@ -8384,39 +8537,55 @@ class SpeechToText {
8384
8537
  }
8385
8538
  }
8386
8539
  }
8540
+ async createSpeechTranslationConfig() {
8541
+ const authorization = await this.authorizationManager.getAuthorization();
8542
+ const speechConfig = SpeechTranslationConfig.fromAuthorizationToken(
8543
+ authorization.token,
8544
+ authorization.region
8545
+ );
8546
+ speechConfig.speechRecognitionLanguage = this.sourceLanguage;
8547
+ speechConfig.addTargetLanguage(
8548
+ this.targetLanguage
8549
+ );
8550
+ this.region = authorization.region;
8551
+ return speechConfig;
8552
+ }
8387
8553
  async handleStartModifier(node, attr) {
8388
8554
  node.addEventListener("click", async (_) => {
8389
- const speechConfig = SpeechTranslationConfig.fromSubscription(this.key, this.region);
8390
- speechConfig.speechRecognitionLanguage = this.sourceLanguage;
8391
- speechConfig.addTargetLanguage(this.targetLanguage);
8392
- const audioConfig = AudioConfig.fromDefaultMicrophoneInput();
8393
- this.recognizer = new TranslationRecognizer(speechConfig, audioConfig);
8394
- document.dispatchEvent(new CustomEvent("COAzureSTTStartedRecording", {}));
8395
- const prevResults = [];
8396
- this.recognizer.recognizing = (sender, event) => {
8397
- const result = event.result;
8398
- if (result && result.reason === ResultReason.TranslatingSpeech) {
8399
- const translation = result.translations.get(this.targetLanguage);
8400
- prevResults["result_" + result.privOffset.toString()] = translation;
8401
- const totalResult = Object.values(prevResults).join(". ");
8402
- const inputElement = document.getElementById(attr.value);
8403
- if (inputElement !== null) {
8404
- if (inputElement instanceof HTMLInputElement) {
8405
- inputElement.value = `${totalResult} `;
8406
- } else {
8407
- inputElement.innerHTML = `${totalResult} `;
8555
+ try {
8556
+ const speechConfig = await this.createSpeechTranslationConfig();
8557
+ const audioConfig = AudioConfig.fromDefaultMicrophoneInput();
8558
+ this.recognizer = new TranslationRecognizer(speechConfig, audioConfig);
8559
+ document.dispatchEvent(new CustomEvent("COAzureSTTStartedRecording", {}));
8560
+ const prevResults = [];
8561
+ this.recognizer.recognizing = (sender, event) => {
8562
+ const result = event.result;
8563
+ if (result && result.reason === ResultReason.TranslatingSpeech) {
8564
+ const translation = result.translations.get(this.targetLanguage);
8565
+ prevResults["result_" + result.privOffset.toString()] = translation;
8566
+ const totalResult = Object.values(prevResults).join(". ");
8567
+ const inputElement = document.getElementById(attr.value);
8568
+ if (inputElement !== null) {
8569
+ if (inputElement instanceof HTMLInputElement) {
8570
+ inputElement.value = `${totalResult} `;
8571
+ } else {
8572
+ inputElement.innerHTML = `${totalResult} `;
8573
+ }
8408
8574
  }
8409
8575
  }
8410
- }
8411
- };
8412
- this.recognizer.startContinuousRecognitionAsync(
8413
- (result) => {
8414
- },
8415
- (err) => {
8416
- console.log(err);
8417
- this.stop();
8418
- }
8419
- );
8576
+ };
8577
+ this.recognizer.startContinuousRecognitionAsync(
8578
+ () => {
8579
+ },
8580
+ (err) => {
8581
+ this.dispatchError(err, "RECOGNITION_ERROR");
8582
+ this.stop();
8583
+ }
8584
+ );
8585
+ } catch (error) {
8586
+ this.dispatchError(error);
8587
+ await this.stop();
8588
+ }
8420
8589
  });
8421
8590
  }
8422
8591
  async handleStopModifier(node, attr) {
@@ -8426,16 +8595,34 @@ class SpeechToText {
8426
8595
  }
8427
8596
  async stop() {
8428
8597
  if (this.recognizer !== void 0) {
8429
- this.recognizer.stopContinuousRecognitionAsync();
8430
- this.recognizer.close();
8598
+ try {
8599
+ this.recognizer.stopContinuousRecognitionAsync();
8600
+ } catch {
8601
+ }
8602
+ try {
8603
+ this.recognizer.close();
8604
+ } catch {
8605
+ }
8431
8606
  this.recognizer = void 0;
8432
8607
  }
8433
8608
  document.dispatchEvent(new CustomEvent("COAzureSTTStoppedRecording", {}));
8434
8609
  }
8610
+ dispatchError(error, code) {
8611
+ const detail = toSafeErrorDetail(error);
8612
+ if (code && !detail.code) {
8613
+ detail.code = code;
8614
+ }
8615
+ document.dispatchEvent(
8616
+ new CustomEvent("COAzureSTTError", {
8617
+ detail: {
8618
+ error: detail
8619
+ }
8620
+ })
8621
+ );
8622
+ }
8435
8623
  }
8436
8624
  class TextToSpeech {
8437
- constructor(key, region, voice, rate = 0, pitch = 0, url = "") {
8438
- __publicField(this, "key");
8625
+ constructor(options) {
8439
8626
  __publicField(this, "region");
8440
8627
  __publicField(this, "voice");
8441
8628
  __publicField(this, "rate");
@@ -8455,18 +8642,24 @@ class TextToSpeech {
8455
8642
  __publicField(this, "currentWord", "");
8456
8643
  __publicField(this, "currentOffset", 0);
8457
8644
  __publicField(this, "wordBoundaryOffset", 0);
8645
+ __publicField(this, "playbackTextOffsetBase");
8458
8646
  __publicField(this, "prevTextOffset", 0);
8459
8647
  __publicField(this, "url", "");
8460
8648
  __publicField(this, "prefetchedAudio", /* @__PURE__ */ new Map());
8461
8649
  __publicField(this, "prefetchPromises", /* @__PURE__ */ new Map());
8462
8650
  __publicField(this, "activePrefetchedAudioUrl", "");
8463
8651
  __publicField(this, "playbackSegments", []);
8464
- this.key = key;
8465
- this.region = region;
8466
- this.voice = voice;
8467
- this.rate = rate;
8468
- this.pitch = pitch;
8469
- this.url = url;
8652
+ __publicField(this, "authorizationManager");
8653
+ var _a, _b, _c, _d, _e;
8654
+ if (!options || typeof options.voice !== "string" || options.voice.trim() === "") {
8655
+ throw new Error("A voice is required.");
8656
+ }
8657
+ this.authorizationManager = new SpeechAuthorizationManager(options);
8658
+ this.region = (_b = (_a = options.region) == null ? void 0 : _a.trim()) != null ? _b : "";
8659
+ this.voice = options.voice;
8660
+ this.rate = (_c = options.rate) != null ? _c : 0;
8661
+ this.pitch = (_d = options.pitch) != null ? _d : 0;
8662
+ this.url = (_e = options.url) != null ? _e : "";
8470
8663
  }
8471
8664
  async start() {
8472
8665
  await this.registerBindings(document);
@@ -8515,7 +8708,7 @@ class TextToSpeech {
8515
8708
  }
8516
8709
  async handleIdModifier(node, attr) {
8517
8710
  node.addEventListener("click", async (_) => {
8518
- var _a, _b;
8711
+ var _a, _b, _c, _d;
8519
8712
  this.stopPlayer();
8520
8713
  await this.createInterval();
8521
8714
  const referenceDiv = document.getElementById(attr.value);
@@ -8526,10 +8719,10 @@ class TextToSpeech {
8526
8719
  if (referenceDiv.hasAttribute("co-tts.text") && referenceDiv.getAttribute("co-tts.text") !== "") {
8527
8720
  this.textToRead = (_a = referenceDiv.getAttribute("co-tts.text")) != null ? _a : "";
8528
8721
  } else {
8529
- this.textToRead = referenceDiv.innerText;
8722
+ this.textToRead = (_c = (_b = referenceDiv.innerText) != null ? _b : referenceDiv.textContent) != null ? _c : "";
8530
8723
  }
8531
8724
  if (referenceDiv.hasAttribute("co-tts.highlight")) {
8532
- if (((_b = referenceDiv.attributes.getNamedItem("co-tts.highlight")) == null ? void 0 : _b.value) !== "") {
8725
+ if (((_d = referenceDiv.attributes.getNamedItem("co-tts.highlight")) == null ? void 0 : _d.value) !== "") {
8533
8726
  const newReferenceDiv = document.getElementById(referenceDiv.attributes.getNamedItem("co-tts.highlight").value);
8534
8727
  this.highlightDiv = newReferenceDiv;
8535
8728
  this.originalHighlightDivInnerHTML = newReferenceDiv.innerHTML;
@@ -8538,7 +8731,7 @@ class TextToSpeech {
8538
8731
  this.originalHighlightDivInnerHTML = referenceDiv.innerHTML;
8539
8732
  }
8540
8733
  }
8541
- this.startSynthesizer(node, attr);
8734
+ await this.startSynthesizer(node, attr);
8542
8735
  });
8543
8736
  }
8544
8737
  async handleAjaxModifier(node, attr) {
@@ -8550,12 +8743,12 @@ class TextToSpeech {
8550
8743
  method: `GET`
8551
8744
  });
8552
8745
  this.textToRead = await response.text();
8553
- this.startSynthesizer(node, attr);
8746
+ await this.startSynthesizer(node, attr);
8554
8747
  });
8555
8748
  }
8556
8749
  async handleDefault(node, attr) {
8557
8750
  node.addEventListener("click", async (_) => {
8558
- var _a;
8751
+ var _a, _b, _c;
8559
8752
  this.stopPlayer();
8560
8753
  await this.createInterval();
8561
8754
  this.clickedNode = node;
@@ -8570,15 +8763,15 @@ class TextToSpeech {
8570
8763
  }
8571
8764
  }
8572
8765
  if (attr.value === "") {
8573
- this.textToRead = node.innerText;
8766
+ this.textToRead = (_c = (_b = node.innerText) != null ? _b : node.textContent) != null ? _c : "";
8574
8767
  } else {
8575
8768
  this.textToRead = attr.value;
8576
8769
  }
8577
- this.startSynthesizer(node, attr);
8770
+ await this.startSynthesizer(node, attr);
8578
8771
  });
8579
8772
  }
8580
8773
  async handleWithoutClick(node, attr) {
8581
- var _a;
8774
+ var _a, _b, _c;
8582
8775
  this.stopPlayer();
8583
8776
  await this.createInterval();
8584
8777
  this.clickedNode = node;
@@ -8595,11 +8788,11 @@ class TextToSpeech {
8595
8788
  }
8596
8789
  }
8597
8790
  if (attr.value === "") {
8598
- this.textToRead = node.innerText;
8791
+ this.textToRead = (_c = (_b = node.innerText) != null ? _b : node.textContent) != null ? _c : "";
8599
8792
  } else {
8600
8793
  this.textToRead = attr.value;
8601
8794
  }
8602
- this.startSynthesizer(node, attr);
8795
+ await this.startSynthesizer(node, attr);
8603
8796
  }
8604
8797
  async handleStopModifier(node, attr) {
8605
8798
  node.addEventListener("click", async (_) => {
@@ -8640,65 +8833,83 @@ class TextToSpeech {
8640
8833
  URL.revokeObjectURL(this.activePrefetchedAudioUrl);
8641
8834
  this.activePrefetchedAudioUrl = "";
8642
8835
  }
8836
+ this.closeSynthesizer();
8643
8837
  this.player = void 0;
8838
+ this.audioConfig = void 0;
8839
+ this.speechConfig = void 0;
8644
8840
  this.highlightDiv = void 0;
8645
8841
  this.prevTextOffset = 0;
8842
+ this.playbackTextOffsetBase = void 0;
8843
+ }
8844
+ async createSpeechConfig() {
8845
+ const authorization = await this.authorizationManager.getAuthorization();
8846
+ const speechConfig = SpeechConfig.fromAuthorizationToken(
8847
+ authorization.token,
8848
+ authorization.region
8849
+ );
8850
+ speechConfig.speechSynthesisVoiceName = `Microsoft Server Speech Text to Speech Voice (${this.voice})`;
8851
+ speechConfig.speechSynthesisOutputFormat = SpeechSynthesisOutputFormat.Audio24Khz160KBitRateMonoMp3;
8852
+ this.region = authorization.region;
8853
+ return speechConfig;
8646
8854
  }
8647
8855
  async startSynthesizer(node, attr) {
8648
- this.speechConfig = SpeechConfig.fromSubscription(this.key, this.region);
8649
- this.speechConfig.speechSynthesisVoiceName = `Microsoft Server Speech Text to Speech Voice (${this.voice})`;
8650
- this.speechConfig.speechSynthesisOutputFormat = SpeechSynthesisOutputFormat.Audio24Khz160KBitRateMonoMp3;
8651
- this.player = new SpeakerAudioDestination();
8652
- this.audioConfig = AudioConfig.fromSpeakerOutput(this.player);
8653
- this.synthesizer = new SpeechSynthesizer(this.speechConfig, this.audioConfig);
8654
- this.synthesizer.wordBoundary = (s, e) => {
8655
- this.wordBoundryList.push(e);
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
- }
8664
- this.player.onAudioEnd = async () => {
8665
- const wasChainedPlayback = this.playbackSegments.length > 0;
8666
- this.stopPlayer();
8667
- if (wasChainedPlayback) {
8668
- document.dispatchEvent(new CustomEvent("COAzureTTSFinishedPlaying", {}));
8669
- return;
8856
+ try {
8857
+ this.speechConfig = await this.createSpeechConfig();
8858
+ this.player = new SpeakerAudioDestination();
8859
+ this.audioConfig = AudioConfig.fromSpeakerOutput(this.player);
8860
+ this.synthesizer = new SpeechSynthesizer(this.speechConfig, this.audioConfig);
8861
+ this.synthesizer.wordBoundary = (s, e) => {
8862
+ this.wordBoundryList.push(e);
8863
+ };
8864
+ const playbackChain = this.collectPlaybackChain(this.clickedNode);
8865
+ const isChainedPlayback = playbackChain.length > 1;
8866
+ if (isChainedPlayback) {
8867
+ this.preparePlaybackChain(playbackChain);
8868
+ } else {
8869
+ this.playbackSegments = [];
8670
8870
  }
8671
- if (this.clickedNode.hasAttribute("co-tts.next")) {
8672
- const nextNode = document.getElementById(this.clickedNode.getAttribute("co-tts.next"));
8673
- if (nextNode && await this.playPrefetchedNode(nextNode)) {
8871
+ this.player.onAudioEnd = async () => {
8872
+ const wasChainedPlayback = this.playbackSegments.length > 0;
8873
+ this.stopPlayer();
8874
+ if (wasChainedPlayback) {
8875
+ document.dispatchEvent(new CustomEvent("COAzureTTSFinishedPlaying", {}));
8674
8876
  return;
8675
8877
  }
8676
- if (nextNode && nextNode.attributes.getNamedItem("co-tts.text")) {
8677
- this.handleWithoutClick(nextNode, nextNode.attributes.getNamedItem("co-tts.text"));
8678
- } else if (nextNode) {
8679
- nextNode.dispatchEvent(new Event("click"));
8878
+ if (this.clickedNode.hasAttribute("co-tts.next")) {
8879
+ const nextNode = document.getElementById(this.clickedNode.getAttribute("co-tts.next"));
8880
+ if (nextNode && await this.playPrefetchedNode(nextNode)) {
8881
+ return;
8882
+ }
8883
+ if (nextNode && nextNode.attributes.getNamedItem("co-tts.text")) {
8884
+ this.handleWithoutClick(nextNode, nextNode.attributes.getNamedItem("co-tts.text"));
8885
+ } else if (nextNode) {
8886
+ nextNode.dispatchEvent(new Event("click"));
8887
+ }
8888
+ } else {
8889
+ document.dispatchEvent(new CustomEvent("COAzureTTSFinishedPlaying", {}));
8680
8890
  }
8681
- } else {
8682
- document.dispatchEvent(new CustomEvent("COAzureTTSFinishedPlaying", {}));
8683
- }
8684
- };
8685
- this.player.onAudioStart = async () => {
8686
- document.dispatchEvent(new CustomEvent("COAzureTTSStartedPlaying", {}));
8687
- };
8688
- if (!isChainedPlayback) {
8689
- this.prefetchNextNode(this.clickedNode);
8690
- }
8691
- this.synthesizer.speakSsmlAsync(
8692
- this.buildSSML(this.textToRead),
8693
- () => {
8694
- this.synthesizer.close();
8695
- this.synthesizer = void 0;
8696
- },
8697
- () => {
8698
- this.synthesizer.close();
8699
- this.synthesizer = void 0;
8891
+ };
8892
+ this.player.onAudioStart = async () => {
8893
+ document.dispatchEvent(new CustomEvent("COAzureTTSStartedPlaying", {}));
8894
+ };
8895
+ if (!isChainedPlayback) {
8896
+ this.prefetchNextNode(this.clickedNode);
8700
8897
  }
8701
- );
8898
+ this.synthesizer.speakSsmlAsync(
8899
+ this.buildSSML(this.textToRead),
8900
+ () => {
8901
+ this.closeSynthesizer();
8902
+ },
8903
+ (error) => {
8904
+ this.dispatchError(error, "SYNTHESIS_ERROR");
8905
+ this.closeSynthesizer();
8906
+ this.stopPlayer();
8907
+ }
8908
+ );
8909
+ } catch (error) {
8910
+ this.dispatchError(error);
8911
+ await this.stopPlayer();
8912
+ }
8702
8913
  }
8703
8914
  collectPlaybackChain(node) {
8704
8915
  var _a, _b;
@@ -8738,6 +8949,7 @@ class TextToSpeech {
8738
8949
  this.wordEncounters = [];
8739
8950
  this.previousWordBoundary = void 0;
8740
8951
  this.prevTextOffset = 0;
8952
+ this.playbackTextOffsetBase = void 0;
8741
8953
  this.currentWord = "";
8742
8954
  this.currentOffset = 0;
8743
8955
  this.wordBoundaryOffset = 0;
@@ -8768,13 +8980,17 @@ class TextToSpeech {
8768
8980
  this.resetPlaybackSegments();
8769
8981
  return;
8770
8982
  }
8771
- const segment = this.playbackSegments.find((candidate) => wordBoundary.textOffset >= candidate.start && wordBoundary.textOffset < candidate.end);
8983
+ if (this.playbackTextOffsetBase === void 0) {
8984
+ this.playbackTextOffsetBase = wordBoundary.textOffset;
8985
+ }
8986
+ const normalizedTextOffset = Math.max(0, wordBoundary.textOffset - this.playbackTextOffsetBase);
8987
+ const segment = this.playbackSegments.find((candidate) => normalizedTextOffset >= candidate.start && normalizedTextOffset < candidate.end);
8772
8988
  this.resetPlaybackSegments();
8773
8989
  if (!(segment == null ? void 0 : segment.highlightDiv)) {
8774
8990
  this.previousWordBoundary = wordBoundary;
8775
8991
  return;
8776
8992
  }
8777
- const relativeTextOffset = wordBoundary.textOffset - segment.start;
8993
+ const relativeTextOffset = normalizedTextOffset - segment.start;
8778
8994
  const currentOffset = this.getPosition(segment.originalHighlightDivInnerHTML, wordBoundary.text, relativeTextOffset);
8779
8995
  if (currentOffset === Number.MAX_SAFE_INTEGER) {
8780
8996
  this.previousWordBoundary = wordBoundary;
@@ -8789,14 +9005,14 @@ class TextToSpeech {
8789
9005
  this.previousWordBoundary = wordBoundary;
8790
9006
  }
8791
9007
  getNodeText(node, attr) {
8792
- var _a;
9008
+ var _a, _b, _c;
8793
9009
  if (attr && attr.value !== "") {
8794
9010
  return attr.value;
8795
9011
  }
8796
9012
  if (node.hasAttribute("co-tts.text") && node.getAttribute("co-tts.text") !== "") {
8797
9013
  return (_a = node.getAttribute("co-tts.text")) != null ? _a : "";
8798
9014
  }
8799
- return node.innerText;
9015
+ return (_c = (_b = node.innerText) != null ? _b : node.textContent) != null ? _c : "";
8800
9016
  }
8801
9017
  getPrefetchKey(node, text) {
8802
9018
  return [
@@ -8835,41 +9051,52 @@ class TextToSpeech {
8835
9051
  if (this.prefetchedAudio.has(key) || this.prefetchPromises.has(key)) {
8836
9052
  return;
8837
9053
  }
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(() => {
9054
+ let synthesizer;
9055
+ const prefetchPromise = (async () => {
9056
+ try {
9057
+ const speechConfig = await this.createSpeechConfig();
9058
+ synthesizer = new SpeechSynthesizer(speechConfig, null);
9059
+ const wordBoundryList = [];
9060
+ synthesizer.wordBoundary = (s, e) => {
9061
+ wordBoundryList.push(e);
9062
+ };
9063
+ return await new Promise((resolve) => {
9064
+ synthesizer == null ? void 0 : synthesizer.speakSsmlAsync(
9065
+ this.buildSSML(text),
9066
+ (result) => {
9067
+ this.closeResource(synthesizer);
9068
+ synthesizer = void 0;
9069
+ if (!(result == null ? void 0 : result.audioData)) {
9070
+ resolve(null);
9071
+ return;
9072
+ }
9073
+ const blob = new Blob([result.audioData], { type: "audio/mpeg" });
9074
+ const url = URL.createObjectURL(blob);
9075
+ const prefetch = {
9076
+ key,
9077
+ nodeId: nextNode.id,
9078
+ text,
9079
+ url,
9080
+ wordBoundryList
9081
+ };
9082
+ this.prefetchedAudio.set(key, prefetch);
9083
+ resolve(prefetch);
9084
+ },
9085
+ (error) => {
9086
+ this.closeResource(synthesizer);
9087
+ synthesizer = void 0;
9088
+ this.dispatchError(error, "PREFETCH_ERROR");
9089
+ resolve(null);
9090
+ }
9091
+ );
9092
+ });
9093
+ } catch (error) {
9094
+ this.closeResource(synthesizer);
9095
+ synthesizer = void 0;
9096
+ this.dispatchError(error);
9097
+ return null;
9098
+ }
9099
+ })().finally(() => {
8873
9100
  this.prefetchPromises.delete(key);
8874
9101
  });
8875
9102
  this.prefetchPromises.set(key, prefetchPromise);
@@ -8906,31 +9133,44 @@ class TextToSpeech {
8906
9133
  }
8907
9134
  }
8908
9135
  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"));
9136
+ try {
9137
+ const audio = new Audio(prefetch.url);
9138
+ this.activePrefetchedAudioUrl = prefetch.url;
9139
+ this.player = audio;
9140
+ audio.addEventListener("play", () => {
9141
+ document.dispatchEvent(new CustomEvent("COAzureTTSStartedPlaying", {}));
9142
+ }, { once: true });
9143
+ audio.addEventListener("ended", async () => {
9144
+ this.stopPlayer();
9145
+ if (this.clickedNode.hasAttribute("co-tts.next")) {
9146
+ const nextNode = document.getElementById(this.clickedNode.getAttribute("co-tts.next"));
9147
+ if (nextNode && await this.playPrefetchedNode(nextNode)) {
9148
+ return;
9149
+ }
9150
+ if (nextNode && nextNode.attributes.getNamedItem("co-tts.text")) {
9151
+ this.handleWithoutClick(nextNode, nextNode.attributes.getNamedItem("co-tts.text"));
9152
+ } else if (nextNode) {
9153
+ nextNode.dispatchEvent(new Event("click"));
9154
+ }
9155
+ } else {
9156
+ document.dispatchEvent(new CustomEvent("COAzureTTSFinishedPlaying", {}));
8926
9157
  }
8927
- } else {
8928
- document.dispatchEvent(new CustomEvent("COAzureTTSFinishedPlaying", {}));
8929
- }
8930
- }, { once: true });
8931
- this.prefetchNextNode(node);
8932
- await audio.play();
8933
- return true;
9158
+ }, { once: true });
9159
+ audio.addEventListener("error", () => {
9160
+ this.dispatchError(
9161
+ new Error("Prefetched audio resource closed unexpectedly."),
9162
+ "AUDIO_RESOURCE_ERROR"
9163
+ );
9164
+ this.stopPlayer();
9165
+ }, { once: true });
9166
+ this.prefetchNextNode(node);
9167
+ await audio.play();
9168
+ return true;
9169
+ } catch (error) {
9170
+ this.dispatchError(error, "AUDIO_RESOURCE_ERROR");
9171
+ await this.stopPlayer();
9172
+ return false;
9173
+ }
8934
9174
  }
8935
9175
  async clearInterval() {
8936
9176
  clearInterval(this.interval);
@@ -8938,7 +9178,7 @@ class TextToSpeech {
8938
9178
  async createInterval() {
8939
9179
  this.interval = setInterval(() => {
8940
9180
  var _a;
8941
- if (this.player !== void 0 && this.highlightDiv) {
9181
+ if (this.player !== void 0 && (this.highlightDiv || this.playbackSegments.length > 0)) {
8942
9182
  const currentTime = this.player.currentTime;
8943
9183
  let wordBoundary;
8944
9184
  for (const e of this.wordBoundryList) {
@@ -8985,6 +9225,8 @@ class TextToSpeech {
8985
9225
  `;
8986
9226
  }
8987
9227
  }
9228
+ } else if (this.playbackSegments.length > 0) {
9229
+ this.resetPlaybackSegments();
8988
9230
  } else {
8989
9231
  this.highlightDiv.innerHTML = this.originalHighlightDivInnerHTML;
8990
9232
  }
@@ -9031,5 +9273,31 @@ class TextToSpeech {
9031
9273
  p.textContent = input;
9032
9274
  return p.innerHTML;
9033
9275
  }
9276
+ closeSynthesizer() {
9277
+ this.closeResource(this.synthesizer);
9278
+ this.synthesizer = void 0;
9279
+ }
9280
+ closeResource(resource) {
9281
+ if (!resource || typeof resource.close !== "function") {
9282
+ return;
9283
+ }
9284
+ try {
9285
+ resource.close();
9286
+ } catch {
9287
+ }
9288
+ }
9289
+ dispatchError(error, code) {
9290
+ const detail = toSafeErrorDetail(error);
9291
+ if (code && !detail.code) {
9292
+ detail.code = code;
9293
+ }
9294
+ document.dispatchEvent(
9295
+ new CustomEvent("COAzureTTSError", {
9296
+ detail: {
9297
+ error: detail
9298
+ }
9299
+ })
9300
+ );
9301
+ }
9034
9302
  }
9035
9303
  export { SpeechToText, TextToSpeech };