Add retry for failed transcriptions
This commit is contained in:
parent
63ea51113f
commit
fde8b168eb
@ -6,6 +6,7 @@ struct AppNotificationView: View {
|
||||
let type: NotificationType
|
||||
let duration: TimeInterval
|
||||
let onClose: () -> Void
|
||||
let onTap: (() -> Void)?
|
||||
|
||||
@State private var progress: Double = 1.0
|
||||
@State private var timer: Timer?
|
||||
@ -37,9 +38,7 @@ struct AppNotificationView: View {
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
// Main content
|
||||
HStack(alignment: .center, spacing: 12) {
|
||||
// App icon on the left side
|
||||
if let appIcon = NSApp.applicationIconImage {
|
||||
Image(nsImage: appIcon)
|
||||
.resizable()
|
||||
@ -64,9 +63,7 @@ struct AppNotificationView: View {
|
||||
Spacer()
|
||||
}
|
||||
.padding(12)
|
||||
.frame(height: 60) // Fixed compact height
|
||||
|
||||
// Close button overlaid on top-right
|
||||
.frame(height: 60)
|
||||
VStack {
|
||||
HStack {
|
||||
Spacer()
|
||||
@ -88,7 +85,6 @@ struct AppNotificationView: View {
|
||||
.fill(Color(nsColor: .controlBackgroundColor).opacity(0.95))
|
||||
)
|
||||
.overlay(
|
||||
// Progress bar at the bottom
|
||||
VStack {
|
||||
Spacer()
|
||||
GeometryReader { geometry in
|
||||
@ -107,6 +103,12 @@ struct AppNotificationView: View {
|
||||
.onDisappear {
|
||||
timer?.invalidate()
|
||||
}
|
||||
.onTapGesture {
|
||||
if let onTap = onTap {
|
||||
onTap()
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func startProgressTimer() {
|
||||
|
||||
@ -14,13 +14,16 @@ class NotificationManager {
|
||||
title: String,
|
||||
message: String,
|
||||
type: AppNotificationView.NotificationType,
|
||||
duration: TimeInterval = 8.0
|
||||
duration: TimeInterval = 8.0,
|
||||
onTap: (() -> Void)? = nil
|
||||
) {
|
||||
// If a notification is already showing, dismiss it before showing the new one.
|
||||
if notificationWindow != nil {
|
||||
dismissNotification()
|
||||
}
|
||||
dismissTimer?.invalidate()
|
||||
dismissTimer = nil
|
||||
|
||||
if let existingWindow = notificationWindow {
|
||||
existingWindow.close()
|
||||
notificationWindow = nil
|
||||
}
|
||||
let notificationView = AppNotificationView(
|
||||
title: title,
|
||||
message: message,
|
||||
@ -30,7 +33,8 @@ class NotificationManager {
|
||||
Task { @MainActor in
|
||||
self?.dismissNotification()
|
||||
}
|
||||
}
|
||||
},
|
||||
onTap: onTap
|
||||
)
|
||||
let hostingController = NSHostingController(rootView: notificationView)
|
||||
let size = hostingController.view.fittingSize
|
||||
@ -44,19 +48,17 @@ class NotificationManager {
|
||||
|
||||
panel.contentView = hostingController.view
|
||||
panel.isFloatingPanel = true
|
||||
panel.level = .mainMenu
|
||||
panel.backgroundColor = .clear
|
||||
panel.level = NSWindow.Level.mainMenu
|
||||
panel.backgroundColor = NSColor.clear
|
||||
panel.hasShadow = false
|
||||
panel.isMovableByWindowBackground = false
|
||||
|
||||
// Position at final location and start with fade animation
|
||||
positionWindow(panel)
|
||||
panel.alphaValue = 0
|
||||
panel.makeKeyAndOrderFront(nil)
|
||||
panel.makeKeyAndOrderFront(nil as Any?)
|
||||
|
||||
self.notificationWindow = panel
|
||||
|
||||
// Simple fade-in animation
|
||||
NSAnimationContext.runAnimationGroup({ context in
|
||||
context.duration = 0.3
|
||||
context.timingFunction = CAMediaTimingFunction(name: .easeOut)
|
||||
@ -74,19 +76,15 @@ class NotificationManager {
|
||||
|
||||
@MainActor
|
||||
private func positionWindow(_ window: NSWindow) {
|
||||
guard let screen = NSScreen.main else { return }
|
||||
let screenRect = screen.visibleFrame
|
||||
let activeScreen = NSApp.keyWindow?.screen ?? NSScreen.main ?? NSScreen.screens[0]
|
||||
let screenRect = activeScreen.visibleFrame
|
||||
let windowRect = window.frame
|
||||
|
||||
let x = screenRect.maxX - windowRect.width - 20 // 20px padding from the right
|
||||
let y = screenRect.maxY - windowRect.height - 20 // 20px padding from the top
|
||||
let x = screenRect.maxX - windowRect.width - 20
|
||||
let y = screenRect.maxY - windowRect.height - 20
|
||||
|
||||
window.setFrameOrigin(NSPoint(x: x, y: y))
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@MainActor
|
||||
func dismissNotification() {
|
||||
|
||||
@ -34,6 +34,8 @@ class WhisperState: NSObject, ObservableObject, AVAudioRecorderDelegate {
|
||||
|
||||
@Published var isVisualizerActive = false
|
||||
|
||||
|
||||
|
||||
@Published var isMiniRecorderVisible = false {
|
||||
didSet {
|
||||
if isMiniRecorderVisible {
|
||||
@ -281,26 +283,24 @@ class WhisperState: NSObject, ObservableObject, AVAudioRecorderDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
guard let model = currentTranscriptionModel else {
|
||||
logger.error("❌ Cannot transcribe: No model selected")
|
||||
return
|
||||
}
|
||||
logger.notice("🔄 Starting transcription...")
|
||||
|
||||
logger.notice("🔄 Starting transcription with model: \(model.displayName)")
|
||||
var permanentURL: URL?
|
||||
|
||||
do {
|
||||
// --- Core Transcription Logic ---
|
||||
permanentURL = try saveRecordingPermanently(url)
|
||||
|
||||
guard let model = currentTranscriptionModel else {
|
||||
throw WhisperStateError.transcriptionFailed
|
||||
}
|
||||
|
||||
|
||||
let transcriptionService: TranscriptionService = (model.provider == .local) ? localTranscriptionService : cloudTranscriptionService
|
||||
var text = try await transcriptionService.transcribe(audioURL: url, model: model)
|
||||
text = text.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
logger.notice("✅ Transcription completed successfully, length: \(text.count) characters")
|
||||
|
||||
// --- Post-processing and Saving ---
|
||||
let permanentURL = try saveRecordingPermanently(url)
|
||||
if UserDefaults.standard.bool(forKey: "IsWordReplacementEnabled") {
|
||||
text = WordReplacementService.shared.applyReplacements(to: text)
|
||||
logger.notice("✅ Word replacements applied")
|
||||
}
|
||||
|
||||
let audioAsset = AVURLAsset(url: url)
|
||||
@ -325,7 +325,7 @@ class WhisperState: NSObject, ObservableObject, AVAudioRecorderDelegate {
|
||||
text: originalText,
|
||||
duration: actualDuration,
|
||||
enhancedText: enhancedText,
|
||||
audioFileURL: permanentURL.absoluteString
|
||||
audioFileURL: permanentURL?.absoluteString
|
||||
)
|
||||
modelContext.insert(newTranscription)
|
||||
try? modelContext.save()
|
||||
@ -334,7 +334,7 @@ class WhisperState: NSObject, ObservableObject, AVAudioRecorderDelegate {
|
||||
let newTranscription = Transcription(
|
||||
text: originalText,
|
||||
duration: actualDuration,
|
||||
audioFileURL: permanentURL.absoluteString
|
||||
audioFileURL: permanentURL?.absoluteString
|
||||
)
|
||||
modelContext.insert(newTranscription)
|
||||
try? modelContext.save()
|
||||
@ -343,7 +343,7 @@ class WhisperState: NSObject, ObservableObject, AVAudioRecorderDelegate {
|
||||
let newTranscription = Transcription(
|
||||
text: originalText,
|
||||
duration: actualDuration,
|
||||
audioFileURL: permanentURL.absoluteString
|
||||
audioFileURL: permanentURL?.absoluteString
|
||||
)
|
||||
modelContext.insert(newTranscription)
|
||||
try? modelContext.save()
|
||||
@ -376,7 +376,48 @@ class WhisperState: NSObject, ObservableObject, AVAudioRecorderDelegate {
|
||||
await cleanupModelResources()
|
||||
|
||||
} catch {
|
||||
logger.error("❌ Transcription failed: \(error.localizedDescription)")
|
||||
if let permanentURL = permanentURL {
|
||||
do {
|
||||
let audioAsset = AVURLAsset(url: permanentURL)
|
||||
let duration = CMTimeGetSeconds(try await audioAsset.load(.duration))
|
||||
|
||||
await MainActor.run {
|
||||
let failedTranscription = Transcription(
|
||||
text: "Transcription Failed: \(error.localizedDescription)",
|
||||
duration: duration,
|
||||
enhancedText: nil,
|
||||
audioFileURL: permanentURL.absoluteString
|
||||
)
|
||||
|
||||
modelContext.insert(failedTranscription)
|
||||
try? modelContext.save()
|
||||
}
|
||||
} catch {
|
||||
// Silently continue if failed transcription record can't be saved
|
||||
}
|
||||
}
|
||||
|
||||
await MainActor.run {
|
||||
if permanentURL != nil {
|
||||
NotificationManager.shared.showNotification(
|
||||
title: "Transcription Failed",
|
||||
message: "🔄 Tap to retry transcription",
|
||||
type: .error,
|
||||
onTap: { [weak self] in
|
||||
Task {
|
||||
await self?.retryLastTranscription()
|
||||
}
|
||||
}
|
||||
)
|
||||
} else {
|
||||
NotificationManager.shared.showNotification(
|
||||
title: "Recording Failed",
|
||||
message: "Could not save audio file. Please try recording again.",
|
||||
type: .error
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
await cleanupModelResources()
|
||||
await dismissMiniRecorder()
|
||||
}
|
||||
@ -388,6 +429,53 @@ class WhisperState: NSObject, ObservableObject, AVAudioRecorderDelegate {
|
||||
try FileManager.default.copyItem(at: tempURL, to: permanentURL)
|
||||
return permanentURL
|
||||
}
|
||||
|
||||
func retryLastTranscription() async {
|
||||
do {
|
||||
let descriptor = FetchDescriptor<Transcription>(
|
||||
sortBy: [SortDescriptor(\.timestamp, order: .reverse)]
|
||||
)
|
||||
let transcriptions = try modelContext.fetch(descriptor)
|
||||
|
||||
guard let lastTranscription = transcriptions.first,
|
||||
lastTranscription.text.hasPrefix("Transcription Failed"),
|
||||
let audioURLString = lastTranscription.audioFileURL,
|
||||
let audioURL = URL(string: audioURLString) else {
|
||||
return
|
||||
}
|
||||
|
||||
guard let model = currentTranscriptionModel else {
|
||||
throw WhisperStateError.transcriptionFailed
|
||||
}
|
||||
|
||||
let transcriptionService = AudioTranscriptionService(modelContext: modelContext, whisperState: self)
|
||||
let newTranscription = try await transcriptionService.retranscribeAudio(from: audioURL, using: model)
|
||||
|
||||
await MainActor.run {
|
||||
NotificationManager.shared.showNotification(
|
||||
title: "Transcription Successful",
|
||||
message: "✅ Retry completed successfully",
|
||||
type: .success
|
||||
)
|
||||
|
||||
let textToPaste = newTranscription.enhancedText ?? newTranscription.text
|
||||
if AXIsProcessTrusted() {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
|
||||
CursorPaster.pasteAtCursor(textToPaste + " ", shouldPreserveClipboard: !self.isAutoCopyEnabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
NotificationManager.shared.showNotification(
|
||||
title: "Retry Failed",
|
||||
message: "Transcription failed again. Check your model and settings.",
|
||||
type: .error
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Loads the default transcription model from UserDefaults
|
||||
private func loadCurrentTranscriptionModel() {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user