Support for zero data retention on
This commit is contained in:
parent
d23d54e208
commit
2970895376
@ -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")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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)")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
69
VoiceInk/Services/TranscriptionAutoCleanupService.swift
Normal file
69
VoiceInk/Services/TranscriptionAutoCleanupService.swift
Normal 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)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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))
|
||||||
|
|||||||
@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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)")
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user