@datalayer/agent-runtimes 1.5.59 → 1.5.61
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-lgDCrM9e.js → ChatView-CjD1TsaE.js} +1 -1
- package/dist-embed/datalayer-app-main.js +96 -96
- package/lib/apps/apps/AppRenderer.js +8 -1
- package/lib/apps/plugins/chat/ChatView.js +3 -1
- package/lib/chat/ChatFloating.js +4 -0
- package/lib/chat/base/ChatBase.js +34 -5
- package/lib/chat/prompt/InputPrompt.js +3 -0
- package/lib/chat/prompt/stack.d.ts +22 -0
- package/lib/chat/prompt/stack.js +40 -1
- package/lib/models/PageTag.d.ts +1 -1
- package/lib/protocols/AGUIAdapter.d.ts +8 -0
- package/lib/protocols/AGUIAdapter.js +26 -0
- package/lib/types/chat.d.ts +6 -0
- package/lib/voice/ComposerVoice.js +2 -2
- package/lib/voice/HandsFree.d.ts +14 -3
- package/lib/voice/HandsFree.js +52 -8
- package/lib/voice/VoiceInput.d.ts +6 -1
- package/lib/voice/VoiceInput.js +21 -6
- package/lib/voice/chatVoice.d.ts +1 -1
- package/lib/voice/chatVoice.js +3 -1
- package/lib/voice/consent.d.ts +6 -2
- package/lib/voice/consent.js +8 -3
- package/lib/voice/handsFree.d.ts +2 -2
- package/lib/voice/handsFree.js +1 -1
- package/lib/voice/hearing.d.ts +13 -0
- package/lib/voice/hearing.js +8 -0
- package/lib/voice/index.d.ts +8 -1
- package/lib/voice/index.js +1 -0
- package/lib/voice/keptSound.d.ts +15 -0
- package/lib/voice/keptSound.js +69 -0
- package/lib/voice/sayAloud.d.ts +7 -0
- package/lib/voice/sayAloud.js +7 -0
- package/lib/voice/serverHearing.d.ts +4 -0
- package/lib/voice/serverHearing.js +20 -0
- package/lib/voice/speaker.d.ts +15 -0
- package/lib/voice/speaker.js +36 -1
- package/lib/voice/types.d.ts +12 -0
- package/lib/voice/useVoiceSpeaker.js +2 -0
- package/package.json +1 -1
package/lib/voice/chatVoice.js
CHANGED
|
@@ -41,7 +41,7 @@ export const DATALAYER_VOICE_ENGINES = {
|
|
|
41
41
|
* - **Speaking**: its answers said by Datalayer's speech service, on request
|
|
42
42
|
* or always (VO-20, VO-21); refused here when its host says no service.
|
|
43
43
|
*/
|
|
44
|
-
export function chatVoiceOf(spec, place) {
|
|
44
|
+
export function chatVoiceOf(spec, place, keepsAudio) {
|
|
45
45
|
if (!spec?.enabled || (spec.input === 'off' && spec.output === 'off')) {
|
|
46
46
|
return undefined;
|
|
47
47
|
}
|
|
@@ -61,6 +61,8 @@ export function chatVoiceOf(spec, place) {
|
|
|
61
61
|
token: place.token,
|
|
62
62
|
consentKey: place.consentKey,
|
|
63
63
|
where: spec.where ?? 'auto',
|
|
64
|
+
// Kept as sound only when it listens, as its record says (VO-63).
|
|
65
|
+
...(keepsAudio && spec.input !== 'off' ? { keepsAudio } : {}),
|
|
64
66
|
};
|
|
65
67
|
}
|
|
66
68
|
/**
|
package/lib/voice/consent.d.ts
CHANGED
|
@@ -10,5 +10,9 @@ export declare const CONSENT_SENTENCE = "What you say is transcribed on this dev
|
|
|
10
10
|
* a yes to one is not a yes to the other.
|
|
11
11
|
*/
|
|
12
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
|
-
/**
|
|
14
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Whose consent is remembered for hearing in a place: the application's,
|
|
15
|
+
* apart for the server, and apart again when what is said is kept as sound
|
|
16
|
+
* (VO-63) — a yes to being heard is not a yes to being kept.
|
|
17
|
+
*/
|
|
18
|
+
export declare function consentKeyFor(app: string, where: 'device' | 'server', keepsAudio?: number): string;
|
package/lib/voice/consent.js
CHANGED
|
@@ -38,7 +38,12 @@ export const CONSENT_SENTENCE = 'What you say is transcribed on this device: the
|
|
|
38
38
|
* a yes to one is not a yes to the other.
|
|
39
39
|
*/
|
|
40
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
|
-
/**
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
/**
|
|
42
|
+
* Whose consent is remembered for hearing in a place: the application's,
|
|
43
|
+
* apart for the server, and apart again when what is said is kept as sound
|
|
44
|
+
* (VO-63) — a yes to being heard is not a yes to being kept.
|
|
45
|
+
*/
|
|
46
|
+
export function consentKeyFor(app, where, keepsAudio) {
|
|
47
|
+
const heard = where === 'server' ? `${app}:server` : app;
|
|
48
|
+
return keepsAudio ? `${heard}:kept` : heard;
|
|
44
49
|
}
|
package/lib/voice/handsFree.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* microphone and the clock, this decides.
|
|
6
6
|
*
|
|
7
7
|
* - **Listening**: the microphone is open; the turn ends after
|
|
8
|
-
* `turnSilenceMs` of silence once the person has spoken (
|
|
8
|
+
* `turnSilenceMs` of silence once the person has spoken (500 ms unless
|
|
9
9
|
* the application or the person says otherwise), and is sent.
|
|
10
10
|
* - **Thinking**: the turn was sent and the agent is answering, nothing
|
|
11
11
|
* said yet.
|
|
@@ -23,7 +23,7 @@ export type HandsFreeState = 'off' | 'listening' | 'thinking' | 'speaking' | 'pa
|
|
|
23
23
|
/** The word for each state, as the composer shows it. */
|
|
24
24
|
export declare const HANDS_FREE_WORDS: Record<HandsFreeState, string>;
|
|
25
25
|
/** The silence after speech that ends a turn, in ms, unless said otherwise (VO-12). */
|
|
26
|
-
export declare const TURN_SILENCE_MS =
|
|
26
|
+
export declare const TURN_SILENCE_MS = 500;
|
|
27
27
|
/** Listening with nothing said for this long pauses it: a microphone is not left open on an empty room. */
|
|
28
28
|
export declare const IDLE_PAUSE_MS: number;
|
|
29
29
|
/** What decides the state. */
|
package/lib/voice/handsFree.js
CHANGED
|
@@ -11,7 +11,7 @@ export const HANDS_FREE_WORDS = {
|
|
|
11
11
|
paused: 'Paused',
|
|
12
12
|
};
|
|
13
13
|
/** The silence after speech that ends a turn, in ms, unless said otherwise (VO-12). */
|
|
14
|
-
export const TURN_SILENCE_MS =
|
|
14
|
+
export const TURN_SILENCE_MS = 500;
|
|
15
15
|
/** Listening with nothing said for this long pauses it: a microphone is not left open on an empty room. */
|
|
16
16
|
export const IDLE_PAUSE_MS = 5 * 60_000;
|
|
17
17
|
/** The state for what is known: speaking wins over thinking, pausing over both. */
|
package/lib/voice/hearing.d.ts
CHANGED
|
@@ -35,6 +35,17 @@ export interface Hearing {
|
|
|
35
35
|
hearSoFar(audio: Float32Array, language: string): Promise<HeardSoFar>;
|
|
36
36
|
/** What was said in a press, or `undefined` when nothing was. */
|
|
37
37
|
hear(audio: Float32Array, language: string): Promise<Transcript | undefined>;
|
|
38
|
+
/**
|
|
39
|
+
* Whether there is speech in some sound, found by the VAD alone — nothing
|
|
40
|
+
* transcribed, nothing sent (VO-52): hands-free's quick look for barge-in.
|
|
41
|
+
*/
|
|
42
|
+
spokeSoFar?(audio: Float32Array, shortestSeconds?: number): Promise<boolean>;
|
|
43
|
+
/**
|
|
44
|
+
* Whether there is speech in some sound and how long it has been quiet
|
|
45
|
+
* since, by the VAD alone — no words (VO-52): hands-free's quick look for
|
|
46
|
+
* the end of a turn once the person has spoken.
|
|
47
|
+
*/
|
|
48
|
+
quietSoFar?(audio: Float32Array): Promise<HeardSoFar>;
|
|
38
49
|
}
|
|
39
50
|
/** Joins the speech a VAD found, in order. */
|
|
40
51
|
export declare function joinSegments(segments: Float32Array[]): Float32Array;
|
|
@@ -73,6 +84,8 @@ export declare class DeviceHearing implements Hearing {
|
|
|
73
84
|
* words, for the composer as they come, and how long it has been silent
|
|
74
85
|
* since the speech ended — what stops a click to talk (VO-10).
|
|
75
86
|
*/
|
|
87
|
+
spokeSoFar(audio: Float32Array, shortestSeconds?: number): Promise<boolean>;
|
|
88
|
+
quietSoFar(audio: Float32Array): Promise<HeardSoFar>;
|
|
76
89
|
hearSoFar(audio: Float32Array, language: string): Promise<HeardSoFar>;
|
|
77
90
|
/**
|
|
78
91
|
* What was said in a press, or `undefined` when nothing was: its speech
|
package/lib/voice/hearing.js
CHANGED
|
@@ -179,6 +179,14 @@ export class DeviceHearing {
|
|
|
179
179
|
* words, for the composer as they come, and how long it has been silent
|
|
180
180
|
* since the speech ended — what stops a click to talk (VO-10).
|
|
181
181
|
*/
|
|
182
|
+
async spokeSoFar(audio, shortestSeconds = 0.2) {
|
|
183
|
+
const { speech } = await this.findSpeech(audio);
|
|
184
|
+
return speech.length >= HEARING_RATE * shortestSeconds;
|
|
185
|
+
}
|
|
186
|
+
async quietSoFar(audio) {
|
|
187
|
+
const { speech, silentMs } = await this.findSpeech(audio);
|
|
188
|
+
return { text: '', spoke: speech.length >= HEARING_RATE * 0.2, silentMs };
|
|
189
|
+
}
|
|
182
190
|
async hearSoFar(audio, language) {
|
|
183
191
|
const { speech, silentMs } = await this.findSpeech(audio);
|
|
184
192
|
if (speech.length < HEARING_RATE * 0.2) {
|
package/lib/voice/index.d.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import type { VoiceEngines } from './types';
|
|
11
11
|
export * from './types';
|
|
12
12
|
export * from './sentences';
|
|
13
|
+
export * from './keptSound';
|
|
13
14
|
export * from './pinned';
|
|
14
15
|
export * from './capture';
|
|
15
16
|
export * from './hearing';
|
|
@@ -56,7 +57,7 @@ export interface ChatVoice {
|
|
|
56
57
|
* `off`: none.
|
|
57
58
|
*/
|
|
58
59
|
input: 'off' | 'push_to_talk' | 'hands_free';
|
|
59
|
-
/** The silence after speech that ends a hands-free turn, in ms (VO-12):
|
|
60
|
+
/** The silence after speech that ends a hands-free turn, in ms (VO-12): 500 when unsaid. */
|
|
60
61
|
turnSilenceMs?: number;
|
|
61
62
|
/**
|
|
62
63
|
* When answers are heard: `always`, as they are written; `on_request`, a
|
|
@@ -87,6 +88,12 @@ export interface ChatVoice {
|
|
|
87
88
|
detect?: boolean;
|
|
88
89
|
/** The token the speech service is asked with. */
|
|
89
90
|
token?: string;
|
|
91
|
+
/**
|
|
92
|
+
* The days the application keeps what is said as sound (VO-63), when its
|
|
93
|
+
* record keeps `audio`: asked in the microphone's first sentence, each
|
|
94
|
+
* turn's recording sent with its words. Never for a visitor.
|
|
95
|
+
*/
|
|
96
|
+
keepsAudio?: number;
|
|
90
97
|
/** Send what is said at once, rather than put it in the composer. */
|
|
91
98
|
sendWhatISay?: boolean;
|
|
92
99
|
/**
|
package/lib/voice/index.js
CHANGED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { SpokenSound } from './types';
|
|
2
|
+
/** The longest a sound is kept, whatever the record keeps (decision 10). */
|
|
3
|
+
export declare const AUDIO_KEPT_DAYS_AT_MOST = 30;
|
|
4
|
+
/**
|
|
5
|
+
* How many days an application keeps what is said as sound: `undefined`
|
|
6
|
+
* when its record does not keep `audio`, else its record's days, 30 at most.
|
|
7
|
+
*/
|
|
8
|
+
export declare function keptAudioDays(record?: {
|
|
9
|
+
include?: readonly string[];
|
|
10
|
+
retentionDays?: number;
|
|
11
|
+
}): number | undefined;
|
|
12
|
+
/** The sentence added to the microphone's first one when the sound is kept (VO-61). */
|
|
13
|
+
export declare const keptAudioSentence: (days: number) => string;
|
|
14
|
+
/** A turn's samples (mono, 16 kHz, -1 to 1) as a WAV, base64, to keep. */
|
|
15
|
+
export declare function soundOf(audio: Float32Array): SpokenSound;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* What the person said, kept as sound (VO-63): an application whose record
|
|
7
|
+
* keeps `audio` (`record.include`), and a person who agreed in the
|
|
8
|
+
* microphone's first sentence, send each turn's recording with its words —
|
|
9
|
+
* `forwardedProps.voice.sound`, a 16-bit mono WAV at 16 kHz — and its
|
|
10
|
+
* runtime gives it to ai-agents to keep, 30 days at most, deleted with the
|
|
11
|
+
* conversation. Never for a visitor without an account.
|
|
12
|
+
*
|
|
13
|
+
* @module voice/keptSound
|
|
14
|
+
*/
|
|
15
|
+
import { HEARING_RATE } from './capture';
|
|
16
|
+
/** The longest a sound is kept, whatever the record keeps (decision 10). */
|
|
17
|
+
export const AUDIO_KEPT_DAYS_AT_MOST = 30;
|
|
18
|
+
/** The most of a turn kept: a minute. */
|
|
19
|
+
const KEPT_SECONDS_AT_MOST = 60;
|
|
20
|
+
/**
|
|
21
|
+
* How many days an application keeps what is said as sound: `undefined`
|
|
22
|
+
* when its record does not keep `audio`, else its record's days, 30 at most.
|
|
23
|
+
*/
|
|
24
|
+
export function keptAudioDays(record) {
|
|
25
|
+
if (!record?.include?.includes('audio')) {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
const days = record.retentionDays || AUDIO_KEPT_DAYS_AT_MOST;
|
|
29
|
+
return Math.max(1, Math.min(days, AUDIO_KEPT_DAYS_AT_MOST));
|
|
30
|
+
}
|
|
31
|
+
/** The sentence added to the microphone's first one when the sound is kept (VO-61). */
|
|
32
|
+
export const keptAudioSentence = (days) => `This application also keeps what you say as sound, with this conversation, for ${days} ${days === 1 ? 'day' : 'days'} at most: its owner can listen to it, and it is deleted with the conversation. Not now, and you type instead.`;
|
|
33
|
+
/** A turn's samples (mono, 16 kHz, -1 to 1) as a WAV, base64, to keep. */
|
|
34
|
+
export function soundOf(audio) {
|
|
35
|
+
const samples = audio.subarray(0, HEARING_RATE * KEPT_SECONDS_AT_MOST);
|
|
36
|
+
const bytes = new Uint8Array(44 + samples.length * 2);
|
|
37
|
+
const view = new DataView(bytes.buffer);
|
|
38
|
+
const text = (at, value) => {
|
|
39
|
+
for (let k = 0; k < value.length; k++) {
|
|
40
|
+
view.setUint8(at + k, value.charCodeAt(k));
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
text(0, 'RIFF');
|
|
44
|
+
view.setUint32(4, 36 + samples.length * 2, true);
|
|
45
|
+
text(8, 'WAVE');
|
|
46
|
+
text(12, 'fmt ');
|
|
47
|
+
view.setUint32(16, 16, true);
|
|
48
|
+
view.setUint16(20, 1, true);
|
|
49
|
+
view.setUint16(22, 1, true);
|
|
50
|
+
view.setUint32(24, HEARING_RATE, true);
|
|
51
|
+
view.setUint32(28, HEARING_RATE * 2, true);
|
|
52
|
+
view.setUint16(32, 2, true);
|
|
53
|
+
view.setUint16(34, 16, true);
|
|
54
|
+
text(36, 'data');
|
|
55
|
+
view.setUint32(40, samples.length * 2, true);
|
|
56
|
+
for (let k = 0; k < samples.length; k++) {
|
|
57
|
+
const sample = Math.max(-1, Math.min(1, samples[k]));
|
|
58
|
+
view.setInt16(44 + k * 2, sample < 0 ? sample * 0x8000 : sample * 0x7fff, true);
|
|
59
|
+
}
|
|
60
|
+
let binary = '';
|
|
61
|
+
for (let k = 0; k < bytes.length; k += 0x8000) {
|
|
62
|
+
binary += String.fromCharCode(...bytes.subarray(k, k + 0x8000));
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
data: btoa(binary),
|
|
66
|
+
type: 'audio/wav',
|
|
67
|
+
seconds: Math.round((samples.length / HEARING_RATE) * 100) / 100,
|
|
68
|
+
};
|
|
69
|
+
}
|
package/lib/voice/sayAloud.d.ts
CHANGED
|
@@ -14,6 +14,11 @@
|
|
|
14
14
|
export declare const VOICE_SAY_EVENT = "voice.say";
|
|
15
15
|
/** The running application's voice, at a run's start. */
|
|
16
16
|
export declare const VOICE_SETTINGS_EVENT = "voice.settings";
|
|
17
|
+
/**
|
|
18
|
+
* The person spoke over the agent, the runtime says (VO-46): a client
|
|
19
|
+
* playing its voice stops it.
|
|
20
|
+
*/
|
|
21
|
+
export declare const VOICE_INTERRUPTED_EVENT = "voice.interrupted";
|
|
17
22
|
/** The longest line said aloud: the runtime refuses longer ones. */
|
|
18
23
|
export declare const SAY_ALOUD_MAX_CHARACTERS = 200;
|
|
19
24
|
/** A custom event or data part, as the chat's adapters give it. */
|
|
@@ -21,5 +26,7 @@ export interface VoiceActivity {
|
|
|
21
26
|
type: string;
|
|
22
27
|
data: unknown;
|
|
23
28
|
}
|
|
29
|
+
/** Whether an activity is the runtime's `voice.interrupted` (VO-46). */
|
|
30
|
+
export declare const isVoiceInterrupted: (activity: VoiceActivity) => boolean;
|
|
24
31
|
/** The line a `voice.say` says, or `undefined` for anything else. */
|
|
25
32
|
export declare function lineToSay(activity: VoiceActivity): string | undefined;
|
package/lib/voice/sayAloud.js
CHANGED
|
@@ -18,9 +18,16 @@
|
|
|
18
18
|
export const VOICE_SAY_EVENT = 'voice.say';
|
|
19
19
|
/** The running application's voice, at a run's start. */
|
|
20
20
|
export const VOICE_SETTINGS_EVENT = 'voice.settings';
|
|
21
|
+
/**
|
|
22
|
+
* The person spoke over the agent, the runtime says (VO-46): a client
|
|
23
|
+
* playing its voice stops it.
|
|
24
|
+
*/
|
|
25
|
+
export const VOICE_INTERRUPTED_EVENT = 'voice.interrupted';
|
|
21
26
|
/** The longest line said aloud: the runtime refuses longer ones. */
|
|
22
27
|
export const SAY_ALOUD_MAX_CHARACTERS = 200;
|
|
23
28
|
const nameOf = (type) => type.replace(/^data-/, '');
|
|
29
|
+
/** Whether an activity is the runtime's `voice.interrupted` (VO-46). */
|
|
30
|
+
export const isVoiceInterrupted = (activity) => nameOf(activity.type) === VOICE_INTERRUPTED_EVENT;
|
|
24
31
|
/** The line a `voice.say` says, or `undefined` for anything else. */
|
|
25
32
|
export function lineToSay(activity) {
|
|
26
33
|
if (nameOf(activity.type) !== VOICE_SAY_EVENT) {
|
|
@@ -94,6 +94,8 @@ export declare class ServerHearing implements Hearing {
|
|
|
94
94
|
* Whether speech was found and how long it has been silent, and the words
|
|
95
95
|
* the speech service heard so far: the sound goes to its stream as it comes.
|
|
96
96
|
*/
|
|
97
|
+
spokeSoFar(audio: Float32Array, shortestSeconds?: number): Promise<boolean>;
|
|
98
|
+
quietSoFar(audio: Float32Array): Promise<HeardSoFar>;
|
|
97
99
|
hearSoFar(audio: Float32Array, language?: string): Promise<HeardSoFar>;
|
|
98
100
|
hear(audio: Float32Array, language: string): Promise<Transcript | undefined>;
|
|
99
101
|
}
|
|
@@ -121,5 +123,7 @@ export declare class AutoHearing implements Hearing {
|
|
|
121
123
|
private get current();
|
|
122
124
|
ready(language: string, progress?: HearingProgress): Promise<void>;
|
|
123
125
|
hearSoFar(audio: Float32Array, language: string): Promise<HeardSoFar>;
|
|
126
|
+
spokeSoFar(audio: Float32Array, shortestSeconds?: number): Promise<boolean>;
|
|
127
|
+
quietSoFar(audio: Float32Array): Promise<HeardSoFar>;
|
|
124
128
|
hear(audio: Float32Array, language: string): Promise<Transcript | undefined>;
|
|
125
129
|
}
|
|
@@ -315,6 +315,14 @@ export class ServerHearing {
|
|
|
315
315
|
* Whether speech was found and how long it has been silent, and the words
|
|
316
316
|
* the speech service heard so far: the sound goes to its stream as it comes.
|
|
317
317
|
*/
|
|
318
|
+
async spokeSoFar(audio, shortestSeconds = 0.2) {
|
|
319
|
+
const { speech } = await this.finder.findSpeech(audio);
|
|
320
|
+
return speech.length >= HEARING_RATE * shortestSeconds;
|
|
321
|
+
}
|
|
322
|
+
async quietSoFar(audio) {
|
|
323
|
+
const { speech, silentMs } = await this.finder.findSpeech(audio);
|
|
324
|
+
return { text: '', spoke: speech.length >= SHORTEST, silentMs };
|
|
325
|
+
}
|
|
318
326
|
async hearSoFar(audio, language = '') {
|
|
319
327
|
const { speech, silentMs } = await this.finder.findSpeech(audio);
|
|
320
328
|
const spoke = speech.length >= SHORTEST;
|
|
@@ -450,6 +458,18 @@ export class AutoHearing {
|
|
|
450
458
|
hearSoFar(audio, language) {
|
|
451
459
|
return this.current.hearSoFar(audio, language);
|
|
452
460
|
}
|
|
461
|
+
async spokeSoFar(audio, shortestSeconds) {
|
|
462
|
+
const current = this.current;
|
|
463
|
+
return current.spokeSoFar
|
|
464
|
+
? current.spokeSoFar(audio, shortestSeconds)
|
|
465
|
+
: (await current.hearSoFar(audio, '')).spoke;
|
|
466
|
+
}
|
|
467
|
+
quietSoFar(audio) {
|
|
468
|
+
const current = this.current;
|
|
469
|
+
return current.quietSoFar
|
|
470
|
+
? current.quietSoFar(audio)
|
|
471
|
+
: current.hearSoFar(audio, '');
|
|
472
|
+
}
|
|
453
473
|
async hear(audio, language) {
|
|
454
474
|
if (this.place === 'device') {
|
|
455
475
|
return this.device.hear(audio, language);
|
package/lib/voice/speaker.d.ts
CHANGED
|
@@ -50,6 +50,13 @@ export interface SpeakerOptions {
|
|
|
50
50
|
onState?: (state: SpeakerState) => void;
|
|
51
51
|
/** Called with how long a sentence took from asked to first sound, in ms. */
|
|
52
52
|
onFirstSound?: (ms: number) => void;
|
|
53
|
+
/**
|
|
54
|
+
* Say a long first sentence's first clause on its own (VO-52): the service
|
|
55
|
+
* makes a sentence whole, in about the time its length takes, so the first
|
|
56
|
+
* sound comes sooner when the first thing asked is shorter. Off for a
|
|
57
|
+
* caller that follows its sentences one by one (a deck's narrator).
|
|
58
|
+
*/
|
|
59
|
+
firstClauseFirst?: boolean;
|
|
53
60
|
fetcher?: typeof fetch;
|
|
54
61
|
}
|
|
55
62
|
/**
|
|
@@ -60,6 +67,14 @@ export interface SpeakerOptions {
|
|
|
60
67
|
* 14 October 2026).
|
|
61
68
|
*/
|
|
62
69
|
export declare const SENTENCES_AHEAD = 2;
|
|
70
|
+
/** A first sentence longer than this is said clause first (`firstClauseFirst`). */
|
|
71
|
+
export declare const FIRST_CLAUSE_SPLIT_CHARS = 60;
|
|
72
|
+
/**
|
|
73
|
+
* A sentence cut after its first clause — a comma, semicolon, colon or dash
|
|
74
|
+
* followed by a space — when it is long and neither part is too short (VO-52);
|
|
75
|
+
* else the sentence alone. Never cut inside a number (`1,000`).
|
|
76
|
+
*/
|
|
77
|
+
export declare function firstClause(sentence: string): string[];
|
|
63
78
|
/** Plays an answer's sentences, said by the speech service. */
|
|
64
79
|
export declare class ServerSpeaker {
|
|
65
80
|
private options;
|
package/lib/voice/speaker.js
CHANGED
|
@@ -21,6 +21,34 @@ export const SYNTHESES_PATH = '/api/ai-agents/v1/speech/syntheses';
|
|
|
21
21
|
* 14 October 2026).
|
|
22
22
|
*/
|
|
23
23
|
export const SENTENCES_AHEAD = 2;
|
|
24
|
+
/** A first sentence longer than this is said clause first (`firstClauseFirst`). */
|
|
25
|
+
export const FIRST_CLAUSE_SPLIT_CHARS = 60;
|
|
26
|
+
/** Neither part of a sentence cut at its first clause is shorter than this. */
|
|
27
|
+
const SHORTEST_CLAUSE_CHARS = 15;
|
|
28
|
+
/**
|
|
29
|
+
* A sentence cut after its first clause — a comma, semicolon, colon or dash
|
|
30
|
+
* followed by a space — when it is long and neither part is too short (VO-52);
|
|
31
|
+
* else the sentence alone. Never cut inside a number (`1,000`).
|
|
32
|
+
*/
|
|
33
|
+
export function firstClause(sentence) {
|
|
34
|
+
if (sentence.length <= FIRST_CLAUSE_SPLIT_CHARS) {
|
|
35
|
+
return [sentence];
|
|
36
|
+
}
|
|
37
|
+
const boundary = /[,;:\u2013\u2014]\s+/g;
|
|
38
|
+
let found;
|
|
39
|
+
while ((found = boundary.exec(sentence)) !== null) {
|
|
40
|
+
const end = found.index + 1;
|
|
41
|
+
if (end < SHORTEST_CLAUSE_CHARS) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const rest = sentence.slice(found.index + found[0].length);
|
|
45
|
+
if (rest.length < SHORTEST_CLAUSE_CHARS) {
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
return [sentence.slice(0, end), rest];
|
|
49
|
+
}
|
|
50
|
+
return [sentence];
|
|
51
|
+
}
|
|
24
52
|
/** Plays an answer's sentences, said by the speech service. */
|
|
25
53
|
export class ServerSpeaker {
|
|
26
54
|
options;
|
|
@@ -99,7 +127,14 @@ export class ServerSpeaker {
|
|
|
99
127
|
if (!sentence) {
|
|
100
128
|
return;
|
|
101
129
|
}
|
|
102
|
-
|
|
130
|
+
// Nothing playing or waiting: this is the first thing heard (VO-52).
|
|
131
|
+
const first = this.options.firstClauseFirst &&
|
|
132
|
+
!this.queue.length &&
|
|
133
|
+
!this.playing &&
|
|
134
|
+
!this.running;
|
|
135
|
+
for (const part of first ? firstClause(sentence) : [sentence]) {
|
|
136
|
+
this.queue.push({ text: part, asked: performance.now() });
|
|
137
|
+
}
|
|
103
138
|
if (!this.state.speaking) {
|
|
104
139
|
// Something new to say: the last refusal is no longer the news.
|
|
105
140
|
this.set({ speaking: true, refused: undefined });
|
package/lib/voice/types.d.ts
CHANGED
|
@@ -80,6 +80,18 @@ export interface SpokenMetadata {
|
|
|
80
80
|
language: string;
|
|
81
81
|
engine: string;
|
|
82
82
|
where: 'device' | 'server';
|
|
83
|
+
/**
|
|
84
|
+
* The turn's sound, sent to be kept with its words when the application's
|
|
85
|
+
* record keeps `audio` and the person agreed (VO-63); never on the
|
|
86
|
+
* conversation's own message.
|
|
87
|
+
*/
|
|
88
|
+
sound?: SpokenSound;
|
|
89
|
+
}
|
|
90
|
+
/** A turn's recording, to keep (VO-63): a WAV, base64. */
|
|
91
|
+
export interface SpokenSound {
|
|
92
|
+
data: string;
|
|
93
|
+
type: 'audio/wav';
|
|
94
|
+
seconds: number;
|
|
83
95
|
}
|
|
84
96
|
/** What the person said, once heard. */
|
|
85
97
|
export interface Transcript {
|
|
@@ -30,6 +30,8 @@ export function useVoiceSpeaker(voice, on = true) {
|
|
|
30
30
|
rate: voice.rate,
|
|
31
31
|
volume: voice.volume,
|
|
32
32
|
onState: setState,
|
|
33
|
+
// The chat's answers: a long first sentence's first clause first.
|
|
34
|
+
firstClauseFirst: true,
|
|
33
35
|
})
|
|
34
36
|
: undefined,
|
|
35
37
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|