@untemps/react-vocal 2.0.0-beta.3 → 2.0.0-beta.5

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/CHANGELOG.md CHANGED
@@ -1,3 +1,22 @@
1
+ # [2.0.0-beta.5](https://github.com/untemps/react-vocal/compare/v2.0.0-beta.4...v2.0.0-beta.5) (2026-05-11)
2
+
3
+
4
+ ### Features
5
+
6
+ * Make fuse.js an optional peer dependency ([#117](https://github.com/untemps/react-vocal/issues/117)) ([a1c2a33](https://github.com/untemps/react-vocal/commit/a1c2a337f8d4ba4729c81f0a9b8664bcf914755f))
7
+
8
+
9
+ ### BREAKING CHANGES
10
+
11
+ * fuse.js must now be installed separately to enable fuzzy matching for phrase commands.
12
+
13
+ # [2.0.0-beta.4](https://github.com/untemps/react-vocal/compare/v2.0.0-beta.3...v2.0.0-beta.4) (2026-05-10)
14
+
15
+
16
+ ### Features
17
+
18
+ * Per-segment command matching, maxAlternatives, and exact lookup ([39b6370](https://github.com/untemps/react-vocal/commit/39b63701b2a15c058b5ae510be8ad4b4437d6763))
19
+
1
20
  # [2.0.0-beta.3](https://github.com/untemps/react-vocal/compare/v2.0.0-beta.2...v2.0.0-beta.3) (2026-05-07)
2
21
 
3
22
 
package/README.md CHANGED
@@ -46,6 +46,14 @@ Although the lack of `SpeechGrammar` and `SpeechGrammarList` is handled by the u
46
46
  yarn add @untemps/react-vocal
47
47
  ```
48
48
 
49
+ Fuzzy matching for phrase commands requires [fuse.js](https://fusejs.io/) as an optional peer dependency:
50
+
51
+ ```bash
52
+ yarn add fuse.js
53
+ ```
54
+
55
+ Without fuse.js, phrase commands fall back to case-insensitive exact matching. Single-word commands always use exact matching and never require fuse.js.
56
+
49
57
  ## Usage
50
58
 
51
59
  ### `Vocal` component
@@ -192,15 +200,17 @@ const commands = {
192
200
 
193
201
  The component utilizes a special hook called `useCommands` to respond to the commands.
194
202
  The hook performs a fuzzy search to match approximate commands if needed. This allows to fix accidental typos or approximate recognition results.
195
- To do so the hook uses [fuse.js](https://fusejs.io/) which implements an algorithm to find strings that are approximately equal to a given input. The score precision that distinguishes acceptable command-to-callback mapping from negative matching can be customized in the hook instantiantion.
203
+ To do so the hook uses [fuse.js](https://fusejs.io/) which implements an algorithm to find strings that are approximately equal to a given input. The score precision that distinguishes acceptable command-to-callback mapping from negative matching can be customized in the hook instantiation.
196
204
 
197
- ```javascript
198
- useCommands(commands, threshold) // threshold is the limit not to exceed to be considered a match
199
- ```
205
+ fuse.js is an optional peer dependency — install it separately to enable fuzzy matching (see [Installation](#installation)). Without it, phrase commands fall back to case-insensitive exact matching.
206
+
207
+ **Single-word command keys** (e.g. `rouge`, `submit`) use exact case-insensitive lookup. When the recognition returns a multi-word transcript, each word is tried individually so a command fires even when embedded in a phrase (e.g. _"je veux du rouge"_ triggers `rouge`).
208
+
209
+ **Phrase command keys** (e.g. `'Change the background color'`) use [fuse.js](https://fusejs.io/) fuzzy matching. The `precision` prop controls the Fuse.js score threshold (default `0.4` — lower is stricter).
200
210
 
201
- See [fuze.js scoring theory](https://fusejs.io/concepts/scoring-theory.html) for more details.
211
+ **Homophone tolerance** is achieved via `maxAlternatives`: by setting it to 3–5, the speech engine returns several transcription candidates per segment. The correct word (e.g. _vert_) may appear as a secondary alternative when the primary is a homophone (e.g. _verre_), and will still trigger the command.
202
212
 
203
- > :warning: **The `Vocal` component doesn't expose that score yet.** For now on you have to deal with the default value (*0.4*)
213
+ **At most one command fires per utterance.** Alternatives and segments are scanned in order and matching stops at the first hit, so a single recognition event can trigger at most one command callback.
204
214
 
205
215
  ---
206
216
 
@@ -208,10 +218,12 @@ See [fuze.js scoring theory](https://fusejs.io/concepts/scoring-theory.html) for
208
218
 
209
219
  | Props | Type | Default | Description |
210
220
  | ------------- | ----------------- | -------------------- | ----------------------------------------------------------------------------------------------- |
211
- | commands | object | null | Callbacks to be triggered when specified commands are detected by the recognition |
212
- | lang | string | 'en-US' | Language understood by the recognition [BCP 47 language tag](https://tools.ietf.org/html/bcp47) |
213
- | grammars | SpeechGrammarList | null | Grammars understood by the recognition [JSpeech Grammar Format](https://www.w3.org/TR/jsgf/) |
214
- | timeout | number | 3000 | Time in ms to wait before discarding the recognition |
221
+ | commands | object | null | Callbacks to be triggered when specified commands are detected by the recognition |
222
+ | lang | string | 'en-US' | Language understood by the recognition [BCP 47 language tag](https://tools.ietf.org/html/bcp47) |
223
+ | grammars | SpeechGrammarList | null | Grammars understood by the recognition [JSpeech Grammar Format](https://www.w3.org/TR/jsgf/) |
224
+ | timeout | number | 3000 | Time in ms to wait before discarding the recognition |
225
+ | precision | number | 0.4 | Fuse.js score threshold for **phrase** command keys only (lower = stricter). Single-word commands always use exact lookup. |
226
+ | maxAlternatives | number | 1 | Maximum number of recognition alternatives per segment. Setting this to 3–5 lets the engine surface the correct word as a secondary transcript, which is useful for handling homophones (e.g. _vert_ / _verre_ in French). |
215
227
  | style | object | null | Styles of the root element if className is not specified |
216
228
  | className | string | null | Class of the root element |
217
229
  | ariaLabel | string | 'start recognition' | Accessible label for the default button |
@@ -286,13 +298,14 @@ const App = () => {
286
298
  #### Signature
287
299
 
288
300
  ```
289
- useVocal(lang, grammars)
301
+ useVocal(lang, grammars, maxAlternatives)
290
302
  ```
291
303
 
292
- | Args | Type | Default | Description |
293
- | -------- | ----------------- | ------- | ----------------------------------------------------------------------------------------------- |
294
- | lang | string | 'en-US' | Language understood by the recognition [BCP 47 language tag](https://tools.ietf.org/html/bcp47) |
295
- | grammars | SpeechGrammarList | null | Grammars understood by the recognition [JSpeech Grammar Format](https://www.w3.org/TR/jsgf/) |
304
+ | Args | Type | Default | Description |
305
+ | --------------- | ----------------- | ------- | ----------------------------------------------------------------------------------------------- |
306
+ | lang | string | 'en-US' | Language understood by the recognition [BCP 47 language tag](https://tools.ietf.org/html/bcp47) |
307
+ | grammars | SpeechGrammarList | null | Grammars understood by the recognition [JSpeech Grammar Format](https://www.w3.org/TR/jsgf/) |
308
+ | maxAlternatives | number | 1 | Maximum number of recognition alternatives per segment |
296
309
 
297
310
  ---
298
311
 
package/dev/src/index.jsx CHANGED
@@ -1,43 +1,56 @@
1
- import React, { useState } from 'react'
1
+ import React, { useMemo, useState } from 'react'
2
2
  import { createRoot } from 'react-dom/client'
3
3
 
4
4
  import Vocal from '../../src'
5
5
 
6
+ const COMMANDS = {
7
+ rouge: 'red',
8
+ bleu: 'blue',
9
+ vert: 'green',
10
+ jaune: 'yellow',
11
+ }
12
+
6
13
  const App = () => {
7
14
  const [logs, setLogs] = useState('')
8
15
  const [borderColor, setBorderColor] = useState()
9
16
 
10
- const _log = (value) => setLogs((logs) => `${logs}${logs.length > 0 ? '\n' : ''} ----- ${value}`)
11
-
12
- const _onVocalStart = () => {
13
- _log('start')
14
- }
15
-
16
- const _onVocalEnd = () => {
17
- _log(`end`)
18
- }
19
-
20
- const _onVocalResult = (result) => {
21
- _log(`result: "${result}"`)
22
- }
23
-
24
- const _onVocalError = (e) => {
25
- _log(e.message)
26
- }
17
+ const _log = (value) => setLogs((prev) => `${prev}${prev.length > 0 ? '\n' : ''} ----- ${value}`)
18
+
19
+ // Memoized to avoid recreating the commands object (and re-indexing useCommands) on every render
20
+ const commands = useMemo(
21
+ () =>
22
+ Object.fromEntries(
23
+ Object.entries(COMMANDS).map(([key, color]) => [
24
+ key,
25
+ (input) => {
26
+ _log(`command matched: "${input}" → ${color}`)
27
+ setBorderColor(color)
28
+ },
29
+ ])
30
+ ),
31
+ // eslint-disable-next-line react-hooks/exhaustive-deps
32
+ []
33
+ )
27
34
 
28
35
  return (
29
36
  <>
30
37
  <Vocal
31
38
  lang="fr"
32
- commands={{
33
- 'Change la bordure en rouge': () => setBorderColor('red'),
34
- }}
35
- onStart={_onVocalStart}
36
- onEnd={_onVocalEnd}
37
- onResult={_onVocalResult}
38
- onError={_onVocalError}
39
+ commands={commands}
40
+ onStart={() => _log('start')}
41
+ onEnd={() => _log('end')}
42
+ onResult={(result) => _log(`result: "${result}"`)}
43
+ onError={(e) => _log(`error: ${e.message}`)}
44
+ maxAlternatives={3}
39
45
  />
40
- <textarea value={logs} rows={30} disabled style={{ width: '100%', marginTop: 16, borderColor }} />
46
+ <p style={{ fontSize: 12, color: '#666', margin: '8px 0' }}>
47
+ Commandes :{' '}
48
+ {Object.keys(COMMANDS).map((k, i) => (
49
+ <span key={k}>{i > 0 && ', '}<code>{k}</code></span>
50
+ ))}
51
+ {' '}— ou dans une phrase (ex : «&nbsp;je veux du vert&nbsp;»)
52
+ </p>
53
+ <textarea value={logs} rows={30} disabled style={{ width: '100%', borderColor }} />
41
54
  </>
42
55
  )
43
56
  }