From 1821fb1d7b4cb8602c1276673694d7ac08e83264 Mon Sep 17 00:00:00 2001 From: June Date: Sat, 7 Feb 2026 22:02:29 -0800 Subject: [PATCH 1/2] Fix: Retry Summarization no longer re-runs transcription When clicking "Retry Summarization", the app previously re-ran the entire Whisper transcription before re-summarizing. This was very slow for long recordings. Now it reuses the existing transcription text from the database and only re-runs the summarization step. Adds `retrySummarization(recordingID:)` to ProcessingCoordinator which skips the transcription phase and goes straight to summarization using the already-stored transcript. Fixes #12 Co-Authored-By: Claude Opus 4.6 --- .../Processing/ProcessingCoordinator.swift | 39 ++++++++++++++++++- .../ProcessingCoordinatorType.swift | 1 + .../Summary/ViewModel/SummaryViewModel.swift | 15 ++----- 3 files changed, 43 insertions(+), 12 deletions(-) diff --git a/Recap/Services/Processing/ProcessingCoordinator.swift b/Recap/Services/Processing/ProcessingCoordinator.swift index 4ad5461..6fd9bd9 100644 --- a/Recap/Services/Processing/ProcessingCoordinator.swift +++ b/Recap/Services/Processing/ProcessingCoordinator.swift @@ -59,9 +59,46 @@ final class ProcessingCoordinator: ProcessingCoordinatorType { func retryProcessing(recordingID: String) async { guard let recording = try? await recordingRepository.fetchRecording(id: recordingID), recording.canRetry else { return } - + await startProcessing(recordingInfo: recording) } + + func retrySummarization(recordingID: String) async { + guard let recording = try? await recordingRepository.fetchRecording(id: recordingID), + let transcriptionText = recording.transcriptionText, + !transcriptionText.isEmpty else { return } + + currentProcessingState = .processing(recordingID: recording.id) + delegate?.processingDidStart(recordingID: recording.id) + + processingTask = Task { + await resumeSummarization(recording, transcriptionText: transcriptionText) + } + + await processingTask?.value + currentProcessingState = .idle + } + + private func resumeSummarization(_ recording: RecordingInfo, transcriptionText: String) async { + let startTime = Date() + + do { + let summaryText = try await performSummarizationPhase(recording, transcriptionText: transcriptionText) + guard !Task.isCancelled else { throw ProcessingError.cancelled } + + await completeProcessing( + recording: recording, + transcriptionText: transcriptionText, + summaryText: summaryText, + startTime: startTime + ) + } catch let error as ProcessingError { + await handleProcessingError(error, for: recording) + } catch { + let processingError = ProcessingError.summarizationFailed(error.localizedDescription) + await handleProcessingError(processingError, for: recording) + } + } private func startQueueProcessing() { queueTask = Task { diff --git a/Recap/Services/Processing/ProcessingCoordinatorType.swift b/Recap/Services/Processing/ProcessingCoordinatorType.swift index ba93d02..a8eaf2d 100644 --- a/Recap/Services/Processing/ProcessingCoordinatorType.swift +++ b/Recap/Services/Processing/ProcessingCoordinatorType.swift @@ -14,6 +14,7 @@ protocol ProcessingCoordinatorType { func startProcessing(recordingInfo: RecordingInfo) async func cancelProcessing(recordingID: String) async func retryProcessing(recordingID: String) async + func retrySummarization(recordingID: String) async } @MainActor diff --git a/Recap/UseCases/Summary/ViewModel/SummaryViewModel.swift b/Recap/UseCases/Summary/ViewModel/SummaryViewModel.swift index 72ed8ae..5a4c86d 100644 --- a/Recap/UseCases/Summary/ViewModel/SummaryViewModel.swift +++ b/Recap/UseCases/Summary/ViewModel/SummaryViewModel.swift @@ -78,20 +78,13 @@ final class SummaryViewModel: SummaryViewModelType { func retryProcessing() async { guard let recording = currentRecording else { return } - + if recording.state == .transcriptionFailed { await processingCoordinator.retryProcessing(recordingID: recording.id) + } else if recording.state == .summarizationFailed || recording.state == .completed { + await processingCoordinator.retrySummarization(recordingID: recording.id) } else { - do { - try await recordingRepository.updateRecordingState( - id: recording.id, - state: .summarizing, - errorMessage: nil - ) - await processingCoordinator.startProcessing(recordingInfo: recording) - } catch { - errorMessage = "Failed to retry summarization: \(error.localizedDescription)" - } + await processingCoordinator.retryProcessing(recordingID: recording.id) } loadRecording(withID: recording.id) From 9564043cfd5601489017bdf54f98be2b365c1d38 Mon Sep 17 00:00:00 2001 From: June Date: Sat, 7 Feb 2026 22:26:04 -0800 Subject: [PATCH 2/2] Add tests for retrySummarization routing in SummaryViewModel Verifies that summarizationFailed and completed states route through retrySummarization instead of retryProcessing, avoiding redundant re-transcription. Co-Authored-By: Claude Opus 4.6 --- .../ViewModels/SummaryViewModelSpec.swift | 46 +++++++++++++++++-- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/RecapTests/UseCases/Summary/ViewModels/SummaryViewModelSpec.swift b/RecapTests/UseCases/Summary/ViewModels/SummaryViewModelSpec.swift index 79c04af..c3a979a 100644 --- a/RecapTests/UseCases/Summary/ViewModels/SummaryViewModelSpec.swift +++ b/RecapTests/UseCases/Summary/ViewModels/SummaryViewModelSpec.swift @@ -110,21 +110,59 @@ final class SummaryViewModelSpec: XCTestCase { func testRetryProcessingForTranscriptionFailed() async throws { let recording = createTestRecording(id: "test-id", state: .transcriptionFailed) sut.currentRecording = recording - + given(mockProcessingCoordinator) .retryProcessing(recordingID: .any) .willReturn() - + given(mockRecordingRepository) .fetchRecording(id: .any) .willReturn(recording) - + await sut.retryProcessing() - + verify(mockProcessingCoordinator) .retryProcessing(recordingID: .any) .called(1) } + + func testRetryProcessingForSummarizationFailed() async throws { + let recording = createTestRecording(id: "test-id", state: .summarizationFailed) + sut.currentRecording = recording + + given(mockProcessingCoordinator) + .retrySummarization(recordingID: .any) + .willReturn() + + given(mockRecordingRepository) + .fetchRecording(id: .any) + .willReturn(recording) + + await sut.retryProcessing() + + verify(mockProcessingCoordinator) + .retrySummarization(recordingID: .any) + .called(1) + } + + func testRetryProcessingForCompletedCallsRetrySummarization() async throws { + let recording = createTestRecording(id: "test-id", state: .completed, summaryText: "Old summary") + sut.currentRecording = recording + + given(mockProcessingCoordinator) + .retrySummarization(recordingID: .any) + .willReturn() + + given(mockRecordingRepository) + .fetchRecording(id: .any) + .willReturn(recording) + + await sut.retryProcessing() + + verify(mockProcessingCoordinator) + .retrySummarization(recordingID: .any) + .called(1) + } func testCopySummaryShowsToast() async throws { let recording = createTestRecording(