-
-
Notifications
You must be signed in to change notification settings - Fork 621
Fix Cohere long-form transcription #763
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
529f9b6
e60b2d5
1c337be
ab571b8
3a19754
a58b21b
7c9d8a4
b934e32
18f536f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -172,6 +172,7 @@ final class ASRService: ObservableObject { | |
| } | ||
|
|
||
| @Published var isRunning: Bool = false | ||
| @Published private(set) var isFileTranscriptionRunning: Bool = false | ||
| @Published var finalText: String = "" | ||
| @Published var partialTranscription: String = "" | ||
| @Published var wordBoostStatusText: String = "Word boost: off" | ||
|
|
@@ -195,6 +196,20 @@ final class ASRService: ObservableObject { | |
| self.isRunning || self.isStarting | ||
| } | ||
|
|
||
| var isAnyTranscriptionRunning: Bool { | ||
| self.isRunningOrStarting || self.isFileTranscriptionRunning | ||
| } | ||
|
|
||
| func beginFileTranscription() -> Bool { | ||
| guard !self.isAnyTranscriptionRunning else { return false } | ||
| self.isFileTranscriptionRunning = true | ||
| return true | ||
| } | ||
|
|
||
| func endFileTranscription() { | ||
| self.isFileTranscriptionRunning = false | ||
| } | ||
|
|
||
| private let audioCaptureReadinessGate = AudioCaptureReadinessGate() | ||
| private let firstPCMTimeoutNanoseconds: UInt64 = 2_000_000_000 | ||
| private var audioCaptureStartGeneration: UInt64 = 0 | ||
|
|
@@ -353,7 +368,7 @@ final class ASRService: ObservableObject { | |
| case .parakeetRealtime: | ||
| return self.getParakeetRealtimeProvider() | ||
| case .cohereTranscribeSixBit: | ||
| return self.getExternalCoreMLProvider() | ||
| return self.getWhisperProvider() | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When Cohere is used to transcribe a meeting file longer than 20 minutes, routing it to Useful? React with 👍 / 👎. |
||
| case .nemotronOffline, .nemotronStreaming, .nemotronStreaming320: | ||
| return self.getNemotronProvider(mode: model.nemotronProviderMode) | ||
| case .qwen3Asr: | ||
|
|
@@ -554,7 +569,7 @@ final class ASRService: ObservableObject { | |
| case .parakeetRealtime: | ||
| return ParakeetRealtimeProvider() | ||
| case .cohereTranscribeSixBit: | ||
| return ExternalCoreMLTranscriptionProvider(modelOverride: model) | ||
| return WhisperProvider(modelOverride: model) | ||
| case .nemotronOffline, .nemotronStreaming, .nemotronStreaming320: | ||
| return NemotronProvider(mode: model.nemotronProviderMode) | ||
| case .qwen3Asr: | ||
|
|
@@ -571,6 +586,9 @@ final class ASRService: ObservableObject { | |
| /// - model: The model to download | ||
| /// - progressHandler: Optional callback for download progress (0.0 to 1.0) | ||
| func downloadModel(_ model: SettingsStore.SpeechModel, progressHandler: ((Double) -> Void)?) async throws { | ||
| guard !self.isFileTranscriptionRunning else { | ||
| throw self.fileTranscriptionModelOperationError() | ||
| } | ||
| guard self.modelDownloadTask == nil, self.ensureReadyTask == nil else { | ||
| throw NSError( | ||
| domain: "ASRService", | ||
|
|
@@ -644,6 +662,13 @@ final class ASRService: ObservableObject { | |
|
|
||
| /// Call this when the transcription provider setting changes to reset state | ||
| func resetTranscriptionProvider() { | ||
| guard !self.isFileTranscriptionRunning else { | ||
| DebugLogger.shared.warning( | ||
| "ASRService: Ignoring model reset during file transcription", | ||
| source: "ASRService" | ||
| ) | ||
| return | ||
| } | ||
| let newModel = SettingsStore.shared.selectedSpeechModel | ||
| DebugLogger.shared.info("ASRService: Switching to '\(newModel.displayName)', resetting provider state...", source: "ASRService") | ||
|
|
||
|
|
@@ -1386,6 +1411,10 @@ final class ASRService: ObservableObject { | |
| ) async -> AudioCaptureStartOutcome { | ||
| DebugLogger.shared.info("🎤 START() called - beginning recording session", source: "ASRService") | ||
|
|
||
| guard !self.isFileTranscriptionRunning else { | ||
| DebugLogger.shared.warning("START() blocked - file transcription is active", source: "ASRService") | ||
| return .failed | ||
| } | ||
| guard self.micStatus == .authorized else { | ||
| DebugLogger.shared.error("❌ START() blocked - mic not authorized", source: "ASRService") | ||
| return .failed | ||
|
|
@@ -3738,6 +3767,9 @@ final class ASRService: ObservableObject { | |
| // MARK: - Cache management | ||
|
|
||
| func clearModelCache() async throws { | ||
| guard !self.isFileTranscriptionRunning else { | ||
| throw self.fileTranscriptionModelOperationError() | ||
| } | ||
| DebugLogger.shared.debug("Clearing model cache via transcription provider", source: "ASRService") | ||
| await self.transcriptionExecutor.cancelAndAwaitPending() | ||
| try await self.transcriptionProvider.clearCache() | ||
|
|
@@ -3746,6 +3778,9 @@ final class ASRService: ObservableObject { | |
| } | ||
|
|
||
| func clearModelCache(for model: SettingsStore.SpeechModel) async throws { | ||
| guard !self.isFileTranscriptionRunning else { | ||
| throw self.fileTranscriptionModelOperationError() | ||
| } | ||
| DebugLogger.shared.debug("Clearing model cache for \(model.displayName)", source: "ASRService") | ||
| if SettingsStore.shared.selectedSpeechModel == model { | ||
| await self.transcriptionExecutor.cancelAndAwaitPending() | ||
|
|
@@ -3762,6 +3797,14 @@ final class ASRService: ObservableObject { | |
| await self.checkIfModelsExistAsync() | ||
| } | ||
|
|
||
| private func fileTranscriptionModelOperationError() -> NSError { | ||
| NSError( | ||
| domain: "ASRService", | ||
| code: -2002, | ||
| userInfo: [NSLocalizedDescriptionKey: "Voice model controls are unavailable during meeting transcription."] | ||
| ) | ||
| } | ||
|
|
||
| // MARK: - Timer-based Streaming Transcription (No VAD) | ||
|
|
||
| private func startStreamingTranscription() { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Routing Cohere through
WhisperProvideralso applies that provider'savailableMemoryGB < requiredMemoryGBguard, while Cohere's requirement is 8 GB andavailableMemoryGB()counts only free, inactive, and purgeable pages. An 8-GB Apple Silicon Mac can never report the full 8 GB as available after macOS is running, so users on an otherwise supported configuration now download the GGUF and then always receive an insufficient-memory error; use total physical memory or a realistic free-memory threshold for this model.Useful? React with 👍 / 👎.