use-read-aloud 1.0.1 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +94 -52
- package/dist/use-read-aloud.cjs.js +1 -1
- package/dist/use-read-aloud.es.js +40 -37
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
# use-read-aloud
|
|
2
2
|
|
|
3
|
-
A lightweight React hook
|
|
3
|
+
A lightweight React hook for reliable text-to-speech (read aloud) in the browser. Designed for long-form content like blogs and articles using the Web Speech API.
|
|
4
4
|
|
|
5
|
-
This hook is designed
|
|
5
|
+
This hook is designed to work around common Web Speech API issues such as:
|
|
6
|
+
|
|
7
|
+
- Random failures when reading long text
|
|
8
|
+
- Unreliable behavior when resuming after long pauses
|
|
9
|
+
- Missing controls like fast-forward, rewind, etc.
|
|
10
|
+
|
|
11
|
+
> Note : This works fully on the client side using the browser’s built-in text-to-speech engines.
|
|
12
|
+
> No external APIs or dependencies are required.
|
|
6
13
|
|
|
7
14
|
## Installation
|
|
8
15
|
|
|
@@ -10,60 +17,26 @@ This hook is designed for blogs, articles, and reading-focused applications.
|
|
|
10
17
|
npm install use-read-aloud
|
|
11
18
|
```
|
|
12
19
|
|
|
13
|
-
## Peer dependency
|
|
14
|
-
|
|
15
|
-
- react >= 18
|
|
16
|
-
|
|
17
20
|
## Usage
|
|
18
21
|
|
|
19
22
|
```tsx
|
|
20
23
|
import { useReadAloud } from "use-read-aloud";
|
|
21
|
-
import { useState } from "react";
|
|
22
|
-
import {
|
|
23
|
-
FastForward,
|
|
24
|
-
Minus,
|
|
25
|
-
Pause,
|
|
26
|
-
Play,
|
|
27
|
-
Plus,
|
|
28
|
-
Rewind,
|
|
29
|
-
RotateCcw,
|
|
30
|
-
Volume2,
|
|
31
|
-
} from "lucide-react";
|
|
32
|
-
|
|
33
|
-
function AudioPlayer({ text }: { text: string }) {
|
|
34
|
-
const [playBackSpeed, setPlayBackSpeed] = useState<number>(1);
|
|
35
24
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
rate: playBackSpeed,
|
|
39
|
-
});
|
|
25
|
+
function AudioPlayer() {
|
|
26
|
+
const { isPaused, togglePlay } = useReadAloud("Text to be read");
|
|
40
27
|
|
|
41
28
|
return (
|
|
42
|
-
|
|
43
|
-
<button onClick={
|
|
44
|
-
|
|
45
|
-
</button>
|
|
46
|
-
<button onClick={togglePlay}>{isPaused ? <Play /> : <Pause />}</button>
|
|
47
|
-
<button onClick={fastForward}>
|
|
48
|
-
<FastForward />
|
|
49
|
-
</button>
|
|
50
|
-
<button onClick={replay}>
|
|
51
|
-
<RotateCcw />
|
|
52
|
-
</button>
|
|
53
|
-
<span>
|
|
54
|
-
<button onClick={() => setPlayBackSpeed(playBackSpeed - 0.25)}>
|
|
55
|
-
<Minus />
|
|
56
|
-
</button>
|
|
57
|
-
{`${playBackSpeed}x`}
|
|
58
|
-
<button onClick={() => setPlayBackSpeed(playBackSpeed + 0.25)}>
|
|
59
|
-
<Plus />
|
|
60
|
-
</button>
|
|
61
|
-
</span>
|
|
62
|
-
</div>
|
|
29
|
+
<>
|
|
30
|
+
<button onClick={togglePlay}>{isPaused ? "Play" : "Pause"}</button>
|
|
31
|
+
</>
|
|
63
32
|
);
|
|
64
33
|
}
|
|
65
34
|
```
|
|
66
35
|
|
|
36
|
+
## Demo
|
|
37
|
+
|
|
38
|
+
A live demo can be found in [this blog](https://pragmaticswe.com/post/escaping-the-new-year-resolutions-matrix), which uses this library under the hood.
|
|
39
|
+
|
|
67
40
|
## API
|
|
68
41
|
|
|
69
42
|
```
|
|
@@ -72,9 +45,9 @@ useReadAloud(text, options?)
|
|
|
72
45
|
|
|
73
46
|
### Parameters
|
|
74
47
|
|
|
75
|
-
- text - the text to be read
|
|
48
|
+
- text - the text to be read
|
|
76
49
|
|
|
77
|
-
- options - options to
|
|
50
|
+
- options - additional options to change the rate and pitch of the speech
|
|
78
51
|
|
|
79
52
|
```ts
|
|
80
53
|
type Options = {
|
|
@@ -97,17 +70,86 @@ type Options = {
|
|
|
97
70
|
}
|
|
98
71
|
```
|
|
99
72
|
|
|
73
|
+
## Examples
|
|
74
|
+
|
|
75
|
+
1. Fast forward / seek backward
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
import { useReadAloud } from "use-read-aloud";
|
|
79
|
+
|
|
80
|
+
function AudioPlayer() {
|
|
81
|
+
const { isPaused, togglePlay, fastForward, seekBackward } =
|
|
82
|
+
useReadAloud("Text to be read");
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<>
|
|
86
|
+
<button onClick={seekBackward}>Rewind</button>
|
|
87
|
+
<button onClick={togglePlay}>{isPaused ? "Play" : "Pause"}</button>
|
|
88
|
+
<button onClick={fastForward}>FastForward</button>
|
|
89
|
+
</>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
2. Speed up / slow down
|
|
95
|
+
|
|
96
|
+
```tsx
|
|
97
|
+
import { useReadAloud } from "use-read-aloud";
|
|
98
|
+
import { useState } from "react";
|
|
99
|
+
|
|
100
|
+
function AudioPlayer() {
|
|
101
|
+
const [playBackSpeed, setPlayBackSpeed] = useState<number>(1);
|
|
102
|
+
|
|
103
|
+
const { isPaused, togglePlay } = useReadAloud("Text to be read", {
|
|
104
|
+
rate: playBackSpeed,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<>
|
|
109
|
+
<button onClick={togglePlay}>{isPaused ? "Play" : "Pause"}</button>
|
|
110
|
+
<span>
|
|
111
|
+
<button onClick={() => setPlayBackSpeed(playBackSpeed - 0.25)}>
|
|
112
|
+
Minus
|
|
113
|
+
</button>
|
|
114
|
+
{`${playBackSpeed}x`}
|
|
115
|
+
<button onClick={() => setPlayBackSpeed(playBackSpeed + 0.25)}>
|
|
116
|
+
Plus
|
|
117
|
+
</button>
|
|
118
|
+
</span>
|
|
119
|
+
</>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
3. Replay from the start
|
|
125
|
+
|
|
126
|
+
```tsx
|
|
127
|
+
import { useReadAloud } from "use-read-aloud";
|
|
128
|
+
|
|
129
|
+
function AudioPlayer() {
|
|
130
|
+
const { isPaused, togglePlay, replay } = useReadAloud("Text to be read");
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<>
|
|
134
|
+
<button onClick={togglePlay}>{isPaused ? "Play" : "Pause"}</button>
|
|
135
|
+
<button onClick={replay}>Replay</button>
|
|
136
|
+
</>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
100
141
|
## Behavior
|
|
101
142
|
|
|
102
|
-
- Long text is automatically split into smaller chunks for reliability
|
|
143
|
+
- Long text is automatically split into smaller chunks of sentences for reliability.
|
|
144
|
+
- A workaround is used as a replacement for the inconsistent `speechSynthesis.resume()` function. (This may restart the current sentence, which is generally acceptable and far more reliable.)
|
|
103
145
|
|
|
104
|
-
|
|
146
|
+
## Peer dependency
|
|
105
147
|
|
|
106
|
-
-
|
|
148
|
+
- react >= 18
|
|
107
149
|
|
|
108
150
|
## Browser support
|
|
109
151
|
|
|
110
|
-
This hook uses the Web Speech API.
|
|
152
|
+
This hook uses the browser built-in Web Speech API, which is supported by most modern browsers. But, the voice quality varies by browser.
|
|
111
153
|
|
|
112
154
|
Supported:
|
|
113
155
|
|
|
@@ -119,7 +161,7 @@ Supported:
|
|
|
119
161
|
|
|
120
162
|
Not supported:
|
|
121
163
|
|
|
122
|
-
- Firefox
|
|
164
|
+
- Firefox (does not support the Web Speech API)
|
|
123
165
|
|
|
124
166
|
## Changelog
|
|
125
167
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("react"),
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("react"),R=220;function M(l){const i=[];let t="";for(const c of l.split("."))(t+c+".").length>R?(i.push(t.trim()),t=c+"."):t+=c+".";return t.trim()&&i.push(t.trim()),i}function P(l,i={}){const{rate:t=1,pitch:c=1}=i,[k,h]=e.useState(!0),u=e.useRef(!0),a=e.useRef([]),r=e.useRef(0),b=e.useCallback(()=>{a.current=[],r.current=0},[]),d=e.useCallback(()=>{if(!l.trim())return;const n=l.split(/\n+/).map(m=>m.trim()).filter(Boolean);a.current=n.flatMap(M),r.current=0},[l]),o=e.useCallback(()=>{u.current=!0,h(!0),r.current=0},[]),f=e.useCallback(()=>{speechSynthesis.cancel(),b(),o()},[b,o]),p=e.useCallback(()=>{if(r.current>=a.current.length){o();return}const n=new SpeechSynthesisUtterance(a.current[r.current]);n.rate=t,n.pitch=c,speechSynthesis.speak(n),n.onend=()=>{r.current+=1,p()},n.onerror=()=>{}},[o,c,t]),C=e.useCallback(()=>{h(!0),u.current=!0,speechSynthesis.pause()},[]),s=e.useCallback(()=>{h(!1),u.current=!1,a.current.length===0&&d(),speechSynthesis.cancel(),p()},[p,d]),y=e.useCallback(()=>{f(),s()},[f,s]),S=e.useCallback(()=>{r.current=Math.max(r.current-1,0),u.current||s()},[s]),g=e.useCallback(()=>{r.current=Math.min(r.current+1,a.current.length-1),u.current||s()},[s]);return e.useEffect(()=>{u.current?speechSynthesis.cancel():s()},[t,s]),e.useEffect(()=>()=>f(),[f]),{isPaused:k,play:s,pause:C,replay:y,seekBackward:S,fastForward:g,togglePlay:k?s:C}}exports.useReadAloud=P;
|
|
@@ -1,57 +1,60 @@
|
|
|
1
|
-
import { useState as
|
|
2
|
-
const
|
|
3
|
-
function b(
|
|
4
|
-
const
|
|
1
|
+
import { useState as w, useRef as m, useCallback as n, useEffect as g } from "react";
|
|
2
|
+
const A = 220;
|
|
3
|
+
function b(i) {
|
|
4
|
+
const o = [];
|
|
5
5
|
let e = "";
|
|
6
|
-
for (const c of
|
|
7
|
-
(e + c + ".").length >
|
|
8
|
-
return e.trim() &&
|
|
6
|
+
for (const c of i.split("."))
|
|
7
|
+
(e + c + ".").length > A ? (o.push(e.trim()), e = c + ".") : e += c + ".";
|
|
8
|
+
return e.trim() && o.push(e.trim()), o;
|
|
9
9
|
}
|
|
10
|
-
function I(
|
|
11
|
-
const { rate: e = 1, pitch: c = 1 } =
|
|
12
|
-
|
|
13
|
-
}, []), S =
|
|
14
|
-
if (!
|
|
10
|
+
function I(i, o = {}) {
|
|
11
|
+
const { rate: e = 1, pitch: c = 1 } = o, [d, l] = w(!0), u = m(!0), a = m([]), t = m(0), y = n(() => {
|
|
12
|
+
a.current = [], t.current = 0;
|
|
13
|
+
}, []), S = n(() => {
|
|
14
|
+
if (!i.trim())
|
|
15
15
|
return;
|
|
16
|
-
const s =
|
|
17
|
-
|
|
18
|
-
}, [
|
|
19
|
-
u.current = !0, l(!0);
|
|
20
|
-
}, []), h =
|
|
16
|
+
const s = i.split(/\n+/).map((P) => P.trim()).filter(Boolean);
|
|
17
|
+
a.current = s.flatMap(b), t.current = 0;
|
|
18
|
+
}, [i]), f = n(() => {
|
|
19
|
+
u.current = !0, l(!0), t.current = 0;
|
|
20
|
+
}, []), h = n(() => {
|
|
21
21
|
speechSynthesis.cancel(), y(), f();
|
|
22
|
-
}, [y, f]), p =
|
|
23
|
-
if (
|
|
22
|
+
}, [y, f]), p = n(() => {
|
|
23
|
+
if (t.current >= a.current.length) {
|
|
24
24
|
f();
|
|
25
25
|
return;
|
|
26
26
|
}
|
|
27
27
|
const s = new SpeechSynthesisUtterance(
|
|
28
|
-
|
|
28
|
+
a.current[t.current]
|
|
29
29
|
);
|
|
30
30
|
s.rate = e, s.pitch = c, speechSynthesis.speak(s), s.onend = () => {
|
|
31
|
-
|
|
31
|
+
t.current += 1, p();
|
|
32
32
|
}, s.onerror = () => {
|
|
33
33
|
};
|
|
34
|
-
}, [f, c, e]), k =
|
|
34
|
+
}, [f, c, e]), k = n(() => {
|
|
35
35
|
l(!0), u.current = !0, speechSynthesis.pause();
|
|
36
|
-
}, []),
|
|
37
|
-
l(!1), u.current = !1,
|
|
38
|
-
}, [p, S]), R =
|
|
39
|
-
h(),
|
|
40
|
-
}, [h,
|
|
41
|
-
|
|
42
|
-
}, [
|
|
43
|
-
|
|
44
|
-
|
|
36
|
+
}, []), r = n(() => {
|
|
37
|
+
l(!1), u.current = !1, a.current.length === 0 && S(), speechSynthesis.cancel(), p();
|
|
38
|
+
}, [p, S]), R = n(() => {
|
|
39
|
+
h(), r();
|
|
40
|
+
}, [h, r]), C = n(() => {
|
|
41
|
+
t.current = Math.max(t.current - 1, 0), u.current || r();
|
|
42
|
+
}, [r]), M = n(() => {
|
|
43
|
+
t.current = Math.min(
|
|
44
|
+
t.current + 1,
|
|
45
|
+
a.current.length - 1
|
|
46
|
+
), u.current || r();
|
|
47
|
+
}, [r]);
|
|
45
48
|
return g(() => {
|
|
46
|
-
u.current ? speechSynthesis.cancel() :
|
|
47
|
-
}, [e,
|
|
48
|
-
isPaused:
|
|
49
|
-
play:
|
|
49
|
+
u.current ? speechSynthesis.cancel() : r();
|
|
50
|
+
}, [e, r]), g(() => () => h(), [h]), {
|
|
51
|
+
isPaused: d,
|
|
52
|
+
play: r,
|
|
50
53
|
pause: k,
|
|
51
54
|
replay: R,
|
|
52
55
|
seekBackward: C,
|
|
53
|
-
fastForward:
|
|
54
|
-
togglePlay:
|
|
56
|
+
fastForward: M,
|
|
57
|
+
togglePlay: d ? r : k
|
|
55
58
|
};
|
|
56
59
|
}
|
|
57
60
|
export {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "use-read-aloud",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A React hook for reading text aloud using the Web Speech API",
|
|
6
6
|
"author": "Siva Murugan",
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"react",
|
|
10
10
|
"react-hook",
|
|
11
11
|
"text-to-speech",
|
|
12
|
+
"tts",
|
|
12
13
|
"speech-synthesis",
|
|
13
14
|
"read-aloud",
|
|
14
15
|
"web-speech-api"
|