use-voice-control 0.1.95 → 0.1.97

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/dist/react.js CHANGED
@@ -1,214 +1,191 @@
1
- import { useState as u, useRef as b, useMemo as T, useEffect as E, useCallback as C } from "react";
2
- import { ReadAloudController as x, isTranscriptionSupported as L, LiveTranscriber as A } from "./client.js";
3
- import { jsx as R } from "react/jsx-runtime";
4
- import { createPortal as I } from "react-dom";
5
- function F(s = {}) {
6
- const [g, h] = u("idle"), [S, y] = u(null), [w, l] = u(null), d = b(s);
7
- d.current = s;
8
- const o = T(
9
- () => new x({
10
- onStateChange: h,
11
- onChunk: (t) => {
12
- var a, c;
13
- y(t), (c = (a = d.current).onChunk) == null || c.call(a, t);
14
- },
15
- onEnd: (t) => {
16
- var a, c;
17
- y(null), (c = (a = d.current).onEnd) == null || c.call(a, t);
18
- },
19
- onError: (t) => {
20
- var a, c;
21
- l(t), (c = (a = d.current).onError) == null || c.call(a, t);
22
- }
23
- }),
24
- []
25
- );
26
- E(() => {
27
- const { onChunk: t, onEnd: a, onError: c, ...P } = s;
28
- o.setOptions({
29
- ...P,
30
- onStateChange: h,
31
- onChunk: (e) => {
32
- var n, r;
33
- y(e), (r = (n = d.current).onChunk) == null || r.call(n, e);
34
- },
35
- onEnd: (e) => {
36
- var n, r;
37
- y(null), (r = (n = d.current).onEnd) == null || r.call(n, e);
38
- },
39
- onError: (e) => {
40
- var n, r;
41
- l(e), (r = (n = d.current).onError) == null || r.call(n, e);
42
- }
43
- });
44
- }, [
45
- o,
46
- s.provider,
47
- s.voice,
48
- s.endpoint,
49
- s.maxChunkLength,
50
- s.format,
51
- s.markdown,
52
- s.synthesize
53
- ]), E(() => () => o.stop(), [o]);
54
- const f = C(
55
- async (t) => {
56
- l(null), await o.speak(t);
57
- },
58
- [o]
59
- ), p = C(() => o.stop(), [o]), v = C(() => o.pause(), [o]), i = C(() => o.resume(), [o]), m = C(
60
- (t) => {
61
- o.isActive() ? o.stop() : f(t);
62
- },
63
- [o, f]
64
- );
65
- return {
66
- state: g,
67
- isActive: g !== "idle",
68
- isSpeaking: g === "speaking",
69
- isPaused: g === "paused",
70
- currentChunk: S,
71
- error: w,
72
- speak: f,
73
- pause: v,
74
- resume: i,
75
- stop: p,
76
- toggle: m
77
- };
1
+ import { n as e, r as t, t as n } from "./client-CwHmea9T.js";
2
+ import { useCallback as r, useEffect as i, useMemo as a, useRef as o, useState as s } from "react";
3
+ import { createPortal as c } from "react-dom";
4
+ import { jsx as l } from "react/jsx-runtime";
5
+ //#region speech/react/useReadAloud.ts
6
+ function u(e = {}) {
7
+ let [n, c] = s("idle"), [l, u] = s(null), [d, f] = s(null), p = o(e);
8
+ p.current = e;
9
+ let m = a(() => new t({
10
+ onStateChange: c,
11
+ onChunk: (e) => {
12
+ u(e), p.current.onChunk?.(e);
13
+ },
14
+ onEnd: (e) => {
15
+ u(null), p.current.onEnd?.(e);
16
+ },
17
+ onError: (e) => {
18
+ f(e), p.current.onError?.(e);
19
+ }
20
+ }), []);
21
+ i(() => {
22
+ let { onChunk: t, onEnd: n, onError: r, ...i } = e;
23
+ m.setOptions({
24
+ ...i,
25
+ onStateChange: c,
26
+ onChunk: (e) => {
27
+ u(e), p.current.onChunk?.(e);
28
+ },
29
+ onEnd: (e) => {
30
+ u(null), p.current.onEnd?.(e);
31
+ },
32
+ onError: (e) => {
33
+ f(e), p.current.onError?.(e);
34
+ }
35
+ });
36
+ }, [
37
+ m,
38
+ e.provider,
39
+ e.voice,
40
+ e.endpoint,
41
+ e.maxChunkLength,
42
+ e.format,
43
+ e.markdown,
44
+ e.synthesize
45
+ ]), i(() => () => m.stop(), [m]);
46
+ let h = r(async (e) => {
47
+ f(null), await m.speak(e);
48
+ }, [m]), g = r(() => m.stop(), [m]), _ = r(() => m.pause(), [m]), v = r(() => m.resume(), [m]), y = r((e) => {
49
+ m.isActive() ? m.stop() : h(e);
50
+ }, [m, h]);
51
+ return {
52
+ state: n,
53
+ isActive: n !== "idle",
54
+ isSpeaking: n === "speaking",
55
+ isPaused: n === "paused",
56
+ currentChunk: l,
57
+ error: d,
58
+ speak: h,
59
+ pause: _,
60
+ resume: v,
61
+ stop: g,
62
+ toggle: y
63
+ };
78
64
  }
79
- function H(s = {}) {
80
- const [g, h] = u(!1), [S, y] = u(!1), [w, l] = u(""), [d, o] = u(""), [f, p] = u(0), [v, i] = u(null), m = b(s);
81
- m.current = s, E(() => y(L()), []);
82
- const t = T(
83
- () => new A({
84
- onStateChange: h,
85
- onPartial: (e) => {
86
- var n, r;
87
- l(e), e && (o(e), p((k) => k + 1)), (r = (n = m.current).onPartial) == null || r.call(n, e);
88
- },
89
- onCommit: (e) => {
90
- var n, r;
91
- l(""), o(e), p((k) => k + 1), (r = (n = m.current).onCommit) == null || r.call(n, e);
92
- },
93
- onError: (e) => {
94
- var n, r;
95
- i(e), (r = (n = m.current).onError) == null || r.call(n, e);
96
- }
97
- }),
98
- []
99
- );
100
- E(() => {
101
- t.setOptions({
102
- engine: s.engine,
103
- language: s.language,
104
- model: s.model,
105
- onStateChange: h,
106
- onPartial: (e) => {
107
- var n, r;
108
- l(e), e && (o(e), p((k) => k + 1)), (r = (n = m.current).onPartial) == null || r.call(n, e);
109
- },
110
- onCommit: (e) => {
111
- var n, r;
112
- l(""), o(e), p((k) => k + 1), (r = (n = m.current).onCommit) == null || r.call(n, e);
113
- },
114
- onError: (e) => {
115
- var n, r;
116
- i(e), (r = (n = m.current).onError) == null || r.call(n, e);
117
- }
118
- });
119
- }, [t, s.engine, s.language, s.model]), E(() => () => {
120
- t.stop();
121
- }, [t]);
122
- const a = C(async () => {
123
- i(null), await t.start();
124
- }, [t]), c = C(async () => {
125
- await t.stop(), l("");
126
- }, [t]), P = C(async () => {
127
- t.isListening() ? await c() : await a();
128
- }, [t, a, c]);
129
- return {
130
- isListening: g,
131
- isSupported: S,
132
- partial: w,
133
- lastPhrase: d,
134
- phraseId: f,
135
- error: v,
136
- start: a,
137
- stop: c,
138
- toggle: P
139
- };
65
+ //#endregion
66
+ //#region speech/react/useLiveTranscription.ts
67
+ function d(t = {}) {
68
+ let [c, l] = s(!1), [u, d] = s(!1), [f, p] = s(""), [m, h] = s(""), [g, _] = s(0), [v, y] = s(null), b = o(t);
69
+ b.current = t, i(() => d(e()), []);
70
+ let x = a(() => new n({
71
+ onStateChange: l,
72
+ onPartial: (e) => {
73
+ p(e), e && (h(e), _((e) => e + 1)), b.current.onPartial?.(e);
74
+ },
75
+ onCommit: (e) => {
76
+ p(""), h(e), _((e) => e + 1), b.current.onCommit?.(e);
77
+ },
78
+ onError: (e) => {
79
+ y(e), b.current.onError?.(e);
80
+ }
81
+ }), []);
82
+ i(() => {
83
+ x.setOptions({
84
+ engine: t.engine,
85
+ language: t.language,
86
+ model: t.model,
87
+ onStateChange: l,
88
+ onPartial: (e) => {
89
+ p(e), e && (h(e), _((e) => e + 1)), b.current.onPartial?.(e);
90
+ },
91
+ onCommit: (e) => {
92
+ p(""), h(e), _((e) => e + 1), b.current.onCommit?.(e);
93
+ },
94
+ onError: (e) => {
95
+ y(e), b.current.onError?.(e);
96
+ }
97
+ });
98
+ }, [
99
+ x,
100
+ t.engine,
101
+ t.language,
102
+ t.model
103
+ ]), i(() => () => {
104
+ x.stop();
105
+ }, [x]);
106
+ let S = r(async () => {
107
+ y(null), await x.start();
108
+ }, [x]), C = r(async () => {
109
+ await x.stop(), p("");
110
+ }, [x]);
111
+ return {
112
+ isListening: c,
113
+ isSupported: u,
114
+ partial: f,
115
+ lastPhrase: m,
116
+ phraseId: g,
117
+ error: v,
118
+ start: S,
119
+ stop: C,
120
+ toggle: r(async () => {
121
+ x.isListening() ? await C() : await S();
122
+ }, [
123
+ x,
124
+ S,
125
+ C
126
+ ])
127
+ };
140
128
  }
141
- const O = 2e3;
142
- function M({
143
- phrase: s,
144
- phraseId: g,
145
- durationMs: h = O,
146
- visible: S = !0,
147
- zIndex: y = 2147483e3,
148
- className: w
149
- }) {
150
- const [l, d] = u(!1), [o, f] = u(!1), [p, v] = u(""), i = b(null);
151
- return E(() => d(!0), []), E(() => {
152
- const a = s.trim();
153
- if (!S || !a) {
154
- S || f(!1);
155
- return;
156
- }
157
- return v(a), f(!0), i.current && clearTimeout(i.current), i.current = setTimeout(() => f(!1), h), () => {
158
- i.current && clearTimeout(i.current);
159
- };
160
- }, [s, g, S, h]), E(() => () => {
161
- i.current && clearTimeout(i.current);
162
- }, []), !l || !p ? null : I(
163
- /* @__PURE__ */ R(
164
- "div",
165
- {
166
- "aria-hidden": "true",
167
- className: w,
168
- "data-spoken-phrase-overlay": "",
169
- style: {
170
- position: "fixed",
171
- inset: 0,
172
- display: "flex",
173
- alignItems: "center",
174
- justifyContent: "center",
175
- pointerEvents: "none",
176
- padding: "6vw",
177
- zIndex: y,
178
- opacity: o ? 1 : 0,
179
- transition: "opacity 320ms ease-out"
180
- },
181
- children: /* @__PURE__ */ R("div", { style: {
182
- maxWidth: "min(1100px, 90vw)",
183
- textAlign: "center",
184
- // Scales with the viewport so a short phrase fills the screen on a laptop
185
- // and still fits on a phone.
186
- fontSize: "clamp(2rem, 7vw, 6rem)",
187
- fontWeight: 700,
188
- lineHeight: 1.1,
189
- letterSpacing: "-0.02em",
190
- color: "#ffffff",
191
- // A dark pill keeps the words readable over light and dark pages alike.
192
- background: "rgba(15, 23, 42, 0.82)",
193
- borderRadius: "1.5rem",
194
- padding: "0.6em 0.8em",
195
- boxShadow: "0 25px 80px rgba(0, 0, 0, 0.45)",
196
- backdropFilter: "blur(8px)",
197
- transform: o ? "scale(1)" : "scale(0.96)",
198
- transition: "transform 320ms ease-out",
199
- overflowWrap: "anywhere"
200
- }, children: p })
201
- }
202
- ),
203
- document.body
204
- );
129
+ //#endregion
130
+ //#region speech/react/SpokenPhraseOverlay.tsx
131
+ var f = 2e3;
132
+ function p({ phrase: e, phraseId: t, durationMs: n = f, visible: r = !0, zIndex: a = 2147483e3, className: u }) {
133
+ let [d, p] = s(!1), [m, h] = s(!1), [g, _] = s(""), v = o(null);
134
+ return i(() => p(!0), []), i(() => {
135
+ let t = e.trim();
136
+ if (!r || !t) {
137
+ r || h(!1);
138
+ return;
139
+ }
140
+ return _(t), h(!0), v.current && clearTimeout(v.current), v.current = setTimeout(() => h(!1), n), () => {
141
+ v.current && clearTimeout(v.current);
142
+ };
143
+ }, [
144
+ e,
145
+ t,
146
+ r,
147
+ n
148
+ ]), i(() => () => {
149
+ v.current && clearTimeout(v.current);
150
+ }, []), !d || !g ? null : c(/* @__PURE__ */ l("div", {
151
+ "aria-hidden": "true",
152
+ className: u,
153
+ "data-spoken-phrase-overlay": "",
154
+ style: {
155
+ position: "fixed",
156
+ inset: 0,
157
+ display: "flex",
158
+ alignItems: "center",
159
+ justifyContent: "center",
160
+ pointerEvents: "none",
161
+ padding: "6vw",
162
+ zIndex: a,
163
+ opacity: +!!m,
164
+ transition: "opacity 320ms ease-out"
165
+ },
166
+ children: /* @__PURE__ */ l("div", {
167
+ style: {
168
+ maxWidth: "min(1100px, 90vw)",
169
+ textAlign: "center",
170
+ fontSize: "clamp(2rem, 7vw, 6rem)",
171
+ fontWeight: 700,
172
+ lineHeight: 1.1,
173
+ letterSpacing: "-0.02em",
174
+ color: "#ffffff",
175
+ background: "rgba(15, 23, 42, 0.82)",
176
+ borderRadius: "1.5rem",
177
+ padding: "0.6em 0.8em",
178
+ boxShadow: "0 25px 80px rgba(0, 0, 0, 0.45)",
179
+ backdropFilter: "blur(8px)",
180
+ transform: m ? "scale(1)" : "scale(0.96)",
181
+ transition: "transform 320ms ease-out",
182
+ overflowWrap: "anywhere"
183
+ },
184
+ children: g
185
+ })
186
+ }), document.body);
205
187
  }
206
- export {
207
- A as LiveTranscriber,
208
- x as ReadAloudController,
209
- M as SpokenPhraseOverlay,
210
- L as isTranscriptionSupported,
211
- H as useLiveTranscription,
212
- F as useReadAloud
213
- };
214
- //# sourceMappingURL=react.js.map
188
+ //#endregion
189
+ export { n as LiveTranscriber, t as ReadAloudController, p as SpokenPhraseOverlay, e as isTranscriptionSupported, d as useLiveTranscription, u as useReadAloud };
190
+
191
+ //# sourceMappingURL=react.js.map
package/dist/react.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"react.js","sources":["../speech/react/useReadAloud.ts","../speech/react/useLiveTranscription.ts","../speech/react/SpokenPhraseOverlay.tsx"],"sourcesContent":["/**\n * @fileoverview React binding for `ReadAloudController`.\n *\n * Keeps one controller alive for the lifetime of the component, mirrors its state\n * into React state, and stops playback on unmount so navigating away never leaves\n * audio running.\n */\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\n\nimport {\n ReadAloudController,\n type ReadAloudChunk,\n type ReadAloudOptions,\n type ReadAloudState,\n} from \"../client/read-aloud\";\n\nexport interface UseReadAloudOptions\n extends Omit<ReadAloudOptions, \"onStateChange\" | \"onChunk\"> {\n /** Called as each chunk starts playing (e.g. to highlight it in the document). */\n onChunk?: (chunk: ReadAloudChunk) => void;\n}\n\nexport interface UseReadAloudReturn {\n state: ReadAloudState;\n /** True while loading, speaking, or paused. */\n isActive: boolean;\n isSpeaking: boolean;\n isPaused: boolean;\n /** The chunk currently being spoken, or `null` between utterances. */\n currentChunk: ReadAloudChunk | null;\n error: Error | null;\n speak: (text: string) => Promise<void>;\n pause: () => void;\n resume: () => void;\n stop: () => void;\n /** Speak `text`, or stop if something is already playing. */\n toggle: (text: string) => void;\n}\n\nexport function useReadAloud(options: UseReadAloudOptions = {}): UseReadAloudReturn {\n const [state, setState] = useState<ReadAloudState>(\"idle\");\n const [currentChunk, setCurrentChunk] = useState<ReadAloudChunk | null>(null);\n const [error, setError] = useState<Error | null>(null);\n\n // Callers routinely pass inline callbacks; hold them in a ref so the controller\n // is never rebuilt mid-playback.\n const optionsRef = useRef(options);\n optionsRef.current = options;\n\n const controller = useMemo(\n () =>\n new ReadAloudController({\n onStateChange: setState,\n onChunk: (chunk) => {\n setCurrentChunk(chunk);\n optionsRef.current.onChunk?.(chunk);\n },\n onEnd: (reason) => {\n setCurrentChunk(null);\n optionsRef.current.onEnd?.(reason);\n },\n onError: (err) => {\n setError(err);\n optionsRef.current.onError?.(err);\n },\n }),\n []\n );\n\n // Re-apply the caller's settings (voice, endpoint, …) whenever they change,\n // preserving the callback wiring above.\n useEffect(() => {\n const { onChunk, onEnd, onError, ...rest } = options;\n controller.setOptions({\n ...rest,\n onStateChange: setState,\n onChunk: (chunk) => {\n setCurrentChunk(chunk);\n optionsRef.current.onChunk?.(chunk);\n },\n onEnd: (reason) => {\n setCurrentChunk(null);\n optionsRef.current.onEnd?.(reason);\n },\n onError: (err) => {\n setError(err);\n optionsRef.current.onError?.(err);\n },\n });\n }, [\n controller,\n options.provider,\n options.voice,\n options.endpoint,\n options.maxChunkLength,\n options.format,\n options.markdown,\n options.synthesize,\n ]);\n\n useEffect(() => () => controller.stop(), [controller]);\n\n const speak = useCallback(\n async (text: string) => {\n setError(null);\n await controller.speak(text);\n },\n [controller]\n );\n\n const stop = useCallback(() => controller.stop(), [controller]);\n const pause = useCallback(() => controller.pause(), [controller]);\n const resume = useCallback(() => controller.resume(), [controller]);\n\n const toggle = useCallback(\n (text: string) => {\n if (controller.isActive()) {\n controller.stop();\n } else {\n void speak(text);\n }\n },\n [controller, speak]\n );\n\n return {\n state,\n isActive: state !== \"idle\",\n isSpeaking: state === \"speaking\",\n isPaused: state === \"paused\",\n currentChunk,\n error,\n speak,\n pause,\n resume,\n stop,\n toggle,\n };\n}\n","/**\n * @fileoverview React binding for `LiveTranscriber`.\n *\n * Exposes the two streams a dictation UI needs: `partial`, which changes on every\n * word while a phrase is in progress, and the committed phrases delivered through\n * `onCommit`. `lastPhrase` carries whatever was most recently heard — partial or\n * committed — along with a monotonically increasing `phraseId` so a display can\n * re-trigger its own timeout even when the same words are said twice in a row.\n */\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\n\nimport {\n LiveTranscriber,\n isTranscriptionSupported,\n type LiveTranscriberOptions,\n} from \"../client/live-transcriber\";\n\nexport interface UseLiveTranscriptionOptions\n extends Omit<LiveTranscriberOptions, \"onStateChange\"> {}\n\nexport interface UseLiveTranscriptionReturn {\n isListening: boolean;\n /** True when this browser can dictate at all. */\n isSupported: boolean;\n /** The in-progress phrase, updating as it is spoken. Empty between phrases. */\n partial: string;\n /** The most recent thing heard, partial or committed. */\n lastPhrase: string;\n /** Increments on every `lastPhrase` update, including repeats of the same words. */\n phraseId: number;\n error: Error | null;\n start: () => Promise<void>;\n stop: () => Promise<void>;\n toggle: () => Promise<void>;\n}\n\nexport function useLiveTranscription(\n options: UseLiveTranscriptionOptions = {}\n): UseLiveTranscriptionReturn {\n const [isListening, setIsListening] = useState(false);\n const [isSupported, setIsSupported] = useState(false);\n const [partial, setPartial] = useState(\"\");\n const [lastPhrase, setLastPhrase] = useState(\"\");\n const [phraseId, setPhraseId] = useState(0);\n const [error, setError] = useState<Error | null>(null);\n\n const optionsRef = useRef(options);\n optionsRef.current = options;\n\n // Support depends on `window`, so it can only be settled after hydration.\n useEffect(() => setIsSupported(isTranscriptionSupported()), []);\n\n const transcriber = useMemo(\n () =>\n new LiveTranscriber({\n onStateChange: setIsListening,\n onPartial: (text) => {\n setPartial(text);\n if (text) {\n setLastPhrase(text);\n setPhraseId((id) => id + 1);\n }\n optionsRef.current.onPartial?.(text);\n },\n onCommit: (text) => {\n setPartial(\"\");\n setLastPhrase(text);\n setPhraseId((id) => id + 1);\n optionsRef.current.onCommit?.(text);\n },\n onError: (err) => {\n setError(err);\n optionsRef.current.onError?.(err);\n },\n }),\n []\n );\n\n // Engine/language changes take effect on the next `start()`.\n useEffect(() => {\n transcriber.setOptions({\n engine: options.engine,\n language: options.language,\n model: options.model,\n onStateChange: setIsListening,\n onPartial: (text) => {\n setPartial(text);\n if (text) {\n setLastPhrase(text);\n setPhraseId((id) => id + 1);\n }\n optionsRef.current.onPartial?.(text);\n },\n onCommit: (text) => {\n setPartial(\"\");\n setLastPhrase(text);\n setPhraseId((id) => id + 1);\n optionsRef.current.onCommit?.(text);\n },\n onError: (err) => {\n setError(err);\n optionsRef.current.onError?.(err);\n },\n });\n }, [transcriber, options.engine, options.language, options.model]);\n\n useEffect(() => {\n return () => {\n void transcriber.stop();\n };\n }, [transcriber]);\n\n const start = useCallback(async () => {\n setError(null);\n await transcriber.start();\n }, [transcriber]);\n\n const stop = useCallback(async () => {\n await transcriber.stop();\n setPartial(\"\");\n }, [transcriber]);\n\n const toggle = useCallback(async () => {\n if (transcriber.isListening()) {\n await stop();\n } else {\n await start();\n }\n }, [transcriber, start, stop]);\n\n return {\n isListening,\n isSupported,\n partial,\n lastPhrase,\n phraseId,\n error,\n start,\n stop,\n toggle,\n };\n}\n","/**\n * @fileoverview Full-screen display of the phrase that was just spoken.\n *\n * While dictating, the words land in whatever input has focus — which is easy to\n * lose track of when you are looking away from the screen. This overlay echoes the\n * latest phrase in large type at the centre of the viewport and fades out two\n * seconds after the last update, so a glance confirms you were heard correctly.\n *\n * It renders into `document.body` through a portal with pointer events disabled, so\n * it never intercepts clicks or gets clipped by the host app's layout, and it is\n * styled inline so it looks the same in every app that mounts it regardless of the\n * CSS framework in play.\n */\nimport { useEffect, useRef, useState, type CSSProperties } from \"react\";\nimport { createPortal } from \"react-dom\";\n\nexport interface SpokenPhraseOverlayProps {\n /** The words to display. An empty string hides the overlay. */\n phrase: string;\n /**\n * Bump this whenever `phrase` is refreshed to restart the hide timer, including\n * when the same words are repeated. `useLiveTranscription` supplies it.\n */\n phraseId?: number;\n /** How long the phrase stays up after the last update. Default 2000ms. */\n durationMs?: number;\n /** Set false to suppress the overlay entirely (e.g. once dictation stops). */\n visible?: boolean;\n /** Stacking order. Default 2147483000, above typical app chrome and modals. */\n zIndex?: number;\n className?: string;\n}\n\nconst HIDE_AFTER_MS = 2000;\n\nexport function SpokenPhraseOverlay({\n phrase,\n phraseId,\n durationMs = HIDE_AFTER_MS,\n visible = true,\n zIndex = 2147483000,\n className,\n}: SpokenPhraseOverlayProps) {\n const [mounted, setMounted] = useState(false);\n const [shown, setShown] = useState(false);\n // Held separately from `phrase` so the text stays put while fading out rather\n // than blanking the moment the transcript clears.\n const [displayed, setDisplayed] = useState(\"\");\n const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n useEffect(() => setMounted(true), []);\n\n useEffect(() => {\n const text = phrase.trim();\n if (!visible || !text) {\n if (!visible) setShown(false);\n return;\n }\n\n setDisplayed(text);\n setShown(true);\n\n if (timerRef.current) clearTimeout(timerRef.current);\n timerRef.current = setTimeout(() => setShown(false), durationMs);\n\n return () => {\n if (timerRef.current) clearTimeout(timerRef.current);\n };\n }, [phrase, phraseId, visible, durationMs]);\n\n useEffect(() => {\n return () => {\n if (timerRef.current) clearTimeout(timerRef.current);\n };\n }, []);\n\n if (!mounted || !displayed) return null;\n\n const containerStyle: CSSProperties = {\n position: \"fixed\",\n inset: 0,\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n pointerEvents: \"none\",\n padding: \"6vw\",\n zIndex,\n opacity: shown ? 1 : 0,\n transition: \"opacity 320ms ease-out\",\n };\n\n const textStyle: CSSProperties = {\n maxWidth: \"min(1100px, 90vw)\",\n textAlign: \"center\",\n // Scales with the viewport so a short phrase fills the screen on a laptop\n // and still fits on a phone.\n fontSize: \"clamp(2rem, 7vw, 6rem)\",\n fontWeight: 700,\n lineHeight: 1.1,\n letterSpacing: \"-0.02em\",\n color: \"#ffffff\",\n // A dark pill keeps the words readable over light and dark pages alike.\n background: \"rgba(15, 23, 42, 0.82)\",\n borderRadius: \"1.5rem\",\n padding: \"0.6em 0.8em\",\n boxShadow: \"0 25px 80px rgba(0, 0, 0, 0.45)\",\n backdropFilter: \"blur(8px)\",\n transform: shown ? \"scale(1)\" : \"scale(0.96)\",\n transition: \"transform 320ms ease-out\",\n overflowWrap: \"anywhere\",\n };\n\n return createPortal(\n <div\n aria-hidden=\"true\"\n className={className}\n data-spoken-phrase-overlay=\"\"\n style={containerStyle}\n >\n <div style={textStyle}>{displayed}</div>\n </div>,\n document.body\n );\n}\n\nexport default SpokenPhraseOverlay;\n"],"names":["useReadAloud","options","state","setState","useState","currentChunk","setCurrentChunk","error","setError","optionsRef","useRef","controller","useMemo","ReadAloudController","chunk","_b","_a","reason","err","useEffect","onChunk","onEnd","onError","rest","speak","useCallback","text","stop","pause","resume","toggle","useLiveTranscription","isListening","setIsListening","isSupported","setIsSupported","partial","setPartial","lastPhrase","setLastPhrase","phraseId","setPhraseId","isTranscriptionSupported","transcriber","LiveTranscriber","id","start","HIDE_AFTER_MS","SpokenPhraseOverlay","phrase","durationMs","visible","zIndex","className","mounted","setMounted","shown","setShown","displayed","setDisplayed","timerRef","createPortal","jsx"],"mappings":"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"}
1
+ {"version":3,"file":"react.js","names":[],"sources":["../speech/react/useReadAloud.ts","../speech/react/useLiveTranscription.ts","../speech/react/SpokenPhraseOverlay.tsx"],"sourcesContent":["/**\n * @fileoverview React binding for `ReadAloudController`.\n *\n * Keeps one controller alive for the lifetime of the component, mirrors its state\n * into React state, and stops playback on unmount so navigating away never leaves\n * audio running.\n */\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\n\nimport {\n ReadAloudController,\n type ReadAloudChunk,\n type ReadAloudOptions,\n type ReadAloudState,\n} from \"../client/read-aloud\";\n\nexport interface UseReadAloudOptions\n extends Omit<ReadAloudOptions, \"onStateChange\" | \"onChunk\"> {\n /** Called as each chunk starts playing (e.g. to highlight it in the document). */\n onChunk?: (chunk: ReadAloudChunk) => void;\n}\n\nexport interface UseReadAloudReturn {\n state: ReadAloudState;\n /** True while loading, speaking, or paused. */\n isActive: boolean;\n isSpeaking: boolean;\n isPaused: boolean;\n /** The chunk currently being spoken, or `null` between utterances. */\n currentChunk: ReadAloudChunk | null;\n error: Error | null;\n speak: (text: string) => Promise<void>;\n pause: () => void;\n resume: () => void;\n stop: () => void;\n /** Speak `text`, or stop if something is already playing. */\n toggle: (text: string) => void;\n}\n\nexport function useReadAloud(options: UseReadAloudOptions = {}): UseReadAloudReturn {\n const [state, setState] = useState<ReadAloudState>(\"idle\");\n const [currentChunk, setCurrentChunk] = useState<ReadAloudChunk | null>(null);\n const [error, setError] = useState<Error | null>(null);\n\n // Callers routinely pass inline callbacks; hold them in a ref so the controller\n // is never rebuilt mid-playback.\n const optionsRef = useRef(options);\n optionsRef.current = options;\n\n const controller = useMemo(\n () =>\n new ReadAloudController({\n onStateChange: setState,\n onChunk: (chunk) => {\n setCurrentChunk(chunk);\n optionsRef.current.onChunk?.(chunk);\n },\n onEnd: (reason) => {\n setCurrentChunk(null);\n optionsRef.current.onEnd?.(reason);\n },\n onError: (err) => {\n setError(err);\n optionsRef.current.onError?.(err);\n },\n }),\n []\n );\n\n // Re-apply the caller's settings (voice, endpoint, …) whenever they change,\n // preserving the callback wiring above.\n useEffect(() => {\n const { onChunk, onEnd, onError, ...rest } = options;\n controller.setOptions({\n ...rest,\n onStateChange: setState,\n onChunk: (chunk) => {\n setCurrentChunk(chunk);\n optionsRef.current.onChunk?.(chunk);\n },\n onEnd: (reason) => {\n setCurrentChunk(null);\n optionsRef.current.onEnd?.(reason);\n },\n onError: (err) => {\n setError(err);\n optionsRef.current.onError?.(err);\n },\n });\n }, [\n controller,\n options.provider,\n options.voice,\n options.endpoint,\n options.maxChunkLength,\n options.format,\n options.markdown,\n options.synthesize,\n ]);\n\n useEffect(() => () => controller.stop(), [controller]);\n\n const speak = useCallback(\n async (text: string) => {\n setError(null);\n await controller.speak(text);\n },\n [controller]\n );\n\n const stop = useCallback(() => controller.stop(), [controller]);\n const pause = useCallback(() => controller.pause(), [controller]);\n const resume = useCallback(() => controller.resume(), [controller]);\n\n const toggle = useCallback(\n (text: string) => {\n if (controller.isActive()) {\n controller.stop();\n } else {\n void speak(text);\n }\n },\n [controller, speak]\n );\n\n return {\n state,\n isActive: state !== \"idle\",\n isSpeaking: state === \"speaking\",\n isPaused: state === \"paused\",\n currentChunk,\n error,\n speak,\n pause,\n resume,\n stop,\n toggle,\n };\n}\n","/**\n * @fileoverview React binding for `LiveTranscriber`.\n *\n * Exposes the two streams a dictation UI needs: `partial`, which changes on every\n * word while a phrase is in progress, and the committed phrases delivered through\n * `onCommit`. `lastPhrase` carries whatever was most recently heard — partial or\n * committed — along with a monotonically increasing `phraseId` so a display can\n * re-trigger its own timeout even when the same words are said twice in a row.\n */\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\n\nimport {\n LiveTranscriber,\n isTranscriptionSupported,\n type LiveTranscriberOptions,\n} from \"../client/live-transcriber\";\n\nexport interface UseLiveTranscriptionOptions\n extends Omit<LiveTranscriberOptions, \"onStateChange\"> {}\n\nexport interface UseLiveTranscriptionReturn {\n isListening: boolean;\n /** True when this browser can dictate at all. */\n isSupported: boolean;\n /** The in-progress phrase, updating as it is spoken. Empty between phrases. */\n partial: string;\n /** The most recent thing heard, partial or committed. */\n lastPhrase: string;\n /** Increments on every `lastPhrase` update, including repeats of the same words. */\n phraseId: number;\n error: Error | null;\n start: () => Promise<void>;\n stop: () => Promise<void>;\n toggle: () => Promise<void>;\n}\n\nexport function useLiveTranscription(\n options: UseLiveTranscriptionOptions = {}\n): UseLiveTranscriptionReturn {\n const [isListening, setIsListening] = useState(false);\n const [isSupported, setIsSupported] = useState(false);\n const [partial, setPartial] = useState(\"\");\n const [lastPhrase, setLastPhrase] = useState(\"\");\n const [phraseId, setPhraseId] = useState(0);\n const [error, setError] = useState<Error | null>(null);\n\n const optionsRef = useRef(options);\n optionsRef.current = options;\n\n // Support depends on `window`, so it can only be settled after hydration.\n useEffect(() => setIsSupported(isTranscriptionSupported()), []);\n\n const transcriber = useMemo(\n () =>\n new LiveTranscriber({\n onStateChange: setIsListening,\n onPartial: (text) => {\n setPartial(text);\n if (text) {\n setLastPhrase(text);\n setPhraseId((id) => id + 1);\n }\n optionsRef.current.onPartial?.(text);\n },\n onCommit: (text) => {\n setPartial(\"\");\n setLastPhrase(text);\n setPhraseId((id) => id + 1);\n optionsRef.current.onCommit?.(text);\n },\n onError: (err) => {\n setError(err);\n optionsRef.current.onError?.(err);\n },\n }),\n []\n );\n\n // Engine/language changes take effect on the next `start()`.\n useEffect(() => {\n transcriber.setOptions({\n engine: options.engine,\n language: options.language,\n model: options.model,\n onStateChange: setIsListening,\n onPartial: (text) => {\n setPartial(text);\n if (text) {\n setLastPhrase(text);\n setPhraseId((id) => id + 1);\n }\n optionsRef.current.onPartial?.(text);\n },\n onCommit: (text) => {\n setPartial(\"\");\n setLastPhrase(text);\n setPhraseId((id) => id + 1);\n optionsRef.current.onCommit?.(text);\n },\n onError: (err) => {\n setError(err);\n optionsRef.current.onError?.(err);\n },\n });\n }, [transcriber, options.engine, options.language, options.model]);\n\n useEffect(() => {\n return () => {\n void transcriber.stop();\n };\n }, [transcriber]);\n\n const start = useCallback(async () => {\n setError(null);\n await transcriber.start();\n }, [transcriber]);\n\n const stop = useCallback(async () => {\n await transcriber.stop();\n setPartial(\"\");\n }, [transcriber]);\n\n const toggle = useCallback(async () => {\n if (transcriber.isListening()) {\n await stop();\n } else {\n await start();\n }\n }, [transcriber, start, stop]);\n\n return {\n isListening,\n isSupported,\n partial,\n lastPhrase,\n phraseId,\n error,\n start,\n stop,\n toggle,\n };\n}\n","/**\n * @fileoverview Full-screen display of the phrase that was just spoken.\n *\n * While dictating, the words land in whatever input has focus — which is easy to\n * lose track of when you are looking away from the screen. This overlay echoes the\n * latest phrase in large type at the centre of the viewport and fades out two\n * seconds after the last update, so a glance confirms you were heard correctly.\n *\n * It renders into `document.body` through a portal with pointer events disabled, so\n * it never intercepts clicks or gets clipped by the host app's layout, and it is\n * styled inline so it looks the same in every app that mounts it regardless of the\n * CSS framework in play.\n */\nimport { useEffect, useRef, useState, type CSSProperties } from \"react\";\nimport { createPortal } from \"react-dom\";\n\nexport interface SpokenPhraseOverlayProps {\n /** The words to display. An empty string hides the overlay. */\n phrase: string;\n /**\n * Bump this whenever `phrase` is refreshed to restart the hide timer, including\n * when the same words are repeated. `useLiveTranscription` supplies it.\n */\n phraseId?: number;\n /** How long the phrase stays up after the last update. Default 2000ms. */\n durationMs?: number;\n /** Set false to suppress the overlay entirely (e.g. once dictation stops). */\n visible?: boolean;\n /** Stacking order. Default 2147483000, above typical app chrome and modals. */\n zIndex?: number;\n className?: string;\n}\n\nconst HIDE_AFTER_MS = 2000;\n\nexport function SpokenPhraseOverlay({\n phrase,\n phraseId,\n durationMs = HIDE_AFTER_MS,\n visible = true,\n zIndex = 2147483000,\n className,\n}: SpokenPhraseOverlayProps) {\n const [mounted, setMounted] = useState(false);\n const [shown, setShown] = useState(false);\n // Held separately from `phrase` so the text stays put while fading out rather\n // than blanking the moment the transcript clears.\n const [displayed, setDisplayed] = useState(\"\");\n const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n useEffect(() => setMounted(true), []);\n\n useEffect(() => {\n const text = phrase.trim();\n if (!visible || !text) {\n if (!visible) setShown(false);\n return;\n }\n\n setDisplayed(text);\n setShown(true);\n\n if (timerRef.current) clearTimeout(timerRef.current);\n timerRef.current = setTimeout(() => setShown(false), durationMs);\n\n return () => {\n if (timerRef.current) clearTimeout(timerRef.current);\n };\n }, [phrase, phraseId, visible, durationMs]);\n\n useEffect(() => {\n return () => {\n if (timerRef.current) clearTimeout(timerRef.current);\n };\n }, []);\n\n if (!mounted || !displayed) return null;\n\n const containerStyle: CSSProperties = {\n position: \"fixed\",\n inset: 0,\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n pointerEvents: \"none\",\n padding: \"6vw\",\n zIndex,\n opacity: shown ? 1 : 0,\n transition: \"opacity 320ms ease-out\",\n };\n\n const textStyle: CSSProperties = {\n maxWidth: \"min(1100px, 90vw)\",\n textAlign: \"center\",\n // Scales with the viewport so a short phrase fills the screen on a laptop\n // and still fits on a phone.\n fontSize: \"clamp(2rem, 7vw, 6rem)\",\n fontWeight: 700,\n lineHeight: 1.1,\n letterSpacing: \"-0.02em\",\n color: \"#ffffff\",\n // A dark pill keeps the words readable over light and dark pages alike.\n background: \"rgba(15, 23, 42, 0.82)\",\n borderRadius: \"1.5rem\",\n padding: \"0.6em 0.8em\",\n boxShadow: \"0 25px 80px rgba(0, 0, 0, 0.45)\",\n backdropFilter: \"blur(8px)\",\n transform: shown ? \"scale(1)\" : \"scale(0.96)\",\n transition: \"transform 320ms ease-out\",\n overflowWrap: \"anywhere\",\n };\n\n return createPortal(\n <div\n aria-hidden=\"true\"\n className={className}\n data-spoken-phrase-overlay=\"\"\n style={containerStyle}\n >\n <div style={textStyle}>{displayed}</div>\n </div>,\n document.body\n );\n}\n\nexport default SpokenPhraseOverlay;\n"],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ //#region speech/utils/semantic-split.js
2
+ function e(e, n = 500) {
3
+ let r = e.split(/\n\s*\n/), i = [];
4
+ for (let e of r) {
5
+ if (e.length <= n) {
6
+ i.push(e.trim());
7
+ continue;
8
+ }
9
+ let r = e.split(/(?<=[.?!])(?=\s+["“”'a-z])/gi), a = "";
10
+ for (let e of r) {
11
+ if (e = e.trim(), e.length > n) {
12
+ let r = t(e, n);
13
+ for (let e of r) (a + " " + e).length > n ? (a && i.push(a.trim()), a = e) : a += (a ? " " : "") + e;
14
+ continue;
15
+ }
16
+ (a + " " + e).length > n ? (a && i.push(a.trim()), a = e) : a += (a ? " " : "") + e;
17
+ }
18
+ a && i.push(a.trim());
19
+ }
20
+ return i;
21
+ }
22
+ function t(e, t) {
23
+ let n = [], r = "", i = e.split(/,\s*/);
24
+ for (let e of i) if ((r + ", " + e).length > t) {
25
+ if (r && n.push(r.trim()), e.length > t) {
26
+ let i = e.split(/\s+/), a = "";
27
+ for (let e of i) (a + " " + e).length > t ? (a && n.push(a.trim()), a = e) : a += (a ? " " : "") + e;
28
+ a && n.push(a.trim()), r = "";
29
+ } else r = e;
30
+ } else r += (r ? ", " : "") + e;
31
+ return r && n.push(r.trim()), n;
32
+ }
33
+ //#endregion
34
+ export { e as t };
35
+
36
+ //# sourceMappingURL=semantic-split-DF26uo-Z.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"semantic-split-DF26uo-Z.js","names":[],"sources":["../speech/utils/semantic-split.js"],"sourcesContent":["/**\n * @fileoverview Splits long text into TTS-sized chunks along paragraph, sentence, comma, and word boundaries so speech synthesis can process it incrementally.\n *\n * Exports `splitTextSmart` (primary chunker, falling back to `splitLongSentence` for\n * oversized sentences) and `splitLongSentence`. Also retains an older, unused\n * `splitTextSmartOld` variant that lacks the long-sentence fallback.\n */\nexport function splitTextSmart(text, maxChunkLength = 500) {\n const paragraphChunks = text.split(/\\n\\s*\\n/);\n const finalChunks = [];\n\n for (let para of paragraphChunks) {\n if (para.length <= maxChunkLength) {\n finalChunks.push(para.trim());\n continue;\n }\n\n const sentenceRegex = /(?<=[.?!])(?=\\s+[\"“”'a-z])/gi;\n const sentences = para.split(sentenceRegex);\n\n let chunk = '';\n for (let sentence of sentences) {\n sentence = sentence.trim();\n\n if (sentence.length > maxChunkLength) {\n // Sentence too long — fallback split\n const subChunks = splitLongSentence(sentence, maxChunkLength);\n for (let sub of subChunks) {\n if ((chunk + ' ' + sub).length > maxChunkLength) {\n if (chunk) finalChunks.push(chunk.trim());\n chunk = sub;\n } else {\n chunk += (chunk ? ' ' : '') + sub;\n }\n }\n continue;\n }\n\n if ((chunk + ' ' + sentence).length > maxChunkLength) {\n if (chunk) finalChunks.push(chunk.trim());\n chunk = sentence;\n } else {\n chunk += (chunk ? ' ' : '') + sentence;\n }\n }\n if (chunk) finalChunks.push(chunk.trim());\n }\n\n return finalChunks;\n}\n\nexport function splitLongSentence(sentence, maxLen) {\n const chunks = [];\n let current = '';\n\n const commaParts = sentence.split(/,\\s*/);\n for (let part of commaParts) {\n if ((current + ', ' + part).length > maxLen) {\n if (current) chunks.push(current.trim());\n if (part.length > maxLen) {\n const words = part.split(/\\s+/);\n let wordChunk = '';\n for (let word of words) {\n if ((wordChunk + ' ' + word).length > maxLen) {\n if (wordChunk) chunks.push(wordChunk.trim());\n wordChunk = word;\n } else {\n wordChunk += (wordChunk ? ' ' : '') + word;\n }\n }\n if (wordChunk) chunks.push(wordChunk.trim());\n current = '';\n } else {\n current = part;\n }\n } else {\n current += (current ? ', ' : '') + part;\n }\n }\n if (current) chunks.push(current.trim());\n\n return chunks;\n}\n\n\nfunction splitTextSmartOld(text, maxChunkLength = 500) {\n const paragraphChunks = text.split(/\\n\\s*\\n/); // Step 1: split on double returns\n const finalChunks = [];\n\n for (let para of paragraphChunks) {\n if (para.length <= maxChunkLength) {\n finalChunks.push(para.trim());\n continue;\n }\n\n // Step 2: Further split on sentence boundaries if too long\n const sentenceRegex = /(?<=[.?!])(?=\\s+[\"“”'a-z])/gi;\n const sentences = para.split(sentenceRegex);\n\n let chunk = '';\n for (let sentence of sentences) {\n sentence = sentence.trim();\n if ((chunk + ' ' + sentence).length > maxChunkLength) {\n if (chunk) finalChunks.push(chunk.trim());\n chunk = sentence;\n } else {\n chunk += (chunk ? ' ' : '') + sentence;\n }\n }\n if (chunk) finalChunks.push(chunk.trim());\n }\n\n return finalChunks;\n}\n"],"mappings":";AAOA,SAAgB,EAAe,GAAM,IAAiB,KAAK;CACzD,IAAM,IAAkB,EAAK,MAAM,SAAS,GACtC,IAAc,CAAC;CAErB,KAAK,IAAI,KAAQ,GAAiB;EAChC,IAAI,EAAK,UAAU,GAAgB;GACjC,EAAY,KAAK,EAAK,KAAK,CAAC;GAC5B;EACF;EAGA,IAAM,IAAY,EAAK,MAAM,8BAAa,GAEtC,IAAQ;EACZ,KAAK,IAAI,KAAY,GAAW;GAG9B,IAFA,IAAW,EAAS,KAAK,GAErB,EAAS,SAAS,GAAgB;IAEpC,IAAM,IAAY,EAAkB,GAAU,CAAc;IAC5D,KAAK,IAAI,KAAO,GACd,CAAK,IAAQ,MAAM,EAAA,CAAK,SAAS,KAC3B,KAAO,EAAY,KAAK,EAAM,KAAK,CAAC,GACxC,IAAQ,KAER,MAAU,IAAQ,MAAM,MAAM;IAGlC;GACF;GAEA,CAAK,IAAQ,MAAM,EAAA,CAAU,SAAS,KAChC,KAAO,EAAY,KAAK,EAAM,KAAK,CAAC,GACxC,IAAQ,KAER,MAAU,IAAQ,MAAM,MAAM;EAElC;EACA,AAAI,KAAO,EAAY,KAAK,EAAM,KAAK,CAAC;CAC1C;CAEA,OAAO;AACT;AAEA,SAAgB,EAAkB,GAAU,GAAQ;CAClD,IAAM,IAAS,CAAC,GACZ,IAAU,IAER,IAAa,EAAS,MAAM,MAAM;CACxC,KAAK,IAAI,KAAQ,GACf,KAAK,IAAU,OAAO,EAAA,CAAM,SAAS,GAAQ;EAE3C,IADI,KAAS,EAAO,KAAK,EAAQ,KAAK,CAAC,GACnC,EAAK,SAAS,GAAQ;GACxB,IAAM,IAAQ,EAAK,MAAM,KAAK,GAC1B,IAAY;GAChB,KAAK,IAAI,KAAQ,GACf,CAAK,IAAY,MAAM,EAAA,CAAM,SAAS,KAChC,KAAW,EAAO,KAAK,EAAU,KAAK,CAAC,GAC3C,IAAY,KAEZ,MAAc,IAAY,MAAM,MAAM;GAI1C,AADI,KAAW,EAAO,KAAK,EAAU,KAAK,CAAC,GAC3C,IAAU;EACZ,OACE,IAAU;CAEd,OACE,MAAY,IAAU,OAAO,MAAM;CAKvC,OAFI,KAAS,EAAO,KAAK,EAAQ,KAAK,CAAC,GAEhC;AACT"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "use-voice-control",
3
- "version": "0.1.95",
3
+ "version": "0.1.97",
4
4
  "description": "React voice control with speech transcription, vocalization, and interruption (STT/TTS/VAD) support, plus a CLI that reads Markdown and text files aloud to audio files.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -54,29 +54,29 @@
54
54
  },
55
55
  "dependencies": {
56
56
  "@huggingface/transformers": "^3.8.1",
57
- "lucide-react": "^0.344.0",
57
+ "lucide-react": "^1.43.0",
58
58
  "@moonshine-ai/moonshine-js": "^0.1.29",
59
59
  "kokoro-js": "^1.2.1"
60
60
  },
61
61
  "devDependencies": {
62
- "@types/node": "^22.10.2",
63
- "@types/react": "^19.2.17",
64
- "@vitejs/plugin-react": "^4.3.4",
65
- "@types/react-dom": "^19.2.3",
66
- "react": "^19.2.7",
67
- "react-dom": "^19.2.7",
68
- "@vitest/coverage-v8": "^4.0.18",
69
- "typescript": "^5.3.3",
70
- "vite": "^5.0.0",
71
- "vite-plugin-dts": "^5.0.0",
72
- "vitest": "^4.0.18"
62
+ "@types/node": "^26.5.0",
63
+ "@types/react": "^19.2.18",
64
+ "@vitejs/plugin-react": "^6.1.1",
65
+ "@types/react-dom": "^19.2.7",
66
+ "react": "^19.2.8",
67
+ "react-dom": "^19.2.8",
68
+ "@vitest/coverage-v8": "^5.0.0",
69
+ "typescript": "^7.0.2",
70
+ "vite": "^8.2.2",
71
+ "vite-plugin-dts": "^5.1.0",
72
+ "vitest": "^5.0.0"
73
73
  },
74
74
  "peerDependencies": {
75
75
  "react": "^18.0.0 || ^19.0.0",
76
76
  "react-dom": "^18.0.0 || ^19.0.0"
77
77
  },
78
78
  "optionalDependencies": {
79
- "@huggingface/transformers": "^3.0.0"
79
+ "@huggingface/transformers": "^4.2.0"
80
80
  },
81
81
  "license": "rights.institute/PROSPER"
82
82
  }