Added Mistral Mini Cloud Model
This commit is contained in:
parent
ffca148417
commit
2f92a632a9
@ -223,6 +223,16 @@ import Foundation
|
|||||||
isMultilingual: true,
|
isMultilingual: true,
|
||||||
supportedLanguages: getLanguageDictionary(isMultilingual: true, provider: .deepgram)
|
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 = [
|
static let allLanguages = [
|
||||||
|
|||||||
@ -6,6 +6,7 @@ enum ModelProvider: String, Codable, Hashable, CaseIterable {
|
|||||||
case groq = "Groq"
|
case groq = "Groq"
|
||||||
case elevenLabs = "ElevenLabs"
|
case elevenLabs = "ElevenLabs"
|
||||||
case deepgram = "Deepgram"
|
case deepgram = "Deepgram"
|
||||||
|
case mistral = "Mistral"
|
||||||
case custom = "Custom"
|
case custom = "Custom"
|
||||||
case nativeApple = "Native Apple"
|
case nativeApple = "Native Apple"
|
||||||
// Future providers can be added here
|
// Future providers can be added here
|
||||||
|
|||||||
@ -30,7 +30,7 @@ enum AIProvider: String, CaseIterable {
|
|||||||
case .openRouter:
|
case .openRouter:
|
||||||
return "https://openrouter.ai/api/v1/chat/completions"
|
return "https://openrouter.ai/api/v1/chat/completions"
|
||||||
case .mistral:
|
case .mistral:
|
||||||
return "https://api.mistral.ai/v1/chat/completions"
|
return "https://api.mistral.ai/v1/audio/transcriptions"
|
||||||
case .elevenLabs:
|
case .elevenLabs:
|
||||||
return "https://api.elevenlabs.io/v1/speech-to-text"
|
return "https://api.elevenlabs.io/v1/speech-to-text"
|
||||||
case .ollama:
|
case .ollama:
|
||||||
@ -106,9 +106,7 @@ enum AIProvider: String, CaseIterable {
|
|||||||
]
|
]
|
||||||
case .mistral:
|
case .mistral:
|
||||||
return [
|
return [
|
||||||
"mistral-large-latest",
|
"voxtral-mini-2507"
|
||||||
"mistral-small-latest",
|
|
||||||
"mistral-saba-latest"
|
|
||||||
]
|
]
|
||||||
case .elevenLabs:
|
case .elevenLabs:
|
||||||
return ["scribe_v1", "scribe_v1_experimental"]
|
return ["scribe_v1", "scribe_v1_experimental"]
|
||||||
@ -298,6 +296,8 @@ class AIService: ObservableObject {
|
|||||||
verifyElevenLabsAPIKey(key, completion: completion)
|
verifyElevenLabsAPIKey(key, completion: completion)
|
||||||
case .deepgram:
|
case .deepgram:
|
||||||
verifyDeepgramAPIKey(key, completion: completion)
|
verifyDeepgramAPIKey(key, completion: completion)
|
||||||
|
case .mistral:
|
||||||
|
verifyMistralAPIKey(key, completion: completion)
|
||||||
default:
|
default:
|
||||||
verifyOpenAICompatibleAPIKey(key, completion: completion)
|
verifyOpenAICompatibleAPIKey(key, completion: completion)
|
||||||
}
|
}
|
||||||
@ -429,6 +429,37 @@ class AIService: ObservableObject {
|
|||||||
}.resume()
|
}.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) {
|
private func verifyDeepgramAPIKey(_ key: String, completion: @escaping (Bool) -> Void) {
|
||||||
let url = URL(string: "https://api.deepgram.com/v1/auth/token")!
|
let url = URL(string: "https://api.deepgram.com/v1/auth/token")!
|
||||||
var request = URLRequest(url: url)
|
var request = URLRequest(url: url)
|
||||||
|
|||||||
@ -38,6 +38,7 @@ class CloudTranscriptionService: TranscriptionService {
|
|||||||
private lazy var groqService = GroqTranscriptionService()
|
private lazy var groqService = GroqTranscriptionService()
|
||||||
private lazy var elevenLabsService = ElevenLabsTranscriptionService()
|
private lazy var elevenLabsService = ElevenLabsTranscriptionService()
|
||||||
private lazy var deepgramService = DeepgramTranscriptionService()
|
private lazy var deepgramService = DeepgramTranscriptionService()
|
||||||
|
private lazy var mistralService = MistralTranscriptionService()
|
||||||
private lazy var openAICompatibleService = OpenAICompatibleTranscriptionService()
|
private lazy var openAICompatibleService = OpenAICompatibleTranscriptionService()
|
||||||
|
|
||||||
func transcribe(audioURL: URL, model: any TranscriptionModel) async throws -> String {
|
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)
|
text = try await elevenLabsService.transcribe(audioURL: audioURL, model: model)
|
||||||
case .deepgram:
|
case .deepgram:
|
||||||
text = try await deepgramService.transcribe(audioURL: audioURL, model: model)
|
text = try await deepgramService.transcribe(audioURL: audioURL, model: model)
|
||||||
|
case .mistral:
|
||||||
|
text = try await mistralService.transcribe(audioURL: audioURL, model: model)
|
||||||
case .custom:
|
case .custom:
|
||||||
guard let customModel = model as? CustomCloudModel else {
|
guard let customModel = model as? CustomCloudModel else {
|
||||||
throw CloudTranscriptionError.unsupportedProvider
|
throw CloudTranscriptionError.unsupportedProvider
|
||||||
|
|||||||
@ -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
|
||||||
|
}
|
||||||
@ -34,6 +34,8 @@ struct CloudModelCardView: View {
|
|||||||
return "ElevenLabs"
|
return "ElevenLabs"
|
||||||
case .deepgram:
|
case .deepgram:
|
||||||
return "Deepgram"
|
return "Deepgram"
|
||||||
|
case .mistral:
|
||||||
|
return "Mistral"
|
||||||
default:
|
default:
|
||||||
return model.provider.rawValue
|
return model.provider.rawValue
|
||||||
}
|
}
|
||||||
@ -266,17 +268,24 @@ struct CloudModelCardView: View {
|
|||||||
isVerifying = true
|
isVerifying = true
|
||||||
verificationStatus = .verifying
|
verificationStatus = .verifying
|
||||||
|
|
||||||
// Set the provider in AIService temporarily for verification
|
switch model.provider {
|
||||||
let originalProvider = aiService.selectedProvider
|
case .groq:
|
||||||
if model.provider == .groq {
|
|
||||||
aiService.selectedProvider = .groq
|
aiService.selectedProvider = .groq
|
||||||
} else if model.provider == .elevenLabs {
|
case .elevenLabs:
|
||||||
aiService.selectedProvider = .elevenLabs
|
aiService.selectedProvider = .elevenLabs
|
||||||
} else if model.provider == .deepgram {
|
case .deepgram:
|
||||||
aiService.selectedProvider = .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 {
|
DispatchQueue.main.async {
|
||||||
self.isVerifying = false
|
self.isVerifying = false
|
||||||
if isValid {
|
if isValid {
|
||||||
@ -294,7 +303,7 @@ struct CloudModelCardView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Restore original provider
|
// Restore original provider
|
||||||
aiService.selectedProvider = originalProvider
|
// aiService.selectedProvider = originalProvider // This line was removed as per the new_code
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,7 +38,7 @@ struct ModelCardRowView: View {
|
|||||||
setDefaultAction: setDefaultAction
|
setDefaultAction: setDefaultAction
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
case .groq, .elevenLabs, .deepgram:
|
case .groq, .elevenLabs, .deepgram, .mistral:
|
||||||
if let cloudModel = model as? CloudModel {
|
if let cloudModel = model as? CloudModel {
|
||||||
CloudModelCardView(
|
CloudModelCardView(
|
||||||
model: cloudModel,
|
model: cloudModel,
|
||||||
|
|||||||
@ -192,7 +192,7 @@ struct ModelManagementView: View {
|
|||||||
case .local:
|
case .local:
|
||||||
return whisperState.allAvailableModels.filter { $0.provider == .local || $0.provider == .nativeApple }
|
return whisperState.allAvailableModels.filter { $0.provider == .local || $0.provider == .nativeApple }
|
||||||
case .cloud:
|
case .cloud:
|
||||||
let cloudProviders: [ModelProvider] = [.groq, .elevenLabs, .deepgram]
|
let cloudProviders: [ModelProvider] = [.groq, .elevenLabs, .deepgram, .mistral]
|
||||||
return whisperState.allAvailableModels.filter { cloudProviders.contains($0.provider) }
|
return whisperState.allAvailableModels.filter { cloudProviders.contains($0.provider) }
|
||||||
case .custom:
|
case .custom:
|
||||||
return whisperState.allAvailableModels.filter { $0.provider == .custom }
|
return whisperState.allAvailableModels.filter { $0.provider == .custom }
|
||||||
|
|||||||
@ -21,6 +21,9 @@ extension WhisperState {
|
|||||||
case .deepgram:
|
case .deepgram:
|
||||||
let key = UserDefaults.standard.string(forKey: "DeepgramAPIKey")
|
let key = UserDefaults.standard.string(forKey: "DeepgramAPIKey")
|
||||||
return key != nil && !key!.isEmpty
|
return key != nil && !key!.isEmpty
|
||||||
|
case .mistral:
|
||||||
|
let key = UserDefaults.standard.string(forKey: "MistralAPIKey")
|
||||||
|
return key != nil && !key!.isEmpty
|
||||||
case .custom:
|
case .custom:
|
||||||
// Custom models are always usable since they contain their own API keys
|
// Custom models are always usable since they contain their own API keys
|
||||||
return true
|
return true
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user