From f6f518b0a95db7b307fdf646f79b8f724c474d7d Mon Sep 17 00:00:00 2001 From: SuperDooper86 Date: Tue, 25 Aug 2026 16:51:00 +0200 Subject: [PATCH] fix: preserve recordings before transcription --- Meetingnotes.xcodeproj/project.pbxproj | 8 ++-- meetingnotes/Managers/AudioManager.swift | 25 ++++++---- .../Managers/LocalStorageManager.swift | 46 ++----------------- .../Managers/RecordingSessionManager.swift | 3 ++ .../ViewModels/MeetingViewModel.swift | 4 +- 5 files changed, 28 insertions(+), 58 deletions(-) diff --git a/Meetingnotes.xcodeproj/project.pbxproj b/Meetingnotes.xcodeproj/project.pbxproj index d207f07..28786a7 100644 --- a/Meetingnotes.xcodeproj/project.pbxproj +++ b/Meetingnotes.xcodeproj/project.pbxproj @@ -276,7 +276,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 34; + CURRENT_PROJECT_VERSION = 35; DEVELOPMENT_ASSET_PATHS = "\"meetingnotes/Preview Content\""; DEVELOPMENT_TEAM = G9LVHZAJNX; ENABLE_HARDENED_RUNTIME = YES; @@ -290,7 +290,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 15.0; - MARKETING_VERSION = 1.1.22; + MARKETING_VERSION = 1.1.23; ONLY_ACTIVE_ARCH = NO; OTHER_SWIFT_FLAGS = "$(inherited) -D ENABLE_TCC_SPI"; PRODUCT_BUNDLE_IDENTIFIER = net.jamesbone.meetingnotes; @@ -312,7 +312,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 34; + CURRENT_PROJECT_VERSION = 35; DEVELOPMENT_ASSET_PATHS = "\"meetingnotes/Preview Content\""; DEVELOPMENT_TEAM = G9LVHZAJNX; ENABLE_HARDENED_RUNTIME = YES; @@ -326,7 +326,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 15.0; - MARKETING_VERSION = 1.1.22; + MARKETING_VERSION = 1.1.23; ONLY_ACTIVE_ARCH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D ENABLE_TCC_SPI"; PRODUCT_BUNDLE_IDENTIFIER = net.jamesbone.meetingnotes; diff --git a/meetingnotes/Managers/AudioManager.swift b/meetingnotes/Managers/AudioManager.swift index a5f99d4..d876431 100644 --- a/meetingnotes/Managers/AudioManager.swift +++ b/meetingnotes/Managers/AudioManager.swift @@ -87,10 +87,11 @@ final class AudioManager: NSObject, ObservableObject { } repairHalfDurationSystemWAVIfNeeded(in: files) - if let mismatch = captureDurationMismatch(in: files) { - let completedFiles = files.compactMap { $0 } - let audioFolder = preserveAudioFiles(completedFiles, meetingID: completedMeetingID) - lastRecoveryAudioFolderName = audioFolder?.lastPathComponent + let completedFiles = files.compactMap { $0 } + let audioFolder = preserveAudioFiles(completedFiles, meetingID: completedMeetingID) + let transcriptionFiles = preservedAudioFiles(files, in: audioFolder) + lastRecoveryAudioFolderName = audioFolder?.lastPathComponent + if let mismatch = captureDurationMismatch(in: transcriptionFiles) { let recoveryMessage = audioFolder == nil ? " The audio remains in the app's temporary folder." : " Audio was kept so it can be recovered." @@ -99,8 +100,8 @@ final class AudioManager: NSObject, ObservableObject { } let model = UserDefaultsManager.shared.transcriptionModel - async let micResult = transcribe(files[0], model: model, diarization: false) - async let systemResult = transcribe(files[1], model: model, diarization: true) + async let micResult = transcribe(transcriptionFiles[0], model: model, diarization: false) + async let systemResult = transcribe(transcriptionFiles[1], model: model, diarization: true) let (micTranscription, systemTranscription) = await (micResult, systemResult) let results = [micTranscription, systemTranscription] @@ -110,9 +111,6 @@ final class AudioManager: NSObject, ObservableObject { existingChunks: transcriptChunks.filter(\.isFinal) ) transcriptChunks = updated - let completedFiles = files.compactMap { $0 } - let audioFolder = preserveAudioFiles(completedFiles, meetingID: completedMeetingID) - lastRecoveryAudioFolderName = audioFolder?.lastPathComponent if !failures.isEmpty { let retentionDays = UserDefaultsManager.shared.audioRetentionDays let retentionUnit = retentionDays == 1 ? "day" : "days" @@ -572,6 +570,15 @@ final class AudioManager: NSObject, ObservableObject { LocalStorageManager.shared.preserveAudioFiles(urls, for: meetingID) } + private func preservedAudioFiles(_ urls: [URL?], in folder: URL?) -> [URL?] { + urls.map { sourceURL in + guard let sourceURL else { return nil } + guard let folder else { return sourceURL } + let preservedURL = folder.appendingPathComponent(sourceURL.lastPathComponent) + return FileManager.default.fileExists(atPath: preservedURL.path) ? preservedURL : sourceURL + } + } + private func captureDurationMismatch(in files: [URL?]) -> String? { guard files.count >= 2, let micDuration = audioDuration(at: files[0]), diff --git a/meetingnotes/Managers/LocalStorageManager.swift b/meetingnotes/Managers/LocalStorageManager.swift index 0c81c13..a954355 100644 --- a/meetingnotes/Managers/LocalStorageManager.swift +++ b/meetingnotes/Managers/LocalStorageManager.swift @@ -256,50 +256,12 @@ class LocalStorageManager { } func findRecoveryAudioFolder(for meeting: Meeting) -> URL? { - if let name = meeting.recoveryAudioFolderName, - let folder = recoveryAudioFolder(named: name) { - return folder - } - - let claimedFolderNames = Set( - loadMeetings() - .filter { $0.id != meeting.id } - .compactMap(\.recoveryAudioFolderName) - ) - guard let folders = try? FileManager.default.contentsOfDirectory( - at: recoveryDirectory, - includingPropertiesForKeys: [.isDirectoryKey, .creationDateKey, .contentModificationDateKey], - options: [.skipsHiddenFiles] - ) else { + let canonicalName = meeting.id.uuidString + guard meeting.recoveryAudioFolderName == nil + || meeting.recoveryAudioFolderName?.caseInsensitiveCompare(canonicalName) == .orderedSame else { return nil } - - let candidates = folders.compactMap { folder -> (url: URL, distance: TimeInterval)? in - let folderValues = try? folder.resourceValues( - forKeys: [.isDirectoryKey, .creationDateKey, .contentModificationDateKey] - ) - let files = recoveryAudioFiles(in: folder) - guard folderValues?.isDirectory == true, - !claimedFolderNames.contains(folder.lastPathComponent), - !files.isEmpty else { - return nil - } - let dates = files.compactMap { file -> Date? in - let values = try? file.url.resourceValues(forKeys: [.creationDateKey, .contentModificationDateKey]) - return values?.creationDate ?? values?.contentModificationDate - } - let referenceDate = dates.min() - ?? folderValues?.creationDate - ?? folderValues?.contentModificationDate - guard let referenceDate else { return nil } - return (folder, abs(referenceDate.timeIntervalSince(meeting.date))) - } - - // This fallback links recovery files created by older app versions. - return candidates - .filter { $0.distance <= 12 * 60 * 60 } - .min(by: { $0.distance < $1.distance })? - .url + return recoveryAudioFolder(named: canonicalName) } func deleteRecoveryAudioFolder(_ folder: URL) { diff --git a/meetingnotes/Managers/RecordingSessionManager.swift b/meetingnotes/Managers/RecordingSessionManager.swift index 0323497..4ffe0dd 100644 --- a/meetingnotes/Managers/RecordingSessionManager.swift +++ b/meetingnotes/Managers/RecordingSessionManager.swift @@ -124,6 +124,9 @@ class RecordingSessionManager: ObservableObject { // Find and update the active meeting if let index = meetings.firstIndex(where: { $0.id == meetingId }) { meetings[index].transcriptChunks = chunks + if let recoveryAudioFolderName = lastRecoveryAudioFolderName { + meetings[index].recoveryAudioFolderName = recoveryAudioFolderName + } // Save the updated meeting let success = LocalStorageManager.shared.saveMeeting(meetings[index]) diff --git a/meetingnotes/ViewModels/MeetingViewModel.swift b/meetingnotes/ViewModels/MeetingViewModel.swift index 77ca7b2..94d906e 100644 --- a/meetingnotes/ViewModels/MeetingViewModel.swift +++ b/meetingnotes/ViewModels/MeetingViewModel.swift @@ -254,9 +254,7 @@ class MeetingViewModel: ObservableObject { private func refreshRecoveryAudioFolder() { recoveryAudioFolderURL = LocalStorageManager.shared.findRecoveryAudioFolder(for: meeting) - if let recoveryAudioFolderURL { - meeting.recoveryAudioFolderName = recoveryAudioFolderURL.lastPathComponent - } + meeting.recoveryAudioFolderName = recoveryAudioFolderURL?.lastPathComponent } func showAudioInFinder() {