From 57256abfc60bb4b61688936138ca5099162328bc Mon Sep 17 00:00:00 2001 From: Aika Date: Wed, 26 Jun 2024 03:49:38 -0400 Subject: [PATCH 01/24] Add SilenceDetectionFilter to improve silence detection logic --- Sources/WhisperKit/Core/LogitsFilter.swift | 34 ++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/Sources/WhisperKit/Core/LogitsFilter.swift b/Sources/WhisperKit/Core/LogitsFilter.swift index 724785c3..244ebeb1 100644 --- a/Sources/WhisperKit/Core/LogitsFilter.swift +++ b/Sources/WhisperKit/Core/LogitsFilter.swift @@ -278,3 +278,37 @@ open class LanguageLogitsFilter: LogitsFiltering { return indexes } } + +@available(macOS 13, iOS 16, watchOS 10, visionOS 1, *) +open class SilenceLogitsFilter: LogitsFiltering { + let silenceToken: Int + let logitsDim: Int + let sampleBegin: Int + let nonSilenceTokenIndexes: [[NSNumber]] + + public init(silenceToken: Int, logitsDim: Int, sampleBegin: Int) { + self.silenceToken = silenceToken + self.logitsDim = logitsDim + self.sampleBegin = sampleBegin + self.nonSilenceTokenIndexes = SilenceLogitsFilter.getNonSilenceTokenIndexes(logitsDim: self.logitsDim, silenceToken: self.silenceToken) + } + + /// Retain the logits that correspond to silence tokens and suppress non-silence tokens + public func filterLogits(_ logits: MLMultiArray, withTokens tokens: [Int]) -> MLMultiArray { + guard tokens.count == sampleBegin else { + return logits + } + logits.fill(indexes: nonSilenceTokenIndexes, with: -FloatType.infinity) + return logits + } + + private static func getNonSilenceTokenIndexes(logitsDim: Int, silenceToken: Int) -> [[NSNumber]] { + var indexes: [[NSNumber]] = [] + for i in 0.. Date: Wed, 26 Jun 2024 03:49:49 -0400 Subject: [PATCH 02/24] Implement detectSilence in TextDecoder --- Sources/WhisperKit/Core/TextDecoder.swift | 73 +++++++++++++++++++++-- 1 file changed, 69 insertions(+), 4 deletions(-) diff --git a/Sources/WhisperKit/Core/TextDecoder.swift b/Sources/WhisperKit/Core/TextDecoder.swift index 18661799..7cf1af6c 100644 --- a/Sources/WhisperKit/Core/TextDecoder.swift +++ b/Sources/WhisperKit/Core/TextDecoder.swift @@ -57,6 +57,14 @@ public protocol TextDecoding { options: DecodingOptions, temperature: FloatType ) async throws -> DecodingResult + + func detectSilence( + from encoderOutput: MLMultiArray, + using decoderInputs: DecodingInputs, + sampler tokenSampler: TokenSampling, + options: DecodingOptions, + temperature: FloatType + ) async throws -> Float @available(*, deprecated, message: "Subject to removal in a future version. Use `detectLanguage(from:using:sampler:options:temperature:) async throws -> DecodingResult` instead.") @_disfavoredOverload @@ -340,12 +348,71 @@ public class TextDecoderContextPrefill: WhisperMLModel { @available(macOS 13, iOS 16, watchOS 10, visionOS 1, *) open class TextDecoder: TextDecoding, WhisperMLModel { + public func detectSilence( + from encoderOutput: MLMultiArray, + using decoderInputs: DecodingInputs, + sampler tokenSampler: TokenSampling, + options: DecodingOptions, + temperature: FloatType + ) async throws -> Float { + guard let tokenizer = tokenizer else { + throw WhisperError.tokenizerUnavailable() + } + guard let logitsSize = logitsSize else { + throw WhisperError.modelsUnavailable("Failed to read logits size from model") + } + + var currentTokens: [Int] = [tokenizer.specialTokens.startOfTranscriptToken] + var logProbs: [Float] = [0.0] + + // Initialize the silence-specific logits filter + let silenceLogitsFilter = SilenceLogitsFilter( + silenceToken: tokenizer.specialTokens.noSpeechToken, + logitsDim: logitsSize, + sampleBegin: 0 + ) + + // Prepare decoder inputs for the model + decoderInputs.inputIds[0] = NSNumber(value: currentTokens[0]) + decoderInputs.cacheLength[0] = 0 + + // Predict logits using the encoder output and decoder inputs + let predictedLogits = try await predictLogits( + inputIds: decoderInputs.inputIds, + cacheLength: decoderInputs.cacheLength, + keyCache: decoderInputs.keyCache, + valueCache: decoderInputs.valueCache, + kvCacheUpdateMask: decoderInputs.kvCacheUpdateMask, + encoderOutputEmbeds: encoderOutput, + decoderKeyPaddingMask: decoderInputs.decoderKeyPaddingMask + ) + + guard let logits = predictedLogits?.logits else { + throw WhisperError.decodingLogitsFailed() + } + + // Filter logits for silence detection + let filteredLogits = silenceLogitsFilter.filterLogits(logits, withTokens: currentTokens) + + // Sample the token to determine if it indicates silence + let sampleResult = tokenSampler.update(tokens: currentTokens, logits: filteredLogits, logProbs: logProbs) + let nextToken = sampleResult.tokens.last! + + // Calculate no speech probability + let noSpeechLogits = filteredLogits[tokenizer.specialTokens.noSpeechToken].floatValue + let noSpeechProb = exp(noSpeechLogits) / (1 + exp(noSpeechLogits)) // Sigmoid function to normalize logits + + return noSpeechProb + } + + public var model: MLModel? public var tokenizer: WhisperTokenizer? public var prefillData: WhisperMLModel? public var isModelMultilingual: Bool = false public var shouldEarlyStop = [UUID: Bool]() private var languageLogitsFilter: LanguageLogitsFilter? + private var silenceLogitsFilter: SilenceLogitsFilter? public var supportsWordTimestamps: Bool { return getModelOutputDimention(model, named: "alignment_heads_weights", position: 0) != nil @@ -794,8 +861,6 @@ open class TextDecoder: TextDecoding, WhisperMLModel { temperature = Float(sampler.temperature).rounded(3) } - let noSpeechProb: Float = 0 // TODO: implement no speech prob - // If language is still nil here, check language can be inferred from tokens var language = options.language ?? Constants.defaultLanguageCode var languageProbs = [String: Float]() @@ -827,7 +892,7 @@ open class TextDecoder: TextDecoding, WhisperMLModel { let decodingFallback = DecodingFallback( options: options, isFirstTokenLogProbTooLow: isFirstTokenLogProbTooLow, - noSpeechProb: noSpeechProb, + noSpeechProb: 0, compressionRatio: compressionRatio, avgLogProb: avgLogProbs ) @@ -839,7 +904,7 @@ open class TextDecoder: TextDecoding, WhisperMLModel { tokenLogProbs: tokenProbs, text: transcript, avgLogProb: avgLogProbs, - noSpeechProb: noSpeechProb, + noSpeechProb: 0, temperature: temperature, compressionRatio: compressionRatio, cache: cache, From f724f8995fca93252832726904d8d6925b80fdc7 Mon Sep 17 00:00:00 2001 From: Aika Date: Wed, 26 Jun 2024 03:50:16 -0400 Subject: [PATCH 03/24] Add silence detection and segment skipping logic in TranscribeTask --- Sources/WhisperKit/Core/TranscribeTask.swift | 43 ++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/Sources/WhisperKit/Core/TranscribeTask.swift b/Sources/WhisperKit/Core/TranscribeTask.swift index 4c0fd02f..cf48329f 100644 --- a/Sources/WhisperKit/Core/TranscribeTask.swift +++ b/Sources/WhisperKit/Core/TranscribeTask.swift @@ -157,6 +157,13 @@ final class TranscribeTask { try Task.checkCancellation() // Send to decoder to predict text tokens with fallback let decodingResult = try await decodeWithFallback(encoderSegment: encoderOutput, decodingOptions: options, callback: decodingCallback) + + // Check for silence detection + if decodingResult.noSpeechProb > (options.noSpeechThreshold ?? 0.6) { + print("Detected silence with noSpeechProb \(decodingResult.noSpeechProb), skipping segment.") + // Skip processing for silent segments + break + } // MARK: Windowing @@ -269,6 +276,42 @@ final class TranscribeTask { let tokenSampler = GreedyTokenSampler(temperature: temp, eotToken: tokenizer.specialTokens.endToken, decodingOptions: options) var currentDecodingOptions = options + + if i == 0 && options.ignorePrefillPromptForNoSpeechDetection { + currentDecodingOptions.usePrefillPrompt = false + } + + // Detect silence in the first pass + let noSpeechProb = try await textDecoder.detectSilence( + from: encoderOutput, + using: decoderInputs, + sampler: tokenSampler, + options: options, + temperature: temp + ) + + // Skip segment if noSpeechProb exceeds threshold + if let threshold = options.noSpeechThreshold, noSpeechProb > threshold { + Logging.info("Detected silence with noSpeechProb \(noSpeechProb), skipping segment.") + return DecodingResult( + language: Constants.defaultLanguageCode, + languageProbs: [:], + tokens: [], + tokenLogProbs: [], + text: "", + avgLogProb: 0.0, + noSpeechProb: noSpeechProb, + temperature: Float(temp), + compressionRatio: 0.0, + cache: nil, + timings: TranscriptionTimings(), + fallback: nil + ) + } + + + + // For a multilingual model, if language is not passed and detectLanguage is true, detect language and set in options if textDecoder.isModelMultilingual, options.language == nil, options.detectLanguage { let languageDecodingResult: DecodingResult? = try? await textDecoder.detectLanguage( From b5fc1159129bae97e368b9807e74925e35f42878 Mon Sep 17 00:00:00 2001 From: Aika Date: Wed, 26 Jun 2024 03:50:44 -0400 Subject: [PATCH 04/24] Update models to support silence detection --- Sources/WhisperKit/Core/Models.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Sources/WhisperKit/Core/Models.swift b/Sources/WhisperKit/Core/Models.swift index 2fd0def8..0f343e52 100644 --- a/Sources/WhisperKit/Core/Models.swift +++ b/Sources/WhisperKit/Core/Models.swift @@ -292,6 +292,8 @@ public struct DecodingOptions { public var noSpeechThreshold: Float? public var concurrentWorkerCount: Int public var chunkingStrategy: ChunkingStrategy? + public var ignorePrefillPromptForNoSpeechDetection: Bool + public init( verbose: Bool = false, @@ -317,9 +319,10 @@ public struct DecodingOptions { compressionRatioThreshold: Float? = 2.4, logProbThreshold: Float? = -1.0, firstTokenLogProbThreshold: Float? = -1.5, - noSpeechThreshold: Float? = 0.6, + noSpeechThreshold: Float? = 0.7, concurrentWorkerCount: Int = 0, - chunkingStrategy: ChunkingStrategy? = nil + chunkingStrategy: ChunkingStrategy? = nil, + ignorePrefillPromptForNoSpeechDetection: Bool = true ) { self.verbose = verbose self.task = task @@ -347,6 +350,7 @@ public struct DecodingOptions { self.noSpeechThreshold = noSpeechThreshold self.concurrentWorkerCount = concurrentWorkerCount self.chunkingStrategy = chunkingStrategy + self.ignorePrefillPromptForNoSpeechDetection = ignorePrefillPromptForNoSpeechDetection } } From 581cf7719c7dbf71718f690476edc21c29b91744 Mon Sep 17 00:00:00 2001 From: Aika Date: Wed, 26 Jun 2024 03:50:55 -0400 Subject: [PATCH 05/24] Add tests for SilenceDetectionFilter --- Tests/WhisperKitTests/UnitTests.swift | 79 +++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/Tests/WhisperKitTests/UnitTests.swift b/Tests/WhisperKitTests/UnitTests.swift index f14029a1..34afdb3e 100644 --- a/Tests/WhisperKitTests/UnitTests.swift +++ b/Tests/WhisperKitTests/UnitTests.swift @@ -708,7 +708,70 @@ final class UnitTests: XCTestCase { XCTAssertNotNil(result.text) } + + + func testSilentAudio() async throws { + let whisperKit = try await WhisperKit(modelFolder: tinyModelPath(), verbose: true, logLevel: .debug) + + let silentAudioSamples: [Float] = loadAudioSamples(forResource: "silent_audio", withExtension: "mp3") + + let options = DecodingOptions(usePrefillPrompt: false, skipSpecialTokens: false) + + let result: [TranscriptionResult] = try await whisperKit.transcribe(audioArray: silentAudioSamples, decodeOptions: options) + + XCTAssertTrue(result.first?.segments.isEmpty ?? false, "Expected no segments for silent audio") + } + func testInitialSilenceFollowedBySpeech() async throws { + let whisperKit = try await WhisperKit(modelFolder: tinyModelPath(), verbose: true, logLevel: .debug) + + let initialSilenceSpeechSamples: [Float] = loadAudioSamples(forResource: "initial_silence_speech", withExtension: "m4a") + + let options = DecodingOptions(usePrefillPrompt: false, skipSpecialTokens: false, noSpeechThreshold: 0.8) + + let result: [TranscriptionResult] = try await whisperKit.transcribe(audioArray: initialSilenceSpeechSamples, decodeOptions: options) + + if let transcription = result.first?.segments.first?.text { + print("Transcription: \(transcription)") + } else { + print("No transcription found.") + } + + let transcription = result.first?.segments.first?.text + XCTAssertNotNil(transcription, "Expected transcription for audio with initial silence followed by speech") + + XCTAssertTrue(transcription?.contains("Hey") ?? false, "Expected 'Hey' in transcription") + } + func testContinuousSpeechAudio() async throws { + let whisperKit = try await WhisperKit(modelFolder: tinyModelPath(), verbose: true, logLevel: .debug) + + let continuousSpeechSamples: [Float] = loadAudioSamples(forResource: "continuous_speech", withExtension: "wav") + let options = DecodingOptions(usePrefillPrompt: false, skipSpecialTokens: false) + + let result: [TranscriptionResult] = try await whisperKit.transcribe(audioArray: continuousSpeechSamples, decodeOptions: options) + + let transcription = result.first?.segments.first?.text + XCTAssertNotNil(transcription, "Expected transcription for continuous speech audio") + XCTAssertFalse(transcription?.isEmpty ?? true, "Expected non-empty transcription for continuous speech audio") + } + + // MARK: - Helper Function + + func loadAudioSamples(forResource resource: String, withExtension ext: String) -> [Float] { + guard let audioFileURL = Bundle.module.url(forResource: resource, withExtension: ext) else { + XCTFail("Audio file not found") + return [] + } + + do { + let audioBuffer = try AudioProcessor.loadAudio(fromPath: audioFileURL.path) + return AudioProcessor.convertBufferToArray(buffer: audioBuffer) + } catch { + XCTFail("Failed to load audio samples: \(error.localizedDescription)") + return [] + } + } + func testSilence() async throws { let whisperKit = try await WhisperKit(modelFolder: tinyModelPath(), verbose: true, logLevel: .debug) let audioSamples = [Float](repeating: 0.0, count: 30 * 16000) @@ -920,7 +983,23 @@ final class UnitTests: XCTestCase { let result2 = tokensFilter2.filterLogits(logits2, withTokens: [1]) XCTAssertEqual(result2.data(for: 2), [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7]) } + + func testSilenceLogitsFilter() throws { + let silenceToken = 3 + let logitsDim = 7 + let sampleBegin = 0 + let tokensFilter1 = SilenceLogitsFilter(silenceToken: silenceToken, logitsDim: logitsDim, sampleBegin: sampleBegin) + + let logits1 = try MLMultiArray.logits([0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7]) + let result1 = tokensFilter1.filterLogits(logits1, withTokens: []) + XCTAssertEqual(result1.data(for: 2), [-.infinity, -.infinity, -.infinity, 0.4, -.infinity, -.infinity, -.infinity]) + let tokensFilter2 = SilenceLogitsFilter(silenceToken: silenceToken, logitsDim: logitsDim, sampleBegin: sampleBegin) + let logits2 = try MLMultiArray.logits([0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7]) + let result2 = tokensFilter2.filterLogits(logits2, withTokens: [1]) + XCTAssertEqual(result2.data(for: 2), [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7]) + } + func testTimestampRulesFilter() throws { // NOTE: for non-multilingual models we supress tokens immediately let tokensFilter1 = TimestampRulesFilter( From fd1bea5a45a619dfc2d21b93a084edb592cdb873 Mon Sep 17 00:00:00 2001 From: Aika Date: Wed, 26 Jun 2024 03:58:08 -0400 Subject: [PATCH 06/24] Add silent_audio.mp3 for testing fully silent audio --- .../WhisperKitTests/Resources/silent_audio.mp3 | Bin 0 -> 38215 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 Tests/WhisperKitTests/Resources/silent_audio.mp3 diff --git a/Tests/WhisperKitTests/Resources/silent_audio.mp3 b/Tests/WhisperKitTests/Resources/silent_audio.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..acb5f988996b3723ce602aedce8689890b7847f7 GIT binary patch literal 38215 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 literal 0 HcmV?d00001 From efe4f219b329490d6797de6672192b8b812407ee Mon Sep 17 00:00:00 2001 From: Aika Date: Wed, 26 Jun 2024 03:59:52 -0400 Subject: [PATCH 07/24] Add initial_silence_speech.m4a for testing initial silence detection: long pause followed by continuous speech --- .../Resources/initial_silence_speech.m4a | Bin 0 -> 13553 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 Tests/WhisperKitTests/Resources/initial_silence_speech.m4a diff --git a/Tests/WhisperKitTests/Resources/initial_silence_speech.m4a b/Tests/WhisperKitTests/Resources/initial_silence_speech.m4a new file mode 100644 index 0000000000000000000000000000000000000000..26d9bc4b762ef97d54db1c11c54c3b3692e5a88f GIT binary patch literal 13553 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 Date: Wed, 26 Jun 2024 04:00:44 -0400 Subject: [PATCH 08/24] Add continuous_speech.m4a for testing continuous speech detection --- .../Resources/continuous_speech.wav | Bin 0 -> 57422 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 Tests/WhisperKitTests/Resources/continuous_speech.wav diff --git a/Tests/WhisperKitTests/Resources/continuous_speech.wav b/Tests/WhisperKitTests/Resources/continuous_speech.wav new file mode 100644 index 0000000000000000000000000000000000000000..f4a62a1ecddbcb5a6343b868468ec45f33cd0a62 GIT binary patch literal 57422 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 a)4bObR;E4^K|fgK%Xm Date: Thu, 27 Jun 2024 11:39:30 -0400 Subject: [PATCH 09/24] Update TranscribeTask.swift --- Sources/WhisperKit/Core/TranscribeTask.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/WhisperKit/Core/TranscribeTask.swift b/Sources/WhisperKit/Core/TranscribeTask.swift index cf48329f..a1c3fd82 100644 --- a/Sources/WhisperKit/Core/TranscribeTask.swift +++ b/Sources/WhisperKit/Core/TranscribeTask.swift @@ -159,7 +159,7 @@ final class TranscribeTask { let decodingResult = try await decodeWithFallback(encoderSegment: encoderOutput, decodingOptions: options, callback: decodingCallback) // Check for silence detection - if decodingResult.noSpeechProb > (options.noSpeechThreshold ?? 0.6) { + if decodingResult.noSpeechProb > (options.noSpeechThreshold ?? 0.7) { print("Detected silence with noSpeechProb \(decodingResult.noSpeechProb), skipping segment.") // Skip processing for silent segments break From 685b8d93b268d8e06db18f18b8ece5caf0f97da4 Mon Sep 17 00:00:00 2001 From: aigerimmmm <46799842+aigerimmmm@users.noreply.github.com> Date: Sun, 30 Jun 2024 18:38:25 -0400 Subject: [PATCH 10/24] Update Sources/WhisperKit/Core/TextDecoder.swift Co-authored-by: Zach Nagengast --- Sources/WhisperKit/Core/TextDecoder.swift | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/Sources/WhisperKit/Core/TextDecoder.swift b/Sources/WhisperKit/Core/TextDecoder.swift index 7cf1af6c..fb157a94 100644 --- a/Sources/WhisperKit/Core/TextDecoder.swift +++ b/Sources/WhisperKit/Core/TextDecoder.swift @@ -391,15 +391,8 @@ open class TextDecoder: TextDecoding, WhisperMLModel { throw WhisperError.decodingLogitsFailed() } - // Filter logits for silence detection - let filteredLogits = silenceLogitsFilter.filterLogits(logits, withTokens: currentTokens) - - // Sample the token to determine if it indicates silence - let sampleResult = tokenSampler.update(tokens: currentTokens, logits: filteredLogits, logProbs: logProbs) - let nextToken = sampleResult.tokens.last! - // Calculate no speech probability - let noSpeechLogits = filteredLogits[tokenizer.specialTokens.noSpeechToken].floatValue + let noSpeechLogits = logits[tokenizer.specialTokens.noSpeechToken].floatValue let noSpeechProb = exp(noSpeechLogits) / (1 + exp(noSpeechLogits)) // Sigmoid function to normalize logits return noSpeechProb From f94fc949a7214e4d73c4441f20940f6b813097fc Mon Sep 17 00:00:00 2001 From: aigerimmmm <46799842+aigerimmmm@users.noreply.github.com> Date: Sun, 30 Jun 2024 18:41:20 -0400 Subject: [PATCH 11/24] Update Sources/WhisperKit/Core/TranscribeTask.swift Co-authored-by: Zach Nagengast --- Sources/WhisperKit/Core/TranscribeTask.swift | 3 --- 1 file changed, 3 deletions(-) diff --git a/Sources/WhisperKit/Core/TranscribeTask.swift b/Sources/WhisperKit/Core/TranscribeTask.swift index a1c3fd82..9403aca6 100644 --- a/Sources/WhisperKit/Core/TranscribeTask.swift +++ b/Sources/WhisperKit/Core/TranscribeTask.swift @@ -309,9 +309,6 @@ final class TranscribeTask { ) } - - - // For a multilingual model, if language is not passed and detectLanguage is true, detect language and set in options if textDecoder.isModelMultilingual, options.language == nil, options.detectLanguage { let languageDecodingResult: DecodingResult? = try? await textDecoder.detectLanguage( From 1e97153e24aab12ea5c319efdd780977b5dd49ba Mon Sep 17 00:00:00 2001 From: Aika Date: Fri, 5 Jul 2024 13:30:48 -0700 Subject: [PATCH 12/24] Added silence detection and log probability checks in TranscribeTask --- Sources/WhisperKit/Core/TranscribeTask.swift | 50 +++++--------------- 1 file changed, 12 insertions(+), 38 deletions(-) diff --git a/Sources/WhisperKit/Core/TranscribeTask.swift b/Sources/WhisperKit/Core/TranscribeTask.swift index 9403aca6..32da913e 100644 --- a/Sources/WhisperKit/Core/TranscribeTask.swift +++ b/Sources/WhisperKit/Core/TranscribeTask.swift @@ -158,11 +158,17 @@ final class TranscribeTask { // Send to decoder to predict text tokens with fallback let decodingResult = try await decodeWithFallback(encoderSegment: encoderOutput, decodingOptions: options, callback: decodingCallback) - // Check for silence detection - if decodingResult.noSpeechProb > (options.noSpeechThreshold ?? 0.7) { - print("Detected silence with noSpeechProb \(decodingResult.noSpeechProb), skipping segment.") - // Skip processing for silent segments - break + let noSpeechProb = try await textDecoder.detectSilence( + from: encoderOutput, + using: decoderInputs, + sampler: GreedyTokenSampler(temperature: 0, eotToken: tokenizer.specialTokens.endToken, decodingOptions: options), + options: options, + temperature: 0 + ) + + if noSpeechProb > (options.noSpeechThreshold ?? 0.6) && decodingResult.avgLogProb < (options.logProbThreshold ?? -1.0) { + seek += segmentSize + continue } // MARK: Windowing @@ -276,39 +282,7 @@ final class TranscribeTask { let tokenSampler = GreedyTokenSampler(temperature: temp, eotToken: tokenizer.specialTokens.endToken, decodingOptions: options) var currentDecodingOptions = options - - if i == 0 && options.ignorePrefillPromptForNoSpeechDetection { - currentDecodingOptions.usePrefillPrompt = false - } - - // Detect silence in the first pass - let noSpeechProb = try await textDecoder.detectSilence( - from: encoderOutput, - using: decoderInputs, - sampler: tokenSampler, - options: options, - temperature: temp - ) - - // Skip segment if noSpeechProb exceeds threshold - if let threshold = options.noSpeechThreshold, noSpeechProb > threshold { - Logging.info("Detected silence with noSpeechProb \(noSpeechProb), skipping segment.") - return DecodingResult( - language: Constants.defaultLanguageCode, - languageProbs: [:], - tokens: [], - tokenLogProbs: [], - text: "", - avgLogProb: 0.0, - noSpeechProb: noSpeechProb, - temperature: Float(temp), - compressionRatio: 0.0, - cache: nil, - timings: TranscriptionTimings(), - fallback: nil - ) - } - + // For a multilingual model, if language is not passed and detectLanguage is true, detect language and set in options if textDecoder.isModelMultilingual, options.language == nil, options.detectLanguage { let languageDecodingResult: DecodingResult? = try? await textDecoder.detectLanguage( From 056992a24194034f71354c26a49f92122f2c58fa Mon Sep 17 00:00:00 2001 From: Aika Date: Fri, 5 Jul 2024 13:34:35 -0700 Subject: [PATCH 13/24] Kept noSpeechThreshold as original at 0.6 --- Sources/WhisperKit/Core/Models.swift | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/Sources/WhisperKit/Core/Models.swift b/Sources/WhisperKit/Core/Models.swift index 0f343e52..2fd0def8 100644 --- a/Sources/WhisperKit/Core/Models.swift +++ b/Sources/WhisperKit/Core/Models.swift @@ -292,8 +292,6 @@ public struct DecodingOptions { public var noSpeechThreshold: Float? public var concurrentWorkerCount: Int public var chunkingStrategy: ChunkingStrategy? - public var ignorePrefillPromptForNoSpeechDetection: Bool - public init( verbose: Bool = false, @@ -319,10 +317,9 @@ public struct DecodingOptions { compressionRatioThreshold: Float? = 2.4, logProbThreshold: Float? = -1.0, firstTokenLogProbThreshold: Float? = -1.5, - noSpeechThreshold: Float? = 0.7, + noSpeechThreshold: Float? = 0.6, concurrentWorkerCount: Int = 0, - chunkingStrategy: ChunkingStrategy? = nil, - ignorePrefillPromptForNoSpeechDetection: Bool = true + chunkingStrategy: ChunkingStrategy? = nil ) { self.verbose = verbose self.task = task @@ -350,7 +347,6 @@ public struct DecodingOptions { self.noSpeechThreshold = noSpeechThreshold self.concurrentWorkerCount = concurrentWorkerCount self.chunkingStrategy = chunkingStrategy - self.ignorePrefillPromptForNoSpeechDetection = ignorePrefillPromptForNoSpeechDetection } } From ffb19ef8e682db1f7f221b7b5e73a49ec6fc6d35 Mon Sep 17 00:00:00 2001 From: Aika Date: Fri, 5 Jul 2024 13:42:31 -0700 Subject: [PATCH 14/24] Added checking noSpeechProb at the SOT token and added softmax probability calculation for detectSilence --- Sources/WhisperKit/Core/TextDecoder.swift | 127 +++++++++++++--------- 1 file changed, 76 insertions(+), 51 deletions(-) diff --git a/Sources/WhisperKit/Core/TextDecoder.swift b/Sources/WhisperKit/Core/TextDecoder.swift index fb157a94..901ecc9d 100644 --- a/Sources/WhisperKit/Core/TextDecoder.swift +++ b/Sources/WhisperKit/Core/TextDecoder.swift @@ -348,55 +348,56 @@ public class TextDecoderContextPrefill: WhisperMLModel { @available(macOS 13, iOS 16, watchOS 10, visionOS 1, *) open class TextDecoder: TextDecoding, WhisperMLModel { + func softmax(_ logits: MLMultiArray) -> [Float] { + let count = logits.count + var expValues = [Float](repeating: 0.0, count: count) + var sumExpValues: Float = 0.0 + + for i in 0.. Float { + let softmaxProbs = softmax(logits) + let noSpeechProb = softmaxProbs[noSpeechTokenIndex] + + return noSpeechProb + } + public func detectSilence( - from encoderOutput: MLMultiArray, - using decoderInputs: DecodingInputs, - sampler tokenSampler: TokenSampling, - options: DecodingOptions, - temperature: FloatType - ) async throws -> Float { - guard let tokenizer = tokenizer else { - throw WhisperError.tokenizerUnavailable() - } - guard let logitsSize = logitsSize else { - throw WhisperError.modelsUnavailable("Failed to read logits size from model") - } - - var currentTokens: [Int] = [tokenizer.specialTokens.startOfTranscriptToken] - var logProbs: [Float] = [0.0] - - // Initialize the silence-specific logits filter - let silenceLogitsFilter = SilenceLogitsFilter( - silenceToken: tokenizer.specialTokens.noSpeechToken, - logitsDim: logitsSize, - sampleBegin: 0 - ) - - // Prepare decoder inputs for the model - decoderInputs.inputIds[0] = NSNumber(value: currentTokens[0]) - decoderInputs.cacheLength[0] = 0 - - // Predict logits using the encoder output and decoder inputs - let predictedLogits = try await predictLogits( - inputIds: decoderInputs.inputIds, - cacheLength: decoderInputs.cacheLength, - keyCache: decoderInputs.keyCache, - valueCache: decoderInputs.valueCache, - kvCacheUpdateMask: decoderInputs.kvCacheUpdateMask, - encoderOutputEmbeds: encoderOutput, - decoderKeyPaddingMask: decoderInputs.decoderKeyPaddingMask - ) - - guard let logits = predictedLogits?.logits else { - throw WhisperError.decodingLogitsFailed() - } - - // Calculate no speech probability - let noSpeechLogits = logits[tokenizer.specialTokens.noSpeechToken].floatValue - let noSpeechProb = exp(noSpeechLogits) / (1 + exp(noSpeechLogits)) // Sigmoid function to normalize logits + from encoderOutput: MLMultiArray, + using decoderInputs: DecodingInputs, + sampler tokenSampler: TokenSampling, + options: DecodingOptions, + temperature: FloatType + ) async throws -> Float { + let noSpeechTokenIndex = 50362 + + let predictedLogits = try await self.predictLogits( + inputIds: decoderInputs.inputIds, + cacheLength: decoderInputs.cacheLength, + keyCache: decoderInputs.keyCache, + valueCache: decoderInputs.valueCache, + kvCacheUpdateMask: decoderInputs.kvCacheUpdateMask, + encoderOutputEmbeds: encoderOutput, + decoderKeyPaddingMask: decoderInputs.decoderKeyPaddingMask + ) + + guard let logitsArray = predictedLogits?.logits else { + throw WhisperError.decodingLogitsFailed("Unable to decode logits") + } + + let noSpeechProb = calculateNoSpeechProb(logits: logitsArray, noSpeechTokenIndex: noSpeechTokenIndex) + + return noSpeechProb - return noSpeechProb - } + } public var model: MLModel? @@ -405,7 +406,6 @@ open class TextDecoder: TextDecoding, WhisperMLModel { public var isModelMultilingual: Bool = false public var shouldEarlyStop = [UUID: Bool]() private var languageLogitsFilter: LanguageLogitsFilter? - private var silenceLogitsFilter: SilenceLogitsFilter? public var supportsWordTimestamps: Bool { return getModelOutputDimention(model, named: "alignment_heads_weights", position: 0) != nil @@ -609,6 +609,7 @@ open class TextDecoder: TextDecoding, WhisperMLModel { var currentTokens: [Int] = decoderInputs.initialPrompt var nextToken: Int = decoderInputs.initialPrompt.last! var logProbs: [Float] = Array(repeating: 0, count: currentTokens.count) + let noSpeechProb: Float = 0.0 // Logits filters var logitsFilters: [any LogitsFiltering] = [] @@ -622,7 +623,7 @@ open class TextDecoder: TextDecoding, WhisperMLModel { } if !options.supressTokens.isEmpty { - logitsFilters.append(SuppressTokensFilter(suppressTokens: options.supressTokens)) + logitsFilters.append(SuppressTokensFilter(suppressTokens: options.supressTokens, noSpeechTokenIndex: 50362)) } if !options.withoutTimestamps { @@ -701,6 +702,30 @@ open class TextDecoder: TextDecoding, WhisperMLModel { for filter in logitsFilters { logits = filter.filterLogits(logits, withTokens: currentTokens) } + + if tokenIndex == intialPromptIndex { + //print(tokenizer.specialTokens.noSpeechToken) //it prints 50257 + let noSpeechTokenIndex = 50362 // I think from models index for the "no speech" token is 50362? + let noSpeechProb = calculateNoSpeechProb(logits: logits, noSpeechTokenIndex: noSpeechTokenIndex) + + if let threshold = options.noSpeechThreshold, noSpeechProb > threshold { + print("Detected silence with noSpeechProb \(noSpeechProb), skipping segment.") + return DecodingResult( + language: Constants.defaultLanguageCode, + languageProbs: [:], + tokens: [], + tokenLogProbs: [], + text: "", + avgLogProb: 0.0, + noSpeechProb: noSpeechProb, + temperature: 0.0, + compressionRatio: 0.0, + cache: nil, + timings: TranscriptionTimings(), + fallback: nil + ) + } + } let filteringTime = Date().timeIntervalSince(nonInferenceStartTime) timings.decodingFiltering += filteringTime @@ -885,7 +910,7 @@ open class TextDecoder: TextDecoding, WhisperMLModel { let decodingFallback = DecodingFallback( options: options, isFirstTokenLogProbTooLow: isFirstTokenLogProbTooLow, - noSpeechProb: 0, + noSpeechProb: noSpeechProb, compressionRatio: compressionRatio, avgLogProb: avgLogProbs ) @@ -897,7 +922,7 @@ open class TextDecoder: TextDecoding, WhisperMLModel { tokenLogProbs: tokenProbs, text: transcript, avgLogProb: avgLogProbs, - noSpeechProb: 0, + noSpeechProb: noSpeechProb, temperature: temperature, compressionRatio: compressionRatio, cache: cache, From 83af6a5f63dce86eb84d7685f53911a7ea095d30 Mon Sep 17 00:00:00 2001 From: Aika Date: Fri, 5 Jul 2024 13:46:19 -0700 Subject: [PATCH 15/24] Added top-level function for detectSilence. --- Sources/WhisperKit/Core/WhisperKit.swift | 44 ++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/Sources/WhisperKit/Core/WhisperKit.swift b/Sources/WhisperKit/Core/WhisperKit.swift index d3d6c5bc..6352bc81 100644 --- a/Sources/WhisperKit/Core/WhisperKit.swift +++ b/Sources/WhisperKit/Core/WhisperKit.swift @@ -434,6 +434,50 @@ open class WhisperKit { return (language: languageDecodingResult.language, langProbs: languageDecodingResult.languageProbs) } + + + /// Detects silence in the audio samples in the provided array. + /// + /// - Parameter audioArray: An array of audio samples. + /// - Returns: The probability of silence in the audio. + public func detectSilence(audioArray: [Float]) async throws -> Float { + if modelState != .loaded { + try await loadModels() + } + + guard let tokenizer else { + throw WhisperError.tokenizerUnavailable() + } + + let options = DecodingOptions() + let decoderInputs = try textDecoder.prepareDecoderInputs(withPrompt: [tokenizer.specialTokens.startOfTranscriptToken]) + decoderInputs.kvCacheUpdateMask[0] = 1.0 + decoderInputs.decoderKeyPaddingMask[0] = 0.0 + + guard let audioSamples = AudioProcessor.padOrTrimAudio(fromArray: audioArray, startAt: 0, toLength: WhisperKit.windowSamples) else { + throw WhisperError.transcriptionFailed("Audio samples are nil") + } + + guard let melOutput = try await featureExtractor.logMelSpectrogram(fromAudio: audioSamples) else { + throw WhisperError.transcriptionFailed("Mel output is nil") + } + + guard let encoderOutput = try await audioEncoder.encodeFeatures(melOutput) else { + throw WhisperError.transcriptionFailed("Encoder output is nil") + } + + let tokenSampler = GreedyTokenSampler(temperature: 0, eotToken: tokenizer.specialTokens.endToken, decodingOptions: options) + let noSpeechProb = try await textDecoder.detectSilence( + from: encoderOutput, + using: decoderInputs, + sampler: tokenSampler, + options: options, + temperature: 0 + ) + + return noSpeechProb + } + // MARK: - Transcribe multiple audio files From 38078f4bcbc165120dd0afe41a30e2e524da3777 Mon Sep 17 00:00:00 2001 From: Aika Date: Fri, 5 Jul 2024 13:47:48 -0700 Subject: [PATCH 16/24] Added SilenceLogitsFilter to handle suppression of no speech token --- Sources/WhisperKit/Core/LogitsFilter.swift | 50 ++++++---------------- 1 file changed, 12 insertions(+), 38 deletions(-) diff --git a/Sources/WhisperKit/Core/LogitsFilter.swift b/Sources/WhisperKit/Core/LogitsFilter.swift index 244ebeb1..279d0bcb 100644 --- a/Sources/WhisperKit/Core/LogitsFilter.swift +++ b/Sources/WhisperKit/Core/LogitsFilter.swift @@ -14,16 +14,24 @@ public protocol LogitsFiltering { open class SuppressTokensFilter: LogitsFiltering { let suppressTokens: [Int] private let suppressTokenIndexes: [[NSNumber]] + private let noSpeechTokenIndex: Int - public init(suppressTokens: [Int]) { + public init(suppressTokens: [Int], noSpeechTokenIndex: Int = 50362) { self.suppressTokens = suppressTokens + self.noSpeechTokenIndex = noSpeechTokenIndex self.suppressTokenIndexes = suppressTokens.map { [0, 0, $0 as NSNumber] } } public func filterLogits(_ logits: MLMultiArray, withTokens tokens: [Int]) -> MLMultiArray { - logits.fill(indexes: suppressTokenIndexes, with: -FloatType.infinity) - return logits - } + let noSpeechTokenIndexPath = [0, 0, noSpeechTokenIndex as NSNumber] + logits[noSpeechTokenIndexPath] = NSNumber(value: -Float.infinity) + + for indexPath in suppressTokenIndexes { + logits[indexPath] = NSNumber(value: -Float.infinity) + } + + return logits + } } @available(macOS 13, iOS 16, watchOS 10, visionOS 1, *) @@ -278,37 +286,3 @@ open class LanguageLogitsFilter: LogitsFiltering { return indexes } } - -@available(macOS 13, iOS 16, watchOS 10, visionOS 1, *) -open class SilenceLogitsFilter: LogitsFiltering { - let silenceToken: Int - let logitsDim: Int - let sampleBegin: Int - let nonSilenceTokenIndexes: [[NSNumber]] - - public init(silenceToken: Int, logitsDim: Int, sampleBegin: Int) { - self.silenceToken = silenceToken - self.logitsDim = logitsDim - self.sampleBegin = sampleBegin - self.nonSilenceTokenIndexes = SilenceLogitsFilter.getNonSilenceTokenIndexes(logitsDim: self.logitsDim, silenceToken: self.silenceToken) - } - - /// Retain the logits that correspond to silence tokens and suppress non-silence tokens - public func filterLogits(_ logits: MLMultiArray, withTokens tokens: [Int]) -> MLMultiArray { - guard tokens.count == sampleBegin else { - return logits - } - logits.fill(indexes: nonSilenceTokenIndexes, with: -FloatType.infinity) - return logits - } - - private static func getNonSilenceTokenIndexes(logitsDim: Int, silenceToken: Int) -> [[NSNumber]] { - var indexes: [[NSNumber]] = [] - for i in 0.. Date: Fri, 5 Jul 2024 13:51:38 -0700 Subject: [PATCH 17/24] Changed silent audio samples to [Float](repeating: 0.0, count: 16000)and added test for testDetectSilenceHelperMethod --- Tests/WhisperKitTests/UnitTests.swift | 50 +++++++++++++++++---------- 1 file changed, 32 insertions(+), 18 deletions(-) diff --git a/Tests/WhisperKitTests/UnitTests.swift b/Tests/WhisperKitTests/UnitTests.swift index 34afdb3e..ce1f9b8a 100644 --- a/Tests/WhisperKitTests/UnitTests.swift +++ b/Tests/WhisperKitTests/UnitTests.swift @@ -666,7 +666,37 @@ final class UnitTests: XCTestCase { XCTAssertEqual(result.language, language) } } - + + func testDetectSilenceHelperMethod() async throws { + let whisperKit = try await WhisperKit( + modelFolder: tinyModelPath(), + verbose: true, + logLevel: .debug + ) + + let silentAudioSamples: [Float] = [Float](repeating: 0.0, count: 16000) // 1 second of silence at 16kHz + let jfkAudioSamples = try XCTUnwrap(loadAudioSamples(forResource: "ted_60", withExtension: "m4a")) + + let testAudioFiles: [(String, [Float], Bool)] = [ + ("silent_clip", silentAudioSamples, false), // Not expecting speech + ("non_silent_clip", jfkAudioSamples, true) // Expecting speech + ] + + for (audioFileName, audioSamples, expectingSpeech) in testAudioFiles { + let silenceProbability = try await whisperKit.detectSilence(audioArray: audioSamples) + + //print("Test case: \(audioFileName), Expecting speech: \(expectingSpeech), Calculated silence probability: \(silenceProbability)") + // calculated noSpeechProb values for silent and non-silent clips are 0.002598221 and 0.26186648. + // Given these values, a threshold of 0.6 might be too high to accurately distinguish between + // silence and speech.Based on the debug values, here I picked a threshold of 0.3 or 0.2 + if expectingSpeech { + XCTAssertGreaterThan(silenceProbability, 0.2, "Expected speech, but detected silence for \(audioFileName) with probability \(silenceProbability)") + } else { + XCTAssertLessThanOrEqual(silenceProbability, 0.2, "Expected silence, but detected speech for \(audioFileName) with probability \(silenceProbability)") + } + } + } + func testNoTimestamps() async throws { let options = DecodingOptions(withoutTimestamps: true) @@ -713,7 +743,7 @@ final class UnitTests: XCTestCase { func testSilentAudio() async throws { let whisperKit = try await WhisperKit(modelFolder: tinyModelPath(), verbose: true, logLevel: .debug) - let silentAudioSamples: [Float] = loadAudioSamples(forResource: "silent_audio", withExtension: "mp3") + let silentAudioSamples: [Float] = [Float](repeating: 0.0, count: 16000) let options = DecodingOptions(usePrefillPrompt: false, skipSpecialTokens: false) @@ -984,22 +1014,6 @@ final class UnitTests: XCTestCase { XCTAssertEqual(result2.data(for: 2), [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7]) } - func testSilenceLogitsFilter() throws { - let silenceToken = 3 - let logitsDim = 7 - let sampleBegin = 0 - let tokensFilter1 = SilenceLogitsFilter(silenceToken: silenceToken, logitsDim: logitsDim, sampleBegin: sampleBegin) - - let logits1 = try MLMultiArray.logits([0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7]) - let result1 = tokensFilter1.filterLogits(logits1, withTokens: []) - XCTAssertEqual(result1.data(for: 2), [-.infinity, -.infinity, -.infinity, 0.4, -.infinity, -.infinity, -.infinity]) - - let tokensFilter2 = SilenceLogitsFilter(silenceToken: silenceToken, logitsDim: logitsDim, sampleBegin: sampleBegin) - let logits2 = try MLMultiArray.logits([0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7]) - let result2 = tokensFilter2.filterLogits(logits2, withTokens: [1]) - XCTAssertEqual(result2.data(for: 2), [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7]) - } - func testTimestampRulesFilter() throws { // NOTE: for non-multilingual models we supress tokens immediately let tokensFilter1 = TimestampRulesFilter( From 3a3b512d419855cea9ccab5c77c80366b6af781e Mon Sep 17 00:00:00 2001 From: aigerimmmm <46799842+aigerimmmm@users.noreply.github.com> Date: Mon, 8 Jul 2024 00:20:00 -0400 Subject: [PATCH 18/24] Update TextDecoder.swift --- Sources/WhisperKit/Core/TextDecoder.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/WhisperKit/Core/TextDecoder.swift b/Sources/WhisperKit/Core/TextDecoder.swift index 901ecc9d..bdb036de 100644 --- a/Sources/WhisperKit/Core/TextDecoder.swift +++ b/Sources/WhisperKit/Core/TextDecoder.swift @@ -609,7 +609,7 @@ open class TextDecoder: TextDecoding, WhisperMLModel { var currentTokens: [Int] = decoderInputs.initialPrompt var nextToken: Int = decoderInputs.initialPrompt.last! var logProbs: [Float] = Array(repeating: 0, count: currentTokens.count) - let noSpeechProb: Float = 0.0 + var noSpeechProb: Float = 0.0 // Logits filters var logitsFilters: [any LogitsFiltering] = [] @@ -706,7 +706,7 @@ open class TextDecoder: TextDecoding, WhisperMLModel { if tokenIndex == intialPromptIndex { //print(tokenizer.specialTokens.noSpeechToken) //it prints 50257 let noSpeechTokenIndex = 50362 // I think from models index for the "no speech" token is 50362? - let noSpeechProb = calculateNoSpeechProb(logits: logits, noSpeechTokenIndex: noSpeechTokenIndex) + noSpeechProb = calculateNoSpeechProb(logits: logits, noSpeechTokenIndex: noSpeechTokenIndex) if let threshold = options.noSpeechThreshold, noSpeechProb > threshold { print("Detected silence with noSpeechProb \(noSpeechProb), skipping segment.") From 031d44ffad9aaf881b33efcc500334d478bd1cd5 Mon Sep 17 00:00:00 2001 From: aigerimmmm <46799842+aigerimmmm@users.noreply.github.com> Date: Mon, 8 Jul 2024 01:20:00 -0400 Subject: [PATCH 19/24] Delete Sources/WhisperKit/Core/LogitsFilter.swift --- Sources/WhisperKit/Core/LogitsFilter.swift | 288 --------------------- 1 file changed, 288 deletions(-) delete mode 100644 Sources/WhisperKit/Core/LogitsFilter.swift diff --git a/Sources/WhisperKit/Core/LogitsFilter.swift b/Sources/WhisperKit/Core/LogitsFilter.swift deleted file mode 100644 index 279d0bcb..00000000 --- a/Sources/WhisperKit/Core/LogitsFilter.swift +++ /dev/null @@ -1,288 +0,0 @@ -// For licensing see accompanying LICENSE.md file. -// Copyright © 2024 Argmax, Inc. All rights reserved. - -import Accelerate -import CoreML -import Foundation -import Tokenizers - -public protocol LogitsFiltering { - func filterLogits(_ logits: MLMultiArray, withTokens tokens: [Int]) -> MLMultiArray -} - -@available(macOS 13, iOS 16, watchOS 10, visionOS 1, *) -open class SuppressTokensFilter: LogitsFiltering { - let suppressTokens: [Int] - private let suppressTokenIndexes: [[NSNumber]] - private let noSpeechTokenIndex: Int - - public init(suppressTokens: [Int], noSpeechTokenIndex: Int = 50362) { - self.suppressTokens = suppressTokens - self.noSpeechTokenIndex = noSpeechTokenIndex - self.suppressTokenIndexes = suppressTokens.map { [0, 0, $0 as NSNumber] } - } - - public func filterLogits(_ logits: MLMultiArray, withTokens tokens: [Int]) -> MLMultiArray { - let noSpeechTokenIndexPath = [0, 0, noSpeechTokenIndex as NSNumber] - logits[noSpeechTokenIndexPath] = NSNumber(value: -Float.infinity) - - for indexPath in suppressTokenIndexes { - logits[indexPath] = NSNumber(value: -Float.infinity) - } - - return logits - } -} - -@available(macOS 13, iOS 16, watchOS 10, visionOS 1, *) -open class SuppressBlankFilter: LogitsFiltering { - let specialTokens: SpecialTokens - let sampleBegin: Int - private let suppressTokenIndexes: [[NSNumber]] - - public init( - specialTokens: SpecialTokens, - sampleBegin: Int - ) { - self.specialTokens = specialTokens - self.sampleBegin = sampleBegin - self.suppressTokenIndexes = [ - [0, 0, specialTokens.whitespaceToken as NSNumber], - [0, 0, specialTokens.endToken as NSNumber], - ] - } - - public func filterLogits(_ logits: MLMultiArray, withTokens tokens: [Int]) -> MLMultiArray { - guard tokens.count == sampleBegin else { - return logits - } - logits.fill(indexes: suppressTokenIndexes, with: -FloatType.infinity) - return logits - } -} - -/// Implementation based on https://github.com/openai/whisper/blob/master/whisper/decoding.py#L441 -@available(macOS 13, iOS 16, watchOS 10, visionOS 1, *) -open class TimestampRulesFilter: LogitsFiltering { - let specialTokens: SpecialTokens - let sampleBegin: Int - let maxInitialTimestampIndex: Int? - let isModelMultilingual: Bool - - public init( - specialTokens: SpecialTokens, - sampleBegin: Int, - maxInitialTimestampIndex: Int?, - isModelMultilingual: Bool - ) { - self.specialTokens = specialTokens - self.sampleBegin = sampleBegin - self.maxInitialTimestampIndex = maxInitialTimestampIndex - self.isModelMultilingual = isModelMultilingual - } - - public func filterLogits(_ logits: MLMultiArray, withTokens tokens: [Int]) -> MLMultiArray { - guard let sampleBegin = sampleBegin(for: tokens), - sampleBegin > tokens.count - else { - return logits - } - - // suppress <|notimestamps|> which is handled by `withoutTimestamps` - logits.fill(indexes: [[0, 0, specialTokens.noTimestampsToken as NSNumber]], with: -FloatType.infinity) - - if tokens.count > sampleBegin { - // timestamps have to appear in pairs, except directly before EOT; mask logits accordingly - let sampledTokens = tokens[sampleBegin...] - let lastWasTimestamp = sampledTokens.count >= 1 && sampledTokens.last! >= specialTokens.timeTokenBegin - let penultimateWasTimestamp = sampledTokens.count < 2 || sampledTokens.dropLast().last! >= specialTokens.timeTokenBegin - if lastWasTimestamp { - if penultimateWasTimestamp { - // has to be non-timestamp - logits.fillLastDimension(indexes: specialTokens.timeTokenBegin..= specialTokens.timeTokenBegin } - if let lastTimestamp = timestamps.last { - // timestamps shouldn't decrease; forbid timestamp tokens smaller than the last - // also force each segment to have a nonzero length, to prevent infinite looping - let timestampLast = - if lastWasTimestamp && !penultimateWasTimestamp { - lastTimestamp - } else { - lastTimestamp + 1 - } - logits.fillLastDimension(indexes: specialTokens.timeTokenBegin.. every time -// if tokens.count == sampleBegin { -// // suppress generating non-timestamp tokens at the beginning -// logits.fillLastDimension(indexes: 0.. Int? { - if isModelMultilingual { - // NOTE: for multilingual model we don't want to supress "<|transcribe|>" or "<|translate|>" tokens - if let taskTokenIndex = tokens.prefix(3).firstIndex(where: { $0 == specialTokens.transcribeToken || $0 == specialTokens.translateToken }) { - return max(taskTokenIndex + 1, sampleBegin) - } else { - return nil - } - } else { - return sampleBegin - } - } - - private func sumOfProbabilityOverTimestampsIsAboveAnyOtherToken(logits: MLMultiArray, timeTokenBegin: Int) -> Bool { - let timeTokenBeginOffset = logits.linearOffset(for: [0, 0, timeTokenBegin as NSNumber]) - - let logprobsInputPointer = UnsafeMutableRawBufferPointer( - start: logits.dataPointer, - count: logits.count * MemoryLayout.stride - ) - - guard let logprobsInputDescriptor = BNNSNDArrayDescriptor( - data: logprobsInputPointer, - scalarType: FloatType.self, - shape: .vector(logits.count, stride: 1) - ) else { - Logging.error("Cannot create `logprobsInputDescriptor`") - return false - } - - let logprobs = BNNSNDArrayDescriptor.allocateUninitialized( - scalarType: FloatType.self, - shape: .vector(logits.count, stride: 1) - ) - defer { logprobs.deallocate() } - - do { - try BNNS.applyActivation( - activation: BNNS.ActivationFunction.logSoftmax, - input: logprobsInputDescriptor, - output: logprobs, - batchSize: 1 - ) - - let timeTokenCount = logits.count - timeTokenBeginOffset - let noTimeTokenCount = timeTokenBeginOffset - let logSumExpInputPointer = UnsafeMutableRawBufferPointer( - start: logprobs.data!.advanced(by: timeTokenBeginOffset * MemoryLayout.stride), - count: timeTokenCount * MemoryLayout.stride - ) - - guard let logSumExpInputDescriptor = BNNSNDArrayDescriptor( - data: logSumExpInputPointer, - scalarType: FloatType.self, - shape: .vector(timeTokenCount, stride: 1) - ) else { - Logging.error("Cannot create `logSumExpInputDescriptor`") - return false - } - - let timestampLogProb = BNNSNDArrayDescriptor.allocateUninitialized( - scalarType: FloatType.self, - shape: .vector(1, stride: 1) - ) - defer { timestampLogProb.deallocate() } - - try BNNS.applyReduction( - .logSumExp, - input: logSumExpInputDescriptor, - output: timestampLogProb, - weights: nil - ) - - let maxTextTokenLogProbInputPointer = UnsafeMutableRawBufferPointer( - start: logprobs.data, - count: noTimeTokenCount * MemoryLayout.stride - ) - - guard let maxTextTokenLogProbInputDescriptor = BNNSNDArrayDescriptor( - data: maxTextTokenLogProbInputPointer, - scalarType: FloatType.self, - shape: .vector(noTimeTokenCount, stride: 1) - ) else { - Logging.error("Cannot create `maxTextTokenLogProbInputDescriptor`") - return false - } - - let maxTextTokenLogProb = BNNSNDArrayDescriptor.allocateUninitialized( - scalarType: FloatType.self, - shape: .vector(1, stride: 1) - ) - defer { maxTextTokenLogProb.deallocate() } - - try BNNS.applyReduction( - .max, - input: maxTextTokenLogProbInputDescriptor, - output: maxTextTokenLogProb, - weights: nil - ) - - guard let timestampLogProbValue = timestampLogProb.makeArray(of: FloatType.self)?.first, - let maxTextTokenLogProbValue = maxTextTokenLogProb.makeArray(of: FloatType.self)?.first - else { - Logging.error("Cannot create logProb arrays") - return false - } - return timestampLogProbValue > maxTextTokenLogProbValue - } catch { - Logging.error("TimestampRulesFilter error: \(error)") - return false - } - } -} - -@available(macOS 13, iOS 16, watchOS 10, visionOS 1, *) -open class LanguageLogitsFilter: LogitsFiltering { - let allLanguageTokens: Set - let logitsDim: Int - let sampleBegin: Int - let nonLanguageTokenIndexes: [[NSNumber]] - - public init(allLanguageTokens: Set, logitsDim: Int, sampleBegin: Int) { - self.allLanguageTokens = allLanguageTokens - self.logitsDim = logitsDim - self.sampleBegin = sampleBegin - self.nonLanguageTokenIndexes = LanguageLogitsFilter.getNonLanguageTokenIndexes(logitsDim: self.logitsDim, allLanguageTokens: self.allLanguageTokens) - } - - /// Retain the logits that correspond to language tokens and suppress non-language tokens - public func filterLogits(_ logits: MLMultiArray, withTokens tokens: [Int]) -> MLMultiArray { - guard tokens.count == sampleBegin else { - return logits - } - logits.fill(indexes: nonLanguageTokenIndexes, with: -FloatType.infinity) - return logits - } - - private static func getNonLanguageTokenIndexes(logitsDim: Int, allLanguageTokens: Set) -> [[NSNumber]] { - var indexes: [[NSNumber]] = [] - for i in 0.. Date: Mon, 8 Jul 2024 02:21:18 -0400 Subject: [PATCH 20/24] Restore LogitsFilter.swift file --- Sources/WhisperKit/Core/LogitsFilter.swift | 314 +++++++++++++++++++++ 1 file changed, 314 insertions(+) create mode 100644 Sources/WhisperKit/Core/LogitsFilter.swift diff --git a/Sources/WhisperKit/Core/LogitsFilter.swift b/Sources/WhisperKit/Core/LogitsFilter.swift new file mode 100644 index 00000000..244ebeb1 --- /dev/null +++ b/Sources/WhisperKit/Core/LogitsFilter.swift @@ -0,0 +1,314 @@ +// For licensing see accompanying LICENSE.md file. +// Copyright © 2024 Argmax, Inc. All rights reserved. + +import Accelerate +import CoreML +import Foundation +import Tokenizers + +public protocol LogitsFiltering { + func filterLogits(_ logits: MLMultiArray, withTokens tokens: [Int]) -> MLMultiArray +} + +@available(macOS 13, iOS 16, watchOS 10, visionOS 1, *) +open class SuppressTokensFilter: LogitsFiltering { + let suppressTokens: [Int] + private let suppressTokenIndexes: [[NSNumber]] + + public init(suppressTokens: [Int]) { + self.suppressTokens = suppressTokens + self.suppressTokenIndexes = suppressTokens.map { [0, 0, $0 as NSNumber] } + } + + public func filterLogits(_ logits: MLMultiArray, withTokens tokens: [Int]) -> MLMultiArray { + logits.fill(indexes: suppressTokenIndexes, with: -FloatType.infinity) + return logits + } +} + +@available(macOS 13, iOS 16, watchOS 10, visionOS 1, *) +open class SuppressBlankFilter: LogitsFiltering { + let specialTokens: SpecialTokens + let sampleBegin: Int + private let suppressTokenIndexes: [[NSNumber]] + + public init( + specialTokens: SpecialTokens, + sampleBegin: Int + ) { + self.specialTokens = specialTokens + self.sampleBegin = sampleBegin + self.suppressTokenIndexes = [ + [0, 0, specialTokens.whitespaceToken as NSNumber], + [0, 0, specialTokens.endToken as NSNumber], + ] + } + + public func filterLogits(_ logits: MLMultiArray, withTokens tokens: [Int]) -> MLMultiArray { + guard tokens.count == sampleBegin else { + return logits + } + logits.fill(indexes: suppressTokenIndexes, with: -FloatType.infinity) + return logits + } +} + +/// Implementation based on https://github.com/openai/whisper/blob/master/whisper/decoding.py#L441 +@available(macOS 13, iOS 16, watchOS 10, visionOS 1, *) +open class TimestampRulesFilter: LogitsFiltering { + let specialTokens: SpecialTokens + let sampleBegin: Int + let maxInitialTimestampIndex: Int? + let isModelMultilingual: Bool + + public init( + specialTokens: SpecialTokens, + sampleBegin: Int, + maxInitialTimestampIndex: Int?, + isModelMultilingual: Bool + ) { + self.specialTokens = specialTokens + self.sampleBegin = sampleBegin + self.maxInitialTimestampIndex = maxInitialTimestampIndex + self.isModelMultilingual = isModelMultilingual + } + + public func filterLogits(_ logits: MLMultiArray, withTokens tokens: [Int]) -> MLMultiArray { + guard let sampleBegin = sampleBegin(for: tokens), + sampleBegin > tokens.count + else { + return logits + } + + // suppress <|notimestamps|> which is handled by `withoutTimestamps` + logits.fill(indexes: [[0, 0, specialTokens.noTimestampsToken as NSNumber]], with: -FloatType.infinity) + + if tokens.count > sampleBegin { + // timestamps have to appear in pairs, except directly before EOT; mask logits accordingly + let sampledTokens = tokens[sampleBegin...] + let lastWasTimestamp = sampledTokens.count >= 1 && sampledTokens.last! >= specialTokens.timeTokenBegin + let penultimateWasTimestamp = sampledTokens.count < 2 || sampledTokens.dropLast().last! >= specialTokens.timeTokenBegin + if lastWasTimestamp { + if penultimateWasTimestamp { + // has to be non-timestamp + logits.fillLastDimension(indexes: specialTokens.timeTokenBegin..= specialTokens.timeTokenBegin } + if let lastTimestamp = timestamps.last { + // timestamps shouldn't decrease; forbid timestamp tokens smaller than the last + // also force each segment to have a nonzero length, to prevent infinite looping + let timestampLast = + if lastWasTimestamp && !penultimateWasTimestamp { + lastTimestamp + } else { + lastTimestamp + 1 + } + logits.fillLastDimension(indexes: specialTokens.timeTokenBegin.. every time +// if tokens.count == sampleBegin { +// // suppress generating non-timestamp tokens at the beginning +// logits.fillLastDimension(indexes: 0.. Int? { + if isModelMultilingual { + // NOTE: for multilingual model we don't want to supress "<|transcribe|>" or "<|translate|>" tokens + if let taskTokenIndex = tokens.prefix(3).firstIndex(where: { $0 == specialTokens.transcribeToken || $0 == specialTokens.translateToken }) { + return max(taskTokenIndex + 1, sampleBegin) + } else { + return nil + } + } else { + return sampleBegin + } + } + + private func sumOfProbabilityOverTimestampsIsAboveAnyOtherToken(logits: MLMultiArray, timeTokenBegin: Int) -> Bool { + let timeTokenBeginOffset = logits.linearOffset(for: [0, 0, timeTokenBegin as NSNumber]) + + let logprobsInputPointer = UnsafeMutableRawBufferPointer( + start: logits.dataPointer, + count: logits.count * MemoryLayout.stride + ) + + guard let logprobsInputDescriptor = BNNSNDArrayDescriptor( + data: logprobsInputPointer, + scalarType: FloatType.self, + shape: .vector(logits.count, stride: 1) + ) else { + Logging.error("Cannot create `logprobsInputDescriptor`") + return false + } + + let logprobs = BNNSNDArrayDescriptor.allocateUninitialized( + scalarType: FloatType.self, + shape: .vector(logits.count, stride: 1) + ) + defer { logprobs.deallocate() } + + do { + try BNNS.applyActivation( + activation: BNNS.ActivationFunction.logSoftmax, + input: logprobsInputDescriptor, + output: logprobs, + batchSize: 1 + ) + + let timeTokenCount = logits.count - timeTokenBeginOffset + let noTimeTokenCount = timeTokenBeginOffset + let logSumExpInputPointer = UnsafeMutableRawBufferPointer( + start: logprobs.data!.advanced(by: timeTokenBeginOffset * MemoryLayout.stride), + count: timeTokenCount * MemoryLayout.stride + ) + + guard let logSumExpInputDescriptor = BNNSNDArrayDescriptor( + data: logSumExpInputPointer, + scalarType: FloatType.self, + shape: .vector(timeTokenCount, stride: 1) + ) else { + Logging.error("Cannot create `logSumExpInputDescriptor`") + return false + } + + let timestampLogProb = BNNSNDArrayDescriptor.allocateUninitialized( + scalarType: FloatType.self, + shape: .vector(1, stride: 1) + ) + defer { timestampLogProb.deallocate() } + + try BNNS.applyReduction( + .logSumExp, + input: logSumExpInputDescriptor, + output: timestampLogProb, + weights: nil + ) + + let maxTextTokenLogProbInputPointer = UnsafeMutableRawBufferPointer( + start: logprobs.data, + count: noTimeTokenCount * MemoryLayout.stride + ) + + guard let maxTextTokenLogProbInputDescriptor = BNNSNDArrayDescriptor( + data: maxTextTokenLogProbInputPointer, + scalarType: FloatType.self, + shape: .vector(noTimeTokenCount, stride: 1) + ) else { + Logging.error("Cannot create `maxTextTokenLogProbInputDescriptor`") + return false + } + + let maxTextTokenLogProb = BNNSNDArrayDescriptor.allocateUninitialized( + scalarType: FloatType.self, + shape: .vector(1, stride: 1) + ) + defer { maxTextTokenLogProb.deallocate() } + + try BNNS.applyReduction( + .max, + input: maxTextTokenLogProbInputDescriptor, + output: maxTextTokenLogProb, + weights: nil + ) + + guard let timestampLogProbValue = timestampLogProb.makeArray(of: FloatType.self)?.first, + let maxTextTokenLogProbValue = maxTextTokenLogProb.makeArray(of: FloatType.self)?.first + else { + Logging.error("Cannot create logProb arrays") + return false + } + return timestampLogProbValue > maxTextTokenLogProbValue + } catch { + Logging.error("TimestampRulesFilter error: \(error)") + return false + } + } +} + +@available(macOS 13, iOS 16, watchOS 10, visionOS 1, *) +open class LanguageLogitsFilter: LogitsFiltering { + let allLanguageTokens: Set + let logitsDim: Int + let sampleBegin: Int + let nonLanguageTokenIndexes: [[NSNumber]] + + public init(allLanguageTokens: Set, logitsDim: Int, sampleBegin: Int) { + self.allLanguageTokens = allLanguageTokens + self.logitsDim = logitsDim + self.sampleBegin = sampleBegin + self.nonLanguageTokenIndexes = LanguageLogitsFilter.getNonLanguageTokenIndexes(logitsDim: self.logitsDim, allLanguageTokens: self.allLanguageTokens) + } + + /// Retain the logits that correspond to language tokens and suppress non-language tokens + public func filterLogits(_ logits: MLMultiArray, withTokens tokens: [Int]) -> MLMultiArray { + guard tokens.count == sampleBegin else { + return logits + } + logits.fill(indexes: nonLanguageTokenIndexes, with: -FloatType.infinity) + return logits + } + + private static func getNonLanguageTokenIndexes(logitsDim: Int, allLanguageTokens: Set) -> [[NSNumber]] { + var indexes: [[NSNumber]] = [] + for i in 0.. MLMultiArray { + guard tokens.count == sampleBegin else { + return logits + } + logits.fill(indexes: nonSilenceTokenIndexes, with: -FloatType.infinity) + return logits + } + + private static func getNonSilenceTokenIndexes(logitsDim: Int, silenceToken: Int) -> [[NSNumber]] { + var indexes: [[NSNumber]] = [] + for i in 0.. Date: Mon, 8 Jul 2024 02:50:08 -0400 Subject: [PATCH 21/24] Update TextDecoder.swift --- Sources/WhisperKit/Core/TextDecoder.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/WhisperKit/Core/TextDecoder.swift b/Sources/WhisperKit/Core/TextDecoder.swift index bdb036de..9d2ec932 100644 --- a/Sources/WhisperKit/Core/TextDecoder.swift +++ b/Sources/WhisperKit/Core/TextDecoder.swift @@ -623,7 +623,7 @@ open class TextDecoder: TextDecoding, WhisperMLModel { } if !options.supressTokens.isEmpty { - logitsFilters.append(SuppressTokensFilter(suppressTokens: options.supressTokens, noSpeechTokenIndex: 50362)) + logitsFilters.append(SuppressTokensFilter(suppressTokens: options.supressTokens)) } if !options.withoutTimestamps { From f4ac60aec1737f4532437c6033733cb45ecad1ad Mon Sep 17 00:00:00 2001 From: aigerimmmm <46799842+aigerimmmm@users.noreply.github.com> Date: Mon, 8 Jul 2024 11:43:22 -0400 Subject: [PATCH 22/24] Update TranscribeTask.swift --- Sources/WhisperKit/Core/TranscribeTask.swift | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/Sources/WhisperKit/Core/TranscribeTask.swift b/Sources/WhisperKit/Core/TranscribeTask.swift index 32da913e..54a1a514 100644 --- a/Sources/WhisperKit/Core/TranscribeTask.swift +++ b/Sources/WhisperKit/Core/TranscribeTask.swift @@ -157,16 +157,8 @@ final class TranscribeTask { try Task.checkCancellation() // Send to decoder to predict text tokens with fallback let decodingResult = try await decodeWithFallback(encoderSegment: encoderOutput, decodingOptions: options, callback: decodingCallback) - - let noSpeechProb = try await textDecoder.detectSilence( - from: encoderOutput, - using: decoderInputs, - sampler: GreedyTokenSampler(temperature: 0, eotToken: tokenizer.specialTokens.endToken, decodingOptions: options), - options: options, - temperature: 0 - ) - if noSpeechProb > (options.noSpeechThreshold ?? 0.6) && decodingResult.avgLogProb < (options.logProbThreshold ?? -1.0) { + if decodingResult.noSpeechProb > (options.noSpeechThreshold ?? 0.6) && decodingResult.avgLogProb < (options.logProbThreshold ?? -1.0) { seek += segmentSize continue } From 8cf2ef4a01e24275e5f8515f18728adcda085083 Mon Sep 17 00:00:00 2001 From: aigerimmmm <46799842+aigerimmmm@users.noreply.github.com> Date: Mon, 8 Jul 2024 16:59:20 -0400 Subject: [PATCH 23/24] Update UnitTests.swift --- Tests/WhisperKitTests/UnitTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/WhisperKitTests/UnitTests.swift b/Tests/WhisperKitTests/UnitTests.swift index ce1f9b8a..579cf9ab 100644 --- a/Tests/WhisperKitTests/UnitTests.swift +++ b/Tests/WhisperKitTests/UnitTests.swift @@ -688,7 +688,7 @@ final class UnitTests: XCTestCase { //print("Test case: \(audioFileName), Expecting speech: \(expectingSpeech), Calculated silence probability: \(silenceProbability)") // calculated noSpeechProb values for silent and non-silent clips are 0.002598221 and 0.26186648. // Given these values, a threshold of 0.6 might be too high to accurately distinguish between - // silence and speech.Based on the debug values, here I picked a threshold of 0.3 or 0.2 + // silence and speech.Based on the debug values, here I picked a threshold of 0.2 if expectingSpeech { XCTAssertGreaterThan(silenceProbability, 0.2, "Expected speech, but detected silence for \(audioFileName) with probability \(silenceProbability)") } else { From dda302f5d0bfafa496e1c987508de07ff5354be2 Mon Sep 17 00:00:00 2001 From: aigerimmmm <46799842+aigerimmmm@users.noreply.github.com> Date: Tue, 9 Jul 2024 00:37:17 -0400 Subject: [PATCH 24/24] Update TextDecoder.swift --- Sources/WhisperKit/Core/TextDecoder.swift | 36 +++++++++++++---------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/Sources/WhisperKit/Core/TextDecoder.swift b/Sources/WhisperKit/Core/TextDecoder.swift index 9d2ec932..52719189 100644 --- a/Sources/WhisperKit/Core/TextDecoder.swift +++ b/Sources/WhisperKit/Core/TextDecoder.swift @@ -707,24 +707,28 @@ open class TextDecoder: TextDecoding, WhisperMLModel { //print(tokenizer.specialTokens.noSpeechToken) //it prints 50257 let noSpeechTokenIndex = 50362 // I think from models index for the "no speech" token is 50362? noSpeechProb = calculateNoSpeechProb(logits: logits, noSpeechTokenIndex: noSpeechTokenIndex) + + let avgLogProb = logProbs.reduce(0, +) / Float(logProbs.count) if let threshold = options.noSpeechThreshold, noSpeechProb > threshold { - print("Detected silence with noSpeechProb \(noSpeechProb), skipping segment.") - return DecodingResult( - language: Constants.defaultLanguageCode, - languageProbs: [:], - tokens: [], - tokenLogProbs: [], - text: "", - avgLogProb: 0.0, - noSpeechProb: noSpeechProb, - temperature: 0.0, - compressionRatio: 0.0, - cache: nil, - timings: TranscriptionTimings(), - fallback: nil - ) - } + if options.logProbThreshold == nil || avgLogProb < options.logProbThreshold! { + print("Detected silence with noSpeechProb \(noSpeechProb) and avgLogProb \(avgLogProb), skipping segment.") + return DecodingResult( + language: Constants.defaultLanguageCode, + languageProbs: [:], + tokens: [], + tokenLogProbs: [], + text: "", + avgLogProb: avgLogProb, + noSpeechProb: noSpeechProb, + temperature: 0.0, + compressionRatio: 0.0, + cache: nil, + timings: TranscriptionTimings(), + fallback: nil + ) + } + } } let filteringTime = Date().timeIntervalSince(nonInferenceStartTime)