Browse Source

fix: resolve fast speech truncation by flushing hardware PCM buffer and extending finalize timeout to 4.5s

main
Ali Alavi 5 days ago
parent
commit
488a2a46d2
  1. 6
      src/AppDelegate.swift
  2. 21
      src/AudioRecorder.swift
  3. 11
      src/SonioxClient.swift

6
src/AppDelegate.swift

@ -81,8 +81,8 @@ public final class AppDelegate: NSObject, NSApplicationDelegate {
} }
} }
self.pendingStopWorkItem = workItem 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 // Dynamic safety timeout watchdog: scale with audio length & network transit
let durationSec = Double(pcmData.count) / 32000.0 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 let watchdog = DispatchWorkItem { [weak self] in
guard let self = self, self.isBusyFinalizing else { return } guard let self = self, self.isBusyFinalizing else { return }

21
src/AudioRecorder.swift

@ -217,14 +217,23 @@ public final class AudioRecorder: NSObject, AVCaptureAudioDataOutputSampleBuffer
public func stopRecording() -> Data { public func stopRecording() -> Data {
lock.lock() 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() 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
} }
} }

11
src/SonioxClient.swift

@ -174,8 +174,9 @@ public final class SonioxLiveSession {
lock.unlock() lock.unlock()
print("SonioxLiveSession: Sending finalize packet...") 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) let silenceMsg = URLSessionWebSocketTask.Message.data(silenceChunk)
task.send(silenceMsg) { _ in } 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 } guard let self = self else { return }
self.lock.lock() self.lock.lock()
let stillOpen = !self.isClosed let stillOpen = !self.isClosed
self.lock.unlock() self.lock.unlock()
if stillOpen { if stillOpen {
print("SonioxLiveSession: Finalize timeout (1.0s) reached — completing.")
print("SonioxLiveSession: Finalize timeout (4.5s) reached — completing.")
self.completeWithCurrentText() self.completeWithCurrentText()
} }
} }

Loading…
Cancel
Save