@adminforth/agent 1.43.1 → 1.43.3
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/build.log +2 -2
- package/custom/composables/useAgentAudio.ts +129 -20
- package/custom/composables/useAgentStore.ts +5 -3
- package/custom/speech_recognition_frontend/MicrophoneButon.vue +1 -0
- package/dist/custom/composables/useAgentAudio.ts +129 -20
- package/dist/custom/composables/useAgentStore.ts +5 -3
- package/dist/custom/speech_recognition_frontend/MicrophoneButon.vue +1 -0
- package/package.json +1 -1
package/build.log
CHANGED
|
@@ -58,5 +58,5 @@ custom/speech_recognition_frontend/voiceActivityDetection.ts
|
|
|
58
58
|
custom/speech_recognition_frontend/types/
|
|
59
59
|
custom/speech_recognition_frontend/types/voice-activity-detection.d.ts
|
|
60
60
|
|
|
61
|
-
sent 1,
|
|
62
|
-
total size is 1,
|
|
61
|
+
sent 1,664,048 bytes received 860 bytes 3,329,816.00 bytes/sec
|
|
62
|
+
total size is 1,660,186 speedup is 1.00
|
|
@@ -14,26 +14,118 @@ type StreamingAudioState = {
|
|
|
14
14
|
isDone: boolean;
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
+
let audioUnlockSourceUrl: string | null = null;
|
|
18
|
+
let audioUnlockInFlight: Promise<void> | null = null;
|
|
19
|
+
let isAudioPlaybackUnlocked = false;
|
|
17
20
|
let standByAudio: HTMLAudioElement | null = null;
|
|
18
21
|
let isStandByAudioPlaying = false;
|
|
22
|
+
|
|
23
|
+
function writeAsciiString(view: DataView, offset: number, value: string) {
|
|
24
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
25
|
+
view.setUint8(offset + index, value.charCodeAt(index));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function createSilentWavBlob(durationMs = 50) {
|
|
30
|
+
const sampleRate = 8000;
|
|
31
|
+
const bitsPerSample = 16;
|
|
32
|
+
const channelCount = 1;
|
|
33
|
+
const bytesPerSample = bitsPerSample / 8;
|
|
34
|
+
const sampleCount = Math.max(1, Math.round((sampleRate * durationMs) / 1000));
|
|
35
|
+
const pcmDataSize = sampleCount * channelCount * bytesPerSample;
|
|
36
|
+
const buffer = new ArrayBuffer(44 + pcmDataSize);
|
|
37
|
+
const view = new DataView(buffer);
|
|
38
|
+
|
|
39
|
+
writeAsciiString(view, 0, 'RIFF');
|
|
40
|
+
view.setUint32(4, 36 + pcmDataSize, true);
|
|
41
|
+
writeAsciiString(view, 8, 'WAVE');
|
|
42
|
+
writeAsciiString(view, 12, 'fmt ');
|
|
43
|
+
view.setUint32(16, 16, true);
|
|
44
|
+
view.setUint16(20, 1, true);
|
|
45
|
+
view.setUint16(22, channelCount, true);
|
|
46
|
+
view.setUint32(24, sampleRate, true);
|
|
47
|
+
view.setUint32(28, sampleRate * channelCount * bytesPerSample, true);
|
|
48
|
+
view.setUint16(32, channelCount * bytesPerSample, true);
|
|
49
|
+
view.setUint16(34, bitsPerSample, true);
|
|
50
|
+
writeAsciiString(view, 36, 'data');
|
|
51
|
+
view.setUint32(40, pcmDataSize, true);
|
|
52
|
+
|
|
53
|
+
return new Blob([buffer], { type: 'audio/wav' });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function getAudioUnlockSourceUrl() {
|
|
57
|
+
if (!audioUnlockSourceUrl) {
|
|
58
|
+
audioUnlockSourceUrl = URL.createObjectURL(createSilentWavBlob());
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return audioUnlockSourceUrl;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function unlockAudioPlayback() {
|
|
65
|
+
if (isAudioPlaybackUnlocked) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (audioUnlockInFlight) {
|
|
70
|
+
await audioUnlockInFlight;
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
audioUnlockInFlight = (async () => {
|
|
75
|
+
const unlockAudio = new Audio(getAudioUnlockSourceUrl());
|
|
76
|
+
unlockAudio.muted = true;
|
|
77
|
+
unlockAudio.preload = 'auto';
|
|
78
|
+
unlockAudio.setAttribute('playsinline', '');
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
await unlockAudio.play();
|
|
82
|
+
unlockAudio.pause();
|
|
83
|
+
unlockAudio.currentTime = 0;
|
|
84
|
+
isAudioPlaybackUnlocked = true;
|
|
85
|
+
} catch (error) {
|
|
86
|
+
console.error('Failed to unlock audio playback:', error);
|
|
87
|
+
} finally {
|
|
88
|
+
unlockAudio.removeAttribute('src');
|
|
89
|
+
unlockAudio.load();
|
|
90
|
+
audioUnlockInFlight = null;
|
|
91
|
+
}
|
|
92
|
+
})();
|
|
93
|
+
|
|
94
|
+
await audioUnlockInFlight;
|
|
95
|
+
}
|
|
96
|
+
|
|
19
97
|
async function playStandByAudio() {
|
|
20
98
|
if (!standByAudio) {
|
|
21
99
|
standByAudio = new Audio(`/plugins/AdminForthAgentPlugin/agentAudio/agent-processing.mp3`);
|
|
100
|
+
standByAudio.preload = 'auto';
|
|
101
|
+
standByAudio.setAttribute('playsinline', '');
|
|
22
102
|
standByAudio.addEventListener('ended', () => {
|
|
23
|
-
if (
|
|
103
|
+
if (standByAudio?.paused === false) {
|
|
24
104
|
restartStandByAudio();
|
|
25
105
|
}
|
|
26
106
|
});
|
|
27
107
|
}
|
|
108
|
+
|
|
109
|
+
if (!standByAudio) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
28
113
|
standByAudio.currentTime = 0;
|
|
29
|
-
|
|
30
|
-
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
await standByAudio.play();
|
|
117
|
+
isStandByAudioPlaying = true;
|
|
118
|
+
} catch (error) {
|
|
119
|
+
isStandByAudioPlaying = false;
|
|
120
|
+
console.error('Failed to play standby audio:', error);
|
|
121
|
+
}
|
|
31
122
|
}
|
|
32
123
|
|
|
33
124
|
function stopStandByAudio() {
|
|
34
125
|
if (!standByAudio) {
|
|
35
126
|
return;
|
|
36
127
|
}
|
|
128
|
+
|
|
37
129
|
standByAudio.pause();
|
|
38
130
|
standByAudio.currentTime = 0;
|
|
39
131
|
isStandByAudioPlaying = false;
|
|
@@ -43,15 +135,15 @@ function restartStandByAudio() {
|
|
|
43
135
|
if (standByAudio) {
|
|
44
136
|
standByAudio.currentTime = 0;
|
|
45
137
|
}
|
|
46
|
-
playStandByAudio();
|
|
47
|
-
}
|
|
48
138
|
|
|
139
|
+
void playStandByAudio();
|
|
140
|
+
}
|
|
49
141
|
|
|
50
142
|
export const useAgentAudio = defineStore('agentAudio', () => {
|
|
51
143
|
const agentStore = useAgentStore();
|
|
52
|
-
const agentAudioMode = ref<'transcribing' | 'streaming' | 'fetchingAudio' | 'playingAgentResponse' | 'readyToRespond'
|
|
144
|
+
const agentAudioMode = ref<'transcribing' | 'streaming' | 'fetchingAudio' | 'playingAgentResponse' | 'readyToRespond'>('readyToRespond');
|
|
53
145
|
const isStreamingResponse = ref(false);
|
|
54
|
-
|
|
146
|
+
|
|
55
147
|
let currentAbortController: AbortController | null = null;
|
|
56
148
|
let isPlaying = false;
|
|
57
149
|
let currentAudio: HTMLAudioElement | null = null;
|
|
@@ -81,6 +173,7 @@ export const useAgentAudio = defineStore('agentAudio', () => {
|
|
|
81
173
|
formData.append('timeZone', Intl.DateTimeFormat().resolvedOptions().timeZone);
|
|
82
174
|
formData.append('currentPage', JSON.stringify(getCurrentPageContext()));
|
|
83
175
|
const fullPath = `${import.meta.env.VITE_ADMINFORTH_PUBLIC_PATH || ''}/adminapi/v1/agent/speech-response`;
|
|
176
|
+
|
|
84
177
|
try {
|
|
85
178
|
agentAudioMode.value = 'transcribing';
|
|
86
179
|
const res = await fetch(fullPath, {
|
|
@@ -91,22 +184,23 @@ export const useAgentAudio = defineStore('agentAudio', () => {
|
|
|
91
184
|
},
|
|
92
185
|
signal: currentAbortController!.signal,
|
|
93
186
|
});
|
|
187
|
+
|
|
94
188
|
isStreamingResponse.value = true;
|
|
189
|
+
|
|
95
190
|
if (res.ok) {
|
|
96
191
|
agentAudioMode.value = 'streaming';
|
|
97
192
|
await readSpeechResponseStream(res);
|
|
98
193
|
} else {
|
|
99
194
|
console.error('Failed to transcribe audio:', res.statusText);
|
|
100
|
-
adminforth.alert({message: 'Failed to transcribe audio', variant: 'danger'});
|
|
195
|
+
adminforth.alert({ message: 'Failed to transcribe audio', variant: 'danger' });
|
|
101
196
|
}
|
|
102
197
|
} catch (error) {
|
|
103
|
-
if (error instanceof DOMException && error.name === 'AbortError') {
|
|
104
|
-
//
|
|
105
|
-
} else {
|
|
198
|
+
if (!(error instanceof DOMException && error.name === 'AbortError')) {
|
|
106
199
|
console.error('Error sending audio to server:', error);
|
|
107
200
|
}
|
|
108
201
|
} finally {
|
|
109
202
|
isStreamingResponse.value = false;
|
|
203
|
+
|
|
110
204
|
if (!wasAudioResponseReceived) {
|
|
111
205
|
setAudioModeReadyToRespond();
|
|
112
206
|
}
|
|
@@ -115,8 +209,9 @@ export const useAgentAudio = defineStore('agentAudio', () => {
|
|
|
115
209
|
|
|
116
210
|
async function readSpeechResponseStream(res: Response) {
|
|
117
211
|
const reader = res.body?.getReader();
|
|
212
|
+
|
|
118
213
|
if (!reader) {
|
|
119
|
-
adminforth.alert({message: 'Speech response stream is not available', variant: 'danger'});
|
|
214
|
+
adminforth.alert({ message: 'Speech response stream is not available', variant: 'danger' });
|
|
120
215
|
return;
|
|
121
216
|
}
|
|
122
217
|
|
|
@@ -124,6 +219,7 @@ export const useAgentAudio = defineStore('agentAudio', () => {
|
|
|
124
219
|
let buffer = '';
|
|
125
220
|
|
|
126
221
|
agentStore.setCurrentChatStatus('streaming');
|
|
222
|
+
|
|
127
223
|
try {
|
|
128
224
|
while (true) {
|
|
129
225
|
const { value, done } = await reader.read();
|
|
@@ -158,6 +254,7 @@ export const useAgentAudio = defineStore('agentAudio', () => {
|
|
|
158
254
|
if (currentAbortController?.signal.aborted) {
|
|
159
255
|
return;
|
|
160
256
|
}
|
|
257
|
+
|
|
161
258
|
const data = eventBlock
|
|
162
259
|
.split('\n')
|
|
163
260
|
.filter((line) => line.startsWith('data:'))
|
|
@@ -171,7 +268,6 @@ export const useAgentAudio = defineStore('agentAudio', () => {
|
|
|
171
268
|
const event = JSON.parse(data) as SpeechStreamEvent;
|
|
172
269
|
|
|
173
270
|
if (event.type === 'error') {
|
|
174
|
-
|
|
175
271
|
return;
|
|
176
272
|
}
|
|
177
273
|
|
|
@@ -209,8 +305,9 @@ export const useAgentAudio = defineStore('agentAudio', () => {
|
|
|
209
305
|
|
|
210
306
|
if (event.type === 'data-tool-call') {
|
|
211
307
|
if (!isStandByAudioPlaying) {
|
|
212
|
-
playStandByAudio();
|
|
308
|
+
void playStandByAudio();
|
|
213
309
|
}
|
|
310
|
+
|
|
214
311
|
agentStore.addDataToolCallMessage(event.data);
|
|
215
312
|
}
|
|
216
313
|
}
|
|
@@ -227,10 +324,16 @@ export const useAgentAudio = defineStore('agentAudio', () => {
|
|
|
227
324
|
currentAudio.currentTime = 0;
|
|
228
325
|
return;
|
|
229
326
|
}
|
|
327
|
+
|
|
230
328
|
agentAudioMode.value = 'playingAgentResponse';
|
|
231
|
-
|
|
329
|
+
|
|
330
|
+
try {
|
|
331
|
+
await currentAudio.play();
|
|
332
|
+
} catch (error) {
|
|
333
|
+
isPlaying = false;
|
|
334
|
+
setAudioModeReadyToRespond();
|
|
232
335
|
console.error('Failed to play audio:', error);
|
|
233
|
-
}
|
|
336
|
+
}
|
|
234
337
|
}
|
|
235
338
|
|
|
236
339
|
function initializeAudioStream(mimeType: string) {
|
|
@@ -244,6 +347,8 @@ export const useAgentAudio = defineStore('agentAudio', () => {
|
|
|
244
347
|
const mediaSource = new MediaSource();
|
|
245
348
|
currentAudioObjectUrl = URL.createObjectURL(mediaSource);
|
|
246
349
|
currentAudio = new Audio(currentAudioObjectUrl);
|
|
350
|
+
currentAudio.preload = 'auto';
|
|
351
|
+
currentAudio.setAttribute('playsinline', '');
|
|
247
352
|
currentAudio.addEventListener('ended', handleAudioEnded, { once: true });
|
|
248
353
|
currentStreamingAudio = {
|
|
249
354
|
mimeType,
|
|
@@ -299,7 +404,7 @@ export const useAgentAudio = defineStore('agentAudio', () => {
|
|
|
299
404
|
|
|
300
405
|
if (!currentStreamingAudio.hasStartedPlayback) {
|
|
301
406
|
currentStreamingAudio.hasStartedPlayback = true;
|
|
302
|
-
setIsPlaying(true);
|
|
407
|
+
void setIsPlaying(true);
|
|
303
408
|
}
|
|
304
409
|
|
|
305
410
|
return;
|
|
@@ -356,6 +461,7 @@ export const useAgentAudio = defineStore('agentAudio', () => {
|
|
|
356
461
|
bufferedAudioMimeType = 'audio/mpeg';
|
|
357
462
|
detachStreamingAudio();
|
|
358
463
|
destroyCurrentAudioElement();
|
|
464
|
+
|
|
359
465
|
if (!dontResetMode) {
|
|
360
466
|
setAudioModeReadyToRespond();
|
|
361
467
|
}
|
|
@@ -369,8 +475,10 @@ export const useAgentAudio = defineStore('agentAudio', () => {
|
|
|
369
475
|
function playAudioChunks(chunks: ArrayBuffer[], mimeType: string) {
|
|
370
476
|
currentAudioObjectUrl = URL.createObjectURL(new Blob(chunks, { type: mimeType }));
|
|
371
477
|
currentAudio = new Audio(currentAudioObjectUrl);
|
|
478
|
+
currentAudio.preload = 'auto';
|
|
479
|
+
currentAudio.setAttribute('playsinline', '');
|
|
372
480
|
currentAudio.addEventListener('ended', handleAudioEnded, { once: true });
|
|
373
|
-
setIsPlaying(true);
|
|
481
|
+
void setIsPlaying(true);
|
|
374
482
|
}
|
|
375
483
|
|
|
376
484
|
function base64ToArrayBuffer(base64: string) {
|
|
@@ -407,8 +515,9 @@ export const useAgentAudio = defineStore('agentAudio', () => {
|
|
|
407
515
|
sendAudioToServerAndHandleResponse,
|
|
408
516
|
stopGenerationAndAudio,
|
|
409
517
|
stopCurrentAudioPlayback,
|
|
518
|
+
unlockAudioPlayback,
|
|
410
519
|
playBeep,
|
|
411
|
-
agentAudioMode
|
|
520
|
+
agentAudioMode,
|
|
521
|
+
playStandByAudio,
|
|
412
522
|
};
|
|
413
|
-
|
|
414
523
|
});
|
|
@@ -43,8 +43,10 @@ export const useAgentStore = defineStore('agent', () => {
|
|
|
43
43
|
const chatWidth = ref(DEFAULT_CHAT_WIDTH);
|
|
44
44
|
const availableModes = ref<AgentMode[]>([]);
|
|
45
45
|
const activeModeName = ref<string | null>(null);
|
|
46
|
-
const { width:
|
|
47
|
-
|
|
46
|
+
const { width: viewportWidth } = useWindowSize({
|
|
47
|
+
type: 'visual',
|
|
48
|
+
includeScrollbar: false,
|
|
49
|
+
});
|
|
48
50
|
const {
|
|
49
51
|
currentChat,
|
|
50
52
|
setCurrentChat,
|
|
@@ -131,7 +133,7 @@ export const useAgentStore = defineStore('agent', () => {
|
|
|
131
133
|
setCurrentChat,
|
|
132
134
|
});
|
|
133
135
|
|
|
134
|
-
watch(() =>
|
|
136
|
+
watch(() => viewportWidth.value, (newWidth) => {
|
|
135
137
|
if (isFullScreen.value) {
|
|
136
138
|
setChatWidth(newWidth, false);
|
|
137
139
|
}
|
|
@@ -111,6 +111,7 @@ function toggleChatMode() {
|
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
async function onStartRecording() {
|
|
114
|
+
await agentAudio.unlockAudioPlayback();
|
|
114
115
|
await requestMicAndStartVAD(saidSomething, stopRecording, onAnySound);
|
|
115
116
|
microphoneButtonMode.value = 'listen';
|
|
116
117
|
agentAudio.playBeep(1000);
|
|
@@ -14,26 +14,118 @@ type StreamingAudioState = {
|
|
|
14
14
|
isDone: boolean;
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
+
let audioUnlockSourceUrl: string | null = null;
|
|
18
|
+
let audioUnlockInFlight: Promise<void> | null = null;
|
|
19
|
+
let isAudioPlaybackUnlocked = false;
|
|
17
20
|
let standByAudio: HTMLAudioElement | null = null;
|
|
18
21
|
let isStandByAudioPlaying = false;
|
|
22
|
+
|
|
23
|
+
function writeAsciiString(view: DataView, offset: number, value: string) {
|
|
24
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
25
|
+
view.setUint8(offset + index, value.charCodeAt(index));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function createSilentWavBlob(durationMs = 50) {
|
|
30
|
+
const sampleRate = 8000;
|
|
31
|
+
const bitsPerSample = 16;
|
|
32
|
+
const channelCount = 1;
|
|
33
|
+
const bytesPerSample = bitsPerSample / 8;
|
|
34
|
+
const sampleCount = Math.max(1, Math.round((sampleRate * durationMs) / 1000));
|
|
35
|
+
const pcmDataSize = sampleCount * channelCount * bytesPerSample;
|
|
36
|
+
const buffer = new ArrayBuffer(44 + pcmDataSize);
|
|
37
|
+
const view = new DataView(buffer);
|
|
38
|
+
|
|
39
|
+
writeAsciiString(view, 0, 'RIFF');
|
|
40
|
+
view.setUint32(4, 36 + pcmDataSize, true);
|
|
41
|
+
writeAsciiString(view, 8, 'WAVE');
|
|
42
|
+
writeAsciiString(view, 12, 'fmt ');
|
|
43
|
+
view.setUint32(16, 16, true);
|
|
44
|
+
view.setUint16(20, 1, true);
|
|
45
|
+
view.setUint16(22, channelCount, true);
|
|
46
|
+
view.setUint32(24, sampleRate, true);
|
|
47
|
+
view.setUint32(28, sampleRate * channelCount * bytesPerSample, true);
|
|
48
|
+
view.setUint16(32, channelCount * bytesPerSample, true);
|
|
49
|
+
view.setUint16(34, bitsPerSample, true);
|
|
50
|
+
writeAsciiString(view, 36, 'data');
|
|
51
|
+
view.setUint32(40, pcmDataSize, true);
|
|
52
|
+
|
|
53
|
+
return new Blob([buffer], { type: 'audio/wav' });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function getAudioUnlockSourceUrl() {
|
|
57
|
+
if (!audioUnlockSourceUrl) {
|
|
58
|
+
audioUnlockSourceUrl = URL.createObjectURL(createSilentWavBlob());
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return audioUnlockSourceUrl;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function unlockAudioPlayback() {
|
|
65
|
+
if (isAudioPlaybackUnlocked) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (audioUnlockInFlight) {
|
|
70
|
+
await audioUnlockInFlight;
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
audioUnlockInFlight = (async () => {
|
|
75
|
+
const unlockAudio = new Audio(getAudioUnlockSourceUrl());
|
|
76
|
+
unlockAudio.muted = true;
|
|
77
|
+
unlockAudio.preload = 'auto';
|
|
78
|
+
unlockAudio.setAttribute('playsinline', '');
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
await unlockAudio.play();
|
|
82
|
+
unlockAudio.pause();
|
|
83
|
+
unlockAudio.currentTime = 0;
|
|
84
|
+
isAudioPlaybackUnlocked = true;
|
|
85
|
+
} catch (error) {
|
|
86
|
+
console.error('Failed to unlock audio playback:', error);
|
|
87
|
+
} finally {
|
|
88
|
+
unlockAudio.removeAttribute('src');
|
|
89
|
+
unlockAudio.load();
|
|
90
|
+
audioUnlockInFlight = null;
|
|
91
|
+
}
|
|
92
|
+
})();
|
|
93
|
+
|
|
94
|
+
await audioUnlockInFlight;
|
|
95
|
+
}
|
|
96
|
+
|
|
19
97
|
async function playStandByAudio() {
|
|
20
98
|
if (!standByAudio) {
|
|
21
99
|
standByAudio = new Audio(`/plugins/AdminForthAgentPlugin/agentAudio/agent-processing.mp3`);
|
|
100
|
+
standByAudio.preload = 'auto';
|
|
101
|
+
standByAudio.setAttribute('playsinline', '');
|
|
22
102
|
standByAudio.addEventListener('ended', () => {
|
|
23
|
-
if (
|
|
103
|
+
if (standByAudio?.paused === false) {
|
|
24
104
|
restartStandByAudio();
|
|
25
105
|
}
|
|
26
106
|
});
|
|
27
107
|
}
|
|
108
|
+
|
|
109
|
+
if (!standByAudio) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
28
113
|
standByAudio.currentTime = 0;
|
|
29
|
-
|
|
30
|
-
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
await standByAudio.play();
|
|
117
|
+
isStandByAudioPlaying = true;
|
|
118
|
+
} catch (error) {
|
|
119
|
+
isStandByAudioPlaying = false;
|
|
120
|
+
console.error('Failed to play standby audio:', error);
|
|
121
|
+
}
|
|
31
122
|
}
|
|
32
123
|
|
|
33
124
|
function stopStandByAudio() {
|
|
34
125
|
if (!standByAudio) {
|
|
35
126
|
return;
|
|
36
127
|
}
|
|
128
|
+
|
|
37
129
|
standByAudio.pause();
|
|
38
130
|
standByAudio.currentTime = 0;
|
|
39
131
|
isStandByAudioPlaying = false;
|
|
@@ -43,15 +135,15 @@ function restartStandByAudio() {
|
|
|
43
135
|
if (standByAudio) {
|
|
44
136
|
standByAudio.currentTime = 0;
|
|
45
137
|
}
|
|
46
|
-
playStandByAudio();
|
|
47
|
-
}
|
|
48
138
|
|
|
139
|
+
void playStandByAudio();
|
|
140
|
+
}
|
|
49
141
|
|
|
50
142
|
export const useAgentAudio = defineStore('agentAudio', () => {
|
|
51
143
|
const agentStore = useAgentStore();
|
|
52
|
-
const agentAudioMode = ref<'transcribing' | 'streaming' | 'fetchingAudio' | 'playingAgentResponse' | 'readyToRespond'
|
|
144
|
+
const agentAudioMode = ref<'transcribing' | 'streaming' | 'fetchingAudio' | 'playingAgentResponse' | 'readyToRespond'>('readyToRespond');
|
|
53
145
|
const isStreamingResponse = ref(false);
|
|
54
|
-
|
|
146
|
+
|
|
55
147
|
let currentAbortController: AbortController | null = null;
|
|
56
148
|
let isPlaying = false;
|
|
57
149
|
let currentAudio: HTMLAudioElement | null = null;
|
|
@@ -81,6 +173,7 @@ export const useAgentAudio = defineStore('agentAudio', () => {
|
|
|
81
173
|
formData.append('timeZone', Intl.DateTimeFormat().resolvedOptions().timeZone);
|
|
82
174
|
formData.append('currentPage', JSON.stringify(getCurrentPageContext()));
|
|
83
175
|
const fullPath = `${import.meta.env.VITE_ADMINFORTH_PUBLIC_PATH || ''}/adminapi/v1/agent/speech-response`;
|
|
176
|
+
|
|
84
177
|
try {
|
|
85
178
|
agentAudioMode.value = 'transcribing';
|
|
86
179
|
const res = await fetch(fullPath, {
|
|
@@ -91,22 +184,23 @@ export const useAgentAudio = defineStore('agentAudio', () => {
|
|
|
91
184
|
},
|
|
92
185
|
signal: currentAbortController!.signal,
|
|
93
186
|
});
|
|
187
|
+
|
|
94
188
|
isStreamingResponse.value = true;
|
|
189
|
+
|
|
95
190
|
if (res.ok) {
|
|
96
191
|
agentAudioMode.value = 'streaming';
|
|
97
192
|
await readSpeechResponseStream(res);
|
|
98
193
|
} else {
|
|
99
194
|
console.error('Failed to transcribe audio:', res.statusText);
|
|
100
|
-
adminforth.alert({message: 'Failed to transcribe audio', variant: 'danger'});
|
|
195
|
+
adminforth.alert({ message: 'Failed to transcribe audio', variant: 'danger' });
|
|
101
196
|
}
|
|
102
197
|
} catch (error) {
|
|
103
|
-
if (error instanceof DOMException && error.name === 'AbortError') {
|
|
104
|
-
//
|
|
105
|
-
} else {
|
|
198
|
+
if (!(error instanceof DOMException && error.name === 'AbortError')) {
|
|
106
199
|
console.error('Error sending audio to server:', error);
|
|
107
200
|
}
|
|
108
201
|
} finally {
|
|
109
202
|
isStreamingResponse.value = false;
|
|
203
|
+
|
|
110
204
|
if (!wasAudioResponseReceived) {
|
|
111
205
|
setAudioModeReadyToRespond();
|
|
112
206
|
}
|
|
@@ -115,8 +209,9 @@ export const useAgentAudio = defineStore('agentAudio', () => {
|
|
|
115
209
|
|
|
116
210
|
async function readSpeechResponseStream(res: Response) {
|
|
117
211
|
const reader = res.body?.getReader();
|
|
212
|
+
|
|
118
213
|
if (!reader) {
|
|
119
|
-
adminforth.alert({message: 'Speech response stream is not available', variant: 'danger'});
|
|
214
|
+
adminforth.alert({ message: 'Speech response stream is not available', variant: 'danger' });
|
|
120
215
|
return;
|
|
121
216
|
}
|
|
122
217
|
|
|
@@ -124,6 +219,7 @@ export const useAgentAudio = defineStore('agentAudio', () => {
|
|
|
124
219
|
let buffer = '';
|
|
125
220
|
|
|
126
221
|
agentStore.setCurrentChatStatus('streaming');
|
|
222
|
+
|
|
127
223
|
try {
|
|
128
224
|
while (true) {
|
|
129
225
|
const { value, done } = await reader.read();
|
|
@@ -158,6 +254,7 @@ export const useAgentAudio = defineStore('agentAudio', () => {
|
|
|
158
254
|
if (currentAbortController?.signal.aborted) {
|
|
159
255
|
return;
|
|
160
256
|
}
|
|
257
|
+
|
|
161
258
|
const data = eventBlock
|
|
162
259
|
.split('\n')
|
|
163
260
|
.filter((line) => line.startsWith('data:'))
|
|
@@ -171,7 +268,6 @@ export const useAgentAudio = defineStore('agentAudio', () => {
|
|
|
171
268
|
const event = JSON.parse(data) as SpeechStreamEvent;
|
|
172
269
|
|
|
173
270
|
if (event.type === 'error') {
|
|
174
|
-
|
|
175
271
|
return;
|
|
176
272
|
}
|
|
177
273
|
|
|
@@ -209,8 +305,9 @@ export const useAgentAudio = defineStore('agentAudio', () => {
|
|
|
209
305
|
|
|
210
306
|
if (event.type === 'data-tool-call') {
|
|
211
307
|
if (!isStandByAudioPlaying) {
|
|
212
|
-
playStandByAudio();
|
|
308
|
+
void playStandByAudio();
|
|
213
309
|
}
|
|
310
|
+
|
|
214
311
|
agentStore.addDataToolCallMessage(event.data);
|
|
215
312
|
}
|
|
216
313
|
}
|
|
@@ -227,10 +324,16 @@ export const useAgentAudio = defineStore('agentAudio', () => {
|
|
|
227
324
|
currentAudio.currentTime = 0;
|
|
228
325
|
return;
|
|
229
326
|
}
|
|
327
|
+
|
|
230
328
|
agentAudioMode.value = 'playingAgentResponse';
|
|
231
|
-
|
|
329
|
+
|
|
330
|
+
try {
|
|
331
|
+
await currentAudio.play();
|
|
332
|
+
} catch (error) {
|
|
333
|
+
isPlaying = false;
|
|
334
|
+
setAudioModeReadyToRespond();
|
|
232
335
|
console.error('Failed to play audio:', error);
|
|
233
|
-
}
|
|
336
|
+
}
|
|
234
337
|
}
|
|
235
338
|
|
|
236
339
|
function initializeAudioStream(mimeType: string) {
|
|
@@ -244,6 +347,8 @@ export const useAgentAudio = defineStore('agentAudio', () => {
|
|
|
244
347
|
const mediaSource = new MediaSource();
|
|
245
348
|
currentAudioObjectUrl = URL.createObjectURL(mediaSource);
|
|
246
349
|
currentAudio = new Audio(currentAudioObjectUrl);
|
|
350
|
+
currentAudio.preload = 'auto';
|
|
351
|
+
currentAudio.setAttribute('playsinline', '');
|
|
247
352
|
currentAudio.addEventListener('ended', handleAudioEnded, { once: true });
|
|
248
353
|
currentStreamingAudio = {
|
|
249
354
|
mimeType,
|
|
@@ -299,7 +404,7 @@ export const useAgentAudio = defineStore('agentAudio', () => {
|
|
|
299
404
|
|
|
300
405
|
if (!currentStreamingAudio.hasStartedPlayback) {
|
|
301
406
|
currentStreamingAudio.hasStartedPlayback = true;
|
|
302
|
-
setIsPlaying(true);
|
|
407
|
+
void setIsPlaying(true);
|
|
303
408
|
}
|
|
304
409
|
|
|
305
410
|
return;
|
|
@@ -356,6 +461,7 @@ export const useAgentAudio = defineStore('agentAudio', () => {
|
|
|
356
461
|
bufferedAudioMimeType = 'audio/mpeg';
|
|
357
462
|
detachStreamingAudio();
|
|
358
463
|
destroyCurrentAudioElement();
|
|
464
|
+
|
|
359
465
|
if (!dontResetMode) {
|
|
360
466
|
setAudioModeReadyToRespond();
|
|
361
467
|
}
|
|
@@ -369,8 +475,10 @@ export const useAgentAudio = defineStore('agentAudio', () => {
|
|
|
369
475
|
function playAudioChunks(chunks: ArrayBuffer[], mimeType: string) {
|
|
370
476
|
currentAudioObjectUrl = URL.createObjectURL(new Blob(chunks, { type: mimeType }));
|
|
371
477
|
currentAudio = new Audio(currentAudioObjectUrl);
|
|
478
|
+
currentAudio.preload = 'auto';
|
|
479
|
+
currentAudio.setAttribute('playsinline', '');
|
|
372
480
|
currentAudio.addEventListener('ended', handleAudioEnded, { once: true });
|
|
373
|
-
setIsPlaying(true);
|
|
481
|
+
void setIsPlaying(true);
|
|
374
482
|
}
|
|
375
483
|
|
|
376
484
|
function base64ToArrayBuffer(base64: string) {
|
|
@@ -407,8 +515,9 @@ export const useAgentAudio = defineStore('agentAudio', () => {
|
|
|
407
515
|
sendAudioToServerAndHandleResponse,
|
|
408
516
|
stopGenerationAndAudio,
|
|
409
517
|
stopCurrentAudioPlayback,
|
|
518
|
+
unlockAudioPlayback,
|
|
410
519
|
playBeep,
|
|
411
|
-
agentAudioMode
|
|
520
|
+
agentAudioMode,
|
|
521
|
+
playStandByAudio,
|
|
412
522
|
};
|
|
413
|
-
|
|
414
523
|
});
|
|
@@ -43,8 +43,10 @@ export const useAgentStore = defineStore('agent', () => {
|
|
|
43
43
|
const chatWidth = ref(DEFAULT_CHAT_WIDTH);
|
|
44
44
|
const availableModes = ref<AgentMode[]>([]);
|
|
45
45
|
const activeModeName = ref<string | null>(null);
|
|
46
|
-
const { width:
|
|
47
|
-
|
|
46
|
+
const { width: viewportWidth } = useWindowSize({
|
|
47
|
+
type: 'visual',
|
|
48
|
+
includeScrollbar: false,
|
|
49
|
+
});
|
|
48
50
|
const {
|
|
49
51
|
currentChat,
|
|
50
52
|
setCurrentChat,
|
|
@@ -131,7 +133,7 @@ export const useAgentStore = defineStore('agent', () => {
|
|
|
131
133
|
setCurrentChat,
|
|
132
134
|
});
|
|
133
135
|
|
|
134
|
-
watch(() =>
|
|
136
|
+
watch(() => viewportWidth.value, (newWidth) => {
|
|
135
137
|
if (isFullScreen.value) {
|
|
136
138
|
setChatWidth(newWidth, false);
|
|
137
139
|
}
|
|
@@ -111,6 +111,7 @@ function toggleChatMode() {
|
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
async function onStartRecording() {
|
|
114
|
+
await agentAudio.unlockAudioPlayback();
|
|
114
115
|
await requestMicAndStartVAD(saidSomething, stopRecording, onAnySound);
|
|
115
116
|
microphoneButtonMode.value = 'listen';
|
|
116
117
|
agentAudio.playBeep(1000);
|