byt-lingxiao-ai 0.3.28 → 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/webSocketMixin.js +7 -10
- package/dist/index.common.js +542 -133
- package/dist/index.common.js.map +1 -1
- package/dist/index.css +1 -1
- package/dist/index.umd.js +542 -133
- 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
|
+
};
|
|
@@ -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';
|