الواجهة البرمجية والبروتوكولات
تُعرّف وحدة 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
| نقطة النهاية | الطريقة | الطلب | الاستجابة |
|---|---|---|---|
/transcribe | POST | جسم audio/wav | JSON { text } (Qwen3-ASR) |
/speak | POST | JSON { text, engine?, language?, voice? } | جسم audio/wav (Qwen3-TTS, CosyVoice, Kokoro) |
/respond | POST | جسم audio/wav | جسم audio/wav (PersonaPlex) |
/enhance | POST | جسم audio/wav | جسم audio/wav (DeepFilterNet3) |
/vad | POST | جسم audio/wav | قائمة مقاطع بصيغة JSON |
/diarize | POST | جسم audio/wav | قائمة DiarizedSegment بصيغة JSON |
/embed-speaker | POST | جسم audio/wav | JSON [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 مشتركة تتولى التنزيل والتخزين المؤقت والتحقق من السلامة.