From 77147e9ac95f471dbfa6846f323a0d13c83dbba8 Mon Sep 17 00:00:00 2001 From: Yegor Kryvenka <82621566+RealSleepi@users.noreply.github.com> Date: Thu, 17 Sep 2026 11:16:46 -0400 Subject: [PATCH] Fix file transcription failing to open .MOV audio (CoreAudio error -54) AVAudioFile(forReading:) cannot open many video containers directly even though .mov/.mp4 are advertised as supported formats. The buffered/chunked transcription path used for video containers was opening the raw video file URL with AVAudioFile, which throws com.apple.coreaudio.avfaudio error -54 ("Could not open audio file"). Extract the audio track to a standalone .m4a via AVAssetExportSession before handing it to AVAudioFile when the source is a video container, and clean up the temporary file afterward. Fixes #982 Co-Authored-By: Claude Sonnet 5 --- .../MeetingTranscriptionService.swift | 58 ++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/Sources/Fluid/Services/MeetingTranscriptionService.swift b/Sources/Fluid/Services/MeetingTranscriptionService.swift index d153935db..e31220680 100644 --- a/Sources/Fluid/Services/MeetingTranscriptionService.swift +++ b/Sources/Fluid/Services/MeetingTranscriptionService.swift @@ -529,10 +529,35 @@ final class MeetingTranscriptionService: ObservableObject { var totalConfidence: Float = 0 var chunkCount = 0 + // AVAudioFile can only open standalone audio files. Video containers (e.g. .mov, .mp4) + // must have their audio track extracted first, otherwise opening them directly fails + // with CoreAudio error -54 ("Could not open audio file") even though the container is + // in `supportedFileExtensions`. + var extractedAudioURL: URL? + defer { + if let extractedAudioURL { + try? FileManager.default.removeItem(at: extractedAudioURL) + } + } + + let audioSourceURL: URL + if isVideoContainer { + do { + let extracted = try await Self.extractAudioTrack(from: fileURL) + extractedAudioURL = extracted + audioSourceURL = extracted + } catch { + throw TranscriptionError + .audioConversionFailed("Could not extract audio track from video: \(error.localizedDescription)") + } + } else { + audioSourceURL = fileURL + } + // Open audio file for reading let audioFile: AVAudioFile do { - audioFile = try AVAudioFile(forReading: fileURL) + audioFile = try AVAudioFile(forReading: audioSourceURL) } catch { throw TranscriptionError.audioConversionFailed("Could not open audio file: \(error.localizedDescription)") } @@ -642,6 +667,37 @@ final class MeetingTranscriptionService: ObservableObject { } } + /// Extracts the audio track of a video container (e.g. .mov, .mp4) into a standalone + /// `.m4a` file that `AVAudioFile` can open directly. `AVAudioFile(forReading:)` rejects + /// many video containers outright (CoreAudio error -54), even ones whose extension is + /// advertised as supported, because it expects an audio-only file. + private static func extractAudioTrack(from videoURL: URL) async throws -> URL { + let asset = AVURLAsset(url: videoURL) + + guard let exportSession = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetAppleM4A) else { + throw TranscriptionError.audioConversionFailed("Could not create audio export session") + } + + let outputURL = FileManager.default.temporaryDirectory + .appendingPathComponent(UUID().uuidString) + .appendingPathExtension("m4a") + + exportSession.outputURL = outputURL + exportSession.outputFileType = .m4a + + await exportSession.export() + + if let error = exportSession.error { + throw error + } + guard exportSession.status == .completed else { + throw TranscriptionError + .audioConversionFailed("Audio export did not complete (status: \(exportSession.status.rawValue))") + } + + return outputURL + } + /// Export transcription result to text file nonisolated func exportToText(_ result: TranscriptionResult, to destinationURL: URL) throws { try result.textExport.write(to: destinationURL, atomically: true, encoding: .utf8)