Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion TypeWhisper/ViewModels/AudioRecorderViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand Down
61 changes: 60 additions & 1 deletion TypeWhisperTests/AudioRecorderViewModelTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down
6 changes: 5 additions & 1 deletion TypeWhisperTests/TypeWhisperIntegrationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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()
Expand Down