vOOice/VoiceInk/Services/CloudTranscription/MistralTranscriptionService.swift
Beingpax 948033ac28 Migrate API key storage to Keychain with iCloud sync
Move API keys from UserDefaults to secure Keychain storage. Add KeychainService and APIKeyManager for centralized key management. Enable iCloud Keychain sync for cross-device sharing between macOS and iOS.
2026-01-05 22:28:34 +05:45

73 lines
3.4 KiB
Swift

import Foundation
import os
class MistralTranscriptionService {
private let logger = Logger(subsystem: "com.prakashjoshipax.voiceink", category: "MistralTranscriptionService")
func transcribe(audioURL: URL, model: any TranscriptionModel) async throws -> String {
logger.notice("Sending transcription request to Mistral for model: \(model.name)")
guard let apiKey = APIKeyManager.shared.getAPIKey(forProvider: "Mistral"), !apiKey.isEmpty else {
logger.error("Mistral API key is missing.")
throw CloudTranscriptionError.missingAPIKey
}
guard let url = URL(string: "https://api.mistral.ai/v1/audio/transcriptions") else {
throw NSError(domain: "MistralTranscriptionService", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid API URL"])
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
let boundary = "Boundary-\(UUID().uuidString)"
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
request.setValue(apiKey, forHTTPHeaderField: "x-api-key")
var body = Data()
// Add model field
body.append("--\(boundary)\r\n".data(using: .utf8)!)
body.append("Content-Disposition: form-data; name=\"model\"\r\n\r\n".data(using: .utf8)!)
body.append(model.name.data(using: .utf8)!)
body.append("\r\n".data(using: .utf8)!)
// Add file data - matching Python SDK structure (no language field as it's commented out in all Python examples)
guard let audioData = try? Data(contentsOf: audioURL) else {
throw CloudTranscriptionError.audioFileNotFound
}
body.append("--\(boundary)\r\n".data(using: .utf8)!)
body.append("Content-Disposition: form-data; name=\"file\"; filename=\"\(audioURL.lastPathComponent)\"\r\n".data(using: .utf8)!)
body.append("Content-Type: audio/wav\r\n\r\n".data(using: .utf8)!)
body.append(audioData)
body.append("\r\n".data(using: .utf8)!)
body.append("--\(boundary)--\r\n".data(using: .utf8)!)
request.httpBody = body
do {
let (data, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
let errorResponse = String(data: data, encoding: .utf8) ?? "No response body"
logger.error("Mistral transcription request failed with status code \((response as? HTTPURLResponse)?.statusCode ?? 500): \(errorResponse)")
throw CloudTranscriptionError.apiRequestFailed(statusCode: (response as? HTTPURLResponse)?.statusCode ?? 500, message: errorResponse)
}
do {
let transcriptionResponse = try JSONDecoder().decode(MistralTranscriptionResponse.self, from: data)
logger.notice("Successfully received transcription from Mistral.")
return transcriptionResponse.text
} catch {
logger.error("Failed to decode Mistral response: \(error.localizedDescription)")
throw CloudTranscriptionError.noTranscriptionReturned
}
} catch {
logger.error("Mistral transcription request threw an error: \(error.localizedDescription)")
throw error
}
}
}
struct MistralTranscriptionResponse: Codable {
let text: String
}