diff --git a/src/preload/backend.ts b/src/preload/backend.ts index b2524e1..0084a7d 100644 --- a/src/preload/backend.ts +++ b/src/preload/backend.ts @@ -1,7 +1,7 @@ import { ipcRenderer } from 'electron'; import type { BackendControlCommand, BackendPageStatus, BackendTranscriptPayload } from '../backends/contracts'; import { getBackendWebAdapter } from '../backends/web-registry'; -import { shouldClickMicrophoneOnCancel } from './speech-session-state'; +import { shouldClickMicrophoneOnCancel, shouldClickMicrophoneOnStop } from './speech-session-state'; import { shouldFinishAfterStop } from './stop-completion'; const backendId = process.argv @@ -180,9 +180,7 @@ function stopSpeech(): void { if (!active || stopping || !adapter) return; stopping = true; const stoppedAt = Date.now(); - const wasRecordingAtStop = adapter.isRecording(document); - adapter.findMicrophone(document)?.click(); - diagnose('stop-clicked', { microphone: controlSnapshot(adapter.findMicrophone(document)) }); + let stopClicked = false; const finish = (): void => { emitCurrentEditorText(); resetSession(); @@ -192,11 +190,22 @@ function stopSpeech(): void { const checkCompletion = (): void => { if (!active || !stopping || !adapter) return; const now = Date.now(); + const isRecording = adapter.isRecording(document); + if (shouldClickMicrophoneOnStop(isRecording, stopClicked)) { + const microphone = adapter.findMicrophone(document); + if (microphone) { + microphone.click(); + stopClicked = true; + diagnose('stop-clicked', { microphone: controlSnapshot(adapter.findMicrophone(document)) }); + } + stopTimer = setTimeout(checkCompletion, 75); + return; + } if (shouldFinishAfterStop({ elapsedMs: now - stoppedAt, quietMs: now - lastTranscriptAt, - wasRecordingAtStop, - isRecording: adapter.isRecording(document), + stopClicked, + isRecording, ...adapter.stopCompletion })) { diagnose('stop-complete', { elapsedMs: now - stoppedAt }); @@ -206,7 +215,7 @@ function stopSpeech(): void { stopTimer = setTimeout(checkCompletion, 75); }; clearTimeout(stopTimer); - stopTimer = setTimeout(checkCompletion, 75); + checkCompletion(); } function cancelSpeech(): void { diff --git a/src/preload/speech-session-state.ts b/src/preload/speech-session-state.ts index a819dc3..ddd2f47 100644 --- a/src/preload/speech-session-state.ts +++ b/src/preload/speech-session-state.ts @@ -1,3 +1,7 @@ export function shouldClickMicrophoneOnCancel(active: boolean, stopping: boolean): boolean { return active && !stopping; } + +export function shouldClickMicrophoneOnStop(isRecording: boolean, stopClicked: boolean): boolean { + return isRecording && !stopClicked; +} diff --git a/src/preload/stop-completion.ts b/src/preload/stop-completion.ts index fd4abb5..2918725 100644 --- a/src/preload/stop-completion.ts +++ b/src/preload/stop-completion.ts @@ -1,7 +1,7 @@ export interface StopCompletionInput { readonly elapsedMs: number; readonly quietMs: number; - readonly wasRecordingAtStop: boolean; + readonly stopClicked: boolean; readonly isRecording: boolean; readonly minimumWaitMs: number; readonly quietWindowMs: number; @@ -9,7 +9,7 @@ export interface StopCompletionInput { } export function shouldFinishAfterStop(input: StopCompletionInput): boolean { + if (!input.stopClicked || input.isRecording) return false; if (input.elapsedMs >= input.timeoutMs) return true; - if (input.wasRecordingAtStop && input.isRecording) return false; return input.elapsedMs >= input.minimumWaitMs && input.quietMs >= input.quietWindowMs; } diff --git a/test/speech-session-state.test.ts b/test/speech-session-state.test.ts index fd2bd74..bbdea41 100644 --- a/test/speech-session-state.test.ts +++ b/test/speech-session-state.test.ts @@ -1,6 +1,6 @@ import assert from 'node:assert/strict'; import test from 'node:test'; -import { shouldClickMicrophoneOnCancel } from '../src/preload/speech-session-state'; +import { shouldClickMicrophoneOnCancel, shouldClickMicrophoneOnStop } from '../src/preload/speech-session-state'; test('停止命令已经点击过麦克风后,取消不得再次点击并重新开启录音', () => { assert.equal(shouldClickMicrophoneOnCancel(true, true), false); @@ -13,3 +13,12 @@ test('录音期间直接取消时仍应点击麦克风停止网页录音', () => test('本地会话未激活时取消不得点击麦克风', () => { assert.equal(shouldClickMicrophoneOnCancel(false, false), false); }); + +test('停止到达但网页尚未开始录音时不得再次点击开始按钮', () => { + assert.equal(shouldClickMicrophoneOnStop(false, false), false); +}); + +test('网页异步进入录音后只点击一次停止按钮', () => { + assert.equal(shouldClickMicrophoneOnStop(true, false), true); + assert.equal(shouldClickMicrophoneOnStop(true, true), false); +}); diff --git a/test/stop-completion.test.ts b/test/stop-completion.test.ts index 91ec8ea..611ddf4 100644 --- a/test/stop-completion.test.ts +++ b/test/stop-completion.test.ts @@ -5,7 +5,7 @@ import { shouldFinishAfterStop } from '../src/preload/stop-completion'; const base = { elapsedMs: 700, quietMs: 500, - wasRecordingAtStop: true, + stopClicked: true, isRecording: false, minimumWaitMs: 250, quietWindowMs: 450, @@ -16,6 +16,10 @@ test('网页仍在录音时不得提前提交', () => { assert.equal(shouldFinishAfterStop({ ...base, isRecording: true }), false); }); +test('尚未点击停止时不得提交', () => { + assert.equal(shouldFinishAfterStop({ ...base, stopClicked: false }), false); +}); + test('网页结束且最终文本静默窗口满足时立即提交', () => { assert.equal(shouldFinishAfterStop(base), true); }); @@ -24,6 +28,10 @@ test('网页结束后仍有文本修正时继续等待', () => { assert.equal(shouldFinishAfterStop({ ...base, quietMs: 120 }), false); }); -test('网页状态未回切时由超时兜底结束', () => { - assert.equal(shouldFinishAfterStop({ ...base, elapsedMs: 4_500, isRecording: true }), true); +test('网页仍在录音时超时也不得提交', () => { + assert.equal(shouldFinishAfterStop({ ...base, elapsedMs: 4_500, isRecording: true }), false); +}); + +test('网页已停止时超时可以结束静默等待', () => { + assert.equal(shouldFinishAfterStop({ ...base, elapsedMs: 4_500, quietMs: 120 }), true); });