import { ref, onUnmounted } from 'vue' const MIN_HOLD_MS = 280 /** * 语音转文字(长按说话):微信小程序 WechatSI;H5 Web Speech API。 * * @param {{ onResult?: (text: string) => void, showToast?: (msg: string) => void }} options */ export function useSpeechToText({ onResult, showToast } = {}) { const sttListening = ref(false) const sttHolding = ref(false) const sttSupported = ref(false) let wxRecordManager = null let h5Recognition = null let holdStartTs = 0 let holdSessionId = 0 let startRequested = false let recordAuthorized = null function notify(msg) { if (!msg) return if (typeof showToast === 'function') { showToast(msg) return } uni.showToast({ title: msg, icon: 'none', duration: 2000 }) } function emitResult(text) { const t = String(text || '').trim() const heldMs = Date.now() - holdStartTs if (!t) { if (heldMs < MIN_HOLD_MS) return notify('没听清,请再试一次') return } if (typeof onResult === 'function') { onResult(t) } } function stopListening() { // #ifdef MP-WEIXIN if (wxRecordManager && (sttListening.value || startRequested)) { try { wxRecordManager.stop() } catch (e) {} } // #endif // #ifdef H5 if (h5Recognition && sttListening.value) { try { h5Recognition.stop() } catch (e) {} } // #endif startRequested = false sttListening.value = false } function ensureRecordAuth(onGranted) { // #ifdef MP-WEIXIN if (recordAuthorized === true) { onGranted() return } uni.authorize({ scope: 'scope.record', success() { recordAuthorized = true onGranted() }, fail() { recordAuthorized = false sttHolding.value = false uni.showModal({ title: '需要麦克风权限', content: '请在设置中允许使用麦克风,以便长按说话', confirmText: '去设置', success(res) { if (res.confirm) { uni.openSetting({}) } } }) } }) // #endif // #ifndef MP-WEIXIN onGranted() // #endif } function startWechatRecord(session) { // #ifdef MP-WEIXIN if (!wxRecordManager || session !== holdSessionId || !sttHolding.value) { return } try { startRequested = true wxRecordManager.start({ duration: 60000, lang: 'zh_CN' }) } catch (e) { startRequested = false sttHolding.value = false notify('无法开始录音') } // #endif } function startH5Record() { // #ifdef H5 if (!h5Recognition) { notify('当前浏览器不支持语音输入') sttHolding.value = false return } try { startRequested = true h5Recognition.start() sttListening.value = true } catch (e) { startRequested = false sttHolding.value = false notify('无法开始语音识别,请检查麦克风权限') } // #endif } /** 长按开始:手指按下 */ function beginHoldSpeech() { if (!sttSupported.value) { notify('当前环境不支持语音输入') return } if (sttHolding.value) return const session = ++holdSessionId holdStartTs = Date.now() sttHolding.value = true // #ifdef MP-WEIXIN ensureRecordAuth(() => startWechatRecord(session)) // #endif // #ifdef H5 startH5Record() // #endif // #ifndef MP-WEIXIN // #ifndef H5 sttHolding.value = false notify('当前端暂不支持语音输入') // #endif // #endif } /** 松手结束:手指抬起 */ function endHoldSpeech() { if (!sttHolding.value && !sttListening.value && !startRequested) { return } sttHolding.value = false if (sttListening.value || startRequested) { stopListening() return } holdSessionId += 1 } // #ifdef MP-WEIXIN try { // eslint-disable-next-line no-undef const plugin = requirePlugin('WechatSI') wxRecordManager = plugin.getRecordRecognitionManager() sttSupported.value = !!wxRecordManager wxRecordManager.onStart = () => { if (!sttHolding.value) { try { wxRecordManager.stop() } catch (e) {} return } startRequested = false sttListening.value = true } wxRecordManager.onStop = (res) => { sttListening.value = false startRequested = false emitResult(res?.result) } wxRecordManager.onError = (res) => { sttListening.value = false startRequested = false sttHolding.value = false notify(res?.msg || '语音识别失败') } } catch (e) { console.warn('WechatSI record recognition not available', e) } // #endif // #ifdef H5 try { if (typeof window !== 'undefined') { const SR = window.SpeechRecognition || window.webkitSpeechRecognition if (SR) { h5Recognition = new SR() h5Recognition.lang = 'zh-CN' h5Recognition.interimResults = false h5Recognition.continuous = false h5Recognition.maxAlternatives = 1 sttSupported.value = true h5Recognition.onresult = (event) => { const item = event.results?.[0]?.[0] emitResult(item?.transcript || '') } h5Recognition.onend = () => { sttListening.value = false startRequested = false sttHolding.value = false } h5Recognition.onerror = (event) => { sttListening.value = false startRequested = false sttHolding.value = false if (event?.error === 'not-allowed') { notify('请允许浏览器使用麦克风') } else if (event?.error !== 'aborted') { notify('语音识别失败') } } } } } catch (e) { console.warn('H5 SpeechRecognition not available', e) } // #endif onUnmounted(() => { sttHolding.value = false holdSessionId += 1 stopListening() }) return { sttListening, sttHolding, sttSupported, beginHoldSpeech, endHoldSpeech, stopSpeechToText: endHoldSpeech } }