vOOice/VoiceInk/Models/TranscriptionModel.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

199 lines
6.7 KiB
Swift

import Foundation
// Enum to differentiate between model providers
enum ModelProvider: String, Codable, Hashable, CaseIterable {
case local = "Local"
case parakeet = "Parakeet"
case groq = "Groq"
case elevenLabs = "ElevenLabs"
case deepgram = "Deepgram"
case mistral = "Mistral"
case gemini = "Gemini"
case soniox = "Soniox"
case custom = "Custom"
case nativeApple = "Native Apple"
// Future providers can be added here
}
// A unified protocol for any transcription model
protocol TranscriptionModel: Identifiable, Hashable {
var id: UUID { get }
var name: String { get }
var displayName: String { get }
var description: String { get }
var provider: ModelProvider { get }
// Language capabilities
var isMultilingualModel: Bool { get }
var supportedLanguages: [String: String] { get }
}
extension TranscriptionModel {
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
var language: String {
isMultilingualModel ? "Multilingual" : "English-only"
}
}
// A new struct for Apple's native models
struct NativeAppleModel: TranscriptionModel {
let id = UUID()
let name: String
let displayName: String
let description: String
let provider: ModelProvider = .nativeApple
let isMultilingualModel: Bool
let supportedLanguages: [String: String]
}
// A new struct for Parakeet models
struct ParakeetModel: TranscriptionModel {
let id = UUID()
let name: String
let displayName: String
let description: String
let provider: ModelProvider = .parakeet
let size: String
let speed: Double
let accuracy: Double
let ramUsage: Double
var isMultilingualModel: Bool {
supportedLanguages.count > 1
}
let supportedLanguages: [String: String]
}
// A new struct for cloud models
struct CloudModel: TranscriptionModel {
let id: UUID
let name: String
let displayName: String
let description: String
let provider: ModelProvider
let speed: Double
let accuracy: Double
let isMultilingualModel: Bool
let supportedLanguages: [String: String]
init(id: UUID = UUID(), name: String, displayName: String, description: String, provider: ModelProvider, speed: Double, accuracy: Double, isMultilingual: Bool, supportedLanguages: [String: String]) {
self.id = id
self.name = name
self.displayName = displayName
self.description = description
self.provider = provider
self.speed = speed
self.accuracy = accuracy
self.isMultilingualModel = isMultilingual
self.supportedLanguages = supportedLanguages
}
}
/// Custom cloud model with API key stored in Keychain.
struct CustomCloudModel: TranscriptionModel, Codable {
let id: UUID
let name: String
let displayName: String
let description: String
let provider: ModelProvider = .custom
let apiEndpoint: String
let modelName: String
let isMultilingualModel: Bool
let supportedLanguages: [String: String]
/// API key retrieved from Keychain by model ID.
var apiKey: String {
APIKeyManager.shared.getCustomModelAPIKey(forModelId: id) ?? ""
}
init(id: UUID = UUID(), name: String, displayName: String, description: String, apiEndpoint: String, modelName: String, isMultilingual: Bool = true, supportedLanguages: [String: String]? = nil) {
self.id = id
self.name = name
self.displayName = displayName
self.description = description
self.apiEndpoint = apiEndpoint
self.modelName = modelName
self.isMultilingualModel = isMultilingual
self.supportedLanguages = supportedLanguages ?? PredefinedModels.getLanguageDictionary(isMultilingual: isMultilingual)
}
/// Custom Codable to migrate legacy apiKey from JSON to Keychain.
private enum CodingKeys: String, CodingKey {
case id, name, displayName, description, apiEndpoint, modelName, isMultilingualModel, supportedLanguages
case apiKey
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(UUID.self, forKey: .id)
name = try container.decode(String.self, forKey: .name)
displayName = try container.decode(String.self, forKey: .displayName)
description = try container.decode(String.self, forKey: .description)
apiEndpoint = try container.decode(String.self, forKey: .apiEndpoint)
modelName = try container.decode(String.self, forKey: .modelName)
isMultilingualModel = try container.decode(Bool.self, forKey: .isMultilingualModel)
supportedLanguages = try container.decode([String: String].self, forKey: .supportedLanguages)
if let legacyApiKey = try container.decodeIfPresent(String.self, forKey: .apiKey), !legacyApiKey.isEmpty {
APIKeyManager.shared.saveCustomModelAPIKey(legacyApiKey, forModelId: id)
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(id, forKey: .id)
try container.encode(name, forKey: .name)
try container.encode(displayName, forKey: .displayName)
try container.encode(description, forKey: .description)
try container.encode(apiEndpoint, forKey: .apiEndpoint)
try container.encode(modelName, forKey: .modelName)
try container.encode(isMultilingualModel, forKey: .isMultilingualModel)
try container.encode(supportedLanguages, forKey: .supportedLanguages)
}
}
struct LocalModel: TranscriptionModel {
let id = UUID()
let name: String
let displayName: String
let size: String
let supportedLanguages: [String: String]
let description: String
let speed: Double
let accuracy: Double
let ramUsage: Double
let provider: ModelProvider = .local
var downloadURL: String {
"https://huggingface.co/ggerganov/whisper.cpp/resolve/main/\(filename)"
}
var filename: String {
"\(name).bin"
}
var isMultilingualModel: Bool {
supportedLanguages.count > 1
}
}
// User-imported local models
struct ImportedLocalModel: TranscriptionModel {
let id = UUID()
let name: String
let displayName: String
let description: String
let provider: ModelProvider = .local
let isMultilingualModel: Bool
let supportedLanguages: [String: String]
init(fileBaseName: String) {
self.name = fileBaseName
self.displayName = fileBaseName
self.description = "Imported local model"
self.isMultilingualModel = true
self.supportedLanguages = PredefinedModels.getLanguageDictionary(isMultilingual: true, provider: .local)
}
}