Browse Source

fix: eliminate 5-10s freeze on silence, dismiss instantly when not speaking, and gate audio recovery to genuine voice energy

main
Ali Alavi 2 weeks ago
parent
commit
02c1beff36
  1. 46
      src/AppDelegate.swift
  2. 7
      src/InputBoxManager.swift
  3. 12
      src/SonioxClient.swift

46
src/AppDelegate.swift

@ -167,12 +167,44 @@ public final class AppDelegate: NSObject, NSApplicationDelegate {
} }
} }
private func hasAudibleSpeech(pcmData: Data) -> Bool {
guard pcmData.count >= 3200 else { return false }
var sumSquares: Double = 0
var peak: Int16 = 0
let count = pcmData.count / 2
pcmData.withUnsafeBytes { raw in
let ptr = raw.bindMemory(to: Int16.self)
for i in 0..<count {
let sample = ptr[i]
let absSample = sample == Int16.min ? Int16.max : abs(sample)
if absSample > peak { peak = absSample }
let normalized = Double(sample) / 32768.0
sumSquares += normalized * normalized
}
}
let rms = sqrt(sumSquares / Double(count))
return peak > 1500 && rms > 0.010
}
public func stopRecording() { public func stopRecording() {
guard audioRecorder.isRecording else { return } guard audioRecorder.isRecording else { return }
let pcmData = audioRecorder.stopRecording() let pcmData = audioRecorder.stopRecording()
self.lastRecordedAudio = pcmData self.lastRecordedAudio = pcmData
// Fast-path: If user did NOT speak (silence / short ambient hold), dismiss immediately with ZERO delay!
let hasSpeech = hasAudibleSpeech(pcmData: pcmData)
if !hasSpeech && !InputBoxManager.shared.hasStreamedText {
print("AppDelegate: No audible speech detected. Immediate dismissal without freeze.")
activeSession?.cancel()
activeSession = nil
isBusyFinalizing = false
statusBarController.updateIcon(state: .idle)
statusBarController.buildMenu(isRecording: false)
HUDOverlayController.shared.hide()
return
}
playSystemSound(name: "Pop") playSystemSound(name: "Pop")
statusBarController.updateIcon(state: .transcribing) statusBarController.updateIcon(state: .transcribing)
statusBarController.buildMenu(isRecording: false) statusBarController.buildMenu(isRecording: false)
@ -180,8 +212,8 @@ public final class AppDelegate: NSObject, NSApplicationDelegate {
isBusyFinalizing = true isBusyFinalizing = true
// Safety timeout watchdog: finalize must complete within 4.0s
DispatchQueue.main.asyncAfter(deadline: .now() + 4.0) { [weak self] in
// Safety timeout watchdog: finalize must complete within 2.5s
DispatchQueue.main.asyncAfter(deadline: .now() + 2.5) { [weak self] in
guard let self = self, self.isBusyFinalizing else { return } guard let self = self, self.isBusyFinalizing else { return }
print("AppDelegate: Finalize safety watchdog triggered — recovering transcript.") print("AppDelegate: Finalize safety watchdog triggered — recovering transcript.")
self.activeSession?.completeWithCurrentText() self.activeSession?.completeWithCurrentText()
@ -195,18 +227,24 @@ public final class AppDelegate: NSObject, NSApplicationDelegate {
self.statusBarController.updateIcon(state: .idle) self.statusBarController.updateIcon(state: .idle)
var rawText = "" var rawText = ""
var isNetworkError = false
switch result { switch result {
case .success(let text): case .success(let text):
rawText = text.trimmingCharacters(in: .whitespacesAndNewlines) rawText = text.trimmingCharacters(in: .whitespacesAndNewlines)
case .failure(let error): case .failure(let error):
isNetworkError = true
print("Soniox primary finalize error:", error) print("Soniox primary finalize error:", error)
} }
let hasSpeech = hasAudibleSpeech(pcmData: self.lastRecordedAudio)
// Check if smart audio recovery is needed: // Check if smart audio recovery is needed:
let needsRetry = (rawText.isEmpty || self.isNetworkDegraded) && self.lastRecordedAudio.count > 25600
// STRICT: Only retry if there was an ACTUAL network failure/drop AND audible speech was recorded.
// NEVER retry when Soniox cleanly returned empty text for silence!
let needsRetry = (isNetworkError || self.isNetworkDegraded) && hasSpeech && self.lastRecordedAudio.count > 16000
if needsRetry { if needsRetry {
print("AppDelegate: Network drop or truncated output detected. Triggering Smart Audio Recovery with \(self.lastRecordedAudio.count) bytes...")
print("AppDelegate: Network drop detected with audible voice. Triggering Smart Audio Recovery with \(self.lastRecordedAudio.count) bytes...")
HUDOverlayController.shared.show(state: .retrying) HUDOverlayController.shared.show(state: .retrying)
let audioCopy = self.lastRecordedAudio let audioCopy = self.lastRecordedAudio

7
src/InputBoxManager.swift

@ -17,6 +17,13 @@ public final class InputBoxManager {
lock.unlock() lock.unlock()
} }
/// Returns true if any text has been streamed into the input box during this session
public var hasStreamedText: Bool {
lock.lock()
defer { lock.unlock() }
return !accumulatedStreamedText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
}
/// Prepares and guarantees that an input box is focused before recording starts. /// Prepares and guarantees that an input box is focused before recording starts.
/// STRICT RULE: If an input box is already focused, NEVER click or steal focus! /// STRICT RULE: If an input box is already focused, NEVER click or steal focus!
public func ensureFocusedInputBox() { public func ensureFocusedInputBox() {

12
src/SonioxClient.swift

@ -294,13 +294,7 @@ public final class SonioxLiveSession {
executeBurstSession(endpoint: primaryWsBase, pcmData: pcmData, languageHints: languageHints, onProgress: onProgress) { result in executeBurstSession(endpoint: primaryWsBase, pcmData: pcmData, languageHints: languageHints, onProgress: onProgress) { result in
switch result { switch result {
case .success(let text): case .success(let text):
if !text.isEmpty {
completion(.success(text))
} else {
// Try fallback endpoint
print("SonioxLiveSession: Primary retry returned empty, attempting fallback endpoint...")
executeBurstSession(endpoint: fallbackWsBase, pcmData: pcmData, languageHints: languageHints, onProgress: onProgress, completion: completion)
}
completion(.success(text))
case .failure(let error): case .failure(let error):
print("SonioxLiveSession: Primary retry failed (\(error)), attempting fallback endpoint...") print("SonioxLiveSession: Primary retry failed (\(error)), attempting fallback endpoint...")
executeBurstSession(endpoint: fallbackWsBase, pcmData: pcmData, languageHints: languageHints, onProgress: onProgress, completion: completion) executeBurstSession(endpoint: fallbackWsBase, pcmData: pcmData, languageHints: languageHints, onProgress: onProgress, completion: completion)
@ -431,8 +425,8 @@ public final class SonioxLiveSession {
task.send(finalizeMsg) { _ in } task.send(finalizeMsg) { _ in }
} }
// Watchdog timeout for burst retry: max 5 seconds
DispatchQueue.global().asyncAfter(deadline: .now() + 5.0) {
// Watchdog timeout for burst retry: max 2.5 seconds (never hangs UI)
DispatchQueue.global().asyncAfter(deadline: .now() + 2.5) {
let combined = fullCommitted.joined() + fullNonFinal.joined() let combined = fullCommitted.joined() + fullNonFinal.joined()
finishOnce(with: combined) finishOnce(with: combined)
} }

Loading…
Cancel
Save