openquack

SPEC-044 — Remote transcription endpoint (bring your own)

Goal

Let a user who wants a bigger model than their Mac can run point transcription at an OpenAI-compatible HTTP endpoint — any endpoint, any provider — without weakening the default local posture. Local stays the default; the remote path is explicitly opt-in, visibly indicated while active, and carries no vendor names in code.

Background

Transcription today is 100 % on-device (WhisperKit; Lightning in bench). That is the right default and the product’s identity, but two real user groups want more:

The agent layer already made exactly this trade once: network use is allowed, but only user-configured, never default (VISION.md privacy contract, clauses 3–4). This spec extends the same carve-out to transcription.

Approval note: this change touches two AGENTS.md hard-rule areas (a network call in the dictation path; the privacy contract). Both were explicitly approved by the maintainer commissioning this spec.

Mechanism

Data model

public struct RemoteProfile: Sendable, Equatable {
    var baseURL: URL          // e.g. https://api.openai.com/v1
    var model: String         // e.g. whisper-1; sent only if non-empty
    var auth: RemoteAuth
}

public enum RemoteAuth: Sendable, Equatable {
    case none                 // local gateways, whisper.cpp server
    case bearer               // Authorization: Bearer <secret>
    case header(name: String) // <name>: <secret> — corporate gateways
}

No secret lives in the profile or in UserDefaults. Secrets go in the Keychain via CredentialStore, keyed by endpoint host (KeychainCredentialStore, one generic-password item per host). Keying by host is the security invariant: an engine can only ever attach the credential saved for the host the request targets, so editing the URL can never leak the old host’s key to a new one.

Engine

RemoteEngine implements the existing TranscriptionEngine protocol (no protocol changes). Per request:

  1. Re-encode the recording to 16 kHz mono 16-bit WAV (AudioResampler, AVFoundation only — the recorder writes device-native rate, often 48 kHz stereo, which is ~6× the upload weight for zero quality gain). Above compressAboveBytes (700 KB, ~22 s) the WAV is further encoded to 32 kbps mono AAC/m4a — ~7× smaller, so long dictations stay inside gateway body caps and upload faster. Measured against a live endpoint, WAV and AAC transcripts matched on clean, noisy, and Chinese speech. Encoding runs through AVAssetWriter (AVAudioFile cannot write compressed formats), and any encoder failure falls back to the WAV rather than failing the dictation.
  2. POST {base}/audio/transcriptions (path not appended if the user already pasted the full path), multipart: file, model, optional language, response_format=json.
  3. Parse {"text": …}; plain-text bodies accepted as a fallback for non-conforming servers.

Transport rules: HTTPS required (plain HTTP allowed for loopback only, for local whisper.cpp/Ollama-style servers); HTTP redirects refused — audio and credential go only to the configured host, never wherever a 3xx points; URLs with embedded user:pass@ rejected (and auto-stripped in Settings) so no secret can ride into UserDefaults; ephemeral URLSession so no response ever lands in the on-disk URL cache; 60 s request timeout. Requests never identify the app to the endpoint: the User-Agent is user-configurable (remoteUserAgent, folded to one header line; a neutral default when empty — the CFNetwork default would embed the bundle name) and the multipart boundary is app-neutral. Errors surface through the existing EngineError path — a misconfigured remote errors, it never silently falls back to local.

EngineKind is untouched: the app constructs RemoteEngine directly (as it already does WhisperKitEngine), because makeEngine(model:) cannot carry profile config. CLI/bench remote support is out of scope.

App integration

Privacy indicator

While a remote profile is active, the recording overlay must never claim locality:

VISION.md privacy-contract clause 1 and README wording change from unconditional to default-scoped in the same PR; the sentence pattern follows the agent carve-out already in VISION.md.

Privacy impact

This is the first feature that can send audio off-device, so the change is fenced:

Acceptance criteria

Out of scope