From 11c1a3401d83ba757172aed2fdac160e1ade3de0 Mon Sep 17 00:00:00 2001 From: zoujing Date: Thu, 14 May 2026 17:42:49 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=9B=86=E6=88=90=E8=AF=AD=E9=9F=B3?= =?UTF-8?q?=E8=AF=86=E5=88=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/constant/speech.js | 6 + src/manifest.json | 8 +- src/pages/ChatMain/ChatInputArea/index.vue | 138 +++++- .../yao-asdRealSpeech/changelog.md | 9 + .../yao-asdRealSpeech/yao-asdRealSpeech.vue | 402 ++++++++++++++++++ .../yao-asdRealSpeech/package.json | 97 +++++ src/uni_modules/yao-asdRealSpeech/readme.md | 80 ++++ 7 files changed, 735 insertions(+), 5 deletions(-) create mode 100644 src/constant/speech.js create mode 100644 src/uni_modules/yao-asdRealSpeech/changelog.md create mode 100644 src/uni_modules/yao-asdRealSpeech/components/yao-asdRealSpeech/yao-asdRealSpeech.vue create mode 100644 src/uni_modules/yao-asdRealSpeech/package.json create mode 100644 src/uni_modules/yao-asdRealSpeech/readme.md diff --git a/src/constant/speech.js b/src/constant/speech.js new file mode 100644 index 0000000..c8933e0 --- /dev/null +++ b/src/constant/speech.js @@ -0,0 +1,6 @@ +// App 端 yao-asdRealSpeech 使用的阿里云 DashScope 实时语音识别配置。 +// 将 apikey 填成实际的 DashScope API Key 后,App 端语音识别即可发起连接。 +export const appSpeechRecognitionOptions = { + apikey: "SnoHqdtJ832riRg4", + language_hints: ["zh"], +}; diff --git a/src/manifest.json b/src/manifest.json index e8a9342..3757d8b 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -44,10 +44,16 @@ "", "", "", + "", + "", "" ] }, - "ios" : {}, + "ios" : { + "privacyDescription" : { + "NSMicrophoneUsageDescription" : "用于语音输入时录制并识别您的语音内容" + } + }, "sdkConfigs" : { "oauth" : {} } diff --git a/src/pages/ChatMain/ChatInputArea/index.vue b/src/pages/ChatMain/ChatInputArea/index.vue index 10afa9b..af30c13 100644 --- a/src/pages/ChatMain/ChatInputArea/index.vue +++ b/src/pages/ChatMain/ChatInputArea/index.vue @@ -74,6 +74,15 @@ + + + + 内容由AI大模型生成,请仔细鉴别 @@ -84,15 +93,25 @@ import { ref, computed, watch, nextTick, onMounted, defineExpose, onUnmounted } from "vue"; import RecordingWaveBtn from "@/components/Speech/RecordingWaveBtn.vue"; import { getCurrentConfig } from "@/constant/base"; +import { appSpeechRecognitionOptions } from "@/constant/speech"; let manager = null; +let speechProvider = ""; const isSpeechRecognitionSupported = ref(false); +const appSpeechOptions = appSpeechRecognitionOptions; // WechatSI 是微信小程序插件,App 原生基座没有 requirePlugin。 // #ifdef MP-WEIXIN const plugin = requirePlugin("WechatSI"); manager = plugin.getRecordRecognitionManager(); isSpeechRecognitionSupported.value = !!manager; +speechProvider = manager ? "wechat" : ""; +// #endif + +// App 端使用 yao-asdRealSpeech;apikey 请在 src/constant/speech.js 中配置。 +// #ifdef APP-PLUS +isSpeechRecognitionSupported.value = true; +speechProvider = "app"; // #endif const props = defineProps({ @@ -112,6 +131,7 @@ const emit = defineEmits([ const textareaRef = ref(null); const recordingWaveBtnRef = ref(null); +const appSpeechRef = ref(null); const placeholder = computed(() => { const config = getCurrentConfig(); return `快告诉${config.name}您在想什么~`; @@ -122,7 +142,10 @@ const keyboardHeight = ref(0); const isVoiceMode = ref(false); const visibleWaveBtn = ref(false); const isRecording = ref(false); +const appRecognizedText = ref(""); let watchDogTimer = null; +let appStopFallbackTimer = null; +let hasSentAppRecognition = false; const resetUI = () => { isRecording.value = false; @@ -148,6 +171,13 @@ const startWatchDog = (timeout = 10000) => { }, timeout); }; +const clearAppStopFallback = () => { + if (appStopFallbackTimer) { + clearTimeout(appStopFallbackTimer); + appStopFallbackTimer = null; + } +}; + // 保持和父组件同步 watch( () => props.modelValue, @@ -172,9 +202,38 @@ const toggleVoiceMode = () => { // 处理语音按钮长按开始 const handleVoiceTouchStart = () => { - if (!manager) return; + if (!isSpeechRecognitionSupported.value) return; + try { - manager.start({ lang: "zh_CN" }); + if (speechProvider === "wechat") { + if (!manager) return; + manager.start({ lang: "zh_CN" }); + } else if (speechProvider === "app") { + if (!appSpeechOptions.apikey) { + uni.showToast({ + title: "请先配置语音识别API Key", + icon: "none", + }); + return; + } + + appRecognizedText.value = ""; + hasSentAppRecognition = false; + + const appSpeech = appSpeechRef.value; + if (!appSpeech || typeof appSpeech.start !== "function") { + uni.showToast({ + title: "语音组件未初始化", + icon: "none", + }); + return; + } + + appSpeech.start(); + } else { + return; + } + isRecording.value = true; visibleWaveBtn.value = true; @@ -194,7 +253,7 @@ const handleVoiceTouchStart = () => { // 处理语音按钮长按结束 const handleVoiceTouchEnd = () => { - if (!manager) { + if (!isSpeechRecognitionSupported.value) { resetUI(); return; } @@ -213,7 +272,16 @@ const handleVoiceTouchEnd = () => { } try { - manager.stop(); + if (speechProvider === "wechat") { + manager?.stop?.(); + } else if (speechProvider === "app") { + appSpeechRef.value?.stop?.(); + clearAppStopFallback(); + appStopFallbackTimer = setTimeout(() => { + appStopFallbackTimer = null; + sendAppRecognizedText(); + }, 600); + } } catch (err) { console.error("record stop error:", err); } finally { @@ -255,6 +323,60 @@ const initRecord = () => { }; }; +const getAppSpeechText = (res) => { + return ( + res?.sentence?.text || + res?.text || + res?.result || + res?.payload?.output?.sentence?.text || + "" + ); +}; + +const sendAppRecognizedText = () => { + if (hasSentAppRecognition) return; + + const text = appRecognizedText.value.trim(); + if (!text) { + console.log("没有说话"); + return; + } + + hasSentAppRecognition = true; + clearAppStopFallback(); + inputMessage.value = text; + emit("send", text); + appRecognizedText.value = ""; +}; + +const handleAppSpeechResult = (res) => { + const text = getAppSpeechText(res); + if (!text) return; + + appRecognizedText.value = text; + inputMessage.value = text; +}; + +const handleAppSpeechChange = (msg) => { + if (!msg || !msg.status) return; + + if (msg.status === "START") { + isRecording.value = true; + return; + } + + if (msg.status === "STOP") { + resetUI(); + sendAppRecognizedText(); + return; + } + + if (msg.status === "ERROR") { + console.error("app speech recognition error", msg.msg); + resetUI(); + } +}; + // 监听键盘高度变化 onMounted(() => { // 监听键盘弹起 @@ -281,6 +403,14 @@ onUnmounted(() => { manager.onStop = null; manager.onError = null; } + if (appSpeechRef.value) { + try { + appSpeechRef.value.stop && appSpeechRef.value.stop(); + } catch (e) { + // ignore + } + } + clearAppStopFallback(); resetUI(); }); diff --git a/src/uni_modules/yao-asdRealSpeech/changelog.md b/src/uni_modules/yao-asdRealSpeech/changelog.md new file mode 100644 index 0000000..e9be3ed --- /dev/null +++ b/src/uni_modules/yao-asdRealSpeech/changelog.md @@ -0,0 +1,9 @@ +## 1.0.3(2026-04-20) +修复录音时间长合成音频文件失败问题 +## 1.0.2(2025-12-06) +. +## 1.0.1(2025-12-05) +停止录音后保存音频 +## 1.0.0(2025-08-17) +# yao-asdRealtimeSpeech +阿里云Paraformer实时语音识别 diff --git a/src/uni_modules/yao-asdRealSpeech/components/yao-asdRealSpeech/yao-asdRealSpeech.vue b/src/uni_modules/yao-asdRealSpeech/components/yao-asdRealSpeech/yao-asdRealSpeech.vue new file mode 100644 index 0000000..a4d8f68 --- /dev/null +++ b/src/uni_modules/yao-asdRealSpeech/components/yao-asdRealSpeech/yao-asdRealSpeech.vue @@ -0,0 +1,402 @@ + + + + + + + diff --git a/src/uni_modules/yao-asdRealSpeech/package.json b/src/uni_modules/yao-asdRealSpeech/package.json new file mode 100644 index 0000000..7e8da99 --- /dev/null +++ b/src/uni_modules/yao-asdRealSpeech/package.json @@ -0,0 +1,97 @@ +{ + "id": "yao-asdRealSpeech", + "displayName": "阿里云实时语音识别", + "version": "1.0.3", + "description": "阿里云实时语音识别 支持:Android,ios", + "keywords": [ + "实时语音识别,语音识别" + ], + "repository": "", + "engines": { + "HBuilderX": "^3.1.0", + "uni-app": "^4.07", + "uni-app-x": "" + }, + "dcloudext": { + "type": "component-vue", + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "3371387322@qq.com" + }, + "declaration": { + "ads": "无", + "data": "插件不采集任何数据", + "permissions": "需要麦克风权限" + }, + "npmurl": "", + "darkmode": "x", + "i18n": "x", + "widescreen": "x" + }, + "uni_modules": { + "dependencies": [], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "x", + "aliyun": "x", + "alipay": "x" + }, + "client": { + "uni-app": { + "vue": { + "vue2": "√", + "vue3": "√" + }, + "web": { + "safari": "x", + "chrome": "x" + }, + "app": { + "vue": "√", + "nvue": "x", + "android": "√", + "ios": "√", + "harmony": "x" + }, + "mp": { + "weixin": "x", + "alipay": "x", + "toutiao": "x", + "baidu": "x", + "kuaishou": "x", + "jd": "x", + "harmony": "x", + "qq": "x", + "lark": "x" + }, + "quickapp": { + "huawei": "x", + "union": "x" + } + }, + "uni-app-x": { + "web": { + "safari": "-", + "chrome": "-" + }, + "app": { + "android": "-", + "ios": "-", + "harmony": "-" + }, + "mp": { + "weixin": "-" + } + } + } + } + } +} \ No newline at end of file diff --git a/src/uni_modules/yao-asdRealSpeech/readme.md b/src/uni_modules/yao-asdRealSpeech/readme.md new file mode 100644 index 0000000..74b23a2 --- /dev/null +++ b/src/uni_modules/yao-asdRealSpeech/readme.md @@ -0,0 +1,80 @@ +# yao-asdRealtimeSpeech + + +# ##配置 +需要将模块下uni_modules/yao-asdRealSpeech的dist复制到static目录下面 +或者 +将uni_modules/yao-asdRealSpeech/dist目录的配置文件放到static/dist目录下面 + + + +# 代码示例 + +```javascript + + + + +``` \ No newline at end of file