@datalayer/agent-runtimes 1.5.28 → 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-C0PWJ_wj.js → ChatView-C_NOLqwI.js} +3 -3
- package/dist-embed/datalayer-app-main.js +83 -83
- 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 +14 -3
- package/lib/apps/embed/embedConfig.d.ts +1 -1
- package/lib/apps/embed/embedConfig.js +1 -1
- package/lib/apps/embed/visitorApp.d.ts +9 -2
- package/lib/apps/embed/visitorApp.js +9 -2
- package/lib/apps/plugins/chat/ChatView.js +4 -1
- package/lib/chat/base/ChatBase.js +12 -3
- package/lib/specs/appspecSchema.js +2 -2
- package/lib/specs/voices.js +42 -0
- package/lib/types/agentspecs.d.ts +1 -1
- package/lib/voice/ComposerVoice.d.ts +10 -2
- package/lib/voice/ComposerVoice.js +10 -2
- package/lib/voice/HandsFree.d.ts +55 -0
- package/lib/voice/HandsFree.js +211 -0
- package/lib/voice/chatVoice.d.ts +2 -1
- package/lib/voice/chatVoice.js +5 -3
- package/lib/voice/handsFree.d.ts +58 -0
- package/lib/voice/handsFree.js +48 -0
- package/lib/voice/index.d.ts +10 -2
- package/lib/voice/index.js +2 -0
- package/package.json +1 -1
- package/scripts/voice/measure.py +74 -0
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,8 +50,9 @@ 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,
|
|
@@ -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/index.d.ts
CHANGED
|
@@ -14,6 +14,8 @@ export * from './pinned';
|
|
|
14
14
|
export * from './capture';
|
|
15
15
|
export * from './hearing';
|
|
16
16
|
export * from './serverHearing';
|
|
17
|
+
export * from './handsFree';
|
|
18
|
+
export { HandsFree, type HandsFreeProps } from './HandsFree';
|
|
17
19
|
export * from './speaker';
|
|
18
20
|
export * from './consent';
|
|
19
21
|
export * from './settings';
|
|
@@ -47,8 +49,14 @@ export type { VoiceSpeaker } from './useVoiceSpeaker';
|
|
|
47
49
|
export interface ChatVoice {
|
|
48
50
|
/** The language listened to and spoken, BCP 47. */
|
|
49
51
|
language: string;
|
|
50
|
-
/**
|
|
51
|
-
|
|
52
|
+
/**
|
|
53
|
+
* `push_to_talk`: the microphone in the composer; `hands_free`: that, and
|
|
54
|
+
* *Talk with it* beside it — a conversation by voice (VO-12, VO-13);
|
|
55
|
+
* `off`: none.
|
|
56
|
+
*/
|
|
57
|
+
input: 'off' | 'push_to_talk' | 'hands_free';
|
|
58
|
+
/** The silence after speech that ends a hands-free turn, in ms (VO-12): 700 when unsaid. */
|
|
59
|
+
turnSilenceMs?: number;
|
|
52
60
|
/**
|
|
53
61
|
* When answers are heard: `always`, as they are written; `on_request`, a
|
|
54
62
|
* *Read aloud* under each answer (VO-21); or `off`, the default.
|
package/lib/voice/index.js
CHANGED
|
@@ -8,6 +8,8 @@ export * from './pinned';
|
|
|
8
8
|
export * from './capture';
|
|
9
9
|
export * from './hearing';
|
|
10
10
|
export * from './serverHearing';
|
|
11
|
+
export * from './handsFree';
|
|
12
|
+
export { HandsFree } from './HandsFree';
|
|
11
13
|
export * from './speaker';
|
|
12
14
|
export * from './consent';
|
|
13
15
|
export * from './settings';
|
package/package.json
CHANGED
package/scripts/voice/measure.py
CHANGED
|
@@ -92,6 +92,67 @@ def transcribe(
|
|
|
92
92
|
return json.loads(done.stdout.strip().splitlines()[-1])
|
|
93
93
|
|
|
94
94
|
|
|
95
|
+
def measure_server(
|
|
96
|
+
store: Path,
|
|
97
|
+
voices: Any,
|
|
98
|
+
fixtures: List[Dict[str, Any]],
|
|
99
|
+
wavs: Dict[str, Path],
|
|
100
|
+
measured: Path,
|
|
101
|
+
threads: int,
|
|
102
|
+
) -> None:
|
|
103
|
+
"""Each server speech-to-text model on the fixtures, as the speech service runs it (VO-47).
|
|
104
|
+
|
|
105
|
+
faster-whisper, 8-bit on the CPU with ``threads`` threads, told the
|
|
106
|
+
language, one clip at a time: what it heard and how long it took.
|
|
107
|
+
"""
|
|
108
|
+
from faster_whisper import WhisperModel # the speech image's own
|
|
109
|
+
|
|
110
|
+
for model in voices.SPEECH_MODEL_CATALOGUE.values():
|
|
111
|
+
if model["task"] != "stt" or model["where"] != ["server"]:
|
|
112
|
+
continue
|
|
113
|
+
started = time.monotonic()
|
|
114
|
+
whisper = WhisperModel(
|
|
115
|
+
str(store / model["provider"] / model["id"]),
|
|
116
|
+
device="cpu",
|
|
117
|
+
compute_type="int8",
|
|
118
|
+
cpu_threads=threads,
|
|
119
|
+
)
|
|
120
|
+
load_ms = round((time.monotonic() - started) * 1000)
|
|
121
|
+
by_language = {}
|
|
122
|
+
for language in ("en", "fr"):
|
|
123
|
+
clips = [clip for clip in fixtures if clip["language"] == language]
|
|
124
|
+
print(f"{model['id']} on {len(clips)} {language} clips", flush=True)
|
|
125
|
+
results = []
|
|
126
|
+
for clip in clips:
|
|
127
|
+
samples, _ = sf.read(wavs[clip["id"]], dtype="float32")
|
|
128
|
+
started = time.monotonic()
|
|
129
|
+
parts, _ = whisper.transcribe(
|
|
130
|
+
samples, language=language, vad_filter=True
|
|
131
|
+
)
|
|
132
|
+
text = " ".join(part.text.strip() for part in parts).strip()
|
|
133
|
+
results.append(
|
|
134
|
+
{
|
|
135
|
+
"id": clip["id"],
|
|
136
|
+
"heard": text,
|
|
137
|
+
"ms": round((time.monotonic() - started) * 1000),
|
|
138
|
+
"audio_s": round(len(samples) / 16000, 2),
|
|
139
|
+
}
|
|
140
|
+
)
|
|
141
|
+
by_language[language] = {"load_ms": load_ms, "clips": results}
|
|
142
|
+
(measured / f"stt-{model['id']}.json").write_text(
|
|
143
|
+
json.dumps(
|
|
144
|
+
{
|
|
145
|
+
"model": model["id"],
|
|
146
|
+
"runtime": f"faster-whisper, int8 on the CPU, {threads} threads",
|
|
147
|
+
"languages": by_language,
|
|
148
|
+
},
|
|
149
|
+
ensure_ascii=False,
|
|
150
|
+
indent=2,
|
|
151
|
+
)
|
|
152
|
+
+ "\n"
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
95
156
|
def main() -> int:
|
|
96
157
|
parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
|
|
97
158
|
parser.add_argument("--store", type=Path, required=True)
|
|
@@ -101,6 +162,14 @@ def main() -> int:
|
|
|
101
162
|
default=None,
|
|
102
163
|
help="Where @huggingface/transformers is installed, when not in this checkout",
|
|
103
164
|
)
|
|
165
|
+
parser.add_argument(
|
|
166
|
+
"--server",
|
|
167
|
+
action="store_true",
|
|
168
|
+
help="Measure the server's speech-to-text models instead, with faster-whisper (in the speech image)",
|
|
169
|
+
)
|
|
170
|
+
parser.add_argument(
|
|
171
|
+
"--threads", type=int, default=4, help="CPU threads for --server"
|
|
172
|
+
)
|
|
104
173
|
args = parser.parse_args()
|
|
105
174
|
voices = catalogue()
|
|
106
175
|
env = dict(os.environ)
|
|
@@ -124,6 +193,11 @@ def main() -> int:
|
|
|
124
193
|
wav = Path(scratch) / f"{clip['id']}.wav"
|
|
125
194
|
sf.write(wav, resample(samples, rate), 16000, subtype="PCM_16")
|
|
126
195
|
wavs[clip["id"]] = wav
|
|
196
|
+
if args.server:
|
|
197
|
+
measure_server(
|
|
198
|
+
args.store, voices, fixtures, wavs, measured, args.threads
|
|
199
|
+
)
|
|
200
|
+
return 0
|
|
127
201
|
# --- speech to text, on the device's models ---------------------------------
|
|
128
202
|
for model in voices.SPEECH_MODEL_CATALOGUE.values():
|
|
129
203
|
if model["task"] != "stt" or "device" not in model["where"]:
|