@@ -2,6 +2,7 @@ import AVFoundation
22import Combine
33import CoreMedia
44import Foundation
5+ import UniformTypeIdentifiers
56
67/// Result of a transcription operation
78struct TranscriptionResult : Identifiable , Sendable , Codable {
@@ -67,6 +68,29 @@ final class MeetingTranscriptionService: ObservableObject {
6768 @Published var error : String ?
6869 @Published var result : TranscriptionResult ?
6970
71+ // MARK: - Supported Formats
72+
73+ /// File extensions the OS can actually decode, queried dynamically from AVFoundation.
74+ /// Filtered to audio/video types only — excludes subtitles, playlists, etc.
75+ static let supportedFileExtensions : Set < String > = {
76+ let avTypes = AVURLAsset . audiovisualTypes ( )
77+ let extensions = avTypes. compactMap { fileType -> String ? in
78+ guard let utType = UTType ( fileType. rawValue) else { return nil }
79+ guard utType. conforms ( to: . audio) || utType. conforms ( to: . movie) else { return nil }
80+ return utType. preferredFilenameExtension
81+ }
82+ return Set ( extensions)
83+ } ( )
84+
85+ /// Content types accepted by the file picker — broad categories so the OS filters naturally.
86+ static let allowedContentTypes : [ UTType ] = [ . audio, . movie]
87+
88+ /// User-facing description of supported formats (curated for readability).
89+ static let supportedFormatsDescription = " Supported: WAV, MP3, M4A, OGG, MP4, MOV, and more "
90+
91+ /// Error copy shown when a dropped file is not accepted.
92+ static let dropErrorCopy = " Accepted file types: WAV, MP3, M4A, OGG, MP4, MOV, and more. "
93+
7094 /// Share the ASR service instance to avoid loading models twice
7195 private let asrService : ASRService
7296
@@ -159,11 +183,10 @@ final class MeetingTranscriptionService: ObservableObject {
159183
160184 // Check file extension
161185 let fileExtension = fileURL. pathExtension. lowercased ( )
162- let supportedFormats = [ " wav " , " mp3 " , " m4a " , " ogg " , " aac " , " flac " , " aiff " , " caf " , " mp4 " , " mov " ]
163186
164- guard supportedFormats . contains ( fileExtension) else {
187+ guard Self . supportedFileExtensions . contains ( fileExtension) else {
165188 throw TranscriptionError
166- . fileNotSupported ( " Format . \( fileExtension) not supported. Supported: \( supportedFormats . joined ( separator : " , " ) ) " )
189+ . fileNotSupported ( " Format . \( fileExtension) not supported. \( Self . supportedFormatsDescription ) " )
167190 }
168191
169192 // Get audio duration for progress display
@@ -181,7 +204,8 @@ final class MeetingTranscriptionService: ObservableObject {
181204 DebugLogger . shared. warning ( " Could not determine audio duration: \( error. localizedDescription) " , source: " MeetingTranscriptionService " )
182205 }
183206
184- let isVideoContainer = [ " mp4 " , " mov " ] . contains ( fileExtension)
207+ let isVideoContainer = UTType ( filenameExtension: fileExtension)
208+ . map { $0. conforms ( to: . movie) } ?? false
185209
186210 if provider. prefersNativeFileTranscription && !isVideoContainer {
187211 self . currentStatus = duration > 0 ? " Transcribing audio ( \( Int ( duration) ) s)... " : " Transcribing audio... "
0 commit comments