import axios from 'axios';
|
import { ElMessage } from 'element-plus';
|
import type { Ref } from 'vue';
|
import { reactive, ref } from 'vue';
|
|
type UseSpeechProps = {
|
inputText: Ref<string>;
|
};
|
export const useSpeech = (props: UseSpeechProps) => {
|
const { inputText } = props;
|
const isSupportSpeech = ref(!!navigator.mediaDevices);
|
const baiduSpeechConfig = {
|
format: 'wav',
|
rate: 16000,
|
channel: 1,
|
cuid: 'jVNmJvBApXOwDVb5aLKETdTjMK8bm3nI',
|
token: '',
|
dev_pid: 80001,
|
ak: 'aV5EwAw2hb80x8kVel8NUKfF',
|
sk: 'TKVhvcIa4rNskak0lfhPLINlxZWaCIUM',
|
};
|
|
const recordState = reactive({
|
isRecording: false,
|
audioContext: null,
|
mediaRecorder: null as MediaRecorder | null,
|
audioChunks: [],
|
});
|
|
const getAccessToken = async () => {
|
try {
|
const response = await axios.post(
|
`https://wi.beng35.com/api/baidu-token/oauth/2.0/token?grant_type=client_credentials&client_id=${baiduSpeechConfig.ak}&client_secret=${baiduSpeechConfig.sk}`
|
);
|
baiduSpeechConfig.token = response.data.access_token;
|
return response.data.access_token;
|
} catch (error) {
|
console.error('获取 access token 失败:', error);
|
ElMessage.error('获取语音识别授权失败');
|
throw error;
|
}
|
};
|
const startRecording = async () => {
|
try {
|
// 确保有 access token
|
if (!baiduSpeechConfig.token) {
|
await getAccessToken();
|
}
|
|
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
recordState.audioContext = new AudioContext({
|
sampleRate: 16000,
|
latencyHint: 'interactive',
|
});
|
|
const source = recordState.audioContext.createMediaStreamSource(stream);
|
const processor = recordState.audioContext.createScriptProcessor(4096, 1, 1);
|
|
// 存储原始音频数据
|
const audioData = [];
|
|
processor.onaudioprocess = (e) => {
|
const inputData = e.inputBuffer.getChannelData(0);
|
// 将 Float32Array 转换为 Int16Array
|
const int16Data = new Int16Array(inputData.length);
|
for (let i = 0; i < inputData.length; i++) {
|
int16Data[i] = inputData[i] * 32767;
|
}
|
audioData.push(int16Data);
|
};
|
|
recordState.mediaRecorder = new MediaRecorder(stream);
|
recordState.audioChunks = [];
|
|
recordState.mediaRecorder.ondataavailable = (event) => {
|
if (event.data.size > 0) {
|
recordState.audioChunks.push(event.data);
|
}
|
};
|
|
recordState.mediaRecorder.onstop = async () => {
|
// 合并所有音频数据
|
const totalLength = audioData.reduce((acc, curr) => acc + curr.length, 0);
|
const mergedData = new Int16Array(totalLength);
|
let offset = 0;
|
audioData.forEach((data) => {
|
mergedData.set(data, offset);
|
offset += data.length;
|
});
|
|
// 将 Int16Array 转换为 base64
|
const buffer = mergedData.buffer;
|
|
let base64Data = '';
|
let len = 0;
|
// 分批处理大型数据
|
// 不然 String.fromCharCode 和展开运算符... 会爆栈
|
const chunkSize = 0.1 * 1024 * 1024; // 0.5MB chunks
|
const uint8Array = new Uint8Array(buffer);
|
const chunks = [];
|
|
for (let i = 0; i < uint8Array.length; i += chunkSize) {
|
const chunk = uint8Array.slice(i, i + chunkSize);
|
chunks.push(String.fromCharCode.apply(null, chunk));
|
}
|
|
base64Data = btoa(chunks.join(''));
|
len = mergedData.length * 2;
|
|
try {
|
// 调用百度语音识别API
|
const response = await fetch('https://wi.beng35.com/api/baidu-speech/pro_api', {
|
method: 'POST',
|
headers: {
|
'Content-Type': 'application/json',
|
},
|
body: JSON.stringify({
|
format: 'pcm',
|
rate: 16000,
|
channel: 1,
|
cuid: baiduSpeechConfig.cuid,
|
dev_pid: 80001, // 普通话识别
|
speech: base64Data,
|
len: len, // PCM 16位,每个采样点2字节
|
token: baiduSpeechConfig.token,
|
}),
|
});
|
|
const result = await response.json();
|
|
if (result.err_no === 0 && result.result && result.result.length > 0) {
|
inputText.value = result.result[0];
|
// showToast('语音识别成功');
|
} else {
|
ElMessage.error('语音识别失败:' + result.err_msg);
|
}
|
} catch (error) {
|
console.error('语音识别请求失败:', error);
|
ElMessage.error('语音识别请求失败');
|
}
|
};
|
|
recordState.mediaRecorder.start();
|
recordState.isRecording = true;
|
source.connect(processor);
|
processor.connect(recordState.audioContext.destination);
|
} catch (error) {
|
console.error('录音失败:', error);
|
ElMessage.error('录音失败,请检查麦克风权限');
|
}
|
};
|
|
const stopRecording = () => {
|
if (recordState.mediaRecorder && recordState.isRecording) {
|
recordState.mediaRecorder.stop();
|
recordState.isRecording = false;
|
if (recordState.audioContext) {
|
recordState.audioContext.close();
|
}
|
|
}
|
};
|
|
const cancelRecording = () => {
|
if (recordState.mediaRecorder && recordState.isRecording) {
|
recordState.mediaRecorder.resume();
|
recordState.isRecording = false;
|
if (recordState.audioContext) {
|
recordState.audioContext.close();
|
}
|
}
|
};
|
|
return {
|
isSupportSpeech,
|
startRecording,
|
stopRecording,
|
recordState,
|
cancelRecording,
|
};
|
};
|