Utter is a macOS menu bar voice input app built with Swift 6 / SwiftUI / AppKit. The Swift package, module, and compatibility identifiers retain the original OpenType name. It runs on macOS 26+ (Apple Silicon only) and uses WhisperKit and MLX-LM for local inference, with optional remote LLM support.
- Pure Swift Package (no .xcodeproj) — everything is driven by
Package.swift - Composition root: executable
OpenTypeunderSources/App/; implementation lives in independently owned SwiftPM targets.UtterBuiltinsassembles registrations. - Functional style preferred — avoid unnecessary classes; use enums, structs, and free functions where possible
- File size: each file should stay under 300 lines (ideally ~100 lines); split when growing
| Directory / target | Responsibility |
|---|---|
App/ / OpenType |
Start and drain BuiltinApplication; no processing or UI feature owners |
UtterRuntime/ |
Plugin graph, scoped services, tasks, revocation and disposal |
UtterContracts/ |
Portable settings, data, providers, processing evidence and session contracts; localization |
UtterMediaContracts/, UtterPresentationContracts/ |
Native media contracts and observable UI projections |
UtterBuiltins/ |
Built-in registrations, replacement policy, desktop/recovery compositions |
UtterData/ |
Settings, credentials, composition persistence, dictionary, lexicons, history and memory |
UtterModels/ |
Artifact catalog, storage, downloads, frozen model locations and resource access |
UtterProcessing/ |
Prompt assembly, fidelity checks and replaceable mode recipes |
UtterSession/ |
Shared session execution, API, follow-up formatting and receipt settlement |
UtterIngress/ |
Hotkey/remote session bindings and HTTP/XPC transports |
UtterAudio/, UtterRemoteMic/, UtterMacServices/ |
Capture, remote device control, OCR, target leases and transactional output |
UtterAppleSpeech/, UtterWhisper/, UtterMLX/, UtterANE/, UtterRemoteInference/ |
Independently registered inference providers |
UtterPresentation/ |
Menu, onboarding, overlay, icons and settings contributions |
UtterEvaluation/ |
Headless evaluation schema and finite budgets |
Resources belong to their owner targets; scripts/resource-bundles.json is the packaging manifest.
UI and ingress depend on contracts and the runtime; only UtterBuiltins imports sibling implementations.
Tests use owner modules directly. Construction fixtures under Tests/OpenTypeTests/Support/ are excluded from production.
@MainActoris used for all UI-touching code; background work usesTask { }andactor- Localization: all user-facing strings go through
L("key")(defined inLoc.swift), with entries in bothen.lprojandzh-Hans.lproj - Settings persistence:
SettingsStoreownsUserDefaultsthroughSettingsService;AppSettingsobserves that service and projects values for SwiftUI. Credentials have a separate service. - Remote LLM:
RemoteLLMClientdispatches to OpenAI-format (/chat/completions) or Anthropic-format (/messages) based onprovider.apiFormat - Prompt management:
Sources/UtterProcessing/PromptBuilder.swiftassembles prompts; fixed prompt text belongs inPromptCatalog.swiftand style presets belong inPromptStylePrompts.swift - Text processing: no hardcoded filler-word removal — the LLM handles all contextual cleanup via the system prompt
- Dev build:
swift build,bash scripts/build-and-run.sh --verify, or openPackage.swiftin Xcode - Release build:
bash scripts/build-app.sh— usesxcodebuild(required for Metal shader bundling), then assembles .app and .dmg - PR CI:
.github/workflows/pr.yml— validates SDLC artifacts, runs linked checks and unit tests, builds a release-style app, and exposes the stableSDLC Gatecheck - Release CI:
.github/workflows/release.yml— accepts a SemVer tag onmain, verifies either the existing self-signed identity or Developer ID, requires Apple notarization for Developer ID, and publishes the mounted-DMG-verified artifact with a checksum - Icon:
scripts/generate-icon.swiftprogrammatically renders the icon and generates.icns;Sources/UtterPresentation/App/AppIcon.swiftrenders the same icon at runtime for the Dock
- Read
docs/sdlc/README.mdbefore non-trivial implementation, automation, test, UI, dependency, permission, or release changes. - Create or update
docs/sdlc/changes/<yyyy-mm-dd-slug>/. Low risk requires intent, plan, and verification; medium/high risk also requires a spec. - Every stage requires explicit human approval before the next one starts. Set the artifact's
Status: pending approvaland stop for the user's decision; never mark a stageapprovedon the user's behalf.approvedrequiresApproved-byandApproved-date. Artifacts without a Status header are legacy merged bundles. - Begin implementation only after intent has observable acceptance criteria and medium/high-risk design choices have been reviewed.
- Always run
bash scripts/sdlc-checks.sh,bash scripts/ci-basic-checks.sh, andswift test. Add release-style build, real-window visual QA, permission/privacy paths, or clean-machine checks in proportion to risk. - High-risk changes require an independent verifier, explicit rollback, PR approval, and protected production approval.
- A production incident must link a corrective intent and add a regression test, deterministic guardrail, eval case, or explicit reason automation is impossible.
- Never fall back to ad-hoc signing when configured signing fails. A self-signed release must use the configured identity, pass the same artifact/checksum checks, and be labeled as not Apple-notarized.
- Swift 6 with
.swiftLanguageMode(.v5)for compatibility - Prefer
enumnamespaces (e.g.,enum PromptBuilder { static func ... }) over classes for stateless logic - Mark
@MainActorexplicitly on types that touch UI or AppKit - Use structured concurrency (
async/await,Task,actor) — avoid GCD - All logging through
Log.info()/Log.error()(defined inConfig/Log.swift) - Keep repository automation under
scripts/; do not create another top-level script directory - Comments: only non-obvious intent, no narrating code
- Metal shaders:
swift builddoes NOT bundle.metallibfiles — always usexcodebuildfor release builds - TCC permissions: ad-hoc signed builds lose permissions on every rebuild; use a signing certificate for stable development
- Screen Recording permission: use
SCShareableContent.currentasync check, notCGPreflightScreenCaptureAccess()alone - Apple Speech: defer
SFSpeechRecognizer.requestAuthorizationuntil actually needed, not at app launch