Plugin erstellen
Erstelle eigene Plugins für TypeWhisper mit Swift und dem TypeWhisperPluginSDK.
Überblick
TypeWhisper-Plugins sind normale macOS-Bundles (.bundle), die in Swift geschrieben werden. Jedes Plugin bindet das TypeWhisperPluginSDK-Paket ein und exportiert eine Principal Class, die einem oder mehreren Plugin-Protokollen entspricht. Die Principal Class muss von NSObject erben und das Attribut @objc(ClassName) verwenden, damit der Bundle-Loader sie instanziieren kann. Plugins werden beim Start aus ~/Library/Application Support/TypeWhisper/Plugins/.
Plugin-Typen
Es gibt vier Plugin-Protokolle, die du implementieren kannst. Ein einzelnes Plugin kann mehreren Protokollen gleichzeitig entsprechen, z.B. Transkription und LLM.
TranscriptionEnginePlugin
Stellt eine Speech-to-Text-Engine bereit. Erhält Audiodaten (16kHz Mono-Float-Samples plus vorcodiertes WAV) und gibt transkribierten Text zurück. Unterstützt Modellauswahl, Spracherkennung, Übersetzung und optionales Streaming über einen Progress-Callback.
LLMProviderPlugin
Stellt ein LLM zur Verarbeitung transkribierten Texts über eigene Prompts bereit. Erhält einen System-Prompt und Benutzertest und gibt die Modellantwort zurück. Geeignet für Textkorrektur, Zusammenfassung, Formatierung und mehr.
PostProcessorPlugin
Verarbeitet Text nach der Transkription in einer prioritätsbasierten Pipeline. Erhält den transkribierten Text und Kontext wie aktive App, URL und Sprache. Läuft neben eingebauten Prozessoren wie Snippets und Wörterbuch.
ActionPlugin
Führt eine Aktion mit LLM-verarbeitetem Text aus, statt ihn einzufügen. Erhält den verarbeiteten Text und Kontext und gibt eine Ergebnisnachricht zurück, die im Notch-Indikator angezeigt wird. Kann eine zu öffnende URL und eine eigene Anzeigedauer enthalten.
Erste Schritte
Voraussetzungen
- - macOS 14.0+ (Sonoma) and Xcode 16+
- - Swift 6.0
- - Grundlegende Vertrautheit mit macOS-Bundle-Targets
1. Ein Bundle-Target erstellen
Erstelle in Xcode ein neues macOS-Bundle-Target. Setze in deiner Info.plist die Principal Class auf den Namen der Hauptklasse deines Plugins. Die Klasse muss von NSObject erben und mit @objc(ClassName) annotiert sein, damit die Runtime sie finden und instanziieren kann.
2. Die SDK-Abhängigkeit hinzufügen
Füge TypeWhisperPluginSDK als Swift-Package-Abhängigkeit hinzu:
// Package.swift
dependencies: [
.package(
url: "https://github.com/TypeWhisper/TypeWhisperPluginSDK.git",
from: "1.0.0"
)
]
// Target dependency
.product(name: "TypeWhisperPluginSDK", package: "TypeWhisperPluginSDK")3. manifest.json erstellen
Lege eine manifest.json im Resources-Verzeichnis deines Bundles an:
{
"id": "com.yourname.myplugin",
"name": "My Plugin",
"version": "1.0.0",
"minHostVersion": "0.9.0",
"minOSVersion": "14.0",
"author": "Your Name",
"principalClass": "MyPlugin"
}principalClass muss zum Namen in deiner @objc(...)-Annotation passen. minOSVersion ist optional und standardmäßig 14.0.
4. Bauen und installieren
Baue dein Bundle und kopiere die .bundle-Datei nach ~/Library/Application Support/TypeWhisper/Plugins/. Starte TypeWhisper neu, um das Plugin zu laden.
SDK-API-Referenz
TypeWhisperPlugin
Basisprotokoll, dem alle Plugins entsprechen müssen.
public protocol TypeWhisperPlugin: AnyObject, Sendable {
static var pluginId: String { get }
static var pluginName: String { get }
init()
func activate(host: HostServices)
func deactivate()
var settingsView: AnyView? { get } // optional, default nil
}HostServices
Wird deinem Plugin bei der Aktivierung übergeben. Bietet Zugriff auf Keychain, Preferences, Dateispeicher, App-Kontext und den Event Bus.
public protocol HostServices: Sendable {
// Keychain (plugin-scoped)
func storeSecret(key: String, value: String) throws
func loadSecret(key: String) -> String?
// UserDefaults (plugin-scoped)
func userDefault(forKey: String) -> Any?
func setUserDefault(_ value: Any?, forKey: String)
// File storage
var pluginDataDirectory: URL { get }
// App context
var activeAppBundleId: String? { get }
var activeAppName: String? { get }
// Event bus
var eventBus: EventBusProtocol { get }
// Profiles
var availableProfileNames: [String] { get }
// Notify host that plugin capabilities changed
func notifyCapabilitiesChanged()
}Rufe notifyCapabilitiesChanged() auf, wenn sich verfügbare Modelle oder der Konfigurationszustand deines Plugins ändern, z.B. nach dem Laden eines Modells oder dem Empfang eines API-Keys.
LLMProviderPlugin
public protocol LLMProviderPlugin: TypeWhisperPlugin {
var providerName: String { get }
var isAvailable: Bool { get }
var supportedModels: [PluginModelInfo] { get }
func process(
systemPrompt: String,
userText: String,
model: String?
) async throws -> String
}PluginModelInfo
public final class PluginModelInfo: @unchecked Sendable {
public let id: String
public let displayName: String
public let sizeDescription: String // e.g. "1.5 GB"
public let languageCount: Int // number of supported languages
public init(
id: String,
displayName: String,
sizeDescription: String = "",
languageCount: Int = 0
)
}TranscriptionEnginePlugin
public protocol TranscriptionEnginePlugin: TypeWhisperPlugin {
var providerId: String { get }
var providerDisplayName: String { get }
var isConfigured: Bool { get }
var transcriptionModels: [PluginModelInfo] { get }
var selectedModelId: String? { get }
func selectModel(_ modelId: String)
var supportsTranslation: Bool { get }
var supportsStreaming: Bool { get } // default false
var supportedLanguages: [String] { get } // default []
// Standard transcription
func transcribe(
audio: AudioData,
language: String?,
translate: Bool,
prompt: String?
) async throws -> PluginTranscriptionResult
// Streaming variant - onProgress returns false to cancel
func transcribe(
audio: AudioData,
language: String?,
translate: Bool,
prompt: String?,
onProgress: @Sendable @escaping (String) -> Bool
) async throws -> PluginTranscriptionResult
}Die Streaming-Variante besitzt eine Standardimplementierung, die auf die normale transcribe-Methode zurückfällt. Überschreibe sie und setze supportsStreaming auf true , wenn deine Engine Teilergebnisse unterstützt.
PostProcessorPlugin
public protocol PostProcessorPlugin: TypeWhisperPlugin {
var processorName: String { get }
var priority: Int { get }
@MainActor func process(
text: String,
context: PostProcessingContext
) async throws -> String
}
public struct PostProcessingContext: Sendable {
public let appName: String?
public let bundleIdentifier: String?
public let url: String?
public let language: String?
}ActionPlugin
public protocol ActionPlugin: TypeWhisperPlugin {
var actionName: String { get }
var actionId: String { get }
var actionIcon: String { get } // SF Symbol name
func execute(
input: String,
context: ActionContext
) async throws -> ActionResult
}
public struct ActionContext: Sendable {
public let appName: String?
public let bundleIdentifier: String?
public let url: String?
public let language: String?
public let originalText: String
}
public struct ActionResult: Sendable {
public let success: Bool
public let message: String
public let url: String? // URL to open after action
public let icon: String? // SF Symbol for result display
public let displayDuration: TimeInterval? // custom display time
}EventBus
Abonniere appweite Events wie Aufnahme-Start/Stopp, abgeschlossene Transkription und Texteinfügung.
public protocol EventBusProtocol: Sendable {
@discardableResult
func subscribe(
handler: @escaping @Sendable (TypeWhisperEvent) async -> Void
) -> UUID
func unsubscribe(id: UUID)
}
public enum TypeWhisperEvent: Sendable {
case recordingStarted(RecordingStartedPayload)
case recordingStopped(RecordingStoppedPayload)
case transcriptionCompleted(TranscriptionCompletedPayload)
case transcriptionFailed(TranscriptionFailedPayload)
case textInserted(TextInsertedPayload)
case actionCompleted(ActionCompletedPayload)
}Hilfsklassen
Das SDK enthält Helfer für häufige Muster:
- -
PluginOpenAITranscriptionHelper- OpenAI-kompatibler Transkriptions-API-Client - -
PluginOpenAIChatHelper- OpenAI-kompatibler Chat-Completion-Client - -
PluginWavEncoder- Kodiert Float-Samples zu WAV-Daten
Beispiel: Minimales LLM-Plugin
Ein vollständiges LLM-Provider-Plugin, das eine OpenAI-kompatible API kapselt:
import Foundation
import TypeWhisperPluginSDK
@objc(MyLLMPlugin)
final class MyLLMPlugin: NSObject, LLMProviderPlugin {
static let pluginId = "com.example.my-llm"
static let pluginName = "My LLM"
private nonisolated(unsafe) var host: HostServices?
private let chatHelper = PluginOpenAIChatHelper(
baseURL: "https://api.example.com"
)
let providerName = "My LLM"
let supportedModels = [
PluginModelInfo(
id: "model-v1",
displayName: "Model V1",
sizeDescription: "Cloud",
languageCount: 50
)
]
var isAvailable: Bool {
host?.loadSecret(key: "apiKey") != nil
}
override init() {
super.init()
}
func activate(host: HostServices) {
self.host = host
}
func deactivate() {
host = nil
}
func process(
systemPrompt: String,
userText: String,
model: String?
) async throws -> String {
guard let apiKey = host?.loadSecret(key: "apiKey") else {
throw PluginChatError.notConfigured
}
return try await chatHelper.process(
apiKey: apiKey,
model: model ?? "model-v1",
systemPrompt: systemPrompt,
userText: userText
)
}
}Distribution
Baue dein Plugin in der Release-Konfiguration und verteile die resultierende .bundle-Datei. Nutzer installieren es, indem sie es nach folgendem Pfad kopieren:
~/Library/Application Support/TypeWhisper/Plugins/MyPlugin.bundleBeim Plugin-Katalog einreichen
Teile dein Plugin mit der TypeWhisper-Community, indem du es in den offiziellen Plugin-Katalog einreichst. Eingereichte Plugins werden automatisch gebaut und auf dieser Website gelistet, damit sie leicht gefunden werden können.
So reichst du es ein
- 1. Forke das Repository TypeWhisper/typewhisper-plugins .
- 2. Erstelle ein Verzeichnis unter
plugins/your-plugin-slug/mit deinermanifest.json,README.md,LICENSE, undsrc/als Verzeichnis mit deinem Swift-Quellcode. - 3. Öffne einen Pull Request - die CI validiert automatisch dein Manifest, prüft erforderliche Dateien und kompiliert dein Plugin.
- 4. Das TypeWhisper-Team prüft deine Einreichung.
- 5. Nach dem Merge wird dein Plugin automatisch gebaut, veröffentlicht und im Katalog gelistet.
Siehe CONTRIBUTING.md für detaillierte Richtlinien zu Manifest-Format, Verzeichnisstruktur und Review-Kriterien.