@@ -24,6 +24,7 @@ struct TranscriptionFeature {
2424 var recordingStartTime : Date ?
2525 var meter : Meter = . init( averagePower: 0 , peakPower: 0 )
2626 var assertionID : IOPMAssertionID ?
27+ var pendingTranscription : String ? // Store original transcription for fallback
2728 @Shared ( . hexSettings) var hexSettings : HexSettings
2829 @Shared ( . transcriptionHistory) var transcriptionHistory : TranscriptionHistory
2930 }
@@ -141,9 +142,20 @@ struct TranscriptionFeature {
141142 print ( " AI Enhancement error due to Ollama connectivity: \( error) " )
142143 return . send( . ollamaBecameUnavailable)
143144 } else {
144- // For other errors, just use the original transcription
145+ // For other errors, we need to:
146+ // 1. Log the error
147+ // 2. Disable AI enhancement status
148+ // 3. Fall back to the original transcription that produced this action
145149 print ( " AI Enhancement error: \( error) " )
146- return . none
150+
151+ // In the enhance method, there's a parameter to capture the original transcription
152+ // We'll modify enhanceWithAI() to store the original transcription for error case
153+
154+ // For now, use the bare minimum error message to inform the user
155+ state. error = " AI enhancement failed: \( error. localizedDescription) . Using original transcription instead. "
156+
157+ // Continue with original transcription processing
158+ return . send( . transcriptionResult( state. pendingTranscription ?? " " ) )
147159 }
148160
149161 case . ollamaBecameUnavailable:
@@ -155,9 +167,13 @@ struct TranscriptionFeature {
155167 return . run { send in
156168 let isAvailable = await aiEnhancement. isOllamaAvailable ( )
157169 if !isAvailable {
158- // Could dispatch to a UI state to show an alert or notification
159170 print ( " [TranscriptionFeature] Ollama is not available. AI enhancement is disabled. " )
160- // Here you would typically update UI state to show an alert
171+ // Update state to show error to the user
172+ await send ( . transcriptionError( NSError (
173+ domain: " TranscriptionFeature " ,
174+ code: - 1002 ,
175+ userInfo: [ NSLocalizedDescriptionKey: " Ollama is not available. AI enhancement is disabled. " ]
176+ ) ) )
161177 }
162178 }
163179
@@ -179,38 +195,51 @@ struct TranscriptionFeature {
179195private extension TranscriptionFeature {
180196 /// Effect to begin observing the audio meter.
181197 func startMeteringEffect( ) -> Effect < Action > {
182- . run { send in
183- // Use a rate limiter to prevent too many updates
184- var lastUpdateTime = Date ( )
185- var lastMeter : Meter ? = nil
186-
187- for await meter in await recording. observeAudioLevel ( ) {
188- // Apply main-thread protection
189- await MainActor . run {
190- // Rate limit updates based on time and significant changes
191- let now = Date ( )
192- let timeSinceLastUpdate = now. timeIntervalSince ( lastUpdateTime)
193-
194- // Determine if we should process this update
195- var shouldUpdate = false
196-
197- // Always update if enough time has passed (ensures UI responsiveness)
198- if timeSinceLastUpdate >= 0.05 { // Max 20 updates per second
199- shouldUpdate = true
200- }
201- // Or if there's a significant change from the last meter we actually sent
202- else if let last = lastMeter {
203- let averageDiff = abs ( meter. averagePower - last. averagePower)
204- let peakDiff = abs ( meter. peakPower - last. peakPower)
205- // More responsive threshold for significant changes
206- shouldUpdate = averageDiff > 0.02 || peakDiff > 0.04
207- }
208-
198+ // Create a separate actor to handle rate limiting safely in Swift 6
199+ actor MeterRateLimiter {
200+ private var lastUpdateTime = Date ( )
201+ private var lastMeter : Meter ? = nil
202+
203+ func shouldUpdate( meter: Meter ) -> Bool {
204+ let now = Date ( )
205+ let timeSinceLastUpdate = now. timeIntervalSince ( lastUpdateTime)
206+
207+ // Always update if enough time has passed (ensures UI responsiveness)
208+ if timeSinceLastUpdate >= 0.05 { // Max 20 updates per second
209+ self . lastUpdateTime = now
210+ self . lastMeter = meter
211+ return true
212+ }
213+ // Or if there's a significant change from the last meter we actually sent
214+ else if let last = lastMeter {
215+ let averageDiff = abs ( meter. averagePower - last. averagePower)
216+ let peakDiff = abs ( meter. peakPower - last. peakPower)
217+ // More responsive threshold for significant changes
218+ let shouldUpdate = averageDiff > 0.02 || peakDiff > 0.04
219+
209220 if shouldUpdate {
210- send ( . audioLevelUpdated( meter) )
211- lastUpdateTime = now
212- lastMeter = meter
221+ self . lastUpdateTime = now
222+ self . lastMeter = meter
213223 }
224+
225+ return shouldUpdate
226+ }
227+
228+ self . lastUpdateTime = now
229+ self . lastMeter = meter
230+ return true // First update always passes through
231+ }
232+ }
233+
234+ return . run { send in
235+ let rateLimiter = MeterRateLimiter ( )
236+
237+ for await meter in await recording. observeAudioLevel ( ) {
238+ // Check if we should send this update
239+ if await rateLimiter. shouldUpdate ( meter: meter) {
240+ // The Effect.run captures its function as @Sendable, so we're already on an appropriate context
241+ // for sending actions. ComposableArchitecture handles dispatching to the main thread as needed.
242+ await send ( . audioLevelUpdated( meter) )
214243 }
215244 }
216245 }
@@ -396,6 +425,9 @@ private extension TranscriptionFeature {
396425 if state. hexSettings. useAIEnhancement {
397426 // Keep state.isTranscribing = true since we're still processing
398427
428+ // Store the original transcription for error handling/fallback
429+ state. pendingTranscription = result
430+
399431 // Extract values to avoid capturing inout parameter
400432 let selectedAIModel = state. hexSettings. selectedAIModel
401433 let promptText = state. hexSettings. aiEnhancementPrompt
@@ -458,7 +490,9 @@ private extension TranscriptionFeature {
458490 . run { send in
459491 do {
460492 print ( " [TranscriptionFeature] Calling aiEnhancement.enhance() " )
461- let enhancedText = try await aiEnhancement. enhance ( result, model, options) { progress in
493+ // Access the raw value directly to avoid argument label issues
494+ let enhanceMethod = aiEnhancement. enhance
495+ let enhancedText = try await enhanceMethod ( result, model, options) { progress in
462496 // Optional: Could update UI with progress information here if needed
463497 }
464498 print ( " [TranscriptionFeature] AI enhancement succeeded " )
@@ -482,6 +516,7 @@ private extension TranscriptionFeature {
482516 state. isTranscribing = false
483517 state. isPrewarming = false
484518 state. isEnhancing = false // Reset the enhancing state
519+ state. pendingTranscription = nil // Clear the pending transcription since enhancement succeeded
485520
486521 // If empty text, nothing else to do
487522 guard !result. isEmpty else {
@@ -576,7 +611,12 @@ private extension TranscriptionFeature {
576611 return . merge(
577612 . cancel( id: CancelID . transcription) ,
578613 . cancel( id: CancelID . delayedRecord) ,
579- // Don't cancel AI enhancement as it might cause issues
614+ // Don't cancel AI enhancement as it might cause issues with Ollama
615+ // This creates a UI inconsistency where the UI shows cancellation
616+ // but enhancement continues in background. We intentionally allow this
617+ // to prevent issues with Ollama's streaming API and ensure stability.
618+ // TODO: Consider implementing a safer cancellation approach or state tracking
619+ // to properly ignore late results after cancellation.
580620 // .cancel(id: CancelID.aiEnhancement),
581621 . run { _ in
582622 await soundEffect. play ( . cancel)
0 commit comments