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
| Endpoint | Metodo | Requisicao | Resposta |
|---|---|---|---|
/transcribe | POST | corpo audio/wav | JSON { text } (Qwen3-ASR) |
/speak | POST | JSON { text, engine?, language?, voice? } | corpo audio/wav (Qwen3-TTS, CosyVoice, Kokoro) |
/respond | POST | corpo audio/wav | corpo audio/wav (PersonaPlex) |
/enhance | POST | corpo audio/wav | corpo audio/wav (DeepFilterNet3) |
/vad | POST | corpo audio/wav | lista de segmentos JSON |
/diarize | POST | corpo audio/wav | lista JSON de DiarizedSegment |
/embed-speaker | POST | corpo audio/wav | JSON [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
| Evento | Proposito |
|---|---|
session.update | Configurar engine, idioma, voz e formato de audio |
input_audio_buffer.append | Anexar um chunk PCM16 base64 ao buffer de entrada |
input_audio_buffer.commit | Confirmar o audio em buffer para transcricao |
input_audio_buffer.clear | Descartar o buffer de entrada atual |
response.create | Solicitar sintese TTS para o texto/instrucoes fornecidos |
Eventos servidor → cliente
| Evento | Significado |
|---|---|
session.created | Handshake completo, configuracao padrao emitida |
session.updated | O session.update mais recente confirmado |
input_audio_buffer.committed | Audio aceito e enfileirado para transcricao |
conversation.item.input_audio_transcription.completed | Resultado de ASR com texto de transcricao final |
response.audio.delta | Chunk PCM16 base64 de audio sintetizado |
response.audio.done | Nao ha mais chunks de audio para esta resposta |
response.done | Resposta finalizada (metadados + estatisticas de latencia) |
error | Envelope 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.