From da7f7736c8ddc4f598ade7653a4d0259637e04ba Mon Sep 17 00:00:00 2001
From: wujingjing <gersonwu@qq.com>
Date: 星期四, 03 四月 2025 15:44:17 +0800
Subject: [PATCH] 语音输入

---
 src/components/chat/components/playBar/PlayBar.vue       |   25 ++++++
 src/components/chat/components/playBar/hook/useSpeech.ts |  169 ++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 194 insertions(+), 0 deletions(-)

diff --git a/src/components/chat/components/playBar/PlayBar.vue b/src/components/chat/components/playBar/PlayBar.vue
index 94e7d97..bf7c726 100644
--- a/src/components/chat/components/playBar/PlayBar.vue
+++ b/src/components/chat/components/playBar/PlayBar.vue
@@ -146,6 +146,18 @@
 								</div>
 							</el-tooltip>
 
+							<el-tooltip v-if="isSupportSpeech" placement="top" content="璇煶杈撳叆">
+								<div class="cursor-pointer size-[24px] relative !z-10 rounded flex-center hover:bg-[#f2f2f2]" @click="speechClick">
+									<div v-if="recordState.isRecording"  class="cursor-pointer flex items-center space-x-[1px]">
+										<div class="w-[2px] h-[6px] bg-[#0284ff] animate-[soundWave_1.5s_ease-in-out_infinite]"></div>
+										<div class="w-[2px] h-[9px] bg-[#0284ff] animate-[soundWave_1.5s_ease-in-out_infinite_0.1s]"></div>
+										<div class="w-[2px] h-[12px] bg-[#0284ff] animate-[soundWave_1.5s_ease-in-out_infinite_0.2s]"></div>
+										<div class="w-[2px] h-[9px] bg-[#0284ff] animate-[soundWave_1.5s_ease-in-out_infinite_0.3s]"></div>
+										<div class="w-[2px] h-[6px] bg-[#0284ff] animate-[soundWave_1.5s_ease-in-out_infinite_0.4s]"></div>
+									</div>
+									<span v-else class="ywifont ywicon-maikefeng !text-[20px]"></span>
+								</div>
+							</el-tooltip>
 							<el-tooltip placement="top" content="鍋滄鐢熸垚" v-if="isTalking">
 								<div class="cursor-pointer !ml-0 size-[38px] bg-[#1d86ff] relative !z-10 rounded-full flex-center" link>
 									<div
@@ -207,6 +219,7 @@
 import { newChatRoomClick, sidebarIsShow, toggleSidebar } from '/@/stores/chatRoom';
 
 import MetricValuesPreview from './metricValues/MetricValuesPreview.vue';
+import { useSpeech } from './hook/useSpeech';
 
 const emits = defineEmits(['sendClick', 'stopGenClick']);
 const props = defineProps({
@@ -246,6 +259,18 @@
 	commonPhraseRef.value.updatePhrase();
 };
 
+const speechClick = () => {
+	if (recordState.isRecording) {
+		stopRecording();
+	} else {
+		startRecording();
+	}
+};
+
+const { isSupportSpeech, startRecording, stopRecording, recordState } = useSpeech({
+	inputText: inputValue,
+});
+
 //#region ====================== 甯哥敤璇姛鑳� ======================
 const commonPhraseRef = useCompRef(CommonPhrases);
 // 甯哥敤璇姛鑳界偣鍑�
diff --git a/src/components/chat/components/playBar/hook/useSpeech.ts b/src/components/chat/components/playBar/hook/useSpeech.ts
new file mode 100644
index 0000000..bb6e02c
--- /dev/null
+++ b/src/components/chat/components/playBar/hook/useSpeech.ts
@@ -0,0 +1,169 @@
+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,
+		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();
+			}
+		}
+	};
+
+	return {
+		isSupportSpeech,
+		startRecording,
+		stopRecording,
+		recordState,
+	};
+};

--
Gitblit v1.9.3