feat: add WhisperKit transcriber and transcribe probe

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-01 09:48:17 +02:00
co-authored by Claude Sonnet 5.5
parent 28515d9c87
commit ee8d56c2fb
5 changed files with 149 additions and 2 deletions
+78
View File
@@ -0,0 +1,78 @@
{
"originHash" : "e6c9264e0494528c9e107c3e12b51524d9f3fe957d63a07601ae76077888c441",
"pins" : [
{
"identity" : "swift-argument-parser",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-argument-parser.git",
"state" : {
"revision" : "6a52f3251125d74daf04fcbd5e6f08a75d074382",
"version" : "1.8.2"
}
},
{
"identity" : "swift-asn1",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-asn1.git",
"state" : {
"revision" : "3b6410f7dee09eb33cdd26260c5fd47fda19b0e2",
"version" : "1.7.3"
}
},
{
"identity" : "swift-collections",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-collections.git",
"state" : {
"revision" : "98ef3c98609a1e31b7e157b5b619579001a789d6",
"version" : "1.7.1"
}
},
{
"identity" : "swift-crypto",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-crypto.git",
"state" : {
"revision" : "da9d28d69ebe3894b18376c8f2395c2f37b8448f",
"version" : "4.5.2"
}
},
{
"identity" : "swift-jinja",
"kind" : "remoteSourceControl",
"location" : "https://github.com/huggingface/swift-jinja.git",
"state" : {
"revision" : "4588064a20f3fc093c95f2f7d3359999bf30cae5",
"version" : "2.5.1"
}
},
{
"identity" : "swift-transformers",
"kind" : "remoteSourceControl",
"location" : "https://github.com/huggingface/swift-transformers.git",
"state" : {
"revision" : "150169bfba0889c229a2ce7494cf8949f18e6906",
"version" : "1.1.9"
}
},
{
"identity" : "whisperkit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/argmaxinc/WhisperKit.git",
"state" : {
"revision" : "e2adabbe7d98dc4d0ab9a5b75424ecc42a9cdbef",
"version" : "0.18.0"
}
},
{
"identity" : "yyjson",
"kind" : "remoteSourceControl",
"location" : "https://github.com/ibireme/yyjson.git",
"state" : {
"revision" : "8b4a38dc994a110abaec8a400615567bd996105f",
"version" : "0.12.0"
}
}
],
"version" : 3
}
+11 -1
View File
@@ -4,9 +4,19 @@ import PackageDescription
let package = Package(
name: "Sst",
platforms: [.macOS(.v14)],
dependencies: [
.package(url: "https://github.com/argmaxinc/WhisperKit.git", from: "0.9.0"),
],
targets: [
.target(name: "SstCore"),
.executableTarget(name: "SstProbe", dependencies: ["SstCore"]),
.target(
name: "SstWhisper",
dependencies: [
"SstCore",
.product(name: "WhisperKit", package: "WhisperKit"),
]
),
.executableTarget(name: "SstProbe", dependencies: ["SstCore", "SstWhisper"]),
.testTarget(name: "SstCoreTests", dependencies: ["SstCore"]),
],
swiftLanguageModes: [.v5]
+17
View File
@@ -0,0 +1,17 @@
import Foundation
import SstWhisper
func runTranscribeProbe() async {
guard let samples = await recordSamples(seconds: 6) else { return }
print("Model laden (eerste keer: download ≈1.5 GB)…")
let transcriber = WhisperKitTranscriber()
do {
try await transcriber.load()
let started = Date.now
let text = try await transcriber.transcribe(samples)
let elapsed = Date.now.timeIntervalSince(started)
print(String(format: "(%.2f s) ", elapsed) + text)
} catch {
print("mislukt:", error.localizedDescription)
}
}
+3 -1
View File
@@ -6,6 +6,8 @@ case "hotkey":
runHotkeyProbe()
case "record":
await runRecordProbe()
case "transcribe":
await runTranscribeProbe()
default:
print("usage: sst-probe hotkey|record")
print("usage: sst-probe hotkey|record|transcribe")
}
@@ -0,0 +1,40 @@
import Foundation
import SstCore
import WhisperKit
public final class WhisperKitTranscriber: Transcriber {
public enum TranscriberError: Error, LocalizedError {
case notLoaded
public var errorDescription: String? { "Spraakmodel is nog niet geladen." }
}
private let modelName: String
private var pipeline: WhisperKit?
public init(model: String = "large-v3-v20240930_turbo") {
self.modelName = model
}
/// Downloads the model on first run (≈1.5 GB) and loads it into memory.
public func load() async throws {
pipeline = try await WhisperKit(WhisperKitConfig(model: modelName))
}
public func transcribe(_ samples: [Float]) async throws -> String {
guard let pipeline else { throw TranscriberError.notLoaded }
// language nil + detectLanguage: Whisper picks NL or EN per utterance.
let options = DecodingOptions(
task: .transcribe,
language: nil,
detectLanguage: true,
skipSpecialTokens: true,
withoutTimestamps: true
)
let results = try await pipeline.transcribe(audioArray: samples, decodeOptions: options)
return results
.map(\.text)
.joined(separator: " ")
.trimmingCharacters(in: .whitespacesAndNewlines)
}
}