From b40181746d62ad53546e707400bfb0cad955f116 Mon Sep 17 00:00:00 2001 From: Ali Alavi Date: Sat, 24 Oct 2026 13:56:32 +0000 Subject: [PATCH] fix(stt): support new Soniox v5 compare termination protocol and dynamic origin (v2.10) --- Info.plist | 2 +- src/SonioxClient.swift | 32 ++++++++++++++++++++++++++------ 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/Info.plist b/Info.plist index ea104d3..2a3f855 100644 --- a/Info.plist +++ b/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 2.09 + 2.10 CFBundleVersion 1 LSMinimumSystemVersion diff --git a/src/SonioxClient.swift b/src/SonioxClient.swift index b6daa84..701f4d1 100644 --- a/src/SonioxClient.swift +++ b/src/SonioxClient.swift @@ -5,7 +5,9 @@ public final class SonioxLiveSession { public static let primaryWsBase = "wss://translate.compare.soniox.com/compare/api/compare-websocket" public static let fallbackWsBase = "wss://stt.compare.soniox.com/compare/api/compare-websocket" private static let userAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" - private static let origin = "https://translate.compare.soniox.com" + public static func origin(for endpoint: String) -> String { + return endpoint.contains("translate") ? "https://translate.compare.soniox.com" : "https://stt.compare.soniox.com" + } // Pre-compiled static regexes for zero-allocation hot path private static let faRegex: NSRegularExpression = { @@ -142,7 +144,7 @@ public final class SonioxLiveSession { var request = URLRequest(url: url) request.setValue(SonioxLiveSession.userAgent, forHTTPHeaderField: "User-Agent") - request.setValue(SonioxLiveSession.origin, forHTTPHeaderField: "Origin") + request.setValue(SonioxLiveSession.origin(for: endpoint), forHTTPHeaderField: "Origin") request.timeoutInterval = 12.0 let config = URLSessionConfiguration.default @@ -255,6 +257,10 @@ public final class SonioxLiveSession { } } + // 3. Send official "END" token for complete Soniox termination + let endMsg = URLSessionWebSocketTask.Message.string("END") + task.send(endMsg) { _ in } + // Safety timeout fallback: allow up to 4.0s for STT inference & network transit DispatchQueue.global().asyncAfter(deadline: .now() + 4.0) { [weak self] in guard let self = self else { return } @@ -425,8 +431,13 @@ public final class SonioxLiveSession { let sessionEnded = json["session_ended"] as? Bool ?? false let sessionDone = (json["type"] as? String) == "session_done" + let isFinished = json["finished"] as? Bool ?? false + let hasRawFinished: Bool = { + guard let raw = json["raw"] as? String else { return false } + return raw.contains("\"finished\":true") || raw.contains("\"finished\": true") + }() - if gotFin || sessionEnded || sessionDone { + if gotFin || sessionEnded || sessionDone || isFinished || hasRawFinished { completeWithCurrentText() } } @@ -623,7 +634,7 @@ public final class SonioxLiveSession { var request = URLRequest(url: url) request.setValue(userAgent, forHTTPHeaderField: "User-Agent") - request.setValue(origin, forHTTPHeaderField: "Origin") + request.setValue(origin(for: endpoint), forHTTPHeaderField: "Origin") let durationSec = Double(pcmData.count) / 32000.0 let watchdogTimeout = max(6.5, min(25.0, durationSec * 0.60 + 5.0)) @@ -721,7 +732,14 @@ public final class SonioxLiveSession { } } - let ended = (json["session_ended"] as? Bool ?? false) || ((json["type"] as? String) == "session_done") + let sessionEnded = json["session_ended"] as? Bool ?? false + let sessionDone = (json["type"] as? String) == "session_done" + let isFinished = json["finished"] as? Bool ?? false + let hasRawFinished: Bool = { + guard let raw = json["raw"] as? String else { return false } + return raw.contains("\"finished\":true") || raw.contains("\"finished\": true") + }() + let ended = sessionEnded || sessionDone || isFinished || hasRawFinished if gotFin || ended { lock.lock() let textToReturn = fullCommitted.joined() + (gotFin ? "" : latestNonFinal.joined()) @@ -780,7 +798,9 @@ public final class SonioxLiveSession { if offset >= pcmData.count { let silence = Data(count: 6400) task.send(.data(silence)) { _ in - task.send(.string("{\"type\": \"finalize\"}")) { _ in } + task.send(.string("{\"type\": \"finalize\"}")) { _ in + task.send(.string("END")) { _ in } + } } return }