2 changed files with 368 additions and 3 deletions
@ -0,0 +1,280 @@ |
|||
import Foundation |
|||
import CryptoKit |
|||
|
|||
class XunFeiAsrHelper: NSObject, URLSessionWebSocketDelegate { |
|||
private let tag = "XunFeiAsrHelper" |
|||
|
|||
private let appId = "43d87d22" |
|||
private let accessKeyId = "2bd01ce06ac35957fc0c082f9b6284ec" |
|||
private let accessKeySecret = "ODU3OTUyYjhjMjY5NDhkMmMxYWY2Zjlm" |
|||
|
|||
private var webSocketTask: URLSessionWebSocketTask? |
|||
private lazy var session: URLSession = { |
|||
let configuration = URLSessionConfiguration.default |
|||
return URLSession(configuration: configuration, delegate: self, delegateQueue: OperationQueue()) |
|||
}() |
|||
|
|||
private var currentSessionId = "" |
|||
private var sessionId = "" |
|||
private var lastIntermediateResult = "" |
|||
|
|||
private var currentLanguage = "zh-CN" |
|||
private var isAutoDetectLanguage = false |
|||
|
|||
private var callback: AzureAsrHelper.ContinuousRecognizeCallback? |
|||
|
|||
func start(callback: AzureAsrHelper.ContinuousRecognizeCallback, language: String, isAutoDetect: Bool) { |
|||
stop() |
|||
|
|||
self.callback = callback |
|||
self.currentLanguage = language |
|||
self.isAutoDetectLanguage = isAutoDetect |
|||
self.currentSessionId = UUID().uuidString |
|||
self.lastIntermediateResult = "" |
|||
self.sessionId = "" |
|||
|
|||
let urlString = getWebSocketUrl() |
|||
guard let url = URL(string: urlString) else { |
|||
print("\(tag): Invalid URL") |
|||
return |
|||
} |
|||
|
|||
let request = URLRequest(url: url) |
|||
webSocketTask = session.webSocketTask(with: request) |
|||
webSocketTask?.resume() |
|||
|
|||
print("\(tag): WebSocket Connecting...") |
|||
receiveMessage() |
|||
} |
|||
|
|||
func sendAudio(frameBuffer: Data) { |
|||
guard let webSocketTask = webSocketTask else { return } |
|||
let message = URLSessionWebSocketTask.Message.data(frameBuffer) |
|||
webSocketTask.send(message) { error in |
|||
if let error = error { |
|||
print("\(self.tag): Send audio failed: \(error)") |
|||
} |
|||
} |
|||
} |
|||
|
|||
func stop() { |
|||
if let webSocketTask = webSocketTask { |
|||
let endJson = "{\"end\": true, \"sessionId\": \"\(sessionId)\"}" |
|||
let message = URLSessionWebSocketTask.Message.string(endJson) |
|||
webSocketTask.send(message) { _ in } |
|||
webSocketTask.cancel(with: .normalClosure, reason: "User stopped".data(using: .utf8)) |
|||
} |
|||
webSocketTask = nil |
|||
} |
|||
|
|||
private func receiveMessage() { |
|||
webSocketTask?.receive { [weak self] result in |
|||
guard let self = self else { return } |
|||
|
|||
switch result { |
|||
case .failure(let error): |
|||
print("\(self.tag): WebSocket Receive Error: \(error)") |
|||
self.callback?.onError(sessionid: self.currentSessionId, 1004, "Xunfei connection failed: \(error.localizedDescription)") |
|||
|
|||
case .success(let message): |
|||
switch message { |
|||
case .string(let text): |
|||
self.processMessage(text) |
|||
case .data(let data): |
|||
if let text = String(data: data, encoding: .utf8) { |
|||
self.processMessage(text) |
|||
} |
|||
@unknown default: |
|||
break |
|||
} |
|||
|
|||
// Continue receiving messages |
|||
self.receiveMessage() |
|||
} |
|||
} |
|||
} |
|||
|
|||
func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didOpenWithProtocol protocol: String?) { |
|||
print("\(tag): WebSocket Opened") |
|||
callback?.onSessionStarted(sessionid: currentSessionId) |
|||
} |
|||
|
|||
func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didCloseWith closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) { |
|||
let reasonString = reason.flatMap { String(data: $0, encoding: .utf8) } ?? "Unknown" |
|||
print("\(tag): WebSocket Closed: \(closeCode) / \(reasonString)") |
|||
|
|||
if !currentSessionId.isEmpty && !lastIntermediateResult.isEmpty { |
|||
let detectedLanguage = detectLanguage(from: lastIntermediateResult) |
|||
callback?.onResult(sessionid: currentSessionId, lastIntermediateResult, detectedLanguage) |
|||
currentSessionId = "" |
|||
} |
|||
callback?.onSessionStopped(sessionid: currentSessionId) |
|||
} |
|||
|
|||
private func processMessage(_ text: String) { |
|||
guard let data = text.data(using: .utf8) else { return } |
|||
|
|||
do { |
|||
guard let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else { return } |
|||
print("\(tag): -------------------\(json)") |
|||
|
|||
let msgType = json["msg_type"] as? String |
|||
|
|||
switch msgType { |
|||
case "action": |
|||
if let sid = json["sessionId"] as? String, !sid.isEmpty { |
|||
self.sessionId = sid |
|||
} |
|||
print("\(tag): Handshake success") |
|||
|
|||
case "result": |
|||
processResult(json) |
|||
|
|||
case "error": |
|||
let errorMsg = "\(json)" |
|||
print("\(tag): Error: \(errorMsg)") |
|||
callback?.onError(sessionid: currentSessionId, 1005, "Xunfei error: \(errorMsg)") |
|||
|
|||
default: |
|||
print("\(tag): Unknown msg_type: \(msgType ?? "nil")") |
|||
callback?.onError(sessionid: currentSessionId, 1006, "Xunfei unknown msg_type: \(msgType ?? "nil")") |
|||
} |
|||
|
|||
} catch { |
|||
print("\(tag): Parse error: \(error)") |
|||
} |
|||
} |
|||
|
|||
private func processResult(_ json: [String: Any]) { |
|||
guard let dataStr = json["data"] as? String, |
|||
let dataData = dataStr.data(using: .utf8), |
|||
let data = try? JSONSerialization.jsonObject(with: dataData, options: []) as? [String: Any] else { |
|||
return |
|||
} |
|||
|
|||
let ls = data["ls"] as? Bool ?? false |
|||
guard let cn = data["cn"] as? [String: Any], |
|||
let st = cn["st"] as? [String: Any], |
|||
let rt = st["rt"] as? [[String: Any]] else { |
|||
return |
|||
} |
|||
|
|||
var resultTextTemp = "" |
|||
var punctuationCount = 0 |
|||
var wordCount = 0 |
|||
|
|||
for j in rt { |
|||
guard let ws = j["ws"] as? [[String: Any]] else { continue } |
|||
for kObj in ws { |
|||
guard let cw = kObj["cw"] as? [[String: Any]] else { continue } |
|||
|
|||
for lObj in cw { |
|||
let w = lObj["w"] as? String ?? "" |
|||
let wp = lObj["wp"] as? String ?? "" |
|||
|
|||
if wp == "p" { |
|||
punctuationCount += 1 |
|||
if resultTextTemp.isEmpty { |
|||
continue |
|||
} |
|||
} else { |
|||
wordCount += 1 |
|||
} |
|||
resultTextTemp += w |
|||
} |
|||
} |
|||
} |
|||
|
|||
if ls && wordCount == 0 && punctuationCount == 1 { |
|||
return |
|||
} |
|||
|
|||
let detectedLanguage = detectLanguage(from: resultTextTemp) |
|||
|
|||
let type = st["type"] as? String |
|||
let typeInt = st["type"] as? Int ?? Int(type ?? "") ?? -1 |
|||
|
|||
if typeInt == 0 { |
|||
callback?.onResult(sessionid: currentSessionId, resultTextTemp, detectedLanguage) |
|||
currentSessionId = "" |
|||
lastIntermediateResult = "" |
|||
} else { |
|||
if currentSessionId.isEmpty { |
|||
currentSessionId = UUID().uuidString |
|||
callback?.onSessionStarted(sessionid: currentSessionId) |
|||
} |
|||
lastIntermediateResult = resultTextTemp |
|||
callback?.onRecognizing(sessionid: currentSessionId, resultTextTemp, detectedLanguage) |
|||
} |
|||
} |
|||
|
|||
private func detectLanguage(from text: String) -> String { |
|||
if isAutoDetectLanguage { |
|||
if text.range(of: "[\\u4e00-\\u9fa5]", options: .regularExpression) != nil { |
|||
return "zh-CN" |
|||
} else { |
|||
return "en-US" |
|||
} |
|||
} else { |
|||
return currentLanguage |
|||
} |
|||
} |
|||
|
|||
private func getWebSocketUrl() -> String { |
|||
let baseWsUrl = "wss://office-api-ast-dx.iflyaisol.com/ast/communicate/v1" |
|||
|
|||
var params: [String: String] = [:] |
|||
params["audio_encode"] = "pcm_s16le" |
|||
params["lang"] = "autodialect" |
|||
params["samplerate"] = "16000" |
|||
params["accessKeyId"] = accessKeyId |
|||
params["appId"] = appId |
|||
params["utc"] = getUtcTime() |
|||
|
|||
let signature = calculateSignature(params: params) |
|||
params["signature"] = signature |
|||
|
|||
var urlComponents = URLComponents(string: baseWsUrl) |
|||
urlComponents?.queryItems = params.map { URLQueryItem(name: $0.key, value: $0.value) } |
|||
|
|||
return urlComponents?.url?.absoluteString ?? baseWsUrl |
|||
} |
|||
|
|||
private func getUtcTime() -> String { |
|||
let dateFormatter = DateFormatter() |
|||
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" |
|||
dateFormatter.timeZone = TimeZone(secondsFromGMT: 8 * 3600) // GMT+8 |
|||
return dateFormatter.string(from: Date()) |
|||
} |
|||
|
|||
private func calculateSignature(params: [String: String]) -> String { |
|||
let sortedKeys = params.keys.sorted() |
|||
var baseStr = "" |
|||
var first = true |
|||
|
|||
for key in sortedKeys { |
|||
if key == "signature" { continue } |
|||
guard let value = params[key], !value.isEmpty else { continue } |
|||
|
|||
if !first { |
|||
baseStr += "&" |
|||
} |
|||
|
|||
let encodedKey = key.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? key |
|||
let encodedValue = value.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? value |
|||
|
|||
baseStr += "\(encodedKey)=\(encodedValue)" |
|||
first = false |
|||
} |
|||
|
|||
let keyData = accessKeySecret.data(using: .utf8)! |
|||
let msgData = baseStr.data(using: .utf8)! |
|||
|
|||
if #available(iOS 13.0, *) { |
|||
let authentication = HMAC<Insecure.SHA1>.authenticationCode(for: msgData, using: SymmetricKey(data: keyData)) |
|||
return Data(authentication).base64EncodedString() |
|||
} else { |
|||
return "" |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue