byt-lingxiao-ai 0.1.5 → 0.1.7
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/README.md +0 -24
- package/components/ChatWindow.vue +330 -16
- package/components/assets/entering.png +0 -0
- package/components/assets/speaking.png +0 -0
- package/components/assets/waiting.png +0 -0
- package/components/index.js +3 -1
- package/dist/img/entering.42f05909.png +0 -0
- package/dist/img/speaking.3ce8b666.png +0 -0
- package/dist/img/waiting.ac21d76e.png +0 -0
- package/dist/index.common.js +1879 -47
- package/dist/index.common.js.map +1 -1
- package/dist/index.css +1 -1
- package/dist/index.umd.js +1879 -47
- package/dist/index.umd.js.map +1 -1
- package/dist/index.umd.min.js +1 -1
- package/dist/index.umd.min.js.map +1 -1
- package/package.json +3 -2
- package/dist/demo.html +0 -1
package/README.md
CHANGED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
# byt-lingxiao-test
|
|
2
|
-
|
|
3
|
-
## Project setup
|
|
4
|
-
```
|
|
5
|
-
yarn install
|
|
6
|
-
```
|
|
7
|
-
|
|
8
|
-
### Compiles and hot-reloads for development
|
|
9
|
-
```
|
|
10
|
-
yarn serve
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
### Compiles and minifies for production
|
|
14
|
-
```
|
|
15
|
-
yarn build
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
### Lints and fixes files
|
|
19
|
-
```
|
|
20
|
-
yarn lint
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
### Customize configuration
|
|
24
|
-
See [Configuration Reference](https://cli.vuejs.org/config/).
|
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="chat">
|
|
3
|
-
|
|
3
|
+
<!-- 隐藏的音频播放器 -->
|
|
4
|
+
<audio
|
|
5
|
+
ref="audioPlayer"
|
|
6
|
+
class="hidden-audio"
|
|
7
|
+
@timeupdate="onTimeUpdate"
|
|
8
|
+
@ended="onAudioEnded"
|
|
9
|
+
>
|
|
10
|
+
<source :src="audioSrc" type="audio/mp3">
|
|
11
|
+
您的浏览器不支持音频元素。
|
|
12
|
+
</audio>
|
|
13
|
+
<div v-if="robotStatus !== 'leaving'" :class="['chat-robot', robotStatus]" ></div>
|
|
14
|
+
<div v-else class="chat-ai" @click="toggleWindow">
|
|
4
15
|
<div class="chat-ai-avater"></div>
|
|
5
16
|
<div class="chat-ai-text">凌霄AI</div>
|
|
6
17
|
</div>
|
|
@@ -63,7 +74,7 @@
|
|
|
63
74
|
@keydown="handleKeyDown"
|
|
64
75
|
></el-input>
|
|
65
76
|
<div class="chat-window-bar">
|
|
66
|
-
<div class="chat-window-send">
|
|
77
|
+
<div class="chat-window-send" @click="handleSend">
|
|
67
78
|
<svg
|
|
68
79
|
xmlns="http://www.w3.org/2000/svg"
|
|
69
80
|
width="20"
|
|
@@ -100,22 +111,21 @@
|
|
|
100
111
|
</div>
|
|
101
112
|
</template>
|
|
102
113
|
<script>
|
|
103
|
-
|
|
104
|
-
|
|
114
|
+
const SAMPLE_RATE = 16000;
|
|
115
|
+
const FRAME_SIZE = 512;
|
|
105
116
|
export default {
|
|
106
117
|
name: 'ChatWindow',
|
|
107
118
|
props: {
|
|
108
119
|
appendToBody: {
|
|
109
120
|
type: Boolean,
|
|
110
121
|
default: true,
|
|
111
|
-
}
|
|
122
|
+
}
|
|
112
123
|
},
|
|
113
124
|
data() {
|
|
114
125
|
return {
|
|
115
|
-
|
|
116
|
-
inputMessage: '',
|
|
117
|
-
visible: false,
|
|
118
|
-
// ... existing code ...
|
|
126
|
+
audioSrc: '/audio/byt.mp3', // 音频URL
|
|
127
|
+
inputMessage: '', // 输入消息
|
|
128
|
+
visible: false, // 窗口是否可见
|
|
119
129
|
messages: [
|
|
120
130
|
{
|
|
121
131
|
id: 1,
|
|
@@ -145,27 +155,297 @@ export default {
|
|
|
145
155
|
time: '',
|
|
146
156
|
content: '你好,欢迎来到凌霄大模型AI对话。',
|
|
147
157
|
},
|
|
148
|
-
],
|
|
158
|
+
], // 消息列表
|
|
159
|
+
isRecording: false, // 正在录制
|
|
160
|
+
isMicAvailable: false, // 麦克风是否可用
|
|
161
|
+
mediaRecorder: null, // 媒体录制器
|
|
162
|
+
ws: null, // WebSocket连接
|
|
163
|
+
// wsUrl: 'wss://mes.shnfonline.top:8316/ai_model/ws/voice-stream', // WebSocket URL
|
|
164
|
+
wsUrl: 'ws://192.168.8.9:9999/ai_model/ws/voice-stream', // WebSocket URL
|
|
165
|
+
isConnected: false, // WebSocket是否已连接
|
|
166
|
+
audioContext: null, // 音频上下文
|
|
167
|
+
microphone: null, // 麦克风输入节点
|
|
168
|
+
processor: null, // 音频处理节点
|
|
169
|
+
robotStatus: 'leaving', // 机器人状态 waiting, speaking, leaving, entering
|
|
170
|
+
audioBuffer: new Float32Array(0) // 音频缓冲区
|
|
149
171
|
}
|
|
150
172
|
},
|
|
151
173
|
mounted() {
|
|
152
|
-
this.
|
|
174
|
+
this.initWebSocket()
|
|
153
175
|
|
|
154
176
|
// 处理append-to-body逻辑
|
|
155
177
|
if (this.appendToBody) {
|
|
156
178
|
this.appendToBodyHandler()
|
|
157
179
|
}
|
|
158
180
|
},
|
|
181
|
+
unmounted() {
|
|
182
|
+
this.closeWebSocket()
|
|
183
|
+
this.stopRecording()
|
|
184
|
+
},
|
|
159
185
|
beforeDestroy() {
|
|
160
186
|
// 组件销毁前,如果元素被移动到body中,需要移除
|
|
161
187
|
if (this.appendToBody && this.$el.parentElement === document.body) {
|
|
162
188
|
document.body.removeChild(this.$el)
|
|
163
189
|
}
|
|
190
|
+
this.closeWebSocket()
|
|
191
|
+
this.stopRecording()
|
|
164
192
|
},
|
|
165
193
|
methods: {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
194
|
+
// 处理音频播放结束事件
|
|
195
|
+
onAudioEnded() {
|
|
196
|
+
this.robotStatus = 'leaving'
|
|
197
|
+
},
|
|
198
|
+
// 处理音频播放时间更新事件
|
|
199
|
+
onTimeUpdate() {
|
|
200
|
+
const audio = this.$refs.audioPlayer
|
|
201
|
+
const currentTime = audio.currentTime
|
|
202
|
+
|
|
203
|
+
if (!this.jumpedTimePoints) {
|
|
204
|
+
this.jumpedTimePoints = new Set()
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const timeJumpPoints = [
|
|
208
|
+
{ time: 30, url: '', title: '生产管理' },
|
|
209
|
+
{ time: 50, url: '', title: '生产信息总览' },
|
|
210
|
+
{ time: 60, url: '', title: '能源管理' },
|
|
211
|
+
{ time: 70, url: '', title: '设备管理' }
|
|
212
|
+
]
|
|
213
|
+
// 检查当前时间是否达到跳转点
|
|
214
|
+
timeJumpPoints.forEach(point => {
|
|
215
|
+
// 使用一定的误差范围,确保不会因为播放进度的微小差异而错过跳转点
|
|
216
|
+
if (currentTime >= point.time && currentTime < point.time + 0.5 && !this.jumpedTimePoints.has(point.time)) {
|
|
217
|
+
this.jumpedTimePoints.add(point.time)
|
|
218
|
+
console.log(`到达时间点 ${point.time} 秒,跳转到 ${point.title}`)
|
|
219
|
+
this.$appOptions.store.dispatch('tags/addTagview', {
|
|
220
|
+
path: point.url,
|
|
221
|
+
name: point.title,
|
|
222
|
+
meta: {
|
|
223
|
+
title: point.title
|
|
224
|
+
}
|
|
225
|
+
})
|
|
226
|
+
this.$appOptions.router.push({
|
|
227
|
+
path: point.url
|
|
228
|
+
})
|
|
229
|
+
}
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
console.log('当前播放时间:', currentTime)
|
|
233
|
+
},
|
|
234
|
+
play() {
|
|
235
|
+
this.robotStatus = 'speaking'
|
|
236
|
+
this.$refs.audioPlayer.play()
|
|
237
|
+
},
|
|
238
|
+
pause() {
|
|
239
|
+
this.robotStatus = 'waiting'
|
|
240
|
+
this.$refs.audioPlayer.pause()
|
|
241
|
+
},
|
|
242
|
+
stop() {
|
|
243
|
+
this.robotStatus = 'leaving'
|
|
244
|
+
this.$refs.audioPlayer.pause()
|
|
245
|
+
this.$refs.audioPlayer.currentTime = 0
|
|
246
|
+
},
|
|
247
|
+
initWebSocket() {
|
|
248
|
+
try {
|
|
249
|
+
this.ws = new WebSocket(this.wsUrl)
|
|
250
|
+
this.ws.binaryType = 'arraybuffer'
|
|
251
|
+
|
|
252
|
+
this.ws.onopen = async () => {
|
|
253
|
+
console.log('连接成功')
|
|
254
|
+
this.isConnected = true
|
|
255
|
+
this.initAudio()
|
|
256
|
+
}
|
|
257
|
+
this.ws.onmessage = (event) => {
|
|
258
|
+
try {
|
|
259
|
+
console.log("收到原始消息:", event.data);
|
|
260
|
+
// 二进制数据直接返回
|
|
261
|
+
if (event.data instanceof ArrayBuffer) {
|
|
262
|
+
console.log("收到二进制音频数据");
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
// 解析JSON数据
|
|
266
|
+
const data = JSON.parse(event.data);
|
|
267
|
+
console.log("解析后的数据:", data);
|
|
268
|
+
|
|
269
|
+
if (data.type === 'config'){
|
|
270
|
+
console.log('配置信息:', data);
|
|
271
|
+
} else if (data.code === 0) {
|
|
272
|
+
if (data.data.type === 'detection') {
|
|
273
|
+
console.log('检测到唤醒词...');
|
|
274
|
+
} else if (data.data.type === 'Collecting') {
|
|
275
|
+
console.log('状态: 采集中...');
|
|
276
|
+
} else if (data.data.type === 'command') {
|
|
277
|
+
// 根据指令改变机器人的状态
|
|
278
|
+
console.log('状态: 处理中...')
|
|
279
|
+
this.analyzeAudioCommand(data.data.category)
|
|
280
|
+
} else {
|
|
281
|
+
console.log('状态: 其他...')
|
|
282
|
+
}
|
|
283
|
+
} else {
|
|
284
|
+
console.error("服务器返回错误:", data.msg);
|
|
285
|
+
}
|
|
286
|
+
} catch (error) {
|
|
287
|
+
console.error("消息解析错误:", error);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
this.ws.onclose = () => {
|
|
291
|
+
console.log('连接关闭')
|
|
292
|
+
this.isConnected = false
|
|
293
|
+
if (this.isRecording) {
|
|
294
|
+
this.stopRecording()
|
|
295
|
+
}
|
|
296
|
+
setTimeout(() => {
|
|
297
|
+
console.log('尝试重新建立连接')
|
|
298
|
+
if (!this.isConnected) {
|
|
299
|
+
// 尝试重连
|
|
300
|
+
this.initWebSocket()
|
|
301
|
+
}
|
|
302
|
+
}, 3000)
|
|
303
|
+
}
|
|
304
|
+
} catch (error) {
|
|
305
|
+
console.error('WebSocket连接失败:', error);
|
|
306
|
+
}
|
|
307
|
+
},
|
|
308
|
+
closeWebSocket() {
|
|
309
|
+
if (this.ws) {
|
|
310
|
+
this.ws.close()
|
|
311
|
+
}
|
|
312
|
+
},
|
|
313
|
+
async initAudio() {
|
|
314
|
+
if (this.isRecording) return;
|
|
315
|
+
try {
|
|
316
|
+
this.isMicAvailable = true;
|
|
317
|
+
// 2. 获取麦克风权限
|
|
318
|
+
const stream = await navigator.mediaDevices.getUserMedia({
|
|
319
|
+
audio: {
|
|
320
|
+
sampleRate: SAMPLE_RATE, // 请求指定采样率
|
|
321
|
+
channelCount: 1, // 单声道
|
|
322
|
+
noiseSuppression: true,
|
|
323
|
+
echoCancellation: true
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
// 3. 创建音频处理环境
|
|
328
|
+
this.audioContext = new AudioContext({ sampleRate: SAMPLE_RATE });
|
|
329
|
+
const actualSampleRate = this.audioContext.sampleRate;
|
|
330
|
+
this.microphone = this.audioContext.createMediaStreamSource(stream);
|
|
331
|
+
|
|
332
|
+
// 4. 创建音频处理器
|
|
333
|
+
this.processor = this.audioContext.createScriptProcessor(FRAME_SIZE, 1, 1);
|
|
334
|
+
this.processor.onaudioprocess = this.processAudio;
|
|
335
|
+
// 连接处理链
|
|
336
|
+
this.microphone.connect(this.processor);
|
|
337
|
+
this.processor.connect(this.audioContext.destination);
|
|
338
|
+
|
|
339
|
+
this.isRecording = true;
|
|
340
|
+
|
|
341
|
+
console.log(`状态: 录音中 (采样率: ${actualSampleRate}Hz)`)
|
|
342
|
+
} catch (error) {
|
|
343
|
+
console.error("音频初始化失败:", error);
|
|
344
|
+
this.isRecording = false
|
|
345
|
+
this.isMicAvailable = false
|
|
346
|
+
}
|
|
347
|
+
},
|
|
348
|
+
async handleSend() {
|
|
349
|
+
if (!this.inputMessage.trim()) {
|
|
350
|
+
return
|
|
351
|
+
}
|
|
352
|
+
const message = this.inputMessage.trim();
|
|
353
|
+
// 发送消息
|
|
354
|
+
this.messages.push({
|
|
355
|
+
id: this.messages.length + 1,
|
|
356
|
+
type: 'user',
|
|
357
|
+
sender: '用户',
|
|
358
|
+
time: new Date().toLocaleTimeString(),
|
|
359
|
+
content: this.inputMessage,
|
|
360
|
+
})
|
|
361
|
+
this.inputMessage = ''
|
|
362
|
+
|
|
363
|
+
try {
|
|
364
|
+
const token = `Bearer 24ab99b4-4b59-42a0-84df-1d73a96e70cd`
|
|
365
|
+
const response = await fetch('/bytserver/api-model/chat/stream', {
|
|
366
|
+
timeout: 30000,
|
|
367
|
+
method: 'POST',
|
|
368
|
+
headers: {
|
|
369
|
+
'Content-Type': 'application/json' ,
|
|
370
|
+
'Authorization': token,
|
|
371
|
+
},
|
|
372
|
+
body: JSON.stringify({ content: message })
|
|
373
|
+
});
|
|
374
|
+
if (!response.ok) {
|
|
375
|
+
throw new Error(`${response.status}`);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
console.log('响应状态:', response.status);
|
|
379
|
+
// 解析响应流
|
|
380
|
+
this.parseResponseStream(response.body)
|
|
381
|
+
} catch (error) {
|
|
382
|
+
console.error('发送消息失败:', error)
|
|
383
|
+
}
|
|
384
|
+
},
|
|
385
|
+
// 分析音频命令
|
|
386
|
+
analyzeAudioCommand(command) {
|
|
387
|
+
console.log('分析音频命令:', command)
|
|
388
|
+
// 解析到开始导览,执行机器人进入动画
|
|
389
|
+
if (command === 'C5') {
|
|
390
|
+
this.robotStatus = 'entering'
|
|
391
|
+
setTimeout(() => {
|
|
392
|
+
this.robotStatus = 'speaking'
|
|
393
|
+
this.play()
|
|
394
|
+
}, 3000)
|
|
395
|
+
}
|
|
396
|
+
// 继续导览
|
|
397
|
+
else if (command === 'C8') {
|
|
398
|
+
this.robotStatus = 'speaking'
|
|
399
|
+
this.play()
|
|
400
|
+
}
|
|
401
|
+
// 解析到暂停导览,执行机器人暂停动画
|
|
402
|
+
else if (command === 'C7') {
|
|
403
|
+
this.robotStatus = 'waiting'
|
|
404
|
+
this.pause()
|
|
405
|
+
}
|
|
406
|
+
// 解析到结束导览,执行机器人离开动画
|
|
407
|
+
else if (command === 'C6') {
|
|
408
|
+
this.robotStatus = 'leaving'
|
|
409
|
+
this.stop()
|
|
410
|
+
}
|
|
411
|
+
},
|
|
412
|
+
// 解析响应流
|
|
413
|
+
parseResponseStream(body) {
|
|
414
|
+
console.log(body)
|
|
415
|
+
},
|
|
416
|
+
processAudio(event) {
|
|
417
|
+
if (!this.isRecording) return;
|
|
418
|
+
// 5. 获取音频数据并处理
|
|
419
|
+
const inputData = event.inputBuffer.getChannelData(0);
|
|
420
|
+
|
|
421
|
+
// 累积音频数据
|
|
422
|
+
const tempBuffer = new Float32Array(this.audioBuffer.length + inputData.length);
|
|
423
|
+
tempBuffer.set(this.audioBuffer, 0);
|
|
424
|
+
tempBuffer.set(inputData, this.audioBuffer.length);
|
|
425
|
+
this.audioBuffer = tempBuffer;
|
|
426
|
+
|
|
427
|
+
// 当累积足够一帧时发送
|
|
428
|
+
while (this.audioBuffer.length >= FRAME_SIZE) {
|
|
429
|
+
const frame = this.audioBuffer.slice(0, FRAME_SIZE);
|
|
430
|
+
this.audioBuffer = this.audioBuffer.slice(FRAME_SIZE);
|
|
431
|
+
|
|
432
|
+
// 转换为16位PCM
|
|
433
|
+
const pcmData = this.floatTo16BitPCM(frame);
|
|
434
|
+
|
|
435
|
+
// 通过WebSocket发送
|
|
436
|
+
if (this.ws && this.ws.readyState === this.ws.OPEN) {
|
|
437
|
+
this.ws.send(pcmData);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
},
|
|
441
|
+
floatTo16BitPCM(input) {
|
|
442
|
+
const output = new Int16Array(input.length);
|
|
443
|
+
for (let i = 0; i < input.length; i++) {
|
|
444
|
+
const s = Math.max(-1, Math.min(1, input[i]));
|
|
445
|
+
output[i] = s < 0 ? s * 0x8000 : s * 0x7FFF;
|
|
446
|
+
}
|
|
447
|
+
return output.buffer;
|
|
448
|
+
},
|
|
169
449
|
toggleWindow() {
|
|
170
450
|
this.visible = !this.visible
|
|
171
451
|
},
|
|
@@ -189,6 +469,18 @@ export default {
|
|
|
189
469
|
this.handleSend()
|
|
190
470
|
}
|
|
191
471
|
},
|
|
472
|
+
stopRecording() {
|
|
473
|
+
if (!this.isRecording) return;
|
|
474
|
+
if (this.microphone) this.microphone.disconnect();
|
|
475
|
+
if (this.processor) this.processor.disconnect();
|
|
476
|
+
if (this.analyser) this.analyser.disconnect();
|
|
477
|
+
if (this.audioContext) {
|
|
478
|
+
this.audioContext.close().catch(e => {
|
|
479
|
+
console.error("关闭音频上下文失败:", e);
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
this.isRecording = false;
|
|
483
|
+
},
|
|
192
484
|
// 添加到body的处理函数
|
|
193
485
|
appendToBodyHandler() {
|
|
194
486
|
// 确保DOM已经渲染完成
|
|
@@ -216,6 +508,12 @@ export default {
|
|
|
216
508
|
}
|
|
217
509
|
</script>
|
|
218
510
|
<style scoped>
|
|
511
|
+
.hidden-audio {
|
|
512
|
+
position: absolute;
|
|
513
|
+
left: -9999px;
|
|
514
|
+
opacity: 0;
|
|
515
|
+
pointer-events: none;
|
|
516
|
+
}
|
|
219
517
|
.chat-overlay {
|
|
220
518
|
position: fixed;
|
|
221
519
|
top: 0;
|
|
@@ -268,6 +566,22 @@ export default {
|
|
|
268
566
|
cursor: pointer;
|
|
269
567
|
user-select: none;
|
|
270
568
|
}
|
|
569
|
+
.chat-robot {
|
|
570
|
+
width: 150px;
|
|
571
|
+
height: 200px;
|
|
572
|
+
}
|
|
573
|
+
.chat-robot.entering {
|
|
574
|
+
background-image: url('./assets/entering.png');
|
|
575
|
+
background-size: cover;
|
|
576
|
+
}
|
|
577
|
+
.chat-robot.waiting {
|
|
578
|
+
background-image: url('./assets/waiting.png');
|
|
579
|
+
background-size: cover;
|
|
580
|
+
}
|
|
581
|
+
.chat-robot.speaking {
|
|
582
|
+
background-image: url('./assets/speaking.png');
|
|
583
|
+
background-size: cover;
|
|
584
|
+
}
|
|
271
585
|
.chat-ai-avater {
|
|
272
586
|
border-radius: 67px;
|
|
273
587
|
border: 1px solid #0f66e4;
|
|
@@ -333,8 +647,8 @@ export default {
|
|
|
333
647
|
background: #fff;
|
|
334
648
|
box-shadow: 0 2px 20px 0 rgba(0, 0, 0, 0.16);
|
|
335
649
|
position: absolute;
|
|
336
|
-
bottom:
|
|
337
|
-
right:
|
|
650
|
+
bottom: 20px;
|
|
651
|
+
right: 60px;
|
|
338
652
|
overflow: hidden;
|
|
339
653
|
display: flex;
|
|
340
654
|
flex-direction: column;
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/components/index.js
CHANGED
|
@@ -4,10 +4,12 @@ const components = {
|
|
|
4
4
|
ChatWindow,
|
|
5
5
|
};
|
|
6
6
|
|
|
7
|
-
const install = function(Vue) {
|
|
7
|
+
const install = function(Vue, options = {}) {
|
|
8
8
|
if (install.installed) return;
|
|
9
9
|
install.installed = true;
|
|
10
10
|
|
|
11
|
+
Vue.prototype.$appOptions = options;
|
|
12
|
+
|
|
11
13
|
Object.keys(components).forEach(name => {
|
|
12
14
|
Vue.component(name, components[name]);
|
|
13
15
|
});
|
|
Binary file
|
|
Binary file
|
|
Binary file
|