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
| Endpunkt | Methode | Anfrage | Antwort |
|---|---|---|---|
/transcribe | POST | audio/wav-Body | JSON { text } (Qwen3-ASR) |
/speak | POST | JSON { text, engine?, language?, voice? } | audio/wav-Body (Qwen3-TTS, CosyVoice, Kokoro) |
/respond | POST | audio/wav-Body | audio/wav-Body (PersonaPlex) |
/enhance | POST | audio/wav-Body | audio/wav-Body (DeepFilterNet3) |
/vad | POST | audio/wav-Body | JSON-Segmentliste |
/diarize | POST | audio/wav-Body | JSON-DiarizedSegment-Liste |
/embed-speaker | POST | audio/wav-Body | JSON [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
| Event | Zweck |
|---|---|
session.update | Engine, Sprache, Stimme und Audioformat konfigurieren |
input_audio_buffer.append | Einen base64-PCM16-Chunk an den Eingabe-Puffer anhängen |
input_audio_buffer.commit | Den gepufferten Audiostrom zur Transkription übergeben |
input_audio_buffer.clear | Den aktuellen Eingabepuffer verwerfen |
response.create | TTS-Synthese für den angegebenen Text / die Anweisungen anfordern |
Server → Client-Events
| Event | Bedeutung |
|---|---|
session.created | Handshake abgeschlossen, Standardkonfiguration ausgegeben |
session.updated | Das letzte session.update bestätigt |
input_audio_buffer.committed | Audio angenommen und zur Transkription eingereiht |
conversation.item.input_audio_transcription.completed | ASR-Ergebnis mit finalem Transkripttext |
response.audio.delta | Base64-PCM16-Chunk des synthetisierten Audios |
response.audio.done | Keine weiteren Audio-Chunks für diese Antwort |
response.done | Antwort finalisiert (Metadaten + Latenzstatistiken) |
error | Fehler-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.