API & Protokolle

Native Laufzeit für Apple · PocketTTS

PocketTTS ergänzt speech-swift um native CoreML-Sprachausgabe. Alle Stufen verwenden standardmäßig nur die CPU. Die englische Stimme Alba liefert jeweils 1.920 Samples mit 24 kHz, also 80 ms pro Audioblock.

Verwendung mit Swift und CLI

import PocketTTS
let model = try PocketTTSModel.fromBundle(URL(fileURLWithPath: "/path/to/bundle"))
try await model.generateStreaming(text: "Hello world.") { chunk in
    await consume(chunk.samples)
}

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)   │
│                      TurnCompletionProvider (EOT)       │
│                      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, WhisperASRModel, 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, VoxCPM2TTSModel, KokoroTTSModel, IndexTTS2TTSModel

IndexTTS2TTSModel ergänzt eine generate-Überladung mit Referenzaudio für Zero-Shot-Stimmklonen und nutzt IndexTTS2SynthesisOptions für Sprechgeschwindigkeit und Pausen.

Lokales Laden von Qwen3-TTS

fromLocal prüft Pflichtdateien und die Vollständigkeit der Checkpoint-Shards und leitet anschließend Modellgröße, Quantisierung und Architektur aus config.json ab. configuration dient nur als Fallback für fehlende Metadaten; ungültige oder widersprüchliche Angaben lösen vor der Modellzuweisung Qwen3TTSLoadingError.invalidConfiguration aus.

public static func fromLocal(
    modelDirectory: URL,
    tokenizerDirectory: URL,
    configuration: Qwen3TTSConfig? = nil,
    wiredMemoryPolicy: Qwen3TTSWiredMemoryPolicy = .none,
    progressHandler: ((Double, String) -> Void)? = nil
) throws -> Qwen3TTSModel

public static func fromPretrained(
    modelId: String = Qwen3TTSModel.defaultModelId,
    tokenizerModelId: String = "Qwen/Qwen3-TTS-Tokenizer-12Hz",
    cacheDir: URL? = nil,
    tokenizerCacheDir: URL? = nil,
    offlineMode: Bool = false,
    wiredMemoryPolicy: Qwen3TTSWiredMemoryPolicy = .pin(fraction: 0.9),
    progressHandler: ((Double, String) -> Void)? = nil
) async throws -> Qwen3TTSModel
Speicherrichtlinie

fromLocal verwendet standardmäßig .none und ändert das prozessweite Metal-Wired-Memory-Limit nicht. fromPretrained behält .pin(fraction: 0.9) bei. Beide Pretrained-Loader akzeptieren ein separates tokenizerCacheDir.

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]
}

TurnCompletionProvider

Protokoll für Turn-Ende-Klassifikatoren, die StreamingVADProcessor bei jeder bestätigten VAD-Pause befragt. Ein VAD hört nur Stille; ein Turn-Completion-Modell hört auf die Prosodie der gesamten Äußerung, sodass eine Pause mitten im Satz den Agenten warten lässt, während ein beendeter Satz sofort eine Antwort bekommt. Entspricht sc_turn_completion_vtable_t in speech-core. Siehe den Smart-Turn-Leitfaden.

public protocol TurnCompletionProvider: AnyObject {
    /// Probability in [0, 1] that the turn is complete, given the audio of the
    /// turn so far (Smart Turn looks at the last 8 s).
    func turnCompleteProbability(audio: [Float], sampleRate: Int) throws -> Float
}

Konforme Typen: SmartTurnModel (Smart Turn v3.2)

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.

SystemAudioTap

Erfasst den System-Ausgabemix — was der Mac gerade abspielt — als Mono-Float32 mit wählbarer Abtastrate (Core-Audio-Process-Taps, macOS 14.4+). Der globale Mono-Tap schließt den aktuellen Prozess standardmäßig aus, sodass die eigene Wiedergabe einer App nie erneut erfasst wird, und steckt in einem privaten Aggregatgerät, das nur den Tap enthält.

let tap = SystemAudioTap()
try tap.start(targetSampleRate: 16000) { samples in
    pipeline.pushAudio(samples)
}
tap.stop()

Die Aufnahme übersteht Wechsel des Standard-Ausgabegeräts — der interne Resampler wird bei geänderter Mix-Rate neu aufgebaut. Die Host-App muss NSAudioCaptureUsageDescription deklarieren. Bei verweigerter Aufnahmeberechtigung kann ein Tap entstehen und trotzdem nur Stille liefern: Wächst framesCaptured, während nonSilentFrames bei 0 bleibt, explizit fehlschlagen.

Unabhängige Quellen mit Zeitstempeln

Beide Aufnahmeklassen bieten Überladungen mit Zeitstempeln. CapturedAudioChunk enthält Mono-samples mit sampleRate sowie eine optionale Mach-hostTime für den ersten Eingabeframe. Da beide Quellen dieselbe Host-Uhr verwenden, kann eine App ihr PCM getrennt halten und abgeleitete Transkriptionsereignisse ohne Audiomischung zeitlich ordnen.

try tap.startTimestamped(targetSampleRate: 16000) { systemChunk in
    systemPipeline.pushAudio(systemChunk.samples)
    recordSystemTime(systemChunk.hostTime)
}

let microphone = AudioIO(enableAEC: true, enablePlayback: false)
try microphone.startMicrophoneTimestamped(targetSampleRate: 16000) { micChunk in
    microphonePipeline.pushAudio(micChunk.samples)
    recordMicrophoneTime(micChunk.hostTime)
}

Für eine reine Aufnahme aktiviert enableAEC: true Apples Sprachverarbeitung, bevor das Mikrofonformat gelesen wird; enablePlayback: false lässt den ungenutzten Player weg. Unter macOS wird die Gerätewiedergabe aus den Mikrofonsamples entfernt. Kann AEC nicht starten, wird der Fehler gemeldet, statt auf den Roh-Eingang zurückzufallen.

Inkrementelle Dateieingabe und Erkennung

CapturedAudioChunk enthält zusätzlich einen monotonen frameIndex und eine isFinal-Markierung. AudioFileLoader.stream erzeugt diese Chunks bei Bedarf mit begrenztem Speicher, resampelt mit dauerhaftem Konverterzustand und mittelt standardmäßig alle Eingangskanäle. Verwende .first oder .select([indices]), wenn das Kanalrouting bekannt ist.

StreamingRecognitionModel und StreamingRecognitionSession stellen eine gemeinsame, Cache-erhaltende inkrementelle API für Parakeet Streaming und Nemotron Core ML/MLX bereit:

let source = AudioFileLoader.stream(
    url: inputURL,
    options: AudioFileStreamOptions(targetSampleRate: 16_000,
                                    chunkDuration: 0.32,
                                    channelSelection: .mixAll))
let session = try model.makeStreamingSession(language: "en-US")

for try await chunk in source {
    for update in try session.push(chunk) {
        render(update.text, final: update.isFinal)
    }
}
for update in try session.finish() {
    render(update.text, final: true)
}

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 speech-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/speech-server --port 8080

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

REST-Endpunkte

EndpunktMethodeAnfrageAntwort
/transcribePOSTaudio/wav-BodyJSON { text } (Qwen3-ASR)
/v1/audio/transcriptionsPOSTMultipart-Formular { file, model, response_format?, language?, prompt?, temperature? }JSON, Text, ausführliches JSON, SRT oder VTT
/v1/audio/speechPOSTJSON { model, input, voice, response_format?, speed? }audio/wav- oder audio/pcm-Body
/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)

/v1/audio/speech ordnet Standardnamen für TTS-Modelle und Stimmen lokalen Engines zu. Standardmäßig wird WAV ausgegeben; response_format: "pcm" liefert headerloses 24-kHz-Mono-Audio als PCM16 Little Endian. Nicht unterstützte komprimierte Formate werden ausdrücklich abgelehnt.

/v1/audio/transcriptions akzeptiert eine WAV-Datei als Multipart-Formulardaten und gibt standardmäßig {"text":"..."} zurück; über response_format lassen sich auch Text, ausführliches JSON, SRT oder VTT anfordern. Auf Apple wählt der Modellalias eine lokale ASR-Engine. Der paketierte speech-core-Server unter Linux und Windows verwendet verzögert geladenes Parakeet, akzeptiert PCM16/24/32- oder Float32-WAV und begrenzt Uploads auf 25 MiB und 10 Minuten.

# 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

# OpenAI-compatible transcription
curl http://localhost:8080/v1/audio/transcriptions \
  -F "[email protected];type=audio/wav" \
  -F "model=whisper-1"

# OpenAI-compatible speech synthesis
curl http://localhost:8080/v1/audio/speech \
  -H "Content-Type: application/json" \
  -d '{"model":"tts-1","voice":"alloy","input":"Hello world","response_format":"wav"}' \
  -o openai-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.

Bei kalten Modellstarts oder langen Generierungen sendet der Server etwa alle 15 Sekunden leichtgewichtige realtime.keepalive-JSON-Events und websocket-pong-Kontrollframes, bis Modellausgabe bereit ist. Clients koennen diese Events ignorieren oder als Aktivitaetsanzeige verwenden.

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

Automatische Turns mit Server-VAD

Manuelles input_audio_buffer.commit bleibt die Voreinstellung. Setze session.turn_detection auf server_vad, damit Silero VAD auf dem Server jeden abgeschlossenen Sprach-Turn automatisch transkribiert. Präfix-Audio bleibt erhalten, der Leerlaufpuffer ist begrenzt, die Ereignisse speech_started, speech_stopped und committed werden der Reihe nach gesendet und ein dauerhaft aktiver Turn wird am konfigurierten Maximum beendet.

ws.send(JSON.stringify({
  type: "session.update",
  session: {
    turn_detection: {
      type: "server_vad",
      threshold: 0.5,
      prefix_padding_ms: 300,
      silence_duration_ms: 500,
      max_turn_duration_ms: 120000
    }
  }
}));
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.

Download-Wiederholungen

Modelllader und Download-APIs akzeptieren downloadOptions: DownloadOptions. Vor jeder Wartezeit erhält onRetry ein DownloadRetryEvent mit modelId, attempt, maxAttempts, dem ursprünglichen error und retryInSeconds. nil bedeutet, dass keine Wiederholungen verbleiben; ein vollständiger Cache kann das Laden weiterhin ermöglichen.

Callbacks sind synchron und @Sendable; die Ausführung auf dem Main Actor ist nicht garantiert. Ein Abbruch erzeugt kein Wiederholungsereignis. Mit retryDelaysSeconds lässt sich die Strategie anpassen ([] bedeutet einen Versuch); das explizite Argument der Download-API hat Vorrang. Die voreingestellte Anzahl der Versuche und der Byte-Fortschritt bleiben erhalten. Fehlversuche werden zusätzlich über AudioLog.download auf Notice-Ebene protokolliert.

Kooperative Abbruchbehandlung

Bei der MLX-Transkription wirft Qwen3ASRModel.transcribeCheckingCancellation(audio:sampleRate:options:) einen CancellationError, wenn vor der Merkmalsextraktion, der Kodierung, dem Decoder-Prefill oder einem Decodierschritt ein Task-Abbruch erkannt wird. Bereits an die GPU übergebene Arbeit kann noch abgeschlossen werden. Die synchronen APIs transcribe und transcribeBatch bleiben nicht werfend und ignorieren Task-Abbrüche. speech-server nutzt für Qwen3-ASR den Einstiegspunkt mit Abbruchprüfung.

MOSS-TTS-Nano

MossTTSNanoModel serialisiert Anfragen und erzeugt pro Äußerung einen neuen CoreML-Zustand. synthesize und synthesizeStream erhalten Stereo; SpeechGenerationModel liefert eine Mono-Mischung. language: nil übergeben. Streams enden mit einer leeren Abschlussmarkierung.

import AudioCommon
import MossTTSNano

let model = try await MossTTSNanoModel.fromPretrained()
let reference = try await model.encodeReference(url: referenceURL)
let result = try await model.synthesize(
    text: "The weather is nice today.", reference: reference,
    options: MossTTSNanoOptions(maxFrames: 300, seed: 1))
try WAVWriter.writeStereo(left: result.left, right: result.right,
    sampleRate: 48000, to: outputURL)

MOSS-TTS-Nano

Optionale Modellverwaltung

SpeechRuntime bietet gemeinsames Laden, abbrechbare FIFO-Zulassung und serielle Ausführung. Qwen3ASRRuntime wählt MLX oder vollständiges CoreML über explizite Deskriptoren; native APIs und CLI-Optionen bleiben verfügbar.

Der Server hält eine Ausführungsfreigabe während der gesamten Anfrage einschließlich Streaming-Ausgabe und lässt bis zu 32 Anfragen warten. ASR-Fehler ergeben HTTP-Fehler oder WebSocket-Fehlerereignisse; gültige leere Transkripte bleiben erfolgreich. Die Wiedergabe behält überzählige Samples. Modelle im Cache und wartende Audiodaten belegen weiterhin Speicher.

import Qwen3ASRRuntime

let runtime = Qwen3ASRRuntime(descriptor: Qwen3ASRDescriptors.small)
let text = try await runtime.transcribe(audio: samples, sampleRate: 16000)
try await runtime.unload()

Voxtral TTS

Voxtral 4B TTS 2603 läuft über das Modul VoxtralTTS lokal auf Apple Silicon: 20 voreingestellte Stimmen, neun Sprachen und Mono-Sprache mit 24 kHz.

import VoxtralTTS

let model = try await VoxtralTTSModel.fromPretrained()
for try await chunk in model.generateStream(text: "Hello, welcome to Voxtral.", voice: "casual_male") {
    print(chunk.frameIndex, chunk.samples.count, chunk.isFinal)
}

Nur vorgegebene Stimmen; das Klonen anhand von Referenzaudio ist nicht verfügbar. SDK, CLI, WebSocket und HTTP PCM geben Audio während der Erzeugung aus. Der Eingabetext muss zu Beginn vollständig vorliegen. Die Modellgewichte stehen unter CC BY-NC 4.0.

import AudioCommon

let player = StreamingAudioPlayer()
player.skipInitialSilence = false

Voxtral-TTS-Anleitung lesen