|
|
@ -204,7 +204,7 @@ public final class AppDelegate: NSObject, NSApplicationDelegate { |
|
|
|
|
|
|
|
|
do { |
|
|
do { |
|
|
try audioRecorder.startRecording() |
|
|
try audioRecorder.startRecording() |
|
|
playSystemSound(name: "Hero") |
|
|
|
|
|
|
|
|
playSystemSound(name: "Tink") |
|
|
statusBarController.updateIcon(state: .recording) |
|
|
statusBarController.updateIcon(state: .recording) |
|
|
statusBarController.buildMenu(isRecording: true) |
|
|
statusBarController.buildMenu(isRecording: true) |
|
|
HUDOverlayController.shared.show(state: .recording(level: 0.1, isLagging: false, pingQuality: PingMonitor.shared.currentQuality)) |
|
|
HUDOverlayController.shared.show(state: .recording(level: 0.1, isLagging: false, pingQuality: PingMonitor.shared.currentQuality)) |
|
|
@ -217,7 +217,8 @@ public final class AppDelegate: NSObject, NSApplicationDelegate { |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
private func hasAudibleSpeech(pcmData: Data) -> Bool { |
|
|
private func hasAudibleSpeech(pcmData: Data) -> Bool { |
|
|
guard pcmData.count >= 3200 else { return false } |
|
|
|
|
|
|
|
|
// Less than 0.25s of audio is just a momentary key tap/click |
|
|
|
|
|
guard pcmData.count >= 8000 else { return false } |
|
|
var sumSquares: Double = 0 |
|
|
var sumSquares: Double = 0 |
|
|
var peak: Int16 = 0 |
|
|
var peak: Int16 = 0 |
|
|
let count = pcmData.count / 2 |
|
|
let count = pcmData.count / 2 |
|
|
@ -232,7 +233,8 @@ public final class AppDelegate: NSObject, NSApplicationDelegate { |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
let rms = sqrt(sumSquares / Double(count)) |
|
|
let rms = sqrt(sumSquares / Double(count)) |
|
|
return peak > 1500 && rms > 0.010 |
|
|
|
|
|
|
|
|
// Raised thresholds: filter out mechanical keyclicks and keyboard taps |
|
|
|
|
|
return peak > 2500 && rms > 0.015 |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
public func stopRecording() { |
|
|
public func stopRecording() { |
|
|
@ -243,10 +245,10 @@ public final class AppDelegate: NSObject, NSApplicationDelegate { |
|
|
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! |
|
|
|
|
|
|
|
|
// Fast-path: If user tapped key or did NOT speak (silence / keyclick), dismiss immediately with ZERO delay! |
|
|
let hasSpeech = hasAudibleSpeech(pcmData: pcmData) |
|
|
let hasSpeech = hasAudibleSpeech(pcmData: pcmData) |
|
|
if !hasSpeech && !InputBoxManager.shared.hasStreamedText { |
|
|
|
|
|
print("AppDelegate: No audible speech detected. Immediate dismissal without freeze.") |
|
|
|
|
|
|
|
|
if (!hasSpeech && !InputBoxManager.shared.hasStreamedText) || pcmData.count < 8000 { |
|
|
|
|
|
print("AppDelegate: Short tap or silence detected. Immediate 0ms dismissal.") |
|
|
activeSession?.cancel() |
|
|
activeSession?.cancel() |
|
|
activeSession = nil |
|
|
activeSession = nil |
|
|
isBusyFinalizing = false |
|
|
isBusyFinalizing = false |
|
|
@ -308,12 +310,21 @@ public final class AppDelegate: NSObject, NSApplicationDelegate { |
|
|
print("Soniox primary finalize error:", error) |
|
|
print("Soniox primary finalize error:", error) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Strict Silence Gating: If server cleanly finished with empty text and no network drop, |
|
|
|
|
|
// it was just silence/key tap. Dismiss immediately with zero freeze! |
|
|
|
|
|
if rawText.isEmpty && !isNetworkError && !self.isNetworkDegraded { |
|
|
|
|
|
print("AppDelegate: Server cleanly returned empty text for silence. Immediate dismissal.") |
|
|
|
|
|
self.isBusyFinalizing = false |
|
|
|
|
|
HUDOverlayController.shared.hide() |
|
|
|
|
|
return |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
let hasSpeech = hasAudibleSpeech(pcmData: self.lastRecordedAudio) |
|
|
let hasSpeech = hasAudibleSpeech(pcmData: self.lastRecordedAudio) |
|
|
let isTruncated = hasSpeech && self.lastRecordedAudio.count > 64000 && rawText.count < 10 |
|
|
let isTruncated = hasSpeech && self.lastRecordedAudio.count > 64000 && rawText.count < 10 |
|
|
let needsRetry = (isNetworkError || self.isNetworkDegraded || isTruncated || rawText.isEmpty) && hasSpeech && self.lastRecordedAudio.count > 16000 |
|
|
|
|
|
|
|
|
let needsRetry = (isNetworkError || self.isNetworkDegraded || isTruncated) && hasSpeech && self.lastRecordedAudio.count > 24000 |
|
|
|
|
|
|
|
|
if needsRetry { |
|
|
if needsRetry { |
|
|
let reason = isNetworkError ? "WebSocket network error" : (isTruncated ? "Truncated transcript (\(rawText.count) chars for \(self.lastRecordedAudio.count) bytes)" : "Empty transcript for audible speech") |
|
|
|
|
|
|
|
|
let reason = isNetworkError ? "WebSocket network error" : "Truncated transcript (\(rawText.count) chars for \(self.lastRecordedAudio.count) bytes)" |
|
|
startSmartAudioRecovery(pcmData: self.lastRecordedAudio, reason: reason) |
|
|
startSmartAudioRecovery(pcmData: self.lastRecordedAudio, reason: reason) |
|
|
return |
|
|
return |
|
|
} |
|
|
} |
|
|
|