Merge pull request #358 from tmm22/fix/production-critical-safety-improvements
Fix critical production safety issues
This commit is contained in:
commit
37f3ee25e4
@ -37,7 +37,9 @@ class ElevenLabsTranscriptionService {
|
||||
throw CloudTranscriptionError.missingAPIKey
|
||||
}
|
||||
|
||||
let apiURL = URL(string: "https://api.elevenlabs.io/v1/speech-to-text")!
|
||||
guard let apiURL = URL(string: "https://api.elevenlabs.io/v1/speech-to-text") else {
|
||||
throw NSError(domain: "ElevenLabsTranscriptionService", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid API URL"])
|
||||
}
|
||||
return APIConfig(url: apiURL, apiKey: apiKey, modelName: model.name)
|
||||
}
|
||||
|
||||
|
||||
@ -40,7 +40,9 @@ class GroqTranscriptionService {
|
||||
throw CloudTranscriptionError.missingAPIKey
|
||||
}
|
||||
|
||||
let apiURL = URL(string: "https://api.groq.com/openai/v1/audio/transcriptions")!
|
||||
guard let apiURL = URL(string: "https://api.groq.com/openai/v1/audio/transcriptions") else {
|
||||
throw NSError(domain: "GroqTranscriptionService", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid API URL"])
|
||||
}
|
||||
return APIConfig(url: apiURL, apiKey: apiKey, modelName: model.name)
|
||||
}
|
||||
|
||||
|
||||
@ -12,7 +12,9 @@ class MistralTranscriptionService {
|
||||
throw CloudTranscriptionError.missingAPIKey
|
||||
}
|
||||
|
||||
let url = URL(string: "https://api.mistral.ai/v1/audio/transcriptions")!
|
||||
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"
|
||||
|
||||
|
||||
@ -5,8 +5,12 @@ class OpenAICompatibleTranscriptionService {
|
||||
private let logger = Logger(subsystem: "com.prakashjoshipax.voiceink", category: "OpenAICompatibleService")
|
||||
|
||||
func transcribe(audioURL: URL, model: CustomCloudModel) async throws -> String {
|
||||
guard let url = URL(string: model.apiEndpoint) else {
|
||||
throw NSError(domain: "OpenAICompatibleTranscriptionService", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid API endpoint URL"])
|
||||
}
|
||||
|
||||
let config = APIConfig(
|
||||
url: URL(string: model.apiEndpoint)!,
|
||||
url: url,
|
||||
apiKey: model.apiKey,
|
||||
modelName: model.modelName
|
||||
)
|
||||
|
||||
@ -10,6 +10,7 @@ import FluidAudio
|
||||
struct VoiceInkApp: App {
|
||||
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
|
||||
let container: ModelContainer
|
||||
let containerInitializationFailed: Bool
|
||||
|
||||
@StateObject private var whisperState: WhisperState
|
||||
@StateObject private var hotkeyManager: HotkeyManager
|
||||
@ -37,32 +38,53 @@ struct VoiceInkApp: App {
|
||||
UserDefaults.standard.set(hasEnabledPowerModes, forKey: "powerModeUIFlag")
|
||||
}
|
||||
|
||||
do {
|
||||
let schema = Schema([
|
||||
Transcription.self
|
||||
])
|
||||
let logger = Logger(subsystem: "com.prakashjoshipax.voiceink", category: "Initialization")
|
||||
let schema = Schema([Transcription.self])
|
||||
var initializationFailed = false
|
||||
|
||||
// Attempt 1: Try persistent storage
|
||||
if let persistentContainer = Self.createPersistentContainer(schema: schema, logger: logger) {
|
||||
container = persistentContainer
|
||||
|
||||
// Create app-specific Application Support directory URL
|
||||
let appSupportURL = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
|
||||
.appendingPathComponent("com.prakashjoshipax.VoiceInk", isDirectory: true)
|
||||
|
||||
// Create the directory if it doesn't exist
|
||||
try? FileManager.default.createDirectory(at: appSupportURL, withIntermediateDirectories: true)
|
||||
|
||||
// Configure SwiftData to use the conventional location
|
||||
let storeURL = appSupportURL.appendingPathComponent("default.store")
|
||||
let modelConfiguration = ModelConfiguration(schema: schema, url: storeURL)
|
||||
|
||||
container = try ModelContainer(for: schema, configurations: [modelConfiguration])
|
||||
|
||||
// Print SwiftData storage location
|
||||
if let url = container.mainContext.container.configurations.first?.url {
|
||||
#if DEBUG
|
||||
// Print SwiftData storage location in debug builds only
|
||||
if let url = persistentContainer.mainContext.container.configurations.first?.url {
|
||||
print("💾 SwiftData storage location: \(url.path)")
|
||||
}
|
||||
|
||||
} catch {
|
||||
fatalError("Failed to create ModelContainer for Transcription: \(error.localizedDescription)")
|
||||
#endif
|
||||
}
|
||||
// Attempt 2: Try in-memory storage
|
||||
else if let memoryContainer = Self.createInMemoryContainer(schema: schema, logger: logger) {
|
||||
container = memoryContainer
|
||||
|
||||
logger.warning("Using in-memory storage as fallback. Data will not persist between sessions.")
|
||||
|
||||
// Show alert to user about storage issue
|
||||
DispatchQueue.main.async {
|
||||
let alert = NSAlert()
|
||||
alert.messageText = "Storage Warning"
|
||||
alert.informativeText = "VoiceInk couldn't access its storage location. Your transcriptions will not be saved between sessions."
|
||||
alert.alertStyle = .warning
|
||||
alert.addButton(withTitle: "OK")
|
||||
alert.runModal()
|
||||
}
|
||||
}
|
||||
// Attempt 3: Try ultra-minimal default container
|
||||
else if let minimalContainer = Self.createMinimalContainer(schema: schema, logger: logger) {
|
||||
container = minimalContainer
|
||||
logger.warning("Using minimal emergency container")
|
||||
}
|
||||
// All attempts failed: Create disabled container and mark for termination
|
||||
else {
|
||||
logger.critical("All ModelContainer initialization attempts failed")
|
||||
initializationFailed = true
|
||||
|
||||
// Create a dummy container to satisfy Swift's initialization requirements
|
||||
// App will show error and terminate in onAppear
|
||||
container = Self.createDummyContainer(schema: schema)
|
||||
}
|
||||
|
||||
containerInitializationFailed = initializationFailed
|
||||
|
||||
// Initialize services with proper sharing of instances
|
||||
let aiService = AIService()
|
||||
@ -97,6 +119,69 @@ struct VoiceInkApp: App {
|
||||
AppShortcuts.updateAppShortcutParameters()
|
||||
}
|
||||
|
||||
// MARK: - Container Creation Helpers
|
||||
|
||||
private static func createPersistentContainer(schema: Schema, logger: Logger) -> ModelContainer? {
|
||||
do {
|
||||
// Create app-specific Application Support directory URL
|
||||
let appSupportURL = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
|
||||
.appendingPathComponent("com.prakashjoshipax.VoiceInk", isDirectory: true)
|
||||
|
||||
// Create the directory if it doesn't exist
|
||||
try? FileManager.default.createDirectory(at: appSupportURL, withIntermediateDirectories: true)
|
||||
|
||||
// Configure SwiftData to use the conventional location
|
||||
let storeURL = appSupportURL.appendingPathComponent("default.store")
|
||||
let modelConfiguration = ModelConfiguration(schema: schema, url: storeURL)
|
||||
|
||||
return try ModelContainer(for: schema, configurations: [modelConfiguration])
|
||||
} catch {
|
||||
logger.error("Failed to create persistent ModelContainer: \(error.localizedDescription)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private static func createInMemoryContainer(schema: Schema, logger: Logger) -> ModelContainer? {
|
||||
do {
|
||||
let configuration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: true)
|
||||
return try ModelContainer(for: schema, configurations: [configuration])
|
||||
} catch {
|
||||
logger.error("Failed to create in-memory ModelContainer: \(error.localizedDescription)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private static func createMinimalContainer(schema: Schema, logger: Logger) -> ModelContainer? {
|
||||
do {
|
||||
// Try default initializer without custom configuration
|
||||
return try ModelContainer(for: schema)
|
||||
} catch {
|
||||
logger.error("Failed to create minimal ModelContainer: \(error.localizedDescription)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private static func createDummyContainer(schema: Schema) -> ModelContainer {
|
||||
// Create an absolute minimal container for initialization
|
||||
// This uses in-memory storage and will never actually be used
|
||||
// as the app will show an error and terminate in onAppear
|
||||
let config = ModelConfiguration(schema: schema, isStoredInMemoryOnly: true)
|
||||
|
||||
// Note: In-memory containers should always succeed unless SwiftData itself is unavailable
|
||||
// (which would indicate a serious system-level issue). We use preconditionFailure here
|
||||
// rather than fatalError because:
|
||||
// 1. This code is only reached after 3 prior initialization attempts have failed
|
||||
// 2. An in-memory container failing indicates SwiftData is completely unavailable
|
||||
// 3. Swift requires non-optional container property to be initialized
|
||||
// 4. The app will immediately terminate in onAppear when containerInitializationFailed is checked
|
||||
do {
|
||||
return try ModelContainer(for: schema, configurations: [config])
|
||||
} catch {
|
||||
// This indicates a system-level SwiftData failure - app cannot function
|
||||
preconditionFailure("Unable to create even a dummy ModelContainer. SwiftData is unavailable: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
if hasCompletedOnboarding {
|
||||
@ -109,6 +194,19 @@ struct VoiceInkApp: App {
|
||||
.environmentObject(enhancementService)
|
||||
.modelContainer(container)
|
||||
.onAppear {
|
||||
// Check if container initialization failed
|
||||
if containerInitializationFailed {
|
||||
let alert = NSAlert()
|
||||
alert.messageText = "Critical Storage Error"
|
||||
alert.informativeText = "VoiceInk cannot initialize its storage system. The app cannot continue.\n\nPlease try reinstalling the app or contact support if the issue persists."
|
||||
alert.alertStyle = .critical
|
||||
alert.addButton(withTitle: "Quit")
|
||||
alert.runModal()
|
||||
|
||||
NSApplication.shared.terminate(nil)
|
||||
return
|
||||
}
|
||||
|
||||
updaterViewModel.silentlyCheckForUpdates()
|
||||
if enableAnnouncements {
|
||||
AnnouncementsService.shared.start()
|
||||
|
||||
@ -106,8 +106,8 @@ class WhisperPrompt: ObservableObject {
|
||||
return customPrompt
|
||||
}
|
||||
|
||||
// Otherwise return the default prompt
|
||||
return languagePrompts[language] ?? languagePrompts["default"]!
|
||||
// Otherwise return the default prompt, with safe fallback
|
||||
return languagePrompts[language] ?? languagePrompts["default"] ?? ""
|
||||
}
|
||||
|
||||
func setCustomPrompt(_ prompt: String, for language: String) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user