Tłumaczenie automatyczne · Oryginał angielski

API i protokoły

Native Apple runtime · PockettTS

PocketTTS dodaje rodzimą syntezę mowy CoreML do speech-swift. Wszystkie etapy domyślne do wykonania tylko CPU. angielskie strumienie Alba preset 1,920- próbki w 24 kHz (80 ms per frame).

Wykorzystanie Swift i 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)
}

W AudioCommon moduł definiuje protokoły model- agnostyczne i typy współdzielone. Każdy model zgodny może być stosowany zamiennie przez te interfejsy.

Przegląd protokołu

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

Mowa o uznaniu modelu

Protokół dotyczący modeli przemówień i tekstów.

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
}

Konfigurowanie typów: Qwen3ASRModel, WhisperASRModel, ParakeetASRModel, ParakeetStreamingASRModel, OmnilingualASRModel (CoreML), OmnilingualASRMLXModel (MLX)

Mowa GenerationModel

Protokół dla modeli tekstowych do mowy.

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() ma domyślną implementację, która zawija generate() jako pojedynczy kawałek. Modele z prawdziwym strumieniowaniem (np. Qwen3-TTS) go nadrabiają.

Konfigurowanie typów: Qwen3TTSModel, CosyVoiceTTSModel, VoxCPM2TTSModel, KokoroTTSModel, IndexTTS2TTSModel

IndexTTS2TTSModel dodaje reference- audio generate przeciążenie do klonowania głosu zeroshot i zastosowań IndexTTS2SynthesisOptions dla głośnomówiących i pauzy kontroli.

Lokalne ładowanie Qwen3-TTS

fromLocal walidates wymagane pliki i kompletność odłamka kontrolnego, a następnie wywodzi rozmiar modelu, kwantyfikacji i architektury z config.json. Opcjonalnie configuration wartość jest tylko zwrotem dla pominiętych metadanych; błędne lub sprzeczne metadane rzuca Qwen3TTSLoadingError.invalidConfiguration przed przydzieleniem modelu.

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
Polityka pamięci

fromLocal domyślne .none, więc nie zmienia limitu pamięci przewodowej Metal. fromPretrained zachowuje swoje .pin(fraction: 0.9) Domyślny. Oba wstępnie przeszkoleni ładowarki akceptują oddzielny tokenizerCacheDir.

ForcedAlignmentModel

Protokół dotyczący ustawiania znaczników czasu na poziomie słownym.

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

Mowa ToSpeechModel

Protokół w sprawie modeli dialogu mowy.

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

Konfigurowanie typów: PersonaPlexModel

HibikiTranslateModel

Sterownik tłumaczenia mowy-to@-@ mowy. Nie odpowiada SpeechToSpeechModel ponieważ umowa jest inna: wejście i wyjście są różnymi językami, długość wyjścia jest zmienna (EOS- driven, capped at max(tSrc * 5/2, tSrc + 20) kroki - mniej więcej 1.0- 1.6 × czas trwania źródła na klipach w stylu FLEURS-), a obok dźwięku znajduje się równoległy strumień text- token. Pełny przewodnik →

public final class HibikiTranslateModel {
    public let cfg: HibikiConfig
    public static var defaultModelId: String { "aufklarer/Hibiki-Zero-3B-MLX-4bit" }
    public static var modelId8bit: String  { "aufklarer/Hibiki-Zero-3B-MLX-8bit" }

    public static func fromPretrained(
        modelId: String = defaultModelId,
        cacheDir: URL? = nil,
        offlineMode: Bool = false,
        progressHandler: ((Double, String) -> Void)? = nil
    ) async throws -> HibikiTranslateModel

    public func warmUp()

    public func translate(
        sourceAudio: [Float],
        sourceLanguage: HibikiSourceLanguage = .fr,
        verbose: Bool = false
    ) -> (audio: [Float], textTokens: [Int32])

    public func translateStream(
        sourceAudio: [Float],
        sourceLanguage: HibikiSourceLanguage = .fr,
        streaming: HibikiStreamingConfig = .default,
        verbose: Bool = false
    ) -> AsyncThrowingStream<AudioChunk, Error>
}

public enum HibikiSourceLanguage: String, CaseIterable, Sendable, Codable {
    case fr, es, pt, de
}

Szybkość próbkowania: 24 kHz mono, zarówno wejście jak i wyjście. Wskaźnik ramki: 12.5 Hz (kodek Mimi). Języki źródłowe: FR / ES / PT / DE. Język docelowy: PL. Ustawienie HIBIKI_GREEDY=1 przy uruchomieniu procesu deterministycznego dekodowania argmax (pasuje do kanarków CI).

VoiceActivityDetectionModel

Protokół dotyczący wykrywania aktywności głosowej.

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

TurnCompletionProvider

Protokół dla klasyfikatorów typu end-of@-@ turn, z którym skonsultowano się StreamingVADProcessor w każdym potwierdzonym VAD A VAD tylko słyszy ciszę; model return-completion słucha prosodii całej wypowiedzi, więc pauza w połowie zdania trzyma agenta czekającego, podczas gdy kończy zdanie otrzymuje natychmiastową odpowiedź. speech-corejest sc_turn_completion_vtable_tPatrz Przewodnik Smart Turn.

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
}

Konfigurowanie typów: SmartTurnModel (Smart Turn v3.2)

Speaker EmbeddingModel

Protokół dotyczący ekstrakcji głośników.

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

Konfigurowanie typów: WeSpeakerModel

SpeakerDiarizationModel

Protokół dla modeli diaryzacji głośników, które przypisują etykiety głośników segmentom audio.

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

Konfigurowanie typów: DiarizationPipeline (Pyannote), SortformerDiarizer

SpeakerExtractionCapable

Rozszerzony protokół diaryzacji dla silników, które obsługują ekstrakcję segmentów głośnika docelowego za pomocą wzorcowego osadzania. Nie wszystkie silniki wspierają to (Sortformer jest end-to@-@ end i nie produkuje osadzania głośników).

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

Konfigurowanie typów: DiarizationPipeline (Tylko Pyannote)

Współdzielone typy

AudioChunk

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

Przemówienie

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

AlignedWordName

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

Parsowany segment tekstu dialogu wielogłośnikowego z opcjonalnymi znacznikami głośnika i emocji. DialogueParser oraz DialogueSynthesizer dla syntezy dialogu CosyVoice3.

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

Tekst wielogłośnikowego dialogu z tagami głośników ([S1]) i znaczniki emocji ((happy)).

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

Built- in emotions: happy/excited, sad, angry, whispers/whispering, laughs/laughing, calm, surprised, serious. Nieznane znaczniki przechodzą jako instrukcje freeform.

Dialogue Synthesizer

Organizuje wielosegmentową syntezę dialogu z klonowaniem głosu przemówcy, lukami milczącymi i przemijającym.

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

Dialogue SynthesisConfig

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

Protokół integracji modelu językowego z rurociągami głosowymi. LLM do VoicePipeline ASR → LLM → TTS Przepływ.

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

Adapter wbudowany: Qwen3PipelineLLM mosty Qwen35MLXChat do tego protokołu z tokenami czyszczenie, anulowanie, i oczekujących na nagromadzenie wyrażenia.

AudioIO

Wielofunkcyjny menedżer audio I / O, który eliminuje płytkę magnetyczną AVAudioEngine. Obsługuje mikrofony, przełączanie, odtwarzanie i pomiar poziomu dźwięku.

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

AudioIO zawiera StreamingAudioPlayer dla wyjścia TTS i AudioRingBuffer do bezpiecznego niwelowania transmisji audio pomiędzy gwintem przechwytywania i inferencji.

SystemAudioTap

Odtwarza mix wyjściowy systemu - co obecnie gra Mac - jako mono Float32 przy częstotliwości próbkowania wybranej przez telefon (krany procesowe Core Audio, macOS 14.4+). Domyślny, globalny kran mononowy wyłącza bieżący proces, tak więc odtwarzanie własnej aplikacji nigdy nie jest ponownie przechwytywane i jest owijane w prywatne urządzenie agregatowe zawierające tylko kran.

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

Przechwytywanie przetrwa wyłączniki default- output- device - wewnętrzny resampler jest przebudowywany, gdy zmienia się szybkość mieszania. Aplikacja hosta musi zadeklarować NSAudioCaptureUsageDescriptionJeśli pozwolenie na nagrywanie nie zostanie wydane, można jeszcze utworzyć kran, który zapewni tylko ciszę: zegarek framesCaptured rosną podczas nonSilentFrames pozostaje 0 i nie powiodło się.

Terminowe źródła niezależne

Obie klasy przechwytywania ujawniają przekroczenie czasu. CapturedAudioChunk ma mono samples at sampleRate plus opcjonalny Mach hostTime dla pierwszej ramki wejściowej. Ponieważ oba źródła używają tego samego zegara hosta, aplikacja może zachować swoje PCM oddzielone i zamówić otrzymane zdarzenia transkrypcyjne bez mieszania dźwięku.

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

Tylko dla słuchaczy, enableAEC: true aktywuje przetwarzanie głosu Apple przed odczytem formatu mikrofonu, oraz enablePlayback: false pomija nieużywany odtwarzacz. Na macOS odtwarzanie urządzenia jest usuwane z próbek mikrofonów i brak uruchomienia AEC jest zgłaszany zamiast wycofywania się do surowego wejścia.

Incremental file input and decognition

CapturedAudioChunk posiada również monotonię frameIndex oraz isFinal marker. AudioFileLoader.stream produkuje te kawałki na żądanie z ograniczoną pamięcią, resempuje raz z trwałym stanem konwertera i domyślnie średnio wszystkie kanały wejściowe. Użyj .first lub .select([indices]) gdy wiadomo o routingu kanału.

StreamingRecognitionModel oraz StreamingRecognitionSession zapewnić jeden zapasowy zapasowy API w całym strumieniu Parakeet i Nemotron Core ML / MLX:

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

Wspólny czytnik protobuf dla SentencePiece .model pliki, mieszka w AudioCommon. Każdy moduł, który musi dekodować elementy SentencePiece (PersonaPlex, OmnilingualaSR, przyszłe porty ASR / TTS) buduje swój własny dekoder na górze tego pojedynczego czytnika zamiast reimplementowania formatu drutu protobuf.

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
}

Używane przez: OmnilingualASR.OmnilingualVocabulary, PersonaPlex.SentencePieceDecoder. Objęte badaniami jednostkowymi 7 w Tests/AudioCommonTests/SentencePieceModelTests.

MLXEY.SDPA

Skalowane dot-produkt pomocników uwagi wspólne w każdym MLX moduł uwagi (Qwen3-ASR / Qwen3-TTS / Qwen3-Chat / CosyVoice / PersonaPlex / OmnilingualaASR). Każdy moduł zachowuje swoje własne projekcje - SDPA tylko obsługuje reshape → uwaga → połączenie płyty 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
}

Wszystkie rozmowy przeobraźliwe korzystają -1 dla wymiaru wsadowego, więc pomocnicy składają się z MLX.compile(shapeless:) wykresy, które różnią się partią w czasie biegu (np. Qwen3-TTS Talker autoregresywny dekod).

Serwer HTTP API

W speech-server binarne eksponuje każdy model w speech-swift jako punkty końcowe HTTP REST plus punkt końcowy WebSocket, który implementuje OpenAI Realtime API. Modele są ładowane lazily na pierwsze życzenie; przejść --preload Ogrzewać je wszystkie przy starcie.

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

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

Punkty końcowe REST

Punkt końcowyMetodaWniosekOdpowiedź
/transcribePOSTaudio/wav ciałoJSON { text } (Qwen3-ASR)
/v1/audio/transcriptionsPOSTWieloczęściowy { file, model, response_format?, language?, prompt?, temperature? }JSON, tekst, verbose JSON lub VTT
/v1/audio/speechPOSTJSON { model, input, voice, response_format?, speed? }audio/wav lub audio/pcm ciało
/speakPOSTJSON { text, engine?, language?, voice? }audio/wav korpus (Qwen3-TTS, CosyVoice i Kokoro)
/respondPOSTaudio/wav ciałoaudio/wav PersonaPlex)
/enhancePOSTaudio/wav ciałoaudio/wav DeepFilterNet3)
/vadPOSTaudio/wav ciałoLista segmentów JSON
/diarizePOSTaudio/wav ciałoJSON DiarizedSegment wykaz
/embed-speakerPOSTaudio/wav ciałoJSON [Float] 256- dim

/v1/audio/speech maps standard TTS model i nazwy głosowe do lokalnych silników. Domyślnie WAV; response_format: "pcm" zwraca beznagłówkowy dźwięk 24 kHz mono PCM16 little- endian. Nieobsługiwane skompresowane formaty są wyraźnie odrzucane.

/v1/audio/transcriptions akceptuje jeden plik WAV jako wieloczęściowy plik tworzący dane i domyślnie {"text":"..."}; response_format może również poprosić tekst, verbose JSON, SRT, lub VTT. Na Apple, aliasy modelu wybrać lokalne ASR silnik. Opakowanie speech-core serwer włączony Linux oraz Windows wykorzystuje lazily loaded Parakeet, akceptuje PCM16/24/32 lub Float32 WAV, i zatyczki do wysyłania w 25 MiB oraz 10 Minuty.

# 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)

WebSocket ws://host:port/v1/realtime implementuje OpenAI Protokół Realtime. Wszystkie wiadomości są JSON z type Dyskryminator; ładunki audio są base64-encoded PCM16 w 24 kHz mono.

Podczas obciążenia modelu zimnego lub długich generacji, serwer emituje lekkie realtime.keepalive Wydarzenia JSON i ramki sterowania pong websocket w przybliżeniu co 15 sekund do momentu, aż model będzie gotowy. Klienci mogą ignorować te zdarzenia lub traktować je jako wskaźniki aktywności.

Klient → Zdarzenia serwera

ZdarzenieCel
session.updateKonfiguracja silnika, języka, głosu, formatu audio i opcjonalnego serwera VAD
input_audio_buffer.appendDołącz element base64 PCM16 do bufora wejściowego
input_audio_buffer.commitWywołaj buforowany dźwięk do transkrypcji
input_audio_buffer.clearWyrzuć bieżący bufor wejściowy
response.createTTS synteza dla dostarczonego tekstu / instrukcji

Serwer → Wydarzenia klientów

ZdarzenieZnaczenie
session.createdUścisk dłoni kompletny, domyślny edytowany config
session.updatedNajnowsze session.update potwierdzony
input_audio_buffer.speech_startedSerwer VAD wykrył początek obrotu mowy
input_audio_buffer.speech_stoppedSerwer VAD zakończył zakręt po cichu lub limitu czasu trwania
input_audio_buffer.committedAudio akceptowane i w kolejce do transkrypcji
conversation.item.input_audio_transcription.completedWynik ASR z końcowym tekstem transkrypcji
response.audio.deltaBase64 PCM16 kawałek zsyntetyzowanego dźwięku
response.audio.doneBrak fragmentów audio dla tej odpowiedzi
response.doneOdpowiedź finalizowana (metadane + statystyki dotyczące opóźnień)
errorKoperta błędów z type oraz message

Automatyczne zakręty z serwerem VAD

Wyrażenie input_audio_buffer.commit pozostaje domyślny. Aby serwer został zamknięty i przepisywany automatycznie z Silero VAD, skonfigurować turn_detection. Serwer przechowuje tylko wymagane prerolla podczas bezczynności, emituje mowę start / stop i commit events w porządku, a force- zamyka ciągły aktywny obrót na max_turn_duration_ms.

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
    }
  }
}));

// Disable automatic turns and return to explicit commits:
// { type: 'session.update', session: { turn_detection: null } }
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

Serwer mieszka w AudioServer Produkt SPM. Przykładowy klient przeglądarki jest wysyłany na Examples/websocket-client.html - otworzyć go obok uruchomionego serwera, aby prowadzić pełną podróż w obie strony ASR + TTS.

Pobieranie modeli

Wszystkie modele są pobierane z HuggingFace przy pierwszym użyciu i buforowane w ~/Library/Caches/qwen3-speech/. AudioCommon moduł zapewnia współdzielone HuggingFaceDownloader który obsługuje pobieranie, buforowanie i weryfikację integralności.

Pobieranie powtórzeń

Ładowarki modelu i pobrać API akceptuj downloadOptions: DownloadOptions. onRetry otrzymuje DownloadRetryEvent przed każdym wycofaniem, modelId, attempt, maxAttempts, oryginał errororaz retryInSeconds. nil oznacza, że nie ma powtórzeń; zapas pamięci podręcznej może się jeszcze udać.

Załączenia są synchroniczne i @Sendable, bez gwarancji głównego aktora. Anulowanie nie powoduje zdarzenia retry. retryDelaysSeconds dostosować politykę ([] oznacza jedną próbę); pierwszeństwo ma wyraźny argument pobierania API. Domyślna liczba prób i postęp bajtów są zachowane. AudioLog.download.

Anulowanie współpracy

dla transkrypcji MLX, Qwen3ASRModel.transcribeCheckingCancellation(audio:sampleRate:options:) rzuty CancellationError gdy anulowanie zadania jest obserwowane przed wydobyciem funkcji, kodowaniem, przedrostkiem dekodera lub krokiem dekodującym. GPU prace już złożone może zakończyć. Synchroniczne transcribe oraz transcribeBatch API zachowują swoje nierzucające zachowanie i ignorują anulowanie zadania. speech-server używa punktu wejścia dla Qwen3-ASR, który jest świadomy anulowania.

MOSS- TTS- Nano

MossTTSNANOModel serializuje żądania i tworzy świeży stan CoreML dla każdego wyniku. syntetyzuj i syntetyzuj Stream zachować stereo; SpeechGenerationModel zwraca mono downmix. Pass language: zero. Streams kończy się z pustym znacznikiem końcowym.

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

Lokalne zgłoszenie rodzime

Dla 12B, powtarzające się przyległe znaczniki zamykające wywołanie narzędzi natychmiast po zakończeniu, zwalidowanego połączenia są konsumowane z tym wywołaniem. Nie tworzą one innego wywołania ani nie zmieniają jego argumentów. Markery standalone i zniekształcone argumenty pozostają błędami; E4B zachowuje ścisłą obsługę.

Aby zachować nieudany natywny chat, ustaw failureCaptureDirectory w manifeście referencyjnym. Domyślnie jest wyłączona przechwytywanie. Katalog musi odmówić dostępu grupowego i innego; nowe katalogi używają 0700 i plików błędów używać 0600. Pliki zawierają dokładny organ żądania, ID wniosku i błąd. Ukończone generacje 12B zachowują również renderowany sygnał, token ID, wyjściowe i stop przed parsowaniem. Niepowodzenia E4B nie mają surowego zapisu generacji. Te pliki lokalne zawierają zawartość rozmowy i narzędzia; błędy HTTP i udane żądania nie zawierają ich.

Gemma4UnifiedChat.generateResponse akceptuje fakultatywny onGeneration z Gemma4GenerationTrace. Wywołanie działa przed parsowaniem narzędzia, w tym po odrzuceniu jego zakończenia. Nagrywanie nie zmienia pobierania próbek lub zaakceptowanej składni narzędzia.

Fakultatywny agentDirectory ładuje ustawione Gemma 4 12B Unifikacja INT4 model tekstowy obok E4B. Użyj modelu: "gemma4-12b-it"dla czatu z natywnymi narzędziami, historii wyników i anulowania. E4B pozostaje dostępna dla innych wniosków. Gemma4UnifiedChat wykorzystuje przypinane MLX implementacja tekstu i uwalnia każdy podręcznik KV. Parsing odpowiedzi usuwa separatory kanałów z zwróconego tekstu. Osierocone terminatory wywołania narzędzi są odrzucane jako nieprawidłowo utworzone odpowiedzi. Gemma 4 12B, zawartość wiadomości narzędziowej zachowuje JSON jako tekst, dopasowując szablon czatu punktu kontrolnego.

Budowy źródeł akceptują --inference-manifest models.json do wczytania ustawione Gemma 4 E4B INT4, VoxCPM2, pełny Whisper Large-v3oraz Silero VADUsługa ta rozpoczyna się w trybie offline i odrzuca niekompatybilne tożsamości modelu.

VoxCPM2 obsługuje ważenie Precision: .float16 dla niezliczonego punktu kontrolnego. Główne parametry tego punktu wykorzystują FP16; AudioVAE i niektóre obliczenia pozostają FP32. Punkty kontrolne INT8 są odrzucane dla tego trybu.

Gemma4ConversationMessage, Gemma4Tool i generateResponse zachować rodzime deklaracje narzędzi, argumenty, i historię wyników. InferenceCancellationToken anuluje generację kooperacyjnie. Silero makeindependent Stream izoluje stan, a procesChunkThrowing propaguje błędy wnioskowania.

Whisper aSRModel.from Ładunki katalogowe wystawione pełne pakiety Large-v3 CoreML. Dekoder z warstwy 32- może zbudować przedrostek bez TextDecoderContextPreferill.mlmodelc i uwalnia perstep temporaria. Domyślny model Whisper CLI pozostaje Turbo.

Native service serializes chat, synteza i transkrypcja; lekki VAD ma oddzielny stan strumienia. Gemma prefill używa serii 256- token, żądanie stanu KV jest zwolniony, a pamięci podręcznej MLX jest ograniczona do 128 MiB z limitem alokatora 20 GiB.

Przemowa ze strumieniem: True zwraca zdarzenia audio NDJSON w 48 kHz, po którym następuje wykonanie lub błąd. Anuluj requestID na /v1/ wniosek / anulować. GET /v1/ wnioskowanie / możliwości i /v1/ inference / prience report model identifies, parametr dtypes, asset pin, and memory counters.

speech-server --inference-manifest models.json --port 8080
POST /v1/inference
{"operation":"chat","model":"gemma4-e4b-it","requestID":"chat-1","messages":[{"role":"user","content":"Hello"}],"maxTokens":128,"temperature":0}

chat: gemma4-e4b-it
speech: voxcpm2
transcribe: whisper-large-v3
vad: silero-vad

Audio: audioBase64 (mono PCM16), sampleRate
VAD: streamID, reset, endStream; 512 samples/frame at 16000 Hz
Tools: name, description, parameters
Tool call: id, name, arguments
Tool result: role="tool", toolCallID, content

Opcjonalny model własności

SpeechRuntime zapewnia wspólne ładowanie, anulowalne wstęp FIFO i seryjne wykonanie. Qwen3ASRRuntime wybiera MLX lub pełne CoreML za pomocą wyraźnych deskryptorów; krajowe API i flagi CLI pozostają dostępne.

Serwer audio posiada jedno zezwolenie na wykonanie za pomocą każdego pełnego żądania, w tym wyjścia strumieniowego, z żądaniami do 32 oczekujących. Niepowodzenia ASR powodują błędy HTTP lub zdarzenia błędów WebSocket; prawidłowe puste transkrypcje pozostają skuteczne. Odtwarzanie zachowuje próbki przepełnienia.

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 działa lokalnie Apple Silicon przez moduł Voxtral TTS: 20 preset voices, nine languages, and 24 KHz mono mowy.

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

Wyłącznie głosy preset; klonowanie referencji audio jest niedostępne. Strumienie wyjściowe audio przez SDK, CLI, WebSocket i HTTP PCM. Dostarczają kompletny tekst wejściowy z przodu. Wagi modelu używają CC BY- NC 4.0.

import AudioCommon

let player = StreamingAudioPlayer()
player.skipInitialSilence = false

Przeczytaj przewodnik Voxtral TTS