Update TranscriptCleanup with interval settings

This commit is contained in:
Beingpax 2025-08-13 09:07:08 +05:45
parent 056bcf3bd2
commit a3c302b50b
4 changed files with 154 additions and 48 deletions

View File

@ -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)

View File

@ -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)")
}
}
}

View File

@ -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)
}
}
}
}

View File

@ -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)