From 590b0366d358df3a36aff96fc9b010eda8bcdb72 Mon Sep 17 00:00:00 2001 From: Carsten Abele Date: Tue, 7 Apr 2026 19:38:56 +0200 Subject: [PATCH] feat: add Voxtral WebSocket client with connect/send/receive --- .../Network/VoxtralWebSocketClient.swift | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 MyVoxtral/MyVoxtral/Network/VoxtralWebSocketClient.swift diff --git a/MyVoxtral/MyVoxtral/Network/VoxtralWebSocketClient.swift b/MyVoxtral/MyVoxtral/Network/VoxtralWebSocketClient.swift new file mode 100644 index 0000000..8283026 --- /dev/null +++ b/MyVoxtral/MyVoxtral/Network/VoxtralWebSocketClient.swift @@ -0,0 +1,90 @@ +import Foundation + +@MainActor +final class VoxtralWebSocketClient { + private var webSocketTask: URLSessionWebSocketTask? + private var session: URLSession? + private let encoder = JSONEncoder() + + var onEvent: ((VoxtralEvent) -> Void)? + + func connect(apiKey: String, delayMs: Int) { + guard let url = URL(string: "wss://api.mistral.ai/v1/audio/transcriptions/realtime") else { return } + + var request = URLRequest(url: url) + request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization") + + session = URLSession(configuration: .default) + webSocketTask = session?.webSocketTask(with: request) + webSocketTask?.resume() + + // Send session config + let config = SessionUpdateMessage( + session: SessionConfig( + audioFormat: AudioFormatConfig(), + targetStreamingDelayMs: delayMs + ) + ) + sendJSON(config) + + // Start receiving + receiveLoop() + } + + func sendAudio(_ pcmData: Data) { + let base64 = pcmData.base64EncodedString() + let msg = AudioAppendMessage(audio: base64) + sendJSON(msg) + } + + func flush() { + sendJSON(AudioFlushMessage()) + } + + func disconnect() { + sendJSON(AudioEndMessage()) + webSocketTask?.cancel(with: .normalClosure, reason: nil) + webSocketTask = nil + session?.invalidateAndCancel() + session = nil + } + + private func sendJSON(_ value: T) { + guard let data = try? encoder.encode(value), + let string = String(data: data, encoding: .utf8) else { return } + webSocketTask?.send(.string(string)) { error in + if let error { + print("WebSocket send error: \(error)") + } + } + } + + private func receiveLoop() { + webSocketTask?.receive { [weak self] result in + switch result { + case .success(let message): + switch message { + case .string(let text): + if let data = text.data(using: .utf8) { + let event = parseVoxtralEvent(from: data) + Task { @MainActor in + self?.onEvent?(event) + } + } + case .data(let data): + let event = parseVoxtralEvent(from: data) + Task { @MainActor in + self?.onEvent?(event) + } + @unknown default: + break + } + self?.receiveLoop() + case .failure(let error): + Task { @MainActor in + self?.onEvent?(.error("Connection lost: \(error.localizedDescription)")) + } + } + } + } +}