From 08c5b22d4a0a3b29ed9356ea0b6e719cc488e6f5 Mon Sep 17 00:00:00 2001 From: john Date: Mon, 29 Jun 2026 12:34:34 +0800 Subject: [PATCH] =?UTF-8?q?fix(voice):=20=E6=B5=8F=E8=A7=88=E5=99=A8?= =?UTF-8?q?=E8=AF=AD=E9=9F=B3=E9=BB=98=E8=AE=A4=E8=B5=B0=E6=9C=8D=E5=8A=A1?= =?UTF-8?q?=E7=AB=AF=20ASR?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Web Speech 依赖 Google,国内浏览器常无法转文字;支持麦克风时改为录音后走自有 ASR,并补全网络异常时的降级匹配。 Co-authored-by: Cursor --- src/hooks/useVoiceSession.ts | 10 ++++------ src/voice/capabilities.ts | 18 ++++++++++++++---- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/hooks/useVoiceSession.ts b/src/hooks/useVoiceSession.ts index a5b1afe..8fae289 100644 --- a/src/hooks/useVoiceSession.ts +++ b/src/hooks/useVoiceSession.ts @@ -1,10 +1,10 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import { transcribeOneShot } from '../voice/asrTransport'; import { mapMicError } from '../voice/audioAnalyser'; -import { shouldFallbackToServerAsr, shouldPreferServerAsr } from '../voice/capabilities'; +import { shouldFallbackToServerAsr, shouldUseLiveSpeech } from '../voice/capabilities'; import { MicCapture } from '../voice/micCapture'; import { waitForMicRelease } from '../voice/micSession'; -import { createLiveSpeechRecognition, isSpeechRecognitionSupported } from '../voice/speechRecognition'; +import { createLiveSpeechRecognition } from '../voice/speechRecognition'; import type { VoiceSessionPhase } from '../voice/types'; const MIN_RECORD_MS = 500; @@ -19,9 +19,7 @@ export function useVoiceSession({ const [phase, setPhase] = useState('idle'); const [text, setText] = useState(''); const [analyser, setAnalyser] = useState(null); - const [liveRecognition, setLiveRecognition] = useState( - isSpeechRecognitionSupported() && !shouldPreferServerAsr(), - ); + const [liveRecognition, setLiveRecognition] = useState(shouldUseLiveSpeech()); const committedRef = useRef(''); const interimRef = useRef(''); @@ -155,7 +153,7 @@ export function useVoiceSession({ if (sessionRef.current !== sessionId) return; startedAtRef.current = Date.now(); - const useLiveSpeech = isSpeechRecognitionSupported() && !shouldPreferServerAsr(); + const useLiveSpeech = shouldUseLiveSpeech(); if (useLiveSpeech) { const recognition = createLiveSpeechRecognition({ diff --git a/src/voice/capabilities.ts b/src/voice/capabilities.ts index 97e787d..cbf1955 100644 --- a/src/voice/capabilities.ts +++ b/src/voice/capabilities.ts @@ -2,16 +2,26 @@ import { isWechatBrowser } from '../utils/wechat'; import { MicCapture } from './micCapture'; import { isSpeechRecognitionSupported } from './speechRecognition'; -/** 微信内置浏览器 Web Speech 不可用,直接走服务端 ASR。 */ +/** + * 优先服务端 ASR(录音后识别): + * - 微信内 Web Speech 不可用; + * - 普通浏览器若支持麦克风,也走自有 ASR(Web Speech 依赖 Google,国内常无结果)。 + */ export function shouldPreferServerAsr(): boolean { - return isWechatBrowser(); + if (isWechatBrowser()) return true; + return MicCapture.isSupported(); } -/** Web Speech 出现这些错误时可改用服务端 ASR(录音 + 完成识别)。 */ +/** 仍尝试 Web Speech 时,出现这些错误可降级为服务端 ASR。 */ export function shouldFallbackToServerAsr(errorMessage: string): boolean { if (shouldPreferServerAsr()) return false; if (!MicCapture.isSupported()) return false; - return /network|service-not-allowed|语音识别失败|无法启动语音识别/i.test(errorMessage); + return /network|网络异常|service-not-allowed|语音识别失败|无法启动语音识别/i.test(errorMessage); +} + +/** 是否使用浏览器 Web Speech 实时听写(仅在不走服务端 ASR 且 API 可用时)。 */ +export function shouldUseLiveSpeech(): boolean { + return isSpeechRecognitionSupported() && !shouldPreferServerAsr(); } /** Whether voice input may work in this browser (mic and/or live speech). */