fix: make post-meeting transcription reliable (#6)

Co-authored-by: superdooper86 <[email protected]>
This commit was merged in pull request #6.
This commit is contained in:
2026-07-15 14:21:24 +02:00
committed by GitHub
co-authored by james
parent 34947e2119
commit d8f5c33141
6 changed files with 121 additions and 90 deletions
+61 -9
View File
@@ -34,6 +34,7 @@ final class AudioManager: NSObject, ObservableObject {
private var systemAudioFile: AVAudioFile?
private var micAudioURL: URL?
private var systemAudioURL: URL?
private var recordingStartedAt = Date()
private override init() {
super.init()
@@ -53,9 +54,10 @@ final class AudioManager: NSObject, ObservableObject {
}
func startRecording() {
sessionID = UUID()
errorMessage = nil
cancelCapture(removeFiles: true)
sessionID = UUID()
recordingStartedAt = Date()
do {
try prepareAudioFiles()
startMicrophoneTap()
@@ -67,11 +69,12 @@ final class AudioManager: NSObject, ObservableObject {
}
func stopRecordingAndTranscribe() async -> [TranscriptChunk] {
let completedSessionID = sessionID
let captureStartedAt = recordingStartedAt
let files = stopCaptureAndCloseFiles()
isProcessing = true
defer {
isProcessing = false
removeAudioFiles(files.compactMap { $0 })
}
let model = UserDefaultsManager.shared.transcriptionModel
@@ -85,18 +88,42 @@ final class AudioManager: NSObject, ObservableObject {
for (source, result) in zip([AudioSource.mic, .system], results) {
guard let result else { continue }
switch result {
case .success(let text):
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
if !trimmed.isEmpty {
updated.append(TranscriptChunk(source: source, text: trimmed, isFinal: true))
case .success(let transcription):
if transcription.segments.isEmpty {
let text = transcription.text.trimmingCharacters(in: .whitespacesAndNewlines)
if !text.isEmpty {
updated.append(TranscriptChunk(timestamp: captureStartedAt, source: source, text: text, isFinal: true))
}
} else {
for segment in transcription.segments {
let text = segment.text.trimmingCharacters(in: .whitespacesAndNewlines)
guard !text.isEmpty else { continue }
updated.append(TranscriptChunk(
timestamp: captureStartedAt.addingTimeInterval(max(0, segment.start)),
source: source,
text: text,
isFinal: true
))
}
}
case .failure(let error):
failures.append("\(source.displayName): \(error.localizedDescription)")
}
}
updated.sort {
if $0.timestamp != $1.timestamp { return $0.timestamp < $1.timestamp }
return $0.source.rawValue < $1.source.rawValue
}
transcriptChunks = updated
if !failures.isEmpty {
errorMessage = "Transcription failed for " + failures.joined(separator: "; ")
let completedFiles = files.compactMap { $0 }
if failures.isEmpty {
removeAudioFiles(completedFiles)
} else {
let recoveryFolder = preserveAudioFiles(completedFiles, sessionID: completedSessionID)
let recoveryMessage = recoveryFolder == nil
? " The audio remains in the app's temporary folder."
: " Audio was saved in Documents/Meetingnotes-Recovery/\(completedSessionID.uuidString)."
errorMessage = "Transcription failed for " + failures.joined(separator: "; ") + recoveryMessage
}
return updated
}
@@ -105,7 +132,7 @@ final class AudioManager: NSObject, ObservableObject {
cancelCapture(removeFiles: true)
}
private func transcribe(_ fileURL: URL?, model: String) async -> Result<String, Error>? {
private func transcribe(_ fileURL: URL?, model: String) async -> Result<CoderAPIClient.Transcription, Error>? {
guard let fileURL else { return nil }
do {
return .success(try await CoderAPIClient.shared.transcribe(fileURL: fileURL, model: model))
@@ -389,6 +416,31 @@ final class AudioManager: NSObject, ObservableObject {
private func removeAudioFiles(_ urls: [URL]) {
for url in urls { try? FileManager.default.removeItem(at: url) }
}
private func preserveAudioFiles(_ urls: [URL], sessionID: UUID) -> URL? {
guard !urls.isEmpty,
let documents = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
return nil
}
let folder = documents
.appendingPathComponent("Meetingnotes-Recovery", isDirectory: true)
.appendingPathComponent(sessionID.uuidString, isDirectory: true)
do {
try FileManager.default.createDirectory(at: folder, withIntermediateDirectories: true)
} catch {
return nil
}
var preservedCount = 0
for url in urls {
do {
try FileManager.default.moveItem(at: url, to: folder.appendingPathComponent(url.lastPathComponent))
preservedCount += 1
} catch {
continue
}
}
return preservedCount > 0 ? folder : nil
}
private func resetAudioLevels() {
micAudioLevel = 0