From 75c51372aeb9e413d5e03ce2763b6d846e3316da Mon Sep 17 00:00:00 2001 From: Marco Hillger Date: Sat, 15 Aug 2026 12:49:49 +0200 Subject: [PATCH 1/2] Transcribe meetings from finalized recordings --- .../ViewModels/AudioRecorderViewModel.swift | 31 +++++++++- .../AudioRecorderViewModelTests.swift | 61 ++++++++++++++++++- 2 files changed, 90 insertions(+), 2 deletions(-) diff --git a/TypeWhisper/ViewModels/AudioRecorderViewModel.swift b/TypeWhisper/ViewModels/AudioRecorderViewModel.swift index 12ccd8764..f3dfd7c99 100644 --- a/TypeWhisper/ViewModels/AudioRecorderViewModel.swift +++ b/TypeWhisper/ViewModels/AudioRecorderViewModel.swift @@ -575,9 +575,17 @@ final class AudioRecorderViewModel: ObservableObject { let providerId = effectiveProviderId let dictionaryPrompt = dictionaryService.getTermsForPrompt(providerId: providerId) let dictionaryTermHints = dictionaryService.getTermHints(providerId: providerId) + let finalSamples = if liveSessionResult == nil { + await finalizedRecordingSamples( + from: url, + fallback: stoppedRecording.transcriptionSamples + ) + } else { + stoppedRecording.transcriptionSamples + } finalTranscriptionRequest = FinalTranscriptionRequest( outputURL: url, - buffer: stoppedRecording.transcriptionSamples, + buffer: finalSamples, languageSelection: languageSelection, task: selectedTask, providerId: providerId, @@ -634,6 +642,27 @@ final class AudioRecorderViewModel: ObservableObject { } } + private func finalizedRecordingSamples( + from outputURL: URL, + fallback captureSamples: [Float] + ) async -> [Float] { + do { + let samples = try await audioSamplesLoader(outputURL) + guard !samples.isEmpty else { + logger.warning( + "Finalized recording contained no transcription samples; using capture buffer" + ) + return captureSamples + } + return samples + } catch { + logger.warning( + "Could not load finalized recording for transcription; using capture buffer: \(error.localizedDescription, privacy: .public)" + ) + return captureSamples + } + } + // MARK: - HTTP API var apiRecorderIsRecording: Bool { diff --git a/TypeWhisperTests/AudioRecorderViewModelTests.swift b/TypeWhisperTests/AudioRecorderViewModelTests.swift index beb8a9ebc..ef482a494 100644 --- a/TypeWhisperTests/AudioRecorderViewModelTests.swift +++ b/TypeWhisperTests/AudioRecorderViewModelTests.swift @@ -450,6 +450,57 @@ final class AudioRecorderViewModelTests: XCTestCase { XCTAssertNil(recording.transcriptionFailure) } + func testCalendarMeetingFinalTranscriptionUsesFinalizedRecordingSamples() async throws { + try preserveStandardDefaults() + setupPluginManager(groqBehavior: .success("complete meeting transcript")) + let defaults = try makeDefaults() + let modelManager = ModelManagerService() + modelManager.selectProvider("groq") + let recordingsDirectory = makeTemporaryDirectory() + let captureSamples = Array( + repeating: Float(0.1), + count: Int(AudioRecorderService.transcriptionSampleRate) + ) + let finalizedFileSamples = Array( + repeating: Float(0.6), + count: Int(AudioRecorderService.transcriptionSampleRate * 2) + ) + let recorderService = makeRecorderService( + recordingsDirectory: recordingsDirectory, + samples: captureSamples + ) + var loadedURL: URL? + let viewModel = makeViewModel( + defaults: defaults, + modelManager: modelManager, + recorderService: recorderService, + audioSamplesLoader: { url in + loadedURL = url + return finalizedFileSamples + } + ) + viewModel.transcriptionEnabled = true + viewModel.livePreviewEnabled = false + + let handle = try await viewModel.startCalendarMeetingRecording( + preferredBaseName: "RC2 Meeting" + ) + try viewModel.stopCalendarMeetingRecording(handle: handle) + + try await waitForRecordingsToLoad(viewModel, count: 1) + + XCTAssertEqual(loadedURL?.standardizedFileURL, handle.outputURL.standardizedFileURL) + let plugin = try XCTUnwrap( + PluginManager.shared.transcriptionEngine(for: "groq") + as? AudioRecorderMockTranscriptionPlugin + ) + let request = try XCTUnwrap(plugin.lastRequest) + XCTAssertEqual(request.audioSampleCount, finalizedFileSamples.count) + XCTAssertEqual(request.firstAudioSample, finalizedFileSamples.first) + XCTAssertNotEqual(request.audioSampleCount, captureSamples.count) + XCTAssertEqual(viewModel.recordings.first?.transcript, "complete meeting transcript") + } + func testFinalTranscriptionDoesNotForceGlobalDefaultModelAsRecorderOverride() async throws { try preserveStandardDefaults() let defaults = try makeDefaults() @@ -1409,6 +1460,8 @@ private final class AudioRecorderMockTranscriptionPlugin: NSObject, Transcriptio let language: String? let translate: Bool let prompt: String? + let audioSampleCount: Int + let firstAudioSample: Float? } enum TranscriptionBehavior { @@ -1468,7 +1521,13 @@ private final class AudioRecorderMockTranscriptionPlugin: NSObject, Transcriptio translate: Bool, prompt: String? ) async throws -> PluginTranscriptionResult { - lastRequest = Request(language: language, translate: translate, prompt: prompt) + lastRequest = Request( + language: language, + translate: translate, + prompt: prompt, + audioSampleCount: audio.samples.count, + firstAudioSample: audio.samples.first + ) return switch behavior { case .success(let text): PluginTranscriptionResult(text: text) From d334a7b383cce50badfc2f11e05619d462caf631 Mon Sep 17 00:00:00 2001 From: Marco Hillger Date: Sat, 15 Aug 2026 13:34:08 +0200 Subject: [PATCH 2/2] Isolate integration tests from CoreAudio prewarm --- TypeWhisperTests/TypeWhisperIntegrationTests.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/TypeWhisperTests/TypeWhisperIntegrationTests.swift b/TypeWhisperTests/TypeWhisperIntegrationTests.swift index 5291d8932..1615378bb 100644 --- a/TypeWhisperTests/TypeWhisperIntegrationTests.swift +++ b/TypeWhisperTests/TypeWhisperIntegrationTests.swift @@ -7252,7 +7252,9 @@ final class TypeWhisperIntegrationTests: XCTestCase { } let audioFileService = AudioFileService() let audioRecordingService = AudioRecordingService( - bluetoothInputRouteStabilizer: audioRecordingBluetoothInputRouteStabilizer + bluetoothInputRouteStabilizer: audioRecordingBluetoothInputRouteStabilizer, + defaultInputController: APIFakeAudioInputDeviceDefaultController(defaultInputDeviceID: nil), + inputTransportResolver: FakeAudioDeviceTransportResolver(transports: [:]) ) let audioRecorderService = AudioRecorderService() audioRecorderService.recordingsDirectoryOverride = appSupportDirectory.appendingPathComponent("recordings") @@ -7578,6 +7580,8 @@ final class TypeWhisperIntegrationTests: XCTestCase { let audioRecordingService = AudioRecordingService( bluetoothInputRouteStabilizer: audioRecordingBluetoothInputRouteStabilizer, + defaultInputController: APIFakeAudioInputDeviceDefaultController(defaultInputDeviceID: nil), + inputTransportResolver: FakeAudioDeviceTransportResolver(transports: [:]), recoveryAudioStore: audioRecordingRecoveryAudioStore ) let hotkeyService = HotkeyService()