byt-lingxiao-ai 0.3.4 → 0.3.5
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/components/AiMessage.vue +1 -1
- package/components/ChatWindow.vue +5 -0
- package/components/ChatWindowDialog.vue +5 -1
- package/components/ChatWindowHeader.vue +7 -3
- package/components/config/index.js +1 -1
- package/components/mixins/audioMixin.js +22 -12
- package/components/mixins/messageMixin.js +6 -5
- package/components/mixins/webSocketMixin.js +13 -0
- package/components/utils/Cookie.js +19 -0
- package/components/utils/StreamParser.js +9 -0
- package/components/utils/Uuid.js +6 -0
- package/components/utils/WorkletSource.js +27 -0
- package/dist/index.common.js +122 -50
- package/dist/index.common.js.map +1 -1
- package/dist/index.css +2 -2
- package/dist/index.umd.js +122 -50
- 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 +1 -1
package/components/AiMessage.vue
CHANGED
|
@@ -58,7 +58,7 @@ function parseMarkdown(text) {
|
|
|
58
58
|
|
|
59
59
|
html = html.replace(/^>\s+(.+)$/gm, '<blockquote>$1</blockquote>');
|
|
60
60
|
// 解析Markdown链接
|
|
61
|
-
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2"
|
|
61
|
+
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" aria-current="page">$1</a>');
|
|
62
62
|
|
|
63
63
|
html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1" />');
|
|
64
64
|
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
:messages="messages"
|
|
34
34
|
:input-message="inputMessage"
|
|
35
35
|
:think-status="thinkStatus"
|
|
36
|
+
:chat-id="chatId"
|
|
36
37
|
@update:inputMessage="inputMessage = $event"
|
|
37
38
|
@send="handleSend"
|
|
38
39
|
@thinking-click="handleThinkingClick"
|
|
@@ -49,9 +50,11 @@ import audioMixin from './mixins/audioMixin'
|
|
|
49
50
|
import webSocketMixin from './mixins/webSocketMixin'
|
|
50
51
|
import messageMixin from './mixins/messageMixin'
|
|
51
52
|
import { AUDIO_URL, TIME_JUMP_POINTS_URL } from './config/index.js'
|
|
53
|
+
import generateUuid from './utils/Uuid.js'
|
|
52
54
|
|
|
53
55
|
const SAMPLE_RATE = 16000;
|
|
54
56
|
const FRAME_SIZE = 512;
|
|
57
|
+
const startTime = null
|
|
55
58
|
|
|
56
59
|
export default {
|
|
57
60
|
name: 'ChatWindow',
|
|
@@ -69,6 +72,7 @@ export default {
|
|
|
69
72
|
},
|
|
70
73
|
data() {
|
|
71
74
|
return {
|
|
75
|
+
chatId: generateUuid(),
|
|
72
76
|
audioSrc: AUDIO_URL,
|
|
73
77
|
inputMessage: '',
|
|
74
78
|
visible: false,
|
|
@@ -80,6 +84,7 @@ export default {
|
|
|
80
84
|
jumpedTimePoints: new Set(),
|
|
81
85
|
SAMPLE_RATE,
|
|
82
86
|
FRAME_SIZE,
|
|
87
|
+
startTime, // 检查性能使用
|
|
83
88
|
dragThreshold: 5, // 拖拽阈值
|
|
84
89
|
isDragging: false,
|
|
85
90
|
dragStartX: 0,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<div class="chat-overlay" v-show="value" @click="$emit('overlay-click')">
|
|
3
3
|
<div class="chat-window" @click.stop>
|
|
4
4
|
<!-- 头部 -->
|
|
5
|
-
<ChatWindowHeader @close="$emit('input', false)" />
|
|
5
|
+
<ChatWindowHeader :chat-id="chatId" @close="$emit('input', false)" />
|
|
6
6
|
|
|
7
7
|
<!-- 消息列表 -->
|
|
8
8
|
<ChatMessageList
|
|
@@ -55,6 +55,10 @@ export default {
|
|
|
55
55
|
loading: {
|
|
56
56
|
type: Boolean,
|
|
57
57
|
default: false
|
|
58
|
+
},
|
|
59
|
+
chatId: {
|
|
60
|
+
type: String,
|
|
61
|
+
default: ''
|
|
58
62
|
}
|
|
59
63
|
}
|
|
60
64
|
}
|
|
@@ -39,11 +39,15 @@
|
|
|
39
39
|
<script>
|
|
40
40
|
export default {
|
|
41
41
|
name: 'ChatWindowHeader',
|
|
42
|
+
props: {
|
|
43
|
+
chatId: {
|
|
44
|
+
type: String,
|
|
45
|
+
default: ''
|
|
46
|
+
}
|
|
47
|
+
},
|
|
42
48
|
methods: {
|
|
43
49
|
handleOpen() {
|
|
44
|
-
|
|
45
|
-
// const baseUrl = window.location.protocol + '//' + window.location.hostname + ':3100/c/' + chatId;
|
|
46
|
-
const baseUrl = window.location.protocol + '//' + window.location.hostname + ':3100/';
|
|
50
|
+
const baseUrl = window.location.protocol + '//' + window.location.hostname + ':3100/c/' + this.chatId;
|
|
47
51
|
window.open(baseUrl, '_blank')
|
|
48
52
|
}
|
|
49
53
|
}
|
|
@@ -2,7 +2,7 @@ const baseUrl = window.location.protocol + '//' + window.location.hostname;
|
|
|
2
2
|
const chatPort = '3100';
|
|
3
3
|
|
|
4
4
|
console.log(baseUrl, chatPort);
|
|
5
|
-
export const API_URL =
|
|
5
|
+
export const API_URL = `${baseUrl}:${chatPort}/lingxiao-byt/api/v1/mcp/ask`;
|
|
6
6
|
export const WS_URL = 'ws://192.168.8.9:9999/ai_model/ws/voice-stream';
|
|
7
7
|
export const AUDIO_URL = '/minio/lingxiaoai/byt.mp3';
|
|
8
8
|
export const TIME_JUMP_POINTS_URL = '/minio/lingxiaoai/timeJumpPoints.json';
|
|
@@ -6,7 +6,9 @@ export default {
|
|
|
6
6
|
audioContext: null,
|
|
7
7
|
microphone: null,
|
|
8
8
|
processor: null,
|
|
9
|
-
audioBuffer: new Float32Array(0)
|
|
9
|
+
audioBuffer: new Float32Array(0),
|
|
10
|
+
// 优化
|
|
11
|
+
int16Buffer: null
|
|
10
12
|
}
|
|
11
13
|
},
|
|
12
14
|
methods: {
|
|
@@ -24,8 +26,15 @@ export default {
|
|
|
24
26
|
});
|
|
25
27
|
|
|
26
28
|
this.audioContext = new AudioContext({ sampleRate: this.SAMPLE_RATE });
|
|
29
|
+
|
|
30
|
+
// 优化 处理 AudioContext 挂起
|
|
31
|
+
if (this.audioContext.state === 'suspended') {
|
|
32
|
+
await this.audioContext.resume();
|
|
33
|
+
}
|
|
34
|
+
|
|
27
35
|
this.microphone = this.audioContext.createMediaStreamSource(stream);
|
|
28
36
|
this.processor = this.audioContext.createScriptProcessor(this.FRAME_SIZE, 1, 1);
|
|
37
|
+
this.int16Buffer = new Int16Array(this.FRAME_SIZE);
|
|
29
38
|
this.processor.onaudioprocess = this.processAudio;
|
|
30
39
|
|
|
31
40
|
this.microphone.connect(this.processor);
|
|
@@ -44,19 +53,20 @@ export default {
|
|
|
44
53
|
if (!this.isRecording) return;
|
|
45
54
|
const inputData = event.inputBuffer.getChannelData(0);
|
|
46
55
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
+
if (this.ws && this.ws.readyState === this.ws.OPEN) {
|
|
57
|
+
if (inputData.length !== this.int16Buffer.length) {
|
|
58
|
+
this.int16Buffer = new Int16Array(inputData.length);
|
|
59
|
+
}
|
|
56
60
|
|
|
57
|
-
|
|
58
|
-
|
|
61
|
+
for (let i = 0; i < inputData.length; i++) {
|
|
62
|
+
let s = inputData[i];
|
|
63
|
+
// 简单的 clamp
|
|
64
|
+
s = s < -1 ? -1 : s > 1 ? 1 : s;
|
|
65
|
+
this.int16Buffer[i] = s < 0 ? s * 0x8000 : s * 0x7FFF;
|
|
59
66
|
}
|
|
67
|
+
|
|
68
|
+
// 4. 发送数据
|
|
69
|
+
this.ws.send(this.int16Buffer.buffer);
|
|
60
70
|
}
|
|
61
71
|
},
|
|
62
72
|
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { StreamParser } from '../utils/StreamParser'
|
|
2
2
|
import { API_URL } from '../config/index.js'
|
|
3
|
+
import { getCookie } from '../utils/Cookie.js'
|
|
4
|
+
|
|
5
|
+
|
|
3
6
|
|
|
4
7
|
export default {
|
|
5
8
|
data() {
|
|
@@ -61,17 +64,15 @@ export default {
|
|
|
61
64
|
|
|
62
65
|
try {
|
|
63
66
|
const startTime = Date.now();
|
|
64
|
-
const
|
|
65
|
-
const token = `Bearer e298f087-85bc-48c2-afb9-7c69ffc911aa`;
|
|
67
|
+
const token = getCookie('bonyear-access_token') || `e298f087-85bc-48c2-afb9-7c69ffc911aa`;
|
|
66
68
|
|
|
67
69
|
const response = await fetch(API_URL, {
|
|
68
70
|
method: 'POST',
|
|
69
|
-
signal: controller.signal,
|
|
70
71
|
headers: {
|
|
71
72
|
'Content-Type': 'application/json',
|
|
72
|
-
'Authorization': token
|
|
73
|
+
'Authorization': `Bearer ${token}`,
|
|
73
74
|
},
|
|
74
|
-
body: JSON.stringify({ content: message })
|
|
75
|
+
body: JSON.stringify({ content: message, chatId: this.chatId })
|
|
75
76
|
});
|
|
76
77
|
|
|
77
78
|
if (!response.ok) {
|
|
@@ -76,11 +76,24 @@ export default {
|
|
|
76
76
|
if (data.type === 'detection') {
|
|
77
77
|
console.log('检测到唤醒词');
|
|
78
78
|
this.avaterStatus = 'normal';
|
|
79
|
+
|
|
80
|
+
// 性能检测起点
|
|
81
|
+
this.startTime = Date.now(); // <-- 新增计时器
|
|
82
|
+
console.log(`[Timer] 指令发送开始计时: ${this.startTime}ms`);
|
|
83
|
+
|
|
79
84
|
} else if (data.type === 'Collecting') {
|
|
80
85
|
console.log('状态: 采集中');
|
|
81
86
|
this.avaterStatus = 'thinking';
|
|
82
87
|
} else if (data.type === 'command') {
|
|
83
88
|
console.log('状态: 处理中');
|
|
89
|
+
|
|
90
|
+
// 性能检测终点
|
|
91
|
+
if (this.startTime) {
|
|
92
|
+
const latency = Date.now() - this.startTime;
|
|
93
|
+
console.log(`[Latency] 完整命令处理耗时: ${latency}ms`); // 记录端到端延迟
|
|
94
|
+
this.startTime = null;
|
|
95
|
+
}
|
|
96
|
+
|
|
84
97
|
this.analyzeAudioCommand(data.category);
|
|
85
98
|
} else {
|
|
86
99
|
console.log('状态: 其他');
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const setCookie = (name, value, days) => {
|
|
2
|
+
const d = new Date();
|
|
3
|
+
d.setTime(d.getTime() + days * 24 * 60 * 60 * 1000);
|
|
4
|
+
const expires = `expires=${d.toUTCString()}`;
|
|
5
|
+
document.cookie = `${name}=${value}; ${expires}`;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const getCookie = cname => {
|
|
9
|
+
const name = `${cname}=`;
|
|
10
|
+
const ca = document.cookie.split(';');
|
|
11
|
+
for (let i = 0; i < ca.length; i++) {
|
|
12
|
+
let c = ca[i];
|
|
13
|
+
while (c.charAt(0) === ' ') c = c.substring(1);
|
|
14
|
+
if (c.indexOf(name) !== -1) return c.substring(name.length, c.length);
|
|
15
|
+
}
|
|
16
|
+
return '';
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export { setCookie, getCookie };
|
|
@@ -41,6 +41,15 @@ export class StreamParser {
|
|
|
41
41
|
if (this.options.debug) {
|
|
42
42
|
console.log('[StreamParser] 收到chunk:', chunk.substring(0, 100));
|
|
43
43
|
}
|
|
44
|
+
|
|
45
|
+
if (!this.buffer.includes('data:') && !this.buffer.includes('\n\n')) {
|
|
46
|
+
// 纯文本流,直接处理
|
|
47
|
+
if (this.options.debug) {
|
|
48
|
+
console.log('[StreamParser] 检测到纯文本流');
|
|
49
|
+
}
|
|
50
|
+
this.processPlainText(callback);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
44
53
|
|
|
45
54
|
// 尝试解析为 SSE 格式
|
|
46
55
|
if (this.buffer.includes('data:')) {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// 在您的组件库的某个 util 文件中定义 (例如: utils/workletSource.js)
|
|
2
|
+
|
|
3
|
+
export const RECORDER_PROCESSOR_CODE = `
|
|
4
|
+
class RecorderProcessor extends AudioWorkletProcessor {
|
|
5
|
+
constructor() {
|
|
6
|
+
super();
|
|
7
|
+
console.log('RecorderProcessor created in Worklet Thread');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
process(inputs, outputs, parameters) {
|
|
11
|
+
const input = inputs[0];
|
|
12
|
+
const inputChannel = input[0];
|
|
13
|
+
|
|
14
|
+
if (inputChannel && inputChannel.length > 0) {
|
|
15
|
+
// 通过 postMessage 将 Float32Array 缓冲区发送回主线程。
|
|
16
|
+
this.port.postMessage(inputChannel.slice());
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
registerProcessor('recorder-processor', RecorderProcessor);
|
|
24
|
+
`;
|
|
25
|
+
|
|
26
|
+
// Worklet 节点在主线程中注册时使用的名称
|
|
27
|
+
export const PROCESSOR_NAME = 'recorder-processor';
|