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
}