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) {