iOS设备端语音转录:WhisperKit对比SFSpeechRecognizer与SpeechAnalyzer#
在为我目前正在开发的日记应用Lanternly设计语音识别层时,需求既简单又毫不妥协:语音笔记必须直接在手机上转换为文字,音频数据一个字节都不允许离开设备。日记是最私密的数据类型之一,"我们在服务器上加密一切"这种说法在这里说服力不足——对我自己不够,对用户也不够。唯一诚实的答案是:这项操作根本不需要服务器。
过去几年里,iOS上出现了三种现实可行的方案:带有设备端标志的老牌SFSpeechRecognizer、基于Whisper模型的第三方引擎WhisperKit(来自Argmax公司),以及iOS 26中全新推出的SpeechAnalyzer。这篇文章不谈营销,只谈数字和代码,分析三者的差异以及我是如何在其中做出选择的。
为什么设备端转录如此重要#
云端转录是最简单的路径:把音频扔给API,拿到文字,写下一行代码。但它有一个开发者们不太愿意提起的代价。首先是隐私:语音数据到达别人的服务器,无论服务商的隐私政策怎么写,从法律和技术角度看,这都已经不再是"仅在你的设备上"了。其次是网络依赖:在没有信号的地铁里录的语音备忘录根本无法转录。第三是成本:通过云端API处理的每一分钟音频都会产生费用,且随用户规模增长而增长。
对于日记这类应用,第一点根本没有讨论的余地——这不是技术偏好,而是产品能否存在的前提条件。所以问题从来不是"本地还是云端",而是"到底选哪个本地引擎"。
摆上桌面的三个引擎#
SFSpeechRecognizer — Speech框架的一部分,自iOS 10起可用。它有一个requiresOnDeviceRecognition标志,可以强制识别完全在设备上进行,不联系Apple的服务器。对于支持的语言,模型已经内置于系统中,开发者无需下载任何东西。
WhisperKit — Argmax公司开发的开源Swift包,将OpenAI的Whisper模型移植到Core ML和Apple Neural Engine上。2026年,Argmax将WhisperKit与说话人分离引擎SpeakerKit、语音合成引擎TTSKit合并为一个统一的开源SDK,但转录核心保持不变:你自己选择并下载所需大小的模型,WhisperKit针对ANE做优化后在本地运行它。
SpeechAnalyzer / SpeechTranscriber — Speech框架中全新的模块化API,在iOS 26中引入。这不是对SFSpeechRecognizer的表面更新,而是完全不同的架构:基于AsyncSequence构建的异步API,拥有独立的模块——SpeechTranscriber(长语音)、DictationTranscriber(短语句,相当于旧版recognizer)以及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,吞吐量却提升约五倍,磁盘占用约600MB。
WhisperKit的核心工程优势并非Whisper模型本身(它是开源的,任何人都可以接入)——而是ANE优化:独立基准测试显示,相比普通的Metal运行,它能额外获得1.3到1.8倍的加速,在M3/M4系列芯片上尤为明显。在iPhone 13及更新机型上,这使得实时流式转录成为可能,而无需等待录音结束。
Whisper作为模型的最大优势是开箱即用的多语言支持:它同时在99种语言上训练,包括中文和俄语,并且能自然处理一句话内的语言切换——"会议是用俄语开的,但夹杂了一个半英文术语",对Whisper来说是家常便饭,而非极端情况。
SpeechAnalyzer:苹果的新王牌#
SpeechAnalyzer不只是又一个API——它是苹果对Whisper这类通用模型在质量上超越系统recognizer这一事实的回应。而这个回应相当有力:在LibriSpeech数据集的干净语音部分,SpeechAnalyzer的词错误率(WER)约为2.12%,在含噪声部分约为4.56%。作为对比,Whisper Small在同一干净语音部分的WER约为3.74%——也就是说,在英语上,苹果全新的系统引擎客观上超越了同等规模的开源模型。
但这一胜利伴随着一个值得诚实说明的重要局限:截至iOS 26发布时,SpeechTranscriber支持的语言主要覆盖苹果早已为Siri投入的那些语言——英语、西班牙语、法语、德语、意大利语、葡萄牙语、中文、日语、韩语。等等——中文其实在列表内,但值得注意的是俄语不在其中。对于像Lanternly这样需要支持俄语转录的应用来说,这一限制立刻让SpeechAnalyzer出局,无论它在英语上的准确率有多亮眼。
还有一个架构细节:SpeechAnalyzer的语言模型并不打包进你的应用二进制文件,而是由系统通过AssetInventory统一管理并按需下载。这减小了你的应用体积,但也带来了一个依赖:正确的语言资源必须真实存在,并且在用户设备上可用。
一次诚实的对比#
下表回答的不是"哪个引擎最好",而是"哪个引擎最适合哪种任务"。三者在不同场景下都有存在的理由。
| 标准 | SFSpeechRecognizer | WhisperKit | SpeechAnalyzer(iOS 26+) |
|---|---|---|---|
| 准确率 | 中等,取决于语言 | 高,对噪声和口音有较强鲁棒性 | 在支持的语言上最高(干净语音WER约2.1%) |
| 支持语言 | locale列表有限,但包含ru-RU | 99种语言,包含俄语,支持自由的语言混杂 | 约9种语言,iOS 26发布时不含俄语 |
| 离线支持 | 需开启requiresOnDeviceRecognition且本地模型存在时可用 | 完全可用,模型只需下载一次 | 可用,系统资源通过AssetInventory获取 |
| 应用体积占用 | 0MB——模型已在系统中 | 从约40MB(tiny)到约1.5GB(large-v3),实践中约600MB(turbo) | 二进制中为0MB,但需要系统层面的语言资源 |
| 最低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——准确率更高,不会让二进制体积膨胀,而且API围绕Swift现代结构化并发设计。但大多数产品的现实是:需要支持俄语,且最低iOS版本低于26,这意味着真正的选择是在SFSpeechRecognizer和WhisperKit之间。
配合requiresOnDeviceRecognition使用的SFSpeechRecognizer是一个诚实且几乎零成本的选项:它已经在系统中,不需要下载任何模型,作为基线方案表现良好。这正是Lanternly目前转录功能的实现方式——我特意将接口设计成可以在代码中的单一位置替换引擎,而无需改动上层任何代码。WhisperKit是下一步,当你需要对噪声、口音和混合语言有更强的鲁棒性时,多花几百兆字节换来这样的质量是值得的。
选择引擎前的检查清单#
- 是否严格要求离线模式、完全不发起任何服务器请求——如果是,任何"带缓存的云端"方案都应立即排除
- 你的主要语言在目标最低iOS版本上是否被
SpeechAnalyzer支持 - 你是否愿意为WhisperKit模型给应用增加150到600MB的体积
- 是否需要语言混杂(一句话中混合多种语言)——如果需要,WhisperKit是明确答案
-
SFSpeechRecognizer内置的准确率对你的场景是否足够,还是用户已经在抱怨识别效果
结语#
"哪个引擎最好"并没有普遍答案——在准确率、体积和语言覆盖范围的权衡曲线上,存在三个诚实的落点。在语言支持足够的场景下,SpeechAnalyzer凭纯粹的准确率胜出。WhisperKit则以模型体积为代价,在多语言和带口音的场景中保持最灵活的选择。而SFSpeechRecognizer是那匹几乎零成本、大多数时候已经足够好用的老黄牛。设计这样一个层级时,最重要的决定从来不是选择哪个具体引擎,而是构建一种架构,让你日后能够替换它,而无需重写整个应用。



