Skip to content

Commit

Permalink
Merge pull request #229 from jankapunkt/fix-max-text-length
Browse files Browse the repository at this point in the history
Fix: throw on max text length exceeded
  • Loading branch information
jankapunkt committed Nov 29, 2023
2 parents 921ed93 + 8874066 commit 9c10c0b
Show file tree
Hide file tree
Showing 11 changed files with 376 additions and 19 deletions.
7 changes: 4 additions & 3 deletions API.md
Expand Up @@ -8,7 +8,7 @@
* [.debug(fn)](#module_EasySpeech--module.exports..EasySpeech.debug)
* [.detect()](#module_EasySpeech--module.exports..EasySpeech.detect) ⇒ <code>object</code>
* [.status()](#module_EasySpeech--module.exports..EasySpeech.status) ⇒ <code>Object</code>
* [.init(maxTimeout, interval, [quiet])](#module_EasySpeech--module.exports..EasySpeech.init) ⇒ <code>Promise.&lt;Boolean&gt;</code>
* [.init(maxTimeout, interval, [quiet], [maxLengthExceeded])](#module_EasySpeech--module.exports..EasySpeech.init) ⇒ <code>Promise.&lt;Boolean&gt;</code>
* [.voices()](#module_EasySpeech--module.exports..EasySpeech.voices) ⇒ <code>Array.&lt;SpeechSynthesisVoice&gt;</code>
* [.on(handlers)](#module_EasySpeech--module.exports..EasySpeech.on) ⇒ <code>Object</code>
* [.defaults([options])](#module_EasySpeech--module.exports..EasySpeech.defaults) ⇒ <code>object</code>
Expand Down Expand Up @@ -62,7 +62,7 @@ const example = async () => {
* [.debug(fn)](#module_EasySpeech--module.exports..EasySpeech.debug)
* [.detect()](#module_EasySpeech--module.exports..EasySpeech.detect) ⇒ <code>object</code>
* [.status()](#module_EasySpeech--module.exports..EasySpeech.status) ⇒ <code>Object</code>
* [.init(maxTimeout, interval, [quiet])](#module_EasySpeech--module.exports..EasySpeech.init) ⇒ <code>Promise.&lt;Boolean&gt;</code>
* [.init(maxTimeout, interval, [quiet], [maxLengthExceeded])](#module_EasySpeech--module.exports..EasySpeech.init) ⇒ <code>Promise.&lt;Boolean&gt;</code>
* [.voices()](#module_EasySpeech--module.exports..EasySpeech.voices) ⇒ <code>Array.&lt;SpeechSynthesisVoice&gt;</code>
* [.on(handlers)](#module_EasySpeech--module.exports..EasySpeech.on) ⇒ <code>Object</code>
* [.defaults([options])](#module_EasySpeech--module.exports..EasySpeech.defaults) ⇒ <code>object</code>
Expand Down Expand Up @@ -161,7 +161,7 @@ EasySpeech.status()
```
<a name="module_EasySpeech--module.exports..EasySpeech.init"></a>

##### EasySpeech.init(maxTimeout, interval, [quiet]) ⇒ <code>Promise.&lt;Boolean&gt;</code>
##### EasySpeech.init(maxTimeout, interval, [quiet], [maxLengthExceeded]) ⇒ <code>Promise.&lt;Boolean&gt;</code>
This is the function you need to run, before being able to speak.
It includes:
- feature detection
Expand Down Expand Up @@ -201,6 +201,7 @@ Note: if once initialized you can't re-init (will skip and resolve to
| maxTimeout | <code>number</code> | [5000] the maximum timeout to wait for voices in ms |
| interval | <code>number</code> | [250] the interval in ms to check for voices |
| [quiet] | <code>boolean</code> | prevent rejection on errors, e.g. if no voices |
| [maxLengthExceeded] | <code>string</code> | 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 |

<a name="module_EasySpeech--module.exports..EasySpeech.voices"></a>

Expand Down
139 changes: 139 additions & 0 deletions FAQ.md
@@ -0,0 +1,139 @@
# FAQ

> Please read this carefully before opening a new issue.
## Overview

<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->

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

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

## 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
34 changes: 28 additions & 6 deletions README.md
Expand Up @@ -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
Expand All @@ -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

<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
**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)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

## 📦 Installation

Install from npm via
Expand Down Expand Up @@ -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
Expand All @@ -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:

Expand Down Expand Up @@ -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:

Expand All @@ -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)
Expand Down Expand Up @@ -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)
21 changes: 20 additions & 1 deletion dist/EasySpeech.cjs.js
Expand Up @@ -59,6 +59,7 @@ var scope = typeof globalThis === 'undefined' ? window : globalThis;
speechSynthesisEvent: null|SpeechSynthesisEvent,
speechSynthesisErrorEvent: null|SpeechSynthesisErrorEvent,
voices: null|Array<SpeechSynthesisVoice>,
maxLengthExceeded: string,
defaults: {
pitch: Number,
rate: Number,
Expand Down Expand Up @@ -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<Boolean>}
* @fulfil {Boolean} true, if initialized, false, if skipped (because already
* initialized)
Expand All @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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),
Expand Down
21 changes: 20 additions & 1 deletion dist/EasySpeech.es5.js
Expand Up @@ -57,6 +57,7 @@ var scope = typeof globalThis === 'undefined' ? window : globalThis;
speechSynthesisEvent: null|SpeechSynthesisEvent,
speechSynthesisErrorEvent: null|SpeechSynthesisErrorEvent,
voices: null|Array<SpeechSynthesisVoice>,
maxLengthExceeded: string,
defaults: {
pitch: Number,
rate: Number,
Expand Down Expand Up @@ -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<Boolean>}
* @fulfil {Boolean} true, if initialized, false, if skipped (because already
* initialized)
Expand All @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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),
Expand Down

0 comments on commit 9c10c0b

Please sign in to comment.