分割パイプライン:VADとSTTが異なる音声を欲しがる理由
ターン検出はクリーンなストリームを欲しがり、文字起こしは生のストリームを欲しがります。その主張の両半分について、いまや私たち自身の数字があります。強調はプールしたWERで5ポイントのコストになり、VADの誤警報を3分の1削りました——一方で18の個別条件のうち11では、rawに文句なしで勝っています。
2026年8月14日更新:この記事が最初に公開されたとき、分割の根拠は他人のものでした——ひとつの狭い研究と、表明されたベンダーの立場です。いまや根拠は私たち自身のものであり、測定されたものであり、以下の数字はその後、13ではなく18の音響条件にわたって再生成されています。絶対値は動きましたが、順位は動かず、ここにあるすべては公開済みのbenchmark.jsonから引用しています。
あなたの音声エージェントにはマイクストリームが1本あり、正反対の要件を持つ消費者が2ついます。ターン検出器は、テレビに割り込まないために、可能な限りクリーンな信号を欲しがります。認識器は、自分が実際に訓練された音声を——アーティファクトも含めて丸ごと——欲しがります。私たちが見てきたほぼすべてのスタックは、この両方に同じバッファを手渡し、そして一方を他方の犠牲でチューニングしています。
この記事は、そのストリームを分割することのエンジニアリング上の論証と、いまやそれを裏づける測定、そして正しくやるのが面倒な部分——遅延の整列、プリロール、そしてエージェントフレームワークのパイプラインのどこに分割が属するか——についてのものです。
2つの消費者は違うものを欲しがる
ターンテイキングはクリーンを欲しがります。 音声区間検出(VAD)とエンドポインティングは、短いフレーム上のエネルギーとスペクトルの手がかりで動作します。それらはしきい値の機械です。SNRを下げれば、較正したすべてのしきい値が動きます。オンセット検出は、音声対ノイズ比がしきい値を越えるのに時間がかかるため遅れて発火し、オフセット検出は、ノイズフロアがハングオーバーのしきい値を下回らないため、早く発火するか、まったく発火しません。音声エージェントで最も価値の大きい失敗モードはバージイン(barge-in)の誤トリガー——部屋の中の第二の話者、テレビ、聞き取れる断片を含むカフェの喧騒——であり、それこそまさに、主要話者を分離する抑制器が、使いものにならない信号を使える信号に変えるケースです。
文字起こしは生を欲しがります。 現代のSTTモデルは、非常に大量の、現実のノイズ入り音声で訓練されています。ノイズは分布内です。ニューラル抑制のアーティファクト——スペクトルの穴、ミュージカルノイズ、過渡音の塗りつぶし、単語の間で不自然に死ぬノイズフロア——はそうではありません。独立した研究(arXiv:2512.17562)は、テストした40構成すべてで強調済み音声が生音声より悪いスコアだったと報告しています。ただし、MetricGAN+のみ、医療音声、採点は意味的な単語誤り率(WER)という限定つきです。AssemblyAIはこれを引用しつつ、別途、Krispのノイズキャンセルの出力をSTTに与えるとWERがほぼ倍になる一方、VADの誤トリガーは約3.5x削減されたと報告しました。それが公開されていた証拠であり、薄いものでした。もはやそれは、私たちが持つ唯一の証拠ではありません。
私たち自身の実行が両半分について言うこと
私たちは10の音声強調(speech enhancement)エンジンを、18の音響条件にわたり、生の対照に対して実行し、同じパスでWERとVADを採点しました。出どころを述べます。単一の認識器、faster-whisper base.en(CTranslate2 int8 CPU、グリーディ)、しきい値0.5のSilero VAD、話者10人/180クリップ/約1406秒、条件あたり約190参照語、Apple M1 Max、onnxruntime 1.28.0をintra-opスレッド1本に固定。完全な手法とただし書きはベンチマーク公開の記事にあります。
文字起こしのブランチでは、強調はプール平均で負けました。 プールしたWERは、rawの14.77%に対し、最良エンジン(ai-coustics Quail L)で15.56%、GTCRNで20.26%。挿入——誰も言っていない単語を認識器が発明すること——は、rawの101件からGTCRNの210件、DeepFilterNet3の380件に増えました。このプール平均で何もしない場合に勝ったものは、10のうちひとつもありません。
条件別では文が変わり、分割パイプラインはその両方が真であることに依存しています。 18条件中11で少なくともひとつのエンジンがrawに勝ち、勝つ場所での差は大きい。0 dBの広帯域ノイズでは10エンジン中8つがrawに勝ち、最良は4.7ポイント差です。強調は音声が本当に悪い場所で割に合い、そうでない場所ではコストになります。完全な表とそのただし書きはノイズ抑制は本当にSTTの役に立つのか?にあります。
ターンテイキングのブランチでは絵が反転します——ただし、人々が普段引用する指標においてではありません。 プールしたVADのF1はほとんど動きませんでした。rawの0.950に対し、最良の強調済みストリームで0.947。VADのF1で強調を採点していたら、何の効果もないと結論したでしょう。
誤警報率はまったく別の物語を語ります。そして誤警報こそ、バージインの正体です。
| 条件 | VAD誤警報率(raw) | 強調後 | エンジン |
|---|---|---|---|
| 5 dBの喧騒 | 97.7% | 68.2% | FastEnhancer-L |
| 5 dBの競合話者 | 58.1% | 37.2% | ai-coustics Quail VF |
5 dBの喧騒下では、rawストリームは検出器が見る実質すべての非音声フレームで誤警報を出します——97.7%とは、ゲートとして機能しなくなったVADのことです。強調はこれを68.2%にします。依然として悪い。そして、まだ話していない通話者にあなたのエージェントが割り込むことへ直接対応するただひとつの数字における、30ポイントの改善です。
これが分割の、測定に基づく根拠です。強調は、ターンテイキングのブランチでは無条件にその席に値し、文字起こしのブランチでは特定の条件でのみ値します——同じ実行で、同じ音声で、同じ認識器で、です。これはトレードオフすべき2つの意見ではありません。異なる2つの信号を欲しがる2つのブランチであり、そのうち片方は、部屋次第で異なる信号を欲しがるのです。
これには正直な限定が2つあります。F1がほとんど動かないということは、強調はある程度まで誤警報を取りこぼしと交換しているということであり、条件あたり190参照語は、小さな効果ではなく大きな効果を分離するものです。そしてこれらはフレームレベルのVAD統計であって、ラベル付きのターン境界ではありません。「誤ったバージインの減少」をエンドツーエンドの製品指標にするには、誤ったバージインの合意された定義がまだ必要で、私たちはまだそれを持っていません。
アーキテクチャ
単一ストリームは、誰も意図的に選んでいない妥協です。
┌──────────────┐
mic ──▶ enhance ──▶│ same buffer │──▶ VAD / endpointing (happy)
└──────────────┘──▶ STT (five points worse, pooled)
分割パイプラインは、同じ分析に出力が2つです。
┌─▶ enhanced ──▶ VAD / endpointing / barge-in
mic ──▶ enhance┤
└─▶ raw (delay-matched) ──▶ ring buffer ──▶ STT
▲
└── turn boundaries index into HERE
推論は1パス、タップは2つ。強調器は内部ですでに音声存在推定を計算しています。両方のストリームを出力する追加コストは、コピー1回と遅延線であって、2つ目のモデルではありません。
皆が間違える部分:生ストリームは早い
強調器にはアルゴリズム的レイテンシ——分析フレームにルックアヘッドを足したもの——があります。私たちのマニフェストのモデル群では、DeepFilterNet3を除いて12から30 msで、DeepFilterNet3はブロックオンラインのアダプタ経由で100 msと測定しています。したがって、強調済みの出力は生の入力からその分だけ遅れます。
両方のストリームを素朴に出力すると、ターン検出器は強調済みストリームの時間で境界を生み、あなたはそれを生ストリームのインデックスに適用することになり、STTのために切り出すすべての発話が1フレームずれます。30 msなら語頭の破裂音を切り落とすのに十分で、切れた/p/はトランスクリプトの置換か削除です。このバグは大声では失敗しないので捉えにくい。ただ、WERのコンマ数ポイントを永遠に奪い続けるだけです。
修正は、生の経路に、強調器の宣言された遅延に合わせた遅延線を入れて、両ストリームがサンプルクロックを共有するようにすることです。私たちのSDKはこれをmodel.audioDelay——16 kHzで15 ms——として宣言し、契約を検証可能にしています。process()は与えられたのと正確に同じ数のサンプルを返し、ストリームの最初のaudioDelayサンプルは無音として返ってきます。遅延線そのものはあなたのコードであり、一度書けば済むリングバッファです。
import { Model, Processor, UsageReporter } from '@anecho/sdk';
const reporter = new UsageReporter(process.env.ANECHO_API_KEY);
const model = Model.fromFile('anecho_focus_16k_v2.anecho'); // from your dashboard
const proc = new Processor(model, await reporter.ensureLicense());
const rawTap = delayLine(model.audioDelay); // yours: audioDelay samples deep
// 20 ms of float32 mono at 16 kHz = 320 samples — one model hop
for await (const block of micBlocks) {
const enhanced = proc.process(block);
const raw = rawTap.push(block); // now sample-aligned with `enhanced`
turnDetector.push(enhanced); // clean stream drives boundaries
rawRing.write(raw); // raw stream is what STT will read
}
モデルファイルは、ダッシュボードからダウンロードする顧客ごとの.anechoです——モデル1つ、16 kHz、4.0 MB。ターン検出器を駆動する音声判定はSDKのVadから取れます。これは同じモデルファイルを読むので、分割のために2つ目のモデルも2つ目のダウンロードも必要ありません。
プリロール、あるいは最初の単語が消える理由
VADは、音声を見た後で音声を宣言します。すべての検出器にはオンセット遅延——確信するために必要だったフレーム——があります。VADが発火した瞬間からSTTバッファを埋め始めるなら、最初の単語のアタックはすでに捨てた後です。
リングバッファが分割そのものより重要なのは、これが理由です。欲しいのは、数百ミリ秒の履歴を持つ、連続的に書き込まれる生のリングバッファと、その中へインデックスするターン境界です。
turnDetector.on('turn', async (turn) => {
// turn.startSample / turn.endSample index the shared sample clock,
// so they are valid indices into the raw ring too
const utterance = rawRing.slice(
turn.startSample - PREROLL_SAMPLES,
turn.endSample + HANGOVER_SAMPLES,
);
await stt.transcribe(utterance);
});
明示しておく価値のあるデフォルトが2つあります。人々が最初にチューニングするのがこれらだからです。
- プリロール300 ms。 安い保険です。16 kHzモノラルfloat32なら、ストリームあたり19.2 KBのメモリです。
- オフセットの先のハングオーバー。 末尾の摩擦音や、開放されない語末の閉鎖音は低エネルギーで、せっかちなエンドポインタに切られます。トランスクリプトが複数形の
/s/を系統的に失っているなら、これが理由です。
Pythonでも形は同じです。
import os
from anecho import Model, Processor, UsageReporter
reporter = UsageReporter(os.environ["ANECHO_API_KEY"])
model = Model.from_file("anecho_focus_16k_v2.anecho")
proc = Processor(model, reporter.ensure_license())
raw_tap = DelayLine(model.audio_delay()) # yours, as above
for block in mic_blocks(320): # 20 ms at 16 kHz — one model hop
enhanced = proc.process(block)
turn_detector.push(enhanced)
raw_ring.write(raw_tap.push(block))
分割が実際のスタックのどこに住むか
エージェントフレームワークでは、分割はフレームワーク自身のルーティングの前に起きなければなりません。フレームワークは音声トラックが1本であることを仮定しているからです。その地点——音声コールバック、フレームプロセッサ、メディアフック、あなたのフレームワークがそれを何と呼ぶにせよ——が、上のループの行き先です。私たちはこれを、フレームワークごとのプラグインとしてではなく、ブロックからブロックへの関数として出荷しています。まさに、そうした継ぎ目のどれにでも収まるようにするためです。プロセッサは下流に何があるかについて意見を持たず、ルーティングはあなたのコードのままです。つまり、測定可能なままだということです。
ルーティング行列を平明に述べます——各行は2つのタップの配線であり、行の切り替えはあなたのループの1行です。
| 配線 | ターンテイキングが受け取るもの | STTが受け取るもの | 使いどころ |
|---|---|---|---|
| vad-only | 強調済み | 生、遅延整列済み | デフォルト。ノイズの多い環境、バージインの問題 |
| both | 強調済み | 強調済み | 競合話者のトラフィック。私たちのデータでSTTも勝つ場所 |
| stt-only | 生 | 強調済み | まれ。VADがすでにノイズ頑健で、STTがそうでない場合のみ |
| disabled | 生 | 生 | 対照。そして、あなたが測定の基準にすべきもの |
私たちがvad-onlyをデフォルトとして推奨するのは、私たちのコーパスで、ひとつの認識器で、私たち自身の測定がそこを指しているからです——あなたのトラフィックについて証明したからではありません。
bothの行は仮説ではなく、まれでもありません。強調は文字起こしで18条件中11でrawに勝ちました——最も明瞭なのは、競合する声や低SNRの広帯域ノイズが本当に音素手がかりを破壊している場所です。単一の結果として最大のものは、5 dBの競合話者におけるai-coustics Quail VFで、挿入を19件から10件に、WERを26.3%から20.5%に削減しています。あなたの通話者が、別の人間が話している部屋に座っているなら、both-enhancedは証拠の上で擁護可能です。同じモデルは、18条件すべてをプールするとrawより3ポイント以上悪くなります。クリーンな音声、残響音声、キャリア劣化音声の上でも動作し、そこでは情報を取り除くことしかできないからです——これがデフォルトではない理由であり、これが、私たちが代わりに選んであげられる設定ではなく、デプロイごとの測定である理由です。
コスト、そして分割がコストにしないもの
よく聞く反論は、これで移動させる音声が2倍になるというものです。生の経路をローカルに保てば、なりません。強調は音声がすでにある場所——ブラウザのワークレット、あなたのメディアサーバー、あなたのNodeプロセス——で走り、ネットワーク境界を越えるのは、それぞれの消費者が必要とするストリームだけです。強調済みストリームが食わせるVADは、普通はインプロセスです。生ストリームはあなたのSTTベンダーへ行きますが、それはどのみち行くはずだったものです。
実際にかかるコストは次のとおりです。
- メモリ。 セッションあたりリングバッファ1本。16 kHzモノラルfloat32で300 msあたりおよそ19 KB。余裕を見てセッションあたり64 KBとしておきましょう。
- 遅延線。 クロックを合わせるために、生の経路に強調器レイテンシの1フレーム分を追加します。これはエンドツーエンドの応答レイテンシを増やしません。生の経路は遅れていたのではなく先行していたのであり、すでに持っていた遅いほうのストリームに整列させているだけだからです。
- 考えることがひとつ増える。 ストリームが2本ということは、バグの隠れ場所が2か所ということです。緩和策は、両方が、ドリフトし得る2本の独立したパイプラインではなく、ひとつのサンプルクロックを持つひとつの
process()呼び出しから導出されることです。
まだ分かっていないこと
- 一部の認識器は強調済み音声で訓練されているかもしれません。 ベンダーが自身の取り込みで前処理しているなら、生を与えることは別の理由で正しいか、捉えにくい理由で間違っています。それらの訓練セットの中身は誰にも見えません。
- SNRの交差点。 強調は低SNRで明確に勝ち、クリーンな音声で明確に負けますが、どこで反転するかにはまだ数字を置けません。勝者の密度は0 dBでピークに達し——10エンジン中8つ——両方向に薄くなり、
clean条件ですらrawに勝ち続けるエンジンが1つあります。これはしきい値ではなく勾配であり、エンジンごとに異なるでしょう。これを正しく特定することこそ、条件付きの強調を出荷可能にするものです。 - ターンテイキングの利得はフレームレベルで、ターンレベルではありません。 私たちが報告しているのはVADのF1と誤警報率です。「誤ったバージインの減少」には、ラベル付きのターン境界と合意された指標が必要です。それが、私たちがベンチマークに負っている次のものです。
- テレフォニーはそれ自体で別のケースです。 8 kHzの狭帯域(narrowband)経路では、あなたが何かに触れる前から認識器は分布外であり、私たちの行列のどの強調器もそこで意味のある改善を生まず、いくつかは大きく劣化させました。論理も変わります。音声AIが実際に壊れるのは8 kHzの世界をご覧ください。私たちはその後、正真正銘ネイティブな狭帯域モデル3つを含む、エンドツーエンド8 kHzの独立した実験を実行しました。ネイティブは同一ベンダーの「リサンプルして祈る」に0.7ポイント差で勝ち、そして手つかずの通話者音声には何ひとつ勝てません。
ルーティングをフレームワークのデフォルトに埋めるのではなく、自分のループに保つ理由は、それがこの問いをデプロイごとにテスト可能にするからです。STTのタップをraw、enhanced、disabledの間で切り替え、自分の音声をハーネスに通し、パススルー対照に対するΔWERを読んでください。あなたのトラフィックでenhancedが勝つなら、enhancedを出荷してください——SDKはどちらの答えが出ようと気にしませんし、測定が本物である限り、私たちも気にしません。
クイックスタートとSDKリファレンスは/docsにあります。ΔWERの数字を生むハーネスはgithub.com/anechoに、公開済みの行列は/benchmarkにあります。