diff --git a/API.md b/API.md
index de133f5..92e0b31 100644
--- a/API.md
+++ b/API.md
@@ -8,7 +8,7 @@
* [.debug(fn)](#module_EasySpeech--module.exports..EasySpeech.debug)
* [.detect()](#module_EasySpeech--module.exports..EasySpeech.detect) ⇒ object
* [.status()](#module_EasySpeech--module.exports..EasySpeech.status) ⇒ Object
- * [.init(maxTimeout, interval, [quiet])](#module_EasySpeech--module.exports..EasySpeech.init) ⇒ Promise.<Boolean>
+ * [.init(maxTimeout, interval, [quiet], [maxLengthExceeded])](#module_EasySpeech--module.exports..EasySpeech.init) ⇒ Promise.<Boolean>
* [.voices()](#module_EasySpeech--module.exports..EasySpeech.voices) ⇒ Array.<SpeechSynthesisVoice>
* [.on(handlers)](#module_EasySpeech--module.exports..EasySpeech.on) ⇒ Object
* [.defaults([options])](#module_EasySpeech--module.exports..EasySpeech.defaults) ⇒ object
@@ -62,7 +62,7 @@ const example = async () => {
* [.debug(fn)](#module_EasySpeech--module.exports..EasySpeech.debug)
* [.detect()](#module_EasySpeech--module.exports..EasySpeech.detect) ⇒ object
* [.status()](#module_EasySpeech--module.exports..EasySpeech.status) ⇒ Object
- * [.init(maxTimeout, interval, [quiet])](#module_EasySpeech--module.exports..EasySpeech.init) ⇒ Promise.<Boolean>
+ * [.init(maxTimeout, interval, [quiet], [maxLengthExceeded])](#module_EasySpeech--module.exports..EasySpeech.init) ⇒ Promise.<Boolean>
* [.voices()](#module_EasySpeech--module.exports..EasySpeech.voices) ⇒ Array.<SpeechSynthesisVoice>
* [.on(handlers)](#module_EasySpeech--module.exports..EasySpeech.on) ⇒ Object
* [.defaults([options])](#module_EasySpeech--module.exports..EasySpeech.defaults) ⇒ object
@@ -161,7 +161,7 @@ EasySpeech.status()
```
-##### EasySpeech.init(maxTimeout, interval, [quiet]) ⇒ Promise.<Boolean>
+##### EasySpeech.init(maxTimeout, interval, [quiet], [maxLengthExceeded]) ⇒ Promise.<Boolean>
This is the function you need to run, before being able to speak.
It includes:
- feature detection
@@ -201,6 +201,7 @@ Note: if once initialized you can't re-init (will skip and resolve to
| maxTimeout | number
| [5000] the maximum timeout to wait for voices in ms |
| interval | number
| [250] the interval in ms to check for voices |
| [quiet] | boolean
| prevent rejection on errors, e.g. if no voices |
+| [maxLengthExceeded] | string
| defines what to do, if max text length (4096 bytes) is exceeded: - 'error' - throw an Error - 'none' - do nothing; note that some voices may not speak the text at all without any error or warning - 'warn' - default, raises a warning |
diff --git a/FAQ.md b/FAQ.md
new file mode 100644
index 0000000..39030cf
--- /dev/null
+++ b/FAQ.md
@@ -0,0 +1,139 @@
+# FAQ
+
+> Please read this carefully before opening a new issue.
+
+## Overview
+
+
+
+
+- [How can I use / install different voices?](#how-can-i-use--install-different-voices)
+- [Why does this library exists if I can use TTS natively in the browser?](#why-does-this-library-exists-if-i-can-use-tts-natively-in-the-browser)
+- [Why not using a cloud-based tts service?](#why-not-using-a-cloud-based-tts-service)
+- [Can I include service xyz with this library?](#can-i-include-service-xyz-with-this-library)
+- [Can I load my own / custom trained voices?](#can-i-load-my-own--custom-trained-voices)
+- [My or my users voices sound all terrible, what can I do?](#my-or-my-users-voices-sound-all-terrible-what-can-i-do)
+- [My voices play faster on a Mac M1 than on other machines](#my-voices-play-faster-on-a-mac-m1-than-on-other-machines)
+- [Init failed with "EasySpeech: browser has no voices (timeout)"](#init-failed-with-easyspeech-browser-has-no-voices-timeout)
+- [Error 'EasySpeech: not initialized. Run EasySpeech.init() first'](#error-easyspeech-not-initialized-run-easyspeechinit-first)
+- [Some specific voices are missing, although they are installed on OS-level](#some-specific-voices-are-missing-although-they-are-installed-on-os-level)
+- [My voices are gone or have changed after I updated my OS](#my-voices-are-gone-or-have-changed-after-i-updated-my-os)
+- [Error 'EasySpeech: text exceeds max length of 4096 bytes.'](#error-easyspeech-text-exceeds-max-length-of-4096-bytes)
+- [Safari plays speech delayed after interaction with other audio](#safari-plays-speech-delayed-after-interaction-with-other-audio)
+
+
+
+## How can I use / install different voices?
+
+> Note: the following cannot be influenced by this tool or JavaScript in general
+> and requires active measures by the user who wants to different/better voices.
+> This is by design and can only be changed if the Web Speech API standard improves.
+
+- Browser-level: switch to Google Chrome as it delivers a set of Google Voices, which all sound pretty decent
+- OS-level: install new voices, which is an OS-specific procedure
+ - [Windows](https://support.microsoft.com/en-us/topic/download-languages-and-voices-for-immersive-reader-read-mode-and-read-aloud-4c83a8d8-7486-42f7-8e46-2b0fdf753130)
+ - [MacOS](https://support.apple.com/guide/mac-help/change-the-voice-your-mac-uses-to-speak-text-mchlp2290/mac)
+ - [Ubuntu](https://github.com/espeak-ng/espeak-ng/blob/master/docs/mbrola.md#installation-of-standard-packages)
+ - [Android](https://support.google.com/accessibility/android/answer/6006983?hl=en&sjid=9301509494880612166-EU)
+ - [iOS](https://support.apple.com/en-us/HT202362)
+
+Please let me know if the guides are outdated or open a PR with updated links.
+
+## Why does this library exists if I can use TTS natively in the browser?
+
+Every browser vendor implements the [Web Speech API](https://developer.mozilla.org/en-US/docs/Web/API/SpeechSynthesis)
+differently and there a multiple nuances that make it difficult to provide similar functionality across major browsers.
+
+## Why not using a cloud-based tts service?
+
+Sure you can do that. However, different projects have different requirements.
+If you can't afford a cloud-based service or are prohibited to do so then this
+tool might be something for you.
+
+## Can I include service xyz with this library?
+
+No, it's solely a wrapper for the [Web Speech API](https://developer.mozilla.org/en-US/docs/Web/API/SpeechSynthesis) "
+standard".
+
+## Can I load my own / custom trained voices?
+
+Unfortunately, no. This is a current limitation of the Web Speech API itself
+and there is nothing we can do about it.
+
+If you want to this to become reality one day, you have to get in contact
+with browser vendors and the [Web Incubator Community Group](https://github.com/WICG/speech-api).
+
+## My or my users voices sound all terrible, what can I do?
+
+Sometimes this is the result of bad settings, like `pitch` and `rate`.
+Please check these value and try to run with explicit values of `1` for both of them.
+
+If this has no effect, then is not an issue of bad pitch/rate. It's very likely that the installed voices
+are simply bad / bad trained or old.
+
+Please read on ["How can I use / install different voices?"](#how-can-i-use--install-different-voices)
+
+## My voices play faster on a Mac M1 than on other machines
+
+This is unfortunately a vendor-specific issue and also supposedly a bug in Safari.
+
+Related issues:
+- https://github.com/jankapunkt/easy-speech/issues/116
+
+## Init failed with "EasySpeech: browser has no voices (timeout)"
+
+This means your browser supports the minimum requirements for speech synthesis,
+but you / your users have no voices installed on your / their system.
+
+Please read on ["How can I use / install different voices?"](#how-can-i-use--install-different-voices)
+
+## Error 'EasySpeech: not initialized. Run EasySpeech.init() first'
+
+This means you haven't run `EasySpeech.init` yet. It's required to set up everything.
+See the [API Docs](./API.md) on how to use it.
+
+## Some specific voices are missing, although they are installed on OS-level
+
+This is something I found on newer iOS versions (16+) to be the case.
+While I have the Siri voice installed, it's not available in the browser.
+This seems to be a vendor-specific issue, so you need to contact your OS vendor (in this case Apple).
+
+## My voices are gone or have changed after I updated my OS
+
+This seems to be a vendor-specific issue, so you need to contact your operating system vendor (Apple, Microsoft).
+
+Related issues:
+- https://github.com/jankapunkt/easy-speech/issues/209
+
+## Error 'EasySpeech: text exceeds max length of 4096 bytes.'
+
+Your text is too long for some voices to process it. You might want to split
+it into smaller chunks and play the next one either by user invocation or
+automatically. A small example:
+
+```js
+let index = 0
+const text = [
+ 'This is the first sentence.',
+ 'This is the second sentence.',
+]
+
+
+async function playToEnd () {
+ const chunk = text[index++]
+ if (!chunk) { return true } // done
+
+ await EasySpeech.speak({ text: chunk })
+ return playToEnd()
+}
+```
+
+Related issues:
+- https://github.com/jankapunkt/easy-speech/issues/227
+
+## Safari plays speech delayed after interaction with other audio
+
+You can try to speak with `volume=0` before your actual voice is intended to speak.
+
+Related issues:
+- https://github.com/jankapunkt/easy-speech/issues/51
diff --git a/README.md b/README.md
index c86547f..bec2970 100644
--- a/README.md
+++ b/README.md
@@ -20,7 +20,6 @@ Cross browser Speech Synthesis; no dependencies.
![npm bundle size](https://img.shields.io/bundlephobia/minzip/easy-speech)
-
## ⭐️ Why EasySpeech?
This project was created, because it's always a struggle to get the synthesis
@@ -41,13 +40,32 @@ part of `Web Speech API` running on most major browsers.
**Note:** this is not a polyfill package, if your target browser does not support speech synthesis or the Web Speech
API, this package is not usable.
+
## 🚀 Live Demo
The live demo is available at https://jankapunkt.github.io/easy-speech/
You can use it to test your browser for `speechSynthesis` support and functionality.
[![live demo screenshot](./docs/demo_screenshot.png)](https://jankapunkt.github.io/easy-speech/)
-
+
+## Table of Contents
+
+
+
+**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)*
+
+- [📦 Installation](#-installation)
+- [👨💻 Usage](#-usage)
+ - [🚀 Initialize](#-initialize)
+ - [📢 Speak a voice](#-speak-a-voice)
+ - [😵💫 Troubleshooting / FAQ](#-troubleshooting--faq)
+- [🔬 API](#-api)
+- [⌨️ Contribution and development](#-contribution-and-development)
+- [📖 Resources](#-resources)
+- [⚖️ License](#-license)
+
+
+
## 📦 Installation
Install from npm via
@@ -115,7 +133,7 @@ If at least `SpeechSynthesis` and `SpeechSynthesisUtterance` are defined you
are good to go.
-### Initialize
+### 🚀 Initialize
Preparing everything to work is not as clear as it should, especially when
targeting cross-browser functionality. The asynchronous init function will help
@@ -127,7 +145,7 @@ EasySpeech.init({ maxTimeout: 5000, interval: 250 })
.catch(e => console.error(e))
```
-#### Loading voices
+#### 💽 Loading voices
The init-routine will go through several stages to setup the environment:
@@ -156,7 +174,7 @@ Note: This fallback voice is not overridden by `EasySpeech.defaults()`, your
default voice will be used in favor but the fallback voice will always be there
in case no voice is found when calling `EasySpeech.speak()`
-### Speak a voice
+### 📢 Speak a voice
This is as easy as it gets:
@@ -177,6 +195,10 @@ an error occurred. You can additionally attach these event listeners if you like
or use `EasySpeech.on` to attach default listeners to every time you call
`EasySpeech.speak`.
+### 😵💫 Troubleshooting / FAQ
+
+There is an own [FAQ section](./FAQ.md) available that aims to help with common issues.
+
## 🔬 API
There is a full API documentation available: [api docs](./API.md)
@@ -206,6 +228,6 @@ This project used several resources to gain insights about how to get the best c
- https://bugs.chromium.org/p/chromium/issues/detail?id=582455
- https://stackoverflow.com/a/65883556
-## License
+## ⚖️ License
MIT, see [license file](./LICENSE)
diff --git a/dist/EasySpeech.cjs.js b/dist/EasySpeech.cjs.js
index 365dcb4..6ef001f 100644
--- a/dist/EasySpeech.cjs.js
+++ b/dist/EasySpeech.cjs.js
@@ -59,6 +59,7 @@ var scope = typeof globalThis === 'undefined' ? window : globalThis;
speechSynthesisEvent: null|SpeechSynthesisEvent,
speechSynthesisErrorEvent: null|SpeechSynthesisErrorEvent,
voices: null|Array,
+ maxLengthExceeded: string,
defaults: {
pitch: Number,
rate: Number,
@@ -304,6 +305,10 @@ var status = function status(s) {
* @param maxTimeout {number}[5000] the maximum timeout to wait for voices in ms
* @param interval {number}[250] the interval in ms to check for voices
* @param quiet {boolean=} prevent rejection on errors, e.g. if no voices
+ * @param maxLengthExceeded {string=} defines what to do, if max text length (4096 bytes) is exceeded:
+ * - 'error' - throw an Error
+ * - 'none' - do nothing; note that some voices may not speak the text at all without any error or warning
+ * - 'warn' - default, raises a warning
* @return {Promise}
* @fulfil {Boolean} true, if initialized, false, if skipped (because already
* initialized)
@@ -323,7 +328,8 @@ EasySpeech.init = function () {
maxTimeout = _ref$maxTimeout === void 0 ? 5000 : _ref$maxTimeout,
_ref$interval = _ref.interval,
interval = _ref$interval === void 0 ? 250 : _ref$interval,
- quiet = _ref.quiet;
+ quiet = _ref.quiet,
+ maxLengthExceeded = _ref.maxLengthExceeded;
return new Promise(function (resolve, reject) {
if (internal.initialized) {
return resolve(false);
@@ -337,6 +343,7 @@ EasySpeech.init = function () {
var timer;
var voicesChangedListener;
var completeCalled = false;
+ internal.maxLengthExceeded = maxLengthExceeded || 'warn';
var fail = function fail(errorMessage) {
status("init: failed (".concat(errorMessage, ")"));
clearInterval(timer);
@@ -682,6 +689,18 @@ EasySpeech.speak = function (_ref3) {
if (!validate.text(text)) {
throw new Error('EasySpeech: at least some valid text is required to speak');
}
+ if (new TextEncoder().encode(text).length > 4096) {
+ var message = 'EasySpeech: text exceeds max length of 4096 bytes, which will not work with some voices.';
+ switch (internal.maxLengthExceeded) {
+ case 'none':
+ break;
+ case 'error':
+ throw new Error(message);
+ case 'warn':
+ default:
+ console.warn(message);
+ }
+ }
var getValue = function getValue(options) {
var _internal$defaults2;
var _Object$entries$ = _slicedToArray(Object.entries(options)[0], 2),
diff --git a/dist/EasySpeech.es5.js b/dist/EasySpeech.es5.js
index 6bb0cb6..0bb2cbc 100644
--- a/dist/EasySpeech.es5.js
+++ b/dist/EasySpeech.es5.js
@@ -57,6 +57,7 @@ var scope = typeof globalThis === 'undefined' ? window : globalThis;
speechSynthesisEvent: null|SpeechSynthesisEvent,
speechSynthesisErrorEvent: null|SpeechSynthesisErrorEvent,
voices: null|Array,
+ maxLengthExceeded: string,
defaults: {
pitch: Number,
rate: Number,
@@ -302,6 +303,10 @@ var status = function status(s) {
* @param maxTimeout {number}[5000] the maximum timeout to wait for voices in ms
* @param interval {number}[250] the interval in ms to check for voices
* @param quiet {boolean=} prevent rejection on errors, e.g. if no voices
+ * @param maxLengthExceeded {string=} defines what to do, if max text length (4096 bytes) is exceeded:
+ * - 'error' - throw an Error
+ * - 'none' - do nothing; note that some voices may not speak the text at all without any error or warning
+ * - 'warn' - default, raises a warning
* @return {Promise}
* @fulfil {Boolean} true, if initialized, false, if skipped (because already
* initialized)
@@ -321,7 +326,8 @@ EasySpeech.init = function () {
maxTimeout = _ref$maxTimeout === void 0 ? 5000 : _ref$maxTimeout,
_ref$interval = _ref.interval,
interval = _ref$interval === void 0 ? 250 : _ref$interval,
- quiet = _ref.quiet;
+ quiet = _ref.quiet,
+ maxLengthExceeded = _ref.maxLengthExceeded;
return new Promise(function (resolve, reject) {
if (internal.initialized) {
return resolve(false);
@@ -335,6 +341,7 @@ EasySpeech.init = function () {
var timer;
var voicesChangedListener;
var completeCalled = false;
+ internal.maxLengthExceeded = maxLengthExceeded || 'warn';
var fail = function fail(errorMessage) {
status("init: failed (".concat(errorMessage, ")"));
clearInterval(timer);
@@ -680,6 +687,18 @@ EasySpeech.speak = function (_ref3) {
if (!validate.text(text)) {
throw new Error('EasySpeech: at least some valid text is required to speak');
}
+ if (new TextEncoder().encode(text).length > 4096) {
+ var message = 'EasySpeech: text exceeds max length of 4096 bytes, which will not work with some voices.';
+ switch (internal.maxLengthExceeded) {
+ case 'none':
+ break;
+ case 'error':
+ throw new Error(message);
+ case 'warn':
+ default:
+ console.warn(message);
+ }
+ }
var getValue = function getValue(options) {
var _internal$defaults2;
var _Object$entries$ = _slicedToArray(Object.entries(options)[0], 2),
diff --git a/dist/EasySpeech.iife.js b/dist/EasySpeech.iife.js
index 55e7d1c..4479c4f 100644
--- a/dist/EasySpeech.iife.js
+++ b/dist/EasySpeech.iife.js
@@ -172,6 +172,7 @@ var EasySpeech = (function () {
speechSynthesisEvent: null|SpeechSynthesisEvent,
speechSynthesisErrorEvent: null|SpeechSynthesisErrorEvent,
voices: null|Array,
+ maxLengthExceeded: string,
defaults: {
pitch: Number,
rate: Number,
@@ -417,6 +418,10 @@ var EasySpeech = (function () {
* @param maxTimeout {number}[5000] the maximum timeout to wait for voices in ms
* @param interval {number}[250] the interval in ms to check for voices
* @param quiet {boolean=} prevent rejection on errors, e.g. if no voices
+ * @param maxLengthExceeded {string=} defines what to do, if max text length (4096 bytes) is exceeded:
+ * - 'error' - throw an Error
+ * - 'none' - do nothing; note that some voices may not speak the text at all without any error or warning
+ * - 'warn' - default, raises a warning
* @return {Promise}
* @fulfil {Boolean} true, if initialized, false, if skipped (because already
* initialized)
@@ -436,7 +441,8 @@ var EasySpeech = (function () {
maxTimeout = _ref$maxTimeout === void 0 ? 5000 : _ref$maxTimeout,
_ref$interval = _ref.interval,
interval = _ref$interval === void 0 ? 250 : _ref$interval,
- quiet = _ref.quiet;
+ quiet = _ref.quiet,
+ maxLengthExceeded = _ref.maxLengthExceeded;
return new Promise(function (resolve, reject) {
if (internal.initialized) {
return resolve(false);
@@ -450,6 +456,7 @@ var EasySpeech = (function () {
var timer;
var voicesChangedListener;
var completeCalled = false;
+ internal.maxLengthExceeded = maxLengthExceeded || 'warn';
var fail = function fail(errorMessage) {
status("init: failed (".concat(errorMessage, ")"));
clearInterval(timer);
@@ -795,6 +802,18 @@ var EasySpeech = (function () {
if (!validate.text(text)) {
throw new Error('EasySpeech: at least some valid text is required to speak');
}
+ if (new TextEncoder().encode(text).length > 4096) {
+ var message = 'EasySpeech: text exceeds max length of 4096 bytes, which will not work with some voices.';
+ switch (internal.maxLengthExceeded) {
+ case 'none':
+ break;
+ case 'error':
+ throw new Error(message);
+ case 'warn':
+ default:
+ console.warn(message);
+ }
+ }
var getValue = function getValue(options) {
var _internal$defaults2;
var _Object$entries$ = _slicedToArray(Object.entries(options)[0], 2),
diff --git a/dist/EasySpeech.js b/dist/EasySpeech.js
index ebcd1db..96be4d7 100644
--- a/dist/EasySpeech.js
+++ b/dist/EasySpeech.js
@@ -42,6 +42,7 @@ const scope = typeof globalThis === 'undefined' ? window : globalThis;
speechSynthesisEvent: null|SpeechSynthesisEvent,
speechSynthesisErrorEvent: null|SpeechSynthesisErrorEvent,
voices: null|Array,
+ maxLengthExceeded: string,
defaults: {
pitch: Number,
rate: Number,
@@ -282,6 +283,10 @@ const status = s => {
* @param maxTimeout {number}[5000] the maximum timeout to wait for voices in ms
* @param interval {number}[250] the interval in ms to check for voices
* @param quiet {boolean=} prevent rejection on errors, e.g. if no voices
+ * @param maxLengthExceeded {string=} defines what to do, if max text length (4096 bytes) is exceeded:
+ * - 'error' - throw an Error
+ * - 'none' - do nothing; note that some voices may not speak the text at all without any error or warning
+ * - 'warn' - default, raises a warning
* @return {Promise}
* @fulfil {Boolean} true, if initialized, false, if skipped (because already
* initialized)
@@ -295,7 +300,7 @@ const status = s => {
* any voices embedded (example: Chromium on *buntu os')
*/
-EasySpeech.init = function ({ maxTimeout = 5000, interval = 250, quiet } = {}) {
+EasySpeech.init = function ({ maxTimeout = 5000, interval = 250, quiet, maxLengthExceeded } = {}) {
return new Promise((resolve, reject) => {
if (internal.initialized) { return resolve(false) }
EasySpeech.reset();
@@ -308,6 +313,8 @@ EasySpeech.init = function ({ maxTimeout = 5000, interval = 250, quiet } = {}) {
let voicesChangedListener;
let completeCalled = false;
+ internal.maxLengthExceeded = maxLengthExceeded || 'warn';
+
const fail = (errorMessage) => {
status(`init: failed (${errorMessage})`);
clearInterval(timer);
@@ -646,6 +653,19 @@ EasySpeech.speak = ({ text, voice, pitch, rate, volume, force, infiniteResume, .
throw new Error('EasySpeech: at least some valid text is required to speak')
}
+ if ((new TextEncoder().encode(text)).length > 4096) {
+ const message = 'EasySpeech: text exceeds max length of 4096 bytes, which will not work with some voices.';
+ switch (internal.maxLengthExceeded) {
+ case 'none':
+ break
+ case 'error':
+ throw new Error(message)
+ case 'warn':
+ default:
+ console.warn(message);
+ }
+ }
+
const getValue = options => {
const [name, value] = Object.entries(options)[0];
diff --git a/docs/demo.js b/docs/demo.js
index 07cdb22..bfb65f3 100644
--- a/docs/demo.js
+++ b/docs/demo.js
@@ -1 +1 @@
-!function(){"use strict";function e(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function t(t){for(var n=1;n=0;--i){var o=this.tryEntries[i],a=o.completion;if("root"===o.tryLoc)return n("end");if(o.tryLoc<=this.prev){var c=r.call(o,"catchLoc"),u=r.call(o,"finallyLoc");if(c&&u){if(this.prev=0;--n){var i=this.tryEntries[n];if(i.tryLoc<=this.prev&&r.call(i,"finallyLoc")&&this.prev=0;--t){var n=this.tryEntries[t];if(n.finallyLoc===e)return this.complete(n.completion,n.afterLoc),L(n),d}},catch:function(e){for(var t=this.tryEntries.length-1;t>=0;--t){var n=this.tryEntries[t];if(n.tryLoc===e){var r=n.completion;if("throw"===r.type){var i=r.arg;L(n)}return i}}throw new Error("illegal catch attempt")},delegateYield:function(e,t,n){return this.delegate={iterator:k(e),resultName:t,nextLoc:n},"next"===this.method&&(this.arg=void 0),d}},e}function r(e){return r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},r(e)}function i(e,t,n,r,i,o,a){try{var c=e[o](a),u=c.value}catch(e){return void n(e)}c.done?t(u):Promise.resolve(u).then(r,i)}function o(e){return function(){var t=this,n=arguments;return new Promise((function(r,o){var a=e.apply(t,n);function c(e){i(a,r,o,c,u,"next",e)}function u(e){i(a,r,o,c,u,"throw",e)}c(void 0)}))}}function a(e,t,n){return(t=function(e){var t=function(e,t){if("object"!=typeof e||null===e)return e;var n=e[Symbol.toPrimitive];if(void 0!==n){var r=n.call(e,t||"default");if("object"!=typeof r)return r;throw new TypeError("@@toPrimitive must return a primitive value.")}return("string"===t?String:Number)(e)}(e,"string");return"symbol"==typeof t?t:String(t)}(t))in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function c(e,t){if(null==e)return{};var n,r,i=function(e,t){if(null==e)return{};var n,r,i={},o=Object.keys(e);for(r=0;r=0||(i[n]=e[n]);return i}(e,t);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(i[n]=e[n])}return i}function u(e,t){return function(e){if(Array.isArray(e))return e}(e)||function(e,t){var n=null==e?null:"undefined"!=typeof Symbol&&e[Symbol.iterator]||e["@@iterator"];if(null!=n){var r,i,o,a,c=[],u=!0,s=!1;try{if(o=(n=n.call(e)).next,0===t){if(Object(n)!==n)return;u=!1}else for(;!(u=(r=o.call(n)).done)&&(c.push(r.value),c.length!==t);u=!0);}catch(e){s=!0,i=e}finally{try{if(!u&&null!=n.return&&(a=n.return(),Object(a)!==a))return}finally{if(s)throw i}}return c}}(e,t)||function(e,t){if(!e)return;if("string"==typeof e)return s(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);"Object"===n&&e.constructor&&(n=e.constructor.name);if("Map"===n||"Set"===n)return Array.from(e);if("Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))return s(e,t)}(e,t)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function s(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n0&&void 0!==arguments[0]?arguments[0]:{},t=arguments.length>1?arguments[1]:void 0;return Object.hasOwnProperty.call(e,t)||t in e||!!e[t]},g=function(){return(d.navigator||{}).userAgent||""},b=function(){return/android/i.test(g())},w=function(){return/kaios/i.test(g())},S=function(){return void 0!==d.InstallTrigger||/firefox/i.test(g())},x=function(){return void 0!==d.GestureEvent},E=["webKit","moz","ms","o"],O=function(e){var t,n="".concat((t=e).charAt(0).toUpperCase()).concat(t.slice(1)),r=E.map((function(e){return"".concat(e).concat(n)})),i=[e,n].concat(r).find(L);return d[i]},L=function(e){return d[e]};f.status=function(){return t({},v)};var j=function(e){p(e),v.status=e};f.init=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=e.maxTimeout,n=void 0===t?5e3:t,r=e.interval,i=void 0===r?250:r,o=e.quiet;return new Promise((function(e,t){if(v.initialized)return e(!1);var r,a;f.reset(),j("init: start");var c=!1,u=function(n){return j("init: failed (".concat(n,")")),clearInterval(r),v.initialized=!1,o?e(!1):t(new Error("EasySpeech: ".concat(n)))},s=function(){if(!c)return j("init: complete"),c=!0,v.initialized=!0,clearInterval(r),h.onvoiceschanged=null,a&&h.removeEventListener("voiceschanged",a),e(!0)},l=y();if(!(!!l.speechSynthesis&&!!l.speechSynthesisUtterance))return u("browser misses features");Object.keys(l).forEach((function(e){v[e]=l[e]}));var h=v.speechSynthesis,p=function(){var e=h.getVoices()||[];if(e.length>0){if(v.voices=e,j("voices loaded: ".concat(e.length)),v.defaultVoice=e.find((function(e){return e.default})),!v.defaultVoice){var t=((d.navigator||{}).language||"").split("-")[0];v.defaultVoice=e.find((function(e){return e.lang&&(e.lang.indexOf("".concat(t,"-"))>-1||e.lang.indexOf("".concat(t,"_"))>-1)}))}return v.defaultVoice||(v.defaultVoice=e[0]),!0}return!1};if(j("init: voices"),p())return s();var g=function(){j("init: voices (timer)");var e=0;r=setInterval((function(){return p()?s():e>n?u("browser has no voices (timeout)"):void(e+=i)}),i)};l.onvoiceschanged?(j("init: voices (onvoiceschanged)"),h.onvoiceschanged=function(){return p()?s():g()},setTimeout((function(){return p()?s():u("browser has no voices (timeout)")}),n)):(m(h,"addEventListener")&&(j("init: voices (addEventListener)"),a=function(){if(p())return s()},h.addEventListener("voiceschanged",a)),g())}))};var k=function(){if(!(arguments.length>0&&void 0!==arguments[0]?arguments[0]:{}).force&&!v.initialized)throw new Error("EasySpeech: not initialized. Run EasySpeech.init() first")};f.voices=function(){return k(),v.voices},f.on=function(e){return k(),N.forEach((function(t){var n=e[t];P.handler(n)&&(v.handlers[t]=n)})),t({},v.handlers)};var N=["boundary","end","error","mark","pause","resume","start"],P={isNumber:function(e){return"number"==typeof e&&!Number.isNaN(e)},pitch:function(e){return P.isNumber(e)&&e>=0&&e<=2},volume:function(e){return P.isNumber(e)&&e>=0&&e<=1},rate:function(e){return P.isNumber(e)&&e>=.1&&e<=10},text:function(e){return"string"==typeof e},handler:function(e){return"function"==typeof e},voice:function(e){return e&&e.lang&&e.name&&e.voiceURI}};f.defaults=function(e){return k(),e&&(v.defaults=v.defaults||{},["voice","pitch","rate","volume"].forEach((function(t){var n=e[t];(0,P[t])(n)&&(v.defaults[t]=n)}))),t({},v.defaults)};f.speak=function(e){var t=e.text,n=e.voice,r=e.pitch,i=e.rate,o=e.volume,a=e.force,s=e.infiniteResume,f=c(e,l);if(k({force:a}),!P.text(t))throw new Error("EasySpeech: at least some valid text is required to speak");var d=function(e){var t,n=u(Object.entries(e)[0],2),r=n[0],i=n[1];return P[r](i)?i:null===(t=v.defaults)||void 0===t?void 0:t[r]};return new Promise((function(e,a){j("init speak");var c=function(e){return new(0,v.speechSynthesisUtterance)(e)}(t),u=function(e){var t,n;return e||(null===(t=v.defaults)||void 0===t?void 0:t.voice)||v.defaultVoice||(null===(n=v.voices)||void 0===n?void 0:n[0])}(n);u&&(c.voice=u,c.lang=u.lang,c.voiceURI=u.voiceURI),c.text=t,c.pitch=d({pitch:r}),c.rate=d({rate:i}),c.volume=d({volume:o}),I(c),N.forEach((function(e){var t,n=f[e];P.handler(n)&&c.addEventListener(e,n),null!==(t=v.handlers)&&void 0!==t&&t[e]&&c.addEventListener(e,v.handlers[e])})),c.addEventListener("start",(function(){h.paused=!1,h.speaking=!0,("boolean"==typeof s?s:!h.isFirefox&&!h.isSafari&&!0!==h.isAndroid)&&q(c)})),c.addEventListener("end",(function(t){j("speak complete"),h.paused=!1,h.speaking=!1,clearTimeout(T),e(t)})),c.addEventListener("error",(function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};j("speak failed: ".concat(e.message)),h.paused=!1,h.speaking=!1,clearTimeout(T),a(e)})),clearTimeout(T),v.speechSynthesis.cancel(),setTimeout((function(){v.speechSynthesis.speak(c)}),10)}))};var T,A,C,I=function(e){var t=e.voice,n=e.pitch,r=e.rate,i=e.volume;p("utterance: voice=".concat(null==t?void 0:t.name," volume=").concat(i," rate=").concat(r," pitch=").concat(n))};function q(e){if(!e&&T)return p("force-clear timeout"),d.clearTimeout(T);var t=v.speechSynthesis,n=t.paused,r=t.speaking||h.speaking,i=n||h.paused;p("resumeInfinity isSpeaking=".concat(r," isPaused=").concat(i)),r&&!i&&(v.speechSynthesis.pause(),v.speechSynthesis.resume()),T=d.setTimeout((function(){q(e)}),5e3)}f.cancel=function(){k(),j("cancelling"),v.speechSynthesis.cancel(),h.paused=!1,h.speaking=!1},f.resume=function(){k(),j("resuming"),h.paused=!1,h.speaking=!0,v.speechSynthesis.resume()},f.pause=function(){if(k(),j("pausing"),h.isAndroid)return p("patch pause on Android with cancel"),v.speechSynthesis.cancel();v.speechSynthesis.pause(),h.paused=!0,h.speaking=!1},f.reset=function(){Object.assign(v,{status:"reset",initialized:!1,speechSynthesis:null,speechSynthesisUtterance:null,speechSynthesisVoice:null,speechSynthesisEvent:null,speechSynthesisErrorEvent:null,voices:null,defaultVoice:null,defaults:{pitch:1,rate:1,volume:1,voice:null},handlers:{}})},document.body.onload=o(n().mark((function e(){var t;return n().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return R(),$(f.detect()),e.next=4,z();case 4:return t=e.sent,e.next=7,D(t);case 7:return F(t),e.next=10,M(t);case 10:B(t);case 11:case"end":return e.stop()}}),e)})));var _={voice:void 0,rate:void 0,pitch:void 0,volume:void 0,text:void 0},U={volume:void 0,rate:void 0,pitch:void 0,text:void 0,language:void 0,voice:void 0};function F(e){if(e){var t=document.querySelector(".volume-value");U.volume=document.querySelector("#volume-input"),U.volume.disabled=!1,U.volume.addEventListener("change",(function(e){_.volume=Number(e.target.value),t.removeChild(t.firstChild),t.appendChild(document.createTextNode(_.volume))}));var n=document.querySelector(".rate-value");U.rate=document.querySelector("#rate-input"),U.rate.disabled=!1,U.rate.addEventListener("change",(function(e){_.rate=Number(e.target.value)/10,n.removeChild(n.firstChild),n.appendChild(document.createTextNode(_.rate))}));var r=document.querySelector(".pitch-value");U.pitch=document.querySelector("#pitch-input"),U.pitch.disabled=!1,U.pitch.addEventListener("change",(function(e){_.pitch=Number(e.target.value),r.removeChild(r.firstChild),r.appendChild(document.createTextNode(_.pitch))})),U.text=document.querySelector("#text-input"),U.text.disabled=!1}}function R(){A=document.querySelector(".log-body"),f.debug(V)}function V(e){A.appendChild(H(e))}function z(){return G.apply(this,arguments)}function G(){return(G=o(n().mark((function e(){var t,r,i,o,a,c,u,s,l;return n().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return t=document.querySelector(".init-status-header"),r=document.querySelector(".init-status-loader"),i=document.querySelector(".init-status-text"),o=document.querySelector(".init-status-body"),e.prev=4,e.next=7,f.init();case 7:a=e.sent,c="Successfully intialized 🎉",u="Successful",e.next=20;break;case 12:e.prev=12,e.t0=e.catch(4),a=!1,c=e.t0.message,u="Failed",(s=document.querySelector(".speak-btn")).classList.add("disabled"),s.setAttribute("disabled","");case 20:return e.prev=20,l=a?"bg-success":"bg-danger",r.classList.add("d-none"),t.classList.remove("bg-info"),t.classList.add(l),i.textContent=u,o.appendChild(H(c)),e.finish(20);case 28:return e.abrupt("return",a);case 29:case"end":return e.stop()}}),e,null,[[4,12,20,28]])})))).apply(this,arguments)}function D(e){return Y.apply(this,arguments)}function Y(){return(Y=o(n().mark((function e(t){var r,i,o,a;return n().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:if(t){e.next=2;break}return e.abrupt("return");case 2:V("find unique languages..."),r=f.voices(),i=new Set,r.forEach((function(e,t){var n=e.lang.split(/[-_]/)[0];i.add(n),e.default&&(o=n,a=e.voiceURI)})),V("found ".concat(i.size," languages")),V("populate languages to select component"),U.language=document.querySelector("#lang-select"),Array.from(i).sort().forEach((function(e){var t=H(e,"option");t.setAttribute("value",e),o&&e===o&&(t.setAttribute("selected",""),setTimeout((function(){return J(r,e,a)}),250),setTimeout((function(){K(C.findIndex((function(e){return e.voiceURI===a}))+1)}),500)),U.language.appendChild(t)})),V("attach events, cleanup"),U.voice=document.querySelector("#voice-select"),U.language.addEventListener("change",(function(e){return J(r,e.target.value)})),U.voice.addEventListener("change",(function(e){K(Number.parseInt(e.target.value,10))})),U.language.classList.remove("disabled"),U.language.removeAttribute("disabled");case 16:case"end":return e.stop()}}),e)})))).apply(this,arguments)}function J(e,t,n){for(;U.voice.firstChild;)U.voice.removeChild(U.voice.lastChild);U.voice.appendChild(H("(Select voice)","option")),t?((C="all"===t?e:e.filter((function(e){return e.lang.indexOf("".concat(t,"-"))>-1||e.lang.indexOf("".concat(t,"_"))>-1})).sort((function(e,t){return e.name.localeCompare(t.name)}))).forEach((function(e,t){var r=e.localService?"local":"remote",i=e.default?"[DEFAULT]":"",o="".concat(i).concat(e.name," - ").concat(e.voiceURI," (").concat(r,")"),a=H(o,"option");a.setAttribute("value",t.toString(10)),n&&n===e.voiceURI&&a.setAttribute("selected",""),U.voice.appendChild(a)})),U.voice.classList.remove("disabled"),U.voice.removeAttribute("disabled")):(U.voice.classList.add("disabled"),U.voice.disabled=!0,_.voice=null,C=null)}function K(e){e<0||e>C.length-1?_.voice=void 0:_.voice=(C||[])[e]}function M(e){if(e){var r=document.querySelector(".speak-btn"),i=Object.values(U);r.addEventListener("click",function(){var e=o(n().mark((function e(o){var a,c,u,s,l,d;return n().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return r.disabled=!0,i.forEach((function(e){e.disabled=!0})),a=t({},_),c=a.pitch,u=a.rate,s=a.voice,l=a.volume,d=U.text.value,e.prev=4,e.next=7,f.speak({text:d,pitch:c,rate:u,voice:s,volume:l});case 7:e.next=12;break;case 9:e.prev=9,e.t0=e.catch(4),V(e.t0.message);case 12:return e.prev=12,r.disabled=!1,i.forEach((function(e){e.disabled=!1})),e.finish(12);case 16:case"end":return e.stop()}}),e,null,[[4,9,12,16]])})));return function(t){return e.apply(this,arguments)}}())}}function $(e){var t=document.querySelector(".features"),n={};Object.entries(e).forEach((function(e){var t=u(e,2),i=t[0],o=t[1];"object"===r(o)?n[i]=o.toString():n[i]="function"==typeof o?o.name:o}));var i=document.createTextNode(JSON.stringify(n,null,2));t.appendChild(i)}function B(e){if(e){var t=function(e){return V("event: ".concat(e.type))};f.on({boundary:t,start:t,end:t,error:t})}}var H=function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"div",n=document.createElement(t);return n.appendChild(document.createTextNode(e)),n}}();
+!function(){"use strict";function e(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function t(t){for(var n=1;n=0;--o){var a=this.tryEntries[o],c=a.completion;if("root"===a.tryLoc)return r("end");if(a.tryLoc<=this.prev){var u=i.call(a,"catchLoc"),s=i.call(a,"finallyLoc");if(u&&s){if(this.prev=0;--n){var r=this.tryEntries[n];if(r.tryLoc<=this.prev&&i.call(r,"finallyLoc")&&this.prev=0;--t){var n=this.tryEntries[t];if(n.finallyLoc===e)return this.complete(n.completion,n.afterLoc),P(n),m}},catch:function(e){for(var t=this.tryEntries.length-1;t>=0;--t){var n=this.tryEntries[t];if(n.tryLoc===e){var r=n.completion;if("throw"===r.type){var i=r.arg;P(n)}return i}}throw new Error("illegal catch attempt")},delegateYield:function(t,n,r){return this.delegate={iterator:C(t),resultName:n,nextLoc:r},"next"===this.method&&(this.arg=e),m}},t}function r(e){return r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},r(e)}function i(e,t,n,r,i,o,a){try{var c=e[o](a),u=c.value}catch(e){return void n(e)}c.done?t(u):Promise.resolve(u).then(r,i)}function o(e){return function(){var t=this,n=arguments;return new Promise((function(r,o){var a=e.apply(t,n);function c(e){i(a,r,o,c,u,"next",e)}function u(e){i(a,r,o,c,u,"throw",e)}c(void 0)}))}}function a(e,t,n){return(t=function(e){var t=function(e,t){if("object"!=typeof e||null===e)return e;var n=e[Symbol.toPrimitive];if(void 0!==n){var r=n.call(e,t||"default");if("object"!=typeof r)return r;throw new TypeError("@@toPrimitive must return a primitive value.")}return("string"===t?String:Number)(e)}(e,"string");return"symbol"==typeof t?t:String(t)}(t))in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function c(e,t){if(null==e)return{};var n,r,i=function(e,t){if(null==e)return{};var n,r,i={},o=Object.keys(e);for(r=0;r=0||(i[n]=e[n]);return i}(e,t);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(i[n]=e[n])}return i}function u(e,t){return function(e){if(Array.isArray(e))return e}(e)||function(e,t){var n=null==e?null:"undefined"!=typeof Symbol&&e[Symbol.iterator]||e["@@iterator"];if(null!=n){var r,i,o,a,c=[],u=!0,s=!1;try{if(o=(n=n.call(e)).next,0===t){if(Object(n)!==n)return;u=!1}else for(;!(u=(r=o.call(n)).done)&&(c.push(r.value),c.length!==t);u=!0);}catch(e){s=!0,i=e}finally{try{if(!u&&null!=n.return&&(a=n.return(),Object(a)!==a))return}finally{if(s)throw i}}return c}}(e,t)||function(e,t){if(!e)return;if("string"==typeof e)return s(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);"Object"===n&&e.constructor&&(n=e.constructor.name);if("Map"===n||"Set"===n)return Array.from(e);if("Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))return s(e,t)}(e,t)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function s(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n0&&void 0!==arguments[0]?arguments[0]:{},t=arguments.length>1?arguments[1]:void 0;return Object.hasOwnProperty.call(e,t)||t in e||!!e[t]},g=function(){return(d.navigator||{}).userAgent||""},b=function(){return/android/i.test(g())},w=function(){return/kaios/i.test(g())},S=function(){return void 0!==d.InstallTrigger||/firefox/i.test(g())},x=function(){return void 0!==d.GestureEvent},E=["webKit","moz","ms","o"],O=function(e){var t,n="".concat((t=e).charAt(0).toUpperCase()).concat(t.slice(1)),r=E.map((function(e){return"".concat(e).concat(n)})),i=[e,n].concat(r).find(L);return d[i]},L=function(e){return d[e]};f.status=function(){return t({},v)};var j=function(e){p(e),v.status=e};f.init=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=e.maxTimeout,n=void 0===t?5e3:t,r=e.interval,i=void 0===r?250:r,o=e.quiet,a=e.maxLengthExceeded;return new Promise((function(e,t){if(v.initialized)return e(!1);var r,c;f.reset(),j("init: start");var u=!1;v.maxLengthExceeded=a||"warn";var s=function(n){return j("init: failed (".concat(n,")")),clearInterval(r),v.initialized=!1,o?e(!1):t(new Error("EasySpeech: ".concat(n)))},l=function(){if(!u)return j("init: complete"),u=!0,v.initialized=!0,clearInterval(r),p.onvoiceschanged=null,c&&p.removeEventListener("voiceschanged",c),e(!0)},h=y();if(!(!!h.speechSynthesis&&!!h.speechSynthesisUtterance))return s("browser misses features");Object.keys(h).forEach((function(e){v[e]=h[e]}));var p=v.speechSynthesis,g=function(){var e=p.getVoices()||[];if(e.length>0){if(v.voices=e,j("voices loaded: ".concat(e.length)),v.defaultVoice=e.find((function(e){return e.default})),!v.defaultVoice){var t=((d.navigator||{}).language||"").split("-")[0];v.defaultVoice=e.find((function(e){return e.lang&&(e.lang.indexOf("".concat(t,"-"))>-1||e.lang.indexOf("".concat(t,"_"))>-1)}))}return v.defaultVoice||(v.defaultVoice=e[0]),!0}return!1};if(j("init: voices"),g())return l();var b=function(){j("init: voices (timer)");var e=0;r=setInterval((function(){return g()?l():e>n?s("browser has no voices (timeout)"):void(e+=i)}),i)};h.onvoiceschanged?(j("init: voices (onvoiceschanged)"),p.onvoiceschanged=function(){return g()?l():b()},setTimeout((function(){return g()?l():s("browser has no voices (timeout)")}),n)):(m(p,"addEventListener")&&(j("init: voices (addEventListener)"),c=function(){if(g())return l()},p.addEventListener("voiceschanged",c)),b())}))};var k=function(){if(!(arguments.length>0&&void 0!==arguments[0]?arguments[0]:{}).force&&!v.initialized)throw new Error("EasySpeech: not initialized. Run EasySpeech.init() first")};f.voices=function(){return k(),v.voices},f.on=function(e){return k(),T.forEach((function(t){var n=e[t];N.handler(n)&&(v.handlers[t]=n)})),t({},v.handlers)};var T=["boundary","end","error","mark","pause","resume","start"],N={isNumber:function(e){return"number"==typeof e&&!Number.isNaN(e)},pitch:function(e){return N.isNumber(e)&&e>=0&&e<=2},volume:function(e){return N.isNumber(e)&&e>=0&&e<=1},rate:function(e){return N.isNumber(e)&&e>=.1&&e<=10},text:function(e){return"string"==typeof e},handler:function(e){return"function"==typeof e},voice:function(e){return e&&e.lang&&e.name&&e.voiceURI}};f.defaults=function(e){return k(),e&&(v.defaults=v.defaults||{},["voice","pitch","rate","volume"].forEach((function(t){var n=e[t];(0,N[t])(n)&&(v.defaults[t]=n)}))),t({},v.defaults)};f.speak=function(e){var t=e.text,n=e.voice,r=e.pitch,i=e.rate,o=e.volume,a=e.force,s=e.infiniteResume,f=c(e,l);if(k({force:a}),!N.text(t))throw new Error("EasySpeech: at least some valid text is required to speak");if((new TextEncoder).encode(t).length>4096){var d="EasySpeech: text exceeds max length of 4096 bytes, which will not work with some voices.";switch(v.maxLengthExceeded){case"none":break;case"error":throw new Error(d);default:console.warn(d)}}var p=function(e){var t,n=u(Object.entries(e)[0],2),r=n[0],i=n[1];return N[r](i)?i:null===(t=v.defaults)||void 0===t?void 0:t[r]};return new Promise((function(e,a){j("init speak");var c=function(e){return new(0,v.speechSynthesisUtterance)(e)}(t),u=function(e){var t,n;return e||(null===(t=v.defaults)||void 0===t?void 0:t.voice)||v.defaultVoice||(null===(n=v.voices)||void 0===n?void 0:n[0])}(n);u&&(c.voice=u,c.lang=u.lang,c.voiceURI=u.voiceURI),c.text=t,c.pitch=p({pitch:r}),c.rate=p({rate:i}),c.volume=p({volume:o}),I(c),T.forEach((function(e){var t,n=f[e];N.handler(n)&&c.addEventListener(e,n),null!==(t=v.handlers)&&void 0!==t&&t[e]&&c.addEventListener(e,v.handlers[e])})),c.addEventListener("start",(function(){h.paused=!1,h.speaking=!0,("boolean"==typeof s?s:!h.isFirefox&&!h.isSafari&&!0!==h.isAndroid)&&q(c)})),c.addEventListener("end",(function(t){j("speak complete"),h.paused=!1,h.speaking=!1,clearTimeout(P),e(t)})),c.addEventListener("error",(function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};j("speak failed: ".concat(e.message)),h.paused=!1,h.speaking=!1,clearTimeout(P),a(e)})),clearTimeout(P),v.speechSynthesis.cancel(),setTimeout((function(){v.speechSynthesis.speak(c)}),10)}))};var P,A,C,I=function(e){var t=e.voice,n=e.pitch,r=e.rate,i=e.volume;p("utterance: voice=".concat(null==t?void 0:t.name," volume=").concat(i," rate=").concat(r," pitch=").concat(n))};function q(e){if(!e&&P)return p("force-clear timeout"),d.clearTimeout(P);var t=v.speechSynthesis,n=t.paused,r=t.speaking||h.speaking,i=n||h.paused;p("resumeInfinity isSpeaking=".concat(r," isPaused=").concat(i)),r&&!i&&(v.speechSynthesis.pause(),v.speechSynthesis.resume()),P=d.setTimeout((function(){q(e)}),5e3)}f.cancel=function(){k(),j("cancelling"),v.speechSynthesis.cancel(),h.paused=!1,h.speaking=!1},f.resume=function(){k(),j("resuming"),h.paused=!1,h.speaking=!0,v.speechSynthesis.resume()},f.pause=function(){if(k(),j("pausing"),h.isAndroid)return p("patch pause on Android with cancel"),v.speechSynthesis.cancel();v.speechSynthesis.pause(),h.paused=!0,h.speaking=!1},f.reset=function(){Object.assign(v,{status:"reset",initialized:!1,speechSynthesis:null,speechSynthesisUtterance:null,speechSynthesisVoice:null,speechSynthesisEvent:null,speechSynthesisErrorEvent:null,voices:null,defaultVoice:null,defaults:{pitch:1,rate:1,volume:1,voice:null},handlers:{}})},document.body.onload=o(n().mark((function e(){var t;return n().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return R(),$(f.detect()),e.next=4,z();case 4:return t=e.sent,e.next=7,D(t);case 7:return F(t),e.next=10,M(t);case 10:B(t);case 11:case"end":return e.stop()}}),e)})));var _={voice:void 0,rate:void 0,pitch:void 0,volume:void 0,text:void 0},U={volume:void 0,rate:void 0,pitch:void 0,text:void 0,language:void 0,voice:void 0};function F(e){if(e){var t=document.querySelector(".volume-value");U.volume=document.querySelector("#volume-input"),U.volume.disabled=!1,U.volume.addEventListener("change",(function(e){_.volume=Number(e.target.value),t.removeChild(t.firstChild),t.appendChild(document.createTextNode(_.volume))}));var n=document.querySelector(".rate-value");U.rate=document.querySelector("#rate-input"),U.rate.disabled=!1,U.rate.addEventListener("change",(function(e){_.rate=Number(e.target.value)/10,n.removeChild(n.firstChild),n.appendChild(document.createTextNode(_.rate))}));var r=document.querySelector(".pitch-value");U.pitch=document.querySelector("#pitch-input"),U.pitch.disabled=!1,U.pitch.addEventListener("change",(function(e){_.pitch=Number(e.target.value),r.removeChild(r.firstChild),r.appendChild(document.createTextNode(_.pitch))})),U.text=document.querySelector("#text-input"),U.text.disabled=!1}}function R(){A=document.querySelector(".log-body"),f.debug(V)}function V(e){A.appendChild(H(e))}function z(){return G.apply(this,arguments)}function G(){return(G=o(n().mark((function e(){var t,r,i,o,a,c,u,s,l;return n().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return t=document.querySelector(".init-status-header"),r=document.querySelector(".init-status-loader"),i=document.querySelector(".init-status-text"),o=document.querySelector(".init-status-body"),e.prev=4,e.next=7,f.init();case 7:a=e.sent,c="Successfully intialized 🎉",u="Successful",e.next=20;break;case 12:e.prev=12,e.t0=e.catch(4),a=!1,c=e.t0.message,u="Failed",(s=document.querySelector(".speak-btn")).classList.add("disabled"),s.setAttribute("disabled","");case 20:return e.prev=20,l=a?"bg-success":"bg-danger",r.classList.add("d-none"),t.classList.remove("bg-info"),t.classList.add(l),i.textContent=u,o.appendChild(H(c)),e.finish(20);case 28:return e.abrupt("return",a);case 29:case"end":return e.stop()}}),e,null,[[4,12,20,28]])})))).apply(this,arguments)}function D(e){return Y.apply(this,arguments)}function Y(){return(Y=o(n().mark((function e(t){var r,i,o,a;return n().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:if(t){e.next=2;break}return e.abrupt("return");case 2:V("find unique languages..."),r=f.voices(),i=new Set,r.forEach((function(e,t){var n=e.lang.split(/[-_]/)[0];i.add(n),e.default&&(o=n,a=e.voiceURI)})),V("found ".concat(i.size," languages")),V("populate languages to select component"),U.language=document.querySelector("#lang-select"),Array.from(i).sort().forEach((function(e){var t=H(e,"option");t.setAttribute("value",e),o&&e===o&&(t.setAttribute("selected",""),setTimeout((function(){return J(r,e,a)}),250),setTimeout((function(){K(C.findIndex((function(e){return e.voiceURI===a}))+1)}),500)),U.language.appendChild(t)})),V("attach events, cleanup"),U.voice=document.querySelector("#voice-select"),U.language.addEventListener("change",(function(e){return J(r,e.target.value)})),U.voice.addEventListener("change",(function(e){K(Number.parseInt(e.target.value,10))})),U.language.classList.remove("disabled"),U.language.removeAttribute("disabled");case 16:case"end":return e.stop()}}),e)})))).apply(this,arguments)}function J(e,t,n){for(;U.voice.firstChild;)U.voice.removeChild(U.voice.lastChild);U.voice.appendChild(H("(Select voice)","option")),t?((C="all"===t?e:e.filter((function(e){return e.lang.indexOf("".concat(t,"-"))>-1||e.lang.indexOf("".concat(t,"_"))>-1})).sort((function(e,t){return e.name.localeCompare(t.name)}))).forEach((function(e,t){var r=e.localService?"local":"remote",i=e.default?"[DEFAULT]":"",o="".concat(i).concat(e.name," - ").concat(e.voiceURI," (").concat(r,")"),a=H(o,"option");a.setAttribute("value",t.toString(10)),n&&n===e.voiceURI&&a.setAttribute("selected",""),U.voice.appendChild(a)})),U.voice.classList.remove("disabled"),U.voice.removeAttribute("disabled")):(U.voice.classList.add("disabled"),U.voice.disabled=!0,_.voice=null,C=null)}function K(e){e<0||e>C.length-1?_.voice=void 0:_.voice=(C||[])[e]}function M(e){if(e){var r=document.querySelector(".speak-btn"),i=Object.values(U);r.addEventListener("click",function(){var e=o(n().mark((function e(o){var a,c,u,s,l,d;return n().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return r.disabled=!0,i.forEach((function(e){e.disabled=!0})),a=t({},_),c=a.pitch,u=a.rate,s=a.voice,l=a.volume,d=U.text.value,e.prev=4,e.next=7,f.speak({text:d,pitch:c,rate:u,voice:s,volume:l});case 7:e.next=12;break;case 9:e.prev=9,e.t0=e.catch(4),V(e.t0.message);case 12:return e.prev=12,r.disabled=!1,i.forEach((function(e){e.disabled=!1})),e.finish(12);case 16:case"end":return e.stop()}}),e,null,[[4,9,12,16]])})));return function(t){return e.apply(this,arguments)}}())}}function $(e){var t=document.querySelector(".features"),n={};Object.entries(e).forEach((function(e){var t=u(e,2),i=t[0],o=t[1];"object"===r(o)?n[i]=o.toString():n[i]="function"==typeof o?o.name:o}));var i=document.createTextNode(JSON.stringify(n,null,2));t.appendChild(i)}function B(e){if(e){var t=function(e){return V("event: ".concat(e.type))};f.on({boundary:t,start:t,end:t,error:t})}}var H=function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"div",n=document.createElement(t);return n.appendChild(document.createTextNode(e)),n}}();
diff --git a/src/EasySpeech.js b/src/EasySpeech.js
index bad786d..fac4c6d 100644
--- a/src/EasySpeech.js
+++ b/src/EasySpeech.js
@@ -42,6 +42,7 @@ const scope = typeof globalThis === 'undefined' ? window : globalThis
speechSynthesisEvent: null|SpeechSynthesisEvent,
speechSynthesisErrorEvent: null|SpeechSynthesisErrorEvent,
voices: null|Array,
+ maxLengthExceeded: string,
defaults: {
pitch: Number,
rate: Number,
@@ -282,6 +283,10 @@ const status = s => {
* @param maxTimeout {number}[5000] the maximum timeout to wait for voices in ms
* @param interval {number}[250] the interval in ms to check for voices
* @param quiet {boolean=} prevent rejection on errors, e.g. if no voices
+ * @param maxLengthExceeded {string=} defines what to do, if max text length (4096 bytes) is exceeded:
+ * - 'error' - throw an Error
+ * - 'none' - do nothing; note that some voices may not speak the text at all without any error or warning
+ * - 'warn' - default, raises a warning
* @return {Promise}
* @fulfil {Boolean} true, if initialized, false, if skipped (because already
* initialized)
@@ -295,7 +300,7 @@ const status = s => {
* any voices embedded (example: Chromium on *buntu os')
*/
-EasySpeech.init = function ({ maxTimeout = 5000, interval = 250, quiet } = {}) {
+EasySpeech.init = function ({ maxTimeout = 5000, interval = 250, quiet, maxLengthExceeded } = {}) {
return new Promise((resolve, reject) => {
if (internal.initialized) { return resolve(false) }
EasySpeech.reset()
@@ -308,6 +313,8 @@ EasySpeech.init = function ({ maxTimeout = 5000, interval = 250, quiet } = {}) {
let voicesChangedListener
let completeCalled = false
+ internal.maxLengthExceeded = maxLengthExceeded || 'warn'
+
const fail = (errorMessage) => {
status(`init: failed (${errorMessage})`)
clearInterval(timer)
@@ -646,6 +653,19 @@ EasySpeech.speak = ({ text, voice, pitch, rate, volume, force, infiniteResume, .
throw new Error('EasySpeech: at least some valid text is required to speak')
}
+ if ((new TextEncoder().encode(text)).length > 4096) {
+ const message = 'EasySpeech: text exceeds max length of 4096 bytes, which will not work with some voices.'
+ switch (internal.maxLengthExceeded) {
+ case 'none':
+ break
+ case 'error':
+ throw new Error(message)
+ case 'warn':
+ default:
+ console.warn(message)
+ }
+ }
+
const getValue = options => {
const [name, value] = Object.entries(options)[0]
diff --git a/tests/test-helpers.js b/tests/test-helpers.js
index a1260aa..0f993b1 100644
--- a/tests/test-helpers.js
+++ b/tests/test-helpers.js
@@ -33,6 +33,8 @@ export const createUtteranceClass = () => {
this.onstart = null
this.onerror = null
}
+
+ addEventListener () {}
}
c.prototype.onend = null
diff --git a/tests/unit.tests.js b/tests/unit.tests.js
index 68800d4..497a245 100644
--- a/tests/unit.tests.js
+++ b/tests/unit.tests.js
@@ -7,6 +7,7 @@ import {
initScope,
createUtteranceClass
} from './test-helpers.js'
+import sinon from 'sinon/pkg/sinon-esm.js'
describe('unit tests', function () {
afterEach(function () {
@@ -210,7 +211,8 @@ describe('unit tests', function () {
onpause: false,
onresume: false,
onstart: false,
- onvoiceschanged: false
+ onvoiceschanged: false,
+ maxLengthExceeded: 'warn'
})
})
@@ -251,7 +253,8 @@ describe('unit tests', function () {
onpause: false,
onresume: false,
onstart: false,
- onvoiceschanged: false
+ onvoiceschanged: false,
+ maxLengthExceeded: 'warn'
})
done()
})
@@ -299,7 +302,8 @@ describe('unit tests', function () {
onpause: false,
onresume: false,
onstart: false,
- onvoiceschanged: true
+ onvoiceschanged: true,
+ maxLengthExceeded: 'warn'
})
done()
})
@@ -363,7 +367,8 @@ describe('unit tests', function () {
onpause: false,
onresume: false,
onstart: false,
- onvoiceschanged: false
+ onvoiceschanged: false,
+ maxLengthExceeded: 'warn'
})
expect(listener).to.equal(null)
expect(listenerAdded).to.equal(true)
@@ -510,6 +515,97 @@ describe('unit tests', function () {
done()
})
})
+ it('ignores if text exceeds 4096 bytes length', function (done) {
+ const spy = sinon.spy(console, 'warn')
+ const SpeechSynthesisUtterance = createUtteranceClass()
+ const id = randomId()
+ const speechSynthesis = {
+ getVoices: () => [{ id }],
+ cancel: () => {},
+ speak: function () {
+ expect(spy.calledWith('EasySpeech: text exceeds max length of 4096 bytes, which will not work with some voices.'))
+ .to.equal(false)
+ spy.restore()
+ done()
+ }
+ }
+ globalThis.SpeechSynthesisUtterance = SpeechSynthesisUtterance
+ globalThis.speechSynthesis = speechSynthesis
+ EasySpeech.init({ maxLengthExceeded: 'none' })
+ .catch(done)
+ .then(() => {
+ const buffer = Buffer.alloc(4097, '0')
+ const decoder = new TextDecoder('UTF-8')
+ const text = decoder.decode(buffer)
+ EasySpeech.init()
+ .catch(done)
+ .then(() => {
+ EasySpeech.speak({ text }).catch(done)
+ })
+ })
+ })
+ it('warns if text exceeds 4096 bytes length', function (done) {
+ const spy = sinon.spy(console, 'warn')
+ const SpeechSynthesisUtterance = createUtteranceClass()
+ const id = randomId()
+ const speechSynthesis = {
+ getVoices: () => [{ id }],
+ cancel: () => {},
+ speak: function () {
+ expect(spy.calledWith('EasySpeech: text exceeds max length of 4096 bytes, which will not work with some voices.'))
+ .to.equal(true)
+ spy.restore()
+ done()
+ }
+ }
+ globalThis.SpeechSynthesisUtterance = SpeechSynthesisUtterance
+ globalThis.speechSynthesis = speechSynthesis
+ EasySpeech.init()
+ .catch(done)
+ .then(() => {
+ const buffer = Buffer.alloc(4097, '0')
+ const decoder = new TextDecoder('UTF-8')
+ const text = decoder.decode(buffer)
+ EasySpeech.init()
+ .catch(done)
+ .then(() => {
+ EasySpeech.speak({ text }).catch(done)
+ })
+ })
+ })
+ it('throws if text exceeds 4096 bytes length', function (done) {
+ const SpeechSynthesisUtterance = createUtteranceClass()
+ const id = randomId()
+ const speechSynthesis = {
+ getVoices: () => [{ id }]
+ }
+ globalThis.SpeechSynthesisUtterance = SpeechSynthesisUtterance
+ globalThis.speechSynthesis = speechSynthesis
+ EasySpeech.init({ maxLengthExceeded: 'error' })
+ .catch(done)
+ .then(() => {
+ const speechSynthesis = {
+ getVoices: () => [{}],
+ speak: function () {
+ done(new Error('should not reach'))
+ },
+ cancel: () => {}
+ }
+ globalThis.SpeechSynthesisUtterance = SpeechSynthesisUtterance
+ globalThis.speechSynthesis = speechSynthesis
+
+ const buffer = Buffer.alloc(4097, '0')
+ const decoder = new TextDecoder('UTF-8')
+ const text = decoder.decode(buffer)
+ EasySpeech.init()
+ .catch(e => done(e))
+ .then(() => {
+ expect(() => EasySpeech.speak({ text }))
+ .to.throw('EasySpeech: text exceeds max length of 4096 bytes, which will not work with some voices.')
+ done()
+ })
+ })
+ })
it('speaks, if at least some text is given', function (done) {
const SpeechSynthesisUtterance = class SpeechSynthesisUtterance {
constructor (text) {