为iOS端侧转写增加模型回退,扩展语言覆盖
文章指出,仅检查SpeechTranscriber会把部分已有端侧模型支持的语言误判为不可用。实现上先选择高质量模型,再回退到同属SpeechAnalyzer的DictationTranscriber,两者均不支持时才报错。
AI 深度解读
CapScribe 原先会把部分用户挡在转录入口:只要 SpeechTranscriber 不支持所选语言,就提示无法在设备上转录。但 iOS 26 的 SpeechAnalyzer 还可以使用 DictationTranscriber,系统其实有另一条识别路径。
作者修复的关键,是把「支持或不支持」改成「优先高质量,否则尝试基础质量」,再把实际使用的档位告诉用户。这是一个很具体的案例:接入 AI 能力时,单个模型的覆盖范围,不能直接当成整个产品的能力边界。
一个模型不支持,为什么就把用户拒绝了?
CapScribe 在设备上转录视频,再把字幕烧录进文件。早期实现只调用 SpeechTranscriber 的 supportedLocale(equivalentTo:);返回 nil 就抛出 EngineError.unsupportedLanguage,流程到此结束。
问题出在判断范围。
据作者描述,新高质量模型 SpeechTranscriber 覆盖大约 10 种语言;同在 iOS 26 中提供的 DictationTranscriber,则额外覆盖约 30 个 locale。locale 是语言与地区的组合,不能把这两个数字直接相加,宣称支持约 40 种语言。
DictationTranscriber 把听写模型作为模块接入同一个 SpeechAnalyzer。用户选的语言可能不在前者的覆盖范围内,却能由后者处理。
模型返回不支持,只能证明这条识别路径走不通。
旧实现把模型能力与产品能力绑在一起,结果是系统能处理的用户,也收到了无法转录的提示。
把入口改成分档,后面的流程还能共用
作者现在按顺序选择引擎档位:先查现代模型,再查听写模型,两者都不支持才返回失败。下面这段代码解决的就是能力选择问题:
enum TranscriptionEngineTier: String, Sendable {
case modern // SpeechTranscriber
case basic // DictationTranscriber fallback
}
func resolveTier(for locale: Locale) async -> (tier: TranscriptionEngineTier, supportedLocale: Locale)? {
if let modern = await dependencies.modernSupportedLocale(locale) {
return (.modern, modern)
}
if let basic = await dependencies.basicSupportedLocale(locale) {
return (.basic, basic)
}
return nil
}
要看的是返回值:它同时携带档位和模型实际支持的 locale。后续既需要知道用了哪种模型,也需要使用匹配后的语言地区。
两个分支创建不同的 transcriber,此后共享资源安装、交给 SpeechAnalyzer 执行、读取结果的流程。基础档位的构造和资源安装如下:
let transcriber = DictationTranscriber(
locale: supportedLocale,
contentHints: [],
transcriptionOptions: [.punctuation],
reportingOptions: [],
attributeOptions: [.audioTimeRange, .transcriptionConfidence]
)
if let request = try await AssetInventory.assetInstallationRequest(supporting: [transcriber]) {
try await request.downloadAndInstall()
}
这里要看 attributeOptions:代码申请了音频时间范围和转录置信度,但申请时间范围,不保证每个词都会返回可用数据。
作者的收集逻辑会跳过没有有效时间范围的词;如果整个片段都没有可用词级时间,就把该片段的词级时间存为 nil,让下游明确知道数据缺失。
资源检查还有一个容易漏掉的细节:supportedLocale(equivalentTo:) 返回的 locale,可能与用户传入的不同。检查模型是否已安装时,应拿这个返回值与 installedLocales 比较,否则可能误判资源状态。
回退成功,也得告诉用户质量变了
作者没有把基础模型藏在后台。
语言菜单先列现代模型支持的语言,再列仅听写模型支持的语言,并添加「(basic quality)」后缀。两种模型都支持的语言只出现一次,放在现代模型分组中。
基础档位运行后,编辑器还会提示该语言使用基础识别,准确率较低。提示依据来自引擎开始转录时的回调,由 view model 保存实际档位,而不是根据菜单选项猜测。
这个设计让质量提示跟着实际执行结果走。
测试也围绕选择逻辑展开。作者把语言查询注入为闭包,在测试中让 modernSupportedLocale 始终返回 nil,再让 basicSupportedLocale 对标识以 el 开头的请求返回 Locale(identifier: "el-GR"),最后用 XCTAssertEqual 验证选择了 .basic。
同一测试文件还覆盖双方都支持时现代模型优先、双方都不支持、设备不可用,以及两个档位的资源状态。这样可以在没有真机的情况下验证分支规则,不过它验证不了真实识别质量。
我的判断
我赞同这次调整,尤其是把「能力选择」和「质量告知」一起完成。对于字幕这种允许用户继续修订的产物,基础识别至少提供了一个起点。
但这仍是有前提的产品取舍。作者明确承认,部分用户拿到的字幕需要更多修改;原文没有提供两个模型的准确率对照、耗时、功耗或用户修订成本。因此,「低质量字幕比直接拒绝更好」属于作者的经验判断,不能扩展成所有转录场景的结论。
与切换到云端转录相比,这套做法继续使用设备上的系统模型,也能共用 SpeechAnalyzer 后续流程。但原文没有比较云端方案,无法据此判断两者的质量与成本高低。
另外,注入闭包能让路由测试稳定,却不能代替真机验证。真实系统上的语言覆盖、资源安装和词级时间返回情况,仍需要另行确认。
能拿来做什么
- 给现有 AI 功能检查一次失败出口:如果一个模型返回不支持,先确认产品是否还有可用的备选路径,再决定是否向用户报错。
- 如果前端需要展示模型质量档位,让后端或执行引擎返回实际使用的档位,并据此显示提示,避免界面状态与执行结果脱节。
- 处理字幕等时间数据时,把缺失表达为
nil或明确的空值,并让下游处理缺失情况,不要为了凑齐字段编造时间。 - 在日常 AI 编程中,把模型能力查询封装成可注入依赖,分别验证优先级、回退和全部失败;真实输出质量另做样本验证。