byt-lingxiao-ai 0.2.5 → 0.2.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.
@@ -0,0 +1,82 @@
1
+ <template>
2
+ <div class="chat-overlay" v-show="value" @click="$emit('overlay-click')">
3
+ <div class="chat-window" @click.stop>
4
+ <!-- 头部 -->
5
+ <ChatWindowHeader @close="$emit('input', false)" />
6
+
7
+ <!-- 消息列表 -->
8
+ <ChatMessageList
9
+ ref="messageList"
10
+ :messages="messages"
11
+ :think-status="thinkStatus"
12
+ @thinking-click="$emit('thinking-click')"
13
+ />
14
+
15
+ <!-- 输入框 -->
16
+ <ChatInputBox
17
+ :value="inputMessage"
18
+ @input="$emit('update:inputMessage', $event)"
19
+ @send="$emit('send')"
20
+ />
21
+ </div>
22
+ </div>
23
+ </template>
24
+
25
+ <script>
26
+ import ChatWindowHeader from './ChatWindowHeader.vue'
27
+ import ChatMessageList from './ChatMessageList.vue'
28
+ import ChatInputBox from './ChatInputBox.vue'
29
+
30
+ export default {
31
+ name: 'ChatWindowDialog',
32
+ components: {
33
+ ChatWindowHeader,
34
+ ChatMessageList,
35
+ ChatInputBox
36
+ },
37
+ props: {
38
+ value: {
39
+ type: Boolean,
40
+ required: true
41
+ },
42
+ messages: {
43
+ type: Array,
44
+ required: true
45
+ },
46
+ inputMessage: {
47
+ type: String,
48
+ default: ''
49
+ },
50
+ thinkStatus: {
51
+ type: Boolean,
52
+ default: true
53
+ }
54
+ }
55
+ }
56
+ </script>
57
+
58
+ <style scoped>
59
+ .chat-overlay {
60
+ position: fixed;
61
+ top: 0;
62
+ left: 0;
63
+ width: 100%;
64
+ height: 100%;
65
+ background: transparent;
66
+ z-index: 10000;
67
+ }
68
+
69
+ .chat-window {
70
+ width: 480px;
71
+ height: 740px;
72
+ border-radius: 14px;
73
+ background: #fff;
74
+ box-shadow: 0 2px 20px 0 rgba(0, 0, 0, 0.16);
75
+ position: absolute;
76
+ bottom: 20px;
77
+ right: 60px;
78
+ overflow: hidden;
79
+ display: flex;
80
+ flex-direction: column;
81
+ }
82
+ </style>
@@ -0,0 +1,95 @@
1
+ <template>
2
+ <div class="chat-window-header">
3
+ <div class="chat-window-header-title">凌霄大模型AI123对话</div>
4
+ <div class="chat-window-header-open" @click="handleOpen">
5
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
6
+ <path d="M14.5 4.5H19.5V9.5" stroke="#4E5969" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
7
+ <path d="M9.5 19.5H4.5V14.5" stroke="#4E5969" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
8
+ <path d="M19.5 4.5L14.0833 9.91667" stroke="#4E5969" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
9
+ <path d="M9.91667 14.083L4.5 19.4997" stroke="#4E5969" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
10
+ </svg>
11
+ </div>
12
+ <div class="chat-window-header-close" @click="$emit('close')">
13
+ <svg
14
+ xmlns="http://www.w3.org/2000/svg"
15
+ width="24"
16
+ height="24"
17
+ viewBox="0 0 24 24"
18
+ fill="none"
19
+ >
20
+ <path
21
+ d="M5.50002 5.5L18.5 18.5"
22
+ stroke="#4E5969"
23
+ stroke-width="1.89404"
24
+ stroke-linecap="round"
25
+ stroke-linejoin="round"
26
+ />
27
+ <path
28
+ d="M5.50002 18.5L18.5 5.5"
29
+ stroke="#4E5969"
30
+ stroke-width="1.89404"
31
+ stroke-linecap="round"
32
+ stroke-linejoin="round"
33
+ />
34
+ </svg>
35
+ </div>
36
+ </div>
37
+ </template>
38
+
39
+ <script>
40
+ export default {
41
+ name: 'ChatWindowHeader',
42
+ methods: {
43
+ handleOpen() {
44
+ window.open('http://192.168.8.87:8090/', '_blank')
45
+ }
46
+ }
47
+ }
48
+ </script>
49
+
50
+ <style scoped>
51
+ .chat-window-header {
52
+ display: flex;
53
+ align-items: center;
54
+ border-bottom: 1px solid #eaeaea;
55
+ background: #fff;
56
+ padding: 16px;
57
+ flex-shrink: 0;
58
+ }
59
+
60
+ .chat-window-header-title {
61
+ color: #29414e;
62
+ font-size: 18px;
63
+ font-weight: 900;
64
+ position: relative;
65
+ padding-left: 36px;
66
+ }
67
+
68
+ .chat-window-header-open {
69
+ width: 24px;
70
+ height: 24px;
71
+ overflow: hidden;
72
+ margin-left: auto;
73
+ margin-right: 20px;
74
+ cursor: pointer;
75
+ }
76
+
77
+ .chat-window-header-title::before {
78
+ content: '';
79
+ position: absolute;
80
+ left: 0;
81
+ top: 50%;
82
+ transform: translateY(-50%);
83
+ width: 32px;
84
+ height: 32px;
85
+ background-image: url('./assets/logo.png');
86
+ background-size: cover;
87
+ }
88
+
89
+ .chat-window-header-close {
90
+ width: 24px;
91
+ height: 24px;
92
+ overflow: hidden;
93
+ cursor: pointer;
94
+ }
95
+ </style>
@@ -0,0 +1,35 @@
1
+ <template>
2
+ <div class="chat-window-message-user">
3
+ <div class="user-message">{{ content }}</div>
4
+ </div>
5
+ </template>
6
+
7
+ <script>
8
+ export default {
9
+ name: 'UserMessage',
10
+ props: {
11
+ content: {
12
+ type: String,
13
+ required: true
14
+ }
15
+ }
16
+ }
17
+ </script>
18
+
19
+ <style scoped>
20
+ .chat-window-message-user {
21
+ display: flex;
22
+ justify-content: flex-end;
23
+ }
24
+
25
+ .user-message {
26
+ max-width: 80%;
27
+ display: flex;
28
+ padding: 8px 12px;
29
+ justify-content: center;
30
+ align-items: center;
31
+ gap: 10px;
32
+ border-radius: 12px;
33
+ background: #e3ecff;
34
+ }
35
+ </style>
@@ -0,0 +1,122 @@
1
+ export default {
2
+ data() {
3
+ return {
4
+ isRecording: false,
5
+ isMicAvailable: false,
6
+ audioContext: null,
7
+ microphone: null,
8
+ processor: null,
9
+ audioBuffer: new Float32Array(0)
10
+ }
11
+ },
12
+ methods: {
13
+ async initAudio() {
14
+ if (this.isRecording) return;
15
+ try {
16
+ this.isMicAvailable = true;
17
+ const stream = await navigator.mediaDevices.getUserMedia({
18
+ audio: {
19
+ sampleRate: this.SAMPLE_RATE,
20
+ channelCount: 1,
21
+ noiseSuppression: true,
22
+ echoCancellation: true
23
+ }
24
+ });
25
+
26
+ this.audioContext = new AudioContext({ sampleRate: this.SAMPLE_RATE });
27
+ this.microphone = this.audioContext.createMediaStreamSource(stream);
28
+ this.processor = this.audioContext.createScriptProcessor(this.FRAME_SIZE, 1, 1);
29
+ this.processor.onaudioprocess = this.processAudio;
30
+
31
+ this.microphone.connect(this.processor);
32
+ this.processor.connect(this.audioContext.destination);
33
+
34
+ this.isRecording = true;
35
+ console.log(`录音中 (采样率: ${this.audioContext.sampleRate}Hz)`);
36
+ } catch (error) {
37
+ console.error("音频初始化失败:", error);
38
+ this.isRecording = false;
39
+ this.isMicAvailable = false;
40
+ }
41
+ },
42
+
43
+ processAudio(event) {
44
+ if (!this.isRecording) return;
45
+ const inputData = event.inputBuffer.getChannelData(0);
46
+
47
+ const tempBuffer = new Float32Array(this.audioBuffer.length + inputData.length);
48
+ tempBuffer.set(this.audioBuffer, 0);
49
+ tempBuffer.set(inputData, this.audioBuffer.length);
50
+ this.audioBuffer = tempBuffer;
51
+
52
+ while (this.audioBuffer.length >= this.FRAME_SIZE) {
53
+ const frame = this.audioBuffer.slice(0, this.FRAME_SIZE);
54
+ this.audioBuffer = this.audioBuffer.slice(this.FRAME_SIZE);
55
+ const pcmData = this.floatTo16BitPCM(frame);
56
+
57
+ if (this.ws && this.ws.readyState === this.ws.OPEN) {
58
+ this.ws.send(pcmData);
59
+ }
60
+ }
61
+ },
62
+
63
+ floatTo16BitPCM(input) {
64
+ const output = new Int16Array(input.length);
65
+ for (let i = 0; i < input.length; i++) {
66
+ const s = Math.max(-1, Math.min(1, input[i]));
67
+ output[i] = s < 0 ? s * 0x8000 : s * 0x7FFF;
68
+ }
69
+ return output.buffer;
70
+ },
71
+
72
+ stopRecording() {
73
+ if (!this.isRecording) return;
74
+ if (this.microphone) this.microphone.disconnect();
75
+ if (this.processor) this.processor.disconnect();
76
+ if (this.analyser) this.analyser.disconnect();
77
+ if (this.audioContext) {
78
+ this.audioContext.close().catch(e => {
79
+ console.error("关闭音频上下文失败:", e);
80
+ });
81
+ }
82
+ this.isRecording = false;
83
+ },
84
+
85
+ play() {
86
+ this.robotStatus = 'speaking';
87
+ this.$refs.audioPlayer.play();
88
+ },
89
+
90
+ pause() {
91
+ this.robotStatus = 'waiting';
92
+ this.$refs.audioPlayer.pause();
93
+ },
94
+
95
+ stop() {
96
+ this.robotStatus = 'leaving';
97
+ this.$refs.audioPlayer.pause();
98
+ this.$refs.audioPlayer.currentTime = 0;
99
+ this.jumpedTimePoints.clear();
100
+ },
101
+
102
+ analyzeAudioCommand(command) {
103
+ console.log('分析音频命令:', command);
104
+ if (command === 'C5') {
105
+ this.robotStatus = 'entering';
106
+ setTimeout(() => {
107
+ this.robotStatus = 'speaking';
108
+ this.play();
109
+ }, 3000);
110
+ } else if (command === 'C8') {
111
+ this.robotStatus = 'speaking';
112
+ this.play();
113
+ } else if (command === 'C7') {
114
+ this.robotStatus = 'waiting';
115
+ this.pause();
116
+ } else if (command === 'C6') {
117
+ this.robotStatus = 'leaving';
118
+ this.stop();
119
+ }
120
+ }
121
+ }
122
+ }
@@ -0,0 +1,158 @@
1
+ import { StreamParser } from '../utils/StreamParser'
2
+
3
+ export default {
4
+ data() {
5
+ return {
6
+ streamParser: null
7
+ }
8
+ },
9
+
10
+ created() {
11
+ // 初始化流解析器
12
+ this.streamParser = new StreamParser({
13
+ updateInterval: 16, // 约60fps
14
+ debug: process.env.NODE_ENV === 'development'
15
+ });
16
+ },
17
+
18
+ methods: {
19
+ createAiMessage() {
20
+ const message = {
21
+ id: this.messages.length + 1,
22
+ type: 'ai',
23
+ sender: 'AI',
24
+ time: '',
25
+ thinking: '',
26
+ charts: [],
27
+ content: '',
28
+ };
29
+ this.messages.push(message);
30
+ this.currentMessage = message;
31
+ return message;
32
+ },
33
+
34
+ createUserMessage(content) {
35
+ const message = {
36
+ id: this.messages.length + 1,
37
+ type: 'user',
38
+ sender: '用户',
39
+ time: '',
40
+ content,
41
+ };
42
+ this.messages.push(message);
43
+ this.inputMessage = '';
44
+ return message;
45
+ },
46
+
47
+ async handleSend() {
48
+ if (!this.inputMessage.trim()) {
49
+ return;
50
+ }
51
+
52
+ const message = this.inputMessage.trim();
53
+ this.createUserMessage(message);
54
+ this.createAiMessage();
55
+
56
+ // 重置解析器
57
+ this.streamParser.reset();
58
+
59
+ try {
60
+ const startTime = Date.now();
61
+ const controller = new AbortController();
62
+ const token = `Bearer ac627d0a-8346-4ae9-b93a-f37ff6210adc`;
63
+
64
+ const response = await fetch('/bytserver/api-model/chat/stream', {
65
+ method: 'POST',
66
+ signal: controller.signal,
67
+ headers: {
68
+ 'Content-Type': 'application/json',
69
+ 'Authorization': token,
70
+ },
71
+ body: JSON.stringify({ content: message })
72
+ });
73
+
74
+ if (!response.ok) {
75
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
76
+ }
77
+
78
+ // 使用优化的流处理
79
+ await this.consumeStream(response.body);
80
+
81
+ // 完成解析
82
+ this.streamParser.finish(this.handleStreamUpdate);
83
+
84
+ // 记录耗时
85
+ const duration = Date.now() - startTime;
86
+ if (this.currentMessage) {
87
+ this.currentMessage.time = (duration / 1000).toFixed(2);
88
+ }
89
+
90
+ console.log(`流处理完成,总耗时: ${duration}ms`);
91
+
92
+ } catch (error) {
93
+ console.error('发送消息失败:', error);
94
+ if (this.currentMessage) {
95
+ this.currentMessage.content = '抱歉,发生了错误,请重试。';
96
+ this.$forceUpdate();
97
+ }
98
+ }
99
+ },
100
+
101
+ /**
102
+ * 消费流数据
103
+ */
104
+ async consumeStream(readableStream) {
105
+ const reader = readableStream.getReader();
106
+ const decoder = new TextDecoder('utf-8');
107
+
108
+ try {
109
+ // eslint-disable-next-line no-constant-condition
110
+ while (true) {
111
+ const { done, value } = await reader.read();
112
+ if (done) break;
113
+
114
+ const chunk = decoder.decode(value, { stream: true });
115
+
116
+ // 使用解析器处理数据块
117
+ this.streamParser.processChunk(chunk, this.handleStreamUpdate);
118
+ }
119
+ } finally {
120
+ reader.releaseLock();
121
+ }
122
+ },
123
+
124
+ /**
125
+ * 处理流更新回调
126
+ */
127
+ handleStreamUpdate(result) {
128
+ if (!this.currentMessage) return;
129
+
130
+ // 更新思考内容
131
+ if (result.thinking) {
132
+ this.currentMessage.thinking += result.thinking;
133
+ }
134
+
135
+ // 更新回复内容
136
+ if (result.content) {
137
+ this.currentMessage.content += result.content;
138
+ }
139
+
140
+ // 更新状态
141
+ if (result.status === 'thinking') {
142
+ this.avaterStatus = 'thinking';
143
+ } else {
144
+ this.avaterStatus = 'output';
145
+ }
146
+
147
+ // 触发视图更新
148
+ this.$forceUpdate();
149
+ }
150
+ },
151
+
152
+ beforeDestroy() {
153
+ // 清理解析器
154
+ if (this.streamParser) {
155
+ this.streamParser.destroy();
156
+ }
157
+ }
158
+ }
@@ -0,0 +1,94 @@
1
+ export default {
2
+ data() {
3
+ return {
4
+ ws: null,
5
+ wsUrl: 'ws://192.168.8.9:9999/ai_model/ws/voice-stream',
6
+ isConnected: false,
7
+ reconnectCount: 0, // 重连尝试次数
8
+ maxReconnectAttempts: 3 // 最大重连尝试次数
9
+ }
10
+ },
11
+ methods: {
12
+ initWebSocket() {
13
+ try {
14
+ this.ws = new WebSocket(this.wsUrl);
15
+ this.ws.binaryType = 'arraybuffer';
16
+
17
+ this.ws.onopen = async () => {
18
+ console.log('WebSocket 连接成功');
19
+ this.isConnected = true;
20
+ this.initAudio();
21
+ };
22
+
23
+ this.ws.onmessage = (event) => {
24
+ try {
25
+ console.log("收到原始消息:", event.data);
26
+
27
+ if (event.data instanceof ArrayBuffer) {
28
+ console.log("收到二进制音频数据");
29
+ return;
30
+ }
31
+
32
+ const data = JSON.parse(event.data);
33
+ console.log("解析后的数据:", data);
34
+
35
+ if (data.type === 'config') {
36
+ console.log('配置信息:', data);
37
+ } else if (data.code === 0) {
38
+ this.handleWebSocketMessage(data.data);
39
+ } else {
40
+ console.error("服务器返回错误:", data.msg);
41
+ }
42
+ } catch (error) {
43
+ console.error("消息解析错误:", error);
44
+ }
45
+ };
46
+
47
+ this.ws.onclose = () => {
48
+ console.log('WebSocket 连接关闭');
49
+ this.isConnected = false;
50
+ if (this.isRecording) {
51
+ this.stopRecording();
52
+ }
53
+ this.reconnectCount++;
54
+ console.log(`已尝试重连 ${this.reconnectCount} 次`);
55
+ if (this.reconnectCount <= this.maxReconnectAttempts) {
56
+ setTimeout(() => {
57
+ console.log('尝试重新建立连接');
58
+ if (!this.isConnected) {
59
+ this.initWebSocket();
60
+ }
61
+ }, 3000);
62
+ }
63
+ };
64
+
65
+ this.ws.onerror = (error) => {
66
+ console.error('WebSocket 错误:', error);
67
+ };
68
+ } catch (error) {
69
+ console.error('WebSocket连接失败:', error);
70
+ }
71
+ },
72
+
73
+ handleWebSocketMessage(data) {
74
+ if (data.type === 'detection') {
75
+ console.log('检测到唤醒词');
76
+ this.avaterStatus = 'normal';
77
+ } else if (data.type === 'Collecting') {
78
+ console.log('状态: 采集中');
79
+ this.avaterStatus = 'thinking';
80
+ } else if (data.type === 'command') {
81
+ console.log('状态: 处理中');
82
+ this.analyzeAudioCommand(data.category);
83
+ } else {
84
+ console.log('状态: 其他');
85
+ }
86
+ },
87
+
88
+ closeWebSocket() {
89
+ if (this.ws) {
90
+ this.ws.close();
91
+ }
92
+ }
93
+ }
94
+ }