Added Mistral Mini Cloud Model

This commit is contained in:
Beingpax 2025-07-17 17:26:44 +05:45
parent ffca148417
commit 2f92a632a9
9 changed files with 140 additions and 13 deletions

View File

@ -223,6 +223,16 @@ import Foundation
isMultilingual: true,
supportedLanguages: getLanguageDictionary(isMultilingual: true, provider: .deepgram)
),
CloudModel(
name: "voxtral-mini-2507",
displayName: "Voxtral Mini (Mistral)",
description: "Mistral's latest SOTA transcription model.",
provider: .mistral,
speed: 0.8,
accuracy: 0.97,
isMultilingual: true,
supportedLanguages: getLanguageDictionary(isMultilingual: true, provider: .mistral)
)
]
static let allLanguages = [

View File

@ -6,6 +6,7 @@ enum ModelProvider: String, Codable, Hashable, CaseIterable {
case groq = "Groq"
case elevenLabs = "ElevenLabs"
case deepgram = "Deepgram"
case mistral = "Mistral"
case custom = "Custom"
case nativeApple = "Native Apple"
// Future providers can be added here

View File

@ -30,7 +30,7 @@ enum AIProvider: String, CaseIterable {
case .openRouter:
return "https://openrouter.ai/api/v1/chat/completions"
case .mistral:
return "https://api.mistral.ai/v1/chat/completions"
return "https://api.mistral.ai/v1/audio/transcriptions"
case .elevenLabs:
return "https://api.elevenlabs.io/v1/speech-to-text"
case .ollama:
@ -106,9 +106,7 @@ enum AIProvider: String, CaseIterable {
]
case .mistral:
return [
"mistral-large-latest",
"mistral-small-latest",
"mistral-saba-latest"
"voxtral-mini-2507"
]
case .elevenLabs:
return ["scribe_v1", "scribe_v1_experimental"]
@ -298,6 +296,8 @@ class AIService: ObservableObject {
verifyElevenLabsAPIKey(key, completion: completion)
case .deepgram:
verifyDeepgramAPIKey(key, completion: completion)
case .mistral:
verifyMistralAPIKey(key, completion: completion)
default:
verifyOpenAICompatibleAPIKey(key, completion: completion)
}
@ -429,6 +429,37 @@ class AIService: ObservableObject {
}.resume()
}
private func verifyMistralAPIKey(_ key: String, completion: @escaping (Bool) -> Void) {
let url = URL(string: "https://api.mistral.ai/v1/models")!
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.addValue("Bearer \(key)", forHTTPHeaderField: "Authorization")
URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
self.logger.error("Mistral API key verification failed: \(error.localizedDescription)")
completion(false)
return
}
if let httpResponse = response as? HTTPURLResponse {
if httpResponse.statusCode == 200 {
completion(true)
} else {
if let data = data, let body = String(data: data, encoding: .utf8) {
self.logger.error("Mistral API key verification failed with status code \(httpResponse.statusCode): \(body)")
} else {
self.logger.error("Mistral API key verification failed with status code \(httpResponse.statusCode) and no response body.")
}
completion(false)
}
} else {
self.logger.error("Mistral API key verification failed: Invalid response from server.")
completion(false)
}
}.resume()
}
private func verifyDeepgramAPIKey(_ key: String, completion: @escaping (Bool) -> Void) {
let url = URL(string: "https://api.deepgram.com/v1/auth/token")!
var request = URLRequest(url: url)

View File

@ -38,6 +38,7 @@ class CloudTranscriptionService: TranscriptionService {
private lazy var groqService = GroqTranscriptionService()
private lazy var elevenLabsService = ElevenLabsTranscriptionService()
private lazy var deepgramService = DeepgramTranscriptionService()
private lazy var mistralService = MistralTranscriptionService()
private lazy var openAICompatibleService = OpenAICompatibleTranscriptionService()
func transcribe(audioURL: URL, model: any TranscriptionModel) async throws -> String {
@ -50,6 +51,8 @@ class CloudTranscriptionService: TranscriptionService {
text = try await elevenLabsService.transcribe(audioURL: audioURL, model: model)
case .deepgram:
text = try await deepgramService.transcribe(audioURL: audioURL, model: model)
case .mistral:
text = try await mistralService.transcribe(audioURL: audioURL, model: model)
case .custom:
guard let customModel = model as? CustomCloudModel else {
throw CloudTranscriptionError.unsupportedProvider

View File

@ -0,0 +1,70 @@
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)")
let apiKey = UserDefaults.standard.string(forKey: "MistralAPIKey") ?? ""
guard !apiKey.isEmpty else {
logger.error("Mistral API key is missing.")
throw CloudTranscriptionError.missingAPIKey
}
let url = URL(string: "https://api.mistral.ai/v1/audio/transcriptions")!
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("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
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
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/mpeg\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
}

View File

@ -34,6 +34,8 @@ struct CloudModelCardView: View {
return "ElevenLabs"
case .deepgram:
return "Deepgram"
case .mistral:
return "Mistral"
default:
return model.provider.rawValue
}
@ -266,17 +268,24 @@ struct CloudModelCardView: View {
isVerifying = true
verificationStatus = .verifying
// Set the provider in AIService temporarily for verification
let originalProvider = aiService.selectedProvider
if model.provider == .groq {
switch model.provider {
case .groq:
aiService.selectedProvider = .groq
} else if model.provider == .elevenLabs {
case .elevenLabs:
aiService.selectedProvider = .elevenLabs
} else if model.provider == .deepgram {
case .deepgram:
aiService.selectedProvider = .deepgram
case .mistral:
aiService.selectedProvider = .mistral
default:
// This case should ideally not be hit for cloud models in this view
print("Warning: verifyAPIKey called for unsupported provider \(model.provider.rawValue)")
isVerifying = false
verificationStatus = .failure
return
}
aiService.verifyAPIKey(apiKey) { [self] isValid in
aiService.saveAPIKey(apiKey) { isValid in
DispatchQueue.main.async {
self.isVerifying = false
if isValid {
@ -294,7 +303,7 @@ struct CloudModelCardView: View {
}
// Restore original provider
aiService.selectedProvider = originalProvider
// aiService.selectedProvider = originalProvider // This line was removed as per the new_code
}
}
}

View File

@ -38,7 +38,7 @@ struct ModelCardRowView: View {
setDefaultAction: setDefaultAction
)
}
case .groq, .elevenLabs, .deepgram:
case .groq, .elevenLabs, .deepgram, .mistral:
if let cloudModel = model as? CloudModel {
CloudModelCardView(
model: cloudModel,

View File

@ -192,7 +192,7 @@ struct ModelManagementView: View {
case .local:
return whisperState.allAvailableModels.filter { $0.provider == .local || $0.provider == .nativeApple }
case .cloud:
let cloudProviders: [ModelProvider] = [.groq, .elevenLabs, .deepgram]
let cloudProviders: [ModelProvider] = [.groq, .elevenLabs, .deepgram, .mistral]
return whisperState.allAvailableModels.filter { cloudProviders.contains($0.provider) }
case .custom:
return whisperState.allAvailableModels.filter { $0.provider == .custom }

View File

@ -21,6 +21,9 @@ extension WhisperState {
case .deepgram:
let key = UserDefaults.standard.string(forKey: "DeepgramAPIKey")
return key != nil && !key!.isEmpty
case .mistral:
let key = UserDefaults.standard.string(forKey: "MistralAPIKey")
return key != nil && !key!.isEmpty
case .custom:
// Custom models are always usable since they contain their own API keys
return true