Skip to content

Commit 9c10c0b

Browse files
authored
Merge pull request #229 from jankapunkt/fix-max-text-length
Fix: throw on max text length exceeded
2 parents 921ed93 + 8874066 commit 9c10c0b

File tree

11 files changed

+376
-19
lines changed

11 files changed

+376
-19
lines changed

API.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
* [.debug(fn)](#module_EasySpeech--module.exports..EasySpeech.debug)
99
* [.detect()](#module_EasySpeech--module.exports..EasySpeech.detect) ⇒ <code>object</code>
1010
* [.status()](#module_EasySpeech--module.exports..EasySpeech.status) ⇒ <code>Object</code>
11-
* [.init(maxTimeout, interval, [quiet])](#module_EasySpeech--module.exports..EasySpeech.init) ⇒ <code>Promise.&lt;Boolean&gt;</code>
11+
* [.init(maxTimeout, interval, [quiet], [maxLengthExceeded])](#module_EasySpeech--module.exports..EasySpeech.init) ⇒ <code>Promise.&lt;Boolean&gt;</code>
1212
* [.voices()](#module_EasySpeech--module.exports..EasySpeech.voices) ⇒ <code>Array.&lt;SpeechSynthesisVoice&gt;</code>
1313
* [.on(handlers)](#module_EasySpeech--module.exports..EasySpeech.on) ⇒ <code>Object</code>
1414
* [.defaults([options])](#module_EasySpeech--module.exports..EasySpeech.defaults) ⇒ <code>object</code>
@@ -62,7 +62,7 @@ const example = async () => {
6262
* [.debug(fn)](#module_EasySpeech--module.exports..EasySpeech.debug)
6363
* [.detect()](#module_EasySpeech--module.exports..EasySpeech.detect) ⇒ <code>object</code>
6464
* [.status()](#module_EasySpeech--module.exports..EasySpeech.status) ⇒ <code>Object</code>
65-
* [.init(maxTimeout, interval, [quiet])](#module_EasySpeech--module.exports..EasySpeech.init) ⇒ <code>Promise.&lt;Boolean&gt;</code>
65+
* [.init(maxTimeout, interval, [quiet], [maxLengthExceeded])](#module_EasySpeech--module.exports..EasySpeech.init) ⇒ <code>Promise.&lt;Boolean&gt;</code>
6666
* [.voices()](#module_EasySpeech--module.exports..EasySpeech.voices) ⇒ <code>Array.&lt;SpeechSynthesisVoice&gt;</code>
6767
* [.on(handlers)](#module_EasySpeech--module.exports..EasySpeech.on) ⇒ <code>Object</code>
6868
* [.defaults([options])](#module_EasySpeech--module.exports..EasySpeech.defaults) ⇒ <code>object</code>
@@ -161,7 +161,7 @@ EasySpeech.status()
161161
```
162162
<a name="module_EasySpeech--module.exports..EasySpeech.init"></a>
163163

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

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

FAQ.md

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
# FAQ
2+
3+
> Please read this carefully before opening a new issue.
4+
5+
## Overview
6+
7+
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
8+
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
9+
10+
- [How can I use / install different voices?](#how-can-i-use--install-different-voices)
11+
- [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)
12+
- [Why not using a cloud-based tts service?](#why-not-using-a-cloud-based-tts-service)
13+
- [Can I include service xyz with this library?](#can-i-include-service-xyz-with-this-library)
14+
- [Can I load my own / custom trained voices?](#can-i-load-my-own--custom-trained-voices)
15+
- [My or my users voices sound all terrible, what can I do?](#my-or-my-users-voices-sound-all-terrible-what-can-i-do)
16+
- [My voices play faster on a Mac M1 than on other machines](#my-voices-play-faster-on-a-mac-m1-than-on-other-machines)
17+
- [Init failed with "EasySpeech: browser has no voices (timeout)"](#init-failed-with-easyspeech-browser-has-no-voices-timeout)
18+
- [Error 'EasySpeech: not initialized. Run EasySpeech.init() first'](#error-easyspeech-not-initialized-run-easyspeechinit-first)
19+
- [Some specific voices are missing, although they are installed on OS-level](#some-specific-voices-are-missing-although-they-are-installed-on-os-level)
20+
- [My voices are gone or have changed after I updated my OS](#my-voices-are-gone-or-have-changed-after-i-updated-my-os)
21+
- [Error 'EasySpeech: text exceeds max length of 4096 bytes.'](#error-easyspeech-text-exceeds-max-length-of-4096-bytes)
22+
- [Safari plays speech delayed after interaction with other audio](#safari-plays-speech-delayed-after-interaction-with-other-audio)
23+
24+
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
25+
26+
## How can I use / install different voices?
27+
28+
> Note: the following cannot be influenced by this tool or JavaScript in general
29+
> and requires active measures by the user who wants to different/better voices.
30+
> This is by design and can only be changed if the Web Speech API standard improves.
31+
32+
- Browser-level: switch to Google Chrome as it delivers a set of Google Voices, which all sound pretty decent
33+
- OS-level: install new voices, which is an OS-specific procedure
34+
- [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)
35+
- [MacOS](https://support.apple.com/guide/mac-help/change-the-voice-your-mac-uses-to-speak-text-mchlp2290/mac)
36+
- [Ubuntu](https://github.com/espeak-ng/espeak-ng/blob/master/docs/mbrola.md#installation-of-standard-packages)
37+
- [Android](https://support.google.com/accessibility/android/answer/6006983?hl=en&sjid=9301509494880612166-EU)
38+
- [iOS](https://support.apple.com/en-us/HT202362)
39+
40+
Please let me know if the guides are outdated or open a PR with updated links.
41+
42+
## Why does this library exists if I can use TTS natively in the browser?
43+
44+
Every browser vendor implements the [Web Speech API](https://developer.mozilla.org/en-US/docs/Web/API/SpeechSynthesis)
45+
differently and there a multiple nuances that make it difficult to provide similar functionality across major browsers.
46+
47+
## Why not using a cloud-based tts service?
48+
49+
Sure you can do that. However, different projects have different requirements.
50+
If you can't afford a cloud-based service or are prohibited to do so then this
51+
tool might be something for you.
52+
53+
## Can I include service xyz with this library?
54+
55+
No, it's solely a wrapper for the [Web Speech API](https://developer.mozilla.org/en-US/docs/Web/API/SpeechSynthesis) "
56+
standard".
57+
58+
## Can I load my own / custom trained voices?
59+
60+
Unfortunately, no. This is a current limitation of the Web Speech API itself
61+
and there is nothing we can do about it.
62+
63+
If you want to this to become reality one day, you have to get in contact
64+
with browser vendors and the [Web Incubator Community Group](https://github.com/WICG/speech-api).
65+
66+
## My or my users voices sound all terrible, what can I do?
67+
68+
Sometimes this is the result of bad settings, like `pitch` and `rate`.
69+
Please check these value and try to run with explicit values of `1` for both of them.
70+
71+
If this has no effect, then is not an issue of bad pitch/rate. It's very likely that the installed voices
72+
are simply bad / bad trained or old.
73+
74+
Please read on ["How can I use / install different voices?"](#how-can-i-use--install-different-voices)
75+
76+
## My voices play faster on a Mac M1 than on other machines
77+
78+
This is unfortunately a vendor-specific issue and also supposedly a bug in Safari.
79+
80+
Related issues:
81+
- https://github.com/jankapunkt/easy-speech/issues/116
82+
83+
## Init failed with "EasySpeech: browser has no voices (timeout)"
84+
85+
This means your browser supports the minimum requirements for speech synthesis,
86+
but you / your users have no voices installed on your / their system.
87+
88+
Please read on ["How can I use / install different voices?"](#how-can-i-use--install-different-voices)
89+
90+
## Error 'EasySpeech: not initialized. Run EasySpeech.init() first'
91+
92+
This means you haven't run `EasySpeech.init` yet. It's required to set up everything.
93+
See the [API Docs](./API.md) on how to use it.
94+
95+
## Some specific voices are missing, although they are installed on OS-level
96+
97+
This is something I found on newer iOS versions (16+) to be the case.
98+
While I have the Siri voice installed, it's not available in the browser.
99+
This seems to be a vendor-specific issue, so you need to contact your OS vendor (in this case Apple).
100+
101+
## My voices are gone or have changed after I updated my OS
102+
103+
This seems to be a vendor-specific issue, so you need to contact your operating system vendor (Apple, Microsoft).
104+
105+
Related issues:
106+
- https://github.com/jankapunkt/easy-speech/issues/209
107+
108+
## Error 'EasySpeech: text exceeds max length of 4096 bytes.'
109+
110+
Your text is too long for some voices to process it. You might want to split
111+
it into smaller chunks and play the next one either by user invocation or
112+
automatically. A small example:
113+
114+
```js
115+
let index = 0
116+
const text = [
117+
'This is the first sentence.',
118+
'This is the second sentence.',
119+
]
120+
121+
122+
async function playToEnd () {
123+
const chunk = text[index++]
124+
if (!chunk) { return true } // done
125+
126+
await EasySpeech.speak({ text: chunk })
127+
return playToEnd()
128+
}
129+
```
130+
131+
Related issues:
132+
- https://github.com/jankapunkt/easy-speech/issues/227
133+
134+
## Safari plays speech delayed after interaction with other audio
135+
136+
You can try to speak with `volume=0` before your actual voice is intended to speak.
137+
138+
Related issues:
139+
- https://github.com/jankapunkt/easy-speech/issues/51

README.md

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ Cross browser Speech Synthesis; no dependencies.
2020
![npm bundle size](https://img.shields.io/bundlephobia/minzip/easy-speech)
2121

2222

23-
2423
## ⭐️ Why EasySpeech?
2524

2625
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.
4140
**Note:** this is not a polyfill package, if your target browser does not support speech synthesis or the Web Speech
4241
API, this package is not usable.
4342

43+
4444
## 🚀 Live Demo
4545

4646
The live demo is available at https://jankapunkt.github.io/easy-speech/
4747
You can use it to test your browser for `speechSynthesis` support and functionality.
4848

4949
[![live demo screenshot](./docs/demo_screenshot.png)](https://jankapunkt.github.io/easy-speech/)
50-
50+
51+
## Table of Contents
52+
53+
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
54+
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
55+
**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)*
56+
57+
- [📦 Installation](#-installation)
58+
- [👨‍💻 Usage](#-usage)
59+
- [🚀 Initialize](#-initialize)
60+
- [📢 Speak a voice](#-speak-a-voice)
61+
- [😵‍💫 Troubleshooting / FAQ](#-troubleshooting--faq)
62+
- [🔬 API](#-api)
63+
- [⌨️ Contribution and development](#-contribution-and-development)
64+
- [📖 Resources](#-resources)
65+
- [⚖️ License](#-license)
66+
67+
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
68+
5169
## 📦 Installation
5270

5371
Install from npm via
@@ -115,7 +133,7 @@ If at least `SpeechSynthesis` and `SpeechSynthesisUtterance` are defined you
115133
are good to go.
116134

117135

118-
### Initialize
136+
### 🚀 Initialize
119137

120138
Preparing everything to work is not as clear as it should, especially when
121139
targeting cross-browser functionality. The asynchronous init function will help
@@ -127,7 +145,7 @@ EasySpeech.init({ maxTimeout: 5000, interval: 250 })
127145
.catch(e => console.error(e))
128146
```
129147

130-
#### Loading voices
148+
#### 💽 Loading voices
131149

132150
The init-routine will go through several stages to setup the environment:
133151

@@ -156,7 +174,7 @@ Note: This fallback voice is not overridden by `EasySpeech.defaults()`, your
156174
default voice will be used in favor but the fallback voice will always be there
157175
in case no voice is found when calling `EasySpeech.speak()`
158176

159-
### Speak a voice
177+
### 📢 Speak a voice
160178

161179
This is as easy as it gets:
162180

@@ -177,6 +195,10 @@ an error occurred. You can additionally attach these event listeners if you like
177195
or use `EasySpeech.on` to attach default listeners to every time you call
178196
`EasySpeech.speak`.
179197

198+
### 😵‍💫 Troubleshooting / FAQ
199+
200+
There is an own [FAQ section](./FAQ.md) available that aims to help with common issues.
201+
180202
## 🔬 API
181203

182204
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
206228
- https://bugs.chromium.org/p/chromium/issues/detail?id=582455
207229
- https://stackoverflow.com/a/65883556
208230

209-
## License
231+
## ⚖️ License
210232

211233
MIT, see [license file](./LICENSE)

dist/EasySpeech.cjs.js

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ var scope = typeof globalThis === 'undefined' ? window : globalThis;
5959
speechSynthesisEvent: null|SpeechSynthesisEvent,
6060
speechSynthesisErrorEvent: null|SpeechSynthesisErrorEvent,
6161
voices: null|Array<SpeechSynthesisVoice>,
62+
maxLengthExceeded: string,
6263
defaults: {
6364
pitch: Number,
6465
rate: Number,
@@ -304,6 +305,10 @@ var status = function status(s) {
304305
* @param maxTimeout {number}[5000] the maximum timeout to wait for voices in ms
305306
* @param interval {number}[250] the interval in ms to check for voices
306307
* @param quiet {boolean=} prevent rejection on errors, e.g. if no voices
308+
* @param maxLengthExceeded {string=} defines what to do, if max text length (4096 bytes) is exceeded:
309+
* - 'error' - throw an Error
310+
* - 'none' - do nothing; note that some voices may not speak the text at all without any error or warning
311+
* - 'warn' - default, raises a warning
307312
* @return {Promise<Boolean>}
308313
* @fulfil {Boolean} true, if initialized, false, if skipped (because already
309314
* initialized)
@@ -323,7 +328,8 @@ EasySpeech.init = function () {
323328
maxTimeout = _ref$maxTimeout === void 0 ? 5000 : _ref$maxTimeout,
324329
_ref$interval = _ref.interval,
325330
interval = _ref$interval === void 0 ? 250 : _ref$interval,
326-
quiet = _ref.quiet;
331+
quiet = _ref.quiet,
332+
maxLengthExceeded = _ref.maxLengthExceeded;
327333
return new Promise(function (resolve, reject) {
328334
if (internal.initialized) {
329335
return resolve(false);
@@ -337,6 +343,7 @@ EasySpeech.init = function () {
337343
var timer;
338344
var voicesChangedListener;
339345
var completeCalled = false;
346+
internal.maxLengthExceeded = maxLengthExceeded || 'warn';
340347
var fail = function fail(errorMessage) {
341348
status("init: failed (".concat(errorMessage, ")"));
342349
clearInterval(timer);
@@ -682,6 +689,18 @@ EasySpeech.speak = function (_ref3) {
682689
if (!validate.text(text)) {
683690
throw new Error('EasySpeech: at least some valid text is required to speak');
684691
}
692+
if (new TextEncoder().encode(text).length > 4096) {
693+
var message = 'EasySpeech: text exceeds max length of 4096 bytes, which will not work with some voices.';
694+
switch (internal.maxLengthExceeded) {
695+
case 'none':
696+
break;
697+
case 'error':
698+
throw new Error(message);
699+
case 'warn':
700+
default:
701+
console.warn(message);
702+
}
703+
}
685704
var getValue = function getValue(options) {
686705
var _internal$defaults2;
687706
var _Object$entries$ = _slicedToArray(Object.entries(options)[0], 2),

dist/EasySpeech.es5.js

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ var scope = typeof globalThis === 'undefined' ? window : globalThis;
5757
speechSynthesisEvent: null|SpeechSynthesisEvent,
5858
speechSynthesisErrorEvent: null|SpeechSynthesisErrorEvent,
5959
voices: null|Array<SpeechSynthesisVoice>,
60+
maxLengthExceeded: string,
6061
defaults: {
6162
pitch: Number,
6263
rate: Number,
@@ -302,6 +303,10 @@ var status = function status(s) {
302303
* @param maxTimeout {number}[5000] the maximum timeout to wait for voices in ms
303304
* @param interval {number}[250] the interval in ms to check for voices
304305
* @param quiet {boolean=} prevent rejection on errors, e.g. if no voices
306+
* @param maxLengthExceeded {string=} defines what to do, if max text length (4096 bytes) is exceeded:
307+
* - 'error' - throw an Error
308+
* - 'none' - do nothing; note that some voices may not speak the text at all without any error or warning
309+
* - 'warn' - default, raises a warning
305310
* @return {Promise<Boolean>}
306311
* @fulfil {Boolean} true, if initialized, false, if skipped (because already
307312
* initialized)
@@ -321,7 +326,8 @@ EasySpeech.init = function () {
321326
maxTimeout = _ref$maxTimeout === void 0 ? 5000 : _ref$maxTimeout,
322327
_ref$interval = _ref.interval,
323328
interval = _ref$interval === void 0 ? 250 : _ref$interval,
324-
quiet = _ref.quiet;
329+
quiet = _ref.quiet,
330+
maxLengthExceeded = _ref.maxLengthExceeded;
325331
return new Promise(function (resolve, reject) {
326332
if (internal.initialized) {
327333
return resolve(false);
@@ -335,6 +341,7 @@ EasySpeech.init = function () {
335341
var timer;
336342
var voicesChangedListener;
337343
var completeCalled = false;
344+
internal.maxLengthExceeded = maxLengthExceeded || 'warn';
338345
var fail = function fail(errorMessage) {
339346
status("init: failed (".concat(errorMessage, ")"));
340347
clearInterval(timer);
@@ -680,6 +687,18 @@ EasySpeech.speak = function (_ref3) {
680687
if (!validate.text(text)) {
681688
throw new Error('EasySpeech: at least some valid text is required to speak');
682689
}
690+
if (new TextEncoder().encode(text).length > 4096) {
691+
var message = 'EasySpeech: text exceeds max length of 4096 bytes, which will not work with some voices.';
692+
switch (internal.maxLengthExceeded) {
693+
case 'none':
694+
break;
695+
case 'error':
696+
throw new Error(message);
697+
case 'warn':
698+
default:
699+
console.warn(message);
700+
}
701+
}
683702
var getValue = function getValue(options) {
684703
var _internal$defaults2;
685704
var _Object$entries$ = _slicedToArray(Object.entries(options)[0], 2),

0 commit comments

Comments
 (0)