diff --git a/src/AppDelegate.swift b/src/AppDelegate.swift index 030e4d9..5617108 100644 --- a/src/AppDelegate.swift +++ b/src/AppDelegate.swift @@ -81,8 +81,8 @@ public final class AppDelegate: NSObject, NSApplicationDelegate { } } self.pendingStopWorkItem = workItem - // Trailing grace period: 1.0s (1000ms) buffer so trailing words/syllables right up to key release are never clipped - DispatchQueue.main.asyncAfter(deadline: .now() + 1.00, execute: workItem) + // Trailing grace period: 0.35s (350ms) buffer after key release so trailing fast speech is never clipped + DispatchQueue.main.asyncAfter(deadline: .now() + 0.35, execute: workItem) } } } @@ -338,7 +338,7 @@ public final class AppDelegate: NSObject, NSApplicationDelegate { // Dynamic safety timeout watchdog: scale with audio length & network transit let durationSec = Double(pcmData.count) / 32000.0 - let finalizeTimeout = max(2.5, min(8.0, durationSec * 0.2 + 2.5)) + let finalizeTimeout = max(4.0, min(12.0, durationSec * 0.25 + 3.5)) let watchdog = DispatchWorkItem { [weak self] in guard let self = self, self.isBusyFinalizing else { return } diff --git a/src/AudioRecorder.swift b/src/AudioRecorder.swift index 0d986d4..ad739f4 100644 --- a/src/AudioRecorder.swift +++ b/src/AudioRecorder.swift @@ -217,14 +217,23 @@ public final class AudioRecorder: NSObject, AVCaptureAudioDataOutputSampleBuffer public func stopRecording() -> Data { lock.lock() - defer { lock.unlock() } + if !isRecording { + let data = pcmBuffer + lock.unlock() + return data + } + lock.unlock() - if !isRecording { return pcmBuffer } - isRecording = false + // Allow in-flight audio sample buffers on captureQueue to finish processing + usleep(60000) - // Immediately stop hardware capture so macOS orange mic indicator turns OFF + lock.lock() + isRecording = false captureSession?.stopRunning() - print("AudioRecorder: Stopped. Microphone hardware OFF. Total PCM captured: \(pcmBuffer.count) bytes.") - return pcmBuffer + let data = pcmBuffer + lock.unlock() + + print("AudioRecorder: Stopped. Microphone hardware OFF. Total PCM captured: \(data.count) bytes.") + return data } } diff --git a/src/SonioxClient.swift b/src/SonioxClient.swift index 2f87216..e773422 100644 --- a/src/SonioxClient.swift +++ b/src/SonioxClient.swift @@ -174,8 +174,9 @@ public final class SonioxLiveSession { lock.unlock() print("SonioxLiveSession: Sending finalize packet...") - // 1. Send trailing 120ms silence frame so acoustic model cleanly resolves trailing syllables - let silenceChunk = Data(count: 3840) + // 1. Send trailing 300ms (9600 bytes at 16kHz 16-bit PCM) silence frame + // so acoustic CTC model cleanly flushes and resolves trailing fast speech syllables + let silenceChunk = Data(count: 9600) let silenceMsg = URLSessionWebSocketTask.Message.data(silenceChunk) task.send(silenceMsg) { _ in } @@ -191,14 +192,14 @@ public final class SonioxLiveSession { } } - // Fast safety timeout fallback: finalize must complete within 1.0s - DispatchQueue.global().asyncAfter(deadline: .now() + 1.0) { [weak self] in + // Safety timeout fallback: allow up to 4.5s for STT inference & network transit + DispatchQueue.global().asyncAfter(deadline: .now() + 4.5) { [weak self] in guard let self = self else { return } self.lock.lock() let stillOpen = !self.isClosed self.lock.unlock() if stillOpen { - print("SonioxLiveSession: Finalize timeout (1.0s) reached — completing.") + print("SonioxLiveSession: Finalize timeout (4.5s) reached — completing.") self.completeWithCurrentText() } }