API et protocoles
Le module AudioCommon définit des protocoles indépendants du modèle et des types partagés. Tout modèle conforme peut être utilisé de manière interchangeable via ces interfaces.
Vue d'ensemble des protocoles
┌─────────────────────────────────────────────────────────┐
│ AudioCommon │
│ │
│ AudioChunk SpeechGenerationModel (TTS) │
│ AlignedWord SpeechRecognitionModel (STT) │
│ SpeechSegment ForcedAlignmentModel │
│ SpeechToSpeechModel │
│ VoiceActivityDetectionModel (VAD) │
│ SpeakerEmbeddingModel │
│ SpeakerDiarizationModel │
│ SpeakerExtractionCapable │
└─────────────────────────────────────────────────────────┘SpeechRecognitionModel
Protocole pour les modèles de reconnaissance vocale.
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
}
Types conformes : Qwen3ASRModel, ParakeetASRModel, ParakeetStreamingASRModel, OmnilingualASRModel (CoreML), OmnilingualASRMLXModel (MLX)
SpeechGenerationModel
Protocole pour les modèles de synthèse vocale.
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() a une implémentation par défaut qui enveloppe generate() en un seul bloc. Les modèles avec vrai streaming (par ex. Qwen3-TTS) la surchargent.
Types conformes : Qwen3TTSModel, CosyVoiceTTSModel, KokoroTTSModel, Qwen35MLXChat
ForcedAlignmentModel
Protocole pour l'alignement d'horodatages au niveau du mot.
public protocol ForcedAlignmentModel: AnyObject {
func align(audio: [Float], text: String, sampleRate: Int, language: String?) -> [AlignedWord]
}
SpeechToSpeechModel
Protocole pour les modèles de dialogue parole-à-parole.
public protocol SpeechToSpeechModel: AnyObject {
var sampleRate: Int { get }
func respond(userAudio: [Float]) -> [Float]
func respondStream(userAudio: [Float]) -> AsyncThrowingStream<AudioChunk, Error>
}
Types conformes : PersonaPlexModel
VoiceActivityDetectionModel
Protocole pour la détection d'activité vocale.
public protocol VoiceActivityDetectionModel: AnyObject {
var inputSampleRate: Int { get }
func detectSpeech(audio: [Float], sampleRate: Int) -> [SpeechSegment]
}
SpeakerEmbeddingModel
Protocole pour l'extraction d'empreintes de locuteur.
public protocol SpeakerEmbeddingModel: AnyObject {
var inputSampleRate: Int { get }
var embeddingDimension: Int { get }
func embed(audio: [Float], sampleRate: Int) -> [Float]
}
Types conformes : WeSpeakerModel
SpeakerDiarizationModel
Protocole pour les modèles de diarisation qui attribuent des étiquettes de locuteur aux segments audio.
public protocol SpeakerDiarizationModel: AnyObject {
var inputSampleRate: Int { get }
func diarize(audio: [Float], sampleRate: Int) -> [DiarizedSegment]
}
Types conformes : DiarizationPipeline (Pyannote), SortformerDiarizer
SpeakerExtractionCapable
Protocole de diarisation étendu pour les moteurs qui prennent en charge l'extraction des segments d'un locuteur cible à l'aide d'une empreinte de référence. Tous les moteurs ne le supportent pas (Sortformer est de bout en bout et ne produit pas d'empreintes de locuteur).
public protocol SpeakerExtractionCapable: SpeakerDiarizationModel {
func extractSpeaker(audio: [Float], sampleRate: Int, targetEmbedding: [Float]) -> [SpeechSegment]
}
Types conformes : DiarizationPipeline (Pyannote uniquement)
Types partagés
AudioChunk
public struct AudioChunk {
public let samples: [Float] // Échantillons PCM
public let sampleRate: Int // Fréquence d'échantillonnage (par ex. 24000)
}
SpeechSegment
public struct SpeechSegment {
public let startTime: Float // Temps de début en secondes
public let endTime: Float // Temps de fin en secondes
}
AlignedWord
public struct AlignedWord {
public let text: String // Le mot
public let startTime: Float // Temps de début en secondes
public let endTime: Float // Temps de fin en secondes
}
DiarizedSegment
public struct DiarizedSegment {
public let startTime: Float // Temps de début en secondes
public let endTime: Float // Temps de fin en secondes
public let speakerId: Int // Identifiant de locuteur (base 0)
}
DialogueSegment
Un segment analysé de texte de dialogue multi-locuteurs avec des balises optionnelles de locuteur et d'émotion. Utilisé avec DialogueParser et DialogueSynthesizer pour la synthèse de dialogue CosyVoice3.
public struct DialogueSegment: Sendable, Equatable {
public let speaker: String? // Identifiant de locuteur ("S1", "S2"), nil si non balisé
public let emotion: String? // Balise d'émotion ("happy", "whispers"), nil si aucune
public let text: String // Texte nettoyé à synthétiser
}
DialogueParser
Analyse le texte de dialogue multi-locuteurs avec des balises de locuteur inline ([S1]) et des balises d'émotion ((happy)).
public enum DialogueParser {
static func parse(_ text: String) -> [DialogueSegment]
static func emotionToInstruction(_ emotion: String) -> String
}
Émotions intégrées : happy/excited, sad, angry, whispers/whispering, laughs/laughing, calm, surprised, serious. Les balises inconnues sont transmises comme instructions libres.
DialogueSynthesizer
Orchestre la synthèse de dialogue multi-segments avec clonage vocal par locuteur, silences entre tours et fondu enchaîné.
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 // Par défaut : 0.2
public var crossfadeSeconds: Float // Par défaut : 0.0
public var defaultInstruction: String // Par défaut : "You are a helpful assistant."
public var maxTokensPerSegment: Int // Par défaut : 500
}
PipelineLLM
Protocole d'intégration de modèles de langage avec les pipelines vocaux. Relie un LLM au flux ASR → LLM → TTS du VoicePipeline.
public protocol PipelineLLM: AnyObject {
func chat(messages: [(role: MessageRole, content: String)],
onToken: @escaping (String, Bool) -> Void)
func cancel()
}
Adaptateur intégré : Qwen3PipelineLLM relie Qwen35MLXChat à ce protocole avec nettoyage de tokens, annulation et accumulation de phrases en attente.
AudioIO
Gestionnaire d'E/S audio réutilisable qui élimine le code boilerplate AVAudioEngine. Gère la capture micro, le rééchantillonnage, la lecture et la mesure du niveau audio.
let audio = AudioIO()
try audio.startMicrophone(targetSampleRate: 16000) { samples in
pipeline.pushAudio(samples)
}
audio.player.scheduleChunk(ttsOutput)
audio.stopMicrophone()
AudioIO inclut un StreamingAudioPlayer pour la sortie TTS et un AudioRingBuffer pour le transfert audio thread-safe entre les threads de capture et d'inférence.
SentencePieceModel
Lecteur protobuf partagé pour les fichiers .model SentencePiece, situé dans AudioCommon. Chaque module qui a besoin de décoder des pieces SentencePiece (PersonaPlex, OmnilingualASR, futurs portages ASR / TTS) construit son propre décodeur au-dessus de ce lecteur unique au lieu de réimplémenter le format 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
}
Utilisé par : OmnilingualASR.OmnilingualVocabulary, PersonaPlex.SentencePieceDecoder. Couvert par 7 tests unitaires dans Tests/AudioCommonTests/SentencePieceModelTests.
MLXCommon.SDPA
Helpers d'attention scaled dot-product partagés entre chaque module d'attention MLX (Qwen3-ASR / Qwen3-TTS / Qwen3-Chat / CosyVoice / PersonaPlex / OmnilingualASR). Chaque module conserve ses propres projections — SDPA ne gère que le boilerplate reshape → attention → merge.
public enum SDPA {
// Entrée plate [B, T, H*D] : projection/reshape à l'intérieur
public static func multiHead(
q: MLXArray, k: MLXArray, v: MLXArray,
numHeads: Int, headDim: Int, scale: Float,
mask: MLXArray? = nil
) -> MLXArray
// Variante GQA / MQA avec nombres de têtes de requête et KV séparés
public static func multiHead(
q: MLXArray, k: MLXArray, v: MLXArray,
numQueryHeads: Int, numKVHeads: Int, headDim: Int, scale: Float,
mask: MLXArray? = nil
) -> MLXArray
// Déjà formé [B, H, T, D] (chemins RoPE / KV cache)
public static func attendAndMerge(
qHeads: MLXArray, kHeads: MLXArray, vHeads: MLXArray,
scale: Float,
mask: MLXArray? = nil
) -> MLXArray
// Idem, avec l'enum ScaledDotProductAttentionMaskMode (API plus récente)
public static func attendAndMerge(
qHeads: MLXArray, kHeads: MLXArray, vHeads: MLXArray,
scale: Float,
mask: MLXFast.ScaledDotProductAttentionMaskMode
) -> MLXArray
// Fusion de têtes bas niveau : [B, H, T, D] → [B, T, H*D]
public static func mergeHeads(_ attn: MLXArray) -> MLXArray
}
Tous les appels reshape utilisent -1 pour la dimension du batch, de sorte que les helpers composent avec les graphes MLX.compile(shapeless:) qui varient le batch à l'exécution (par ex. le décodage autoregressif Talker Qwen3-TTS).
Serveur API HTTP
Le binaire audio-server expose chaque modèle de speech-swift via des endpoints REST HTTP et un endpoint WebSocket qui implémente l'API Realtime OpenAI. Les modèles sont chargés à la demande à la première requête ; passez --preload pour tous les préchauffer au démarrage.
swift build -c release
.build/release/audio-server --port 8080
# Précharger chaque modèle au démarrage
.build/release/audio-server --port 8080 --preload
Endpoints REST
| Endpoint | Méthode | Requête | Réponse |
|---|---|---|---|
/transcribe | POST | Corps audio/wav | JSON { text } (Qwen3-ASR) |
/speak | POST | JSON { text, engine?, language?, voice? } | Corps audio/wav (Qwen3-TTS, CosyVoice, Kokoro) |
/respond | POST | Corps audio/wav | Corps audio/wav (PersonaPlex) |
/enhance | POST | Corps audio/wav | Corps audio/wav (DeepFilterNet3) |
/vad | POST | Corps audio/wav | Liste JSON de segments |
/diarize | POST | Corps audio/wav | Liste JSON de DiarizedSegment |
/embed-speaker | POST | Corps audio/wav | JSON [Float] (256 dim) |
# Transcrire un fichier
curl -X POST http://localhost:8080/transcribe \
--data-binary @recording.wav \
-H "Content-Type: audio/wav"
# Synthétiser la parole
curl -X POST http://localhost:8080/speak \
-H "Content-Type: application/json" \
-d '{"text": "Hello world", "engine": "cosyvoice"}' \
-o output.wav
# Aller-retour parole-à-parole complet
curl -X POST http://localhost:8080/respond \
--data-binary @question.wav \
-o response.wav
API Realtime OpenAI (/v1/realtime)
L'endpoint WebSocket à ws://host:port/v1/realtime implémente le protocole OpenAI Realtime. Tous les messages sont en JSON avec un discriminateur type ; les payloads audio sont encodés en base64 PCM16 à 24 kHz mono.
Événements client → serveur
| Événement | Objectif |
|---|---|
session.update | Configurer le moteur, la langue, la voix et le format audio |
input_audio_buffer.append | Ajouter un bloc PCM16 base64 au tampon d'entrée |
input_audio_buffer.commit | Valider l'audio tamponné pour transcription |
input_audio_buffer.clear | Écarter le tampon d'entrée actuel |
response.create | Demander la synthèse TTS du texte/instructions fournis |
Événements serveur → client
| Événement | Signification |
|---|---|
session.created | Handshake terminé, configuration par défaut émise |
session.updated | Dernier session.update acquitté |
input_audio_buffer.committed | Audio accepté et mis en file pour transcription |
conversation.item.input_audio_transcription.completed | Résultat ASR avec le texte de transcription final |
response.audio.delta | Bloc PCM16 base64 d'audio synthétisé |
response.audio.done | Plus de blocs audio pour cette réponse |
response.done | Réponse finalisée (métadonnées + statistiques de latence) |
error | Enveloppe d'erreur avec type et message |
const ws = new WebSocket('ws://localhost:8080/v1/realtime');
// ASR : pousser l'audio, demander la 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 : demander la synthèse et streamer les deltas audio
ws.send(JSON.stringify({
type: 'response.create',
response: { modalities: ['audio', 'text'], instructions: 'Hello world' }
}));
// → response.audio.delta (répété), response.audio.done, response.done
Le serveur se trouve dans le produit SPM AudioServer. Un exemple de client navigateur est livré dans Examples/websocket-client.html — ouvrez-le en parallèle d'un serveur en cours d'exécution pour piloter l'aller-retour ASR + TTS complet.
Téléchargements de modèles
Tous les modèles sont téléchargés depuis HuggingFace à la première utilisation et mis en cache dans ~/Library/Caches/qwen3-speech/. Le module AudioCommon fournit un HuggingFaceDownloader partagé qui gère le téléchargement, la mise en cache et la vérification d'intégrité.