Update TranscriptCleanup with interval settings
This commit is contained in:
parent
056bcf3bd2
commit
a3c302b50b
@ -13,7 +13,8 @@ struct GeneralSettings: Codable {
|
||||
let isMenuBarOnly: Bool?
|
||||
let useAppleScriptPaste: Bool?
|
||||
let recorderType: String?
|
||||
let doNotMaintainTranscriptHistory: Bool?
|
||||
let isTranscriptionCleanupEnabled: Bool?
|
||||
let transcriptionRetentionMinutes: Int?
|
||||
let isAudioCleanupEnabled: Bool?
|
||||
let audioRetentionPeriod: Int?
|
||||
|
||||
@ -45,8 +46,9 @@ class ImportExportService {
|
||||
private let keyIsMenuBarOnly = "IsMenuBarOnly"
|
||||
private let keyUseAppleScriptPaste = "UseAppleScriptPaste"
|
||||
private let keyRecorderType = "RecorderType"
|
||||
private let keyDoNotMaintainTranscriptHistory = "DoNotMaintainTranscriptHistory"
|
||||
private let keyIsAudioCleanupEnabled = "IsAudioCleanupEnabled"
|
||||
private let keyIsTranscriptionCleanupEnabled = "IsTranscriptionCleanupEnabled"
|
||||
private let keyTranscriptionRetentionMinutes = "TranscriptionRetentionMinutes"
|
||||
private let keyAudioRetentionPeriod = "AudioRetentionPeriod"
|
||||
|
||||
private let keyIsSoundFeedbackEnabled = "isSoundFeedbackEnabled"
|
||||
@ -90,7 +92,8 @@ class ImportExportService {
|
||||
isMenuBarOnly: menuBarManager.isMenuBarOnly,
|
||||
useAppleScriptPaste: UserDefaults.standard.bool(forKey: keyUseAppleScriptPaste),
|
||||
recorderType: whisperState.recorderType,
|
||||
doNotMaintainTranscriptHistory: UserDefaults.standard.bool(forKey: keyDoNotMaintainTranscriptHistory),
|
||||
isTranscriptionCleanupEnabled: UserDefaults.standard.bool(forKey: keyIsTranscriptionCleanupEnabled),
|
||||
transcriptionRetentionMinutes: UserDefaults.standard.integer(forKey: keyTranscriptionRetentionMinutes),
|
||||
isAudioCleanupEnabled: UserDefaults.standard.bool(forKey: keyIsAudioCleanupEnabled),
|
||||
audioRetentionPeriod: UserDefaults.standard.integer(forKey: keyAudioRetentionPeriod),
|
||||
|
||||
@ -235,8 +238,12 @@ class ImportExportService {
|
||||
if let recType = general.recorderType {
|
||||
whisperState.recorderType = recType
|
||||
}
|
||||
if let doNotMaintainHistory = general.doNotMaintainTranscriptHistory {
|
||||
UserDefaults.standard.set(doNotMaintainHistory, forKey: self.keyDoNotMaintainTranscriptHistory)
|
||||
|
||||
if let transcriptionCleanup = general.isTranscriptionCleanupEnabled {
|
||||
UserDefaults.standard.set(transcriptionCleanup, forKey: self.keyIsTranscriptionCleanupEnabled)
|
||||
}
|
||||
if let transcriptionMinutes = general.transcriptionRetentionMinutes {
|
||||
UserDefaults.standard.set(transcriptionMinutes, forKey: self.keyTranscriptionRetentionMinutes)
|
||||
}
|
||||
if let audioCleanup = general.isAudioCleanupEnabled {
|
||||
UserDefaults.standard.set(audioCleanup, forKey: self.keyIsAudioCleanupEnabled)
|
||||
|
||||
@ -2,68 +2,126 @@ 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 let keyIsEnabled = "IsTranscriptionCleanupEnabled"
|
||||
private let keyRetentionMinutes = "TranscriptionRetentionMinutes"
|
||||
|
||||
private let defaultRetentionMinutes: Int = 24 * 60
|
||||
|
||||
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")
|
||||
|
||||
if UserDefaults.standard.bool(forKey: keyIsEnabled) {
|
||||
|
||||
Task { [weak self] in
|
||||
guard let self = self, let modelContext = self.modelContext else { return }
|
||||
await self.sweepOldTranscriptions(modelContext: modelContext)
|
||||
}
|
||||
} else {}
|
||||
}
|
||||
|
||||
/// Stop monitoring for transcriptions
|
||||
|
||||
func stopMonitoring() {
|
||||
NotificationCenter.default.removeObserver(self, name: .transcriptionCreated, object: nil)
|
||||
logger.info("TranscriptionAutoCleanupService stopped monitoring")
|
||||
|
||||
}
|
||||
|
||||
|
||||
func runManualCleanup(modelContext: ModelContext) async {
|
||||
await sweepOldTranscriptions(modelContext: modelContext)
|
||||
}
|
||||
|
||||
@objc private func handleTranscriptionCreated(_ notification: Notification) {
|
||||
// Check if no-retention mode is enabled
|
||||
guard UserDefaults.standard.bool(forKey: "DoNotMaintainTranscriptHistory") else {
|
||||
let isEnabled = UserDefaults.standard.bool(forKey: keyIsEnabled)
|
||||
guard isEnabled else { return }
|
||||
|
||||
let minutes = UserDefaults.standard.integer(forKey: keyRetentionMinutes)
|
||||
if minutes > 0 {
|
||||
// Trigger a sweep based on the retention window whenever a new item is added
|
||||
if let modelContext = self.modelContext {
|
||||
Task { [weak self] in
|
||||
guard let self = self else { return }
|
||||
await self.sweepOldTranscriptions(modelContext: modelContext)
|
||||
}
|
||||
}
|
||||
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)")
|
||||
}
|
||||
}
|
||||
|
||||
private func sweepOldTranscriptions(modelContext: ModelContext) async {
|
||||
guard UserDefaults.standard.bool(forKey: keyIsEnabled) else {
|
||||
return
|
||||
}
|
||||
|
||||
let retentionMinutes = UserDefaults.standard.integer(forKey: keyRetentionMinutes)
|
||||
let effectiveMinutes = max(retentionMinutes, 0)
|
||||
|
||||
let cutoffDate = Date().addingTimeInterval(TimeInterval(-effectiveMinutes * 60))
|
||||
|
||||
do {
|
||||
try await MainActor.run {
|
||||
let descriptor = FetchDescriptor<Transcription>(
|
||||
predicate: #Predicate<Transcription> { transcription in
|
||||
transcription.timestamp < cutoffDate
|
||||
}
|
||||
)
|
||||
let items = try modelContext.fetch(descriptor)
|
||||
var deletedCount = 0
|
||||
for transcription in items {
|
||||
// Remove audio file if present
|
||||
if let urlString = transcription.audioFileURL,
|
||||
let url = URL(string: urlString),
|
||||
FileManager.default.fileExists(atPath: url.path) {
|
||||
try? FileManager.default.removeItem(at: url)
|
||||
}
|
||||
modelContext.delete(transcription)
|
||||
deletedCount += 1
|
||||
}
|
||||
if deletedCount > 0 { try modelContext.save() }
|
||||
}
|
||||
} catch {
|
||||
logger.error("Failed during transcription cleanup: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,12 +1,12 @@
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
|
||||
/// A view component for configuring audio cleanup settings
|
||||
struct AudioCleanupSettingsView: View {
|
||||
@EnvironmentObject private var whisperState: WhisperState
|
||||
|
||||
// Audio cleanup settings
|
||||
@AppStorage("DoNotMaintainTranscriptHistory") private var doNotMaintainTranscriptHistory = false
|
||||
@AppStorage("IsTranscriptionCleanupEnabled") private var isTranscriptionCleanupEnabled = false
|
||||
@AppStorage("TranscriptionRetentionMinutes") private var transcriptionRetentionMinutes = 24 * 60
|
||||
@AppStorage("IsAudioCleanupEnabled") private var isAudioCleanupEnabled = true
|
||||
@AppStorage("AudioRetentionPeriod") private var audioRetentionPeriod = 7
|
||||
@State private var isPerformingCleanup = false
|
||||
@ -14,6 +14,7 @@ struct AudioCleanupSettingsView: View {
|
||||
@State private var cleanupInfo: (fileCount: Int, totalSize: Int64, transcriptions: [Transcription]) = (0, 0, [])
|
||||
@State private var showResultAlert = false
|
||||
@State private var cleanupResult: (deletedCount: Int, errorCount: Int) = (0, 0)
|
||||
@State private var showTranscriptCleanupResult = false
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
@ -22,28 +23,60 @@ struct AudioCleanupSettingsView: View {
|
||||
.foregroundColor(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
Toggle("Do not maintain transcript history", isOn: $doNotMaintainTranscriptHistory)
|
||||
Toggle("Automatically delete transcript history", isOn: $isTranscriptionCleanupEnabled)
|
||||
.toggleStyle(.switch)
|
||||
.padding(.vertical, 4)
|
||||
|
||||
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 isTranscriptionCleanupEnabled {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Picker("Delete transcripts older than", selection: $transcriptionRetentionMinutes) {
|
||||
Text("Immediately").tag(0)
|
||||
Text("1 hour").tag(60)
|
||||
Text("1 day").tag(24 * 60)
|
||||
Text("3 days").tag(3 * 24 * 60)
|
||||
Text("7 days").tag(7 * 24 * 60)
|
||||
}
|
||||
.pickerStyle(.menu)
|
||||
|
||||
Text("Older transcripts will be deleted automatically based on your selection.")
|
||||
.font(.system(size: 13))
|
||||
.foregroundColor(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.padding(.top, 2)
|
||||
|
||||
Button(action: {
|
||||
Task {
|
||||
await TranscriptionAutoCleanupService.shared.runManualCleanup(modelContext: whisperState.modelContext)
|
||||
await MainActor.run {
|
||||
showTranscriptCleanupResult = true
|
||||
}
|
||||
}
|
||||
}) {
|
||||
HStack {
|
||||
Image(systemName: "trash.circle")
|
||||
Text("Run Transcript Cleanup Now")
|
||||
}
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.controlSize(.large)
|
||||
.alert("Transcript Cleanup", isPresented: $showTranscriptCleanupResult) {
|
||||
Button("OK", role: .cancel) { }
|
||||
} message: {
|
||||
Text("Cleanup triggered. Old transcripts are cleaned up according to your retention setting.")
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
|
||||
if !doNotMaintainTranscriptHistory {
|
||||
|
||||
if !isTranscriptionCleanupEnabled {
|
||||
Divider()
|
||||
.padding(.vertical, 8)
|
||||
|
||||
Toggle("Enable automatic audio cleanup", isOn: $isAudioCleanupEnabled)
|
||||
.toggleStyle(.switch)
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
|
||||
if isAudioCleanupEnabled && !doNotMaintainTranscriptHistory {
|
||||
|
||||
if isAudioCleanupEnabled && !isTranscriptionCleanupEnabled {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Picker("Keep audio files for", selection: $audioRetentionPeriod) {
|
||||
Text("1 day").tag(1)
|
||||
@ -143,5 +176,12 @@ struct AudioCleanupSettingsView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: isTranscriptionCleanupEnabled) { _, newValue in
|
||||
if newValue {
|
||||
AudioCleanupManager.shared.stopAutomaticCleanup()
|
||||
} else if isAudioCleanupEnabled {
|
||||
AudioCleanupManager.shared.startAutomaticCleanup(modelContext: whisperState.modelContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -78,7 +78,6 @@ struct VoiceInkApp: App {
|
||||
)
|
||||
_menuBarManager = StateObject(wrappedValue: menuBarManager)
|
||||
|
||||
// Configure ActiveWindowService with enhancementService
|
||||
let activeWindowService = ActiveWindowService.shared
|
||||
activeWindowService.configure(with: enhancementService)
|
||||
activeWindowService.configureWhisperState(whisperState)
|
||||
@ -99,11 +98,13 @@ struct VoiceInkApp: App {
|
||||
.onAppear {
|
||||
updaterViewModel.silentlyCheckForUpdates()
|
||||
|
||||
// Start the automatic audio cleanup process
|
||||
audioCleanupManager.startAutomaticCleanup(modelContext: container.mainContext)
|
||||
|
||||
// Start the transcription auto-cleanup service for zero data retention
|
||||
// Start the transcription auto-cleanup service (handles immediate and scheduled transcript deletion)
|
||||
transcriptionAutoCleanupService.startMonitoring(modelContext: container.mainContext)
|
||||
|
||||
// Start the automatic audio cleanup process only if transcript cleanup is not enabled
|
||||
if !UserDefaults.standard.bool(forKey: "IsTranscriptionCleanupEnabled") {
|
||||
audioCleanupManager.startAutomaticCleanup(modelContext: container.mainContext)
|
||||
}
|
||||
}
|
||||
.background(WindowAccessor { window in
|
||||
WindowManager.shared.configureWindow(window)
|
||||
@ -111,11 +112,11 @@ struct VoiceInkApp: App {
|
||||
.onDisappear {
|
||||
whisperState.unloadModel()
|
||||
|
||||
// Stop the automatic audio cleanup process
|
||||
audioCleanupManager.stopAutomaticCleanup()
|
||||
|
||||
// Stop the transcription auto-cleanup service
|
||||
transcriptionAutoCleanupService.stopMonitoring()
|
||||
|
||||
// Stop the automatic audio cleanup process
|
||||
audioCleanupManager.stopAutomaticCleanup()
|
||||
}
|
||||
} else {
|
||||
OnboardingView(hasCompletedOnboarding: $hasCompletedOnboarding)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user