Zurück zu Add-ons

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:

swift
// 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:

json
{
  "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.

swift
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.

swift
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

swift
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

swift
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

swift
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

swift
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

swift
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.

swift
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:

swift
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:

bash
~/Library/Application Support/TypeWhisper/Plugins/MyPlugin.bundle

Beim 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. 1. Forke das Repository TypeWhisper/typewhisper-plugins .
  2. 2. Erstelle ein Verzeichnis unter plugins/your-plugin-slug/ mit deiner manifest.json, README.md, LICENSE, und src/ als Verzeichnis mit deinem Swift-Quellcode.
  3. 3. Öffne einen Pull Request - die CI validiert automatisch dein Manifest, prüft erforderliche Dateien und kompiliert dein Plugin.
  4. 4. Das TypeWhisper-Team prüft deine Einreichung.
  5. 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.