Compare commits

...
1 Commits
Author SHA1 Message Date
coder f6f518b0a9 fix: preserve recordings before transcription 2026-08-25 16:51:00 +02:00
5 changed files with 28 additions and 58 deletions
+4 -4
View File
@@ -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;
+16 -9
View File
@@ -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]),
@@ -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) {
@@ -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])
@@ -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() {