ストリーミングディクテーション
Parakeet-EOU-120Mは、Apple SiliconのNeural Engine上でリアルタイムディクテーションのために構築された、明示的な発話終端(EOU)ヘッドを備えた小型のRNN-TストリーミングASRモデルです。このガイドでは、ハンズフリーでどこにでもペーストできるディクテーションのために、ストリーミングモデルとSilero VADを結び付けたmacOSメニューバーリファレンスアプリのDictateDemoも説明します。
概要
- ライブ部分結果 — 話している間、各チャンクの約340 ms後にテキストが更新されます
- 明示的なEOU — モデルが発話の終了を決定し、手動ボタンが不要
- VAD駆動の強制確定 — 背景ノイズでEOUが停止しても、Sileroバックストップが発話をコミットします
- 120 MB INT8 CoreML — Neural Engine上で動作し、他のモデルのためにGPUを解放します
- 25の欧州言語 — 上流のNeMo Parakeet TDTと同じ語彙ファミリー
アーキテクチャ
音声チャンクごとにパイプライン化された3つのCoreMLモデル:
| コンポーネント | 説明 |
|---|---|
| エンコーダー | キャッシュ対応Conformer。64フレームのmelチャンク(640 ms)と6つの状態テンソル(アテンションKVキャッシュ、深さ方向convキャッシュ、チャンク境界を越えてFFTが連続信号を見られるように最近の過去の音声を前置するpre_cache mel ループバック)を取ります。 |
| デコーダー | シングルステップLSTM予測ネットワーク。前の非ブランクトークンを消費し、埋め込みと更新された(h, c)状態を出力します。 |
| Joint + EOUヘッド | エンコーダーとデコーダーの出力をvocab + blank + EOUに対するロジットに融合します。EOUクラスは、発話が終了したというモデルのハードシグナルです。 |
なぜ別のEOUトークンが必要か
通常のRNNTは無音中にブランクを出力し、デコーダーは「発話終了」のシグナルなしに喜んで吸収します。専用のEOUヘッドにより、モデルは部分結果を最終結果にコミットし、句読点/大文字小文字の状態をリセットし、アプリへのペーストのような下流アクションをトリガーするためのハードカットを行うことができます。
「無音」の休止中のキーボードクリック、マウスの動き、部屋のトーンが、Jointに時折非ブランクトークンを出力させ、EOUデバウンスタイマーをリセットしてコミットを停止させることがあります。本番パイプラインでは、Joint EOUを外部VAD駆動のforceEndOfUtterance()バックストップと組み合わせます — 下記のDictateDemoを参照してください。
モデル
| モデル | サイズ | HuggingFace |
|---|---|---|
| Parakeet-EOU-120M (CoreML INT8) | 約120 MB | aufklarer/Parakeet-EOU-120M-CoreML-INT8 |
パフォーマンス
| 指標 | 値 |
|---|---|
| ウェイトメモリ | 約120 MB (INT8) |
| ピーク推論メモリ | 約200 MB |
| チャンクレイテンシー (Mシリーズ) | 約30 msの計算 / 640 msの音声 (RTF 約0.056) |
| エンドツーエンドの部分結果レイテンシー | 約340 ms (1チャンク) |
| コミットレイテンシー (VADパス) | 音声停止後約1秒 |
| 計算ターゲット | Neural Engine (CoreML) |
クイックスタート — バッチ文字起こし
ストリーミングモデルはSpeechRecognitionModelにも準拠しているため、汎用STTモデルを受け取る任意のコードでドロップインとして動作します:
import ParakeetStreamingASR
let model = try await ParakeetStreamingASRModel.fromPretrained()
let text = try model.transcribeAudio(audioSamples, sampleRate: 16000)
クイックスタート — 非同期ストリーミング
for await partial in model.transcribeStream(audio: samples, sampleRate: 16000) {
if partial.isFinal { print("FINAL: \(partial.text)") }
else { print("... \(partial.text)") }
}
各PartialTranscriptは、text、isFinal、confidence、eouDetected(Jointが発火したか強制確定されたか)、および単調増加するsegmentIndexを持ちます。
長寿命セッションAPI(マイク入力)
ライブディクテーションの場合、セッションを一度作成して、マイクから到着するチャンクを供給します。セッションは内部でバッファリングし、十分なサンプルが蓄積されるとエンコーダーを実行するため、任意のチャンクサイズをプッシュできます:
let session = try model.createSession()
// 各マイクチャンク:
let partials = try session.pushAudio(float32Chunk16kHz)
for p in partials {
if p.isFinal { commit(p.text) }
else { showPartial(p.text) }
}
// ストリームが終了したとき:
let trailing = try session.finalize()
VAD強制確定パターン
パイプライン内でSilero VADがすでに実行されている場合、背景ノイズがEOUデバウンスタイマーを停止できないように、フォールバックコミットを駆動するためにそれを使用します:
if hasPendingUtterance && !vadSpeechActive && vadSilentChunks >= 30 {
// Sileroあたり約960 msの持続した無音
if let forced = session.forceEndOfUtterance() {
commit(forced.text)
}
hasPendingUtterance = false
}
// ガードレール:JointがすでにEOUを発火した場合は二重コミットしない
if partials.contains(where: { $0.isFinal }) {
hasPendingUtterance = false
}
DictateDemo — macOSメニューバーリファレンスアプリ
DictateDemoは、ストリーミングセッションの上に構築された完全なmacOSメニューバーagentです。バックグラウンドアプリとして実行され、ライブ部分結果でマイクから文字起こしし、EOUまたはVAD無音で発話を自動コミットし、結果を最前面のアプリにペーストします。
- グローバル
Cmd+Shift+Dホットキーを備えたメニューバーアプリ - フローティングHUDとオーディオレベルインジケーターを備えたライブ部分結果
- VAD保護付き強制確定(上記の本番パターン)
Cmd+Shift+Vで最前面アプリへのペースト- 初回起動時のモデル自動ダウンロード(約120 MB)
cd Examples/DictateDemo
swift build
.build/debug/DictateDemo
完全な実装はExamples/DictateDemo/DictateDemo/DictateViewModel.swiftにあります:ロック保護されたバッファを備えたメイン外の音声シンク、それを排出する300 msタイマーティック、残余サンプルキャリーオーバー付きSilero VAD、および保護付き強制確定。Examples/DictateDemo/Tests/DictateDemoTests.swiftの対応する回帰テストは、マルチ発話、スタックEOU、ノイズのある無音シナリオをカバーします。
ストリーミング vs バッチParakeet
| Parakeet-EOU-120M (ストリーミング) | Parakeet TDT 0.6B (バッチ) | |
|---|---|---|
| ユースケース | ライブディクテーション、リアルタイムキャプション | ファイル文字起こし、オフラインジョブ |
| デコーダー | RNN-T + EOUヘッド | Token-and-Duration Transducer |
| チャンクサイズ | 640 ms ストリーミング | ファイル全体バッチ |
| ウェイトメモリ | 約120 MB | 500 MB |
| スループット | 約18倍リアルタイム | 約32倍リアルタイム |
| レイテンシー | 約340 msの部分結果 | ファイル終了時のみ |
…ユーザーが話し終わる前に部分結果が必要な場合。音声ファイルのバッチ文字起こしでは、大きなParakeet TDT 0.6Bの方がエンドツーエンドで高速で、より正確です。2つのモデルは同じSentencePiece語彙を共有しているため、トークン化を変更せずに交換できます。