Type4me Macos Voice Input

作者 reason-machines2384a003145a無授權條款83 個星標收錄於 2026年10月8日更新於 2026年10月8日儲存庫3 個月前更新

MacOS voice input tool with local/cloud ASR engines, LLM text optimization, and fully local storage built in Swift

AI 產生的概覽

指導建置、擴充與排解 Type4Me 這款內建本機與雲端 ASR 引擎的 macOS 語音輸入應用程式。

功能
此技能說明 Type4Me macOS 語音輸入應用程式,涵蓋其架構、建置與部署指令碼、ASR 供應方協定、憑證儲存、處理模式與工作階段狀態機。它說明如何撰寫供應方設定與用戶端並完成註冊,以新增 ASR 供應方。內容也包含模型安裝、權限設定,以及移除隔離屬性、憑證檔案權限等排解步驟。
適用情境
適用於為 Type4Me 新增 ASR 供應方或雲端語音服務、從原始碼建置或部署應用程式、設定本機 Sherpa 模型,或排解語音輸入、音訊擷取與憑證問題。
執行需求
需要 macOS 14.0 或更新版本、Xcode 命令列工具,從原始碼建置還需 CMake;本機 ASR 建議使用 Apple 晶片。雲端 ASR 供應方需要網路連線與廠商憑證。此技能僅為說明文件,不附帶指令碼,但會引用 Type4Me 儲存庫中的 build-sherpa.sh 與 deploy.sh。

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

bash
# Xcode Command Line Toolsxcode-select --install
# CMake (for local ASR engine)brew install cmake

Build & Deploy from Source

bash
git clone https://github.com/joewongjc/type4me.gitcd type4me
# Step 1: Compile SherpaOnnx local engine (~5 min, one-time)bash scripts/build-sherpa.sh
# Step 2: Build, bundle, sign, install to /Applications, and launchbash 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:

bash
xattr -d com.apple.quarantine /Applications/Type4Me.app

Download Local ASR Models

bash
mkdir -p ~/Library/Application\ Support/Type4Me/Models
# Option A: Lightweight ~20MBtar xjf ~/Downloads/sherpa-onnx-streaming-zipformer-small-ctc-zh-int8-2025-04-01.tar.bz2 \    -C ~/Library/Application\ Support/Type4Me/Models/
# Option B: Balanced ~236MB (recommended)tar xjf ~/Downloads/sherpa-onnx-streaming-zipformer-ctc-multi-zh-hans-2023-12-13.tar.bz2 \    -C ~/Library/Application\ Support/Type4Me/Models/
# Option C: Bilingual Chinese+English ~1GBtar 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:

swift
protocol SpeechRecognizer: AnyObject {    /// Start a new recognition session    func startRecognition() async throws        /// Feed raw PCM audio data    func appendAudio(_ buffer: AVAudioPCMBuffer) async        /// Stop and get final result    func stopRecognition() async throws -> String        /// Cancel without result    func cancelRecognition() async        /// Streaming partial results (optional)    var partialResultHandler: ((String) -> Void)? { get set }}

ASRProviderConfig Protocol

Each vendor's credential definition:

swift
protocol ASRProviderConfig {    /// Unique identifier string    static var providerID: String { get }        /// Display name in Settings UI    static var displayName: String { get }        /// Credential fields shown in Settings    static var credentialFields: [CredentialField] { get }        /// Validate credentials before use    static func validate(_ credentials: [String: String]) -> Bool        /// Create the recognizer instance    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:

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: String],        config: RecognitionConfig    ) throws -> SpeechRecognizer {        guard let apiKey = credentials["api_key"] else {            throw ASRError.missingCredential("api_key")        }        let model = credentials["model"] ?? "whisper-1"        return OpenAIWhisperASRClient(apiKey: apiKey, model: model, config: config)    }}

Step 2: Implement the ASR Client

Create Type4Me/ASR/OpenAIWhisperASRClient.swift:

swift
import Foundationimport 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 {        // Convert PCM buffer to raw bytes and accumulate        guard let channelData = buffer.floatChannelData?[0] else { return }        let frameCount = Int(buffer.frameLength)        let bytes = UnsafeBufferPointer(start: channelData, count: frameCount)        // Convert Float32 PCM to Int16 for Whisper API        let int16Samples = bytes.map { sample -> Int16 in            return Int16(max(-32768, min(32767, Int(sample * 32767))))        }        int16Samples.withUnsafeBytes { ptr in            audioData.append(contentsOf: ptr)        }    }        func stopRecognition() async throws -> String {        // Build multipart form request to Whisper API        var request = URLRequest(url: URL(string: "https://api.openai.com/v1/audio/transcriptions")!)        request.httpMethod = "POST"        request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")                let boundary = UUID().uuidString        request.setValue("multipart/form-data; boundary=\(boundary)",                         forHTTPHeaderField: "Content-Type")                var body = Data()        // Append audio file part        body.append("--\(boundary)\r\n".data(using: .utf8)!)        body.append("Content-Disposition: form-data; name=\"file\"; filename=\"audio.raw\"\r\n".data(using: .utf8)!)        body.append("Content-Type: audio/raw\r\n\r\n".data(using: .utf8)!)        body.append(audioData)        body.append("\r\n".data(using: .utf8)!)        // Append model part        body.append("--\(boundary)\r\n".data(using: .utf8)!)        body.append("Content-Disposition: form-data; name=\"model\"\r\n\r\n".data(using: .utf8)!)        body.append("\(model)\r\n".data(using: .utf8)!)        body.append("--\(boundary)--\r\n".data(using: .utf8)!)                request.httpBody = body                let (data, response) = try await URLSession.shared.data(for: request)        guard let httpResponse = response as? HTTPURLResponse,              httpResponse.statusCode == 200 else {            throw ASRError.networkError("Whisper API returned error")        }                let result = try JSONDecoder().decode(WhisperResponse.self, from: data)        return result.text    }        func cancelRecognition() async {        audioData = Data()    }}
private struct WhisperResponse: Codable {    let text: String}

Step 3: Register the Provider

In Type4Me/ASR/ASRProviderRegistry.swift, add to the all array:

swift
struct ASRProviderRegistry {    static let all: [any ASRProviderConfig.Type] = [        SherpaParaformerProvider.self,        VolcengineProvider.self,        DeepgramProvider.self,        OpenAIWhisperProvider.self,   // ← Add your provider here    ]}

Credentials Storage

Credentials are stored at ~/Library/Application Support/Type4Me/credentials.json with permissions 0600. Never hardcode secrets — always load via CredentialStore:

swift
// Reading credentialslet store = CredentialStore.sharedlet apiKey = store.get(providerID: "openai_whisper", key: "api_key")
// Writing credentials  store.set(providerID: "openai_whisper", key: "api_key", value: userInputKey)
// Checking if configuredlet 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:

VariableValue
{text}Recognized speech text
{selected}Text selected in active app at record start
{clipboard}Clipboard content at record start

Example custom mode prompts:

swift
// Translate selection using voice commandlet translatePrompt = """The user selected this text: {selected}Voice command: {text}Execute the command on the selected text. Output only the result."""
// Code review via voicelet codeReviewPrompt = """Code to review:{clipboard}
Review instruction: {text}
Provide focused feedback addressing the instruction."""
// Email reply draftinglet emailPrompt = """Original email: {selected}My reply intent (spoken): {text}Write a professional email reply. Output only the email body."""

Built-in Processing Modes

swift
enum ProcessingMode {    case fast           // Direct ASR output, zero latency    case performance    // Dual-channel: streaming + offline refinement    case englishTranslation  // Chinese speech → English text    case promptOptimize // Raw prompt → optimized prompt via LLM    case command        // Voice command + selected/clipboard context → LLM action    case custom(prompt: String)  // User-defined prompt template}

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

bash
cd type4megit pullbash scripts/deploy.sh# SherpaOnnx does NOT need recompiling unless engine version changed

Troubleshooting

App won't open (security warning)

bash
xattr -d com.apple.quarantine /Applications/Type4Me.app

Local model not recognized in Settings

Verify the directory structure exactly matches:

bash
ls ~/Library/Application\ Support/Type4Me/Models/sherpa-onnx-streaming-paraformer-bilingual-zh-en/# Must show: encoder.int8.onnx  decoder.int8.onnx  tokens.txt

SherpaOnnx build fails

bash
# Ensure cmake is installedbrew install cmake# Clean and retryrm -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

bash
# Check file exists and has correct permissionsls -la ~/Library/Application\ Support/Type4Me/credentials.json# Should show: -rw------- (0600)# Fix permissions if needed: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:

bash
~/Library/Application\ Support/Type4Me/history.db# Direct query: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

來源與署名

來源:reason-machines/trending-skills位於skills/type4me-macos-voice-input提交2384a00

授權條款: 無授權條款

內容歸原作者所有。SourceWeft 從公開儲存庫中收錄這些內容。

檢舉或申請下架