الواجهة البرمجية والبروتوكولات

تُعرّف وحدة AudioCommon بروتوكولات مستقلة عن النموذج وأنواعًا مشتركة. يمكن استخدام أي نموذج يلتزم بهذه البروتوكولات بالتبادل من خلال هذه الواجهات.

نظرة عامة على البروتوكولات

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

SpeechRecognitionModel

بروتوكول لنماذج تحويل الكلام إلى نص.

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
}

الأنواع الملتزمة: Qwen3ASRModel, ParakeetASRModel, ParakeetStreamingASRModel, OmnilingualASRModel (CoreML), OmnilingualASRMLXModel (MLX)

SpeechGenerationModel

بروتوكول لنماذج تحويل النص إلى كلام.

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() كقطعة واحدة. النماذج التي تدعم البث الحقيقي (مثل Qwen3-TTS) تتجاوز هذا التطبيق.

الأنواع الملتزمة: Qwen3TTSModel, CosyVoiceTTSModel, KokoroTTSModel, Qwen35MLXChat

ForcedAlignmentModel

بروتوكول لمحاذاة الطوابع الزمنية على مستوى الكلمة.

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

SpeechToSpeechModel

بروتوكول لنماذج الحوار من كلام إلى كلام.

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

الأنواع الملتزمة: PersonaPlexModel

VoiceActivityDetectionModel

بروتوكول لكشف النشاط الصوتي.

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

SpeakerEmbeddingModel

بروتوكول لاستخراج تضمينات المتحدث.

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

الأنواع الملتزمة: WeSpeakerModel

SpeakerDiarizationModel

بروتوكول لنماذج فصل المتحدثين التي تُسنِد تسميات المتحدث إلى مقاطع الصوت.

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

الأنواع الملتزمة: DiarizationPipeline (Pyannote), SortformerDiarizer

SpeakerExtractionCapable

بروتوكول فصل ممتد للمحركات التي تدعم استخراج مقاطع متحدث مستهدف باستخدام تضمين مرجعي. لا تدعمه جميع المحركات (Sortformer شامل من طرف إلى طرف ولا يُنتج تضمينات للمتحدث).

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

الأنواع الملتزمة: DiarizationPipeline (Pyannote فقط)

الأنواع المشتركة

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

مقطع مُحلَّل من نص حوار متعدد المتحدثين مع وسوم اختيارية للمتحدث والمشاعر. يُستخدم مع DialogueParser وDialogueSynthesizer لتوليف الحوار في 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

يحلّل نص الحوار متعدد المتحدثين بوسوم متضمنة للمتحدث ([S1]) ووسوم المشاعر ((happy)).

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

المشاعر المضمّنة: happy/excited, sad, angry, whispers/whispering, laughs/laughing, calm, surprised, serious. الوسوم غير المعروفة تُمرَّر كتعليمات نصية حرة.

DialogueSynthesizer

يُنسّق توليف الحوار متعدد المقاطع مع استنساخ الصوت لكل متحدث، وفجوات صمت، وتلاشي متقاطع.

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

بروتوكول لدمج النماذج اللغوية مع خطوط معالجة الصوت. يربط نموذجًا لغويًا بتدفق ASR → LLM → TTS في VoicePipeline.

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

المحوّل المضمَّن: Qwen3PipelineLLM يربط Qwen35MLXChat بهذا البروتوكول مع تنظيف الرموز، والإلغاء، وتجميع العبارات المعلّقة.

AudioIO

مدير قابل لإعادة الاستخدام لمدخلات/مخرجات الصوت يُلغي تكرار شيفرة AVAudioEngine. يتولى التقاط الميكروفون، وإعادة التشكيل، والتشغيل، وقياس مستوى الصوت.

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

تتضمن AudioIO مكوّن StreamingAudioPlayer لإخراج TTS وAudioRingBuffer لنقل الصوت بأمان بين خيوط الالتقاط والاستدلال.

SentencePieceModel

قارئ protobuf مشترك لملفات .model الخاصة بـ SentencePiece، يقع في AudioCommon. كل وحدة بحاجة إلى فك ترميز قطع SentencePiece (PersonaPlex، OmnilingualASR، نقلات ASR / TTS المستقبلية) تبني فاكَّ الترميز الخاص بها فوق هذا القارئ الوحيد بدلًا من إعادة تنفيذ تنسيق wire الخاص بـ 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
}

يُستخدم بواسطة: OmnilingualASR.OmnilingualVocabulary, PersonaPlex.SentencePieceDecoder. مغطى بسبع اختبارات وحدة في Tests/AudioCommonTests/SentencePieceModelTests.

MLXCommon.SDPA

مساعدات الانتباه scaled dot-product المشتركة بين كل وحدات الانتباه في MLX (Qwen3-ASR / Qwen3-TTS / Qwen3-Chat / CosyVoice / PersonaPlex / OmnilingualASR). كل وحدة تحتفظ بإسقاطاتها الخاصة — وحدة SDPA تعالج فقط تكرار شيفرة reshape → الانتباه → merge.

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
}

كل استدعاءات reshape تستخدم -1 لبُعد الـ batch، بحيث تتركّب المساعدات مع رسومات MLX.compile(shapeless:) التي تختلف فيها قيمة batch وقت التشغيل (مثل فك التشفير التراجعي للـ Talker في Qwen3-TTS).

خادم HTTP API

يكشف الملف التنفيذي speech-server كل نموذج في speech-swift كنقاط نهاية HTTP REST بالإضافة إلى نقطة WebSocket تُنفّذ OpenAI Realtime API. تُحمَّل النماذج بشكل كسول عند أول طلب؛ مرّر --preload لتسخين الجميع عند الإقلاع.

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

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

نقاط نهاية REST

نقطة النهايةالطريقةالطلبالاستجابة
/transcribePOSTجسم audio/wavJSON { text } (Qwen3-ASR)
/speakPOSTJSON { text, engine?, language?, voice? }جسم audio/wav (Qwen3-TTS, CosyVoice, Kokoro)
/respondPOSTجسم audio/wavجسم audio/wav (PersonaPlex)
/enhancePOSTجسم audio/wavجسم audio/wav (DeepFilterNet3)
/vadPOSTجسم audio/wavقائمة مقاطع بصيغة JSON
/diarizePOSTجسم audio/wavقائمة DiarizedSegment بصيغة JSON
/embed-speakerPOSTجسم audio/wavJSON [Float] (256 بُعدًا)
# 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)

نقطة WebSocket عند ws://host:port/v1/realtime تُنفّذ بروتوكول OpenAI Realtime. جميع الرسائل بصيغة JSON مع مميّز type؛ وحمولات الصوت هي PCM16 مُرمَّز بـ base64 عند 24 كيلوهرتز أحادي القناة.

أحداث من العميل → إلى الخادم

الحدثالغرض
session.updateإعداد المحرّك واللغة والصوت وصيغة الصوت
input_audio_buffer.appendإضافة قطعة PCM16 بترميز base64 إلى مخزّن الإدخال
input_audio_buffer.commitتثبيت الصوت المخزَّن للتفريغ
input_audio_buffer.clearإسقاط مخزّن الإدخال الحالي
response.createطلب توليف TTS للنص/التعليمات المُمرَّرة

أحداث من الخادم → إلى العميل

الحدثالمعنى
session.createdاكتمل التصافح، وأُصدِر الإعداد الافتراضي
session.updatedتم تأكيد آخر session.update
input_audio_buffer.committedقُبِل الصوت وأُدرِج في طابور التفريغ
conversation.item.input_audio_transcription.completedنتيجة ASR مع النص النهائي المُفرَّغ
response.audio.deltaقطعة PCM16 بترميز base64 من الصوت المُولَّد
response.audio.doneلا مزيد من قطع الصوت لهذه الاستجابة
response.doneاكتملت الاستجابة (بيانات وصفية + إحصائيات زمن الاستجابة)
errorغلاف الخطأ مع type و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

الخادم يقع داخل منتج SPM المسمّى AudioServer. يُشحَن مثال لعميل متصفح في Examples/websocket-client.html — افتحه إلى جانب خادم قيد التشغيل لتشغيل دورة ASR + TTS الكاملة.

تنزيل النماذج

تُنزَّل كل النماذج من HuggingFace عند أول استخدام وتُخزَّن مؤقتًا في ~/Library/Caches/qwen3-speech/. توفر وحدة AudioCommon أداة HuggingFaceDownloader مشتركة تتولى التنزيل والتخزين المؤقت والتحقق من السلامة.