@datalayer/agent-runtimes 1.5.27 → 1.5.30
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-embed/chunks/ChatView-C_NOLqwI.js +32 -0
- package/dist-embed/datalayer-app-main.js +103 -93
- package/lib/apps/apps/AppKept.js +5 -1
- package/lib/apps/apps/appspec.d.ts +0 -5
- package/lib/apps/apps/appspec.js +1 -6
- package/lib/apps/apps/checks.js +4 -9
- package/lib/apps/embed/element.js +24 -7
- package/lib/apps/embed/embedConfig.d.ts +11 -2
- package/lib/apps/embed/embedConfig.js +26 -2
- package/lib/apps/embed/visitorApp.d.ts +9 -2
- package/lib/apps/embed/visitorApp.js +9 -2
- package/lib/apps/plugins/chat/ChatView.js +6 -3
- package/lib/chat/assistant/AssistantStage.js +17 -0
- package/lib/chat/assistant/clearance.d.ts +10 -0
- package/lib/chat/assistant/clearance.js +14 -0
- package/lib/chat/base/ChatBase.js +12 -3
- package/lib/specs/appspecSchema.js +2 -2
- package/lib/specs/voices.js +89 -0
- package/lib/types/agentspecs.d.ts +1 -1
- package/lib/voice/ComposerVoice.d.ts +10 -2
- package/lib/voice/ComposerVoice.js +35 -5
- package/lib/voice/HandsFree.d.ts +55 -0
- package/lib/voice/HandsFree.js +211 -0
- package/lib/voice/VoiceInput.d.ts +2 -2
- package/lib/voice/VoiceInput.js +6 -4
- package/lib/voice/VoiceMenu.js +2 -1
- package/lib/voice/chatVoice.d.ts +6 -4
- package/lib/voice/chatVoice.js +11 -12
- package/lib/voice/consent.d.ts +8 -0
- package/lib/voice/consent.js +10 -0
- package/lib/voice/handsFree.d.ts +58 -0
- package/lib/voice/handsFree.js +48 -0
- package/lib/voice/hearing.d.ts +23 -1
- package/lib/voice/hearing.js +18 -3
- package/lib/voice/index.d.ts +21 -8
- package/lib/voice/index.js +3 -0
- package/lib/voice/serverHearing.d.ts +58 -0
- package/lib/voice/serverHearing.js +136 -0
- package/lib/voice/settings.d.ts +5 -0
- package/lib/voice/settings.js +8 -0
- package/lib/voice/speaker.d.ts +4 -3
- package/lib/voice/speaker.js +1 -2
- package/lib/voice/useVoiceSpeaker.js +0 -3
- package/package.json +2 -2
- package/scripts/voice/measure.py +74 -0
- package/dist-embed/chunks/ChatView-CUWE6I5M.js +0 -32
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* *Talk with it*: a conversation by voice, hands-free (VOICE.md VO-12,
|
|
3
|
+
* VO-13), beside the composer's microphone where the application's voice
|
|
4
|
+
* says `input: hands_free`.
|
|
5
|
+
*
|
|
6
|
+
* On, the microphone stays open. The words are written in the composer as
|
|
7
|
+
* they are heard (VO-11); once the person has spoken and been silent for the
|
|
8
|
+
* turn's silence (`turnSilenceMs`, 700 ms unless said), what was said is
|
|
9
|
+
* heard whole and **sent** — nothing waits for a click. The answer is said as
|
|
10
|
+
* it is written (hands-free implies `output: always`), and listening goes
|
|
11
|
+
* on through it: speech heard while the agent thinks or speaks is
|
|
12
|
+
* **barge-in** (VO-13) — its voice stops at once, the run is stopped while
|
|
13
|
+
* the answer is still being written, the interrupted answer is marked in
|
|
14
|
+
* the conversation, and what was heard starts the next turn. The state is
|
|
15
|
+
* shown at all times: *Listening…*, *Thinking…*, *Speaking…*, *Paused*.
|
|
16
|
+
* `Esc` or *Pause* pauses it; the toggle again turns it off.
|
|
17
|
+
*
|
|
18
|
+
* The microphone is opened anew when the agent begins answering, so what it
|
|
19
|
+
* holds then is the person's; the browser's echo cancellation keeps the
|
|
20
|
+
* answer's own sound out (`openMicrophone`). The first use asks for the
|
|
21
|
+
* microphone as the click-to-talk does (VO-61), under the same consent.
|
|
22
|
+
*
|
|
23
|
+
* @module voice/HandsFree
|
|
24
|
+
*/
|
|
25
|
+
import type { JSX } from 'react';
|
|
26
|
+
import type { Hearing } from './hearing';
|
|
27
|
+
import type { SpokenMetadata } from './types';
|
|
28
|
+
export interface HandsFreeProps {
|
|
29
|
+
hearing: Hearing;
|
|
30
|
+
/** The language listened to, BCP 47. */
|
|
31
|
+
language: string;
|
|
32
|
+
/** Whose consent to the microphone is remembered: the application's id. */
|
|
33
|
+
consentKey: string;
|
|
34
|
+
/** The microphone the person chose (VO-30); the browser's own when unsaid. */
|
|
35
|
+
microphoneId?: string;
|
|
36
|
+
/** The silence after speech that ends a turn, in ms; 700 unless said (VO-12). */
|
|
37
|
+
turnSilenceMs?: number;
|
|
38
|
+
/** A turn, heard whole: sent at once. */
|
|
39
|
+
send: (text: string, metadata: SpokenMetadata) => void;
|
|
40
|
+
/** The words heard so far, for the composer (VO-11); `''` gives it back. */
|
|
41
|
+
onInterim?: (text: string) => void;
|
|
42
|
+
/** The agent is answering: a run is in progress. */
|
|
43
|
+
running?: boolean;
|
|
44
|
+
/** The agent's answer is being said. */
|
|
45
|
+
speaking?: boolean;
|
|
46
|
+
/** Stops the agent's voice at once (barge-in, VO-13). */
|
|
47
|
+
onStopSpeaking?: () => void;
|
|
48
|
+
/** Stops the run while the answer is still being written (barge-in, VO-13). */
|
|
49
|
+
onStopRun?: () => void;
|
|
50
|
+
/** The answer being written was interrupted: marked in the conversation (VO-13). */
|
|
51
|
+
onInterrupted?: () => void;
|
|
52
|
+
disabled?: boolean;
|
|
53
|
+
}
|
|
54
|
+
export declare function HandsFree({ hearing, language, consentKey, microphoneId, turnSilenceMs, send, onInterim, running, speaking, onStopSpeaking, onStopRun, onInterrupted, disabled, }: HandsFreeProps): JSX.Element;
|
|
55
|
+
export default HandsFree;
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { Button, IconButton, Text, Tooltip } from '@primer/react';
|
|
4
|
+
import { Box } from '@datalayer/primer-addons';
|
|
5
|
+
import { openMicrophone } from './capture';
|
|
6
|
+
import { CONSENT_SENTENCE, SERVER_CONSENT_SENTENCE, consent, consentKeyFor, consented, } from './consent';
|
|
7
|
+
import { HANDS_FREE_WORDS, IDLE_PAUSE_MS, TURN_SILENCE_MS, bargedIn, handsFreeState, turnEnded, } from './handsFree';
|
|
8
|
+
import { INTERIM_MS } from './VoiceInput';
|
|
9
|
+
/** Waits. */
|
|
10
|
+
const pause = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
11
|
+
export function HandsFree({ hearing, language, consentKey, microphoneId, turnSilenceMs = TURN_SILENCE_MS, send, onInterim, running = false, speaking = false, onStopSpeaking, onStopRun, onInterrupted, disabled = false, }) {
|
|
12
|
+
const [on, setOn] = useState(false);
|
|
13
|
+
const [paused, setPaused] = useState(false);
|
|
14
|
+
const [asking, setAsking] = useState(false);
|
|
15
|
+
const [problem, setProblem] = useState();
|
|
16
|
+
const state = handsFreeState({ on, paused, speaking, running });
|
|
17
|
+
const stateRef = useRef(state);
|
|
18
|
+
stateRef.current = state;
|
|
19
|
+
const runningRef = useRef(running);
|
|
20
|
+
runningRef.current = running;
|
|
21
|
+
const capture = useRef(undefined);
|
|
22
|
+
const latest = useRef({
|
|
23
|
+
send,
|
|
24
|
+
onInterim,
|
|
25
|
+
onStopSpeaking,
|
|
26
|
+
onStopRun,
|
|
27
|
+
onInterrupted,
|
|
28
|
+
});
|
|
29
|
+
latest.current = {
|
|
30
|
+
send,
|
|
31
|
+
onInterim,
|
|
32
|
+
onStopSpeaking,
|
|
33
|
+
onStopRun,
|
|
34
|
+
onInterrupted,
|
|
35
|
+
};
|
|
36
|
+
const key = consentKeyFor(consentKey, hearing.where);
|
|
37
|
+
const turnOn = useCallback(() => {
|
|
38
|
+
if (!consented(key)) {
|
|
39
|
+
setAsking(true);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
setProblem(undefined);
|
|
43
|
+
setPaused(false);
|
|
44
|
+
setOn(true);
|
|
45
|
+
}, [key]);
|
|
46
|
+
const turnOff = useCallback(() => {
|
|
47
|
+
setOn(false);
|
|
48
|
+
setPaused(false);
|
|
49
|
+
capture.current?.cancel();
|
|
50
|
+
capture.current = undefined;
|
|
51
|
+
latest.current.onInterim?.('');
|
|
52
|
+
}, []);
|
|
53
|
+
/*
|
|
54
|
+
* The loop: while on and not paused, the microphone is open and what it
|
|
55
|
+
* heard so far is looked at every INTERIM_MS. Listening, the turn ends on
|
|
56
|
+
* the silence and is sent; thinking or speaking, speech is barge-in. The
|
|
57
|
+
* microphone is opened anew when the agent begins answering, and after
|
|
58
|
+
* each turn, so what it holds is only what came since.
|
|
59
|
+
*/
|
|
60
|
+
const heardWhile = useRef('listening');
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (!on || paused) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
let alive = true;
|
|
66
|
+
let quietSince = Date.now();
|
|
67
|
+
void (async () => {
|
|
68
|
+
try {
|
|
69
|
+
await hearing.ready(language);
|
|
70
|
+
capture.current = await openMicrophone(microphoneId);
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
if (alive) {
|
|
74
|
+
setProblem(error instanceof Error
|
|
75
|
+
? error.message
|
|
76
|
+
: 'The microphone could not be opened.');
|
|
77
|
+
setOn(false);
|
|
78
|
+
}
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
heardWhile.current =
|
|
82
|
+
stateRef.current === 'listening' ? 'listening' : 'answering';
|
|
83
|
+
while (alive) {
|
|
84
|
+
await pause(INTERIM_MS);
|
|
85
|
+
const open = capture.current;
|
|
86
|
+
if (!alive || !open) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const now = stateRef.current;
|
|
90
|
+
const answering = now === 'thinking' || now === 'speaking';
|
|
91
|
+
// The agent began answering, or finished: hear anew from here.
|
|
92
|
+
if ((answering && heardWhile.current === 'listening') ||
|
|
93
|
+
(!answering && heardWhile.current === 'answering')) {
|
|
94
|
+
open.cancel();
|
|
95
|
+
try {
|
|
96
|
+
capture.current = await openMicrophone(microphoneId);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
heardWhile.current = answering ? 'answering' : 'listening';
|
|
102
|
+
quietSince = Date.now();
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
let so;
|
|
106
|
+
try {
|
|
107
|
+
so = await hearing.hearSoFar(open.peek(), language);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
so = undefined;
|
|
111
|
+
}
|
|
112
|
+
if (!alive || capture.current !== open || !so) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (so.text) {
|
|
116
|
+
latest.current.onInterim?.(so.text);
|
|
117
|
+
}
|
|
118
|
+
if (so.spoke) {
|
|
119
|
+
quietSince = Date.now();
|
|
120
|
+
}
|
|
121
|
+
if (bargedIn(now, so)) {
|
|
122
|
+
// The person speaks over the agent (VO-13): its voice and its run
|
|
123
|
+
// stop; what was heard goes on as the next turn.
|
|
124
|
+
latest.current.onStopSpeaking?.();
|
|
125
|
+
if (now === 'thinking' || runningRef.current) {
|
|
126
|
+
latest.current.onStopRun?.();
|
|
127
|
+
}
|
|
128
|
+
latest.current.onInterrupted?.();
|
|
129
|
+
heardWhile.current = 'listening';
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (now === 'listening' && turnEnded(so, turnSilenceMs)) {
|
|
133
|
+
capture.current = undefined;
|
|
134
|
+
const audio = await open.stop();
|
|
135
|
+
let heard;
|
|
136
|
+
try {
|
|
137
|
+
heard = await hearing.hear(audio, language);
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
heard = undefined;
|
|
141
|
+
if (alive) {
|
|
142
|
+
setProblem(error instanceof Error
|
|
143
|
+
? error.message
|
|
144
|
+
: 'What you said could not be heard.');
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (!alive) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
latest.current.onInterim?.('');
|
|
151
|
+
if (heard) {
|
|
152
|
+
latest.current.send(heard.text, heard.metadata);
|
|
153
|
+
}
|
|
154
|
+
try {
|
|
155
|
+
capture.current = await openMicrophone(microphoneId);
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
heardWhile.current = 'listening';
|
|
161
|
+
quietSince = Date.now();
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (now === 'listening' && Date.now() - quietSince >= IDLE_PAUSE_MS) {
|
|
165
|
+
setPaused(true);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
})();
|
|
170
|
+
return () => {
|
|
171
|
+
alive = false;
|
|
172
|
+
capture.current?.cancel();
|
|
173
|
+
capture.current = undefined;
|
|
174
|
+
};
|
|
175
|
+
// The loop reads the state through a ref: it is not restarted on it.
|
|
176
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
177
|
+
}, [on, paused, hearing, language, microphoneId, turnSilenceMs]);
|
|
178
|
+
// Esc pauses it.
|
|
179
|
+
useEffect(() => {
|
|
180
|
+
if (!on) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const down = (event) => {
|
|
184
|
+
if (event.key === 'Escape' && !paused) {
|
|
185
|
+
setPaused(true);
|
|
186
|
+
capture.current?.cancel();
|
|
187
|
+
capture.current = undefined;
|
|
188
|
+
latest.current.onInterim?.('');
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
window.addEventListener('keydown', down);
|
|
192
|
+
return () => window.removeEventListener('keydown', down);
|
|
193
|
+
}, [on, paused]);
|
|
194
|
+
return (_jsxs(Box, { display: "inline-flex", alignItems: "center", sx: { gap: 1, position: 'relative' }, "data-hands-free": state, children: [_jsx(Tooltip, { text: on
|
|
195
|
+
? 'Stop talking with it'
|
|
196
|
+
: 'Talk with it: speak, it answers, and listens on (hands-free)', direction: "n", children: _jsx(Button, { size: "small", variant: on ? 'primary' : 'invisible', "aria-pressed": on, "aria-label": on ? 'Stop talking with it' : 'Talk with it', disabled: disabled, "data-hands-free-toggle": "", onClick: () => (on ? turnOff() : turnOn()), children: HANDS_FREE_WORDS.off }) }), on ? (_jsxs(_Fragment, { children: [_jsx(Text, { sx: { fontSize: 0, color: 'fg.muted' }, role: "status", "aria-live": "polite", "data-hands-free-state": state, children: HANDS_FREE_WORDS[state] }), paused ? (_jsx(IconButton, { icon: () => _jsx("span", { "aria-hidden": "true", children: "\u25B6" }), size: "small", variant: "invisible", "aria-label": "Resume listening", "data-hands-free-resume": "", onClick: () => setPaused(false) })) : (_jsx(IconButton, { icon: () => _jsx("span", { "aria-hidden": "true", children: "\u275A\u275A" }), size: "small", variant: "invisible", "aria-label": "Pause listening (Esc)", "data-hands-free-pause": "", onClick: () => {
|
|
197
|
+
setPaused(true);
|
|
198
|
+
capture.current?.cancel();
|
|
199
|
+
capture.current = undefined;
|
|
200
|
+
latest.current.onInterim?.('');
|
|
201
|
+
} }))] })) : null, problem ? (_jsx(Text, { sx: { fontSize: 0, color: 'danger.fg' }, "data-hands-free-problem": "", children: problem })) : null, asking ? (_jsxs(Box, { role: "dialog", "aria-label": "The microphone", "data-hands-free-consent": "", position: "absolute", bottom: "100%", left: 0, width: 280, p: 3, bg: "canvas.overlay", border: "1px solid", borderColor: "border.default", borderRadius: 2, boxShadow: "shadow.large", zIndex: 10, children: [_jsx(Text, { as: "p", sx: { fontSize: 1, mb: 2 }, children: hearing.where === 'server'
|
|
202
|
+
? SERVER_CONSENT_SENTENCE
|
|
203
|
+
: CONSENT_SENTENCE }), _jsxs(Box, { display: "flex", gap: 2, justifyContent: "flex-end", children: [_jsx(Button, { size: "small", onClick: () => setAsking(false), children: "Not now" }), _jsx(Button, { size: "small", variant: "primary", "data-hands-free-consent-yes": "", onClick: () => {
|
|
204
|
+
consent(key);
|
|
205
|
+
setAsking(false);
|
|
206
|
+
setProblem(undefined);
|
|
207
|
+
setPaused(false);
|
|
208
|
+
setOn(true);
|
|
209
|
+
}, children: "Use the microphone" })] })] })) : null] }));
|
|
210
|
+
}
|
|
211
|
+
export default HandsFree;
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* @module voice/VoiceInput
|
|
19
19
|
*/
|
|
20
20
|
import type { JSX } from 'react';
|
|
21
|
-
import type {
|
|
21
|
+
import type { Hearing } from './hearing';
|
|
22
22
|
import type { Transcript } from './types';
|
|
23
23
|
/** What the microphone is doing, said in words (VO-67, VO-68). */
|
|
24
24
|
export type ListeningState = 'idle' | 'asking' | 'loading' | 'listening' | 'hearing';
|
|
@@ -27,7 +27,7 @@ export declare const INTERIM_MS = 700;
|
|
|
27
27
|
/** The longest a microphone listens in one go, in ms: then it stops by itself. */
|
|
28
28
|
export declare const LONGEST_MS = 60000;
|
|
29
29
|
export interface VoiceInputProps {
|
|
30
|
-
hearing:
|
|
30
|
+
hearing: Hearing;
|
|
31
31
|
/** The language listened to, BCP 47. */
|
|
32
32
|
language: string;
|
|
33
33
|
/** What was said, once heard. */
|
package/lib/voice/VoiceInput.js
CHANGED
|
@@ -5,7 +5,7 @@ import { Box } from '@datalayer/primer-addons';
|
|
|
5
5
|
import { MicrophoneIcon } from '@datalayer/icons-react';
|
|
6
6
|
import { MuteIcon } from '@primer/octicons-react';
|
|
7
7
|
import { MicrophoneRefused, openMicrophone } from './capture';
|
|
8
|
-
import { CONSENT_SENTENCE, consent, consented } from './consent';
|
|
8
|
+
import { CONSENT_SENTENCE, SERVER_CONSENT_SENTENCE, consent, consentKeyFor, consented, } from './consent';
|
|
9
9
|
const WORDS = {
|
|
10
10
|
idle: '',
|
|
11
11
|
asking: '',
|
|
@@ -49,7 +49,7 @@ export function VoiceInput({ hearing, language, onTranscript, onInterim, silence
|
|
|
49
49
|
if (disabled || stateRef.current !== 'idle') {
|
|
50
50
|
return;
|
|
51
51
|
}
|
|
52
|
-
if (!consented(consentKey)) {
|
|
52
|
+
if (!consented(consentKeyFor(consentKey, hearing.where))) {
|
|
53
53
|
setState('asking');
|
|
54
54
|
return;
|
|
55
55
|
}
|
|
@@ -277,8 +277,10 @@ export function VoiceInput({ hearing, language, onTranscript, onInterim, silence
|
|
|
277
277
|
void start();
|
|
278
278
|
}
|
|
279
279
|
}
|
|
280
|
-
} }) }), state === 'asking' && (_jsxs(Box, { role: "dialog", "aria-label": "Talk instead of typing", "data-voice-consent": "", position: "absolute", bottom: "100%", right: 0, mb: 2, width: 280, p: 3, bg: "canvas.overlay", border: "1px solid", borderColor: "border.default", borderRadius: 2, boxShadow: "shadow.large", zIndex: 10, children: [_jsx(Text, { as: "p", sx: { fontSize: 1, mb: 2 }, children:
|
|
281
|
-
|
|
280
|
+
} }) }), state === 'asking' && (_jsxs(Box, { role: "dialog", "aria-label": "Talk instead of typing", "data-voice-consent": "", position: "absolute", bottom: "100%", right: 0, mb: 2, width: 280, p: 3, bg: "canvas.overlay", border: "1px solid", borderColor: "border.default", borderRadius: 2, boxShadow: "shadow.large", zIndex: 10, children: [_jsx(Text, { as: "p", sx: { fontSize: 1, mb: 2 }, children: hearing.where === 'server'
|
|
281
|
+
? SERVER_CONSENT_SENTENCE
|
|
282
|
+
: CONSENT_SENTENCE }), _jsxs(Box, { display: "flex", gap: 2, justifyContent: "flex-end", children: [_jsx(Button, { size: "small", onClick: () => setState('idle'), children: "Not now" }), _jsx(Button, { size: "small", variant: "primary", "data-voice-consent-yes": "", onClick: () => {
|
|
283
|
+
consent(consentKeyFor(consentKey, hearing.where));
|
|
282
284
|
// Asked, and said yes: listen now, without another press.
|
|
283
285
|
stateRef.current = 'idle';
|
|
284
286
|
setState('idle');
|
package/lib/voice/VoiceMenu.js
CHANGED
|
@@ -20,6 +20,7 @@ export function VoiceMenu({ voice, answers, disabled = false, }) {
|
|
|
20
20
|
const [devices, setDevices] = useState([]);
|
|
21
21
|
const choose = (key, value) => keep(withVoiceSetting(settings, key, value, catalogueVoiceSpeaks));
|
|
22
22
|
const onDevice = Boolean(settings.onDeviceOnly);
|
|
23
|
+
const detect = Boolean(settings.detectLanguage);
|
|
23
24
|
const voices = voicesSpeaking(voice.language);
|
|
24
25
|
const named = devices.filter(device => device.label);
|
|
25
26
|
return (_jsxs(ActionMenu, { onOpenChange: open => {
|
|
@@ -27,6 +28,6 @@ export function VoiceMenu({ voice, answers, disabled = false, }) {
|
|
|
27
28
|
void microphones().then(setDevices);
|
|
28
29
|
}
|
|
29
30
|
}, children: [_jsx(ActionMenu.Anchor, { children: _jsx(IconButton, { icon: GearIcon, size: "small", variant: "invisible", "aria-label": "Voice settings", "data-voice-menu": "", disabled: disabled }) }), _jsx(ActionMenu.Overlay, { width: "medium", children: _jsxs(ActionList, { children: [_jsxs(ActionList.Group, { selectionVariant: "single", children: [_jsx(ActionList.GroupHeading, { children: "Language" }), voiceLanguages().map(language => (_jsx(ActionList.Item, { selected: language.value === voice.language, onSelect: () => choose('language', language.value), "data-voice-language": language.value, children: language.label }, language.value)))] }), answers && !onDevice ? (_jsxs(_Fragment, { children: [_jsx(ActionList.Divider, {}), _jsxs(ActionList.Group, { selectionVariant: "single", children: [_jsx(ActionList.GroupHeading, { children: "Answers heard" }), OUTPUT_CHOICES.map(output => (_jsx(ActionList.Item, { selected: output.value === voice.output, onSelect: () => choose('output', output.value), "data-voice-output": output.value, children: output.label }, output.value)))] }), voices.length > 0 ? (_jsxs(ActionList.Group, { selectionVariant: "single", children: [_jsx(ActionList.GroupHeading, { children: "Voice" }), voices.map(spoken => (_jsx(ActionList.Item, { selected: spoken.id === voice.voice, onSelect: () => choose('voice', spoken.id), "data-voice-voice": spoken.id, children: spoken.name }, spoken.id)))] })) : null, _jsxs(ActionList.Group, { selectionVariant: "single", children: [_jsx(ActionList.GroupHeading, { children: "Speed" }), RATE_CHOICES.map(rate => (_jsx(ActionList.Item, { selected: rate.value === (voice.rate ?? 1), onSelect: () => choose('rate', rate.value), "data-voice-rate": rate.value, children: rate.label }, rate.value)))] }), _jsxs(ActionList.Group, { selectionVariant: "single", children: [_jsx(ActionList.GroupHeading, { children: "Loudness" }), VOLUME_CHOICES.map(volume => (_jsx(ActionList.Item, { selected: volume.value === (voice.volume ?? 1), onSelect: () => choose('volume', volume.value), "data-voice-volume": volume.value, children: volume.label }, volume.value)))] })] })) : null, named.length > 1 ? (_jsxs(_Fragment, { children: [_jsx(ActionList.Divider, {}), _jsxs(ActionList.Group, { selectionVariant: "single", children: [_jsx(ActionList.GroupHeading, { children: "Microphone" }), named.map(device => (_jsx(ActionList.Item, { selected: device.deviceId ===
|
|
30
|
-
(voice.microphoneId ?? named[0].deviceId), onSelect: () => choose('microphoneId', device.deviceId), "data-voice-microphone": device.deviceId, children: device.label }, device.deviceId)))] })] })) : null, _jsx(ActionList.Divider, {}), _jsxs(ActionList.Group, { selectionVariant: "multiple", children: [_jsxs(ActionList.Item, { selected: Boolean(voice.sendWhatISay), onSelect: () => choose('sendWhatISay', !voice.sendWhatISay), "data-voice-send-what-i-say": "", children: ["Send what I say", _jsx(ActionList.Description, { variant: "block", children: "Sent as soon as it is heard, not left in the box." })] }), _jsxs(ActionList.Item, { selected: onDevice, onSelect: () => choose('onDeviceOnly', !onDevice), "data-voice-on-device": "", children: ["Keep speech on this device", _jsx(ActionList.Description, { variant: "block", children: "Nothing is sent to Datalayer\u2019s speech service: you are heard in the browser, answers are not spoken." })] })] }), Object.keys(settings).length > 0 ? (_jsxs(_Fragment, { children: [_jsx(ActionList.Divider, {}), _jsx(ActionList.Item, { onSelect: () => keep({}), "data-voice-settings-reset": "", children: "Use the application\u2019s" })] })) : null] }) })] }));
|
|
31
|
+
(voice.microphoneId ?? named[0].deviceId), onSelect: () => choose('microphoneId', device.deviceId), "data-voice-microphone": device.deviceId, children: device.label }, device.deviceId)))] })] })) : null, _jsx(ActionList.Divider, {}), _jsxs(ActionList.Group, { selectionVariant: "multiple", children: [_jsxs(ActionList.Item, { selected: Boolean(voice.sendWhatISay), onSelect: () => choose('sendWhatISay', !voice.sendWhatISay), "data-voice-send-what-i-say": "", children: ["Send what I say", _jsx(ActionList.Description, { variant: "block", children: "Sent as soon as it is heard, not left in the box." })] }), voice.speechUrl && !onDevice ? (_jsxs(ActionList.Item, { selected: detect, onSelect: () => choose('detectLanguage', !detect), "data-voice-detect": "", children: ["Detect the language I speak", _jsx(ActionList.Description, { variant: "block", children: "Heard on Datalayer\u2019s servers by Whisper, which finds the language; nothing is kept there." })] })) : null, _jsxs(ActionList.Item, { selected: onDevice, onSelect: () => choose('onDeviceOnly', !onDevice), "data-voice-on-device": "", children: ["Keep speech on this device", _jsx(ActionList.Description, { variant: "block", children: "Nothing is sent to Datalayer\u2019s speech service: you are heard in the browser, answers are not spoken." })] })] }), Object.keys(settings).length > 0 ? (_jsxs(_Fragment, { children: [_jsx(ActionList.Divider, {}), _jsx(ActionList.Item, { onSelect: () => keep({}), "data-voice-settings-reset": "", children: "Use the application\u2019s" })] })) : null] }) })] }));
|
|
31
32
|
}
|
|
32
33
|
export default VoiceMenu;
|
package/lib/voice/chatVoice.d.ts
CHANGED
|
@@ -35,7 +35,8 @@ export interface ChatVoicePlace {
|
|
|
35
35
|
* - **Listening**: a microphone in the composer — click it and speak, the
|
|
36
36
|
* words written in the box as they are heard, stopped by a second click
|
|
37
37
|
* or when the person stops speaking; or held, and let go (VO-10, VO-11).
|
|
38
|
-
* `hands_free` (VO-12)
|
|
38
|
+
* `hands_free` (VO-12): the microphone, and *Talk with it* beside it; it
|
|
39
|
+
* implies answers said as they are written.
|
|
39
40
|
* - **Speaking**: its answers said by Datalayer's speech service, on request
|
|
40
41
|
* or always (VO-20, VO-21); refused here when its host says no service.
|
|
41
42
|
*/
|
|
@@ -44,10 +45,11 @@ export declare function chatVoiceOf(spec: AppVoiceSpec | undefined, place: ChatV
|
|
|
44
45
|
* A chat's voice asked of the speech service as the chat itself speaks to
|
|
45
46
|
* its runtime (VO-37, VO-19): the token its host gave, else the chat's own —
|
|
46
47
|
* a visitor's token minted from ai-inference, an embed's token, or the
|
|
47
|
-
* person's
|
|
48
|
-
*
|
|
48
|
+
* person's. One token for the conversation and its voice, never a second.
|
|
49
|
+
* An embed's deployment is the one its token names (decision 16): the page
|
|
50
|
+
* says none.
|
|
49
51
|
*/
|
|
50
|
-
export declare function voiceAskedAs(voice: ChatVoice | undefined, token: string | undefined
|
|
52
|
+
export declare function voiceAskedAs(voice: ChatVoice | undefined, token: string | undefined): ChatVoice | undefined;
|
|
51
53
|
/** A chat's voice with every place said: what the chat reads. */
|
|
52
54
|
export type SettledVoice = Required<Pick<ChatVoice, 'language' | 'input' | 'output' | 'voice' | 'modelsUrl' | 'engines' | 'silenceMs'>> & Omit<ChatVoice, 'language' | 'input' | 'output' | 'voice' | 'modelsUrl' | 'engines' | 'silenceMs'>;
|
|
53
55
|
/** How long a silence after speech stops a microphone clicked on, when unsaid (VO-10). */
|
package/lib/voice/chatVoice.js
CHANGED
|
@@ -35,7 +35,8 @@ export const DATALAYER_VOICE_ENGINES = {
|
|
|
35
35
|
* - **Listening**: a microphone in the composer — click it and speak, the
|
|
36
36
|
* words written in the box as they are heard, stopped by a second click
|
|
37
37
|
* or when the person stops speaking; or held, and let go (VO-10, VO-11).
|
|
38
|
-
* `hands_free` (VO-12)
|
|
38
|
+
* `hands_free` (VO-12): the microphone, and *Talk with it* beside it; it
|
|
39
|
+
* implies answers said as they are written.
|
|
39
40
|
* - **Speaking**: its answers said by Datalayer's speech service, on request
|
|
40
41
|
* or always (VO-20, VO-21); refused here when its host says no service.
|
|
41
42
|
*/
|
|
@@ -49,34 +50,32 @@ export function chatVoiceOf(spec, place) {
|
|
|
49
50
|
const language = spec.language || place.language || 'en-US';
|
|
50
51
|
return {
|
|
51
52
|
language,
|
|
52
|
-
input: spec.input
|
|
53
|
-
|
|
53
|
+
input: spec.input,
|
|
54
|
+
// Hands-free implies the answers heard as they are written (VO-12).
|
|
55
|
+
output: spec.input === 'hands_free' ? 'always' : spec.output,
|
|
54
56
|
voice: spec.voice || voiceFor(language)?.id || '',
|
|
55
57
|
modelsUrl: place.modelsUrl || DATALAYER_SPEECH_MODELS_URL,
|
|
56
58
|
engines: DATALAYER_VOICE_ENGINES,
|
|
57
59
|
speechUrl: place.speechUrl,
|
|
58
60
|
token: place.token,
|
|
59
61
|
consentKey: place.consentKey,
|
|
62
|
+
where: spec.where ?? 'auto',
|
|
60
63
|
};
|
|
61
64
|
}
|
|
62
65
|
/**
|
|
63
66
|
* A chat's voice asked of the speech service as the chat itself speaks to
|
|
64
67
|
* its runtime (VO-37, VO-19): the token its host gave, else the chat's own —
|
|
65
68
|
* a visitor's token minted from ai-inference, an embed's token, or the
|
|
66
|
-
* person's
|
|
67
|
-
*
|
|
69
|
+
* person's. One token for the conversation and its voice, never a second.
|
|
70
|
+
* An embed's deployment is the one its token names (decision 16): the page
|
|
71
|
+
* says none.
|
|
68
72
|
*/
|
|
69
|
-
export function voiceAskedAs(voice, token
|
|
73
|
+
export function voiceAskedAs(voice, token) {
|
|
70
74
|
if (!voice) {
|
|
71
75
|
return undefined;
|
|
72
76
|
}
|
|
73
77
|
const asked = voice.token || token;
|
|
74
|
-
|
|
75
|
-
return {
|
|
76
|
-
...voice,
|
|
77
|
-
...(asked ? { token: asked } : {}),
|
|
78
|
-
...(runs ? { deployment: runs } : {}),
|
|
79
|
-
};
|
|
78
|
+
return asked ? { ...voice, token: asked } : { ...voice };
|
|
80
79
|
}
|
|
81
80
|
/** How long a silence after speech stops a microphone clicked on, when unsaid (VO-10). */
|
|
82
81
|
export const SILENCE_MS = 1500;
|
package/lib/voice/consent.d.ts
CHANGED
|
@@ -4,3 +4,11 @@ export declare function consented(app: string): boolean;
|
|
|
4
4
|
export declare function consent(app: string): void;
|
|
5
5
|
/** The sentence said before the first use: where the audio goes (VO-61). */
|
|
6
6
|
export declare const CONSENT_SENTENCE = "What you say is transcribed on this device: the sound never leaves it, and is not kept. The words go in the message box, for you to send.";
|
|
7
|
+
/**
|
|
8
|
+
* The sentence said before the first use when what is said is heard on
|
|
9
|
+
* Datalayer's servers (VO-34, VO-61): asked apart from the device's, since
|
|
10
|
+
* a yes to one is not a yes to the other.
|
|
11
|
+
*/
|
|
12
|
+
export declare const SERVER_CONSENT_SENTENCE = "What you say is sent to Datalayer\u2019s servers to be transcribed: the sound and the words are not kept there. The words go in the message box, for you to send.";
|
|
13
|
+
/** Whose consent is remembered for hearing in a place: the application's, apart for the server. */
|
|
14
|
+
export declare function consentKeyFor(app: string, where: 'device' | 'server'): string;
|
package/lib/voice/consent.js
CHANGED
|
@@ -32,3 +32,13 @@ export function consent(app) {
|
|
|
32
32
|
}
|
|
33
33
|
/** The sentence said before the first use: where the audio goes (VO-61). */
|
|
34
34
|
export const CONSENT_SENTENCE = 'What you say is transcribed on this device: the sound never leaves it, and is not kept. The words go in the message box, for you to send.';
|
|
35
|
+
/**
|
|
36
|
+
* The sentence said before the first use when what is said is heard on
|
|
37
|
+
* Datalayer's servers (VO-34, VO-61): asked apart from the device's, since
|
|
38
|
+
* a yes to one is not a yes to the other.
|
|
39
|
+
*/
|
|
40
|
+
export const SERVER_CONSENT_SENTENCE = 'What you say is sent to Datalayer’s servers to be transcribed: the sound and the words are not kept there. The words go in the message box, for you to send.';
|
|
41
|
+
/** Whose consent is remembered for hearing in a place: the application's, apart for the server. */
|
|
42
|
+
export function consentKeyFor(app, where) {
|
|
43
|
+
return where === 'server' ? `${app}:server` : app;
|
|
44
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A conversation by voice, hands-free (VOICE.md VO-12, VO-13): what the
|
|
3
|
+
* composer is doing at every moment, and when a turn ends or the person
|
|
4
|
+
* speaks over the agent. Pure: the component (`HandsFree`) keeps the
|
|
5
|
+
* microphone and the clock, this decides.
|
|
6
|
+
*
|
|
7
|
+
* - **Listening**: the microphone is open; the turn ends after
|
|
8
|
+
* `turnSilenceMs` of silence once the person has spoken (700 ms unless
|
|
9
|
+
* the application or the person says otherwise), and is sent.
|
|
10
|
+
* - **Thinking**: the turn was sent and the agent is answering, nothing
|
|
11
|
+
* said yet.
|
|
12
|
+
* - **Speaking**: the answer is being said. The microphone stays open:
|
|
13
|
+
* speech heard now is barge-in (VO-13) — the voice stops at once, the run
|
|
14
|
+
* is stopped when the answer is still being written, and what was heard
|
|
15
|
+
* starts the next turn.
|
|
16
|
+
* - **Paused**: the person paused it (`Esc`, *Pause*), or nothing was said
|
|
17
|
+
* for `IDLE_PAUSE_MS`; nothing is heard until resumed.
|
|
18
|
+
*
|
|
19
|
+
* @module voice/handsFree
|
|
20
|
+
*/
|
|
21
|
+
/** What hands-free is doing, shown at all times (VO-12). */
|
|
22
|
+
export type HandsFreeState = 'off' | 'listening' | 'thinking' | 'speaking' | 'paused';
|
|
23
|
+
/** The word for each state, as the composer shows it. */
|
|
24
|
+
export declare const HANDS_FREE_WORDS: Record<HandsFreeState, string>;
|
|
25
|
+
/** The silence after speech that ends a turn, in ms, unless said otherwise (VO-12). */
|
|
26
|
+
export declare const TURN_SILENCE_MS = 700;
|
|
27
|
+
/** Listening with nothing said for this long pauses it: a microphone is not left open on an empty room. */
|
|
28
|
+
export declare const IDLE_PAUSE_MS: number;
|
|
29
|
+
/** What decides the state. */
|
|
30
|
+
export interface HandsFreeInputs {
|
|
31
|
+
/** The toggle is on. */
|
|
32
|
+
on: boolean;
|
|
33
|
+
/** The person paused it, or the room went quiet. */
|
|
34
|
+
paused: boolean;
|
|
35
|
+
/** The agent's answer is being said. */
|
|
36
|
+
speaking: boolean;
|
|
37
|
+
/** A run is in progress: the agent is answering. */
|
|
38
|
+
running: boolean;
|
|
39
|
+
}
|
|
40
|
+
/** The state for what is known: speaking wins over thinking, pausing over both. */
|
|
41
|
+
export declare function handsFreeState(inputs: HandsFreeInputs): HandsFreeState;
|
|
42
|
+
/** What the microphone heard so far, as the hearing says it (`HeardSoFar`). */
|
|
43
|
+
export interface HeardSoFarLike {
|
|
44
|
+
spoke: boolean;
|
|
45
|
+
silentMs: number;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Whether the turn ended: the person spoke, and has been silent for the
|
|
49
|
+
* turn's silence since (VO-12).
|
|
50
|
+
*/
|
|
51
|
+
export declare function turnEnded(heard: HeardSoFarLike, turnSilenceMs?: number): boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Whether the person spoke over the agent (VO-13): speech found since the
|
|
54
|
+
* agent began answering, while it thinks or speaks. The microphone is opened
|
|
55
|
+
* anew when the agent begins, so what it holds is the person's, not the
|
|
56
|
+
* answer's echo — the browser's echo cancellation keeps that out.
|
|
57
|
+
*/
|
|
58
|
+
export declare function bargedIn(state: HandsFreeState, heard: HeardSoFarLike): boolean;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
/** The word for each state, as the composer shows it. */
|
|
6
|
+
export const HANDS_FREE_WORDS = {
|
|
7
|
+
off: 'Talk with it',
|
|
8
|
+
listening: 'Listening…',
|
|
9
|
+
thinking: 'Thinking…',
|
|
10
|
+
speaking: 'Speaking…',
|
|
11
|
+
paused: 'Paused',
|
|
12
|
+
};
|
|
13
|
+
/** The silence after speech that ends a turn, in ms, unless said otherwise (VO-12). */
|
|
14
|
+
export const TURN_SILENCE_MS = 700;
|
|
15
|
+
/** Listening with nothing said for this long pauses it: a microphone is not left open on an empty room. */
|
|
16
|
+
export const IDLE_PAUSE_MS = 5 * 60_000;
|
|
17
|
+
/** The state for what is known: speaking wins over thinking, pausing over both. */
|
|
18
|
+
export function handsFreeState(inputs) {
|
|
19
|
+
if (!inputs.on) {
|
|
20
|
+
return 'off';
|
|
21
|
+
}
|
|
22
|
+
if (inputs.paused) {
|
|
23
|
+
return 'paused';
|
|
24
|
+
}
|
|
25
|
+
if (inputs.speaking) {
|
|
26
|
+
return 'speaking';
|
|
27
|
+
}
|
|
28
|
+
if (inputs.running) {
|
|
29
|
+
return 'thinking';
|
|
30
|
+
}
|
|
31
|
+
return 'listening';
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Whether the turn ended: the person spoke, and has been silent for the
|
|
35
|
+
* turn's silence since (VO-12).
|
|
36
|
+
*/
|
|
37
|
+
export function turnEnded(heard, turnSilenceMs = TURN_SILENCE_MS) {
|
|
38
|
+
return heard.spoke && heard.silentMs >= turnSilenceMs;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Whether the person spoke over the agent (VO-13): speech found since the
|
|
42
|
+
* agent began answering, while it thinks or speaks. The microphone is opened
|
|
43
|
+
* anew when the agent begins, so what it holds is the person's, not the
|
|
44
|
+
* answer's echo — the browser's echo cancellation keeps that out.
|
|
45
|
+
*/
|
|
46
|
+
export function bargedIn(state, heard) {
|
|
47
|
+
return (state === 'speaking' || state === 'thinking') && heard.spoke;
|
|
48
|
+
}
|
package/lib/voice/hearing.d.ts
CHANGED
|
@@ -22,15 +22,30 @@ export interface HeardSoFar {
|
|
|
22
22
|
/** How long it has been silent since the speech last ended, in ms. */
|
|
23
23
|
silentMs: number;
|
|
24
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* What hears a press, wherever it runs (§5): in the page (`DeviceHearing`)
|
|
27
|
+
* or on Datalayer's servers (`ServerHearing`).
|
|
28
|
+
*/
|
|
29
|
+
export interface Hearing {
|
|
30
|
+
/** Where the sound is heard: what the consent says, and a transcript's `where`. */
|
|
31
|
+
readonly where: 'device' | 'server';
|
|
32
|
+
/** Load what hears a language, saying how far it has come. */
|
|
33
|
+
ready(language: string, progress?: HearingProgress): Promise<void>;
|
|
34
|
+
/** What was said so far while the microphone stays open (VO-11). */
|
|
35
|
+
hearSoFar(audio: Float32Array, language: string): Promise<HeardSoFar>;
|
|
36
|
+
/** What was said in a press, or `undefined` when nothing was. */
|
|
37
|
+
hear(audio: Float32Array, language: string): Promise<Transcript | undefined>;
|
|
38
|
+
}
|
|
25
39
|
/** Joins the speech a VAD found, in order. */
|
|
26
40
|
export declare function joinSegments(segments: Float32Array[]): Float32Array;
|
|
27
41
|
/**
|
|
28
42
|
* The page's hearing: loaded once, on first use, from Datalayer's origin
|
|
29
43
|
* (`modelsUrl`), every file checked against its pin.
|
|
30
44
|
*/
|
|
31
|
-
export declare class DeviceHearing {
|
|
45
|
+
export declare class DeviceHearing implements Hearing {
|
|
32
46
|
private readonly modelsUrl;
|
|
33
47
|
private readonly engines;
|
|
48
|
+
readonly where: "device";
|
|
34
49
|
private recognizers;
|
|
35
50
|
private vad?;
|
|
36
51
|
constructor(modelsUrl: string, engines: VoiceEngines);
|
|
@@ -40,6 +55,13 @@ export declare class DeviceHearing {
|
|
|
40
55
|
sizeFor(language: string): number;
|
|
41
56
|
/** Load what hears a language, saying how far it has come. */
|
|
42
57
|
ready(language: string, progress?: HearingProgress): Promise<void>;
|
|
58
|
+
/** Load only what finds speech (Silero VAD): what hearing on the server needs here. */
|
|
59
|
+
readyToFindSpeech(progress?: HearingProgress): Promise<void>;
|
|
60
|
+
/** The speech in what was heard, and how long it has been silent since it last ended, in ms. */
|
|
61
|
+
findSpeech(audio: Float32Array): Promise<{
|
|
62
|
+
speech: Float32Array;
|
|
63
|
+
silentMs: number;
|
|
64
|
+
}>;
|
|
43
65
|
private recognizer;
|
|
44
66
|
private activity;
|
|
45
67
|
/** The speech Silero VAD finds in what was heard, and where the last ended, in ms. */
|
package/lib/voice/hearing.js
CHANGED
|
@@ -45,6 +45,7 @@ export function joinSegments(segments) {
|
|
|
45
45
|
export class DeviceHearing {
|
|
46
46
|
modelsUrl;
|
|
47
47
|
engines;
|
|
48
|
+
where = 'device';
|
|
48
49
|
recognizers = new Map();
|
|
49
50
|
vad;
|
|
50
51
|
constructor(modelsUrl, engines) {
|
|
@@ -67,6 +68,22 @@ export class DeviceHearing {
|
|
|
67
68
|
async ready(language, progress) {
|
|
68
69
|
await Promise.all([this.recognizer(language, progress), this.activity()]);
|
|
69
70
|
}
|
|
71
|
+
/** Load only what finds speech (Silero VAD): what hearing on the server needs here. */
|
|
72
|
+
async readyToFindSpeech(progress) {
|
|
73
|
+
const total = downloadSize('silero-vad');
|
|
74
|
+
progress?.(0, total);
|
|
75
|
+
await this.activity();
|
|
76
|
+
progress?.(total, total);
|
|
77
|
+
}
|
|
78
|
+
/** The speech in what was heard, and how long it has been silent since it last ended, in ms. */
|
|
79
|
+
async findSpeech(audio) {
|
|
80
|
+
const { speech, lastEnd } = await this.speechIn(audio);
|
|
81
|
+
const heardMs = (audio.length / HEARING_RATE) * 1000;
|
|
82
|
+
return {
|
|
83
|
+
speech,
|
|
84
|
+
silentMs: lastEnd === undefined ? 0 : Math.max(0, heardMs - lastEnd),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
70
87
|
recognizer(language, progress) {
|
|
71
88
|
const model = this.modelFor(language);
|
|
72
89
|
let loading = this.recognizers.get(model.id);
|
|
@@ -163,9 +180,7 @@ export class DeviceHearing {
|
|
|
163
180
|
* since the speech ended — what stops a click to talk (VO-10).
|
|
164
181
|
*/
|
|
165
182
|
async hearSoFar(audio, language) {
|
|
166
|
-
const { speech,
|
|
167
|
-
const heardMs = (audio.length / HEARING_RATE) * 1000;
|
|
168
|
-
const silentMs = lastEnd === undefined ? 0 : Math.max(0, heardMs - lastEnd);
|
|
183
|
+
const { speech, silentMs } = await this.findSpeech(audio);
|
|
169
184
|
if (speech.length < HEARING_RATE * 0.2) {
|
|
170
185
|
return { text: '', spoke: false, silentMs };
|
|
171
186
|
}
|