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

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,10 @@
1
+ # [2.0.0-beta.6](https://github.com/untemps/react-vocal/compare/v2.0.0-beta.5...v2.0.0-beta.6) (2026-05-12)
2
+
3
+
4
+ ### Features
5
+
6
+ * Add continuous session support ([#118](https://github.com/untemps/react-vocal/issues/118)) ([690ba61](https://github.com/untemps/react-vocal/commit/690ba617746aa28499d7ea2d48751453a652ff5e))
7
+
1
8
  # [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
9
 
3
10
 
package/README.md CHANGED
@@ -224,6 +224,8 @@ fuse.js is an optional peer dependency — install it separately to enable fuzzy
224
224
  | timeout | number | 3000 | Time in ms to wait before discarding the recognition |
225
225
  | precision | number | 0.4 | Fuse.js score threshold for **phrase** command keys only (lower = stricter). Single-word commands always use exact lookup. |
226
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). |
227
+ | continuous | boolean | false | Keep the recognition session open after each result. The session accumulates transcript across segments and stops when the button is clicked again or `silenceTimeout` expires. Commands are not evaluated in continuous mode. |
228
+ | silenceTimeout | number | null | When `continuous` is true, automatically stop the session after this many ms of inactivity following the last recognized result. `null` or `0` disables auto-stop (button click required). |
227
229
  | style | object | null | Styles of the root element if className is not specified |
228
230
  | className | string | null | Class of the root element |
229
231
  | ariaLabel | string | 'start recognition' | Accessible label for the default button |
@@ -298,7 +300,7 @@ const App = () => {
298
300
  #### Signature
299
301
 
300
302
  ```
301
- useVocal(lang, grammars, maxAlternatives)
303
+ useVocal(lang, grammars, maxAlternatives, continuous)
302
304
  ```
303
305
 
304
306
  | Args | Type | Default | Description |
@@ -306,6 +308,7 @@ useVocal(lang, grammars, maxAlternatives)
306
308
  | lang | string | 'en-US' | Language understood by the recognition [BCP 47 language tag](https://tools.ietf.org/html/bcp47) |
307
309
  | grammars | SpeechGrammarList | null | Grammars understood by the recognition [JSpeech Grammar Format](https://www.w3.org/TR/jsgf/) |
308
310
  | maxAlternatives | number | 1 | Maximum number of recognition alternatives per segment |
311
+ | continuous | boolean | false | Keep the recognition session open after each result |
309
312
 
310
313
  ---
311
314
 
package/dev/src/index.jsx CHANGED
@@ -13,6 +13,7 @@ const COMMANDS = {
13
13
  const App = () => {
14
14
  const [logs, setLogs] = useState('')
15
15
  const [borderColor, setBorderColor] = useState()
16
+ const [continuous, setContinuous] = useState(false)
16
17
 
17
18
  const _log = (value) => setLogs((prev) => `${prev}${prev.length > 0 ? '\n' : ''} ----- ${value}`)
18
19
 
@@ -22,8 +23,8 @@ const App = () => {
22
23
  Object.fromEntries(
23
24
  Object.entries(COMMANDS).map(([key, color]) => [
24
25
  key,
25
- (input) => {
26
- _log(`command matched: "${input}" → ${color}`)
26
+ (rawInput, commandKey) => {
27
+ _log(`command matched: "${commandKey}" → ${color}`)
27
28
  setBorderColor(color)
28
29
  },
29
30
  ])
@@ -37,12 +38,19 @@ const App = () => {
37
38
  <Vocal
38
39
  lang="fr"
39
40
  commands={commands}
41
+ continuous={continuous}
40
42
  onStart={() => _log('start')}
41
43
  onEnd={() => _log('end')}
42
- onResult={(result) => _log(`result: "${result}"`)}
44
+ onResult={(result) => _log(`transcript: "${result}"`)}
43
45
  onError={(e) => _log(`error: ${e.message}`)}
44
46
  maxAlternatives={3}
45
47
  />
48
+ <p style={{ fontSize: 12, color: '#666', margin: '8px 0' }}>
49
+ <label style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
50
+ <input type="checkbox" checked={continuous} onChange={(e) => setContinuous(e.target.checked)} />
51
+ Mode continu
52
+ </label>
53
+ </p>
46
54
  <p style={{ fontSize: 12, color: '#666', margin: '8px 0' }}>
47
55
  Commandes :{' '}
48
56
  {Object.keys(COMMANDS).map((k, i) => (
package/dist/index.es.js CHANGED
@@ -672,39 +672,41 @@ x(N, "defaultOptions", {
672
672
  });
673
673
  //#endregion
674
674
  //#region node_modules/@untemps/utils/dist/function/isFunction.js
675
- var P = (e) => typeof e == "function", F = (e = "en-US", t = null, i = 1, o = null) => {
676
- let s = a(null);
675
+ var P = (e) => typeof e == "function", F = (e = "en-US", t = null, i = 1, o = !1, s = null) => {
676
+ let c = a(null);
677
677
  return r(() => {
678
- if (N.isSupported) return s.current = o || new N({
678
+ if (N.isSupported) return c.current = s || new N({
679
679
  lang: e,
680
680
  grammars: t,
681
- maxAlternatives: i
681
+ maxAlternatives: i,
682
+ continuous: o
682
683
  }), () => {
683
- s.current.abort(), s.current.cleanup();
684
+ c.current.abort(), c.current.cleanup();
684
685
  };
685
686
  }, [
686
687
  e,
687
688
  t,
688
689
  i,
689
- o
690
- ]), [s, {
690
+ o,
691
+ s
692
+ ]), [c, {
691
693
  start: n(() => {
692
- s.current && s.current.start();
694
+ c.current && c.current.start();
693
695
  }, []),
694
696
  stop: n(() => {
695
- s.current && s.current.stop();
697
+ c.current && c.current.stop();
696
698
  }, []),
697
699
  abort: n(() => {
698
- s.current && s.current.abort();
700
+ c.current && c.current.abort();
699
701
  }, []),
700
702
  subscribe: n((e, t) => {
701
- s.current && s.current.addEventListener(e, t);
703
+ c.current && c.current.addEventListener(e, t);
702
704
  }, []),
703
705
  unsubscribe: n((e, t) => {
704
- s.current && s.current.removeEventListener(e, t);
706
+ c.current && c.current.removeEventListener(e, t);
705
707
  }, []),
706
708
  clean: n(() => {
707
- s.current && s.current.cleanup();
709
+ c.current && c.current.cleanup();
708
710
  }, [])
709
711
  }];
710
712
  }, I = (e, t = 0) => {
@@ -742,17 +744,20 @@ var P = (e) => typeof e == "function", F = (e = "en-US", t = null, i = 1, o = nu
742
744
  let t = e.trim().split(/\s+/), r = t.length > 1 ? t : [e.trim()];
743
745
  for (let e of r) {
744
746
  let t = e.toLowerCase();
745
- if (t in n) return n[t]?.(e);
747
+ if (t in n) return n[t]?.(e, t);
746
748
  }
747
749
  return null;
748
750
  }
749
751
  let r = c.current;
750
752
  if (r) {
751
753
  let i = r.search(e).filter((e) => e.score < t);
752
- if (i?.length) return n[i[0].item.toLowerCase()]?.(e);
754
+ if (i?.length) {
755
+ let t = i[0].item.toLowerCase();
756
+ return n[t]?.(e, t);
757
+ }
753
758
  } else {
754
759
  let t = e.toLowerCase(), r = o.find((e) => t.includes(e) || e.includes(t));
755
- if (r) return n[r]?.(e);
760
+ if (r) return n[r]?.(e, r);
756
761
  }
757
762
  return null;
758
763
  };
@@ -950,32 +955,39 @@ var P = (e) => typeof e == "function", F = (e = "en-US", t = null, i = 1, o = nu
950
955
  })] })
951
956
  }), te = (e, t) => {
952
957
  for (let { alternatives: n } of e) for (let e of n) if (t(e) !== null) return;
953
- }, V = ({ children: r, commands: s = null, lang: c = "en-US", grammars: l = null, timeout: u = 3e3, precision: d = .4, maxAlternatives: f = 1, ariaLabel: p = "start recognition", style: m = null, className: h = null, outlineStyle: g = "2px solid", onStart: _ = null, onEnd: v = null, onSpeechStart: y = null, onSpeechEnd: b = null, onResult: x = null, onError: S = null, onNoMatch: C = null, __rsInstance: w }) => {
954
- let T = a(null), [E, D] = o(!1), [, { start: O, stop: k, subscribe: A, unsubscribe: j }] = F(c, l, f, w), M = L(s, d), R = a({});
955
- R.current = {
956
- onStart: _,
957
- onEnd: v,
958
- onSpeechStart: y,
959
- onSpeechEnd: b,
960
- onResult: x,
961
- onError: S,
962
- onNoMatch: C
958
+ }, V = ({ children: r, commands: s = null, lang: c = "en-US", grammars: l = null, timeout: u = 3e3, silenceTimeout: d = null, precision: f = .4, maxAlternatives: p = 1, continuous: m = !1, ariaLabel: h = "start recognition", style: g = null, className: _ = null, outlineStyle: v = "2px solid", onStart: y = null, onEnd: b = null, onSpeechStart: x = null, onSpeechEnd: S = null, onResult: C = null, onError: w = null, onNoMatch: T = null, __rsInstance: E }) => {
959
+ let D = a(null), [O, k] = o(!1), [, { start: A, stop: j, subscribe: M, unsubscribe: R }] = F(c, l, p, m, E), z = L(s, f), V = a({});
960
+ V.current = {
961
+ onStart: y,
962
+ onEnd: b,
963
+ onSpeechStart: x,
964
+ onSpeechEnd: S,
965
+ onResult: C,
966
+ onError: w,
967
+ onNoMatch: T
963
968
  };
964
- let z = a(M);
965
- z.current = M;
966
- let V = a(null), H = a(null), [U, W] = I(n(() => H.current?.(), []), u), G = n(() => {
969
+ let H = a(m);
970
+ H.current = m;
971
+ let U = a({
972
+ transcript: "",
973
+ event: null
974
+ }), W = a(z);
975
+ W.current = z;
976
+ let G = a(null), ne = a(null), re = a(d);
977
+ re.current = d;
978
+ let ie = n(() => ne.current?.(), []), [K, q] = I(ie, u), [ae, J] = I(ie, d ?? 0), Y = n(() => {
967
979
  try {
968
- D(!1), k();
980
+ k(!1), j();
969
981
  } catch (e) {
970
- R.current.onError?.(e), V.current?.();
982
+ V.current.onError?.(e), G.current?.();
971
983
  }
972
- }, [k]), K = n((e) => {
973
- U(), R.current.onStart?.(e);
974
- }, [U]), q = n((e) => {
975
- W(), R.current.onSpeechStart?.(e);
976
- }, [W]), J = n((e) => {
977
- U(), R.current.onSpeechEnd?.(e);
978
- }, [U]), Y = n((e) => {
984
+ }, [j]), oe = n((e) => {
985
+ K(), V.current.onStart?.(e);
986
+ }, [K]), se = n((e) => {
987
+ q(), V.current.onSpeechStart?.(e);
988
+ }, [q]), ce = n((e) => {
989
+ K(), V.current.onSpeechEnd?.(e);
990
+ }, [K]), le = n((e) => {
979
991
  let t = Array.from(e?.results ?? [], (e) => {
980
992
  let t = {
981
993
  confidence: -Infinity,
@@ -990,74 +1002,83 @@ var P = (e) => typeof e == "function", F = (e = "en-US", t = null, i = 1, o = nu
990
1002
  alternatives: n
991
1003
  };
992
1004
  }), n = t.map((e) => e.best).join("");
993
- W(), G(), te(t, z.current), R.current.onResult?.(n, e);
994
- }, [W, G]), X = n((e) => {
995
- G(), R.current.onError?.(e);
996
- }, [G]), ne = n((e) => {
997
- W(), G(), R.current.onNoMatch?.(e);
998
- }, [W, G]), Z = n((e) => {
999
- W();
1005
+ q(), H.current ? (U.current.transcript = n, U.current.event = e, re.current > 0 && ae()) : (te(t, W.current), Y(), V.current.onResult?.(n, e));
1006
+ }, [
1007
+ q,
1008
+ ae,
1009
+ Y
1010
+ ]), X = n((e) => {
1011
+ Y(), V.current.onError?.(e);
1012
+ }, [Y]), ue = n((e) => {
1013
+ q(), Y(), V.current.onNoMatch?.(e);
1014
+ }, [q, Y]), Z = n((e) => {
1015
+ q(), J();
1000
1016
  try {
1001
- G(), V.current?.();
1017
+ Y(), G.current?.(), H.current && U.current.transcript && (V.current.onResult?.(U.current.transcript, U.current.event), U.current.transcript = "", U.current.event = null);
1002
1018
  } finally {
1003
- R.current.onEnd?.(e);
1019
+ V.current.onEnd?.(e);
1004
1020
  }
1005
- }, [W, G]);
1006
- H.current = Z;
1021
+ }, [
1022
+ q,
1023
+ J,
1024
+ Y
1025
+ ]);
1026
+ ne.current = Z;
1007
1027
  let Q = i(() => ({
1008
- start: K,
1028
+ start: oe,
1009
1029
  end: Z,
1010
- speechstart: q,
1011
- speechend: J,
1012
- result: Y,
1030
+ speechstart: se,
1031
+ speechend: ce,
1032
+ result: le,
1013
1033
  error: X,
1014
- nomatch: ne
1034
+ nomatch: ue
1015
1035
  }), [
1016
- K,
1036
+ oe,
1017
1037
  Z,
1018
- q,
1019
- J,
1020
- Y,
1038
+ se,
1039
+ ce,
1040
+ le,
1021
1041
  X,
1022
- ne
1042
+ ue
1023
1043
  ]);
1024
- V.current = () => Object.entries(Q).forEach(([e, t]) => j?.(e, t));
1044
+ G.current = () => Object.entries(Q).forEach(([e, t]) => R?.(e, t));
1025
1045
  let $ = n(() => {
1026
1046
  try {
1027
- D(!0), Object.entries(Q).forEach(([e, t]) => A(e, t)), O();
1047
+ U.current.transcript = "", U.current.event = null, J(), k(!0), Object.entries(Q).forEach(([e, t]) => M(e, t)), A();
1028
1048
  } catch (e) {
1029
1049
  X(e);
1030
1050
  }
1031
1051
  }, [
1032
1052
  Q,
1053
+ M,
1033
1054
  A,
1034
- O,
1055
+ J,
1035
1056
  X
1036
- ]), re = () => {
1037
- !h && g && (T.current.style.outline = g);
1038
- }, ie = () => {
1039
- !h && g && (T.current.style.outline = "none");
1057
+ ]), de = () => {
1058
+ !_ && v && (D.current.style.outline = v);
1059
+ }, fe = () => {
1060
+ !_ && v && (D.current.style.outline = "none");
1040
1061
  };
1041
- return N.isSupported ? P(r) ? r($, G, E) : t(r) ? e(r, { ...!E && { onClick: $ } }) : /* @__PURE__ */ (0, B.jsx)("button", {
1062
+ return N.isSupported ? P(r) ? r($, Y, O) : t(r) ? e(r, { ...!O && { onClick: $ } }) : /* @__PURE__ */ (0, B.jsx)("button", {
1042
1063
  "data-testid": "__vocal-root__",
1043
- ref: T,
1044
- role: "button",
1045
- "aria-label": p,
1046
- style: h ? null : {
1064
+ ref: D,
1065
+ "aria-label": h,
1066
+ "aria-pressed": O,
1067
+ style: _ ? null : {
1047
1068
  width: 24,
1048
1069
  height: 24,
1049
1070
  backgroundColor: "transparent",
1050
1071
  border: "none",
1051
1072
  padding: 0,
1052
- cursor: E ? "default" : "pointer",
1053
- ...m
1073
+ cursor: !m && O ? "default" : "pointer",
1074
+ ...g
1054
1075
  },
1055
- className: h,
1056
- onFocus: re,
1057
- onBlur: ie,
1058
- onClick: $,
1076
+ className: _,
1077
+ onFocus: de,
1078
+ onBlur: fe,
1079
+ onClick: O ? Y : $,
1059
1080
  children: /* @__PURE__ */ (0, B.jsx)(ee, {
1060
- isActive: E,
1081
+ isActive: O,
1061
1082
  color: "#aaa"
1062
1083
  })
1063
1084
  }) : null;