| name | type4me-macos-voice-input |
| description | MacOS voice input tool with local/cloud ASR engines, LLM text optimization, and fully local storage built in Swift |
| triggers | ["add a new ASR provider to type4me","build and deploy type4me from source","configure local voice recognition with sherpa","set up volcengine speech recognition","add custom prompt mode for voice input","implement speech recognizer protocol","troubleshoot type4me voice input not working","extend type4me with new cloud ASR service"] |
Type4Me macOS Voice Input
Skill by ara.so — Daily 2026 Skills collection.
Type4Me is a macOS voice input tool that captures audio via global hotkey, transcribes it using local (SherpaOnnx/Paraformer/Zipformer) or cloud (Volcengine/Deepgram) ASR engines, optionally post-processes text via LLM, and injects the result into any app. All credentials and history are stored locally — no telemetry, no cloud sync.
Architecture Overview
Type4Me/
├── ASR/ # ASR engine abstraction
│ ├── ASRProvider.swift # Provider enum + protocols
│ ├── ASRProviderRegistry.swift # Plugin registry
│ ├── Providers/ # Per-vendor config files
│ ├── SherpaASRClient.swift # Local streaming ASR
│ ├── SherpaOfflineASRClient.swift
│ ├── VolcASRClient.swift # Volcengine streaming ASR
│ └── DeepgramASRClient.swift # Deepgram streaming ASR
├── Bridge/ # SherpaOnnx C API Swift bridge
├── Audio/ # Audio capture
├── Session/ # Core state machine: record→ASR→inject
├── Input/ # Global hotkey management
├── Services/ # Credentials, hotwords, model manager
├── Protocol/ # Volcengine WebSocket codec
└── UI/ # SwiftUI (FloatingBar + Settings)
Installation
Prerequisites
xcode-select --install
brew install cmake
Build & Deploy from Source
git clone https://github.com/joewongjc/type4me.git
cd type4me
bash scripts/build-sherpa.sh
bash scripts/deploy.sh
Download Pre-built App
Download Type4Me-v1.2.3.dmg from releases (cloud ASR only, no local engine):
https://github.com/joewongjc/type4me/releases/tag/v1.2.3
If macOS blocks the app:
xattr -d com.apple.quarantine /Applications/Type4Me.app
Download Local ASR Models
mkdir -p ~/Library/Application\ Support/Type4Me/Models
tar xjf ~/Downloads/sherpa-onnx-streaming-zipformer-small-ctc-zh-int8-2025-04-01.tar.bz2 \
-C ~/Library/Application\ Support/Type4Me/Models/
tar xjf ~/Downloads/sherpa-onnx-streaming-zipformer-ctc-multi-zh-hans-2023-12-13.tar.bz2 \
-C ~/Library/Application\ Support/Type4Me/Models/
tar xjf ~/Downloads/sherpa-onnx-streaming-paraformer-bilingual-zh-en.tar.bz2 \
-C ~/Library/Application\ Support/Type4Me/Models/
Expected structure for Paraformer model:
~/Library/Application Support/Type4Me/Models/
└── sherpa-onnx-streaming-paraformer-bilingual-zh-en/
├── encoder.int8.onnx
├── decoder.int8.onnx
└── tokens.txt
Key Protocols
SpeechRecognizer Protocol
Every ASR client must implement this protocol:
protocol SpeechRecognizer: AnyObject {
func startRecognition() async throws
func appendAudio(_ buffer: AVAudioPCMBuffer) async
func stopRecognition() async throws -> String
func cancelRecognition() async
var partialResultHandler: ((String) -> Void)? { get set }
}
ASRProviderConfig Protocol
Each vendor's credential definition:
protocol ASRProviderConfig {
static var providerID: String { get }
static var displayName: String { get }
static var credentialFields: [CredentialField] { get }
static func validate(_ credentials: [String: String]) -> Bool
static func createClient(
credentials: [String: String],
config: RecognitionConfig
) throws -> SpeechRecognizer
}
Adding a New ASR Provider
Step 1: Create Provider Config
Create Type4Me/ASR/Providers/OpenAIWhisperProvider.swift:
import Foundation
struct OpenAIWhisperProvider: ASRProviderConfig {
static let providerID = "openai_whisper"
static let displayName = "OpenAI Whisper"
static let credentialFields: [CredentialField] = [
CredentialField(
key: "api_key",
label: "API Key",
placeholder: "sk-...",
isSecret: true
),
CredentialField(
key: "model",
label: "Model",
placeholder: "whisper-1",
isSecret: false
)
]
static func validate(_ credentials: [String: String]) -> Bool {
guard let apiKey = credentials["api_key"], !apiKey.isEmpty else {
return false
}
return apiKey.hasPrefix("sk-")
}
static func createClient(
credentials: [String: ],
:
) -> {
apiKey credentials[] {
.missingCredential()
}
model credentials[]
(apiKey: apiKey, model: model, config: config)
}
}
Step 2: Implement the ASR Client
Create Type4Me/ASR/OpenAIWhisperASRClient.swift:
import Foundation
import AVFoundation
final class OpenAIWhisperASRClient: SpeechRecognizer {
var partialResultHandler: ((String) -> Void)?
private let apiKey: String
private let model: String
private let config: RecognitionConfig
private var audioData: Data = Data()
init(apiKey: String, model: String, config: RecognitionConfig) {
self.apiKey = apiKey
self.model = model
self.config = config
}
func startRecognition() async throws {
audioData = Data()
}
func appendAudio(_ buffer: AVAudioPCMBuffer) async {
guard let channelData buffer.floatChannelData[] { }
frameCount (buffer.frameLength)
bytes (start: channelData, count: frameCount)
int16Samples bytes.map { sample ->
((, (, (sample ))))
}
int16Samples.withUnsafeBytes { ptr
audioData.append(contentsOf: ptr)
}
}
() -> {
request (url: (string: ))
request.httpMethod
request.setValue(, forHTTPHeaderField: )
boundary ().uuidString
request.setValue(,
forHTTPHeaderField: )
body ()
body.append(.data(using: .utf8))
body.append(.data(using: .utf8))
body.append(.data(using: .utf8))
body.append(audioData)
body.append(.data(using: .utf8))
body.append(.data(using: .utf8))
body.append(.data(using: .utf8))
body.append(.data(using: .utf8))
body.append(.data(using: .utf8))
request.httpBody body
(data, response) .shared.data(for: request)
httpResponse response ,
httpResponse.statusCode {
.networkError()
}
result ().decode(., from: data)
result.text
}
() {
audioData ()
}
}
: {
text:
}
Step 3: Register the Provider
In Type4Me/ASR/ASRProviderRegistry.swift, add to the all array:
struct ASRProviderRegistry {
static let all: [any ASRProviderConfig.Type] = [
SherpaParaformerProvider.self,
VolcengineProvider.self,
DeepgramProvider.self,
OpenAIWhisperProvider.self,
]
}
Credentials Storage
Credentials are stored at ~/Library/Application Support/Type4Me/credentials.json with permissions 0600. Never hardcode secrets — always load via CredentialStore:
let store = CredentialStore.shared
let apiKey = store.get(providerID: "openai_whisper", key: "api_key")
store.set(providerID: "openai_whisper", key: "api_key", value: userInputKey)
let isConfigured = store.isConfigured(providerID: "openai_whisper",
fields: OpenAIWhisperProvider.credentialFields)
Custom Processing Modes with Prompt Variables
Processing modes use LLM post-processing with three context variables:
| Variable | Value |
|---|
{text} | Recognized speech text |
{selected} | Text selected in active app at record start |
{clipboard} | Clipboard content at record start |
Example custom mode prompts:
let translatePrompt = """
The user selected this text: {selected}
Voice command: {text}
Execute the command on the selected text. Output only the result.
"""
let codeReviewPrompt = """
Code to review:
{clipboard}
Review instruction: {text}
Provide focused feedback addressing the instruction.
"""
let emailPrompt = """
Original email: {selected}
My reply intent (spoken): {text}
Write a professional email reply. Output only the email body.
"""
Built-in Processing Modes
enum ProcessingMode {
case fast
case performance
case englishTranslation
case promptOptimize
case command
case custom(prompt: String)
}
Session State Machine
The core recording flow in Session/:
[Idle]
→ hotkey pressed → [Recording] → audio streams to ASR client
→ hotkey released/pressed again → [Processing]
→ ASR returns text → [LLM Post-processing] (if mode requires)
→ [Injecting] → text injected into active app
→ [Idle]
Updating After Source Changes
cd type4me
git pull
bash scripts/deploy.sh
Troubleshooting
App won't open (security warning)
xattr -d com.apple.quarantine /Applications/Type4Me.app
Local model not recognized in Settings
Verify the directory structure exactly matches:
ls ~/Library/Application\ Support/Type4Me/Models/sherpa-onnx-streaming-paraformer-bilingual-zh-en/
SherpaOnnx build fails
brew install cmake
rm -rf Frameworks/
bash scripts/build-sherpa.sh
New ASR provider not appearing in Settings
- Confirm the provider type is added to
ASRProviderRegistry.all
- Ensure
providerID is unique across all providers
- Clean build:
swift package clean && bash scripts/deploy.sh
Audio not captured / no floating bar
- Grant microphone permission: System Settings → Privacy & Security → Microphone → Type4Me ✓
- Grant Accessibility permission for text injection: System Settings → Privacy & Security → Accessibility → Type4Me ✓
Credentials not saving
ls -la ~/Library/Application\ Support/Type4Me/credentials.json
chmod 0600 ~/Library/Application\ Support/Type4Me/credentials.json
Export history to CSV
Open Settings → History → select date range → Export CSV. The SQLite database is at:
~/Library/Application\ Support/Type4Me/history.db
sqlite3 ~/Library/Application\ Support/Type4Me/history.db \
"SELECT datetime(timestamp,'unixepoch'), text FROM records ORDER BY timestamp DESC LIMIT 20;"
System Requirements
- macOS 14.0 (Sonoma) or later
- Apple Silicon (M1/M2/M3/M4) recommended for local ASR inference
- Xcode Command Line Tools + CMake for source builds
- Internet connection only needed for cloud ASR providers