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

EndpointMéthodeRequêteRéponse
/transcribePOSTCorps audio/wavJSON { text } (Qwen3-ASR)
/speakPOSTJSON { text, engine?, language?, voice? }Corps audio/wav (Qwen3-TTS, CosyVoice, Kokoro)
/respondPOSTCorps audio/wavCorps audio/wav (PersonaPlex)
/enhancePOSTCorps audio/wavCorps audio/wav (DeepFilterNet3)
/vadPOSTCorps audio/wavListe JSON de segments
/diarizePOSTCorps audio/wavListe JSON de DiarizedSegment
/embed-speakerPOSTCorps audio/wavJSON [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énementObjectif
session.updateConfigurer le moteur, la langue, la voix et le format audio
input_audio_buffer.appendAjouter un bloc PCM16 base64 au tampon d'entrée
input_audio_buffer.commitValider l'audio tamponné pour transcription
input_audio_buffer.clearÉcarter le tampon d'entrée actuel
response.createDemander la synthèse TTS du texte/instructions fournis

Événements serveur → client

ÉvénementSignification
session.createdHandshake terminé, configuration par défaut émise
session.updatedDernier session.update acquitté
input_audio_buffer.committedAudio accepté et mis en file pour transcription
conversation.item.input_audio_transcription.completedRésultat ASR avec le texte de transcription final
response.audio.deltaBloc PCM16 base64 d'audio synthétisé
response.audio.donePlus de blocs audio pour cette réponse
response.doneRéponse finalisée (métadonnées + statistiques de latence)
errorEnveloppe 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é.