byt-lingxiao-ai 0.1.6 → 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.
@@ -1,6 +1,17 @@
1
1
  <template>
2
2
  <div class="chat">
3
- <div class="chat-ai" @click="toggleWindow">
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
- // ... existing code ...
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
- wsUrl: '',
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.scrollToBottom()
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
- initWebSocket() {},
167
- initAudio() {},
168
- handleSend() {},
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: 0;
337
- right: 50px;
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
@@ -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