From 479169b258d85907f1fa1a1d408668c8a4855441 Mon Sep 17 00:00:00 2001 From: Theo Weijmer Date: Thu, 1 Oct 2026 09:58:09 +0200 Subject: [PATCH] fix: skip silent recordings instead of transcribing them Co-Authored-By: Claude Sonnet 5.5 --- Sources/SstCore/DictationController.swift | 11 ++++++++++- .../SstCoreTests/DictationControllerTests.swift | 17 +++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/Sources/SstCore/DictationController.swift b/Sources/SstCore/DictationController.swift index 0577f6e..5db278c 100644 --- a/Sources/SstCore/DictationController.swift +++ b/Sources/SstCore/DictationController.swift @@ -13,20 +13,24 @@ public final class DictationController { private let cleaner: Cleaner private let inserter: TextInserting private let minimumSamples: Int + private let silenceThreshold: Float /// - Parameter minimumSamples: 16 kHz samples; default is 0.3 s. + /// - Parameter silenceThreshold: RMS below which a recording counts as silence and is skipped. public init( recorder: AudioRecording, transcriber: Transcriber, cleaner: Cleaner, inserter: TextInserting, - minimumSamples: Int = 4_800 + minimumSamples: Int = 4_800, + silenceThreshold: Float = 0.005 ) { self.recorder = recorder self.transcriber = transcriber self.cleaner = cleaner self.inserter = inserter self.minimumSamples = minimumSamples + self.silenceThreshold = silenceThreshold } public func handle(_ event: HotkeyEvent) async { @@ -44,6 +48,11 @@ public final class DictationController { state = .idle return } + let energy = samples.reduce(0) { $0 + $1 * $1 } + guard (energy / Float(samples.count)).squareRoot() >= silenceThreshold else { + state = .idle + return + } await process(samples) default: break diff --git a/Tests/SstCoreTests/DictationControllerTests.swift b/Tests/SstCoreTests/DictationControllerTests.swift index 3ba6832..0fa5329 100644 --- a/Tests/SstCoreTests/DictationControllerTests.swift +++ b/Tests/SstCoreTests/DictationControllerTests.swift @@ -45,6 +45,23 @@ struct DictationControllerTests { #expect(inserter.inserted.isEmpty) } + @Test func silentRecordingIsDiscarded() async { + recorder.samples = [Float](repeating: 0, count: 16_000) + let controller = makeController() + var states: [DictationState] = [] + var errors: [String] = [] + controller.onStateChange = { states.append($0) } + controller.onError = { errors.append($0) } + + await controller.handle(.pressed) + await controller.handle(.released) + + #expect(states == [.recording, .idle]) + #expect(transcriber.callCount == 0) + #expect(inserter.inserted.isEmpty) + #expect(errors.isEmpty) + } + @Test func emptyTranscriptionIsNotInserted() async { recorder.samples = Self.oneSecond transcriber.result = .success(" \n")