@datalayer/agent-runtimes 1.5.42 → 1.5.44

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.
Files changed (83) hide show
  1. package/dist-embed/chunks/1f30a-DxwAt2Ba.js +1 -0
  2. package/dist-embed/chunks/1f310-AqUM9a8q.js +1 -0
  3. package/dist-embed/chunks/1f324-8MHLOhTj.js +1 -0
  4. package/dist-embed/chunks/1f326-YSBr3aXI.js +1 -0
  5. package/dist-embed/chunks/1f327-Ie-nQBNO.js +1 -0
  6. package/dist-embed/chunks/1f33e-Cfl2WM63.js +1 -0
  7. package/dist-embed/chunks/1f372-3XLDlXkE.js +1 -0
  8. package/dist-embed/chunks/1f373-BPUJt2bp.js +1 -0
  9. package/dist-embed/chunks/1f39b-CSNtbAXr.js +1 -0
  10. package/dist-embed/chunks/1f3ab-BUCNQHz1.js +1 -0
  11. package/dist-embed/chunks/1f41b-Dre1z0gP.js +1 -0
  12. package/dist-embed/chunks/1f441-D8yah4C4.js +1 -0
  13. package/dist-embed/chunks/1f465-CPle6Hjq.js +1 -0
  14. package/dist-embed/chunks/1f4b9-aacFyckt.js +1 -0
  15. package/dist-embed/chunks/1f4be-DPAcAU4R.js +1 -0
  16. package/dist-embed/chunks/1f4c1-Ch6aJ0cj.js +1 -0
  17. package/dist-embed/chunks/1f4c2-Dx7BKmLO.js +1 -0
  18. package/dist-embed/chunks/1f4c4-CNvjvC5k.js +1 -0
  19. package/dist-embed/chunks/1f4d3-GTxRgjv7.js +1 -0
  20. package/dist-embed/chunks/1f4d6-COOZY-Q_.js +1 -0
  21. package/dist-embed/chunks/1f4dc-BhoW4cjD.js +1 -0
  22. package/dist-embed/chunks/1f4e1-DezU8ow-.js +1 -0
  23. package/dist-embed/chunks/1f4e2-DsStZsI-.js +1 -0
  24. package/dist-embed/chunks/1f4e7-BBi7c_zx.js +1 -0
  25. package/dist-embed/chunks/1f500-D9xRo-IT.js +1 -0
  26. package/dist-embed/chunks/1f501-CzKeDPcw.js +1 -0
  27. package/dist-embed/chunks/1f504-BYIHMoDP.js +1 -0
  28. package/dist-embed/chunks/1f52d-2QxCLKhU.js +1 -0
  29. package/dist-embed/chunks/1f552-4d0Y0kn7.js +1 -0
  30. package/dist-embed/chunks/1f58b-Cp8F_HBg.js +1 -0
  31. package/dist-embed/chunks/1f5b2-D6BUULuh.js +1 -0
  32. package/dist-embed/chunks/1f5c3-DjkgJLaz.js +1 -0
  33. package/dist-embed/chunks/1f5d1-C4Kpe4HL.js +1 -0
  34. package/dist-embed/chunks/1f5dc-DyLO7aB9.js +1 -0
  35. package/dist-embed/chunks/1f604-CEekcaYE.js +1 -0
  36. package/dist-embed/chunks/1f917-CyyS9fgf.js +1 -0
  37. package/dist-embed/chunks/1f9d1-200d-2696-D9eSB-0q.js +1 -0
  38. package/dist-embed/chunks/1fa79-DHhIZbWB.js +1 -0
  39. package/dist-embed/chunks/1fa9d-DUI9e77m.js +1 -0
  40. package/dist-embed/chunks/1fa9f-10ALsMaD.js +1 -0
  41. package/dist-embed/chunks/23f3-ASjqFsbT.js +1 -0
  42. package/dist-embed/chunks/2611-BnnmLm-K.js +1 -0
  43. package/dist-embed/chunks/2702-CfLyp35r.js +1 -0
  44. package/dist-embed/chunks/270f-CLwo30mk.js +1 -0
  45. package/dist-embed/chunks/2728-Qg6be-6p.js +1 -0
  46. package/dist-embed/chunks/2b1c-Uc1aRwAf.js +1 -0
  47. package/dist-embed/datalayer-app-main.js +67 -67
  48. package/lib/apps/apps/sessionRules.d.ts +14 -1
  49. package/lib/apps/apps/sessionRules.js +68 -2
  50. package/lib/apps/core/canvasParts.d.ts +7 -1
  51. package/lib/apps/plugins/app-rules/AppRulesCard.js +2 -2
  52. package/lib/apps/plugins/canvas-parts/index.d.ts +9 -1
  53. package/lib/apps/plugins/canvas-parts/index.js +28 -0
  54. package/lib/chat/ChatInline.js +43 -22
  55. package/lib/chat/base/ChatBase.js +43 -2
  56. package/lib/components/agents/AgentSummary.d.ts +2 -24
  57. package/lib/components/agents/AgentSummary.js +3 -19
  58. package/lib/hooks/useChat.js +110 -144
  59. package/lib/portable/activity.d.ts +78 -0
  60. package/lib/portable/activity.js +129 -0
  61. package/lib/portable/fixtures.d.ts +7 -0
  62. package/lib/portable/fixtures.js +50 -0
  63. package/lib/portable/index.d.ts +4 -1
  64. package/lib/portable/index.js +7 -1
  65. package/lib/portable/lifecycle.d.ts +77 -0
  66. package/lib/portable/lifecycle.js +135 -0
  67. package/lib/portable/runtimes.d.ts +68 -0
  68. package/lib/portable/runtimes.js +69 -0
  69. package/lib/voice/VoiceMenu.d.ts +2 -1
  70. package/lib/voice/VoiceMenu.js +2 -2
  71. package/lib/voice/chatVoice.d.ts +5 -2
  72. package/lib/voice/chatVoice.js +10 -4
  73. package/lib/voice/choices.d.ts +2 -0
  74. package/lib/voice/choices.js +6 -0
  75. package/lib/voice/index.d.ts +1 -0
  76. package/lib/voice/index.js +1 -0
  77. package/lib/voice/sayAloud.d.ts +25 -0
  78. package/lib/voice/sayAloud.js +34 -0
  79. package/lib/voice/serverHearing.d.ts +34 -2
  80. package/lib/voice/serverHearing.js +234 -6
  81. package/lib/voice/settings.d.ts +12 -0
  82. package/lib/voice/settings.js +9 -0
  83. package/package.json +1 -1
@@ -6,7 +6,8 @@
6
6
  * Hearing on Datalayer's servers (VOICE.md VO-34, VO-14, §5): what was said
7
7
  * in one press, kept to its speech by Silero VAD in the page, then sent as a
8
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
9
+ * Whisper hears it — small for a spoken turn, large-v3-turbo past one
10
+ * (decision 17) — in any language it knows, and finding
10
11
  * the language itself when the person asks for that (detection, VO-14). The
11
12
  * sound is held in the request only: the service keeps neither it nor the
12
13
  * words (VO-60).
@@ -14,8 +15,15 @@
14
15
  * Used where the application's voice says `where: server`, where it says
15
16
  * `auto` and no model in the browser hears the language, and where the
16
17
  * 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).
18
+ * keep speech on the device.
19
+ *
20
+ * While the person speaks, the words come too (VO-11): the first look at
21
+ * what was heard so far opens `WS /speech/transcriptions/stream` (the token
22
+ * in its first message, VO-37), the sound is sent as it is captured, 16-bit
23
+ * PCM at 16 kHz, and the service's partial words are what was heard so far,
24
+ * greyed in the composer until final. At the end of the press the stream is
25
+ * told so and its final words are the transcript. A press heard without a
26
+ * look before it (a short one) is sent whole as a recording instead.
19
27
  *
20
28
  * @module voice/serverHearing
21
29
  */
@@ -23,6 +31,12 @@ import { transcriberFor } from '../specs/voices';
23
31
  import { HEARING_RATE } from './capture';
24
32
  /** Where the speech service hears a recording, on ai-agents' host. */
25
33
  export const TRANSCRIPTIONS_PATH = '/api/ai-agents/v1/speech/transcriptions';
34
+ /** Where the speech service hears speech as it is said. */
35
+ export const TRANSCRIPTION_STREAM_PATH = '/api/ai-agents/v1/speech/transcriptions/stream';
36
+ /** A stream no look or hearing has fed for this long is closed: its press was cancelled. */
37
+ export const STREAM_IDLE_MS = 3000;
38
+ /** How long the final words of a stream are waited for. */
39
+ export const STREAM_FINAL_MS = 60_000;
26
40
  /** The model the speech service hears with (agentspecs' catalogue). */
27
41
  export const SERVER_HEARING_MODEL = 'whisper-large-v3-turbo';
28
42
  /** The shortest speech worth hearing, in samples: a word said alone. */
@@ -72,6 +86,162 @@ export function wavOf(samples, rate = HEARING_RATE) {
72
86
  }
73
87
  return bytes;
74
88
  }
89
+ /** 16 kHz samples in [-1, 1] as little-endian 16-bit PCM: what a `pcm16` stream takes. */
90
+ export function pcm16Of(samples) {
91
+ const bytes = new ArrayBuffer(samples.length * 2);
92
+ const view = new DataView(bytes);
93
+ for (let i = 0; i < samples.length; i++) {
94
+ const sample = Math.max(-1, Math.min(1, samples[i]));
95
+ view.setInt16(i * 2, sample < 0 ? sample * 0x8000 : sample * 0x7fff, true);
96
+ }
97
+ return bytes;
98
+ }
99
+ /** The ws(s) address of the stream on ai-agents' origin. */
100
+ export function streamUrlOf(speechUrl) {
101
+ return `${speechUrl.replace(/\/+$/, '').replace(/^http/, 'ws')}${TRANSCRIPTION_STREAM_PATH}`;
102
+ }
103
+ /**
104
+ * One press heard as it is said: the sound sent as it comes, the service's
105
+ * partial words kept, its final words awaited at the end.
106
+ */
107
+ class PressStream {
108
+ /** Samples of the press sent so far. */
109
+ sent = 0;
110
+ /** The newest partial words. */
111
+ partial = '';
112
+ /** Why the service refused, in its sentence. */
113
+ refused;
114
+ socket;
115
+ ready = false;
116
+ queued = [];
117
+ final;
118
+ waiting;
119
+ closed = false;
120
+ idle;
121
+ constructor(open, speechUrl, first) {
122
+ this.socket = open(streamUrlOf(speechUrl));
123
+ this.socket.binaryType = 'arraybuffer';
124
+ this.socket.onopen = () => {
125
+ this.socket.send(JSON.stringify({
126
+ token: first.token ?? '',
127
+ language: first.language ?? '',
128
+ format: 'pcm16',
129
+ }));
130
+ };
131
+ this.socket.onmessage = event => {
132
+ let said;
133
+ try {
134
+ said = JSON.parse(String(event.data));
135
+ }
136
+ catch {
137
+ return;
138
+ }
139
+ if (said.type === 'ready') {
140
+ this.ready = true;
141
+ for (const chunk of this.queued.splice(0)) {
142
+ this.socket.send(chunk);
143
+ }
144
+ }
145
+ else if (said.type === 'partial' && typeof said.text === 'string') {
146
+ this.partial = said.text.trim();
147
+ }
148
+ else if (said.type === 'final') {
149
+ this.final = {
150
+ text: typeof said.text === 'string' ? said.text.trim() : '',
151
+ language: typeof said.language === 'string' ? said.language : '',
152
+ };
153
+ this.settle();
154
+ }
155
+ else if (said.type === 'refused') {
156
+ this.refused =
157
+ typeof said.detail === 'string' && said.detail
158
+ ? said.detail
159
+ : "What you said could not be heard on Datalayer's servers.";
160
+ this.settle();
161
+ }
162
+ };
163
+ this.socket.onclose = () => {
164
+ this.closed = true;
165
+ this.settle();
166
+ };
167
+ this.socket.onerror = () => undefined;
168
+ this.touch();
169
+ }
170
+ /** Send what the press added since the last call. */
171
+ feed(audio) {
172
+ this.touch();
173
+ if (audio.length <= this.sent || this.closed) {
174
+ return;
175
+ }
176
+ const chunk = pcm16Of(audio.subarray(this.sent));
177
+ this.sent = audio.length;
178
+ if (this.ready) {
179
+ this.socket.send(chunk);
180
+ }
181
+ else {
182
+ this.queued.push(chunk);
183
+ }
184
+ }
185
+ /** The end of the press: the final words, or `undefined` when the stream ended without them. */
186
+ finish(audio) {
187
+ this.feed(audio);
188
+ if (this.final || this.closed || this.refused) {
189
+ return Promise.resolve(this.final);
190
+ }
191
+ return new Promise(resolve => {
192
+ const timer = setTimeout(() => {
193
+ this.waiting = undefined;
194
+ this.close();
195
+ resolve(undefined);
196
+ }, STREAM_FINAL_MS);
197
+ this.waiting = final => {
198
+ clearTimeout(timer);
199
+ resolve(final);
200
+ };
201
+ const end = JSON.stringify({ type: 'end' });
202
+ if (this.ready) {
203
+ this.socket.send(end);
204
+ }
205
+ else {
206
+ // Said once the service is ready, after the sound queued before it.
207
+ this.queued.push(end);
208
+ }
209
+ });
210
+ }
211
+ close() {
212
+ if (this.idle) {
213
+ clearTimeout(this.idle);
214
+ }
215
+ this.closed = true;
216
+ try {
217
+ this.socket.close(1000);
218
+ }
219
+ catch {
220
+ // Already closed.
221
+ }
222
+ this.settle();
223
+ }
224
+ touch() {
225
+ if (this.idle) {
226
+ clearTimeout(this.idle);
227
+ }
228
+ this.idle = setTimeout(() => {
229
+ if (!this.waiting) {
230
+ this.close();
231
+ }
232
+ }, STREAM_IDLE_MS);
233
+ }
234
+ settle() {
235
+ const waiting = this.waiting;
236
+ this.waiting = undefined;
237
+ waiting?.(this.final);
238
+ if (this.final || this.refused) {
239
+ if (this.idle) {
240
+ clearTimeout(this.idle);
241
+ }
242
+ }
243
+ }
244
+ }
75
245
  /**
76
246
  * Hears on Datalayer's servers: the browser's VAD finds the speech and when
77
247
  * it stopped; Whisper, on the speech service, writes it.
@@ -88,17 +258,75 @@ export class ServerHearing {
88
258
  ready(_language, progress) {
89
259
  return this.finder.readyToFindSpeech(progress);
90
260
  }
91
- /** Whether speech was found and how long it has been silent; its words come at the end. */
92
- async hearSoFar(audio) {
261
+ /** The press being heard as it is said, while one is. */
262
+ press;
263
+ /** The press's stream, opened anew when a new press began (its sound is shorter). */
264
+ streamFor(audio, language) {
265
+ const open = this.options.socket ??
266
+ (typeof WebSocket === 'undefined'
267
+ ? undefined
268
+ : (url) => new WebSocket(url));
269
+ if (!open) {
270
+ return undefined;
271
+ }
272
+ if (this.press && audio.length < this.press.sent) {
273
+ this.press.close();
274
+ this.press = undefined;
275
+ }
276
+ if (!this.press) {
277
+ this.press = new PressStream(open, this.options.speechUrl, {
278
+ token: this.options.token,
279
+ language: this.options.detect ? '' : language,
280
+ });
281
+ }
282
+ return this.press;
283
+ }
284
+ /**
285
+ * Whether speech was found and how long it has been silent, and the words
286
+ * the speech service heard so far: the sound goes to its stream as it comes.
287
+ */
288
+ async hearSoFar(audio, language = '') {
93
289
  const { speech, silentMs } = await this.finder.findSpeech(audio);
94
- return { text: '', spoke: speech.length >= SHORTEST, silentMs };
290
+ const spoke = speech.length >= SHORTEST;
291
+ const press = this.streamFor(audio, language);
292
+ press?.feed(audio);
293
+ return { text: spoke ? (press?.partial ?? '') : '', spoke, silentMs };
95
294
  }
96
295
  async hear(audio, language) {
97
296
  const started = performance.now();
297
+ const press = this.press && audio.length >= this.press.sent ? this.press : undefined;
298
+ if (this.press && !press) {
299
+ this.press.close();
300
+ }
301
+ this.press = undefined;
98
302
  const { speech } = await this.finder.findSpeech(audio);
99
303
  if (speech.length < SHORTEST) {
304
+ press?.close();
100
305
  return undefined;
101
306
  }
307
+ if (press) {
308
+ const final = await press.finish(audio);
309
+ press.close();
310
+ if (press.refused) {
311
+ throw new Error(press.refused);
312
+ }
313
+ if (!final) {
314
+ throw new Error("What you said could not be heard on Datalayer's servers: the stream ended before its words.");
315
+ }
316
+ return final.text
317
+ ? {
318
+ text: final.text,
319
+ metadata: {
320
+ input: 'voice',
321
+ language: final.language || language,
322
+ engine: SERVER_HEARING_MODEL,
323
+ where: 'server',
324
+ },
325
+ seconds: audio.length / HEARING_RATE,
326
+ ms: Math.round(performance.now() - started),
327
+ }
328
+ : undefined;
329
+ }
102
330
  const { speechUrl, token, detect, fetcher = fetch } = this.options;
103
331
  const query = detect ? '' : `?language=${encodeURIComponent(language)}`;
104
332
  const answered = await fetcher(`${speechUrl.replace(/\/+$/, '')}${TRANSCRIPTIONS_PATH}${query}`, {
@@ -33,6 +33,16 @@ export declare const VOLUME_RANGE: {
33
33
  readonly max: 1;
34
34
  readonly default: 1;
35
35
  };
36
+ /**
37
+ * How much longer than usual the person pauses before they are done
38
+ * (VOICE.md VO-69): the silence that stops a microphone clicked on, and the
39
+ * one that ends a hands-free turn, times this.
40
+ */
41
+ export declare const PAUSE_RANGE: {
42
+ readonly min: 1;
43
+ readonly max: 4;
44
+ readonly default: 1;
45
+ };
36
46
  /**
37
47
  * What a person chose for themselves. Every field is optional: an absent one
38
48
  * means "whatever the application says".
@@ -62,6 +72,8 @@ export interface VoiceSettings {
62
72
  * heard on Datalayer's servers, which detect it, rather than in the browser.
63
73
  */
64
74
  detectLanguage?: boolean;
75
+ /** How much longer they pause before they are done (VO-69): 1 is usual. */
76
+ pause?: number;
65
77
  }
66
78
  /** The settings as they can be used: numbers in range, nothing else trusted. */
67
79
  export declare function saneVoiceSettings(settings: VoiceSettings | null | undefined): VoiceSettings;
@@ -7,6 +7,12 @@ export const VOICE_SETTINGS_KEY = 'datalayer-voice-settings';
7
7
  /** How fast and how loud, as multipliers of the voice's own. */
8
8
  export const RATE_RANGE = { min: 0.5, max: 2, default: 1 };
9
9
  export const VOLUME_RANGE = { min: 0, max: 1, default: 1 };
10
+ /**
11
+ * How much longer than usual the person pauses before they are done
12
+ * (VOICE.md VO-69): the silence that stops a microphone clicked on, and the
13
+ * one that ends a hands-free turn, times this.
14
+ */
15
+ export const PAUSE_RANGE = { min: 1, max: 4, default: 1 };
10
16
  function clamp(value, { min, max }) {
11
17
  return Math.min(max, Math.max(min, value));
12
18
  }
@@ -43,6 +49,9 @@ export function saneVoiceSettings(settings) {
43
49
  if (typeof kept.detectLanguage === 'boolean') {
44
50
  sane.detectLanguage = kept.detectLanguage;
45
51
  }
52
+ if (typeof kept.pause === 'number' && Number.isFinite(kept.pause)) {
53
+ sane.pause = clamp(kept.pause, PAUSE_RANGE);
54
+ }
46
55
  return sane;
47
56
  }
48
57
  /** What was kept, or nothing. Storage may be refused, or hold nonsense. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datalayer/agent-runtimes",
3
- "version": "1.5.42",
3
+ "version": "1.5.44",
4
4
  "type": "module",
5
5
  "workspaces": [
6
6
  ".",