Skip to content

Commit dd4356f

Browse files
committed
fix
1 parent e2ce7ca commit dd4356f

2 files changed

Lines changed: 79 additions & 39 deletions

File tree

Hex.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Hex/Features/Transcription/TranscriptionFeature.swift

Lines changed: 76 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -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 {
179195
private 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

Comments
 (0)