byt-lingxiao-ai 0.1.6 → 0.1.8

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