API & Protokolle

Das AudioCommon-Modul definiert modellagnostische Protokolle und gemeinsame Typen. Jedes konforme Modell kann über diese Schnittstellen austauschbar verwendet werden.

Protokollübersicht

┌─────────────────────────────────────────────────────────┐
│                    AudioCommon                          │
│                                                         │
│  AudioChunk          SpeechGenerationModel (TTS)        │
│  AlignedWord         SpeechRecognitionModel (STT)       │
│  SpeechSegment       ForcedAlignmentModel               │
│                      SpeechToSpeechModel                │
│                      VoiceActivityDetectionModel (VAD)   │
│                      SpeakerEmbeddingModel              │
│                      SpeakerDiarizationModel            │
│                      SpeakerExtractionCapable           │
└─────────────────────────────────────────────────────────┘

SpeechRecognitionModel

Protokoll für Sprache-zu-Text-Modelle.

public protocol SpeechRecognitionModel: AnyObject {
    var inputSampleRate: Int { get }
    func transcribe(audio: [Float], sampleRate: Int, language: String?) -> String
    func transcribeWithLanguage(audio: [Float], sampleRate: Int, language: String?) -> TranscriptionResult
}

Konforme Typen: Qwen3ASRModel, ParakeetASRModel, ParakeetStreamingASRModel, OmnilingualASRModel (CoreML), OmnilingualASRMLXModel (MLX)

SpeechGenerationModel

Protokoll für Text-zu-Sprache-Modelle.

public protocol SpeechGenerationModel: AnyObject {
    var sampleRate: Int { get }
    func generate(text: String, language: String?) async throws -> [Float]
    func generateStream(text: String, language: String?) -> AsyncThrowingStream<AudioChunk, Error>  // has default impl
}

generateStream() hat eine Standardimplementierung, die generate() als einzelnen Chunk kapselt. Modelle mit echtem Streaming (z. B. Qwen3-TTS) überschreiben sie.

Konforme Typen: Qwen3TTSModel, CosyVoiceTTSModel, KokoroTTSModel, Qwen35MLXChat

ForcedAlignmentModel

Protokoll für wortgenaue Zeitstempel-Zuordnung.

public protocol ForcedAlignmentModel: AnyObject {
    func align(audio: [Float], text: String, sampleRate: Int, language: String?) -> [AlignedWord]
}

SpeechToSpeechModel

Protokoll für Sprache-zu-Sprache-Dialogmodelle.

public protocol SpeechToSpeechModel: AnyObject {
    var sampleRate: Int { get }
    func respond(userAudio: [Float]) -> [Float]
    func respondStream(userAudio: [Float]) -> AsyncThrowingStream<AudioChunk, Error>
}

Konforme Typen: PersonaPlexModel

VoiceActivityDetectionModel

Protokoll für die Sprachaktivitätserkennung.

public protocol VoiceActivityDetectionModel: AnyObject {
    var inputSampleRate: Int { get }
    func detectSpeech(audio: [Float], sampleRate: Int) -> [SpeechSegment]
}

SpeakerEmbeddingModel

Protokoll zur Extraktion von Sprechereinbettungen.

public protocol SpeakerEmbeddingModel: AnyObject {
    var inputSampleRate: Int { get }
    var embeddingDimension: Int { get }
    func embed(audio: [Float], sampleRate: Int) -> [Float]
}

Konforme Typen: WeSpeakerModel

SpeakerDiarizationModel

Protokoll für Sprecherdiarisierungsmodelle, die Sprecher-Labels zu Audiosegmenten zuordnen.

public protocol SpeakerDiarizationModel: AnyObject {
    var inputSampleRate: Int { get }
    func diarize(audio: [Float], sampleRate: Int) -> [DiarizedSegment]
}

Konforme Typen: DiarizationPipeline (Pyannote), SortformerDiarizer

SpeakerExtractionCapable

Erweitertes Diarisierungsprotokoll für Engines, die das Extrahieren der Segmente eines Zielsprechers anhand einer Referenz-Einbettung unterstützen. Nicht alle Engines bieten dies (Sortformer ist durchgängig und erzeugt keine Sprechereinbettungen).

public protocol SpeakerExtractionCapable: SpeakerDiarizationModel {
    func extractSpeaker(audio: [Float], sampleRate: Int, targetEmbedding: [Float]) -> [SpeechSegment]
}

Konforme Typen: DiarizationPipeline (nur Pyannote)

Gemeinsame Typen

AudioChunk

public struct AudioChunk {
    public let samples: [Float]   // PCM samples
    public let sampleRate: Int    // Sample rate (e.g. 24000)
}

SpeechSegment

public struct SpeechSegment {
    public let startTime: Float   // Start time in seconds
    public let endTime: Float     // End time in seconds
}

AlignedWord

public struct AlignedWord {
    public let text: String       // The word
    public let startTime: Float   // Start time in seconds
    public let endTime: Float     // End time in seconds
}

DiarizedSegment

public struct DiarizedSegment {
    public let startTime: Float   // Start time in seconds
    public let endTime: Float     // End time in seconds
    public let speakerId: Int     // Speaker identifier (0-based)
}

DialogueSegment

Ein geparster Abschnitt eines Mehrsprecher-Dialogtexts mit optionalen Sprecher- und Emotions-Tags. Wird mit DialogueParser und DialogueSynthesizer für die CosyVoice3-Dialogsynthese verwendet.

public struct DialogueSegment: Sendable, Equatable {
    public let speaker: String?   // Speaker identifier ("S1", "S2"), nil for untagged
    public let emotion: String?   // Emotion tag ("happy", "whispers"), nil if none
    public let text: String       // Cleaned text to synthesize
}

DialogueParser

Parst Mehrsprecher-Dialogtexte mit inline Sprecher-Tags ([S1]) und Emotions-Tags ((happy)).

public enum DialogueParser {
    static func parse(_ text: String) -> [DialogueSegment]
    static func emotionToInstruction(_ emotion: String) -> String
}

Integrierte Emotionen: happy/excited, sad, angry, whispers/whispering, laughs/laughing, calm, surprised, serious. Unbekannte Tags werden als freie Anweisungen durchgereicht.

DialogueSynthesizer

Orchestriert die Synthese mehrerer Dialogsegmente mit Stimmklonen pro Sprecher, Pausen und Crossfade.

public enum DialogueSynthesizer {
    static func synthesize(
        segments: [DialogueSegment],
        speakerEmbeddings: [String: [Float]],
        model: CosyVoiceTTSModel,
        language: String,
        config: DialogueSynthesisConfig,
        verbose: Bool
    ) -> [Float]
}

DialogueSynthesisConfig

public struct DialogueSynthesisConfig: Sendable {
    public var turnGapSeconds: Float      // Default: 0.2
    public var crossfadeSeconds: Float    // Default: 0.0
    public var defaultInstruction: String // Default: "You are a helpful assistant."
    public var maxTokensPerSegment: Int   // Default: 500
}

PipelineLLM

Protokoll zur Integration von Sprachmodellen in Voice-Pipelines. Bindet ein LLM in den ASR → LLM → TTS-Fluss der VoicePipeline ein.

public protocol PipelineLLM: AnyObject {
    func chat(messages: [(role: MessageRole, content: String)],
              onToken: @escaping (String, Bool) -> Void)
    func cancel()
}

Eingebauter Adapter: Qwen3PipelineLLM verbindet Qwen35MLXChat mit diesem Protokoll inkl. Token-Bereinigung, Abbruch und Pufferung ausstehender Phrasen.

AudioIO

Wiederverwendbarer Audio-I/O-Manager, der AVAudioEngine-Boilerplate eliminiert. Kümmert sich um Mikrofonaufnahme, Resampling, Wiedergabe und Audiopegel-Messung.

let audio = AudioIO()
try audio.startMicrophone(targetSampleRate: 16000) { samples in
    pipeline.pushAudio(samples)
}
audio.player.scheduleChunk(ttsOutput)
audio.stopMicrophone()

AudioIO enthält einen StreamingAudioPlayer für TTS-Ausgabe und einen AudioRingBuffer für thread-sicheren Audio-Transfer zwischen Aufnahme- und Inferenz-Threads.

SentencePieceModel

Gemeinsamer Protobuf-Reader für SentencePiece-.model-Dateien, lebt in AudioCommon. Jedes Modul, das SentencePiece-Stücke dekodieren muss (PersonaPlex, OmnilingualASR, künftige ASR-/TTS-Ports), baut seinen eigenen Decoder auf diesem einzigen Reader auf, statt das Protobuf-Wire-Format neu zu implementieren.

public struct SentencePieceModel: Sendable {
    public struct Piece: Sendable, Equatable {
        public let text: String
        public let score: Float
        public let type: Int32
        public var pieceType: PieceType? { get }
        public var isControlOrUnknown: Bool { get }
    }
    public enum PieceType: Int32 {
        case normal = 1, unknown = 2, control = 3,
             userDefined = 4, unused = 5, byte = 6
    }
    public let pieces: [Piece]
    public var count: Int { get }
    public subscript(_ id: Int) -> Piece? { get }
    public init(contentsOf url: URL) throws
    public init(modelPath: String) throws
    public init(data: Data) throws
}

Verwendet von: OmnilingualASR.OmnilingualVocabulary, PersonaPlex.SentencePieceDecoder. Abgedeckt durch 7 Unit-Tests in Tests/AudioCommonTests/SentencePieceModelTests.

MLXCommon.SDPA

Hilfsfunktionen für Scaled-Dot-Product-Attention, die in jedem MLX-Attention-Modul (Qwen3-ASR / Qwen3-TTS / Qwen3-Chat / CosyVoice / PersonaPlex / OmnilingualASR) geteilt werden. Jedes Modul behält seine eigenen Projektionen — SDPA übernimmt nur das Reshape → Attention → Merge-Boilerplate.

public enum SDPA {
    // Flat [B, T, H*D] input: project/reshape happens inside
    public static func multiHead(
        q: MLXArray, k: MLXArray, v: MLXArray,
        numHeads: Int, headDim: Int, scale: Float,
        mask: MLXArray? = nil
    ) -> MLXArray

    // GQA / MQA variant with separate query and KV head counts
    public static func multiHead(
        q: MLXArray, k: MLXArray, v: MLXArray,
        numQueryHeads: Int, numKVHeads: Int, headDim: Int, scale: Float,
        mask: MLXArray? = nil
    ) -> MLXArray

    // Already-shaped [B, H, T, D] (RoPE / KV cache paths)
    public static func attendAndMerge(
        qHeads: MLXArray, kHeads: MLXArray, vHeads: MLXArray,
        scale: Float,
        mask: MLXArray? = nil
    ) -> MLXArray

    // Same, with ScaledDotProductAttentionMaskMode enum (newer API)
    public static func attendAndMerge(
        qHeads: MLXArray, kHeads: MLXArray, vHeads: MLXArray,
        scale: Float,
        mask: MLXFast.ScaledDotProductAttentionMaskMode
    ) -> MLXArray

    // Low-level head merge: [B, H, T, D] → [B, T, H*D]
    public static func mergeHeads(_ attn: MLXArray) -> MLXArray
}

Alle Reshape-Aufrufe verwenden -1 für die Batch-Dimension, damit die Helper mit MLX.compile(shapeless:)-Graphen komponierbar sind, die den Batch zur Laufzeit variieren (z. B. Qwen3-TTS-Talker-Autoregressive-Decode).

HTTP-API-Server

Die audio-server-Binary stellt jedes Modell aus speech-swift als HTTP-REST-Endpunkte plus einen WebSocket-Endpunkt bereit, der die OpenAI Realtime API implementiert. Modelle werden bei der ersten Anfrage träge geladen; mit --preload werden sie alle beim Start vorgewärmt.

swift build -c release
.build/release/audio-server --port 8080

# Preload every model at startup
.build/release/audio-server --port 8080 --preload

REST-Endpunkte

EndpunktMethodeAnfrageAntwort
/transcribePOSTaudio/wav-BodyJSON { text } (Qwen3-ASR)
/speakPOSTJSON { text, engine?, language?, voice? }audio/wav-Body (Qwen3-TTS, CosyVoice, Kokoro)
/respondPOSTaudio/wav-Bodyaudio/wav-Body (PersonaPlex)
/enhancePOSTaudio/wav-Bodyaudio/wav-Body (DeepFilterNet3)
/vadPOSTaudio/wav-BodyJSON-Segmentliste
/diarizePOSTaudio/wav-BodyJSON-DiarizedSegment-Liste
/embed-speakerPOSTaudio/wav-BodyJSON [Float] (256-dim)
# Transcribe a file
curl -X POST http://localhost:8080/transcribe \
  --data-binary @recording.wav \
  -H "Content-Type: audio/wav"

# Synthesize speech
curl -X POST http://localhost:8080/speak \
  -H "Content-Type: application/json" \
  -d '{"text": "Hello world", "engine": "cosyvoice"}' \
  -o output.wav

# Full speech-to-speech round trip
curl -X POST http://localhost:8080/respond \
  --data-binary @question.wav \
  -o response.wav

OpenAI Realtime API (/v1/realtime)

Der WebSocket-Endpunkt unter ws://host:port/v1/realtime implementiert das OpenAI-Realtime-Protokoll. Alle Nachrichten sind JSON mit einem type-Diskriminator; Audio-Payloads sind base64-kodiertes PCM16 bei 24 kHz Mono.

Client → Server-Events

EventZweck
session.updateEngine, Sprache, Stimme und Audioformat konfigurieren
input_audio_buffer.appendEinen base64-PCM16-Chunk an den Eingabe-Puffer anhängen
input_audio_buffer.commitDen gepufferten Audiostrom zur Transkription übergeben
input_audio_buffer.clearDen aktuellen Eingabepuffer verwerfen
response.createTTS-Synthese für den angegebenen Text / die Anweisungen anfordern

Server → Client-Events

EventBedeutung
session.createdHandshake abgeschlossen, Standardkonfiguration ausgegeben
session.updatedDas letzte session.update bestätigt
input_audio_buffer.committedAudio angenommen und zur Transkription eingereiht
conversation.item.input_audio_transcription.completedASR-Ergebnis mit finalem Transkripttext
response.audio.deltaBase64-PCM16-Chunk des synthetisierten Audios
response.audio.doneKeine weiteren Audio-Chunks für diese Antwort
response.doneAntwort finalisiert (Metadaten + Latenzstatistiken)
errorFehler-Envelope mit type und message
const ws = new WebSocket('ws://localhost:8080/v1/realtime');

// ASR: push audio, request transcription
ws.send(JSON.stringify({ type: 'input_audio_buffer.append', audio: base64PCM16 }));
ws.send(JSON.stringify({ type: 'input_audio_buffer.commit' }));
// → conversation.item.input_audio_transcription.completed

// TTS: request synthesis and stream audio deltas
ws.send(JSON.stringify({
  type: 'response.create',
  response: { modalities: ['audio', 'text'], instructions: 'Hello world' }
}));
// → response.audio.delta (repeated), response.audio.done, response.done

Der Server lebt im AudioServer-SPM-Produkt. Ein beispielhafter Browser-Client wird unter Examples/websocket-client.html mitgeliefert — öffne ihn parallel zu einem laufenden Server, um den vollständigen ASR- + TTS-Roundtrip auszuführen.

Modell-Downloads

Alle Modelle werden beim ersten Gebrauch von HuggingFace heruntergeladen und in ~/Library/Caches/qwen3-speech/ zwischengespeichert. Das AudioCommon-Modul stellt einen gemeinsamen HuggingFaceDownloader bereit, der Download, Caching und Integritätsprüfung übernimmt.