Skip to content
Closed
Show file tree
Hide file tree
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
4 changes: 4 additions & 0 deletions Fluid.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
DA7100020000000000000002 /* DirectAudioReliabilityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA7100010000000000000001 /* DirectAudioReliabilityTests.swift */; };
7CFA1D0B2F500000C0DEF001 /* TypingServiceTransientPasteboardTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CFA1D0B2F500000C0DEF002 /* TypingServiceTransientPasteboardTests.swift */; };
803000000000000000000002 /* MediaPlaybackServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 803000000000000000000001 /* MediaPlaybackServiceTests.swift */; };
0B05F11E000000000000A002 /* SupportedFileExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B05F11E000000000000A001 /* SupportedFileExtensionsTests.swift */; };
803000000000000000000003 /* MediaRemoteAdapter in Frameworks */ = {isa = PBXBuildFile; productRef = 803000000000000000000004 /* MediaRemoteAdapter */; };
7CDB0A2F2F3C4D5600FB7CAD /* dictation_fixture.wav in Resources */ = {isa = PBXBuildFile; fileRef = 7CDB0A2B2F3C4D5600FB7CAD /* dictation_fixture.wav */; };
7CDB0A302F3C4D5600FB7CAD /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7CDB0A2C2F3C4D5600FB7CAD /* XCTest.framework */; };
Expand Down Expand Up @@ -73,6 +74,7 @@
CD1C7A0000000000000000B1 /* CustomDictionaryManualEntryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomDictionaryManualEntryTests.swift; sourceTree = "<group>"; };
7CFA1D0B2F500000C0DEF002 /* TypingServiceTransientPasteboardTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingServiceTransientPasteboardTests.swift; sourceTree = "<group>"; };
803000000000000000000001 /* MediaPlaybackServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPlaybackServiceTests.swift; sourceTree = "<group>"; };
0B05F11E000000000000A001 /* SupportedFileExtensionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupportedFileExtensionsTests.swift; sourceTree = "<group>"; };
7CDB0A2A2F3C4D5600FB7CAD /* AudioFixtureLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioFixtureLoader.swift; sourceTree = "<group>"; };
7CDB0A2B2F3C4D5600FB7CAD /* dictation_fixture.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = dictation_fixture.wav; sourceTree = "<group>"; };
7CDB0A2C2F3C4D5600FB7CAD /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Platforms/MacOSX.platform/Developer/Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; };
Expand Down Expand Up @@ -155,6 +157,7 @@
DA7100010000000000000001 /* DirectAudioReliabilityTests.swift */,
7CFA1D0B2F500000C0DEF002 /* TypingServiceTransientPasteboardTests.swift */,
803000000000000000000001 /* MediaPlaybackServiceTests.swift */,
0B05F11E000000000000A001 /* SupportedFileExtensionsTests.swift */,
);
path = FluidDictationIntegrationTests;
sourceTree = "<group>";
Expand Down Expand Up @@ -324,6 +327,7 @@
DA7100020000000000000002 /* DirectAudioReliabilityTests.swift in Sources */,
7CFA1D0B2F500000C0DEF001 /* TypingServiceTransientPasteboardTests.swift in Sources */,
803000000000000000000002 /* MediaPlaybackServiceTests.swift in Sources */,
0B05F11E000000000000A002 /* SupportedFileExtensionsTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
16 changes: 9 additions & 7 deletions Sources/Fluid/Services/MeetingTranscriptionService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -112,24 +112,26 @@ final class MeetingTranscriptionService: ObservableObject {

/// File extensions the OS can actually decode, queried dynamically from AVFoundation.
/// Filtered to audio/video types only — excludes subtitles, playlists, etc.
/// Uses every extension tag of each type, not just the preferred one: `.opus`/`.oga`
/// (e.g. WhatsApp voice notes) map to the same UTType as `.ogg` and decode natively.
static let supportedFileExtensions: Set<String> = {
let avTypes = AVURLAsset.audiovisualTypes()
let extensions = avTypes.compactMap { fileType -> String? in
guard let utType = UTType(fileType.rawValue) else { return nil }
guard utType.conforms(to: .audio) || utType.conforms(to: .movie) else { return nil }
return utType.preferredFilenameExtension
let extensions = avTypes.flatMap { fileType -> [String] in
guard let utType = UTType(fileType.rawValue) else { return [] }
guard utType.conforms(to: .audio) || utType.conforms(to: .movie) else { return [] }
return utType.tags[.filenameExtension] ?? []
}
return Set(extensions)
return Set(extensions.map { $0.lowercased() })
}()

/// Content types accepted by the file picker — broad categories so the OS filters naturally.
static let allowedContentTypes: [UTType] = [.audio, .movie]

/// User-facing description of supported formats (curated for readability).
static let supportedFormatsDescription = "Supported: WAV, MP3, M4A, OGG, MP4, MOV, and more"
static let supportedFormatsDescription = "Supported: WAV, MP3, M4A, OGG, OPUS, MP4, MOV, and more"

/// Error copy shown when a dropped file is not accepted.
static let dropErrorCopy = "Accepted file types: WAV, MP3, M4A, OGG, MP4, MOV, and more."
static let dropErrorCopy = "Accepted file types: WAV, MP3, M4A, OGG, OPUS, MP4, MOV, and more."

/// Share the ASR service instance to avoid loading models twice
private let asrService: ASRService
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
@testable import FluidVoice_Debug
import XCTest

// Regression tests for file-type acceptance in meeting/file transcription.
// WhatsApp voice notes are Ogg Opus files with a `.opus` extension. macOS maps
// `.opus` (and `.oga`) to the same UTType as `.ogg` (`org.xiph.ogg-audio`) and can
// decode them natively, but the accepted-extension set was built from each UTType's
// single `preferredFilenameExtension`, which dropped every alternate extension —
// so drag-and-dropping a WhatsApp `.opus` file was rejected as unsupported.

@MainActor
final class SupportedFileExtensionsTests: XCTestCase {
func testWhatsAppOpusExtensionsAreAccepted() {
let supported = MeetingTranscriptionService.supportedFileExtensions

XCTAssertTrue(supported.contains("opus"), "WhatsApp voice notes use .opus — macOS decodes them as Ogg Opus")
XCTAssertTrue(supported.contains("oga"), ".oga is an alternate Ogg audio extension macOS decodes natively")
XCTAssertTrue(supported.contains("ogg"), ".ogg must remain accepted")
}

func testCommonFormatsRemainAccepted() {
let supported = MeetingTranscriptionService.supportedFileExtensions

for ext in ["wav", "mp3", "m4a", "mp4", "mov", "flac", "aac"] {
XCTAssertTrue(supported.contains(ext), ".\(ext) must remain accepted")
}
}

func testOnlyDecodableExtensionsAreAccepted() {
let supported = MeetingTranscriptionService.supportedFileExtensions

// Every accepted extension must map to a UTType AVFoundation reports as decodable
// audio/video — the set must not drift into subtitles, playlists, or arbitrary types.
XCTAssertFalse(supported.contains("srt"), "subtitle files are not transcribable audio")
XCTAssertFalse(supported.contains("m3u"), "playlists are not transcribable audio")
XCTAssertFalse(supported.contains("txt"))
}
}
Loading