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サンプル
public let sampleRate: Int // サンプルレート(例: 24000)
}
SpeechSegment
public struct SpeechSegment {
public let startTime: Float // 開始時刻(秒)
public let endTime: Float // 終了時刻(秒)
}
AlignedWord
public struct AlignedWord {
public let text: String // 単語
public let startTime: Float // 開始時刻(秒)
public let endTime: Float // 終了時刻(秒)
}
DiarizedSegment
public struct DiarizedSegment {
public let startTime: Float // 開始時刻(秒)
public let endTime: Float // 終了時刻(秒)
public let speakerId: Int // 話者識別子(0始まり)
}
DialogueSegment
オプションの話者タグと感情タグを含む、マルチ話者対話テキストの解析済みセグメント。CosyVoice3の対話合成でDialogueParserおよびDialogueSynthesizerと共に使用されます。
public struct DialogueSegment: Sendable, Equatable {
public let speaker: String? // 話者識別子("S1"、"S2")、タグなしはnil
public let emotion: String? // 感情タグ("happy"、"whispers")、なければnil
public let text: String // 合成用のクリーニング済みテキスト
}
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 // デフォルト: 0.2
public var crossfadeSeconds: Float // デフォルト: 0.0
public var defaultInstruction: String // デフォルト: "You are a helpful assistant."
public var maxTokensPerSegment: Int // デフォルト: 500
}
PipelineLLM
音声パイプラインとの言語モデル統合用プロトコル。VoicePipelineのASR → LLM → TTSフローにLLMをブリッジします。
public protocol PipelineLLM: AnyObject {
func chat(messages: [(role: MessageRole, content: String)],
onToken: @escaping (String, Bool) -> Void)
func cancel()
}
組み込みアダプター: Qwen3PipelineLLMは、トークンクリーンアップ、キャンセル、保留フレーズの蓄積を備えてQwen35MLXChatをこのプロトコルにブリッジします。
AudioIO
AVAudioEngineのボイラープレートを排除する、再利用可能な音声I/Oマネージャ。マイクキャプチャ、リサンプリング、再生、音声レベルメータリングを処理します。
let audio = AudioIO()
try audio.startMicrophone(targetSampleRate: 16000) { samples in
pipeline.pushAudio(samples)
}
audio.player.scheduleChunk(ttsOutput)
audio.stopMicrophone()
AudioIOには、TTS出力用のStreamingAudioPlayerと、キャプチャスレッドと推論スレッド間のスレッドセーフな音声転送のためのAudioRingBufferが含まれています。
SentencePieceModel
SentencePieceの.modelファイル用の共有protobufリーダで、AudioCommonに存在します。SentencePieceピースをデコードする必要があるすべてのモジュール(PersonaPlex、OmnilingualASR、将来のASR / TTSポート)は、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
}
使用箇所: OmnilingualASR.OmnilingualVocabulary、PersonaPlex.SentencePieceDecoder。Tests/AudioCommonTests/SentencePieceModelTestsの7つのユニットテストでカバーされています。
MLXCommon.SDPA
すべてのMLXアテンションモジュール(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呼び出しはバッチ次元に-1を使用するため、ヘルパーはランタイムでバッチが変動するMLX.compile(shapeless:)グラフ(例: 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
# 起動時にすべてのモデルをプリロード
.build/release/audio-server --port 8080 --preload
RESTエンドポイント
| エンドポイント | Method | リクエスト | レスポンス |
|---|---|---|---|
/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次元) |
# ファイルを文字起こし
curl -X POST http://localhost:8080/transcribe \
--data-binary @recording.wav \
-H "Content-Type: audio/wav"
# 音声を合成
curl -X POST http://localhost:8080/speak \
-H "Content-Type: application/json" \
-d '{"text": "Hello world", "engine": "cosyvoice"}' \
-o output.wav
# 完全な音声間ラウンドトリップ
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です。音声ペイロードは24 kHzモノラルのbase64エンコードされた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 | 最終のtranscriptテキスト付きASR結果 |
response.audio.delta | 合成された音声のbase64 PCM16 chunk |
response.audio.done | このレスポンスの音声chunksはこれ以上なし |
response.done | レスポンス確定(メタデータ + レイテンシ統計) |
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プロダクトに存在します。ブラウザクライアントの例はExamples/websocket-client.htmlに同梱されています — 実行中のサーバーと共に開くことで、完全なASR + TTSのラウンドトリップを駆動できます。
モデルダウンロード
すべてのモデルは初回使用時にHuggingFaceからダウンロードされ、~/Library/Caches/qwen3-speech/にキャッシュされます。AudioCommonモジュールは、ダウンロード、キャッシング、整合性検証を処理する共有のHuggingFaceDownloaderを提供します。