Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 57 additions & 1 deletion Sources/Fluid/Services/MeetingTranscriptionService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)")
}
Expand Down Expand Up @@ -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
Comment on lines +681 to +698

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Failed exports leave temporary files

If an export creates its destination before failing or being cancelled, extractAudioTrack throws without removing that file. The caller cannot clean it up because it receives the URL only after a successful return, so repeated failures can accumulate temporary .m4a files. Register cleanup inside this helper and preserve the file only after export completes.

Suggested change
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
let outputURL = FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString)
.appendingPathExtension("m4a")
var shouldRemoveOutput = true
defer {
if shouldRemoveOutput {
try? FileManager.default.removeItem(at: outputURL)
}
}
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))")
}
shouldRemoveOutput = false
return outputURL
Prompt To Fix With AI
This is a comment left during a code review.
Path: Sources/Fluid/Services/MeetingTranscriptionService.swift
Line: 681-698

Comment:
**Failed exports leave temporary files**

If an export creates its destination before failing or being cancelled, `extractAudioTrack` throws without removing that file. The caller cannot clean it up because it receives the URL only after a successful return, so repeated failures can accumulate temporary `.m4a` files. Register cleanup inside this helper and preserve the file only after export completes.

```suggestion
        let outputURL = FileManager.default.temporaryDirectory
            .appendingPathComponent(UUID().uuidString)
            .appendingPathExtension("m4a")
        var shouldRemoveOutput = true
        defer {
            if shouldRemoveOutput {
                try? FileManager.default.removeItem(at: outputURL)
            }
        }

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

        shouldRemoveOutput = false
        return outputURL
```

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Codex

}

/// 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)
Expand Down
Loading