byt-lingxiao-ai 0.3.27 → 0.3.29
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/AudioSubtitle.vue +29 -0
- package/components/ChatWindow.vue +362 -70
- package/components/config/blacklist.js +3 -0
- package/components/config/index.js +12 -3
- package/components/mixins/audioMixin.js +137 -80
- package/components/mixins/messageMixin.js +2 -2
- package/components/mixins/webSocketMixin.js +7 -10
- package/dist/index.common.js +544 -135
- package/dist/index.common.js.map +1 -1
- package/dist/index.css +1 -1
- package/dist/index.umd.js +544 -135
- 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 +2 -2
|
@@ -1,20 +1,94 @@
|
|
|
1
1
|
export default {
|
|
2
2
|
data() {
|
|
3
3
|
return {
|
|
4
|
+
audioContext: null,
|
|
5
|
+
workletNode: null,
|
|
6
|
+
mediaStreamSource: null,
|
|
4
7
|
isRecording: false,
|
|
5
8
|
isMicAvailable: false,
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
processor: null,
|
|
9
|
-
audioBuffer: new Float32Array(0)
|
|
10
|
-
}
|
|
9
|
+
audioData: new Float32Array(128)
|
|
10
|
+
};
|
|
11
11
|
},
|
|
12
12
|
methods: {
|
|
13
|
+
async initAudioWorklet() {
|
|
14
|
+
if (this.audioContext) return;
|
|
15
|
+
|
|
16
|
+
const AudioContext = window.AudioContext || window.webkitAudioContext;
|
|
17
|
+
this.audioContext = new AudioContext({ sampleRate: this.SAMPLE_RATE });
|
|
18
|
+
|
|
19
|
+
const processorCode = `
|
|
20
|
+
class RecorderProcessor extends AudioWorkletProcessor {
|
|
21
|
+
constructor() {
|
|
22
|
+
super();
|
|
23
|
+
this.buffer = [];
|
|
24
|
+
this.frameSize = ${this.FRAME_SIZE};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
process(inputs) {
|
|
28
|
+
const input = inputs[0];
|
|
29
|
+
if (!input || !input[0]) return true;
|
|
30
|
+
|
|
31
|
+
const channel = input[0];
|
|
32
|
+
|
|
33
|
+
for (let i = 0; i < channel.length; i++) {
|
|
34
|
+
this.buffer.push(channel[i]);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
while (this.buffer.length >= this.frameSize) {
|
|
38
|
+
const frame = this.buffer.splice(0, this.frameSize);
|
|
39
|
+
|
|
40
|
+
// PCM
|
|
41
|
+
const pcm = this.floatTo16BitPCM(frame);
|
|
42
|
+
this.port.postMessage({
|
|
43
|
+
type: 'audio',
|
|
44
|
+
payload: pcm
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Visual(下采样)
|
|
48
|
+
this.port.postMessage({
|
|
49
|
+
type: 'visual',
|
|
50
|
+
payload: new Float32Array(frame.slice(0, 128))
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
floatTo16BitPCM(float32Array) {
|
|
57
|
+
const pcm = new Int16Array(float32Array.length);
|
|
58
|
+
for (let i = 0; i < float32Array.length; i++) {
|
|
59
|
+
let s = Math.max(-1, Math.min(1, float32Array[i]));
|
|
60
|
+
pcm[i] = s < 0 ? s * 0x8000 : s * 0x7fff;
|
|
61
|
+
}
|
|
62
|
+
return pcm.buffer;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
registerProcessor('recorder-processor', RecorderProcessor);
|
|
67
|
+
`;
|
|
68
|
+
|
|
69
|
+
const blob = new Blob([processorCode], {
|
|
70
|
+
type: 'application/javascript'
|
|
71
|
+
});
|
|
72
|
+
const moduleUrl = URL.createObjectURL(blob);
|
|
73
|
+
|
|
74
|
+
await this.audioContext.audioWorklet.addModule(moduleUrl);
|
|
75
|
+
|
|
76
|
+
this.workletNode = new AudioWorkletNode(
|
|
77
|
+
this.audioContext,
|
|
78
|
+
'recorder-processor'
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
this.workletNode.port.onmessage = this.handleWorkletMessage;
|
|
82
|
+
},
|
|
13
83
|
async initAudio() {
|
|
14
84
|
if (this.isRecording) return;
|
|
85
|
+
|
|
15
86
|
try {
|
|
16
87
|
this.isMicAvailable = true;
|
|
17
|
-
|
|
88
|
+
|
|
89
|
+
await this.initAudioWorklet();
|
|
90
|
+
|
|
91
|
+
this.stream = await navigator.mediaDevices.getUserMedia({
|
|
18
92
|
audio: {
|
|
19
93
|
sampleRate: this.SAMPLE_RATE,
|
|
20
94
|
channelCount: 1,
|
|
@@ -24,102 +98,85 @@ export default {
|
|
|
24
98
|
}
|
|
25
99
|
});
|
|
26
100
|
|
|
27
|
-
this.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
this.
|
|
31
|
-
|
|
32
|
-
this.
|
|
33
|
-
|
|
101
|
+
this.mediaStreamSource =
|
|
102
|
+
this.audioContext.createMediaStreamSource(this.stream);
|
|
103
|
+
|
|
104
|
+
this.mediaStreamSource.connect(this.workletNode);
|
|
105
|
+
|
|
106
|
+
if (this.audioContext.state === 'suspended') {
|
|
107
|
+
await this.audioContext.resume();
|
|
108
|
+
}
|
|
34
109
|
|
|
35
110
|
this.isRecording = true;
|
|
36
|
-
console.log(
|
|
37
|
-
|
|
38
|
-
|
|
111
|
+
console.log(`AudioWorklet 录音中 (${this.SAMPLE_RATE}Hz)`);
|
|
112
|
+
|
|
113
|
+
} catch (e) {
|
|
114
|
+
console.error('音频初始化失败:', e);
|
|
39
115
|
this.isRecording = false;
|
|
40
116
|
this.isMicAvailable = false;
|
|
41
117
|
}
|
|
42
118
|
},
|
|
119
|
+
handleWorkletMessage(e) {
|
|
120
|
+
const { type, payload } = e.data;
|
|
43
121
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const tempBuffer = new Float32Array(this.audioBuffer.length + inputData.length);
|
|
49
|
-
tempBuffer.set(this.audioBuffer, 0);
|
|
50
|
-
tempBuffer.set(inputData, this.audioBuffer.length);
|
|
51
|
-
this.audioBuffer = tempBuffer;
|
|
52
|
-
|
|
53
|
-
while (this.audioBuffer.length >= this.FRAME_SIZE) {
|
|
54
|
-
const frame = this.audioBuffer.slice(0, this.FRAME_SIZE);
|
|
55
|
-
this.audioBuffer = this.audioBuffer.slice(this.FRAME_SIZE);
|
|
56
|
-
const pcmData = this.floatTo16BitPCM(frame);
|
|
57
|
-
|
|
58
|
-
if (this.ws && this.ws.readyState === this.ws.OPEN) {
|
|
59
|
-
this.ws.send(pcmData);
|
|
122
|
+
if (type === 'audio') {
|
|
123
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
124
|
+
this.ws.send(payload);
|
|
60
125
|
}
|
|
61
126
|
}
|
|
62
127
|
},
|
|
128
|
+
stopRecording() {
|
|
129
|
+
if (!this.isRecording) return;
|
|
63
130
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
131
|
+
if (this.mediaStreamSource) this.mediaStreamSource.disconnect();
|
|
132
|
+
if (this.workletNode) this.workletNode.disconnect();
|
|
133
|
+
|
|
134
|
+
if (this.stream) {
|
|
135
|
+
this.stream.getTracks().forEach(t => t.stop());
|
|
69
136
|
}
|
|
70
|
-
return output.buffer;
|
|
71
|
-
},
|
|
72
137
|
|
|
73
|
-
stopRecording() {
|
|
74
|
-
if (!this.isRecording) return;
|
|
75
|
-
if (this.microphone) this.microphone.disconnect();
|
|
76
|
-
if (this.processor) this.processor.disconnect();
|
|
77
|
-
if (this.analyser) this.analyser.disconnect();
|
|
78
138
|
if (this.audioContext) {
|
|
79
|
-
this.audioContext.close()
|
|
80
|
-
|
|
81
|
-
});
|
|
139
|
+
this.audioContext.close();
|
|
140
|
+
this.audioContext = null;
|
|
82
141
|
}
|
|
142
|
+
|
|
83
143
|
this.isRecording = false;
|
|
144
|
+
console.log('录音已停止');
|
|
84
145
|
},
|
|
146
|
+
handleWebSocketMessage(data) {
|
|
147
|
+
if (data.type === 'detection') {
|
|
148
|
+
if (this.robotStatus === 'speaking') {
|
|
149
|
+
this.robotStatus = 'waiting';
|
|
150
|
+
}
|
|
151
|
+
this.avaterStatus = 'normal';
|
|
152
|
+
this.startTime = Date.now();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (data.type === 'Collecting') {
|
|
156
|
+
this.avaterStatus = 'thinking';
|
|
157
|
+
}
|
|
85
158
|
|
|
159
|
+
if (data.type === 'command') {
|
|
160
|
+
if (this.startTime) {
|
|
161
|
+
console.log(
|
|
162
|
+
`[Latency] ${Date.now() - this.startTime}ms`
|
|
163
|
+
);
|
|
164
|
+
this.startTime = null;
|
|
165
|
+
}
|
|
166
|
+
this.analyzeAudioCommand(data.category);
|
|
167
|
+
}
|
|
168
|
+
},
|
|
86
169
|
play() {
|
|
87
|
-
this.
|
|
88
|
-
this.$refs.audioPlayer.play();
|
|
170
|
+
this.$refs.audioPlayer?.play();
|
|
89
171
|
},
|
|
90
|
-
|
|
91
172
|
pause() {
|
|
92
|
-
|
|
93
|
-
this.robotStatus = 'waiting';
|
|
94
|
-
this.$refs.audioPlayer.pause();
|
|
173
|
+
this.$refs.audioPlayer?.pause();
|
|
95
174
|
},
|
|
96
|
-
|
|
97
175
|
stop() {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
},
|
|
103
|
-
|
|
104
|
-
analyzeAudioCommand(command) {
|
|
105
|
-
console.log('分析音频命令:', command);
|
|
106
|
-
if (command === 'C5') {
|
|
107
|
-
this.robotStatus = 'entering';
|
|
108
|
-
setTimeout(() => {
|
|
109
|
-
this.robotStatus = 'speaking';
|
|
110
|
-
this.play();
|
|
111
|
-
}, 3000);
|
|
112
|
-
} else if (command === 'C8') {
|
|
113
|
-
this.robotStatus = 'speaking';
|
|
114
|
-
this.play();
|
|
115
|
-
} else if (command === 'C7') {
|
|
116
|
-
this.robotStatus = 'waiting';
|
|
117
|
-
this.pause();
|
|
118
|
-
} else if (command === 'C6') {
|
|
119
|
-
this.robotStatus = 'leaving';
|
|
120
|
-
this.avaterStatus = 'normal';
|
|
121
|
-
this.stop();
|
|
122
|
-
}
|
|
176
|
+
const p = this.$refs.audioPlayer;
|
|
177
|
+
if (!p) return;
|
|
178
|
+
p.pause();
|
|
179
|
+
p.currentTime = 0;
|
|
123
180
|
}
|
|
124
181
|
}
|
|
125
|
-
}
|
|
182
|
+
};
|
|
@@ -92,6 +92,7 @@ export default {
|
|
|
92
92
|
// 记录耗时
|
|
93
93
|
const duration = Date.now() - startTime;
|
|
94
94
|
if (this.currentMessage) {
|
|
95
|
+
this.messageLoading = false;
|
|
95
96
|
this.currentMessage.time = (duration / 1000).toFixed(2);
|
|
96
97
|
}
|
|
97
98
|
|
|
@@ -101,10 +102,9 @@ export default {
|
|
|
101
102
|
} catch (error) {
|
|
102
103
|
console.error('发送消息失败:', error);
|
|
103
104
|
if (this.currentMessage) {
|
|
105
|
+
this.messageLoading = false;
|
|
104
106
|
this.currentMessage.content = '抱歉,发生了错误,请重试。';
|
|
105
107
|
}
|
|
106
|
-
} finally {
|
|
107
|
-
this.messageLoading = false;
|
|
108
108
|
}
|
|
109
109
|
},
|
|
110
110
|
|
|
@@ -12,11 +12,7 @@ export default {
|
|
|
12
12
|
methods: {
|
|
13
13
|
initWebSocket() {
|
|
14
14
|
try {
|
|
15
|
-
// this.ws = new WebSocket('ws://10.2.233.41:9999');
|
|
16
|
-
// 测试
|
|
17
|
-
// console.log('WS_URL:', WS_URL)
|
|
18
15
|
this.ws = new WebSocket(WS_URL);
|
|
19
|
-
// this.ws = new WebSocket('ws://192.168.8.87/audio/ws/');
|
|
20
16
|
this.ws.binaryType = 'arraybuffer';
|
|
21
17
|
|
|
22
18
|
this.ws.onopen = async () => {
|
|
@@ -74,7 +70,6 @@ export default {
|
|
|
74
70
|
console.error('WebSocket连接失败:', error);
|
|
75
71
|
}
|
|
76
72
|
},
|
|
77
|
-
|
|
78
73
|
handleWebSocketMessage(data) {
|
|
79
74
|
if (data.type === 'detection') {
|
|
80
75
|
console.log('检测到唤醒词');
|
|
@@ -95,13 +90,15 @@ export default {
|
|
|
95
90
|
console.log('状态: 处理中');
|
|
96
91
|
|
|
97
92
|
// 性能检测终点
|
|
98
|
-
if (this.startTime) {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
93
|
+
// if (this.startTime) {
|
|
94
|
+
// const latency = Date.now() - this.startTime;
|
|
95
|
+
// console.log(`[Latency] 完整命令处理耗时: ${latency}ms`); // 记录端到端延迟
|
|
96
|
+
// this.startTime = null;
|
|
97
|
+
// }
|
|
103
98
|
|
|
104
99
|
this.analyzeAudioCommand(data.category);
|
|
100
|
+
} else if (data.type === 'voice') {
|
|
101
|
+
this.analyzeVoiceCommand(data.category);
|
|
105
102
|
} else {
|
|
106
103
|
console.log('状态: 其他');
|
|
107
104
|
this.avaterStatus = 'normal';
|