@datalayer/agent-runtimes 1.5.27 → 1.5.28
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-CUWE6I5M.js → ChatView-C0PWJ_wj.js} +4 -4
- package/dist-embed/datalayer-app-main.js +51 -41
- package/lib/apps/apps/AppKept.js +5 -1
- package/lib/apps/embed/element.js +10 -4
- package/lib/apps/embed/embedConfig.d.ts +10 -1
- package/lib/apps/embed/embedConfig.js +25 -1
- package/lib/apps/plugins/chat/ChatView.js +2 -2
- 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/specs/voices.js +47 -0
- package/lib/voice/ComposerVoice.js +25 -3
- 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 +4 -3
- package/lib/voice/chatVoice.js +6 -9
- package/lib/voice/consent.d.ts +8 -0
- package/lib/voice/consent.js +10 -0
- package/lib/voice/hearing.d.ts +23 -1
- package/lib/voice/hearing.js +18 -3
- package/lib/voice/index.d.ts +11 -6
- package/lib/voice/index.js +1 -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
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Hearing on Datalayer's servers (VOICE.md VO-34, VO-14, §5): what was said
|
|
7
|
+
* in one press, kept to its speech by Silero VAD in the page, then sent as a
|
|
8
|
+
* WAV recording to the speech service's `POST /speech/transcriptions`, where
|
|
9
|
+
* Whisper large-v3-turbo hears it — in any language it knows, and finding
|
|
10
|
+
* the language itself when the person asks for that (detection, VO-14). The
|
|
11
|
+
* sound is held in the request only: the service keeps neither it nor the
|
|
12
|
+
* words (VO-60).
|
|
13
|
+
*
|
|
14
|
+
* Used where the application's voice says `where: server`, where it says
|
|
15
|
+
* `auto` and no model in the browser hears the language, and where the
|
|
16
|
+
* person asked for their language to be detected — never when they chose to
|
|
17
|
+
* keep speech on the device. A clip model: its words come at the end, not
|
|
18
|
+
* while the person speaks (VO-11).
|
|
19
|
+
*
|
|
20
|
+
* @module voice/serverHearing
|
|
21
|
+
*/
|
|
22
|
+
import { transcriberFor } from '../specs/voices';
|
|
23
|
+
import { HEARING_RATE } from './capture';
|
|
24
|
+
/** Where the speech service hears a recording, on ai-agents' host. */
|
|
25
|
+
export const TRANSCRIPTIONS_PATH = '/api/ai-agents/v1/speech/transcriptions';
|
|
26
|
+
/** The model the speech service hears with (agentspecs' catalogue). */
|
|
27
|
+
export const SERVER_HEARING_MODEL = 'whisper-large-v3-turbo';
|
|
28
|
+
/** The shortest speech worth hearing, in samples: a word said alone. */
|
|
29
|
+
const SHORTEST = HEARING_RATE * 0.2;
|
|
30
|
+
/**
|
|
31
|
+
* Where a chat's voice hears (§5): in the browser unless the speech service
|
|
32
|
+
* is named and the person asked for detection, the application says
|
|
33
|
+
* `server`, or it says `auto` and no model in the browser hears the language.
|
|
34
|
+
*/
|
|
35
|
+
export function hearingPlace(voice) {
|
|
36
|
+
if (!voice.speechUrl) {
|
|
37
|
+
return 'device';
|
|
38
|
+
}
|
|
39
|
+
if (voice.detect || voice.where === 'server') {
|
|
40
|
+
return 'server';
|
|
41
|
+
}
|
|
42
|
+
if (voice.where === 'device') {
|
|
43
|
+
return 'device';
|
|
44
|
+
}
|
|
45
|
+
return transcriberFor(voice.language) ? 'device' : 'server';
|
|
46
|
+
}
|
|
47
|
+
/** Mono samples at 16 kHz as a 16-bit PCM WAV file: what the service reads. */
|
|
48
|
+
export function wavOf(samples, rate = HEARING_RATE) {
|
|
49
|
+
const bytes = new ArrayBuffer(44 + samples.length * 2);
|
|
50
|
+
const view = new DataView(bytes);
|
|
51
|
+
const text = (at, value) => {
|
|
52
|
+
for (let i = 0; i < value.length; i++) {
|
|
53
|
+
view.setUint8(at + i, value.charCodeAt(i));
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
text(0, 'RIFF');
|
|
57
|
+
view.setUint32(4, 36 + samples.length * 2, true);
|
|
58
|
+
text(8, 'WAVE');
|
|
59
|
+
text(12, 'fmt ');
|
|
60
|
+
view.setUint32(16, 16, true);
|
|
61
|
+
view.setUint16(20, 1, true); // PCM
|
|
62
|
+
view.setUint16(22, 1, true); // mono
|
|
63
|
+
view.setUint32(24, rate, true);
|
|
64
|
+
view.setUint32(28, rate * 2, true);
|
|
65
|
+
view.setUint16(32, 2, true);
|
|
66
|
+
view.setUint16(34, 16, true);
|
|
67
|
+
text(36, 'data');
|
|
68
|
+
view.setUint32(40, samples.length * 2, true);
|
|
69
|
+
for (let i = 0; i < samples.length; i++) {
|
|
70
|
+
const sample = Math.max(-1, Math.min(1, samples[i]));
|
|
71
|
+
view.setInt16(44 + i * 2, sample < 0 ? sample * 0x8000 : sample * 0x7fff, true);
|
|
72
|
+
}
|
|
73
|
+
return bytes;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Hears on Datalayer's servers: the browser's VAD finds the speech and when
|
|
77
|
+
* it stopped; Whisper, on the speech service, writes it.
|
|
78
|
+
*/
|
|
79
|
+
export class ServerHearing {
|
|
80
|
+
finder;
|
|
81
|
+
options;
|
|
82
|
+
where = 'server';
|
|
83
|
+
constructor(finder, options) {
|
|
84
|
+
this.finder = finder;
|
|
85
|
+
this.options = options;
|
|
86
|
+
}
|
|
87
|
+
/** Only the VAD loads in the page: the words are written on the server. */
|
|
88
|
+
ready(_language, progress) {
|
|
89
|
+
return this.finder.readyToFindSpeech(progress);
|
|
90
|
+
}
|
|
91
|
+
/** Whether speech was found and how long it has been silent; its words come at the end. */
|
|
92
|
+
async hearSoFar(audio) {
|
|
93
|
+
const { speech, silentMs } = await this.finder.findSpeech(audio);
|
|
94
|
+
return { text: '', spoke: speech.length >= SHORTEST, silentMs };
|
|
95
|
+
}
|
|
96
|
+
async hear(audio, language) {
|
|
97
|
+
const started = performance.now();
|
|
98
|
+
const { speech } = await this.finder.findSpeech(audio);
|
|
99
|
+
if (speech.length < SHORTEST) {
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
const { speechUrl, token, detect, fetcher = fetch } = this.options;
|
|
103
|
+
const query = detect ? '' : `?language=${encodeURIComponent(language)}`;
|
|
104
|
+
const answered = await fetcher(`${speechUrl.replace(/\/+$/, '')}${TRANSCRIPTIONS_PATH}${query}`, {
|
|
105
|
+
method: 'POST',
|
|
106
|
+
headers: {
|
|
107
|
+
'Content-Type': 'audio/wav',
|
|
108
|
+
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
109
|
+
},
|
|
110
|
+
body: wavOf(speech),
|
|
111
|
+
});
|
|
112
|
+
const body = (await answered.json().catch(() => ({})));
|
|
113
|
+
if (!answered.ok) {
|
|
114
|
+
throw new Error(typeof body.detail === 'string' && body.detail
|
|
115
|
+
? body.detail
|
|
116
|
+
: `What you said could not be heard on Datalayer's servers (${answered.status}).`);
|
|
117
|
+
}
|
|
118
|
+
const text = typeof body.text === 'string' ? body.text.trim() : '';
|
|
119
|
+
if (!text) {
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
text,
|
|
124
|
+
metadata: {
|
|
125
|
+
input: 'voice',
|
|
126
|
+
language: typeof body.language === 'string' && body.language
|
|
127
|
+
? body.language
|
|
128
|
+
: language,
|
|
129
|
+
engine: SERVER_HEARING_MODEL,
|
|
130
|
+
where: 'server',
|
|
131
|
+
},
|
|
132
|
+
seconds: speech.length / HEARING_RATE,
|
|
133
|
+
ms: Math.round(performance.now() - started),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
}
|
package/lib/voice/settings.d.ts
CHANGED
|
@@ -57,6 +57,11 @@ export interface VoiceSettings {
|
|
|
57
57
|
* speech service, so answers are spoken only if the browser can do it.
|
|
58
58
|
*/
|
|
59
59
|
onDeviceOnly?: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* Let Whisper find the language they speak (VO-14): what they say is
|
|
62
|
+
* heard on Datalayer's servers, which detect it, rather than in the browser.
|
|
63
|
+
*/
|
|
64
|
+
detectLanguage?: boolean;
|
|
60
65
|
}
|
|
61
66
|
/** The settings as they can be used: numbers in range, nothing else trusted. */
|
|
62
67
|
export declare function saneVoiceSettings(settings: VoiceSettings | null | undefined): VoiceSettings;
|
package/lib/voice/settings.js
CHANGED
|
@@ -40,6 +40,9 @@ export function saneVoiceSettings(settings) {
|
|
|
40
40
|
if (typeof kept.onDeviceOnly === 'boolean') {
|
|
41
41
|
sane.onDeviceOnly = kept.onDeviceOnly;
|
|
42
42
|
}
|
|
43
|
+
if (typeof kept.detectLanguage === 'boolean') {
|
|
44
|
+
sane.detectLanguage = kept.detectLanguage;
|
|
45
|
+
}
|
|
43
46
|
return sane;
|
|
44
47
|
}
|
|
45
48
|
/** What was kept, or nothing. Storage may be refused, or hold nonsense. */
|
|
@@ -174,6 +177,9 @@ export function voiceForPerson(voice, person = {}, browser = browserLanguage())
|
|
|
174
177
|
if (chosen.microphoneId) {
|
|
175
178
|
next.microphoneId = chosen.microphoneId;
|
|
176
179
|
}
|
|
180
|
+
if (chosen.detectLanguage) {
|
|
181
|
+
next.detect = true;
|
|
182
|
+
}
|
|
177
183
|
if (chosen.onDeviceOnly) {
|
|
178
184
|
// Nothing is asked of the speech service: no token, no origin. Answers
|
|
179
185
|
// are said by that service only (decision 1: no voice made in the
|
|
@@ -181,6 +187,8 @@ export function voiceForPerson(voice, person = {}, browser = browserLanguage())
|
|
|
181
187
|
next.speechUrl = undefined;
|
|
182
188
|
next.token = undefined;
|
|
183
189
|
next.output = 'off';
|
|
190
|
+
next.where = 'device';
|
|
191
|
+
next.detect = false;
|
|
184
192
|
}
|
|
185
193
|
return next;
|
|
186
194
|
}
|
package/lib/voice/speaker.d.ts
CHANGED
|
@@ -24,10 +24,11 @@ export interface SpeakerOptions {
|
|
|
24
24
|
/** The voice of the catalogue. */
|
|
25
25
|
voice: string;
|
|
26
26
|
language: string;
|
|
27
|
-
/**
|
|
27
|
+
/**
|
|
28
|
+
* The person's token, an application's principal's, a visitor's or an
|
|
29
|
+
* embed's — whose deployment is the one it names (VO-19, decision 16).
|
|
30
|
+
*/
|
|
28
31
|
token?: string;
|
|
29
|
-
/** The deployment an embed's page speaks for (VO-19): sent with each sentence. */
|
|
30
|
-
deployment?: string;
|
|
31
32
|
/**
|
|
32
33
|
* How fast it speaks, as a multiplier of the voice's own pace (VO-25,
|
|
33
34
|
* VO-30). Applied here rather than asked of the service: `POST
|
package/lib/voice/speaker.js
CHANGED
|
@@ -114,7 +114,7 @@ export class ServerSpeaker {
|
|
|
114
114
|
}
|
|
115
115
|
}
|
|
116
116
|
async fetch(text) {
|
|
117
|
-
const { speechUrl, voice, language, token,
|
|
117
|
+
const { speechUrl, voice, language, token, fetcher = fetch } = this.options;
|
|
118
118
|
try {
|
|
119
119
|
const answered = await fetcher(`${speechUrl.replace(/\/+$/, '')}${SYNTHESES_PATH}`, {
|
|
120
120
|
method: 'POST',
|
|
@@ -127,7 +127,6 @@ export class ServerSpeaker {
|
|
|
127
127
|
voice,
|
|
128
128
|
language,
|
|
129
129
|
format: 'opus',
|
|
130
|
-
...(deployment ? { deployment } : {}),
|
|
131
130
|
}),
|
|
132
131
|
});
|
|
133
132
|
if (!answered.ok) {
|
|
@@ -27,7 +27,6 @@ export function useVoiceSpeaker(voice, on = true) {
|
|
|
27
27
|
voice: voice.voice,
|
|
28
28
|
language: voice.language,
|
|
29
29
|
token: voice.token,
|
|
30
|
-
deployment: voice.deployment,
|
|
31
30
|
rate: voice.rate,
|
|
32
31
|
volume: voice.volume,
|
|
33
32
|
onState: setState,
|
|
@@ -40,7 +39,6 @@ export function useVoiceSpeaker(voice, on = true) {
|
|
|
40
39
|
voice: voice?.voice,
|
|
41
40
|
language: voice?.language,
|
|
42
41
|
token: voice?.token,
|
|
43
|
-
deployment: voice?.deployment,
|
|
44
42
|
rate: voice?.rate,
|
|
45
43
|
volume: voice?.volume,
|
|
46
44
|
});
|
|
@@ -49,7 +47,6 @@ export function useVoiceSpeaker(voice, on = true) {
|
|
|
49
47
|
voice?.voice,
|
|
50
48
|
voice?.language,
|
|
51
49
|
voice?.token,
|
|
52
|
-
voice?.deployment,
|
|
53
50
|
voice?.rate,
|
|
54
51
|
voice?.volume,
|
|
55
52
|
]);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@datalayer/agent-runtimes",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.28",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"workspaces": [
|
|
6
6
|
".",
|
|
@@ -158,7 +158,7 @@
|
|
|
158
158
|
"@datalayer/jupyter-react": "^2.0.20",
|
|
159
159
|
"@datalayer/jupyter-server-nbmodel": "^0.2.8",
|
|
160
160
|
"@datalayer/lexical-loro": "^1.0.6",
|
|
161
|
-
"@datalayer/primer-addons": "^1.0.
|
|
161
|
+
"@datalayer/primer-addons": "^1.0.40",
|
|
162
162
|
"@datalayer/primer-rjsf": "^1.0.3",
|
|
163
163
|
"@datalayer/personal-agent-protocol": "^0.24.0",
|
|
164
164
|
"@datalayer/reactor": "^1.3.0",
|