API e protocolos

O modulo AudioCommon define protocolos agnosticos de modelo e tipos compartilhados. Qualquer modelo conformante pode ser usado de forma intercambiavel atraves dessas interfaces.

Visao geral dos protocolos

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

SpeechRecognitionModel

Protocolo para modelos de fala para texto.

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
}

Tipos conformantes: Qwen3ASRModel, ParakeetASRModel, ParakeetStreamingASRModel, OmnilingualASRModel (CoreML), OmnilingualASRMLXModel (MLX)

SpeechGenerationModel

Protocolo para modelos de texto para fala.

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() tem uma implementacao padrao que envolve generate() como um unico chunk. Modelos com streaming verdadeiro (por exemplo Qwen3-TTS) o sobrescrevem.

Tipos conformantes: Qwen3TTSModel, CosyVoiceTTSModel, KokoroTTSModel, Qwen35MLXChat

ForcedAlignmentModel

Protocolo para alinhamento de timestamps por palavra.

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

SpeechToSpeechModel

Protocolo para modelos de dialogo fala para fala.

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

Tipos conformantes: PersonaPlexModel

VoiceActivityDetectionModel

Protocolo para deteccao de atividade de voz.

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

SpeakerEmbeddingModel

Protocolo para extracao de embedding de falante.

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

Tipos conformantes: WeSpeakerModel

SpeakerDiarizationModel

Protocolo para modelos de diarizacao de falantes que atribuem rotulos de falante a segmentos de audio.

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

Tipos conformantes: DiarizationPipeline (Pyannote), SortformerDiarizer

SpeakerExtractionCapable

Protocolo de diarizacao estendido para engines que suportam extrair os segmentos de um falante alvo usando um embedding de referencia. Nem todas as engines suportam isso (Sortformer e ponta-a-ponta e nao produz embeddings de falante).

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

Tipos conformantes: DiarizationPipeline (somente Pyannote)

Tipos compartilhados

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

Um segmento analisado de texto de dialogo multi-locutor com tags opcionais de falante e emocao. Usado com DialogueParser e DialogueSynthesizer para sintese de dialogo 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

Analisa texto de dialogo multi-locutor com tags inline de falante ([S1]) e tags de emocao ((happy)).

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

Emocoes integradas: happy/excited, sad, angry, whispers/whispering, laughs/laughing, calm, surprised, serious. Tags desconhecidas passam como instrucoes em forma livre.

DialogueSynthesizer

Orquestra a sintese de dialogo multi-segmento com clonagem de voz por locutor, intervalos de silencio e crossfade.

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

Protocolo para integracao de modelo de linguagem com voice pipelines. Conecta um LLM ao fluxo ASR → LLM → TTS do VoicePipeline.

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

Adaptador integrado: Qwen3PipelineLLM conecta Qwen35MLXChat a este protocolo com limpeza de tokens, cancelamento e acumulacao de frases pendentes.

AudioIO

Gerenciador de I/O de audio reutilizavel que elimina o boilerplate do AVAudioEngine. Lida com captura de microfone, reamostragem, reproducao e medicao de nivel de audio.

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

AudioIO inclui um StreamingAudioPlayer para saida TTS e um AudioRingBuffer para transferencia de audio thread-safe entre threads de captura e inferencia.

SentencePieceModel

Leitor protobuf compartilhado para arquivos .model SentencePiece, que vive em AudioCommon. Cada modulo que precisa decodificar pieces de SentencePiece (PersonaPlex, OmnilingualASR, futuras portas de ASR / TTS) constroi seu proprio decodificador sobre este unico leitor em vez de re-implementar o formato wire de 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
}

Usado por: OmnilingualASR.OmnilingualVocabulary, PersonaPlex.SentencePieceDecoder. Coberto por 7 testes unitarios em Tests/AudioCommonTests/SentencePieceModelTests.

MLXCommon.SDPA

Helpers de scaled dot-product attention compartilhados entre cada modulo de atencao MLX (Qwen3-ASR / Qwen3-TTS / Qwen3-Chat / CosyVoice / PersonaPlex / OmnilingualASR). Cada modulo mantem suas proprias projecoes — SDPA lida apenas com o boilerplate de reshape → attention → 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
}

Todas as chamadas de reshape usam -1 para a dimensao de batch para que os helpers se componham com grafos MLX.compile(shapeless:) que variam batch em tempo de execucao (por exemplo, decode autoregressivo do Talker do Qwen3-TTS).

Servidor HTTP API

O binario audio-server expoe cada modelo no speech-swift como endpoints HTTP REST mais um endpoint WebSocket que implementa a OpenAI Realtime API. Os modelos sao carregados sob demanda na primeira requisicao; passe --preload para aquece-los todos na inicializacao.

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

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

Endpoints REST

EndpointMetodoRequisicaoResposta
/transcribePOSTcorpo audio/wavJSON { text } (Qwen3-ASR)
/speakPOSTJSON { text, engine?, language?, voice? }corpo audio/wav (Qwen3-TTS, CosyVoice, Kokoro)
/respondPOSTcorpo audio/wavcorpo audio/wav (PersonaPlex)
/enhancePOSTcorpo audio/wavcorpo audio/wav (DeepFilterNet3)
/vadPOSTcorpo audio/wavlista de segmentos JSON
/diarizePOSTcorpo audio/wavlista JSON de DiarizedSegment
/embed-speakerPOSTcorpo audio/wavJSON [Float] (256 dim)
# 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)

O endpoint WebSocket em ws://host:port/v1/realtime implementa o protocolo OpenAI Realtime. Todas as mensagens sao JSON com um discriminador type; os payloads de audio sao PCM16 codificados em base64 a 24 kHz mono.

Eventos cliente → servidor

EventoProposito
session.updateConfigurar engine, idioma, voz e formato de audio
input_audio_buffer.appendAnexar um chunk PCM16 base64 ao buffer de entrada
input_audio_buffer.commitConfirmar o audio em buffer para transcricao
input_audio_buffer.clearDescartar o buffer de entrada atual
response.createSolicitar sintese TTS para o texto/instrucoes fornecidos

Eventos servidor → cliente

EventoSignificado
session.createdHandshake completo, configuracao padrao emitida
session.updatedO session.update mais recente confirmado
input_audio_buffer.committedAudio aceito e enfileirado para transcricao
conversation.item.input_audio_transcription.completedResultado de ASR com texto de transcricao final
response.audio.deltaChunk PCM16 base64 de audio sintetizado
response.audio.doneNao ha mais chunks de audio para esta resposta
response.doneResposta finalizada (metadados + estatisticas de latencia)
errorEnvelope de erro com type e 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

O servidor vive no produto SPM AudioServer. Um cliente browser de exemplo e fornecido em Examples/websocket-client.html — abra-o ao lado de um servidor em execucao para acionar o ciclo completo ASR + TTS.

Downloads de modelos

Todos os modelos sao baixados do HuggingFace no primeiro uso e armazenados em cache em ~/Library/Caches/qwen3-speech/. O modulo AudioCommon fornece um HuggingFaceDownloader compartilhado que lida com download, cache e verificacao de integridade.