C.W.K.
Stream
Lesson 02 of 04 · published

No Engine Leaks Upward

~11 min · stt-adapter, leaky-abstraction, contract, engine

Level 0Unlit
0 XP0/36 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"If the word 'WhisperKit' appears above the seam, the abstraction has already failed."

The Adapter's One Job

An STT adapter's job is to make its engine look like the interface and nothing more. WhisperKit has its own decoding options, model enums, and audio format expectations; whisper.cpp has a C API; SpeechAnalyzer has async result streams. None of that is allowed to leak above the seam. Each adapter absorbs its engine's quirks and exposes only the shared contract: give me a clip and a language, I return text plus the language actually detected. The controller holds any STTEngine and never learns which one it's talking to.

The Leak Test

Here's the concrete test for whether your abstraction is real: grep the code above the seam for engine-specific words. If DecodingOptions, WhisperKitConfig, or a model filename appears in the DictationController, the abstraction leaks — the controller now depends on a detail it claimed not to know. A clean adapter means those words appear only inside the adapter file. Everything above speaks the neutral contract.

Leaky (bad):
  controller -> reads WhisperKit's DecodingOptions directly   # engine leaked up

Clean (good):
  controller -> STTEngine.finalize(clip, language)            # neutral contract
  WhisperKitEngine -> builds DecodingOptions internally         # quirk absorbed

The Contract Carries What Downstream Needs

The return type is deliberately small but complete: text, and the language the engine detected. Why the language? Because the very next step — cleanup — needs it, and the engine is the only component that knows. If the adapter dropped the language, cleanup would have to re-detect it or guess, re-solving a problem the engine already solved. A good contract carries exactly what downstream consumers need and no more — not the engine's internal confidence scores, not its token timings, just the two facts the rest of the app acts on.

An adapter absorbs quirks; it doesn't forward them. The measure of a good adapter is that its engine's peculiarities stop at its boundary. If callers have to know the engine to use the adapter, you've built a pass-through, not an abstraction. Absorb the config, the formats, the error shapes — expose only the neutral contract.
The first WhisperKit adapter I wrote leaked — the controller was reaching in to set decoding options 'just for now.' It felt harmless until we added the SpeechAnalyzer engine and the controller suddenly needed a branch: 'if WhisperKit, set options; if SpeechAnalyzer, do this other thing.' That branch was the leak billing me. Moving the options back inside each adapter deleted the branch and the coupling with it.

Code

The engine's quirks stop at the adapter boundary·swift
final class WhisperKitEngine: STTEngine {
    let id = "whisperkit"
    private let kit: WhisperKit

    func finalize(_ clip: AudioClip, language: LanguageMode) async throws -> STTResult {
        // All WhisperKit-specific config is built HERE, never exposed.
        var opts = DecodingOptions()
        switch language {
        case .korean:  opts.language = "ko"
        case .english: opts.language = "en"
        case .auto:    opts.detectLanguage = true
        }
        let out = try await kit.transcribe(audioArray: clip.samples, decodeOptions: opts)
        return STTResult(text: out.text, detectedLanguage: out.language ?? "en")
    }
}
// The controller sees STTResult. It never sees DecodingOptions.

External links

Exercise

Take an adapter or wrapper in your own code and grep the code above it for the underlying library's type names. Every hit is a leak. Pick one and describe what breaks when you try to swap the underlying library — that broken thing is exactly what the leak costs you.
Hint
Each leaked type name is a place the caller secretly depends on the specific library. When you swap libraries, every one of those sites needs editing — which is the coupling the adapter was supposed to prevent. The number of grep hits is a direct measure of how much a swap will actually cost.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.