@untemps/react-vocal 2.0.0-beta.2 → 2.0.0-beta.4

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,17 @@
1
+ # [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)
2
+
3
+
4
+ ### Features
5
+
6
+ * Per-segment command matching, maxAlternatives, and exact lookup ([39b6370](https://github.com/untemps/react-vocal/commit/39b63701b2a15c058b5ae510be8ad4b4437d6763))
7
+
8
+ # [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)
9
+
10
+
11
+ ### Features
12
+
13
+ * Aggregate all result segments picking highest-confidence alternative ([#113](https://github.com/untemps/react-vocal/issues/113)) ([600600d](https://github.com/untemps/react-vocal/commit/600600dfde457df6fe93974d76d0b48599846082))
14
+
1
15
  # [2.0.0-beta.2](https://github.com/untemps/react-vocal/compare/v2.0.0-beta.1...v2.0.0-beta.2) (2026-05-07)
2
16
 
3
17
 
package/README.md CHANGED
@@ -190,17 +190,15 @@ const commands = {
190
190
  }
191
191
  ```
192
192
 
193
- The component utilizes a special hook called `useCommands` to respond to the commands.
194
- 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.
193
+ The component utilizes a special hook called `useCommands` to respond to the commands.
196
194
 
197
- ```javascript
198
- useCommands(commands, threshold) // threshold is the limit not to exceed to be considered a match
199
- ```
195
+ **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`).
196
+
197
+ **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
198
 
201
- See [fuze.js scoring theory](https://fusejs.io/concepts/scoring-theory.html) for more details.
199
+ **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
200
 
203
- > :warning: **The `Vocal` component doesn't expose that score yet.** For now on you have to deal with the default value (*0.4*)
201
+ **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
202
 
205
203
  ---
206
204
 
@@ -208,10 +206,12 @@ See [fuze.js scoring theory](https://fusejs.io/concepts/scoring-theory.html) for
208
206
 
209
207
  | Props | Type | Default | Description |
210
208
  | ------------- | ----------------- | -------------------- | ----------------------------------------------------------------------------------------------- |
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 |
209
+ | commands | object | null | Callbacks to be triggered when specified commands are detected by the recognition |
210
+ | lang | string | 'en-US' | Language understood by the recognition [BCP 47 language tag](https://tools.ietf.org/html/bcp47) |
211
+ | grammars | SpeechGrammarList | null | Grammars understood by the recognition [JSpeech Grammar Format](https://www.w3.org/TR/jsgf/) |
212
+ | timeout | number | 3000 | Time in ms to wait before discarding the recognition |
213
+ | precision | number | 0.4 | Fuse.js score threshold for **phrase** command keys only (lower = stricter). Single-word commands always use exact lookup. |
214
+ | 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
215
  | style | object | null | Styles of the root element if className is not specified |
216
216
  | className | string | null | Class of the root element |
217
217
  | ariaLabel | string | 'start recognition' | Accessible label for the default button |
@@ -286,13 +286,14 @@ const App = () => {
286
286
  #### Signature
287
287
 
288
288
  ```
289
- useVocal(lang, grammars)
289
+ useVocal(lang, grammars, maxAlternatives)
290
290
  ```
291
291
 
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/) |
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/) |
296
+ | maxAlternatives | number | 1 | Maximum number of recognition alternatives per segment |
296
297
 
297
298
  ---
298
299
 
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
  }