Two design cleanups, both for visual polish on the record disc
(the hero control of the keyboard preview AND the live keyboard
extension). The shadows were a leftover from the iOS 18 mockup
that visually fought the brand-green disc; the processing-state
spinner was the system default at 1.1x, which on a 96-120pt disc
reads as "a dot in the middle" rather than "an active process".
Changes:
1) Remove the disc body's drop shadow on BOTH surfaces.
- `KeyboardPreviewStub.swift:244` — `.shadow(color: .black.opacity(0.4),
radius: 10, y: 6)` deleted. Hardcoded dark shadow on a
light surface, completely theme-blind (looked like a 2009
skeuomorphic button).
- `RecordButton.swift:112` — `.shadow(color: .black.opacity(0.45),
radius: 14, y: 8)` deleted. Same problem on the real
keyboard's disc, plus the shadow competed with the existing
"halo" radial gradient and the outer breathing ring.
The internal "ambient glow" (radial green gradient inside the
disc on idle) is NOT touched — that's brand colour, not a
drop shadow, and matches the live keyboard's idle accent.
2) Scale up the processing-state spinner on BOTH surfaces.
- Preview: `ProgressView().tint(.white).scaleEffect(1.1)` →
`ProgressView().progressViewStyle(.circular).tint(palette.textPrimary)
.scaleEffect(2.5)`.
- 1.1x (~22pt) → 2.5x (~50pt) = 52% of the 96pt disc.
- Tint changed from hardcoded `.white` to `palette.textPrimary`
because the processing-state disc gradient is
`surfaceElevated → surface` (light grey in light mode);
a white spinner on a light grey disc is invisible regardless
of scale.
- Real keyboard: `ProgressView().scaleEffect(1.2)` →
`ProgressView().scaleEffect(2.5)`. Same absolute spinner
size (~50pt) — 42% of the 120pt disc — so the user gets the
same visual weight whether they're looking at the in-app
preview or the live keyboard. Tint was already
`palette.textPrimary` so no change there.
Proportion rationale: ~50% of the disc diameter is the sweet
spot. 100% would crowd the disc's edge, <30% reads as
"decoration" rather than "process". 50pt absolute size matches
the same spinner across both surfaces — same weight, different
disc sizes — so the user doesn't have to re-learn the visual
language when they switch from preview to real keyboard.
Tests: 27/27 pass (no behavioural change, just visual).
Build: BUILD SUCCEEDED.
🤖 Generated with Claude Code
OSGKeyboard
Hold a key, speak, release — AI-polished text appears at your cursor in any app. An open-source, custom-keyboard-based voice input tool for iOS 18+, inspired by Typeless and OpenLess.
What is it?
OSGKeyboard is a free, open alternative to commercial voice-input tools. It runs as a Custom Keyboard Extension on iOS, so you can use it in any app — Messages, Notes, Mail, ChatGPT, Claude, Cursor, you name it.
- Press and hold the mic key
- Speak naturally
- Release — the AI polishes your words into clean text and inserts it at the cursor
The audio stays on-device (transcribed by Apple's on-device SFSpeechRecognizer on iOS 18/19; iOS 26+ SpeechAnalyzer planned for the next release). Only the polished transcript is sent to your chosen cloud LLM. No audio ever leaves your phone.
Features
- 🎙 Push-to-talk with a Typeless-style circular mic button
- 🧠 On-device ASR (iOS 18/19
SFSpeechRecognizer; iOS 26+SpeechAnalyzer+DictationTranscriberplanned) - ✍️ AI polishing — adds structure, punctuation, fixes grammar, optionally produces lists
- 🔌 Bring-your-own API — works with any OpenAI-compatible endpoint (OpenAI, DeepSeek, Qwen DashScope, your own self-hosted server, …)
- 🔒 Privacy first — audio never leaves your device; transcripts only sent to the LLM you choose
- 🎨 Native SwiftUI — dark theme, frosted glass, ~2000 lines of Swift
- 🪶 Zero dependencies — no SwiftPM packages, no CocoaPods, no Carthage
Quick start
Requirements
- macOS with Xcode 16+ (Xcode 26 recommended)
- iPhone running iOS 18.0+
- XcodeGen:
brew install xcodegen - An OpenAI-compatible API key (e.g. from OpenAI, DeepSeek, or Qwen DashScope)
Build & run
git clone https://github.com/hkgood/OSGKeyboard.git
cd OSGKeyboard
xcodegen generate # produces OSGKeyboard.xcodeproj
open OSGKeyboard.xcodeproj # or build via CLI:
xcodebuild -project OSGKeyboard.xcodeproj -scheme OSGKeyboard \
-destination 'generic/platform=iOS Simulator' build
Enable the keyboard in iOS
- Run the app on your device or simulator.
- Follow the 3-step onboarding: enable the keyboard in iOS Settings, then allow Full Access (required for the mic and LLM calls), then paste your API key.
- In any text field, tap 🌐 to switch to OSGKeyboard.
- Press and hold the mic, speak, release. ✨
"Allow Full Access" is required. Without it, iOS blocks the keyboard from using the microphone and from making network requests. We never log, store, or transmit your keystrokes — see
PrivacyInfo.xcprivacy.
Architecture
OSGKeyboard/
├── OSGKeyboard/ # Main iOS app (settings, onboarding)
│ ├── Views/ # SwiftUI screens
│ ├── OSGKeyboardApp.swift # @main entry
│ ├── PrivacyInfo.xcprivacy # Required privacy manifest
│ └── OSGKeyboard.entitlements # App Group declaration
├── OSGKeyboardExt/ # Custom Keyboard Extension
│ ├── KeyboardViewController.swift # Principal class
│ ├── Services/
│ │ ├── AudioCaptureService.swift # AVAudioEngine → 16 kHz PCM
│ │ ├── ASRService.swift # iOS 26 + iOS 18 ASR
│ │ └── PolishingService.swift # LLM call with timeout
│ └── Views/ # RecordButton, Waveform, KeyboardRootView
├── OSGKeyboardShared/ # Framework shared by app + extension
│ ├── Models/ # ProviderConfig, LLMRequest, LLMProvider
│ ├── Services/ # LLMClient (OpenAI-compatible)
│ └── Constants/ # AppGroup identifier
├── OSGKeyboardTests/ # XCTest unit tests
├── project.yml # XcodeGen project definition
└── .github/workflows/ci.yml # Lint + build CI
Data flow
[Long-press mic] → AudioCaptureService → AudioBufferSnapshot (16 kHz mono)
↓
ASRService.transcribe()
↓
ASREvent.final(rawTranscript)
↓
PolishingService.polish()
↓
LLMClient (OpenAI-compatible)
↓
textDocumentProxy.insertText(polished)
Adding a new LLM provider
Open OSGKeyboardShared/Models/LLMProvider.swift and append a new LLMProvider to the presets array. The default OpenAICompatibleClient handles any endpoint that speaks the POST /chat/completions protocol.
LLMProvider(
id: "groq",
name: "Groq",
defaultBaseURL: "https://api.groq.com/openai/v1",
defaultModel: "llama-3.1-70b-versatile",
apiKeyURL: URL(string: "https://console.groq.com/keys")
)
That's it. No other code changes required.
Limitations
- iOS sandboxes keyboard extensions: ~60 MB memory cap, Full Access required.
- The keyboard does not work in password fields or some
WKWebViewtextareas (iOS limitation). - iOS 18/19 ships with
SFSpeechRecognizerfor on-device ASR. iOS 26+SpeechAnalyzeris planned for the next release — it is significantly faster and supports more locales. - iOS 26+ users in v0.1.1 use the iOS 18
SFSpeechRecognizerpath; the iOS 26SpeechAnalyzeris planned for 0.2.0.
License
MIT — use it, fork it, ship it. No warranty.
Acknowledgements
- Inspired by Typeless and the desktop open-source OpenLess
- Built with XcodeGen
- Powered by Apple's SpeechAnalyzer and SFSpeechRecognizer
Note: the project is published at hkgood/OSGKeyboard; badges and git clone URLs already point there.