Support for zero data retention on

This commit is contained in:
Beingpax 2025-08-07 01:18:35 +05:45
parent d23d54e208
commit 2970895376
9 changed files with 133 additions and 7 deletions

View File

@ -11,4 +11,5 @@ extension Notification.Name {
static let navigateToDestination = Notification.Name("navigateToDestination") static let navigateToDestination = Notification.Name("navigateToDestination")
static let promptSelectionChanged = Notification.Name("promptSelectionChanged") static let promptSelectionChanged = Notification.Name("promptSelectionChanged")
static let powerModeConfigurationApplied = Notification.Name("powerModeConfigurationApplied") static let powerModeConfigurationApplied = Notification.Name("powerModeConfigurationApplied")
static let transcriptionCreated = Notification.Name("transcriptionCreated")
} }

View File

@ -137,6 +137,7 @@ class AudioTranscriptionManager: ObservableObject {
) )
modelContext.insert(transcription) modelContext.insert(transcription)
try modelContext.save() try modelContext.save()
NotificationCenter.default.post(name: .transcriptionCreated, object: transcription)
currentTranscription = transcription currentTranscription = transcription
} catch { } catch {
logger.error("Enhancement failed: \(error.localizedDescription)") logger.error("Enhancement failed: \(error.localizedDescription)")
@ -149,6 +150,7 @@ class AudioTranscriptionManager: ObservableObject {
) )
modelContext.insert(transcription) modelContext.insert(transcription)
try modelContext.save() try modelContext.save()
NotificationCenter.default.post(name: .transcriptionCreated, object: transcription)
currentTranscription = transcription currentTranscription = transcription
} }
} else { } else {
@ -161,6 +163,7 @@ class AudioTranscriptionManager: ObservableObject {
) )
modelContext.insert(transcription) modelContext.insert(transcription)
try modelContext.save() try modelContext.save()
NotificationCenter.default.post(name: .transcriptionCreated, object: transcription)
currentTranscription = transcription currentTranscription = transcription
} }

View File

@ -110,6 +110,7 @@ class AudioTranscriptionService: ObservableObject {
modelContext.insert(newTranscription) modelContext.insert(newTranscription)
do { do {
try modelContext.save() try modelContext.save()
NotificationCenter.default.post(name: .transcriptionCreated, object: newTranscription)
} catch { } catch {
logger.error("❌ Failed to save transcription: \(error.localizedDescription)") logger.error("❌ Failed to save transcription: \(error.localizedDescription)")
} }
@ -130,6 +131,7 @@ class AudioTranscriptionService: ObservableObject {
modelContext.insert(newTranscription) modelContext.insert(newTranscription)
do { do {
try modelContext.save() try modelContext.save()
NotificationCenter.default.post(name: .transcriptionCreated, object: newTranscription)
} catch { } catch {
logger.error("❌ Failed to save transcription: \(error.localizedDescription)") logger.error("❌ Failed to save transcription: \(error.localizedDescription)")
} }

View File

@ -13,6 +13,7 @@ struct GeneralSettings: Codable {
let isMenuBarOnly: Bool? let isMenuBarOnly: Bool?
let useAppleScriptPaste: Bool? let useAppleScriptPaste: Bool?
let recorderType: String? let recorderType: String?
let doNotMaintainTranscriptHistory: Bool?
let isAudioCleanupEnabled: Bool? let isAudioCleanupEnabled: Bool?
let audioRetentionPeriod: Int? let audioRetentionPeriod: Int?
@ -43,6 +44,7 @@ class ImportExportService {
private let keyIsMenuBarOnly = "IsMenuBarOnly" private let keyIsMenuBarOnly = "IsMenuBarOnly"
private let keyUseAppleScriptPaste = "UseAppleScriptPaste" private let keyUseAppleScriptPaste = "UseAppleScriptPaste"
private let keyRecorderType = "RecorderType" private let keyRecorderType = "RecorderType"
private let keyDoNotMaintainTranscriptHistory = "DoNotMaintainTranscriptHistory"
private let keyIsAudioCleanupEnabled = "IsAudioCleanupEnabled" private let keyIsAudioCleanupEnabled = "IsAudioCleanupEnabled"
private let keyAudioRetentionPeriod = "AudioRetentionPeriod" private let keyAudioRetentionPeriod = "AudioRetentionPeriod"
@ -87,6 +89,7 @@ class ImportExportService {
isMenuBarOnly: menuBarManager.isMenuBarOnly, isMenuBarOnly: menuBarManager.isMenuBarOnly,
useAppleScriptPaste: UserDefaults.standard.bool(forKey: keyUseAppleScriptPaste), useAppleScriptPaste: UserDefaults.standard.bool(forKey: keyUseAppleScriptPaste),
recorderType: whisperState.recorderType, recorderType: whisperState.recorderType,
doNotMaintainTranscriptHistory: UserDefaults.standard.bool(forKey: keyDoNotMaintainTranscriptHistory),
isAudioCleanupEnabled: UserDefaults.standard.bool(forKey: keyIsAudioCleanupEnabled), isAudioCleanupEnabled: UserDefaults.standard.bool(forKey: keyIsAudioCleanupEnabled),
audioRetentionPeriod: UserDefaults.standard.integer(forKey: keyAudioRetentionPeriod), audioRetentionPeriod: UserDefaults.standard.integer(forKey: keyAudioRetentionPeriod),
@ -230,6 +233,9 @@ class ImportExportService {
if let recType = general.recorderType { if let recType = general.recorderType {
whisperState.recorderType = recType whisperState.recorderType = recType
} }
if let doNotMaintainHistory = general.doNotMaintainTranscriptHistory {
UserDefaults.standard.set(doNotMaintainHistory, forKey: self.keyDoNotMaintainTranscriptHistory)
}
if let audioCleanup = general.isAudioCleanupEnabled { if let audioCleanup = general.isAudioCleanupEnabled {
UserDefaults.standard.set(audioCleanup, forKey: self.keyIsAudioCleanupEnabled) UserDefaults.standard.set(audioCleanup, forKey: self.keyIsAudioCleanupEnabled)
} }

View File

@ -0,0 +1,69 @@
import Foundation
import SwiftData
import OSLog
/// A service that automatically deletes transcriptions when "Do Not Maintain Transcript History" is enabled
class TranscriptionAutoCleanupService {
static let shared = TranscriptionAutoCleanupService()
private let logger = Logger(subsystem: "com.prakashjoshipax.voiceink", category: "TranscriptionAutoCleanupService")
private var modelContext: ModelContext?
private init() {}
/// Start monitoring for new transcriptions and auto-delete if needed
func startMonitoring(modelContext: ModelContext) {
self.modelContext = modelContext
NotificationCenter.default.addObserver(
self,
selector: #selector(handleTranscriptionCreated(_:)),
name: .transcriptionCreated,
object: nil
)
logger.info("TranscriptionAutoCleanupService started monitoring")
}
/// Stop monitoring for transcriptions
func stopMonitoring() {
NotificationCenter.default.removeObserver(self, name: .transcriptionCreated, object: nil)
logger.info("TranscriptionAutoCleanupService stopped monitoring")
}
@objc private func handleTranscriptionCreated(_ notification: Notification) {
// Check if no-retention mode is enabled
guard UserDefaults.standard.bool(forKey: "DoNotMaintainTranscriptHistory") else {
return
}
guard let transcription = notification.object as? Transcription,
let modelContext = self.modelContext else {
logger.error("Invalid transcription or missing model context")
return
}
logger.info("Auto-deleting transcription for zero data retention")
// Delete the audio file if it exists
if let urlString = transcription.audioFileURL,
let url = URL(string: urlString) {
do {
try FileManager.default.removeItem(at: url)
logger.debug("Deleted audio file: \(url.lastPathComponent)")
} catch {
logger.error("Failed to delete audio file: \(error.localizedDescription)")
}
}
// Delete the transcription from the database
modelContext.delete(transcription)
do {
try modelContext.save()
logger.debug("Successfully deleted transcription from database")
} catch {
logger.error("Failed to save after transcription deletion: \(error.localizedDescription)")
}
}
}

View File

@ -6,6 +6,7 @@ struct AudioCleanupSettingsView: View {
@EnvironmentObject private var whisperState: WhisperState @EnvironmentObject private var whisperState: WhisperState
// Audio cleanup settings // Audio cleanup settings
@AppStorage("DoNotMaintainTranscriptHistory") private var doNotMaintainTranscriptHistory = false
@AppStorage("IsAudioCleanupEnabled") private var isAudioCleanupEnabled = true @AppStorage("IsAudioCleanupEnabled") private var isAudioCleanupEnabled = true
@AppStorage("AudioRetentionPeriod") private var audioRetentionPeriod = 7 @AppStorage("AudioRetentionPeriod") private var audioRetentionPeriod = 7
@State private var isPerformingCleanup = false @State private var isPerformingCleanup = false
@ -16,16 +17,47 @@ struct AudioCleanupSettingsView: View {
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: 12) {
Text("VoiceInk can automatically delete audio files from transcription history while preserving the text transcripts.") Text("Control how VoiceInk handles your transcription data and audio recordings for privacy and storage management.")
.font(.system(size: 13)) .font(.system(size: 13))
.foregroundColor(.secondary) .foregroundColor(.secondary)
.fixedSize(horizontal: false, vertical: true) .fixedSize(horizontal: false, vertical: true)
Toggle("Enable automatic audio cleanup", isOn: $isAudioCleanupEnabled) Text("Data Retention")
.font(.system(size: 14, weight: .semibold))
.foregroundColor(.primary)
.padding(.top, 8)
Toggle("Do not maintain transcript history", isOn: $doNotMaintainTranscriptHistory)
.toggleStyle(.switch) .toggleStyle(.switch)
.padding(.vertical, 4) .padding(.vertical, 4)
if isAudioCleanupEnabled { if doNotMaintainTranscriptHistory {
Text("When enabled, no transcription history will be saved. This provides zero data retention for maximum privacy.")
.font(.system(size: 13))
.foregroundColor(.orange)
.fixedSize(horizontal: false, vertical: true)
.padding(.top, 2)
}
if !doNotMaintainTranscriptHistory {
Divider()
.padding(.vertical, 8)
Text("Audio File Management")
.font(.system(size: 14, weight: .semibold))
.foregroundColor(.primary)
Text("Automatically delete audio files from transcription history while preserving the text transcripts.")
.font(.system(size: 13))
.foregroundColor(.secondary)
.fixedSize(horizontal: false, vertical: true)
Toggle("Enable automatic audio cleanup", isOn: $isAudioCleanupEnabled)
.toggleStyle(.switch)
.padding(.vertical, 4)
}
if isAudioCleanupEnabled && !doNotMaintainTranscriptHistory {
VStack(alignment: .leading, spacing: 8) { VStack(alignment: .leading, spacing: 8) {
Text("Retention Period") Text("Retention Period")
.font(.system(size: 14, weight: .medium)) .font(.system(size: 14, weight: .medium))

View File

@ -193,11 +193,11 @@ struct SettingsView: View {
} }
} }
// Audio Cleanup Section // Data & Privacy Section
SettingsSection( SettingsSection(
icon: "trash.circle", icon: "lock.shield",
title: "Audio Cleanup", title: "Data & Privacy",
subtitle: "Manage recording storage" subtitle: "Control transcript history and storage"
) { ) {
AudioCleanupSettingsView() AudioCleanupSettingsView()
} }

View File

@ -21,6 +21,9 @@ struct VoiceInkApp: App {
// Audio cleanup manager for automatic deletion of old audio files // Audio cleanup manager for automatic deletion of old audio files
private let audioCleanupManager = AudioCleanupManager.shared private let audioCleanupManager = AudioCleanupManager.shared
// Transcription auto-cleanup service for zero data retention
private let transcriptionAutoCleanupService = TranscriptionAutoCleanupService.shared
init() { init() {
do { do {
let schema = Schema([ let schema = Schema([
@ -98,6 +101,9 @@ struct VoiceInkApp: App {
// Start the automatic audio cleanup process // Start the automatic audio cleanup process
audioCleanupManager.startAutomaticCleanup(modelContext: container.mainContext) audioCleanupManager.startAutomaticCleanup(modelContext: container.mainContext)
// Start the transcription auto-cleanup service for zero data retention
transcriptionAutoCleanupService.startMonitoring(modelContext: container.mainContext)
} }
.background(WindowAccessor { window in .background(WindowAccessor { window in
WindowManager.shared.configureWindow(window) WindowManager.shared.configureWindow(window)
@ -107,6 +113,9 @@ struct VoiceInkApp: App {
// Stop the automatic audio cleanup process // Stop the automatic audio cleanup process
audioCleanupManager.stopAutomaticCleanup() audioCleanupManager.stopAutomaticCleanup()
// Stop the transcription auto-cleanup service
transcriptionAutoCleanupService.stopMonitoring()
} }
} else { } else {
OnboardingView(hasCompletedOnboarding: $hasCompletedOnboarding) OnboardingView(hasCompletedOnboarding: $hasCompletedOnboarding)

View File

@ -311,6 +311,7 @@ class WhisperState: NSObject, ObservableObject {
) )
modelContext.insert(newTranscription) modelContext.insert(newTranscription)
try? modelContext.save() try? modelContext.save()
NotificationCenter.default.post(name: .transcriptionCreated, object: newTranscription)
text = enhancedText text = enhancedText
} catch { } catch {
let newTranscription = Transcription( let newTranscription = Transcription(
@ -323,6 +324,7 @@ class WhisperState: NSObject, ObservableObject {
) )
modelContext.insert(newTranscription) modelContext.insert(newTranscription)
try? modelContext.save() try? modelContext.save()
NotificationCenter.default.post(name: .transcriptionCreated, object: newTranscription)
await MainActor.run { await MainActor.run {
NotificationManager.shared.showNotification( NotificationManager.shared.showNotification(
@ -341,6 +343,7 @@ class WhisperState: NSObject, ObservableObject {
) )
modelContext.insert(newTranscription) modelContext.insert(newTranscription)
try? modelContext.save() try? modelContext.save()
NotificationCenter.default.post(name: .transcriptionCreated, object: newTranscription)
} }
if case .trialExpired = licenseViewModel.licenseState { if case .trialExpired = licenseViewModel.licenseState {
@ -395,6 +398,7 @@ class WhisperState: NSObject, ObservableObject {
modelContext.insert(failedTranscription) modelContext.insert(failedTranscription)
try? modelContext.save() try? modelContext.save()
NotificationCenter.default.post(name: .transcriptionCreated, object: failedTranscription)
} }
} catch { } catch {
logger.error("❌ Could not create a record for the failed transcription: \(error.localizedDescription)") logger.error("❌ Could not create a record for the failed transcription: \(error.localizedDescription)")