API 与协议
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() 包装为单个 chunk。具有真正 streaming 能力的模型(例如 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
说话人 embedding 提取的协议。
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
扩展的说话人分离协议,适用于支持通过参考 embedding 提取目标说话人片段的引擎。并非所有引擎都支持(Sortformer 是端到端的,不产生说话人 embedding)。
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
用于将语言模型集成到语音流水线的协议。将 LLM 桥接到 VoicePipeline 的 ASR → LLM → TTS 流程。
public protocol PipelineLLM: AnyObject {
func chat(messages: [(role: MessageRole, content: String)],
onToken: @escaping (String, Bool) -> Void)
func cancel()
}
内置适配器: Qwen3PipelineLLM 将 Qwen35MLXChat 桥接到该协议,提供 token 清理、取消和待处理短语累积。
AudioIO
可复用的音频 I/O 管理器,免去 AVAudioEngine 的样板代码。处理麦克风采集、重采样、播放以及音频电平测量。
let audio = AudioIO()
try audio.startMicrophone(targetSampleRate: 16000) { samples in
pipeline.pushAudio(samples)
}
audio.player.scheduleChunk(ttsOutput)
audio.stopMicrophone()
AudioIO 包含用于 TTS 输出的 StreamingAudioPlayer 以及用于在采集与推理线程之间进行线程安全音频传输的 AudioRingBuffer。
SentencePieceModel
位于 AudioCommon 中的共享 protobuf 读取器,用于 SentencePiece .model 文件。每个需要解码 SentencePiece pieces 的模块(PersonaPlex、OmnilingualASR、未来的 ASR / TTS 移植)都在这个唯一读取器之上构建自己的解码器,而不是各自重新实现 protobuf wire format。
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 中由 7 个单元测试覆盖。
MLXCommon.SDPA
在每个 MLX attention 模块(Qwen3-ASR / Qwen3-TTS / Qwen3-Chat / CosyVoice / PersonaPlex / OmnilingualASR)之间共享的 scaled dot-product attention 辅助工具。每个模块保留自己的投影——SDPA 只负责 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
}
所有 reshape 调用都为 batch 维度使用 -1,这样辅助函数就能与 MLX.compile(shapeless:) 图组合——这些图在运行时 batch 会变化(例如 Qwen3-TTS Talker 的自回归解码)。
HTTP API 服务器
audio-server 二进制将 speech-swift 中的每个模型都暴露为 HTTP REST 端点,外加一个实现 OpenAI Realtime API 的 WebSocket 端点。模型在首次请求时才惰性加载;传入 --preload 可在启动时预热所有模型。
swift build -c release
.build/release/audio-server --port 8080
# Preload every model at startup
.build/release/audio-server --port 8080 --preload
REST 端点
| 端点 | 方法 | 请求 | 响应 |
|---|---|---|---|
/transcribe | POST | audio/wav body | JSON { text }(Qwen3-ASR) |
/speak | POST | JSON { text, engine?, language?, voice? } | audio/wav body(Qwen3-TTS、CosyVoice、Kokoro) |
/respond | POST | audio/wav body | audio/wav body(PersonaPlex) |
/enhance | POST | audio/wav body | audio/wav body(DeepFilterNet3) |
/vad | POST | audio/wav body | JSON 段列表 |
/diarize | POST | audio/wav body | JSON DiarizedSegment 列表 |
/embed-speaker | POST | audio/wav body | 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)
ws://host:port/v1/realtime 的 WebSocket 端点实现了 OpenAI Realtime 协议。所有消息都是带 type 鉴别字段的 JSON;音频负载是 base64 编码的 24 kHz 单声道 PCM16。
客户端 → 服务器事件
| 事件 | 用途 |
|---|---|
session.update | 配置引擎、语言、音色和音频格式 |
input_audio_buffer.append | 向输入缓冲区追加一个 base64 PCM16 chunk |
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 | 合成音频的 base64 PCM16 chunk |
response.audio.done | 本次响应没有更多音频 chunks |
response.done | 响应最终化(metadata + 延迟统计) |
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
该服务器位于 AudioServer SPM product 中。仓库提供了一个示例浏览器客户端 Examples/websocket-client.html——在服务器运行时打开它,就可以驱动完整的 ASR + TTS 往返。
模型下载
所有模型在首次使用时从 HuggingFace 下载,并缓存在 ~/Library/Caches/qwen3-speech/。AudioCommon 模块提供了共享的 HuggingFaceDownloader,负责下载、缓存和完整性校验。