API ve Protokoller

AudioCommon modülü, modelden bağımsız protokolleri ve ortak tipleri tanımlar. Bu arayüzleri uygulayan herhangi bir model, birbirinin yerine kullanılabilir.

Protokol Genel Bakışı

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

SpeechRecognitionModel

Konuşmadan metne modelleri için protokol.

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
}

Uygulayan tipler: Qwen3ASRModel, WhisperASRModel, ParakeetASRModel, ParakeetStreamingASRModel, OmnilingualASRModel (CoreML), OmnilingualASRMLXModel (MLX)

SpeechGenerationModel

Metinden konuşmaya modelleri için protokol.

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(), generate()'i tek bir parça olarak saran varsayılan bir uygulamaya sahiptir. Gerçek streaming'e sahip modeller (örn. Qwen3-TTS) bunu geçersiz kılar.

Uygulayan tipler: Qwen3TTSModel, CosyVoiceTTSModel, VoxCPM2TTSModel, KokoroTTSModel, IndexTTS2TTSModel

IndexTTS2TTSModel, zero-shot ses klonlama için referans audio alan bir generate overload ekler ve konuşma hızı ile duraklama kontrolleri için IndexTTS2SynthesisOptions kullanır.

ForcedAlignmentModel

Sözcük düzeyinde zaman damgası hizalaması için protokol.

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

SpeechToSpeechModel

Konuşmadan konuşmaya diyalog modelleri için protokol.

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

Uygulayan tipler: PersonaPlexModel

VoiceActivityDetectionModel

Ses etkinliği algılama için protokol.

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

SpeakerEmbeddingModel

Konuşmacı embedding çıkarımı için protokol.

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

Uygulayan tipler: WeSpeakerModel

SpeakerDiarizationModel

Ses segmentlerine konuşmacı etiketleri atayan konuşmacı ayrıştırma modelleri için protokol.

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

Uygulayan tipler: DiarizationPipeline (Pyannote), SortformerDiarizer

SpeakerExtractionCapable

Bir referans embedding kullanarak hedef konuşmacının segmentlerini çıkarmayı destekleyen motorlar için genişletilmiş ayrıştırma protokolü. Tüm motorlar bunu desteklemez (Sortformer uçtan uca çalışır ve konuşmacı embedding'leri üretmez).

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

Uygulayan tipler: DiarizationPipeline (yalnızca Pyannote)

Ortak Tipler

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

İsteğe bağlı konuşmacı ve duygu etiketleriyle çok konuşmacılı diyalog metninin ayrıştırılmış bir segmenti. CosyVoice3 diyalog sentezi için DialogueParser ve DialogueSynthesizer ile birlikte kullanılır.

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

Satır içi konuşmacı etiketleri ([S1]) ve duygu etiketleri ((happy)) ile çok konuşmacılı diyalog metnini ayrıştırır.

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

Yerleşik duygular: happy/excited, sad, angry, whispers/whispering, laughs/laughing, calm, surprised, serious. Bilinmeyen etiketler serbest biçimli yönergeler olarak doğrudan geçer.

DialogueSynthesizer

Konuşmacı başına ses klonlama, sessizlik boşlukları ve crossfade ile çok segmentli diyalog sentezini düzenler.

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

Ses pipeline'larıyla dil modeli entegrasyonu için protokol. Bir LLM'i VoicePipeline'ın ASR → LLM → TTS akışına köprüler.

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

Yerleşik adaptör: Qwen3PipelineLLM, token temizleme, iptal ve bekleyen ifade biriktirme ile Qwen35MLXChat'i bu protokole köprüler.

AudioIO

AVAudioEngine boilerplate'ini ortadan kaldıran yeniden kullanılabilir ses I/O yöneticisi. Mikrofon yakalama, yeniden örnekleme, oynatma ve ses seviyesi ölçümünü yönetir.

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

AudioIO, TTS çıktısı için bir StreamingAudioPlayer ve yakalama ile çıkarım iş parçacıkları arasında iş parçacığı güvenli ses aktarımı için bir AudioRingBuffer içerir.

SystemAudioTap

Sistem çıkış miksini — Mac'in şu anda çaldığı sesi — çağıranın seçtiği örnekleme hızında mono Float32 olarak yakalar (Core Audio process tap'leri, macOS 14.4+). Küresel mono tap varsayılan olarak geçerli süreci dışlar; böylece uygulamanın kendi çalması asla yeniden yakalanmaz ve tap, yalnızca tap'i içeren özel bir birleşik aygıta sarılır.

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

Yakalama, varsayılan çıkış aygıtı değişse de sürer — miks hızı değiştiğinde iç yeniden örnekleyici yeniden kurulur. Ana uygulama NSAudioCaptureUsageDescription bildirmelidir. Kayıt izni reddedilirse tap yine oluşturulabilir ama yalnızca sessizlik verir: framesCaptured artarken nonSilentFrames 0'da kalıyorsa açıkça başarısız olun.

Zaman damgalı bağımsız kaynaklar

Her iki yakalama sınıfı da zaman damgalı aşırı yüklemeler sunar. CapturedAudioChunk, sampleRate hızında mono samples ve ilk giriş karesi için isteğe bağlı bir Mach hostTime taşır. İki kaynak aynı ana bilgisayar saatini kullandığından uygulama PCM'lerini ayrı tutabilir ve türetilen transkript olaylarını sesi karıştırmadan sıralayabilir.

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

Yalnızca dinleme amaçlı yakalamada enableAEC: true, mikrofon biçimi okunmadan önce Apple Voice Processing’i etkinleştirir; enablePlayback: false ise kullanılmayan oynatıcıyı eklemez. macOS’ta cihazın çaldığı ses mikrofon örneklerinden çıkarılır; AEC başlatılamazsa ham girişe geri dönmek yerine hata bildirilir.

SentencePieceModel

SentencePiece .model dosyaları için AudioCommon içinde bulunan ortak protobuf okuyucusu. SentencePiece parçalarını çözmesi gereken her modül (PersonaPlex, OmnilingualASR, gelecekteki ASR / TTS portları) protobuf wire formatını yeniden uygulamak yerine kendi kod çözücüsünü bu tek okuyucunun üzerine kurar.

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
}

Kullananlar: OmnilingualASR.OmnilingualVocabulary, PersonaPlex.SentencePieceDecoder. Tests/AudioCommonTests/SentencePieceModelTests içinde 7 birim testiyle kapsanır.

MLXCommon.SDPA

Her MLX dikkat modülünde (Qwen3-ASR / Qwen3-TTS / Qwen3-Chat / CosyVoice / PersonaPlex / OmnilingualASR) paylaşılan scaled dot-product attention yardımcıları. Her modül kendi projeksiyonlarını korur — SDPA yalnızca reshape → attention → merge boilerplate'ini yönetir.

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
}

Tüm reshape çağrıları batch boyutu için -1 kullanır; böylece yardımcılar, çalışma zamanında batch'i değişen MLX.compile(shapeless:) grafikleriyle (örn. Qwen3-TTS Talker otoregresif kod çözme) birleşebilir.

HTTP API Sunucusu

speech-server ikili dosyası, speech-swift içindeki her modeli HTTP REST uç noktaları olarak ve OpenAI Realtime API'sini uygulayan bir WebSocket uç noktası olarak sunar. Modeller ilk istekte tembel olarak yüklenir; başlangıçta hepsini ısıtmak için --preload geçirin.

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

# Başlangıçta her modeli önceden yükle
.build/release/speech-server --port 8080 --preload

REST Uç Noktaları

Uç NoktaYöntemİstekYanıt
/transcribePOSTaudio/wav gövdesiJSON { text } (Qwen3-ASR)
/v1/audio/transcriptionsPOSTMultipart form { file, model, response_format?, language?, prompt?, temperature? }JSON, metin, ayrıntılı JSON, SRT veya VTT
/v1/audio/speechPOSTJSON { model, input, voice, response_format?, speed? }audio/wav veya audio/pcm ses gövdesi
/speakPOSTJSON { text, engine?, language?, voice? }audio/wav gövdesi (Qwen3-TTS, CosyVoice, Kokoro)
/respondPOSTaudio/wav gövdesiaudio/wav gövdesi (PersonaPlex)
/enhancePOSTaudio/wav gövdesiaudio/wav gövdesi (DeepFilterNet3)
/vadPOSTaudio/wav gövdesiJSON segment listesi
/diarizePOSTaudio/wav gövdesiJSON DiarizedSegment listesi
/embed-speakerPOSTaudio/wav gövdesiJSON [Float] (256 boyut)

/v1/audio/speech, standart TTS model ve ses adlarını yerel motorlarla eşler. Varsayılan biçim WAV'dır; response_format: "pcm", başlıksız 24 kHz mono PCM16 little-endian ses döndürür. Desteklenmeyen sıkıştırılmış biçimler açıkça reddedilir.

/v1/audio/transcriptions, multipart form verisi olarak tek bir WAV dosyası alır ve varsayılan olarak {"text":"..."} döndürür; response_format ile metin, ayrıntılı JSON, SRT veya VTT de istenebilir. Apple'da model takma adları yerel bir ASR motoru seçer. Linux ve Windows için paketlenmiş speech-core sunucusu gecikmeli yüklenen Parakeet kullanır, PCM16/24/32 veya Float32 WAV kabul eder ve yüklemeleri 25 MiB ile 10 dakikayla sınırlar.

# Bir dosyayı transkribe et
curl -X POST http://localhost:8080/transcribe \
  --data-binary @recording.wav \
  -H "Content-Type: audio/wav"

# Konuşma sentezle
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

# Tam konuşmadan konuşmaya gidiş-dönüş
curl -X POST http://localhost:8080/respond \
  --data-binary @question.wav \
  -o response.wav

OpenAI Realtime API (/v1/realtime)

ws://host:port/v1/realtime'deki WebSocket uç noktası, OpenAI Realtime protokolünü uygular. Tüm mesajlar type ayırt edicisiyle JSON'dur; ses yükleri 24 kHz mono'da base64 ile kodlanmış PCM16'dır.

Soğuk model yüklemeleri veya uzun üretimler sırasında sunucu, model çıktısı hazır olana kadar yaklaşık her 15 saniyede bir hafif realtime.keepalive JSON olayları ve websocket pong kontrol çerçeveleri gönderir. İstemciler bu olayları yok sayabilir veya etkinlik göstergesi olarak kullanabilir.

İstemci → Sunucu olayları

OlayAmaç
session.updateMotor, dil, ses ve ses biçimini yapılandır
input_audio_buffer.appendGiriş tamponuna base64 PCM16 parçası ekle
input_audio_buffer.commitTamponlanmış sesi transkripsiyon için commit et
input_audio_buffer.clearMevcut giriş tamponunu iptal et
response.createVerilen metin/yönergeler için TTS sentezi iste

Sunucu → İstemci olayları

OlayAnlamı
session.createdEl sıkışma tamamlandı, varsayılan yapılandırma yayımlandı
session.updatedEn son session.update onaylandı
input_audio_buffer.committedSes kabul edildi ve transkripsiyon için kuyruğa alındı
conversation.item.input_audio_transcription.completedNihai transkript metniyle ASR sonucu
response.audio.deltaSentezlenen sesin base64 PCM16 parçası
response.audio.doneBu yanıt için artık ses parçası yok
response.doneYanıt tamamlandı (metadata + gecikme istatistikleri)
errortype ve message ile hata zarfı
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

Sunucu, AudioServer SPM ürününde bulunur. Examples/websocket-client.html'da örnek bir tarayıcı istemcisi sağlanır — tam ASR + TTS gidiş-dönüşünü çalıştırmak için onu çalışan bir sunucuyla birlikte açın.

Model İndirmeleri

Tüm modeller ilk kullanımda HuggingFace'ten indirilir ve ~/Library/Caches/qwen3-speech/ içinde önbelleğe alınır. AudioCommon modülü, indirme, önbellekleme ve bütünlük doğrulamayı yöneten ortak bir HuggingFaceDownloader sağlar.