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, WhisperASRModelParakeetASRModelParakeetStreamingASRModelOmnilingualASRModel(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, VoxCPM2TTSModel, KokoroTTSModel, IndexTTS2TTSModel

IndexTTS2TTSModel は zero-shot 音声クローン用に参照音声付きの generate オーバーロードを追加し、話速とポーズ制御に IndexTTS2SynthesisOptions を使います。

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/excitedsadangrywhispers/whisperinglaughs/laughingcalmsurprisedserious。未知のタグはフリーフォームの指示としてそのまま渡されます。

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が含まれています。

SystemAudioTap

システム出力ミックス——Mac がいま再生している音——を、呼び出し側が選ぶサンプルレートのモノラル Float32 として取得します(Core Audio のプロセスタップ、macOS 14.4+)。グローバルなモノラルタップは既定で現在のプロセスを除外するため、アプリ自身の再生が再取得されることはなく、タップだけを含むプライベートな集約デバイスにラップされます。

let tap = SystemAudioTap()
try tap.start(targetSampleRate: 16000) { samples in
    pipeline.pushAudio(samples)
}
tap.stop()

既定出力デバイスを切り替えてもキャプチャは継続します——ミックスのレートが変わると内部リサンプラーが再構築されます。ホストアプリは NSAudioCaptureUsageDescription を宣言する必要があります。録音許可が拒否されてもタップは作成され得ますが無音のみを出力します:framesCaptured が増え続ける一方で nonSilentFrames が 0 のままなら、明示的に失敗させてください。

タイムスタンプ付きの独立音源

両方のキャプチャクラスにタイムスタンプ付きオーバーロードがあります。CapturedAudioChunksampleRate のモノラル samples と、最初の入力フレームを示す任意の Mach hostTime を保持します。両音源が同じホストクロックを使うため、PCM を分離したまま、音声をミックスせずに派生した文字起こしイベントを並べられます。

try tap.startTimestamped(targetSampleRate: 16000) { systemChunk in
    systemPipeline.pushAudio(systemChunk.samples)
    recordSystemTime(systemChunk.hostTime)
}

let microphone = AudioIO(enableAEC: true, enablePlayback: false)
try microphone.startMicrophoneTimestamped(targetSampleRate: 16000) { micChunk in
    microphonePipeline.pushAudio(micChunk.samples)
    recordMicrophoneTime(micChunk.hostTime)
}

聴取専用のキャプチャでは、enableAEC: true がマイク形式を読み取る前に Apple Voice Processing を有効にし、enablePlayback: false が未使用のプレーヤーを省きます。macOS ではデバイスの再生音がマイクサンプルから除去され、AEC を開始できない場合は生入力へフォールバックせずエラーを通知します。

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.OmnilingualVocabularyPersonaPlex.SentencePieceDecoderTests/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サーバー

speech-serverバイナリは、speech-swiftのすべてのモデルをHTTP RESTエンドポイントおよびOpenAI Realtime APIを実装するWebSocketエンドポイントとして公開します。モデルは初回リクエスト時に遅延ロードされます。起動時にすべてウォームアップするには--preloadを渡してください。

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

# 起動時にすべてのモデルをプリロード
.build/release/speech-server --port 8080 --preload

RESTエンドポイント

エンドポイントMethodリクエストレスポンス
/transcribePOSTaudio/wav bodyJSON { text }(Qwen3-ASR)
/v1/audio/transcriptionsPOSTmultipart { file, model, response_format?, language?, prompt?, temperature? }JSON、テキスト、verbose JSON、SRT、VTT
/v1/audio/speechPOSTJSON { model, input, voice, response_format?, speed? }audio/wav または audio/pcm の音声本文
/speakPOSTJSON { text, engine?, language?, voice? }audio/wav body(Qwen3-TTS、CosyVoice、Kokoro)
/respondPOSTaudio/wav bodyaudio/wav body(PersonaPlex)
/enhancePOSTaudio/wav bodyaudio/wav body(DeepFilterNet3)
/vadPOSTaudio/wav bodyJSONセグメントリスト
/diarizePOSTaudio/wav bodyJSON DiarizedSegmentリスト
/embed-speakerPOSTaudio/wav bodyJSON [Float](256次元)

/v1/audio/speech は標準の TTS モデル名と音声名をローカルエンジンに割り当てます。既定は WAV です。response_format: "pcm" では、ヘッダーなしの 24 kHz モノラル PCM16 リトルエンディアン音声を返します。非対応の圧縮形式は明示的に拒否されます。

/v1/audio/transcriptions は multipart フォームで 1 つの WAV ファイルを受け取り、既定で {"text":"..."} を返します。response_format ではテキスト、verbose JSON、SRT、VTT も指定できます。Apple ではモデル別名がローカル ASR エンジンを選択します。Linux と Windows の speech-core パッケージ版サーバーは遅延読み込みの Parakeet を使用し、PCM16/24/32 または Float32 WAV を受け付け、アップロードを 25 MiB、10 分までに制限します。

# ファイルを文字起こし
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

# OpenAI-compatible transcription
curl http://localhost:8080/v1/audio/transcriptions \
  -F "[email protected];type=audio/wav" \
  -F "model=whisper-1"

# OpenAI-compatible speech synthesis
curl http://localhost:8080/v1/audio/speech \
  -H "Content-Type: application/json" \
  -d '{"model":"tts-1","voice":"alloy","input":"Hello world","response_format":"wav"}' \
  -o openai-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です。

コールドスタートのモデル読み込みや長い生成中は、モデル出力の準備ができるまで、サーバーが約15秒ごとに軽量なrealtime.keepalive JSONイベントとwebsocket pong制御フレームを送信します。クライアントはこれらのイベントを無視するか、動作中のシグナルとして扱えます。

クライアント → サーバーイベント

イベント目的
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レスポンス確定(メタデータ + レイテンシ統計)
errortypemessageを持つエラーエンベロープ
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を提供します。