Browse Source

fix(stt): support new Soniox v5 compare termination protocol and dynamic origin (v2.10)

main
Ali Alavi 6 days ago
parent
commit
b40181746d
  1. 2
      Info.plist
  2. 32
      src/SonioxClient.swift

2
Info.plist

@ -19,7 +19,7 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>2.09</string>
<string>2.10</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>1</string> <string>1</string>
<key>LSMinimumSystemVersion</key> <key>LSMinimumSystemVersion</key>

32
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 primaryWsBase = "wss://translate.compare.soniox.com/compare/api/compare-websocket"
public static let fallbackWsBase = "wss://stt.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 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 // Pre-compiled static regexes for zero-allocation hot path
private static let faRegex: NSRegularExpression = { private static let faRegex: NSRegularExpression = {
@ -142,7 +144,7 @@ public final class SonioxLiveSession {
var request = URLRequest(url: url) var request = URLRequest(url: url)
request.setValue(SonioxLiveSession.userAgent, forHTTPHeaderField: "User-Agent") 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 request.timeoutInterval = 12.0
let config = URLSessionConfiguration.default 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 // Safety timeout fallback: allow up to 4.0s for STT inference & network transit
DispatchQueue.global().asyncAfter(deadline: .now() + 4.0) { [weak self] in DispatchQueue.global().asyncAfter(deadline: .now() + 4.0) { [weak self] in
guard let self = self else { return } guard let self = self else { return }
@ -425,8 +431,13 @@ public final class SonioxLiveSession {
let sessionEnded = json["session_ended"] as? Bool ?? false let sessionEnded = json["session_ended"] as? Bool ?? false
let sessionDone = (json["type"] as? String) == "session_done" 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() completeWithCurrentText()
} }
} }
@ -623,7 +634,7 @@ public final class SonioxLiveSession {
var request = URLRequest(url: url) var request = URLRequest(url: url)
request.setValue(userAgent, forHTTPHeaderField: "User-Agent") 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 durationSec = Double(pcmData.count) / 32000.0
let watchdogTimeout = max(6.5, min(25.0, durationSec * 0.60 + 5.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 { if gotFin || ended {
lock.lock() lock.lock()
let textToReturn = fullCommitted.joined() + (gotFin ? "" : latestNonFinal.joined()) let textToReturn = fullCommitted.joined() + (gotFin ? "" : latestNonFinal.joined())
@ -780,7 +798,9 @@ public final class SonioxLiveSession {
if offset >= pcmData.count { if offset >= pcmData.count {
let silence = Data(count: 6400) let silence = Data(count: 6400)
task.send(.data(silence)) { _ in task.send(.data(silence)) { _ in
task.send(.string("{\"type\": \"finalize\"}")) { _ in }
task.send(.string("{\"type\": \"finalize\"}")) { _ in
task.send(.string("END")) { _ in }
}
} }
return return
} }

Loading…
Cancel
Save