Skip to content

Commit 32294c5

Browse files
committed
fix: decode short Whisper clips without a padding retry
1 parent 7d832ee commit 32294c5

2 files changed

Lines changed: 17 additions & 54 deletions

File tree

Sources/VocaMac/Services/WhisperService.swift

Lines changed: 6 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ final class WhisperService: @unchecked Sendable {
174174
usePrefillPrompt: language != nil || promptTokens != nil,
175175
detectLanguage: language == nil,
176176
wordTimestamps: false,
177+
windowClipTime: Self.windowClipTime(sampleCount: audioData.count),
177178
promptTokens: promptTokens,
178179
chunkingStrategy: nil // No chunking for short dictation clips
179180
)
@@ -206,22 +207,6 @@ final class WhisperService: @unchecked Sendable {
206207
fullText = Self.filterHallucinationTokens(rawText)
207208
}
208209

209-
// Whisper Tiny can return no tokens for valid sub-second speech.
210-
// Retry only an empty short result with trailing silence so normal
211-
// successful dictation keeps the single-pass fast path.
212-
if let paddedAudio = Self.paddedAudioForShortEmptyTranscription(
213-
audioData,
214-
transcription: fullText
215-
) {
216-
VocaLogger.warning(
217-
.whisperService,
218-
"Short transcription was empty for \(loadedModelName ?? "unknown model"); retrying with trailing silence"
219-
)
220-
results = try await kit.transcribe(audioArray: paddedAudio, decodeOptions: options)
221-
rawText = results.map { $0.text }.joined(separator: " ")
222-
fullText = Self.filterHallucinationTokens(rawText)
223-
}
224-
225210
let elapsed = CFAbsoluteTimeGetCurrent() - startTime
226211

227212
// Get detected language from first result
@@ -309,22 +294,11 @@ final class WhisperService: @unchecked Sendable {
309294
promptTokens != nil && rawText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
310295
}
311296

312-
/// Some Whisper models return no tokens for otherwise valid sub-second
313-
/// speech. A small trailing-silence pad gives the retry more context without
314-
/// changing the reported audio length.
315-
static func paddedAudioForShortEmptyTranscription(
316-
_ audio: [Float],
317-
transcription: String
318-
) -> [Float]? {
319-
let minimumSampleCount = 17_600 // 1.1 seconds at 16 kHz
320-
guard transcription.isEmpty,
321-
!audio.isEmpty,
322-
audio.count < minimumSampleCount else {
323-
return nil
324-
}
325-
var padded = audio
326-
padded.append(contentsOf: repeatElement(0, count: minimumSampleCount - audio.count))
327-
return padded
297+
/// WhisperKit only decodes while seek < end - windowClipTime. Its default
298+
/// one-second exclusion skips the entire clip at or below 16,000 samples.
299+
/// Retain the default trailing-window protection for longer recordings.
300+
static func windowClipTime(sampleCount: Int) -> Float {
301+
sampleCount <= 16_000 ? 0 : 1
328302
}
329303

330304
/// Encode custom vocabulary into WhisperKit conditioning tokens.

Tests/VocaMacTests/WhisperServiceTests.swift

Lines changed: 11 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
// Tests for WhisperService: translation and hallucination filtering.
55

66
import XCTest
7+
import WhisperKit
78
@testable import VocaMac
89

910
// MARK: - WhisperService Translation Tests
@@ -100,29 +101,17 @@ final class WhisperServiceVocabularyTests: XCTestCase {
100101
// MARK: - WhisperService Short Audio Tests
101102

102103
final class WhisperServiceShortAudioTests: XCTestCase {
103-
func testEmptyShortTranscriptionPadsToMinimumDecoderWindow() throws {
104-
let audio: [Float] = [0.25, -0.5, 0.75]
105-
let padded = try XCTUnwrap(
106-
WhisperService.paddedAudioForShortEmptyTranscription(audio, transcription: "")
107-
)
108-
109-
XCTAssertEqual(padded.count, 17_600)
110-
XCTAssertEqual(Array(padded.prefix(audio.count)), audio)
111-
XCTAssertTrue(padded.dropFirst(audio.count).allSatisfy { $0 == 0 })
104+
func testShortClipsEnterWhisperKitDecodeLoop() {
105+
for count in [1, 7_970, 15_999, 16_000] {
106+
let options = DecodingOptions(windowClipTime: WhisperService.windowClipTime(sampleCount: count))
107+
let excludedSamples = Int(options.windowClipTime * Float(WhisperKit.sampleRate))
108+
XCTAssertGreaterThan(count - excludedSamples, 0, "Clip of \(count) samples must be decoded")
109+
}
112110
}
113111

114-
func testSuccessfulOrLongTranscriptionDoesNotRetry() {
115-
XCTAssertNil(
116-
WhisperService.paddedAudioForShortEmptyTranscription([0.5], transcription: "Hello")
117-
)
118-
XCTAssertNil(
119-
WhisperService.paddedAudioForShortEmptyTranscription(
120-
Array(repeating: 0.5, count: 17_600),
121-
transcription: ""
122-
)
123-
)
124-
XCTAssertNil(
125-
WhisperService.paddedAudioForShortEmptyTranscription([], transcription: "")
126-
)
112+
func testLongerClipsKeepDefaultTrailingWindowProtection() {
113+
for count in [16_001, 17_600, 480_000, 960_000] {
114+
XCTAssertEqual(WhisperService.windowClipTime(sampleCount: count), DecodingOptions().windowClipTime)
115+
}
127116
}
128117
}

0 commit comments

Comments
 (0)