From fc73d5ff392157fe2130b8e056ac6434af8f27ed Mon Sep 17 00:00:00 2001 From: chengyongru <2755839590@qq.com> Date: Wed, 29 Jul 2026 23:59:59 +0800 Subject: [PATCH] fix(webui): avoid false microphone silence errors --- webui/src/hooks/useVoiceRecorder.ts | 50 +----------------------- webui/src/tests/thread-composer.test.tsx | 10 +++-- 2 files changed, 8 insertions(+), 52 deletions(-) diff --git a/webui/src/hooks/useVoiceRecorder.ts b/webui/src/hooks/useVoiceRecorder.ts index 4b0da8504..f16538eac 100644 --- a/webui/src/hooks/useVoiceRecorder.ts +++ b/webui/src/hooks/useVoiceRecorder.ts @@ -8,7 +8,6 @@ import { const VOICE_RECORDING_MAX_MS = 120_000; const VOICE_RECORDING_MIN_MS = 650; -const VOICE_NO_INPUT_HINT_MS = 1_100; const VOICE_HOLD_START_MS = 140; const VOICE_WAVEFORM_BAR_COUNT = 64; const VOICE_WAVEFORM_SILENT_HEIGHT = 3; @@ -30,7 +29,6 @@ export type VoiceRecorderState = "idle" | "recording" | "transcribing"; export type VoiceRecorderErrorKey = | "failed" | "noDevice" - | "noInput" | "notConfigured" | "permission" | "tooLong" @@ -61,7 +59,6 @@ export function useVoiceRecorder({ const audioRef = useRef(null); const startedAtRef = useRef(0); const maxTimerRef = useRef | null>(null); - const inputHintTimerRef = useRef | null>(null); const holdTimerRef = useRef | null>(null); const holdActiveRef = useRef(false); const startPendingRef = useRef(false); @@ -69,15 +66,10 @@ export function useVoiceRecorder({ const suppressClickRef = useRef(false); const suppressClickTimerRef = useRef | null>(null); const shortcutActiveRef = useRef(false); - const levelObservedRef = useRef(false); - const peakLevelRef = useRef(0); - const levelReliableRef = useRef(false); - const noInputHintVisibleRef = useRef(false); const [state, setState] = useState("idle"); const [elapsedMs, setElapsedMs] = useState(0); const [levels, setLevels] = useState(VOICE_WAVEFORM_IDLE_LEVELS); - const clearInputHintTimer = useCallback(() => clearTimer(inputHintTimerRef), []); const clearSuppressClickTimer = useCallback(() => clearTimer(suppressClickTimerRef), []); const suppressNextClick = useCallback(() => { @@ -128,16 +120,6 @@ export function useVoiceRecorder({ } current.analyser.getByteTimeDomainData(current.data); const level = voiceLevelFromSamples(current.data); - levelReliableRef.current = true; - levelObservedRef.current = true; - peakLevelRef.current = Math.max(peakLevelRef.current, level); - if (level >= VOICE_MIN_LEVEL) { - clearInputHintTimer(); - if (noInputHintVisibleRef.current) { - noInputHintVisibleRef.current = false; - onClearError(); - } - } setLevels((currentLevels) => [ ...currentLevels.slice(1), waveformHeightFromLevel(level), @@ -150,11 +132,10 @@ export function useVoiceRecorder({ } catch { stopWaveform(); } - }, [clearInputHintTimer, onClearError, stopWaveform]); + }, [stopWaveform]); const cleanupRecording = useCallback(() => { clearTimer(holdTimerRef); - clearInputHintTimer(); clearTimer(maxTimerRef); stopWaveform(); streamRef.current?.getTracks().forEach((track) => track.stop()); @@ -162,8 +143,7 @@ export function useVoiceRecorder({ mediaRecorderRef.current = null; startPendingRef.current = false; shortcutActiveRef.current = false; - noInputHintVisibleRef.current = false; - }, [clearInputHintTimer, stopWaveform]); + }, [stopWaveform]); const stopRecording = useCallback(() => { const recorder = mediaRecorderRef.current; @@ -197,10 +177,6 @@ export function useVoiceRecorder({ streamRef.current = stream; mediaRecorderRef.current = recorder; startedAtRef.current = Date.now(); - levelObservedRef.current = false; - peakLevelRef.current = 0; - levelReliableRef.current = false; - noInputHintVisibleRef.current = false; setElapsedMs(0); startWaveform(stream); recorder.ondataavailable = (event) => { @@ -210,10 +186,6 @@ export function useVoiceRecorder({ const chunks = chunksRef.current.splice(0); const durationMs = Math.max(0, Date.now() - startedAtRef.current); const mimeType = recorder.mimeType || "audio/webm"; - const hasMeasuredSilence = - levelReliableRef.current - && levelObservedRef.current - && peakLevelRef.current < VOICE_MIN_LEVEL; cleanupRecording(); if (chunks.length === 0) { setState("idle"); @@ -224,11 +196,6 @@ export function useVoiceRecorder({ onError("tooShort"); return; } - if (hasMeasuredSilence) { - setState("idle"); - onError("noInput"); - return; - } setState("transcribing"); const blob = new Blob(chunks, { type: mimeType }); const audioPromise = wantsWav ? convertBlobToWav(blob) : blobToDataUrl(blob); @@ -242,19 +209,6 @@ export function useVoiceRecorder({ setState("recording"); onClearError(); maxTimerRef.current = setTimeout(stopRecording, VOICE_RECORDING_MAX_MS); - inputHintTimerRef.current = setTimeout(() => { - const recording = mediaRecorderRef.current?.state === "recording"; - if ( - !recording - || !levelReliableRef.current - || !levelObservedRef.current - || peakLevelRef.current >= VOICE_MIN_LEVEL - ) { - return; - } - noInputHintVisibleRef.current = true; - onError("noInput"); - }, VOICE_NO_INPUT_HINT_MS); } catch (error) { cleanupRecording(); setState("idle"); diff --git a/webui/src/tests/thread-composer.test.tsx b/webui/src/tests/thread-composer.test.tsx index 545a7233c..5753f6e00 100644 --- a/webui/src/tests/thread-composer.test.tsx +++ b/webui/src/tests/thread-composer.test.tsx @@ -825,10 +825,10 @@ describe("ThreadComposer", () => { expect(onTranscribeAudio).not.toHaveBeenCalled(); }); - it("warns during recording when microphone input is silent", async () => { + it("transcribes recorded audio even when waveform samples are silent", async () => { mockVoiceRecorder(); mockVoiceAudioInput(); - const onTranscribeAudio = vi.fn(async () => "should not appear"); + const onTranscribeAudio = vi.fn(async () => "quiet voice"); render( { await new Promise((resolve) => setTimeout(resolve, 1_150)); }); - expect(screen.getByText("No microphone input detected.")).toBeInTheDocument(); + expect(screen.queryByText("No microphone input detected.")).not.toBeInTheDocument(); fireEvent.click(await screen.findByRole("button", { name: "Stop recording" })); - expect(onTranscribeAudio).not.toHaveBeenCalled(); + + await waitFor(() => expect(onTranscribeAudio).toHaveBeenCalledTimes(1)); + expect(screen.getByDisplayValue("quiet voice")).toBeInTheDocument(); }); it("does not treat unavailable microphone levels as silence", async () => {