iOSのオンデバイス音声文字起こし: WhisperKit vs SFSpeechRecognizer vs SpeechAnalyzer#
現在開発中の日記アプリ「Lanternly」の音声認識レイヤーを設計したとき、要件はシンプルであると同時に一切の妥協を許さないものだった。音声メモはスマートフォン上でその場でテキストに変換され、音声データは1バイトたりともデバイスの外に出てはならない。日記というのは存在するデータの中でも最もプライベートな部類に入る。「サーバー側ではすべて暗号化しています」という説明は、ここでは説得力を持たない――私自身にとっても、ユーザーにとっても。唯一の誠実な答えは、この処理のためのサーバーをそもそも持たないことだった。
この数年でiOSには、現実的な選択肢が3つ登場した。オンデバイスフラグを持つ従来のSFSpeechRecognizer、Argmax社によるWhisperモデルベースのサードパーティエンジンWhisperKit、そしてiOS 26で登場したばかりのSpeechAnalyzerだ。この記事ではマーケティング的な言葉を排し、数字とコードをもとに、それぞれの違いと私がどう選んだかを解説する。
オンデバイス文字起こしがそもそも必要な理由#
クラウド文字起こしは最も簡単な道だ。音声をAPIに投げてテキストを受け取り、次のコード行に進む。しかし開発者があまり語りたがらないコストが存在する。第一にプライバシー。音声は他者のサーバーに届き、プロバイダのプライバシーポリシーが何と書いてあろうと、法的にも技術的にも「デバイス内だけ」ではなくなる。第二にネットワーク依存。地下鉄で電波のない状態で録音したメモは、そのままでは文字起こしされない。第三にコスト。クラウドAPI経由の音声は1分ごとに課金され、ユーザー基盤が増えるほど請求額も膨らむ。
日記のようなアプリにとって、第一の点はそもそも議論の余地がない。技術的な好みではなく、プロダクトが存在するための条件だ。だから問いは「ローカルかクラウドか」ではなく、「どのローカルエンジンか」だった。
俎上に載せる3つのエンジン#
SFSpeechRecognizer — iOS 10から利用可能なSpeechフレームワークの一部。requiresOnDeviceRecognitionフラグにより、Appleのサーバーに一切アクセスせず、完全にデバイス上で認識を行わせることができる。対応言語であればモデルはすでにシステムに組み込まれており、開発者側で何かをダウンロードする必要はない。
WhisperKit — Argmax社によるオープンソースSwiftパッケージで、OpenAIのWhisperモデルをCore MLとApple Neural Engineに移植する。2026年、ArgmaxはWhisperKitを話者分離のSpeakerKit、音声合成のTTSKitと統合し、単一のオープンソースSDKとした。ただし文字起こしのコア部分は変わらず、開発者が必要なモデルサイズを選んでダウンロードし、WhisperKitがANE向けに最適化してローカルで実行する。
SpeechAnalyzer / SpeechTranscriber — iOS 26で導入されたSpeechフレームワークの新しいモジュール式API。SFSpeechRecognizerの表面的な改良ではなく、まったく異なるアーキテクチャだ。AsyncSequenceをベースにした非同期API上に、長い発話向けのSpeechTranscriber、短い発話向け(旧recognizer相当)のDictationTranscriber、音声区間検出のSpeechDetectorという独立したモジュールが用意されている。
WhisperKitの中身#
WhisperKitは自前でモデルを学習しているわけではない。既存のWhisperの重み(tiny、base、small、medium、large-v3、そして軽量版のlarge-v3-turbo)を受け取り、Core MLに変換してCPU・GPU・Neural Engineに処理を分散させる。モデル間のサイズ差は大きく、tinyは約40MBでデバッグ用途向け、large-v3は約1.5GBに達する。iPhone 13以降の実運用でよく使われる現実的な妥協点はlarge-v3-turboで、完全なlarge-v3にほぼ匹敵する精度を約5倍のスループットで実現し、ディスク上のサイズは約600MBに収まる。
WhisperKitの本質的な優位性は、Whisperモデル自体(これはオープンであり誰でも組み込める)ではなく、ANE最適化にある。独立したベンチマークによれば、素のMetal実行に対してさらに1.3〜1.8倍の高速化が得られ、特にM3/M4系チップで顕著だという。iPhone 13以降であれば、録音の終了を待たずにリアルタイムでストリーミング文字起こしができる。
モデルとしてのWhisperの最大の強みは、最初から多言語対応であることだ。99言語を同時に学習しており、ロシア語も含まれる。一文の中での言語切り替えも自然に処理する――「会議はロシア語で行われたが、専門用語は英語だった」といった状況は、Whisperにとって例外ではなく日常だ。
Appleの新しい切り札、SpeechAnalyzer#
SpeechAnalyzerはただのAPI追加ではない。Whisperのような汎用モデルがシステムのrecognizerを品質面で上回っていたことへの、Apple自身の回答だ。そしてその回答は強力だ。LibriSpeechデータセットのクリーンな部分で、SpeechAnalyzerの単語誤り率(WER)は約2.12%、ノイズを含む部分では約4.56%となる。比較として、Whisper Smallは同じクリーンな部分で約3.74%――つまり英語においては、Appleの新しいシステムエンジンが同程度のサイズのオープンモデルを客観的に上回っている。
しかしこの勝利には正直に述べるべき重要な制約がある。iOS 26の時点で、SpeechTranscriberが対応する言語は、Appleが長年Siriに投資してきた言語――英語、スペイン語、フランス語、ドイツ語、イタリア語、ポルトガル語、中国語、日本語、韓国語――が中心だ。ロシア語はこのリストに含まれていない。Lanternlyのようにロシア語での文字起こしが必要なアプリにとって、これはSpeechAnalyzerを即座に選択肢から外すことを意味する。英語での精度がどれほど印象的であってもだ。
もう一つのアーキテクチャ上の特徴として、SpeechAnalyzerの言語モデルはアプリのバイナリに焼き込まれず、AssetInventoryを通じてシステムが管理し、必要に応じて取得する。これによりアプリバンドルのサイズは小さくなるが、その言語のアセットがユーザーのデバイス上に実際に存在し利用可能であるという前提に依存することになる。
正直な比較#
以下の表は「どのエンジンが最良か」ではなく、「どのエンジンがどの用途に最適か」を示すものだ。3つとも、それぞれ異なるシナリオで存在意義がある。
| 基準 | SFSpeechRecognizer | WhisperKit | SpeechAnalyzer (iOS 26+) |
|---|---|---|---|
| 精度 | 中程度、言語依存 | 高い、ノイズや訛りに強い | 対応言語では最高水準(クリーン音声でWER約2.1%) |
| 対応言語 | 限られたロケール一覧だがru-RUを含む | 99言語、ロシア語含む、自由なコードスイッチング | 約9言語、iOS 26発表時点でロシア語なし |
| オフライン | requiresOnDeviceRecognitionとローカルモデルがあれば可 | 完全に可能、モデルは一度だけダウンロード | 可能、システムアセットをAssetInventory経由で取得 |
| アプリの重さ | 0MB――モデルは既にシステム内にある | tinyで約40MBからlarge-v3で約1.5GB、実運用ではturboで約600MB | バイナリ内は0MBだが、OS側の言語アセットが必要 |
| 最低iOS | iOS 10+(オンデバイスはiOS 13+) | iOS 16+(モデルによる) | iOS 26+ |
実践: コードではこう見える#
以下はWhisperKit統合の基本的な骨組みだ。初期化、デバイスのメモリに応じたモデル選択、録音済みファイルの文字起こし、録音中のストリーミング処理を扱う。教育目的の簡略化コードではあるが、実際の設計思想に忠実だ――これはまさに、Lanternlyでエンジンを呼び出し側のコードに触れずに差し替えられるように設計した箇所そのものである。
import WhisperKit
final class LocalTranscriber {
private var pipeline: WhisperKit?
/// Initializes WhisperKit, downloading and warming up the model
func setUp() async throws {
let config = WhisperKitConfig(
model: "large-v3-turbo", // compact yet accurate option
downloadBase: nil, // model is pulled from the Hugging Face Hub
verbose: false,
logLevel: .none,
prewarm: true, // warm up the Neural Engine ahead of time
load: true
)
pipeline = try await WhisperKit(config)
}
}extension LocalTranscriber {
/// Picks a model based on the device's available memory
static func recommendedModel() -> String {
let physicalMemory = ProcessInfo.processInfo.physicalMemory
let memoryGB = Double(physicalMemory) / 1_073_741_824
switch memoryGB {
case ..<4:
return "tiny" // ~40 MB, for older devices
case 4..<6:
return "base" // ~150 MB, speed/accuracy balance
case 6..<8:
return "small" // ~500 MB
default:
return "large-v3-turbo" // ~600 MB, best balance on the Neural Engine
}
}
}extension LocalTranscriber {
/// Transcribes an already-recorded audio file (m4a/wav) entirely on-device
func transcribe(fileAt url: URL) async throws -> String {
guard let pipeline else {
throw TranscriptionError.notReady
}
let options = DecodingOptions(
language: "ru", // a hint to the model, not a hard constraint
temperature: 0.0,
withoutTimestamps: true
)
let results = try await pipeline.transcribe(
audioPath: url.path,
decodeOptions: options
)
return results?.text ?? ""
}
}
enum TranscriptionError: Error {
case notReady
}extension LocalTranscriber {
/// Streaming transcription while a voice note is being recorded
func startStreaming(onPartialResult: @escaping (String) -> Void) async throws {
guard let pipeline else {
throw TranscriptionError.notReady
}
let streamer = AudioStreamTranscriber(
audioEncoder: pipeline.audioEncoder,
featureExtractor: pipeline.featureExtractor,
segmentSeeker: pipeline.segmentSeeker,
textDecoder: pipeline.textDecoder,
tokenizer: pipeline.tokenizer!,
audioProcessor: pipeline.audioProcessor,
decodingOptions: DecodingOptions(language: "ru")
) { _, result in
onPartialResult(result.text)
}
try await streamer.startStreamTranscription()
}
}私ならどう選ぶか#
もしロシア語対応が必須でなく、最低対応バージョンをiOS 26にできるなら、迷わずSpeechAnalyzerを選ぶ。精度が高く、バイナリも肥大化せず、Swiftの構造化並行性を前提にモダンに設計されたAPIだからだ。しかし、ほとんどのプロダクトの現実はロシア語対応と、iOS 26未満の最低対応バージョンだ。つまり実質的な選択はSFSpeechRecognizerとWhisperKitの間になる。
requiresOnDeviceRecognitionを設定したSFSpeechRecognizerは、誠実でほぼコストゼロの選択肢だ。すでにシステムに組み込まれておりモデルのダウンロードも不要で、ベースラインとして十分に機能する。実際、Lanternlyの文字起こしは現在これで動いている――私は意図的に、上位のコードに手を加えることなくエンジンをコード内の一箇所で差し替えられるようにインターフェースを設計した。WhisperKitは次の一手であり、ノイズや訛り、混在言語への耐性がより必要になったとき、数百メガバイトの追加は十分に見合うコストとなる。
エンジン選定前のチェックリスト#
- サーバーへのリクエストが一切ないオフラインモードが必須か――そうであれば「キャッシュ付きクラウド」的な選択肢はすぐに除外される
- 対象とする最低iOSバージョンで、主要言語が
SpeechAnalyzerに対応しているか - WhisperKitのモデルのために150〜600MBをアプリに追加することを受け入れられるか
- コードスイッチング(一文の中での言語混在)が必要か――必要ならWhisperKitが明確な答えだ
-
SFSpeechRecognizerの標準精度でシナリオとして十分か、それともユーザーから認識精度への不満が出ているか
まとめ#
「どのエンジンが最良か」に普遍的な答えは存在しない。精度・重さ・言語カバレッジのトレードオフ曲線上に、3つの誠実な選択肢があるだけだ。SpeechAnalyzerは対応言語が足りている場面では純粋な精度で勝る。WhisperKitはモデルの重さというコストと引き換えに、多言語・訛りのあるシナリオで最も柔軟な選択肢であり続ける。そしてSFSpeechRecognizerは、ほぼコストゼロで多くの場合十分な働き者だ。こうしたレイヤーを設計するうえで最も重要な決定は、どのエンジンを選ぶかではなく、後からアプリを書き直すことなくエンジンを差し替えられるアーキテクチャを用意することにある。



