|
|
|
@ -7,6 +7,12 @@ public final class SonioxLiveSession { |
|
|
|
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 let targetLanguage: String? |
|
|
|
public var isTranslationMode: Bool { |
|
|
|
guard let target = targetLanguage, !target.isEmpty, target != "off" else { return false } |
|
|
|
return true |
|
|
|
} |
|
|
|
|
|
|
|
private var webSocketTask: URLSessionWebSocketTask? |
|
|
|
private var urlSession: URLSession? |
|
|
|
private var isFinalizing = false |
|
|
|
@ -71,9 +77,19 @@ public final class SonioxLiveSession { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
public init(languageHints: [String] = ["fa", "en", "ar"], endpoint: String = SonioxLiveSession.primaryWsBase) { |
|
|
|
public init( |
|
|
|
languageHints: [String] = ["fa", "en", "ar"], |
|
|
|
targetLanguage: String? = nil, |
|
|
|
endpoint: String = SonioxLiveSession.primaryWsBase |
|
|
|
) { |
|
|
|
let effectiveTarget = targetLanguage ?? (SonioxSettings.shared.targetLanguage.isEnabled ? SonioxSettings.shared.targetLanguage.rawValue : nil) |
|
|
|
self.targetLanguage = effectiveTarget |
|
|
|
|
|
|
|
let hints = languageHints.map { "language_hints=\($0)" }.joined(separator: "&") |
|
|
|
let urlStr = "\(endpoint)?\(hints)&enable_speaker_diarization=false&enable_language_identification=true&enable_endpoint_detection=false&providers=soniox" |
|
|
|
var urlStr = "\(endpoint)?\(hints)&enable_speaker_diarization=false&enable_language_identification=true&enable_endpoint_detection=false&providers=soniox" |
|
|
|
if let target = effectiveTarget, !target.isEmpty, target != "off" { |
|
|
|
urlStr += "&target_language=\(target)" |
|
|
|
} |
|
|
|
guard let url = URL(string: urlStr) else { return } |
|
|
|
|
|
|
|
var request = URLRequest(url: url) |
|
|
|
@ -221,9 +237,15 @@ public final class SonioxLiveSession { |
|
|
|
if let type = json["type"] as? String, type == "data", |
|
|
|
let parts = json["parts"] as? [[String: Any]] { |
|
|
|
for part in parts { |
|
|
|
let transStatus = part["translation_status"] as? String |
|
|
|
if transStatus == "translation" { |
|
|
|
continue |
|
|
|
let isTranslation = (part["translation_status"] as? String) == "translation" |
|
|
|
if self.isTranslationMode { |
|
|
|
if !isTranslation { |
|
|
|
continue |
|
|
|
} |
|
|
|
} else { |
|
|
|
if isTranslation { |
|
|
|
continue |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
let pText = part["text"] as? String ?? "" |
|
|
|
@ -318,6 +340,7 @@ public final class SonioxLiveSession { |
|
|
|
public static func retryFullBufferedAudio( |
|
|
|
pcmData: Data, |
|
|
|
languageHints: [String], |
|
|
|
targetLanguage: String? = nil, |
|
|
|
onProgress: ((String) -> Void)? = nil, |
|
|
|
completion: @escaping (Result<String, Error>) -> Void |
|
|
|
) { |
|
|
|
@ -326,16 +349,17 @@ public final class SonioxLiveSession { |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
print("SonioxLiveSession: Starting Smart Burst Retry with \(pcmData.count) bytes (\(Double(pcmData.count)/32000.0)s audio)...") |
|
|
|
let effectiveTarget = targetLanguage ?? (SonioxSettings.shared.targetLanguage.isEnabled ? SonioxSettings.shared.targetLanguage.rawValue : nil) |
|
|
|
print("SonioxLiveSession: Starting Smart Burst Retry with \(pcmData.count) bytes (\(Double(pcmData.count)/32000.0)s audio, target: \(effectiveTarget ?? "orig"))...") |
|
|
|
|
|
|
|
// Try Primary endpoint, failover to Fallback if needed |
|
|
|
executeBurstSession(endpoint: primaryWsBase, pcmData: pcmData, languageHints: languageHints, onProgress: onProgress) { result in |
|
|
|
executeBurstSession(endpoint: primaryWsBase, pcmData: pcmData, languageHints: languageHints, targetLanguage: effectiveTarget, onProgress: onProgress) { result in |
|
|
|
switch result { |
|
|
|
case .success(let text): |
|
|
|
completion(.success(text)) |
|
|
|
case .failure(let error): |
|
|
|
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, targetLanguage: effectiveTarget, onProgress: onProgress, completion: completion) |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
@ -344,11 +368,16 @@ public final class SonioxLiveSession { |
|
|
|
endpoint: String, |
|
|
|
pcmData: Data, |
|
|
|
languageHints: [String], |
|
|
|
targetLanguage: String?, |
|
|
|
onProgress: ((String) -> Void)?, |
|
|
|
completion: @escaping (Result<String, Error>) -> Void |
|
|
|
) { |
|
|
|
let isTranslationMode = targetLanguage != nil && !targetLanguage!.isEmpty && targetLanguage != "off" |
|
|
|
let hints = languageHints.map { "language_hints=\($0)" }.joined(separator: "&") |
|
|
|
let urlStr = "\(endpoint)?\(hints)&enable_speaker_diarization=false&enable_language_identification=true&enable_endpoint_detection=false&providers=soniox" |
|
|
|
var urlStr = "\(endpoint)?\(hints)&enable_speaker_diarization=false&enable_language_identification=true&enable_endpoint_detection=false&providers=soniox" |
|
|
|
if let target = targetLanguage, !target.isEmpty, target != "off" { |
|
|
|
urlStr += "&target_language=\(target)" |
|
|
|
} |
|
|
|
guard let url = URL(string: urlStr) else { |
|
|
|
completion(.failure(NSError(domain: "Soniox", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid URL"]))) |
|
|
|
return |
|
|
|
@ -404,7 +433,12 @@ public final class SonioxLiveSession { |
|
|
|
var gotFin = false |
|
|
|
if let parts = json["parts"] as? [[String: Any]] { |
|
|
|
for p in parts { |
|
|
|
if (p["translation_status"] as? String) == "translation" { continue } |
|
|
|
let isTranslation = (p["translation_status"] as? String) == "translation" |
|
|
|
if isTranslationMode { |
|
|
|
if !isTranslation { continue } |
|
|
|
} else { |
|
|
|
if isTranslation { continue } |
|
|
|
} |
|
|
|
let t = p["text"] as? String ?? "" |
|
|
|
let isFinal = p["is_final"] as? Bool ?? false |
|
|
|
if t.contains("<fin>") { |
|
|
|
@ -584,7 +618,8 @@ public final class SonioxSessionPool { |
|
|
|
DispatchQueue.global(qos: .userInitiated).async { [weak self] in |
|
|
|
guard let self = self else { return } |
|
|
|
let hints = SonioxSettings.shared.language.hints |
|
|
|
let session = SonioxLiveSession(languageHints: hints) |
|
|
|
let target = SonioxSettings.shared.targetLanguage.isEnabled ? SonioxSettings.shared.targetLanguage.rawValue : nil |
|
|
|
let session = SonioxLiveSession(languageHints: hints, targetLanguage: target) |
|
|
|
|
|
|
|
// Allow 400ms for connection & initial ping |
|
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) { [weak self] in |
|
|
|
@ -629,6 +664,7 @@ public final class SonioxSessionPool { |
|
|
|
|
|
|
|
print("SonioxSessionPool: Prewarmed session not ready/stale (age: \(String(format: "%.1f", age))s), creating immediate on-demand session.") |
|
|
|
let hints = SonioxSettings.shared.language.hints |
|
|
|
return SonioxLiveSession(languageHints: hints) |
|
|
|
let target = SonioxSettings.shared.targetLanguage.isEnabled ? SonioxSettings.shared.targetLanguage.rawValue : nil |
|
|
|
return SonioxLiveSession(languageHints: hints, targetLanguage: target) |
|
|
|
} |
|
|
|
} |